@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
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Podcast generation prompts
|
|
3
|
-
*/
|
|
4
|
-
export interface StructurePromptParams {
|
|
5
|
-
title: string;
|
|
6
|
-
description?: string;
|
|
7
|
-
userPrompt: string;
|
|
8
|
-
studyGuideContent: string;
|
|
9
|
-
generateIntro: boolean;
|
|
10
|
-
generateOutro: boolean;
|
|
11
|
-
}
|
|
12
|
-
export interface RegenerateSegmentPromptParams {
|
|
13
|
-
oldContent: string;
|
|
14
|
-
userPrompt: string;
|
|
15
|
-
segmentTitle: string;
|
|
16
|
-
context: {
|
|
17
|
-
episodeTitle: string;
|
|
18
|
-
allSegmentTitles: string[];
|
|
19
|
-
segmentIndex: number;
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Generate prompt for structuring podcast content
|
|
24
|
-
*/
|
|
25
|
-
export declare function createStructurePrompt(params: StructurePromptParams): string;
|
|
26
|
-
/**
|
|
27
|
-
* Generate prompt for episode summary
|
|
28
|
-
*/
|
|
29
|
-
export declare function createSummaryPrompt(episodeTitle: string, segments: Array<{
|
|
30
|
-
title: string;
|
|
31
|
-
keyPoints: string[];
|
|
32
|
-
}>): string;
|
|
33
|
-
/**
|
|
34
|
-
* Generate improved prompt for segment regeneration
|
|
35
|
-
*/
|
|
36
|
-
export declare function createRegenerateSegmentPrompt(params: RegenerateSegmentPromptParams): string;
|
|
37
|
-
/**
|
|
38
|
-
* Validate environment for podcast generation
|
|
39
|
-
*/
|
|
40
|
-
export declare function validatePodcastEnvironment(): {
|
|
41
|
-
valid: boolean;
|
|
42
|
-
errors: string[];
|
|
43
|
-
};
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Podcast generation prompts
|
|
3
|
-
*/
|
|
4
|
-
/**
|
|
5
|
-
* Generate prompt for structuring podcast content
|
|
6
|
-
*/
|
|
7
|
-
export function createStructurePrompt(params) {
|
|
8
|
-
const { title, description, userPrompt, studyGuideContent, generateIntro, generateOutro } = params;
|
|
9
|
-
return `You are an expert podcast content creator and scriptwriter. Your task is to create an engaging, educational podcast episode with professional structure and natural conversational language.
|
|
10
|
-
|
|
11
|
-
OBJECTIVE:
|
|
12
|
-
Create a complete podcast episode that is informative, engaging, and flows naturally. The content should be suitable for audio delivery with a conversational tone.
|
|
13
|
-
|
|
14
|
-
USER REQUEST:
|
|
15
|
-
Title: ${title}
|
|
16
|
-
Description: ${description || 'No description provided'}
|
|
17
|
-
Prompt: ${userPrompt}
|
|
18
|
-
|
|
19
|
-
REQUIREMENTS:
|
|
20
|
-
1. Create segments that are 2-5 minutes each when spoken at a normal pace
|
|
21
|
-
2. Use natural, conversational language (avoid reading lists, use storytelling)
|
|
22
|
-
3. Include smooth transitions between segments
|
|
23
|
-
4. Each segment should have a clear focus and takeaways
|
|
24
|
-
5. Make content accessible but informative
|
|
25
|
-
${generateIntro ? '6. Start with an engaging hook that captures attention immediately' : ''}
|
|
26
|
-
${generateOutro ? '7. End with a strong conclusion that summarizes and motivates action' : ''}
|
|
27
|
-
|
|
28
|
-
CONTEXT (if available):
|
|
29
|
-
${studyGuideContent ? `Study Guide Content (use as reference, don't copy verbatim):\n${studyGuideContent.substring(0, 2000)}` : 'No study guide available'}
|
|
30
|
-
|
|
31
|
-
OUTPUT FORMAT (strict JSON):
|
|
32
|
-
{
|
|
33
|
-
"episodeTitle": "Enhanced, engaging title for the podcast",
|
|
34
|
-
"totalEstimatedDuration": "XX minutes",
|
|
35
|
-
"segments": [
|
|
36
|
-
{
|
|
37
|
-
"title": "Segment title (concise and descriptive)",
|
|
38
|
-
"content": "Natural conversational script. Use 'I', 'we', 'you'. Tell stories. Ask rhetorical questions. Use analogies. Make it sound like a real conversation, not a lecture.",
|
|
39
|
-
"keyPoints": ["Main point 1", "Main point 2", "Main point 3"],
|
|
40
|
-
"estimatedDuration": "X minutes",
|
|
41
|
-
"order": 1
|
|
42
|
-
}
|
|
43
|
-
]
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
IMPORTANT:
|
|
47
|
-
- Write like you're having a conversation with a friend
|
|
48
|
-
- Use contractions (I'm, you're, let's)
|
|
49
|
-
- Include verbal cues ("Now, here's the interesting part...", "You know what's fascinating?")
|
|
50
|
-
- Vary sentence length for natural rhythm
|
|
51
|
-
- Each segment should be self-contained but connected to the narrative
|
|
52
|
-
|
|
53
|
-
Return ONLY the JSON, no additional text.`;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Generate prompt for episode summary
|
|
57
|
-
*/
|
|
58
|
-
export function createSummaryPrompt(episodeTitle, segments) {
|
|
59
|
-
return `Create a comprehensive analysis and summary for this podcast episode.
|
|
60
|
-
|
|
61
|
-
EPISODE: "${episodeTitle}"
|
|
62
|
-
|
|
63
|
-
SEGMENTS:
|
|
64
|
-
${JSON.stringify(segments, null, 2)}
|
|
65
|
-
|
|
66
|
-
Generate a detailed summary with the following structure (return as JSON):
|
|
67
|
-
|
|
68
|
-
{
|
|
69
|
-
"executiveSummary": "2-3 sentence overview of what listeners will learn",
|
|
70
|
-
"learningObjectives": ["Specific, actionable objectives (3-5 items)"],
|
|
71
|
-
"keyConcepts": ["Main concepts covered (5-7 items)"],
|
|
72
|
-
"followUpActions": ["Concrete next steps listeners can take (3-5 items)"],
|
|
73
|
-
"targetAudience": "Detailed description of who will benefit most",
|
|
74
|
-
"prerequisites": ["Knowledge or background helpful but not required (2-3 items, or empty array)"],
|
|
75
|
-
"tags": ["Relevant searchable tags (5-8 items)"]
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
Make it specific, actionable, and useful for potential listeners trying to decide if this episode is for them.
|
|
79
|
-
|
|
80
|
-
Return ONLY the JSON, no additional text.`;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Generate improved prompt for segment regeneration
|
|
84
|
-
*/
|
|
85
|
-
export function createRegenerateSegmentPrompt(params) {
|
|
86
|
-
const { oldContent, userPrompt, segmentTitle, context } = params;
|
|
87
|
-
return `You are revising a specific segment of a podcast episode.
|
|
88
|
-
|
|
89
|
-
EPISODE CONTEXT:
|
|
90
|
-
- Episode Title: "${context.episodeTitle}"
|
|
91
|
-
- All Segments: ${context.allSegmentTitles.map((t, i) => `${i + 1}. ${t}`).join(', ')}
|
|
92
|
-
- Current Segment: #${context.segmentIndex + 1} - "${segmentTitle}"
|
|
93
|
-
|
|
94
|
-
CURRENT CONTENT:
|
|
95
|
-
"""
|
|
96
|
-
${oldContent}
|
|
97
|
-
"""
|
|
98
|
-
|
|
99
|
-
USER REQUEST FOR REVISION:
|
|
100
|
-
"""
|
|
101
|
-
${userPrompt}
|
|
102
|
-
"""
|
|
103
|
-
|
|
104
|
-
TASK:
|
|
105
|
-
Rewrite this segment following the user's request while maintaining:
|
|
106
|
-
1. Natural, conversational tone
|
|
107
|
-
2. Connection to the overall episode narrative
|
|
108
|
-
3. Appropriate length (2-5 minutes spoken)
|
|
109
|
-
4. Engaging and informative content
|
|
110
|
-
|
|
111
|
-
GUIDELINES:
|
|
112
|
-
- If user asks to make it "longer", expand on concepts with examples and stories
|
|
113
|
-
- If user asks to make it "shorter", focus on key points and remove tangents
|
|
114
|
-
- If user asks to "add more detail", include specific examples, data, or anecdotes
|
|
115
|
-
- If user asks to "simplify", use clearer language and better analogies
|
|
116
|
-
- If user asks to "make it more engaging", add hooks, questions, and storytelling
|
|
117
|
-
|
|
118
|
-
Return ONLY the revised content as plain text, no JSON or formatting. Write as if you're speaking to listeners.`;
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Validate environment for podcast generation
|
|
122
|
-
*/
|
|
123
|
-
export function validatePodcastEnvironment() {
|
|
124
|
-
const errors = [];
|
|
125
|
-
if (!process.env.MURF_TTS_KEY) {
|
|
126
|
-
errors.push('MURF_TTS_KEY environment variable is not set');
|
|
127
|
-
}
|
|
128
|
-
if (!process.env.INFERENCE_API_URL && !process.env.OPENAI_API_KEY) {
|
|
129
|
-
errors.push('No AI inference API configured (need INFERENCE_API_URL or OPENAI_API_KEY)');
|
|
130
|
-
}
|
|
131
|
-
return {
|
|
132
|
-
valid: errors.length === 0,
|
|
133
|
-
errors,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
File without changes
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import type { PrismaClient } from '@prisma/client';
|
|
2
|
-
import type { CreateFlashcardInput } from '../types/index.js';
|
|
3
|
-
export declare class FlashcardService {
|
|
4
|
-
private db;
|
|
5
|
-
constructor(db: PrismaClient);
|
|
6
|
-
jsonToFlashcards(json: string): Promise<any>;
|
|
7
|
-
/**
|
|
8
|
-
* List all flashcard sets for a workspace
|
|
9
|
-
*/
|
|
10
|
-
listFlashcardSets(workspaceId: string, userId: string): Promise<({
|
|
11
|
-
flashcards: {
|
|
12
|
-
id: string;
|
|
13
|
-
createdAt: Date;
|
|
14
|
-
artifactId: string;
|
|
15
|
-
order: number;
|
|
16
|
-
front: string;
|
|
17
|
-
back: string;
|
|
18
|
-
tags: string[];
|
|
19
|
-
}[];
|
|
20
|
-
} & {
|
|
21
|
-
id: string;
|
|
22
|
-
createdAt: Date;
|
|
23
|
-
updatedAt: Date;
|
|
24
|
-
title: string;
|
|
25
|
-
description: string | null;
|
|
26
|
-
workspaceId: string;
|
|
27
|
-
type: import("@prisma/client").$Enums.ArtifactType;
|
|
28
|
-
isArchived: boolean;
|
|
29
|
-
generating: boolean;
|
|
30
|
-
generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
|
|
31
|
-
difficulty: import("@prisma/client").$Enums.Difficulty | null;
|
|
32
|
-
estimatedTime: string | null;
|
|
33
|
-
imageObjectKey: string | null;
|
|
34
|
-
createdById: string | null;
|
|
35
|
-
})[]>;
|
|
36
|
-
/**
|
|
37
|
-
* Get a single flashcard set
|
|
38
|
-
*/
|
|
39
|
-
getFlashcardSet(setId: string, userId: string): Promise<{
|
|
40
|
-
flashcards: {
|
|
41
|
-
id: string;
|
|
42
|
-
createdAt: Date;
|
|
43
|
-
artifactId: string;
|
|
44
|
-
order: number;
|
|
45
|
-
front: string;
|
|
46
|
-
back: string;
|
|
47
|
-
tags: string[];
|
|
48
|
-
}[];
|
|
49
|
-
} & {
|
|
50
|
-
id: string;
|
|
51
|
-
createdAt: Date;
|
|
52
|
-
updatedAt: Date;
|
|
53
|
-
title: string;
|
|
54
|
-
description: string | null;
|
|
55
|
-
workspaceId: string;
|
|
56
|
-
type: import("@prisma/client").$Enums.ArtifactType;
|
|
57
|
-
isArchived: boolean;
|
|
58
|
-
generating: boolean;
|
|
59
|
-
generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
|
|
60
|
-
difficulty: import("@prisma/client").$Enums.Difficulty | null;
|
|
61
|
-
estimatedTime: string | null;
|
|
62
|
-
imageObjectKey: string | null;
|
|
63
|
-
createdById: string | null;
|
|
64
|
-
}>;
|
|
65
|
-
/**
|
|
66
|
-
* Create a new flashcard set
|
|
67
|
-
*/
|
|
68
|
-
createFlashcardSet(data: {
|
|
69
|
-
workspaceId: string;
|
|
70
|
-
title: string;
|
|
71
|
-
userId: string;
|
|
72
|
-
flashcards?: CreateFlashcardInput[];
|
|
73
|
-
}): Promise<{
|
|
74
|
-
flashcards: {
|
|
75
|
-
id: string;
|
|
76
|
-
createdAt: Date;
|
|
77
|
-
artifactId: string;
|
|
78
|
-
order: number;
|
|
79
|
-
front: string;
|
|
80
|
-
back: string;
|
|
81
|
-
tags: string[];
|
|
82
|
-
}[];
|
|
83
|
-
} & {
|
|
84
|
-
id: string;
|
|
85
|
-
createdAt: Date;
|
|
86
|
-
updatedAt: Date;
|
|
87
|
-
title: string;
|
|
88
|
-
description: string | null;
|
|
89
|
-
workspaceId: string;
|
|
90
|
-
type: import("@prisma/client").$Enums.ArtifactType;
|
|
91
|
-
isArchived: boolean;
|
|
92
|
-
generating: boolean;
|
|
93
|
-
generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
|
|
94
|
-
difficulty: import("@prisma/client").$Enums.Difficulty | null;
|
|
95
|
-
estimatedTime: string | null;
|
|
96
|
-
imageObjectKey: string | null;
|
|
97
|
-
createdById: string | null;
|
|
98
|
-
}>;
|
|
99
|
-
/**
|
|
100
|
-
* Update a flashcard set
|
|
101
|
-
*/
|
|
102
|
-
updateFlashcardSet(data: {
|
|
103
|
-
id: string;
|
|
104
|
-
title?: string;
|
|
105
|
-
userId: string;
|
|
106
|
-
flashcards?: (CreateFlashcardInput & {
|
|
107
|
-
id?: string;
|
|
108
|
-
})[];
|
|
109
|
-
}): Promise<{
|
|
110
|
-
flashcards: {
|
|
111
|
-
id: string;
|
|
112
|
-
createdAt: Date;
|
|
113
|
-
artifactId: string;
|
|
114
|
-
order: number;
|
|
115
|
-
front: string;
|
|
116
|
-
back: string;
|
|
117
|
-
tags: string[];
|
|
118
|
-
}[];
|
|
119
|
-
} & {
|
|
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
|
-
generating: boolean;
|
|
129
|
-
generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
|
|
130
|
-
difficulty: import("@prisma/client").$Enums.Difficulty | null;
|
|
131
|
-
estimatedTime: string | null;
|
|
132
|
-
imageObjectKey: string | null;
|
|
133
|
-
createdById: string | null;
|
|
134
|
-
}>;
|
|
135
|
-
/**
|
|
136
|
-
* Delete a flashcard set
|
|
137
|
-
*/
|
|
138
|
-
deleteFlashcardSet(setId: string, userId: string): Promise<{
|
|
139
|
-
success: boolean;
|
|
140
|
-
}>;
|
|
141
|
-
/**
|
|
142
|
-
* Add a flashcard to a set
|
|
143
|
-
*/
|
|
144
|
-
addFlashcard(data: {
|
|
145
|
-
setId: string;
|
|
146
|
-
userId: string;
|
|
147
|
-
flashcard: CreateFlashcardInput;
|
|
148
|
-
}): Promise<{
|
|
149
|
-
id: string;
|
|
150
|
-
createdAt: Date;
|
|
151
|
-
artifactId: string;
|
|
152
|
-
order: number;
|
|
153
|
-
front: string;
|
|
154
|
-
back: string;
|
|
155
|
-
tags: string[];
|
|
156
|
-
}>;
|
|
157
|
-
/**
|
|
158
|
-
* Update a flashcard
|
|
159
|
-
*/
|
|
160
|
-
updateFlashcard(data: {
|
|
161
|
-
flashcardId: string;
|
|
162
|
-
userId: string;
|
|
163
|
-
updates: Partial<CreateFlashcardInput>;
|
|
164
|
-
}): Promise<{
|
|
165
|
-
id: string;
|
|
166
|
-
createdAt: Date;
|
|
167
|
-
artifactId: string;
|
|
168
|
-
order: number;
|
|
169
|
-
front: string;
|
|
170
|
-
back: string;
|
|
171
|
-
tags: string[];
|
|
172
|
-
}>;
|
|
173
|
-
/**
|
|
174
|
-
* Delete a flashcard
|
|
175
|
-
*/
|
|
176
|
-
deleteFlashcard(flashcardId: string, userId: string): Promise<{
|
|
177
|
-
success: boolean;
|
|
178
|
-
}>;
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Factory function to create flashcard service
|
|
182
|
-
*/
|
|
183
|
-
export declare function createFlashcardService(db: PrismaClient): FlashcardService;
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import { NotFoundError } from '../lib/errors.js';
|
|
2
|
-
export class FlashcardService {
|
|
3
|
-
constructor(db) {
|
|
4
|
-
this.db = db;
|
|
5
|
-
}
|
|
6
|
-
async jsonToFlashcards(json) {
|
|
7
|
-
const flashcards = JSON.parse(json);
|
|
8
|
-
return flashcards.map((card) => ({
|
|
9
|
-
front: card.front,
|
|
10
|
-
back: card.back,
|
|
11
|
-
}));
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* List all flashcard sets for a workspace
|
|
15
|
-
*/
|
|
16
|
-
async listFlashcardSets(workspaceId, userId) {
|
|
17
|
-
return this.db.artifact.findMany({
|
|
18
|
-
where: {
|
|
19
|
-
workspaceId,
|
|
20
|
-
type: 'FLASHCARD_SET',
|
|
21
|
-
workspace: { ownerId: userId },
|
|
22
|
-
},
|
|
23
|
-
include: {
|
|
24
|
-
flashcards: {
|
|
25
|
-
orderBy: { order: 'asc' },
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
orderBy: { updatedAt: 'desc' },
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Get a single flashcard set
|
|
33
|
-
*/
|
|
34
|
-
async getFlashcardSet(setId, userId) {
|
|
35
|
-
const flashcardSet = await this.db.artifact.findFirst({
|
|
36
|
-
where: {
|
|
37
|
-
id: setId,
|
|
38
|
-
type: 'FLASHCARD_SET',
|
|
39
|
-
workspace: { ownerId: userId },
|
|
40
|
-
},
|
|
41
|
-
include: {
|
|
42
|
-
flashcards: {
|
|
43
|
-
orderBy: { order: 'asc' },
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
if (!flashcardSet) {
|
|
48
|
-
throw new NotFoundError('Flashcard set');
|
|
49
|
-
}
|
|
50
|
-
return flashcardSet;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Create a new flashcard set
|
|
54
|
-
*/
|
|
55
|
-
async createFlashcardSet(data) {
|
|
56
|
-
// Verify workspace ownership
|
|
57
|
-
const workspace = await this.db.workspace.findFirst({
|
|
58
|
-
where: {
|
|
59
|
-
id: data.workspaceId,
|
|
60
|
-
ownerId: data.userId,
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
if (!workspace) {
|
|
64
|
-
throw new NotFoundError('Workspace');
|
|
65
|
-
}
|
|
66
|
-
const { flashcards, ...setData } = data;
|
|
67
|
-
return this.db.artifact.create({
|
|
68
|
-
data: {
|
|
69
|
-
workspaceId: data.workspaceId,
|
|
70
|
-
type: 'FLASHCARD_SET',
|
|
71
|
-
title: data.title,
|
|
72
|
-
createdById: data.userId,
|
|
73
|
-
flashcards: flashcards
|
|
74
|
-
? {
|
|
75
|
-
create: flashcards.map((card, index) => ({
|
|
76
|
-
...card,
|
|
77
|
-
order: card.order ?? index,
|
|
78
|
-
})),
|
|
79
|
-
}
|
|
80
|
-
: undefined,
|
|
81
|
-
},
|
|
82
|
-
include: {
|
|
83
|
-
flashcards: {
|
|
84
|
-
orderBy: { order: 'asc' },
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Update a flashcard set
|
|
91
|
-
*/
|
|
92
|
-
async updateFlashcardSet(data) {
|
|
93
|
-
const { id, flashcards, userId, ...updateData } = data;
|
|
94
|
-
// Verify ownership
|
|
95
|
-
const existingSet = await this.db.artifact.findFirst({
|
|
96
|
-
where: {
|
|
97
|
-
id,
|
|
98
|
-
type: 'FLASHCARD_SET',
|
|
99
|
-
workspace: { ownerId: userId },
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
if (!existingSet) {
|
|
103
|
-
throw new NotFoundError('Flashcard set');
|
|
104
|
-
}
|
|
105
|
-
// Handle flashcards update if provided
|
|
106
|
-
if (flashcards) {
|
|
107
|
-
// Delete existing flashcards
|
|
108
|
-
await this.db.flashcard.deleteMany({
|
|
109
|
-
where: { artifactId: id },
|
|
110
|
-
});
|
|
111
|
-
// Create new flashcards
|
|
112
|
-
await this.db.flashcard.createMany({
|
|
113
|
-
data: flashcards.map((card, index) => ({
|
|
114
|
-
artifactId: id,
|
|
115
|
-
front: card.front,
|
|
116
|
-
back: card.back,
|
|
117
|
-
tags: card.tags || [],
|
|
118
|
-
order: card.order ?? index,
|
|
119
|
-
})),
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
return this.db.artifact.update({
|
|
123
|
-
where: { id },
|
|
124
|
-
data: updateData,
|
|
125
|
-
include: {
|
|
126
|
-
flashcards: {
|
|
127
|
-
orderBy: { order: 'asc' },
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Delete a flashcard set
|
|
134
|
-
*/
|
|
135
|
-
async deleteFlashcardSet(setId, userId) {
|
|
136
|
-
const deleted = await this.db.artifact.deleteMany({
|
|
137
|
-
where: {
|
|
138
|
-
id: setId,
|
|
139
|
-
type: 'FLASHCARD_SET',
|
|
140
|
-
workspace: { ownerId: userId },
|
|
141
|
-
},
|
|
142
|
-
});
|
|
143
|
-
if (deleted.count === 0) {
|
|
144
|
-
throw new NotFoundError('Flashcard set');
|
|
145
|
-
}
|
|
146
|
-
return { success: true };
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Add a flashcard to a set
|
|
150
|
-
*/
|
|
151
|
-
async addFlashcard(data) {
|
|
152
|
-
// Verify ownership
|
|
153
|
-
const set = await this.db.artifact.findFirst({
|
|
154
|
-
where: {
|
|
155
|
-
id: data.setId,
|
|
156
|
-
type: 'FLASHCARD_SET',
|
|
157
|
-
workspace: { ownerId: data.userId },
|
|
158
|
-
},
|
|
159
|
-
});
|
|
160
|
-
if (!set) {
|
|
161
|
-
throw new NotFoundError('Flashcard set');
|
|
162
|
-
}
|
|
163
|
-
// Get the next order number
|
|
164
|
-
const maxOrder = await this.db.flashcard.aggregate({
|
|
165
|
-
where: { artifactId: data.setId },
|
|
166
|
-
_max: { order: true },
|
|
167
|
-
});
|
|
168
|
-
return this.db.flashcard.create({
|
|
169
|
-
data: {
|
|
170
|
-
artifactId: data.setId,
|
|
171
|
-
front: data.flashcard.front,
|
|
172
|
-
back: data.flashcard.back,
|
|
173
|
-
tags: data.flashcard.tags || [],
|
|
174
|
-
order: data.flashcard.order ?? (maxOrder._max.order ?? 0) + 1,
|
|
175
|
-
},
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Update a flashcard
|
|
180
|
-
*/
|
|
181
|
-
async updateFlashcard(data) {
|
|
182
|
-
// Verify ownership
|
|
183
|
-
const flashcard = await this.db.flashcard.findFirst({
|
|
184
|
-
where: {
|
|
185
|
-
id: data.flashcardId,
|
|
186
|
-
artifact: {
|
|
187
|
-
type: 'FLASHCARD_SET',
|
|
188
|
-
workspace: { ownerId: data.userId },
|
|
189
|
-
},
|
|
190
|
-
},
|
|
191
|
-
});
|
|
192
|
-
if (!flashcard) {
|
|
193
|
-
throw new NotFoundError('Flashcard');
|
|
194
|
-
}
|
|
195
|
-
return this.db.flashcard.update({
|
|
196
|
-
where: { id: data.flashcardId },
|
|
197
|
-
data: data.updates,
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Delete a flashcard
|
|
202
|
-
*/
|
|
203
|
-
async deleteFlashcard(flashcardId, userId) {
|
|
204
|
-
const flashcard = await this.db.flashcard.findFirst({
|
|
205
|
-
where: {
|
|
206
|
-
id: flashcardId,
|
|
207
|
-
artifact: { workspace: { ownerId: userId } },
|
|
208
|
-
},
|
|
209
|
-
});
|
|
210
|
-
if (!flashcard) {
|
|
211
|
-
throw new NotFoundError('Flashcard');
|
|
212
|
-
}
|
|
213
|
-
await this.db.flashcard.delete({
|
|
214
|
-
where: { id: flashcardId },
|
|
215
|
-
});
|
|
216
|
-
return { success: true };
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Factory function to create flashcard service
|
|
221
|
-
*/
|
|
222
|
-
export function createFlashcardService(db) {
|
|
223
|
-
return new FlashcardService(db);
|
|
224
|
-
}
|
|
File without changes
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// import type { PrismaClient } from '@prisma/client';
|
|
3
|
-
// import { NotFoundError } from '../lib/errors.js';
|
|
4
|
-
// export interface ReorderSegmentData {
|
|
5
|
-
// id: string;
|
|
6
|
-
// newOrder: number;
|
|
7
|
-
// }
|
|
8
|
-
// export class PodcastSegmentReorderService {
|
|
9
|
-
// constructor(private db: PrismaClient) {}
|
|
10
|
-
// /**
|
|
11
|
-
// * Reorder podcast segments and recalculate start times
|
|
12
|
-
// */
|
|
13
|
-
// async reorderSegments(data: {
|
|
14
|
-
// episodeId: string;
|
|
15
|
-
// userId: string;
|
|
16
|
-
// newOrder: ReorderSegmentData[];
|
|
17
|
-
// }) {
|
|
18
|
-
// const { episodeId, userId, newOrder } = data;
|
|
19
|
-
// // Verify ownership
|
|
20
|
-
// const episode = await this.db.artifact.findFirst({
|
|
21
|
-
// where: {
|
|
22
|
-
// id: episodeId,
|
|
23
|
-
// type: 'PODCAST_EPISODE',
|
|
24
|
-
// workspace: { ownerId: userId },
|
|
25
|
-
// },
|
|
26
|
-
// include: {
|
|
27
|
-
// podcastSegments: {
|
|
28
|
-
// orderBy: { order: 'asc' },
|
|
29
|
-
// },
|
|
30
|
-
// },
|
|
31
|
-
// });
|
|
32
|
-
// if (!episode) {
|
|
33
|
-
// throw new NotFoundError('Podcast episode');
|
|
34
|
-
// }
|
|
35
|
-
// // Validate all segment IDs exist
|
|
36
|
-
// const segmentIds = episode.podcastSegments.map((s) => s.id);
|
|
37
|
-
// const invalidIds = newOrder.filter((item) => !segmentIds.includes(item.id));
|
|
38
|
-
// if (invalidIds.length > 0) {
|
|
39
|
-
// throw new Error(`Invalid segment IDs: ${invalidIds.map((i) => i.id).join(', ')}`);
|
|
40
|
-
// }
|
|
41
|
-
// // Validate order values are sequential
|
|
42
|
-
// const orderValues = newOrder.map((item) => item.newOrder).sort((a, b) => a - b);
|
|
43
|
-
// const expectedOrder = Array.from({ length: newOrder.length }, (_, i) => i + 1);
|
|
44
|
-
// if (JSON.stringify(orderValues) !== JSON.stringify(expectedOrder)) {
|
|
45
|
-
// throw new Error('Order values must be sequential starting from 1');
|
|
46
|
-
// }
|
|
47
|
-
// return this.db.$transaction(async (tx) => {
|
|
48
|
-
// // Update each segment's order
|
|
49
|
-
// for (const item of newOrder) {
|
|
50
|
-
// await tx.podcastSegment.update({
|
|
51
|
-
// where: { id: item.id },
|
|
52
|
-
// data: { order: item.newOrder },
|
|
53
|
-
// });
|
|
54
|
-
// }
|
|
55
|
-
// // Get all segments in new order
|
|
56
|
-
// const reorderedSegments = await tx.podcastSegment.findMany({
|
|
57
|
-
// where: { artifactId: episodeId },
|
|
58
|
-
// orderBy: { order: 'asc' },
|
|
59
|
-
// });
|
|
60
|
-
// // Recalculate start times
|
|
61
|
-
// let currentTime = 0;
|
|
62
|
-
// for (const segment of reorderedSegments) {
|
|
63
|
-
// await tx.podcastSegment.update({
|
|
64
|
-
// where: { id: segment.id },
|
|
65
|
-
// data: { startTime: currentTime },
|
|
66
|
-
// });
|
|
67
|
-
// currentTime += segment.duration;
|
|
68
|
-
// }
|
|
69
|
-
// // Update total duration in latest version
|
|
70
|
-
// const latestVersion = await tx.artifactVersion.findFirst({
|
|
71
|
-
// where: { artifactId: episodeId },
|
|
72
|
-
// orderBy: { version: 'desc' },
|
|
73
|
-
// });
|
|
74
|
-
// if (latestVersion) {
|
|
75
|
-
// const metadata = latestVersion.data as any;
|
|
76
|
-
// if (metadata) {
|
|
77
|
-
// metadata.totalDuration = currentTime;
|
|
78
|
-
// // Create new version with updated metadata
|
|
79
|
-
// await tx.artifactVersion.create({
|
|
80
|
-
// data: {
|
|
81
|
-
// artifactId: episodeId,
|
|
82
|
-
// version: latestVersion.version + 1,
|
|
83
|
-
// content: latestVersion.content,
|
|
84
|
-
// data: metadata,
|
|
85
|
-
// createdById: userId,
|
|
86
|
-
// },
|
|
87
|
-
// });
|
|
88
|
-
// }
|
|
89
|
-
// }
|
|
90
|
-
// // Update artifact timestamp
|
|
91
|
-
// await tx.artifact.update({
|
|
92
|
-
// where: { id: episodeId },
|
|
93
|
-
// data: { updatedAt: new Date() },
|
|
94
|
-
// });
|
|
95
|
-
// return {
|
|
96
|
-
// totalDuration: currentTime,
|
|
97
|
-
// segmentsReordered: reorderedSegments.length,
|
|
98
|
-
// };
|
|
99
|
-
// });
|
|
100
|
-
// }
|
|
101
|
-
// }
|
|
102
|
-
// /**
|
|
103
|
-
// * Factory function
|
|
104
|
-
// */
|
|
105
|
-
// export function createPodcastSegmentReorderService(db: PrismaClient) {
|
|
106
|
-
// return new PodcastSegmentReorderService(db);
|
|
107
|
-
// }
|
|
File without changes
|