"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { flushSync } from "react-dom"; import { Loader2, ScanText, MessageCircle, Plus, Send, Sparkles, UserRound, WandSparkles, X, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { EmptyState } from "@/components/layout/empty-state"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { PageHeader } from "@/components/layout/page-header"; import { useJobs } from "@/components/layout/jobs-provider"; import { JobProgressPanel } from "@/components/job-progress-panel"; import { InlineAlert } from "@/components/ui/inline-alert"; import { Textarea } from "@/components/ui/textarea"; import { useActionFeedback } from "@/lib/use-action-feedback"; import { parseFetchJson } from "@/lib/utils"; import { createEmptyStyle8DProfile, parseStyle8DProfile, STYLE_8D_KEYS, STYLE_8D_LABELS, type StoredStyle8DProfile, type Style8DKey, } from "@/lib/types/style-profile"; interface Account { id: string; username?: string | null; displayName?: string | null; persona?: string | null; styleProfile?: string | null; styleBenchmark?: string | null; brief?: string | null; productBrief?: string | null; targetAudience?: string | null; goals?: string | null; } interface AssistantMessage { role: "user" | "assistant"; content: string; } type Style8DResult = StoredStyle8DProfile; export default function AccountsPage() { const [account, setAccount] = useState(null); const [draft, setDraft] = useState(null); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(null); const [newName, setNewName] = useState(""); const [assistantOpen, setAssistantOpen] = useState(true); const [assistantInput, setAssistantInput] = useState(""); const [assistantBusy, setAssistantBusy] = useState(false); const [benchmarkUsername, setBenchmarkUsername] = useState(""); const [styleResult, setStyleResult] = useState(null); const assistantInputRef = useRef(null); const { activeJobs } = useJobs(); const { feedback, clearFeedback, showError, showSuccess } = useActionFeedback(); const [assistantMessages, setAssistantMessages] = useState([ { role: "assistant", content: "跟我說你想經營的方向,我會把這頁策略欄位先幫你補好。", }, ]); const styleJob = activeJobs.find( (job) => job.type === "style-8d" && (!draft?.id || job.accountId === draft.id) ); const visibleStyle = styleResult ?? createEmptyStyle8DProfile(benchmarkUsername.replace(/^@/, "").trim()); const load = useCallback(async () => { setLoading(true); const res = await fetch("/api/accounts"); const data = await parseFetchJson<{ accounts?: Account[]; activeAccountId?: string }>(res); const rows = (data.accounts ?? []) as Account[]; const active = rows.find((row) => row.id === data.activeAccountId) ?? rows[0] ?? null; setAccount(active); setDraft(active); setStyleResult(parseStyle8DProfile(active?.styleProfile)); setBenchmarkUsername(active?.styleBenchmark ?? ""); setLoading(false); }, []); useEffect(() => { load(); }, [load]); useEffect(() => { function onJobCompleted(event: Event) { const { job } = (event as CustomEvent).detail as { job: { type: string; accountId?: string | null; status: string }; }; if (job.type === "style-8d" && job.accountId === account?.id && job.status === "completed") { void load(); } } window.addEventListener("job-completed", onJobCompleted); return () => window.removeEventListener("job-completed", onJobCompleted); }, [account?.id, load]); async function createAccount() { if (!newName.trim()) return; clearFeedback(); setBusy("create"); const res = await fetch("/api/accounts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ displayName: newName.trim() }), }); let data: { error?: string } = {}; try { data = await parseFetchJson(res); } catch (err) { setBusy(null); showError(err instanceof Error ? err.message : "伺服器回應異常", "建立失敗"); return; } setBusy(null); if (!res.ok) { showError(data.error ?? "無法建立帳號", "建立失敗"); return; } setNewName(""); showSuccess("帳號策略已建立"); load(); } async function save() { if (!draft) return; clearFeedback(); setBusy("save"); const res = await fetch(`/api/accounts/${draft.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ displayName: draft.displayName, username: draft.username, persona: draft.persona, styleProfile: draft.styleProfile, styleBenchmark: draft.styleBenchmark, brief: draft.brief, productBrief: draft.productBrief, targetAudience: draft.targetAudience, goals: draft.goals, }), }); let data: { error?: string } = {}; try { data = await parseFetchJson(res); } catch (err) { setBusy(null); showError(err instanceof Error ? err.message : "伺服器回應異常", "儲存失敗"); return; } setBusy(null); if (!res.ok) { showError(data.error ?? "無法儲存策略", "儲存失敗"); return; } showSuccess("帳號策略已儲存"); load(); } function updateDraft(patch: Partial) { setDraft((prev) => (prev ? { ...prev, ...patch } : prev)); } async function analyzeBenchmark() { if (!draft || !benchmarkUsername.trim()) return; clearFeedback(); setBusy("8d"); try { const res = await fetch(`/api/accounts/${draft.id}/style-analysis`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ benchmarkUsername }), }); const data = await parseFetchJson<{ error?: string; jobId?: string; message?: string }>(res); if (!res.ok) { showError(data.error ?? "無法完成 8D 分析", "8D 分析失敗"); return; } showSuccess(data.message ?? "8D 分析已在背景執行,可自由切換頁面"); window.dispatchEvent(new Event("haixun:jobs-updated")); } catch (error) { showError(error instanceof Error ? error.message : "8D 分析失敗", "8D 分析失敗"); } finally { setBusy(null); } } function updateStyleDimension(key: Style8DKey, summary: string) { const base = styleResult ?? createEmptyStyle8DProfile(benchmarkUsername.replace(/^@/, "").trim()); const next: Style8DResult = { ...base, analysis: { ...base.analysis, [key]: { ...base.analysis[key], summary }, }, }; setStyleResult(next); updateDraft({ styleProfile: JSON.stringify(next) }); } const clearAssistantInput = useCallback(() => { setAssistantInput(""); if (assistantInputRef.current) { assistantInputRef.current.value = ""; } }, []); async function askAssistant(preset?: string) { if (!draft) return; const instruction = (preset ?? assistantInput).trim(); if (!instruction || assistantBusy) return; flushSync(() => { if (!preset) clearAssistantInput(); setAssistantMessages((prev) => [...prev, { role: "user", content: instruction }]); }); setAssistantBusy(true); const res = await fetch("/api/accounts/strategy-assistant", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ instruction, current: { displayName: draft.displayName, username: draft.username, brief: draft.brief, persona: draft.persona, targetAudience: draft.targetAudience, productBrief: draft.productBrief, goals: draft.goals, style8D: Object.fromEntries( STYLE_8D_KEYS.map((key) => [key, visibleStyle.analysis[key]?.summary ?? null]) ), }, }), }); let data: { error?: string; fields?: Partial & { style8D?: Partial> }; message?: string; } = {}; try { data = await parseFetchJson(res); } catch (err) { setAssistantBusy(false); const message = err instanceof Error ? err.message : "伺服器回應異常"; setAssistantMessages((prev) => [...prev, { role: "assistant", content: message }]); return; } setAssistantBusy(false); if (!res.ok) { const message = data.error ?? "小幫手暫時無法產生內容"; setAssistantMessages((prev) => [...prev, { role: "assistant", content: message }]); return; } const fields = data.fields ?? {}; const { style8D, ...accountFields } = fields; let nextStyleResult = styleResult; if (style8D && Object.values(style8D).some((value) => value?.trim())) { const base = styleResult ?? createEmptyStyle8DProfile(benchmarkUsername.replace(/^@/, "").trim()); nextStyleResult = { ...base, analysis: { ...base.analysis }, }; for (const key of STYLE_8D_KEYS) { const summary = style8D[key]?.trim(); if (summary) { nextStyleResult.analysis[key] = { ...base.analysis[key], summary }; } } setStyleResult(nextStyleResult); } setDraft((prev) => prev ? { ...prev, ...Object.fromEntries( Object.entries(accountFields).filter(([, value]) => value !== null && value !== undefined) ), ...(nextStyleResult ? { styleProfile: JSON.stringify(nextStyleResult) } : {}), } : prev ); setAssistantMessages((prev) => [ ...prev, { role: "assistant", content: data.message ?? "我已經依照你的方向補上這頁欄位,你可以再微調後儲存。", }, ]); } return (
{busy === "save" && } 儲存策略 ) } /> {feedback && ( )} {loading ? (
) : !draft ? ( setNewName(e.target.value)} placeholder="例如:個人品牌、產品號" />
} /> ) : (
{account?.displayName ?? account?.username ?? "未命名帳號"}
目前帳號
updateDraft({ displayName: e.target.value })} placeholder="給自己看的名稱" />
updateDraft({ username: e.target.value })} placeholder="@username" />