/* global React, PR */ const { useState, useMemo } = React; const { I, Avatar } = PR; const EVAL_STAGES = ["idea", "greybox", "prototype", "coldtest"]; // ================================================================= // Radar / Spider Chart — pure SVG, follows the registry's visual system // ================================================================= function Radar({ axes, polygons, size = 360 }) { // Wheel itself is square (size × size). Extend the SVG viewBox horizontally // to give right/left label gutters so long labels never clip the panel. const sideGutter = 110; const totalW = size + sideGutter * 2; const wheelR = size / 2 - 12; const cx = sideGutter + size / 2, cy = size / 2; const r = wheelR; const N = axes.length; const angle = (i) => -Math.PI / 2 + (i / N) * Math.PI * 2; const point = (i, value) => { const a = angle(i); const rr = r * (value / 10); return [cx + Math.cos(a) * rr, cy + Math.sin(a) * rr]; }; const ringValues = [2, 4, 6, 8, 10]; const polygonPath = (scores) => { return axes.map((ax, i) => { const v = scores[ax.id] ?? 0; const [x, y] = point(i, v); return `${i === 0 ? "M" : "L"}${x.toFixed(1)},${y.toFixed(1)}`; }).join(" ") + " Z"; }; return ( {ringValues.map((v) => ( point(i, v).map(n => n.toFixed(1)).join(",")).join(" ")} /> ))} {axes.map((_, i) => { const [x, y] = point(i, 10); return ; })} {polygons.map((p) => ( {p.kind === "you" && axes.map((ax, i) => { const [x, y] = point(i, p.scores[ax.id] ?? 0); return ; })} ))} {axes.map((ax, i) => { const a = angle(i); const lx = cx + Math.cos(a) * (r + 14); const ly = cy + Math.sin(a) * (r + 14); const cosA = Math.cos(a); const anchor = Math.abs(cosA) < 0.25 ? "middle" : (cosA > 0 ? "start" : "end"); return ( {ax.label} ); })} {ringValues.map(v => { const [, y] = point(0, v); return {v}; })} ); } // ================================================================= // Vote view — independent scoring sliders // ================================================================= function VoteView({ draft, onChange, onSubmit, votersCount, alreadySubmitted }) { const groups = window.EVALUATOR_DATA.groups; const totalAxes = groups.reduce((n, g) => n + g.axes.length, 0); const filled = Object.values(draft || {}).filter(v => v != null).length; const complete = filled === totalAxes; return (
Independent evaluation
Score 1–10 on each criterion. Your scores are hidden from the team until you submit. {votersCount > 0 && ` ${votersCount} ${votersCount === 1 ? "teammate has" : "teammates have"} already voted.`}
{filled} / {totalAxes}
{groups.map(g => (
{g.label}
{g.sub}
{g.axes.map(ax => { const v = draft?.[ax.id] ?? null; return (
{ax.label} {ax.examples && ( ? 10 {ax.examples.high} 1 {ax.examples.low} )}
{ax.hint}
onChange(ax.id, parseInt(e.target.value, 10))} className={`ev-slider ${v == null ? "ev-slider-untouched" : ""}`} />
{[1,2,3,4,5,6,7,8,9,10].map(n => {n === 1 || n === 10 ? n : ""})}
{v == null ? "—" : v}
); })}
))}
{alreadySubmitted ? "You already submitted a vote on this idea. Updating will overwrite your previous scores." : complete ? "All criteria scored. You're ready to submit." : `Score the remaining ${totalAxes - filled} criteria to submit.` }
); } // ================================================================= // Report view — radar, strengths/weaknesses, AI assistant // ================================================================= function ReportView({ hypothesis, axes, teamAvg, yourScores, voters }) { const polygons = [ { key: "team", kind: "team", scores: teamAvg }, ...(yourScores ? [{ key: "you", kind: "you", scores: yourScores }] : []), ]; const sortedAxes = axes.slice().sort((a, b) => (teamAvg[b.id] ?? 0) - (teamAvg[a.id] ?? 0)); const strengths = sortedAxes.filter(ax => (teamAvg[ax.id] ?? 0) >= 7.5).slice(0, 3); const weaknesses = sortedAxes.slice().reverse().filter(ax => (teamAvg[ax.id] ?? 0) <= 5.5).slice(0, 3); const overall = (axes.reduce((sum, ax) => sum + (teamAvg[ax.id] ?? 0), 0) / axes.length); const groupAvgs = window.EVALUATOR_DATA.groups.map(g => { const sum = g.axes.reduce((s, ax) => s + (teamAvg[ax.id] ?? 0), 0); return { id: g.id, label: g.label, avg: sum / g.axes.length }; }); const aiInsights = useMemo( () => buildInsights(hypothesis, teamAvg, axes), [hypothesis.id, JSON.stringify(teamAvg)], ); return (

