import { useCallback, useRef, type CSSProperties, type ReactNode, type RefObject } from 'react' import { AiPanelComposer, AiPanelHeader, AiPanelMessageHistory, } from './AiPanelChrome' import { DEFAULT_AI_AGENT, getAiAgentDefinition, type AiAgentId, type AiAgentReadiness, } from '../lib/aiAgents' import type { AiTarget } from '../lib/aiTargets' import type { AppLocale } from '../lib/i18n' import { type NoteListItem } from '../utils/ai-context' import type { VaultEntry } from '../types' import { useAiPanelController, type AiPanelController } from './useAiPanelController' import { useAiPanelPromptQueue } from './useAiPanelPromptQueue' import { useAiPanelFocus } from './useAiPanelFocus' export type { AiAgentMessage } from '../hooks/useCliAiAgent' interface AiPanelProps { onClose: () => void onOpenNote?: (path: string) => void onUnsupportedAiPaste?: (message: string) => void defaultAiAgent?: AiAgentId defaultAiTarget?: AiTarget defaultAiAgentReadiness?: AiAgentReadiness defaultAiAgentReady?: boolean locale?: AppLocale onFileCreated?: (relativePath: string) => void onFileModified?: (relativePath: string) => void onVaultChanged?: () => void vaultPath: string vaultPaths?: 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 } } interface AiPanelViewProps { controller: AiPanelController onClose: () => void onOpenNote?: (path: string) => void onUnsupportedAiPaste?: (message: string) => void defaultAiAgent?: AiAgentId defaultAiTarget?: AiTarget defaultAiAgentReadiness?: AiAgentReadiness defaultAiAgentReady?: boolean locale?: AppLocale activeEntry?: VaultEntry | null entries?: VaultEntry[] interactive?: boolean showHeader?: boolean showLeftBorder?: boolean surface?: 'default' | 'sidebar' composerControls?: ReactNode onForkMessage?: (messageId: string) => void onQueuedPromptTarget?: (targetId: string) => void onSendPrompt?: (text: string) => void onMessageHistoryScrollStateChange?: (scrolled: boolean) => void targetId?: string } function readinessFromReadyFlag(ready: boolean | undefined): AiAgentReadiness { return (ready ?? true) ? 'ready' : 'missing' } interface AiPanelViewModel { agentLabel: string defaultAiAgent: AiAgentId defaultAiAgentReadiness: AiAgentReadiness targetKind: AiTarget['kind'] } function resolveAiPanelViewModel({ defaultAiAgent, defaultAiAgentReadiness, defaultAiAgentReady, defaultAiTarget, }: { defaultAiAgent?: AiAgentId defaultAiAgentReadiness?: AiAgentReadiness defaultAiAgentReady?: boolean defaultAiTarget?: AiTarget }): AiPanelViewModel { const resolvedAgent = defaultAiAgent ?? DEFAULT_AI_AGENT const resolvedReadiness = defaultAiAgentReadiness ?? readinessFromReadyFlag(defaultAiAgentReady) return { agentLabel: defaultAiTarget?.label ?? getAiAgentDefinition(resolvedAgent).label, defaultAiAgent: resolvedAgent, defaultAiAgentReadiness: resolvedReadiness, targetKind: defaultAiTarget?.kind ?? 'agent', } } function aiPanelFrameStyle(isActive: boolean, showLeftBorder: boolean): CSSProperties { return { outline: 'none', borderLeft: showLeftBorder ? isActive ? '2px solid var(--accent-blue)' : '1px solid var(--border)' : undefined, animation: showLeftBorder && isActive ? 'ai-border-pulse 2s ease-in-out infinite' : undefined, transition: showLeftBorder ? 'border-color 0.3s ease' : undefined, } } function AiPanelFrame({ children, isActive, panelRef, showLeftBorder, surface, }: { children: ReactNode isActive: boolean panelRef: RefObject showLeftBorder: boolean surface: 'default' | 'sidebar' }) { return ( ) } export function AiPanelView({ controller, onClose, onOpenNote, onUnsupportedAiPaste, defaultAiAgent: providedDefaultAiAgent, defaultAiTarget, defaultAiAgentReadiness: providedDefaultAiAgentReadiness, defaultAiAgentReady: providedDefaultAiAgentReady, locale = 'en', entries, interactive = true, showHeader = true, showLeftBorder = true, surface = 'default', composerControls, onForkMessage, onQueuedPromptTarget, onSendPrompt, onMessageHistoryScrollStateChange, targetId, }: AiPanelViewProps) { const view = resolveAiPanelViewModel({ defaultAiAgent: providedDefaultAiAgent, defaultAiAgentReadiness: providedDefaultAiAgentReadiness, defaultAiAgentReady: providedDefaultAiAgentReady, defaultAiTarget, }) const inputRef = useRef(null) const panelRef = useRef(null) const { agent, input, setInput, hasContext, isActive, permissionMode, handleSend, handleStop, handleNavigateWikilink, handlePermissionModeChange, handleNewChat, } = controller useAiPanelPromptQueue({ agent, currentTargetId: targetId, input, isActive, onTargetChange: onQueuedPromptTarget, setInput, enabled: interactive, }) useAiPanelFocus({ inputRef, panelRef, hasMessages: agent.messages.length > 0, isActive, onClose, enabled: interactive, }) const handleComposerSend = useCallback((text: string, references: Parameters[1]) => { if (!text.trim() || isActive) return onSendPrompt?.(text) handleSend(text, references) }, [handleSend, isActive, onSendPrompt]) return ( {showHeader && ( )} ) } export function AiPanel({ onClose, onOpenNote, onUnsupportedAiPaste, defaultAiAgent: providedDefaultAiAgent, defaultAiTarget, defaultAiAgentReadiness: providedDefaultAiAgentReadiness, defaultAiAgentReady: providedDefaultAiAgentReady, locale = 'en', onFileCreated, onFileModified, onVaultChanged, vaultPath, vaultPaths, activeEntry, activeNoteContent, entries, openTabs, noteList, noteListFilter, }: AiPanelProps) { const defaultAiAgentReadiness = providedDefaultAiAgentReadiness ?? readinessFromReadyFlag(providedDefaultAiAgentReady) const controller = useAiPanelController({ vaultPath, vaultPaths, defaultAiAgent: providedDefaultAiAgent ?? DEFAULT_AI_AGENT, defaultAiTarget, defaultAiAgentReady: providedDefaultAiAgentReady ?? true, defaultAiAgentReadiness, activeEntry, activeNoteContent, entries, openTabs, noteList, noteListFilter, locale, onOpenNote, onFileCreated, onFileModified, onVaultChanged, }) return ( ) }