質問データを読み込み中...
// app.jsx — メインアプリ(適応型診断エンジン版) const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "hologram", "showHints": true }/*EDITMODE-END*/; const STORAGE_KEY = "latent.state.v1"; const bridge = window.__latentBridge; const api = window.__latentAPI; function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [stage, setStage] = React.useState("loading"); const [engine, setEngine] = React.useState(null); const [currentQ, setCurrentQ] = React.useState(null); const [questionCount, setQuestionCount] = React.useState(0); const [maxQuestions, setMaxQuestions] = React.useState(15); const [answers, setAnswers] = React.useState([]); const [diagnosis, setDiagnosis] = React.useState(null); const [demographics, setDemographics] = React.useState({ gender: null, age: null, genre: null }); const [productSearchStart, setProductSearchStart] = React.useState(null); const [thinking, setThinking] = React.useState(null); const [apiLoading, setApiLoading] = React.useState(false); const [apiError, setApiError] = React.useState(null); React.useEffect(() => { DiagnosisEngine.load("question-bank.json").then(function (eng) { setEngine(eng); try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) { const s = JSON.parse(raw); if (s.stage === "reveal" && s.diagnosis) { setDiagnosis(s.diagnosis); setStage("reveal"); return; } } } catch (_) {} setStage("intro"); }).catch(function (err) { console.error("Failed to load question bank:", err); setStage("intro"); }); }, []); React.useEffect(() => { if (stage === "loading") return; const snap = { stage, diagnosis }; try { localStorage.setItem(STORAGE_KEY, JSON.stringify(snap)); } catch (_) {} bridge && bridge.store.set(snap); }, [stage, diagnosis]); const goDemo = () => { setStage("demo"); }; const startQuiz = (demo) => { if (!engine) return; setDemographics(demo); engine.init(); setQuestionCount(0); setAnswers([]); setDiagnosis(null); setApiError(null); const result = engine.next(); if (result.status === "question") { setCurrentQ(result.question); setQuestionCount(0); setMaxQuestions(result.maxQuestions); setStage("quiz"); } bridge && bridge.send({ type: "started", demographics: demo }); }; const callDiagnoseAPI = async () => { setApiLoading(true); setApiError(null); setStage("analyzing"); try { const paramOutputs = engine.export(); bridge && bridge.send({ type: "analyzing", paramOutputs }); // Phase 1: レポートを取得(商品なし・高速) const report = await api.diagnose(paramOutputs, demographics); setDiagnosis(report); setStage("reveal"); bridge && bridge.send({ type: "report_ready", diagnosis: report }); // Phase 2: 商品をバックグラウンドでポーリング if (report.session_id) { setProductSearchStart(Date.now()); api.pollProducts(report.session_id, function (result) { if (result.status === "done" && result.products) { setDiagnosis(function (prev) { var updated = Object.assign({}, prev, { products: result.products }); bridge && bridge.send({ type: "products_ready", products: result.products }); return updated; }); } else if (result.status === "error") { setDiagnosis(function (prev) { return Object.assign({}, prev, { productError: result.detail || "商品検索に失敗しました" }); }); } }); } } catch (e) { console.error("API error:", e); setApiError(e.message); if (window.SAMPLE_DIAGNOSIS) { setDiagnosis(window.SAMPLE_DIAGNOSIS); setStage("reveal"); } else { setStage("error"); } } finally { setApiLoading(false); } }; const handleAnswer = (choice) => { const answer = choice === "a" ? "A" : "B"; setThinking({ choice }); setTimeout(() => { const updateResult = engine.update(answer); setQuestionCount(updateResult.questionCount); setAnswers(function (prev) { return prev.concat([{ id: currentQ.id, answer: answer }]); }); if (updateResult.terminated) { setThinking(null); callDiagnoseAPI(); } else { const nextResult = engine.next(); if (nextResult.status === "question") { setCurrentQ(nextResult.question); setThinking(null); } else { setThinking(null); callDiagnoseAPI(); } } }, 500); }; const reset = () => { setStage("intro"); setQuestionCount(0); setCurrentQ(null); setAnswers([]); setDiagnosis(null); setApiError(null); bridge && bridge.send({ type: "reset" }); }; const handleProductTap = (productId) => { bridge && bridge.send({ type: "productTapped", productId }); }; React.useEffect(() => { if (!window.LATENT) return; window.LATENT._bind("start", goDemo); window.LATENT._bind("reset", reset); window.LATENT._bind("setTheme", (theme) => { if (["obsidian", "porcelain", "hologram"].includes(theme)) { setTweak("theme", theme); } }); window.LATENT._bind("restoreState", (s) => { if (!s) return; if (s.stage) setStage(s.stage); if (s.diagnosis) setDiagnosis(s.diagnosis); }); }, [engine]); if (stage === "loading") { return (
質問データを読み込み中...
二択に答えるだけ。あなたの潜在意識が
探していたものを、霧の向こうから連れてきます。
より精度の高い商品提案のために教えてください
{MESSAGES[msgIdx]}{dots}
あなたの潜在欲求を診断しています
{message || "サーバーとの通信に問題が発生しました"}