← back to Norma

components/email-analyzer/useReferences.ts

313 lines

'use client';

import { useState, useCallback } from 'react';

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

export interface Reference {
  id: string;
  session_id: string;
  ref_type: 'article' | 'link' | 'notes';
  title: string | null;
  source_url: string | null;
  source_domain: string | null;
  content: string;
  key_points: string[];
  include_in_rewrite: boolean;
  sort_order: number;
  created_at: string;
}

export interface FetchedUrlData {
  title: string;
  description: string | null;
  content: string;
  source: string;
  author: string | null;
  publishedDate: string | null;
  keyPoints: string[];
}

/* ─── Hook ───────────────────────────────────────────────────────────────── */

/** Dispatched when a reference is added from outside the useReferences hook
 *  (e.g. from SuggestionsPanel). Sessions listen for this and reload. */
export const REFS_EXTERNAL_CHANGE_EVENT = 'norma-analyzer-refs-changed';

export function dispatchRefsChanged(sessionId: string): void {
  if (typeof window === 'undefined') return;
  window.dispatchEvent(
    new CustomEvent(REFS_EXTERNAL_CHANGE_EVENT, { detail: { sessionId } }),
  );
}

export function useReferences(sessionId: string) {
  const [references, setReferences] = useState<Reference[]>([]);
  const [loading, setLoading] = useState(false);
  const [fetchingUrl, setFetchingUrl] = useState(false);
  const [error, setError] = useState<string | null>(null);

  /* ── Load references from API ──────────────────────────────────────────── */
  const loadReferences = useCallback(async () => {
    if (!sessionId) return;
    setLoading(true);
    setError(null);
    try {
      const res = await fetch(`/api/email-analyzer/references?session_id=${sessionId}`);
      if (!res.ok) throw new Error('Failed to load references');
      const data = await res.json();
      const refs = (data.references || []).map((r: Record<string, unknown>) => ({
        ...r,
        key_points: typeof r.key_points === 'string' ? JSON.parse(r.key_points as string) : (r.key_points || []),
      }));
      setReferences(refs);
    } catch (err) {
      setError((err as Error).message);
    } finally {
      setLoading(false);
    }
  }, [sessionId]);

  /* ── Add a pasted text reference ───────────────────────────────────────── */
  const addTextReference = useCallback(
    async (text: string, title?: string): Promise<Reference | null> => {
      if (!sessionId || !text.trim()) return null;
      setError(null);
      try {
        const res = await fetch('/api/email-analyzer/references', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            session_id: sessionId,
            ref_type: 'article',
            title: title || text.split('\n')[0]?.substring(0, 80) || 'Pasted Article',
            content: text,
            key_points: [],
            include_in_rewrite: true,
          }),
        });
        if (!res.ok) {
          const errData = await res.json().catch(() => ({}));
          throw new Error(errData.error || 'Failed to save reference');
        }
        const data = await res.json();
        const ref = {
          ...data.reference,
          key_points: typeof data.reference.key_points === 'string'
            ? JSON.parse(data.reference.key_points)
            : (data.reference.key_points || []),
        };
        setReferences((prev) => [...prev, ref]);
        return ref;
      } catch (err) {
        setError((err as Error).message);
        return null;
      }
    },
    [sessionId],
  );

  /* ── Add quick notes reference ─────────────────────────────────────────── */
  const addNotesReference = useCallback(
    async (text: string): Promise<Reference | null> => {
      if (!sessionId || !text.trim()) return null;
      setError(null);
      try {
        const res = await fetch('/api/email-analyzer/references', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            session_id: sessionId,
            ref_type: 'notes',
            title: 'Notes',
            content: text,
            key_points: [],
            include_in_rewrite: true,
          }),
        });
        if (!res.ok) {
          const errData = await res.json().catch(() => ({}));
          throw new Error(errData.error || 'Failed to save notes');
        }
        const data = await res.json();
        const ref = {
          ...data.reference,
          key_points: typeof data.reference.key_points === 'string'
            ? JSON.parse(data.reference.key_points)
            : (data.reference.key_points || []),
        };
        setReferences((prev) => [...prev, ref]);
        return ref;
      } catch (err) {
        setError((err as Error).message);
        return null;
      }
    },
    [sessionId],
  );

  /* ── Add a URL reference (fetch + save) ────────────────────────────────── */
  const addUrlReference = useCallback(
    async (url: string): Promise<Reference | null> => {
      if (!sessionId || !url.trim()) return null;
      setFetchingUrl(true);
      setError(null);
      try {
        // Step 1: Fetch and extract article content
        const fetchRes = await fetch('/api/email-analyzer/fetch-url', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ url }),
        });
        if (!fetchRes.ok) {
          const errData = await fetchRes.json().catch(() => ({}));
          throw new Error(errData.error || 'Failed to fetch URL');
        }
        const fetchedData: FetchedUrlData = await fetchRes.json();

        // Step 2: Save as reference
        const saveRes = await fetch('/api/email-analyzer/references', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            session_id: sessionId,
            ref_type: 'link',
            title: fetchedData.title,
            source_url: url,
            source_domain: fetchedData.source,
            content: fetchedData.content,
            key_points: fetchedData.keyPoints,
            include_in_rewrite: true,
          }),
        });
        if (!saveRes.ok) {
          const errData = await saveRes.json().catch(() => ({}));
          throw new Error(errData.error || 'Failed to save reference');
        }
        const data = await saveRes.json();
        const ref = {
          ...data.reference,
          key_points: typeof data.reference.key_points === 'string'
            ? JSON.parse(data.reference.key_points)
            : (data.reference.key_points || []),
        };
        setReferences((prev) => [...prev, ref]);
        return ref;
      } catch (err) {
        setError((err as Error).message);
        return null;
      } finally {
        setFetchingUrl(false);
      }
    },
    [sessionId],
  );

  /* ── Remove a reference ────────────────────────────────────────────────── */
  const removeReference = useCallback(async (id: string): Promise<boolean> => {
    setError(null);
    try {
      const res = await fetch(`/api/email-analyzer/references?id=${id}`, {
        method: 'DELETE',
      });
      if (!res.ok) {
        const errData = await res.json().catch(() => ({}));
        throw new Error(errData.error || 'Failed to delete reference');
      }
      setReferences((prev) => prev.filter((r) => r.id !== id));
      return true;
    } catch (err) {
      setError((err as Error).message);
      return false;
    }
  }, []);

  /* ── Toggle include_in_rewrite ─────────────────────────────────────────── */
  const toggleInclude = useCallback(async (id: string): Promise<boolean> => {
    const ref = references.find((r) => r.id === id);
    if (!ref) return false;

    const newValue = !ref.include_in_rewrite;
    // Optimistic update
    setReferences((prev) =>
      prev.map((r) => (r.id === id ? { ...r, include_in_rewrite: newValue } : r)),
    );

    try {
      const res = await fetch('/api/email-analyzer/references', {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ id, include_in_rewrite: newValue }),
      });
      if (!res.ok) {
        // Revert on failure
        setReferences((prev) =>
          prev.map((r) =>
            r.id === id ? { ...r, include_in_rewrite: !newValue } : r,
          ),
        );
        return false;
      }
      return true;
    } catch {
      // Revert on failure
      setReferences((prev) =>
        prev.map((r) =>
          r.id === id ? { ...r, include_in_rewrite: !newValue } : r,
        ),
      );
      return false;
    }
  }, [references]);

  /* ── Build context string for AI rewrite ───────────────────────────────── */
  const getContextString = useCallback((): string => {
    const active = references.filter((r) => r.include_in_rewrite);
    if (active.length === 0) return '';

    const sections: string[] = ['## Reference Materials'];

    for (const ref of active) {
      const keyPoints = Array.isArray(ref.key_points) ? ref.key_points : [];

      if (ref.ref_type === 'article') {
        sections.push(`### Article: ${ref.title || 'Untitled'}`);
        sections.push(ref.content.substring(0, 1500));
        if (keyPoints.length > 0) {
          sections.push('Key points:');
          keyPoints.forEach((p) => sections.push(`- ${p}`));
        }
      } else if (ref.ref_type === 'link') {
        const domain = ref.source_domain ? ` (${ref.source_domain})` : '';
        sections.push(`### Link: ${ref.title || 'Untitled'}${domain}`);
        sections.push(ref.content.substring(0, 1500));
        if (keyPoints.length > 0) {
          sections.push('Key points:');
          keyPoints.forEach((p) => sections.push(`- ${p}`));
        }
      } else if (ref.ref_type === 'notes') {
        sections.push('### Notes:');
        sections.push(ref.content.substring(0, 1500));
      }

      sections.push(''); // blank line between references
    }

    return sections.join('\n');
  }, [references]);

  return {
    references,
    loading,
    fetchingUrl,
    error,
    loadReferences,
    addTextReference,
    addNotesReference,
    addUrlReference,
    removeReference,
    toggleInclude,
    getContextString,
  };
}