Destroyed!

Let AI judge who won the debate.

code.tsx

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 />);

Markdown source · More bulbs by samples · Typebulb home