diff --git a/backend/.env.example b/backend/.env.example index 1db370a9..c947ebd9 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -10,5 +10,6 @@ R2_BUCKET_NAME=mike GEMINI_API_KEY=your-gemini-key ANTHROPIC_API_KEY=your-anthropic-key +OPENAI_API_KEY=your-openai-key OPENROUTER_API_KEY=your-openrouter-key RESEND_API_KEY=your-resend-key diff --git a/backend/migrations/000_one_shot_schema.sql b/backend/migrations/000_one_shot_schema.sql index 80d563af..a7756d43 100644 --- a/backend/migrations/000_one_shot_schema.sql +++ b/backend/migrations/000_one_shot_schema.sql @@ -20,6 +20,7 @@ create table if not exists public.user_profiles ( tabular_model text not null default 'gemini-3-flash-preview', claude_api_key text, gemini_api_key text, + openai_api_key text, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); diff --git a/backend/migrations/001_add_openai_api_key.sql b/backend/migrations/001_add_openai_api_key.sql new file mode 100644 index 00000000..a5742353 --- /dev/null +++ b/backend/migrations/001_add_openai_api_key.sql @@ -0,0 +1,2 @@ +alter table public.user_profiles + add column if not exists openai_api_key text; diff --git a/backend/package-lock.json b/backend/package-lock.json index 86f82382..2852d57f 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -24,6 +24,7 @@ "libreoffice-convert": "^1.6.0", "mammoth": "^1.9.0", "multer": "^1.4.5-lts.2", + "openai": "^6.35.0", "pdfjs-dist": "^4.10.38", "resend": "^4.5.1" }, @@ -4135,6 +4136,27 @@ "node": ">= 0.8" } }, + "node_modules/openai": { + "version": "6.35.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.35.0.tgz", + "integrity": "sha512-L/skwIGnt5xQZHb0UfTu9uAUKbis3ehKypOuJKi20QvG7UStV6C8IC3myGYHcdiF4kms/bAvOJ9UqqNWqi8x/Q==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/option": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", diff --git a/backend/package.json b/backend/package.json index 50dfb585..3955db6e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -24,6 +24,7 @@ "libreoffice-convert": "^1.6.0", "mammoth": "^1.9.0", "multer": "^1.4.5-lts.2", + "openai": "^6.35.0", "pdfjs-dist": "^4.10.38", "resend": "^4.5.1" }, diff --git a/backend/src/lib/llm/index.ts b/backend/src/lib/llm/index.ts index 518ddc01..4b5e9793 100644 --- a/backend/src/lib/llm/index.ts +++ b/backend/src/lib/llm/index.ts @@ -1,5 +1,6 @@ import { streamClaude, completeClaudeText } from "./claude"; import { streamGemini, completeGeminiText } from "./gemini"; +import { streamOpenAI, completeOpenAIText } from "./openai"; import { providerForModel } from "./models"; import type { StreamChatParams, StreamChatResult, UserApiKeys } from "./types"; @@ -11,6 +12,7 @@ export async function streamChatWithTools( ): Promise { const provider = providerForModel(params.model); if (provider === "claude") return streamClaude(params); + if (provider === "openai") return streamOpenAI(params); return streamGemini(params); } @@ -23,5 +25,6 @@ export async function completeText(params: { }): Promise { const provider = providerForModel(params.model); if (provider === "claude") return completeClaudeText(params); + if (provider === "openai") return completeOpenAIText(params); return completeGeminiText(params); } diff --git a/backend/src/lib/llm/models.ts b/backend/src/lib/llm/models.ts index 52314007..0714e8c0 100644 --- a/backend/src/lib/llm/models.ts +++ b/backend/src/lib/llm/models.ts @@ -9,15 +9,18 @@ export const GEMINI_MAIN_MODELS = [ "gemini-3.1-pro-preview", "gemini-3-flash-preview", ] as const; +export const OPENAI_MAIN_MODELS = ["gpt-5.5"] as const; // Mid-tier (used for tabular review) — user picks one in account settings. export const CLAUDE_MID_MODELS = ["claude-sonnet-4-6"] as const; export const GEMINI_MID_MODELS = ["gemini-3-flash-preview"] as const; +export const OPENAI_MID_MODELS = ["gpt-5.4-nano"] as const; // Low-tier (used for title generation, lightweight extractions) — user picks // one in account settings. export const CLAUDE_LOW_MODELS = ["claude-haiku-4-5"] as const; export const GEMINI_LOW_MODELS = ["gemini-3.1-flash-lite-preview"] as const; +export const OPENAI_LOW_MODELS = ["gpt-5.4-nano"] as const; export const DEFAULT_MAIN_MODEL = "gemini-3-flash-preview"; export const DEFAULT_TITLE_MODEL = "gemini-3.1-flash-lite-preview"; @@ -26,10 +29,13 @@ export const DEFAULT_TABULAR_MODEL = "gemini-3-flash-preview"; const ALL_MODELS = new Set([ ...CLAUDE_MAIN_MODELS, ...GEMINI_MAIN_MODELS, + ...OPENAI_MAIN_MODELS, ...CLAUDE_MID_MODELS, ...GEMINI_MID_MODELS, + ...OPENAI_MID_MODELS, ...CLAUDE_LOW_MODELS, ...GEMINI_LOW_MODELS, + ...OPENAI_LOW_MODELS, ]); // --------------------------------------------------------------------------- @@ -39,6 +45,7 @@ const ALL_MODELS = new Set([ export function providerForModel(model: string): Provider { if (model.startsWith("claude")) return "claude"; if (model.startsWith("gemini")) return "gemini"; + if (model.startsWith("gpt-") || model.startsWith("o1-") || model.startsWith("o3-") || model.startsWith("o4-")) return "openai"; throw new Error(`Unknown model id: ${model}`); } diff --git a/backend/src/lib/llm/openai.ts b/backend/src/lib/llm/openai.ts new file mode 100644 index 00000000..296a1e1d --- /dev/null +++ b/backend/src/lib/llm/openai.ts @@ -0,0 +1,161 @@ +import OpenAI from "openai"; +import type { + StreamChatParams, + StreamChatResult, + NormalizedToolCall, +} from "./types"; + +function client(override?: string | null): OpenAI { + const apiKey = override?.trim() || process.env.OPENAI_API_KEY || ""; + return new OpenAI({ apiKey }); +} + +function toOpenAITools( + tools: StreamChatParams["tools"], +): OpenAI.ChatCompletionTool[] | undefined { + if (!tools?.length) return undefined; + return tools.map((t) => ({ + type: "function" as const, + function: { + name: t.function.name, + description: t.function.description, + parameters: t.function.parameters, + }, + })); +} + +export async function streamOpenAI( + params: StreamChatParams, +): Promise { + const { + model, + systemPrompt, + tools = [], + callbacks = {}, + runTools, + apiKeys, + } = params; + const maxIter = params.maxIterations ?? 10; + const openai = client(apiKeys?.openai); + const openaiTools = toOpenAITools(tools); + + const messages: OpenAI.ChatCompletionMessageParam[] = [ + { role: "system", content: systemPrompt }, + ...params.messages.map( + (m): OpenAI.ChatCompletionMessageParam => ({ + role: m.role, + content: m.content, + }), + ), + ]; + + let fullText = ""; + + for (let iter = 0; iter < maxIter; iter++) { + const stream = await openai.chat.completions.create({ + model, + messages, + tools: openaiTools, + stream: true, + }); + + const textParts: string[] = []; + const toolCalls: NormalizedToolCall[] = []; + const toolCallAccumulators: Map< + number, + { id: string; name: string; args: string } + > = new Map(); + + for await (const chunk of stream) { + const delta = chunk.choices[0]?.delta; + if (!delta) continue; + + if (delta.content) { + textParts.push(delta.content); + callbacks.onContentDelta?.(delta.content); + } + + if (delta.tool_calls) { + for (const tc of delta.tool_calls) { + const existing = toolCallAccumulators.get(tc.index); + if (existing) { + if (tc.function?.arguments) + existing.args += tc.function.arguments; + } else { + toolCallAccumulators.set(tc.index, { + id: tc.id ?? `tool-${tc.index}`, + name: tc.function?.name ?? "", + args: tc.function?.arguments ?? "", + }); + } + } + } + } + + for (const [, acc] of toolCallAccumulators) { + let input: Record = {}; + try { + input = JSON.parse(acc.args); + } catch {} + const call: NormalizedToolCall = { + id: acc.id, + name: acc.name, + input, + }; + callbacks.onToolCallStart?.(call); + toolCalls.push(call); + } + + fullText += textParts.join(""); + + if (!toolCalls.length || !runTools) { + break; + } + + const results = await runTools(toolCalls); + + messages.push({ + role: "assistant", + content: textParts.join("") || null, + tool_calls: toolCalls.map((tc) => ({ + id: tc.id, + type: "function" as const, + function: { + name: tc.name, + arguments: JSON.stringify(tc.input), + }, + })), + }); + + for (const r of results) { + messages.push({ + role: "tool", + tool_call_id: r.tool_use_id, + content: r.content, + }); + } + } + + return { fullText }; +} + +export async function completeOpenAIText(params: { + model: string; + systemPrompt?: string; + user: string; + maxTokens?: number; + apiKeys?: { openai?: string | null }; +}): Promise { + const openai = client(params.apiKeys?.openai); + const messages: OpenAI.ChatCompletionMessageParam[] = []; + if (params.systemPrompt) { + messages.push({ role: "system", content: params.systemPrompt }); + } + messages.push({ role: "user", content: params.user }); + const resp = await openai.chat.completions.create({ + model: params.model, + messages, + max_tokens: params.maxTokens ?? 512, + }); + return resp.choices[0]?.message?.content ?? ""; +} diff --git a/backend/src/lib/llm/types.ts b/backend/src/lib/llm/types.ts index 8cc411a7..a8409d80 100644 --- a/backend/src/lib/llm/types.ts +++ b/backend/src/lib/llm/types.ts @@ -2,7 +2,7 @@ // Callers always speak OpenAI-style tools + { role, content } messages; each // provider translates internally. -export type Provider = "claude" | "gemini"; +export type Provider = "claude" | "gemini" | "openai"; export type OpenAIToolSchema = { type: "function"; @@ -39,6 +39,7 @@ export type StreamCallbacks = { export type UserApiKeys = { claude?: string | null; gemini?: string | null; + openai?: string | null; }; export type StreamChatParams = { diff --git a/backend/src/lib/userSettings.ts b/backend/src/lib/userSettings.ts index c798b636..c97dfc8c 100644 --- a/backend/src/lib/userSettings.ts +++ b/backend/src/lib/userSettings.ts @@ -19,6 +19,7 @@ export type UserModelSettings = { function resolveTitleModel(apiKeys: UserApiKeys): string { if (apiKeys.gemini?.trim()) return DEFAULT_TITLE_MODEL; if (apiKeys.claude?.trim()) return "claude-haiku-4-5"; + if (apiKeys.openai?.trim()) return "gpt-5.4-nano"; return DEFAULT_TITLE_MODEL; } @@ -29,13 +30,14 @@ export async function getUserModelSettings( const client = db ?? createServerSupabase(); const { data } = await client .from("user_profiles") - .select("tabular_model, claude_api_key, gemini_api_key") + .select("tabular_model, claude_api_key, gemini_api_key, openai_api_key") .eq("user_id", userId) .single(); const api_keys: UserApiKeys = { claude: data?.claude_api_key ?? null, gemini: data?.gemini_api_key ?? null, + openai: data?.openai_api_key ?? null, }; return { @@ -52,11 +54,12 @@ export async function getUserApiKeys( const client = db ?? createServerSupabase(); const { data } = await client .from("user_profiles") - .select("claude_api_key, gemini_api_key") + .select("claude_api_key, gemini_api_key, openai_api_key") .eq("user_id", userId) .single(); return { claude: data?.claude_api_key ?? null, gemini: data?.gemini_api_key ?? null, + openai: data?.openai_api_key ?? null, }; } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5782999f..5f66b0fc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -33,7 +33,7 @@ "lucide-react": "^0.553.0", "mammoth": "^1.11.0", "marked": "^17.0.1", - "next": "16.0.3", + "next": "16.0.11", "nextjs-toploader": "^3.9.17", "pdfjs-dist": "4.10.38", "react": "19.2.0", @@ -59,7 +59,7 @@ "babel-plugin-react-compiler": "1.0.0", "baseline-browser-mapping": "^2.9.11", "eslint": "^9", - "eslint-config-next": "16.0.3", + "eslint-config-next": "16.0.11", "tailwindcss": "^4", "tw-animate-css": "^1.4.0", "typescript": "^5", @@ -1588,7 +1588,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1598,7 +1598,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1691,7 +1691,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -1705,7 +1705,6 @@ "version": "0.4.2", "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", "integrity": "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==", - "dev": true, "license": "MIT OR Apache-2.0", "engines": { "node": ">=18.0.0" @@ -1715,7 +1714,6 @@ "version": "2.16.0", "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.0.tgz", "integrity": "sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg==", - "dev": true, "license": "MIT OR Apache-2.0", "peerDependencies": { "unenv": "2.0.0-rc.24", @@ -1734,7 +1732,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1751,7 +1748,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1768,7 +1764,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1785,7 +1780,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1802,7 +1796,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1816,7 +1809,6 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" @@ -1829,7 +1821,6 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", @@ -1890,7 +1881,6 @@ "version": "1.9.2", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2251,7 +2241,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2604,7 +2593,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -2617,7 +2605,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2640,7 +2627,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2663,7 +2649,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2680,7 +2665,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2697,7 +2681,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2714,7 +2697,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2731,7 +2713,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2748,7 +2729,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2765,7 +2745,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2782,7 +2761,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2799,7 +2777,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2816,7 +2793,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2833,7 +2809,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2856,7 +2831,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2879,7 +2853,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2902,7 +2875,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2925,7 +2897,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2948,7 +2919,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2971,7 +2941,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2994,7 +2963,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -3017,7 +2985,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -3037,7 +3004,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -3057,7 +3023,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -3077,7 +3042,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -3419,15 +3383,15 @@ } }, "node_modules/@next/env": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.3.tgz", - "integrity": "sha512-IqgtY5Vwsm14mm/nmQaRMmywCU+yyMIYfk3/MHZ2ZTJvwVbBn3usZnjMi1GacrMVzVcAxJShTCpZlPs26EdEjQ==", + "version": "16.0.11", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.11.tgz", + "integrity": "sha512-hULMheQaOhFK1vAoFPigXca42LguwyLILtJKPRzpY1d+og6jk0YNAQVwLGNYYhWEMd2zj4gcIWSf1yC5PffqqA==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.3.tgz", - "integrity": "sha512-6sPWmZetzFWMsz7Dhuxsdmbu3fK+/AxKRtj7OB0/3OZAI2MHB/v2FeYh271LZ9abvnM1WIwWc/5umYjx0jo5sQ==", + "version": "16.0.11", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.11.tgz", + "integrity": "sha512-aqyGPoeZwo2+iVRq+c9RSHuzWF+P5WHo3PZNt6I/baP1JeL7sJk8ut3pJyTd/WKquQ3B6CHMLX6YrmB5Iug5DQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3435,9 +3399,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.3.tgz", - "integrity": "sha512-MOnbd92+OByu0p6QBAzq1ahVWzF6nyfiH07dQDez4/Nku7G249NjxDVyEfVhz8WkLiOEU+KFVnqtgcsfP2nLXg==", + "version": "16.0.11", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.11.tgz", + "integrity": "sha512-3G7Rx6m6tgLqkc3Ce3QY/Yrsx7nJF4ithdHfx70Jmzel8m2xpjnGRC+oB4UcCHvQwN0ZP5YsLJakwx/M0vWbSQ==", "cpu": [ "arm64" ], @@ -3451,9 +3415,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.3.tgz", - "integrity": "sha512-i70C4O1VmbTivYdRlk+5lj9xRc2BlK3oUikt3yJeHT1unL4LsNtN7UiOhVanFdc7vDAgZn1tV/9mQwMkWOJvHg==", + "version": "16.0.11", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.11.tgz", + "integrity": "sha512-poUTsYKRwuG+eApDngouEiN6AGcAMq8TAQYP8Nou7iMS7x6+q3dFhhyhgodIzTF9acsEINl4cIzMaM9XJor8kw==", "cpu": [ "x64" ], @@ -3467,12 +3431,15 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.3.tgz", - "integrity": "sha512-O88gCZ95sScwD00mn/AtalyCoykhhlokxH/wi1huFK+rmiP5LAYVs/i2ruk7xST6SuXN4NI5y4Xf5vepb2jf6A==", + "version": "16.0.11", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.11.tgz", + "integrity": "sha512-Q9shvB+eLNrK/n8w+/ZTWSzbEIzJ56mP83ZVaqmHay6/Ulcn6THEId4gxfYCXmSwEG/xPAtv58FBWeZkp36XUA==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3483,12 +3450,15 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.3.tgz", - "integrity": "sha512-CEErFt78S/zYXzFIiv18iQCbRbLgBluS8z1TNDQoyPi8/Jr5qhR3e8XHAIxVxPBjDbEMITprqELVc5KTfFj0gg==", + "version": "16.0.11", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.11.tgz", + "integrity": "sha512-rq+d/a0FZHVPEh3zismoQgfVkSIEzlTbNhD4Z8bToLMszUlggAh1D1syhJ4MHkYzXRszhjS2emy0PYXz7Uwttw==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3499,12 +3469,15 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.3.tgz", - "integrity": "sha512-Tc3i+nwt6mQ+Dwzcri/WNDj56iWdycGVh5YwwklleClzPzz7UpfaMw1ci7bLl6GRYMXhWDBfe707EXNjKtiswQ==", + "version": "16.0.11", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.11.tgz", + "integrity": "sha512-82Wroterii1p15O+ZF/DDsHPuxKptR1JGK+obgbAk13vrc3B/fTJ2qOOmdeoMwAQ15gb/9mN4LQl9+IzFje76Q==", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3515,12 +3488,15 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.3.tgz", - "integrity": "sha512-zTh03Z/5PBBPdTurgEtr6nY0vI9KR9Ifp/jZCcHlODzwVOEKcKRBtQIGrkc7izFgOMuXDEJBmirwpGqdM/ZixA==", + "version": "16.0.11", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.11.tgz", + "integrity": "sha512-YK9RoeZuHWBd+wHi5/7VLp6P5ZOldAjQfBjjtzcR4f14FNmwT0a3ozMMlG2txDxh53krAd5yOO601RbJxH0gCQ==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3531,9 +3507,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.3.tgz", - "integrity": "sha512-Jc1EHxtZovcJcg5zU43X3tuqzl/sS+CmLgjRP28ZT4vk869Ncm2NoF8qSTaL99gh6uOzgM99Shct06pSO6kA6g==", + "version": "16.0.11", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.11.tgz", + "integrity": "sha512-pcDMpSckekV8xj2SSKO8PaqaJhrmDx84zUNip0kOWsT/ERhhDpnWkr6KXMqRXVp2y5CW9pp4LwOFdtpt3rhRgw==", "cpu": [ "arm64" ], @@ -3547,9 +3523,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.3.tgz", - "integrity": "sha512-N7EJ6zbxgIYpI/sWNzpVKRMbfEGgsWuOIvzkML7wxAAZhPk1Msxuo/JDu1PKjWGrAoOLaZcIX5s+/pF5LIbBBg==", + "version": "16.0.11", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.11.tgz", + "integrity": "sha512-Zzo9NLLRzBSHw9zOGpER/gdc5rofZHLjR2OIUIfoBaN2Oo5zWRl43IF5rMSX2LX7MPLTx4Ww8+5lNHAhXgitnA==", "cpu": [ "x64" ], @@ -3923,7 +3899,6 @@ "version": "4.1.6", "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", - "dev": true, "license": "MIT", "dependencies": { "kleur": "^4.1.5" @@ -3933,7 +3908,6 @@ "version": "0.6.5", "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", - "dev": true, "license": "MIT", "dependencies": { "@poppinss/colors": "^4.1.5", @@ -3945,7 +3919,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", - "dev": true, "license": "MIT" }, "node_modules/@radix-ui/primitive": { @@ -4597,7 +4570,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -5328,7 +5300,6 @@ "version": "1.2.15", "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.15.tgz", "integrity": "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==", - "dev": true, "license": "CC0-1.0" }, "node_modules/@stablelib/base64": { @@ -6393,7 +6364,6 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -6403,7 +6373,6 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -7527,7 +7496,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/types": "^7.26.0" @@ -7643,7 +7612,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", - "dev": true, "license": "MIT" }, "node_modules/bluebird": { @@ -8316,7 +8284,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/d3-array": { @@ -8617,7 +8584,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -8863,7 +8829,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" @@ -9181,13 +9146,13 @@ } }, "node_modules/eslint-config-next": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.3.tgz", - "integrity": "sha512-5F6qDjcZldf0Y0ZbqvWvap9xzYUxyDf7/of37aeyhvkrQokj/4bT1JYWZdlWUr283aeVa+s52mPq9ogmGg+5dw==", + "version": "16.0.11", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.11.tgz", + "integrity": "sha512-FZOlXGIBvd9zcFlGl6WneiuazaNSjQod0QmAl/3BZlm8ZuDcj6T/7T+7eyCvg0/T3qDVEYGdSCGkxyS7hyHgtw==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.0.3", + "@next/eslint-plugin-next": "16.0.11", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", @@ -10103,7 +10068,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -11772,7 +11736,6 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -13369,7 +13332,6 @@ "version": "4.20260401.0", "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260401.0.tgz", "integrity": "sha512-lngHPzZFN9sxYG/mhzvnWiBMNVAN5MsO/7g32ttJ07rymtiK/ZBalODTKb8Od+BQdlU5DOR4CjVt9NydjnUyYg==", - "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "0.8.1", @@ -13390,7 +13352,6 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -13522,13 +13483,12 @@ } }, "node_modules/next": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/next/-/next-16.0.3.tgz", - "integrity": "sha512-Ka0/iNBblPFcIubTA1Jjh6gvwqfjrGq1Y2MTI5lbjeLIAfmC+p5bQmojpRZqgHHVu5cG4+qdIiwXiBSm/8lZ3w==", - "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.", + "version": "16.0.11", + "resolved": "https://registry.npmjs.org/next/-/next-16.0.11.tgz", + "integrity": "sha512-Xlo2aFWaoypPzXr4PFLSNmxrzNptlp+hgxnG9Y2THYvHrvmXIuHUyNAWO6Q+F4rm4/bmTOukprXEyF/j4qsC2A==", "license": "MIT", "dependencies": { - "@next/env": "16.0.3", + "@next/env": "16.0.11", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -13541,14 +13501,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.0.3", - "@next/swc-darwin-x64": "16.0.3", - "@next/swc-linux-arm64-gnu": "16.0.3", - "@next/swc-linux-arm64-musl": "16.0.3", - "@next/swc-linux-x64-gnu": "16.0.3", - "@next/swc-linux-x64-musl": "16.0.3", - "@next/swc-win32-arm64-msvc": "16.0.3", - "@next/swc-win32-x64-msvc": "16.0.3", + "@next/swc-darwin-arm64": "16.0.11", + "@next/swc-darwin-x64": "16.0.11", + "@next/swc-linux-arm64-gnu": "16.0.11", + "@next/swc-linux-arm64-musl": "16.0.11", + "@next/swc-linux-x64-gnu": "16.0.11", + "@next/swc-linux-x64-musl": "16.0.11", + "@next/swc-win32-arm64-msvc": "16.0.11", + "@next/swc-win32-x64-msvc": "16.0.11", "sharp": "^0.34.4" }, "peerDependencies": { @@ -14157,7 +14117,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, "license": "MIT" }, "node_modules/pdfjs-dist": { @@ -15598,7 +15557,6 @@ "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -15643,7 +15601,6 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -16122,7 +16079,6 @@ "version": "10.2.2", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -16607,7 +16563,6 @@ "version": "7.24.4", "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", - "dev": true, "license": "MIT", "engines": { "node": ">=20.18.1" @@ -16623,7 +16578,6 @@ "version": "2.0.0-rc.24", "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", - "dev": true, "license": "MIT", "dependencies": { "pathe": "^2.0.3" @@ -17174,7 +17128,6 @@ "version": "1.20260401.1", "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260401.1.tgz", "integrity": "sha512-mUYCd+ohaWJWF5nhDzxugWaAD/DM8Dw0ze3B7bu8JaA7S70+XQJXcvcvwE8C4qGcxSdCyqjsrFzqxKubECDwzg==", - "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "bin": { @@ -17195,7 +17148,6 @@ "version": "4.80.0", "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.80.0.tgz", "integrity": "sha512-2ZKF7uPeOZy65BGk3YfvqBCPo/xH1MrAlMmH9mVP+tCNBrTUMnwOHSj1HrZHgR8LttkAqhko0fGz+I4ax1rzyQ==", - "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", @@ -17233,7 +17185,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17250,7 +17201,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17267,7 +17217,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17284,7 +17233,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17301,7 +17249,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17318,7 +17265,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17335,7 +17281,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17352,7 +17297,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17369,7 +17313,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17386,7 +17329,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17403,7 +17345,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17420,7 +17361,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17437,7 +17377,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17454,7 +17393,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17471,7 +17409,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17488,7 +17425,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17505,7 +17441,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17522,7 +17457,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17539,7 +17473,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17556,7 +17489,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17573,7 +17505,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17590,7 +17521,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17607,7 +17537,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17624,7 +17553,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17641,7 +17569,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -17655,7 +17582,6 @@ "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -17871,7 +17797,6 @@ "version": "4.1.0-beta.10", "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", - "dev": true, "license": "MIT", "dependencies": { "@poppinss/colors": "^4.1.5", @@ -17885,7 +17810,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", - "dev": true, "license": "MIT", "dependencies": { "@poppinss/exception": "^1.2.2", diff --git a/frontend/package.json b/frontend/package.json index 520d74dc..adbee73a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,7 +38,7 @@ "lucide-react": "^0.553.0", "mammoth": "^1.11.0", "marked": "^17.0.1", - "next": "16.0.3", + "next": "16.0.11", "nextjs-toploader": "^3.9.17", "pdfjs-dist": "4.10.38", "react": "19.2.0", @@ -64,7 +64,7 @@ "babel-plugin-react-compiler": "1.0.0", "baseline-browser-mapping": "^2.9.11", "eslint": "^9", - "eslint-config-next": "16.0.3", + "eslint-config-next": "16.0.11", "tailwindcss": "^4", "tw-animate-css": "^1.4.0", "typescript": "^5", diff --git a/frontend/src/app/(pages)/account/models/page.tsx b/frontend/src/app/(pages)/account/models/page.tsx index cf3720ea..0ad941c5 100644 --- a/frontend/src/app/(pages)/account/models/page.tsx +++ b/frontend/src/app/(pages)/account/models/page.tsx @@ -44,6 +44,7 @@ export default function ModelsAndApiKeysPage() { apiKeys={{ claudeApiKey: profile?.claudeApiKey ?? null, geminiApiKey: profile?.geminiApiKey ?? null, + openaiApiKey: profile?.openaiApiKey ?? null, }} onChange={(id) => updateModelPreference("tabularModel", id) @@ -87,6 +88,14 @@ export default function ModelsAndApiKeysPage() { updateApiKey("gemini", value.trim() || null) } /> + + updateApiKey("openai", value.trim() || null) + } + /> @@ -100,12 +109,12 @@ function TabularModelDropdown({ }: { value: string; onChange: (id: string) => void; - apiKeys: { claudeApiKey: string | null; geminiApiKey: string | null }; + apiKeys: { claudeApiKey: string | null; geminiApiKey: string | null; openaiApiKey: string | null }; }) { const [isOpen, setIsOpen] = useState(false); const selected = MODELS.find((m) => m.id === value); const selectedAvailable = isModelAvailable(value, apiKeys); - const groups: ("Anthropic" | "Google")[] = ["Anthropic", "Google"]; + const groups: ("Anthropic" | "Google" | "OpenAI")[] = ["Anthropic", "Google", "OpenAI"]; return ( diff --git a/frontend/src/app/components/assistant/ChatInput.tsx b/frontend/src/app/components/assistant/ChatInput.tsx index 7f56192b..b97018b5 100644 --- a/frontend/src/app/components/assistant/ChatInput.tsx +++ b/frontend/src/app/components/assistant/ChatInput.tsx @@ -70,6 +70,7 @@ export const ChatInput = forwardRef(function ChatInput( const apiKeys = { claudeApiKey: profile?.claudeApiKey ?? null, geminiApiKey: profile?.geminiApiKey ?? null, + openaiApiKey: profile?.openaiApiKey ?? null, }; const textareaRef = useRef(null); const [docSelectorOpen, setDocSelectorOpen] = useState(false); diff --git a/frontend/src/app/components/assistant/ModelToggle.tsx b/frontend/src/app/components/assistant/ModelToggle.tsx index cc10d518..01936996 100644 --- a/frontend/src/app/components/assistant/ModelToggle.tsx +++ b/frontend/src/app/components/assistant/ModelToggle.tsx @@ -15,7 +15,7 @@ import { isModelAvailable } from "@/app/lib/modelAvailability"; export interface ModelOption { id: string; label: string; - group: "Anthropic" | "Google"; + group: "Anthropic" | "Google" | "OpenAI"; } export const MODELS: ModelOption[] = [ @@ -23,13 +23,15 @@ export const MODELS: ModelOption[] = [ { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", group: "Anthropic" }, { id: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro", group: "Google" }, { id: "gemini-3-flash-preview", label: "Gemini 3 Flash", group: "Google" }, + { id: "gpt-5.5", label: "GPT-5.5", group: "OpenAI" }, + { id: "gpt-5.4-nano", label: "GPT-5.4 Nano", group: "OpenAI" }, ]; export const DEFAULT_MODEL_ID = "gemini-3-flash-preview"; export const ALLOWED_MODEL_IDS = new Set(MODELS.map((m) => m.id)); -const GROUP_ORDER: ModelOption["group"][] = ["Anthropic", "Google"]; +const GROUP_ORDER: ModelOption["group"][] = ["Anthropic", "Google", "OpenAI"]; interface Props { value: string; @@ -37,6 +39,7 @@ interface Props { apiKeys?: { claudeApiKey: string | null; geminiApiKey: string | null; + openaiApiKey: string | null; }; } diff --git a/frontend/src/app/components/tabular/TRChatPanel.tsx b/frontend/src/app/components/tabular/TRChatPanel.tsx index 3522df3a..02d06df2 100644 --- a/frontend/src/app/components/tabular/TRChatPanel.tsx +++ b/frontend/src/app/components/tabular/TRChatPanel.tsx @@ -453,7 +453,7 @@ function TRChatInput({ onCancel: () => void; model: string; onModelChange: (id: string) => void; - apiKeys: { claudeApiKey: string | null; geminiApiKey: string | null }; + apiKeys: { claudeApiKey: string | null; geminiApiKey: string | null; openaiApiKey: string | null }; }) { const [value, setValue] = useState(""); const textareaRef = useRef(null); @@ -610,6 +610,7 @@ export function TRChatPanel({ const apiKeys = { claudeApiKey: profile?.claudeApiKey ?? null, geminiApiKey: profile?.geminiApiKey ?? null, + openaiApiKey: profile?.openaiApiKey ?? null, }; const currentModel = profile?.tabularModel ?? "gemini-3-flash-preview"; const [apiKeyModalProvider, setApiKeyModalProvider] = diff --git a/frontend/src/app/components/tabular/TabularReviewView.tsx b/frontend/src/app/components/tabular/TabularReviewView.tsx index af875899..cdf90a24 100644 --- a/frontend/src/app/components/tabular/TabularReviewView.tsx +++ b/frontend/src/app/components/tabular/TabularReviewView.tsx @@ -90,6 +90,7 @@ export function TRView({ reviewId, projectId }: Props) { const apiKeys = { claudeApiKey: profile?.claudeApiKey ?? null, geminiApiKey: profile?.geminiApiKey ?? null, + openaiApiKey: profile?.openaiApiKey ?? null, }; const tabularModel = profile?.tabularModel ?? "gemini-3-flash-preview"; diff --git a/frontend/src/app/lib/modelAvailability.ts b/frontend/src/app/lib/modelAvailability.ts index 933a8c2d..9bf6d1c9 100644 --- a/frontend/src/app/lib/modelAvailability.ts +++ b/frontend/src/app/lib/modelAvailability.ts @@ -1,39 +1,51 @@ import { MODELS, type ModelOption } from "../components/assistant/ModelToggle"; -export type ModelProvider = "claude" | "gemini"; +export type ModelProvider = "claude" | "gemini" | "openai"; + +export type ApiKeys = { + claudeApiKey: string | null; + geminiApiKey: string | null; + openaiApiKey: string | null; +}; export function getModelProvider(modelId: string): ModelProvider | null { const model = MODELS.find((m) => m.id === modelId); if (!model) return null; - return model.group === "Anthropic" ? "claude" : "gemini"; + if (model.group === "Anthropic") return "claude"; + if (model.group === "OpenAI") return "openai"; + return "gemini"; } export function isModelAvailable( modelId: string, - apiKeys: { claudeApiKey: string | null; geminiApiKey: string | null }, + apiKeys: ApiKeys, ): boolean { const provider = getModelProvider(modelId); if (!provider) return false; - return provider === "claude" - ? !!apiKeys.claudeApiKey?.trim() - : !!apiKeys.geminiApiKey?.trim(); + if (provider === "claude") return !!apiKeys.claudeApiKey?.trim(); + if (provider === "openai") return !!apiKeys.openaiApiKey?.trim(); + return !!apiKeys.geminiApiKey?.trim(); } export function isProviderAvailable( provider: ModelProvider, - apiKeys: { claudeApiKey: string | null; geminiApiKey: string | null }, + apiKeys: ApiKeys, ): boolean { - return provider === "claude" - ? !!apiKeys.claudeApiKey?.trim() - : !!apiKeys.geminiApiKey?.trim(); + if (provider === "claude") return !!apiKeys.claudeApiKey?.trim(); + if (provider === "openai") return !!apiKeys.openaiApiKey?.trim(); + return !!apiKeys.geminiApiKey?.trim(); } export function providerLabel(provider: ModelProvider): string { - return provider === "claude" ? "Anthropic (Claude)" : "Google (Gemini)"; + if (provider === "claude") return "Anthropic (Claude)"; + if (provider === "openai") return "OpenAI (GPT)"; + return "Google (Gemini)"; } export function modelGroupToProvider( group: ModelOption["group"], ): ModelProvider { - return group === "Anthropic" ? "claude" : "gemini"; + if (group === "Anthropic") return "claude"; + if (group === "OpenAI") return "openai"; + return "gemini"; } diff --git a/frontend/src/contexts/UserProfileContext.tsx b/frontend/src/contexts/UserProfileContext.tsx index 12061076..79a08644 100644 --- a/frontend/src/contexts/UserProfileContext.tsx +++ b/frontend/src/contexts/UserProfileContext.tsx @@ -21,6 +21,7 @@ interface UserProfile { tabularModel: string; claudeApiKey: string | null; geminiApiKey: string | null; + openaiApiKey: string | null; } interface UserProfileContextType { @@ -33,7 +34,7 @@ interface UserProfileContextType { value: string, ) => Promise; updateApiKey: ( - provider: "claude" | "gemini", + provider: "claude" | "gemini" | "openai", value: string | null, ) => Promise; reloadProfile: () => Promise; @@ -77,6 +78,7 @@ export function UserProfileProvider({ children }: { children: ReactNode }) { tabularModel: "gemini-3-flash-preview", claudeApiKey: null, geminiApiKey: null, + openaiApiKey: null, }); return; } @@ -111,6 +113,7 @@ export function UserProfileProvider({ children }: { children: ReactNode }) { data.tabular_model || "gemini-3-flash-preview", claudeApiKey: data.claude_api_key ?? null, geminiApiKey: data.gemini_api_key ?? null, + openaiApiKey: data.openai_api_key ?? null, }); // 2. Update database in background if needed @@ -148,6 +151,7 @@ export function UserProfileProvider({ children }: { children: ReactNode }) { tabularModel: "gemini-3-flash-preview", claudeApiKey: null, geminiApiKey: null, + openaiApiKey: null, }); } finally { setLoading(false); @@ -245,14 +249,22 @@ export function UserProfileProvider({ children }: { children: ReactNode }) { const updateApiKey = useCallback( async ( - provider: "claude" | "gemini", + provider: "claude" | "gemini" | "openai", value: string | null, ): Promise => { if (!user) return false; const dbField = - provider === "claude" ? "claude_api_key" : "gemini_api_key"; + provider === "claude" + ? "claude_api_key" + : provider === "openai" + ? "openai_api_key" + : "gemini_api_key"; const stateField = - provider === "claude" ? "claudeApiKey" : "geminiApiKey"; + provider === "claude" + ? "claudeApiKey" + : provider === "openai" + ? "openaiApiKey" + : "geminiApiKey"; const normalized = value?.trim() ? value.trim() : null; try { const { error } = await supabase