diff --git a/frontend/src/components/session/ResolutionOutputPanel.tsx b/frontend/src/components/session/ResolutionOutputPanel.tsx new file mode 100644 index 00000000..de920069 --- /dev/null +++ b/frontend/src/components/session/ResolutionOutputPanel.tsx @@ -0,0 +1,239 @@ +import { useState, useEffect, useCallback } from 'react' +import { FileText, BookOpen, MessageSquare, Pencil, Copy, Send, Check } from 'lucide-react' +import { cn } from '@/lib/utils' +import { toast } from '@/lib/toast' +import { resolutionsApi } from '@/api/resolutions' +import type { + ResolutionOutputResponse, + ResolutionOutputType, +} from '@/types/branching' + +interface Tab { + type: ResolutionOutputType + label: string + icon: React.ElementType +} + +const TABS: Tab[] = [ + { type: 'psa_ticket_notes', label: 'PSA Notes', icon: FileText }, + { type: 'knowledge_base', label: 'KB Article', icon: BookOpen }, + { type: 'client_summary', label: 'Client Summary', icon: MessageSquare }, +] + +interface ResolutionOutputPanelProps { + sessionId: string + className?: string +} + +export function ResolutionOutputPanel({ sessionId, className }: ResolutionOutputPanelProps) { + const [outputs, setOutputs] = useState([]) + const [activeType, setActiveType] = useState('psa_ticket_notes') + const [isLoading, setIsLoading] = useState(false) + const [isEditing, setIsEditing] = useState(false) + const [editValue, setEditValue] = useState('') + const [isSaving, setIsSaving] = useState(false) + const [isPushing, setIsPushing] = useState(false) + const [copied, setCopied] = useState(false) + + const loadOutputs = useCallback(async () => { + setIsLoading(true) + try { + const result = await resolutionsApi.getOutputs(sessionId) + setOutputs(result.outputs) + } catch { + toast.error('Failed to load resolution outputs') + } finally { + setIsLoading(false) + } + }, [sessionId]) + + useEffect(() => { + loadOutputs() + }, [loadOutputs]) + + const activeOutput = outputs.find(o => o.output_type === activeType) ?? null + const displayContent = activeOutput?.edited_content ?? activeOutput?.generated_content ?? '' + + function handleTabChange(type: ResolutionOutputType) { + setActiveType(type) + setIsEditing(false) + setEditValue('') + } + + function handleEditToggle() { + if (!isEditing) { + setEditValue(displayContent) + setIsEditing(true) + } else { + setIsEditing(false) + setEditValue('') + } + } + + async function handleSaveEdit() { + if (!activeOutput) return + setIsSaving(true) + try { + const updated = await resolutionsApi.editOutput(sessionId, activeOutput.id, { + edited_content: editValue, + }) + setOutputs(prev => prev.map(o => (o.id === updated.id ? updated : o))) + setIsEditing(false) + setEditValue('') + toast.success('Output saved') + } catch { + toast.error('Failed to save edit') + } finally { + setIsSaving(false) + } + } + + async function handleCopy() { + try { + await navigator.clipboard.writeText(displayContent) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } catch { + toast.error('Failed to copy to clipboard') + } + } + + async function handlePushToPsa() { + if (!activeOutput) return + setIsPushing(true) + try { + const updated = await resolutionsApi.pushOutput(sessionId, activeOutput.id, { + destination: 'psa', + }) + setOutputs(prev => prev.map(o => (o.id === updated.id ? updated : o))) + toast.success('Pushed to PSA') + } catch { + toast.error('Failed to push to PSA') + } finally { + setIsPushing(false) + } + } + + return ( +
+ {/* Tab bar */} +
+ {TABS.map(tab => { + const Icon = tab.icon + const isActive = tab.type === activeType + return ( + + ) + })} +
+ + {/* Content */} +
+ {isLoading ? ( +
+ Loading… +
+ ) : !activeOutput ? ( +
+ No output generated yet. +
+ ) : isEditing ? ( +