diff --git a/TECHNICAL_SPEC_BIBLIOGRAPHY.md b/TECHNICAL_SPEC_BIBLIOGRAPHY.md new file mode 100644 index 0000000..7936b4a --- /dev/null +++ b/TECHNICAL_SPEC_BIBLIOGRAPHY.md @@ -0,0 +1,1635 @@ +# 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. + +```typescript +// 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` + +```typescript +/** + * 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; + // Settings + settings: BibliographySettings; +} +``` + +### 2.2 Extend Existing Types + +**Location**: `/src/types/index.ts` + +```typescript +// 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` + +```typescript +// 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` + +```typescript +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; // 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) => string; + updateReference: (id: string, updates: Partial) => 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) => 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[]>; + exportAs: (format: 'bibtex' | 'ris' | 'json') => string; +} + +export const useBibliographyStore = create((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 = {}; + 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` + +```typescript +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) => { + const id = addReference(ref); + pushHistory(activeDocumentId!, `Added reference: ${ref.title}`); + return id; + }; + + const updateReferenceWithHistory = (id: string, updates: Partial) => { + 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` + +```typescript +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 => { + 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` + +```typescript +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` + +```typescript +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 => { + 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 ` + + + + Bibliography + + + +

Bibliography

+ ${bibliography} + +`; + } 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` + +```typescript +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 = ({ 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 ( +
+ +