Files
tolaria/src/components/AiPanel.tsx
2026-04-28 14:03:10 +02:00

202 lines
5.6 KiB
TypeScript

import { useRef } from 'react'
import {
AiPanelComposer,
AiPanelContextBar,
AiPanelHeader,
AiPanelMessageHistory,
} from './AiPanelChrome'
import {
DEFAULT_AI_AGENT,
getAiAgentDefinition,
type AiAgentId,
type AiAgentReadiness,
} from '../lib/aiAgents'
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
onCopyMcpConfig?: () => void
onOpenNote?: (path: string) => void
onUnsupportedAiPaste?: (message: string) => void
defaultAiAgent?: AiAgentId
defaultAiAgentReadiness?: AiAgentReadiness
defaultAiAgentReady?: boolean
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 }
}
interface AiPanelViewProps {
controller: AiPanelController
onClose: () => void
onCopyMcpConfig?: () => void
onOpenNote?: (path: string) => void
onUnsupportedAiPaste?: (message: string) => void
defaultAiAgent?: AiAgentId
defaultAiAgentReadiness?: AiAgentReadiness
defaultAiAgentReady?: boolean
activeEntry?: VaultEntry | null
entries?: VaultEntry[]
}
function readinessFromReadyFlag(ready: boolean | undefined): AiAgentReadiness {
return (ready ?? true) ? 'ready' : 'missing'
}
export function AiPanelView({
controller,
onClose,
onCopyMcpConfig,
onOpenNote,
onUnsupportedAiPaste,
defaultAiAgent: providedDefaultAiAgent,
defaultAiAgentReadiness: providedDefaultAiAgentReadiness,
defaultAiAgentReady: providedDefaultAiAgentReady,
activeEntry,
entries,
}: AiPanelViewProps) {
const defaultAiAgent = providedDefaultAiAgent ?? DEFAULT_AI_AGENT
const defaultAiAgentReadiness = providedDefaultAiAgentReadiness
?? readinessFromReadyFlag(providedDefaultAiAgentReady)
const inputRef = useRef<HTMLDivElement>(null)
const panelRef = useRef<HTMLElement>(null)
const agentLabel = getAiAgentDefinition(defaultAiAgent).label
const {
agent,
input,
setInput,
linkedEntries,
hasContext,
isActive,
handleSend,
handleNavigateWikilink,
handleNewChat,
} = controller
useAiPanelPromptQueue({ agent, input, isActive, setInput })
useAiPanelFocus({
inputRef,
panelRef,
hasMessages: agent.messages.length > 0,
isActive,
onClose,
})
return (
<aside
ref={panelRef}
tabIndex={-1}
className="flex flex-1 flex-col overflow-hidden bg-background text-foreground"
style={{
outline: 'none',
borderLeft: isActive
? '2px solid var(--accent-blue)'
: '1px solid var(--border)',
animation: isActive ? 'ai-border-pulse 2s ease-in-out infinite' : undefined,
transition: 'border-color 0.3s ease',
}}
data-testid="ai-panel"
data-ai-active={isActive || undefined}
>
<AiPanelHeader
agentLabel={agentLabel}
agentReadiness={defaultAiAgentReadiness}
onClose={onClose}
onCopyMcpConfig={onCopyMcpConfig}
onNewChat={handleNewChat}
/>
{activeEntry && (
<AiPanelContextBar activeEntry={activeEntry} linkedCount={linkedEntries.length} />
)}
<AiPanelMessageHistory
agentLabel={agentLabel}
agentReadiness={defaultAiAgentReadiness}
messages={agent.messages}
isActive={isActive}
onOpenNote={onOpenNote}
onNavigateWikilink={handleNavigateWikilink}
hasContext={hasContext}
/>
<AiPanelComposer
entries={entries ?? []}
agentLabel={agentLabel}
agentReadiness={defaultAiAgentReadiness}
input={input}
inputRef={inputRef}
isActive={isActive}
onChange={setInput}
onSend={handleSend}
onUnsupportedAiPaste={onUnsupportedAiPaste}
/>
</aside>
)
}
export function AiPanel({
onClose,
onCopyMcpConfig,
onOpenNote,
onUnsupportedAiPaste,
defaultAiAgent: providedDefaultAiAgent,
defaultAiAgentReadiness: providedDefaultAiAgentReadiness,
defaultAiAgentReady: providedDefaultAiAgentReady,
onFileCreated,
onFileModified,
onVaultChanged,
vaultPath,
activeEntry,
activeNoteContent,
entries,
openTabs,
noteList,
noteListFilter,
}: AiPanelProps) {
const defaultAiAgentReadiness = providedDefaultAiAgentReadiness
?? readinessFromReadyFlag(providedDefaultAiAgentReady)
const controller = useAiPanelController({
vaultPath,
defaultAiAgent: providedDefaultAiAgent ?? DEFAULT_AI_AGENT,
defaultAiAgentReady: providedDefaultAiAgentReady ?? true,
defaultAiAgentReadiness,
activeEntry,
activeNoteContent,
entries,
openTabs,
noteList,
noteListFilter,
onOpenNote,
onFileCreated,
onFileModified,
onVaultChanged,
})
return (
<AiPanelView
controller={controller}
onClose={onClose}
onCopyMcpConfig={onCopyMcpConfig}
onOpenNote={onOpenNote}
onUnsupportedAiPaste={onUnsupportedAiPaste}
defaultAiAgent={providedDefaultAiAgent}
defaultAiAgentReadiness={defaultAiAgentReadiness}
defaultAiAgentReady={providedDefaultAiAgentReady}
activeEntry={activeEntry}
entries={entries}
/>
)
}