import { memo, useCallback, useEffect, useRef, type CSSProperties, type ReactNode } from 'react' import { Sparkle, X, PaperPlaneRight, Plus, Link, Stop } from '@phosphor-icons/react' import { AiMessage } from './AiMessage' import { Button } from '@/components/ui/button' import { ActionTooltip } from '@/components/ui/action-tooltip' import { TooltipProvider } from '@/components/ui/tooltip' import { WikilinkChatInput } from './WikilinkChatInput' import { extractInlineWikilinkReferences } from './inlineWikilinkText' import { aiAgentPermissionModeLabels, type AiAgentPermissionMode, } from '../lib/aiAgentPermissionMode' import { createTranslator, type AppLocale } from '../lib/i18n' import type { AiAgentMessage } from '../hooks/useCliAiAgent' import type { AiAgentReadiness } from '../lib/aiAgents' import type { NoteReference } from '../utils/ai-context' import type { VaultEntry } from '../types' import { cn } from '@/lib/utils' interface AiPanelHeaderProps { agentLabel: string agentReadiness: AiAgentReadiness targetKind?: 'agent' | 'api_model' locale?: AppLocale permissionMode: AiAgentPermissionMode permissionModeDisabled: boolean onPermissionModeChange: (mode: AiAgentPermissionMode) => void onClose: () => void onNewChat: () => void } interface AiPanelContextBarProps { activeEntry: VaultEntry locale?: AppLocale linkedCount: number } interface AiPanelMessageHistoryProps { agentLabel: string agentReadiness: AiAgentReadiness locale?: AppLocale messages: AiAgentMessage[] isActive: boolean onForkMessage?: (messageId: string) => void onOpenNote?: (path: string) => void onNavigateWikilink?: (target: string) => void onRegenerateMessage?: (messageId: string) => void onScrollStateChange?: (scrolled: boolean) => void hasContext: boolean } interface AiPanelComposerProps { entries: VaultEntry[] agentLabel: string agentReadiness: AiAgentReadiness locale?: AppLocale input: string inputRef: React.RefObject isActive: boolean controls?: ReactNode onChange: (value: string) => void onSend: (text: string, references: NoteReference[]) => void onStop: () => void onUnsupportedAiPaste?: (message: string) => void } function getComposerPlaceholder( agentLabel: string, agentReadiness: AiAgentReadiness, t: ReturnType, ): string { if (agentReadiness === 'checking') { return t('ai.panel.placeholder.checking') } if (agentReadiness === 'missing') { return t('ai.panel.placeholder.missing', { agent: agentLabel }) } return t('ai.panel.placeholder.ready', { agent: agentLabel }) } function composerSendButtonStyle(canSend: boolean): CSSProperties { return { background: canSend ? 'var(--primary)' : 'var(--muted)', color: canSend ? 'var(--primary-foreground)' : 'var(--muted-foreground)', borderRadius: 8, width: 30, height: 30, cursor: canSend ? 'pointer' : 'not-allowed', } } function composerStopButtonStyle(): CSSProperties { return { background: 'var(--destructive)', color: 'var(--destructive-foreground)', borderRadius: 8, width: 30, height: 30, cursor: 'pointer', } } function ComposerInput({ disabled, entries, hasControls, input, inputRef, onChange, onSend, onUnsupportedAiPaste, placeholder, }: { disabled: boolean entries: VaultEntry[] hasControls: boolean input: string inputRef: React.RefObject onChange: (value: string) => void onSend: (text: string, references: NoteReference[]) => void onUnsupportedAiPaste?: (message: string) => void placeholder: string }) { return ( ) } function ComposerSendButton({ canSend, entries, input, label, onSend, }: { canSend: boolean entries: VaultEntry[] input: string label: string onSend: (text: string, references: NoteReference[]) => void }) { return ( ) } function ComposerStopButton({ label, onStop, }: { label: string onStop: () => void }) { return ( ) } function ComposerControlsRow({ children, hasControls, sendButton, }: { children?: ReactNode hasControls: boolean sendButton: ReactNode }) { if (!hasControls) return <>{sendButton} return (
{children}
{sendButton}
) } function permissionModeTooltip( mode: AiAgentPermissionMode, t: ReturnType, ): { label: string } { return { label: t(mode === 'power_user' ? 'ai.permission.powerUser.tooltip' : 'ai.permission.safe.tooltip'), } } function headerStatusText({ agentLabel, agentReadiness, modeLabel, t, }: { agentLabel: string agentReadiness: AiAgentReadiness modeLabel: string t: ReturnType }): string { if (agentReadiness === 'checking') return t('ai.panel.status.checking') if (agentReadiness === 'missing') return t('ai.panel.status.missing', { agent: agentLabel }) return t('ai.panel.status.ready', { agent: agentLabel, mode: modeLabel }) } function AiPanelEmptyState({ agentLabel, agentReadiness, hasContext, locale = 'en', }: Pick) { const t = createTranslator(locale) if (agentReadiness === 'checking') { return (

{t('ai.panel.empty.checkingTitle')}

{t('ai.panel.empty.checkingDescription')}

) } if (agentReadiness === 'missing') { return (

{t('ai.panel.empty.missingTitle', { agent: agentLabel })}

{t('ai.panel.empty.missingDescription')}

) } return (

{hasContext ? t('ai.panel.empty.withContextTitle', { agent: agentLabel }) : t('ai.panel.empty.noContextTitle', { agent: agentLabel }) }

{hasContext ? t('ai.panel.empty.withContextDescription') : t('ai.panel.empty.noContextDescription') }

) } export const AiPanelHeader = memo(function AiPanelHeader({ agentLabel, agentReadiness, targetKind = 'agent', locale = 'en', permissionMode, permissionModeDisabled, onPermissionModeChange, onClose, onNewChat, }: AiPanelHeaderProps) { const t = createTranslator(locale) const modeLabel = targetKind === 'api_model' ? t('ai.panel.mode.chat') : aiAgentPermissionModeLabels(permissionMode, locale).short return (
{t('ai.panel.title')} {headerStatusText({ agentLabel, agentReadiness, modeLabel, t })}
{targetKind === 'agent' ? ( ) : (
{t('ai.panel.mode.chatDescription')}
)}
) }) function AiPermissionModeToggle({ value, locale = 'en', disabled, onChange, }: { value: AiAgentPermissionMode locale?: AppLocale disabled: boolean onChange: (mode: AiAgentPermissionMode) => void }) { const t = createTranslator(locale) return (
{(['safe', 'power_user'] as const).map((mode) => { const selected = value === mode return ( ) })}
) } export const AiPanelContextBar = memo(function AiPanelContextBar({ activeEntry, linkedCount, locale = 'en', }: AiPanelContextBarProps) { const t = createTranslator(locale) return (
{activeEntry.title} {linkedCount > 0 && ( {t('ai.panel.linkedCount', { count: linkedCount })} )}
) }) export const AiPanelMessageHistory = memo(function AiPanelMessageHistory({ agentLabel, agentReadiness, locale = 'en', messages, isActive, onForkMessage, onOpenNote, onNavigateWikilink, onRegenerateMessage, onScrollStateChange, hasContext, }: AiPanelMessageHistoryProps) { const containerRef = useRef(null) const endRef = useRef(null) const updateScrollState = useCallback(() => { const element = containerRef.current onScrollStateChange?.((element?.scrollTop ?? 0) > 1) }, [onScrollStateChange]) useEffect(() => { void isActive void messages endRef.current?.scrollIntoView({ behavior: 'smooth' }) if (typeof window.requestAnimationFrame === 'function') window.requestAnimationFrame(updateScrollState) else updateScrollState() }, [messages, isActive, updateScrollState]) return (
{messages.length === 0 && !isActive && ( )} {messages.map((message, index) => ( ))}
) }) export function AiPanelComposer({ entries, agentLabel, agentReadiness, locale = 'en', input, inputRef, isActive, controls, onChange, onSend, onStop, onUnsupportedAiPaste, }: AiPanelComposerProps) { const t = createTranslator(locale) const composerDisabled = isActive || agentReadiness !== 'ready' const canSend = !composerDisabled && input.trim().length > 0 const placeholder = getComposerPlaceholder(agentLabel, agentReadiness, t) const hasControls = controls !== undefined && controls !== null const sendButton = isActive ? : ( ) return (
{controls}
) }