import React, { useState } from 'react'; import Icon from './CanvasIcon'; const fireToast = (msg, kind = 'success') => window.dispatchEvent(new CustomEvent('canvas-toast', { detail: { msg, kind } })); // Critic widgets are scripted in canvas; the *real* critique should happen in // the main chat so message history lives in one place (per Daniel's review). // "Open in chat" stashes a draft prompt + persona hint then asks CanvasPage to // navigate to chat — the chat page can read `canvas-chat-handoff` from localStorage. const handoffToChat = (persona, prompt, context = {}) => { try { localStorage.setItem('canvas-chat-handoff', JSON.stringify({ at: Date.now(), persona, prompt, ...context, })); } catch { /* ignore */ } window.dispatchEvent(new CustomEvent('canvas-open-in-chat', { detail: { persona, prompt } })); fireToast(`Opening ${persona} in chat — full history will be there.`); }; // ---------- Reviewer 2 widget ---------- export function Reviewer2Widget({ state, setState, openModal }) { return ( <>
"Paste a draft paragraph, abstract, or section. I'll respond as the most uncharitable but technically competent reviewer your work will ever see."
{state.lastReview ? (
Last critique · {state.lastReview.severity}/10 severity
Major: {state.lastReview.major}
+{state.lastReview.minorCount} minor issues, {state.lastReview.suggestionCount} suggestions
) : (
No drafts reviewed yet.
)}
tone
harsh
); } // ---------- Devil's Advocate widget ---------- export function DevilsAdvocateWidget({ state, setState, openModal }) { return ( <>
Your claim
"{state.claim}"
Strongest counters · {state.counters.length}
{state.counters.slice(0, 3).map((c, i) => (
{c.lbl}
{c.text}
))}
); } // ---------- Scope realism widget ---------- export function ScopeRealismWidget({ state, openModal }) { return (
{state.score.toFixed(1)}
Feasibility
{state.label}
target: {state.target}
{state.factors.map(f => (
{f.label} {f.val}
))}
); } // =================================================================== // Critic modals // =================================================================== const REVIEW_TEMPLATES = [ { severity: 8, major: 'The search plan states a goal ("land an internship") before defining what success looks like. You never specify the measurable target — which role family, how many tailored apps/week, by when — so there is no way to tell if the plan is working or when to change it.', minor: [ 'Weekly volume is described as "a lot of applications" without a concrete count or quality bar.', 'No rule for when to pause spray-and-pray and rewrite materials instead.', 'Networking is mentioned once with no outreach cadence or tracking.', 'No Friday review is scheduled — response rate will stay invisible.', ], suggestions: [ 'Add one sentence naming the target: e.g. "6 tailored analytics apps/week through April 15."', 'Define what "quality application" means (resume match + short note).', 'Add a weekly pipeline review with callback rate.', ], }, { severity: 7, major: 'You claim this approach is "optimal for getting interviews." That word is doing too much work. The plan is also compatible with worse outcomes (generic resumes, ignored follow-ups, burnout). Without a way to check whether callbacks improve, "optimal" is untestable.', minor: [ 'The channel mix is justified by "everyone uses LinkedIn" rather than by your major and fair calendar.', 'No statement of what would make you change the plan.', 'A search with no tracked numbers is a red flag — you cannot audit it later.', ], suggestions: [ 'Replace "optimal" with a specific target the plan either hits or misses.', 'List 1–2 signals (0 callbacks after 15 apps, missed apply blocks) that trigger a change.', ], }, { severity: 9, major: 'This reads like a wish list, not a plan. There is no number. There is no timeframe. The strongest claim is that you want to "find something good" — which is the lowest possible bar. If you actually care about the result, lead with the measurable goal, not the vibe.', minor: [ 'The word "networking" appears without a single outreach message drafted.', '"Dream company" is undefined; name 10 real targets.', 'No mention of resume quality, despite it driving most first-round outcomes.', ], suggestions: [ 'Lead sentence: "By I will ."', 'Cut "I want a good internship" entirely. Replace it with role lane, volume, and a date.', ], }, ]; export function ReviewerModal({ data, onClose }) { const [draft, setDraft] = useState(data.initial || ''); const [running, setRunning] = useState(false); const [review, setReview] = useState(null); const run = () => { if (!draft.trim()) return; setRunning(true); setReview(null); setTimeout(() => { const idx = (draft.length + draft.split(' ').length) % REVIEW_TEMPLATES.length; const r = REVIEW_TEMPLATES[idx]; setReview(r); setRunning(false); }, 900); }; const accept = () => { if (!review) return; data.onComplete({ draft, severity: review.severity, major: review.major, minorCount: review.minor.length, suggestionCount: review.suggestions.length, }); fireToast('Critique saved · severity ' + review.severity + '/10', 'critic'); onClose(); }; return (
e.stopPropagation()}>
Reviewer 2 Simulator
Paste a draft. Get the harshest competent peer review you'll ever read — before a real reviewer does.