// 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 (

質問データを読み込み中...

); } return (
ホシイカモ
{stage === "quiz" && ( QUESTION {String(questionCount + 1).padStart(2, "0")} )} {stage === "intro" && READY} {stage === "demo" && PROFILE} {stage === "analyzing" && ANALYZING} {stage === "reveal" && REVEAL} {stage === "error" && ERROR}
{stage === "intro" && } {stage === "demo" && setStage("intro")} />} {stage === "quiz" && currentQ && ( <> {thinking && } )} {stage === "analyzing" && ( )} {stage === "reveal" && diagnosis && ( )} {stage === "error" && ( )}
setTweak("showHints", v)} />
); } function IntroScreen({ onStart }) { return (
10–15 QUESTIONS · ADAPTIVE · NO COMPROMISE

まだ言葉に
なっていない
欲しいものを、
見つけにいく。

二択に答えるだけ。あなたの潜在意識が
探していたものを、霧の向こうから連れてきます。

1:30 所要時間
10–15 質問
5 提案
); } const GENRE_OPTIONS = [ { value: null, label: "オールジャンル", sub: "特に絞らない" }, { value: "fashion", label: "ファッション", sub: "服・靴・バッグ" }, { value: "cosme", label: "美容・コスメ", sub: "スキンケア・メイク" }, { value: "fitness", label: "健康・フィットネス", sub: "サプリ・器具・ウェア" }, { value: "gadget", label: "ガジェット・家電", sub: "デバイス・スマート家電" }, { value: "food", label: "食品・グルメ", sub: "お取り寄せ・飲料" }, { value: "book", label: "本・学び", sub: "書籍・講座・教材" }, { value: "home", label: "インテリア・生活", sub: "家具・雑貨・収納" }, { value: "experience", label: "体験・サービス", sub: "旅行・チケット・サブスク" }, ]; function DemoScreen({ onComplete, onBack }) { const [gender, setGender] = React.useState(null); const [age, setAge] = React.useState(null); const [genre, setGenre] = React.useState(null); const canProceed = gender !== null && age !== null; const handleStart = () => { if (!canProceed) return; onComplete({ gender, age, genre }); }; return (
PROFILE · あなたのことを少しだけ

より精度の高い商品提案のために教えてください

性別
{[ { value: "male", label: "男性" }, { value: "female", label: "女性" }, { value: "other", label: "その他" }, ].map((opt) => ( ))}
年代
{[ { value: "10s", label: "10代" }, { value: "20s", label: "20代" }, { value: "30s", label: "30代" }, { value: "40s", label: "40代" }, { value: "50s", label: "50代以上" }, ].map((opt) => ( ))}
探したいジャンル 任意
{GENRE_OPTIONS.map((opt) => ( ))}
); } function ElapsedTimer({ startTime }) { const [elapsed, setElapsed] = React.useState(0); React.useEffect(() => { const t = startTime || Date.now(); const tick = () => setElapsed(Math.floor((Date.now() - t) / 1000)); tick(); const id = setInterval(tick, 1000); return () => clearInterval(id); }, [startTime]); const min = Math.floor(elapsed / 60); const sec = elapsed % 60; const display = min > 0 ? `${min}:${String(sec).padStart(2, "0")}` : `0:${String(sec).padStart(2, "0")}`; return {display}; } function AnalyzingScreen() { const MESSAGES = [ "あなたの選択を解析しています", "潜在意識のパターンを読み取っています", "心理軸のスコアを算出しています", "診断レポートを生成しています", ]; const [msgIdx, setMsgIdx] = React.useState(0); const [dots, setDots] = React.useState(""); const [startTime] = React.useState(Date.now); React.useEffect(() => { const interval = setInterval(() => { setMsgIdx((prev) => (prev + 1) % MESSAGES.length); }, 8000); return () => clearInterval(interval); }, []); React.useEffect(() => { const interval = setInterval(() => { setDots((prev) => (prev.length >= 3 ? "" : prev + ".")); }, 500); return () => clearInterval(interval); }, []); return (

{MESSAGES[msgIdx]}{dots}

あなたの潜在欲求を診断しています

経過時間
); } function ErrorScreen({ message, onRetry, onReset }) { return (

解析に失敗しました

{message || "サーバーとの通信に問題が発生しました"}

); } window.ElapsedTimer = ElapsedTimer; const root = ReactDOM.createRoot(document.getElementById("root")); root.render();