@goscribe/server 1.0.8 → 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 (57) 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/lib/ai-session.d.ts +26 -0
  9. package/dist/lib/ai-session.js +343 -0
  10. package/dist/lib/inference.d.ts +2 -0
  11. package/dist/lib/inference.js +21 -0
  12. package/dist/lib/pusher.d.ts +14 -0
  13. package/dist/lib/pusher.js +94 -0
  14. package/dist/lib/storage.d.ts +10 -2
  15. package/dist/lib/storage.js +63 -6
  16. package/dist/routers/_app.d.ts +840 -58
  17. package/dist/routers/_app.js +6 -0
  18. package/dist/routers/ai-session.d.ts +0 -0
  19. package/dist/routers/ai-session.js +1 -0
  20. package/dist/routers/auth.d.ts +1 -0
  21. package/dist/routers/auth.js +6 -4
  22. package/dist/routers/chat.d.ts +171 -0
  23. package/dist/routers/chat.js +270 -0
  24. package/dist/routers/flashcards.d.ts +37 -0
  25. package/dist/routers/flashcards.js +128 -0
  26. package/dist/routers/meetingsummary.d.ts +0 -0
  27. package/dist/routers/meetingsummary.js +377 -0
  28. package/dist/routers/podcast.d.ts +277 -0
  29. package/dist/routers/podcast.js +847 -0
  30. package/dist/routers/studyguide.d.ts +54 -0
  31. package/dist/routers/studyguide.js +125 -0
  32. package/dist/routers/worksheets.d.ts +138 -51
  33. package/dist/routers/worksheets.js +317 -7
  34. package/dist/routers/workspace.d.ts +162 -7
  35. package/dist/routers/workspace.js +440 -8
  36. package/dist/server.js +6 -2
  37. package/package.json +11 -4
  38. package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +213 -0
  39. package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +31 -0
  40. package/prisma/migrations/migration_lock.toml +3 -0
  41. package/prisma/schema.prisma +87 -6
  42. package/prisma/seed.mjs +135 -0
  43. package/src/lib/ai-session.ts +411 -0
  44. package/src/lib/inference.ts +21 -0
  45. package/src/lib/pusher.ts +104 -0
  46. package/src/lib/storage.ts +89 -6
  47. package/src/routers/_app.ts +6 -0
  48. package/src/routers/auth.ts +8 -4
  49. package/src/routers/chat.ts +275 -0
  50. package/src/routers/flashcards.ts +142 -0
  51. package/src/routers/meetingsummary.ts +416 -0
  52. package/src/routers/podcast.ts +934 -0
  53. package/src/routers/studyguide.ts +144 -0
  54. package/src/routers/worksheets.ts +336 -7
  55. package/src/routers/workspace.ts +487 -8
  56. package/src/server.ts +7 -2
  57. package/test-ai-integration.js +134 -0
@@ -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',
@@ -106,6 +109,145 @@ export const flashcards = router({
106
109
  await ctx.db.flashcard.delete({ where: { id: input.cardId } });
107
110
  return true;
108
111
  }),
112
+
113
+ deleteSet: authedProcedure
114
+ .input(z.object({ setId: z.string().uuid() }))
115
+ .mutation(async ({ ctx, input }) => {
116
+ const deleted = await ctx.db.artifact.deleteMany({
117
+ where: { id: input.setId, type: ArtifactType.FLASHCARD_SET, workspace: { ownerId: ctx.session.user.id } },
118
+ });
119
+ if (deleted.count === 0) throw new TRPCError({ code: 'NOT_FOUND' });
120
+ return true;
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
+ }),
109
251
  });
110
252
 
111
253