import { useState, useEffect, useCallback, useRef } from 'react' import { useNavigate } from 'react-router-dom' import { Sparkles, Loader2 } from 'lucide-react' import { analytics } from '@/lib/analytics' import { toast } from '@/lib/toast' import { kbAcceleratorApi } from '@/api' import { UploadScreen } from '@/components/kb-accelerator/UploadScreen' import { ReviewScreen } from '@/components/kb-accelerator/ReviewScreen' import { SuccessScreen } from '@/components/kb-accelerator/SuccessScreen' import { getTreeEditorPath } from '@/lib/routing' import type { KBImport, KBQuotaResponse, KBCommitResponse, KBNodeEditRequest } from '@/types/kbAccelerator' type Phase = 'upload' | 'processing' | 'review' | 'success' type TargetType = 'troubleshooting' | 'procedural' export default function KBAcceleratorPage() { const navigate = useNavigate() const [phase, setPhase] = useState('upload') const [quota, setQuota] = useState(null) const [importId, setImportId] = useState(null) const [kbImport, setKbImport] = useState(null) const [commitResult, setCommitResult] = useState(null) const [loading, setLoading] = useState(false) const pollRef = useRef | null>(null) // Load quota on mount useEffect(() => { kbAcceleratorApi.getQuota().then(setQuota).catch(() => {}) }, []) // Poll for processing status const startPolling = useCallback((id: string) => { if (pollRef.current) clearInterval(pollRef.current) pollRef.current = setInterval(async () => { try { const data = await kbAcceleratorApi.get(id) if (data.status === 'ready') { if (pollRef.current) clearInterval(pollRef.current) setKbImport(data) setPhase('review') } else if (data.status === 'failed') { if (pollRef.current) clearInterval(pollRef.current) toast.error(data.error_message || 'Conversion failed') setPhase('upload') } } catch { // Keep polling on transient errors } }, 2000) }, []) // Cleanup polling on unmount useEffect(() => { return () => { if (pollRef.current) clearInterval(pollRef.current) } }, []) const handleSubmitText = async (content: string, title: string, targetType: TargetType) => { setLoading(true) try { const resp = await kbAcceleratorApi.uploadText({ content, title: title || undefined, target_type: targetType, }) setImportId(resp.id) analytics.aiFeatureUsed({ feature: 'kb_accelerator' }) setPhase('processing') startPolling(resp.id) } catch (err: unknown) { const message = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail ?? 'Upload failed' toast.error(message) } finally { setLoading(false) } } const handleSubmitFile = async (file: File, targetType: TargetType) => { setLoading(true) try { const resp = await kbAcceleratorApi.uploadFile(file, targetType) setImportId(resp.id) setPhase('processing') startPolling(resp.id) } catch (err: unknown) { const message = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail ?? 'Upload failed' toast.error(message) } finally { setLoading(false) } } const handleApproveAll = async () => { if (!importId || !kbImport) return const unapproved = kbImport.nodes.filter(n => !n.user_approved) if (unapproved.length === 0) return setLoading(true) try { await Promise.all( unapproved.map(n => kbAcceleratorApi.editNode(importId, n.id, { operation: 'approve' })) ) setKbImport(prev => { if (!prev) return prev return { ...prev, nodes: prev.nodes.map(n => ({ ...n, user_approved: true })) } }) } catch { toast.error('Failed to approve all nodes') } finally { setLoading(false) } } const handleEditNode = async (nodeId: string, data: KBNodeEditRequest) => { if (!importId) return const updatedNode = await kbAcceleratorApi.editNode(importId, nodeId, data) setKbImport(prev => { if (!prev) return prev if (data.operation === 'delete') { return { ...prev, nodes: prev.nodes.filter(n => n.id !== nodeId) } } if (data.operation === 'insert_after') { const idx = prev.nodes.findIndex(n => n.id === nodeId) const newNodes = [...prev.nodes] newNodes.splice(idx + 1, 0, updatedNode) return { ...prev, nodes: newNodes } } return { ...prev, nodes: prev.nodes.map(n => n.id === updatedNode.id ? updatedNode : n), } }) } const handleCommit = async () => { if (!importId) return setLoading(true) try { const result = await kbAcceleratorApi.commit(importId) setCommitResult(result) setPhase('success') // Refresh quota kbAcceleratorApi.getQuota().then(setQuota).catch(() => {}) } catch (err: unknown) { const detail = (err as { response?: { data?: { detail?: string | { message?: string; validation_errors?: string[] } } } })?.response?.data?.detail if (typeof detail === 'object' && detail !== null) { const msg = detail.message || 'Commit failed' const errors = detail.validation_errors toast.error(errors?.length ? `${msg}\n${errors.join('\n')}` : msg) } else { toast.error(typeof detail === 'string' ? detail : 'Commit failed') } } finally { setLoading(false) } } const handleDiscard = async () => { if (!importId) return setLoading(true) try { await kbAcceleratorApi.delete(importId) resetWizard() } catch { toast.error('Failed to discard') } finally { setLoading(false) } } const resetWizard = () => { setPhase('upload') setImportId(null) setKbImport(null) setCommitResult(null) setLoading(false) } return (
{/* Page title */}

KB Accelerator

{/* Phases */} {phase === 'upload' && ( )} {phase === 'processing' && (

Converting your KB article...

AI is analyzing your content and generating an interactive flow.

)} {phase === 'review' && kbImport && (
)} {phase === 'success' && commitResult && ( { const path = getTreeEditorPath(commitResult.tree_id, commitResult.tree_type as 'troubleshooting' | 'procedural') navigate(path) }} onConvertAnother={resetWizard} /> )}
) }