- .gitignore: keep both graphify-out/ entries and main's .gitnexus entry - ScriptCodeBlock/ScriptPreviewModal: take main's border-border and text-accent-text for filename labels; use neutral ghost style for Save button in ScriptCodeBlock; use bg-accent (normalized from bg-primary) for Save button in ScriptPreviewModal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
234 lines
9.0 KiB
TypeScript
234 lines
9.0 KiB
TypeScript
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,
|
|
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-4 sm:space-y-5">
|
|
{/* PSA Push Status */}
|
|
{currentPushStatus && currentPushStatus !== 'no_psa' && (
|
|
<div
|
|
className={`rounded-xl border px-3 py-3 sm:px-4 flex flex-wrap items-center gap-2 sm:gap-3 ${
|
|
currentPushStatus === 'sent'
|
|
? 'border-emerald-400/20 bg-emerald-400/5'
|
|
: currentPushStatus === 'pending_retry'
|
|
? 'border-warning/20 bg-warning/5'
|
|
: 'border-danger/20 bg-rose-500/5'
|
|
}`}
|
|
>
|
|
{currentPushStatus === 'sent' && (
|
|
<>
|
|
<CheckCircle2 size={16} className="text-success shrink-0" />
|
|
<span className="text-sm text-success">
|
|
Documentation pushed to ticket {ticketId ? `#${ticketId}` : ''}
|
|
</span>
|
|
</>
|
|
)}
|
|
{currentPushStatus === 'pending_retry' && (
|
|
<>
|
|
<Loader2 size={16} className="text-warning shrink-0 animate-spin" />
|
|
<span className="text-sm text-warning">
|
|
Documentation queued for push — will sync shortly
|
|
</span>
|
|
</>
|
|
)}
|
|
{currentPushStatus === 'failed' && (
|
|
<>
|
|
<AlertTriangle size={16} className="text-danger shrink-0" />
|
|
<div className="flex-1">
|
|
<span className="text-sm text-danger">
|
|
Failed to push to ticket{currentPushError ? ` — ${currentPushError}` : ''}
|
|
</span>
|
|
</div>
|
|
<button
|
|
onClick={handleRetry}
|
|
disabled={retrying}
|
|
className="flex items-center gap-1.5 rounded-lg bg-danger-dim px-3 py-1.5 text-xs font-medium text-danger hover:bg-danger/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-accent/20 bg-accent/5 px-4 py-3 flex items-start gap-3">
|
|
<Info size={16} className="text-accent shrink-0 mt-0.5" />
|
|
<span className="text-sm text-accent">
|
|
Time entry was not created — {memberMappingWarning} Session timing is included in the ticket note.
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Header */}
|
|
<div className="card-flat p-3 sm:p-4 lg:p-5">
|
|
<div className="flex items-start gap-3">
|
|
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-accent-dim text-primary">
|
|
<FileText size={16} />
|
|
</span>
|
|
<div className="flex-1">
|
|
<h3 className="font-heading text-lg font-semibold text-foreground">Session Documentation</h3>
|
|
<p className="mt-1 text-sm text-muted-foreground">{documentation.problem_summary}</p>
|
|
<div className="mt-2 flex items-center gap-3 flex-wrap">
|
|
{documentation.problem_domain && (
|
|
<span className="font-sans text-xs rounded-md bg-accent-dim px-2 py-0.5 text-[0.625rem] uppercase tracking-wider text-primary">
|
|
{documentation.problem_domain}
|
|
</span>
|
|
)}
|
|
{documentation.duration_display && (
|
|
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
<Clock size={12} />
|
|
{documentation.duration_display}
|
|
</span>
|
|
)}
|
|
<span className="text-xs text-muted-foreground">
|
|
{documentation.total_steps} steps
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Outcome */}
|
|
{(documentation.resolution_summary || documentation.escalation_reason) && (
|
|
<div className={`card-flat p-4 border-l-2 ${documentation.resolution_summary ? 'border-l-success' : 'border-l-warning'}`}>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
{documentation.resolution_summary ? (
|
|
<CheckCircle2 size={14} className="text-success" />
|
|
) : (
|
|
<ArrowUpRight size={14} className="text-warning" />
|
|
)}
|
|
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-muted-foreground">
|
|
{documentation.resolution_summary ? 'Resolved' : 'Escalated'}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-foreground">
|
|
{documentation.resolution_summary || documentation.escalation_reason}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Intake summary */}
|
|
<div className="card-flat p-3 sm:p-4">
|
|
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-muted-foreground mb-2">
|
|
Original intake
|
|
</h4>
|
|
<p className="text-sm text-foreground whitespace-pre-wrap">{documentation.intake_summary}</p>
|
|
</div>
|
|
|
|
{/* Diagnostic steps */}
|
|
<div className="space-y-2">
|
|
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-muted-foreground px-1">
|
|
Diagnostic trail
|
|
</h4>
|
|
{documentation.diagnostic_steps.map((step) => (
|
|
<div key={step.step_number} className="card-flat p-3 sm:p-4">
|
|
<div className="flex items-start gap-3">
|
|
<span className="font-sans text-xs flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-card text-[0.625rem] text-muted-foreground border border-border">
|
|
{step.step_number}
|
|
</span>
|
|
<div className="flex-1">
|
|
<p className="text-sm text-foreground">{step.description}</p>
|
|
{step.engineer_response && (
|
|
<p className="mt-1 text-xs text-primary">→ {step.engineer_response}</p>
|
|
)}
|
|
{step.outcome && (
|
|
<p className="mt-1 text-xs text-muted-foreground">Outcome: {step.outcome}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Follow-up recommendations */}
|
|
{documentation.follow_up_recommendations.length > 0 && (
|
|
<div className="card-flat p-3 sm:p-4">
|
|
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-muted-foreground mb-2">
|
|
Follow-up
|
|
</h4>
|
|
<ul className="space-y-1">
|
|
{documentation.follow_up_recommendations.map((rec, i) => (
|
|
<li key={i} className="flex items-start gap-2 text-sm text-foreground">
|
|
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-primary" />
|
|
{rec}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
{/* Rating */}
|
|
{onRate && (
|
|
<div className="card-flat p-3 sm:p-4 text-center">
|
|
<p className="text-sm text-muted-foreground mb-2">How helpful was this session?</p>
|
|
<div className="flex items-center justify-center gap-1">
|
|
{[1, 2, 3, 4, 5].map((star) => (
|
|
<button
|
|
key={star}
|
|
onClick={() => onRate(star)}
|
|
className="p-2 sm:p-1 transition-colors"
|
|
>
|
|
<Star
|
|
size={20}
|
|
className={
|
|
(currentRating ?? 0) >= star
|
|
? 'fill-warning text-warning'
|
|
: 'text-muted-foreground hover:text-warning'
|
|
}
|
|
/>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|