@tuturuuu/ai 0.8.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tuturuuu/ai",
3
3
  "license": "MIT",
4
- "version": "0.8.2",
4
+ "version": "0.8.4",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/tutur3u/platform",
@@ -22,68 +22,68 @@
22
22
  "type-check": "tsc --project tsconfig.typecheck.json"
23
23
  },
24
24
  "dependencies": {
25
- "@ai-sdk/amazon-bedrock": "^5.0.48",
26
- "@ai-sdk/anthropic": "^4.0.34",
27
- "@ai-sdk/azure": "^4.0.35",
28
- "@ai-sdk/cerebras": "^3.0.25",
29
- "@ai-sdk/cohere": "^4.0.23",
30
- "@ai-sdk/deepgram": "^3.0.23",
31
- "@ai-sdk/deepinfra": "^3.0.25",
32
- "@ai-sdk/deepseek": "^3.0.24",
33
- "@ai-sdk/elevenlabs": "^3.0.24",
34
- "@ai-sdk/fal": "^3.0.24",
35
- "@ai-sdk/fireworks": "^3.0.27",
36
- "@ai-sdk/gateway": "^4.0.44",
37
- "@ai-sdk/gladia": "^3.0.23",
38
- "@ai-sdk/google": "^4.0.37",
39
- "@ai-sdk/google-vertex": "^5.0.45",
40
- "@ai-sdk/groq": "^4.0.24",
41
- "@ai-sdk/hume": "^3.0.23",
42
- "@ai-sdk/lmnt": "^3.0.23",
43
- "@ai-sdk/luma": "^3.0.24",
44
- "@ai-sdk/mistral": "^4.0.25",
45
- "@ai-sdk/openai": "^4.0.34",
46
- "@ai-sdk/openai-compatible": "^3.0.25",
47
- "@ai-sdk/perplexity": "^4.0.25",
48
- "@ai-sdk/react": "^4.0.59",
49
- "@ai-sdk/replicate": "^3.0.24",
50
- "@ai-sdk/revai": "^3.0.23",
51
- "@ai-sdk/togetherai": "^3.0.26",
52
- "@ai-sdk/xai": "^4.0.31",
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.36.0",
56
- "@chat-adapter/gchat": "^4.36.0",
57
- "@chat-adapter/github": "^4.36.0",
58
- "@chat-adapter/linear": "^4.36.0",
59
- "@chat-adapter/messenger": "^4.36.0",
60
- "@chat-adapter/slack": "^4.36.0",
61
- "@chat-adapter/state-ioredis": "^4.36.0",
62
- "@chat-adapter/state-memory": "^4.36.0",
63
- "@chat-adapter/state-pg": "^4.36.0",
64
- "@chat-adapter/state-redis": "^4.36.0",
65
- "@chat-adapter/teams": "^4.36.0",
66
- "@chat-adapter/telegram": "^4.36.0",
67
- "@chat-adapter/web": "^4.36.0",
68
- "@chat-adapter/whatsapp": "^4.36.0",
69
- "@json-render/core": "^0.19.0",
70
- "@json-render/react": "^0.19.0",
71
- "@liveblocks/chat-sdk-adapter": "^3.23.1",
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
+ "@json-render/core": "^0.20.0",
70
+ "@json-render/react": "^0.20.0",
71
+ "@liveblocks/chat-sdk-adapter": "^3.24.1",
72
72
  "@octokit/rest": "^22.0.1",
73
- "@resend/chat-sdk-adapter": "^0.2.2",
73
+ "@resend/chat-sdk-adapter": "^0.3.0",
74
74
  "@streamdown/cjk": "^1.0.3",
75
75
  "@streamdown/code": "^1.1.1",
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.29.1",
80
- "@tuturuuu/supabase": "0.5.0",
81
- "@tuturuuu/utils": "0.24.2",
82
- "@vercel/sandbox": "^2.9.2",
83
- "@zernio/chat-sdk-adapter": "^0.5.0",
84
- "ai": "^7.0.56",
85
- "bash-tool": "^1.3.18",
86
- "chat": "^4.36.0",
79
+ "@tuturuuu/internal-api": "0.33.1",
80
+ "@tuturuuu/supabase": "0.5.1",
81
+ "@tuturuuu/utils": "0.27.1",
82
+ "@vercel/sandbox": "^3.2.1",
83
+ "@zernio/chat-sdk-adapter": "^0.5.1",
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",
@@ -92,20 +92,20 @@
92
92
  "chat-adapter-zalo": "^0.1.0",
93
93
  "chat-state-cloudflare-do": "^0.2.0",
94
94
  "chat-state-mysql": "^0.1.0",
95
- "dayjs": "^1.11.21",
96
- "next": "^16.3.0",
95
+ "dayjs": "^1.11.23",
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.5.0",
101
- "uuid": "^14.0.1",
100
+ "streamdown": "^2.6.0",
101
+ "uuid": "^14.0.2",
102
102
  "zca-js": "^2.1.2",
103
- "zod": "^4.4.3"
103
+ "zod": "^4.5.4"
104
104
  },
105
105
  "devDependencies": {
106
- "@tuturuuu/types": "0.27.0",
107
- "@tuturuuu/typescript-config": "0.1.1",
108
- "@types/node": "^26.1.2",
106
+ "@tuturuuu/types": "0.28.0",
107
+ "@tuturuuu/typescript-config": "0.1.2",
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"
@@ -113,5 +113,5 @@
113
113
  "exports": {
114
114
  "./*": "./src/*.ts"
115
115
  },
116
- "packageManager": "bun@1.3.14"
116
+ "packageManager": "bun@1.4.0"
117
117
  }
@@ -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()) as {
109
- id?: string;
110
- model?: string;
111
- message?: string;
112
- isMiraMode?: boolean;
113
- };
114
- const parsedId =
115
- requestBody.id === undefined
116
- ? { data: undefined, success: true as const }
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 { model, message, isMiraMode } = requestBody;
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> {
@@ -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 { resolveAiRouteAuth } from '../route-auth';
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 = await resolveAiRouteAuth(req);
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
- }