From df2e373bebd9158cda03ca273b5df9abfc0453d5 Mon Sep 17 00:00:00 2001 From: kurihada Date: Mon, 2 Mar 2026 11:26:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=B9=E8=BF=9B=E8=AE=A1=E5=88=92?= =?UTF-8?q?=E7=94=9F=E6=88=90=E4=BD=93=E9=AA=8C=E4=B8=8E=20AI=20=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 生成中状态改为滚动日志列表,底部新增消息自动滚动 - 返回想法池不再清空计划,pool 页面保留"待确认计划"横幅 - 换方案时才清空旧计划 - 提示词补充午餐/晚餐时间窗口约束(午餐11:30-13:00,晚餐17:30-19:30) - get_travel_time 从驾车改为公共交通,阈值从30分钟调整为45分钟 --- src/app/blindbox/[code]/page.tsx | 69 +++++++++++++++++++++++++------- src/lib/ai.ts | 13 +++--- src/lib/blindboxPlanGen.ts | 33 ++++++++------- 3 files changed, 80 insertions(+), 35 deletions(-) diff --git a/src/app/blindbox/[code]/page.tsx b/src/app/blindbox/[code]/page.tsx index 1001535..36ee47e 100644 --- a/src/app/blindbox/[code]/page.tsx +++ b/src/app/blindbox/[code]/page.tsx @@ -116,7 +116,8 @@ export default function BlindboxRoomPage() { const [planDays, setPlanDays] = useState([]); const [planAccepted, setPlanAccepted] = useState(false); const [generating, setGenerating] = useState(false); - const [planStatusMessage, setPlanStatusMessage] = useState("正在分析你们的想法..."); + const [planStatusMessages, setPlanStatusMessages] = useState([]); + const planLogRef = useRef(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} )} + + {planDays.length > 0 && !planAccepted && ( + 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 }} + > +
+ + 有一个待确认的计划 +
+ +
+ )} )} @@ -1017,23 +1039,42 @@ export default function BlindboxRoomPage() { {phase === "planning" && (
- + -

- {planStatusMessage} -

-

搜索地点 · 优化路线 · 安排时间

+
+
+ {planStatusMessages.map((msg, i) => ( + + {i === planStatusMessages.length - 1 ? ( + + ) : ( + + )} + {msg} + + ))} +
+
)} @@ -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); }} /> diff --git a/src/lib/ai.ts b/src/lib/ai.ts index febd93b..6d4e83b 100644 --- a/src/lib/ai.ts +++ b/src/lib/ai.ts @@ -34,12 +34,13 @@ const SCHEDULE_SYSTEM_PROMPT = `你是一个周末行程规划师。根据用户 规划原则: 1. 选择地理位置相近的 POI,最小化总移动距离 -2. 尊重活动的时间偏好(公园上午、正餐在饭点、电影灵活) -3. 活动之间留出合理的交通时间(15-30分钟) -4. 如果有"category"类型的活动,选择离其他已确定地点最近的候选 -5. 高低强度活动交替安排,避免连续高体力活动 -6. 费用平衡,避免连续安排 premium 级别活动 -7. 需要预约(needsBooking)的活动,在 reason 中提醒用户提前预约 +2. 严格遵守用餐时间窗口:午餐安排在 11:30-13:00,晚餐安排在 17:30-19:30,不得超出此范围 +3. 尊重活动的时间偏好(morning 的活动放 9:00-12:00,afternoon 的活动放 13:00-17:00,evening 的活动放 19:00 以后) +4. 活动之间留出合理的交通时间(15-30分钟) +5. 如果有"category"类型的活动,选择离其他已确定地点最近的候选 +6. 高低强度活动交替安排,避免连续高体力活动 +7. 费用平衡,避免连续安排 premium 级别活动 +8. 需要预约(needsBooking)的活动,在 reason 中提醒用户提前预约 返回 JSON 格式: { diff --git a/src/lib/blindboxPlanGen.ts b/src/lib/blindboxPlanGen.ts index 75c3f87..3ea28a8 100644 --- a/src/lib/blindboxPlanGen.ts +++ b/src/lib/blindboxPlanGen.ts @@ -195,7 +195,7 @@ export interface PlanGenResult { const AGENT_SYSTEM_PROMPT = `你是一个周末行程规划 Agent。你有以下工具可以使用: - list_ideas: 查看想法池中的所有活动 - search_poi: 在地图上搜索地点(支持品牌名、地名、品类搜索) -- get_travel_time: 查询两点间驾车时间和距离 +- get_travel_time: 查询两点间公共交通(地铁/公交)时间和距离 - finalize_plan: 提交最终行程方案 规划流程: @@ -203,13 +203,14 @@ const AGENT_SYSTEM_PROMPT = `你是一个周末行程规划 Agent。你有以下 2. 根据时间、多样性、强度平衡选出合适的活动组合 3. 为每个活动 search_poi 找到具体地点 4. 如果搜索结果不理想(0结果或不相关),尝试换关键词重搜 -5. 用 get_travel_time 检查关键路段的交通时间,如果某段超过 30 分钟考虑换更近的地点 +5. 用 get_travel_time 检查关键路段的公共交通时间,如果某段超过 45 分钟考虑换更近的地点 6. 综合地理位置、时间安排、活动特点,规划最优路线 7. 确认无误后 finalize_plan 提交 规划原则: - 地理位置相近,最小化移动距离 -- 尊重时间偏好(morning 的活动放上午,dining 在饭点) +- 严格遵守用餐时间窗口:午餐安排在 11:30-13:00,晚餐安排在 17:30-19:30,不得超出此范围 +- 尊重时间偏好(morning 的活动放 9:00-12:00,afternoon 的活动放 13:00-17:00,evening 的活动放 19:00 以后) - 活动间留 15-30 分钟交通时间 - 高低强度交替,避免连续高体力活动 - 费用均衡,不连续安排 premium 活动 @@ -236,6 +237,7 @@ function buildAgentTools( taggedIdeas: TaggedIdea[], lat: number, lng: number, + city: string, ): AgentTool[] { const listIdeasTool: AgentTool = { name: "list_ideas", @@ -301,7 +303,7 @@ function buildAgentTools( const getTravelTimeTool: AgentTool = { name: "get_travel_time", - description: "查询两点之间的驾车预估时间和距离。用于验证活动之间的交通是否合理", + description: "查询两点之间的公共交通(地铁/公交)预估时间和距离。用于验证活动之间的交通是否合理", parameters: { type: "object", properties: { @@ -318,31 +320,32 @@ function buildAgentTools( const dLat = Number(args.dest_lat); const dLng = Number(args.dest_lng); const apiKey = requireAmapApiKey(); - const url = new URL("https://restapi.amap.com/v5/direction/driving"); + const url = new URL("https://restapi.amap.com/v5/direction/transit/integrated"); url.searchParams.set("key", apiKey); url.searchParams.set("origin", `${oLng},${oLat}`); url.searchParams.set("destination", `${dLng},${dLat}`); + url.searchParams.set("city", city); url.searchParams.set("show_fields", "cost"); try { const res = await fetch(url.toString()); const data = await res.json(); - if (data.status !== "1" || !data.route?.paths?.length) { - return JSON.stringify({ error: "未找到路线" }); + if (data.status !== "1" || !data.route?.transits?.length) { + return JSON.stringify({ error: "未找到公交路线" }); } - const path = data.route.paths[0]; + const transit = data.route.transits[0]; return JSON.stringify({ - distanceKm: Math.round(Number(path.distance) / 100) / 10, - durationMin: Math.ceil(Number(path.duration) / 60), + distanceKm: Math.round(Number(data.route.distance) / 100) / 10, + durationMin: Math.ceil(Number(transit.duration) / 60), }); } catch { return JSON.stringify({ error: "路线查询失败" }); } }, - progressBefore: () => "正在查询交通时间...", + progressBefore: () => "正在查询公共交通时间...", progressAfter: (_args, result) => { const r = JSON.parse(result); if (r.error) return `路线查询失败`; - return `驾车约 ${r.durationMin} 分钟(${r.distanceKm}km)`; + return `公共交通约 ${r.durationMin} 分钟(${r.distanceKm}km)`; }, }; @@ -394,13 +397,13 @@ function buildAgentTools( } async function runAgentPlanGeneration( - room: { lat: number; lng: number }, + room: { lat: number; lng: number; city: string }, taggedIdeas: TaggedIdea[], availableTime: PlanGenAvailableTime, onProgress?: (message: string) => void, ): Promise<{ days: FinalizePlanDay[] }> { const at = availableTime; - const tools = buildAgentTools(taggedIdeas, room.lat, room.lng); + const tools = buildAgentTools(taggedIdeas, room.lat, room.lng, room.city ?? ""); const userPrompt = `帮我规划行程。 可用时间:${at.date},${at.startHour}:00 - ${at.endHour}:00 @@ -632,7 +635,7 @@ export async function runPlanGeneration( try { const agentResult = await runAgentPlanGeneration( - { lat: room.lat, lng: room.lng }, + { lat: room.lat, lng: room.lng, city: room.city ?? "" }, taggedIdeas, availableTime, onProgress,