import type { Brand, InspireChatMessage, InspireElement, Persona, } from "../domain/types"; import { newId } from "./id"; import { mockDelay } from "./mockAi"; import { isPersonaReady } from "./personaPrompt"; import { nowUnixNano } from "./time"; export type ResolvedInspireContext = { roles: string[]; snippets: string[]; trends: string[]; persona?: Persona | null; brand?: Brand | null; labels: string[]; }; export function resolveInspireContext( elements: InspireElement[], opts?: { personas?: Persona[]; brands?: Brand[]; }, ): ResolvedInspireContext { const personas = opts?.personas || []; const brands = opts?.brands || []; const roles: string[] = []; const snippets: string[] = []; const trends: string[] = []; const labels: string[] = []; let persona: Persona | null = null; let brand: Brand | null = null; for (const el of elements) { labels.push(el.title); if (el.kind === "role") { roles.push(el.body || el.title); } else if (el.kind === "snippet") { snippets.push(el.body || el.title); } else if (el.kind === "trend") { trends.push(el.body || el.title); } else if (el.kind === "persona" && el.ref_id) { const p = personas.find((x) => x.id === el.ref_id); if (p && isPersonaReady(p)) persona = p; else if (el.body) snippets.push(el.body); } else if (el.kind === "brand" && el.ref_id) { const b = brands.find((x) => x.id === el.ref_id); if (b) brand = b; else if (el.body) snippets.push(el.body); } else if (el.body) { snippets.push(el.body); } } return { roles, snippets, trends, persona, brand, labels }; } function toneLead(ctx: ResolvedInspireContext): string { if (ctx.roles.some((r) => /朋友|私訊/.test(r))) return "懂你…"; if (ctx.roles.some((r) => /鉤子|短句/.test(r))) return "先講結論:"; if (ctx.roles.some((r) => /真實|反業配/.test(r))) return "講實話——"; if (ctx.persona?.style?.draft?.hooks) { return ctx.persona.style.draft.hooks.slice(0, 24); } if (ctx.persona?.name) return `以「${ctx.persona.name}」的口吻:`; return ""; } function buildDraftBody(userText: string, ctx: ResolvedInspireContext): string { const topic = userText.trim() || ctx.trends[0] || ctx.brand?.display_name || "日常小事"; const lead = toneLead(ctx); const lines: string[] = []; if (lead) lines.push(lead); if (ctx.trends.length) { lines.push(`最近大家在聊 ${ctx.trends[0]!.replace(/^主題:/, "").slice(0, 40)},`); } const personaBit = ctx.persona?.brief || ctx.persona?.style?.draft?.tone || ctx.persona?.voice || ""; const brandBit = ctx.brand ? `(想到 ${ctx.brand.display_name}${ctx.brand.brief ? `:${ctx.brand.brief.slice(0, 40)}` : ""})` : ""; if (/改短|短一點|精簡/.test(userText)) { lines.push(`${topic.slice(0, 60)}——有人也卡在這嗎?`); } else if (/更口語|隨便|碎念/.test(userText)) { lines.push(`欸所以 ${topic.slice(0, 50)} 這件事,我真的想問你們怎麼處理的。`); } else { lines.push( `${topic.slice(0, 80)}${personaBit ? `。${personaBit.slice(0, 48)}` : ""}${brandBit}`, ); lines.push("我自己目前比較在意「實際用起來」而不是包裝怎麼寫。"); } const wantAsk = ctx.snippets.some((s) => /問句|留言/.test(s)) || ctx.roles.some((r) => /鉤子|問句/.test(r)); if (wantAsk) { lines.push("你們最近有類似經驗嗎?"); } if (ctx.snippets.some((s) => /誇大|療效|禁止/.test(s))) { // 語氣收斂:不另加誇大句 } // 短貼感 let body = lines.filter(Boolean).join("\n"); if (ctx.snippets.some((s) => /短貼|180|280/.test(s)) && body.length > 200) { body = body.slice(0, 180) + "…"; } return body.trim(); } function assistantNote(ctx: ResolvedInspireContext, mode: "chat" | "generate"): string { if (mode === "chat") { if (ctx.labels.length) { return `好。這輪會帶上:${ctx.labels.slice(0, 4).join("、")}${ctx.labels.length > 4 ? "…" : ""}。你可以直接說想寫什麼,或按「產文」。`; } return "可以。想寫什麼主題?也可以先從右側套用角色/人設/片段,或點下方夯什麼。"; } if (ctx.labels.length) { return `已依 ${ctx.labels.slice(0, 3).join("、")} 寫一則草稿(可再改):`; } return "這是一則草稿(尚未套用特別元素,可在右側 pin 後再產):"; } export async function mockInspireChat(opts: { userMessage: string; mode: "chat" | "generate"; elements: InspireElement[]; personas?: Persona[]; brands?: Brand[]; }): Promise { await mockDelay(opts.mode === "generate" ? 650 : 380); const now = nowUnixNano(); const ctx = resolveInspireContext(opts.elements, { personas: opts.personas, brands: opts.brands, }); const userText = opts.userMessage.trim(); const out: InspireChatMessage[] = []; if (userText) { out.push({ id: newId("im"), role: "user", text: userText, created_at: now, }); } if (opts.mode === "generate") { const body = buildDraftBody(userText || "寫一則可發的 Threads", ctx); out.push({ id: newId("im"), role: "assistant", text: assistantNote(ctx, "generate"), draft: { title: (ctx.trends[0] || userText || "靈感草稿").slice(0, 32), body, }, created_at: now + 1, }); } else { out.push({ id: newId("im"), role: "assistant", text: assistantNote(ctx, "chat"), created_at: now + 1, }); } return out; } export function emptyInspireSession(): import("../domain/types").InspireSession { const now = nowUnixNano(); return { id: newId("isess"), messages: [ { id: newId("im"), role: "assistant", text: "說你想寫什麼。右側可套用角色、人設、品牌、片段;下方是最近 Threads 夯什麼。", created_at: now, }, ], pinned_element_ids: [], updated_at: now, }; }