'use client' import * as React from 'react' import { createPortal } from 'react-dom' import { Command } from 'cmdk' import { Loader2, Send, Square, X, Minimize2, PanelRight, PanelLeft, PanelBottom, MessageCircle, } from 'lucide-react' import { cn } from '@open-mercato/shared/lib/utils' import { Button } from '@open-mercato/ui/primitives/button' import { useT } from '@open-mercato/shared/lib/i18n/context' import { useCommandPaletteContext } from '../CommandPalette/CommandPaletteProvider' import { CommandInput } from '../CommandPalette/CommandInput' import { CommandHeader } from '../CommandPalette/CommandHeader' import { CommandFooter } from '../CommandPalette/CommandFooter' import { ToolChatPage } from '../CommandPalette/ToolChatPage' import { DebugPanel } from '../CommandPalette/DebugPanel' import { AiDot } from '../AiDot' import type { DockPosition } from '../../types' import { useDockPosition } from '../../hooks/useDockPosition' // Idle state - shown when palette is open but no query submitted function IdleState() { const t = useT() return (

{t('ai_assistant.chat.idleTitle')}

{t('ai_assistant.chat.idleExamples')}

) } // Routing indicator - shown while fast model analyzes intent function RoutingIndicator() { const t = useT() return (
{t('ai_assistant.status.analyzing')}
) } interface DockControlsProps { position: DockPosition onPositionChange: (position: DockPosition) => void onMinimize: () => void onClose: () => void } function DockControls({ position, onPositionChange, onMinimize, onClose, }: DockControlsProps) { const t = useT() const positions: { value: DockPosition; icon: React.ReactNode; labelKey: string }[] = [ { value: 'floating', icon: , labelKey: 'ai_assistant.dock.floating' }, { value: 'left', icon: , labelKey: 'ai_assistant.dock.left' }, { value: 'bottom', icon: , labelKey: 'ai_assistant.dock.bottom' }, { value: 'right', icon: , labelKey: 'ai_assistant.dock.right' }, ] return (
{positions.map((pos) => ( ))}
) } const FLOATING_POSITION_STYLE: React.CSSProperties = { bottom: 24, right: 24, } export function DockableChat() { const t = useT() const { state, isThinking, agentStatus, isSessionAuthorized, messages, pendingToolCalls, selectedTool, close, reset, handleSubmit, sendAgenticMessage, stopExecution, approveToolCall, rejectToolCall, debugEvents, showDebug, setShowDebug, clearDebugEvents, pendingQuestion, answerQuestion, } = useCommandPaletteContext() const { dockState, setPosition, toggleMinimized, setMinimized, isFloating, isHydrated, } = useDockPosition() const { isOpen, phase, isLoading, isStreaming, connectionStatus, } = state const [localInput, setLocalInput] = React.useState('') const [chatInput, setChatInput] = React.useState('') const chatInputRef = React.useRef(null) // Reset local input when phase changes to idle React.useEffect(() => { if (phase === 'idle') { setLocalInput('') setChatInput('') } }, [phase]) // Focus chat input when entering chatting phase React.useEffect(() => { if (phase === 'chatting' || phase === 'confirming' || phase === 'executing') { setTimeout(() => chatInputRef.current?.focus(), 50) } }, [phase]) const handleInputSubmit = async () => { const query = localInput.trim() if (!query) return setLocalInput('') await handleSubmit(query) } const handleInputKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && phase === 'idle' && localInput.trim()) { e.preventDefault() handleInputSubmit() } } const handleChatSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!chatInput.trim() || isStreaming) return const content = chatInput setChatInput('') await sendAgenticMessage(content) } const handleChatKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Escape') { e.stopPropagation() if (phase !== 'idle') { reset() } else { close() } } if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() if (chatInput.trim() && !isStreaming) { const content = chatInput setChatInput('') sendAgenticMessage(content) } } } // Don't render until hydrated to avoid SSR mismatch if (!isHydrated) return null // When minimized in any mode, show the AiDot in bottom-right corner if (isOpen && dockState.isMinimized) { return typeof document !== 'undefined' ? createPortal( setMinimized(false)} isActive={isStreaming || isThinking} hasMessages={messages.length > 0} position="bottom-right" />, document.body ) : null } // Render as floating panel when in floating mode if (isFloating) { if (!isOpen) return null return typeof document !== 'undefined' ? createPortal( <>
{/* Dock controls header */}
{phase === 'idle' && ( )}
{phase === 'idle' && !localInput && } {phase === 'routing' && } {(phase === 'chatting' || phase === 'confirming' || phase === 'executing') && ( )}
{(phase === 'chatting' || phase === 'confirming' || phase === 'executing') && (
setChatInput(e.target.value)} onKeyDown={handleChatKeyDown} placeholder={t('ai_assistant.chat.describePlaceholder')} className={cn( 'flex-1 bg-muted rounded-lg px-4 py-2 text-sm outline-none', 'focus-visible:ring-2 focus-visible:ring-ring', 'disabled:opacity-50' )} disabled={isStreaming} />
)} setShowDebug(!showDebug)} />
{showDebug && (
setShowDebug(false)} />
)} , document.body ) : null } // Render as docked panel (right, left, bottom) if (!isOpen) return null const positionStyles: Record, React.CSSProperties> = { right: { position: 'fixed', top: 0, right: 0, bottom: 0, width: dockState.width, zIndex: 40, }, left: { position: 'fixed', top: 0, left: 0, bottom: 0, width: dockState.width, zIndex: 40, }, bottom: { position: 'fixed', bottom: 0, left: 0, right: 0, height: dockState.height, zIndex: 40, }, } const panelPosition = dockState.position as Exclude return typeof document !== 'undefined' ? createPortal(
{/* Docked panel header */}
{!dockState.isMinimized && ( <> {phase === 'idle' && ( )}
{phase === 'idle' && !localInput && } {phase === 'routing' && } {(phase === 'chatting' || phase === 'confirming' || phase === 'executing') && ( )}
{(phase === 'chatting' || phase === 'confirming' || phase === 'executing') && (
setChatInput(e.target.value)} onKeyDown={handleChatKeyDown} placeholder={t('ai_assistant.chat.describePlaceholder')} className={cn( 'flex-1 bg-muted rounded-lg px-4 py-2 text-sm outline-none', 'focus-visible:ring-2 focus-visible:ring-ring', 'disabled:opacity-50' )} disabled={isStreaming} />
)} setShowDebug(!showDebug)} />
)} {showDebug && (
setShowDebug(false)} />
)}
, document.body ) : null }