@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,275 @@
1
+ import { TRPCError } from "@trpc/server";
2
+ import { authedProcedure, router } from "../trpc.js";
3
+ import z from "zod";
4
+ import PusherService from "../lib/pusher.js";
5
+
6
+ export const chat = router({
7
+ getChannels: authedProcedure
8
+ .input(z.object({ workspaceId: z.string() }))
9
+ .query(async ({ input, ctx }) => {
10
+ const channels = await ctx.db.channel.findMany({
11
+ where: { workspaceId: input.workspaceId },
12
+ include: { chats: {
13
+ include: {
14
+ user: {
15
+ select: {
16
+ id: true,
17
+ name: true,
18
+ image: true,
19
+ }
20
+ }
21
+ }
22
+ } },
23
+ });
24
+ if (!channels) {
25
+ const defaultChannel = await ctx.db.channel.create({
26
+ data: { workspaceId: input.workspaceId, name: "General" },
27
+ });
28
+ return [defaultChannel];
29
+ }
30
+ return channels;
31
+ }),
32
+ getChannel: authedProcedure
33
+ .input(z.object({ workspaceId: z.string().optional(), channelId: z.string().optional() }))
34
+ .query(async ({ input, ctx }) => {
35
+ if (!input.channelId && input.workspaceId) {
36
+ const defaultChannel = await ctx.db.channel.create({
37
+ data: { workspaceId: input.workspaceId, name: "General" },
38
+ include: { chats: {
39
+ include: {
40
+ user: {
41
+ select: {
42
+ id: true,
43
+ name: true,
44
+ image: true,
45
+ }
46
+ }
47
+ }
48
+ } },
49
+ });
50
+
51
+ await PusherService.emitTaskComplete(input.workspaceId, "new_channel", {
52
+ channelId: defaultChannel.id,
53
+ workspaceId: input.workspaceId,
54
+ name: "General",
55
+ createdAt: defaultChannel.createdAt,
56
+ });
57
+
58
+ return defaultChannel;
59
+ }
60
+ const channel = await ctx.db.channel.findUnique({
61
+ where: { id: input.channelId },
62
+ include: { chats: {
63
+ include: {
64
+ user: {
65
+ select: {
66
+ id: true,
67
+ name: true,
68
+ image: true,
69
+ }
70
+ }
71
+ }
72
+ } },
73
+ });
74
+
75
+ if (!channel) {
76
+ throw new TRPCError({ code: "NOT_FOUND", message: "Channel not found" });
77
+ }
78
+
79
+ return channel;
80
+ }),
81
+ removeChannel: authedProcedure
82
+ .input(z.object({ workspaceId: z.string(), channelId: z.string() }))
83
+ .mutation(async ({ input, ctx }) => {
84
+ await ctx.db.channel.delete({ where: { id: input.channelId } });
85
+ await PusherService.emitTaskComplete(input.workspaceId, "remove_channel", {
86
+ channelId: input.channelId,
87
+ deletedAt: new Date().toISOString(),
88
+ });
89
+ return { success: true };
90
+ }),
91
+ editChannel: authedProcedure
92
+ .input(z.object({ workspaceId: z.string(), channelId: z.string(), name: z.string() }))
93
+ .mutation(async ({ input, ctx }) => {
94
+ const channel = await ctx.db.channel.update({
95
+ where: { id: input.channelId },
96
+ data: { name: input.name },
97
+ include: {
98
+ chats: {
99
+ include: {
100
+ user: {
101
+ select: {
102
+ id: true,
103
+ name: true,
104
+ image: true,
105
+ }
106
+ },
107
+ }
108
+ }
109
+ }
110
+ });
111
+ await PusherService.emitTaskComplete(input.workspaceId, "edit_channel", {
112
+ channelId: input.channelId,
113
+ workspaceId: input.workspaceId,
114
+ name: input.name,
115
+ });
116
+ return channel;
117
+ }),
118
+ createChannel: authedProcedure
119
+ .input(z.object({ workspaceId: z.string(), name: z.string() }))
120
+ .mutation(async ({ input, ctx }) => {
121
+ const channel = await ctx.db.channel.create({
122
+ data: { workspaceId: input.workspaceId, name: input.name },
123
+ include: {
124
+ chats: {
125
+ include: {
126
+ user: {
127
+ select: {
128
+ id: true,
129
+ name: true,
130
+ image: true,
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
136
+ });
137
+ // Notify via Pusher
138
+ await PusherService.emitTaskComplete(input.workspaceId, "new_channel", {
139
+ channelId: channel.id,
140
+ workspaceId: input.workspaceId,
141
+ name: input.name,
142
+ createdAt: channel.createdAt,
143
+ });
144
+ return channel;
145
+ }),
146
+ postMessage: authedProcedure
147
+ .input(z.object({ channelId: z.string(), message: z.string() }))
148
+ .mutation(async ({ input, ctx }) => {
149
+ const channel = await ctx.db.channel.findUnique({
150
+ where: { id: input.channelId },
151
+ include: {
152
+ chats: {
153
+ include: {
154
+ user: {
155
+ select: {
156
+ id: true,
157
+ name: true,
158
+ image: true,
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+ });
165
+ if (!channel) {
166
+ throw new TRPCError({ code: "NOT_FOUND", message: "Channel not found" });
167
+ }
168
+ const chat = await ctx.db.chat.create({
169
+ data: {
170
+ channelId: input.channelId,
171
+ userId: ctx.session.user.id,
172
+ message: input.message,
173
+ },
174
+ include: {
175
+ user: {
176
+ select: {
177
+ id: true,
178
+ name: true,
179
+ image: true,
180
+ }
181
+ }
182
+ }
183
+ });
184
+ // Notify via Pusher
185
+ await PusherService.emitChannelEvent(input.channelId, "new_message", chat);
186
+ return chat;
187
+ }),
188
+ editMessage: authedProcedure
189
+ .input(z.object({ chatId: z.string(), message: z.string() }))
190
+ .mutation(async ({ input, ctx }) => {
191
+ const chat = await ctx.db.chat.findUnique({
192
+ where: { id: input.chatId },
193
+ include: {
194
+ user: {
195
+ select: {
196
+ id: true,
197
+ name: true,
198
+ image: true,
199
+ }
200
+ }
201
+ }
202
+ });
203
+ if (!chat) {
204
+ throw new TRPCError({ code: "NOT_FOUND", message: "Chat message not found" });
205
+ }
206
+ if (chat.userId !== ctx.session.user.id) {
207
+ throw new TRPCError({ code: "FORBIDDEN", message: "Not your message to edit" });
208
+ }
209
+ const updatedChat = await ctx.db.chat.update({
210
+ where: { id: input.chatId },
211
+ data: { message: input.message },
212
+ include: {
213
+ user: {
214
+ select: {
215
+ id: true,
216
+ name: true,
217
+ image: true,
218
+ }
219
+ }
220
+ }
221
+ });
222
+ // Notify via Pusher
223
+ await PusherService.emitChannelEvent(chat.channelId, "edit_message", {
224
+ chatId: updatedChat.id,
225
+ channelId: updatedChat.channelId,
226
+ userId: updatedChat.userId,
227
+ message: input.message,
228
+ updatedAt: updatedChat.updatedAt,
229
+ user: {
230
+ id: ctx.session.user.id,
231
+ name: updatedChat.user?.name,
232
+ image: updatedChat.user?.image,
233
+ },
234
+ });
235
+ return updatedChat;
236
+ }),
237
+ deleteMessage: authedProcedure
238
+ .input(z.object({ chatId: z.string() }))
239
+ .mutation(async ({ input, ctx }) => {
240
+ const chat = await ctx.db.chat.findUnique({
241
+ where: { id: input.chatId },
242
+ include: {
243
+ user: {
244
+ select: {
245
+ id: true,
246
+ name: true,
247
+ image: true,
248
+ }
249
+ },
250
+ }
251
+ });
252
+ if (!chat) {
253
+ throw new TRPCError({ code: "NOT_FOUND", message: "Chat message not found" });
254
+ }
255
+ if (chat.userId !== ctx.session.user.id) {
256
+ throw new TRPCError({ code: "FORBIDDEN", message: "Not your message to delete" });
257
+ }
258
+ await ctx.db.chat.delete({
259
+ where: { id: input.chatId },
260
+ });
261
+ // Notify via Pusher
262
+ await PusherService.emitChannelEvent(chat.channelId, "delete_message", {
263
+ chatId: chat.id,
264
+ channelId: chat.channelId,
265
+ userId: chat.userId,
266
+ deletedAt: new Date().toISOString(),
267
+ user: {
268
+ id: ctx.session.user.id,
269
+ name: chat.user?.name,
270
+ image: chat.user?.image,
271
+ },
272
+ });
273
+ return { success: true };
274
+ }),
275
+ });
@@ -1,6 +1,9 @@
1
1
  import { z } from 'zod';
2
2
  import { TRPCError } from '@trpc/server';
3
3
  import { router, authedProcedure } from '../trpc.js';
4
+ import createInferenceService from '../lib/inference.js';
5
+ import { aiSessionService } from '../lib/ai-session.js';
6
+ import PusherService from '../lib/pusher.js';
4
7
  // Prisma enum values mapped manually to avoid type import issues in ESM
5
8
  const ArtifactType = {
6
9
  STUDY_GUIDE: 'STUDY_GUIDE',
@@ -12,7 +15,7 @@ const ArtifactType = {
12
15
 
13
16
  export const flashcards = router({
14
17
  listSets: authedProcedure
15
- .input(z.object({ workspaceId: z.string().uuid() }))
18
+ .input(z.object({ workspaceId: z.string() }))
16
19
  .query(async ({ ctx, input }) => {
17
20
  const workspace = await ctx.db.workspace.findFirst({
18
21
  where: { id: input.workspaceId, ownerId: ctx.session.user.id },
@@ -29,42 +32,22 @@ export const flashcards = router({
29
32
  orderBy: { updatedAt: 'desc' },
30
33
  });
31
34
  }),
32
-
33
- createSet: authedProcedure
34
- .input(z.object({ workspaceId: z.string().uuid(), title: z.string().min(1).max(120) }))
35
- .mutation(async ({ ctx, input }) => {
36
- const workspace = await ctx.db.workspace.findFirst({
37
- where: { id: input.workspaceId, ownerId: ctx.session.user.id },
38
- });
39
- if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
40
- return ctx.db.artifact.create({
41
- data: {
42
- workspaceId: input.workspaceId,
43
- type: ArtifactType.FLASHCARD_SET,
44
- title: input.title,
45
- createdById: ctx.session.user.id,
46
- },
47
- });
48
- }),
49
-
50
- getSet: authedProcedure
51
- .input(z.object({ setId: z.string().uuid() }))
35
+ listCards: authedProcedure
36
+ .input(z.object({ workspaceId: z.string() }))
52
37
  .query(async ({ ctx, input }) => {
53
38
  const set = await ctx.db.artifact.findFirst({
54
- where: {
55
- id: input.setId,
56
- type: ArtifactType.FLASHCARD_SET,
57
- workspace: { ownerId: ctx.session.user.id },
39
+ where: { workspaceId: input.workspaceId, type: ArtifactType.FLASHCARD_SET, workspace: { ownerId: ctx.session.user.id } },
40
+ include: {
41
+ flashcards: true,
58
42
  },
59
- include: { flashcards: true },
43
+ orderBy: { updatedAt: 'desc' },
60
44
  });
61
45
  if (!set) throw new TRPCError({ code: 'NOT_FOUND' });
62
- return set;
46
+ return set.flashcards;
63
47
  }),
64
-
65
48
  createCard: authedProcedure
66
49
  .input(z.object({
67
- setId: z.string().uuid(),
50
+ workspaceId: z.string(),
68
51
  front: z.string().min(1),
69
52
  back: z.string().min(1),
70
53
  tags: z.array(z.string()).optional(),
@@ -72,12 +55,18 @@ export const flashcards = router({
72
55
  }))
73
56
  .mutation(async ({ ctx, input }) => {
74
57
  const set = await ctx.db.artifact.findFirst({
75
- where: { id: input.setId, type: ArtifactType.FLASHCARD_SET, workspace: { ownerId: ctx.session.user.id } },
58
+ where: { type: ArtifactType.FLASHCARD_SET, workspace: {
59
+ id: input.workspaceId,
60
+ } },
61
+ include: {
62
+ flashcards: true,
63
+ },
64
+ orderBy: { updatedAt: 'desc' },
76
65
  });
77
66
  if (!set) throw new TRPCError({ code: 'NOT_FOUND' });
78
67
  return ctx.db.flashcard.create({
79
68
  data: {
80
- artifactId: input.setId,
69
+ artifactId: set.id,
81
70
  front: input.front,
82
71
  back: input.back,
83
72
  tags: input.tags ?? [],
@@ -88,7 +77,7 @@ export const flashcards = router({
88
77
 
89
78
  updateCard: authedProcedure
90
79
  .input(z.object({
91
- cardId: z.string().uuid(),
80
+ cardId: z.string(),
92
81
  front: z.string().optional(),
93
82
  back: z.string().optional(),
94
83
  tags: z.array(z.string()).optional(),
@@ -111,7 +100,7 @@ export const flashcards = router({
111
100
  }),
112
101
 
113
102
  deleteCard: authedProcedure
114
- .input(z.object({ cardId: z.string().uuid() }))
103
+ .input(z.object({ cardId: z.string() }))
115
104
  .mutation(async ({ ctx, input }) => {
116
105
  const card = await ctx.db.flashcard.findFirst({
117
106
  where: { id: input.cardId, artifact: { workspace: { ownerId: ctx.session.user.id } } },
@@ -130,6 +119,135 @@ export const flashcards = router({
130
119
  if (deleted.count === 0) throw new TRPCError({ code: 'NOT_FOUND' });
131
120
  return true;
132
121
  }),
122
+
123
+ // Generate a flashcard set from a user prompt
124
+ generateFromPrompt: authedProcedure
125
+ .input(z.object({
126
+ workspaceId: z.string(),
127
+ prompt: z.string().min(1),
128
+ numCards: z.number().int().min(1).max(50).default(10),
129
+ difficulty: z.enum(['easy', 'medium', 'hard']).default('medium'),
130
+ title: z.string().optional(),
131
+ tags: z.array(z.string()).optional(),
132
+ }))
133
+ .mutation(async ({ ctx, input }) => {
134
+ // Verify workspace ownership
135
+ const workspace = await ctx.db.workspace.findFirst({
136
+ where: { id: input.workspaceId, ownerId: ctx.session.user.id },
137
+ });
138
+ if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
139
+
140
+ // Pusher start
141
+ await PusherService.emitTaskComplete(input.workspaceId, 'flash_card_load_start', { source: 'prompt' });
142
+ const flashcardCurrent = await ctx.db.artifact.findFirst({
143
+ where: {
144
+ workspaceId: input.workspaceId,
145
+ type: ArtifactType.FLASHCARD_SET,
146
+ },
147
+ select: {
148
+ flashcards: true,
149
+ },
150
+ orderBy: {
151
+ updatedAt: 'desc',
152
+ },
153
+ });
154
+
155
+
156
+ const formattedPreviousCards = flashcardCurrent?.flashcards.map((card) => ({
157
+ front: card.front,
158
+ back: card.back,
159
+ }));
160
+
161
+
162
+ const partialPrompt = `
163
+ This is the users previous flashcards, avoid repeating any existing cards.
164
+ Please generate ${input.numCards} new cards,
165
+ Of a ${input.difficulty} difficulty,
166
+ Of a ${input.tags?.join(', ')} tag,
167
+ Of a ${input.title} title.
168
+ ${formattedPreviousCards?.map((card) => `Front: ${card.front}\nBack: ${card.back}`).join('\n')}
169
+
170
+ The user has also left you this prompt: ${input.prompt}
171
+ `
172
+ // Init AI session and seed with prompt as instruction
173
+ const session = await aiSessionService.initSession(input.workspaceId);
174
+ await aiSessionService.setInstruction(session.id, partialPrompt);
175
+
176
+ await aiSessionService.startLLMSession(session.id);
177
+
178
+ const currentCards = flashcardCurrent?.flashcards.length || 0;
179
+ const newCards = input.numCards - currentCards;
180
+
181
+ // Generate
182
+ await PusherService.emitTaskComplete(input.workspaceId, 'flash_card_info', { status: 'generating', numCards: input.numCards, difficulty: input.difficulty });
183
+ const content = await aiSessionService.generateFlashcardQuestions(session.id, input.numCards, input.difficulty);
184
+
185
+ // Previous cards
186
+
187
+ // Create artifact
188
+ const artifact = await ctx.db.artifact.create({
189
+ data: {
190
+ workspaceId: input.workspaceId,
191
+ type: ArtifactType.FLASHCARD_SET,
192
+ title: input.title || `Flashcards - ${new Date().toLocaleString()}`,
193
+ createdById: ctx.session.user.id,
194
+ flashcards: {
195
+ create: flashcardCurrent?.flashcards.map((card) => ({
196
+ front: card.front,
197
+ back: card.back,
198
+ })),
199
+ },
200
+ },
201
+ });
202
+
203
+ // Parse and create cards
204
+ let createdCards = 0;
205
+ try {
206
+ const flashcardData = JSON.parse(content);
207
+ for (let i = 0; i < Math.min(flashcardData.length, input.numCards); i++) {
208
+ const card = flashcardData[i];
209
+ const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
210
+ const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
211
+ await ctx.db.flashcard.create({
212
+ data: {
213
+ artifactId: artifact.id,
214
+ front,
215
+ back,
216
+ order: i,
217
+ tags: input.tags ?? ['ai-generated', input.difficulty],
218
+ },
219
+ });
220
+ createdCards++;
221
+ }
222
+ } catch {
223
+ // Fallback to text parsing if JSON fails
224
+ const lines = content.split('\n').filter(line => line.trim());
225
+ for (let i = 0; i < Math.min(lines.length, input.numCards); i++) {
226
+ const line = lines[i];
227
+ if (line.includes(' - ')) {
228
+ const [front, back] = line.split(' - ');
229
+ await ctx.db.flashcard.create({
230
+ data: {
231
+ artifactId: artifact.id,
232
+ front: front.trim(),
233
+ back: back.trim(),
234
+ order: i,
235
+ tags: input.tags ?? ['ai-generated', input.difficulty],
236
+ },
237
+ });
238
+ createdCards++;
239
+ }
240
+ }
241
+ }
242
+
243
+ // Pusher complete
244
+ await PusherService.emitFlashcardComplete(input.workspaceId, artifact);
245
+
246
+ // Cleanup AI session (best-effort)
247
+ aiSessionService.deleteSession(session.id);
248
+
249
+ return { artifact, createdCards };
250
+ }),
133
251
  });
134
252
 
135
253