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