constellation-analyzer/TECHNICAL_SPEC_BIBLIOGRAPHY.md
Jan-Henrik Bruhn 36f44d61ac feat: add comprehensive bibliography and citation system
Implements complete bibliography management with citation assignment to
nodes and edges, following CSL-JSON standard.

Features:
- Bibliography store with Zustand and citation.js integration
- Smart import supporting DOI, BibTeX, PubMed ID, and URLs
- Manual entry and editing forms for references
- Citation selector with autocomplete text field interface
- History tracking for undo/redo support
- Workspace integration for import/export
- Support for multiple reference types including interview and other
- Description/notes field for additional reference information

Components:
- CitationSelector: autocomplete UI for selecting citations
- BibliographyConfigModal: main bibliography management interface
- QuickAddReferenceForm: smart import and manual entry
- EditReferenceInline: full reference editing form
- ReferenceManagementList: list view with citation counts

Integration:
- NodeEditorPanel: citation assignment to actors
- EdgeEditorPanel: citation assignment to relations
- MenuBar: bibliography menu item
- WorkspaceStore: bibliography persistence in workspace files

Technical details:
- CSL-JSON format for bibliographic data
- citation.js for parsing and formatting
- TypeScript with proper type definitions
- Debounced updates for performance
- Citation count tracking across graph elements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 14:43:55 +02:00

45 KiB

Bibliography System - Technical Specification (Revised for citation.js)

Overview

This document provides detailed technical specifications for implementing the bibliography/reference management system in Constellation Analyzer, deeply integrated with the citation.js library to avoid reinventing functionality. Based on the UX design in UX_BIBLIOGRAPHY_DESIGN.md.

Key Change: This revision leverages citation.js for all parsing, formatting, and conversion tasks, eliminating custom implementations where the library already provides the functionality.


1. Citation.js Architecture

1.1 What citation.js Provides

Citation.js will handle:

  • Input parsing: DOI, BibTeX, RIS, URLs, ISBNs, PubMed IDs, etc.
  • Format conversion: Between BibTeX, RIS, CSL-JSON, and more
  • Citation formatting: APA, MLA, Chicago, Vancouver, and 10,000+ CSL styles
  • Data normalization: All inputs → CSL-JSON internally
  • Bibliography generation: Formatted HTML, text, RTF output
  • Async fetching: DOI/ISBN/PubMed metadata retrieval

1.2 What We Build on Top

We still need to build:

  • UI components (modals, forms, selectors)
  • State management (Zustand stores)
  • Document persistence (storing bibliography with graph)
  • App-specific metadata (tags, favorites, usage tracking)
  • Citation linking (connecting references to nodes/edges)
  • History tracking (undo/redo integration)

1.3 Integration Strategy

Core Pattern: Use a Cite instance per document as the single source of truth for references, synced with our Zustand store for UI state.

// citation.js manages the CSL-JSON data
const citeInstance = new Cite(references);

// Our store manages app-specific data and UI state
const bibliographyStore = {
  citeInstance,      // The citation.js instance
  appMetadata: {},   // App-specific fields (_app)
  // ... other state
};

2. Data Models

2.1 CSL-JSON Types (from citation.js)

Citation.js uses standard CSL-JSON. We import types from @citation-js/core or define compatible interfaces:

Location: /src/types/bibliography.ts

/**
 * CSL-JSON types are provided by citation.js
 * We extend with app-specific metadata
 */

// Standard CSL-JSON types (compatible with citation.js)
export type ReferenceType =
  | 'article-journal'
  | 'article-magazine'
  | 'article-newspaper'
  | 'book'
  | 'chapter'
  | 'paper-conference'
  | 'report'
  | 'thesis'
  | 'webpage'
  | 'interview'
  | 'manuscript'
  | 'personal_communication'
  | 'entry-encyclopedia'
  | 'entry-dictionary';

/**
 * Standard CSL-JSON reference structure
 * This is what citation.js expects
 */
export interface CSLReference {
  id: string;
  type: ReferenceType;
  title?: string;
  author?: Array<{ family?: string; given?: string; literal?: string }>;
  issued?: { 'date-parts': [[number, number?, number?]] };
  'container-title'?: string;
  publisher?: string;
  volume?: string | number;
  issue?: string | number;
  page?: string;
  DOI?: string;
  ISBN?: string;
  URL?: string;
  // ... many more CSL-JSON fields
  [key: string]: any;  // CSL-JSON is extensible
}

/**
 * App-specific metadata (NOT part of CSL-JSON)
 * Stored separately from the Cite instance
 */
export interface ReferenceAppMetadata {
  id: string;  // Matches CSL reference ID
  tags?: string[];
  favorite?: boolean;
  color?: string;
  createdAt: string;
  updatedAt: string;
}

/**
 * Combined reference for UI display
 * Merges CSL data + app metadata
 */
export interface BibliographyReference extends CSLReference {
  _app?: ReferenceAppMetadata;
}

/**
 * Bibliography settings per document
 */
export interface BibliographySettings {
  defaultStyle: string;  // CSL style ID (e.g., "apa", "chicago")
  sortOrder: 'author' | 'year' | 'title';
}

/**
 * Complete bibliography data structure for persistence
 */
export interface Bibliography {
  // CSL-JSON array (can be loaded directly into Cite)
  references: CSLReference[];
  // App-specific metadata (stored separately)
  metadata: Record<string, ReferenceAppMetadata>;
  // Settings
  settings: BibliographySettings;
}

2.2 Extend Existing Types

Location: /src/types/index.ts

// Add to ActorData interface
export interface ActorData {
  // ... existing fields
  citations?: string[];  // Array of reference IDs
}

