@goscribe/server 1.1.2 → 1.1.3
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/dist/lib/ai-session.d.ts +13 -3
- package/dist/lib/ai-session.js +66 -146
- package/dist/lib/pusher.js +1 -1
- package/dist/routers/_app.d.ts +114 -7
- package/dist/routers/chat.js +2 -23
- package/dist/routers/flashcards.d.ts +25 -1
- package/dist/routers/flashcards.js +0 -14
- package/dist/routers/members.d.ts +18 -0
- package/dist/routers/members.js +14 -1
- package/dist/routers/worksheets.js +5 -4
- package/dist/routers/workspace.d.ts +89 -6
- package/dist/routers/workspace.js +389 -259
- package/dist/services/flashcard-progress.service.d.ts +25 -1
- package/dist/services/flashcard-progress.service.js +70 -31
- package/package.json +2 -2
- package/prisma/schema.prisma +12 -2
- package/src/lib/ai-session.ts +3 -21
- package/src/routers/flashcards.ts +0 -16
- package/src/routers/members.ts +13 -2
- package/src/routers/workspace.ts +468 -439
- package/ANALYSIS_PROGRESS_SPEC.md +0 -463
- package/PROGRESS_QUICK_REFERENCE.md +0 -239
- package/dist/lib/podcast-prompts.d.ts +0 -43
- package/dist/lib/podcast-prompts.js +0 -135
- package/dist/routers/ai-session.d.ts +0 -0
- package/dist/routers/ai-session.js +0 -1
- package/dist/services/flashcard.service.d.ts +0 -183
- package/dist/services/flashcard.service.js +0 -224
- package/dist/services/podcast-segment-reorder.d.ts +0 -0
- package/dist/services/podcast-segment-reorder.js +0 -107
- package/dist/services/podcast.service.d.ts +0 -0
- package/dist/services/podcast.service.js +0 -326
- package/dist/services/worksheet.service.d.ts +0 -0
- package/dist/services/worksheet.service.js +0 -295
|
@@ -94,7 +94,7 @@ export declare class FlashcardProgressService {
|
|
|
94
94
|
/**
|
|
95
95
|
* Get flashcards due for review, non-studied flashcards, and flashcards with low mastery
|
|
96
96
|
*/
|
|
97
|
-
getDueFlashcards(userId: string, workspaceId: string): Promise<({
|
|
97
|
+
getDueFlashcards(userId: string, workspaceId: string): Promise<(({
|
|
98
98
|
artifact: {
|
|
99
99
|
id: string;
|
|
100
100
|
createdAt: Date;
|
|
@@ -119,6 +119,30 @@ export declare class FlashcardProgressService {
|
|
|
119
119
|
front: string;
|
|
120
120
|
back: string;
|
|
121
121
|
tags: string[];
|
|
122
|
+
}) | {
|
|
123
|
+
artifact: {
|
|
124
|
+
id: string;
|
|
125
|
+
createdAt: Date;
|
|
126
|
+
updatedAt: Date;
|
|
127
|
+
title: string;
|
|
128
|
+
description: string | null;
|
|
129
|
+
workspaceId: string;
|
|
130
|
+
type: import("@prisma/client").$Enums.ArtifactType;
|
|
131
|
+
isArchived: boolean;
|
|
132
|
+
generating: boolean;
|
|
133
|
+
generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
|
|
134
|
+
difficulty: import("@prisma/client").$Enums.Difficulty | null;
|
|
135
|
+
estimatedTime: string | null;
|
|
136
|
+
imageObjectKey: string | null;
|
|
137
|
+
createdById: string | null;
|
|
138
|
+
};
|
|
139
|
+
id: string;
|
|
140
|
+
createdAt: Date;
|
|
141
|
+
artifactId: string;
|
|
142
|
+
order: number;
|
|
143
|
+
front: string;
|
|
144
|
+
back: string;
|
|
145
|
+
tags: string[];
|
|
122
146
|
})[]>;
|
|
123
147
|
/**
|
|
124
148
|
* Get user statistics for a flashcard set
|
|
@@ -226,12 +226,23 @@ export class FlashcardProgressService {
|
|
|
226
226
|
async getDueFlashcards(userId, workspaceId) {
|
|
227
227
|
const now = new Date();
|
|
228
228
|
const LOW_MASTERY_THRESHOLD = 50; // Consider mastery < 50 as low
|
|
229
|
-
// Get
|
|
229
|
+
// Get the latest artifact in the workspace
|
|
230
|
+
const latestArtifact = await this.db.artifact.findFirst({
|
|
231
|
+
where: {
|
|
232
|
+
workspaceId,
|
|
233
|
+
type: 'FLASHCARD_SET',
|
|
234
|
+
},
|
|
235
|
+
orderBy: {
|
|
236
|
+
updatedAt: 'desc',
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
if (!latestArtifact) {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
// Get all flashcards from the latest artifact
|
|
230
243
|
const allFlashcards = await this.db.flashcard.findMany({
|
|
231
244
|
where: {
|
|
232
|
-
|
|
233
|
-
workspaceId,
|
|
234
|
-
},
|
|
245
|
+
artifactId: latestArtifact.id,
|
|
235
246
|
},
|
|
236
247
|
include: {
|
|
237
248
|
artifact: true,
|
|
@@ -242,6 +253,7 @@ export class FlashcardProgressService {
|
|
|
242
253
|
},
|
|
243
254
|
},
|
|
244
255
|
});
|
|
256
|
+
console.log('allFlashcards', allFlashcards.length);
|
|
245
257
|
const TAKE_NUMBER = (allFlashcards.length > 10) ? 10 : allFlashcards.length;
|
|
246
258
|
// Get progress records for flashcards that are due or have low mastery
|
|
247
259
|
const progressRecords = await this.db.flashcardProgress.findMany({
|
|
@@ -265,9 +277,7 @@ export class FlashcardProgressService {
|
|
|
265
277
|
}
|
|
266
278
|
],
|
|
267
279
|
flashcard: {
|
|
268
|
-
|
|
269
|
-
workspaceId,
|
|
270
|
-
},
|
|
280
|
+
artifactId: latestArtifact.id,
|
|
271
281
|
},
|
|
272
282
|
},
|
|
273
283
|
include: {
|
|
@@ -279,6 +289,38 @@ export class FlashcardProgressService {
|
|
|
279
289
|
},
|
|
280
290
|
take: TAKE_NUMBER,
|
|
281
291
|
});
|
|
292
|
+
console.log('TAKE_NUMBER', TAKE_NUMBER);
|
|
293
|
+
console.log('TAKE_NUMBER - progressRecords.length', TAKE_NUMBER - progressRecords.length);
|
|
294
|
+
console.log('progressRecords', progressRecords.map((progress) => progress.flashcard.id));
|
|
295
|
+
// Get flashcard IDs that already have progress records
|
|
296
|
+
const flashcardIdsWithProgress = new Set(progressRecords.map((progress) => progress.flashcard.id));
|
|
297
|
+
// Find flashcards without progress records (non-studied) to pad the results
|
|
298
|
+
const nonStudiedFlashcards = allFlashcards
|
|
299
|
+
.filter((flashcard) => !flashcardIdsWithProgress.has(flashcard.id))
|
|
300
|
+
.slice(0, TAKE_NUMBER - progressRecords.length);
|
|
301
|
+
// Create progress-like structures for non-studied flashcards
|
|
302
|
+
const progressRecordsPadding = nonStudiedFlashcards.map((flashcard) => {
|
|
303
|
+
const { progress, ...flashcardWithoutProgress } = flashcard;
|
|
304
|
+
return {
|
|
305
|
+
id: `temp-${flashcard.id}`,
|
|
306
|
+
userId,
|
|
307
|
+
flashcardId: flashcard.id,
|
|
308
|
+
timesStudied: 0,
|
|
309
|
+
timesCorrect: 0,
|
|
310
|
+
timesIncorrect: 0,
|
|
311
|
+
timesIncorrectConsecutive: 0,
|
|
312
|
+
easeFactor: 2.5,
|
|
313
|
+
interval: 0,
|
|
314
|
+
repetitions: 0,
|
|
315
|
+
masteryLevel: 0,
|
|
316
|
+
lastStudiedAt: null,
|
|
317
|
+
nextReviewAt: null,
|
|
318
|
+
flashcard: flashcardWithoutProgress,
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
console.log('progressRecordsPadding', progressRecordsPadding.length);
|
|
322
|
+
console.log('progressRecords', progressRecords.length);
|
|
323
|
+
const selectedCards = [...progressRecords, ...progressRecordsPadding];
|
|
282
324
|
// Build result array: include progress records and non-studied flashcards
|
|
283
325
|
const results = [];
|
|
284
326
|
// Add flashcards with progress (due or low mastery)
|
|
@@ -286,30 +328,27 @@ export class FlashcardProgressService {
|
|
|
286
328
|
results.push(progress);
|
|
287
329
|
}
|
|
288
330
|
// Sort by priority: due first (by nextReviewAt), then low mastery, then non-studied
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
return 0;
|
|
311
|
-
});
|
|
312
|
-
return results.map((progress) => progress.flashcard);
|
|
331
|
+
// @todo: make an actual algorithm. research
|
|
332
|
+
// results.sort((a, b) => {
|
|
333
|
+
// // Due flashcards first (nextReviewAt <= now)
|
|
334
|
+
// const aIsDue = a.nextReviewAt && a.nextReviewAt <= now;
|
|
335
|
+
// const bIsDue = b.nextReviewAt && b.nextReviewAt <= now;
|
|
336
|
+
// // if (aIsDue && !bIsDue) return -1;
|
|
337
|
+
// // if (!aIsDue && bIsDue) return 1;
|
|
338
|
+
// // Among due flashcards, sort by nextReviewAt
|
|
339
|
+
// if (aIsDue && bIsDue && a.nextReviewAt && b.nextReviewAt) {
|
|
340
|
+
// return a.nextReviewAt.getTime() - b.nextReviewAt.getTime();
|
|
341
|
+
// }
|
|
342
|
+
// // Then low mastery (lower mastery first)
|
|
343
|
+
// if (a.masteryLevel !== b.masteryLevel) {
|
|
344
|
+
// return a.masteryLevel - b.masteryLevel;
|
|
345
|
+
// }
|
|
346
|
+
// // Finally, non-studied (timesStudied === 0)
|
|
347
|
+
// if (a.timesStudied === 0 && b.timesStudied !== 0) return -1;
|
|
348
|
+
// if (a.timesStudied !== 0 && b.timesStudied === 0) return 1;
|
|
349
|
+
// return 0;
|
|
350
|
+
// });
|
|
351
|
+
return selectedCards.map((progress) => progress.flashcard);
|
|
313
352
|
}
|
|
314
353
|
/**
|
|
315
354
|
* Get user statistics for a flashcard set
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goscribe/server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@auth/express": "^0.11.0",
|
|
24
24
|
"@auth/prisma-adapter": "^2.10.0",
|
|
25
|
-
"@goscribe/server": "^1.
|
|
25
|
+
"@goscribe/server": "^1.1.2",
|
|
26
26
|
"@prisma/client": "^6.14.0",
|
|
27
27
|
"@supabase/supabase-js": "^2.76.1",
|
|
28
28
|
"@trpc/server": "^11.5.0",
|
package/prisma/schema.prisma
CHANGED
|
@@ -63,11 +63,21 @@ model User {
|
|
|
63
63
|
// Invitations
|
|
64
64
|
sentInvitations WorkspaceInvitation[] @relation("UserInvitations")
|
|
65
65
|
|
|
66
|
+
notifications Notification[]
|
|
66
67
|
chats Chat[]
|
|
67
68
|
createdAt DateTime @default(now())
|
|
68
|
-
updatedAt DateTime @updatedAt
|
|
69
|
+
updatedAt DateTime @updatedAt
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
model Notification {
|
|
73
|
+
id String @id @default(cuid())
|
|
74
|
+
userId String
|
|
75
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
76
|
+
content String
|
|
77
|
+
read Boolean @default(false)
|
|
78
|
+
createdAt DateTime @default(now())
|
|
79
|
+
updatedAt DateTime @updatedAt
|
|
80
|
+
}
|
|
71
81
|
model Session {
|
|
72
82
|
id String @id @default(cuid())
|
|
73
83
|
userId String
|
|
@@ -193,7 +203,7 @@ model FileAsset {
|
|
|
193
203
|
objectKey String?
|
|
194
204
|
url String? // optional if serving via signed GET per-view
|
|
195
205
|
checksum String? // optional server-side integrity
|
|
196
|
-
aiTranscription
|
|
206
|
+
aiTranscription Json? @default("{}")
|
|
197
207
|
|
|
198
208
|
meta Json? // arbitrary metadata
|
|
199
209
|
|
package/src/lib/ai-session.ts
CHANGED
|
@@ -136,11 +136,6 @@ export class AISessionService {
|
|
|
136
136
|
fileType: 'image' | 'pdf',
|
|
137
137
|
maxPages?: number
|
|
138
138
|
): Promise<ProcessFileResult> {
|
|
139
|
-
const session = this.sessions.get(sessionId);
|
|
140
|
-
if (!session) {
|
|
141
|
-
throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
|
|
142
|
-
}
|
|
143
|
-
|
|
144
139
|
await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
|
|
145
140
|
|
|
146
141
|
// Mock mode - return fake processing result
|
|
@@ -168,6 +163,8 @@ export class AISessionService {
|
|
|
168
163
|
formData.append('maxPages', maxPages.toString());
|
|
169
164
|
}
|
|
170
165
|
|
|
166
|
+
console.log('formData', formData);
|
|
167
|
+
|
|
171
168
|
// Retry logic for file processing
|
|
172
169
|
const maxRetries = 3;
|
|
173
170
|
let lastError: Error | null = null;
|
|
@@ -183,7 +180,7 @@ export class AISessionService {
|
|
|
183
180
|
const response = await fetch(AI_SERVICE_URL, {
|
|
184
181
|
method: 'POST',
|
|
185
182
|
body: formData,
|
|
186
|
-
signal: controller.signal,
|
|
183
|
+
// signal: controller.signal,
|
|
187
184
|
});
|
|
188
185
|
|
|
189
186
|
clearTimeout(timeoutId);
|
|
@@ -201,11 +198,6 @@ export class AISessionService {
|
|
|
201
198
|
throw new Error(result.error || 'File processing failed');
|
|
202
199
|
}
|
|
203
200
|
|
|
204
|
-
// Update session
|
|
205
|
-
session.files.push(fileUrl);
|
|
206
|
-
session.updatedAt = new Date();
|
|
207
|
-
this.sessions.set(sessionId, session);
|
|
208
|
-
|
|
209
201
|
return result as ProcessFileResult;
|
|
210
202
|
|
|
211
203
|
} catch (error) {
|
|
@@ -235,11 +227,6 @@ export class AISessionService {
|
|
|
235
227
|
|
|
236
228
|
// Generate study guide
|
|
237
229
|
async generateStudyGuide(sessionId: string, user: string): Promise<string> {
|
|
238
|
-
const session = this.sessions.get(sessionId);
|
|
239
|
-
if (!session) {
|
|
240
|
-
throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
|
|
241
|
-
}
|
|
242
|
-
|
|
243
230
|
await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
|
|
244
231
|
// Mock mode - return fake study guide
|
|
245
232
|
if (MOCK_MODE) {
|
|
@@ -291,11 +278,6 @@ This mock study guide demonstrates the structure and format that would be genera
|
|
|
291
278
|
|
|
292
279
|
// Generate flashcard questions
|
|
293
280
|
async generateFlashcardQuestions(sessionId: string, user: string, numQuestions: number, difficulty: 'easy' | 'medium' | 'hard'): Promise<string> {
|
|
294
|
-
// const session = this.sessions.get(sessionId);
|
|
295
|
-
// if (!session) {
|
|
296
|
-
// throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
|
|
297
|
-
// }
|
|
298
|
-
|
|
299
281
|
await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
|
|
300
282
|
// Mock mode - return fake flashcard questions
|
|
301
283
|
if (MOCK_MODE) {
|
|
@@ -177,22 +177,6 @@ export const flashcards = router({
|
|
|
177
177
|
|
|
178
178
|
await PusherService.emitTaskComplete(input.workspaceId, 'flash_card_info', { status: 'generating', numCards: input.numCards, difficulty: input.difficulty });
|
|
179
179
|
|
|
180
|
-
const formattedPreviousCards = flashcardCurrent?.flashcards.map((card) => ({
|
|
181
|
-
front: card.front,
|
|
182
|
-
back: card.back,
|
|
183
|
-
}));
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const partialPrompt = `
|
|
187
|
-
This is the users previous flashcards, avoid repeating any existing cards.
|
|
188
|
-
Please generate ${input.numCards} new cards,
|
|
189
|
-
Of a ${input.difficulty} difficulty,
|
|
190
|
-
Of a ${input.tags?.join(', ')} tag,
|
|
191
|
-
Of a ${input.title} title.
|
|
192
|
-
${formattedPreviousCards?.map((card) => `Front: ${card.front}\nBack: ${card.back}`).join('\n')}
|
|
193
|
-
|
|
194
|
-
The user has also left you this prompt: ${input.prompt}
|
|
195
|
-
`
|
|
196
180
|
const artifact = await ctx.db.artifact.create({
|
|
197
181
|
data: {
|
|
198
182
|
workspaceId: input.workspaceId,
|
package/src/routers/members.ts
CHANGED
|
@@ -230,7 +230,16 @@ export const members = router({
|
|
|
230
230
|
invitedByName: invitation.workspace.owner.name || invitation.workspace.owner.email,
|
|
231
231
|
};
|
|
232
232
|
}),
|
|
233
|
-
|
|
233
|
+
getInvitations: authedProcedure
|
|
234
|
+
.input(z.object({
|
|
235
|
+
workspaceId: z.string(),
|
|
236
|
+
}))
|
|
237
|
+
.query(async ({ ctx, input }) => {
|
|
238
|
+
const invitations = await ctx.db.workspaceInvitation.findMany({
|
|
239
|
+
where: { workspaceId: input.workspaceId },
|
|
240
|
+
});
|
|
241
|
+
return invitations;
|
|
242
|
+
}),
|
|
234
243
|
/**
|
|
235
244
|
* Accept an invitation (public endpoint)
|
|
236
245
|
*/
|
|
@@ -277,8 +286,10 @@ export const members = router({
|
|
|
277
286
|
});
|
|
278
287
|
}
|
|
279
288
|
|
|
289
|
+
const user = await ctx.db.user.findFirst({ where: { id: ctx.session.user.id } });
|
|
290
|
+
if (!user || !user.email) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
280
291
|
// Check if the email matches the user's email
|
|
281
|
-
if (
|
|
292
|
+
if (user.email !== invitation.email) {
|
|
282
293
|
throw new TRPCError({
|
|
283
294
|
code: 'BAD_REQUEST',
|
|
284
295
|
message: 'This invitation was sent to a different email address'
|