Let AI judge who won the debate.
import React, { useState, useRef } from "react";
import { createRoot } from "react-dom/client";
import html2canvas from "html2canvas";
interface Technique {
name: string;
type: "fallacy" | "tactic" | "rhetoric";
description: string;
snippet: string;
}
interface Turn {
speaker: string;
text: string;
techniques: Technique[];
}
interface ParticipantMetrics {
logic_facts: number;
sway: number;
class: number;
good_faith: number;
novelty: number;
summary: string;
}
interface TooltipContent {
name: string;
type?: string;
description: string;
color: string;
}
interface TooltipState {
visible: boolean;
x: number;
y: number;
content: TooltipContent | null;
}
interface Analysis {
summary: string;
turns: Turn[];
participants: Record<string, ParticipantMetrics>;
headline: string;
}
type MetricKey = Exclude<keyof ParticipantMetrics, 'summary'>;
const TECHNIQUE_COLORS: Record<Technique['type'], string> = {
fallacy: '#ef4444',
tactic: '#10b981',
rhetoric: '#8b5cf6'
};
const METRICS: { key: MetricKey; label: string; shortLabel: string; description: string }[] = [
{ key: 'logic_facts', label: 'Logic & Facts', shortLabel: 'Logic', description: 'Is the reasoning valid & facts correct?' },
{ key: 'sway', label: 'Sway', shortLabel: 'Sway', description: 'Is it persuasive?' },
{ key: 'class', label: 'Class', shortLabel: 'Class', description: 'Is the conduct clean?' },
{ key: 'good_faith', label: 'Good Faith', shortLabel: 'Good Faith', description: 'Is the engagement sincere?' },
{ key: 'novelty', label: 'Novelty', shortLabel: 'Novelty', description: 'Is it fresh?' },
];
const SPEAKER_COLORS = ["#2563eb", "#db2777", "#059669", "#d97706", "#7c3aed"];
const TAGLINE = "Let AI judge who won the debate";
const CameraIcon = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/>
</svg>
);
function Tooltip({ state }: { state: TooltipState }) {
if (!state.visible || !state.content) return null;
const { name, type, description, color } = state.content;
return (
<div className="technique-tooltip" style={{ left: state.x, top: state.y, borderColor: color }}>
<div className="tooltip-header">
<span className="tooltip-dot" style={{ backgroundColor: color }} />
<span className="tooltip-name">{name}</span>
{type && <span className="tooltip-type" style={{ color }}>{type}</span>}
</div>
<div className="tooltip-desc">{description}</div>
</div>
);
}
function VerdictText({ verdict, speakerColors, tabloidMode }: {
verdict: { winner: string; verb: string; loser: string };
speakerColors: Record<string, string>;
tabloidMode: boolean;
}) {
const displayVerb = tabloidMode ? verdict.verb : "debates";
return <>
<span style={{ color: speakerColors[verdict.winner] }}>{verdict.winner}</span>
{" "}{displayVerb}{" "}
<span style={{ color: speakerColors[verdict.loser], opacity: 0.7 }}>{verdict.loser}</span>
</>;
}
function tooltipHandlers(
onTooltip: (e: React.MouseEvent, content: TooltipContent | null) => void,
content: TooltipContent
) {
return {
onMouseEnter: (e: React.MouseEvent) => onTooltip(e, content),
onMouseMove: (e: React.MouseEvent) => onTooltip(e, content),
onMouseLeave: (e: React.MouseEvent) => onTooltip(e, null),
};
}
function MetricBar({ label, value, color, description, onTooltip }: {
label: string;
value: number;
color?: string;
description: string;
onTooltip: (e: React.MouseEvent, content: TooltipContent | null) => void;
}) {
const tooltipContent: TooltipContent = { name: label, description, color: color || '#666' };
return (
<div className="metric-container">
<div className="metric-header">
<span className="metric-label-group">
{label}
<span
className="info-icon"
{...tooltipHandlers(onTooltip, tooltipContent)}
>?</span>
</span>
<span>{value}%</span>
</div>
<div className="metric-track" style={{ '--p-color': color } as React.CSSProperties}>
<div className="metric-fill" style={{ width: `${value}%`, backgroundColor: color }} />
</div>
</div>
);
}
function HighlightedText({ text, techniques, onTooltip }: {
text: string;
techniques: Technique[];
onTooltip: (e: React.MouseEvent, content: TooltipContent | null) => void;
}) {
if (!techniques || techniques.length === 0) return <span>{text}</span>;
const matches: { start: number; end: number; tech: Technique }[] = [];
techniques.forEach(tech => {
if (!tech.snippet) return;
let startPos = 0;
while ((startPos = text.indexOf(tech.snippet, startPos)) !== -1) {
matches.push({ start: startPos, end: startPos + tech.snippet.length, tech });
startPos += tech.snippet.length;
}
});
matches.sort((a, b) => a.start - b.start || (b.end - a.end));
const result: React.ReactNode[] = [];
let lastIndex = 0;
for (const match of matches) {
if (match.start < lastIndex) continue;
if (match.start > lastIndex) result.push(text.substring(lastIndex, match.start));
const color = TECHNIQUE_COLORS[match.tech.type];
const tooltipContent: TooltipContent = {
name: match.tech.name, type: match.tech.type, description: match.tech.description, color
};
result.push(
<span
key={`${match.tech.name}-${match.start}`}
className={`highlight ${match.tech.type}`}
style={{ '--highlight-color': color } as React.CSSProperties}
{...tooltipHandlers(onTooltip, tooltipContent)}
>
{text.substring(match.start, match.end)}
</span>
);
lastIndex = match.end;
}
if (lastIndex < text.length) result.push(text.substring(lastIndex));
return <>{result}</>;
}
function ShareCard({ analysis, verdict, speakerColors, tabloidMode }: {
analysis: Analysis;
verdict: { winner: string; verb: string; loser: string };
speakerColors: Record<string, string>;
tabloidMode: boolean;
}) {
const displayTitle = tabloidMode ? "DESTROYED!" : "DEBATE ANALYZER";
const participants = Object.entries(analysis.participants);
const hasTwo = participants.length >= 2;
return (
<div className="share-card">
<div className={`share-header fire-title ${tabloidMode ? 'glow' : ''}`}>{displayTitle}</div>
<div className="share-verdict"><VerdictText verdict={verdict} speakerColors={speakerColors} tabloidMode={tabloidMode} /></div>
{tabloidMode && <div className="share-headline sensational"><em>{analysis.headline}</em></div>}
{analysis.summary && <div className="share-summary">{analysis.summary}</div>}
{hasTwo ? (
<div className="share-scores-dual">
<div className="share-legend">
<span className="share-legend-name" style={{ color: speakerColors[participants[0][0]] }}>{participants[0][0]}</span>
<span className="share-legend-spacer" />
<span className="share-legend-name" style={{ color: speakerColors[participants[1][0]] }}>{participants[1][0]}</span>
</div>
{METRICS.map(({ key, shortLabel }) => {
const leftVal = participants[0][1][key] as number;
const rightVal = participants[1][1][key] as number;
return (
<div className="share-dual-row" key={key}>
<span className="share-dual-value">{leftVal}</span>
<div className="share-dual-bar share-dual-bar-left">
<div className="share-dual-fill" style={{ width: `${leftVal}%`, backgroundColor: speakerColors[participants[0][0]] }} />
</div>
<span className="share-dual-label">{shortLabel}</span>
<div className="share-dual-bar share-dual-bar-right">
<div className="share-dual-fill" style={{ width: `${rightVal}%`, backgroundColor: speakerColors[participants[1][0]] }} />
</div>
<span className="share-dual-value">{rightVal}</span>
</div>
);
})}
</div>
) : (
<div className="share-scores">
{participants.map(([name, data]) => (
<div key={name} className="share-participant">
<div className="share-name" style={{ color: speakerColors[name] }}>{name}</div>
<div className="share-bars">
{METRICS.map(({ key, shortLabel }) => (
<div key={key} className="share-bar-row">
<span>{shortLabel}</span>
<div className="share-bar-track">
<div className="share-bar-fill" style={{ width: `${data[key]}%`, backgroundColor: speakerColors[name] }} />
</div>
<span>{data[key]}</span>
</div>
))}
</div>
</div>
))}
</div>
)}
<div className="share-footer">
Let AI judge who won the debate at <span className="share-link">typebulb.com/u/samples/destroyed</span>
</div>
</div>
);
}
function App() {
const [analysis, setAnalysis] = useState<Analysis | null>(tb.insight<Analysis>());
const [loading, setLoading] = useState(false);
const [sharing, setSharing] = useState(false);
const shareCardRef = useRef<HTMLDivElement>(null);
const [tooltip, setTooltip] = useState<TooltipState>({ visible: false, x: 0, y: 0, content: null });
const [tabloidMode, setTabloidMode] = React.useState(() => {
return new URLSearchParams(window.location.search).get('analytical') !== 'true';
});
React.useEffect(() => {
const url = new URL(window.location.href);
if (!tabloidMode) {
url.searchParams.set('analytical', 'true');
} else {
url.searchParams.delete('analytical');
}
window.history.replaceState({}, '', url);
}, [tabloidMode]);
const handleTooltip = (e: React.MouseEvent, content: TooltipContent | null) => {
if (content) {
const tooltipWidth = 300, tooltipHeight = 90, padding = 12, edgeMargin = 8;
const vw = window.visualViewport?.width ?? window.innerWidth;
const vh = window.visualViewport?.height ?? window.innerHeight;
const offsetLeft = window.visualViewport?.offsetLeft ?? 0;
const offsetTop = window.visualViewport?.offsetTop ?? 0;
let x = e.clientX + padding, y = e.clientY + padding;
if (x + tooltipWidth > vw + offsetLeft - edgeMargin) x = e.clientX - tooltipWidth - padding;
if (y + tooltipHeight > vh + offsetTop - edgeMargin) y = e.clientY - tooltipHeight - padding;
x = Math.max(edgeMargin + offsetLeft, Math.min(x, vw + offsetLeft - tooltipWidth - edgeMargin));
y = Math.max(edgeMargin + offsetTop, Math.min(y, vh + offsetTop - tooltipHeight - edgeMargin));
setTooltip({ visible: true, x, y, content });
} else {
setTooltip(prev => ({ ...prev, visible: false }));
}
};
const runAnalysis = async () => {
setLoading(true);
try {
const result = await tb.infer<Analysis>();
if (result) setAnalysis(result);
} finally {
setLoading(false);
}
};
const handleShare = async () => {
if (!shareCardRef.current) return;
setSharing(true);
try {
const canvas = await html2canvas(shareCardRef.current, { backgroundColor: '#121212', scale: 3 });
canvas.toBlob(async (blob) => {
if (!blob) return;
try {
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
alert('Image copied to clipboard!');
} catch {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'destroyed-verdict.png';
a.click();
URL.revokeObjectURL(url);
}
}, 'image/png');
} finally {
setSharing(false);
}
};
const getVerdict = (data: Analysis) => {
const scores = Object.entries(data.participants).map(([name, m]) => ({
name,
score: (m.logic_facts * 1.5) + (m.sway * 1.2) + m.good_faith + (m.class * 0.8) + m.novelty
})).sort((a, b) => b.score - a.score);
if (scores.length < 1) return null;
const [winner, runnerUp] = scores;
if (!runnerUp) return { winner: winner.name, verb: "wins", loser: "" };
const diff = winner.score - runnerUp.score;
const verb = diff < 10 ? "edges out" : diff > 25 ? "destroys" : "beats";
return { winner: winner.name, verb, loser: runnerUp.name };
};
const analysisButtonText = loading ? "Let's settle this..." : "Settle Another Debate";
const displayTitle = tabloidMode ? "DESTROYED!" : "DEBATE ANALYZER";
if (!analysis && !loading) {
return (
<div className="setup-screen">
<h1 className={`hero-title fire-title ${tabloidMode ? 'glow' : ''}`}>{displayTitle}</h1>
<p className="hero-subtitle">{TAGLINE}</p>
<button onClick={runAnalysis} disabled={loading} className="primary-btn">{analysisButtonText}</button>
</div>
);
}
const speakers = analysis ? Object.keys(analysis.participants) : [];
const speakerColors = Object.fromEntries(speakers.map((name, i) => [name, SPEAKER_COLORS[i % SPEAKER_COLORS.length]]));
const verdict = analysis ? getVerdict(analysis) : null;
const winnerName = verdict?.winner;
return (
<div className="container">
<Tooltip state={tooltip} />
<header className="main-header">
<h1 className={`fire-title ${tabloidMode ? 'glow' : ''}`}>{displayTitle}</h1>
<p className="header-tagline">{TAGLINE}</p>
</header>
{analysis && verdict && winnerName && (
<>
<section className="winner-announcement" style={{ borderColor: speakerColors[winnerName] }}>
<h2 className="winner-name"><VerdictText verdict={verdict} speakerColors={speakerColors} tabloidMode={tabloidMode} /></h2>
{tabloidMode && analysis && <p className="winner-headline sensational"><em>{analysis.headline}</em></p>}
{analysis.summary && <p className="winner-summary">{analysis.summary}</p>}
<div className="action-buttons">
<button onClick={handleShare} title="Share As Image" disabled={sharing} className="share-btn">
{sharing ? "..." : <CameraIcon />}
</button>
<button onClick={runAnalysis} disabled={loading} className="secondary-btn">
{analysisButtonText}
</button>
<button
onClick={() => setTabloidMode(!tabloidMode)}
className={`tabloid-btn ${tabloidMode ? 'active' : ''}`}
>
{tabloidMode ? "Tabloid Mode" : "Analytical Mode"}
</button>
</div>
</section>
<div className="share-card-container" ref={shareCardRef}>
<ShareCard analysis={analysis} verdict={verdict} speakerColors={speakerColors} tabloidMode={tabloidMode} />
</div>
</>
)}
<div className="layout-grid">
<section className="conversation-section">
<h2>Dialogue Breakdown</h2>
<div className="conversation-list">
{analysis?.turns.map((turn, i) => {
const color = speakerColors[turn.speaker];
const isSecond = speakers.indexOf(turn.speaker) % 2 === 1;
return (
<div key={i} className={`turn-card ${isSecond ? 'turn-alt' : ''}`} style={{ '--p-color': color } as React.CSSProperties}>
<div className="turn-speaker" style={{ color }}>{turn.speaker}</div>
<div className="turn-text">
<HighlightedText text={turn.text} techniques={turn.techniques} onTooltip={handleTooltip} />
</div>
</div>
);
})}
</div>
</section>
<section className="metrics-section">
<h2>Performance Stats</h2>
<div className="metrics-grid">
{analysis && Object.entries(analysis.participants).map(([name, data]) => {
const color = speakerColors[name];
return (
<div key={name} className="participant-card" style={{ '--p-color': color } as React.CSSProperties}>
<div className="participant-header">
<h3 style={{ color }}>{name}</h3>
{tabloidMode && name === winnerName && <span className="winner-badge">Winner</span>}
</div>
{METRICS.map(({ key, label, description }) => (
<MetricBar key={key} label={label} value={data[key]} color={color} description={description} onTooltip={handleTooltip} />
))}
<p className="participant-summary">{data.summary}</p>
</div>
);
})}
</div>
</section>
</div>
</div>
);
}
const container = document.getElementById("root");
const root = createRoot(container!);
root.render(<App />);