import { useBorgesStore } from '../../store/borgesStore' type AnalysisModeAI = 'compression' | 'ending' | 'tone' | 'market_fit' | 'chat' const MODES: { mode: AnalysisModeAI; label: string; title: string }[] = [ { mode: 'compression', label: 'Compression', title: 'Find redundancy, hedging, and low-yield content' }, { mode: 'ending', label: 'Ending', title: 'Evaluate the final paragraph' }, { mode: 'tone', label: 'Tone', title: 'Map tonal register and flag outliers' }, { mode: 'market_fit', label: 'Market fit', title: 'Evaluate fit against selected market' }, ] export function AnalysisToolbar(): JSX.Element { const { analysisMode, setAnalysisMode, clearAnnotations, activeStoryId, useCollectionContext, toggleCollectionContext, useMarketBrief, toggleMarketBrief, selectedMarketId, isAILoading, activeStoryContent, activeStoryPath, addUserMessage, startAssistantMessage, appendToLastMessage, setAILoading, setAIError, setAnnotations, setChatOpen, markets } = useBorgesStore() const selectedMarket = markets.find((m) => m.id === selectedMarketId) const runAnalysis = async (mode: AnalysisModeAI): Promise => { if (!activeStoryId || !activeStoryPath || isAILoading) return const story = useBorgesStore.getState().stories.find((s) => s.id === activeStoryId) const cfg = await window.api.getCollectionConfig() if (analysisMode === mode) { clearAnnotations() return } setAnalysisMode(mode) clearAnnotations() setChatOpen(true) const modeLabels: Record = { compression: 'Run compression analysis', ending: 'Evaluate the ending', tone: 'Map tonal register', market_fit: 'Evaluate market fit', chat: 'Chat', none: '' } addUserMessage(modeLabels[mode]) startAssistantMessage() setAILoading(true) setAIError(null) try { let fullResponse = '' await window.api.streamAIMessage( { mode, storyContent: activeStoryContent, storyId: activeStoryId, wordCountTarget: story?.meta.wordCountTarget, targetMarket: selectedMarket ?? undefined, collectionContext: cfg.collectionContext, useCollectionContext: useCollectionContext && !!cfg.collectionContext, useMarketBrief: useMarketBrief && !!selectedMarket, conversationHistory: [], userMessage: modeLabels[mode] }, (chunk) => { appendToLastMessage(chunk) fullResponse += chunk } ) // Parse annotations from compression/tone responses if (mode === 'compression' || mode === 'tone') { const parsed = parseAnnotations(fullResponse, activeStoryContent) if (parsed.length > 0) setAnnotations(parsed) } } catch (err) { setAIError(err instanceof Error ? err.message : 'Error') } finally { setAILoading(false) } } return (
{MODES.map(({ mode, label, title }) => ( ))}
) } function parseAnnotations(text: string, doc: string): import('../../types/borges').TextAnnotation[] { const annotations: import('../../types/borges').TextAnnotation[] = [] const blockRe = /ISSUE:[^\n]*\nPASSAGE:\s*"([^"]+)"\nPROBLEM:\s*([^\n]+)(?:\nSUGGESTION:\s*"([^"]*)")?/g let m: RegExpExecArray | null while ((m = blockRe.exec(text)) !== null) { const passage = m[1]?.trim() const problem = m[2]?.trim() const suggestion = m[3]?.trim() if (!passage || !problem) continue if (!doc.includes(passage)) continue annotations.push({ id: `ann-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, passage, problem, suggestion: suggestion || undefined }) } return annotations }