Skip to content

feat(overview): 「近 7 天」脱离历史 200 条上限,并支持近 7/30 天 × 条数/字数/时长切换 - #909

Open
bigsongeth wants to merge 3 commits into
Open-Less:betafrom
bigsongeth:feat/overview-period-metrics
Open

feat(overview): 「近 7 天」脱离历史 200 条上限,并支持近 7/30 天 × 条数/字数/时长切换#909
bigsongeth wants to merge 3 commits into
Open-Less:betafrom
bigsongeth:feat/overview-period-metrics

Conversation

@bigsongeth

@bigsongeth bigsongeth commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

User description

问题:概览页的「近 7 天」会说谎

「近 7 天」柱状图从 list_history() 现算,而历史有 200 条硬上限。日均上百次听写的用户,两三天就把上周挤没了。

实测本机真实数据(history.json 正好卡在 200 条):

日期 近 7 天显示 年度活动实际
07-29 0 40
07-30 0 118
07-31 0 108
08-01 0 37
08-02 10 44
08-03 158 156
08-04 32 32

前四天全画成 0,而同一个页面上方的年度热力图那四天是亮的。同一页两块数据自相矛盾,用户没法判断该信哪个。

改动

1. 换数据源

「近 7 天」改读 activity 存储 —— 它保留两年、只存聚合数字、与历史保留策略解耦,本来就是年度热力图的数据源。改完之后柱子和格子同源,对得上。

2. activity 记录字数与时长

activity 此前每天只存一个条数。要支持「近 30 天说了多少字」,得先扩成 {count, chars, durationMs}。写入点手边就有 polishedraw.duration_ms,口径刻意与已有 UI 对齐:

  • chars = 最终插入文本的 Unicode 字符数,与历史详情页的「N 字」一致
  • durationMs = 录音时长,不含识别/润色耗时,与详情页「录音 x.x 秒」同源

磁盘格式向后兼容:老用户的 activity.json 全是裸数字({"2026-08-01": 5}),读不回来的话年度热力图会一次性清空,会被当成数据丢失。用 untagged enum 兼容,四个单测钉住契约:纯旧格式、纯新格式、新旧混排(升级当天必然出现)、缺字段的对象。

旧日期没有字数/时长,读回 0 —— 诚实缺省,好过整天丢掉条数。

存储代价可忽略:只存聚合数字、不存文本,一天一行,两年上限 731 行。

3. 周期 × 指标切换

卡片标题位变成「近 7 天 / 近 30 天」,右侧原来的「条数 / 天」变成「条数 / 字数 / 时长」。

卡片顶部显示周期总计(大字)+ 日均:想知道「这个月总共说了多少字」是要一个数,不是在 30 根柱子里目测求和。30 天模式下柱子只有几像素宽,逐柱数字会糊成一片,改为悬浮显示、横轴只标首/中/末三个日期。

4. ⌘R 刷新

历史页早就有这个键,概览页没有 —— 想看到新数据只能切到别的页再切回来触发重挂载。一次刷新本页三份数据(历史、活动、凭据),preventDefault 拦掉 webview 默认的整页 reload。

