@goscribe/server 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/AUTH_FRONTEND_SPEC.md +21 -0
  2. package/CHAT_FRONTEND_SPEC.md +474 -0
  3. package/MEETINGSUMMARY_FRONTEND_SPEC.md +28 -0
  4. package/PODCAST_FRONTEND_SPEC.md +595 -0
  5. package/STUDYGUIDE_FRONTEND_SPEC.md +18 -0
  6. package/WORKSHEETS_FRONTEND_SPEC.md +26 -0
  7. package/WORKSPACE_FRONTEND_SPEC.md +47 -0
  8. package/dist/context.d.ts +1 -1
  9. package/dist/lib/ai-session.d.ts +26 -0
  10. package/dist/lib/ai-session.js +343 -0
  11. package/dist/lib/auth.js +10 -6
  12. package/dist/lib/inference.d.ts +2 -0
  13. package/dist/lib/inference.js +21 -0
  14. package/dist/lib/pusher.d.ts +14 -0
  15. package/dist/lib/pusher.js +94 -0
  16. package/dist/lib/storage.d.ts +10 -2
  17. package/dist/lib/storage.js +63 -6
  18. package/dist/routers/_app.d.ts +878 -100
  19. package/dist/routers/_app.js +8 -2
  20. package/dist/routers/ai-session.d.ts +0 -0
  21. package/dist/routers/ai-session.js +1 -0
  22. package/dist/routers/auth.d.ts +13 -11
  23. package/dist/routers/auth.js +50 -21
  24. package/dist/routers/chat.d.ts +171 -0
  25. package/dist/routers/chat.js +270 -0
  26. package/dist/routers/flashcards.d.ts +51 -39
  27. package/dist/routers/flashcards.js +143 -31
  28. package/dist/routers/meetingsummary.d.ts +0 -0
  29. package/dist/routers/meetingsummary.js +377 -0
  30. package/dist/routers/podcast.d.ts +277 -0
  31. package/dist/routers/podcast.js +847 -0
  32. package/dist/routers/studyguide.d.ts +54 -0
  33. package/dist/routers/studyguide.js +125 -0
  34. package/dist/routers/worksheets.d.ts +147 -40
  35. package/dist/routers/worksheets.js +348 -33
  36. package/dist/routers/workspace.d.ts +163 -8
  37. package/dist/routers/workspace.js +453 -8
  38. package/dist/server.d.ts +1 -1
  39. package/dist/server.js +7 -2
  40. package/dist/trpc.d.ts +5 -5
  41. package/package.json +11 -3
  42. package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +213 -0
  43. package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +31 -0
  44. package/prisma/migrations/migration_lock.toml +3 -0
  45. package/prisma/schema.prisma +87 -6
  46. package/prisma/seed.mjs +135 -0
  47. package/src/lib/ai-session.ts +411 -0
  48. package/src/lib/auth.ts +1 -1
  49. package/src/lib/inference.ts +21 -0
  50. package/src/lib/pusher.ts +104 -0
  51. package/src/lib/storage.ts +89 -6
  52. package/src/routers/_app.ts +6 -0
  53. package/src/routers/auth.ts +8 -4
  54. package/src/routers/chat.ts +275 -0
  55. package/src/routers/flashcards.ts +151 -33
  56. package/src/routers/meetingsummary.ts +416 -0
  57. package/src/routers/podcast.ts +934 -0
  58. package/src/routers/studyguide.ts +144 -0
  59. package/src/routers/worksheets.ts +346 -18
  60. package/src/routers/workspace.ts +500 -8
  61. package/src/server.ts +7 -2
  62. package/test-ai-integration.js +134 -0
  63. package/dist/context.d.ts.map +0 -1
  64. package/dist/index.d.ts.map +0 -1
  65. package/dist/lib/auth.d.ts.map +0 -1
  66. package/dist/lib/file.d.ts.map +0 -1
  67. package/dist/lib/prisma.d.ts.map +0 -1
  68. package/dist/lib/storage.d.ts.map +0 -1
  69. package/dist/routers/_app.d.ts.map +0 -1
  70. package/dist/routers/auth.d.ts.map +0 -1
  71. package/dist/routers/sample.js +0 -21
  72. package/dist/routers/workspace.d.ts.map +0 -1
  73. package/dist/server.d.ts.map +0 -1
  74. package/dist/trpc.d.ts.map +0 -1
