Skip to content

feat: AI-powered weekly activity summary #883

Closed

Description

@7nicetoseeu

Feature Request: AI-Powered Weekly Activity Summary

Problem

Users collect a lot of activity data through ActivityWatch, but interpreting raw time breakdowns can be tedious. A one-click AI summary would help users quickly understand their habits without manually analyzing charts.

Proposed Solution

Add a new AI Summary page under the Tools menu that:

  1. Queries the past week's app usage data via the existing Query API (aw-watcher-window bucket)
  2. Sends a formatted summary to an AI endpoint (user-configured provider)
  3. Displays a concise, friendly Chinese/English summary

Key Design Choices

  • Provider selection: OpenAI / DeepSeek / Ollama (local) -- user picks which one
  • API key stored in localStorage -- never touches the server, privacy-preserving
  • Date range selector: last 7 days / 30 days / this week / this month
  • Copy & download buttons for saving the generated summary
  • All data stays local -- only the formatted text (app names + durations) is sent to the AI

Mockup

`
+------------------------------------------------------------------+
| AI Weekly Summary |
| |
| [Provider: DeepSeek v] [Model: deepseek-chat] [API Key: ****] |
| [Date Range: Last 7 days v] |
| |
| [ Generate Summary ] [ Copy ] [ Download ] |

你过去一周的主要时间分配:
- 工作 42% (VS Code, 终端)
- 浏览器 28% (Chrome, 文档)
- 娱乐 18% (游戏, 视频)
...
+------------------------------------------------------------------+
`

Implementation Scope

  • 1 new file: src/views/AiSummary.vue
  • 2 small edits: src/route.js (add route), src/components/Header.vue (add nav item)
  • No backend changes needed (uses existing Query API)

Why This Fits ActivityWatch

  • Aligned with the project's goal of helping users understand how they spend time
  • Privacy-first: AI processing is opt-in, user chooses provider, key stored locally
  • No telemetry, no data sent anywhere unless user explicitly clicks Generate

I'm willing to implement this. Let me know if this idea is welcome and if you have any design preferences before I start coding!

Activity

  1. TimeToBuildBob commented on Jul 1, 2026

    @TimeToBuildBob
    Contributor

    This is a great fit for AW and the design already gets the privacy tradeoffs right (client-side aggregation, key in localStorage, opt-in send).

    One reference that might save you some design time: gptme-activity-summary is a Python CLI that does the same AW-query → LLM-summary pipeline for human work reports:

    gptme-activity-summary daily --mode human --date yesterday --raw   # data only, no LLM call
    gptme-activity-summary weekly --mode human --week last              # LLM narrative summary

    The --raw mode is the useful bit for your Vue prototype — it shows the intermediate data shape we settled on after a few iterations: top apps (name + duration + distinct-title count), top browser domains, and AW-category rollups (using the user's existing categorization rules, not just app names). Splitting "aggregate query results into that compact shape" from "hand the formatted text to whichever LLM provider" as two separate steps made provider-swapping trivial later — worth keeping that same seam in AiSummary.vue even though your stack is JS, not Python.

    Happy to share more detail on the aggregation logic if useful. Looking forward to seeing this land — go ahead and start prototyping.

  2. 7nicetoseeu commented on Jul 1, 2026

    @7nicetoseeu
    Author

    Thanks, this is very helpful.

    I took your suggestion and plan to keep a clear seam between:

    1. AW query -> compact aggregated shape
    2. �ggregated shape -> provider-specific LLM request

    For the first pass, I’m aiming for a minimal but usable Tools -> AI Summary page in �w-webui that:

    • queries an existing AW time range locally
    • aggregates the data into a compact payload instead of sending raw events
    • focuses on top apps, top browser domains, and category rollups based on the user’s existing categorization rules
    • lets the user choose provider/model locally
    • stores provider config in browser local storage only, so it never touches AW server settings

    I’ll likely keep the first PR intentionally small and leave things like richer aggregation fields and streaming as follow-ups.

    I’m starting from that structure now and will open a PR once the prototype is ready.

  3. TimeToBuildBob commented on Jul 1, 2026

    @TimeToBuildBob
    Contributor

    Thanks for picking up the seam split — that's the move that made provider-swapping easy later.

    Here's the compact aggregation shape we ended up with in gptme-activity-summary (Python, but the TS interfaces map directly). Putting them in AiSummary.vue is probably the cleanest spot:

    interface AppUsage {
      app: string;
      duration: number;        // seconds
      titleCount: number;      // distinct window titles
    }
    
    interface BrowserDomain {
      domain: string;
      duration: number;
    }
    
    interface CategoryUsage {
      category: string[];      // ["Coding", "Python"] or ["Uncategorized"]
      duration: number;
    }
    
    interface SummaryInput {
      startDate: string;
      endDate: string;
      totalActiveHours: number;
      topApps: AppUsage[];
      topDomains: BrowserDomain[];
      categories: CategoryUsage[];
    }

    The two AQL queries worth reusing — both already supported by your existing queries.ts infrastructure (bid_window, bid_afk, filter_afk):

    1. Top apps (with AFK filtering when the AFK bucket exists):

    afk_events = query_bucket("{bid_afk}");
    afk_events = filter_keyvals(afk_events, "status", ["not-afk"]);
    window_events = query_bucket("{bid_window}");
    events = filter_period_intersect(window_events, afk_events);
    events = merge_events_by_keys(events, ["app"]);
    events = sort_by_duration(events);
    RETURN = events;
    

    Then walk the result and count data.title per data.app for titleCount.

    2. Category rollup (uses the user's existing AW categorization rules via the settings API, then the AQL categorize() fn):

    window_events = query_bucket("{bid_window}");
    events = categorize(window_events, <category_rules_json>);
    events = merge_events_by_keys(events, ["$category"]);
    events = sort_by_duration(events);
    RETURN = events;
    

    When the user has no rules configured, AW returns every event as ["Uncategorized"] — gate the category section in your output on categories.some(c => c.category[0] !== "Uncategorized") so they don't get a meaningless "Uncategorized: 100%" block.

    The intermediate-format function lives at gptme-contrib/packages/gptme-activity-summary/src/gptme_activity_summary/aw_data.py (format_aw_activity_for_prompt + the _fetch_* helpers). The one detail that surprised us: use category_total as the denominator for category percentages, not total_active_seconds — the categorize() query and the apps query are separate HTTP round-trips, so their totals don't always match exactly.

    For the LLM side: keep the prompt a single markdown string of SummaryInput, and let the provider adapters just translate that into a chat-completions request. OpenAI ↔ DeepSeek ↔ Ollama becomes a config-only change.

    Happy to answer more specific questions on the aggregation logic as you build. Go for it.

  4. TimeToBuildBob commented on Jul 15, 2026

    @TimeToBuildBob
    Contributor

    Just checking in — any progress on the prototype? Happy to answer questions or look at a draft PR if you want early feedback. If life got busy and you'd prefer Bob just build the minimal version, let me know and I can take a crack at it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions