Implement session outcomes, step timing, and live timer fixes

This commit is contained in:
Michael Chihlas
2026-02-11 17:52:12 -05:00
parent 2a1ed4d250
commit ca4ce7cad6
15 changed files with 574 additions and 59 deletions

View File

@@ -0,0 +1,122 @@
import { useEffect, useState } from 'react'
import { Modal } from '@/components/common/Modal'
import { cn } from '@/lib/utils'
import type { SessionOutcome } from '@/types'
interface SessionOutcomeModalProps {
isOpen: boolean
onClose: () => void
onSubmit: (data: { outcome: SessionOutcome; outcome_notes?: string }) => Promise<void>
isSubmitting?: boolean
}
const OUTCOME_OPTIONS: Array<{ value: SessionOutcome; label: string; description: string }> = [
{ value: 'resolved', label: 'Resolved', description: 'Issue fully resolved in this session.' },
{ value: 'workaround', label: 'Workaround', description: 'Temporary fix applied, root cause remains.' },
{ value: 'escalated', label: 'Escalated', description: 'Handed off to another engineer/team.' },
{ value: 'unresolved', label: 'Unresolved', description: 'No fix or workaround identified yet.' },
]
export function SessionOutcomeModal({
isOpen,
onClose,
onSubmit,
isSubmitting = false,
}: SessionOutcomeModalProps) {
const [outcome, setOutcome] = useState<SessionOutcome>('resolved')
const [outcomeNotes, setOutcomeNotes] = useState('')
useEffect(() => {
if (!isOpen) return
setOutcome('resolved')
setOutcomeNotes('')
}, [isOpen])
const handleSubmit = async () => {
await onSubmit({
outcome,
outcome_notes: outcomeNotes.trim() || undefined,
})
}
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title="Session Outcome"
footer={(
<div className="flex justify-end gap-2">
<button
type="button"
onClick={onClose}
disabled={isSubmitting}
className={cn(
'rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60',
'hover:bg-white/10 hover:text-white disabled:opacity-50'
)}
>
Cancel
</button>
<button
type="button"
onClick={handleSubmit}
disabled={isSubmitting}
className={cn(
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90 disabled:opacity-50'
)}
>
{isSubmitting ? 'Completing...' : 'Complete Session'}
</button>
</div>
)}
>
<div className="space-y-4">
<p className="text-sm text-white/70">
Select the session outcome before completion.
</p>
<div className="space-y-2">
{OUTCOME_OPTIONS.map((option) => (
<label
key={option.value}
className={cn(
'block cursor-pointer rounded-lg border border-white/10 p-3 transition-colors',
outcome === option.value ? 'border-white/30 bg-white/10' : 'hover:bg-white/[0.04]'
)}
>
<div className="flex items-start gap-3">
<input
type="radio"
name="session-outcome"
value={option.value}
checked={outcome === option.value}
onChange={() => setOutcome(option.value)}
className="mt-1 h-4 w-4"
/>
<div>
<p className="text-sm font-medium text-white">{option.label}</p>
<p className="text-xs text-white/50">{option.description}</p>
</div>
</div>
</label>
))}
</div>
<div>
<label className="block text-sm font-medium text-white">Outcome Notes (optional)</label>
<textarea
value={outcomeNotes}
onChange={(e) => setOutcomeNotes(e.target.value)}
rows={3}
placeholder="Add context for this outcome..."
className={cn(
'mt-1 block w-full rounded-md border border-white/10 bg-black/50 px-3 py-2',
'text-sm text-white placeholder:text-white/40',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
)}
/>
</div>
</div>
</Modal>
)
}

View File

@@ -2,3 +2,4 @@ export { PostStepActionModal } from './PostStepActionModal'
export { ContinuationModal, type DescendantNode } from './ContinuationModal'
export { ForkTreeModal } from './ForkTreeModal'
export { ScratchpadSidebar } from './ScratchpadSidebar'
export { SessionOutcomeModal } from './SessionOutcomeModal'