@@ -0,0 +1,595 @@
1
+ # Podcast Frontend Integration Specification
2
+
3
+ ## Overview
4
+ This document outlines the frontend requirements for integrating with the podcast functionality, including real-time updates via Pusher, UI components, and user interactions.
5
+
6
+ ## User Prompt Field
7
+ The `userPrompt` field is the primary input for podcast generation. Users provide a prompt describing:
8
+ - The topic they want the podcast to cover
9
+ - Specific questions they want answered
10
+ - The type of content they're looking for
11
+ - Any particular angle or perspective they want
12
+
13
+ Examples of user prompts:
14
+ - "Create a podcast about the history of artificial intelligence"
15
+ - "Explain quantum computing in simple terms for beginners"
16
+ - "Discuss the impact of social media on mental health"
17
+ - "Create a podcast episode about sustainable living practices"
18
+
19
+ The AI will use this prompt to generate complete podcast content, including all segments, scripts, and structure.
20
+
21
+ ## Audio Joining Functionality
22
+ The podcast system now generates both individual segment audio files and a complete joined episode audio file:
23
+
24
+ ### Individual Segments
25
+ - Each segment is generated as a separate audio file
26
+ - Useful for segment-by-segment playback and editing
27
+ - Allows users to jump to specific parts of the episode
28
+
29
+ ### Full Episode Audio
30
+ - All segments are joined into a single continuous audio file
31
+ - Provides a seamless listening experience
32
+ - Useful for traditional podcast playback
33
+ - Stored as a separate file with its own signed URL
34
+
35
+ ### Audio Joining Process
36
+ 1. After all individual segments are generated
37
+ 2. Audio buffers are concatenated in the correct order
38
+ 3. The joined audio is uploaded to cloud storage
39
+ 4. A signed URL is generated for the full episode
40
+ 5. Both individual segments and full episode remain available
41
+
42
+ ## Real-Time Events (Pusher)
43
+
44
+ ### Event Channels
45
+ All podcast events are broadcast on the channel: `workspace_{workspaceId}`
46
+
47
+ ### Event Types
48
+
49
+ #### 1. Podcast Generation Events
50
+ ```typescript
51
+ // Generation Start
52
+ {
53
+ event: 'workspace_{workspaceId}_podcast_generation_start',
54
+ data: {
55
+ title: string
56
+ }
57
+ }
58
+
59
+ // Structure Complete
60
+ {
61
+ event: 'workspace_{workspaceId}_podcast_structure_complete',
62
+ data: {
63
+ segmentsCount: number
64
+ }
65
+ }
66
+
67
+ // Audio Generation Start
68
+ {
69
+ event: 'workspace_{workspaceId}_podcast_audio_generation_start',
70
+ data: {
71
+ totalSegments: number
72
+ }
73
+ }
74
+
75
+ // Segment Progress
76
+ {
77
+ event: 'workspace_{workspaceId}_podcast_segment_progress',
78
+ data: {
79
+ currentSegment: number,
80
+ totalSegments: number,
81
+ segmentTitle: string
82
+ }
83
+ }
84
+
85
+ // Audio Generation Complete
86
+ {
87
+ event: 'workspace_{workspaceId}_podcast_audio_generation_complete',
88
+ data: {
89
+ totalSegments: number,
90
+ totalDuration: number
91
+ }
92
+ }
93
+
94
+ // Audio Joining Start
95
+ {
96
+ event: 'workspace_{workspaceId}_podcast_audio_joining_start',
97
+ data: {
98
+ totalSegments: number
99
+ }
100
+ }
101
+
102
+ // Audio Joining Complete
103
+ {
104
+ event: 'workspace_{workspaceId}_podcast_audio_joining_complete',
105
+ data: {
106
+ fullEpisodeObjectKey: string
107
+ }
108
+ }
109
+
110
+ // Audio Joining Error
111
+ {
112
+ event: 'workspace_{workspaceId}_podcast_audio_joining_error',
113
+ data: {
114
+ error: string
115
+ }
116
+ }
117
+
118
+ // Summary Complete
119
+ {
120
+ event: 'workspace_{workspaceId}_podcast_summary_complete',
121
+ data: {
122
+ summaryGenerated: boolean
123
+ }
124
+ }
125
+
126
+ // Generation Complete
127
+ {
128
+ event: 'workspace_{workspaceId}_podcast_ended',
129
+ data: {
130
+ artifactId: string,
131
+ title: string,
132
+ status: 'completed'
133
+ }
134
+ }
135
+ ```
136
+
137
+ #### 2. Segment Regeneration Events
138
+ ```typescript
139
+ // Regeneration Start
140
+ {
141
+ event: 'workspace_{workspaceId}_podcast_segment_regeneration_start',
142
+ data: {
143
+ segmentId: string,
144
+ segmentTitle: string
145
+ }
146
+ }
147
+
148
+ // Regeneration Complete
149
+ {
150
+ event: 'workspace_{workspaceId}_podcast_segment_regeneration_complete',
151
+ data: {
152
+ segmentId: string,
153
+ segmentTitle: string,
154
+ duration: number
155
+ }
156
+ }
157
+
158
+ // Full Episode Regeneration Start (after segment update)
159
+ {
160
+ event: 'workspace_{workspaceId}_podcast_full_episode_regeneration_start',
161
+ data: {
162
+ reason: 'segment_updated'
163
+ }
164
+ }
165
+
166
+ // Full Episode Regeneration Complete
167
+ {
168
+ event: 'workspace_{workspaceId}_podcast_full_episode_regeneration_complete',
169
+ data: {
170
+ fullEpisodeObjectKey: string
171
+ }
172
+ }
173
+
174
+ // Full Episode Regeneration Error
175
+ {
176
+ event: 'workspace_{workspaceId}_podcast_full_episode_regeneration_error',
177
+ data: {
178
+ error: string
179
+ }
180
+ }
181
+ ```
182
+
183
+ #### 3. Episode Deletion Events
184
+ ```typescript
185
+ // Deletion Start
186
+ {
187
+ event: 'workspace_{workspaceId}_podcast_deletion_start',
188
+ data: {
189
+ episodeId: string,
190
+ episodeTitle: string
191
+ }
192
+ }
193
+
194
+ // Deletion Complete
195
+ {
196
+ event: 'workspace_{workspaceId}_podcast_deletion_complete',
197
+ data: {
198
+ episodeId: string,
199
+ episodeTitle: string
200
+ }
201
+ }
202
+ ```
203
+
204
+ #### 4. Error Events
205
+ ```typescript
206
+ // General Error
207
+ {
208
+ event: 'workspace_{workspaceId}_podcast_error',
209
+ data: {
210
+ error: string,
211
+ analysisType: 'podcast',
212
+ timestamp: string
213
+ }
214
+ }
215
+
216
+ // Segment Error
217
+ {
218
+ event: 'workspace_{workspaceId}_podcast_segment_error',
219
+ data: {
220
+ segmentIndex: number,
221
+ error: string
222
+ }
223
+ }
224
+
225
+ // Summary Error
226
+ {
227
+ event: 'workspace_{workspaceId}_podcast_summary_error',
228
+ data: {
229
+ error: string
230
+ }
231
+ }
232
+ ```
233
+
234
+ ## UI Components
235
+
236
+ ### 1. Podcast Generation Form
237
+ ```typescript
238
+ interface PodcastGenerationForm {
239
+ title: string;
240
+ description?: string;
241
+ userPrompt: string;
242
+ voice: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer';
243
+ speed: number; // 0.25 - 4.0
244
+ generateIntro: boolean;
245
+ generateOutro: boolean;
246
+ segmentByTopics: boolean;
247
+ }
248
+ ```
249
+
250
+ **Required Fields:**
251
+ - Title (required)
252
+ - User Prompt (required) - The topic, question, or request for the podcast
253
+ - Voice (default: 'nova')
254
+ - Speed (default: 1.0)
255
+
256
+ **Optional Fields:**
257
+ - Description
258
+ - Generate Intro (default: true)
259
+ - Generate Outro (default: true)
260
+ - Segment by Topics (default: true)
261
+
262
+ ### 2. Podcast List View
263
+ ```typescript
264
+ interface PodcastEpisode {
265
+ id: string;
266
+ title: string;
267
+ description?: string;
268
+ metadata: {
269
+ totalDuration: number;
270
+ voice: string;
271
+ speed: number;
272
+ segments: PodcastSegment[];
273
+ fullEpisodeObjectKey?: string; // Reference to the full joined episode audio
274
+ summary: {
275
+ executiveSummary: string;
276
+ learningObjectives: string[];
277
+ keyConcepts: string[];
278
+ followUpActions: string[];
279
+ targetAudience: string;
280
+ prerequisites: string[];
281
+ tags: string[];
282
+ };
283
+ generatedAt: string;
284
+ };
285
+ segments: {
286
+ id: string;
287
+ title: string;
288
+ audioUrl?: string;
289
+ objectKey?: string;
290
+ startTime: number;
291
+ duration: number;
292
+ order: number;
293
+ }[];
294
+ fullEpisodeUrl?: string; // Signed URL for the full episode
295
+ createdAt: string;
296
+ updatedAt: string;
297
+ }
298
+ ```
299
+
300
+ ### 3. Podcast Player Component
301
+ ```typescript
302
+ interface PodcastPlayer {
303
+ currentSegment: number;
304
+ isPlaying: boolean;
305
+ currentTime: number;
306
+ duration: number;
307
+ segments: PodcastSegment[];
308
+ onSegmentChange: (segmentId: string) => void;
309
+ onPlayPause: () => void;
310
+ onSeek: (time: number) => void;
311
+ }
312
+ ```
313
+
314
+ ### 4. Segment Editor
315
+ ```typescript
316
+ interface SegmentEditor {
317
+ segmentId: string;
318
+ title: string;
319
+ content: string;
320
+ keyPoints: string[];
321
+ order: number;
322
+ onSave: (segment: Partial<PodcastSegment>) => void;
323
+ onRegenerate: (newContent?: string) => void;
324
+ }
325
+ ```
326
+
327
+ ## API Endpoints
328
+
329
+ ### 1. List Episodes
330
+ ```typescript
331
+ // GET /trpc/podcast.listEpisodes
332
+ {
333
+ input: {
334
+ workspaceId: string;
335
+ }
336
+ }
337
+ ```
338
+
339
+ ### 2. Get Episode
340
+ ```typescript
341
+ // GET /trpc/podcast.getEpisode
342
+ {
343
+ input: {
344
+ episodeId: string;
345
+ }
346
+ }
347
+ ```
348
+
349
+ ### 3. Generate Episode
350
+ ```typescript
351
+ // POST /trpc/podcast.generateEpisode
352
+ {
353
+ input: {
354
+ workspaceId: string;
355
+ podcastData: PodcastGenerationForm;
356
+ }
357
+ }
358
+ ```
359
+
360
+ ### 4. Get Episode Schema
361
+ ```typescript
362
+ // GET /trpc/podcast.getEpisodeSchema
363
+ {
364
+ input: {
365
+ episodeId: string;
366
+ }
367
+ }
368
+ ```
369
+
370
+ ### 5. Update Episode
371
+ ```typescript
372
+ // POST /trpc/podcast.updateEpisode
373
+ {
374
+ input: {
375
+ episodeId: string;
376
+ title?: string;
377
+ description?: string;
378
+ }
379
+ }
380
+ ```
381
+
382
+ ### 6. Delete Episode
383
+ ```typescript
384
+ // POST /trpc/podcast.deleteEpisode
385
+ {
386
+ input: {
387
+ episodeId: string;
388
+ }
389
+ }
390
+ ```
391
+
392
+ ### 7. Get Available Voices
393
+ ```typescript
394
+ // GET /trpc/podcast.getAvailableVoices
395
+ // No input required
396
+ ```
397
+
398
+ ### 8. Get Signed URLs
399
+ ```typescript
400
+ // GET /trpc/podcast.getSignedUrls
401
+ {
402
+ input: {
403
+ episodeId: string;
404
+ }
405
+ }
406
+ ```
407
+
408
+ ### 9. Get Full Episode URL
409
+ ```typescript
410
+ // GET /trpc/podcast.getFullEpisodeUrl
411
+ {
412
+ input: {
413
+ episodeId: string;
414
+ }
415
+ }
416
+ ```
417
+
418
+ ### 10. Regenerate Segment
419
+ ```typescript
420
+ // POST /trpc/podcast.regenerateSegment
421
+ {
422
+ input: {
423
+ episodeId: string;
424
+ segmentId: string;
425
+ newContent?: string; // Optional: new content for the segment
426
+ voice?: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer';
427
+ speed?: number; // 0.25 - 4.0
428
+ }
429
+ }
430
+ ```
431
+
432
+ ## User Experience Flow
433
+
434
+ ### 1. Podcast Generation
435
+ 1. User fills out podcast generation form with:
436
+ - Title for the podcast
437
+ - User prompt describing the topic, question, or request
438
+ - Optional description
439
+ - Voice and speed preferences
440
+ - Generation options (intro, outro, segmentation)
441
+ 2. Submit form → show loading state
442
+ 3. Listen for real-time progress updates:
443
+ - Generation start
444
+ - Structure complete (AI creates episode content from prompt)
445
+ - Audio generation start
446
+ - Segment progress (update progress bar)
447
+ - Audio generation complete
448
+ - Audio joining start (combining segments into full episode)
449
+ - Audio joining complete
450
+ - Summary complete
451
+ - Generation complete
452
+ 4. Redirect to podcast player or show success message
453
+
454
+ ### 2. Podcast Playback
455
+ 1. Load episode data
456
+ 2. Get fresh signed URLs for audio segments
457
+ 3. Initialize audio player with segments
458
+ 4. Handle segment navigation
459
+ 5. Update progress indicators
460
+
461
+ ### 3. Segment Management
462
+ 1. Display segments in list/grid view
463
+ 2. Allow editing segment content
464
+ 3. Provide regenerate option for individual segments
465
+ 4. Show regeneration progress:
466
+ - Segment regeneration start
467
+ - Segment regeneration complete
468
+ - Full episode regeneration start (to sync all segments)
469
+ - Full episode regeneration complete
470
+ 5. Update audio URLs after regeneration (both individual segments and full episode)
471
+
472
+ ### 4. Episode Management
473
+ 1. List all episodes with metadata
474
+ 2. Allow editing episode title/description
475
+ 3. Provide delete option with confirmation
476
+ 4. Show deletion progress
477
+
478
+ ## Error Handling
479
+
480
+ ### 1. Generation Errors
481
+ - Display error message with retry option
482
+ - Show which step failed
483
+ - Provide fallback options
484
+
485
+ ### 2. Audio Loading Errors
486
+ - Retry loading audio files
487
+ - Show placeholder for failed segments
488
+ - Provide manual refresh option
489
+
490
+ ### 3. Network Errors
491
+ - Implement retry logic
492
+ - Show offline indicators
493
+ - Cache episode data when possible
494
+
495
+ ## Loading States
496
+
497
+ ### 1. Generation Progress
498
+ ```typescript
499
+ interface GenerationProgress {
500
+ stage: 'structuring' | 'generating_audio' | 'creating_summary' | 'complete';
501
+ currentSegment?: number;
502
+ totalSegments?: number;
503
+ progress: number; // 0-100
504
+ }
505
+ ```
506
+
507
+ ### 2. Segment Regeneration
508
+ ```typescript
509
+ interface SegmentRegeneration {
510
+ segmentId: string;
511
+ status: 'pending' | 'generating' | 'complete' | 'error';
512
+ progress?: number;
513
+ }
514
+ ```
515
+
516
+ ## Accessibility Requirements
517
+
518
+ ### 1. Audio Controls
519
+ - Keyboard navigation for play/pause
520
+ - Screen reader support for progress
521
+ - Volume controls with labels
522
+
523
+ ### 2. Form Accessibility
524
+ - Proper form labels
525
+ - Error message associations
526
+ - Required field indicators
527
+
528
+ ### 3. Navigation
529
+ - Skip links for main content
530
+ - Focus management
531
+ - ARIA labels for interactive elements
532
+
533
+ ## Performance Considerations
534
+
535
+ ### 1. Audio Loading
536
+ - Lazy load audio segments
537
+ - Preload next segment
538
+ - Implement audio caching
539
+
540
+ ### 2. Real-time Updates
541
+ - Debounce frequent updates
542
+ - Batch UI updates
543
+ - Optimize re-renders
544
+
545
+ ### 3. Data Management
546
+ - Implement pagination for episode lists
547
+ - Cache episode metadata
548
+ - Optimize image/audio loading
549
+
550
+ ## Testing Requirements
551
+
552
+ ### 1. Unit Tests
553
+ - Component rendering
554
+ - Event handling
555
+ - Form validation
556
+
557
+ ### 2. Integration Tests
558
+ - API interactions
559
+ - Real-time event handling
560
+ - Audio playback
561
+
562
+ ### 3. E2E Tests
563
+ - Complete podcast generation flow
564
+ - Playback functionality
565
+ - Error scenarios
566
+
567
+ ## Browser Support
568
+
569
+ ### Required
570
+ - Chrome 90+
571
+ - Firefox 88+
572
+ - Safari 14+
573
+ - Edge 90+
574
+
575
+ ### Audio Support
576
+ - Web Audio API
577
+ - Media Source Extensions (for streaming)
578
+ - AudioContext support
579
+
580
+ ## Security Considerations
581
+
582
+ ### 1. Audio URLs
583
+ - Signed URLs expire after 24 hours
584
+ - Implement URL refresh logic
585
+ - Handle expired URL errors
586
+
587
+ ### 2. User Permissions
588
+ - Verify workspace ownership
589
+ - Validate episode access
590
+ - Sanitize user inputs
591
+
592
+ ### 3. Content Security
593
+ - Validate audio file types
594
+ - Implement file size limits
595
+ - Sanitize episode metadata
@@ -0,0 +1,18 @@
1
+ ## Study Guide Frontend Spec
2
+
3
+ ### Endpoints (tRPC)
4
+ - **studyguide.get**: `{ workspaceId: string }` → `{ artifactId: string; title: string; latestVersion: { id: string; content: string; data?: Record<string, unknown> | null; version: number; createdById?: string | null; createdAt: Date } | null }`
5
+ - **studyguide.edit**: `{ workspaceId: string; studyGuideId?: string; content: string; data?: Record<string, unknown>; title?: string }` → `{ artifactId: string; version: { id: string; version: number } }`
6
+
7
+ ### Behavior
8
+ - `get` will create a default study guide (Editor.js starter block) if none exists.
9
+ - `edit` creates a new artifact version and optionally renames the artifact.
10
+
11
+ ### Types (simplified)
12
+ - **StudyGuideArtifact**: `{ id: string; workspaceId: string; type: 'STUDY_GUIDE'; title: string }`
13
+ - **StudyGuideVersion**: `{ id: string; artifactId: string; content: string; data?: Record<string, unknown> | null; version: number; createdById?: string | null; createdAt: Date }`
14
+
15
+ ### UX Notes
16
+ - Store Editor.js document JSON in `content`.
17
+ - On save, call `studyguide.edit` (debounce to avoid excessive versions).
18
+ - Handle null `latestVersion` gracefully (should be rare due to auto-create).
@@ -0,0 +1,26 @@
1
+ ## Worksheets Frontend Spec
2
+
3
+ ### Endpoints (tRPC)
4
+ - **worksheets.list**: `{ workspaceId: string }` → `Worksheet[]` (latest version included, questions merged with user progress)
5
+ - **worksheets.create**: `{ workspaceId: string; title: string; description?: string; difficulty?: 'EASY'|'MEDIUM'|'HARD'; estimatedTime?: string; problems?: { question: string; answer: string; type?: QuestionType; options?: string[] }[] }` → `Worksheet & { questions: Question[] }`
6
+ - **worksheets.get**: `{ worksheetId: string }` → `Worksheet & { questions: Question[] }` (questions merged with progress)
7
+ - **worksheets.createWorksheetQuestion**: `{ worksheetId: string; prompt: string; answer?: string; type?: QuestionType; difficulty?: Difficulty; order?: number; meta?: Record<string, unknown> }` → `Question`
8
+ - **worksheets.updateWorksheetQuestion**: `{ worksheetQuestionId: string; prompt?: string; answer?: string; type?: QuestionType; difficulty?: Difficulty; order?: number; meta?: Record<string, unknown> }` → `Question`
9
+ - **worksheets.deleteWorksheetQuestion**: `{ worksheetQuestionId: string }` → `true`
10
+ - **worksheets.updateProblemStatus**: `{ problemId: string; completed: boolean; answer?: string }` → `WorksheetQuestionProgress`
11
+ - **worksheets.getProgress**: `{ worksheetId: string }` → `WorksheetQuestionProgress[]`
12
+ - **worksheets.update**: `{ id: string; title?: string; description?: string; difficulty?: Difficulty; estimatedTime?: string; problems?: { id?: string; question: string; answer: string; type?: QuestionType; options?: string[] }[] }` → `Worksheet & { questions: Question[] }`
13
+ - **worksheets.delete**: `{ id: string }` → `true`
14
+
15
+ ### Types (simplified)
16
+ - **Worksheet**: `{ id: string; workspaceId: string; type: 'WORKSHEET'; title: string; description?: string | null; difficulty?: 'EASY'|'MEDIUM'|'HARD' | null; estimatedTime?: string | null; createdAt: Date; updatedAt: Date }`
17
+ - **Question**: `{ id: string; artifactId: string; prompt: string; answer?: string | null; type: QuestionType; difficulty: Difficulty; order: number; meta?: { options?: string[]; completed?: boolean; userAnswer?: string | null; completedAt?: string | Date | null } }`
18
+ - **QuestionType**: `'MULTIPLE_CHOICE'|'TEXT'|'NUMERIC'|'TRUE_FALSE'|'MATCHING'|'FILL_IN_THE_BLANK'`
19
+ - **Difficulty**: `'EASY'|'MEDIUM'|'HARD'`
20
+ - **WorksheetQuestionProgress**: `{ id: string; worksheetQuestionId: string; userId: string; completed: boolean; userAnswer?: string | null; completedAt?: Date | null; attempts: number }`
21
+
22
+ ### UX Notes
23
+ - When listing or fetching a worksheet, question `meta` already contains per-user progress fields.
24
+ - For multiple-choice, render `meta.options` if present.
25
+ - Use optimistic UI for creating/updating/deleting questions.
26
+ - Respect ordering via `order`.
@@ -0,0 +1,47 @@
1
+ ## Workspace Frontend Spec
2
+
3
+ ### Endpoints (tRPC)
4
+ - **workspace.list**: `{ parentId?: string }` → `{ workspaces: Workspace[], folders: Folder[] }`
5
+ - **workspace.create**: `{ name: string; description?: string; parentId?: string }` → `Workspace`
6
+ - **workspace.createFolder**: `{ name: string; parentId?: string }` → `Folder`
7
+ - **workspace.get**: `{ id: string }` → `Workspace & { artifacts: Artifact[]; folder?: Folder; uploads: FileAsset[] }`
8
+ - **workspace.share**: `{ id: string }` → `{ shareLink: string } | void`
9
+ - **workspace.join**: `{ shareLink: string }` → `{ id; title; description; ownerId; createdAt; updatedAt }`
10
+ - **workspace.update**: `{ id: string; name?: string; description?: string }` → `Workspace`
11
+ - **workspace.delete**: `{ id: string }` → `true`
12
+ - **workspace.getFolderInformation**: `{ id: string }` → `{ folder: Folder; parents: Folder[] }`
13
+ - **workspace.uploadFiles**: `{ id: string; files: { filename: string; contentType: string; size: number }[] }` → `{ fileId: string; uploadUrl: string }[]`
14
+ - **workspace.deleteFiles**: `{ id: string; fileId: string[] }` → `true`
15
+ - **workspace.uploadAndAnalyzeMedia**: `{ workspaceId: string; file: { filename: string; contentType: string; size: number; content: string /* base64 */ }; generateStudyGuide?: boolean; generateFlashcards?: boolean; generateWorksheet?: boolean }` → `{ filename: string; artifacts: { studyGuide: Artifact | null; flashcards: Artifact | null; worksheet: Artifact | null } }`
16
+ - **workspace.search**: `{ query: string; limit?: number }` → `Workspace[]`
17
+
18
+ ### Types (simplified)
19
+ - **Workspace**: `{ id: string; title: string; description?: string | null; ownerId: string; folderId?: string | null; createdAt: Date; updatedAt: Date; shareLink?: string | null }`
20
+ - **Folder**: `{ id: string; name: string; ownerId: string; parentId?: string | null; createdAt: Date; updatedAt: Date }`
21
+ - **Artifact**: `{ id: string; workspaceId: string; type: string; title: string; createdById?: string | null; createdAt: Date; updatedAt: Date }`
22
+ - **FileAsset**: `{ id: string; userId: string; workspaceId: string; name: string; mimeType: string; size: number; bucket?: string | null; objectKey?: string | null }`
23
+
24
+ ### File Upload Flow
25
+ 1. Call `workspace.uploadFiles` to get write-signed URLs.
26
+ 2. PUT file bytes to each `uploadUrl` with correct `Content-Type`.
27
+ 3. Optionally refresh via `workspace.get` to see uploads.
28
+
29
+ ### Media Analysis Flow
30
+ 1. Call `workspace.uploadAndAnalyzeMedia` with base64 content and desired generation flags.
31
+ 2. Subscribe to Pusher `workspace_{workspaceId}` for progress events.
32
+ 3. On completion, fetch artifacts via `studyguide`, `worksheets`, and flashcards routers.
33
+
34
+ ### Real-Time Events (Pusher)
35
+ - **Channel**: `workspace_{workspaceId}`
36
+ - **Events** (examples):
37
+ - `{workspaceId}_file_analysis_start`, `{workspaceId}_file_analysis_complete`
38
+ - `{workspaceId}_study_guide_load_start`, `{workspaceId}_study_guide_info`
39
+ - `{workspaceId}_flash_card_load_start`, `{workspaceId}_flash_card_info`
40
+ - `{workspaceId}_worksheet_load_start`, `{workspaceId}_worksheet_info`
41
+ - `{workspaceId}_analysis_cleanup_start`, `{workspaceId}_analysis_cleanup_complete`
42
+ - Overall: `{workspaceId}_analysis_ended`
43
+
44
+ ### UX Notes
45
+ - Empty state: show create workspace/folder actions.
46
+ - Uploads: show progress per file; deletions from GCS are best-effort.
47
+ - Search: debounce input; pass `limit` (default 20).
package/dist/context.d.ts CHANGED
@@ -4,6 +4,6 @@ export declare function createContext({ req, res }: CreateExpressContextOptions)
4
4
  session: any;
5
5
  req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
6
6
  res: import("express").Response<any, Record<string, any>>;
7
- cookies: any;
7
+ cookies: Record<string, string | undefined>;
8
8
  }>;
9
9
  export type Context = Awaited<ReturnType<typeof createContext>>;