Files
resolutionflow/frontend/src/hooks/useEditorAI.ts
chihlasm c44edc5088 feat: wire remaining PostHog events across all key user actions (#111)
- 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>
2026-03-16 18:49:01 -04:00

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,
}
}