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,15 +1,111 @@
|
||||
import { FileText, Clock, CheckCircle2, ArrowUpRight, Star } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { FileText, Clock, CheckCircle2, ArrowUpRight, Star, AlertTriangle, Loader2, RefreshCw, Info } from 'lucide-react'
|
||||
import { aiSessionsApi } from '@/api'
|
||||
import { toast } from '@/lib/toast'
|
||||
import type { SessionDocumentation } from '@/types/ai-session'
|
||||
|
||||
interface SessionDocViewProps {
|
||||
documentation: SessionDocumentation
|
||||
onRate?: (rating: number) => void
|
||||
currentRating?: number | null
|
||||
psaPushStatus?: string | null
|
||||
psaPushError?: string | null
|
||||
memberMappingWarning?: string | null
|
||||
sessionId?: string
|
||||
ticketId?: string | null
|
||||
}
|
||||
|
||||
export function SessionDocView({ documentation, onRate, currentRating }: SessionDocViewProps) {
|
||||
export function SessionDocView({
|
||||
documentation,
|
||||
onRate,
|
||||
currentRating,
|
||||
psaPushStatus,
|
||||
psaPushError,
|
||||
memberMappingWarning,
|
||||
sessionId,
|
||||
ticketId,
|
||||
}: SessionDocViewProps) {
|
||||
const [retrying, setRetrying] = useState(false)
|
||||
const [currentPushStatus, setCurrentPushStatus] = useState(psaPushStatus)
|
||||
const [currentPushError, setCurrentPushError] = useState(psaPushError)
|
||||
|
||||
const handleRetry = async () => {
|
||||
if (!sessionId) return
|
||||
setRetrying(true)
|
||||
try {
|
||||
const result = await aiSessionsApi.retryPsaPush(sessionId)
|
||||
setCurrentPushStatus(result.psa_push_status)
|
||||
setCurrentPushError(result.psa_push_error)
|
||||
if (result.psa_push_status === 'sent') {
|
||||
toast.success('Documentation pushed to ticket successfully')
|
||||
}
|
||||
} catch {
|
||||
toast.error('Retry failed')
|
||||
} finally {
|
||||
setRetrying(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* PSA Push Status */}
|
||||
{currentPushStatus && currentPushStatus !== 'no_psa' && (
|
||||
<div
|
||||
className={`rounded-xl border px-4 py-3 flex items-center gap-3 ${
|
||||
currentPushStatus === 'sent'
|
||||
? 'border-emerald-400/20 bg-emerald-400/5'
|
||||
: currentPushStatus === 'pending_retry'
|
||||
? 'border-amber-400/20 bg-amber-400/5'
|
||||
: 'border-rose-500/20 bg-rose-500/5'
|
||||
}`}
|
||||
>
|
||||
{currentPushStatus === 'sent' && (
|
||||
<>
|
||||
<CheckCircle2 size={16} className="text-emerald-400 shrink-0" />
|
||||
<span className="text-sm text-emerald-400">
|
||||
Documentation pushed to ticket {ticketId ? `#${ticketId}` : ''}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{currentPushStatus === 'pending_retry' && (
|
||||
<>
|
||||
<Loader2 size={16} className="text-amber-400 shrink-0 animate-spin" />
|
||||
<span className="text-sm text-amber-400">
|
||||
Documentation queued for push — will sync shortly
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{currentPushStatus === 'failed' && (
|
||||
<>
|
||||
<AlertTriangle size={16} className="text-rose-500 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<span className="text-sm text-rose-500">
|
||||
Failed to push to ticket{currentPushError ? ` — ${currentPushError}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleRetry}
|
||||
disabled={retrying}
|
||||
className="flex items-center gap-1.5 rounded-lg bg-rose-500/10 px-3 py-1.5 text-xs font-medium text-rose-500 hover:bg-rose-500/20 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{retrying ? <Loader2 size={12} className="animate-spin" /> : <RefreshCw size={12} />}
|
||||
Retry
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Member mapping warning */}
|
||||
{memberMappingWarning && (
|
||||
<div className="rounded-xl border border-blue-400/20 bg-blue-400/5 px-4 py-3 flex items-start gap-3">
|
||||
<Info size={16} className="text-blue-400 shrink-0 mt-0.5" />
|
||||
<span className="text-sm text-blue-400">
|
||||
Time entry was not created — {memberMappingWarning} Session timing is included in the ticket note.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Header */}
|
||||
<div className="glass-card-static p-5">
|
||||
<div className="flex items-start gap-3">
|
||||
|
||||
Reference in New Issue
Block a user