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