@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.
- package/AUTH_FRONTEND_SPEC.md +21 -0
- package/CHAT_FRONTEND_SPEC.md +474 -0
- package/MEETINGSUMMARY_FRONTEND_SPEC.md +28 -0
- package/PODCAST_FRONTEND_SPEC.md +595 -0
- package/STUDYGUIDE_FRONTEND_SPEC.md +18 -0
- package/WORKSHEETS_FRONTEND_SPEC.md +26 -0
- package/WORKSPACE_FRONTEND_SPEC.md +47 -0
- package/dist/context.d.ts +1 -1
- package/dist/lib/ai-session.d.ts +26 -0
- package/dist/lib/ai-session.js +343 -0
- package/dist/lib/auth.js +10 -6
- package/dist/lib/inference.d.ts +2 -0
- package/dist/lib/inference.js +21 -0
- package/dist/lib/pusher.d.ts +14 -0
- package/dist/lib/pusher.js +94 -0
- package/dist/lib/storage.d.ts +10 -2
- package/dist/lib/storage.js +63 -6
- package/dist/routers/_app.d.ts +878 -100
- package/dist/routers/_app.js +8 -2
- package/dist/routers/ai-session.d.ts +0 -0
- package/dist/routers/ai-session.js +1 -0
- package/dist/routers/auth.d.ts +13 -11
- package/dist/routers/auth.js +50 -21
- package/dist/routers/chat.d.ts +171 -0
- package/dist/routers/chat.js +270 -0
- package/dist/routers/flashcards.d.ts +51 -39
- package/dist/routers/flashcards.js +143 -31
- package/dist/routers/meetingsummary.d.ts +0 -0
- package/dist/routers/meetingsummary.js +377 -0
- package/dist/routers/podcast.d.ts +277 -0
- package/dist/routers/podcast.js +847 -0
- package/dist/routers/studyguide.d.ts +54 -0
- package/dist/routers/studyguide.js +125 -0
- package/dist/routers/worksheets.d.ts +147 -40
- package/dist/routers/worksheets.js +348 -33
- package/dist/routers/workspace.d.ts +163 -8
- package/dist/routers/workspace.js +453 -8
- package/dist/server.d.ts +1 -1
- package/dist/server.js +7 -2
- package/dist/trpc.d.ts +5 -5
- package/package.json +11 -3
- package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +213 -0
- package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +31 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +87 -6
- package/prisma/seed.mjs +135 -0
- package/src/lib/ai-session.ts +411 -0
- package/src/lib/auth.ts +1 -1
- package/src/lib/inference.ts +21 -0
- package/src/lib/pusher.ts +104 -0
- package/src/lib/storage.ts +89 -6
- package/src/routers/_app.ts +6 -0
- package/src/routers/auth.ts +8 -4
- package/src/routers/chat.ts +275 -0
- package/src/routers/flashcards.ts +151 -33
- package/src/routers/meetingsummary.ts +416 -0
- package/src/routers/podcast.ts +934 -0
- package/src/routers/studyguide.ts +144 -0
- package/src/routers/worksheets.ts +346 -18
- package/src/routers/workspace.ts +500 -8
- package/src/server.ts +7 -2
- package/test-ai-integration.js +134 -0
- package/dist/context.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/lib/auth.d.ts.map +0 -1
- package/dist/lib/file.d.ts.map +0 -1
- package/dist/lib/prisma.d.ts.map +0 -1
- package/dist/lib/storage.d.ts.map +0 -1
- package/dist/routers/_app.d.ts.map +0 -1
- package/dist/routers/auth.d.ts.map +0 -1
- package/dist/routers/sample.js +0 -21
- package/dist/routers/workspace.d.ts.map +0 -1
- package/dist/server.d.ts.map +0 -1
- 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()
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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:
|
|
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()
|
|
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()
|
|
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
|
|