import { useContext, useId, useRef, useState } from "react"; import { ArrowUpRight, CheckCircle2, FileCheck2, Info, LoaderCircle, MapPin, TriangleAlert } from "lucide-react"; import { useAnswerMotion } from "./answer-motion-context"; import { motion } from "motion/react"; import { agentEase } from "../components/agent-motion"; import { AnswerDisclosure, AnswerDisclosureChevron } from "./answer-motion"; import { Button } from "@/shared/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/shared/ui/card"; import { DateTimeField } from "@/shared/ui/date-time-field"; import { Input } from "@/shared/ui/input"; import { Textarea } from "@/shared/ui/textarea"; import { AnswerInteractionContext as InteractionContext, type AnswerUiMapTarget } from "./answer-ui-interaction-context"; import { useAnswerNodeState } from "./answer-view-state-provider"; import { answerStatePointer, type AnswerFormState } from "./answer-view-state"; type ActionState = "idle" | "pending" | "success" | "partial" | "unconfirmed" | "error"; function ActionFeedback({ state, children }: { state: ActionState; children: string }) { const shouldAnimate = useAnswerMotion(); const animate = shouldAnimate(); const Icon = state === "pending" ? LoaderCircle : state === "success" ? CheckCircle2 : state === "error" || state === "partial" ? TriangleAlert : Info; return {children}; } export function LocateButton({ label, source, ...target }: AnswerUiMapTarget & { label: string; source: string }) { const { disabled, onLocate } = useContext(InteractionContext); const runningRef = useRef(false); const [showIds, setShowIds] = useState(false); const [state, setState] = useState("idle"); const running = state === "pending"; const [feedback, setFeedback] = useState(null); return ( { if (!onLocate || disabled || runningRef.current) return; runningRef.current = true; setState("pending"); setFeedback(null); try { const result = await onLocate(target); if (result === false) throw new Error("关联要素暂不可定位,请检查地图提示。"); const output = result as { located_ids?: string[]; missing_ids?: string[] } | undefined; if (!output || !Array.isArray(output.located_ids)) { setState("unconfirmed"); setFeedback("定位请求已处理,未返回定位结果,请查看地图确认。"); } else { const located = output.located_ids.length; const missing = output.missing_ids?.length ?? 0; setState(!located ? "error" : missing ? "partial" : "success"); setFeedback(!located ? "未找到可定位的关联要素,请检查要素编号后重试。" : missing ? `已定位 ${located} 个,另有 ${missing} 个未找到` : `已定位 ${located} 个关联要素到地图`); } } catch (error) { setState("error"); setFeedback(error instanceof Error ? error.message : "地图定位失败,请重试。"); } finally { runningRef.current = false; } }} > {running ? "正在定位…" : state === "error" ? `重试${label}` : label} {target.feature_ids.length} 个关联要素 · 依据:{source} 查看关联要素}> {target.feature_ids.join("、")} {feedback || running || !onLocate ? {running ? "正在查找关联要素…" : feedback ?? "当前视图不支持地图定位,可查看关联要素编号。"} : null} ); } type FormField = { name: string; label: string; kind: "text" | "datetime" | "number"; required: boolean; }; export function FollowUpForm({ title, description, fields }: { title: string; description: string; fields: FormField[]; }) { const { disabled, onSubmit, submitMode } = useContext(InteractionContext); const prefix = useId(); const [{ values }, patch] = useAnswerNodeState("FollowUpForm", { title, description, fields }); const submittingRef = useRef(false); const [state, setState] = useState("idle"); const submitting = state === "pending"; const [invalidFields, setInvalidFields] = useState([]); const setValue = (name: string, value: string) => { patch({ values: { ...values, [name]: value } }, [`values/${answerStatePointer(name)}`]); setState("idle"); setFeedback(null); setInvalidFields((current) => current.filter((field) => field !== name)); }; const [feedback, setFeedback] = useState(null); return ( {title} {description} { event.preventDefault(); if (!onSubmit || disabled || submittingRef.current) return; const missing = fields.filter((field) => field.required && !values[field.name]?.trim()).map((field) => field.name); setInvalidFields(missing); if (missing.length) { setState("error"); setFeedback("请填写必填资料。"); return; } const entries = fields .filter((field) => values[field.name]?.trim()) .map( (field) => `${field.label}:${values[field.name].trim()}${field.kind === "datetime" ? "(北京时间)" : ""}` ); if (!entries.length) { setState("error"); setFeedback("请至少填写一项资料。"); return; } if (entries.join("\n").length > 9000) { setState("error"); setFeedback("补充资料过长,请分次发送。"); return; } submittingRef.current = true; setState("pending"); setFeedback(null); try { await onSubmit( `我补充以下资料,请按用户提供的信息处理,说明保存或调查的实际结果:\n${entries.join("\n")}` ); setState("success"); setFeedback(submitMode === "draft" ? "资料已带入对话,请确认后发送。" : "资料已发送,请查看 Agent 回复。"); } catch (error) { setState("error"); setFeedback(error instanceof Error ? error.message : "资料发送失败,请重试。"); } finally { submittingRef.current = false; } }} > {fields.map((field) => ( {field.kind === "datetime" ? ( setValue(field.name, value)} /> ) : ( {field.label} {field.required ? "(必填)" : ""} {field.kind === "text" ? ( { const value = event.currentTarget.value; setValue(field.name, value); }} /> ) : ( { const value = event.currentTarget.value; setValue(field.name, value); }} /> )} )} {invalidFields.includes(field.name) ? 请填写{field.label}。 : null} ))} {submitting ? submitMode === "draft" ? "正在处理…" : "正在发送…" : submitMode === "draft" ? "带入对话" : "发送补充资料"} {feedback || submitting || !onSubmit ? {submitting ? submitMode === "draft" ? "正在带入对话…" : "正在发送补充资料…" : feedback ?? "当前视图无法提交资料。"} : null} ); }
{target.feature_ids.length} 个关联要素 · 依据:{source}
{target.feature_ids.join("、")}
请填写{field.label}。