v0.0.4,照着微信重做了界面
仿微信界面设计的概念插画
上一篇讲到 Mio 的 Telegram bot 已经能上生产了,输入校验有了,心跳带上 context 了,测试在跑,部署也不会断线。但 Telegram 不是中国用户的主场。web 端倒是一直有一个,只能说就是个光秃秃的单页聊天框,你打字,它回话,没有导航,也没有别的页面。能用是能用,跟命令行也能用是一个意思。所以 v0.0.4 就干了一件事,把整个 web 端推倒,照着微信重做一遍。不是在原来那个聊天框外面贴层皮,是整个重建。
为什么照着微信?这个其实没什么好纠结的。你要找一个中国用户不用学就会用的界面,那只能是微信。十几亿人每天都在用,底部四个 tab、按时间排的聊天列表、绿色的主色调、发现页,这一套大家早就用成肌肉记忆了。自己发明一套导航,用户还得从头学。直接用大家已经会的这套,用户第一次打开 app 就知道东西在哪。
四个 tab
新布局完全照着微信来,四个 tab 是这样的:
- 消息:聊天列表,按最后一条消息的时间排序,带搜索和相对时间戳
- 通讯录:agent 列表,按预设类型分组,加一个「添加 agent」的入口
- 发现:预设市场,可以浏览人格预设、看简介、一键添加
- 我:个人信息卡片、设置、退出登录
每个 tab 都是一个独立的页面,有自己的路由。这是正经的多页架构,外面套一个共享布局,没有用那种在单页里假装切 tab 的做法。
文案从第一天就是中文
用户能看到的所有文字,一开始就是直接拿中文写的。这个区别很大,因为翻译过来的 UI 总有种说不出的别扭,间距不对,措辞生硬,该随意的地方又太正式。最好的例子是「对方正在输入...」,它不是从「typing...」翻过来的,是中国用户在微信里看了几千遍的原话,直接拿来用就行。Mio 是做给中国用户的,界面从第一个字起就得拿中文去想。
不用组件库,从零搭设计系统
这版一个组件库都没用,Ant Design、Material UI、shadcn 都没上,所有组件从零写,样式全走 CSS 自定义属性。这套 design token 我管它叫「微信 2025」,颜色、间距、圆角、阴影、字号都定义成 CSS 变量,参考了微信现在的设计语言,但没有照抄。
为什么不用库,大概两个原因:
- Mio 的 UI 要什么很明确,就是要长得像微信,不能像「一个套了中文皮的 React 项目」。组件库都有自己的视觉语言,你跟它较劲花的时间比从头写还多。
- 控制依赖。v0.0.4 加了 30 多个组件,
package.json里一个新包都没多。所有组件共用同一套 design token,看起来是统一的,也不用背外部库的包袱。
另外还写了一个共享的头像工具函数,保证 agent 的头像在四个 tab 和聊天页里长得一样。这是个很小的细节,但要是不同页面的头像长得不一样,整个 app 一下就显得糙了。
引导做成聊天,不做成表单
Telegram 那边的引导是按钮式的,用 inline keyboard 加 callback query,是标准的 bot 交互。能用,但用起来就像在填表。
web 端这次完全换了做法,引导本身就是一场聊天。Mio 用聊天气泡问你问题,你点带样式的按钮来回答。选人设故事这一步也没用下拉菜单或者单选框,Mio 直接把不同的人设故事当成消息一条一条发给你,你顺着对话读完,选哪个也就成了聊天的一部分。等引导走完,你其实已经跟 Mio 聊过第一次了,虽然那些回复全是预设好的。
这个事我觉得还挺重要的。引导要是做成表单,用户上来就会把 Mio 当工具用。做成聊天的话,用户从第一分钟起就是在跟 TA 聊天了。
引导做成表单还是聊天决定用户的心态:表单式引导让人把 Mio 当工具,聊天式引导让人从第一分钟就把 TA 当伙伴
打字感知的消息合并
这个功能不起眼,但很重要。最简单的做法是每按一次回车就发一条消息。可用户实际打字都是短句连发的,比如「嘿」,发送;「你好吗」,发送;「我今天有点烦」,发送。按最简单的做法,这就要调三次 API,回来三个各说各的 AI 回复,对话一下就碎了。
合并系统做的事,就是盯着你的打字节奏。你发了一条又马上开始打字,系统就先把第一条按住不发,等你停下来够久了,再把这几条合成一条完整的消息发给 AI。微信里大家聊天本来就是这个节奏,人就是会连发短消息,「嘿」过两秒再跟一句「你好吗」,其实是同一个念头拆成两条发出来,所以界面也得照着这个习惯来处理。实现上也没加依赖,就是一个自定义的 usePolling hook,定时去轮询,它会看页面是不是可见,tab 藏起来就暂停,切回来再接着轮询。
打字感知的消息合并:连发的短消息被按住等待,停顿够久后合成一条再发给 AI,换来一个完整回复
路由
每个聊天都在 /chats/[agentId] 这个动态路由下面,它负责加载这个 agent 的对话历史,显示「对方正在输入...」这个打字提示,还有处理发消息。四个 tab 都有 middleware 挡着,没登录就直接重定向。老的 /chat 也留了个重定向到 /chats,为的是向后兼容。URL 发出去了就别弄断,存过旧链接的用户点开还得能用。
agent 列表和会话数据都统一走 useAgents 这一个 hook,只有一个数据源,四个 tab 都从它拿数据。这样就避开了那个经典问题,聊完天回到列表,列表显示的还是旧数据。
两轮 audit
新增和改动的组件有 30 多个,能出 bug 的地方不少,所以写完之后跑了两轮完整的 audit。第一轮抓出来的是 CSS 变量不一致、消息渲染有 XSS 隐患,还有一些无障碍问题。第二轮抓出来的是头像逻辑重复了(后来收到了一处),还有通讯录分组的边界情况。两轮找出的 CRITICAL 和 HIGH 全部修完了,类型检查也干净,还是一个新依赖都没加。思路跟上一篇一样,audit 过了才发版。
没装一个新包
四个 tab 页、聊天页、引导流程、设计系统、hooks、middleware,这一整套重构加了 30 多个组件,一个新 npm 包都没装。usePolling 就 30 行代码,useAgents 不到 100 行,agent 状态、会话数据和实时更新都归它管。
少装依赖倒不是想当纯粹主义者,主要是装一个外部依赖,就等于让别人替你做了决定,出了问题还得你自己收拾。Mio 的 web 前端现在还够小,从零写反而更快,不用去学别人的抽象,也不用修别人的 bug。
从 bot 到应用
v0.0.3 的时候 Mio 是个靠谱的 Telegram bot,到了 v0.0.4,TA 成了一个正经的 web 应用。这差别不小。做 Telegram bot,UI、交互规则、更新节奏全捏在别人手里。web 是自己的,引导怎么走、导航怎么排,全都自己定。
现在 Mio 有两个家,喜欢用即时通讯的用户用 Telegram,想要完整体验的就上 web。背后是同一个 AI,人格和记忆也是同一套,只是给不同的场景换了个界面。
从第一篇走到这里,Mio 从一个架构上的决定,变成了一个多平台的产品,也有了自己的设计语言。下一步是让 TA 开口说话,那是 v0.0.5 的事了。


