schedule-sheet.tsx
1 'use client' 2 3 import { useEffect, useState, useMemo } from 'react' 4 import { useAppStore } from '@/stores/use-app-store' 5 import { createSchedule, updateSchedule, deleteSchedule } from '@/lib/schedules/schedules' 6 import { BottomSheet } from '@/components/shared/bottom-sheet' 7 import { AgentPickerList } from '@/components/shared/agent-picker-list' 8 import { ConfirmDialog } from '@/components/shared/confirm-dialog' 9 import { inputClass } from '@/components/shared/form-styles' 10 import { AgentAvatar } from '@/components/agents/agent-avatar' 11 import type { ScheduleTaskMode, ScheduleType, ScheduleStatus } from '@/types' 12 import cronstrue from 'cronstrue' 13 import { SectionLabel } from '@/components/shared/section-label' 14 import { SCHEDULE_TEMPLATES, type ScheduleTemplate } from '@/lib/schedules/schedule-templates' 15 import { HintTip } from '@/components/shared/hint-tip' 16 import { isUserCreatedSchedule } from '@/lib/schedules/schedule-origin' 17 import { toast } from 'sonner' 18 import { 19 Newspaper, BarChart3, HeartPulse, PenLine, Trash2, 20 Activity, ShieldCheck, DatabaseBackup, FileText, 21 } from 'lucide-react' 22 23 const TEMPLATE_ICONS: Record<string, React.ComponentType<{ className?: string; size?: number }>> = { 24 Newspaper, BarChart3, HeartPulse, PenLine, Trash2, 25 Activity, ShieldCheck, DatabaseBackup, FileText, 26 } 27 28 const CRON_PRESETS = [ 29 { label: 'Every hour', cron: '0 * * * *' }, 30 { label: 'Every 6 hours', cron: '0 */6 * * *' }, 31 { label: 'Daily at 9am', cron: '0 9 * * *' }, 32 { label: 'Weekly Mon 9am', cron: '0 9 * * 1' }, 33 ] 34 35 async function getNextRunsAsync(cron: string, count: number = 3): Promise<Date[]> { 36 try { 37 const { CronExpressionParser } = await import('cron-parser') 38 const interval = CronExpressionParser.parse(cron) 39 const runs: Date[] = [] 40 for (let i = 0; i < count; i++) { 41 runs.push(interval.next().toDate()) 42 } 43 return runs 44 } catch { 45 return [] 46 } 47 } 48 49 function formatCronHuman(cron: string): string { 50 try { 51 return cronstrue.toString(cron, { use24HourTimeFormat: false }) 52 } catch { 53 return 'Invalid cron expression' 54 } 55 } 56 57 function formatDate(d: Date): string { 58 return d.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' }) + 59 ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) 60 } 61 62 const STEPS_CREATE = ['Template', 'What', 'When', 'Review'] as const 63 const STEPS_EDIT = ['What', 'When', 'Review'] as const 64 type Step = 0 | 1 | 2 | 3 65 66 function applyTemplate( 67 tpl: ScheduleTemplate, 68 setters: { 69 setName: (v: string) => void 70 setTaskPrompt: (v: string) => void 71 setScheduleType: (v: ScheduleType) => void 72 setCron: (v: string) => void 73 setIntervalMs: (v: number) => void 74 setCustomCron: (v: boolean) => void 75 }, 76 ) { 77 setters.setName(tpl.name) 78 setters.setTaskPrompt(tpl.defaults.taskPrompt) 79 setters.setScheduleType(tpl.defaults.scheduleType) 80 if (tpl.defaults.cron) { 81 setters.setCron(tpl.defaults.cron) 82 setters.setCustomCron(!CRON_PRESETS.some((p) => p.cron === tpl.defaults.cron)) 83 } 84 if (tpl.defaults.intervalMs) setters.setIntervalMs(tpl.defaults.intervalMs) 85 } 86 87 export function ScheduleSheet() { 88 const open = useAppStore((s) => s.scheduleSheetOpen) 89 const setOpen = useAppStore((s) => s.setScheduleSheetOpen) 90 const editingId = useAppStore((s) => s.editingScheduleId) 91 const setEditingId = useAppStore((s) => s.setEditingScheduleId) 92 const schedules = useAppStore((s) => s.schedules) 93 const loadSchedules = useAppStore((s) => s.loadSchedules) 94 const agents = useAppStore((s) => s.agents) 95 const loadAgents = useAppStore((s) => s.loadAgents) 96 const templatePrefill = useAppStore((s) => s.scheduleTemplatePrefill) 97 const setTemplatePrefill = useAppStore((s) => s.setScheduleTemplatePrefill) 98 99 const [step, setStep] = useState<Step>(0) 100 const [name, setName] = useState('') 101 const [agentId, setAgentId] = useState('') 102 const [taskPrompt, setTaskPrompt] = useState('') 103 const [scheduleType, setScheduleType] = useState<ScheduleType>('cron') 104 const [cron, setCron] = useState('0 * * * *') 105 const [intervalMs, setIntervalMs] = useState(3600000) 106 const [status, setStatus] = useState<ScheduleStatus>('active') 107 const [taskMode, setTaskMode] = useState<ScheduleTaskMode>('task') 108 const [message, setMessage] = useState('') 109 const [customCron, setCustomCron] = useState(false) 110 const [confirmDelete, setConfirmDelete] = useState(false) 111 const [deleting, setDeleting] = useState(false) 112 113 const editing = editingId ? schedules[editingId] : null 114 const isCreating = !editing 115 const steps = isCreating ? STEPS_CREATE : STEPS_EDIT 116 const agentList = Object.values(agents).sort((a, b) => a.name.localeCompare(b.name)) 117 118 // Compute which logical step we're on (template step only exists in create mode) 119 const templateStep = isCreating ? 0 : -1 120 const whatStep = isCreating ? 1 : 0 121 const whenStep = isCreating ? 2 : 1 122 const reviewStep = isCreating ? 3 : 2 123 124 useEffect(() => { 125 if (open) { 126 loadAgents() 127 if (editing) { 128 setStep(0) 129 setName(editing.name || '') 130 setAgentId(editing.agentId) 131 setTaskPrompt(editing.taskPrompt) 132 setScheduleType(editing.scheduleType) 133 setCron(editing.cron || '0 * * * *') 134 setIntervalMs(editing.intervalMs || 3600000) 135 setStatus(editing.status) 136 setTaskMode(editing.taskMode === 'wake_only' ? 'wake_only' : editing.taskMode === 'protocol' ? 'protocol' : 'task') 137 setMessage(editing.message || '') 138 setCustomCron(!CRON_PRESETS.some((p) => p.cron === editing.cron)) 139 } else if (templatePrefill) { 140 // Opened from a quick-start card with pre-filled values 141 setName(templatePrefill.name) 142 setTaskPrompt(templatePrefill.taskPrompt) 143 setScheduleType(templatePrefill.scheduleType) 144 if (templatePrefill.cron) { 145 setCron(templatePrefill.cron) 146 setCustomCron(!CRON_PRESETS.some((p) => p.cron === templatePrefill.cron)) 147 } 148 if (templatePrefill.intervalMs) setIntervalMs(templatePrefill.intervalMs) 149 setAgentId('') 150 setStatus('active') 151 setStep(1) // Skip template picker, go to "What" step 152 setTemplatePrefill(null) 153 } else { 154 setStep(0) // Start at template picker 155 setName('') 156 setAgentId('') 157 setTaskPrompt('') 158 setScheduleType('cron') 159 setCron('0 * * * *') 160 setIntervalMs(3600000) 161 setStatus('active') 162 setTaskMode('task') 163 setMessage('') 164 setCustomCron(false) 165 } 166 } 167 // eslint-disable-next-line react-hooks/exhaustive-deps 168 }, [open, editingId]) 169 170 const cronHuman = useMemo(() => formatCronHuman(cron), [cron]) 171 const [nextRuns, setNextRuns] = useState<Date[]>([]) 172 useEffect(() => { 173 getNextRunsAsync(cron).then(setNextRuns) 174 }, [cron]) 175 176 const onClose = () => { 177 setConfirmDelete(false) 178 setDeleting(false) 179 setOpen(false) 180 setEditingId(null) 181 } 182 183 const handleSave = async () => { 184 const data = { 185 name: name.trim(), 186 agentId, 187 taskPrompt: taskMode === 'wake_only' ? message : taskPrompt, 188 taskMode, 189 message: taskMode === 'task' ? undefined : message, 190 protocolTemplateId: taskMode === 'protocol' ? 'single_agent_structured_run' : undefined, 191 scheduleType, 192 cron: scheduleType === 'cron' ? cron : undefined, 193 intervalMs: scheduleType === 'interval' ? intervalMs : undefined, 194 runAt: scheduleType === 'once' ? Date.now() + intervalMs : undefined, 195 status, 196 } 197 try { 198 if (editing) { 199 await updateSchedule(editing.id, data) 200 toast.success('Schedule updated successfully') 201 } else { 202 await createSchedule(data) 203 toast.success('Schedule created successfully') 204 } 205 await loadSchedules() 206 onClose() 207 } catch (err: unknown) { 208 toast.error(err instanceof Error ? err.message : 'Failed to save schedule') 209 } 210 } 211 212 const handleDelete = async () => { 213 if (!editing) return 214 setDeleting(true) 215 try { 216 await deleteSchedule(editing.id) 217 toast.success('Schedule archived') 218 await loadSchedules() 219 setConfirmDelete(false) 220 onClose() 221 } catch (err: unknown) { 222 toast.error(err instanceof Error ? err.message : 'Failed to delete schedule') 223 } finally { 224 setDeleting(false) 225 } 226 } 227 228 // Step validation 229 const step0Valid = name.trim().length > 0 && agentId.length > 0 && (taskMode === 'wake_only' ? message.trim().length > 0 : taskPrompt.trim().length > 0) 230 const step1Valid = scheduleType === 'cron' ? cron.trim().length > 0 : intervalMs > 0 231 232 const selectedAgent = agentId ? agents[agentId] : null 233 const creatorAgent = editing?.createdByAgentId ? agents[editing.createdByAgentId] : null 234 235 return ( 236 <BottomSheet open={open} onClose={onClose} wide> 237 <div className="mb-8"> 238 <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2"> 239 {editing ? 'Edit Schedule' : 'New Schedule'} 240 </h2> 241 <p className="text-[14px] text-text-3">Automate agent tasks on a schedule</p> 242 </div> 243 244 {/* Step indicator */} 245 <div className="flex items-center gap-2 mb-10"> 246 {steps.map((label, i) => ( 247 <div key={label} className="flex items-center gap-2"> 248 {i > 0 && <div className={`w-8 h-px ${i <= step ? 'bg-accent-bright/40' : 'bg-white/[0.06]'}`} />} 249 <button 250 onClick={() => { 251 if (i < step) setStep(i as Step) 252 else if (i === step + 1) { 253 if (step === whatStep && step0Valid) setStep(i as Step) 254 else if (step === whenStep && step1Valid) setStep(i as Step) 255 else if (step === templateStep) setStep(i as Step) 256 } 257 }} 258 className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-[8px] text-[12px] font-600 cursor-pointer transition-all border-none 259 ${i === step 260 ? 'bg-accent-soft text-accent-bright' 261 : i < step 262 ? 'bg-white/[0.04] text-text-2' 263 : 'bg-transparent text-text-3/50'}`} 264 style={{ fontFamily: 'inherit' }} 265 > 266 <span className={`w-5 h-5 rounded-full text-[10px] font-700 flex items-center justify-center 267 ${i === step 268 ? 'bg-accent-bright text-white' 269 : i < step 270 ? 'bg-emerald-400/20 text-emerald-400' 271 : 'bg-white/[0.06] text-text-3/50'}`}> 272 {i < step ? ( 273 <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round"><polyline points="20 6 9 17 4 12" /></svg> 274 ) : ( 275 i + 1 276 )} 277 </span> 278 {label} 279 </button> 280 </div> 281 ))} 282 </div> 283 284 {/* Template Picker (create only) */} 285 {step === templateStep && isCreating && ( 286 <div> 287 <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-4"> 288 {SCHEDULE_TEMPLATES.map((tpl) => { 289 const IconComp = TEMPLATE_ICONS[tpl.icon] || FileText 290 return ( 291 <button 292 key={tpl.id} 293 onClick={() => { 294 const setters = { setName, setTaskPrompt, setScheduleType, setCron, setIntervalMs, setCustomCron } 295 applyTemplate(tpl, setters) 296 setStep(whatStep as Step) 297 }} 298 className="flex items-start gap-3.5 p-4 rounded-[14px] border border-white/[0.06] bg-surface 299 text-left cursor-pointer transition-all duration-200 hover:bg-surface-2 hover:border-white/[0.1] 300 active:scale-[0.98]" 301 style={{ fontFamily: 'inherit' }} 302 > 303 <div className="w-9 h-9 rounded-[10px] bg-accent-soft flex items-center justify-center shrink-0 mt-0.5"> 304 <IconComp size={16} className="text-accent-bright" /> 305 </div> 306 <div className="min-w-0"> 307 <div className="text-[14px] font-600 text-text mb-0.5">{tpl.name}</div> 308 <div className="text-[12px] text-text-3/70 leading-[1.4]">{tpl.description}</div> 309 <div className="mt-1.5 text-[11px] text-text-3/40 capitalize">{tpl.category}</div> 310 </div> 311 </button> 312 ) 313 })} 314 </div> 315 <button 316 onClick={() => setStep(whatStep as Step)} 317 className="w-full py-3.5 rounded-[14px] border border-dashed border-white/[0.08] bg-transparent 318 text-text-3 text-[14px] font-600 cursor-pointer transition-all hover:bg-surface hover:text-text-2 hover:border-white/[0.12]" 319 style={{ fontFamily: 'inherit' }} 320 > 321 Start from scratch 322 </button> 323 </div> 324 )} 325 326 {/* Step: What */} 327 {step === whatStep && ( 328 <div> 329 <div className="mb-8"> 330 <SectionLabel>Name</SectionLabel> 331 <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Daily keyword research" className={inputClass} style={{ fontFamily: 'inherit' }} /> 332 </div> 333 334 <div className="mb-8"> 335 <SectionLabel>Agent</SectionLabel> 336 <AgentPickerList 337 agents={agentList} 338 selected={agentId} 339 onSelect={(id) => setAgentId(id)} 340 showDelegationBadge={true} 341 /> 342 </div> 343 344 <div className="mb-8"> 345 <div className="flex items-center gap-2 mb-3"> 346 <SectionLabel className="mb-0">Task Mode</SectionLabel> 347 <HintTip text="Create task: creates a board task. Wake agent only: sends a message without a task. Structured session: launches a bounded structured run instead of a normal task." /> 348 </div> 349 <div className="grid grid-cols-3 gap-3"> 350 <button 351 onClick={() => setTaskMode('task')} 352 className={`py-3 px-4 rounded-[14px] text-center cursor-pointer transition-all duration-200 353 active:scale-[0.97] text-[14px] font-600 border 354 ${taskMode === 'task' 355 ? 'bg-accent-soft border-accent-bright/25 text-accent-bright' 356 : 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`} 357 style={{ fontFamily: 'inherit' }} 358 > 359 Create task 360 </button> 361 <button 362 onClick={() => setTaskMode('wake_only')} 363 className={`py-3 px-4 rounded-[14px] text-center cursor-pointer transition-all duration-200 364 active:scale-[0.97] text-[14px] font-600 border 365 ${taskMode === 'wake_only' 366 ? 'bg-accent-soft border-accent-bright/25 text-accent-bright' 367 : 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`} 368 style={{ fontFamily: 'inherit' }} 369 > 370 Wake agent only 371 </button> 372 <button 373 onClick={() => setTaskMode('protocol')} 374 className={`py-3 px-4 rounded-[14px] text-center cursor-pointer transition-all duration-200 375 active:scale-[0.97] text-[14px] font-600 border 376 ${taskMode === 'protocol' 377 ? 'bg-accent-soft border-accent-bright/25 text-accent-bright' 378 : 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`} 379 style={{ fontFamily: 'inherit' }} 380 > 381 Structured session 382 </button> 383 </div> 384 </div> 385 386 {taskMode === 'wake_only' ? ( 387 <div className="mb-8"> 388 <SectionLabel>Wake Message</SectionLabel> 389 <textarea 390 value={message} 391 onChange={(e) => setMessage(e.target.value)} 392 placeholder="Message to send to the agent when woken" 393 rows={4} 394 className={`${inputClass} resize-y min-h-[100px]`} 395 style={{ fontFamily: 'inherit' }} 396 /> 397 </div> 398 ) : taskMode === 'protocol' ? ( 399 <> 400 <div className="mb-4"> 401 <SectionLabel>Structured Session Goal</SectionLabel> 402 <textarea 403 value={taskPrompt} 404 onChange={(e) => setTaskPrompt(e.target.value)} 405 placeholder="What should this structured session accomplish?" 406 rows={4} 407 className={`${inputClass} resize-y min-h-[100px]`} 408 style={{ fontFamily: 'inherit' }} 409 /> 410 </div> 411 <div className="mb-8"> 412 <SectionLabel>Kickoff Context</SectionLabel> 413 <textarea 414 value={message} 415 onChange={(e) => setMessage(e.target.value)} 416 placeholder="Optional context for the structured session run" 417 rows={3} 418 className={`${inputClass} resize-y min-h-[88px]`} 419 style={{ fontFamily: 'inherit' }} 420 /> 421 </div> 422 </> 423 ) : ( 424 <div className="mb-8"> 425 <SectionLabel>Task Prompt</SectionLabel> 426 <textarea 427 value={taskPrompt} 428 onChange={(e) => setTaskPrompt(e.target.value)} 429 placeholder="What should the agent do when triggered?" 430 rows={4} 431 className={`${inputClass} resize-y min-h-[100px]`} 432 style={{ fontFamily: 'inherit' }} 433 /> 434 </div> 435 )} 436 </div> 437 )} 438 439 {/* Step: When */} 440 {step === whenStep && ( 441 <div> 442 <div className="mb-8"> 443 <div className="flex items-center gap-2 mb-3"> 444 <SectionLabel className="mb-0">Schedule Type</SectionLabel> 445 <HintTip text="Once: runs a single time. Interval: repeats every N minutes. Cron: advanced scheduling with cron syntax" /> 446 </div> 447 <div className="grid grid-cols-3 gap-3"> 448 {(['cron', 'interval', 'once'] as ScheduleType[]).map((t) => ( 449 <button 450 key={t} 451 onClick={() => setScheduleType(t)} 452 className={`py-3.5 px-4 rounded-[14px] text-center cursor-pointer transition-all duration-200 453 active:scale-[0.97] text-[14px] font-600 capitalize border 454 ${scheduleType === t 455 ? 'bg-accent-soft border-accent-bright/25 text-accent-bright' 456 : 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`} 457 style={{ fontFamily: 'inherit' }} 458 > 459 {t} 460 </button> 461 ))} 462 </div> 463 </div> 464 465 {scheduleType === 'cron' && ( 466 <div className="mb-8"> 467 <div className="flex items-center gap-2 mb-3"> 468 <SectionLabel className="mb-0">Schedule</SectionLabel> 469 <HintTip text="Standard cron format: minute hour day month weekday (e.g. 0 9 * * 1-5 = weekdays at 9am)" /> 470 </div> 471 472 {/* Preset buttons */} 473 <div className="flex flex-wrap gap-2 mb-4"> 474 {CRON_PRESETS.map((p) => ( 475 <button 476 key={p.cron} 477 onClick={() => { setCron(p.cron); setCustomCron(false) }} 478 className={`px-3.5 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border 479 ${cron === p.cron && !customCron 480 ? 'bg-accent-soft border-accent-bright/25 text-accent-bright' 481 : 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`} 482 style={{ fontFamily: 'inherit' }} 483 > 484 {p.label} 485 </button> 486 ))} 487 <button 488 onClick={() => setCustomCron(true)} 489 className={`px-3.5 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border 490 ${customCron 491 ? 'bg-accent-soft border-accent-bright/25 text-accent-bright' 492 : 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`} 493 style={{ fontFamily: 'inherit' }} 494 > 495 Custom 496 </button> 497 </div> 498 499 {/* Custom cron input */} 500 {customCron && ( 501 <input type="text" value={cron} onChange={(e) => setCron(e.target.value)} placeholder="0 * * * *" className={`${inputClass} font-mono text-[14px] mb-3`} /> 502 )} 503 504 {/* Human-readable preview */} 505 <div className="p-4 rounded-[14px] bg-surface border border-white/[0.06]"> 506 <div className="text-[14px] text-text-2 font-600 mb-2">{cronHuman}</div> 507 {cron && ( 508 <div className="font-mono text-[12px] text-text-3/50 mb-3">{cron}</div> 509 )} 510 {nextRuns.length > 0 && ( 511 <div className="space-y-1.5"> 512 <div className="text-[11px] text-text-3/60 uppercase tracking-wider font-600">Next runs</div> 513 {nextRuns.map((d, i) => ( 514 <div key={i} className="text-[12px] text-text-3 font-mono">{formatDate(d)}</div> 515 ))} 516 </div> 517 )} 518 </div> 519 </div> 520 )} 521 522 {scheduleType === 'interval' && ( 523 <div className="mb-8"> 524 <SectionLabel>Interval (minutes)</SectionLabel> 525 <input 526 type="number" 527 value={Math.round(intervalMs / 60000)} 528 onChange={(e) => setIntervalMs(Math.max(1, parseInt(e.target.value) || 1) * 60000)} 529 className={inputClass} 530 style={{ fontFamily: 'inherit' }} 531 /> 532 </div> 533 )} 534 535 {editing && ( 536 <div className="mb-8"> 537 <SectionLabel>Status</SectionLabel> 538 <div className="flex gap-2"> 539 {(['active', 'paused'] as ScheduleStatus[]).map((s) => ( 540 <button 541 key={s} 542 onClick={() => setStatus(s)} 543 className={`px-4 py-2 rounded-[10px] text-[13px] font-600 capitalize cursor-pointer transition-all border 544 ${status === s 545 ? 'bg-accent-soft border-accent-bright/25 text-accent-bright' 546 : 'bg-surface border-white/[0.06] text-text-3'}`} 547 style={{ fontFamily: 'inherit' }} 548 > 549 {s} 550 </button> 551 ))} 552 </div> 553 </div> 554 )} 555 </div> 556 )} 557 558 {/* Step: Review */} 559 {step === reviewStep && ( 560 <div className="mb-8"> 561 <div className="p-5 rounded-[16px] bg-surface border border-white/[0.06] space-y-4"> 562 <div> 563 <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Name</span> 564 <div className="text-[14px] text-text font-600 mt-0.5">{name}</div> 565 </div> 566 <div> 567 <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Agent</span> 568 <div className="text-[14px] text-text font-600 mt-0.5">{selectedAgent?.name || agentId}</div> 569 </div> 570 {editing && ( 571 <div> 572 <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Created By</span> 573 {creatorAgent ? ( 574 <div className="mt-1 inline-flex items-center gap-2 rounded-[10px] bg-white/[0.04] px-3 py-2 text-[13px] text-text-2"> 575 <AgentAvatar 576 seed={creatorAgent.avatarSeed} 577 avatarUrl={creatorAgent.avatarUrl} 578 name={creatorAgent.name} 579 size={18} 580 /> 581 <span>{creatorAgent.name}</span> 582 </div> 583 ) : ( 584 <div className="text-[13px] text-text-2 mt-0.5"> 585 {isUserCreatedSchedule(editing) ? 'Manual / user-created' : 'Unknown'} 586 </div> 587 )} 588 </div> 589 )} 590 <div> 591 <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Mode</span> 592 <div className="text-[14px] text-text font-600 mt-0.5">{taskMode === 'wake_only' ? 'Wake agent only' : taskMode === 'protocol' ? 'Structured session' : 'Create task'}</div> 593 </div> 594 <div> 595 <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">{taskMode === 'wake_only' ? 'Wake Message' : taskMode === 'protocol' ? 'Session Goal' : 'Task'}</span> 596 <div className="text-[13px] text-text-2 mt-0.5 whitespace-pre-wrap">{taskMode === 'wake_only' ? message : taskPrompt}</div> 597 </div> 598 {taskMode === 'protocol' && message.trim() && ( 599 <div> 600 <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Kickoff Context</span> 601 <div className="text-[13px] text-text-2 mt-0.5 whitespace-pre-wrap">{message}</div> 602 </div> 603 )} 604 {taskMode === 'protocol' && ( 605 <div> 606 <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Template</span> 607 <div className="text-[13px] text-text-2 mt-0.5">Single-agent structured run</div> 608 </div> 609 )} 610 <div className="h-px bg-white/[0.06]" /> 611 <div> 612 <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Schedule</span> 613 <div className="text-[14px] text-text font-600 mt-0.5 capitalize">{scheduleType}</div> 614 {scheduleType === 'cron' && ( 615 <div className="text-[12px] text-text-3 font-mono mt-0.5">{cronHuman} ({cron})</div> 616 )} 617 {scheduleType === 'interval' && ( 618 <div className="text-[12px] text-text-3 font-mono mt-0.5">Every {Math.round(intervalMs / 60000)} minutes</div> 619 )} 620 {scheduleType === 'once' && ( 621 <div className="text-[12px] text-text-3 font-mono mt-0.5">Run once</div> 622 )} 623 </div> 624 {editing && ( 625 <div> 626 <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Status</span> 627 <div className="text-[14px] text-text font-600 mt-0.5 capitalize">{status}</div> 628 </div> 629 )} 630 </div> 631 </div> 632 )} 633 634 {/* Footer */} 635 <div className="flex gap-3 pt-2 border-t border-white/[0.04]"> 636 {editing && step === 0 && ( 637 <button onClick={() => setConfirmDelete(true)} className="py-3.5 px-6 rounded-[14px] border border-red-500/20 bg-transparent text-red-400 text-[15px] font-600 cursor-pointer hover:bg-red-500/10 transition-all" style={{ fontFamily: 'inherit' }}> 638 Archive 639 </button> 640 )} 641 {step > (isCreating ? templateStep : 0) && step !== templateStep && ( 642 <button 643 onClick={() => setStep((step - 1) as Step)} 644 className="py-3.5 px-6 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer hover:bg-surface-2 transition-all" 645 style={{ fontFamily: 'inherit' }} 646 > 647 Back 648 </button> 649 )} 650 <div className="flex-1" /> 651 {step !== templateStep && ( 652 <button 653 onClick={onClose} 654 className="py-3.5 px-6 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer hover:bg-surface-2 transition-all" 655 style={{ fontFamily: 'inherit' }} 656 > 657 Cancel 658 </button> 659 )} 660 {step === templateStep && isCreating ? ( 661 <button 662 onClick={onClose} 663 className="py-3.5 px-6 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer hover:bg-surface-2 transition-all" 664 style={{ fontFamily: 'inherit' }} 665 > 666 Cancel 667 </button> 668 ) : step < reviewStep ? ( 669 <button 670 onClick={() => setStep((step + 1) as Step)} 671 disabled={step === whatStep ? !step0Valid : !step1Valid} 672 className="py-3.5 px-8 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] disabled:opacity-30 transition-all shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110" 673 style={{ fontFamily: 'inherit' }} 674 > 675 Next 676 </button> 677 ) : ( 678 <button 679 onClick={handleSave} 680 className="py-3.5 px-8 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] transition-all shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110" 681 style={{ fontFamily: 'inherit' }} 682 > 683 {editing ? 'Save' : 'Create'} 684 </button> 685 )} 686 </div> 687 <ConfirmDialog 688 open={confirmDelete} 689 title="Archive Schedule?" 690 message={editing ? `Archive "${editing.name}"? Future runs will stop and any in-flight scheduled task will be cancelled.` : 'Archive this schedule?'} 691 confirmLabel={deleting ? 'Archiving...' : 'Archive'} 692 confirmDisabled={deleting} 693 cancelDisabled={deleting} 694 danger 695 onConfirm={() => { void handleDelete() }} 696 onCancel={() => { if (!deleting) setConfirmDelete(false) }} 697 /> 698 </BottomSheet> 699 ) 700 }