// Add to RelationData interface
export interface RelationData {
  // ... existing fields
  citations?: string[];  // Array of reference IDs
}

Location: /src/stores/persistence/types.ts

// Add to ConstellationDocument interface
export interface ConstellationDocument {
  metadata: {...};
  nodeTypes: NodeTypeConfig[];
  edgeTypes: EdgeTypeConfig[];
  labels?: LabelConfig[];
  bibliography?: Bibliography;  // NEW FIELD
  timeline: {...};
}

3. State Management with citation.js

3.1 Bibliography Store (Revised)

Location: /src/stores/bibliographyStore.ts

import { create } from 'zustand';
import { Cite } from '@citation-js/core';
// Load plugins
import '@citation-js/plugin-csl';
import '@citation-js/plugin-doi';
import '@citation-js/plugin-bibtex';
import '@citation-js/plugin-ris';

import type {
  Bibliography,
  BibliographyReference,
  CSLReference,
  ReferenceAppMetadata,
  BibliographySettings
} from '@/types/bibliography';

interface BibliographyStore {
  // State
  citeInstance: Cite;  // The citation.js instance
  appMetadata: Record<string, ReferenceAppMetadata>;  // App-specific data
  settings: BibliographySettings;

  // Getters
  getReferences: () => BibliographyReference[];  // Merges CSL + app data
  getReferenceById: (id: string) => BibliographyReference | undefined;
  getCSLData: () => CSLReference[];  // Raw CSL-JSON from Cite

  // CRUD operations (use citation.js methods)
  addReference: (ref: Partial<CSLReference>) => string;
  updateReference: (id: string, updates: Partial<CSLReference>) => void;
  deleteReference: (id: string) => void;
  duplicateReference: (id: string) => string;

  // Bulk operations
  setReferences: (refs: CSLReference[]) => void;
  importReferences: (refs: CSLReference[]) => void;
  clearAll: () => void;

  // Metadata operations
  updateMetadata: (id: string, metadata: Partial<ReferenceAppMetadata>) => void;

  // Settings
  setSettings: (settings: BibliographySettings) => void;

  // Citation.js powered operations
  formatReference: (id: string, style?: string, format?: 'html' | 'text') => string;
  formatBibliography: (style?: string, format?: 'html' | 'text') => string;
  parseInput: (input: string) => Promise<Partial<CSLReference>[]>;
  exportAs: (format: 'bibtex' | 'ris' | 'json') => string;
}

export const useBibliographyStore = create<BibliographyStore>((set, get) => ({
  citeInstance: new Cite([]),
  appMetadata: {},
  settings: {
    defaultStyle: 'apa',
    sortOrder: 'author',
  },

  getReferences: () => {
    const csl = get().citeInstance.data as CSLReference[];
    const metadata = get().appMetadata;

    // Merge CSL data with app metadata
    return csl.map(ref => ({
      ...ref,
      _app: metadata[ref.id],
    }));
  },

  getReferenceById: (id) => {
    const refs = get().getReferences();
    return refs.find(ref => ref.id === id);
  },

  getCSLData: () => {
    return get().citeInstance.data as CSLReference[];
  },

  addReference: (ref) => {
    const { citeInstance } = get();

    // Generate ID if not provided
    const id = ref.id || `ref-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
    const fullRef = { ...ref, id };

    // Use citation.js add() method
    citeInstance.add(fullRef);

    // Add app metadata
    const now = new Date().toISOString();
    set(state => ({
      appMetadata: {
        ...state.appMetadata,
        [id]: {
          id,
          tags: [],
          createdAt: now,
          updatedAt: now,
        },
      },
    }));

    return id;
  },

  updateReference: (id, updates) => {
    const { citeInstance } = get();
    const data = citeInstance.data as CSLReference[];

    // Find and update the reference
    const updatedData = data.map(ref =>
      ref.id === id ? { ...ref, ...updates } : ref
    );

    // Use citation.js set() method to replace all data
    citeInstance.set(updatedData);

    // Update metadata timestamp
    set(state => ({
      appMetadata: {
        ...state.appMetadata,
        [id]: {
          ...state.appMetadata[id],
          updatedAt: new Date().toISOString(),
        },
      },
    }));
  },

  deleteReference: (id) => {
    const { citeInstance } = get();
    const data = citeInstance.data as CSLReference[];

    // Remove from citation.js
    const filteredData = data.filter(ref => ref.id !== id);
    citeInstance.set(filteredData);

    // Remove metadata
    set(state => {
      const { [id]: removed, ...rest } = state.appMetadata;
      return { appMetadata: rest };
    });
  },

  duplicateReference: (id) => {
    const original = get().getReferenceById(id);
    if (!original) return '';

    const newId = `ref-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
    const duplicate = {
      ...original,
      id: newId,
      title: `${original.title} (Copy)`,
    };

    // Remove _app before adding to Cite
    const { _app, ...cslData } = duplicate;
    get().addReference(cslData);

    return newId;
  },

  setReferences: (refs) => {
    const { citeInstance } = get();

    // Use citation.js set() to replace all references
    citeInstance.set(refs);

    // Initialize metadata for new references
    const metadata: Record<string, ReferenceAppMetadata> = {};
    const now = new Date().toISOString();

    refs.forEach(ref => {
      metadata[ref.id] = {
        id: ref.id,
        tags: [],
        createdAt: now,
        updatedAt: now,
      };
    });

    set({ appMetadata: metadata });
  },

  importReferences: (refs) => {
    const { citeInstance } = get();

    // Use citation.js add() to append references
    citeInstance.add(refs);

    // Add metadata for new references
    const now = new Date().toISOString();
    set(state => {
      const newMetadata = { ...state.appMetadata };
      refs.forEach(ref => {
        if (!newMetadata[ref.id]) {
          newMetadata[ref.id] = {
            id: ref.id,
            tags: [],
            createdAt: now,
            updatedAt: now,
          };
        }
      });
      return { appMetadata: newMetadata };
    });
  },

  clearAll: () => {
    get().citeInstance.reset();  // citation.js method to clear all data
    set({ appMetadata: {} });
  },

  updateMetadata: (id, updates) => {
    set(state => ({
      appMetadata: {
        ...state.appMetadata,
        [id]: {
          ...state.appMetadata[id],
          ...updates,
          updatedAt: new Date().toISOString(),
        },
      },
    }));
  },

  setSettings: (settings) => set({ settings }),

  formatReference: (id, style, format = 'html') => {
    const { citeInstance, settings } = get();
    const styleToUse = style || settings.defaultStyle;

    // Find the reference
    const ref = (citeInstance.data as CSLReference[]).find(r => r.id === id);
    if (!ref) return '';

    // Create temporary Cite instance for single reference
    const cite = new Cite(ref);

    // Use citation.js format() method
    return cite.format('bibliography', {
      format: format,
      template: styleToUse,
      lang: 'en-US',
    });
  },

  formatBibliography: (style, format = 'html') => {
    const { citeInstance, settings } = get();
    const styleToUse = style || settings.defaultStyle;

    // Use citation.js format() method on entire instance
    return citeInstance.format('bibliography', {
      format: format,
      template: styleToUse,
      lang: 'en-US',
    });
  },

  parseInput: async (input) => {
    try {
      // Use citation.js async parsing
      // This handles DOIs, URLs, BibTeX, RIS, etc. automatically
      const cite = await Cite.async(input);
      return cite.data as CSLReference[];
    } catch (error) {
      console.error('Failed to parse input:', error);
      throw new Error('Could not parse citation data');
    }
  },

  exportAs: (format) => {
    const { citeInstance } = get();

    // Use citation.js format() method
    switch (format) {
      case 'bibtex':
        return citeInstance.format('bibtex');
      case 'ris':
        return citeInstance.format('ris');
      case 'json':
        return JSON.stringify(citeInstance.data, null, 2);
      default:
        return '';
    }
  },
}));

