import { useCallback, useEffect, useMemo, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { api, ApiError } from '../api/client' import { BrandProductPicker } from '../components/BrandProductPicker' import { rememberTopicId } from '../lib/brandContext' import { brandHasCatalogProducts } from '../lib/productCatalog' import type { ExpandKnowledgeGraphData } from '../lib/knowledgeGraph' import { requestJobMonitorRefresh } from '../lib/jobMonitorRefresh' import { formatActiveJobConflictError } from '../lib/jobStatus' import { hasResearchMap, topicResearchMapPath, topicTitle } from '../lib/placementTopics' import type { BrandData, ListBrandsData } from '../types/brand' import type { PlacementTopicData } from '../types/placementTopic' import { AcLink, Button, Card, ErrorText, Field, Input, PageTitle, SuccessText, Textarea } from '../components/ui' export function PlacementTopicSettingsPage() { const navigate = useNavigate() const { id = '' } = useParams() const [catalogBrands, setCatalogBrands] = useState([]) const [topic, setTopic] = useState(null) const [brandId, setBrandId] = useState('') const [productId, setProductId] = useState(null) const [topicName, setTopicName] = useState('') const [seedQuery, setSeedQuery] = useState('') const [brief, setBrief] = useState('') const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [generating, setGenerating] = useState(false) const [error, setError] = useState('') const [message, setMessage] = useState('') const selectedBrand = useMemo( () => catalogBrands.find((item) => item.id === brandId) ?? null, [brandId, catalogBrands], ) const load = useCallback(async () => { if (!id) return setLoading(true) setError('') try { const [brandsRes, topicRes] = await Promise.all([ api.get('/api/v1/brands/', { auth: true }), api.get(`/api/v1/placement/topics/${encodeURIComponent(id)}`, { auth: true }), ]) setCatalogBrands(brandsRes.list ?? []) setTopic(topicRes) setBrandId(topicRes.brand_id) setProductId(topicRes.product_id?.trim() || null) setTopicName(topicRes.topic_name ?? '') setSeedQuery(topicRes.seed_query ?? '') setBrief(topicRes.brief ?? '') rememberTopicId(id) } catch (e) { setError(e instanceof ApiError ? e.message : '載入失敗') } finally { setLoading(false) } }, [id]) useEffect(() => { void load() }, [load]) const validateForm = () => { if (!topicName.trim()) { setError('請填寫主題名稱') return false } if (!seedQuery.trim() || !brief.trim()) { setError('請填寫種子關鍵字與主題目標') return false } if (!brandHasCatalogProducts(selectedBrand)) { setError('請先到品牌庫為此品牌新增至少一個產品') return false } return true } const saveTopic = async () => { if (!id) return null const updated = await api.patch( `/api/v1/placement/topics/${encodeURIComponent(id)}`, { brand_id: brandId, topic_name: topicName.trim(), seed_query: seedQuery.trim(), brief: brief.trim(), product_id: productId ?? '', }, { auth: true }, ) setTopic(updated) rememberTopicId(id) return updated } const save = async () => { if (!id) return if (!validateForm()) return setSaving(true) setError('') setMessage('') try { await saveTopic() setMessage('已儲存主題設定') } catch (e) { setError(e instanceof ApiError ? e.message : '儲存失敗') } finally { setSaving(false) } } const generateMap = async () => { if (!id) return if (!validateForm()) return setGenerating(true) setError('') setMessage('') try { await saveTopic() const job = await api.post( `/api/v1/placement/topics/${encodeURIComponent(id)}/knowledge-graph/expand`, { seed_query: seedQuery.trim(), regenerate_map: true }, { auth: true }, ) requestJobMonitorRefresh() navigate(topicResearchMapPath(id), { state: { expandJobId: job.job_id } }) } catch (e) { const raw = e instanceof ApiError ? e.message : '產生研究地圖失敗' setError(formatActiveJobConflictError(raw)) } finally { setGenerating(false) } } if (!id) { return (

未指定主題。

) } const title = topic ? topicTitle(topic) : '主題設定' return (
← 找 TA 主題 {loading ? (

載入中…

) : ( <> setTopicName(e.target.value)} placeholder="例:敏感肌換季保養" /> setSeedQuery(e.target.value)} placeholder="例:敏感肌、換季泛紅" />