"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import ReactMarkdown from "react-markdown"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Progress } from "@/components/ui/progress"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogTrigger, } from "@/components/ui/dialog"; import { Wand2, Save, FileSpreadsheet, FileDown, Loader2, Plus, Search, CloudUpload, } from "lucide-react"; import { toast } from "sonner"; import { apiBase, customersApi, projectsApi, templatesApi, pushProjectToCloud, downloadFile, downloadFileAsBlob, type CustomerRead, type QuoteGenerateResponse, type TemplateInfo, } from "@/lib/api/client"; export default function WorkspacePage() { const [customers, setCustomers] = useState([]); const [customerId, setCustomerId] = useState(""); const [rawText, setRawText] = useState(""); const [solutionMd, setSolutionMd] = useState(""); const [projectId, setProjectId] = useState(null); const [lastQuote, setLastQuote] = useState(null); const [analyzing, setAnalyzing] = useState(false); const [analyzeProgress, setAnalyzeProgress] = useState(0); const [saving, setSaving] = useState(false); const [generatingQuote, setGeneratingQuote] = useState(false); const [addCustomerOpen, setAddCustomerOpen] = useState(false); const [newCustomerName, setNewCustomerName] = useState(""); const [newCustomerContact, setNewCustomerContact] = useState(""); const [newCustomerTags, setNewCustomerTags] = useState(""); const [addingCustomer, setAddingCustomer] = useState(false); const [quoteTemplates, setQuoteTemplates] = useState([]); const [selectedQuoteTemplate, setSelectedQuoteTemplate] = useState(""); const [customerSearch, setCustomerSearch] = useState(""); const [pushToCloudLoading, setPushToCloudLoading] = useState(false); const didInitRef = useRef(false); const loadCustomers = useCallback( async (search?: string) => { try { const list = await customersApi.list(search?.trim() ? { q: search.trim() } : undefined); setCustomers(list); if (list.length > 0 && !customerId) setCustomerId(String(list[0].id)); if (!search?.trim()) { try { sessionStorage.setItem("opc_customers", JSON.stringify({ t: Date.now(), v: list })); } catch {} } } catch { toast.error("加载客户列表失败"); } }, [customerId], ); useEffect(() => { if (!customerSearch.trim()) { if (!didInitRef.current) { didInitRef.current = true; try { const raw = sessionStorage.getItem("opc_customers"); if (raw) { const parsed = JSON.parse(raw); if (parsed?.v && Date.now() - (parsed.t || 0) < 60_000) setCustomers(parsed.v); } } catch {} void loadCustomers(""); return; } } void loadCustomers(customerSearch); }, [loadCustomers, customerSearch]); const loadQuoteTemplates = useCallback(async () => { try { const list = await templatesApi.list(); setQuoteTemplates(list.filter((t) => t.type === "excel")); try { sessionStorage.setItem("opc_templates", JSON.stringify({ t: Date.now(), v: list })); } catch {} } catch { // ignore } }, []); useEffect(() => { try { const raw = sessionStorage.getItem("opc_templates"); if (raw) { const parsed = JSON.parse(raw); if (parsed?.v && Date.now() - (parsed.t || 0) < 60_000) { setQuoteTemplates((parsed.v as TemplateInfo[]).filter((t) => t.type === "excel")); } } } catch {} void loadQuoteTemplates(); }, [loadQuoteTemplates]); const handleAddCustomer = async (e: React.FormEvent) => { e.preventDefault(); const name = newCustomerName.trim(); if (!name) { toast.error("请填写客户名称"); return; } setAddingCustomer(true); try { const created = await customersApi.create({ name, contact_info: newCustomerContact.trim() || null, tags: newCustomerTags.trim() || null, }); toast.success("客户已添加"); setAddCustomerOpen(false); setNewCustomerName(""); setNewCustomerContact(""); setNewCustomerTags(""); await loadCustomers(customerSearch); setCustomerId(String(created.id)); } catch (err) { const msg = err instanceof Error ? err.message : "添加失败"; toast.error(msg); } finally { setAddingCustomer(false); } }; const handleAnalyze = async () => { if (!customerId || !rawText.trim()) { toast.error("请选择客户并输入原始需求"); return; } setAnalyzing(true); setAnalyzeProgress(8); setSolutionMd(""); try { const base = apiBase(); const res = await fetch(`${base}/projects/analyze_stream`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ customer_id: Number(customerId), raw_text: rawText.trim(), }), }); if (!res.ok || !res.body) { const text = await res.text(); throw new Error(text || "AI 解析失败"); } let buf = ""; const reader = res.body.getReader(); const decoder = new TextDecoder("utf-8"); const progressTimer = window.setInterval(() => { setAnalyzeProgress((p) => (p < 90 ? p + Math.max(1, Math.round((90 - p) / 10)) : p)); }, 400); try { while (true) { const { value, done } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); const parts = buf.split("\n\n"); buf = parts.pop() || ""; for (const chunk of parts) { const line = chunk.split("\n").find((l) => l.startsWith("data: ")); if (!line) continue; const payload = line.slice(6); let evt: any; try { evt = JSON.parse(payload); } catch { continue; } if (evt.type === "delta" && typeof evt.content === "string") { setSolutionMd((prev) => prev + evt.content); } else if (evt.type === "done" && typeof evt.project_id === "number") { setProjectId(evt.project_id); setLastQuote(null); setAnalyzeProgress(100); } else if (evt.type === "error") { throw new Error(evt.message || "AI 解析失败"); } } } } finally { window.clearInterval(progressTimer); } toast.success("方案已生成(流式),可在右侧编辑"); } catch (e: unknown) { const msg = e instanceof Error ? e.message : "AI 解析失败"; toast.error(msg); } finally { setAnalyzing(false); } }; const handlePushToCloud = async (platform: "feishu" | "yuque" | "tencent") => { if (projectId == null) return; const md = solutionMd?.trim() || ""; if (!md) { toast.error("请先在编辑器中填写方案内容后再推送"); return; } setPushToCloudLoading(true); try { const res = await pushProjectToCloud(projectId, { platform, body_md: md }); toast.success("已推送到云文档", { action: res.url ? { label: "打开链接", onClick: () => window.open(res.url, "_blank"), } : undefined, }); } catch (e) { toast.error(e instanceof Error ? e.message : "推送失败"); } finally { setPushToCloudLoading(false); } }; const handleSaveToArchive = async () => { if (projectId == null) { toast.error("请先进行 AI 解析"); return; } setSaving(true); try { await projectsApi.update(projectId, { ai_solution_md: solutionMd }); toast.success("已保存到项目档案"); } catch (e: unknown) { const msg = e instanceof Error ? e.message : "保存失败"; toast.error(msg); } finally { setSaving(false); } }; const handleDraftQuote = async () => { if (projectId == null) { toast.error("请先进行 AI 解析并保存"); return; } setGeneratingQuote(true); try { const res = await projectsApi.generateQuote(projectId, selectedQuoteTemplate || undefined); setLastQuote(res); toast.success("报价单已生成"); downloadFile(res.excel_path, `quote_project_${projectId}.xlsx`); } catch (e: unknown) { const msg = e instanceof Error ? e.message : "生成报价失败"; toast.error(msg); } finally { setGeneratingQuote(false); } }; const handleExportPdf = async () => { if (lastQuote?.pdf_path) { downloadFile(lastQuote.pdf_path, `quote_project_${projectId}.pdf`); toast.success("PDF 已下载"); return; } if (projectId == null) { toast.error("请先进行 AI 解析"); return; } setGeneratingQuote(true); try { const res = await projectsApi.generateQuote(projectId, selectedQuoteTemplate || undefined); setLastQuote(res); await downloadFileAsBlob(res.pdf_path, `quote_project_${projectId}.pdf`); toast.success("PDF 已下载"); } catch (e: unknown) { const msg = e instanceof Error ? e.message : "生成 PDF 失败"; toast.error(msg); } finally { setGeneratingQuote(false); } }; return (
原始需求
setCustomerSearch(e.target.value)} className="pl-8 h-9" />
新建客户
setNewCustomerName(e.target.value)} placeholder="必填" />
setNewCustomerContact(e.target.value)} placeholder="选填" />
setNewCustomerTags(e.target.value)} placeholder="如:重点客户, 已签约" />