import { useState, useEffect, useRef } from "react"; // ════════════════════════════════════════════════════════════════ // POD Push Traffic Evaluator v3.1 // DETERMINISTIC SCORING: AI extracts → JS scores → AI reasons // ════════════════════════════════════════════════════════════════ const API_KEY = "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"; const MODEL = "claude-sonnet-4-20250514"; // ── Storage ── const SK = { winners: "p4w", losers: "p4l", patterns: "p4p", tests: "p4t" }; async function sGet(k) { try { const r = await window.storage.get(k); return r ? JSON.parse(r.value) : null; } catch { return null; } } async function sSet(k, v) { try { await window.storage.set(k, JSON.stringify(v)); } catch(e) { console.error(e); } } // ── AI helpers ── async function callAI(messages, maxTokens = 4096) { const res = await fetch("https://api.anthropic.com/v1/messages", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": API_KEY, "anthropic-version": "2023-06-01", "anthropic-dangerous-direct-browser-access": "true" }, body: JSON.stringify({ model: MODEL, max_tokens: maxTokens, messages }), }); const data = await res.json(); return data.content?.map(b => b.text || "").join("") || ""; } async function callAIJSON(messages, maxTokens = 4096) { const raw = await callAI(messages, maxTokens); return JSON.parse(raw.replace(/```json|```/g, "").trim()); } function fileToBase64(file) { return new Promise((res, rej) => { const r = new FileReader(); r.onload = () => res({ base64: r.result.split(",")[1], mediaType: file.type }); r.onerror = rej; r.readAsDataURL(file); }); } // ══════════════════════════════════════════════════════════════ // RULE-BASED SCORING ENGINE — 100% Deterministic // ══════════════════════════════════════════════════════════════ const NICHE_DATA = { "father's day": { marketSize: 9, trend: 7, peakMonth: 6, seasonal: true }, "mother's day": { marketSize: 8, trend: 7, peakMonth: 5, seasonal: true }, "christmas": { marketSize: 9, trend: 7, peakMonth: 11, seasonal: true }, "halloween": { marketSize: 7, trend: 8, peakMonth: 10, seasonal: true }, "valentine": { marketSize: 7, trend: 6, peakMonth: 2, seasonal: true }, "couple": { marketSize: 6, trend: 9, peakMonth: null, seasonal: false }, "sister": { marketSize: 7, trend: 2, peakMonth: null, seasonal: false }, "bestie": { marketSize: 7, trend: 3, peakMonth: null, seasonal: false }, "family": { marketSize: 8, trend: 7, peakMonth: null, seasonal: false }, "dog": { marketSize: 7, trend: 7, peakMonth: null, seasonal: false }, "cat": { marketSize: 6, trend: 6, peakMonth: null, seasonal: false }, "nurse": { marketSize: 5, trend: 6, peakMonth: 5, seasonal: true }, "teacher": { marketSize: 5, trend: 6, peakMonth: 5, seasonal: true }, "baby": { marketSize: 6, trend: 7, peakMonth: null, seasonal: false }, "grandparent": { marketSize: 6, trend: 6, peakMonth: null, seasonal: false }, "motivational": { marketSize: 6, trend: 5, peakMonth: null, seasonal: false }, "self-love": { marketSize: 5, trend: 5, peakMonth: null, seasonal: false }, "encouragement": { marketSize: 5, trend: 6, peakMonth: null, seasonal: false }, "mental health": { marketSize: 5, trend: 7, peakMonth: null, seasonal: false }, "christian": { marketSize: 6, trend: 6, peakMonth: null, seasonal: false }, "camping": { marketSize: 5, trend: 6, peakMonth: 7, seasonal: true }, "fishing": { marketSize: 5, trend: 5, peakMonth: 6, seasonal: true }, }; const PRODUCT_TYPE_DATA = { "mug": { noveltyBase: 3, canvasWeight: false, personalizeEase: 8, scalability: 8 }, "canvas": { noveltyBase: 4, canvasWeight: true, personalizeEase: 7, scalability: 7 }, "poster": { noveltyBase: 3, canvasWeight: true, personalizeEase: 7, scalability: 7 }, "puzzle": { noveltyBase: 6, canvasWeight: false, personalizeEase: 6, scalability: 6 }, "t-shirt": { noveltyBase: 2, canvasWeight: false, personalizeEase: 5, scalability: 9 }, "hoodie": { noveltyBase: 3, canvasWeight: false, personalizeEase: 5, scalability: 7 }, "blanket": { noveltyBase: 5, canvasWeight: true, personalizeEase: 6, scalability: 5 }, "ornament": { noveltyBase: 5, canvasWeight: false, personalizeEase: 6, scalability: 6 }, "tumbler": { noveltyBase: 4, canvasWeight: false, personalizeEase: 7, scalability: 7 }, "onesie": { noveltyBase: 6, canvasWeight: false, personalizeEase: 5, scalability: 5 }, "car visor clip": { noveltyBase: 9, canvasWeight: false, personalizeEase: 5, scalability: 4 }, "phone case": { noveltyBase: 3, canvasWeight: false, personalizeEase: 6, scalability: 7 }, "tote bag": { noveltyBase: 4, canvasWeight: false, personalizeEase: 6, scalability: 6 }, "crochet": { noveltyBase: 7, canvasWeight: false, personalizeEase: 3, scalability: 4 }, "twotone mug": { noveltyBase: 6, canvasWeight: false, personalizeEase: 8, scalability: 7 }, }; const EMOTION_SCORES = { "pride": 9, "love": 8, "joy": 7, "admiration": 7, "nostalgia": 7, "contentment": 5, "humor": 6, "sarcasm": 5, "gratitude": 6, "hope": 5, "encouragement": 5, "generic positive": 3, "none": 1, }; const ART_STYLE_NICHE_FIT = { "cartoon": { "bestie": 8, "sister": 8, "friend": 8, "family": 6, "couple": 6, "dog": 8, "cat": 8, "default": 6 }, "semi real": { "family": 8, "mother's day": 9, "father's day": 8, "couple": 8, "default": 7 }, "watercolor": { "family": 8, "mother's day": 8, "couple": 7, "default": 6 }, "text only": { "motivational": 7, "sarcasm": 8, "funny": 8, "default": 5 }, "photo": { "couple": 6, "family": 7, "default": 5 }, "handmade": { "encouragement": 7, "self-love": 7, "gift": 7, "default": 6 }, "icon/symbol": { "default": 5 }, }; function getSeasonalMultiplier(nicheKey, currentMonth) { const nd = NICHE_DATA[nicheKey]; if (!nd || !nd.seasonal || !nd.peakMonth) return 1.0; const diff = nd.peakMonth - currentMonth; const weeksBefore = diff >= 0 ? diff * 4.3 : (diff + 12) * 4.3; if (weeksBefore >= 2 && weeksBefore <= 4) return 1.5; if (weeksBefore > 4 && weeksBefore <= 8) return 1.3; return 1.0; } function findNicheKey(nicheStr) { if (!nicheStr) return null; const lower = nicheStr.toLowerCase(); for (const key of Object.keys(NICHE_DATA)) { if (lower.includes(key)) return key; } return null; } function findProductKey(ptStr) { if (!ptStr) return null; const lower = ptStr.toLowerCase(); for (const key of Object.keys(PRODUCT_TYPE_DATA)) { if (lower.includes(key)) return key; } return null; } function findArtStyleKey(asStr) { if (!asStr) return null; const lower = asStr.toLowerCase(); if (lower.includes("cartoon")) return "cartoon"; if (lower.includes("semi real") || lower.includes("semi-real") || lower.includes("realistic")) return "semi real"; if (lower.includes("watercolor")) return "watercolor"; if (lower.includes("text only") || lower.includes("text-only") || lower.includes("typography")) return "text only"; if (lower.includes("photo")) return "photo"; if (lower.includes("handmade") || lower.includes("crochet") || lower.includes("craft")) return "handmade"; if (lower.includes("icon") || lower.includes("symbol")) return "icon/symbol"; return null; } function findEmotionKey(emotionStr) { if (!emotionStr) return "none"; const lower = emotionStr.toLowerCase(); for (const key of Object.keys(EMOTION_SCORES)) { if (lower.includes(key)) return key; } return "generic positive"; } // ── MAIN SCORING FUNCTION ── function scoreProduct(attrs) { const nicheKey = findNicheKey(attrs.niche); const productKey = findProductKey(attrs.productType); const artKey = findArtStyleKey(attrs.artStyle); const emotionKey = findEmotionKey(attrs.emotionalTrigger); const currentMonth = new Date().getMonth() + 1; const nd = nicheKey ? NICHE_DATA[nicheKey] : null; const pd = productKey ? PRODUCT_TYPE_DATA[productKey] : null; const results = {}; let totalWeightedScore = 0; let totalWeight = 0; // ── 1. TOPIC FIT ── const tf_marketSize = nd ? nd.marketSize : 5; const tf_buyerClarity = attrs.buyerUserSame === "different" ? 8 : attrs.buyerUserSame === "same" ? 6 : 5; const tf_subtopicDepth = nd ? Math.min(nd.marketSize + 1, 10) : 5; const tf_trend = nd ? nd.trend : 5; const tf_confidence = nd ? 0.85 : 0.4; const tf_score = Math.round(((tf_marketSize + tf_buyerClarity + tf_subtopicDepth + tf_trend) / 4) * 10) / 10; results.topicFit = { score: Math.min(10, Math.max(1, tf_score)), confidence: tf_confidence, subFactors: [ { name: "Market Size", score: tf_marketSize, note: nd ? `Niche "${nicheKey}" trong database` : "Niche chưa có trong database" }, { name: "Buyer vs User Clarity", score: tf_buyerClarity, note: attrs.buyerUserSame === "different" ? "Buyer ≠ User → rõ ràng (gift)" : "Buyer có thể = User" }, { name: "Sub-topic Depth", score: tf_subtopicDepth, note: nd ? "Có data sub-topics" : "Chưa rõ depth" }, { name: "Trend Direction", score: tf_trend, note: nd ? `Trend score: ${tf_trend}/10` : "Không đủ data trend" }, ], }; // ── 2. DEMAND STRENGTH ── const seasonalMult = nicheKey ? getSeasonalMultiplier(nicheKey, currentMonth) : 1.0; const dm_insight = attrs.insightDepth === "strong" ? 8 : attrs.insightDepth === "medium" ? 6 : attrs.insightDepth === "weak" ? 3 : 4; const dm_emotion = EMOTION_SCORES[emotionKey] || 4; const dm_category = attrs.demandCategory === "spiritual" ? 7 : attrs.demandCategory === "material" ? 5 : 6; const dm_seasonality = nd?.seasonal ? (seasonalMult > 1 ? 8 : 5) : 5; const dm_raw = (dm_insight + dm_emotion + dm_category + dm_seasonality) / 4; const dm_score = Math.min(10, dm_raw * seasonalMult); const dm_confidence = attrs.insightDepth ? 0.8 : 0.5; results.demandStrength = { score: Math.round(Math.min(10, Math.max(1, dm_score)) * 10) / 10, confidence: dm_confidence, seasonalMultiplier: seasonalMult, subFactors: [ { name: "Insight Depth", score: dm_insight, note: `Insight: ${attrs.insightDepth || "chưa xác định"}` }, { name: "Emotional Trigger", score: dm_emotion, note: `Emotion: ${emotionKey} (${dm_emotion}/10)` }, { name: "Demand Category", score: dm_category, note: attrs.demandCategory === "spiritual" ? "Tinh thần → mạnh cho personalized" : "Vật chất/Chưa rõ" }, { name: "Seasonality", score: dm_seasonality, note: seasonalMult > 1 ? `Peak sắp đến! ×${seasonalMult}` : "Evergreen hoặc ngoài mùa" }, ], }; // ── 3. CONCEPT COHERENCE ── const cc_firstImpression = attrs.firstImpressionClear === "yes" ? 8 : attrs.firstImpressionClear === "somewhat" ? 6 : 4; let cc_artNicheFit = 5; if (artKey && nicheKey) { const fitMap = ART_STYLE_NICHE_FIT[artKey]; cc_artNicheFit = fitMap?.[nicheKey] || fitMap?.["default"] || 5; } else if (artKey) { cc_artNicheFit = ART_STYLE_NICHE_FIT[artKey]?.["default"] || 5; } const cc_messageTone = attrs.messageToneMatch === "perfect" ? 9 : attrs.messageToneMatch === "good" ? 7 : attrs.messageToneMatch === "mismatch" ? 3 : 5; const cc_productUseCase = attrs.productUseCaseFit === "perfect" ? 9 : attrs.productUseCaseFit === "good" ? 7 : attrs.productUseCaseFit === "weak" ? 4 : 5; const cc_focalPoint = attrs.hasSingleFocalPoint === "yes" ? 8 : attrs.hasSingleFocalPoint === "somewhat" ? 6 : 4; const cc_score = (cc_firstImpression + cc_artNicheFit + cc_messageTone + cc_productUseCase + cc_focalPoint) / 5; const cc_confidence = (attrs.firstImpressionClear && attrs.messageToneMatch) ? 0.85 : 0.6; results.conceptCoherence = { score: Math.round(Math.min(10, Math.max(1, cc_score)) * 10) / 10, confidence: cc_confidence, subFactors: [ { name: "First Impression (2s)", score: cc_firstImpression, note: attrs.firstImpressionClear === "yes" ? "Hiểu ngay trong 2s" : "Cần thời gian để hiểu" }, { name: "Art Style × Niche", score: cc_artNicheFit, note: `${artKey || "?"} × ${nicheKey || "?"} = ${cc_artNicheFit}/10` }, { name: "Message × Tone", score: cc_messageTone, note: `Match: ${attrs.messageToneMatch || "chưa rõ"}` }, { name: "Product × Use Case", score: cc_productUseCase, note: `Fit: ${attrs.productUseCaseFit || "chưa rõ"}` }, { name: "Single Focal Point", score: cc_focalPoint, note: attrs.hasSingleFocalPoint === "yes" ? "Có 1 điểm nhấn rõ" : "Thiếu focal point" }, ], }; // ── 4. NOVELTY & DELIGHT ── const nv_productNovelty = pd ? pd.noveltyBase : 5; const nv_conceptFresh = attrs.conceptFreshness === "very fresh" ? 9 : attrs.conceptFreshness === "fresh" ? 7 : attrs.conceptFreshness === "common" ? 4 : attrs.conceptFreshness === "saturated" ? 2 : 5; const nv_personalizationInno = attrs.personalizationInnovation === "innovative" ? 9 : attrs.personalizationInnovation === "above average" ? 7 : attrs.personalizationInnovation === "standard" ? 4 : attrs.personalizationInnovation === "none" ? 2 : 4; const nv_surprise = attrs.surpriseElement === "high" ? 9 : attrs.surpriseElement === "moderate" ? 6 : attrs.surpriseElement === "low" ? 3 : 4; const nv_score = (nv_productNovelty + nv_conceptFresh + nv_personalizationInno + nv_surprise) / 4; const nv_confidence = (attrs.conceptFreshness && attrs.surpriseElement) ? 0.8 : 0.5; results.noveltyDelight = { score: Math.round(Math.min(10, Math.max(1, nv_score)) * 10) / 10, confidence: nv_confidence, subFactors: [ { name: "Product Type Novelty", score: nv_productNovelty, note: pd ? `${productKey}: base novelty ${nv_productNovelty}` : "Product type chưa có data" }, { name: "Concept Freshness", score: nv_conceptFresh, note: `${attrs.conceptFreshness || "chưa rõ"}` }, { name: "Personalization Innovation", score: nv_personalizationInno, note: `${attrs.personalizationInnovation || "chưa rõ"}` }, { name: "Surprise Element", score: nv_surprise, note: `${attrs.surpriseElement || "chưa rõ"}` }, ], }; // ── 5. DESIGN EXECUTION ── const hasArt = attrs.hasArt !== "no"; const de_applicable = []; const de_na = []; const de_typography = attrs.typographyQuality === "excellent" ? 9 : attrs.typographyQuality === "good" ? 7 : attrs.typographyQuality === "ok" ? 5 : attrs.typographyQuality === "poor" ? 3 : 5; de_applicable.push({ name: "Typography", score: de_typography, note: `Quality: ${attrs.typographyQuality || "chưa rõ"}` }); const de_color = attrs.colorHarmony === "excellent" ? 9 : attrs.colorHarmony === "good" ? 7 : attrs.colorHarmony === "ok" ? 5 : attrs.colorHarmony === "poor" ? 3 : 5; de_applicable.push({ name: "Color Harmony", score: de_color, note: `${attrs.colorHarmony || "chưa rõ"}` }); if (hasArt) { const de_artQ = attrs.artQuality === "excellent" ? 9 : attrs.artQuality === "good" ? 7 : attrs.artQuality === "ok" ? 5 : attrs.artQuality === "poor" ? 3 : 5; de_applicable.push({ name: "Art Quality", score: de_artQ, note: `${attrs.artQuality || "chưa rõ"}` }); } else { de_na.push("Art Quality"); } const de_bg = attrs.backgroundQuality === "excellent" ? 9 : attrs.backgroundQuality === "good" ? 7 : attrs.backgroundQuality === "ok" ? 5 : attrs.backgroundQuality === "poor" ? 3 : 5; de_applicable.push({ name: "Background", score: de_bg, note: `${attrs.backgroundQuality || "chưa rõ"}` }); const de_layout = attrs.layoutBalance === "excellent" ? 9 : attrs.layoutBalance === "good" ? 7 : attrs.layoutBalance === "ok" ? 5 : attrs.layoutBalance === "poor" ? 3 : 5; de_applicable.push({ name: "Layout Balance", score: de_layout, note: `${attrs.layoutBalance || "chưa rõ"}` }); const de_score = de_applicable.reduce((s, f) => s + f.score, 0) / de_applicable.length; const de_confidence = attrs.typographyQuality ? 0.8 : 0.5; results.designExecution = { score: Math.round(Math.min(10, Math.max(1, de_score)) * 10) / 10, confidence: de_confidence, subFactors: de_applicable, notApplicable: de_na, }; // ── 6. PERSONALIZATION POWER ── const pp_depth = attrs.personalizationLevel === "deep" ? 9 : attrs.personalizationLevel === "medium" ? 7 : attrs.personalizationLevel === "basic" ? 5 : attrs.personalizationLevel === "none" ? 2 : 4; const pp_options = attrs.optionVariety === "rich" ? 9 : attrs.optionVariety === "moderate" ? 6 : attrs.optionVariety === "limited" ? 4 : attrs.optionVariety === "none" ? 2 : 4; const pp_placement = attrs.namePlacement === "natural" ? 8 : attrs.namePlacement === "ok" ? 6 : attrs.namePlacement === "forced" ? 3 : attrs.namePlacement === "none" ? 2 : 4; const pp_backendUX = attrs.backendUX === "excellent" ? 9 : attrs.backendUX === "good" ? 7 : attrs.backendUX === "ok" ? 5 : 5; const pp_score = (pp_depth + pp_options + pp_placement + pp_backendUX) / 4; const pp_confidence = attrs.personalizationLevel ? 0.85 : 0.5; // If product has NO personalization at all, mark applicable but low score (don't skip) results.personalizationPower = { score: Math.round(Math.min(10, Math.max(1, pp_score)) * 10) / 10, confidence: pp_confidence, subFactors: [ { name: "Depth", score: pp_depth, note: `Level: ${attrs.personalizationLevel || "chưa rõ"}` }, { name: "Option Variety", score: pp_options, note: `${attrs.optionVariety || "chưa rõ"}` }, { name: "Name Placement", score: pp_placement, note: `${attrs.namePlacement || "chưa rõ"}` }, { name: "Backend UX", score: pp_backendUX, note: `${attrs.backendUX || "chưa rõ"}` }, ], }; // ── FINAL SCORE (Adaptive Weighted Sum + Confidence) ── const productWeights = getWeightsForProduct(productKey); const patternEntries = [ { key: "topicFit", wKey: "topic" }, { key: "demandStrength", wKey: "demand" }, { key: "conceptCoherence", wKey: "concept" }, { key: "noveltyDelight", wKey: "novelty" }, { key: "designExecution", wKey: "design" }, { key: "personalizationPower", wKey: "personalization" }, ]; for (const pe of patternEntries) { const pat = results[pe.key]; const w = productWeights[pe.wKey] || 0.15; totalWeightedScore += w * pat.score * pat.confidence; totalWeight += w * pat.confidence; } const finalScore = totalWeight > 0 ? Math.round((totalWeightedScore / totalWeight) * 10) / 10 : 0; const verdict = finalScore >= 7 ? "GO" : finalScore >= 5 ? "MAYBE" : "NOGO"; // ── SCALE POTENTIAL (separate) ── const sp_topic = nd ? Math.min(nd.marketSize + 1, 10) : 5; const sp_product = pd ? pd.scalability : 5; const sp_scamper = attrs.scamperPotential === "high" ? 8 : attrs.scamperPotential === "medium" ? 6 : 4; const scalePotential = Math.round(((sp_topic + sp_product + sp_scamper) / 3) * 10) / 10; return { scoring: results, finalScore: Math.min(10, Math.max(1, finalScore)), verdict, scalePotential: { score: scalePotential, directions: [] }, weights: productWeights, seasonalMultiplier: seasonalMult, }; } function getWeightsForProduct(productKey) { const presets = { "canvas": { topic: 0.18, demand: 0.22, concept: 0.20, novelty: 0.13, design: 0.17, personalization: 0.10 }, "poster": { topic: 0.18, demand: 0.22, concept: 0.20, novelty: 0.13, design: 0.17, personalization: 0.10 }, "puzzle": { topic: 0.20, demand: 0.25, concept: 0.18, novelty: 0.15, design: 0.12, personalization: 0.10 }, "mug": { topic: 0.20, demand: 0.25, concept: 0.18, novelty: 0.15, design: 0.12, personalization: 0.10 }, }; return presets[productKey] || { topic: 0.20, demand: 0.25, concept: 0.18, novelty: 0.15, design: 0.12, personalization: 0.10 }; } // ── AI EXTRACT PROMPT ── const EXTRACT_PROMPT = `Bạn là expert POD product analyst. Phân tích ảnh/mô tả product và extract CHÍNH XÁC các thuộc tính sau. CRITICAL: Chỉ trả về JSON, không markdown, không backtick. Trả lời bằng các giá trị ENUM được chỉ định. { "title": "tên mô tả ngắn product", "productType": "mug|canvas|poster|puzzle|t-shirt|hoodie|blanket|ornament|tumbler|onesie|car visor clip|phone case|tote bag|crochet|twotone mug|other: ...", "niche": "father's day|mother's day|christmas|halloween|valentine|couple|sister|bestie|family|dog|cat|nurse|teacher|baby|grandparent|motivational|self-love|encouragement|mental health|christian|camping|fishing|other: ...", "artStyle": "cartoon|semi real|watercolor|text only|photo|handmade|icon/symbol|other: ...", "hasArt": "yes|no", "quote": "text quote nếu có, hoặc null", "colorPalette": "mô tả ngắn màu chính", "targetBuyer": "mô tả buyer ngắn", "targetUser": "mô tả user ngắn", "buyerUserSame": "same|different|unclear", "emotionalTrigger": "pride|love|joy|admiration|nostalgia|contentment|humor|sarcasm|gratitude|hope|encouragement|generic positive|none", "insightDepth": "strong|medium|weak", "demandCategory": "spiritual|material|mixed", "firstImpressionClear": "yes|somewhat|no", "messageToneMatch": "perfect|good|ok|mismatch", "productUseCaseFit": "perfect|good|weak", "hasSingleFocalPoint": "yes|somewhat|no", "conceptFreshness": "very fresh|fresh|common|saturated", "personalizationLevel": "deep|medium|basic|none", "personalizationInnovation": "innovative|above average|standard|none", "optionVariety": "rich|moderate|limited|none", "namePlacement": "natural|ok|forced|none", "backendUX": "excellent|good|ok|unknown", "surpriseElement": "high|moderate|low", "typographyQuality": "excellent|good|ok|poor", "colorHarmony": "excellent|good|ok|poor", "artQuality": "excellent|good|ok|poor|n/a", "backgroundQuality": "excellent|good|ok|poor", "layoutBalance": "excellent|good|ok|poor", "scamperPotential": "high|medium|low" }`; // ══════════════════════════════════════════ // MAIN APP // ══════════════════════════════════════════ export default function App() { const [tab, setTab] = useState("evaluate"); const [winners, setWinners] = useState([]); const [losers, setLosers] = useState([]); const [tests, setTests] = useState([]); const [patterns, setPatterns] = useState([]); useEffect(() => { (async () => { const [w, l, t, p] = await Promise.all([sGet(SK.winners), sGet(SK.losers), sGet(SK.tests), sGet(SK.patterns)]); if (w) setWinners(w); if (l) setLosers(l); if (t) setTests(t); if (p) setPatterns(p); })(); }, []); const saveW = async d => { setWinners(d); await sSet(SK.winners, d); }; const saveL = async d => { setLosers(d); await sSet(SK.losers, d); }; const saveT = async d => { setTests(d); await sSet(SK.tests, d); }; const saveP = async d => { setPatterns(d); await sSet(SK.patterns, d); }; const tabs = [ { id: "evaluate", label: "Đánh giá", icon: "🔍" }, { id: "training", label: `Training (${winners.length + losers.length})`, icon: "🧠" }, { id: "patterns", label: `Patterns`, icon: "📊" }, { id: "test", label: `Test (${tests.length})`, icon: "🧪" }, ]; return (
{/* Header */}
POD × FB ADS

