← back to Norma

components/email-analyzer/EmailSearchPanel.tsx

217 lines

'use client';

/**
 * Search across saved Email Analyzer drafts + versions.
 * Favorites float to the top, then trigram similarity, then newest.
 * Each result exposes two actions: load (via parent callback) and
 * "Save to Universal References" (works without loading).
 */

import { useEffect, useState, useCallback } from 'react';
import { Search, Star, BookmarkPlus, ExternalLink, Filter } from 'lucide-react';
import { useToast } from '@/components/ToastProvider';

interface SearchResult {
  id: string;
  session_id: string;
  version_number: number;
  label: string | null;
  tone: string | null;
  word_count: number;
  paragraph_count: number;
  content: string;
  is_favorite: boolean;
  created_at: string;
  sim: number;
}

interface EmailSearchPanelProps {
  onOpenSession?: (sessionId: string) => void;
}

export default function EmailSearchPanel({ onOpenSession }: EmailSearchPanelProps) {
  const { addToast } = useToast();
  const [q, setQ] = useState('');
  const [favoritesOnly, setFavoritesOnly] = useState(false);
  const [results, setResults] = useState<SearchResult[]>([]);
  const [loading, setLoading] = useState(false);

  const runSearch = useCallback(async () => {
    setLoading(true);
    try {
      const params = new URLSearchParams();
      if (q.trim()) params.set('q', q.trim());
      if (favoritesOnly) params.set('favorites_only', 'true');
      const res = await fetch(`/api/email-analyzer/search?${params.toString()}`);
      if (!res.ok) throw new Error('search failed');
      const data = await res.json();
      setResults(data.results || []);
    } catch {
      addToast('Search failed', 'error');
    } finally {
      setLoading(false);
    }
  }, [q, favoritesOnly, addToast]);

  // Debounced search on query/filter change
  useEffect(() => {
    const t = setTimeout(() => { void runSearch(); }, 220);
    return () => clearTimeout(t);
  }, [runSearch]);

  async function saveToUniversal(r: SearchResult) {
    const plain = r.content.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
    try {
      const res = await fetch('/api/email-analyzer/universal-references', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          title: `${r.label || 'Saved email'} (v${r.version_number})`,
          content: plain,
          tone: r.tone,
          source_session_id: r.session_id,
        }),
      });
      if (!res.ok) throw new Error('save failed');
      addToast('Added to Universal References', 'success');
    } catch {
      addToast('Failed to save', 'error');
    }
  }

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
      {/* Controls */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
        <div style={{ position: 'relative', flex: 1 }}>
          <Search
            size={13}
            style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)', color: 'var(--color-text-muted)' }}
          />
          <input
            type="text"
            className="input"
            style={{ width: '100%', paddingLeft: 30, fontSize: 13 }}
            placeholder="Search saved emails... (e.g., 'thank you donor')"
            value={q}
            onChange={(e) => setQ(e.target.value)}
          />
        </div>
        <button
          type="button"
          onClick={() => setFavoritesOnly((p) => !p)}
          className="btn btn-ghost btn-sm"
          style={{
            gap: 4,
            fontSize: 11,
            padding: '4px 10px',
            minHeight: 30,
            color: favoritesOnly ? '#f59e0b' : 'var(--color-text-muted)',
            borderColor: favoritesOnly ? '#f59e0b' : 'var(--color-border)',
            background: favoritesOnly ? 'rgba(245,158,11,0.08)' : 'transparent',
          }}
          title={favoritesOnly ? 'Show all' : 'Favorites only'}
        >
          <Filter size={11} />
          {favoritesOnly ? 'Favs only' : 'All'}
        </button>
      </div>

      {/* Results */}
      {loading ? (
        <div style={{ padding: '18px 10px', textAlign: 'center', fontSize: 12, color: 'var(--color-text-muted)' }}>
          Searching...
        </div>
      ) : results.length === 0 ? (
        <div style={{ padding: '18px 10px', textAlign: 'center', fontSize: 12, color: 'var(--color-text-muted)' }}>
          {q ? 'No results.' : 'Type to search your saved emails.'}
        </div>
      ) : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 6, maxHeight: 520, overflowY: 'auto' }}>
          {results.map((r) => (
            <div
              key={r.id}
              style={{
                padding: 8,
                border: '1px solid var(--color-border)',
                borderRadius: 8,
                backgroundColor: 'var(--color-surface)',
              }}
            >
              <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3 }}>
                {r.is_favorite && (
                  <Star size={12} fill="#f59e0b" strokeWidth={2} style={{ color: '#f59e0b', flexShrink: 0 }} />
                )}
                <span
                  style={{
                    fontSize: 13,
                    fontWeight: 600,
                    color: 'var(--color-text)',
                    flex: 1,
                    overflow: 'hidden',
                    textOverflow: 'ellipsis',
                    whiteSpace: 'nowrap',
                  }}
                  title={r.label || `v${r.version_number}`}
                >
                  {r.label || `Version ${r.version_number}`}
                </span>
                <span style={{ fontSize: 10, color: 'var(--color-text-muted)', flexShrink: 0 }}>
                  v{r.version_number} · {r.word_count}w
                </span>
              </div>
              <div
                style={{
                  fontSize: 11,
                  color: 'var(--color-text-secondary)',
                  display: '-webkit-box',
                  WebkitLineClamp: 2,
                  WebkitBoxOrient: 'vertical',
                  overflow: 'hidden',
                  lineHeight: 1.35,
                  marginBottom: 6,
                }}
              >
                {r.content.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 240)}
              </div>
              <div style={{ display: 'flex', gap: 4 }}>
                {onOpenSession && (
                  <button
                    type="button"
                    className="btn btn-ghost btn-sm"
                    style={{ gap: 3, fontSize: 11, padding: '2px 8px', minHeight: 24 }}
                    onClick={() => onOpenSession(r.session_id)}
                    title="Open this analysis session"
                  >
                    <ExternalLink size={10} /> Open
                  </button>
                )}
                <button
                  type="button"
                  className="btn btn-ghost btn-sm"
                  style={{
                    gap: 3,
                    fontSize: 11,
                    padding: '2px 8px',
                    minHeight: 24,
                    color: 'var(--color-lane-b)',
                  }}
                  onClick={() => void saveToUniversal(r)}
                  title="Save to Universal References"
                >
                  <BookmarkPlus size={10} /> Universal
                </button>
                {r.sim > 0 && (
                  <span style={{ marginLeft: 'auto', fontSize: 10, color: 'var(--color-text-muted)' }}>
                    match {(r.sim * 100).toFixed(0)}%
                  </span>
                )}
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}