See how the same persuasion techniques can argue opposite positions.
import React, { useState } from "react";
import { createRoot } from "react-dom/client";
type Segment = { text: string; deviceId?: string };
type Device = { id: string; name: string; color: string; description: string };
type Panel = { label: string; author: string; segments: Segment[] };
interface MirrorData {
title: string;
original: Panel;
mirrored: Panel;
devices: Device[];
}
interface TooltipState {
visible: boolean;
x: number;
y: number;
device: Device | null;
}
// Helper for active/dimmed class names
const stateClasses = (id: string, activeId: string | null) =>
activeId === id ? 'active' : activeId ? 'dimmed' : '';
function Header() {
return (
<>
<h1>Rhetorical Mirror</h1>
<p className="app-summary">
See how the same persuasion techniques can argue opposite positions.
</p>
</>
);
}
function Tooltip({ state }: { state: TooltipState }) {
if (!state.visible || !state.device) return null;
const { color } = state.device;
return (
<div
className="device-tooltip"
style={{ left: state.x, top: state.y, borderColor: color, '--tooltip-color': color } as React.CSSProperties}
>
<div className="tooltip-header">
<span className="tooltip-dot" style={{ backgroundColor: color }} />
<span className="tooltip-name">{state.device.name}</span>
</div>
<div className="tooltip-desc">{state.device.description}</div>
</div>
);
}
function HighlightedText({
segments,
devices,
activeDevice,
onHover,
onTooltip
}: {
segments: Segment[];
devices: Device[];
activeDevice: string | null;
onHover: (id: string | null) => void;
onTooltip: (e: React.MouseEvent, device: Device | null) => void;
}) {
const deviceMap = new Map(devices.map(d => [d.id, d]));
return (
<div className="highlighted-text">
{segments.map((seg, i) => {
const device = seg.deviceId ? deviceMap.get(seg.deviceId) : null;
if (!device) return <span key={i}>{seg.text}</span>;
const { color } = device;
return (
<span
key={i}
className={`highlight ${stateClasses(seg.deviceId!, activeDevice)}`}
style={{
backgroundColor: `${color}30`,
borderBottom: `3px solid ${color}`,
'--highlight-color': color
} as React.CSSProperties}
onMouseEnter={(e) => { onHover(seg.deviceId!); onTooltip(e, device); }}
onMouseMove={(e) => onTooltip(e, device)}
onMouseLeave={(e) => { onHover(null); onTooltip(e, null); }}
>
{seg.text}
</span>
);
})}
</div>
);
}
function TextPanel({
panel, devices, activeDevice, onHover, onTooltip
}: {
panel: Panel;
devices: Device[];
activeDevice: string | null;
onHover: (id: string | null) => void;
onTooltip: (e: React.MouseEvent, device: Device | null) => void;
}) {
return (
<div className="text-panel">
<div className="panel-header">
<span className="panel-label">{panel.label}</span>
<span className="panel-author">{panel.author}</span>
</div>
<HighlightedText
segments={panel.segments}
devices={devices}
activeDevice={activeDevice}
onHover={onHover}
onTooltip={onTooltip}
/>
</div>
);
}
function DeviceLegend({ devices, activeDevice, onHover }: {
devices: Device[];
activeDevice: string | null;
onHover: (id: string | null) => void;
}) {
return (
<div className="device-legend">
{devices.map(({ id, color, name, description }) => (
<div
key={id}
className={`device-tag ${stateClasses(id, activeDevice)}`}
style={{ borderColor: color, backgroundColor: activeDevice === id ? `${color}20` : undefined }}
onMouseEnter={() => onHover(id)}
onMouseLeave={() => onHover(null)}
>
<span className="device-dot" style={{ backgroundColor: color }} />
<span className="device-name">{name}</span>
<span className="device-desc">{description}</span>
</div>
))}
</div>
);
}
function App() {
const [data, setData] = useState<MirrorData | undefined>(tb.insight<MirrorData>());
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [activeDevice, setActiveDevice] = useState<string | null>(null);
const [tooltip, setTooltip] = useState<TooltipState>({ visible: false, x: 0, y: 0, device: null });
const handleAnalyze = async () => {
setIsAnalyzing(true);
setError(null);
try {
const result = await tb.infer<MirrorData>();
if (result) setData(result);
} catch (err: any) {
setError(err.message || 'Analysis failed');
} finally {
setIsAnalyzing(false);
}
};
const handleTooltip = (e: React.MouseEvent, device: Device | null) => {
if (!device) return setTooltip(prev => ({ ...prev, visible: false }));
const { clientX, clientY } = e;
const vw = window.visualViewport?.width ?? window.innerWidth;
const vh = window.visualViewport?.height ?? window.innerHeight;
const [w, h, pad, margin] = [280, 80, 12, 8];
let x = clientX + pad, y = clientY + pad;
if (x + w > vw - margin) x = clientX - w - pad;
if (y + h > vh - margin) y = clientY - h - pad;
setTooltip({ visible: true, x: Math.max(margin, Math.min(x, vw - w - margin)), y: Math.max(margin, Math.min(y, vh - h - margin)), device });
};
const sharedProps = { devices: data?.devices || [], activeDevice, onHover: setActiveDevice, onTooltip: handleTooltip };
if (!data) {
return (
<div className="container" style={{ textAlign: 'center', paddingTop: '10vh' }}>
<div className="mirror-icon" style={{ fontSize: '4rem', marginBottom: '1rem' }}>
<span className="emoji">🪞</span>
</div>
<Header />
{isAnalyzing && <p className="subtitle">Mapping persuasive techniques and generating the counter-mirror.</p>}
{error && <p className="error-message">{error}</p>}
{!isAnalyzing && (
<button className="analyze-btn" onClick={handleAnalyze}>
<span className="emoji">✨</span> Analyze with AI
</button>
)}
</div>
);
}
return (
<div className="container">
<Tooltip state={tooltip} />
<header>
<Header />
<h2 className="dynamic-title">{data.title}</h2>
<p className="subtitle">Original Statement — {data.original.author}</p>
</header>
<div className="action-bar">
<button className="analyze-btn" onClick={handleAnalyze} disabled={isAnalyzing}>
{isAnalyzing ? 'Analyzing...' : '🔄 Analyze New Quote'}
</button>
{error && <p className="error-message">{error}</p>}
</div>
<section className="mirror-section">
<div className="mirror-grid">
<TextPanel panel={data.original} {...sharedProps} />
<div className="mirror-divider">
<span className="mirror-icon"><span className="emoji">🪞</span></span>
</div>
<TextPanel panel={data.mirrored} {...sharedProps} />
</div>
</section>
<section className="legend-section">
<h2>🏷️ Rhetorical Devices</h2>
<p className="legend-intro">Hover over any device to highlight it in both texts above</p>
<DeviceLegend devices={data.devices} activeDevice={activeDevice} onHover={setActiveDevice} />
</section>
<section className="intro-section">
<h2>About the Mirror</h2>
<p>
Skilled arguers use tricks: loaded words, implied threats, us-vs-them.
This tool finds them, then shows what they look like arguing for the opposing POV.
</p>
</section>
</div>
);
}
createRoot(document.getElementById("root")!).render(<App />);