Push Traffic Evaluator

{winners.length}
WINNERS
{tests.length}
TESTING
{tabs.map(t => )}
{tab === "evaluate" && } {tab === "training" && } {tab === "patterns" && } {tab === "test" && }
); } // ── Shared helpers ── const sc = s => s >= 7.5 ? "var(--green)" : s >= 5 ? "var(--gold)" : "var(--red)"; const vStyle = v => v === "GO" ? { bg: "var(--green-light)", c: "var(--green)" } : v === "MAYBE" ? { bg: "var(--gold-light)", c: "var(--gold)" } : { bg: "var(--red-light)", c: "var(--red)" }; function ScoreCard({ result, title, subtitle }) { const vs = vStyle(result.verdict); return (
{result.finalScore?.toFixed(1)}
/10
{result.verdict} {title}
{subtitle &&
{subtitle}
}
); } function ScoringBreakdown({ scoring }) { const labels = { topicFit: "🎯 Topic Fit", demandStrength: "💪 Demand", conceptCoherence: "🎨 Concept", noveltyDelight: "✨ Novelty", designExecution: "🖌️ Design", personalizationPower: "🔧 Personal." }; return (
📊 CHẤM ĐIỂM — 7 PATTERNS (Rule-based)
{Object.entries(scoring).map(([key, pat]) => (
{labels[key] || key} {pat.seasonalMultiplier > 1 && ×{pat.seasonalMultiplier} seasonal}
conf: {(pat.confidence * 100).toFixed(0)}% {pat.score}
{pat.subFactors?.map((sf, i) => (
{sf.name}: {sf.note} {sf.score}/10
))} {pat.notApplicable?.length > 0 &&
N/A: {pat.notApplicable.join(", ")}
}
))}
); } function DebateChat({ context }) { const [msgs, setMsgs] = useState([]); const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const send = async () => { if (!input.trim() || loading) return; const msg = input.trim(); setInput(""); setMsgs(p => [...p, { role: "user", text: msg }]); setLoading(true); try { const raw = await callAI([{ role: "user", content: `${context}\n\nUser: ${msg}\n\nTrả lời ngắn gọn bằng tiếng Việt.` }], 1024); setMsgs(p => [...p, { role: "ai", text: raw }]); } catch { setMsgs(p => [...p, { role: "ai", text: "Lỗi kết nối." }]); } setLoading(false); }; return (
💬 DEBATE VỚI AGENT
{msgs.length === 0 &&
Hỏi tại sao chấm cao/thấp, đề xuất điều chỉnh...
} {msgs.map((m, i) =>
{m.text}
)} {loading &&
}
setInput(e.target.value)} placeholder="Debate..." onKeyDown={e => e.key === "Enter" && send()} style={{ flex: 1 }} />
); } // ══════════════════════════════════════════ // TAB: EVALUATE // ══════════════════════════════════════════ function EvalTab({ winners, losers, patterns, tests, saveT }) { const [imgs, setImgs] = useState([]); const [name, setName] = useState(""); const [niche, setNiche] = useState(""); const [pt, setPt] = useState(""); const [desc, setDesc] = useState(""); const [loading, setLoading] = useState(false); const [attrs, setAttrs] = useState(null); const [result, setResult] = useState(null); const handleFiles = async files => { for (const f of [...files].slice(0, 4 - imgs.length)) { const img = await fileToBase64(f); setImgs(p => [...p, img]); } }; const analyze = async () => { if (!imgs.length && !desc && !name) return; setLoading(true); setAttrs(null); setResult(null); try { const userCtx = `Tên: ${name || "Xem ảnh"}\nNiche: ${niche || "Chưa rõ"}\nProduct type: ${pt || "Chưa rõ"}\nGhi chú: ${desc || "Không"}`; const msgContent = [ ...imgs.map(img => ({ type: "image", source: { type: "base64", media_type: img.mediaType, data: img.base64 } })), { type: "text", text: `${EXTRACT_PROMPT}\n\nUser input:\n${userCtx}` } ]; const extracted = await callAIJSON([{ role: "user", content: msgContent }]); setAttrs(extracted); const scored = scoreProduct(extracted); setResult(scored); } catch (e) { alert("Lỗi: " + e.message); } setLoading(false); }; const reset = () => { setImgs([]); setName(""); setNiche(""); setPt(""); setDesc(""); setAttrs(null); setResult(null); }; const addToTest = async () => { if (!result || !attrs) return; await saveT([...tests, { id: Date.now().toString(), name: attrs.title || name, niche: attrs.niche || niche, productType: attrs.productType || pt, score: result.finalScore, verdict: result.verdict, addedAt: new Date().toISOString(), status: "testing" }]); alert("Đã đưa vào Test!"); }; return (
{!result ? (
🔍 ĐÁNH GIÁ PRODUCT MỚI
handleFiles(e.target.files)} /> {imgs.length ?
{imgs.map((img, i) =>
)}
:
📷
Upload ảnh mockup/design (tối đa 4)
}
Tên sản phẩm
setName(e.target.value)} placeholder="VD: Funny nurse cat mug..." />
Product Type
setPt(e.target.value)} placeholder="Mug, Canvas, Puzzle..." />
Niche
setNiche(e.target.value)} placeholder="Mother's Day, Dad, Couple..." />
Ghi chú