| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import React, { useState, useCallback } from 'react' |
| import { |
| X, |
| ChevronLeft, |
| ChevronRight, |
| Users, |
| Settings, |
| FileText, |
| Check, |
| Zap, |
| MessageSquare, |
| Crown, |
| Activity, |
| Workflow, |
| DollarSign, |
| } from 'lucide-react' |
| import type { PersonaSummary } from './types' |
| import { PersonaSelectorEnterprise } from './PersonaSelectorEnterprise' |
| import { TEAM_BUNDLES } from './teamBundles' |
|
|
| |
| |
| |
|
|
| export interface CreateSessionWizardProps { |
| personas: PersonaSummary[] |
| backendUrl: string |
| onCancel: () => void |
| onCreate: (params: { |
| name: string |
| description: string |
| participant_ids: string[] |
| turn_mode: string |
| agenda: string[] |
| policy?: { engine?: string; crew?: { profile_id?: string; budget_limit_eur?: number } } |
| }) => Promise<void> |
| } |
|
|
| |
| |
| |
|
|
| const TURN_MODES = [ |
| { |
| value: 'reactive', |
| label: 'Reactive (Recommended)', |
| description: 'Only relevant personas speak β best for brainstorming and productivity', |
| icon: <Activity size={16} />, |
| }, |
| { |
| value: 'round-robin', |
| label: 'Round Robin', |
| description: 'Each persona responds in order after you speak', |
| icon: <Zap size={16} />, |
| }, |
| { |
| value: 'free-form', |
| label: 'Free Form', |
| description: 'Personas respond naturally as in a real conversation', |
| icon: <MessageSquare size={16} />, |
| }, |
| { |
| value: 'moderated', |
| label: 'Moderated', |
| description: 'You choose which persona speaks next', |
| icon: <Crown size={16} />, |
| }, |
| ] |
|
|
| const ENGINE_OPTIONS = [ |
| { |
| value: 'native' as const, |
| label: 'Conversation', |
| description: 'Free-form discussion with smart speaker selection', |
| icon: <MessageSquare size={16} />, |
| }, |
| { |
| value: 'crew' as const, |
| label: 'Task Workflow', |
| description: 'Stage-based collaboration with structured output', |
| icon: <Workflow size={16} />, |
| }, |
| ] |
|
|
| const CREW_PROFILES = [ |
| { id: 'task_planner_v1', label: 'Task Planner', description: 'Steps, details, review, and finalize' }, |
| { id: 'brainstorm_v1', label: 'Brainstorm', description: 'Ideate, evaluate, and recommend' }, |
| { id: 'draft_and_edit_v1', label: 'Draft & Edit', description: 'Outline, draft, and refine content' }, |
| ] |
|
|
| |
| |
| |
|
|
| export function CreateSessionWizard({ |
| personas, |
| backendUrl, |
| onCancel, |
| onCreate, |
| }: CreateSessionWizardProps) { |
| const [step, setStep] = useState(1) |
| const totalSteps = 3 |
|
|
| |
| const [name, setName] = useState('') |
| const [description, setDescription] = useState('') |
|
|
| |
| const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set()) |
|
|
| |
| const [engine, setEngine] = useState<'native' | 'crew'>('native') |
| const [crewProfileId, setCrewProfileId] = useState('task_planner_v1') |
| const [budgetLimit, setBudgetLimit] = useState('') |
| const [turnMode, setTurnMode] = useState('reactive') |
| const [agendaText, setAgendaText] = useState('') |
|
|
| const [creating, setCreating] = useState(false) |
|
|
| const togglePersona = useCallback((id: string) => { |
| setSelectedIds((prev) => { |
| const next = new Set(prev) |
| if (next.has(id)) next.delete(id) |
| else next.add(id) |
| return next |
| }) |
| }, []) |
|
|
| const canProceed = step === 1 ? name.trim().length > 0 |
| : step === 2 ? selectedIds.size > 0 |
| : true |
|
|
| const handleCreate = useCallback(async () => { |
| if (creating) return |
| setCreating(true) |
| try { |
| const agenda = agendaText |
| .split('\n') |
| .map((l) => l.trim()) |
| .filter(Boolean) |
| const policy = engine === 'crew' |
| ? { |
| engine: 'crew' as const, |
| crew: { |
| profile_id: crewProfileId, |
| ...(budgetLimit ? { budget_limit_eur: parseFloat(budgetLimit) } : {}), |
| }, |
| } |
| : { engine: 'native' as const } |
|
|
| await onCreate({ |
| name: name.trim(), |
| description: description.trim(), |
| participant_ids: Array.from(selectedIds), |
| turn_mode: turnMode, |
| agenda, |
| policy, |
| }) |
| } catch (e) { |
| console.error('Failed to create session:', e) |
| } finally { |
| setCreating(false) |
| } |
| }, [creating, name, description, selectedIds, turnMode, agendaText, engine, crewProfileId, budgetLimit, onCreate]) |
|
|
| return ( |
| <div className="h-full w-full bg-black text-white font-sans overflow-hidden flex flex-col min-h-0"> |
| |
| {/* βββββββββββββββ HEADER βββββββββββββββ */} |
| <div className="flex-shrink-0 flex items-center justify-between px-6 py-4 border-b border-white/[0.06]"> |
| <div className="flex items-center gap-3"> |
| <Users size={18} className="text-cyan-400" /> |
| <span className="text-sm font-semibold">New Team Session</span> |
| </div> |
| <button |
| onClick={onCancel} |
| className="p-2 rounded-lg hover:bg-white/5 text-white/40 hover:text-white/60 transition-colors" |
| > |
| <X size={16} /> |
| </button> |
| </div> |
| |
| {/* βββββββββββββββ STEP INDICATOR βββββββββββββββ */} |
| <div className="flex-shrink-0 px-6 py-3 flex items-center gap-2"> |
| {[ |
| { n: 1, label: 'Details', icon: <FileText size={12} /> }, |
| { n: 2, label: 'Personas', icon: <Users size={12} /> }, |
| { n: 3, label: 'Settings', icon: <Settings size={12} /> }, |
| ].map(({ n, label, icon }) => ( |
| <React.Fragment key={n}> |
| {n > 1 && <div className={`flex-1 h-px ${step >= n ? 'bg-cyan-500/40' : 'bg-white/[0.06]'}`} />} |
| <div |
| className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${ |
| step === n |
| ? 'bg-cyan-500/15 text-cyan-300 border border-cyan-500/20' |
| : step > n |
| ? 'bg-emerald-500/10 text-emerald-300 border border-emerald-500/20' |
| : 'bg-white/[0.03] text-white/30 border border-white/[0.06]' |
| }`} |
| > |
| {step > n ? <Check size={12} /> : icon} |
| {label} |
| </div> |
| </React.Fragment> |
| ))} |
| </div> |
| |
| {/* βββββββββββββββ STEP CONTENT βββββββββββββββ */} |
| <div className="flex-1 min-h-0 overflow-y-auto px-6 py-4 scrollbar-hide"> |
| |
| {/* Step 1: Details */} |
| {step === 1 && ( |
| <div className="max-w-lg mx-auto space-y-5"> |
| <div> |
| <h2 className="text-lg font-semibold text-white mb-1">Name your session</h2> |
| <p className="text-xs text-white/35">Give your meeting room a name and optional description.</p> |
| </div> |
| |
| <div className="space-y-3"> |
| <div> |
| <label className="block text-xs text-white/50 mb-1.5">Session Name</label> |
| <input |
| type="text" |
| value={name} |
| onChange={(e) => setName(e.target.value)} |
| placeholder="e.g., Morning Standup, Strategy Review..." |
| className="w-full px-3.5 py-2.5 rounded-xl bg-white/[0.04] border border-white/[0.08] text-sm text-white placeholder:text-white/20 focus:outline-none focus:border-cyan-500/40 transition-colors" |
| autoFocus |
| /> |
| </div> |
| |
| <div> |
| <label className="block text-xs text-white/50 mb-1.5">Description (optional)</label> |
| <textarea |
| value={description} |
| onChange={(e) => setDescription(e.target.value)} |
| placeholder="What's this session about?" |
| rows={3} |
| className="w-full px-3.5 py-2.5 rounded-xl bg-white/[0.04] border border-white/[0.08] text-sm text-white placeholder:text-white/20 focus:outline-none focus:border-cyan-500/40 transition-colors resize-none" |
| /> |
| </div> |
| </div> |
| </div> |
| )} |
| |
| {/* Step 2: Select Personas (Enterprise) */} |
| {step === 2 && ( |
| <div className="max-w-5xl mx-auto"> |
| <PersonaSelectorEnterprise |
| personas={personas} |
| backendUrl={backendUrl} |
| selectedIds={selectedIds} |
| onToggle={togglePersona} |
| onSetSelected={setSelectedIds} |
| bundles={TEAM_BUNDLES} |
| /> |
| </div> |
| )} |
| |
| {/* Step 3: Settings */} |
| {step === 3 && ( |
| <div className="max-w-lg mx-auto space-y-5"> |
| <div> |
| <h2 className="text-lg font-semibold text-white mb-1">Session settings</h2> |
| <p className="text-xs text-white/35">Choose how the conversation flows and set an optional agenda.</p> |
| </div> |
| |
| {/* Engine type */} |
| <div> |
| <label className="block text-xs text-white/50 mb-2">Session Type</label> |
| <div className="flex gap-2"> |
| {ENGINE_OPTIONS.map((opt) => ( |
| <button |
| key={opt.value} |
| type="button" |
| onClick={() => setEngine(opt.value)} |
| className={`flex-1 flex items-center gap-3 p-3 rounded-xl border transition-all text-left ${ |
| engine === opt.value |
| ? 'bg-cyan-500/[0.08] border-cyan-500/30' |
| : 'bg-white/[0.02] border-white/[0.06] hover:border-white/15' |
| }`} |
| > |
| <div className={`w-8 h-8 rounded-lg flex items-center justify-center ${ |
| engine === opt.value ? 'bg-cyan-500/20 text-cyan-300' : 'bg-white/5 text-white/30' |
| }`}> |
| {opt.icon} |
| </div> |
| <div className="flex-1"> |
| <div className="text-sm font-medium text-white">{opt.label}</div> |
| <div className="text-[10px] text-white/35">{opt.description}</div> |
| </div> |
| {engine === opt.value && ( |
| <div className="w-5 h-5 rounded-full bg-cyan-500 flex items-center justify-center"> |
| <Check size={12} className="text-white" /> |
| </div> |
| )} |
| </button> |
| ))} |
| </div> |
| </div> |
| |
| {/* Crew workflow settings (only when engine === 'crew') */} |
| {engine === 'crew' && ( |
| <div className="space-y-3 rounded-xl border border-cyan-500/15 bg-cyan-500/[0.03] p-4"> |
| <div> |
| <label className="block text-xs text-white/50 mb-2">Workflow Profile</label> |
| <div className="space-y-2"> |
| {CREW_PROFILES.map((profile) => ( |
| <button |
| key={profile.id} |
| type="button" |
| onClick={() => setCrewProfileId(profile.id)} |
| className={`w-full flex items-center gap-3 p-2.5 rounded-lg border transition-all text-left ${ |
| crewProfileId === profile.id |
| ? 'bg-cyan-500/[0.08] border-cyan-500/25' |
| : 'bg-white/[0.02] border-white/[0.06] hover:border-white/15' |
| }`} |
| > |
| <div className="flex-1"> |
| <div className="text-sm font-medium text-white">{profile.label}</div> |
| <div className="text-[10px] text-white/35">{profile.description}</div> |
| </div> |
| {crewProfileId === profile.id && ( |
| <div className="w-4 h-4 rounded-full bg-cyan-500 flex items-center justify-center"> |
| <Check size={10} className="text-white" /> |
| </div> |
| )} |
| </button> |
| ))} |
| </div> |
| </div> |
| <div> |
| <label className="block text-xs text-white/50 mb-1.5">Budget Limit (optional)</label> |
| <div className="flex items-center gap-2"> |
| <DollarSign size={14} className="text-white/30" /> |
| <input |
| type="number" |
| value={budgetLimit} |
| onChange={(e) => setBudgetLimit(e.target.value)} |
| placeholder="No limit" |
| min="0" |
| step="5" |
| className="flex-1 px-3 py-2 rounded-lg bg-white/[0.04] border border-white/[0.08] text-sm text-white placeholder:text-white/20 focus:outline-none focus:border-cyan-500/40 transition-colors" |
| /> |
| </div> |
| </div> |
| </div> |
| )} |
| |
| {/* Turn mode (only visible for native engine) */} |
| {engine === 'native' && ( |
| <div> |
| <label className="block text-xs text-white/50 mb-2">Conversation Style</label> |
| <div className="space-y-2"> |
| {TURN_MODES.map((tm) => ( |
| <button |
| key={tm.value} |
| type="button" |
| onClick={() => setTurnMode(tm.value)} |
| className={`w-full flex items-center gap-3 p-3 rounded-xl border transition-all text-left ${ |
| turnMode === tm.value |
| ? 'bg-cyan-500/[0.08] border-cyan-500/30' |
| : 'bg-white/[0.02] border-white/[0.06] hover:border-white/15' |
| }`} |
| > |
| <div className={`w-8 h-8 rounded-lg flex items-center justify-center ${ |
| turnMode === tm.value ? 'bg-cyan-500/20 text-cyan-300' : 'bg-white/5 text-white/30' |
| }`}> |
| {tm.icon} |
| </div> |
| <div className="flex-1"> |
| <div className="text-sm font-medium text-white">{tm.label}</div> |
| <div className="text-[10px] text-white/35">{tm.description}</div> |
| </div> |
| {turnMode === tm.value && ( |
| <div className="w-5 h-5 rounded-full bg-cyan-500 flex items-center justify-center"> |
| <Check size={12} className="text-white" /> |
| </div> |
| )} |
| </button> |
| ))} |
| </div> |
| </div> |
| )} |
| |
| {/* Agenda */} |
| <div> |
| <label className="block text-xs text-white/50 mb-1.5">Agenda (optional, one topic per line)</label> |
| <textarea |
| value={agendaText} |
| onChange={(e) => setAgendaText(e.target.value)} |
| placeholder={"Status updates\nBlockers\nAction items"} |
| rows={4} |
| className="w-full px-3.5 py-2.5 rounded-xl bg-white/[0.04] border border-white/[0.08] text-sm text-white placeholder:text-white/20 focus:outline-none focus:border-cyan-500/40 transition-colors resize-none" |
| /> |
| </div> |
| |
| {/* Summary */} |
| <div className="rounded-xl border border-white/[0.06] bg-white/[0.02] p-4 space-y-2"> |
| <div className="text-xs text-white/50 font-medium">Summary</div> |
| <div className="text-sm text-white font-medium">{name || 'Untitled'}</div> |
| <div className="flex items-center gap-3 text-[11px] text-white/40"> |
| <span className="flex items-center gap-1"> |
| <Users size={10} /> |
| {selectedIds.size} persona{selectedIds.size !== 1 ? 's' : ''} + You |
| </span> |
| <span className="flex items-center gap-1"> |
| {ENGINE_OPTIONS.find((e) => e.value === engine)?.icon} |
| {ENGINE_OPTIONS.find((e) => e.value === engine)?.label} |
| </span> |
| {engine === 'native' && ( |
| <span className="flex items-center gap-1"> |
| {TURN_MODES.find((t) => t.value === turnMode)?.icon} |
| {TURN_MODES.find((t) => t.value === turnMode)?.label} |
| </span> |
| )} |
| {engine === 'crew' && ( |
| <span className="flex items-center gap-1 text-cyan-300/50"> |
| <Workflow size={10} /> |
| {CREW_PROFILES.find((p) => p.id === crewProfileId)?.label} |
| </span> |
| )} |
| </div> |
| </div> |
| </div> |
| )} |
| </div> |
| |
| {/* βββββββββββββββ FOOTER βββββββββββββββ */} |
| <div className="flex-shrink-0 px-6 py-4 border-t border-white/[0.06] flex items-center justify-between"> |
| <button |
| onClick={step > 1 ? () => setStep((s) => s - 1) : onCancel} |
| className="flex items-center gap-1.5 px-4 py-2 rounded-xl text-sm text-white/50 hover:text-white/70 bg-white/[0.04] hover:bg-white/[0.08] border border-white/[0.06] transition-all" |
| > |
| <ChevronLeft size={14} /> |
| {step > 1 ? 'Back' : 'Cancel'} |
| </button> |
| |
| <div className="text-xs text-white/20"> |
| Step {step} of {totalSteps} |
| </div> |
| |
| <button |
| onClick={step < totalSteps ? () => setStep((s) => s + 1) : handleCreate} |
| disabled={!canProceed || creating} |
| className={`flex items-center gap-1.5 px-5 py-2 rounded-xl text-sm font-semibold transition-all ${ |
| canProceed && !creating |
| ? step < totalSteps |
| ? 'bg-cyan-600 hover:bg-cyan-500 text-white border border-cyan-500/20' |
| : 'bg-gradient-to-r from-cyan-600 to-blue-600 hover:brightness-110 text-white border border-cyan-500/20 shadow-lg shadow-cyan-500/10' |
| : 'bg-white/[0.04] text-white/20 border border-white/[0.04] cursor-not-allowed' |
| }`} |
| > |
| {creating ? ( |
| 'Creating...' |
| ) : step < totalSteps ? ( |
| <>Next <ChevronRight size={14} /></> |
| ) : ( |
| <>Create Session <Check size={14} /></> |
| )} |
| </button> |
| </div> |
|
|
| <style>{` |
| .scrollbar-hide::-webkit-scrollbar { display: none; } |
| .scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; } |
| `}</style> |
| </div> |
| ) |
| } |
|
|