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
);
}
// ===================================================================
// 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.
{!review && !running && (
What this is: a simulated peer review tuned to push back hard. It will name unstated operationalizations, rival explanations, and weak phrasings. It will not flatter you. That's the point.
{review.minor.length} minor · {review.suggestions.length} suggestions
Major issue
{review.major}
Minor issues
{review.minor.map((m, i) =>
{m}
)}
Suggestions
{review.suggestions.map((s, i) =>
{s}
)}
)}
{review ? (
<>
>
) : (
)}
);
}
const HARDER_COUNTERS = [
{ lbl: 'Reverse causation', text: 'You assume the program drove your gains. The opposite is just as likely early on — newbie gains and better sleep would have driven progress on almost any plan. You may be crediting the split for something else.' },
{ lbl: 'Goalpost shift', text: 'When the scale doesn\'t move you\'ll be tempted to redefine "progress" until it does (now it\'s "recomp," now it\'s "strength"). Pick your primary metric up front or you can\'t honestly say the plan worked.' },
{ lbl: 'Wrong lever', text: 'You\'re tweaking exercise selection when the real bottleneck is total volume and calories. You\'re optimizing the variable that matters least.' },
{ lbl: 'Copied context', text: 'This program was built for a lifter training 5 days a week with years of experience. On your schedule and recovery it may be testing a plan that doesn\'t apply to you.' },
];
export function DevilsModal({ data, onClose }) {
const [counters, setCounters] = useState(data.counters);
const [pushing, setPushing] = useState(false);
const pushHarder = () => {
setPushing(true);
setTimeout(() => {
const next = HARDER_COUNTERS.find(c => !counters.find(x => x.lbl === c.lbl));
if (next) {
const nc = [...counters, next];
setCounters(nc);
data.onUpdate({ counters: nc });
fireToast('Stronger counter added: "' + next.lbl + '"', 'critic');
} else {
fireToast('No more counters — your plan is more robust than I thought.');
}
setPushing(false);
}, 800);
};
return (
e.stopPropagation()}>
Devil's Advocate
The strongest counter-arguments to your plan, ranked by how much they should worry you.
Your claim
"{data.claim}"
{counters.map((c, i) => (
{c.lbl}
{c.text}
))}
);
}
export function ScopeModal({ data, onClose }) {
const s = data.state;
return (
e.stopPropagation()}>
Scope Realism Check
A brutal feasibility verdict on "{s.target}" given your current pace.