'use client';
import { useEffect, useRef, useState } from 'react';
import { useRouter } from 'next/navigation';
function Starfield() {
const canvasRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
let animId;
const STARS = 180;
const stars = Array.from({ length: STARS }, () => ({
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
z: Math.random() * window.innerWidth,
pz: 0,
}));
const resize = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
resize();
window.addEventListener('resize', resize);
const cx = () => canvas.width / 2;
const cy = () => canvas.height / 2;
const tick = () => {
ctx.fillStyle = 'rgba(5,3,15,0.25)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
stars.forEach((s) => {
s.pz = s.z;
s.z -= 4;
if (s.z <= 0) {
s.x = Math.random() * canvas.width;
s.y = Math.random() * canvas.height;
s.z = canvas.width;
s.pz = s.z;
}
const sx = (s.x - cx()) * (canvas.width / s.z) + cx();
const sy = (s.y - cy()) * (canvas.width / s.z) + cy();
const px = (s.x - cx()) * (canvas.width / s.pz) + cx();
const py = (s.y - cy()) * (canvas.width / s.pz) + cy();
const size = Math.max(0.3, (1 - s.z / canvas.width) * 2.5);
const alpha = 1 - s.z / canvas.width;
ctx.strokeStyle = `rgba(200,190,255,${alpha})`;
ctx.lineWidth = size;
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(sx, sy);
ctx.stroke();
});
animId = requestAnimationFrame(tick);
};
tick();
return () => {
cancelAnimationFrame(animId);
window.removeEventListener('resize', resize);
};
}, []);
return ;
}
function FadeText({ text, visible, className = '' }) {
return (
{text}
);
}
function Intro({ onDone }) {
const [phase, setPhase] = useState(0);
useEffect(() => {
const timers = [
setTimeout(() => setPhase(1), 600),
setTimeout(() => setPhase(2), 2200),
setTimeout(() => setPhase(3), 3000),
setTimeout(() => setPhase(4), 4800),
setTimeout(() => setPhase(5), 5600),
];
return () => timers.forEach(clearTimeout);
}, []);
return (
= 5 ? 1 : 0, transform: phase >= 5 ? 'translateY(0)' : 'translateY(12px)' }}>
);
}
const fixedOptions = {
fixed_gender: ['男生', '女生', '不想说'],
fixed_age: ['20以下', '20-25', '26-30', '31-40', '40以上'],
fixed_birth_month: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
fixed_birth_day: Array.from({ length: 31 }, (_, i) => `${i + 1}日`),
};
function QuestionView({ prompt, onAnswer }) {
const [visible, setVisible] = useState(false);
const [input, setInput] = useState('');
const [leaving, setLeaving] = useState(false);
const [picked, setPicked] = useState('');
const inputRef = useRef(null);
useEffect(() => {
setVisible(false);
setInput('');
setLeaving(false);
setPicked('');
const t = setTimeout(() => {
setVisible(true);
if ((prompt?.type || 'text') === 'text') {
setTimeout(() => inputRef.current?.focus(), 600);
}
}, 100);
return () => clearTimeout(t);
}, [prompt]);
const submit = (val) => {
const answer = (val || '').trim();
if (!answer) return;
setLeaving(true);
setTimeout(() => onAnswer(answer), 700);
};
const type = prompt?.type || 'text';
const isChoice = type === 'choice' || type.startsWith('fixed_');
const question = prompt?.question || prompt?.reply || '';
const options = type === 'choice' ? (prompt.options || []) : (fixedOptions[type] || []);
const compact = type === 'fixed_birth_day';
return (
{question}
{isChoice ? (
{options.map((op, i) => {
const selected = picked === op;
const othersFade = picked && picked !== op;
return (
);
})}
) : (
)}
);
}
export default function ChatPage() {
const [stage, setStage] = useState('intro');
const [prompt, setPrompt] = useState({ type: 'text', reply: '' });
const [sid, setSid] = useState('');
const router = useRouter();
// 启动时检查:如果已有session且报告done,直接跳报告页
useEffect(() => {
const savedSid = localStorage.getItem('lingjing_sid');
if (!savedSid) return;
fetch(`/api/report?sessionId=${encodeURIComponent(savedSid)}`)
.then(r => r.json())
.then(d => {
if (d?.status === 'done') {
router.replace(`/report-preview?sessionId=${encodeURIComponent(savedSid)}`);
}
})
.catch(() => {});
}, [router]);
const startChat = async () => {
setStage('loading');
try {
const r = await fetch('/api/session/new', { method: 'POST' });
const d = await r.json();
const sessionId = d.sessionId || '';
setSid(sessionId);
localStorage.setItem('lingjing_sid', sessionId);
setPrompt({ type: 'text', reply: d.opening || d.reply || '' });
setStage('chat');
} catch {
setStage('chat');
setPrompt({ type: 'text', reply: '开始前,我想先简单认识你一下。就像朋友聊天,想到什么说什么就行,没有标准答案。咱们先从几个轻松的小问题开始,可以吗?' });
}
};
const handleAnswer = async (answer) => {
setStage('waiting');
try {
const r = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: sid, answer }),
});
const d = await r.json();
if (d.done) {
setStage('done');
const targetSid = d.sessionId || sid;
setTimeout(() => router.push(`/waiting?sessionId=${encodeURIComponent(targetSid || '')}`), 1200);
return;
}
if (d.type === 'choice') {
setPrompt({ type: 'choice', question: d.question, options: d.options || [] });
} else if (d.type && d.type.startsWith('fixed_')) {
setPrompt({ type: d.type, question: d.question });
} else {
setPrompt({ type: 'text', reply: d.reply || d.question || '我听到了,我们继续。' });
}
setStage('chat');
} catch {
setPrompt({ type: 'text', reply: '网络有点问题,我们继续。你刚才的回答我已经记住了。' });
setStage('chat');
}
};
return (
<>
{stage === 'intro' && }
{stage === 'waiting' && (
)}
{stage === 'loading' && (
)}
{stage === 'chat' && }
{stage === 'done' && (
)}
>
);
}