实现注记

  • 聚合逻辑抽成 lib/activityMetrics.ts 带单测:窗口长度恒定、缺失日期补 0、跨月边界、老数据无 chars 时不产生 NaN(NaN 会把柱状图的 max 算坏)
  • 日期键一律用本地年月日拼,不能用 toISOString() —— 后者按 UTC 切日,东八区凌晨的会话会算到前一天,与后端 chrono::Local 写的键对不上
  • 活动数据的 IPC 不再按 mobile 跳过。热力图在移动端仍不渲染(fix(android): 移动端隐藏概览页「年度活动」热力图 #861),但周期指标卡要渲染,跳过 IPC 会让它在移动端永远空

验证

cargo check / cargo test --lib / npm test / tsc --noEmit 全绿;浏览器里逐个切过两个周期 × 三个指标,核对总计与逐日数值;本地装机实测。

🤖 Generated with Claude Code


PR Type

Bug fix, Enhancement, Tests


Description

  • activity 存储新增每日字数与时长聚合

  • 近 7/30 天指标改用 activity 数据源

  • 支持条数/字数/时长切换与总计、日均

  • 兼容旧格式数据,新增测试与多语文案


Diagram Walkthrough

flowchart LR
  A["Daily dictation"] --> B["ActivityStore bump(count, chars, durationMs)"]
  B --> C["getActivityStats"]
  C --> D["Overview PeriodMetricsCard"]
  D --> E["7 / 30 days × count / chars / duration"]
Loading

File Walkthrough

Relevant files
Enhancement
7 files
history.rs
Extend activity stats response with chars and duration     
+9/-3     
dictation.rs
Record char count and duration on session end                       
+10/-6   
activity.rs
Expand activity store to aggregate chars and duration       
+133/-9 
types.rs
Add chars and duration fields to ActivityDay                         
+9/-1     
activityMetrics.ts
Add period series builder for activity metrics                     
+77/-0   
types.ts
Add optional chars and duration fields to ActivityDay       
+4/-0     
Overview.tsx
Replace week chart with switchable period metrics card     
+269/-63
Localization
5 files
en.ts
Add period and metric localization strings                             
+15/-2   
ja.ts
Add Japanese period and metric localization strings           
+15/-2   
ko.ts
Add Korean period and metric localization strings               
+15/-2   
zh-CN.ts
Add Simplified Chinese period and metric localization strings
+15/-2   
zh-TW.ts
Add Traditional Chinese period and metric localization strings
+15/-2   
Tests
2 files
activityMetrics.test.ts
Add tests for period series and legacy entries                     
+86/-0   
mock-data.ts
Update mock activity days with chars and duration               
+14/-1   

bigsongeth and others added 3 commits August 4, 2026 14:21
activity.json 此前每天只存一个条数。概览页想给出「近 30 天说了多少字」
这类周期指标时,只能回头从 list_history() 现算 —— 而历史有 200 条硬上限,
日均上百次的用户两三天就把上周挤没了,算出来的字数与时长必然偏低。

把单日值从裸数字扩成 {count, chars, durationMs}:
- chars 口径 = 最终插入文本的 Unicode 字符数,与历史详情页的「N 字」一致;
- durationMs 口径 = 录音时长,不含识别/润色耗时,与详情页「录音 x.x 秒」同源。
两个口径都刻意跟已有 UI 对齐,避免同一个数在两处对不上。

磁盘格式用 untagged enum 兼容旧文件:老用户的 activity.json 全是裸数字,
读不回来的话年度热力图会一次性清空(会被当成数据丢失)。旧日期没有字数/
时长,读回 0 —— 诚实缺省,好过整天丢掉条数。四个单测钉住这个契约:纯旧
格式、纯新格式、新旧混排(升级当天必然出现)、以及缺字段的对象。

存储代价可忽略:只存聚合数字、不存文本,一天一行,两年上限 731 行。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
两件事,同一张卡:

1. 修数据源。「近 7 天」此前从 list_history() 现算,而历史有 200 条硬上限。
   实测(日均约 88 次听写的真实数据):history.json 正好卡在 200 条,只装得下
   两天多,于是近 7 天里前 4 天全画成 0 —— 而同一页的年度热力图上那 4 天分别是
   40 / 118 / 108 / 37,亮着的。同一个页面两块数据自相矛盾。改读 activity 后
   两者同源,柱子和格子对得上。

2. 加周期与指标切换。卡片标题位变成「近 7 天 / 近 30 天」,右侧原来的「条数 /
   天」变成「条数 / 字数 / 时长」。月度字数是用户实际想看的数(跟按月计费的
   竞品比较时要用),此前完全看不到。

卡片顶部显示周期总计(大字)+ 日均:想知道「这个月总共说了多少字」是要一个数,
不是在 30 根柱子里目测求和。30 天模式下柱子只有几像素宽,逐柱数字会糊成一片,
改为 title 悬浮显示、横轴只标首/中/末三个日期。

聚合逻辑抽成 lib/activityMetrics.ts 并带单测:窗口长度恒定、缺失日期补 0、
跨月边界、老数据无 chars/durationMs 时不产生 NaN(NaN 会把柱状图的 max 算坏)。
日期键一律用本地年月日拼,不能用 toISOString() —— 后者按 UTC 切日,东八区凌晨
的会话会算到前一天,与后端 chrono::Local 写的键对不上。

顺带:活动数据的 IPC 不再按 mobile 跳过。热力图在移动端仍不渲染(issue Open-Less#861),
但周期指标卡是要渲染的,跳过 IPC 会让它在移动端永远空。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
历史页早就有这个键(History.tsx),概览页没有 —— 想看到新数据只能切到别的
页再切回来触发重挂载。⌘R 是「重新加载」的通用直觉,两个数据页应该一致。

一次刷新本页全部三份数据:历史、活动、凭据。preventDefault 拦掉 webview
默认的整页 reload,只重发 IPC,不让整个前端重挂载。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

861 - Partially compliant

Compliant requirements:

  • 热力图组件在移动端仍不渲染(渲染条件保持移动端门控)。
  • 桌面端热力图渲染路径未改动,设置开关逻辑保留。

Non-compliant requirements:

  • 移动端仍然会调用 getActivityStats():本 PR 移除了原来的 if (mobile) return 守卫,并在每次挂载概览页时拉取活动数据。

Requires further human verification:

  • 需要在 Android / 窄屏真机上验证「年度活动」卡片确实不显示。
  • 需要人工验证移动端底边栏宽度与对齐、无横向溢出。
  • 需要人工验证桌面端设置开关仍能正常开/关热力图。
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Mobile IPC regression

The previous mobile guard that skipped getActivityStats() on Android / narrow viewports was removed, so every Overview mount now performs the activity IPC request and transfers the full daily-stats payload even when the heatmap is hidden. This contradicts ticket #861's acceptance criterion that mobile must not call getActivityStats(), and reintroduces the mobile WebView cost it was meant to avoid.

const [activity, setActivity] = useState<ActivityDay[] | null>(null);
const [activityError, setActivityError] = useState(false);
const refreshActivity = useCallback(() => {
  setActivityError(false);
  getActivityStats()
    .then(setActivity)
    .catch(error => {
      console.error('[overview] failed to load activity stats', error);
      setActivity(null);
      setActivityError(true);
    });
}, []);
useEffect(() => {
  refreshActivity();
}, [refreshActivity]);

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant