The Twits

Dramatize any conversation: boring parts removed, subtext made overt, and honesty cranked up to 11.

code.tsx

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

Markdown source · More bulbs by samples · Typebulb home