feat: mid-session status updates — ticket notes, client updates, email drafts
Engineers can now generate AI-powered status updates during active FlowPilot
sessions and after resolve/escalate. Three audiences (Ticket Notes, Client
Update, Email Draft) with Quick/Detailed length options. Copy to clipboard
with one click. Client names auto-inserted from intake/PSA context.
Backend: new endpoint POST /ai-sessions/{id}/status-update with audience-aware
system prompts. Frontend: StatusUpdateModal with 2-step selection flow,
Share Update button in action bar, Share Resolution/Escalation on completed
sessions. Also updates Solutions Library spec with Community tier design.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,8 @@ import type {
|
||||
AISessionSearchResult,
|
||||
SimilarSession,
|
||||
PickupSessionRequest,
|
||||
StatusUpdateRequest,
|
||||
StatusUpdateResponse,
|
||||
} from '@/types/ai-session'
|
||||
|
||||
export const aiSessionsApi = {
|
||||
@@ -136,6 +138,14 @@ export const aiSessionsApi = {
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
async generateStatusUpdate(sessionId: string, data: StatusUpdateRequest): Promise<StatusUpdateResponse> {
|
||||
const response = await apiClient.post<StatusUpdateResponse>(
|
||||
`/ai-sessions/${sessionId}/status-update`,
|
||||
data
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
}
|
||||
|
||||
export default aiSessionsApi
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { useState } from 'react'
|
||||
import { CheckCircle2, ArrowUpRight, Pause, X } from 'lucide-react'
|
||||
import { CheckCircle2, ArrowUpRight, Pause, X, FileText } from 'lucide-react'
|
||||
import { EscalateModal } from './EscalateModal'
|
||||
import type { ResolveSessionRequest, EscalateSessionRequest, SessionDocumentation } from '@/types/ai-session'
|
||||
import { StatusUpdateModal } from './StatusUpdateModal'
|
||||
import type {
|
||||
ResolveSessionRequest,
|
||||
EscalateSessionRequest,
|
||||
SessionDocumentation,
|
||||
StatusUpdateAudience,
|
||||
StatusUpdateLength,
|
||||
StatusUpdateContext,
|
||||
StatusUpdateResponse,
|
||||
} from '@/types/ai-session'
|
||||
|
||||
interface FlowPilotActionBarProps {
|
||||
canResolve: boolean
|
||||
@@ -9,10 +18,12 @@ interface FlowPilotActionBarProps {
|
||||
isProcessing: boolean
|
||||
hasPsaTicket?: boolean
|
||||
sessionId?: string
|
||||
canShareUpdate?: boolean
|
||||
onResolve: (data: ResolveSessionRequest) => Promise<SessionDocumentation>
|
||||
onEscalate: (data: EscalateSessionRequest) => Promise<SessionDocumentation>
|
||||
onPause?: () => Promise<void>
|
||||
onAbandon?: () => Promise<void>
|
||||
onGenerateStatusUpdate?: (audience: StatusUpdateAudience, length: StatusUpdateLength, context: StatusUpdateContext) => Promise<StatusUpdateResponse>
|
||||
}
|
||||
|
||||
export function FlowPilotActionBar({
|
||||
@@ -21,14 +32,17 @@ export function FlowPilotActionBar({
|
||||
isProcessing,
|
||||
hasPsaTicket = false,
|
||||
sessionId,
|
||||
canShareUpdate = false,
|
||||
onResolve,
|
||||
onEscalate,
|
||||
onPause,
|
||||
onAbandon,
|
||||
onGenerateStatusUpdate,
|
||||
}: FlowPilotActionBarProps) {
|
||||
const [showResolve, setShowResolve] = useState(false)
|
||||
const [showEscalate, setShowEscalate] = useState(false)
|
||||
const [showAbandon, setShowAbandon] = useState(false)
|
||||
const [showStatusUpdate, setShowStatusUpdate] = useState(false)
|
||||
const [resolutionSummary, setResolutionSummary] = useState('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
@@ -90,6 +104,17 @@ export function FlowPilotActionBar({
|
||||
<ArrowUpRight size={16} />
|
||||
Escalate
|
||||
</button>
|
||||
{canShareUpdate && onGenerateStatusUpdate && (
|
||||
<button
|
||||
onClick={() => setShowStatusUpdate(true)}
|
||||
disabled={isProcessing}
|
||||
className="flex flex-1 sm:flex-initial items-center justify-center gap-2 rounded-lg bg-cyan-500/10 border border-cyan-500/20 px-4 py-2 min-h-[44px] text-sm font-medium text-cyan-400 hover:bg-cyan-500/20 disabled:opacity-40 disabled:pointer-events-none transition-colors"
|
||||
>
|
||||
<FileText size={16} />
|
||||
<span className="hidden sm:inline">Share Update</span>
|
||||
<span className="sm:hidden">Update</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2 sm:ml-auto">
|
||||
{onPause && (
|
||||
@@ -184,6 +209,17 @@ export function FlowPilotActionBar({
|
||||
hasPsaTicket={hasPsaTicket}
|
||||
sessionId={sessionId}
|
||||
/>
|
||||
|
||||
{/* Status Update modal */}
|
||||
{onGenerateStatusUpdate && (
|
||||
<StatusUpdateModal
|
||||
open={showStatusUpdate}
|
||||
onClose={() => setShowStatusUpdate(false)}
|
||||
onGenerate={onGenerateStatusUpdate}
|
||||
context="status"
|
||||
hasPsaTicket={hasPsaTicket}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Network, Clock, Hash, Play, Ticket, ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import { Network, Clock, Hash, Play, Ticket, ChevronDown, ChevronUp, FileText } from 'lucide-react'
|
||||
import type {
|
||||
AISessionDetail,
|
||||
AISessionStepResponse,
|
||||
@@ -7,12 +7,17 @@ import type {
|
||||
ResolveSessionRequest,
|
||||
EscalateSessionRequest,
|
||||
SessionDocumentation,
|
||||
StatusUpdateAudience,
|
||||
StatusUpdateLength,
|
||||
StatusUpdateContext,
|
||||
StatusUpdateResponse,
|
||||
} from '@/types/ai-session'
|
||||
import { ConfidenceIndicator } from './ConfidenceIndicator'
|
||||
import { FlowPilotStepCard } from './FlowPilotStepCard'
|
||||
import { FlowPilotActionBar } from './FlowPilotActionBar'
|
||||
import { FlowPilotMessageBar } from './FlowPilotMessageBar'
|
||||
import { SessionDocView } from './SessionDocView'
|
||||
import { StatusUpdateModal } from './StatusUpdateModal'
|
||||
import { SessionTicketCard } from './SessionTicketCard'
|
||||
import { SimilarSessions } from './SimilarSessions'
|
||||
import { TicketPickerModal } from '@/components/session/TicketPickerModal'
|
||||
@@ -39,6 +44,7 @@ interface FlowPilotSessionProps {
|
||||
onAbandon?: () => Promise<void>
|
||||
onRate: (rating: number) => void
|
||||
onReloadSession?: () => Promise<void>
|
||||
onGenerateStatusUpdate?: (audience: StatusUpdateAudience, length: StatusUpdateLength, context: StatusUpdateContext) => Promise<StatusUpdateResponse>
|
||||
}
|
||||
|
||||
export function FlowPilotSession({
|
||||
@@ -60,10 +66,12 @@ export function FlowPilotSession({
|
||||
onAbandon,
|
||||
onRate,
|
||||
onReloadSession,
|
||||
onGenerateStatusUpdate,
|
||||
}: FlowPilotSessionProps) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const [showTicketPicker, setShowTicketPicker] = useState(false)
|
||||
const [linkingTicket, setLinkingTicket] = useState(false)
|
||||
const [showShareCommunication, setShowShareCommunication] = useState(false)
|
||||
const [showMobileSidebar, setShowMobileSidebar] = useState(false)
|
||||
|
||||
const handleLinkTicket = async (ticketId: string, _ticket: PSATicketInfo) => {
|
||||
@@ -115,9 +123,24 @@ export function FlowPilotSession({
|
||||
|
||||
// Show documentation view for completed sessions
|
||||
if (isCompleted && documentation) {
|
||||
const shareContext = session.status === 'resolved' ? 'resolution' as const : 'escalation' as const
|
||||
const shareLabel = session.status === 'resolved' ? 'Share Resolution' : 'Share Escalation'
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex-1 overflow-y-auto p-3 sm:p-4 lg:p-6">
|
||||
{/* Share Resolution/Escalation button */}
|
||||
{onGenerateStatusUpdate && (
|
||||
<div className="mb-4">
|
||||
<button
|
||||
onClick={() => setShowShareCommunication(true)}
|
||||
className="flex items-center gap-2 rounded-lg bg-cyan-500/10 border border-cyan-500/20 px-4 py-2.5 text-sm font-medium text-cyan-400 hover:bg-cyan-500/20 transition-colors"
|
||||
>
|
||||
<FileText size={16} />
|
||||
{shareLabel}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SessionDocView
|
||||
documentation={documentation}
|
||||
onRate={onRate}
|
||||
@@ -129,6 +152,17 @@ export function FlowPilotSession({
|
||||
ticketId={session.psa_ticket_id}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Share communication modal for resolved/escalated sessions */}
|
||||
{onGenerateStatusUpdate && (
|
||||
<StatusUpdateModal
|
||||
open={showShareCommunication}
|
||||
onClose={() => setShowShareCommunication(false)}
|
||||
onGenerate={onGenerateStatusUpdate}
|
||||
context={shareContext}
|
||||
hasPsaTicket={!!session.psa_ticket_id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -320,10 +354,12 @@ export function FlowPilotSession({
|
||||
isProcessing={isProcessing}
|
||||
hasPsaTicket={!!session.psa_ticket_id}
|
||||
sessionId={session.id}
|
||||
canShareUpdate={allSteps.length >= 2}
|
||||
onResolve={onResolve}
|
||||
onEscalate={onEscalate}
|
||||
onPause={onPause}
|
||||
onAbandon={onAbandon}
|
||||
onGenerateStatusUpdate={onGenerateStatusUpdate}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
241
frontend/src/components/flowpilot/StatusUpdateModal.tsx
Normal file
241
frontend/src/components/flowpilot/StatusUpdateModal.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
import { useState } from 'react'
|
||||
import { FileText, User, Mail, Zap, AlignLeft, Copy, Check, RotateCcw, ArrowLeftRight, Loader2 } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from '@/lib/toast'
|
||||
import type { StatusUpdateAudience, StatusUpdateLength, StatusUpdateContext, StatusUpdateResponse } from '@/types/ai-session'
|
||||
|
||||
interface StatusUpdateModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onGenerate: (audience: StatusUpdateAudience, length: StatusUpdateLength, context: StatusUpdateContext) => Promise<StatusUpdateResponse>
|
||||
context?: StatusUpdateContext
|
||||
hasPsaTicket?: boolean
|
||||
}
|
||||
|
||||
const AUDIENCES: { value: StatusUpdateAudience; icon: typeof FileText; label: string; description: string }[] = [
|
||||
{ value: 'ticket_notes', icon: FileText, label: 'Ticket Notes', description: 'Technical, for your PSA' },
|
||||
{ value: 'client_update', icon: User, label: 'Client Update', description: 'Professional, non-technical' },
|
||||
{ value: 'email_draft', icon: Mail, label: 'Email Draft', description: 'Full email with subject line' },
|
||||
]
|
||||
|
||||
const LENGTHS: { value: StatusUpdateLength; icon: typeof Zap; label: string; description: string }[] = [
|
||||
{ value: 'quick', icon: Zap, label: 'Quick', description: '1-2 sentences' },
|
||||
{ value: 'detailed', icon: AlignLeft, label: 'Detailed', description: 'Full breakdown' },
|
||||
]
|
||||
|
||||
type ModalStep = 'audience' | 'length' | 'generating' | 'result'
|
||||
|
||||
export function StatusUpdateModal({ open, onClose, onGenerate, context = 'status', hasPsaTicket = false }: StatusUpdateModalProps) {
|
||||
const [step, setStep] = useState<ModalStep>('audience')
|
||||
const [audience, setAudience] = useState<StatusUpdateAudience | null>(null)
|
||||
const [length, setLength] = useState<StatusUpdateLength | null>(null)
|
||||
const [result, setResult] = useState<StatusUpdateResponse | null>(null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const contextLabels: Record<StatusUpdateContext, string> = {
|
||||
status: 'Share Status Update',
|
||||
resolution: 'Share Resolution',
|
||||
escalation: 'Share Escalation',
|
||||
}
|
||||
|
||||
const handleAudienceSelect = (value: StatusUpdateAudience) => {
|
||||
setAudience(value)
|
||||
setStep('length')
|
||||
}
|
||||
|
||||
const handleLengthSelect = async (value: StatusUpdateLength) => {
|
||||
if (!audience) return
|
||||
setLength(value)
|
||||
setStep('generating')
|
||||
try {
|
||||
const res = await onGenerate(audience, value, context)
|
||||
setResult(res)
|
||||
setStep('result')
|
||||
} catch {
|
||||
setStep('audience')
|
||||
setAudience(null)
|
||||
setLength(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (!result) return
|
||||
await navigator.clipboard.writeText(result.content)
|
||||
setCopied(true)
|
||||
toast.success('Copied to clipboard')
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
const handleRegenerate = async () => {
|
||||
if (!audience || !length) return
|
||||
setStep('generating')
|
||||
try {
|
||||
const res = await onGenerate(audience, length, context)
|
||||
setResult(res)
|
||||
setStep('result')
|
||||
} catch {
|
||||
setStep('result')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSwitchAudience = () => {
|
||||
setStep('audience')
|
||||
setAudience(null)
|
||||
setLength(null)
|
||||
setResult(null)
|
||||
setCopied(false)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setStep('audience')
|
||||
setAudience(null)
|
||||
setLength(null)
|
||||
setResult(null)
|
||||
setCopied(false)
|
||||
onClose()
|
||||
}
|
||||
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="card-flat w-full max-w-full sm:max-w-lg mx-0 sm:mx-4 rounded-t-2xl sm:rounded-2xl overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 sm:px-6 pt-4 sm:pt-6 pb-3" style={{ borderBottom: '1px solid var(--color-border-default)' }}>
|
||||
<h3 className="font-heading text-lg font-semibold text-foreground">
|
||||
{contextLabels[context]}
|
||||
</h3>
|
||||
<button onClick={handleClose} className="text-muted-foreground hover:text-foreground text-sm">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="px-4 sm:px-6 py-4 sm:py-5">
|
||||
{/* Step 1: Audience */}
|
||||
{step === 'audience' && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-muted-foreground mb-4">Who is this update for?</p>
|
||||
{AUDIENCES.map((opt) => {
|
||||
const Icon = opt.icon
|
||||
return (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => handleAudienceSelect(opt.value)}
|
||||
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)' }}
|
||||
>
|
||||
<Icon size={18} className="text-muted-foreground shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">{opt.label}</p>
|
||||
<p className="text-xs text-muted-foreground">{opt.description}</p>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Length */}
|
||||
{step === 'length' && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-muted-foreground mb-4">How detailed?</p>
|
||||
{LENGTHS.map((opt) => {
|
||||
const Icon = opt.icon
|
||||
return (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => handleLengthSelect(opt.value)}
|
||||
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)' }}
|
||||
>
|
||||
<Icon size={18} className="text-muted-foreground shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">{opt.label}</p>
|
||||
<p className="text-xs text-muted-foreground">{opt.description}</p>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
<button
|
||||
onClick={() => { setStep('audience'); setAudience(null) }}
|
||||
className="text-xs text-muted-foreground hover:text-foreground mt-2"
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Generating */}
|
||||
{step === 'generating' && (
|
||||
<div className="flex flex-col items-center justify-center py-8 gap-3">
|
||||
<Loader2 size={24} className="animate-spin text-cyan-400" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Generating {audience === 'email_draft' ? 'email draft' : audience === 'client_update' ? 'client update' : 'ticket notes'}...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 4: Result */}
|
||||
{step === 'result' && result && (
|
||||
<div className="space-y-4">
|
||||
{/* Meta badges */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium bg-cyan-500/10 text-cyan-400 border border-cyan-500/20">
|
||||
{AUDIENCES.find(a => a.value === result.audience)?.label}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium bg-[rgba(255,255,255,0.06)] text-muted-foreground border border-[rgba(255,255,255,0.08)]">
|
||||
{result.length === 'quick' ? 'Quick' : 'Detailed'}
|
||||
</span>
|
||||
{result.time_spent_display && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{result.steps_completed} steps · {result.time_spent_display}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Generated content */}
|
||||
<div
|
||||
className="max-h-64 overflow-y-auto rounded-lg p-4 text-sm text-foreground whitespace-pre-wrap font-mono leading-relaxed"
|
||||
style={{ background: 'var(--color-bg-page)', border: '1px solid var(--color-border-default)' }}
|
||||
>
|
||||
{result.content}
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg px-4 py-2 min-h-[44px] text-sm font-medium transition-colors',
|
||||
copied
|
||||
? 'bg-emerald-500/20 border border-emerald-500/30 text-emerald-400'
|
||||
: 'bg-cyan-500/10 border border-cyan-500/20 text-cyan-400 hover:bg-cyan-500/20'
|
||||
)}
|
||||
>
|
||||
{copied ? <Check size={16} /> : <Copy size={16} />}
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleRegenerate}
|
||||
className="flex items-center gap-2 rounded-lg bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] px-4 py-2 min-h-[44px] text-sm font-medium text-muted-foreground hover:text-foreground hover:border-[rgba(255,255,255,0.12)] transition-colors"
|
||||
>
|
||||
<RotateCcw size={16} />
|
||||
Regenerate
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleSwitchAudience}
|
||||
className="flex items-center gap-2 rounded-lg bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] px-4 py-2 min-h-[44px] text-sm font-medium text-muted-foreground hover:text-foreground hover:border-[rgba(255,255,255,0.12)] transition-colors"
|
||||
>
|
||||
<ArrowLeftRight size={16} />
|
||||
Switch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -13,3 +13,4 @@ export { SessionBriefing } from './SessionBriefing'
|
||||
export { ProposalCard } from './ProposalCard'
|
||||
export { ProposalDetail } from './ProposalDetail'
|
||||
export { InSessionScriptGenerator } from './InSessionScriptGenerator'
|
||||
export { StatusUpdateModal } from './StatusUpdateModal'
|
||||
|
||||
@@ -10,6 +10,8 @@ import type {
|
||||
ResolveSessionRequest,
|
||||
EscalateSessionRequest,
|
||||
SessionDocumentation,
|
||||
StatusUpdateRequest,
|
||||
StatusUpdateResponse,
|
||||
} from '@/types/ai-session'
|
||||
import { toast } from '@/lib/toast'
|
||||
|
||||
@@ -31,6 +33,7 @@ export interface UseFlowPilotSession {
|
||||
resumeOwnSession: () => Promise<void>
|
||||
abandonSession: () => Promise<void>
|
||||
rateSession: (rating: number, feedback?: string) => Promise<void>
|
||||
generateStatusUpdate: (data: StatusUpdateRequest) => Promise<StatusUpdateResponse>
|
||||
loadSession: (sessionId: string) => Promise<void>
|
||||
|
||||
// Derived
|
||||
@@ -214,6 +217,17 @@ export function useFlowPilotSession(): UseFlowPilotSession {
|
||||
}
|
||||
}, [session])
|
||||
|
||||
const generateStatusUpdate = useCallback(async (data: StatusUpdateRequest): Promise<StatusUpdateResponse> => {
|
||||
if (!session) throw new Error('No active session')
|
||||
try {
|
||||
return await aiSessionsApi.generateStatusUpdate(session.id, data)
|
||||
} catch (e: unknown) {
|
||||
const message = e instanceof Error ? e.message : 'Failed to generate status update'
|
||||
toast.error(message)
|
||||
throw e
|
||||
}
|
||||
}, [session])
|
||||
|
||||
const loadSession = useCallback(async (sessionId: string) => {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
@@ -260,6 +274,7 @@ export function useFlowPilotSession(): UseFlowPilotSession {
|
||||
resumeOwnSession,
|
||||
abandonSession,
|
||||
rateSession,
|
||||
generateStatusUpdate,
|
||||
loadSession,
|
||||
isActive,
|
||||
canResolve,
|
||||
|
||||
@@ -237,6 +237,7 @@ export default function FlowPilotSessionPage() {
|
||||
}}
|
||||
onRate={fp.rateSession}
|
||||
onReloadSession={() => fp.loadSession(fp.session!.id)}
|
||||
onGenerateStatusUpdate={fp.generateStatusUpdate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -128,6 +128,28 @@ export interface SessionCloseResponse {
|
||||
member_mapping_warning: string | null
|
||||
}
|
||||
|
||||
export type StatusUpdateAudience = 'ticket_notes' | 'client_update' | 'email_draft'
|
||||
export type StatusUpdateLength = 'quick' | 'detailed'
|
||||
export type StatusUpdateContext = 'status' | 'resolution' | 'escalation'
|
||||
|
||||
export interface StatusUpdateRequest {
|
||||
audience: StatusUpdateAudience
|
||||
length: StatusUpdateLength
|
||||
context: StatusUpdateContext
|
||||
}
|
||||
|
||||
export interface StatusUpdateResponse {
|
||||
content: string
|
||||
audience: StatusUpdateAudience
|
||||
length: StatusUpdateLength
|
||||
context: StatusUpdateContext
|
||||
session_status: string
|
||||
steps_completed: number
|
||||
time_spent_display: string | null
|
||||
client_name: string | null
|
||||
generated_at: string
|
||||
}
|
||||
|
||||
export interface RateSessionRequest {
|
||||
rating: number
|
||||
feedback?: string | null
|
||||
|
||||
Reference in New Issue
Block a user