diff --git a/frontend/src/api/editorAI.ts b/frontend/src/api/editorAI.ts new file mode 100644 index 00000000..a9d55800 --- /dev/null +++ b/frontend/src/api/editorAI.ts @@ -0,0 +1,42 @@ +import { apiClient } from './client' +import type { AIActionType } from '@/types' + +export interface SendMessageParams { + sessionId: string + content: string + actionType?: AIActionType + focalNodeId?: string | null +} + +export const editorAIApi = { + startSession: async (flowType: 'troubleshooting' | 'procedural', treeId?: string) => { + const { data } = await apiClient.post('/ai/chat/sessions', { + flow_type: flowType, + tree_id: treeId, + }) + return data + }, + + sendMessage: async ({ sessionId, content, actionType, focalNodeId }: SendMessageParams) => { + const { data } = await apiClient.post(`/ai/chat/sessions/${sessionId}/messages`, { + content, + action_type: actionType || 'open_chat', + focal_node_id: focalNodeId, + }) + return data + }, + + getSession: async (sessionId: string) => { + const { data } = await apiClient.get(`/ai/chat/sessions/${sessionId}`) + return data + }, + + generateFull: async (sessionId: string) => { + const { data } = await apiClient.post(`/ai/chat/sessions/${sessionId}/generate`) + return data + }, + + abandonSession: async (sessionId: string) => { + await apiClient.delete(`/ai/chat/sessions/${sessionId}`) + }, +} diff --git a/frontend/src/hooks/useEditorAI.ts b/frontend/src/hooks/useEditorAI.ts new file mode 100644 index 00000000..57f6cf64 --- /dev/null +++ b/frontend/src/hooks/useEditorAI.ts @@ -0,0 +1,151 @@ +import { useState, useCallback, useRef } from 'react' +import { editorAIApi } from '@/api/editorAI' +import type { + AIActionType, + EditorAIChatMessage, + AISuggestion, + ContextMenuPosition, +} from '@/types' + +interface UseEditorAIOptions { + flowType: 'troubleshooting' | 'procedural' + treeId?: string | null +} + +export function useEditorAI({ flowType, treeId }: UseEditorAIOptions) { + const [isOpen, setIsOpen] = useState(false) + const [focalNodeId, setFocalNodeId] = useState(null) + const [contextMenu, setContextMenu] = useState<{ + position: ContextMenuPosition + nodeId: string + } | null>(null) + const [sessionId, setSessionId] = useState(null) + const [messages, setMessages] = useState([]) + const [input, setInput] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [suggestions, setSuggestions] = useState([]) + + const pendingActionRef = useRef('open_chat') + + const ensureSession = useCallback(async () => { + if (sessionId) return sessionId + try { + const result = await editorAIApi.startSession(flowType, treeId || undefined) + setSessionId(result.session_id) + if (result.greeting) { + setMessages((prev) => [ + ...prev, + { + role: 'assistant' as const, + content: result.greeting, + timestamp: new Date().toISOString(), + }, + ]) + } + return result.session_id + } catch { + return null + } + }, [sessionId, flowType, treeId]) + + const openPanel = useCallback((nodeId?: string, actionType?: AIActionType) => { + setIsOpen(true) + if (nodeId) setFocalNodeId(nodeId) + if (actionType) pendingActionRef.current = actionType + }, []) + + const closePanel = useCallback(() => { + setIsOpen(false) + setContextMenu(null) + }, []) + + const openContextMenu = useCallback((e: React.MouseEvent, nodeId: string) => { + e.preventDefault() + e.stopPropagation() + setContextMenu({ position: { x: e.clientX, y: e.clientY }, nodeId }) + }, []) + + const closeContextMenu = useCallback(() => { + setContextMenu(null) + }, []) + + const sendMessage = useCallback(async () => { + if (!input.trim() || isLoading) return + + const currentInput = input + const currentAction = pendingActionRef.current + const currentFocalNodeId = focalNodeId + + const userMessage: EditorAIChatMessage = { + role: 'user', + content: currentInput, + timestamp: new Date().toISOString(), + action_type: currentAction, + } + setMessages((prev) => [...prev, userMessage]) + setInput('') + setIsLoading(true) + + try { + const sid = await ensureSession() + if (!sid) return + + const result = await editorAIApi.sendMessage({ + sessionId: sid, + content: currentInput, + actionType: currentAction, + focalNodeId: currentFocalNodeId, + }) + + setMessages((prev) => [ + ...prev, + { + role: 'assistant', + content: result.content, + timestamp: new Date().toISOString(), + }, + ]) + } catch { + setMessages((prev) => [ + ...prev, + { + role: 'assistant', + content: 'Sorry, something went wrong. Please try again.', + timestamp: new Date().toISOString(), + }, + ]) + } finally { + setIsLoading(false) + pendingActionRef.current = 'open_chat' + } + }, [input, isLoading, ensureSession, focalNodeId]) + + const triggerAction = useCallback( + (nodeId: string, actionType: AIActionType, prompt: string) => { + setFocalNodeId(nodeId) + pendingActionRef.current = actionType + setInput(prompt) + setIsOpen(true) + }, + [] + ) + + return { + isOpen, + openPanel, + closePanel, + focalNodeId, + setFocalNodeId, + contextMenu, + openContextMenu, + closeContextMenu, + messages, + input, + setInput, + sendMessage, + isLoading, + suggestions, + setSuggestions, + triggerAction, + } +}