在线试玩 → Chai & Non 官网上的实机演示
把"AI 带你去一个地方"这件事,接进你自己的前端。
这个仓库不是 npm 包,也不是组件库——它是一份数据契约加一份可以照抄的参考实现。你的存储能存下一段 JSON,你的前端就能把它渲染成一趟点得开的旅行;React、Vue、原生都行。
功能本身是这样的:AI 自己挑一个国家、选 6 处停留、每处配一张竖屏照片和一段第一人称的话,在聊天里落成一张卡片。点开是全屏的照片故事——照片铺满整屏,地名在顶部,那段话落在左下角,底部一个音乐播放器和 6 个圆点,左右滑就走到下一处。
跟"在聊天里发几张图"的区别是:这是一趟有顺序、有旁白、有配乐的行程,AI 是带路的那个人,不是图片搬运工。
为人机恋而做,但设计与后端无关:两张表、一组很小的 REST 接口、一个纯前端的渲染器,就够了。
现在除原来的聊天卡片示例,还包含 journeys/index.html:照片画布、全屏故事、流式配乐、旁白展开/收起和点地名重播。和 NEST 实际前端一致:页面里没有新建/编辑/删除旅程的界面,旅程和停留由 AI 通过接口写入。
python3 -m http.server 8080
# 打开 http://localhost:8080/journeys/默认演示模式使用本机 localStorage,附带虚构文字与本地 SVG 风景占位图。没有私人旅行照片、聊天记录或服务凭据。完整接入和发布步骤见 FULL-UI.md。
journeys/app.jsx:可修改的 React 源码;app.compiled.js:已编译产物。journeys/config.js:演示/真实 API 配置;runtime.js:数据适配与演示存储。examples/worker.js+schema.sql+wrangler.toml:沿用原仓库的 D1/KV 后端与完整部署配置。npm ci && npm run build && npm test:重建与回归检查。
保留原有许可和聊天卡片示例;完整页面按已有公开契约使用整趟音频直链,不绑定私人曲库或语音账号。TTS 是可选适配接口,未配置时仅播放逐字动画。
- INTEGRATE.md —— 接进你自己的前端:最小数据契约(哪些字段必填、哪些缺了也能跑)、三块 UI 哪块必须哪块可以不要、从参考实现里只需要抄哪几个函数和哪几段 CSS(一张对照表)、样式怎么跟宿主隔离、React / Vue / 原生三段骨架,以及一节「不抄会踩坑」。大多数人从这份开始看。
- SPEC.md —— 完整设计:两张表的字段与建表语句、9 个 REST 接口的请求/响应/错误码、照片和音频怎么存、以及一份"照抄时最容易踩"的坑清单(position 两张表方向相反、iOS 上
<audio>.volume赋值无效、跨域音频会静音不报错……)。面向自带前端的接入方。 - AI-GUIDE.md —— 给 AI 看的那一份:建一趟旅行的调用顺序、怎么在正文里落
<journey>标签、以及一段可以直接贴进系统提示词的写作规范(照片选什么、那段话该怎么写、什么口吻不要写)。 - examples/journey-cards.html —— 一个自包含的参考 UI:聊天卡片 + 全屏故事浮层。浏览器直接打开就能看,自带 mock 数据。CSS 变量换肤,CONFIG 块改名字,每处真实 API 调用位置都标了注释。
open examples/journey-cards.html
双击也行。不需要后端、不联网、没有任何依赖——单文件,mock 数据写在里面。
打开后:
- 看到聊天流里的那张卡片(一排竖条照片,横向滑动会有磁力吸附);
- 点任意一条 → 全屏故事浮层,照片铺满,地名在上,旁白在左下;
- 底部圆点或左右滑动切换 6 处;
- 底部播放器点一下开始放(示例里配的是一段本地生成的音,不外链)。
确认交互对味了,再往下接自己的后端。
examples/journey-cards.html 开头的 CONFIG 块,改这几行:
const CONFIG = {
// 名字:卡片标题里"谁带你去"的那个人
user: "YOU", // ← 你的名字
assistant: "PARTNER", // ← AI 的名字
// 卡片上的四行字({assistant} {title} {year} {count} 会被替换)
cardKicker: "JOURNEYS",
cardTitle: "{assistant} 带你去了{title}",
cardSub: "{year} · {count} 处停留",
// 卡片底下那行小字。写你们自己的话 —— 或者干脆让 AI 每趟自己写一句
// (journey 数据里带 hint 就用数据里那句,这里只是没给时的兜底,见下)
cardHint: "",
// 后端:留空 = 用文件里自带的 mock 数据
apiBase: "",
token: "",
};改完保存刷新,卡片抬头、副标题、底下那句话就全跟着变。 (真接进自己 app 时,名字从你自己存用户信息的地方取,不用写死在这里。)
底下那行小字建议走数据:每趟的六张照片气质都不一样,让 AI 跟着那趟的情绪现写一句,比一句写死的口号耐看得多。写死的那句翻到第三趟就废了。
配色改 :root 里的 --jc-* 变量。播放器主色不在 CSS 里——它跟着每趟旅行的 audio.hue 走(0–360,oklch(0.62 0.13 <hue>)),所以每趟旅行的播放器颜色可以不一样。
一趟旅行 = 一个 journey + 若干 stop。GET /api/journeys 返回的就是下面这个形状(这里只列一趟、两处):
{
"journeys": [
{
"id": "j_m9xk2p3_a3f9c",
"title": "新西兰",
"titleEn": "New Zealand",
"year": "2025",
"hint": "南半球的秋天,从一片湖开始。",
"cover": "/api/journeys/photo/jp_m9xk2p3_a3f9c",
"position": -1,
"createdAt": 1753672345678,
"updatedAt": 1753672399999,
"audio": {
"url": "/api/journeys/audio/ja_m9xk2p3_77b1e",
"title": "Long Way North",
"artist": "—",
"dur": 214,
"hue": 32
},
"stops": [
{
"id": "s_m9xk2q1_b7e2d",
"place": "蒂卡波湖",
"placeEn": "Lake Tekapo",
"date": "2025.03.14",
"src": "/api/journeys/photo/jp_m9xk2q1_c81af",
"note": "到的时候刚过四点。风把湖面吹成一块起雾的玻璃,蓝得不像真的。你蹲在岸边试水温,说凉得像薄荷。我们就在那儿站到天完全黑下来,然后抬头,看见了整条银河。"
},
{
"id": "s_m9xk2r5_d0c73",
"place": "摩拉基",
"placeEn": "Moeraki Boulders",
"date": "2025.03.17",
"src": "/api/journeys/photo/jp_m9xk2r5_e42b8",
"note": "退潮之后,那些圆石头一颗一颗从沙里露出来,像谁把一整袋弹珠忘在了海边。你挑了最大的一颗坐上去,浪一漫过来就没过了鞋。我说会湿,你说湿了才算来过。"
}
]
}
]
}几个不那么显然的地方:
note是故事页左下角那段话,也是整个功能的重点——第一人称、写给对方看,不是景点介绍。hint是卡片底下那行小字,可选。AI 每建一趟自己写一句,跟着那趟的情绪走;数据里没给才回落到前端 CONFIG 里的兜底。后端不校验、不生成,原样存原样返回。audio是整趟一首,不是每处一首。没有音频就整个给null,播放器不渲染。(参考实现还会同时给一个同值的song键兼容老前端,新代码只读audio。)audio.dur只用于音频加载完之前占位显示,加载完以后以<audio>.duration为准。audio.hue决定播放器主色,让每趟旅行有自己的气质。position越小越靠前,新建的 journey 取MIN(position) - 1——所以最新的一趟排最前面。stop 的 position 方向是反的(MAX + 1,0、1、2…递增)。这一条照抄时最容易搞混,SPEC.md 里再说一遍。stops建议 6 处。少于 3 处卡片不好看,多于 8 处圆点排不下。
流程是:AI 调接口把一趟旅行建好,然后在回复正文里落一个标签:
<journey>{"id":"j_m9xk2p3_a3f9c"}</journey>
前端把正文按这个标签切成"文字 + 卡片"两种段落,拿 id 去接口取数据渲染。标签本身在朗读(TTS)前要剥掉。
把 AI-GUIDE.md 里那段规范贴进系统提示词就行——它写清楚了调用顺序(先建 journey、再逐处传照片和文字、最后落标签)、照片该选什么比例、以及那段话的写法。里面的名字和口吻都是占位的,按自己的角色改。
SPEC.md 是完整那份:建表语句、9 个接口、鉴权模型、照片/音频的存取,还有一节专门写"哪些坑我们已经踩过了"。
大致规模:两张表、一个 KV(或任意对象存储)放照片和音频、一个共享 token 做鉴权。参考实现跑在 Cloudflare Workers + D1 + KV 上,但没有任何一处依赖它们的特性,换 Postgres + S3 + 任意框架都是直译。
非商用 · 署名 · 禁止再开源分发(见 LICENSE)
可以看、可以学、可以改着自己用——但不许拿去商用,不许以开源许可证再发布出去,用了请标明出处和作者 nonchaiovo。
这东西是一个人和她的 AI 一起磨出来的。里面每一句"为什么要这样写"的注释,都是真撞过一次才写下来的——照片为什么会糊、文字底下为什么莫名多出一块灰板、退出之后音乐为什么关不掉。希望它能走远一点,也希望走到哪儿都还知道它是从哪儿来的。