feat: add status update generation to assistant chat
Wire StatusUpdateModal into AssistantChatPage with "Update" button in the chat toolbar. Enhance ConcludeSessionModal pause/escalate outcomes to offer ticket notes, client update, or email draft generation instead of static messages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,9 @@ import {
|
||||
ClipboardList,
|
||||
Sparkles,
|
||||
AlertTriangle,
|
||||
FileText,
|
||||
User,
|
||||
Mail,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { MarkdownContent } from '@/components/ui/MarkdownContent'
|
||||
@@ -76,6 +79,7 @@ export function ConcludeSessionModal({
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [streaming, setStreaming] = useState(false)
|
||||
const [streamError, setStreamError] = useState<string | null>(null)
|
||||
const [generatingUpdate, setGeneratingUpdate] = useState(false)
|
||||
const summaryRef = useRef('')
|
||||
|
||||
// Reset state when modal opens
|
||||
@@ -90,6 +94,7 @@ export function ConcludeSessionModal({
|
||||
setError(null)
|
||||
setStreaming(false)
|
||||
setStreamError(null)
|
||||
setGeneratingUpdate(false)
|
||||
summaryRef.current = ''
|
||||
}
|
||||
}, [isOpen])
|
||||
@@ -142,10 +147,9 @@ export function ConcludeSessionModal({
|
||||
})
|
||||
},
|
||||
)
|
||||
} else if (outcome === 'escalated') {
|
||||
setSummary('Session escalated. Ticket notes will be generated when the session is resolved.')
|
||||
} else {
|
||||
setSummary('Session paused. Progress saved — you can resume anytime.')
|
||||
// For paused/escalated: don't set summary yet — show status update options
|
||||
setSummary('')
|
||||
}
|
||||
} catch {
|
||||
setError('Failed to conclude session. Please try again.')
|
||||
@@ -176,6 +180,25 @@ export function ConcludeSessionModal({
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleGenerateStatusUpdate = async (audience: 'ticket_notes' | 'client_update' | 'email_draft') => {
|
||||
if (!sessionId) return
|
||||
setGeneratingUpdate(true)
|
||||
try {
|
||||
const context = outcome === 'escalated' ? 'escalation' : 'status'
|
||||
const result = await aiSessionsApi.generateStatusUpdate(sessionId, {
|
||||
audience,
|
||||
length: 'detailed',
|
||||
context,
|
||||
})
|
||||
setSummary(result.content)
|
||||
setCopied(false)
|
||||
} catch {
|
||||
setSummary('Failed to generate status update. You can copy the conversation from the chat.')
|
||||
} finally {
|
||||
setGeneratingUpdate(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
const selectedOutcome = OUTCOMES.find(o => o.value === outcome)
|
||||
@@ -354,42 +377,115 @@ export function ConcludeSessionModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generated ticket notes */}
|
||||
<div
|
||||
className="rounded-xl border p-5 bg-card"
|
||||
style={{ borderColor: 'var(--color-border-default)' }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-muted-foreground flex items-center gap-1.5">
|
||||
<Sparkles size={10} className="text-primary" />
|
||||
Ticket Notes
|
||||
</span>
|
||||
{streaming && (
|
||||
<Loader2 size={14} className="animate-spin text-primary" />
|
||||
)}
|
||||
</div>
|
||||
{/* Resolved: streamed ticket notes */}
|
||||
{outcome === 'resolved' && (
|
||||
<div
|
||||
className="rounded-xl border p-5 bg-card"
|
||||
style={{ borderColor: 'var(--color-border-default)' }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-muted-foreground flex items-center gap-1.5">
|
||||
<Sparkles size={10} className="text-primary" />
|
||||
Ticket Notes
|
||||
</span>
|
||||
{streaming && (
|
||||
<Loader2 size={14} className="animate-spin text-primary" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Streaming content or skeleton */}
|
||||
{summary ? (
|
||||
{summary ? (
|
||||
<div className="prose-sm text-foreground">
|
||||
<MarkdownContent content={summary} className="text-[0.8125rem] leading-relaxed" />
|
||||
</div>
|
||||
) : streaming ? (
|
||||
<div className="space-y-3 animate-pulse">
|
||||
<div className="h-4 bg-elevated rounded w-1/3" />
|
||||
<div className="h-3 bg-elevated rounded w-full" />
|
||||
<div className="h-3 bg-elevated rounded w-5/6" />
|
||||
<div className="h-4 bg-elevated rounded w-1/4 mt-4" />
|
||||
<div className="h-3 bg-elevated rounded w-full" />
|
||||
<div className="h-3 bg-elevated rounded w-4/5" />
|
||||
</div>
|
||||
) : streamError ? (
|
||||
<div className="flex items-center gap-2 text-sm text-amber-400">
|
||||
<AlertTriangle size={14} />
|
||||
{streamError}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Paused/Escalated: status update options */}
|
||||
{(outcome === 'paused' || outcome === 'escalated') && !summary && !generatingUpdate && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{outcome === 'paused'
|
||||
? 'Session paused. Generate a status update to share progress.'
|
||||
: 'Session escalated. Generate an update to document the handoff.'}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={() => handleGenerateStatusUpdate('ticket_notes')}
|
||||
className="flex w-full items-center gap-3 rounded-lg px-4 py-3 text-left transition-colors hover:bg-[var(--color-bg-elevated)]"
|
||||
style={{ border: '1px solid var(--color-border-default)' }}
|
||||
>
|
||||
<FileText size={18} className="text-muted-foreground shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">Ticket Notes</p>
|
||||
<p className="text-xs text-muted-foreground">Technical, for your PSA</p>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleGenerateStatusUpdate('client_update')}
|
||||
className="flex w-full items-center gap-3 rounded-lg px-4 py-3 text-left transition-colors hover:bg-[var(--color-bg-elevated)]"
|
||||
style={{ border: '1px solid var(--color-border-default)' }}
|
||||
>
|
||||
<User size={18} className="text-muted-foreground shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">Client Update</p>
|
||||
<p className="text-xs text-muted-foreground">Professional, non-technical</p>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleGenerateStatusUpdate('email_draft')}
|
||||
className="flex w-full items-center gap-3 rounded-lg px-4 py-3 text-left transition-colors hover:bg-[var(--color-bg-elevated)]"
|
||||
style={{ border: '1px solid var(--color-border-default)' }}
|
||||
>
|
||||
<Mail size={18} className="text-muted-foreground shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">Email Draft</p>
|
||||
<p className="text-xs text-muted-foreground">Full email with subject line</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Paused/Escalated: generating spinner */}
|
||||
{(outcome === 'paused' || outcome === 'escalated') && generatingUpdate && (
|
||||
<div className="flex flex-col items-center justify-center py-8 gap-3">
|
||||
<Loader2 size={24} className="animate-spin text-orange-400" />
|
||||
<p className="text-sm text-muted-foreground">Generating status update...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Paused/Escalated: generated result */}
|
||||
{(outcome === 'paused' || outcome === 'escalated') && summary && !generatingUpdate && (
|
||||
<div
|
||||
className="rounded-xl border p-5 bg-card"
|
||||
style={{ borderColor: 'var(--color-border-default)' }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-muted-foreground flex items-center gap-1.5">
|
||||
<Sparkles size={10} className="text-primary" />
|
||||
Status Update
|
||||
</span>
|
||||
</div>
|
||||
<div className="prose-sm text-foreground">
|
||||
<MarkdownContent content={summary} className="text-[0.8125rem] leading-relaxed" />
|
||||
</div>
|
||||
) : streaming ? (
|
||||
<div className="space-y-3 animate-pulse">
|
||||
<div className="h-4 bg-elevated rounded w-1/3" />
|
||||
<div className="h-3 bg-elevated rounded w-full" />
|
||||
<div className="h-3 bg-elevated rounded w-5/6" />
|
||||
<div className="h-4 bg-elevated rounded w-1/4 mt-4" />
|
||||
<div className="h-3 bg-elevated rounded w-full" />
|
||||
<div className="h-3 bg-elevated rounded w-4/5" />
|
||||
</div>
|
||||
) : streamError ? (
|
||||
<div className="flex items-center gap-2 text-sm text-amber-400">
|
||||
<AlertTriangle size={14} />
|
||||
{streamError}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -451,9 +547,17 @@ export function ConcludeSessionModal({
|
||||
Resume in New Chat
|
||||
</button>
|
||||
)}
|
||||
{(outcome === 'paused' || outcome === 'escalated') && summary && !generatingUpdate && (
|
||||
<button
|
||||
onClick={() => { setSummary(''); setCopied(false) }}
|
||||
className="flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm font-medium text-muted-foreground hover:text-foreground bg-input border border-border hover:border-border-hover transition-all"
|
||||
>
|
||||
Switch Format
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{summary && !streaming && (
|
||||
{summary && !streaming && !generatingUpdate && (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className={cn(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
import { Sparkles, Send, Loader2, Flag, MessageSquare, Paperclip, Terminal, X, RotateCcw, ImagePlus, ListChecks } from 'lucide-react'
|
||||
import { Sparkles, Send, Loader2, Flag, MessageSquare, Paperclip, Terminal, X, RotateCcw, ImagePlus, ListChecks, FileText } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { uploadsApi } from '@/api/uploads'
|
||||
import type { PendingUpload } from '@/types/upload'
|
||||
@@ -14,6 +14,7 @@ import { ChatSidebar, ChatSidebarCollapsedBar } from '@/components/assistant/Cha
|
||||
import { ChatMessage } from '@/components/assistant/ChatMessage'
|
||||
import { TaskLane } from '@/components/assistant/TaskLane'
|
||||
import { ConcludeSessionModal } from '@/components/assistant/ConcludeSessionModal'
|
||||
import { StatusUpdateModal } from '@/components/flowpilot/StatusUpdateModal'
|
||||
import type { ChatListItem, ConclusionOutcome } from '@/types/assistant-chat'
|
||||
import type { SuggestedFlow } from '@/types/copilot'
|
||||
|
||||
@@ -39,6 +40,7 @@ export default function AssistantChatPage() {
|
||||
const [input, setInput] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showConclude, setShowConclude] = useState(false)
|
||||
const [showStatusUpdate, setShowStatusUpdate] = useState(false)
|
||||
const branching = useBranching()
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
|
||||
const [showLogs, setShowLogs] = useState(false)
|
||||
@@ -746,10 +748,16 @@ export default function AssistantChatPage() {
|
||||
</button>
|
||||
)}
|
||||
{messages.length >= 2 && (
|
||||
<button type="button" onClick={() => setShowConclude(true)} disabled={loading} className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-muted-foreground hover:text-amber-400 hover:bg-amber-400/10 transition-colors disabled:opacity-40" title="Conclude session">
|
||||
<Flag size={14} />
|
||||
<span className="hidden sm:inline">Conclude</span>
|
||||
</button>
|
||||
<>
|
||||
<button type="button" onClick={() => setShowStatusUpdate(true)} disabled={loading} className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-muted-foreground hover:text-orange-400 hover:bg-orange-500/10 transition-colors disabled:opacity-40" title="Share status update">
|
||||
<FileText size={14} />
|
||||
<span className="hidden sm:inline">Update</span>
|
||||
</button>
|
||||
<button type="button" onClick={() => setShowConclude(true)} disabled={loading} className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-muted-foreground hover:text-amber-400 hover:bg-amber-400/10 transition-colors disabled:opacity-40" title="Conclude session">
|
||||
<Flag size={14} />
|
||||
<span className="hidden sm:inline">Conclude</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{!showTaskLane && (activeQuestions.length > 0 || activeActions.length > 0) && (
|
||||
<button
|
||||
@@ -825,6 +833,18 @@ export default function AssistantChatPage() {
|
||||
chatTitle={chats.find(c => c.id === activeChatId)?.title ?? 'Chat'}
|
||||
sessionId={activeChatId}
|
||||
/>
|
||||
|
||||
{/* Status Update Modal */}
|
||||
{activeChatId && (
|
||||
<StatusUpdateModal
|
||||
open={showStatusUpdate}
|
||||
onClose={() => setShowStatusUpdate(false)}
|
||||
onGenerate={(audience, length, context) =>
|
||||
aiSessionsApi.generateStatusUpdate(activeChatId, { audience, length, context })
|
||||
}
|
||||
context="status"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user