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,
|
ClipboardList,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
|
FileText,
|
||||||
|
User,
|
||||||
|
Mail,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { MarkdownContent } from '@/components/ui/MarkdownContent'
|
import { MarkdownContent } from '@/components/ui/MarkdownContent'
|
||||||
@@ -76,6 +79,7 @@ export function ConcludeSessionModal({
|
|||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [streaming, setStreaming] = useState(false)
|
const [streaming, setStreaming] = useState(false)
|
||||||
const [streamError, setStreamError] = useState<string | null>(null)
|
const [streamError, setStreamError] = useState<string | null>(null)
|
||||||
|
const [generatingUpdate, setGeneratingUpdate] = useState(false)
|
||||||
const summaryRef = useRef('')
|
const summaryRef = useRef('')
|
||||||
|
|
||||||
// Reset state when modal opens
|
// Reset state when modal opens
|
||||||
@@ -90,6 +94,7 @@ export function ConcludeSessionModal({
|
|||||||
setError(null)
|
setError(null)
|
||||||
setStreaming(false)
|
setStreaming(false)
|
||||||
setStreamError(null)
|
setStreamError(null)
|
||||||
|
setGeneratingUpdate(false)
|
||||||
summaryRef.current = ''
|
summaryRef.current = ''
|
||||||
}
|
}
|
||||||
}, [isOpen])
|
}, [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 {
|
} else {
|
||||||
setSummary('Session paused. Progress saved — you can resume anytime.')
|
// For paused/escalated: don't set summary yet — show status update options
|
||||||
|
setSummary('')
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setError('Failed to conclude session. Please try again.')
|
setError('Failed to conclude session. Please try again.')
|
||||||
@@ -176,6 +180,25 @@ export function ConcludeSessionModal({
|
|||||||
onClose()
|
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
|
if (!isOpen) return null
|
||||||
|
|
||||||
const selectedOutcome = OUTCOMES.find(o => o.value === outcome)
|
const selectedOutcome = OUTCOMES.find(o => o.value === outcome)
|
||||||
@@ -354,42 +377,115 @@ export function ConcludeSessionModal({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Generated ticket notes */}
|
{/* Resolved: streamed ticket notes */}
|
||||||
<div
|
{outcome === 'resolved' && (
|
||||||
className="rounded-xl border p-5 bg-card"
|
<div
|
||||||
style={{ borderColor: 'var(--color-border-default)' }}
|
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">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<Sparkles size={10} className="text-primary" />
|
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-muted-foreground flex items-center gap-1.5">
|
||||||
Ticket Notes
|
<Sparkles size={10} className="text-primary" />
|
||||||
</span>
|
Ticket Notes
|
||||||
{streaming && (
|
</span>
|
||||||
<Loader2 size={14} className="animate-spin text-primary" />
|
{streaming && (
|
||||||
)}
|
<Loader2 size={14} className="animate-spin text-primary" />
|
||||||
</div>
|
)}
|
||||||
|
</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">
|
<div className="prose-sm text-foreground">
|
||||||
<MarkdownContent content={summary} className="text-[0.8125rem] leading-relaxed" />
|
<MarkdownContent content={summary} className="text-[0.8125rem] leading-relaxed" />
|
||||||
</div>
|
</div>
|
||||||
) : streaming ? (
|
</div>
|
||||||
<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>
|
</div>
|
||||||
@@ -451,9 +547,17 @@ export function ConcludeSessionModal({
|
|||||||
Resume in New Chat
|
Resume in New Chat
|
||||||
</button>
|
</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>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{summary && !streaming && (
|
{summary && !streaming && !generatingUpdate && (
|
||||||
<button
|
<button
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
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 { cn } from '@/lib/utils'
|
||||||
import { uploadsApi } from '@/api/uploads'
|
import { uploadsApi } from '@/api/uploads'
|
||||||
import type { PendingUpload } from '@/types/upload'
|
import type { PendingUpload } from '@/types/upload'
|
||||||
@@ -14,6 +14,7 @@ import { ChatSidebar, ChatSidebarCollapsedBar } from '@/components/assistant/Cha
|
|||||||
import { ChatMessage } from '@/components/assistant/ChatMessage'
|
import { ChatMessage } from '@/components/assistant/ChatMessage'
|
||||||
import { TaskLane } from '@/components/assistant/TaskLane'
|
import { TaskLane } from '@/components/assistant/TaskLane'
|
||||||
import { ConcludeSessionModal } from '@/components/assistant/ConcludeSessionModal'
|
import { ConcludeSessionModal } from '@/components/assistant/ConcludeSessionModal'
|
||||||
|
import { StatusUpdateModal } from '@/components/flowpilot/StatusUpdateModal'
|
||||||
import type { ChatListItem, ConclusionOutcome } from '@/types/assistant-chat'
|
import type { ChatListItem, ConclusionOutcome } from '@/types/assistant-chat'
|
||||||
import type { SuggestedFlow } from '@/types/copilot'
|
import type { SuggestedFlow } from '@/types/copilot'
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ export default function AssistantChatPage() {
|
|||||||
const [input, setInput] = useState('')
|
const [input, setInput] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [showConclude, setShowConclude] = useState(false)
|
const [showConclude, setShowConclude] = useState(false)
|
||||||
|
const [showStatusUpdate, setShowStatusUpdate] = useState(false)
|
||||||
const branching = useBranching()
|
const branching = useBranching()
|
||||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
|
||||||
const [showLogs, setShowLogs] = useState(false)
|
const [showLogs, setShowLogs] = useState(false)
|
||||||
@@ -746,10 +748,16 @@ export default function AssistantChatPage() {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{messages.length >= 2 && (
|
{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} />
|
<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">
|
||||||
<span className="hidden sm:inline">Conclude</span>
|
<FileText size={14} />
|
||||||
</button>
|
<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) && (
|
{!showTaskLane && (activeQuestions.length > 0 || activeActions.length > 0) && (
|
||||||
<button
|
<button
|
||||||
@@ -825,6 +833,18 @@ export default function AssistantChatPage() {
|
|||||||
chatTitle={chats.find(c => c.id === activeChatId)?.title ?? 'Chat'}
|
chatTitle={chats.find(c => c.id === activeChatId)?.title ?? 'Chat'}
|
||||||
sessionId={activeChatId}
|
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>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user