feat: add useEditorAI hook and editorAI API client

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-06 23:31:22 -05:00
parent 776c0e43c2
commit 2d2313d119
2 changed files with 193 additions and 0 deletions

View File

@@ -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}`)
},
}

View File

@@ -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<string | null>(null)
const [contextMenu, setContextMenu] = useState<{
position: ContextMenuPosition
nodeId: string
} | null>(null)
const [sessionId, setSessionId] = useState<string | null>(null)
const [messages, setMessages] = useState<EditorAIChatMessage[]>([])
const [input, setInput] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [suggestions, setSuggestions] = useState<AISuggestion[]>([])
const pendingActionRef = useRef<AIActionType>('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,
}
}