← back to Norma

components/email-analyzer/VersionTimeline.tsx

666 lines

'use client';

import { useState } from 'react';
import {
  Clock,
  Copy,
  Send,
  Download,
  Check,
  FileText,
  ChevronDown,
  ChevronUp,
  GitCompare,
  Star,
  BookmarkPlus,
  ArrowDownUp,
} from 'lucide-react';
import { useToast } from '@/components/ToastProvider';

/* ─── Types ──────────────────────────────────────────────────────────────── */

export interface VersionEntry {
  id: string;
  versionNumber: number;
  content: string;
  label: string;
  tone: string;
  wordCount: number;
  paragraphCount: number;
  timestamp: Date;
  isFavorite?: boolean;
  score?: number;            // 0-100 quality score when available
  instruction?: string;      // rewrite instruction that produced this version
  readingTimeSec?: number;   // estimated reading time in seconds
}

interface VersionTimelineProps {
  versions: VersionEntry[];
  activeVersionId: string | null;
  onSelectVersion: (version: VersionEntry) => void;
  onCompare: (versionA: VersionEntry, versionB: VersionEntry) => void;
  onExportCompose: (content: string) => void;
  onToggleFavorite?: (versionId: string) => void;
  onAddToReferences?: (version: VersionEntry, scope: 'project' | 'universal') => void;
}

/* ─── Helpers ────────────────────────────────────────────────────────────── */

function timeAgo(date: Date): string {
  const diff = Date.now() - date.getTime();
  const secs = Math.floor(diff / 1000);
  if (secs < 60) return 'just now';
  const mins = Math.floor(secs / 60);
  if (mins < 60) return `${mins}m ago`;
  const hours = Math.floor(mins / 60);
  if (hours < 24) return `${hours}h ago`;
  const days = Math.floor(hours / 24);
  return `${days}d ago`;
}

const TONE_COLORS: Record<string, string> = {
  professional: '#4f46e5',
  friendly: '#0a7c59',
  urgent: '#dc2626',
  empathetic: '#d97706',
  formal: '#6366f1',
  original: '#585e6b',
};

/* ─── Component ──────────────────────────────────────────────────────────── */

