Skip to content

Commit bf65c13

Browse files
Merge pull request #23 from databricks-solutions/vivian_dev
feat: Multi-workshop support with one db
2 parents 6fe382e + 54fd463 commit bf65c13

7 files changed

Lines changed: 414 additions & 17 deletions

File tree

client/src/components/ProductionLogin.tsx

Lines changed: 156 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react';
1+
import React, { useState, useEffect } from 'react';
22
import { useUser } from '@/context/UserContext';
33
import { useWorkshopContext } from '@/context/WorkshopContext';
44
import { UsersService } from '@/client';
@@ -7,12 +7,41 @@ import { Input } from '@/components/ui/input';
77
import { Label } from '@/components/ui/label';
88
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
99
import { Alert, AlertDescription } from '@/components/ui/alert';
10+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
11+
import { Loader2, Plus } from 'lucide-react';
12+
import type { Workshop } from '@/client';
1013

1114
export const ProductionLogin: React.FC = () => {
1215
const { setUser } = useUser();
13-
const { workshopId } = useWorkshopContext();
16+
const { workshopId, setWorkshopId } = useWorkshopContext();
1417
const [isLoading, setIsLoading] = useState(false);
1518
const [error, setError] = useState<string | null>(null);
19+
const [workshops, setWorkshops] = useState<Workshop[]>([]);
20+
const [isLoadingWorkshops, setIsLoadingWorkshops] = useState(true);
21+
const [selectedWorkshopId, setSelectedWorkshopId] = useState<string>(workshopId || '');
22+
const [createNewWorkshop, setCreateNewWorkshop] = useState(false);
23+
24+
// Fetch available workshops on mount
25+
useEffect(() => {
26+
const fetchWorkshops = async () => {
27+
try {
28+
const response = await fetch('/workshops/');
29+
if (response.ok) {
30+
const data = await response.json();
31+
setWorkshops(data);
32+
// If there's only one workshop, auto-select it
33+
if (data.length === 1 && !workshopId) {
34+
setSelectedWorkshopId(data[0].id);
35+
}
36+
}
37+
} catch (err) {
38+
console.error('Failed to fetch workshops:', err);
39+
} finally {
40+
setIsLoadingWorkshops(false);
41+
}
42+
};
43+
fetchWorkshops();
44+
}, [workshopId]);
1645

1746
// Login form state
1847
const [loginData, setLoginData] = useState({
@@ -25,12 +54,33 @@ export const ProductionLogin: React.FC = () => {
2554
setIsLoading(true);
2655
setError(null);
2756

57+
// Validate workshop selection for non-facilitators
58+
if (!selectedWorkshopId && !loginData.password) {
59+
setError('Please select a workshop to join.');
60+
setIsLoading(false);
61+
return;
62+
}
63+
2864
try {
2965
const response = await UsersService.loginUsersAuthLoginPost({
3066
email: loginData.email,
3167
password: loginData.password
3268
});
3369

70+
// Handle facilitator creating new workshop
71+
if (loginData.password && createNewWorkshop) {
72+
// Clear workshop ID to go to workshop creation page
73+
setWorkshopId(null);
74+
localStorage.removeItem('workshop_id');
75+
window.history.replaceState({}, '', '/');
76+
}
77+
// Set workshop ID if selected (for existing workshops)
78+
else if (selectedWorkshopId && !response.user.workshop_id) {
79+
// Update the workshop context
80+
setWorkshopId(selectedWorkshopId);
81+
window.history.pushState({}, '', `?workshop=${selectedWorkshopId}`);
82+
}
83+
3484
// Set the user in context
3585
await setUser(response.user);
3686
} catch (error: any) {
@@ -81,14 +131,116 @@ export const ProductionLogin: React.FC = () => {
81131
/>
82132
</div>
83133

134+
{/* Workshop Selection for Participants/SMEs */}
135+
{!loginData.password && (
136+
<div className="space-y-2">
137+
<Label htmlFor="workshop">Select Workshop</Label>
138+
{isLoadingWorkshops ? (
139+
<div className="flex items-center justify-center py-2">
140+
<Loader2 className="h-4 w-4 animate-spin mr-2" />
141+
<span className="text-sm text-gray-500">Loading workshops...</span>
142+
</div>
143+
) : workshops.length === 0 ? (
144+
<div className="text-sm text-amber-600 bg-amber-50 p-3 rounded-lg">
145+
No workshops available. Please wait for a facilitator to create one.
146+
</div>
147+
) : (
148+
<Select
149+
value={selectedWorkshopId}
150+
onValueChange={setSelectedWorkshopId}
151+
>
152+
<SelectTrigger>
153+
<SelectValue placeholder="Select a workshop to join" />
154+
</SelectTrigger>
155+
<SelectContent>
156+
{workshops.map((workshop) => (
157+
<SelectItem key={workshop.id} value={workshop.id}>
158+
{workshop.name}
159+
</SelectItem>
160+
))}
161+
</SelectContent>
162+
</Select>
163+
)}
164+
</div>
165+
)}
166+
167+
{/* Workshop Selection for Facilitators */}
168+
{loginData.password && (
169+
<div className="space-y-3">
170+
<Label>Workshop</Label>
171+
172+
{/* Toggle between existing and new */}
173+
<div className="flex gap-2">
174+
<Button
175+
type="button"
176+
variant={!createNewWorkshop ? "default" : "outline"}
177+
size="sm"
178+
className="flex-1"
179+
onClick={() => setCreateNewWorkshop(false)}
180+
>
181+
Join Existing
182+
</Button>
183+
<Button
184+
type="button"
185+
variant={createNewWorkshop ? "default" : "outline"}
186+
size="sm"
187+
className="flex-1"
188+
onClick={() => setCreateNewWorkshop(true)}
189+
>
190+
<Plus className="h-4 w-4 mr-1" />
191+
Create New
192+
</Button>
193+
</div>
194+
195+
{!createNewWorkshop ? (
196+
// Existing workshop selection
197+
isLoadingWorkshops ? (
198+
<div className="flex items-center justify-center py-2">
199+
<Loader2 className="h-4 w-4 animate-spin mr-2" />
200+
<span className="text-sm text-gray-500">Loading workshops...</span>
201+
</div>
202+
) : workshops.length === 0 ? (
203+
<div className="text-sm text-blue-600 bg-blue-50 p-3 rounded-lg">
204+
No existing workshops. Click "Create New" to start a new workshop.
205+
</div>
206+
) : (
207+
<Select
208+
value={selectedWorkshopId}
209+
onValueChange={setSelectedWorkshopId}
210+
>
211+
<SelectTrigger>
212+
<SelectValue placeholder="Select a workshop" />
213+
</SelectTrigger>
214+
<SelectContent>
215+
{workshops.map((workshop) => (
216+
<SelectItem key={workshop.id} value={workshop.id}>
217+
{workshop.name}
218+
</SelectItem>
219+
))}
220+
</SelectContent>
221+
</Select>
222+
)
223+
) : (
224+
// New workshop indicator
225+
<div className="text-sm text-green-600 bg-green-50 p-3 rounded-lg">
226+
A new workshop will be created after you sign in.
227+
</div>
228+
)}
229+
</div>
230+
)}
231+
84232
{error && (
85233
<Alert variant="destructive">
86234
<AlertDescription>{error}</AlertDescription>
87235
</Alert>
88236
)}
89237

90-
<Button type="submit" className="w-full" disabled={isLoading}>
91-
{isLoading ? 'Signing in...' : 'Sign In'}
238+
<Button
239+
type="submit"
240+
className="w-full"
241+
disabled={isLoading || (!loginData.password && !selectedWorkshopId) || (loginData.password && !createNewWorkshop && !selectedWorkshopId && workshops.length > 0)}
242+
>
243+
{isLoading ? 'Signing in...' : createNewWorkshop ? 'Sign In & Create Workshop' : 'Sign In'}
92244
</Button>
93245
</form>
94246

client/src/components/WorkshopCreationPage.tsx

Lines changed: 106 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,22 @@ import { Input } from '@/components/ui/input';
55
import { Label } from '@/components/ui/label';
66
import { Textarea } from '@/components/ui/textarea';
77
import { Alert, AlertDescription } from '@/components/ui/alert';
8-
import { Loader2, Sparkles, Users, Target } from 'lucide-react';
8+
import { Loader2, Sparkles, Users, Target, FolderOpen, Calendar, Clock, ChevronRight } from 'lucide-react';
99
import { useWorkshopContext } from '@/context/WorkshopContext';
1010
import { useUser } from '@/context/UserContext';
11-
import { useCreateWorkshop } from '@/hooks/useWorkshopApi';
11+
import { useCreateWorkshop, useListWorkshops } from '@/hooks/useWorkshopApi';
12+
import type { Workshop } from '@/client';
1213

1314
export function WorkshopCreationPage() {
1415
const { setWorkshopId } = useWorkshopContext();
1516
const { user } = useUser();
1617
const createWorkshop = useCreateWorkshop();
18+
const { data: workshops, isLoading: isLoadingWorkshops } = useListWorkshops({
19+
userId: user?.id,
20+
enabled: !!user?.id
21+
});
1722

23+
const [showExisting, setShowExisting] = useState(true);
1824
const [formData, setFormData] = useState({
1925
name: 'LLM Judge Calibration Workshop',
2026
description: 'A collaborative workshop to calibrate LLM judges through structured evaluation and consensus building.'
@@ -66,9 +72,40 @@ export function WorkshopCreationPage() {
6672
}
6773
};
6874

75+
const handleSelectWorkshop = (workshop: Workshop) => {
76+
setWorkshopId(workshop.id);
77+
window.history.pushState({}, '', `?workshop=${workshop.id}`);
78+
};
79+
80+
const formatDate = (dateString: string | null | undefined) => {
81+
if (!dateString) return 'Unknown';
82+
const date = new Date(dateString);
83+
return date.toLocaleDateString('en-US', {
84+
month: 'short',
85+
day: 'numeric',
86+
year: 'numeric',
87+
hour: '2-digit',
88+
minute: '2-digit'
89+
});
90+
};
91+
92+
const getPhaseLabel = (phase: string | null | undefined) => {
93+
if (!phase) return 'Not Started';
94+
const phases: Record<string, string> = {
95+
'intake': 'Intake',
96+
'discovery': 'Discovery',
97+
'rubric': 'Rubric Creation',
98+
'annotation': 'Annotation',
99+
'results': 'Results Review',
100+
'judge_tuning': 'Judge Tuning',
101+
'unity_volume': 'Unity Volume'
102+
};
103+
return phases[phase] || phase;
104+
};
105+
69106
return (
70-
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-6">
71-
<div className="w-full max-w-2xl">
107+
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 overflow-auto py-8 px-6">
108+
<div className="w-full max-w-2xl mx-auto">
72109
{/* Header */}
73110
<div className="text-center mb-8">
74111
<div className="flex justify-center items-center gap-3 mb-4">
@@ -78,10 +115,73 @@ export function WorkshopCreationPage() {
78115
</h1>
79116
</div>
80117
<p className="text-xl text-gray-600 max-w-xl mx-auto">
81-
Create your LLM Judge Calibration workshop to get started
118+
{workshops && workshops.length > 0
119+
? 'Continue an existing workshop or create a new one'
120+
: 'Create your LLM Judge Calibration workshop to get started'
121+
}
82122
</p>
83123
</div>
84124

125+
{/* Existing Workshops */}
126+
{workshops && workshops.length > 0 && (
127+
<Card className="mb-6 border-green-200 bg-green-50/50">
128+
<CardHeader className="cursor-pointer" onClick={() => setShowExisting(!showExisting)}>
129+
<CardTitle className="flex items-center justify-between">
130+
<div className="flex items-center gap-2">
131+
<FolderOpen className="h-5 w-5 text-green-600" />
132+
Your Workshops ({workshops.length})
133+
</div>
134+
<ChevronRight className={`h-5 w-5 text-gray-400 transition-transform ${showExisting ? 'rotate-90' : ''}`} />
135+
</CardTitle>
136+
<CardDescription>
137+
Click to {showExisting ? 'hide' : 'show'} your existing workshops
138+
</CardDescription>
139+
</CardHeader>
140+
{showExisting && (
141+
<CardContent className="space-y-3">
142+
{isLoadingWorkshops ? (
143+
<div className="flex items-center justify-center py-4">
144+
<Loader2 className="h-6 w-6 animate-spin text-green-600" />
145+
<span className="ml-2 text-gray-600">Loading workshops...</span>
146+
</div>
147+
) : (
148+
workshops.map((workshop) => (
149+
<div
150+
key={workshop.id}
151+
className="p-4 bg-white rounded-lg border border-gray-200 hover:border-green-400 hover:shadow-md transition-all cursor-pointer group"
152+
onClick={() => handleSelectWorkshop(workshop)}
153+
>
154+
<div className="flex items-start justify-between">
155+
<div className="flex-1">
156+
<h3 className="font-semibold text-gray-900 group-hover:text-green-700 transition-colors">
157+
{workshop.name}
158+
</h3>
159+
{workshop.description && (
160+
<p className="text-sm text-gray-500 mt-1 line-clamp-2">
161+
{workshop.description}
162+
</p>
163+
)}
164+
<div className="flex items-center gap-4 mt-2 text-xs text-gray-400">
165+
<span className="flex items-center gap-1">
166+
<Calendar className="h-3 w-3" />
167+
{formatDate(workshop.created_at)}
168+
</span>
169+
<span className="flex items-center gap-1 px-2 py-0.5 rounded-full bg-blue-100 text-blue-700">
170+
<Clock className="h-3 w-3" />
171+
{getPhaseLabel(workshop.current_phase)}
172+
</span>
173+
</div>
174+
</div>
175+
<ChevronRight className="h-5 w-5 text-gray-300 group-hover:text-green-500 group-hover:translate-x-1 transition-all" />
176+
</div>
177+
</div>
178+
))
179+
)}
180+
</CardContent>
181+
)}
182+
</Card>
183+
)}
184+
85185
{/* Quick Start Option */}
86186
<Card className="mb-6 border-blue-200 bg-blue-50/50">
87187
<CardHeader>
@@ -108,7 +208,7 @@ export function WorkshopCreationPage() {
108208
) : (
109209
<>
110210
<Sparkles className="h-4 w-4 mr-2" />
111-
Start Workshop Now
211+
Create New Workshop
112212
</>
113213
)}
114214
</Button>

0 commit comments

Comments
 (0)