export type SearchIntent = | "痛點" | "知識" | "經驗" | "對比" | "工具" | "語錄" | "需求" | "求助"; export const SEARCH_INTENTS: SearchIntent[] = [ "痛點", "知識", "經驗", "對比", "工具", "語錄", "需求", "求助", ]; export const SEARCH_TAG_TYPES = ["短詞", "情境", "帳號", "語錄"] as const; /** 短詞=2-4字高命中 | 情境=5-8字 | 帳號=相似創作者 | 語錄=故事型小語 */ export type SearchTagType = "短詞" | "情境" | "帳號" | "語錄"; export interface SuggestedTag { tag: string; reason: string; searchIntent?: SearchIntent; searchType?: SearchTagType; } export type AccountConfidence = "high" | "medium" | "low"; export interface SimilarAccount { username: string; reason: string; /** web=網路搜尋找到的真實連結;threads=Threads 關鍵字搜尋熱門作者 */ source?: "web" | "threads" | "scan"; profileUrl?: string; postUrl?: string; confidence?: AccountConfidence; lastActiveAt?: string; } export interface ResearchMap { audienceSummary: string; contentGoal: string; questions: string[]; pillars: string[]; suggestedTags: SuggestedTag[]; similarAccounts?: SimilarAccount[]; exclusions: string[]; } export type QualityTier = "GOOD" | "OK" | "EXCLUDE"; export interface QualityAssessment { id: string; relevanceScore: number; placementScore?: number; qualityTier: QualityTier; qualityReason: string; placementReason?: string; } export interface MatrixRow { sortOrder: number; searchTag: string; angle: string; hook: string; text: string; referenceNotes: string; sourcePermalinks: string[]; rationale: string; } export function parseResearchMap(raw: string | null | undefined): ResearchMap | null { if (!raw) return null; try { return JSON.parse(raw) as ResearchMap; } catch { return null; } } export function parseSelectedTags(raw: string | null | undefined): string[] { if (!raw) return []; try { const parsed = JSON.parse(raw) as unknown; return Array.isArray(parsed) ? parsed.filter((t): t is string => typeof t === "string") : []; } catch { return []; } } export function isAccountTag(tag: string): boolean { return tag.startsWith("@"); } export function normalizeUsername(tag: string): string { return tag.replace(/^@/, "").trim(); } export function threadsProfileUrl(username: string): string | null { const clean = normalizeUsername(username); if (!clean) return null; return `https://www.threads.com/@${encodeURIComponent(clean)}`; } const THREADS_POST_URL_RE = /(?:https?:)?\/\/(?:www\.)?threads\.(?:com|net)\/@([a-zA-Z0-9._]+)\/post\/([^/?#\s]+)/i; /** 從任意 Threads 網址正規化成可開啟的貼文連結 */ export function normalizeThreadsPostUrl(url: string): string | null { const trimmed = url.trim(); if (!trimmed) return null; const match = trimmed.match(THREADS_POST_URL_RE); if (!match) return null; return `https://www.threads.com/@${match[1]}/post/${match[2]}`; } export const SIMILAR_ACCOUNT_SOURCE_LABELS: Record< NonNullable, string > = { web: "網路搜尋", threads: "Threads 搜尋", scan: "海巡發現", }; export const SIMILAR_ACCOUNT_CONFIDENCE_LABELS: Record< NonNullable, string > = { high: "高", medium: "中", low: "低", };