feat: v0.1封版——沉浸式对话MVP,AI动态提问,探索中loading动画

This commit is contained in:
root 2026-02-23 10:35:08 +00:00
parent 133e417298
commit a7b746df5f
2 changed files with 53 additions and 27 deletions

View File

@ -210,7 +210,7 @@ function QuestionView({ question, onAnswer, questionIndex, total }) {
// //
export default function ChatPage() { export default function ChatPage() {
const [stage, setStage] = useState('intro'); // intro | chat | loading | done const [stage, setStage] = useState('intro'); // intro | chat | waiting | loading | done
const [question, setQuestion] = useState(''); const [question, setQuestion] = useState('');
const [qIndex, setQIndex] = useState(0); const [qIndex, setQIndex] = useState(0);
const [total, setTotal] = useState(30); const [total, setTotal] = useState(30);
@ -236,6 +236,7 @@ export default function ChatPage() {
}; };
const handleAnswer = async (answer) => { const handleAnswer = async (answer) => {
setStage('waiting'); //
try { try {
const r = await fetch('/api/chat', { const r = await fetch('/api/chat', {
method: 'POST', method: 'POST',
@ -250,9 +251,11 @@ export default function ChatPage() {
} }
setQuestion(d.reply); setQuestion(d.reply);
setQIndex((i) => i + 1); setQIndex((i) => i + 1);
setStage('chat');
} catch { } catch {
setQuestion('网络有点问题,我们继续。你刚才的回答我已经记住了。'); setQuestion('网络有点问题,我们继续。你刚才的回答我已经记住了。');
setQIndex((i) => i + 1); setQIndex((i) => i + 1);
setStage('chat');
} }
}; };
@ -260,6 +263,22 @@ export default function ChatPage() {
<> <>
<Starfield /> <Starfield />
{stage === 'intro' && <Intro onDone={startChat} />} {stage === 'intro' && <Intro onDone={startChat} />}
{stage === 'waiting' && (
<div className="relative z-10 flex h-screen h-dvh items-center justify-center">
<div
className="text-base tracking-[0.3em] text-white/60"
style={{ animation: 'breathe 2.4s ease-in-out infinite' }}
>
探索中
</div>
<style>{`
@keyframes breathe {
0%, 100% { opacity: 0.2; }
50% { opacity: 0.9; }
}
`}</style>
</div>
)}
{stage === 'loading' && ( {stage === 'loading' && (
<div className="relative z-10 flex h-screen h-dvh items-center justify-center"> <div className="relative z-10 flex h-screen h-dvh items-center justify-center">
<div className="text-sm tracking-widest text-white/30 animate-pulse">正在连接...</div> <div className="text-sm tracking-widest text-white/30 animate-pulse">正在连接...</div>

View File

@ -17,7 +17,7 @@ const opening = '开始前,我想先简单认识你一下。就像朋友聊天
const questionBank = [ const questionBank = [
'你今年大概在哪个年龄段比如20岁以下、20-29、30-39这样。', '你今年大概在哪个年龄段比如20岁以下、20-29、30-39这样。',
'你平时怎么称呼自己更舒服', '你是男生还是女生呀',
'你现在主要在做什么?上学、上班、自己做事,还是在休息调整?', '你现在主要在做什么?上学、上班、自己做事,还是在休息调整?',
'最近这几天,有没有一件小事让你心情变好?', '最近这几天,有没有一件小事让你心情变好?',
'如果只说一件事,你现在最发愁的是什么?', '如果只说一件事,你现在最发愁的是什么?',
@ -60,21 +60,25 @@ function loadSystemPrompt() {
async function callAI(messages) { async function callAI(messages) {
const key = process.env.YCAPIS_API_KEY; const key = process.env.YCAPIS_API_KEY;
if (!key) return null; if (!key) return null;
const res = await fetch('https://ycapis.com/v1/chat/completions', { try {
method: 'POST', const res = await fetch('https://ycapis.com/v1/responses', {
headers: { method: 'POST',
'Content-Type': 'application/json', headers: {
Authorization: `Bearer ${key}`, 'Content-Type': 'application/json',
}, Authorization: `Bearer ${key}`,
body: JSON.stringify({ },
model: 'gpt-5.3-codex', body: JSON.stringify({
temperature: 0.7, model: 'gpt-5.3-codex',
messages, input: messages.map(m => ({ role: m.role, content: m.content })),
}), max_output_tokens: 300,
}); }),
if (!res.ok) return null; });
const data = await res.json(); if (!res.ok) return null;
return data?.choices?.[0]?.message?.content || null; const data = await res.json();
return data?.output?.[0]?.content?.[0]?.text || null;
} catch {
return null;
}
} }
function previewReport(session) { function previewReport(session) {
@ -118,7 +122,7 @@ app.prepare().then(() => {
server.post('/api/session/new', (req, res) => { server.post('/api/session/new', (req, res) => {
const sessionId = `lgj_${Date.now().toString(36)}`; const sessionId = `lgj_${Date.now().toString(36)}`;
sessions.set(sessionId, { index: 0, answers: [], skips: 0, prompt: loadSystemPrompt() }); sessions.set(sessionId, { index: 0, answers: [], questions: [], skips: 0, prompt: loadSystemPrompt() });
res.json({ sessionId, opening, total: 30 }); res.json({ sessionId, opening, total: 30 });
}); });
@ -134,25 +138,28 @@ app.prepare().then(() => {
s.index += 1; s.index += 1;
} }
const done = s.index >= questionBank.length; const done = s.index >= 30;
if (done) { if (done) {
return res.json({ done: true, reply: '你已经完成全部对话。接下来我会为你生成专属灵镜报告。' }); return res.json({ done: true, reply: '你已经完成全部对话。接下来我会为你生成专属灵镜报告。' });
} }
const progress = s.index > 0 && s.index % 5 === 0 ? `你已经完成第${Math.ceil(s.index / 6)}/5阶段了。` : ''; const progress = s.index > 0 && s.index % 6 === 0 ? `(你已经完成第${Math.ceil(s.index / 6)}/5阶段了。` : '';
const bridge = userAnswer ? '我听懂了。' : '我们继续。';
const nextQ = questionBank[s.index];
let aiReply = `${bridge}${progress ? ` ${progress}` : ''} ${nextQ}`.trim();
// 全部交给AI动态生成不用硬编码题库
const ds = await callAI([ const ds = await callAI([
{ role: 'system', content: s.prompt }, { role: 'system', content: s.prompt },
{ role: 'assistant', content: opening }, { role: 'assistant', content: opening },
...s.answers.slice(-8).map((a) => ({ role: 'user', content: a })), ...s.answers.flatMap((a, i) => [
{ role: 'user', content: `请按规则继续下一问。当前问题建议:${nextQ}` }, { role: 'assistant', content: s.questions[i] || '' },
{ role: 'user', content: a }
]).filter(m => m.content),
{ role: 'user', content: `[系统提示] 用户刚才回答了:"${userAnswer}"。${progress}请用温暖口语接住他的回答,然后继续问下一个问题。注意:只问一个问题,用大白话,不用专业术语。当前已问第${s.index}共30题。` }
]); ]);
if (ds) aiReply = ds; const aiReply = ds || '嗯,我听到了。我们继续,你现在主要在做什么?上学、上班、还是在休整?';
// 记录AI问的问题
s.questions.push(aiReply);
return res.json({ done: false, reply: aiReply, index: s.index + 1, total: 30 }); return res.json({ done: false, reply: aiReply, index: s.index + 1, total: 30 });
}); });