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:
106
frontend/src/components/flowpilot/SessionTicketCard.tsx
Normal file
106
frontend/src/components/flowpilot/SessionTicketCard.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { ExternalLink, Cpu, Building2 } from 'lucide-react'
|
||||
|
||||
interface TicketData {
|
||||
ticket?: {
|
||||
id?: number | string
|
||||
summary?: string
|
||||
status?: string
|
||||
priority?: string
|
||||
board?: string
|
||||
}
|
||||
company?: {
|
||||
name?: string
|
||||
}
|
||||
configurations?: Array<{
|
||||
device_identifier?: string
|
||||
type?: string
|
||||
ip_address?: string
|
||||
}>
|
||||
}
|
||||
|
||||
interface SessionTicketCardProps {
|
||||
ticketId: string
|
||||
ticketData: TicketData | null
|
||||
siteUrl?: string
|
||||
}
|
||||
|
||||
export function SessionTicketCard({ ticketId, ticketData, siteUrl }: SessionTicketCardProps) {
|
||||
const ticket = ticketData?.ticket
|
||||
const company = ticketData?.company
|
||||
const configs = ticketData?.configurations
|
||||
|
||||
const ticketUrl = siteUrl
|
||||
? `${siteUrl}/v4_6_release/services/system_io/Service/fv_sr100_request.rails?service_recid=${ticketId}`
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-primary/20 bg-primary/5 p-3 space-y-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170]">
|
||||
Linked Ticket
|
||||
</h4>
|
||||
{ticketUrl && (
|
||||
<a
|
||||
href={ticketUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-muted-foreground hover:text-primary transition-colors"
|
||||
title="Open in ConnectWise"
|
||||
>
|
||||
<ExternalLink size={12} />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm font-semibold text-foreground">
|
||||
<span className="text-primary">#{ticketId}</span>
|
||||
{ticket?.summary && (
|
||||
<span> — {ticket.summary}</span>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
|
||||
{company?.name && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Building2 size={10} />
|
||||
{company.name}
|
||||
</span>
|
||||
)}
|
||||
{ticket?.priority && (
|
||||
<>
|
||||
<span className="text-[#5a6170]">•</span>
|
||||
<span>{ticket.priority}</span>
|
||||
</>
|
||||
)}
|
||||
{ticket?.status && (
|
||||
<>
|
||||
<span className="text-[#5a6170]">•</span>
|
||||
<span>{ticket.status}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{configs && configs.length > 0 && (
|
||||
<div className="border-t border-border/50 pt-2 mt-2">
|
||||
<p className="font-label text-[0.5625rem] uppercase tracking-wider text-[#5a6170] mb-1">
|
||||
Devices
|
||||
</p>
|
||||
<div className="space-y-0.5">
|
||||
{configs.slice(0, 3).map((cfg, i) => (
|
||||
<div key={i} className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Cpu size={10} />
|
||||
<span>{cfg.device_identifier}</span>
|
||||
{cfg.type && <span className="text-[#5a6170]">({cfg.type})</span>}
|
||||
</div>
|
||||
))}
|
||||
{configs.length > 3 && (
|
||||
<p className="text-[0.625rem] text-[#5a6170]">
|
||||
+{configs.length - 3} more
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user