3.2 Integration with GraphStore (Unchanged)

Location: /src/hooks/useBibliographyWithHistory.ts

import { useGraphStore } from '@/stores/graphStore';
import { useBibliographyStore } from '@/stores/bibliographyStore';
import { useHistoryStore } from '@/stores/historyStore';
import { useWorkspaceStore } from '@/stores/workspaceStore';
import type { CSLReference } from '@/types/bibliography';

/**
 * Hook that wraps bibliography operations with history tracking
 * Similar to useGraphWithHistory pattern
 */
export const useBibliographyWithHistory = () => {
  const { addReference, updateReference, deleteReference } = useBibliographyStore();
  const { pushHistory } = useHistoryStore();
  const { activeDocumentId } = useWorkspaceStore();
  const { nodes, edges, updateNode, updateEdge } = useGraphStore();

  const addReferenceWithHistory = (ref: Partial<CSLReference>) => {
    const id = addReference(ref);
    pushHistory(activeDocumentId!, `Added reference: ${ref.title}`);
    return id;
  };

  const updateReferenceWithHistory = (id: string, updates: Partial<CSLReference>) => {
    const ref = useBibliographyStore.getState().getReferenceById(id);
    updateReference(id, updates);
    pushHistory(activeDocumentId!, `Updated reference: ${ref?.title}`);
  };

  const deleteReferenceWithHistory = (id: string) => {
    const ref = useBibliographyStore.getState().getReferenceById(id);

    // Remove citations from nodes and edges
    nodes.forEach(node => {
      if (node.data.citations?.includes(id)) {
        updateNode(node.id, {
          citations: node.data.citations.filter(cid => cid !== id),
        });
      }
    });

    edges.forEach(edge => {
      if (edge.data?.citations?.includes(id)) {
        updateEdge(edge.id, {
          citations: edge.data.citations.filter(cid => cid !== id),
        });
      }
    });

    deleteReference(id);
    pushHistory(activeDocumentId!, `Deleted reference: ${ref?.title}`);
  };

  const getCitationCount = (referenceId: string) => {
    const nodeCount = nodes.filter(n => n.data.citations?.includes(referenceId)).length;
    const edgeCount = edges.filter(e => e.data?.citations?.includes(referenceId)).length;
    return { nodes: nodeCount, edges: edgeCount };
  };

  return {
    addReference: addReferenceWithHistory,
    updateReference: updateReferenceWithHistory,
    deleteReference: deleteReferenceWithHistory,
    getCitationCount,
  };
};

4. Utility Functions (Simplified with citation.js)

4.1 Smart Input Parser (Uses citation.js)

Location: /src/utils/bibliography/smart-parser.ts

import { Cite } from '@citation-js/core';
import type { CSLReference } from '@/types/bibliography';

/**
 * Parse any citation input using citation.js
 * Handles: DOI, URL, BibTeX, RIS, ISBN, PubMed ID, etc.
 */
export const parseSmartInput = async (input: string): Promise<CSLReference[]> => {
  try {
    const cite = await Cite.async(input);
    return cite.data as CSLReference[];
  } catch (error) {
    throw new Error(`Could not parse input: ${error instanceof Error ? error.message : 'Unknown error'}`);
  }
};

/**
 * Check if input looks like it can be parsed
 */
