- export_generated: session copy, copy-for-ticket, download - ai_feature_used: copilot, assistant chat, session-to-flow, KB accelerator, flow assist - psa_connected: ConnectWise integration creation - session_shared: share link creation - flow_created: troubleshooting editor, procedural editor, session-to-flow All 9 events from the product analytics plan are now fully wired. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
164 lines
4.6 KiB
TypeScript
164 lines
4.6 KiB
TypeScript
import { useState, useCallback, useRef } from 'react'
|
|
import { editorAIApi } from '@/api/editorAI'
|
|
import { analytics } from '@/lib/analytics'
|
|
import type {
|
|
AIActionType,
|
|
EditorAIChatMessage,
|
|
AISuggestion,
|
|
ContextMenuPosition,
|
|
} from '@/types'
|
|
|
|
interface UseEditorAIOptions {
|
|
flowType: 'troubleshooting' | 'procedural'
|
|
treeId?: string | null
|
|
/** Returns the live flow structure from the editor for AI context */
|
|
getFlowContext?: () => Record<string, unknown> | null
|
|
/** Called when the AI response contains a working_tree update */
|
|
onFlowUpdate?: (workingTree: Record<string, unknown>, metadata?: Record<string, unknown> | null) => void
|
|
}
|
|
|
|
export function useEditorAI({ flowType, treeId, getFlowContext, onFlowUpdate }: 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,
|
|
flowContext: getFlowContext?.() || null,
|
|
})
|
|
analytics.aiFeatureUsed({ feature: 'flow_assist' })
|
|
|
|
setMessages((prev) => [
|
|
...prev,
|
|
{
|
|
role: 'assistant',
|
|
content: result.content,
|
|
timestamp: new Date().toISOString(),
|
|
},
|
|
])
|
|
|
|
// Apply AI-generated flow structure to the editor
|
|
if (result.working_tree && onFlowUpdate) {
|
|
onFlowUpdate(result.working_tree, result.tree_metadata || null)
|
|
}
|
|
} 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, getFlowContext, onFlowUpdate])
|
|
|
|
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,
|
|
}
|
|
}
|