const store = require('../../utils/store.js'); const api = require('../../utils/api.js'); const { toastErr } = require('../../utils/ui.js'); const SESSION = 'chat'; const PRESETS = ['猫咪今天吐了一次怎么办?', '幼猫多久驱虫一次?', '换粮后软便怎么办?', '疫苗打完要注意什么?']; // 打字机:每帧吐几个字。太快没效果,太慢让人等,30ms/2 字接近正常朗读速度。 const TYPE_STEP = 2; const TYPE_INTERVAL = 30; Page({ data: { pet: {}, messages: [], input: '', presets: PRESETS, sending: false, loaded: false, }, onLoad() { this._unsub = store.subscribe((pet) => this.setData({ pet })); store .ready() .then(() => { this.setData({ pet: store.getPet() }); this.loadHistory(); }) .catch((e) => toastErr(e)); }, onUnload() { if (this._unsub) this._unsub(); this.stopTyping(); }, loadHistory() { const opening = { role: 'ai', text: '我是' + (store.getPet().name || '毛孩子') + '的养宠助手。我会优先根据档案、计划和历史记录回答,而不是泛泛聊天。', }; api .aiMessages(SESSION, 50) .then((list) => { const history = (list || []).map((m) => ({ role: m.role === 'user' ? 'user' : 'ai', text: m.text })); this.setData({ messages: [opening].concat(history), loaded: true }, () => this.scrollToBottom()); }) .catch(() => this.setData({ messages: [opening], loaded: true })); }, onInput(e) { this.setData({ input: e.detail.value }); }, onSend(e) { const preset = e.currentTarget && e.currentTarget.dataset.text; const text = (preset || this.data.input || '').trim(); if (!text || this.data.sending) return; // 先把用户消息和一个空的 AI 气泡放上去,AI 气泡随后逐字填充 const messages = this.data.messages.concat([ { role: 'user', text }, { role: 'ai', text: '', pending: true }, ]); this.setData({ messages, input: '', sending: true }, () => this.scrollToBottom()); api .aiChat({ pet_id: store.currentPetId() || null, session: SESSION, text }) .then((res) => this.typewrite(res.reply || '(没有返回内容)')) .catch((e) => { const msg = (e && e.message) || '网络异常,请稍后再试。'; // 额度用完不是故障,别让用户以为是网络问题反复重试 this.typewrite(msg.indexOf('次数') >= 0 ? msg + '\n\n每天的次数是为了控制成本,明天 0 点恢复。' : msg); }); }, // 逐字把回复填进最后一个气泡 typewrite(full) { this.stopTyping(); const idx = this.data.messages.length - 1; let n = 0; this._timer = setInterval(() => { n = Math.min(full.length, n + TYPE_STEP); this.setData({ [`messages[${idx}].text`]: full.slice(0, n) }); if (n % 20 === 0) this.scrollToBottom(); if (n >= full.length) { this.stopTyping(); this.setData({ [`messages[${idx}].pending`]: false, sending: false }, () => this.scrollToBottom()); } }, TYPE_INTERVAL); }, stopTyping() { if (this._timer) { clearInterval(this._timer); this._timer = null; } }, // 打字过程中点一下可以跳过,不用干等 onSkip() { if (!this.data.sending) return; this.stopTyping(); this.setData({ sending: false }); }, scrollToBottom() { this.setData({ scrollInto: 'chat-bottom' }); }, goAIPlan() { wx.navigateTo({ url: '/pages/plan/plan?tab=aiPlan' }); }, onShareAppMessage() { return { title: '肉垫计划 · AI 养宠助手', path: '/pages/ai/ai' }; }, });