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:
@@ -1,26 +1,30 @@
|
||||
import { useState } from 'react'
|
||||
import { CheckCircle2, ArrowUpRight } from 'lucide-react'
|
||||
import { CheckCircle2, ArrowUpRight, Pause } from 'lucide-react'
|
||||
import { EscalateModal } from './EscalateModal'
|
||||
import type { ResolveSessionRequest, EscalateSessionRequest, SessionDocumentation } from '@/types/ai-session'
|
||||
|
||||
interface FlowPilotActionBarProps {
|
||||
canResolve: boolean
|
||||
canEscalate: boolean
|
||||
isProcessing: boolean
|
||||
hasPsaTicket?: boolean
|
||||
onResolve: (data: ResolveSessionRequest) => Promise<SessionDocumentation>
|
||||
onEscalate: (data: EscalateSessionRequest) => Promise<SessionDocumentation>
|
||||
onPause?: () => Promise<void>
|
||||
}
|
||||
|
||||
export function FlowPilotActionBar({
|
||||
canResolve,
|
||||
canEscalate,
|
||||
isProcessing,
|
||||
hasPsaTicket = false,
|
||||
onResolve,
|
||||
onEscalate,
|
||||
onPause,
|
||||
}: FlowPilotActionBarProps) {
|
||||
const [showResolve, setShowResolve] = useState(false)
|
||||
const [showEscalate, setShowEscalate] = useState(false)
|
||||
const [resolutionSummary, setResolutionSummary] = useState('')
|
||||
const [escalationReason, setEscalationReason] = useState('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const handleResolve = async () => {
|
||||
@@ -34,14 +38,14 @@ export function FlowPilotActionBar({
|
||||
}
|
||||
}
|
||||
|
||||
const handleEscalate = async () => {
|
||||
if (!escalationReason.trim() || escalationReason.length < 5) return
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await onEscalate({ escalation_reason: escalationReason })
|
||||
setShowEscalate(false)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
const handlePause = async () => {
|
||||
if (onPause) {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await onPause()
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,13 +65,23 @@ export function FlowPilotActionBar({
|
||||
Resolve
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setShowEscalate(true); setShowResolve(false) }}
|
||||
onClick={() => setShowEscalate(true)}
|
||||
disabled={!canEscalate || isProcessing}
|
||||
className="flex items-center gap-2 rounded-lg bg-amber-500/10 border border-amber-500/20 px-4 py-2 text-sm font-medium text-amber-400 hover:bg-amber-500/20 disabled:opacity-40 disabled:pointer-events-none transition-colors"
|
||||
>
|
||||
<ArrowUpRight size={16} />
|
||||
Escalate
|
||||
</button>
|
||||
{onPause && (
|
||||
<button
|
||||
onClick={handlePause}
|
||||
disabled={isProcessing || submitting}
|
||||
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 text-sm font-medium text-muted-foreground hover:text-foreground hover:border-[rgba(255,255,255,0.12)] disabled:opacity-40 disabled:pointer-events-none transition-colors ml-auto"
|
||||
>
|
||||
<Pause size={16} />
|
||||
Pause
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Resolve modal */}
|
||||
@@ -104,37 +118,13 @@ export function FlowPilotActionBar({
|
||||
)}
|
||||
|
||||
{/* Escalate modal */}
|
||||
{showEscalate && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="glass-card-static w-full max-w-lg p-6">
|
||||
<h3 className="font-heading text-lg font-semibold text-foreground mb-1">Escalate Session</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">Explain why this needs escalation. FlowPilot will package the context for the next engineer.</p>
|
||||
<textarea
|
||||
value={escalationReason}
|
||||
onChange={(e) => setEscalationReason(e.target.value)}
|
||||
placeholder="Why does this need to be escalated?"
|
||||
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none resize-none"
|
||||
rows={4}
|
||||
autoFocus
|
||||
/>
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setShowEscalate(false)}
|
||||
className="rounded-lg px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleEscalate}
|
||||
disabled={escalationReason.length < 5 || submitting}
|
||||
className="rounded-lg bg-amber-500/20 border border-amber-500/30 px-4 py-2 text-sm font-medium text-amber-400 hover:bg-amber-500/30 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{submitting ? 'Escalating...' : 'Escalate Session'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<EscalateModal
|
||||
open={showEscalate}
|
||||
onClose={() => setShowEscalate(false)}
|
||||
onEscalate={onEscalate}
|
||||
isProcessing={isProcessing || submitting}
|
||||
hasPsaTicket={hasPsaTicket}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user