feat(ai-session): add Phase 2 PSA integration, escalation handoff, and session management
Phase 2 of the FlowPilot-First Pivot connecting AI sessions to ConnectWise PSA: Slice 1 — PSA Ticket Intake: - FlowPilotEngine accepts psa_ticket intake with graceful CW API fallback - Ticket picker on intake screen (refactored TicketPickerModal for dual-mode) - Ticket context card in session sidebar Slice 2 — Auto Documentation Push: - PSA documentation service with resolution/escalation note formatting - Time entry creation via new ConnectWise provider method - Automatic retry scheduler (APScheduler, 5min interval, 3 retries) - PSA push status indicators in frontend with manual retry button - Member mapping warning when CW member not mapped Slice 3 — Session Pause/Resume & Escalation Handoff: - Pause/resume endpoints for same-engineer session bookmarking - Escalation flow: requesting_escalation status, self-escalation blocked - Enhanced escalation package with LLM-generated hypotheses/suggestions - Pickup endpoint with continue/fresh resume modes and briefing step - Escalation queue (sidebar nav + dedicated page) - SessionBriefing component with continue/fresh choice UI - EscalateModal with PSA-aware button text Slice 4 — Mid-Session Ticket Linking: - Link ticket retroactively with context injection into system prompt - Link Ticket button in session sidebar Slice 5 — FlowPilot PSA Settings: - Settings tab on IntegrationsPage with 7 configurable options - Stored as flowpilot_settings JSONB on PsaConnection Database: 2 migrations (flowpilot_settings, psa_post_log changes, status constraint) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
173
frontend/src/components/flowpilot/SessionBriefing.tsx
Normal file
173
frontend/src/components/flowpilot/SessionBriefing.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import { useState } from 'react'
|
||||
import { ArrowRight, ChevronDown, ChevronRight, MessageSquare } from 'lucide-react'
|
||||
|
||||
interface EscalationPackage {
|
||||
original_user_id?: string
|
||||
problem_summary?: string
|
||||
escalation_reason?: string
|
||||
steps_tried?: Array<{
|
||||
step_type?: string
|
||||
description?: string
|
||||
response?: string
|
||||
}>
|
||||
steps_ruled_out?: string[]
|
||||
remaining_hypotheses?: string[]
|
||||
suggested_next_steps?: string[]
|
||||
confidence_at_escalation?: number
|
||||
}
|
||||
|
||||
interface SessionBriefingProps {
|
||||
escalationPackage: EscalationPackage
|
||||
originalEngineerName?: string
|
||||
onContinue: () => void
|
||||
onFresh: (context: string) => void
|
||||
isProcessing: boolean
|
||||
}
|
||||
|
||||
export function SessionBriefing({
|
||||
escalationPackage,
|
||||
originalEngineerName,
|
||||
onContinue,
|
||||
onFresh,
|
||||
isProcessing,
|
||||
}: SessionBriefingProps) {
|
||||
const [showSteps, setShowSteps] = useState(false)
|
||||
const [freshMode, setFreshMode] = useState(false)
|
||||
const [freshContext, setFreshContext] = useState('')
|
||||
|
||||
const pkg = escalationPackage
|
||||
|
||||
return (
|
||||
<div className="glass-card-static border-l-2 border-l-amber-500 p-5 space-y-4">
|
||||
<div>
|
||||
<h3 className="font-heading text-base font-semibold text-foreground">
|
||||
Escalation from {originalEngineerName || 'another engineer'}
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Review the briefing below, then choose how to proceed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Problem */}
|
||||
{pkg.problem_summary && (
|
||||
<div>
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-1">Problem</h4>
|
||||
<p className="text-sm text-foreground">{pkg.problem_summary}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Escalation reason */}
|
||||
{pkg.escalation_reason && (
|
||||
<div>
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-1">Why escalated</h4>
|
||||
<p className="text-sm text-amber-400">{pkg.escalation_reason}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Steps taken (collapsible) */}
|
||||
{pkg.steps_tried && pkg.steps_tried.length > 0 && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setShowSteps(!showSteps)}
|
||||
className="flex items-center gap-1.5 font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] hover:text-foreground transition-colors"
|
||||
>
|
||||
{showSteps ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
Steps taken ({pkg.steps_tried.length})
|
||||
</button>
|
||||
{showSteps && (
|
||||
<div className="mt-2 space-y-1.5">
|
||||
{pkg.steps_tried.map((step, i) => (
|
||||
<div key={i} className="rounded-lg bg-card/50 px-3 py-2 text-xs">
|
||||
<p className="text-foreground">{i + 1}. {step.description}</p>
|
||||
{step.response && (
|
||||
<p className="mt-0.5 text-primary">→ {step.response}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Remaining hypotheses */}
|
||||
{pkg.remaining_hypotheses && pkg.remaining_hypotheses.length > 0 && (
|
||||
<div>
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-1">Remaining hypotheses</h4>
|
||||
<ul className="space-y-1">
|
||||
{pkg.remaining_hypotheses.map((h, i) => (
|
||||
<li key={i} className="text-sm text-foreground flex items-start gap-2">
|
||||
<span className="text-primary mt-0.5">•</span>
|
||||
{h}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Suggested next steps */}
|
||||
{pkg.suggested_next_steps && pkg.suggested_next_steps.length > 0 && (
|
||||
<div>
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-1">Suggested next steps</h4>
|
||||
<ul className="space-y-1">
|
||||
{pkg.suggested_next_steps.map((s, i) => (
|
||||
<li key={i} className="text-sm text-foreground flex items-start gap-2">
|
||||
<span className="text-emerald-400 mt-0.5">→</span>
|
||||
{s}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action buttons */}
|
||||
{!freshMode ? (
|
||||
<div className="flex gap-2 pt-2">
|
||||
<button
|
||||
onClick={onContinue}
|
||||
disabled={isProcessing}
|
||||
className="flex-1 flex items-center justify-center gap-2 rounded-lg bg-gradient-brand px-4 py-2.5 text-sm font-semibold text-[#101114] shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] disabled:opacity-40 transition-all"
|
||||
>
|
||||
<ArrowRight size={14} />
|
||||
Continue Where They Left Off
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFreshMode(true)}
|
||||
disabled={isProcessing}
|
||||
className="flex-1 flex items-center justify-center gap-2 rounded-lg bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] px-4 py-2.5 text-sm font-medium text-foreground hover:border-[rgba(255,255,255,0.12)] disabled:opacity-40 transition-colors"
|
||||
>
|
||||
<MessageSquare size={14} />
|
||||
Start Fresh With Context
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 pt-2">
|
||||
<textarea
|
||||
value={freshContext}
|
||||
onChange={(e) => setFreshContext(e.target.value)}
|
||||
placeholder="What additional information do you have, or what would you like to investigate first?"
|
||||
className="w-full rounded-lg border border-border bg-card px-4 py-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none resize-none"
|
||||
rows={3}
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setFreshMode(false)}
|
||||
disabled={isProcessing}
|
||||
className="rounded-lg bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] px-4 py-2 text-sm font-medium text-foreground hover:border-[rgba(255,255,255,0.12)] disabled:opacity-40 transition-colors"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
onClick={() => freshContext.trim() && onFresh(freshContext.trim())}
|
||||
disabled={!freshContext.trim() || isProcessing}
|
||||
className="flex-1 flex items-center justify-center gap-2 rounded-lg bg-gradient-brand px-4 py-2 text-sm font-semibold text-[#101114] shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] disabled:opacity-40 transition-all"
|
||||
>
|
||||
<ArrowRight size={14} />
|
||||
Start Diagnosis
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user