/** * Assistant Panel Component * * Slide-in panel container for the project assistant chat. * Slides in from the right side of the screen. * Manages conversation state with localStorage persistence. */ import { useState, useEffect, useCallback, useRef } from 'react' import { X, Bot } from 'lucide-react' import { AssistantChat } from './AssistantChat' import { useConversation } from '../hooks/useConversations' import type { ChatMessage } from '../lib/types' import { Button } from '@/components/ui/button' interface AssistantPanelProps { projectName: string isOpen: boolean onClose: () => void } const STORAGE_KEY_PREFIX = 'assistant-conversation-' const WIDTH_STORAGE_KEY = 'assistant-panel-width' const DEFAULT_WIDTH = 400 const MIN_WIDTH = 300 const MAX_WIDTH_VW = 90 function getStoredConversationId(projectName: string): number | null { try { const stored = localStorage.getItem(`${STORAGE_KEY_PREFIX}${projectName}`) if (stored) { const data = JSON.parse(stored) return data.conversationId || null } } catch { // Invalid stored data, ignore } return null } function setStoredConversationId(projectName: string, conversationId: number | null) { const key = `${STORAGE_KEY_PREFIX}${projectName}` if (conversationId) { localStorage.setItem(key, JSON.stringify({ conversationId })) } else { localStorage.removeItem(key) } } export function AssistantPanel({ projectName, isOpen, onClose }: AssistantPanelProps) { // Load initial conversation ID from localStorage const [conversationId, setConversationId] = useState(() => getStoredConversationId(projectName) ) // Fetch conversation details when we have an ID const { data: conversationDetail, isLoading: isLoadingConversation, error: conversationError } = useConversation( projectName, conversationId ) // Clear stored conversation ID if it no longer exists (404 error) useEffect(() => { if (conversationError && conversationId) { const message = conversationError.message.toLowerCase() // Only clear for 404 errors, not transient network issues if (message.includes('not found') || message.includes('404')) { console.warn(`Conversation ${conversationId} not found, clearing stored ID`) setConversationId(null) } } }, [conversationError, conversationId]) // Convert API messages to ChatMessage format for the chat component const initialMessages: ChatMessage[] | undefined = conversationDetail?.messages.map((msg) => ({ id: `db-${msg.id}`, role: msg.role, content: msg.content, timestamp: msg.timestamp ? new Date(msg.timestamp) : new Date(), })) // Persist conversation ID changes to localStorage useEffect(() => { setStoredConversationId(projectName, conversationId) }, [projectName, conversationId]) // Reset conversation ID when project changes useEffect(() => { setConversationId(getStoredConversationId(projectName)) }, [projectName]) // Handle starting a new chat const handleNewChat = useCallback(() => { setConversationId(null) }, []) // Handle selecting a conversation from history const handleSelectConversation = useCallback((id: number) => { setConversationId(id) }, []) // Handle when a new conversation is created (from WebSocket) const handleConversationCreated = useCallback((id: number) => { setConversationId(id) }, []) // Resizable panel width const [panelWidth, setPanelWidth] = useState(() => { try { const stored = localStorage.getItem(WIDTH_STORAGE_KEY) if (stored) return Math.max(MIN_WIDTH, parseInt(stored, 10)) } catch { /* ignore */ } return DEFAULT_WIDTH }) const isResizing = useRef(false) const handleMouseDown = useCallback((e: React.MouseEvent) => { e.preventDefault() isResizing.current = true const startX = e.clientX const startWidth = panelWidth const maxWidth = window.innerWidth * (MAX_WIDTH_VW / 100) const handleMouseMove = (e: MouseEvent) => { if (!isResizing.current) return const delta = startX - e.clientX const newWidth = Math.min(maxWidth, Math.max(MIN_WIDTH, startWidth + delta)) setPanelWidth(newWidth) } const handleMouseUp = () => { isResizing.current = false document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mouseup', handleMouseUp) document.body.style.cursor = '' document.body.style.userSelect = '' // Persist width setPanelWidth((w) => { localStorage.setItem(WIDTH_STORAGE_KEY, String(w)) return w }) } document.body.style.cursor = 'col-resize' document.body.style.userSelect = 'none' document.addEventListener('mousemove', handleMouseMove) document.addEventListener('mouseup', handleMouseUp) }, [panelWidth]) return ( <> {/* Backdrop - click to close */} {isOpen && (