export default function VersionTimeline({
  versions,
  activeVersionId,
  onSelectVersion,
  onCompare,
  onExportCompose,
  onToggleFavorite,
  onAddToReferences,
}: VersionTimelineProps) {
  const { addToast } = useToast();
  const [compareMode, setCompareMode] = useState(false);
  const [compareSelections, setCompareSelections] = useState<string[]>([]);
  const [expandedId, setExpandedId] = useState<string | null>(null);
  const [sortByFavorites, setSortByFavorites] = useState(false);
  const favCount = versions.filter((v) => v.isFavorite).length;

  function handleCopy(content: string) {
    const plainText = content.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
    navigator.clipboard.writeText(plainText).then(
      () => addToast('Copied to clipboard', 'success'),
      () => addToast('Failed to copy', 'error'),
    );
  }

  function handleDownloadHtml(content: string, label: string) {
    const blob = new Blob([content], { type: 'text/html' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `${label.replace(/[^a-z0-9]/gi, '_').slice(0, 40)}.html`;
    a.click();
    URL.revokeObjectURL(url);
    addToast('Downloaded HTML', 'success');
  }

  function toggleCompareSelect(id: string) {
    setCompareSelections((prev) => {
      if (prev.includes(id)) return prev.filter((x) => x !== id);
      if (prev.length >= 2) return [prev[1], id];
      return [...prev, id];
    });
  }

  function runCompare() {
    if (compareSelections.length !== 2) return;
    const a = versions.find((v) => v.id === compareSelections[0]);
    const b = versions.find((v) => v.id === compareSelections[1]);
    if (a && b) {
      onCompare(a, b);
      setCompareMode(false);
      setCompareSelections([]);
    }
  }

  // Newest-first by default; when sort-by-favorites is on, stars float to the top
  // while each group internally remains newest-first (stable sort via index).
  const reversedVersions = (() => {
    const base = versions.map((v, i) => ({ v, i })).reverse();
    if (!sortByFavorites) return base.map((x) => x.v);
    return [...base]
      .sort((a, b) => {
        const fa = a.v.isFavorite ? 1 : 0;
        const fb = b.v.isFavorite ? 1 : 0;
        if (fa !== fb) return fb - fa;
        return 0; // stable within-group (reverse already applied above)
      })
      .map((x) => x.v);
  })();

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
      {/* ── Header ──────────────────────────────────────────────────────── */}
      <div
        style={{
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'space-between',
          gap: 8,
        }}
      >
        <div
          style={{
            fontSize: 12,
            fontWeight: 700,
            color: 'var(--color-text-muted)',
            textTransform: 'uppercase',
            letterSpacing: '0.06em',
          }}
        >
          Versions ({versions.length}{favCount > 0 ? ` · ★ ${favCount}` : ''})
        </div>
        {onToggleFavorite && favCount > 0 && (
          <button
            type="button"
            className="btn btn-ghost btn-sm"
            style={{
              fontSize: 11,
              padding: '2px 8px',
              minHeight: 26,
              gap: 4,
              color: sortByFavorites ? '#f59e0b' : 'var(--color-text-muted)',
              borderColor: sortByFavorites ? '#f59e0b' : 'var(--color-border)',
              background: sortByFavorites ? 'rgba(245,158,11,0.08)' : 'transparent',
            }}
            onClick={() => setSortByFavorites((p) => !p)}
            title={sortByFavorites ? 'Clear favorites sort' : 'Sort favorites first'}
          >
            <ArrowDownUp size={11} />
            {sortByFavorites ? 'Favs first' : 'Sort ★'}
          </button>
        )}
        {versions.length >= 2 && (
          <button
            type="button"
            className="btn btn-ghost btn-sm"
            style={{
              fontSize: 11,
              padding: '2px 8px',
              minHeight: 26,
              gap: 4,
            }}
            onClick={() => {
              setCompareMode((p) => !p);
              setCompareSelections([]);
            }}
          >
            <GitCompare size={12} />
            {compareMode ? 'Cancel' : 'Compare'}
          </button>
        )}
      </div>

      {compareMode && (
        <div
          style={{
            display: 'flex',
            alignItems: 'center',
            gap: 8,
            fontSize: 12,
            color: 'var(--color-text)',
            padding: '8px 10px',
            borderRadius: 8,
            backgroundColor: compareSelections.length === 2 ? 'rgba(16,185,129,0.08)' : 'var(--color-surface-el)',
            border: `1px solid ${compareSelections.length === 2 ? 'var(--color-primary)' : 'var(--color-border)'}`,
            transition: 'var(--transition)',
          }}
        >
          <span style={{ flex: 1, fontWeight: compareSelections.length === 2 ? 600 : 400 }}>
            {compareSelections.length === 0 && 'Select 2 versions to compare'}
            {compareSelections.length === 1 && 'Select 1 more version…'}
            {compareSelections.length === 2 && 'Ready — click Compare →'}
          </span>
          <button
            type="button"
            className="btn btn-primary btn-sm"
            style={{
              fontSize: 12,
              padding: '4px 14px',
              minHeight: 28,
              gap: 4,
              opacity: compareSelections.length === 2 ? 1 : 0.4,
              pointerEvents: compareSelections.length === 2 ? 'auto' : 'none',
            }}
            onClick={runCompare}
            disabled={compareSelections.length !== 2}
          >
            <GitCompare size={12} />
            Compare
          </button>
        </div>
      )}

      {/* ── Timeline ────────────────────────────────────────────────────── */}
      {versions.length === 0 && (
        <div
          style={{
            padding: '24px 12px',
            textAlign: 'center',
            color: 'var(--color-text-muted)',
            fontSize: 13,
          }}
        >
          <FileText
            size={24}
            style={{ color: 'var(--color-text-muted)', marginBottom: 6, opacity: 0.5 }}
          />
          <p>No versions saved yet</p>
          <p style={{ fontSize: 12, marginTop: 4 }}>
            Paste an email and start rewriting to build your version history
          </p>
        </div>
      )}

      <div style={{ position: 'relative' }}>
        {/* Connecting line */}
        {reversedVersions.length > 1 && (
          <div
            aria-hidden="true"
            style={{
              position: 'absolute',
              left: 11,
              top: 16,
              bottom: 16,
              width: 2,
              backgroundColor: 'var(--color-border)',
              borderRadius: 1,
            }}
          />
        )}

        <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
          {reversedVersions.map((version) => {
            const isActive = version.id === activeVersionId;
            const isCompareSelected = compareSelections.includes(version.id);
            const isExpanded = expandedId === version.id;
            const toneColor = TONE_COLORS[version.tone] || 'var(--color-text-muted)';

            return (
              <div
                key={version.id}
                style={{
                  display: 'flex',
                  gap: 10,
                  position: 'relative',
                }}
              >
                {/* Timeline dot (or compare-selection badge) */}
                <div
                  style={{
                    width: compareMode ? 28 : 22,
                    height: compareMode ? 28 : 22,
                    borderRadius: '50%',
                    display: 'flex',
                    alignItems: 'center',
                    justifyContent: 'center',
                    flexShrink: 0,
                    marginTop: 10,
                    backgroundColor: isCompareSelected
                      ? '#4f46e5'
                      : isActive
                        ? 'var(--color-primary)'
                        : compareMode
                          ? 'var(--color-surface-el)'
                          : 'var(--color-surface)',
                    border: `2px solid ${isCompareSelected ? '#4f46e5' : isActive ? 'var(--color-primary)' : compareMode ? 'var(--color-lane-b)' : 'var(--color-border)'}`,
                    borderStyle: compareMode && !isCompareSelected ? 'dashed' : 'solid',
                    zIndex: 1,
                    transition: 'all 0.15s',
                  }}
                  title={compareMode ? (isCompareSelected ? 'Selected — click to deselect' : 'Click to select for compare') : ''}
                >
                  {compareMode && isCompareSelected ? (
                    <Check size={14} style={{ color: '#fff' }} />
                  ) : isActive && !compareMode ? (
                    <Check size={10} style={{ color: '#fff' }} />
                  ) : (
                    <span
                      style={{
                        fontSize: compareMode ? 11 : 9,
                        fontWeight: 700,
                        color: compareMode ? 'var(--color-lane-b)' : 'var(--color-text-muted)',
                      }}
                    >
                      {version.versionNumber}
                    </span>
                  )}
                </div>

                {/* Card */}
                <div
                  role="button"
                  tabIndex={0}
                  onClick={() => {
                    if (compareMode) {
                      toggleCompareSelect(version.id);
                    } else {
                      onSelectVersion(version);
                    }
                  }}
                  onKeyDown={(e) => {
                    if (e.key === 'Enter' || e.key === ' ') {
                      e.preventDefault();
                      if (compareMode) {
                        toggleCompareSelect(version.id);
                      } else {
                        onSelectVersion(version);
                      }
                    }
                  }}
                  style={{
                    flex: 1,
                    padding: '8px 10px',
                    borderRadius: 8,
                    cursor: 'pointer',
                    backgroundColor: isCompareSelected
                      ? 'rgba(79,70,229,0.12)'
                      : isActive && !compareMode
                        ? 'var(--color-primary-dim)'
                        : compareMode
                          ? 'var(--color-surface-el)'
                          : 'var(--color-surface)',
                    border: `${isCompareSelected ? 2 : 1}px solid ${isCompareSelected ? '#4f46e5' : isActive && !compareMode ? 'rgba(10,124,89,0.3)' : compareMode ? 'var(--color-lane-b)' : 'var(--color-border)'}`,
                    borderStyle: compareMode && !isCompareSelected ? 'dashed' : 'solid',
                    transition: 'all 0.15s',
                    position: 'relative',
                  }}
                >
                  {/* Top row: label + time */}
                  <div
                    style={{
                      display: 'flex',
                      alignItems: 'center',
                      justifyContent: 'space-between',
                      gap: 6,
                    }}
                  >
                    <span
                      style={{
                        fontSize: 13,
                        fontWeight: 600,
                        color: 'var(--color-text)',
                        overflow: 'hidden',
                        textOverflow: 'ellipsis',
                        whiteSpace: 'nowrap',
                        flex: 1,
                      }}
                    >
                      v{version.versionNumber}: {version.label}
                    </span>
                    {compareMode && isCompareSelected && (
                      <span
                        style={{
                          fontSize: 10,
                          fontWeight: 700,
                          padding: '2px 6px',
                          borderRadius: 999,
                          background: '#4f46e5',
                          color: '#fff',
                          whiteSpace: 'nowrap',
                        }}
                      >
                        #{compareSelections.indexOf(version.id) + 1}
                      </span>
                    )}
                    <span
                      style={{
                        fontSize: 11,
                        color: 'var(--color-text-muted)',
                        flexShrink: 0,
                        display: 'flex',
                        alignItems: 'center',
                        gap: 3,
                      }}
                    >
                      <Clock size={10} />
                      {timeAgo(version.timestamp)}
                    </span>
                  </div>

                  {/* Bottom row: tone badge + stats */}
                  <div
                    style={{
                      display: 'flex',
                      alignItems: 'center',
                      gap: 6,
                      marginTop: 4,
                    }}
                  >
                    <span
                      className="badge"
                      style={{
                        fontSize: 10,
                        padding: '1px 6px',
                        backgroundColor: `${toneColor}12`,
                        color: toneColor,
                        border: `1px solid ${toneColor}30`,
                        textTransform: 'capitalize',
                      }}
                    >
                      {version.tone}
                    </span>
                    <span
                      style={{
                        fontSize: 11,
                        color: 'var(--color-text-muted)',
                      }}
                    >
                      {version.wordCount}w
                    </span>
                    <span
                      style={{
                        fontSize: 11,
                        color: 'var(--color-text-muted)',
                      }}
                    >
                      {version.paragraphCount}p
                    </span>
                    {typeof version.readingTimeSec === 'number' && version.readingTimeSec > 0 && (
                      <span
                        style={{ fontSize: 11, color: 'var(--color-text-muted)' }}
                        title={`~${version.readingTimeSec}s read`}
                      >
                        {version.readingTimeSec < 60
                          ? `${version.readingTimeSec}s`
                          : `${Math.round(version.readingTimeSec / 60)}m`}
                      </span>
                    )}
                    {typeof version.score === 'number' && (
                      <span
                        className="badge"
                        style={{
                          fontSize: 10,
                          padding: '1px 6px',
                          backgroundColor:
                            version.score >= 80
                              ? 'rgba(16,185,129,0.12)'
                              : version.score >= 60
                                ? 'rgba(245,158,11,0.12)'
                                : 'rgba(244,63,94,0.12)',
                          color:
                            version.score >= 80
                              ? '#10b981'
                              : version.score >= 60
                                ? '#f59e0b'
                                : '#f43f5e',
                          border: '1px solid transparent',
                        }}
                        title="Quality score"
                      >
                        {version.score}
                      </span>
                    )}

                    {/* Favorite toggle (only when handler provided — orig-* rows skip it) */}
                    {onToggleFavorite && (
                      <button
                        type="button"
                        onClick={(e) => {
                          e.stopPropagation();
                          onToggleFavorite(version.id);
                        }}
                        style={{
                          marginLeft: 'auto',
                          background: 'none',
                          border: 'none',
                          cursor: 'pointer',
                          color: version.isFavorite ? '#f59e0b' : 'var(--color-text-muted)',
                          padding: 2,
                          display: 'flex',
                        }}
                        title={version.isFavorite ? 'Remove from favorites' : 'Mark as favorite'}
                        aria-pressed={!!version.isFavorite}
                      >
                        <Star
                          size={13}
                          fill={version.isFavorite ? '#f59e0b' : 'none'}
                          strokeWidth={version.isFavorite ? 2 : 1.8}
                        />
                      </button>
                    )}

                    {/* Expand/collapse */}
                    <button
                      type="button"
                      onClick={(e) => {
                        e.stopPropagation();
                        setExpandedId(isExpanded ? null : version.id);
                      }}
                      style={{
                        marginLeft: onToggleFavorite ? 0 : 'auto',
                        background: 'none',
                        border: 'none',
                        cursor: 'pointer',
                        color: 'var(--color-text-muted)',
                        padding: 2,
                        display: 'flex',
                      }}
                      title={isExpanded ? 'Collapse' : 'Expand actions'}
                    >
                      {isExpanded ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
                    </button>
                  </div>

                  {/* Expanded actions */}
                  {isExpanded && (
                    <div
                      style={{
                        marginTop: 8,
                        paddingTop: 8,
                        borderTop: '1px solid var(--color-border)',
                        display: 'flex',
                        flexDirection: 'column',
                        gap: 6,
                      }}
                    >
                      {version.instruction && (
                        <div
                          style={{
                            fontSize: 11,
                            color: 'var(--color-text-muted)',
                            fontStyle: 'italic',
                            lineHeight: 1.3,
                          }}
                          title="Rewrite instruction"
                        >
                          “{version.instruction}”
                        </div>
                      )}
                      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
                        <button
                          type="button"
                          className="btn btn-ghost btn-sm"
                          style={{ fontSize: 11, padding: '2px 8px', minHeight: 26, gap: 3 }}
                          onClick={(e) => {
                            e.stopPropagation();
                            handleCopy(version.content);
                          }}
                        >
                          <Copy size={11} /> Copy
                        </button>
                        <button
                          type="button"
                          className="btn btn-ghost btn-sm"
                          style={{ fontSize: 11, padding: '2px 8px', minHeight: 26, gap: 3 }}
                          onClick={(e) => {
                            e.stopPropagation();
                            onExportCompose(version.content);
                          }}
                        >
                          <Send size={11} /> Compose
                        </button>
                        <button
                          type="button"
                          className="btn btn-ghost btn-sm"
                          style={{ fontSize: 11, padding: '2px 8px', minHeight: 26, gap: 3 }}
                          onClick={(e) => {
                            e.stopPropagation();
                            handleDownloadHtml(version.content, version.label);
                          }}
                        >
                          <Download size={11} /> HTML
                        </button>
                        {onAddToReferences && (
                          <>
                            <button
                              type="button"
                              className="btn btn-ghost btn-sm"
                              style={{
                                fontSize: 11,
                                padding: '2px 8px',
                                minHeight: 26,
                                gap: 3,
                                color: 'var(--color-primary)',
                              }}
                              onClick={(e) => {
                                e.stopPropagation();
                                onAddToReferences(version, 'project');
                              }}
                              title="Save as reference for THIS analysis only"
                            >
                              <BookmarkPlus size={11} /> Ref: project
                            </button>
                            <button
                              type="button"
                              className="btn btn-ghost btn-sm"
                              style={{
                                fontSize: 11,
                                padding: '2px 8px',
                                minHeight: 26,
                                gap: 3,
                                color: 'var(--color-lane-b)',
                              }}
                              onClick={(e) => {
                                e.stopPropagation();
                                onAddToReferences(version, 'universal');
                              }}
                              title="Save to Universal References (available on every analysis)"
                            >
                              <BookmarkPlus size={11} /> Ref: universal
                            </button>
                          </>
                        )}
                      </div>
                    </div>
                  )}
                </div>
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}