← back to Norma

components/email-analyzer/SavedAnalyses.tsx

261 lines

'use client';

import { useEffect, useState } from 'react';
import { FolderClock, Sparkles, FileText, Trash2 } from 'lucide-react';
import { useToast } from '@/components/ToastProvider';

interface SavedSession {
  id: number;
  session_id: string;
  version_number: number;
  label: string | null;
  tone: string | null;
  word_count: number | null;
  paragraph_count: number | null;
  created_by: string | null;
  created_at: string;
  original_preview: string | null;
}

interface SavedAnalysesProps {
  onOpen: (sessionId: string) => void;
}

function formatRelative(iso: string): string {
  const d = new Date(iso);
  const diffMs = Date.now() - d.getTime();
  const mins = Math.floor(diffMs / 60000);
  if (mins < 1) return 'just now';
  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);
  if (days < 7) return `${days}d ago`;
  return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
}

function deriveTopic(s: SavedSession): string {
  if (s.label && s.label.trim()) return s.label.trim();
  if (s.original_preview) {
    const clean = s.original_preview.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
    return clean.slice(0, 80) || 'Untitled analysis';
  }
  return 'Untitled analysis';
}

export default function SavedAnalyses({ onOpen }: SavedAnalysesProps) {
  const { addToast } = useToast();
  const [sessions, setSessions] = useState<SavedSession[]>([]);
  const [loading, setLoading] = useState(true);
  const [query, setQuery] = useState('');

  useEffect(() => {
    void fetchSessions();
  }, []);

  async function fetchSessions() {
    setLoading(true);
    try {
      const res = await fetch('/api/email-analyzer/drafts?limit=100', { credentials: 'include' });
      if (!res.ok) throw new Error('Failed to load saved analyses');
      const data = await res.json();
      setSessions(Array.isArray(data.sessions) ? data.sessions : []);
    } catch (err) {
      addToast((err as Error).message || 'Failed to load', 'error');
    } finally {
      setLoading(false);
    }
  }

  async function handleDelete(sessionId: string, evt: React.MouseEvent): Promise<void> {
    evt.stopPropagation();
    if (!confirm('Delete all versions of this analysis? This cannot be undone.')) return;
    try {
      const res = await fetch(
        `/api/email-analyzer/drafts?session_id=${encodeURIComponent(sessionId)}`,
        { method: 'DELETE', credentials: 'include' },
      );
      if (!res.ok) throw new Error('Delete failed');
      setSessions((prev) => prev.filter((s) => s.session_id !== sessionId));
      addToast('Analysis deleted', 'success');
    } catch (err) {
      addToast((err as Error).message || 'Delete failed', 'error');
    }
  }

  const filtered = query.trim()
    ? sessions.filter((s) => {
        const hay = `${deriveTopic(s)} ${s.tone || ''} ${s.created_by || ''}`.toLowerCase();
        return hay.includes(query.trim().toLowerCase());
      })
    : sessions;

  return (
    <div className="p-4 sm:p-6 max-w-5xl mx-auto">
      <div className="flex items-center gap-2.5 mb-5">
        <div
          style={{
            width: 36,
            height: 36,
            borderRadius: 'var(--radius-md)',
            background: 'linear-gradient(135deg, rgba(79,70,229,0.15), rgba(99,102,241,0.15))',
            border: '1px solid rgba(79,70,229,0.25)',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            flexShrink: 0,
          }}
          aria-hidden="true"
        >
          <FolderClock size={18} style={{ color: '#6366f1' }} />
        </div>
        <div style={{ flex: 1 }}>
          <h1 className="text-lg font-semibold" style={{ color: 'var(--color-text)' }}>
            Saved Analyses
          </h1>
          <p className="text-sm" style={{ color: 'var(--color-text-muted)' }}>
            Pick a saved Email Analyzer session to reopen
          </p>
        </div>
        <input
          type="search"
          placeholder="Search topic, tone…"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          className="input"
          style={{ maxWidth: 240 }}
        />
      </div>

      {loading && (
        <div className="card" style={{ padding: 24, textAlign: 'center' }}>
          <span className="spinner" aria-hidden="true" />
          <span style={{ marginLeft: 10, color: 'var(--color-text-muted)' }}>Loading…</span>
        </div>
      )}

      {!loading && filtered.length === 0 && (
        <div
          className="card"
          style={{
            padding: 40,
            textAlign: 'center',
            color: 'var(--color-text-muted)',
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            gap: 12,
          }}
        >
          <FileText size={28} style={{ color: 'var(--color-text-muted)' }} />
          <div>
            <div style={{ fontWeight: 600, color: 'var(--color-text)' }}>
              {query ? 'No matches' : 'No saved analyses yet'}
            </div>
            <div style={{ fontSize: 13, marginTop: 4 }}>
              {query
                ? 'Try a different search term.'
                : 'Open Email Analyzer, click Save on a version, and it will appear here.'}
            </div>
          </div>
        </div>
      )}

      {!loading && filtered.length > 0 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
          {filtered.map((s) => (
            <button
              key={s.session_id}
              type="button"
              onClick={() => onOpen(s.session_id)}
              className="card"
              style={{
                display: 'flex',
                alignItems: 'center',
                gap: 14,
                padding: '14px 16px',
                cursor: 'pointer',
                textAlign: 'left',
                transition: 'border-color var(--transition), background-color var(--transition)',
              }}
              onMouseEnter={(e) => {
                e.currentTarget.style.borderColor = '#6366f1';
                e.currentTarget.style.backgroundColor = 'var(--color-surface-el)';
              }}
              onMouseLeave={(e) => {
                e.currentTarget.style.borderColor = 'var(--color-border)';
                e.currentTarget.style.backgroundColor = 'var(--color-surface)';
              }}
            >
              <div
                style={{
                  width: 36,
                  height: 36,
                  borderRadius: 'var(--radius-md)',
                  background: 'rgba(99,102,241,0.12)',
                  border: '1px solid rgba(99,102,241,0.25)',
                  display: 'flex',
                  alignItems: 'center',
                  justifyContent: 'center',
                  flexShrink: 0,
                }}
              >
                <Sparkles size={16} style={{ color: '#6366f1' }} />
              </div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div
                  style={{
                    fontSize: 14,
                    fontWeight: 600,
                    color: 'var(--color-text)',
                    overflow: 'hidden',
                    textOverflow: 'ellipsis',
                    whiteSpace: 'nowrap',
                  }}
                >
                  {deriveTopic(s)}
                </div>
                <div
                  style={{
                    fontSize: 12,
                    color: 'var(--color-text-muted)',
                    marginTop: 3,
                    display: 'flex',
                    gap: 10,
                    flexWrap: 'wrap',
                  }}
                >
                  <span>v{s.version_number}</span>
                  <span>·</span>
                  <span>{formatRelative(s.created_at)}</span>
                  {s.tone && (
                    <>
                      <span>·</span>
                      <span style={{ textTransform: 'capitalize' }}>{s.tone}</span>
                    </>
                  )}
                  {s.word_count != null && (
                    <>
                      <span>·</span>
                      <span>{s.word_count} words</span>
                    </>
                  )}
                </div>
              </div>
              <button
                type="button"
                onClick={(e) => handleDelete(s.session_id, e)}
                className="btn btn-ghost btn-sm"
                aria-label="Delete analysis"
                style={{ padding: '6px 8px', flexShrink: 0 }}
              >
                <Trash2 size={14} />
              </button>
            </button>
          ))}
        </div>
      )}
    </div>
  );
}