'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')}
"{t('ai_assistant.chat.example.search')}"
"{t('ai_assistant.chat.example.create')}"
"{t('ai_assistant.chat.example.show')}"
)
}
// 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) => (
onPositionChange(pos.value)}
title={t(pos.labelKey)}
>
{pos.icon}
))}
)
}
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') && (
)}
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') && (
)}
setShowDebug(!showDebug)}
/>
>
)}
{showDebug && (
setShowDebug(false)}
/>
)}
,
document.body
) : null
}