
围绕一张波浪图重新设计仪表盘
GeekBye v1.8.2 把仪表盘从一个录音遥控器变成了只读的分析视图——一条统计条和一张 7 天波浪图。这两个招牌元素都是第二稿,都在初稿出来后 20 分钟内、同一个下午里发布。
有些版本是加一个功能。GeekBye v1.8.2 主要是_去掉_了一个——而这次去掉本身就是故事。2026 年 2 月的一个下午,18 个 commits 把仪表盘首页从一个录音遥控器变成了只读的分析视图:四个统计数字和一张活动图。一路上它发了一张只活了 22 分钟的柱状图、一个只活了 20 分钟的统计卡片网格,以及一条把两者都替换掉的手写 SVG 波浪。整段搜索过程都保存在 commit 的时间戳里,而这是世上最好的一种设计文档。
仪表盘不再是遥控器
在这个版本之前,仪表盘首页是一块控制面板。打头的是一张 hero 卡片——一个大大的录音小组件,带 LIVE 徽标、Pause/Resume/Stop 按钮、渐变色块,还有一个把这一切接到录音器上的 useSessionState hook。仪表盘想同时当两样东西:你_看_自己会议的地方,和你_操作_录音器的地方。两个产品,一块屏幕,悄悄地在较劲。
v1.8.2 选了一个。它删掉了 hero 卡片和 useSessionState 的接线——包括一个在会话一结束就自动把你带到某场会议详情页的 effect,而这正是那种会在你做事做到一半时冷不丁跳走的"贴心"。开始和隐藏录音器被挪了出去,成了导航栏上一个朴素的开关(一个改了用途的按钮,从会话控制器变成开/关开关的过程里掉了大概一百行),背后由一个新的 navbar:hide IPC handler 支撑。留下来的是一个只做一件事的仪表盘:把你录下来的东西展示给你。只读。安静。
这次转换里有一个小小的、很有人味的 bug 值得点出来,因为它正是那种重设计悄悄带进来、又被好眼力在同一个小时里抓住的东西。两个新的侧边栏按钮——Start 和 Hide——发布时用了_同一个_通用网格图标,所以看上去一模一样。12 分钟后一个跟进的改动给 Start 配了播放图标,给 Hide 配了一个眼睛划掉的图标,还把 Hide 染成了琥珀色,这样"让应用消失"读起来就是一个刻意的动作,而不是"开始"的一个中性双胞胎。图标就是文案。两个长得一样的按钮说的是同一句话,而这两个按钮意思并不一样。
那张只活了 22 分钟的柱状图
这里是我最喜欢的部分,而且完全能从 git log 里读出来。在 16:29:34,重设计的初稿落地了:一张用柱子画的 7 天活动图——带动画的列、一个让极小的日子也能看见的最小高度、零的日子给一个 2 像素的暗淡小桩。合理。常规。就是那种你不假思索就会伸手去拿的图表。
在 16:51:21——22 分钟之后——它被删了。替换它的 commit 把自己干的事说得清清楚楚:"replace bar chart with smooth wave area chart for 7-day activity"。而在 16:52:21,也就是一分钟后,又一个 commit 把波浪周围的圆角卡片和边框都剥掉了,"for cleaner look"。
为什么?commit 只说_改了什么_,所以_为什么_是一种解读——但数据的形状让答案一目了然。这是一个典型用户在任何一天都只有零个、一个或两个会议的工具。七根柱子,大多数只有 2 像素高,看上去像一个基本空着的停车场。同样这七个数字画成一片连续的填充区域,就像一个_趋势_——一条在这一周忙碌处轻轻抬起的波浪。什么都没造假;y 值一模一样。重设计只是选了那个让稀疏而诚实的数据读起来像一个形状、而不是像"什么都没有"的编码方式。
统计数字在同一个时钟上受到了同样的处理。初稿(16:29)把四个数字放进一个 2×4 的卡片网格。到 16:49,网格塌缩成了一条内联的统计条——一行带边框的东西,用细如发丝的分隔线、更小的字号、一个容器而不是四个。屏幕上的五个盒子(四张卡片加一张图表卡片)变成了两个(一条条和一条波浪)。下一个 PR 又加了一处诚实的润色:"Total Time"这个标签变成了"Time in Meetings",因为"总共_什么_"是一个谁都不该被迫去问的正当问题。
20 分钟,从初稿到定稿,全都在一个 pull request 里。要是这些被 squash 成一个"redesign dashboard"的 commit,这段搜索就看不见了。正因为没有,你才能看着一个设计师实时地收敛。
波浪,从机制上讲
这里没有图表库。波浪是一段用 JSX 搭出来的内联 SVG,值得一看,因为它是一份小而诚实、可以偷来用在任何 sparkline 上的配方。
平滑是诀窍所在。对每一对相邻的点,路径都用一段三次贝塞尔曲线,它的两个控制点落在两点之间的水平中点上——cpx = (prev.x + curr.x) / 2——而控制点各自保持在两端各自的高度上。这就是经典的平切线平滑:曲线平顺地穿过每一个数据点,从不越过最高值之上、也不落到最低值之下——而一条天真的 Catmull-Rom 样条就会那么干。对于一张讲会议次数的图表,"绝不画出一个暗示了根本没发生过的值的凸起"不是锦上添花,它就是正确性本身。那片填充区域是同一条曲线沿底部闭合、再用一道竖直渐变(青色,从 30% 淡到几乎为零)涂上去的,而那条线用一个 framer-motion 的 pathLength 动画从左到右把自己画出来。
SVG 底下是数据的管道,而这是那个不出声的主角。转录仓库上一个新的 getAggregateStats() 方法,对本地 SQLite 的 transcript_sessions 表跑三条预编译语句:全时段总计(数量、时长总和、平均时长)、一个滚动的七天计数,以及一个按天的 GROUP BY date(created_at) 分桶。然后——这正是让图表健壮起来的那个细节——一小段 JavaScript 循环走过最近的七个日历日,把查询没返回的每一天都用 0 回填进去。于是图表_必然_永远正好是七个点。
正是这个保证,让那些边界情况直接消失,而不需要在视图里到处撒一堆守卫。空的一周?七个零,再加上分母上的一个 Math.max(count, 1),让线平贴在底部而不是去除以零。一个会让 i / (n − 1) 这条 x 间距公式算出 NaN 的单一数据点?不可能发生——仓库永远不会吐出少于七个点。就连星期标签都是在 T12:00:00 而不是光秃秃的午夜去解析每个分桶的,这样一个 YYYY-MM-DD 字符串就不会在负 UTC 时区里滑到前一天。在源头把数据的形状修好,渲染代码就可以理直气壮地天真下去。
这一切都留在机器上。统计查询读的是本地数据库,通过一个带类型的 transcript:get-stats IPC 通道返回,并在挂载时动画成四个往上数的数字。为了画这张图,你的会议历史没有任何一点离开这台笔记本——对一个录下你对话的工具来说,这是搭一个分析视图唯一能接受的方式。
再来一个 bug,因为重设计会把它们拽到亮处
跟这个版本捆在一起的,还有一个和仪表盘毫无关系、却和那种重设计会绊上的债务大有关系的修复。应用的深色滚动条样式已经漂移成了同一个类的两个互相打架的 CSS 定义——一个更老的紫色和一个更新的深底白色——谁赢取决于层叠顺序。更糟的是,只有那些记得加上这个类的元素才有样式,其他一切都退回了默认的浅色滚动条,而水平滚动条从来没被设过样式,因为旧规则只设了宽度、从没设过高度。这个修复删掉了两个定义,对_每一个_元素都全局地给滚动条上样式,并从背着这个类的十个组件里把这个如今已死的类移除。这是一个重复了十遍的两行 diff——把同一样东西设了两遍样式、又让两者都活着,所要交的那份毫不光彩的税。
这个版本教给我们的三件事
- 做减法来聚焦。 仪表盘是靠做得_更少_才变好的——甩掉了它从不需要的录音控制,成了一个清清楚楚的东西。一块既是查看器又是遥控器的屏幕,是两个半成品;选一个,把另一个挪到它该在的地方。
- 给数据配上它配得上的编码。 同样的数字,稀疏的计数在柱状图里读成"没有",在面积图里读成一个趋势。这不是修辞——这是挑一个诚实的形状,让真实而单薄的数据能开口说话。
- 在上游就保证数据的形状。 那个回填到七的循环,正是图表代码里没有散落一堆 null 检查的原因:除以零、单点 NaN、缺失的日子,全都消失了,因为查询承诺了正好七个分桶。健壮的渲染,是从下面一层开始的。
v1 故事的上一章,见26 小时四个版本(v1.7.6–v1.8.1);整段弧线则见把软件打磨到完美地发布出去的解剖学。