diff --git a/.env.example b/.env.example
deleted file mode 100644
index 61c6c81..0000000
--- a/.env.example
+++ /dev/null
@@ -1,3 +0,0 @@
-# OpenAI API Key
-# Get your key at: https://platform.openai.com/account/api-keys
-OPENAI_API_KEY=sk_your_api_key_here
\ No newline at end of file
diff --git a/README.md b/README.md
index 5250ae8..7c06ebb 100644
--- a/README.md
+++ b/README.md
@@ -37,27 +37,26 @@ Assertify is an intelligent testing assistant that analyzes your project descrip
- Next.js 14
- TypeScript
- Tailwind CSS
-- OpenAI API
+- Multi-LLM Support (OpenAI, Anthropic Claude, Google Gemini)
## Installation
1. Install dependencies: `npm install`
-2. Copy the environment file: `cp .env.example .env.local`
-3. Add your OpenAI API key to `.env.local`
-4. Start the dev server: `npm run dev`
-5. Open http://localhost:3000 in your browser
+2. Start the dev server: `npm run dev`
+3. Open http://localhost:3000 in your browser
+4. **Enter your API key in the UI**. Keys are stored in your browser's `sessionStorage` in plaintext for convenience; they are not encrypted or transmitted to our servers. Be aware that browser extensions or any cross-site scripting (XSS) vulnerability in this tab could potentially access this session data.
## How to Use
-1. Provide your project description from the landing page; the app automatically classifies the category.
-2. Answer the context questions (or skip) so the generator can tailor scenarios to your needs.
-3. Review generated tests on the results page, filter by type or priority, and inspect the suggested testing strategy and risk areas.
-4. Generate boilerplate code for your preferred frameworks or export the dataset as JSON/CSV.
-5. Manage settings at `/settings` to define default context, disable frameworks, and control boilerplate sample sizes.
+1. Select your preferred LLM provider (OpenAI, Anthropic, or Gemini) and model from the landing page or settings.
+2. Provide your project description from the landing page; the app automatically classifies the category.
+3. Answer the context questions (or skip) so the generator can tailor scenarios to your needs.
+4. Review generated tests on the results page, filter by type or priority, and inspect the suggested testing strategy and risk areas.
+5. Generate boilerplate code for your preferred frameworks or export the dataset as JSON/CSV.
+6. Manage settings at `/settings` to define default context, disable frameworks, and control boilerplate sample sizes.
## Future Improvements
-- Allow selecting different LLM providers and models per generation so teams can optimize for latency or cost.
- Offer more granular configuration for question generation (e.g., required question count, tone, or domain presets).
- Optimize large test suites by streaming responses and deduplicating similar scenarios before persistence.
- Provide deeper integrations with CI/CD by exporting ready-to-run suites or syncing with test management tools.
diff --git a/app/api/classify/route.ts b/app/api/classify/route.ts
index f75e3b7..6c6c854 100644
--- a/app/api/classify/route.ts
+++ b/app/api/classify/route.ts
@@ -1,9 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
-import { OpenAI } from "openai";
+import { createProvider, isValidProviderKey, normalizeProviderError } from "@/lib/llm";
export async function POST(req: NextRequest) {
try {
- const { projectDescription, apiKey } = await req.json();
+ const { projectDescription, apiKey, provider = "openai", model } = await req.json();
if (!projectDescription) {
return NextResponse.json({ error: "Project description required" }, { status: 400 });
@@ -13,9 +13,11 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: "API key required" }, { status: 400 });
}
- const openai = new OpenAI({
- apiKey: apiKey,
- });
+ if (!isValidProviderKey(provider)) {
+ return NextResponse.json({ error: `Unsupported provider: "${provider}"` }, { status: 400 });
+ }
+
+ const llm = createProvider(provider, apiKey, model);
const categories = [
"backend-api",
@@ -26,39 +28,38 @@ export async function POST(req: NextRequest) {
"data-pipeline",
];
- const message = await openai.chat.completions.create({
- model: "gpt-4",
- messages: [
- {
- role: "system",
- content: `You are a project classifier. Classify the given project description into one of these categories: ${categories.join(
- ", "
- )}. Respond with ONLY the category name, nothing else.`,
- },
- {
- role: "user",
- content: projectDescription,
- },
- ],
- });
+ const content = await llm.chatCompletion([
+ {
+ role: "system",
+ content: `You are a project classifier. Classify the given project description into one of these categories: ${categories.join(
+ ", "
+ )}. Respond with ONLY the category name, nothing else.`,
+ },
+ {
+ role: "user",
+ content: projectDescription,
+ },
+ ]);
- let category = (message.choices[0].message.content || "other").trim().toLowerCase();
+ let category = (content || "other").trim().toLowerCase();
if (!categories.includes(category)) {
category = "other";
}
return NextResponse.json({ category });
- } catch (error: any) {
+ } catch (error: unknown) {
console.error("Classification error:", error);
- if (error.status === 401) {
+ const normalized = normalizeProviderError(error);
+
+ if (normalized.isAuthError) {
return NextResponse.json({ error: "Unauthorized: Invalid API key" }, { status: 401 });
}
return NextResponse.json(
- { error: "Classification failed", details: String(error.message) },
- { status: 500 }
+ { error: "Classification failed", details: normalized.message },
+ { status: normalized.status }
);
}
}
diff --git a/app/api/generate-questions/route.ts b/app/api/generate-questions/route.ts
index 7fb4501..686193f 100644
--- a/app/api/generate-questions/route.ts
+++ b/app/api/generate-questions/route.ts
@@ -1,13 +1,14 @@
import { NextRequest, NextResponse } from "next/server";
-import { OpenAI } from "openai";
+import { createProvider, isValidProviderKey, normalizeProviderError } from "@/lib/llm";
export async function POST(req: NextRequest) {
try {
- const { projectDescription, category, apiKey } = await req.json();
+ const { projectDescription, category, apiKey, provider = "openai", model } = await req.json();
console.log("Received:", {
projectDescription: !!projectDescription,
category,
+ provider,
apiKey: !!apiKey,
});
@@ -22,9 +23,11 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: "API key required" }, { status: 400 });
}
- const openai = new OpenAI({
- apiKey: apiKey,
- });
+ if (!isValidProviderKey(provider)) {
+ return NextResponse.json({ error: `Unsupported provider: "${provider}"` }, { status: 400 });
+ }
+
+ const llm = createProvider(provider, apiKey, model);
const prompt = `You are a QA expert. Based on the following project description and category, generate exactly 10 specific, actionable questions that a developer should answer to help write comprehensive tests for this project.
@@ -42,9 +45,8 @@ Format your response as a JSON array of strings, like this:
Respond ONLY with the JSON array, no additional text or markdown.`;
- const message = await openai.chat.completions.create({
- model: "gpt-4",
- messages: [
+ const content = await llm.chatCompletion(
+ [
{
role: "system",
content:
@@ -55,10 +57,8 @@ Respond ONLY with the JSON array, no additional text or markdown.`;
content: prompt,
},
],
- temperature: 0.7,
- });
-
- const content = message.choices[0].message.content || "[]";
+ { temperature: 0.7 }
+ );
let questions;
try {
@@ -84,16 +84,18 @@ Respond ONLY with the JSON array, no additional text or markdown.`;
}
return NextResponse.json({ questions });
- } catch (error: any) {
+ } catch (error: unknown) {
console.error("Generate questions error:", error);
- if (error.status === 401) {
+ const normalized = normalizeProviderError(error);
+
+ if (normalized.isAuthError) {
return NextResponse.json({ error: "Unauthorized: Invalid API key" }, { status: 401 });
}
return NextResponse.json(
- { error: "Failed to generate questions", details: String(error.message) },
- { status: 500 }
+ { error: "Failed to generate questions", details: normalized.message },
+ { status: normalized.status }
);
}
}
diff --git a/app/api/generate/route.ts b/app/api/generate/route.ts
index 27fbf14..c087750 100644
--- a/app/api/generate/route.ts
+++ b/app/api/generate/route.ts
@@ -1,9 +1,16 @@
import { NextRequest, NextResponse } from "next/server";
-import { OpenAI } from "openai";
+import { createProvider, isValidProviderKey, normalizeProviderError } from "@/lib/llm";
export async function POST(req: NextRequest) {
try {
- const { projectDescription, category, answers, apiKey } = await req.json();
+ const {
+ projectDescription,
+ category,
+ answers,
+ apiKey,
+ provider = "openai",
+ model,
+ } = await req.json();
if (!projectDescription || !category || !answers) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
@@ -13,9 +20,11 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: "API key required" }, { status: 400 });
}
- const openai = new OpenAI({
- apiKey: apiKey,
- });
+ if (!isValidProviderKey(provider)) {
+ return NextResponse.json({ error: `Unsupported provider: "${provider}"` }, { status: 400 });
+ }
+
+ const llm = createProvider(provider, apiKey, model);
const prompt = `You are a QA expert. Generate comprehensive test cases for the following project:
@@ -52,9 +61,8 @@ Generate 12-15 diverse test cases covering:
Respond ONLY with valid JSON, no markdown or extra text.`;
- const message = await openai.chat.completions.create({
- model: "gpt-4",
- messages: [
+ const content = await llm.chatCompletion(
+ [
{
role: "system",
content:
@@ -65,10 +73,8 @@ Respond ONLY with valid JSON, no markdown or extra text.`;
content: prompt,
},
],
- temperature: 0.7,
- });
-
- const content = message.choices[0].message.content || "{}";
+ { temperature: 0.7 }
+ );
let parsedResponse;
try {
@@ -83,16 +89,18 @@ Respond ONLY with valid JSON, no markdown or extra text.`;
}
return NextResponse.json(parsedResponse);
- } catch (error: any) {
+ } catch (error: unknown) {
console.error("Generation error:", error);
- if (error.status === 401) {
+ const normalized = normalizeProviderError(error);
+
+ if (normalized.isAuthError) {
return NextResponse.json({ error: "Unauthorized: Invalid API key" }, { status: 401 });
}
return NextResponse.json(
- { error: "Test case generation failed", details: String(error.message) },
- { status: 500 }
+ { error: "Test case generation failed", details: normalized.message },
+ { status: normalized.status }
);
}
}
diff --git a/app/page.tsx b/app/page.tsx
index b7c0f2f..8898d29 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -10,6 +10,7 @@ import { buildAutoContext } from "@/lib/autoContext";
import { useSettings } from "@/components/SettingsProvider";
import { filterTestCasesBySettings } from "@/lib/testCaseUtils";
import { useThemeMode } from "@/components/ThemeProvider";
+import { PROVIDER_META, PROVIDER_KEYS } from "@/lib/settings";
export default function Home() {
const [input, setInput] = useState("");
@@ -25,7 +26,7 @@ export default function Home() {
const router = useRouter();
const { addToast } = useToast();
const { confirm } = useConfirmDialog();
- const { settings } = useSettings();
+ const { settings, updateSettings } = useSettings();
const { themeMode, themeLabel, toggleTheme, mounted } = useThemeMode();
useEffect(() => {
@@ -38,29 +39,35 @@ export default function Home() {
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
+ const providerMeta = PROVIDER_META[settings.provider];
+ const storageKeyName = `llm_api_key_${settings.provider}`;
+
useEffect(() => {
- const savedKey = localStorage.getItem("openai_api_key");
+ const savedKey = sessionStorage.getItem(storageKeyName);
if (savedKey) {
setApiKey(savedKey);
setApiKeySaved(true);
+ } else {
+ setApiKey("");
+ setApiKeySaved(false);
}
- }, []);
+ }, [storageKeyName]);
const handleSaveApiKey = () => {
if (!apiKey.trim()) {
- addToast({ message: "Please enter your OpenAI API key.", type: "error" });
+ addToast({ message: `Please enter your ${providerMeta.name} API key.`, type: "error" });
return;
}
- if (!apiKey.startsWith("sk-")) {
+ if (!providerMeta.validateKey(apiKey)) {
addToast({
- message: 'Invalid API key. It should start with "sk-".',
+ message: `Invalid API key format for ${providerMeta.name}.`,
type: "error",
});
return;
}
- localStorage.setItem("openai_api_key", apiKey);
+ sessionStorage.setItem(storageKeyName, apiKey);
setApiKeySaved(true);
setShowApiKeyInput(false);
addToast({
@@ -72,8 +79,7 @@ export default function Home() {
const handleRemoveApiKey = async () => {
const confirmed = await confirm({
title: "Remove API key?",
- description:
- "This will remove your saved OpenAI API key from this browser. You can add it again later.",
+ description: `This will remove your saved ${providerMeta.name} API key from this browser. You can add it again later.`,
confirmLabel: "Remove key",
variant: "danger",
});
@@ -82,7 +88,7 @@ export default function Home() {
return;
}
- localStorage.removeItem("openai_api_key");
+ sessionStorage.removeItem(storageKeyName);
setApiKey("");
setApiKeySaved(false);
addToast({ message: "API key removed.", type: "info" });
@@ -114,12 +120,15 @@ export default function Home() {
.filter(Boolean)
.join("\n\n");
try {
+ const currentApiKey = sessionStorage.getItem(storageKeyName);
const response = await fetch("/api/classify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
projectDescription: classificationPayload,
- apiKey: localStorage.getItem("openai_api_key"),
+ apiKey: currentApiKey,
+ provider: settings.provider,
+ model: settings.model,
}),
});
@@ -136,7 +145,7 @@ export default function Home() {
if (!response.ok) {
addToast({
- message: `Error: ${data.error || "Failed to classify project"}`,
+ message: `Error: ${data.error || "Failed to classify project"}${data.details ? ` (${data.details})` : ""}`,
type: "error",
});
return;
@@ -174,7 +183,9 @@ export default function Home() {
projectDescription: generationPayload,
category: normalizedCategory,
answers: autoContext,
- apiKey: localStorage.getItem("openai_api_key"),
+ apiKey: currentApiKey,
+ provider: settings.provider,
+ model: settings.model,
}),
});
@@ -268,13 +279,62 @@ export default function Home() {
)}
- {/* API Key Section */}
+ {/* Provider & API Key Section */}
+ {/* Provider selector */}
+
+
+ LLM Provider
+
+
+ {PROVIDER_KEYS.map((key) => {
+ const meta = PROVIDER_META[key];
+ const isActive = settings.provider === key;
+ return (
+ {
+ updateSettings({ ...settings, provider: key, model: meta.defaultModel });
+ setShowApiKeyInput(false);
+ }}
+ className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all border ${
+ isActive
+ ? "bg-blue-600 text-white border-blue-600 shadow-md"
+ : "bg-white dark:bg-slate-700 text-slate-700 dark:text-slate-200 border-slate-200 dark:border-slate-600 hover:border-blue-400"
+ }`}
+ >
+
+ {meta.name}
+
+ );
+ })}
+
+
+
+ {/* Model selector */}
+
+
+ Model
+
+ updateSettings({ ...settings, model: e.target.value })}
+ className="w-full p-2.5 border-2 border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-slate-700 dark:text-white text-sm"
+ >
+ {providerMeta.models.map((m) => (
+
+ {m}
+
+ ))}
+
+
+
+ {/* API key status / input */}
- OpenAI API Key
+ {providerMeta.name} API Key
{apiKeySaved ? (
@@ -284,7 +344,7 @@ export default function Home() {
) : (
- Please add your OpenAI API key to continue
+ Please add your {providerMeta.name} API key to continue
)}
@@ -299,25 +359,26 @@ export default function Home() {
{showApiKeyInput && (
- Get your free API key at{" "}
+ Get your API key at{" "}
- platform.openai.com/account/api-keys
+ {providerMeta.keyHelpLabel}
setApiKey(e.target.value)}
- placeholder="sk-proj-..."
+ placeholder={providerMeta.keyPlaceholder}
className="w-full p-3 border-2 border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-slate-700 dark:text-white text-sm"
/>
- Your API key is stored locally in your browser only. Never shared with us.
+ Your API key is kept locally for the current session. Note: Browser storage is not
+ encrypted.
{
const newAnswers = [...answers];
@@ -93,7 +96,8 @@ export default function QuestionsPage() {
setLoading(true);
try {
- const apiKey = localStorage.getItem("openai_api_key");
+ const storageKeyName = `llm_api_key_${settings.provider}`;
+ const apiKey = sessionStorage.getItem(storageKeyName);
const supplementalContext: string[] = [];
if (projectRequirements.trim()) {
@@ -111,6 +115,8 @@ export default function QuestionsPage() {
category,
answers: [...answers, ...supplementalContext],
apiKey: apiKey,
+ provider: settings.provider,
+ model: settings.model,
}),
});
diff --git a/app/settings/page.tsx b/app/settings/page.tsx
index 7c490c4..440bb68 100644
--- a/app/settings/page.tsx
+++ b/app/settings/page.tsx
@@ -4,7 +4,13 @@ import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useSettings } from "@/components/SettingsProvider";
import { useToast } from "@/components/ToastProvider";
-import { boilerplateOptions, defaultSettings, testTypeOptions } from "@/lib/settings";
+import {
+ boilerplateOptions,
+ defaultSettings,
+ testTypeOptions,
+ PROVIDER_META,
+ PROVIDER_KEYS,
+} from "@/lib/settings";
export default function SettingsPage() {
const router = useRouter();
@@ -46,6 +52,56 @@ export default function SettingsPage() {
+
+ LLM Provider
+
+ Choose which AI provider and model to use for test generation.
+
+
+ {PROVIDER_KEYS.map((key) => {
+ const meta = PROVIDER_META[key];
+ const isActive = formState.provider === key;
+ return (
+
+ setFormState((prev) => ({
+ ...prev,
+ provider: key,
+ model: meta.defaultModel,
+ }))
+ }
+ className={`flex items-center justify-center gap-2 rounded-2xl border p-4 text-sm font-medium transition-all ${
+ isActive
+ ? "border-blue-500 bg-blue-50 text-blue-700 shadow-md dark:border-blue-400 dark:bg-blue-900/30 dark:text-blue-300"
+ : "border-slate-200 bg-white/70 text-slate-700 shadow-sm hover:border-blue-400 dark:border-slate-700 dark:bg-slate-900/40 dark:text-slate-200"
+ }`}
+ >
+
+ {meta.name}
+
+ );
+ })}
+
+
+
+ Model
+
+ setFormState((prev) => ({ ...prev, model: e.target.value }))}
+ className="w-full rounded-2xl border border-slate-200 bg-slate-50 p-3 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-slate-700 dark:bg-slate-900/40 dark:text-white"
+ >
+ {PROVIDER_META[formState.provider].models.map((m) => (
+
+ {m}
+
+ ))}
+
+
+
+
Default context
diff --git a/lib/llm/anthropic.ts b/lib/llm/anthropic.ts
new file mode 100644
index 0000000..3ec4164
--- /dev/null
+++ b/lib/llm/anthropic.ts
@@ -0,0 +1,31 @@
+import Anthropic from "@anthropic-ai/sdk";
+import type { ChatMessage, CompletionOptions, LLMProvider } from "./types";
+
+export class AnthropicProvider implements LLMProvider {
+ private client: Anthropic;
+ private defaultModel: string;
+
+ constructor(apiKey: string, defaultModel = "claude-3-5-sonnet-latest") {
+ this.client = new Anthropic({ apiKey });
+ this.defaultModel = defaultModel;
+ }
+
+ async chatCompletion(messages: ChatMessage[], options?: CompletionOptions): Promise {
+ // Anthropic requires the system message to be passed separately
+ const systemMessage = messages.find((m) => m.role === "system");
+ const nonSystemMessages = messages.filter((m) => m.role !== "system");
+
+ const response = await this.client.messages.create({
+ model: options?.model ?? this.defaultModel,
+ max_tokens: 4096,
+ system: systemMessage?.content,
+ messages: nonSystemMessages.map((m) => ({
+ role: m.role as "user" | "assistant",
+ content: m.content,
+ })),
+ });
+
+ const textBlock = response.content.find((block) => block.type === "text");
+ return textBlock && textBlock.type === "text" ? textBlock.text : "";
+ }
+}
diff --git a/lib/llm/errors.ts b/lib/llm/errors.ts
new file mode 100644
index 0000000..210840d
--- /dev/null
+++ b/lib/llm/errors.ts
@@ -0,0 +1,52 @@
+export interface NormalizedLLMError {
+ status: number;
+ message: string;
+ isAuthError: boolean;
+}
+
+const AUTH_ERROR_PATTERNS = [
+ "401",
+ "unauthorized",
+ "invalid api key",
+ "invalid x-goog-api-key",
+ "api key not valid",
+ "authentication",
+ "permission denied",
+ "api_key_invalid",
+];
+
+export function normalizeProviderError(error: unknown): NormalizedLLMError {
+ const err = error as Record;
+
+ const status =
+ typeof err?.status === "number"
+ ? err.status
+ : typeof err?.statusCode === "number"
+ ? err.statusCode
+ : 500;
+
+ let message = typeof err?.message === "string" ? err.message : String(error);
+
+ // Sometimes SDKs return a JSON payload embedded in the error string.
+ try {
+ const jsonStart = message.indexOf("{");
+ const jsonEnd = message.lastIndexOf("}");
+ if (jsonStart !== -1 && jsonEnd > jsonStart) {
+ const parsed = JSON.parse(message.substring(jsonStart, jsonEnd + 1));
+ if (parsed.error && typeof parsed.error.message === "string") {
+ message = parsed.error.message;
+ } else if (typeof parsed.message === "string") {
+ message = parsed.message;
+ }
+ }
+ } catch {
+ // Ignore parsing errors, stick to the original message
+ }
+
+ const isAuthError =
+ status === 401 ||
+ status === 403 ||
+ AUTH_ERROR_PATTERNS.some((pattern) => message.toLowerCase().includes(pattern));
+
+ return { status: isAuthError ? 401 : status, message, isAuthError };
+}
diff --git a/lib/llm/gemini.ts b/lib/llm/gemini.ts
new file mode 100644
index 0000000..2f0daa8
--- /dev/null
+++ b/lib/llm/gemini.ts
@@ -0,0 +1,62 @@
+import { GoogleGenerativeAI } from "@google/generative-ai";
+import type { ChatMessage, CompletionOptions, LLMProvider } from "./types";
+
+export class GeminiProvider implements LLMProvider {
+ private genAI: GoogleGenerativeAI;
+ private defaultModel: string;
+
+ constructor(apiKey: string, defaultModel = "gemini-2.5-flash") {
+ this.genAI = new GoogleGenerativeAI(apiKey);
+ this.defaultModel = defaultModel;
+ }
+
+ async chatCompletion(messages: ChatMessage[], options?: CompletionOptions): Promise {
+ const modelName = options?.model ?? this.defaultModel;
+ const systemMessage = messages.find((m) => m.role === "system");
+ const nonSystemMessages = messages.filter((m) => m.role !== "system");
+
+ const model = this.genAI.getGenerativeModel(
+ {
+ model: modelName,
+ generationConfig: {
+ temperature: options?.temperature ?? 0.7,
+ },
+ },
+ { apiVersion: "v1beta" }
+ );
+
+ if (nonSystemMessages.length === 0) {
+ throw new Error("At least one non-system message is required");
+ }
+
+ try {
+ // Prepend system message content to the first message if it exists
+ let firstMessageContent = nonSystemMessages[0].content;
+ if (systemMessage) {
+ firstMessageContent = `[SYSTEM INSTRUCTION]\n${systemMessage.content}\n\n[USER INPUT]\n${firstMessageContent}`;
+ }
+
+ // Use simpler generateContent for single-turn requests
+ if (nonSystemMessages.length === 1) {
+ const result = await model.generateContent(firstMessageContent);
+ return result.response.text();
+ }
+
+ // Use chat for multi-turn requests
+ const chat = model.startChat({
+ history: nonSystemMessages.slice(0, -1).map((m, index) => ({
+ role: m.role === "assistant" ? "model" : "user",
+ parts: [{ text: index === 0 ? firstMessageContent : m.content }],
+ })),
+ });
+
+ const lastMessage = nonSystemMessages[nonSystemMessages.length - 1];
+ const result = await chat.sendMessage(lastMessage.content);
+ return result.response.text();
+ } catch (error: any) {
+ console.error("Gemini API Error:", error);
+ // Re-throw to be caught by the route handler
+ throw error;
+ }
+ }
+}
diff --git a/lib/llm/index.ts b/lib/llm/index.ts
new file mode 100644
index 0000000..af2125a
--- /dev/null
+++ b/lib/llm/index.ts
@@ -0,0 +1,41 @@
+import type { LLMProvider, ProviderKey } from "./types";
+import { OpenAIProvider } from "./openai";
+import { AnthropicProvider } from "./anthropic";
+import { GeminiProvider } from "./gemini";
+
+export type {
+ LLMProvider,
+ ChatMessage,
+ CompletionOptions,
+ ProviderKey,
+ ProviderMeta,
+} from "./types";
+import { PROVIDER_META, PROVIDER_KEYS, isValidProviderKey } from "./types";
+export { PROVIDER_META, PROVIDER_KEYS, isValidProviderKey };
+export { normalizeProviderError } from "./errors";
+export type { NormalizedLLMError } from "./errors";
+
+export function createProvider(
+ providerKey: ProviderKey,
+ apiKey: string,
+ model?: string
+): LLMProvider {
+ const meta = PROVIDER_META[providerKey];
+ if (!meta) {
+ throw new Error(`Unsupported LLM provider: ${providerKey}`);
+ }
+
+ const resolvedModel = model || meta.defaultModel;
+ if (!meta.models.includes(resolvedModel)) {
+ throw new Error(`Unsupported model '${resolvedModel}' for provider '${providerKey}'`);
+ }
+
+ switch (providerKey) {
+ case "openai":
+ return new OpenAIProvider(apiKey, resolvedModel);
+ case "anthropic":
+ return new AnthropicProvider(apiKey, resolvedModel);
+ case "gemini":
+ return new GeminiProvider(apiKey, resolvedModel);
+ }
+}
diff --git a/lib/llm/openai.ts b/lib/llm/openai.ts
new file mode 100644
index 0000000..585c75a
--- /dev/null
+++ b/lib/llm/openai.ts
@@ -0,0 +1,22 @@
+import { OpenAI } from "openai";
+import type { ChatMessage, CompletionOptions, LLMProvider } from "./types";
+
+export class OpenAIProvider implements LLMProvider {
+ private client: OpenAI;
+ private defaultModel: string;
+
+ constructor(apiKey: string, defaultModel = "gpt-4o-mini") {
+ this.client = new OpenAI({ apiKey });
+ this.defaultModel = defaultModel;
+ }
+
+ async chatCompletion(messages: ChatMessage[], options?: CompletionOptions): Promise {
+ const response = await this.client.chat.completions.create({
+ model: options?.model ?? this.defaultModel,
+ messages,
+ temperature: options?.temperature ?? 0.7,
+ });
+
+ return response.choices[0].message.content || "";
+ }
+}
diff --git a/lib/llm/types.ts b/lib/llm/types.ts
new file mode 100644
index 0000000..3aeab26
--- /dev/null
+++ b/lib/llm/types.ts
@@ -0,0 +1,75 @@
+export interface ChatMessage {
+ role: "system" | "user" | "assistant";
+ content: string;
+}
+
+export interface CompletionOptions {
+ model?: string;
+ temperature?: number;
+}
+
+export interface LLMProvider {
+ chatCompletion(messages: ChatMessage[], options?: CompletionOptions): Promise;
+}
+
+export type ProviderKey = "openai" | "anthropic" | "gemini";
+
+export interface ProviderMeta {
+ key: ProviderKey;
+ name: string;
+ icon: string;
+ keyPlaceholder: string;
+ keyHelpUrl: string;
+ keyHelpLabel: string;
+ defaultModel: string;
+ models: string[];
+ validateKey: (key: string) => boolean;
+}
+
+export const PROVIDER_META: Record = {
+ openai: {
+ key: "openai",
+ name: "OpenAI",
+ icon: "fa-robot",
+ keyPlaceholder: "sk-proj-...",
+ keyHelpUrl: "https://platform.openai.com/account/api-keys",
+ keyHelpLabel: "platform.openai.com/account/api-keys",
+ defaultModel: "gpt-4o-mini",
+ models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-4", "gpt-3.5-turbo"],
+ validateKey: (key: string) => key.startsWith("sk-"),
+ },
+ anthropic: {
+ key: "anthropic",
+ name: "Anthropic",
+ icon: "fa-brain",
+ keyPlaceholder: "sk-ant-...",
+ keyHelpUrl: "https://console.anthropic.com/settings/keys",
+ keyHelpLabel: "console.anthropic.com/settings/keys",
+ defaultModel: "claude-3-5-sonnet-latest",
+ models: [
+ "claude-3-5-sonnet-latest",
+ "claude-3-5-haiku-latest",
+ "claude-3-opus-latest",
+ "claude-3-sonnet-20240229",
+ "claude-3-haiku-20240307",
+ ],
+ validateKey: (key: string) => key.startsWith("sk-ant-"),
+ },
+ gemini: {
+ key: "gemini",
+ name: "Google Gemini",
+ icon: "fa-gem",
+ keyPlaceholder: "AIza...",
+ keyHelpUrl: "https://aistudio.google.com/apikey",
+ keyHelpLabel: "aistudio.google.com/apikey",
+ defaultModel: "gemini-2.5-flash",
+ models: ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash", "gemini-flash-latest"],
+ validateKey: (key: string) => key.length > 10,
+ },
+};
+
+export const PROVIDER_KEYS = Object.keys(PROVIDER_META) as ProviderKey[];
+
+export function isValidProviderKey(value: unknown): value is ProviderKey {
+ return typeof value === "string" && PROVIDER_KEYS.includes(value as ProviderKey);
+}
diff --git a/lib/settings.ts b/lib/settings.ts
index f0a5527..aa63cae 100644
--- a/lib/settings.ts
+++ b/lib/settings.ts
@@ -1,6 +1,10 @@
import { TestType, TEST_TYPE_VALUES } from "./testTypes";
+import { PROVIDER_META, PROVIDER_KEYS } from "./llm/types";
+import type { ProviderKey } from "./llm/types";
export { TestType } from "./testTypes";
+export type { ProviderKey } from "./llm/types";
+export { PROVIDER_META, PROVIDER_KEYS } from "./llm/types";
export const boilerplateOptions = [
"vitest",
@@ -20,6 +24,8 @@ export interface Settings {
disabledTestTypes: Record;
boilerplateSampleSize: number;
disabledBoilerplates: Record;
+ provider: ProviderKey;
+ model: string;
}
export const SETTINGS_STORAGE_KEY = "tcg_settings";
@@ -43,9 +49,22 @@ export const defaultSettings: Settings = {
},
{} as Record
),
+ provider: "openai",
+ model: PROVIDER_META.openai.defaultModel,
};
export function sanitizeSettings(partial?: Partial): Settings {
+ const provider: ProviderKey =
+ partial?.provider && PROVIDER_KEYS.includes(partial.provider)
+ ? partial.provider
+ : defaultSettings.provider;
+
+ const providerMeta = PROVIDER_META[provider];
+ const model =
+ partial?.model && providerMeta.models.includes(partial.model)
+ ? partial.model
+ : providerMeta.defaultModel;
+
const merged: Settings = {
defaultContext: partial?.defaultContext ?? defaultSettings.defaultContext,
boilerplateSampleSize:
@@ -60,6 +79,8 @@ export function sanitizeSettings(partial?: Partial): Settings {
...defaultSettings.disabledBoilerplates,
...(partial?.disabledBoilerplates ?? {}),
},
+ provider,
+ model,
};
merged.boilerplateSampleSize = Math.min(5, Math.max(1, Math.round(merged.boilerplateSampleSize)));
diff --git a/package-lock.json b/package-lock.json
index e9b933c..82250e3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,14 +1,16 @@
{
"name": "assertify",
- "version": "0.1.0",
+ "version": "0.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "assertify",
- "version": "0.1.0",
+ "version": "0.1.1",
"dependencies": {
- "next": "14.1.0",
+ "@anthropic-ai/sdk": "^0.78.0",
+ "@google/generative-ai": "^0.24.1",
+ "next": "^14.2.35",
"openai": "^4.28.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
@@ -21,7 +23,7 @@
"@typescript-eslint/parser": "^8.48.0",
"autoprefixer": "^10.4.16",
"eslint": "8.57.1",
- "eslint-config-next": "14.2.18",
+ "eslint-config-next": "^14.2.35",
"eslint-config-prettier": "^9.1.0",
"husky": "^9.1.7",
"postcss": "^8.4.32",
@@ -43,6 +45,35 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@anthropic-ai/sdk": {
+ "version": "0.78.0",
+ "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.78.0.tgz",
+ "integrity": "sha512-PzQhR715td/m1UaaN5hHXjYB8Gl2lF9UVhrrGrZeysiF6Rb74Wc9GCB8hzLdzmQtBd1qe89F9OptgB9Za1Ib5w==",
+ "license": "MIT",
+ "dependencies": {
+ "json-schema-to-ts": "^3.1.1"
+ },
+ "bin": {
+ "anthropic-ai-sdk": "bin/cli"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
+ "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@emnapi/core": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
@@ -78,9 +109,9 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
- "version": "4.9.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
- "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -152,9 +183,9 @@
}
},
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -174,6 +205,15 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@google/generative-ai": {
+ "version": "0.24.1",
+ "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz",
+ "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -202,9 +242,9 @@
}
},
"node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -268,13 +308,13 @@
}
},
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
- "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ansi-regex": "^6.0.1"
+ "ansi-regex": "^6.2.2"
},
"engines": {
"node": ">=12"
@@ -336,15 +376,15 @@
}
},
"node_modules/@next/env": {
- "version": "14.1.0",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.0.tgz",
- "integrity": "sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==",
+ "version": "14.2.35",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz",
+ "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
- "version": "14.2.18",
- "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.18.tgz",
- "integrity": "sha512-KyYTbZ3GQwWOjX3Vi1YcQbekyGP0gdammb7pbmmi25HBUCINzDReyrzCMOJIeZisK1Q3U6DT5Rlc4nm2/pQeXA==",
+ "version": "14.2.35",
+ "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.35.tgz",
+ "integrity": "sha512-Jw9A3ICz2183qSsqwi7fgq4SBPiNfmOLmTPXKvlnzstUwyvBrtySiY+8RXJweNAs9KThb1+bYhZh9XWcNOr2zQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -352,9 +392,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "14.1.0",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.0.tgz",
- "integrity": "sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==",
+ "version": "14.2.33",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz",
+ "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==",
"cpu": [
"arm64"
],
@@ -368,9 +408,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "14.1.0",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.0.tgz",
- "integrity": "sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==",
+ "version": "14.2.33",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz",
+ "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==",
"cpu": [
"x64"
],
@@ -384,9 +424,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "14.1.0",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.0.tgz",
- "integrity": "sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==",
+ "version": "14.2.33",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz",
+ "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==",
"cpu": [
"arm64"
],
@@ -400,9 +440,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "14.1.0",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.0.tgz",
- "integrity": "sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==",
+ "version": "14.2.33",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz",
+ "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==",
"cpu": [
"arm64"
],
@@ -416,9 +456,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "14.1.0",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.0.tgz",
- "integrity": "sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==",
+ "version": "14.2.33",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz",
+ "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==",
"cpu": [
"x64"
],
@@ -432,9 +472,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "14.1.0",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.0.tgz",
- "integrity": "sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==",
+ "version": "14.2.33",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz",
+ "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==",
"cpu": [
"x64"
],
@@ -448,9 +488,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "14.1.0",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.0.tgz",
- "integrity": "sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==",
+ "version": "14.2.33",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz",
+ "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==",
"cpu": [
"arm64"
],
@@ -464,9 +504,9 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
- "version": "14.1.0",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.0.tgz",
- "integrity": "sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==",
+ "version": "14.2.33",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz",
+ "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==",
"cpu": [
"ia32"
],
@@ -480,9 +520,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "14.1.0",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.0.tgz",
- "integrity": "sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==",
+ "version": "14.2.33",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz",
+ "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==",
"cpu": [
"x64"
],
@@ -562,18 +602,25 @@
"license": "MIT"
},
"node_modules/@rushstack/eslint-patch": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz",
- "integrity": "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==",
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.16.1.tgz",
+ "integrity": "sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==",
"dev": true,
"license": "MIT"
},
+ "node_modules/@swc/counter": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
+ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
+ "license": "Apache-2.0"
+ },
"node_modules/@swc/helpers": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
- "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==",
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
+ "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
"license": "Apache-2.0",
"dependencies": {
+ "@swc/counter": "^0.1.3",
"tslib": "^2.4.0"
}
},
@@ -643,20 +690,20 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.50.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz",
- "integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==",
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz",
+ "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "8.50.0",
- "@typescript-eslint/type-utils": "8.50.0",
- "@typescript-eslint/utils": "8.50.0",
- "@typescript-eslint/visitor-keys": "8.50.0",
- "ignore": "^7.0.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.57.0",
+ "@typescript-eslint/type-utils": "8.57.0",
+ "@typescript-eslint/utils": "8.57.0",
+ "@typescript-eslint/visitor-keys": "8.57.0",
+ "ignore": "^7.0.5",
"natural-compare": "^1.4.0",
- "ts-api-utils": "^2.1.0"
+ "ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -666,23 +713,23 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.50.0",
- "eslint": "^8.57.0 || ^9.0.0",
+ "@typescript-eslint/parser": "^8.57.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.50.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz",
- "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==",
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz",
+ "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.50.0",
- "@typescript-eslint/types": "8.50.0",
- "@typescript-eslint/typescript-estree": "8.50.0",
- "@typescript-eslint/visitor-keys": "8.50.0",
- "debug": "^4.3.4"
+ "@typescript-eslint/scope-manager": "8.57.0",
+ "@typescript-eslint/types": "8.57.0",
+ "@typescript-eslint/typescript-estree": "8.57.0",
+ "@typescript-eslint/visitor-keys": "8.57.0",
+ "debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -692,20 +739,20 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/project-service": {
- "version": "8.50.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz",
- "integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==",
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz",
+ "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.50.0",
- "@typescript-eslint/types": "^8.50.0",
- "debug": "^4.3.4"
+ "@typescript-eslint/tsconfig-utils": "^8.57.0",
+ "@typescript-eslint/types": "^8.57.0",
+ "debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -719,14 +766,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.50.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz",
- "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==",
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz",
+ "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.50.0",
- "@typescript-eslint/visitor-keys": "8.50.0"
+ "@typescript-eslint/types": "8.57.0",
+ "@typescript-eslint/visitor-keys": "8.57.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -737,9 +784,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.50.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz",
- "integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==",
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz",
+ "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -754,17 +801,17 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.50.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz",
- "integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==",
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz",
+ "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.50.0",
- "@typescript-eslint/typescript-estree": "8.50.0",
- "@typescript-eslint/utils": "8.50.0",
- "debug": "^4.3.4",
- "ts-api-utils": "^2.1.0"
+ "@typescript-eslint/types": "8.57.0",
+ "@typescript-eslint/typescript-estree": "8.57.0",
+ "@typescript-eslint/utils": "8.57.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -774,14 +821,14 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.50.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz",
- "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==",
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz",
+ "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -793,21 +840,21 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.50.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz",
- "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==",
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz",
+ "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.50.0",
- "@typescript-eslint/tsconfig-utils": "8.50.0",
- "@typescript-eslint/types": "8.50.0",
- "@typescript-eslint/visitor-keys": "8.50.0",
- "debug": "^4.3.4",
- "minimatch": "^9.0.4",
- "semver": "^7.6.0",
+ "@typescript-eslint/project-service": "8.57.0",
+ "@typescript-eslint/tsconfig-utils": "8.57.0",
+ "@typescript-eslint/types": "8.57.0",
+ "@typescript-eslint/visitor-keys": "8.57.0",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
"tinyglobby": "^0.2.15",
- "ts-api-utils": "^2.1.0"
+ "ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -821,16 +868,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.50.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz",
- "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==",
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz",
+ "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/eslint-utils": "^4.7.0",
- "@typescript-eslint/scope-manager": "8.50.0",
- "@typescript-eslint/types": "8.50.0",
- "@typescript-eslint/typescript-estree": "8.50.0"
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.57.0",
+ "@typescript-eslint/types": "8.57.0",
+ "@typescript-eslint/typescript-estree": "8.57.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -840,19 +887,19 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.50.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz",
- "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==",
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz",
+ "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.50.0",
- "eslint-visitor-keys": "^4.2.1"
+ "@typescript-eslint/types": "8.57.0",
+ "eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -863,13 +910,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
- "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
@@ -1199,9 +1246,9 @@
}
},
"node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1573,13 +1620,26 @@
}
},
"node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
+ "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "balanced-match": "^1.0.0"
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/brace-expansion/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
}
},
"node_modules/braces": {
@@ -2321,13 +2381,13 @@
}
},
"node_modules/eslint-config-next": {
- "version": "14.2.18",
- "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.18.tgz",
- "integrity": "sha512-SuDRcpJY5VHBkhz5DijJ4iA4bVnBA0n48Rb+YSJSCDr+h7kKAcb1mZHusLbW+WA8LDB6edSolomXA55eG3eOVA==",
+ "version": "14.2.35",
+ "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.35.tgz",
+ "integrity": "sha512-BpLsv01UisH193WyT/1lpHqq5iJ/Orfz9h/NOOlAmTUq4GY349PextQ62K4XpnaM9supeiEn3TaOTeQO07gURg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@next/eslint-plugin-next": "14.2.18",
+ "@next/eslint-plugin-next": "14.2.35",
"@rushstack/eslint-patch": "^1.3.3",
"@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
"@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
@@ -2515,9 +2575,9 @@
}
},
"node_modules/eslint-plugin-import/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -2579,9 +2639,9 @@
}
},
"node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -2662,9 +2722,9 @@
}
},
"node_modules/eslint-plugin-react/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -2754,9 +2814,9 @@
}
},
"node_modules/eslint/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -3191,6 +3251,7 @@
"version": "10.3.10",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
"integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -3223,6 +3284,32 @@
"node": ">=10.13.0"
}
},
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/globals": {
"version": "13.24.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
@@ -3990,6 +4077,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json-schema-to-ts": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
+ "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "ts-algebra": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -4194,16 +4294,16 @@
}
},
"node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "version": "10.2.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true,
- "license": "ISC",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "brace-expansion": "^2.0.1"
+ "brace-expansion": "^5.0.2"
},
"engines": {
- "node": ">=16 || 14 >=14.17"
+ "node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -4220,11 +4320,11 @@
}
},
"node_modules/minipass": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
- "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
"dev": true,
- "license": "ISC",
+ "license": "BlueOak-1.0.0",
"engines": {
"node": ">=16 || 14 >=14.17"
}
@@ -4289,14 +4389,13 @@
"license": "MIT"
},
"node_modules/next": {
- "version": "14.1.0",
- "resolved": "https://registry.npmjs.org/next/-/next-14.1.0.tgz",
- "integrity": "sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==",
- "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.",
+ "version": "14.2.35",
+ "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz",
+ "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==",
"license": "MIT",
"dependencies": {
- "@next/env": "14.1.0",
- "@swc/helpers": "0.5.2",
+ "@next/env": "14.2.35",
+ "@swc/helpers": "0.5.5",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
"graceful-fs": "^4.2.11",
@@ -4310,18 +4409,19 @@
"node": ">=18.17.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "14.1.0",
- "@next/swc-darwin-x64": "14.1.0",
- "@next/swc-linux-arm64-gnu": "14.1.0",
- "@next/swc-linux-arm64-musl": "14.1.0",
- "@next/swc-linux-x64-gnu": "14.1.0",
- "@next/swc-linux-x64-musl": "14.1.0",
- "@next/swc-win32-arm64-msvc": "14.1.0",
- "@next/swc-win32-ia32-msvc": "14.1.0",
- "@next/swc-win32-x64-msvc": "14.1.0"
+ "@next/swc-darwin-arm64": "14.2.33",
+ "@next/swc-darwin-x64": "14.2.33",
+ "@next/swc-linux-arm64-gnu": "14.2.33",
+ "@next/swc-linux-arm64-musl": "14.2.33",
+ "@next/swc-linux-x64-gnu": "14.2.33",
+ "@next/swc-linux-x64-musl": "14.2.33",
+ "@next/swc-win32-arm64-msvc": "14.2.33",
+ "@next/swc-win32-ia32-msvc": "14.2.33",
+ "@next/swc-win32-x64-msvc": "14.2.33"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
+ "@playwright/test": "^1.41.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.3.0"
@@ -4330,6 +4430,9 @@
"@opentelemetry/api": {
"optional": true
},
+ "@playwright/test": {
+ "optional": true
+ },
"sass": {
"optional": true
}
@@ -5226,9 +5329,9 @@
}
},
"node_modules/rimraf/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -5593,13 +5696,13 @@
}
},
"node_modules/string-width/node_modules/strip-ansi": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
- "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ansi-regex": "^6.0.1"
+ "ansi-regex": "^6.2.2"
},
"engines": {
"node": ">=12"
@@ -5978,10 +6081,16 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
+ "node_modules/ts-algebra": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
+ "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
+ "license": "MIT"
+ },
"node_modules/ts-api-utils": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
- "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
+ "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -6469,13 +6578,13 @@
}
},
"node_modules/wrap-ansi/node_modules/strip-ansi": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
- "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ansi-regex": "^6.0.1"
+ "ansi-regex": "^6.2.2"
},
"engines": {
"node": ">=12"
@@ -6503,6 +6612,17 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
}
}
diff --git a/package.json b/package.json
index 0d7f952..b59de33 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,9 @@
"format:check": "prettier --check ."
},
"dependencies": {
- "next": "14.1.0",
+ "@anthropic-ai/sdk": "^0.78.0",
+ "@google/generative-ai": "^0.24.1",
+ "next": "^14.2.35",
"openai": "^4.28.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
@@ -25,7 +27,7 @@
"@typescript-eslint/parser": "^8.48.0",
"autoprefixer": "^10.4.16",
"eslint": "8.57.1",
- "eslint-config-next": "14.2.18",
+ "eslint-config-next": "^14.2.35",
"eslint-config-prettier": "^9.1.0",
"husky": "^9.1.7",
"postcss": "^8.4.32",