import { sanitizePromptText } from "@/lib/utils"; export interface PersonaFields { identity: string; tone: string; audience: string; hooks: string; examples: string; avoid: string; styleStrategy: string; } const SECTION_KEYS: Array<{ key: keyof PersonaFields; label: string }> = [ { key: "identity", label: "【我是誰】" }, { key: "tone", label: "【語氣】" }, { key: "audience", label: "【對誰說】" }, { key: "hooks", label: "【開場習慣】" }, { key: "examples", label: "【代表句範例】" }, { key: "avoid", label: "【避免】" }, { key: "styleStrategy", label: "【8D 風格策略】" }, ]; const EMPTY_PERSONA: PersonaFields = { identity: "", tone: "", audience: "", hooks: "", examples: "", avoid: "", styleStrategy: "", }; export function emptyPersonaFields(): PersonaFields { return { ...EMPTY_PERSONA }; } export function hasPersonaConfigured(raw?: string | null): boolean { const fields = parsePersona(raw); return Object.values(fields).some((value) => value.trim().length > 0); } export function parsePersona(raw?: string | null): PersonaFields { if (!raw?.trim()) return emptyPersonaFields(); const text = raw.trim(); if (!text.includes("【")) { return { ...EMPTY_PERSONA, identity: text }; } const fields = emptyPersonaFields(); const pattern = /【[^】]+】/g; const markers = [...text.matchAll(pattern)]; if (markers.length === 0) { return { ...EMPTY_PERSONA, identity: text }; } for (let i = 0; i < markers.length; i++) { const marker = markers[i]; const label = marker[0]; const start = (marker.index ?? 0) + label.length; const end = i + 1 < markers.length ? (markers[i + 1].index ?? text.length) : text.length; const content = text.slice(start, end).trim(); const section = SECTION_KEYS.find((item) => item.label === label); if (section) { fields[section.key] = content; } } return fields; } export function serializePersona(fields: PersonaFields): string { return SECTION_KEYS.map(({ key, label }) => { const value = fields[key].trim(); return value ? `${label}\n${value}` : ""; }) .filter(Boolean) .join("\n\n"); } export function buildPersonaPromptBlock(raw?: string | null): string { const fields = parsePersona(raw); if (!hasPersonaConfigured(raw)) { return `## 創作者人設 尚未設定人設。請用專業、有觀點、語氣自然的台灣 Threads 創作者口吻撰寫,像真人在分享見解,避免 AI 模板感。`; } const sections: string[] = []; if (fields.identity) sections.push(`### 我是誰\n${sanitizePromptText(fields.identity)}`); if (fields.tone) sections.push(`### 語氣特質\n${sanitizePromptText(fields.tone)}`); if (fields.audience) sections.push(`### 對誰說\n${sanitizePromptText(fields.audience)}`); if (fields.hooks) sections.push(`### 開場習慣\n${sanitizePromptText(fields.hooks)}`); if (fields.examples) { sections.push( `### 代表句範例(最高優先)\n${sanitizePromptText(fields.examples)}\n` + `寫作時語感、句長、用詞、停頓節奏要向以上句子靠攏,讀起來像同一個人在說話。` ); } if (fields.avoid) sections.push(`### 絕對避免\n${sanitizePromptText(fields.avoid)}`); if (fields.styleStrategy) { sections.push(`### 帳號 8D 風格策略(產文與回覆都必須執行)\n${sanitizePromptText(fields.styleStrategy)}`); } return `## 創作者人設(每篇都必須貫徹) ${sections.join("\n\n")} ## 人設執行規則 1. 人設優先於你的預設寫法;寧可少寫資訊,也不要寫成通用 AI 文 2. hook 要符合「開場習慣」,正文語氣要符合「語氣特質」 3. 有「代表句範例」時,把它當成聲音樣本,不是素材可抄的內容 4. 「絕對避免」裡的用語、語氣、套路一篇都不能出現 5. 寫完自檢:關掉作者名,讀起來還像這個人嗎?不像就改到像為止`; }