@tuturuuu/ai 0.8.3 → 0.8.4
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/package.json +54 -54
- package/src/chat/google/new/route.ts +46 -20
- package/src/chat/google/route-auth.ts +91 -0
- package/src/chat/google/route.ts +35 -30
- package/src/chat/google/summary/route.ts +59 -12
- package/src/object/flashcards/route.ts +0 -140
- package/src/object/quizzes/explanation/route.ts +0 -145
- package/src/object/quizzes/route.ts +0 -142
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/ai",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.4",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/tutur3u/platform",
|
|
@@ -22,50 +22,50 @@
|
|
|
22
22
|
"type-check": "tsc --project tsconfig.typecheck.json"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@ai-sdk/amazon-bedrock": "^5.0.
|
|
26
|
-
"@ai-sdk/anthropic": "^4.0.
|
|
27
|
-
"@ai-sdk/azure": "^4.0.
|
|
28
|
-
"@ai-sdk/cerebras": "^3.0.
|
|
29
|
-
"@ai-sdk/cohere": "^4.0.
|
|
30
|
-
"@ai-sdk/deepgram": "^3.
|
|
31
|
-
"@ai-sdk/deepinfra": "^3.0.
|
|
32
|
-
"@ai-sdk/deepseek": "^3.0.
|
|
33
|
-
"@ai-sdk/elevenlabs": "^3.0.
|
|
34
|
-
"@ai-sdk/fal": "^3.0.
|
|
35
|
-
"@ai-sdk/fireworks": "^3.0.
|
|
36
|
-
"@ai-sdk/gateway": "^4.0.
|
|
37
|
-
"@ai-sdk/gladia": "^3.0.
|
|
38
|
-
"@ai-sdk/google": "^4.0.
|
|
39
|
-
"@ai-sdk/google-vertex": "^5.0.
|
|
40
|
-
"@ai-sdk/groq": "^4.0.
|
|
41
|
-
"@ai-sdk/hume": "^3.0.
|
|
42
|
-
"@ai-sdk/lmnt": "^3.0.
|
|
43
|
-
"@ai-sdk/luma": "^3.0.
|
|
44
|
-
"@ai-sdk/mistral": "^4.0.
|
|
45
|
-
"@ai-sdk/openai": "^4.0.
|
|
46
|
-
"@ai-sdk/openai-compatible": "^3.0.
|
|
47
|
-
"@ai-sdk/perplexity": "^4.0.
|
|
48
|
-
"@ai-sdk/react": "^4.0.
|
|
49
|
-
"@ai-sdk/replicate": "^3.0.
|
|
50
|
-
"@ai-sdk/revai": "^3.0.
|
|
51
|
-
"@ai-sdk/togetherai": "^3.0.
|
|
52
|
-
"@ai-sdk/xai": "^4.0.
|
|
25
|
+
"@ai-sdk/amazon-bedrock": "^5.0.73",
|
|
26
|
+
"@ai-sdk/anthropic": "^4.0.49",
|
|
27
|
+
"@ai-sdk/azure": "^4.0.60",
|
|
28
|
+
"@ai-sdk/cerebras": "^3.0.43",
|
|
29
|
+
"@ai-sdk/cohere": "^4.0.37",
|
|
30
|
+
"@ai-sdk/deepgram": "^3.1.7",
|
|
31
|
+
"@ai-sdk/deepinfra": "^3.0.43",
|
|
32
|
+
"@ai-sdk/deepseek": "^3.0.39",
|
|
33
|
+
"@ai-sdk/elevenlabs": "^3.0.37",
|
|
34
|
+
"@ai-sdk/fal": "^3.0.37",
|
|
35
|
+
"@ai-sdk/fireworks": "^3.0.46",
|
|
36
|
+
"@ai-sdk/gateway": "^4.0.73",
|
|
37
|
+
"@ai-sdk/gladia": "^3.0.36",
|
|
38
|
+
"@ai-sdk/google": "^4.0.63",
|
|
39
|
+
"@ai-sdk/google-vertex": "^5.0.75",
|
|
40
|
+
"@ai-sdk/groq": "^4.0.37",
|
|
41
|
+
"@ai-sdk/hume": "^3.0.36",
|
|
42
|
+
"@ai-sdk/lmnt": "^3.0.36",
|
|
43
|
+
"@ai-sdk/luma": "^3.0.37",
|
|
44
|
+
"@ai-sdk/mistral": "^4.0.39",
|
|
45
|
+
"@ai-sdk/openai": "^4.0.57",
|
|
46
|
+
"@ai-sdk/openai-compatible": "^3.0.43",
|
|
47
|
+
"@ai-sdk/perplexity": "^4.0.39",
|
|
48
|
+
"@ai-sdk/react": "^4.0.94",
|
|
49
|
+
"@ai-sdk/replicate": "^3.0.37",
|
|
50
|
+
"@ai-sdk/revai": "^3.0.36",
|
|
51
|
+
"@ai-sdk/togetherai": "^3.0.44",
|
|
52
|
+
"@ai-sdk/xai": "^4.0.54",
|
|
53
53
|
"@beeper/chat-adapter-matrix": "^0.2.0",
|
|
54
54
|
"@bitbasti/chat-adapter-webex": "^0.1.0",
|
|
55
|
-
"@chat-adapter/discord": "^4.
|
|
56
|
-
"@chat-adapter/gchat": "^4.
|
|
57
|
-
"@chat-adapter/github": "^4.
|
|
58
|
-
"@chat-adapter/linear": "^4.
|
|
59
|
-
"@chat-adapter/messenger": "^4.
|
|
60
|
-
"@chat-adapter/slack": "^4.
|
|
61
|
-
"@chat-adapter/state-ioredis": "^4.
|
|
62
|
-
"@chat-adapter/state-memory": "^4.
|
|
63
|
-
"@chat-adapter/state-pg": "^4.
|
|
64
|
-
"@chat-adapter/state-redis": "^4.
|
|
65
|
-
"@chat-adapter/teams": "^4.
|
|
66
|
-
"@chat-adapter/telegram": "^4.
|
|
67
|
-
"@chat-adapter/web": "^4.
|
|
68
|
-
"@chat-adapter/whatsapp": "^4.
|
|
55
|
+
"@chat-adapter/discord": "^4.39.0",
|
|
56
|
+
"@chat-adapter/gchat": "^4.39.0",
|
|
57
|
+
"@chat-adapter/github": "^4.39.0",
|
|
58
|
+
"@chat-adapter/linear": "^4.39.0",
|
|
59
|
+
"@chat-adapter/messenger": "^4.39.0",
|
|
60
|
+
"@chat-adapter/slack": "^4.39.0",
|
|
61
|
+
"@chat-adapter/state-ioredis": "^4.39.0",
|
|
62
|
+
"@chat-adapter/state-memory": "^4.39.0",
|
|
63
|
+
"@chat-adapter/state-pg": "^4.39.0",
|
|
64
|
+
"@chat-adapter/state-redis": "^4.39.0",
|
|
65
|
+
"@chat-adapter/teams": "^4.39.0",
|
|
66
|
+
"@chat-adapter/telegram": "^4.39.0",
|
|
67
|
+
"@chat-adapter/web": "^4.39.0",
|
|
68
|
+
"@chat-adapter/whatsapp": "^4.39.0",
|
|
69
69
|
"@json-render/core": "^0.20.0",
|
|
70
70
|
"@json-render/react": "^0.20.0",
|
|
71
71
|
"@liveblocks/chat-sdk-adapter": "^3.24.1",
|
|
@@ -76,14 +76,14 @@
|
|
|
76
76
|
"@streamdown/math": "^1.0.2",
|
|
77
77
|
"@streamdown/mermaid": "^1.0.2",
|
|
78
78
|
"@tuturuuu/google": "0.1.0",
|
|
79
|
-
"@tuturuuu/internal-api": "0.
|
|
79
|
+
"@tuturuuu/internal-api": "0.33.1",
|
|
80
80
|
"@tuturuuu/supabase": "0.5.1",
|
|
81
|
-
"@tuturuuu/utils": "0.
|
|
82
|
-
"@vercel/sandbox": "^3.
|
|
81
|
+
"@tuturuuu/utils": "0.27.1",
|
|
82
|
+
"@vercel/sandbox": "^3.2.1",
|
|
83
83
|
"@zernio/chat-sdk-adapter": "^0.5.1",
|
|
84
|
-
"ai": "^7.0.
|
|
85
|
-
"bash-tool": "^1.3.
|
|
86
|
-
"chat": "^4.
|
|
84
|
+
"ai": "^7.0.91",
|
|
85
|
+
"bash-tool": "^1.3.19",
|
|
86
|
+
"chat": "^4.39.0",
|
|
87
87
|
"chat-adapter-baileys": "^2.1.0",
|
|
88
88
|
"chat-adapter-blooio": "^0.1.0",
|
|
89
89
|
"chat-adapter-imessage": "^1.1.0",
|
|
@@ -93,19 +93,19 @@
|
|
|
93
93
|
"chat-state-cloudflare-do": "^0.2.0",
|
|
94
94
|
"chat-state-mysql": "^0.1.0",
|
|
95
95
|
"dayjs": "^1.11.23",
|
|
96
|
-
"next": "^16.3.
|
|
96
|
+
"next": "^16.3.4",
|
|
97
97
|
"qrcode": "^1.5.4",
|
|
98
98
|
"react": "^19.2.8",
|
|
99
99
|
"react-dom": "^19.2.8",
|
|
100
|
-
"streamdown": "^2.
|
|
100
|
+
"streamdown": "^2.6.0",
|
|
101
101
|
"uuid": "^14.0.2",
|
|
102
102
|
"zca-js": "^2.1.2",
|
|
103
|
-
"zod": "^4.4
|
|
103
|
+
"zod": "^4.5.4"
|
|
104
104
|
},
|
|
105
105
|
"devDependencies": {
|
|
106
|
-
"@tuturuuu/types": "0.
|
|
106
|
+
"@tuturuuu/types": "0.28.0",
|
|
107
107
|
"@tuturuuu/typescript-config": "0.1.2",
|
|
108
|
-
"@types/node": "^26.
|
|
108
|
+
"@types/node": "^26.4.1",
|
|
109
109
|
"@types/qrcode": "^1.5.6",
|
|
110
110
|
"@types/react": "^19.2.18",
|
|
111
111
|
"typescript": "7.0.2"
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
withAiMemory,
|
|
18
18
|
} from '../../../memory';
|
|
19
19
|
import {
|
|
20
|
+
authorizeAiWorkspace,
|
|
20
21
|
isInternalTuturuuuAiUser,
|
|
21
22
|
resolveSupabaseSessionUser,
|
|
22
23
|
} from '../route-auth';
|
|
@@ -27,6 +28,13 @@ const AI_PROMPT = '\n\nAssistant:';
|
|
|
27
28
|
/** Always use a lightweight model for title generation */
|
|
28
29
|
const TITLE_MODEL = 'gemini-3.1-flash-lite';
|
|
29
30
|
const NewChatIdSchema = z.string().trim().pipe(z.uuid());
|
|
31
|
+
const NewChatBodySchema = z.object({
|
|
32
|
+
id: NewChatIdSchema.optional(),
|
|
33
|
+
isMiraMode: z.boolean().optional(),
|
|
34
|
+
message: z.string().optional(),
|
|
35
|
+
model: z.string().optional(),
|
|
36
|
+
wsId: z.string().optional(),
|
|
37
|
+
});
|
|
30
38
|
|
|
31
39
|
async function buildRateLimitResponse(
|
|
32
40
|
req: Request,
|
|
@@ -94,6 +102,7 @@ type CreatePostOptions = {
|
|
|
94
102
|
resolveGatewayAuth?: (request: Request) => Promise<GatewayAuthResolution>;
|
|
95
103
|
/** Gateway provider prefix for bare model names. Defaults to 'google'. */
|
|
96
104
|
defaultProvider?: string;
|
|
105
|
+
requireWorkspaceId?: boolean;
|
|
97
106
|
};
|
|
98
107
|
|
|
99
108
|
type ChatMessageInsertMode = 'direct' | 'rpc';
|
|
@@ -105,21 +114,17 @@ type AuthenticatedContext = GatewayAuthenticatedClient & {
|
|
|
105
114
|
export function createPOST(options: CreatePostOptions = {}) {
|
|
106
115
|
return async function handler(req: Request) {
|
|
107
116
|
try {
|
|
108
|
-
const requestBody = (await req.json())
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
: NewChatIdSchema.safeParse(requestBody.id);
|
|
118
|
-
if (!parsedId.success) {
|
|
119
|
-
return NextResponse.json('Invalid chat id', { status: 400 });
|
|
117
|
+
const requestBody = NewChatBodySchema.safeParse(await req.json());
|
|
118
|
+
if (!requestBody.success) {
|
|
119
|
+
const invalidId = requestBody.error.issues.some(
|
|
120
|
+
(issue) => issue.path[0] === 'id'
|
|
121
|
+
);
|
|
122
|
+
return NextResponse.json(
|
|
123
|
+
invalidId ? 'Invalid chat id' : 'Invalid request body',
|
|
124
|
+
{ status: 400 }
|
|
125
|
+
);
|
|
120
126
|
}
|
|
121
|
-
const {
|
|
122
|
-
const id = parsedId.data;
|
|
127
|
+
const { id, isMiraMode, message, model, wsId } = requestBody.data;
|
|
123
128
|
|
|
124
129
|
if (!message)
|
|
125
130
|
return NextResponse.json('No message provided', { status: 400 });
|
|
@@ -176,6 +181,13 @@ export function createPOST(options: CreatePostOptions = {}) {
|
|
|
176
181
|
|
|
177
182
|
const { messageInsertMode, supabase, user } = auth;
|
|
178
183
|
|
|
184
|
+
if (!wsId && options.requireWorkspaceId) {
|
|
185
|
+
return NextResponse.json(
|
|
186
|
+
{ error: 'Invalid workspace identifier' },
|
|
187
|
+
{ status: 422 }
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
179
191
|
if (isMiraMode) {
|
|
180
192
|
const allowed = await isInternalTuturuuuAiUser({
|
|
181
193
|
ok: true,
|
|
@@ -193,6 +205,25 @@ export function createPOST(options: CreatePostOptions = {}) {
|
|
|
193
205
|
}
|
|
194
206
|
}
|
|
195
207
|
|
|
208
|
+
let selectedWsId: string;
|
|
209
|
+
if (wsId) {
|
|
210
|
+
const workspaceAuthorization = await authorizeAiWorkspace({
|
|
211
|
+
request: req,
|
|
212
|
+
supabase,
|
|
213
|
+
userId: user.id,
|
|
214
|
+
wsId,
|
|
215
|
+
});
|
|
216
|
+
if (!workspaceAuthorization.ok) {
|
|
217
|
+
return workspaceAuthorization.response;
|
|
218
|
+
}
|
|
219
|
+
selectedWsId = workspaceAuthorization.wsId;
|
|
220
|
+
} else {
|
|
221
|
+
selectedWsId = await resolveAiMemoryWorkspaceIdForUser({
|
|
222
|
+
supabase,
|
|
223
|
+
userId: user.id,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
196
227
|
const prompt = buildPrompt([
|
|
197
228
|
{
|
|
198
229
|
id: 'initial-message',
|
|
@@ -201,11 +232,6 @@ export function createPOST(options: CreatePostOptions = {}) {
|
|
|
201
232
|
},
|
|
202
233
|
]);
|
|
203
234
|
|
|
204
|
-
const wsId = await resolveAiMemoryWorkspaceIdForUser({
|
|
205
|
-
supabase,
|
|
206
|
-
userId: user.id,
|
|
207
|
-
});
|
|
208
|
-
|
|
209
235
|
// Always use TITLE_MODEL for generating chat titles (cheap + fast)
|
|
210
236
|
const result = await generateText({
|
|
211
237
|
model: await withAiMemory({
|
|
@@ -216,7 +242,7 @@ export function createPOST(options: CreatePostOptions = {}) {
|
|
|
216
242
|
source: 'ai_chat_title',
|
|
217
243
|
surface: 'ai_chat_title',
|
|
218
244
|
userId: user.id,
|
|
219
|
-
wsId,
|
|
245
|
+
wsId: selectedWsId,
|
|
220
246
|
}),
|
|
221
247
|
prompt,
|
|
222
248
|
providerOptions: {
|
|
@@ -7,11 +7,21 @@ import {
|
|
|
7
7
|
validateAiTempAuthRequest,
|
|
8
8
|
} from '@tuturuuu/utils/ai-temp-auth';
|
|
9
9
|
import { isExactTuturuuuDotComEmail } from '@tuturuuu/utils/email/client';
|
|
10
|
+
import {
|
|
11
|
+
normalizeWorkspaceId,
|
|
12
|
+
verifyWorkspaceMembershipType,
|
|
13
|
+
WorkspaceAuthError,
|
|
14
|
+
WorkspaceNotFoundError,
|
|
15
|
+
} from '@tuturuuu/utils/workspace-helper';
|
|
10
16
|
import { NextResponse } from 'next/server';
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
|
|
19
|
+
const WorkspaceIdSchema = z.uuid();
|
|
11
20
|
|
|
12
21
|
export type AiRouteAuthResult =
|
|
13
22
|
| {
|
|
14
23
|
ok: true;
|
|
24
|
+
messageInsertMode?: 'direct' | 'rpc';
|
|
15
25
|
supabase: TypedSupabaseClient;
|
|
16
26
|
user: SupabaseUser;
|
|
17
27
|
tempAuthContext?: AiTempAuthContext;
|
|
@@ -23,6 +33,87 @@ export type AiRouteAuthResult =
|
|
|
23
33
|
|
|
24
34
|
export const resolveSupabaseSessionUser = resolveAuthenticatedSessionUser;
|
|
25
35
|
|
|
36
|
+
export async function authorizeAiWorkspace({
|
|
37
|
+
membershipClient,
|
|
38
|
+
request,
|
|
39
|
+
supabase,
|
|
40
|
+
userId,
|
|
41
|
+
wsId,
|
|
42
|
+
}: {
|
|
43
|
+
membershipClient?: TypedSupabaseClient;
|
|
44
|
+
request: Request;
|
|
45
|
+
supabase: TypedSupabaseClient;
|
|
46
|
+
userId: string;
|
|
47
|
+
wsId: string;
|
|
48
|
+
}): Promise<{ ok: true; wsId: string } | { ok: false; response: Response }> {
|
|
49
|
+
let normalizedWsId: string;
|
|
50
|
+
try {
|
|
51
|
+
normalizedWsId = await normalizeWorkspaceId(
|
|
52
|
+
wsId,
|
|
53
|
+
supabase,
|
|
54
|
+
request as never
|
|
55
|
+
);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(
|
|
58
|
+
'Workspace ID normalization failed:',
|
|
59
|
+
error instanceof Error ? error.message : error
|
|
60
|
+
);
|
|
61
|
+
const status =
|
|
62
|
+
error instanceof WorkspaceAuthError
|
|
63
|
+
? 401
|
|
64
|
+
: error instanceof WorkspaceNotFoundError
|
|
65
|
+
? 404
|
|
66
|
+
: 500;
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
response: NextResponse.json(
|
|
70
|
+
{ error: 'Unable to resolve workspace identifier' },
|
|
71
|
+
{ status }
|
|
72
|
+
),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!WorkspaceIdSchema.safeParse(normalizedWsId).success) {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
response: NextResponse.json(
|
|
80
|
+
{ error: 'Invalid workspace identifier' },
|
|
81
|
+
{ status: 422 }
|
|
82
|
+
),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const membership = await verifyWorkspaceMembershipType({
|
|
87
|
+
wsId: normalizedWsId,
|
|
88
|
+
userId,
|
|
89
|
+
supabase: membershipClient ?? supabase,
|
|
90
|
+
requiredType: 'MEMBER',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (membership.error === 'membership_lookup_failed') {
|
|
94
|
+
console.error('DB error checking workspace membership');
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
response: NextResponse.json(
|
|
98
|
+
{ error: 'Internal error verifying workspace access' },
|
|
99
|
+
{ status: 500 }
|
|
100
|
+
),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!membership.ok) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
response: NextResponse.json(
|
|
108
|
+
{ error: 'Workspace access denied' },
|
|
109
|
+
{ status: 403 }
|
|
110
|
+
),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { ok: true, wsId: normalizedWsId };
|
|
115
|
+
}
|
|
116
|
+
|
|
26
117
|
export async function resolveAiRouteAuth(
|
|
27
118
|
request: Request
|
|
28
119
|
): Promise<AiRouteAuthResult> {
|
package/src/chat/google/route.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
} from './request-persistence-lease';
|
|
40
40
|
import {
|
|
41
41
|
type AiRouteAuthResult,
|
|
42
|
+
authorizeAiWorkspace,
|
|
42
43
|
isInternalTuturuuuAiUser,
|
|
43
44
|
resolveAiRouteAuth,
|
|
44
45
|
} from './route-auth';
|
|
@@ -102,6 +103,7 @@ export function createPOST(
|
|
|
102
103
|
serverAPIKeyFallback?: boolean;
|
|
103
104
|
/** Gateway provider prefix for bare model names (e.g., 'openai', 'anthropic', 'vertex'). Defaults to 'google'. */
|
|
104
105
|
defaultProvider?: string;
|
|
106
|
+
requireWorkspaceId?: boolean;
|
|
105
107
|
resolveAuth?: (request: NextRequest) => Promise<AiRouteAuthResult | null>;
|
|
106
108
|
} = {}
|
|
107
109
|
) {
|
|
@@ -162,7 +164,7 @@ export function createPOST(
|
|
|
162
164
|
const auth =
|
|
163
165
|
(await _options.resolveAuth?.(req)) ?? (await resolveAiRouteAuth(req));
|
|
164
166
|
if (!auth.ok) return auth.response;
|
|
165
|
-
const { supabase, user } = auth;
|
|
167
|
+
const { messageInsertMode = 'rpc', supabase, user } = auth;
|
|
166
168
|
|
|
167
169
|
if (isMiraMode && !(await isInternalTuturuuuAiUser(auth))) {
|
|
168
170
|
return NextResponse.json(
|
|
@@ -171,13 +173,30 @@ export function createPOST(
|
|
|
171
173
|
);
|
|
172
174
|
}
|
|
173
175
|
|
|
174
|
-
// Normalize both workspace identifiers so slugs like 'personal' resolve to UUIDs.
|
|
175
176
|
let normalizedWsId: string | null = null;
|
|
176
177
|
let requestedCreditWsId: string | undefined;
|
|
178
|
+
if (!wsId && _options.requireWorkspaceId) {
|
|
179
|
+
return NextResponse.json(
|
|
180
|
+
{ error: 'Invalid workspace identifier' },
|
|
181
|
+
{ status: 422 }
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (wsId) {
|
|
186
|
+
const workspaceAuthorization = await authorizeAiWorkspace({
|
|
187
|
+
membershipClient: sbAdmin,
|
|
188
|
+
request: req,
|
|
189
|
+
supabase,
|
|
190
|
+
userId: user.id,
|
|
191
|
+
wsId,
|
|
192
|
+
});
|
|
193
|
+
if (!workspaceAuthorization.ok) {
|
|
194
|
+
return workspaceAuthorization.response;
|
|
195
|
+
}
|
|
196
|
+
normalizedWsId = workspaceAuthorization.wsId;
|
|
197
|
+
}
|
|
198
|
+
|
|
177
199
|
try {
|
|
178
|
-
normalizedWsId = wsId
|
|
179
|
-
? await normalizeWorkspaceId(wsId, supabase, req)
|
|
180
|
-
: null;
|
|
181
200
|
requestedCreditWsId = rawCreditWsId
|
|
182
201
|
? await normalizeWorkspaceId(rawCreditWsId, supabase, req)
|
|
183
202
|
: undefined;
|
|
@@ -192,30 +211,6 @@ export function createPOST(
|
|
|
192
211
|
);
|
|
193
212
|
}
|
|
194
213
|
|
|
195
|
-
if (normalizedWsId) {
|
|
196
|
-
const contextMembership = await verifyWorkspaceMembershipType({
|
|
197
|
-
wsId: normalizedWsId,
|
|
198
|
-
userId: user.id,
|
|
199
|
-
supabase: sbAdmin,
|
|
200
|
-
requiredType: 'MEMBER',
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
if (contextMembership.error === 'membership_lookup_failed') {
|
|
204
|
-
console.error('DB error checking workspace membership');
|
|
205
|
-
return NextResponse.json(
|
|
206
|
-
{ error: 'Internal error verifying workspace access' },
|
|
207
|
-
{ status: 500 }
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (!contextMembership.ok) {
|
|
212
|
-
return NextResponse.json(
|
|
213
|
-
{ error: 'Workspace access denied' },
|
|
214
|
-
{ status: 403 }
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
214
|
const requestedCreditSource: SharedCreditSource =
|
|
220
215
|
requestedCreditSourceRaw ?? 'workspace';
|
|
221
216
|
let billingWsId: string | null = normalizedWsId ?? null;
|
|
@@ -413,7 +408,17 @@ export function createPOST(
|
|
|
413
408
|
processedMessages,
|
|
414
409
|
chatId,
|
|
415
410
|
insertChatMessage: async (args) => {
|
|
416
|
-
if (persistenceRequestId) {
|
|
411
|
+
if (messageInsertMode === 'direct' || persistenceRequestId) {
|
|
412
|
+
if (!persistenceRequestId) {
|
|
413
|
+
const { error } = await supabase.from('ai_chat_messages').insert({
|
|
414
|
+
chat_id: chatId,
|
|
415
|
+
content: args.message,
|
|
416
|
+
creator_id: user.id,
|
|
417
|
+
metadata: { source: args.source },
|
|
418
|
+
role: 'USER',
|
|
419
|
+
});
|
|
420
|
+
return { error };
|
|
421
|
+
}
|
|
417
422
|
return persistRequestScopedUserMessage({
|
|
418
423
|
chatId,
|
|
419
424
|
content: args.message,
|
|
@@ -5,7 +5,11 @@ import {
|
|
|
5
5
|
resolveAiMemoryWorkspaceIdForUser,
|
|
6
6
|
withAiMemory,
|
|
7
7
|
} from '../../../memory';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
type AiRouteAuthResult,
|
|
10
|
+
authorizeAiWorkspace,
|
|
11
|
+
resolveAiRouteAuth,
|
|
12
|
+
} from '../route-auth';
|
|
9
13
|
|
|
10
14
|
const model = 'gemini-3.1-flash-lite';
|
|
11
15
|
|
|
@@ -15,19 +19,63 @@ type RawChatMessage = {
|
|
|
15
19
|
role: string;
|
|
16
20
|
};
|
|
17
21
|
|
|
18
|
-
export function createPATCH(
|
|
22
|
+
export function createPATCH(
|
|
23
|
+
options: {
|
|
24
|
+
requireWorkspaceId?: boolean;
|
|
25
|
+
resolveAuth?: (request: NextRequest) => Promise<AiRouteAuthResult | null>;
|
|
26
|
+
} = {}
|
|
27
|
+
) {
|
|
19
28
|
return async function handler(req: NextRequest) {
|
|
20
|
-
const { id } = (await req.json()) as {
|
|
29
|
+
const { id, wsId } = (await req.json()) as {
|
|
21
30
|
id?: string;
|
|
31
|
+
wsId?: string;
|
|
22
32
|
};
|
|
23
33
|
|
|
24
34
|
try {
|
|
25
35
|
if (!id) return new Response('Missing chat ID', { status: 400 });
|
|
26
36
|
|
|
27
|
-
const auth =
|
|
37
|
+
const auth =
|
|
38
|
+
(await options.resolveAuth?.(req)) ?? (await resolveAiRouteAuth(req));
|
|
28
39
|
if (!auth.ok) return auth.response;
|
|
29
40
|
const { supabase } = auth;
|
|
30
41
|
|
|
42
|
+
if (!wsId && options.requireWorkspaceId) {
|
|
43
|
+
return NextResponse.json(
|
|
44
|
+
{ error: 'Invalid workspace identifier' },
|
|
45
|
+
{ status: 422 }
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let selectedWsId: string;
|
|
50
|
+
if (wsId) {
|
|
51
|
+
const workspaceAuthorization = await authorizeAiWorkspace({
|
|
52
|
+
request: req,
|
|
53
|
+
supabase,
|
|
54
|
+
userId: auth.user.id,
|
|
55
|
+
wsId,
|
|
56
|
+
});
|
|
57
|
+
if (!workspaceAuthorization.ok) {
|
|
58
|
+
return workspaceAuthorization.response;
|
|
59
|
+
}
|
|
60
|
+
selectedWsId = workspaceAuthorization.wsId;
|
|
61
|
+
} else {
|
|
62
|
+
selectedWsId = await resolveAiMemoryWorkspaceIdForUser({
|
|
63
|
+
supabase,
|
|
64
|
+
userId: auth.user.id,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const { data: ownedChat, error: chatAccessError } = await supabase
|
|
69
|
+
.from('ai_chats')
|
|
70
|
+
.select('id')
|
|
71
|
+
.eq('id', id)
|
|
72
|
+
.eq('creator_id', auth.user.id)
|
|
73
|
+
.maybeSingle();
|
|
74
|
+
|
|
75
|
+
if (chatAccessError)
|
|
76
|
+
return new Response(chatAccessError.message, { status: 500 });
|
|
77
|
+
if (!ownedChat) return new Response('Chat not found', { status: 404 });
|
|
78
|
+
|
|
31
79
|
const { data: rawMessages, error: messagesError } = await supabase
|
|
32
80
|
.from('ai_chat_messages')
|
|
33
81
|
.select('id, content, role')
|
|
@@ -56,11 +104,6 @@ export function createPATCH() {
|
|
|
56
104
|
return new Response('Cannot summarize user message', { status: 400 });
|
|
57
105
|
|
|
58
106
|
const aiMessages = await convertToModelMessages(messages);
|
|
59
|
-
const wsId = await resolveAiMemoryWorkspaceIdForUser({
|
|
60
|
-
supabase,
|
|
61
|
-
userId: auth.user.id,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
107
|
const result = await generateText({
|
|
65
108
|
model: await withAiMemory({
|
|
66
109
|
addMemory: 'never',
|
|
@@ -70,7 +113,7 @@ export function createPATCH() {
|
|
|
70
113
|
source: 'ai_chat_summary',
|
|
71
114
|
surface: 'ai_chat_summary',
|
|
72
115
|
userId: auth.user.id,
|
|
73
|
-
wsId,
|
|
116
|
+
wsId: selectedWsId,
|
|
74
117
|
}),
|
|
75
118
|
messages: aiMessages,
|
|
76
119
|
system: systemInstruction,
|
|
@@ -105,15 +148,19 @@ export function createPATCH() {
|
|
|
105
148
|
if (!messages[messages.length - 1]?.id)
|
|
106
149
|
return new Response('Internal Server Error', { status: 500 });
|
|
107
150
|
|
|
108
|
-
const { error } = await supabase
|
|
151
|
+
const { data: updatedChat, error } = await supabase
|
|
109
152
|
.from('ai_chats')
|
|
110
153
|
.update({
|
|
111
154
|
latest_summarized_message_id: messages[messages.length - 1]?.id,
|
|
112
155
|
summary: completion,
|
|
113
156
|
})
|
|
114
|
-
.eq('id', id)
|
|
157
|
+
.eq('id', id)
|
|
158
|
+
.eq('creator_id', auth.user.id)
|
|
159
|
+
.select('id')
|
|
160
|
+
.maybeSingle();
|
|
115
161
|
|
|
116
162
|
if (error) return new Response(error.message, { status: 500 });
|
|
163
|
+
if (!updatedChat) return new Response('Chat not found', { status: 404 });
|
|
117
164
|
|
|
118
165
|
return new Response(JSON.stringify({ response: completion }), {
|
|
119
166
|
status: 200,
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { google } from '@ai-sdk/google';
|
|
2
|
-
import {
|
|
3
|
-
createAdminClient,
|
|
4
|
-
createClient,
|
|
5
|
-
} from '@tuturuuu/supabase/next/server';
|
|
6
|
-
import { Output, streamText } from 'ai';
|
|
7
|
-
import { NextResponse } from 'next/server';
|
|
8
|
-
import { withAiMemory } from '../../memory';
|
|
9
|
-
import { flashcardSchema } from '../types';
|
|
10
|
-
|
|
11
|
-
const DEFAULT_MODEL_NAME = 'gemini-3.1-flash-lite';
|
|
12
|
-
|
|
13
|
-
export async function POST(req: Request) {
|
|
14
|
-
const sbAdmin = await createAdminClient();
|
|
15
|
-
|
|
16
|
-
const { wsId, context } = (await req.json()) as {
|
|
17
|
-
wsId?: string;
|
|
18
|
-
context?: string;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
// if (!id) return new Response('Missing chat ID', { status: 400 });
|
|
23
|
-
if (!wsId) return new Response('Missing workspace ID', { status: 400 });
|
|
24
|
-
if (!context) return new Response('Missing context', { status: 400 });
|
|
25
|
-
|
|
26
|
-
// eslint-disable-next-line no-undef
|
|
27
|
-
const apiKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY;
|
|
28
|
-
if (!apiKey) return new Response('Missing API key', { status: 400 });
|
|
29
|
-
|
|
30
|
-
const supabase = await createClient();
|
|
31
|
-
|
|
32
|
-
const {
|
|
33
|
-
data: { user },
|
|
34
|
-
} = await supabase.auth.getUser();
|
|
35
|
-
|
|
36
|
-
if (!user) return new Response('Unauthorized', { status: 401 });
|
|
37
|
-
|
|
38
|
-
const { count, error } = await sbAdmin
|
|
39
|
-
.from('workspace_secrets')
|
|
40
|
-
.select('*', { count: 'exact', head: true })
|
|
41
|
-
.eq('ws_id', wsId)
|
|
42
|
-
.eq('name', 'ENABLE_CHAT')
|
|
43
|
-
.eq('value', 'true');
|
|
44
|
-
|
|
45
|
-
if (error) return new Response(error.message, { status: 500 });
|
|
46
|
-
if (count === 0)
|
|
47
|
-
return new Response('You are not allowed to use this feature.', {
|
|
48
|
-
status: 401,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
// let chatId = id;
|
|
52
|
-
|
|
53
|
-
// if (!chatId) {
|
|
54
|
-
// const { data, error } = await sbAdmin
|
|
55
|
-
// .from('ai_chats')
|
|
56
|
-
// .select('id')
|
|
57
|
-
// .eq('creator_id', user?.id)
|
|
58
|
-
// .order('created_at', { ascending: false })
|
|
59
|
-
// .limit(1)
|
|
60
|
-
// .single();
|
|
61
|
-
|
|
62
|
-
// if (error) return new Response(error.message, { status: 500 });
|
|
63
|
-
// if (!data) return new Response('Internal Server Error', { status: 500 });
|
|
64
|
-
|
|
65
|
-
// chatId = data.id;
|
|
66
|
-
// }
|
|
67
|
-
|
|
68
|
-
const result = streamText({
|
|
69
|
-
model: await withAiMemory({
|
|
70
|
-
customId: `flashcards-${Date.now()}`,
|
|
71
|
-
model: google(DEFAULT_MODEL_NAME),
|
|
72
|
-
product: 'education',
|
|
73
|
-
source: 'flashcard_generation',
|
|
74
|
-
surface: 'flashcard_generation',
|
|
75
|
-
userId: user.id,
|
|
76
|
-
wsId,
|
|
77
|
-
}),
|
|
78
|
-
prompt: `Generate 10 flashcards with the following context (in the same language as the provided context): ${context}`,
|
|
79
|
-
output: Output.object({ schema: flashcardSchema }),
|
|
80
|
-
providerOptions: {
|
|
81
|
-
google: {
|
|
82
|
-
safetySettings: [
|
|
83
|
-
{
|
|
84
|
-
category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
|
|
85
|
-
threshold: 'BLOCK_NONE',
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
category: 'HARM_CATEGORY_HATE_SPEECH',
|
|
89
|
-
threshold: 'BLOCK_NONE',
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
category: 'HARM_CATEGORY_HARASSMENT',
|
|
93
|
-
threshold: 'BLOCK_NONE',
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
|
|
97
|
-
threshold: 'BLOCK_NONE',
|
|
98
|
-
},
|
|
99
|
-
],
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
// onFinish: async (response) => {
|
|
103
|
-
// console.log('AI Response:', response);
|
|
104
|
-
// if (!response.object) {
|
|
105
|
-
// console.log('No content found');
|
|
106
|
-
// throw new Error('No content found');
|
|
107
|
-
// }
|
|
108
|
-
// const { error } = await sbAdmin.from('ai_chat_messages').insert({
|
|
109
|
-
// chat_id: chatId,
|
|
110
|
-
// creator_id: user.id,
|
|
111
|
-
// content: response.text,
|
|
112
|
-
// role: 'ASSISTANT',
|
|
113
|
-
// model: model.toLowerCase(),
|
|
114
|
-
// finish_reason: response.finishReason,
|
|
115
|
-
// prompt_tokens: response.usage.promptTokens,
|
|
116
|
-
// completion_tokens: response.usage.completionTokens,
|
|
117
|
-
// metadata: { source: 'Tuturuuu' },
|
|
118
|
-
// });
|
|
119
|
-
// if (error) {
|
|
120
|
-
// console.log('ERROR ORIGIN: ROOT COMPLETION');
|
|
121
|
-
// console.log(error);
|
|
122
|
-
// throw new Error(error.message);
|
|
123
|
-
// }
|
|
124
|
-
// console.log('AI Response saved to database');
|
|
125
|
-
// },
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
return result.toTextStreamResponse();
|
|
129
|
-
} catch (error) {
|
|
130
|
-
console.log(error);
|
|
131
|
-
return NextResponse.json(
|
|
132
|
-
{
|
|
133
|
-
message: `## Edge API Failure\nCould not complete the request. Please view the **Stack trace** below.\n\`\`\`bash\n${(error as Error)?.stack || 'No stack trace available'}`,
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
status: 500,
|
|
137
|
-
}
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { google } from '@ai-sdk/google';
|
|
2
|
-
import {
|
|
3
|
-
createAdminClient,
|
|
4
|
-
createClient,
|
|
5
|
-
} from '@tuturuuu/supabase/next/server';
|
|
6
|
-
import { Output, streamText } from 'ai';
|
|
7
|
-
import { NextResponse } from 'next/server';
|
|
8
|
-
import { withAiMemory } from '../../../memory';
|
|
9
|
-
import { quizOptionExplanationSchema } from '../../types';
|
|
10
|
-
|
|
11
|
-
const DEFAULT_MODEL_NAME = 'gemini-3.1-flash-lite';
|
|
12
|
-
|
|
13
|
-
export async function POST(req: Request) {
|
|
14
|
-
const sbAdmin = await createAdminClient();
|
|
15
|
-
|
|
16
|
-
const { wsId, question, option } = (await req.json()) as {
|
|
17
|
-
wsId?: string;
|
|
18
|
-
question?: string;
|
|
19
|
-
option?: {
|
|
20
|
-
is_correct?: boolean;
|
|
21
|
-
value?: string;
|
|
22
|
-
};
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
// if (!id) return new Response('Missing chat ID', { status: 400 });
|
|
27
|
-
if (!wsId) return new Response('Missing workspace ID', { status: 400 });
|
|
28
|
-
if (!question || !option)
|
|
29
|
-
return new Response('Missing context', { status: 400 });
|
|
30
|
-
|
|
31
|
-
// eslint-disable-next-line no-undef
|
|
32
|
-
const apiKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY;
|
|
33
|
-
if (!apiKey) return new Response('Missing API key', { status: 400 });
|
|
34
|
-
|
|
35
|
-
const supabase = await createClient();
|
|
36
|
-
|
|
37
|
-
const {
|
|
38
|
-
data: { user },
|
|
39
|
-
} = await supabase.auth.getUser();
|
|
40
|
-
|
|
41
|
-
if (!user) return new Response('Unauthorized', { status: 401 });
|
|
42
|
-
|
|
43
|
-
const { count, error } = await sbAdmin
|
|
44
|
-
.from('workspace_secrets')
|
|
45
|
-
.select('*', { count: 'exact', head: true })
|
|
46
|
-
.eq('ws_id', wsId)
|
|
47
|
-
.eq('name', 'ENABLE_CHAT')
|
|
48
|
-
.eq('value', 'true');
|
|
49
|
-
|
|
50
|
-
if (error) return new Response(error.message, { status: 500 });
|
|
51
|
-
if (count === 0)
|
|
52
|
-
return new Response('You are not allowed to use this feature.', {
|
|
53
|
-
status: 401,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// let chatId = id;
|
|
57
|
-
|
|
58
|
-
// if (!chatId) {
|
|
59
|
-
// const { data, error } = await sbAdmin
|
|
60
|
-
// .from('ai_chats')
|
|
61
|
-
// .select('id')
|
|
62
|
-
// .eq('creator_id', user?.id)
|
|
63
|
-
// .order('created_at', { ascending: false })
|
|
64
|
-
// .limit(1)
|
|
65
|
-
// .single();
|
|
66
|
-
|
|
67
|
-
// if (error) return new Response(error.message, { status: 500 });
|
|
68
|
-
// if (!data) return new Response('Internal Server Error', { status: 500 });
|
|
69
|
-
|
|
70
|
-
// chatId = data.id;
|
|
71
|
-
// }
|
|
72
|
-
|
|
73
|
-
const result = streamText({
|
|
74
|
-
model: await withAiMemory({
|
|
75
|
-
customId: `quiz-explanation-${Date.now()}`,
|
|
76
|
-
model: google(DEFAULT_MODEL_NAME),
|
|
77
|
-
product: 'education',
|
|
78
|
-
source: 'quiz_explanation',
|
|
79
|
-
surface: 'quiz_explanation',
|
|
80
|
-
userId: user.id,
|
|
81
|
-
wsId,
|
|
82
|
-
}),
|
|
83
|
-
prompt: `Generate an explanation with the following context: \n\n"""Question: ${question}""" \n\n"""Option: ${option.value}"""\n\nIs this option correct? ${option.is_correct ? 'Yes' : 'No'}\n\nNOTE: Provide it in the same language as the question and option, be concise and clear.`,
|
|
84
|
-
output: Output.object({ schema: quizOptionExplanationSchema }),
|
|
85
|
-
providerOptions: {
|
|
86
|
-
google: {
|
|
87
|
-
safetySettings: [
|
|
88
|
-
{
|
|
89
|
-
category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
|
|
90
|
-
threshold: 'BLOCK_NONE',
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
category: 'HARM_CATEGORY_HATE_SPEECH',
|
|
94
|
-
threshold: 'BLOCK_NONE',
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
category: 'HARM_CATEGORY_HARASSMENT',
|
|
98
|
-
threshold: 'BLOCK_NONE',
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
|
|
102
|
-
threshold: 'BLOCK_NONE',
|
|
103
|
-
},
|
|
104
|
-
],
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
// onFinish: async (response) => {
|
|
108
|
-
// console.log('AI Response:', response);
|
|
109
|
-
// if (!response.object) {
|
|
110
|
-
// console.log('No content found');
|
|
111
|
-
// throw new Error('No content found');
|
|
112
|
-
// }
|
|
113
|
-
// const { error } = await sbAdmin.from('ai_chat_messages').insert({
|
|
114
|
-
// chat_id: chatId,
|
|
115
|
-
// creator_id: user.id,
|
|
116
|
-
// content: response.text,
|
|
117
|
-
// role: 'ASSISTANT',
|
|
118
|
-
// model: model.toLowerCase(),
|
|
119
|
-
// finish_reason: response.finishReason,
|
|
120
|
-
// prompt_tokens: response.usage.promptTokens,
|
|
121
|
-
// completion_tokens: response.usage.completionTokens,
|
|
122
|
-
// metadata: { source: 'Tuturuuu' },
|
|
123
|
-
// });
|
|
124
|
-
// if (error) {
|
|
125
|
-
// console.log('ERROR ORIGIN: ROOT COMPLETION');
|
|
126
|
-
// console.log(error);
|
|
127
|
-
// throw new Error(error.message);
|
|
128
|
-
// }
|
|
129
|
-
// console.log('AI Response saved to database');
|
|
130
|
-
// },
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
return result.toTextStreamResponse();
|
|
134
|
-
} catch (error) {
|
|
135
|
-
console.log(error);
|
|
136
|
-
return NextResponse.json(
|
|
137
|
-
{
|
|
138
|
-
message: `## Edge API Failure\nCould not complete the request. Please view the **Stack trace** below.\n\`\`\`bash\n${(error as Error)?.stack || 'No stack trace available'}`,
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
status: 500,
|
|
142
|
-
}
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { google } from '@ai-sdk/google';
|
|
2
|
-
import {
|
|
3
|
-
createAdminClient,
|
|
4
|
-
createClient,
|
|
5
|
-
} from '@tuturuuu/supabase/next/server';
|
|
6
|
-
import { Output, streamText } from 'ai';
|
|
7
|
-
import { NextResponse } from 'next/server';
|
|
8
|
-
import { withAiMemory } from '../../memory';
|
|
9
|
-
import { quizSchema } from '../types';
|
|
10
|
-
|
|
11
|
-
const DEFAULT_MODEL_NAME = 'gemini-3.1-flash-lite';
|
|
12
|
-
|
|
13
|
-
export async function POST(req: Request) {
|
|
14
|
-
const sbAdmin = await createAdminClient();
|
|
15
|
-
|
|
16
|
-
const { wsId, context } = (await req.json()) as {
|
|
17
|
-
wsId?: string;
|
|
18
|
-
context?: string;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
// if (!id) return new Response('Missing chat ID', { status: 400 });
|
|
23
|
-
if (!wsId) return new Response('Missing workspace ID', { status: 400 });
|
|
24
|
-
if (!context) return new Response('Missing context', { status: 400 });
|
|
25
|
-
|
|
26
|
-
// eslint-disable-next-line no-undef
|
|
27
|
-
const apiKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY;
|
|
28
|
-
if (!apiKey) return new Response('Missing API key', { status: 400 });
|
|
29
|
-
|
|
30
|
-
const supabase = await createClient();
|
|
31
|
-
|
|
32
|
-
const {
|
|
33
|
-
data: { user },
|
|
34
|
-
} = await supabase.auth.getUser();
|
|
35
|
-
|
|
36
|
-
if (!user) return new Response('Unauthorized', { status: 401 });
|
|
37
|
-
|
|
38
|
-
const { count, error } = await sbAdmin
|
|
39
|
-
.from('workspace_secrets')
|
|
40
|
-
.select('*', { count: 'exact', head: true })
|
|
41
|
-
.eq('ws_id', wsId)
|
|
42
|
-
.eq('name', 'ENABLE_CHAT')
|
|
43
|
-
.eq('value', 'true');
|
|
44
|
-
|
|
45
|
-
if (error) return new Response(error.message, { status: 500 });
|
|
46
|
-
if (count === 0)
|
|
47
|
-
return new Response('You are not allowed to use this feature.', {
|
|
48
|
-
status: 401,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
// let chatId = id;
|
|
52
|
-
|
|
53
|
-
// if (!chatId) {
|
|
54
|
-
// const { data, error } = await sbAdmin
|
|
55
|
-
// .from('ai_chats')
|
|
56
|
-
// .select('id')
|
|
57
|
-
// .eq('creator_id', user?.id)
|
|
58
|
-
// .order('created_at', { ascending: false })
|
|
59
|
-
// .limit(1)
|
|
60
|
-
// .single();
|
|
61
|
-
|
|
62
|
-
// if (error) return new Response(error.message, { status: 500 });
|
|
63
|
-
// if (!data) return new Response('Internal Server Error', { status: 500 });
|
|
64
|
-
|
|
65
|
-
// chatId = data.id;
|
|
66
|
-
// }
|
|
67
|
-
|
|
68
|
-
const result = streamText({
|
|
69
|
-
model: await withAiMemory({
|
|
70
|
-
customId: `quiz-${Date.now()}`,
|
|
71
|
-
model: google(DEFAULT_MODEL_NAME),
|
|
72
|
-
product: 'education',
|
|
73
|
-
source: 'quiz_generation',
|
|
74
|
-
surface: 'quiz_generation',
|
|
75
|
-
userId: user.id,
|
|
76
|
-
wsId,
|
|
77
|
-
}),
|
|
78
|
-
prompt:
|
|
79
|
-
`Generate 10 quizzes with the following context (in the same language as the provided context): ` +
|
|
80
|
-
context,
|
|
81
|
-
output: Output.object({ schema: quizSchema }),
|
|
82
|
-
providerOptions: {
|
|
83
|
-
google: {
|
|
84
|
-
safetySettings: [
|
|
85
|
-
{
|
|
86
|
-
category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
|
|
87
|
-
threshold: 'BLOCK_NONE',
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
category: 'HARM_CATEGORY_HATE_SPEECH',
|
|
91
|
-
threshold: 'BLOCK_NONE',
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
category: 'HARM_CATEGORY_HARASSMENT',
|
|
95
|
-
threshold: 'BLOCK_NONE',
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
|
|
99
|
-
threshold: 'BLOCK_NONE',
|
|
100
|
-
},
|
|
101
|
-
],
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
// onFinish: async (response) => {
|
|
105
|
-
// console.log('AI Response:', response);
|
|
106
|
-
// if (!response.object) {
|
|
107
|
-
// console.log('No content found');
|
|
108
|
-
// throw new Error('No content found');
|
|
109
|
-
// }
|
|
110
|
-
// const { error } = await sbAdmin.from('ai_chat_messages').insert({
|
|
111
|
-
// chat_id: chatId,
|
|
112
|
-
// creator_id: user.id,
|
|
113
|
-
// content: response.text,
|
|
114
|
-
// role: 'ASSISTANT',
|
|
115
|
-
// model: model.toLowerCase(),
|
|
116
|
-
// finish_reason: response.finishReason,
|
|
117
|
-
// prompt_tokens: response.usage.promptTokens,
|
|
118
|
-
// completion_tokens: response.usage.completionTokens,
|
|
119
|
-
// metadata: { source: 'Tuturuuu' },
|
|
120
|
-
// });
|
|
121
|
-
// if (error) {
|
|
122
|
-
// console.log('ERROR ORIGIN: ROOT COMPLETION');
|
|
123
|
-
// console.log(error);
|
|
124
|
-
// throw new Error(error.message);
|
|
125
|
-
// }
|
|
126
|
-
// console.log('AI Response saved to database');
|
|
127
|
-
// },
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
return result.toTextStreamResponse();
|
|
131
|
-
} catch (error) {
|
|
132
|
-
console.log(error);
|
|
133
|
-
return NextResponse.json(
|
|
134
|
-
{
|
|
135
|
-
message: `## Edge API Failure\nCould not complete the request. Please view the **Stack trace** below.\n\`\`\`bash\n${(error as Error)?.stack || 'No stack trace available'}`,
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
status: 500,
|
|
139
|
-
}
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
}
|