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:
82
frontend/src/components/flowpilot/EscalateModal.tsx
Normal file
82
frontend/src/components/flowpilot/EscalateModal.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useState } from 'react'
|
||||
import { AlertTriangle, Loader2 } from 'lucide-react'
|
||||
import { Modal } from '@/components/common/Modal'
|
||||
import type { EscalateSessionRequest } from '@/types/ai-session'
|
||||
|
||||
interface EscalateModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onEscalate: (data: EscalateSessionRequest) => Promise<unknown>
|
||||
isProcessing: boolean
|
||||
hasPsaTicket: boolean
|
||||
}
|
||||
|
||||
export function EscalateModal({ open, onClose, onEscalate, isProcessing, hasPsaTicket }: EscalateModalProps) {
|
||||
const [reason, setReason] = useState('')
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!reason.trim() || reason.trim().length < 5) return
|
||||
await onEscalate({ escalation_reason: reason.trim() })
|
||||
setReason('')
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isProcessing) {
|
||||
setReason('')
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={open} onClose={handleClose} title="Escalate Session" size="sm">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-3 rounded-xl border border-amber-400/20 bg-amber-400/5 p-3">
|
||||
<AlertTriangle size={16} className="text-amber-400 shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-amber-400">
|
||||
This will mark the session as requesting escalation. Team members will see it in their escalation queue and can pick it up with full context.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-foreground">
|
||||
Why are you escalating?
|
||||
</label>
|
||||
<textarea
|
||||
value={reason}
|
||||
onChange={(e) => setReason(e.target.value)}
|
||||
placeholder="e.g. I've exhausted all networking diagnostics and suspect this is a firewall policy issue that requires senior admin access..."
|
||||
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={4}
|
||||
autoFocus
|
||||
/>
|
||||
<p className="mt-1 text-[0.625rem] text-[#5a6170]">
|
||||
Minimum 5 characters. This will be shown to the engineer who picks up.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleClose}
|
||||
disabled={isProcessing}
|
||||
className="flex-1 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)] transition-colors disabled:opacity-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!reason.trim() || reason.trim().length < 5 || isProcessing}
|
||||
className="flex-1 flex items-center justify-center gap-2 rounded-lg bg-amber-500/90 px-4 py-2.5 text-sm font-semibold text-[#101114] hover:bg-amber-500 active:scale-[0.97] disabled:opacity-40 transition-all"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
) : (
|
||||
<AlertTriangle size={14} />
|
||||
)}
|
||||
{hasPsaTicket ? 'Escalate & Update Ticket' : 'Escalate'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user