Score profile

Overlay of your evaluation against the team average across {axes.length} criteria.

Team average
{yourScores &&
Your score
}
Consolidated score
{overall.toFixed(1)}/10
{groupAvgs.map(ga => (
{ga.label}
{ga.avg.toFixed(1)}
))}

Evaluated by

{voters.length} {voters.length === 1 ? "person has" : "people have"} submitted independent scores.

{voters.slice(0, 6).map(v => (
))}
{voters.length === 0 ? "No teammate scores yet." : voters.length <= 3 ? voters.map(v => v.name).join(", ") : `${voters.slice(0, 2).map(v => v.name).join(", ")} and ${voters.length - 2} others`}
{I.check}Strengths
{strengths.length === 0 && No criteria scored ≥ 7.5 yet.} {strengths.map(ax => ( {ax.label}{(teamAvg[ax.id] ?? 0).toFixed(1)} ))}
{I.flag}Weaknesses
{weaknesses.length === 0 && No criteria scored ≤ 5.5.} {weaknesses.map(ax => ( {ax.label}{(teamAvg[ax.id] ?? 0).toFixed(1)} ))}
AI

R&D Lab Assistant

Actionable recommendations generated from the lowest-scoring axes.

{aiInsights.map((ins, i) => (
{ins.axis} {ins.score.toFixed(1)}/10
{ins.body}
{ins.suggestion && (
Suggestion {ins.suggestion}
)}
))}
); } function buildInsights(hypothesis, teamAvg, axes) { const recipes = { three_sec: { tone: "warn", body: "Players don't read the core action within the first three seconds. That's the hyper-casual death zone — drop-off before the first feedback loop fires.", suggestion: "Simplify the visual language: strong color contrast on the interactable, hide most UI on first run, and make the core action a single swipe or tap.", }, playable_ad: { tone: "warn", body: "Translating the loop into a 15-second playable creative looks expensive. Without that, paid UA can't validate at scale.", suggestion: "Identify a 5-second 'wow' moment and storyboard a creative around just that beat — even if it skips the meta layer entirely.", }, one_screen: { tone: "warn", body: "The loop currently spans multiple screens. Test cycles will be slow and signal will be noisy.", suggestion: "Compress the entire core loop onto one screen for the greybox build. Save navigation for after the prototype gate.", }, ltv: { tone: "info", body: "Meta depth scored low. The game can hook but may not retain past D7 without more economy or progression hooks.", suggestion: "Sketch the second-week loop now — what does the player chase on day 8? Define currencies and unlock cadence before scale.", }, viral: { tone: "info", body: "Shareability is below threshold. Without organic creators, paid UA absorbs the entire growth budget.", suggestion: "Add one moment per session that a player would film: a slow-mo, a chain reaction, an unexpected fail state.", }, satisfying: { tone: "info", body: "The core action lacks tactile satisfaction. The metric most strongly correlated with D1 retention in our archive.", suggestion: "Pass the build to audio next sprint. Layered SFX, screen shake, and chunky particles before more features.", }, ai_assets: { tone: "info", body: "Asset pipeline isn't AI-friendly — the genre or art direction blocks generative tools.", suggestion: "Consider a stylization pass: low-poly, flat-shaded, or hand-drawn looks reduce iteration cost dramatically.", }, innovation: { tone: "info", body: "The team rates this as familiar. That's not necessarily bad, but it sets a high bar for execution polish.", suggestion: "Double down on a single distinguishing element. If you can't name it in one sentence, the player can't either.", }, positioning: { tone: "info", body: "Positioning is unclear — the team can't predict why a player would download this over a closest comp.", suggestion: "Write the App Store subtitle now (max 30 chars). If three teammates write three different subtitles, the concept needs sharpening.", }, }; const sorted = axes.slice().sort((a, b) => (teamAvg[a.id] ?? 0) - (teamAvg[b.id] ?? 0)); const out = []; for (const ax of sorted) { if (out.length >= 2) break; const score = teamAvg[ax.id] ?? 0; if (score >= 6.5) break; const r = recipes[ax.id]; if (r) out.push({ axis: ax.label, score, ...r }); } const sortedDesc = axes.slice().sort((a, b) => (teamAvg[b.id] ?? 0) - (teamAvg[a.id] ?? 0)); const top = sortedDesc[0]; if (top && (teamAvg[top.id] ?? 0) >= 8) { out.push({ axis: top.label, score: teamAvg[top.id], tone: "good", body: `${top.label} scored highest across the team — this is the load-bearing pillar of the pitch.`, suggestion: "Build the next prototype iteration around amplifying this. Cut anything that distracts from it.", }); } if (out.length === 0) { out.push({ axis: "Overall", score: axes.reduce((s, ax) => s + (teamAvg[ax.id] ?? 0), 0) / axes.length, tone: "info", body: "Scores are middling across the board — no single weakness, but no breakout signal either.", suggestion: "Schedule a half-day pitch revision. Pick one criterion and aim for a 9 on it specifically.", }); } return out; } // ================================================================= // Container — switches between Vote and Report // ================================================================= function EvaluatorView({ data }) { const evalData = window.EVALUATOR_DATA; // Build the candidate list: real hypotheses in evaluable stages, ordered by // the pinned list first (mock votes refer to those), then the rest. const evaluable = useMemo(() => { const all = data.hypotheses.filter(h => EVAL_STAGES.includes(h.stage)); const pinIndex = new Map(evalData.evaluable.map((id, i) => [id, i])); return all.slice().sort((a, b) => { const ai = pinIndex.has(a.id) ? pinIndex.get(a.id) : Infinity; const bi = pinIndex.has(b.id) ? pinIndex.get(b.id) : Infinity; if (ai !== bi) return ai - bi; return (b.updated || "").localeCompare(a.updated || ""); }); }, [data.hypotheses]); const [selectedId, setSelectedId] = useState(evaluable[0]?.id || null); const [tab, setTab] = useState("vote"); const [drafts, setDrafts] = useState(() => { const seed = {}; Object.entries(evalData.drafts || {}).forEach(([id, d]) => { seed[id] = d ? { ...d } : {}; }); return seed; }); const [submittedSet, setSubmittedSet] = useState(() => new Set(evalData.submitted)); // Author map: real authors merged with synthetic mock voter authors so // avatars / names resolve for the pre-baked teammate votes. const authorMap = useMemo(() => { const m = Object.fromEntries(data.authors.map(a => [a.id, a])); (evalData.voterAuthors || []).forEach(a => { if (!m[a.id]) m[a.id] = a; }); return m; }, [data.authors]); const hypothesis = evaluable.find(h => h.id === selectedId) || null; const author = hypothesis ? authorMap[hypothesis.author] : null; const axes = useMemo(() => evalData.groups.flatMap(g => g.axes), []); const allVotes = evalData.votes.filter(v => v.h === selectedId); const teamVotes = allVotes.filter(v => v.v !== evalData.currentVoter); const voterIds = [...new Set(teamVotes.map(v => v.v))]; const voters = voterIds.map(id => authorMap[id]).filter(Boolean); const teamAvg = useMemo(() => { const out = {}; axes.forEach(ax => { const vals = teamVotes.map(v => v.s[ax.id]).filter(n => typeof n === "number"); out[ax.id] = vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : 0; }); return out; }, [selectedId]); const draft = drafts[selectedId] || {}; const alreadySubmitted = submittedSet.has(selectedId); const yourScores = alreadySubmitted ? draft : null; const handleChange = (axisId, value) => { setDrafts(prev => ({ ...prev, [selectedId]: { ...(prev[selectedId] || {}), [axisId]: value } })); }; const handleSubmit = () => { setSubmittedSet(prev => new Set([...prev, selectedId])); setTab("report"); }; const handleSelect = (id) => { setSelectedId(id); setTab(submittedSet.has(id) ? "report" : "vote"); }; if (!hypothesis) return
No hypotheses to evaluate.
; return (
{hypothesis.id} · Pitched by {author?.name || "—"}

{hypothesis.title}

Voters
{voterIds.length + (alreadySubmitted ? 1 : 0)}
Tags
{(hypothesis.tags || []).map(t => {t})}
{hypothesis.coreLoop &&

{hypothesis.coreLoop}

}
{tab === "vote" ? ( ) : ( )}
); } window.PREvaluator = { EvaluatorView };