export const isValidCitationInput = (input: string): boolean => {
  const trimmed = input.trim();

  // DOI patterns
  if (/^(https?:\/\/)?(dx\.)?doi\.org\/10\.\d{4,}/i.test(trimmed)) return true;
  if (/^10\.\d{4,}\/.+/.test(trimmed)) return true;

  // URL patterns
  if (/^https?:\/\/.+/.test(trimmed)) return true;

  // BibTeX patterns
  if (/^@\w+\{/.test(trimmed)) return true;

  // PubMed ID
  if (/^PMID:\s*\d+/i.test(trimmed)) return true;

  // ISBN
  if (/^ISBN[:\s]*[\d-]+/i.test(trimmed)) return true;

  return false;
};

/**
 * Get input type hint for user
 */
export const getInputTypeHint = (input: string): string => {
  const trimmed = input.trim();

  if (/^10\.\d{4,}\//.test(trimmed)) return 'DOI';
  if (/^https?:\/\//.test(trimmed)) return 'URL';
  if (/^@\w+\{/.test(trimmed)) return 'BibTeX';
  if (/^PMID:/i.test(trimmed)) return 'PubMed ID';
  if (/^ISBN/i.test(trimmed)) return 'ISBN';

  return 'Unknown';
};

4.2 Citation Formatting (Uses citation.js)

Location: /src/utils/bibliography/formatting.ts

import { Cite } from '@citation-js/core';
import type { CSLReference } from '@/types/bibliography';

/**
 * Format a single reference using citation.js
 */
export const formatReference = (
  ref: CSLReference,
  style: string = 'apa',
  format: 'html' | 'text' = 'text'
): string => {
  try {
    const cite = new Cite(ref);
    return cite.format('bibliography', {
      format,
      template: style,
      lang: 'en-US',
    });
  } catch (error) {
    console.error('Formatting error:', error);
    return `[Error formatting reference: ${ref.title}]`;
  }
};

/**
 * Format short citation for lists (Author, Year)
 * Uses citation.js citation format
 */
export const formatShortCitation = (
  ref: CSLReference,
  style: string = 'apa'
): string => {
  try {
    const cite = new Cite(ref);
    // Use citation format (in-text) instead of bibliography
    return cite.format('citation', {
      format: 'text',
      template: style,
      lang: 'en-US',
    });
  } catch (error) {
    // Fallback to simple format
    const author = ref.author?.[0];
    const authorStr = author?.family || author?.literal || 'Unknown';
    const year = ref.issued?.['date-parts']?.[0]?.[0] || 'n.d.';
    return `${authorStr} (${year})`;
  }
};

/**
 * Format full bibliography for multiple references
 */
export const formatBibliography = (
  refs: CSLReference[],
  style: string = 'apa',
  format: 'html' | 'text' = 'html'
): string => {
  try {
    const cite = new Cite(refs);
    return cite.format('bibliography', {
      format,
      template: style,
      lang: 'en-US',
    });
  } catch (error) {
    console.error('Bibliography formatting error:', error);
    return '[Error formatting bibliography]';
  }
};

/**
 * Get list of available citation styles
 * Citation.js supports 10,000+ styles via CSL
 */
export const getAvailableStyles = (): Array<{ id: string; label: string }> => {
  // Common styles for social sciences
  return [
    { id: 'apa', label: 'APA 7th Edition' },
    { id: 'chicago-author-date', label: 'Chicago Author-Date' },
    { id: 'chicago-note-bibliography', label: 'Chicago Notes' },
    { id: 'mla', label: 'MLA 9th Edition' },
    { id: 'harvard1', label: 'Harvard' },
    { id: 'vancouver', label: 'Vancouver' },
    { id: 'american-sociological-association', label: 'ASA' },
    { id: 'american-political-science-association', label: 'APSA' },
  ];
};

4.3 Import/Export Utilities (Uses citation.js)

Location: /src/utils/bibliography/import-export.ts

import { Cite } from '@citation-js/core';
import type { CSLReference } from '@/types/bibliography';

/**
 * Import from various formats using citation.js
 */
export const importFromFile = async (
  content: string,
  format: 'bibtex' | 'ris' | 'json'
): Promise<CSLReference[]> => {
  try {
    const cite = await Cite.async(content);
    return cite.data as CSLReference[];
  } catch (error) {
    throw new Error(`Failed to import ${format}: ${error instanceof Error ? error.message : 'Unknown error'}`);
  }
};

/**
 * Export to various formats using citation.js
 */
export const exportToFormat = (
  refs: CSLReference[],
  format: 'bibtex' | 'ris' | 'json'
): string => {
  try {
    const cite = new Cite(refs);

    switch (format) {
      case 'bibtex':
        return cite.format('bibtex');
      case 'ris':
        return cite.format('ris');
      case 'json':
        return JSON.stringify(cite.data, null, 2);
      default:
        throw new Error(`Unsupported format: ${format}`);
    }
  } catch (error) {
    throw new Error(`Failed to export: ${error instanceof Error ? error.message : 'Unknown error'}`);
  }
};

/**
 * Export formatted bibliography as HTML
 */
export const exportFormattedBibliography = (
  refs: CSLReference[],
  style: string = 'apa'
): string => {
  try {
    const cite = new Cite(refs);
    const bibliography = cite.format('bibliography', {
      format: 'html',
      template: style,
      lang: 'en-US',
    });

    return `<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Bibliography</title>
  <style>
    body {
      font-family: 'Times New Roman', serif;
      max-width: 800px;
      margin: 2em auto;
      line-height: 1.6;
    }
    h1 { text-align: center; }
    .csl-entry {
      margin-bottom: 1em;
      padding-left: 2em;
      text-indent: -2em;
    }
  </style>
</head>
<body>
  <h1>Bibliography</h1>
  ${bibliography}
</body>
</html>`;
  } catch (error) {
    throw new Error(`Failed to generate HTML: ${error instanceof Error ? error.message : 'Unknown error'}`);
  }
};

/**
 * Detect format of input string
 */
export const detectFormat = (content: string): 'bibtex' | 'ris' | 'json' | 'unknown' => {
  const trimmed = content.trim();

  if (trimmed.startsWith('@')) return 'bibtex';
  if (trimmed.startsWith('TY  -')) return 'ris';
  if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
    try {
      JSON.parse(trimmed);
      return 'json';
    } catch {
      return 'unknown';
    }
  }

  return 'unknown';
};

5. Component Integration (Key Changes)

5.1 Smart Input Component

Location: /src/components/Bibliography/SmartInputField.tsx

import React, { useState } from 'react';
import { parseSmartInput, isValidCitationInput, getInputTypeHint } from '@/utils/bibliography/smart-parser';
import { useBibliographyWithHistory } from '@/hooks/useBibliographyWithHistory';
import { useToastStore } from '@/stores/toastStore';

interface Props {
  onSuccess?: () => void;
}

export const SmartInputField: React.FC<Props> = ({ onSuccess }) => {
  const [input, setInput] = useState('');
  const [loading, setLoading] = useState(false);
  const { addReference } = useBibliographyWithHistory();
  const { showToast } = useToastStore();

  const handleParse = async () => {
    if (!input.trim()) return;

    setLoading(true);
    try {
      // Use citation.js to parse the input
      const references = await parseSmartInput(input);

      if (references.length === 0) {
        showToast('No references found in input', 'warning');
        return;
      }

      // Add all parsed references
      references.forEach(ref => {
        addReference(ref);
      });

      showToast(
        `Added ${references.length} reference${references.length > 1 ? 's' : ''}`,
        'success'
      );

      setInput('');
      onSuccess?.();
    } catch (error) {
      showToast(
        error instanceof Error ? error.message : 'Failed to parse input',
        'error'
      );
    } finally {
      setLoading(false);
    }
  };

  const isValid = isValidCitationInput(input);
  const hint = input.trim() ? getInputTypeHint(input) : '';

  return (
    <div className="space-y-2">
      <label className="block text-sm font-medium text-gray-700">
        Smart Input
      </label>
      <textarea
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="Paste DOI, URL, BibTeX, RIS, ISBN, or PubMed ID..."
        className="w-full px-3 py-2 border rounded-lg font-mono text-sm"
        rows={3}
      />
      {hint && (
        <div className="text-xs text-gray-600">
          Detected: <span className="font-medium">{hint}</span>
        </div>
      )}
      <div className="flex gap-2">
        <button
          onClick={handleParse}
          disabled={!isValid || loading}
          className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
        >
          {loading ? 'Parsing...' : 'Auto-Fill'}
        </button>
        <button
          onClick={() => setInput('')}
          className="px-4 py-2 border border-gray-300 rounded hover:bg-gray-50"
        >
          Clear
        </button>
      </div>
      <div className="text-xs text-gray-500">
        💡 Tip: citation.js automatically detects and parses most citation formats
      </div>
    </div>
  );
};

5.2 Citation Preview Component

Location: /src/components/Bibliography/CitationPreview.tsx

import React, { useMemo } from 'react';
import { formatReference } from '@/utils/bibliography/formatting';
import { useBibliographyStore } from '@/stores/bibliographyStore';
import type { CSLReference } from '@/types/bibliography';

interface Props {
  reference: CSLReference;
}

export const CitationPreview: React.FC<Props> = ({ reference }) => {
  const { settings } = useBibliographyStore();
  const [style, setStyle] = useState(settings.defaultStyle);

  const formattedCitation = useMemo(() => {
    return formatReference(reference, style, 'html');
  }, [reference, style]);

  return (
    <div className="border rounded-lg p-4 bg-gray-50">
      <div className="flex items-center justify-between mb-2">
        <label className="text-sm font-medium text-gray-700">
          Citation Preview
        </label>
        <select
          value={style}
          onChange={(e) => setStyle(e.target.value)}
          className="text-xs border rounded px-2 py-1"
        >
          <option value="apa">APA</option>
          <option value="chicago-author-date">Chicago</option>
          <option value="mla">MLA</option>
          <option value="harvard1">Harvard</option>
        </select>
      </div>
      <div
        className="text-sm text-gray-800 prose prose-sm"
        dangerouslySetInnerHTML={{ __html: formattedCitation }}
      />
    </div>
  );
};

6. Workspace Integration (Detailed)

6.1 Understanding the Existing Persistence Architecture

The app uses a multi-document workspace with these key components:

  1. ConstellationDocument: Complete document structure with metadata, types, labels, timeline
  2. WorkspaceStore: Manages multiple documents, tabs, active document
  3. LocalStorage: Persists workspace state and documents
  4. Timeline: All graphs stored in timeline states (not at document root)

Key Pattern: The document is the source of truth for types, labels, and bibliography. Timeline states contain only nodes and edges.

6.2 Integration Points in WorkspaceStore

Location: /src/stores/workspaceStore.ts

6.2.1 Document Loading (loadDocument and switchToDocument)

Add bibliography loading when switching to a document:

// In loadDocument function (line 274)
loadDocument: async (documentId: string) => {
  const state = get();

  // Check if already loaded
  if (state.documents.has(documentId)) {
    return;
  }

  // Load from storage
  const doc = loadDocumentFromStorage(documentId);
  if (!doc) {
    console.error(`Document ${documentId} not found`);
    return;
  }

  // Load timeline if it exists
  if (doc.timeline) {
    useTimelineStore.getState().loadTimeline(documentId, doc.timeline as unknown as Timeline);
  }

  // **NEW: Load bibliography if it exists**
  if (doc.bibliography) {
    const { references, metadata, settings } = doc.bibliography;
    const cite = new Cite(references);

    useBibliographyStore.setState({
      citeInstance: cite,
      appMetadata: metadata,
      settings,
    });
  } else {
    // Initialize empty bibliography for documents without one
    useBibliographyStore.setState({
      citeInstance: new Cite([]),
      appMetadata: {},
      settings: {
        defaultStyle: 'apa',
        sortOrder: 'author',
      },
    });
  }

  set((state) => {
    const newDocuments = new Map(state.documents);
    newDocuments.set(documentId, doc);

    return { documents: newDocuments };
  });
},

6.2.2 Document Saving (saveDocument)

Add bibliography to saved document:

// In saveDocument function (line 715)
saveDocument: (documentId: string) => {
  const state = get();
  const doc = state.documents.get(documentId);
  if (doc) {
    doc.metadata.updatedAt = new Date().toISOString();

    // Save timeline data if exists
    const timelineState = useTimelineStore.getState();
    const timeline = timelineState.timelines.get(documentId);

    if (timeline) {
      // Serialize timeline (convert Map to object)
      const serializedStates: Record<string, ConstellationState> = {};
      timeline.states.forEach((state: ConstellationState, id: string) => {
        serializedStates[id] = state;
      });

      doc.timeline = {
        states: serializedStates,
        currentStateId: timeline.currentStateId,
        rootStateId: timeline.rootStateId,
      };
    }

    // **NEW: Save bibliography data if exists**
    const bibliographyState = useBibliographyStore.getState();
    const { getCSLData, appMetadata, settings } = bibliographyState;
    const references = getCSLData();

    // Only save bibliography if there are references or metadata
    if (references.length > 0 || Object.keys(appMetadata).length > 0) {
      doc.bibliography = {
        references,
        metadata: appMetadata,
        settings,
      };
    }

    saveDocumentToStorage(documentId, doc);

    const metadata = state.documentMetadata.get(documentId);
    if (metadata) {
      metadata.isDirty = false;
      metadata.lastModified = doc.metadata.updatedAt;
      saveDocumentMetadata(documentId, metadata);

      set((state) => {
        const newMetadata = new Map(state.documentMetadata);
        newMetadata.set(documentId, metadata);
        return { documentMetadata: newMetadata };
      });
    }
  }
},

6.2.3 Document Creation (createDocument)

Initialize empty bibliography for new documents:

// In createDocument function (line 129)
createDocument: (title = 'Untitled Analysis') => {
  const state = get();
  const documentId = generateDocumentId();
  const now = new Date().toISOString();

  // Create copies of the default types
  const nodeTypes = state.settings.defaultNodeTypes.map(nt => ({ ...nt }));
  const edgeTypes = state.settings.defaultEdgeTypes.map(et => ({ ...et }));

  const newDoc = createDocumentHelper(
    [],
    [],
    nodeTypes,
    edgeTypes
  );
  newDoc.metadata.documentId = documentId;
  newDoc.metadata.title = title;
  newDoc.labels = [];  // Initialize with empty labels

  // **NEW: Initialize empty bibliography**
  newDoc.bibliography = {
    references: [],
    metadata: {},
    settings: {
      defaultStyle: 'apa',
      sortOrder: 'author',
    },
  };

  // ... rest of function
},

6.2.4 Document Duplication (duplicateDocument)

Copy bibliography when duplicating:

// In duplicateDocument function (line 440)
duplicateDocument: (documentId: string) => {
  const state = get();
  const sourceDoc = state.documents.get(documentId);
  if (!sourceDoc) {
    console.error(`Document ${documentId} not found`);
    useToastStore.getState().showToast('Failed to duplicate: Document not found', 'error');
    return '';
  }

  const newDocumentId = generateDocumentId();
  const sourceMeta = state.documentMetadata.get(documentId);
  const newTitle = `${sourceMeta?.title || 'Untitled'} (Copy)`;

  const duplicatedDoc: ConstellationDocument = {
    ...sourceDoc,
    metadata: {
      ...sourceDoc.metadata,
      documentId: newDocumentId,
      title: newTitle,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    },
    // **NEW: Bibliography is already copied via spread operator**
    // But we could optionally give user choice to copy or start fresh
  };

  // ... rest of function

  // **NEW: Load bibliography into store for new document**
  if (duplicatedDoc.bibliography) {
    const { references, metadata, settings } = duplicatedDoc.bibliography;
    const cite = new Cite(references);

    useBibliographyStore.setState({
      citeInstance: cite,
      appMetadata: metadata,
      settings,
    });
  }

  return newDocumentId;
},

6.2.5 Document Import (importDocumentFromFile)

Handle bibliography in imported documents:

// In importDocumentFromFile function (line 550)
importDocumentFromFile: async () => {
  return new Promise((resolve) => {
    selectFileForImport(
      (importedDoc) => {
        const documentId = generateDocumentId();
        const now = new Date().toISOString();

        // Use the imported document as-is, preserving complete structure
        importedDoc.metadata.documentId = documentId;
        importedDoc.metadata.title = importedDoc.metadata.title || 'Imported Analysis';
        importedDoc.metadata.updatedAt = now;

        // **NEW: Bibliography is preserved from imported document**
        // Validate bibliography structure if present
        if (importedDoc.bibliography) {
          // Ensure it has the expected structure
          if (!importedDoc.bibliography.references) {
            importedDoc.bibliography.references = [];
          }
          if (!importedDoc.bibliography.metadata) {
            importedDoc.bibliography.metadata = {};
          }
          if (!importedDoc.bibliography.settings) {
            importedDoc.bibliography.settings = {
              defaultStyle: 'apa',
              sortOrder: 'author',
            };
          }
        }

        const metadata: DocumentMetadata = {
          id: documentId,
          title: importedDoc.metadata.title || 'Imported Analysis',
          isDirty: false,
          lastModified: now,
        };

        saveDocumentToStorage(documentId, importedDoc);
        saveDocumentMetadata(documentId, metadata);

        // Load timeline
        useTimelineStore.getState().loadTimeline(documentId, importedDoc.timeline as unknown as Timeline);

        // **NEW: Load bibliography if present**
        if (importedDoc.bibliography) {
          const { references, metadata: bibMetadata, settings } = importedDoc.bibliography;
          const cite = new Cite(references);

          useBibliographyStore.setState({
            citeInstance: cite,
            appMetadata: bibMetadata,
            settings,
          });
        } else {
          // Initialize empty bibliography
          useBibliographyStore.setState({
            citeInstance: new Cite([]),
            appMetadata: {},
            settings: {
              defaultStyle: 'apa',
              sortOrder: 'author',
            },
          });
        }

        // ... rest of function
      },
      // ... error handler
    );
  });
},

6.2.6 Document Export (exportDocument)

Ensure bibliography is included in export:

// In exportDocument function (line 617)
exportDocument: (documentId: string) => {
  const doc = get().documents.get(documentId);
  if (!doc) {
    console.error(`Document ${documentId} not found`);
    useToastStore.getState().showToast('Failed to export: Document not found', 'error');
    return;
  }

  try {
    // Ensure timeline is up-to-date before exporting
    const timelineState = useTimelineStore.getState();
    const timeline = timelineState.timelines.get(documentId);

    if (timeline) {
      const serializedStates: Record<string, ConstellationState> = {};
      timeline.states.forEach((state: ConstellationState, id: string) => {
        serializedStates[id] = state;
      });

      doc.timeline = {
        states: serializedStates,
        currentStateId: timeline.currentStateId,
        rootStateId: timeline.rootStateId,
      };
    }

    // **NEW: Ensure bibliography is up-to-date before exporting**
    const bibliographyState = useBibliographyStore.getState();
    const { getCSLData, appMetadata, settings } = bibliographyState;
    const references = getCSLData();

    if (references.length > 0 || Object.keys(appMetadata).length > 0) {
      doc.bibliography = {
        references,
        metadata: appMetadata,
        settings,
      };
    }

    // Export the complete document with all data
    exportDocumentToFile(doc);
    useToastStore.getState().showToast('Document exported successfully', 'success');
  } catch (error) {
    const message = error instanceof Error ? error.message : 'Unknown error';
    useToastStore.getState().showToast(`Failed to export document: ${message}`, 'error', 5000);
  }
},

6.3 Bibliography Store Lifecycle

The bibliography store needs to be aware of document switching:

// Add to bibliographyStore.ts

// Clear bibliography when switching documents
export const clearBibliographyForDocumentSwitch = () => {
  useBibliographyStore.setState({
    citeInstance: new Cite([]),
    appMetadata: {},
    settings: {
      defaultStyle: 'apa',
      sortOrder: 'author',
    },
  });
};

6.4 Workspace Export/Import

Bibliography data is automatically included when using existing workspace export/import:

Location: /src/stores/workspace/workspaceIO.ts

The existing functions already handle complete document structures, so bibliography will be included:

// exportWorkspace function (line 44) - NO CHANGES NEEDED
// It iterates through all documents and saves them with JSON.stringify
// This automatically includes the bibliography field

// importWorkspaceFromZip function (line 91) - NO CHANGES NEEDED
// It parses the complete document structure
// This automatically includes the bibliography field

6.5 Marking Documents as Dirty

When bibliography changes, mark document as dirty:

// In useBibliographyWithHistory hook

const addReferenceWithHistory = (ref: Partial<CSLReference>) => {
  const id = addReference(ref);
  pushHistory(activeDocumentId!, `Added reference: ${ref.title}`);

  // **NEW: Mark document as dirty**
  useWorkspaceStore.getState().markDocumentDirty(activeDocumentId!);

  return id;
};

const updateReferenceWithHistory = (id: string, updates: Partial<CSLReference>) => {
  const ref = useBibliographyStore.getState().getReferenceById(id);
  updateReference(id, updates);
  pushHistory(activeDocumentId!, `Updated reference: ${ref?.title}`);

  // **NEW: Mark document as dirty**
  useWorkspaceStore.getState().markDocumentDirty(activeDocumentId!);
};

const deleteReferenceWithHistory = (id: string) => {
  const ref = useBibliographyStore.getState().getReferenceById(id);

  // Remove citations from nodes and edges
  nodes.forEach(node => {
    if (node.data.citations?.includes(id)) {
      updateNode(node.id, {
        citations: node.data.citations.filter(cid => cid !== id),
      });
    }
  });

  edges.forEach(edge => {
    if (edge.data?.citations?.includes(id)) {
      updateEdge(edge.id, {
        citations: edge.data.citations.filter(cid => cid !== id),
      });
    }
  });

  deleteReference(id);
  pushHistory(activeDocumentId!, `Deleted reference: ${ref?.title}`);

  // **NEW: Mark document as dirty**
  useWorkspaceStore.getState().markDocumentDirty(activeDocumentId!);
};

6.6 Data Flow Summary

User Action (Add Reference)
    ↓
BibliographyConfigModal
    ↓
useBibliographyWithHistory
    ↓
bibliographyStore.addReference() ← Updates citation.js Cite instance
    ↓
historyStore.pushHistory() ← Track for undo/redo
    ↓
workspaceStore.markDocumentDirty() ← Indicate unsaved changes
    ↓
(User triggers save: Ctrl+S or File → Export)
    ↓
workspaceStore.saveDocument()
    ↓
Get CSL data from bibliographyStore
    ↓
Update doc.bibliography field
    ↓
saveDocumentToStorage() ← Persist to localStorage

6.7 Backward Compatibility

Documents created before bibliography feature:

// In loadDocument function
if (doc.bibliography) {
  // Load existing bibliography
  const { references, metadata, settings } = doc.bibliography;
  const cite = new Cite(references);
  useBibliographyStore.setState({ citeInstance: cite, appMetadata: metadata, settings });
} else {
  // Old document without bibliography - initialize empty
  useBibliographyStore.setState({
    citeInstance: new Cite([]),
    appMetadata: {},
    settings: { defaultStyle: 'apa', sortOrder: 'author' },
  });

  // Note: Don't save the document automatically
  // Let user add first reference before marking dirty
}

7. Dependencies (Updated)

7.1 NPM Packages

{
  "dependencies": {
    "@citation-js/core": "^0.7.14",
    "@citation-js/plugin-bibtex": "^0.7.14",
    "@citation-js/plugin-csl": "^0.7.14",
    "@citation-js/plugin-doi": "^0.7.14",
    "@citation-js/plugin-ris": "^0.7.14",
    "@citation-js/plugin-isbn": "^0.7.0",
    "@citation-js/plugin-pubmed": "^0.7.0"
  },
  "devDependencies": {
    "@types/citation-js": "^0.0.1"  // Community types if available
  }
}

Note: Each plugin is loaded separately, allowing code-splitting and lazy loading.


8. Implementation Phases (Revised)

Phase 1: Core MVP with citation.js

Setup:

  1. Install citation.js packages
  2. Create type definitions
  3. Set up bibliography store with Cite instance

Features:

  • Smart input parsing (DOI, URL, BibTeX, RIS via citation.js)
  • CRUD operations using citation.js methods
  • Multiple citation styles (APA, Chicago, MLA via citation.js)
  • Citation links to nodes/edges
  • Search/filter references (in-app)
  • Export as CSL-JSON, BibTeX, RIS (via citation.js)
  • Formatted bibliography generation (via citation.js)
  • Undo/redo support

Estimated Effort: 2-3 days (reduced from 3-5 days due to citation.js)

Phase 2: Enhanced UX

Features:

  • Advanced search with Fuse.js
  • Tag management and filtering
  • Usage tracking (which nodes/edges cite what)
  • Import modal with file upload
  • Export formatted HTML/PDF
  • Citation style selector

Estimated Effort: 2-3 days

Phase 3: Power Features

Features:

  • Duplicate detection
  • Bulk operations
  • Custom CSL style upload
  • Citation graph visualization
  • PDF metadata extraction (via external service)

Estimated Effort: 3-4 days


9. Key Advantages of citation.js Integration

What We Don't Need to Build Anymore:

  1. DOI lookup - citation.js handles it via @citation-js/plugin-doi
  2. BibTeX parser - citation.js handles it via @citation-js/plugin-bibtex
  3. RIS parser - citation.js handles it via @citation-js/plugin-ris
  4. CSL formatting engine - citation.js handles it via @citation-js/plugin-csl
  5. Format conversion logic - citation.js handles all conversions
  6. Citation style templates - citation.js supports 10,000+ styles
  7. ISBN lookup - citation.js handles it via @citation-js/plugin-isbn
  8. PubMed lookup - citation.js handles it via @citation-js/plugin-pubmed

What We Still Build:

  1. UI Components - Modal, forms, selectors
  2. State Management - Zustand store wrapping Cite instance
  3. App Metadata - Tags, favorites, created/updated timestamps
  4. Citation Linking - Connecting references to nodes/edges
  5. History Tracking - Undo/redo integration
  6. Document Persistence - Saving bibliography with graph
  7. Usage Analytics - Tracking which items cite what

10. Testing Strategy

Unit Tests

Test citation.js integration:

// test: parsing DOI
test('parseSmartInput handles DOI', async () => {
  const result = await parseSmartInput('10.1234/example');
  expect(result).toHaveLength(1);
  expect(result[0]).toHaveProperty('DOI');
});

// test: formatting
test('formatReference produces APA citation', () => {
  const ref = { id: '1', type: 'book', title: 'Test', author: [...] };
  const formatted = formatReference(ref, 'apa');
  expect(formatted).toContain('Test');
});

// test: export
test('exportToFormat generates BibTeX', () => {
  const refs = [{ id: '1', type: 'book', title: 'Test' }];
  const bibtex = exportToFormat(refs, 'bibtex');
  expect(bibtex).toContain('@book');
});

Integration Tests

  • Smart input → parse → add to store → display
  • Import BibTeX file → convert → show in list
  • Format bibliography → export HTML
  • Update reference → see preview update

E2E Tests

  • Paste DOI → auto-fill → save → cite in node → export
  • Import BibTeX file → select refs → cite in edge → format bibliography

Conclusion

This revised specification leverages citation.js extensively to avoid rebuilding functionality that the library already provides. This results in:

  • Less code to maintain (no custom parsers/formatters)
  • Better compatibility (standard CSL-JSON throughout)
  • More features (10,000+ citation styles, multiple input formats)
  • Faster development (estimated 30-40% time savings)
  • Higher quality (battle-tested library used by Zotero and others)

Architecture Summary:

  • citation.js Cite instance = source of truth for CSL-JSON data
  • Zustand store = wrapper providing UI state + app metadata
  • All parsing/formatting/conversion delegates to citation.js
  • We build only UI, persistence, and app-specific features

Next Steps:

  1. Review and approve revised specification
  2. Install citation.js packages
  3. Implement Phase 1 with citation.js integration
  4. Test DOI parsing and formatting
  5. Build UI components