写自己的AI聊天伴侣页面,这事听起来有点标题党,但真做起来也就是一个HTML文件的事。我之前老觉得现在写前端,动不动就要脚手架、依赖、构建,明明只想验证一个聊天交互,结果光装环境就花掉半小时。后来被逼急了,给自己定了三条硬规矩:纯前端、零依赖、单文件,所有代码塞进一个HTML里,双击浏览器打开就能聊。实测下来体验反而极好——没有构建步骤,没有环境变量,没有跨语言调试,一个文件既是项目也是产物,发给别人就是一个文件的事。
这个方案特别适合想快速体验大模型接口的人,也适合做AI情感陪伴类小Demo的前端开发者。不需要你懂Node、Python或者部署,只要会一点HTML/CSS/JavaScript,再有一个能调用的大模型API,就能拼出一个可以语音、可以记住聊天历史、还能设定性格人设的AI角色页面。下面我把整个设计思路和实现过程拆开写一遍,尽量把能踩的坑都提前帮你踩了。
1. 项目设计思路:为什么要坚持零依赖和单文件
先解释一下“零依赖”到底指什么。不是不需要网络、不需要大模型API,而是说你不需要安装任何前端框架、UI库、打包工具和运行时。浏览器自带的fetch能发请求,原生CSS能做样式,DOM API能处理交互,Web Speech API还能负责语音识别与合成,这些能力加起来已经能覆盖一个完整聊天产品的绝大部分需求。所以结论是:做一个聊天伴侣页面,工程化并不是必需品。
单文件的核心优势是分发成本低。我试过把一个单HTML挂在任意静态托管上,或者在本地用python -m http.server跑起来,同一个文件,走到哪都能用。更关键的是排查问题简单——不会出现“本地是好的,打包后就坏了”这种环境差异。以前聊AI应用动不动就起个Vite项目,在别人电脑上还要先装Node再装依赖,这对一个只想看效果的Demo来说太重了。单文件的另一层好处是方便改:想换个性格设定,搜一下“system prompt”就能找到入口;想调样式,直接改CSS变量,不用翻半天的组件树。
当然,单文件也有代价,最明显的是代码组织。所有逻辑都放在一个<script>里,如果写得乱,很快就会变成一坨谁都不想看的代码。我的做法是把整个文件当成一个“微型单体应用”来设计:配置区独立放最前面,UI结构用一段HTML,样式用CSS自定义属性统一管理,脚本部分严格按功能拆成几段注释标记的模块。只要你愿意在注释上花点时间,单文件也可以保持清晰的边界。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 单文件应用内的“架构”:页面结构、状态流与消息管理
2.1 页面骨架与样式策略
先画一个极简结构:顶部是AI角色信息,中间是滚动消息区,底部是输入框和发送按钮。这个结构几乎不用思考,但它能跑通主要是因为底部输入区要固定在视野内,中间内容区要可以自由滚动,所以我用了三块纵向Flex布局,把聊天主区域设为flex: 1; overflow-y: auto,这样消息多了也不会撑破页面。
视觉上建议做成手机聊天窗口的感觉会更讨喜。整个页面最大宽度控制在480px左右居中,背景用深色渐变加一层柔和光晕,聊天气泡左侧是头像、右侧是你发的消息。头像可以直接用CSS画一个简单的圆形渐变,也可以放一张本地图片。CSS变量把所有主题色都收在一起,想换配色就改几个变量,改起来很省事。
输入框要用textarea而不是单行input,这样才能支持多行输入和自然换行。回车发送、Shift+回车换行是常见交互,这里的实现是在keydown事件里判断event.key === 'Enter' && !event.shiftKey,然后preventDefault()再触发送逻辑。这个体验在移动端键盘上尤其重要,不然手机会很难受。
2.2 数据结构与状态管理
聊天页面虽然看起来简单,但状态管理如果不在第一时间理清楚,后面加记忆、插件、语音等功能时就会乱。我的做法是维护一个全局messages数组,只保存原始结构:
javascript复制const state = {
config: { /* API地址、密钥、模型名、角色参数 */ },
messages: [
{ role: 'system', content: '你是住在这个浏览器页面里的AI角色……' },
{ role: 'user', content: '你好' },
{ role: 'assistant', content: '嗨,终于等到你来了。' }
],
isGenerating: false,
currentGPTMessage: ''
};
UI层不直接改这条数据,而是每次通过渲染函数统一把state.messages画到页面上。这样不用做繁杂的DOM双向同步,所有状态变更都收敛到数组的操作上,调试的时候直接在控制台打印就能看得一清二楚。发送消息时会先push一条user消息,再调用大模型接口,拿到结果后把回复push成assistant消息,然后触发一次完整渲染。
有一点要注意:system消息不应该出现在聊天界面上,角色信息已经在顶部展示过了,所以渲染时要从messages[1]开始遍历。另外发送按钮和输入框在isGenerating状态时应该禁用,否则
