feat: 改进计划生成体验与 AI 提示词
- 生成中状态改为滚动日志列表,底部新增消息自动滚动 - 返回想法池不再清空计划,pool 页面保留"待确认计划"横幅 - 换方案时才清空旧计划 - 提示词补充午餐/晚餐时间窗口约束(午餐11:30-13:00,晚餐17:30-19:30) - get_travel_time 从驾车改为公共交通,阈值从30分钟调整为45分钟
This commit is contained in:
@@ -116,7 +116,8 @@ export default function BlindboxRoomPage() {
|
||||
const [planDays, setPlanDays] = useState<WeekendPlanData[]>([]);
|
||||
const [planAccepted, setPlanAccepted] = useState(false);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [planStatusMessage, setPlanStatusMessage] = useState("正在分析你们的想法...");
|
||||
const [planStatusMessages, setPlanStatusMessages] = useState<string[]>([]);
|
||||
const planLogRef = useRef<HTMLDivElement>(null);
|
||||
const [showPlanShareCard, setShowPlanShareCard] = useState(false);
|
||||
const [activeContract, setActiveContract] = useState<{
|
||||
id: string;
|
||||
@@ -286,6 +287,12 @@ export default function BlindboxRoomPage() {
|
||||
return () => clearTimeout(timer);
|
||||
}, [activeContract?.endTime]);
|
||||
|
||||
useEffect(() => {
|
||||
if (planLogRef.current) {
|
||||
planLogRef.current.scrollTop = planLogRef.current.scrollHeight;
|
||||
}
|
||||
}, [planStatusMessages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMember && inputRef.current) {
|
||||
const t = setTimeout(() => inputRef.current?.focus(), 300);
|
||||
@@ -343,7 +350,7 @@ export default function BlindboxRoomPage() {
|
||||
setGenerating(true);
|
||||
setPhase("planning");
|
||||
setError("");
|
||||
setPlanStatusMessage(PLAN_STATUS_STEPS[0]);
|
||||
setPlanStatusMessages([PLAN_STATUS_STEPS[0]]);
|
||||
const payload = {
|
||||
roomId: room.id,
|
||||
userId: profile.id,
|
||||
@@ -352,7 +359,7 @@ export default function BlindboxRoomPage() {
|
||||
const stepRef = { current: 0 };
|
||||
const fallbackTimer = setInterval(() => {
|
||||
stepRef.current = (stepRef.current + 1) % PLAN_STATUS_STEPS.length;
|
||||
setPlanStatusMessage(PLAN_STATUS_STEPS[stepRef.current]);
|
||||
setPlanStatusMessages((prev) => [...prev, PLAN_STATUS_STEPS[stepRef.current]]);
|
||||
}, 2800);
|
||||
try {
|
||||
const res = await fetch("/api/blindbox/plan/stream", {
|
||||
@@ -381,7 +388,7 @@ export default function BlindboxRoomPage() {
|
||||
if (line.startsWith("event:")) eventType = line.slice(6).trim();
|
||||
else if (line.startsWith("data:")) data = line.slice(5).trim();
|
||||
}
|
||||
if (eventType === "status") setPlanStatusMessage(data);
|
||||
if (eventType === "status") setPlanStatusMessages((prev) => [...prev, data]);
|
||||
else if (eventType === "plan") {
|
||||
const parsed = JSON.parse(data);
|
||||
setPlanId(parsed.id);
|
||||
@@ -916,6 +923,21 @@ export default function BlindboxRoomPage() {
|
||||
{error}
|
||||
</motion.p>
|
||||
)}
|
||||
|
||||
{planDays.length > 0 && !planAccepted && (
|
||||
<motion.button
|
||||
onClick={() => setPhase("plan_reveal")}
|
||||
className="flex w-full items-center justify-between rounded-xl bg-purple-600/10 px-4 py-2.5 ring-1 ring-purple-500/30 active:bg-purple-600/20"
|
||||
initial={{ opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles size={14} className="text-purple-400" />
|
||||
<span className="text-xs font-bold text-purple-300">有一个待确认的计划</span>
|
||||
</div>
|
||||
<ChevronRight size={14} className="text-purple-400" />
|
||||
</motion.button>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -1017,23 +1039,42 @@ export default function BlindboxRoomPage() {
|
||||
{phase === "planning" && (
|
||||
<motion.div
|
||||
key="planning"
|
||||
className="mt-8 flex flex-col items-center gap-4"
|
||||
className="mt-8 flex flex-col items-center gap-4 w-full px-4"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<motion.div
|
||||
className="relative flex h-20 w-20 items-center justify-center"
|
||||
className="relative flex h-16 w-16 items-center justify-center"
|
||||
animate={{ rotate: [0, 360] }}
|
||||
transition={{ duration: 3, repeat: Infinity, ease: "linear" }}
|
||||
>
|
||||
<div className="absolute inset-0 rounded-full bg-purple-600/15 blur-lg" />
|
||||
<Sparkles size={28} className="relative text-purple-400" />
|
||||
<Sparkles size={24} className="relative text-purple-400" />
|
||||
</motion.div>
|
||||
<p className="text-sm font-bold text-purple-300 animate-pulse">
|
||||
{planStatusMessage}
|
||||
</p>
|
||||
<p className="text-[11px] text-dim">搜索地点 · 优化路线 · 安排时间</p>
|
||||
<div className="w-full max-w-sm rounded-xl bg-surface/60 ring-1 ring-border/60 overflow-hidden">
|
||||
<div
|
||||
ref={planLogRef}
|
||||
className="h-40 overflow-y-auto scrollbar-none p-3 flex flex-col gap-1.5"
|
||||
>
|
||||
{planStatusMessages.map((msg, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className={`flex items-center gap-2 text-xs ${i === planStatusMessages.length - 1 ? "text-purple-300 font-medium" : "text-dim"}`}
|
||||
>
|
||||
{i === planStatusMessages.length - 1 ? (
|
||||
<Loader2 size={11} className="shrink-0 animate-spin text-purple-400" />
|
||||
) : (
|
||||
<span className="shrink-0 text-[10px] text-purple-500">✓</span>
|
||||
)}
|
||||
{msg}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -1076,14 +1117,14 @@ export default function BlindboxRoomPage() {
|
||||
}, 1500));
|
||||
}}
|
||||
onRegenerate={() => {
|
||||
setPlanId(null);
|
||||
setPlanDays([]);
|
||||
setPlanAccepted(false);
|
||||
setPhase("time_select");
|
||||
}}
|
||||
onShare={() => setShowPlanShareCard(true)}
|
||||
onBack={() => {
|
||||
setPhase("pool");
|
||||
setPlanId(null);
|
||||
setPlanDays([]);
|
||||
setPlanAccepted(false);
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
Reference in New Issue
Block a user