Dramatize any conversation: boring parts removed, subtext made overt, and honesty cranked up to 11.
import React, { useState, useRef, useLayoutEffect } from "react";
import { createRoot } from "react-dom/client";
import Markdown from "react-markdown";
import html2canvas from "html2canvas";
interface DialogueLine {
speaker: string;
text: string;
original: string;
subtext: string;
analysis: string;
}
interface DramatizeData {
title: string;
summary: string;
lines: DialogueLine[];
}
function Header() {
return (
<header className="twits-header">
<h1>The Twits</h1>
<p>Dramatize any conversation: boring parts removed, subtext made overt, and honesty, or lack thereof, cranked up to 11.
Any correlation with the truth is unfortunate.
</p>
</header>
);
}
function LineCard({
line,
showOriginal,
onToggle,
onShare,
rowRef,
shareDisabled
}: {
line: DialogueLine;
showOriginal: boolean;
onToggle: (e: React.MouseEvent) => void;
onShare: (e: React.MouseEvent) => void;
rowRef?: React.Ref<HTMLDivElement>;
shareDisabled?: boolean;
}) {
return (
<div className="dialogue-line">
<div className="speaker-row" ref={rowRef}>
<button
className="flip-toggle share-toggle"
onClick={onShare}
title="Share image"
disabled={shareDisabled}
>
<span className="toggle-icon">📸</span>
</button>
<button
className="flip-toggle"
onClick={onToggle}
title={showOriginal ? "Switch to Original" : "Switch to Id"}
>
<span className="toggle-icon">{showOriginal ? '😇' : '😈'}</span>
</button>
<div className="persona-label">
{showOriginal ? line.speaker : `${line.speaker}'s Id`}
</div>
</div>
<div className="dialogue-bubble">
<Markdown>{showOriginal ? line.original : line.text}</Markdown>
</div>
<div className="insight-grid">
<div className="insight-box">
<div className="insight-label">
<span>👀</span> Subtext
</div>
<div className="insight-body">
<Markdown>{line.subtext}</Markdown>
</div>
</div>
<div className="insight-box">
<div className="insight-label">
<span>🛋️</span> Therapist's Note
</div>
<div className="insight-body">
<Markdown>{line.analysis}</Markdown>
</div>
</div>
</div>
</div>
);
}
function ShareCard({ line }: { line: DialogueLine }) {
return (
<div className="share-card">
<div className="share-card-section">
<div className="share-card-header">
<div className="share-card-toggle-preview">😇</div>
<div className="share-card-speaker">{line.speaker}</div>
</div>
<div className="share-card-bubble">
<Markdown>{line.original}</Markdown>
</div>
</div>
<div className="share-card-section">
<div className="share-card-header">
<div className="share-card-toggle-preview">😈</div>
<div className="share-card-speaker">{line.speaker}'s Id</div>
</div>
<div className="share-card-bubble">
<Markdown>{line.text}</Markdown>
</div>
</div>
<div className="share-card-meta">
<div className="share-card-meta-block">
<div className="share-card-meta-label">
<span>👀</span> Subtext
</div>
<div className="share-card-meta-body">
<Markdown>{line.subtext}</Markdown>
</div>
</div>
<div className="share-card-meta-block">
<div className="share-card-meta-label">
<span>🛋️</span> Therapist's Note
</div>
<div className="share-card-meta-body">
<Markdown>{line.analysis}</Markdown>
</div>
</div>
</div>
<div className="share-card-footer">
<div className="share-card-brand">THE TWITS</div>
<div className="share-card-link">typebulb.com/u/antypica/the-twits</div>
<div className="share-card-tagline">
Dramatize any conversation: boring parts removed, subtext made overt, and honesty cranked up to 11. Any correlation with the truth is unfortunate.
</div>
</div>
</div>
);
}
function App() {
const [data, setData] = useState<DramatizeData | undefined>(tb.insight<DramatizeData>());
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [sharing, setSharing] = useState(false);
const [showOriginal, setShowOriginal] = useState(false);
const [pendingScrollIndex, setPendingScrollIndex] = useState<number | null>(null);
const [pendingScrollOffset, setPendingScrollOffset] = useState(0);
const rowRefs = useRef<(HTMLDivElement | null)[]>([]);
const shareCardRefs = useRef<(HTMLDivElement | null)[]>([]);
const handleToggle = (index: number, e: React.MouseEvent) => {
const rect = e.currentTarget.getBoundingClientRect();
setPendingScrollIndex(index);
setPendingScrollOffset(rect.top);
setShowOriginal(o => !o);
};
useLayoutEffect(() => {
if (pendingScrollIndex !== null && rowRefs.current[pendingScrollIndex]) {
const newRect = rowRefs.current[pendingScrollIndex]!.getBoundingClientRect();
window.scrollBy(0, newRect.top - pendingScrollOffset);
setPendingScrollIndex(null);
}
}, [showOriginal, pendingScrollIndex, pendingScrollOffset]);
const handleAnalyze = async () => {
setIsAnalyzing(true);
setError(null);
try {
const result = await tb.infer<DramatizeData>();
if (result) {
setData(result);
}
} catch (err: any) {
setError(err.message || "Twit-ification failed");
} finally {
setIsAnalyzing(false);
}
};
const handleShare = async (card: HTMLDivElement | null) => {
if (!card) return;
setSharing(true);
try {
const canvas = await html2canvas(card, { backgroundColor: '#09090b', scale: 2 });
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 = 'the-twits.png';
a.click();
URL.revokeObjectURL(url);
}
}, 'image/png');
} finally {
setSharing(false);
}
};
if (!data) {
return (
<div className="container empty-state">
<Header />
{isAnalyzing && <p className="status-text">Directing the script... removing the sluff...</p>}
{error && <p className="error-message">{error}</p>}
{!isAnalyzing && (
<button className="analyze-btn" onClick={handleAnalyze}>
🎬 Action
</button>
)}
</div>
);
}
return (
<div className="container">
<Header />
<div className="title-bar">
<h2>{data.title}</h2>
<div className="title-bar-buttons">
<button className="analyze-btn small" onClick={handleAnalyze} disabled={isAnalyzing}>
{isAnalyzing ? "Not Another Drama..." : "🔄 New Drama"}
</button>
</div>
</div>
<div className="share-card-container">
{data.lines.map((line, i) => (
<div key={i} ref={(el) => { shareCardRefs.current[i] = el; }}>
<ShareCard line={line} />
</div>
))}
</div>
<div className="script-lines">
{data.lines.map((line, i) => (
<LineCard
key={i}
line={line}
showOriginal={showOriginal}
onToggle={(e) => handleToggle(i, e)}
onShare={() => handleShare(shareCardRefs.current[i])}
shareDisabled={sharing}
rowRef={(el) => { rowRefs.current[i] = el; }}
/>
))}
</div>
<section className="director-vision">
<h2>Director's Vision</h2>
<p>{data.summary}</p>
</section>
</div>
);
}
createRoot(document.getElementById("root")!).render(<App />);