haixunMaster/components/inspiration/topic-form-dialog.tsx

207 lines
6.7 KiB
TypeScript
Raw Normal View History

2026-06-21 12:50:31 +00:00
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Plus } from "lucide-react";
import { BrandProductPicker } from "@/components/product-profile/brand-product-picker";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { InlineAlert } from "@/components/ui/inline-alert";
import type { ActionFeedback } from "@/lib/use-action-feedback";
import type { TopicGoal } from "@/lib/types/topic-goal";
import { parseFetchJson } from "@/lib/utils";
interface TopicFormDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
defaultGoal: TopicGoal;
onCreated?: () => void;
}
export function TopicFormDialog({
open,
onOpenChange,
defaultGoal,
onCreated,
}: TopicFormDialogProps) {
const router = useRouter();
const [label, setLabel] = useState("");
const [query, setQuery] = useState("");
const [brief, setBrief] = useState("");
const topicGoal = defaultGoal;
const [brandProfileId, setBrandProfileId] = useState<string | null>(null);
const [productProfileId, setProductProfileId] = useState<string | null>(null);
const [productContext, setProductContext] = useState("");
const [submitting, setSubmitting] = useState(false);
const [formFeedback, setFormFeedback] = useState<ActionFeedback | null>(null);
useEffect(() => {
if (!open) return;
setLabel("");
setQuery("");
setBrief("");
setBrandProfileId(null);
setProductProfileId(null);
setProductContext("");
setFormFeedback(null);
}, [open, defaultGoal]);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!label.trim() || !query.trim()) {
setFormFeedback({ type: "warning", message: "請填寫主題名稱與種子關鍵字。" });
return;
}
if (topicGoal === "placement" && !brandProfileId) {
setFormFeedback({ type: "warning", title: "請選擇品牌", message: "置入模式需先選擇品牌。" });
return;
}
setFormFeedback(null);
setSubmitting(true);
const res = await fetch("/api/topics", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
label: label.trim(),
query: query.trim(),
brief: brief.trim() || undefined,
topicGoal,
brandProfileId: topicGoal === "placement" ? brandProfileId : undefined,
productProfileId: topicGoal === "placement" ? productProfileId : undefined,
productContext: topicGoal === "placement" ? productContext : undefined,
}),
});
let data: { error?: string; topic?: { id: string } } = {};
try {
data = await parseFetchJson(res);
} catch (err) {
setSubmitting(false);
setFormFeedback({
type: "error",
title: "建立失敗",
message: err instanceof Error ? err.message : "伺服器回應異常",
});
return;
}
if (!res.ok) {
setSubmitting(false);
setFormFeedback({
type: "error",
title: "建立失敗",
message: data.error ?? "無法建立主題",
});
return;
}
const topicId = data.topic?.id;
if (!topicId) {
setSubmitting(false);
setFormFeedback({ type: "error", title: "建立失敗", message: "未取得主題編號" });
return;
}
const analyzeRes = await fetch("/api/analyze-topic", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ topicId, brief: brief.trim() || undefined }),
});
setSubmitting(false);
onCreated?.();
onOpenChange(false);
if (analyzeRes.ok) window.dispatchEvent(new Event("haixun:jobs-updated"));
router.push(`/scans/${topicId}?mode=${topicGoal}`);
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-[min(100vw-2rem,36rem)]">
<DialogHeader>
<DialogTitle>{topicGoal === "placement" ? "新增找 TA 任務" : "新增拷貝忍者任務"}</DialogTitle>
<DialogDescription> AI </DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
{formFeedback && (
<InlineAlert
type={formFeedback.type}
title={formFeedback.title}
message={formFeedback.message}
onDismiss={() => setFormFeedback(null)}
/>
)}
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-1.5">
<Label></Label>
<Input
value={label}
onChange={(e) => setLabel(e.target.value)}
placeholder="居家狗狗洗澡"
autoFocus
/>
</div>
<div className="space-y-1.5">
<Label></Label>
<Input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="狗狗洗澡" />
</div>
</div>
<div className="space-y-1.5">
<Label>Brief</Label>
<Textarea
value={brief}
onChange={(e) => setBrief(e.target.value)}
rows={2}
placeholder={
topicGoal === "placement"
? "想服務誰、什麼情境"
: "你是誰、想服務誰、內容方向"
}
/>
</div>
{topicGoal === "placement" && (
<div className="rounded-lg border border-border bg-muted/30 p-3">
<BrandProductPicker
brandId={brandProfileId}
productId={productProfileId}
onChange={({ brandId, productId, context }) => {
setBrandProfileId(brandId);
setProductProfileId(productId);
if (context) setProductContext(context);
}}
/>
</div>
)}
<DialogFooter>
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
</Button>
<Button type="submit" disabled={submitting}>
{submitting ? "建立並分析中…" : (
<>
<Plus className="h-4 w-4" />
</>
)}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}