import { useState, useRef, useEffect, useCallback, useMemo } from 'react' import { Robot, X, PaperPlaneRight, Plus, Link } from '@phosphor-icons/react' import { AiMessage } from './AiMessage' import { WikilinkChatInput } from './WikilinkChatInput' import { useAiAgent, type AiAgentMessage, type AgentFileCallbacks } from '../hooks/useAiAgent' import { collectLinkedEntries, buildContextSnapshot, type NoteReference, type NoteListItem } from '../utils/ai-context' import type { VaultEntry } from '../types' export type { AiAgentMessage } from '../hooks/useAiAgent' interface AiPanelProps { onClose: () => void onOpenNote?: (path: string) => void onFileCreated?: (relativePath: string) => void onFileModified?: (relativePath: string) => void onVaultChanged?: () => void vaultPath: string activeEntry?: VaultEntry | null /** Direct content of the active note from the editor tab. */ activeNoteContent?: string | null entries?: VaultEntry[] openTabs?: VaultEntry[] noteList?: NoteListItem[] noteListFilter?: { type: string | null; query: string } } function PanelHeader({ onClose, onClear }: { onClose: () => void; onClear: () => void }) { return (
AI Chat
) } function ContextBar({ activeEntry, linkedCount }: { activeEntry: VaultEntry; linkedCount: number }) { return (
{activeEntry.title} {linkedCount > 0 && ( + {linkedCount} linked )}
) } function EmptyState({ hasContext }: { hasContext: boolean }) { return (

{hasContext ? 'Ask about this note and its linked context' : 'Open a note, then ask the AI about it' }

{hasContext ? 'Summarize, find connections, expand ideas' : 'The AI will use the active note as context' }

) } function MessageHistory({ messages, isActive, onOpenNote, onNavigateWikilink, hasContext }: { messages: AiAgentMessage[]; isActive: boolean; onOpenNote?: (path: string) => void; onNavigateWikilink?: (target: string) => void; hasContext: boolean }) { const endRef = useRef(null) useEffect(() => { endRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages, isActive]) return (
{messages.length === 0 && !isActive && } {messages.map((msg, i) => ( ))}
) } export function AiPanel({ onClose, onOpenNote, onFileCreated, onFileModified, onVaultChanged, vaultPath, activeEntry, activeNoteContent, entries, openTabs, noteList, noteListFilter }: AiPanelProps) { const [input, setInput] = useState('') const [pendingRefs, setPendingRefs] = useState([]) const inputRef = useRef(null) const panelRef = useRef(null) const linkedEntries = useMemo(() => { if (!activeEntry || !entries) return [] return collectLinkedEntries(activeEntry, entries) }, [activeEntry, entries]) const contextPrompt = useMemo(() => { if (!activeEntry || !entries) return undefined return buildContextSnapshot({ activeEntry, activeNoteContent: activeNoteContent ?? undefined, openTabs, noteList, noteListFilter, entries, references: pendingRefs.length > 0 ? pendingRefs : undefined, }) }, [activeEntry, activeNoteContent, openTabs, noteList, noteListFilter, entries, pendingRefs]) const fileCallbacks = useMemo(() => ({ onFileCreated, onFileModified, onVaultChanged, }), [onFileCreated, onFileModified, onVaultChanged]) const agent = useAiAgent(vaultPath, contextPrompt, fileCallbacks) const hasContext = !!activeEntry const isActive = agent.status === 'thinking' || agent.status === 'tool-executing' useEffect(() => { const timer = setTimeout(() => inputRef.current?.focus(), 0) return () => clearTimeout(timer) }, []) useEffect(() => { if (isActive) { panelRef.current?.focus() } else { inputRef.current?.focus() } }, [isActive]) const handleEscape = useCallback((e: KeyboardEvent) => { if (e.key === 'Escape' && panelRef.current?.contains(document.activeElement)) { e.preventDefault() onClose() } }, [onClose]) useEffect(() => { window.addEventListener('keydown', handleEscape) return () => window.removeEventListener('keydown', handleEscape) }, [handleEscape]) const handleNavigateWikilink = useCallback((target: string) => { onOpenNote?.(target) }, [onOpenNote]) const handleSend = useCallback((text: string, references: NoteReference[]) => { if (!text.trim() || isActive) return setPendingRefs(references) agent.sendMessage(text, references) setInput('') }, [isActive, agent]) return ( ) }