@goscribe/server 1.1.1 → 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 +14 -1
- package/src/lib/ai-session.ts +97 -158
- package/src/routers/flashcards.ts +0 -16
- package/src/routers/members.ts +13 -2
- package/src/routers/podcast.ts +0 -1
- package/src/routers/worksheets.ts +3 -2
- package/src/routers/workspace.ts +516 -399
- 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
|
|
@@ -130,6 +140,8 @@ model Workspace {
|
|
|
130
140
|
|
|
131
141
|
analysisProgress Json?
|
|
132
142
|
|
|
143
|
+
needsAnalysis Boolean @default(false)
|
|
144
|
+
|
|
133
145
|
// Raw uploads attached to this workspace
|
|
134
146
|
uploads FileAsset[]
|
|
135
147
|
|
|
@@ -191,6 +203,7 @@ model FileAsset {
|
|
|
191
203
|
objectKey String?
|
|
192
204
|
url String? // optional if serving via signed GET per-view
|
|
193
205
|
checksum String? // optional server-side integrity
|
|
206
|
+
aiTranscription Json? @default("{}")
|
|
194
207
|
|
|
195
208
|
meta Json? // arbitrary metadata
|
|
196
209
|
|
package/src/lib/ai-session.ts
CHANGED
|
@@ -26,6 +26,19 @@ export interface AISession {
|
|
|
26
26
|
|
|
27
27
|
const IMITATE_WAIT_TIME_MS = MOCK_MODE ? 1000 * 10 : 0;
|
|
28
28
|
|
|
29
|
+
export interface ProcessFileResult {
|
|
30
|
+
status: 'success' | 'error';
|
|
31
|
+
textContent: string | null;
|
|
32
|
+
imageDescriptions: Array<{
|
|
33
|
+
page: number;
|
|
34
|
+
description: string;
|
|
35
|
+
hasVisualContent: boolean;
|
|
36
|
+
}>;
|
|
37
|
+
comprehensiveDescription: string | null;
|
|
38
|
+
pageCount: number;
|
|
39
|
+
error?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
29
42
|
export class AISessionService {
|
|
30
43
|
private sessions = new Map<string, AISession>();
|
|
31
44
|
|
|
@@ -115,67 +128,105 @@ export class AISessionService {
|
|
|
115
128
|
});
|
|
116
129
|
}
|
|
117
130
|
|
|
118
|
-
//
|
|
119
|
-
async
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
131
|
+
// Process file (PDF/image) and return comprehensive text descriptions
|
|
132
|
+
async processFile(
|
|
133
|
+
sessionId: string,
|
|
134
|
+
user: string,
|
|
135
|
+
fileUrl: string,
|
|
136
|
+
fileType: 'image' | 'pdf',
|
|
137
|
+
maxPages?: number
|
|
138
|
+
): Promise<ProcessFileResult> {
|
|
125
139
|
await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
|
|
126
|
-
|
|
140
|
+
|
|
141
|
+
// Mock mode - return fake processing result
|
|
127
142
|
if (MOCK_MODE) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
143
|
+
logger.info(`🎭 MOCK MODE: Processing ${fileType} file from URL for session ${sessionId}`);
|
|
144
|
+
const mockPageCount = fileType === 'pdf' ? 15 : 1;
|
|
145
|
+
return {
|
|
146
|
+
status: 'success',
|
|
147
|
+
textContent: `Mock extracted text content from ${fileType} file. This would contain the full text extracted from the document.`,
|
|
148
|
+
imageDescriptions: Array.from({ length: mockPageCount }, (_, i) => ({
|
|
149
|
+
page: i + 1,
|
|
150
|
+
description: `Page ${i + 1} contains educational content with diagrams and text.`,
|
|
151
|
+
hasVisualContent: true,
|
|
152
|
+
})),
|
|
153
|
+
comprehensiveDescription: `DOCUMENT SUMMARY (${mockPageCount} ${mockPageCount === 1 ? 'page' : 'pages'})\n\nTEXT CONTENT:\nMock extracted text content...\n\nVISUAL CONTENT DESCRIPTIONS:\nPage-by-page descriptions of visual elements.`,
|
|
154
|
+
pageCount: mockPageCount,
|
|
155
|
+
};
|
|
133
156
|
}
|
|
134
157
|
|
|
135
|
-
const command = fileType === 'image' ? 'append_image' : 'append_pdflike';
|
|
136
|
-
|
|
137
158
|
const formData = new FormData();
|
|
138
|
-
formData.append('command',
|
|
139
|
-
formData.append('
|
|
140
|
-
formData.append('
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
method: 'POST',
|
|
145
|
-
body: formData,
|
|
146
|
-
});
|
|
159
|
+
formData.append('command', 'process_file');
|
|
160
|
+
formData.append('fileUrl', fileUrl);
|
|
161
|
+
formData.append('fileType', fileType);
|
|
162
|
+
if (maxPages) {
|
|
163
|
+
formData.append('maxPages', maxPages.toString());
|
|
164
|
+
}
|
|
147
165
|
|
|
148
|
-
|
|
149
|
-
throw new Error(`AI service error: ${response.status} ${response.statusText}`);
|
|
150
|
-
}
|
|
166
|
+
console.log('formData', formData);
|
|
151
167
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
throw new Error(`AI service error: No response message`);
|
|
156
|
-
}
|
|
168
|
+
// Retry logic for file processing
|
|
169
|
+
const maxRetries = 3;
|
|
170
|
+
let lastError: Error | null = null;
|
|
157
171
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
173
|
+
try {
|
|
174
|
+
logger.info(`📄 Processing ${fileType} file attempt ${attempt}/${maxRetries} for session ${sessionId}`);
|
|
175
|
+
|
|
176
|
+
// Set timeout for large files (5 minutes)
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
const timeoutId = setTimeout(() => controller.abort(), 300000); // 5 min timeout
|
|
179
|
+
|
|
180
|
+
const response = await fetch(AI_SERVICE_URL, {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
body: formData,
|
|
183
|
+
// signal: controller.signal,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
clearTimeout(timeoutId);
|
|
187
|
+
|
|
188
|
+
if (!response.ok) {
|
|
189
|
+
const errorText = await response.text();
|
|
190
|
+
logger.error(`❌ File processing error response:`, errorText);
|
|
191
|
+
throw new Error(`AI service error: ${response.status} ${response.statusText} - ${errorText}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const result = await response.json();
|
|
195
|
+
logger.info(`📋 File processing result: status=${result.status}, pageCount=${result.pageCount}`);
|
|
196
|
+
|
|
197
|
+
if (result.status === 'error') {
|
|
198
|
+
throw new Error(result.error || 'File processing failed');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return result as ProcessFileResult;
|
|
202
|
+
|
|
203
|
+
} catch (error) {
|
|
204
|
+
lastError = error instanceof Error ? error : new Error('Unknown error');
|
|
205
|
+
logger.error(`❌ File processing attempt ${attempt} failed:`, lastError.message);
|
|
206
|
+
|
|
207
|
+
if (attempt < maxRetries) {
|
|
208
|
+
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff: 2s, 4s, 8s
|
|
209
|
+
logger.info(`⏳ Retrying file processing in ${delay}ms...`);
|
|
210
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
167
213
|
}
|
|
214
|
+
|
|
215
|
+
logger.error(`💥 All ${maxRetries} file processing attempts failed. Last error:`, lastError?.message);
|
|
216
|
+
return {
|
|
217
|
+
status: 'error',
|
|
218
|
+
textContent: null,
|
|
219
|
+
imageDescriptions: [],
|
|
220
|
+
comprehensiveDescription: null,
|
|
221
|
+
pageCount: 0,
|
|
222
|
+
error: `Failed to process file after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`,
|
|
223
|
+
};
|
|
168
224
|
}
|
|
169
225
|
|
|
170
226
|
|
|
171
227
|
|
|
172
228
|
// Generate study guide
|
|
173
229
|
async generateStudyGuide(sessionId: string, user: string): Promise<string> {
|
|
174
|
-
const session = this.sessions.get(sessionId);
|
|
175
|
-
if (!session) {
|
|
176
|
-
throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
|
|
177
|
-
}
|
|
178
|
-
|
|
179
230
|
await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
|
|
180
231
|
// Mock mode - return fake study guide
|
|
181
232
|
if (MOCK_MODE) {
|
|
@@ -227,11 +278,6 @@ This mock study guide demonstrates the structure and format that would be genera
|
|
|
227
278
|
|
|
228
279
|
// Generate flashcard questions
|
|
229
280
|
async generateFlashcardQuestions(sessionId: string, user: string, numQuestions: number, difficulty: 'easy' | 'medium' | 'hard'): Promise<string> {
|
|
230
|
-
// const session = this.sessions.get(sessionId);
|
|
231
|
-
// if (!session) {
|
|
232
|
-
// throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
|
|
233
|
-
// }
|
|
234
|
-
|
|
235
281
|
await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
|
|
236
282
|
// Mock mode - return fake flashcard questions
|
|
237
283
|
if (MOCK_MODE) {
|
|
@@ -503,113 +549,6 @@ This mock study guide demonstrates the structure and format that would be genera
|
|
|
503
549
|
}
|
|
504
550
|
|
|
505
551
|
|
|
506
|
-
// Analyse PDF
|
|
507
|
-
async analysePDF(sessionId: string, user: string): Promise<string> {
|
|
508
|
-
const session = this.sessions.get(sessionId);
|
|
509
|
-
if (!session) {
|
|
510
|
-
throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
|
|
514
|
-
// Mock mode - return fake PDF analysis
|
|
515
|
-
if (MOCK_MODE) {
|
|
516
|
-
console.log(`🎭 MOCK MODE: Analysing PDF for session ${sessionId}`);
|
|
517
|
-
return `Mock PDF Analysis Results:
|
|
518
|
-
|
|
519
|
-
Document Type: Educational Material
|
|
520
|
-
Pages: 15
|
|
521
|
-
Language: English
|
|
522
|
-
Key Topics Identified:
|
|
523
|
-
- Introduction to Machine Learning
|
|
524
|
-
- Data Preprocessing Techniques
|
|
525
|
-
- Model Training and Validation
|
|
526
|
-
- Performance Metrics
|
|
527
|
-
|
|
528
|
-
Summary: This mock PDF analysis shows the structure and content that would be extracted from an uploaded PDF document. The analysis includes document metadata, key topics, and a summary of the content.
|
|
529
|
-
|
|
530
|
-
Note: This is a mock response generated when DONT_TEST_INFERENCE=true`;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
const formData = new FormData();
|
|
534
|
-
formData.append('command', 'analyse_pdf');
|
|
535
|
-
formData.append('session', sessionId);
|
|
536
|
-
formData.append('user', user);
|
|
537
|
-
try {
|
|
538
|
-
const response = await fetch(AI_SERVICE_URL, {
|
|
539
|
-
method: 'POST',
|
|
540
|
-
body: formData,
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
if (!response.ok) {
|
|
544
|
-
throw new Error(`AI service error: ${response.status} ${response.statusText}`);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
const result = await response.json();
|
|
548
|
-
return result.message || 'PDF analysis completed';
|
|
549
|
-
} catch (error) {
|
|
550
|
-
throw new TRPCError({
|
|
551
|
-
code: 'INTERNAL_SERVER_ERROR',
|
|
552
|
-
message: `Failed to analyse PDF: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// Analyse Image
|
|
558
|
-
async analyseImage(sessionId: string, user: string): Promise<string> {
|
|
559
|
-
const session = this.sessions.get(sessionId);
|
|
560
|
-
if (!session) {
|
|
561
|
-
throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
|
|
565
|
-
// Mock mode - return fake image analysis
|
|
566
|
-
if (MOCK_MODE) {
|
|
567
|
-
console.log(`🎭 MOCK MODE: Analysing image for session ${sessionId}`);
|
|
568
|
-
return `Mock Image Analysis Results:
|
|
569
|
-
|
|
570
|
-
Image Type: Educational Diagram
|
|
571
|
-
Format: PNG
|
|
572
|
-
Dimensions: 1920x1080
|
|
573
|
-
Content Description:
|
|
574
|
-
- Contains a flowchart or diagram
|
|
575
|
-
- Shows a process or system architecture
|
|
576
|
-
- Includes text labels and annotations
|
|
577
|
-
- Educational or instructional content
|
|
578
|
-
|
|
579
|
-
Key Elements Identified:
|
|
580
|
-
- Process flow arrows
|
|
581
|
-
- Decision points
|
|
582
|
-
- Input/output elements
|
|
583
|
-
- Descriptive text
|
|
584
|
-
|
|
585
|
-
Summary: This mock image analysis demonstrates the type of content extraction that would be performed on uploaded images. The analysis identifies visual elements, text content, and overall structure.
|
|
586
|
-
|
|
587
|
-
Note: This is a mock response generated when DONT_TEST_INFERENCE=true`;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
const formData = new FormData();
|
|
591
|
-
formData.append('command', 'analyse_img');
|
|
592
|
-
formData.append('session', sessionId);
|
|
593
|
-
formData.append('user', user);
|
|
594
|
-
try {
|
|
595
|
-
const response = await fetch(AI_SERVICE_URL, {
|
|
596
|
-
method: 'POST',
|
|
597
|
-
body: formData,
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
if (!response.ok) {
|
|
601
|
-
throw new Error(`AI service error: ${response.status} ${response.statusText}`);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
const result = await response.json();
|
|
605
|
-
return result.message || 'Image analysis completed';
|
|
606
|
-
} catch (error) {
|
|
607
|
-
throw new TRPCError({
|
|
608
|
-
code: 'INTERNAL_SERVER_ERROR',
|
|
609
|
-
message: `Failed to analyse image: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
552
|
|
|
614
553
|
async generatePodcastImage(sessionId: string, user: string, summary: string): Promise<string> {
|
|
615
554
|
|
|
@@ -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'
|
package/src/routers/podcast.ts
CHANGED
|
@@ -281,6 +281,7 @@ export const worksheets = router({
|
|
|
281
281
|
userAnswer: input.answer,
|
|
282
282
|
completedAt: input.completed ? new Date() : null,
|
|
283
283
|
attempts: { increment: 1 },
|
|
284
|
+
correct: input.correct,
|
|
284
285
|
},
|
|
285
286
|
});
|
|
286
287
|
|
|
@@ -490,7 +491,7 @@ export const worksheets = router({
|
|
|
490
491
|
|
|
491
492
|
// Parse question meta to get mark_scheme
|
|
492
493
|
const questionMeta = question.meta ? (typeof question.meta === 'object' ? question.meta : JSON.parse(question.meta as any)) : {} as any;
|
|
493
|
-
const markScheme = questionMeta.
|
|
494
|
+
const markScheme = questionMeta.markScheme;
|
|
494
495
|
|
|
495
496
|
let isCorrect = false;
|
|
496
497
|
let userMarkScheme = null;
|
|
@@ -517,7 +518,7 @@ export const worksheets = router({
|
|
|
517
518
|
}
|
|
518
519
|
} else {
|
|
519
520
|
// Simple string comparison if no mark scheme
|
|
520
|
-
|
|
521
|
+
throw new TRPCError({ code: 'BAD_REQUEST', message: 'No mark scheme found for question' });
|
|
521
522
|
}
|
|
522
523
|
|
|
523
524
|
|