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

207 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}