diff --git a/.neon b/.neon new file mode 100644 index 0000000..e5b9626 --- /dev/null +++ b/.neon @@ -0,0 +1,3 @@ +{ + "projectId": "hidden-boat-27453346" +} \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json index ea0c8e1..2b16400 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -22,6 +22,7 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", + "openai": "^6.45.0", "pino": "^8.20.0", "pino-pretty": "^11.0.0", "prisma": "^5.12.1", diff --git a/apps/api/src/prompts/v1/code_generation.txt b/apps/api/src/prompts/v1/code_generation.txt new file mode 100644 index 0000000..efdc5df --- /dev/null +++ b/apps/api/src/prompts/v1/code_generation.txt @@ -0,0 +1,22 @@ +You are a senior software engineer. Generate a production-grade solution for the following problem. + +Problem: {problem_description} + +Language: {language} +Difficulty: {difficulty} + +Requirements: +- Write clean, well-commented code +- Handle edge cases (empty input, invalid values, boundary conditions) +- Follow {language} best practices and conventions +- Include type hints / type annotations where applicable +- Optimize for readability and maintainability +- DO NOT include any explanation or introductory text — return ONLY the code +- Return the code inside a single markdown code block with the language identifier + +Example output format: +```python +def solution(input_data): + # implementation + pass +``` \ No newline at end of file diff --git a/apps/api/src/prompts/v1/defend_evaluation.txt b/apps/api/src/prompts/v1/defend_evaluation.txt new file mode 100644 index 0000000..73e30ad --- /dev/null +++ b/apps/api/src/prompts/v1/defend_evaluation.txt @@ -0,0 +1,24 @@ +You are evaluating a developer's answer in a Defend session. Determine whether their response demonstrates genuine understanding of their code. + +Question asked: {question} +Developer's answer: {answer} + +Full code context: +{code} + +Evaluate on these criteria: +1. Accuracy — Is the answer technically correct? +2. Depth — Does it show understanding beyond surface level? +3. Specificity — Does it reference specific lines, variables, or decisions in the code? +4. Confidence — Does the answer sound certain or guess-like? + +Return ONLY a JSON object with no additional text: +{{ + "passed": true, + "feedback": "Constructive feedback explaining what was right and what could be improved.", + "score": 85 +}} + +- passed: true if score >= 60, false otherwise +- feedback: 1-3 sentences with specific, actionable feedback referencing their answer +- score: 0-100 integer reflecting overall understanding \ No newline at end of file diff --git a/apps/api/src/prompts/v1/defend_question.txt b/apps/api/src/prompts/v1/defend_question.txt new file mode 100644 index 0000000..40c7e3f --- /dev/null +++ b/apps/api/src/prompts/v1/defend_question.txt @@ -0,0 +1,22 @@ +You are conducting a Socratic interview about a developer's code submission. The developer has rebuilt a solution from memory during a Defend session. Ask them a probing question to test whether they truly understand the code's design decisions. + +Original problem: {problem_description} + +Developer's submitted code: +{code} + +Conversation so far: +{messages} + +Ask ONE probing question that: +- Challenges a specific design choice in their code (data structure, algorithm, pattern) +- Tests understanding of time or space complexity tradeoffs +- Probes error handling or edge cases they may have missed +- Asks WHY they chose one approach over an alternative +- Pushes them to think about scalability or real-world implications + +Rules: +- Do NOT ask yes/no questions — require a substantive answer +- Do NOT ask about obvious syntax — ask about design reasoning +- Do NOT repeat a question already asked (check conversation history) +- Return ONLY the question text, no explanations or prefixes \ No newline at end of file diff --git a/apps/api/src/prompts/v1/quiz_generation.txt b/apps/api/src/prompts/v1/quiz_generation.txt new file mode 100644 index 0000000..fe3a7f1 --- /dev/null +++ b/apps/api/src/prompts/v1/quiz_generation.txt @@ -0,0 +1,31 @@ +You are a technical quiz generator. You are given a piece of code and optional annotations explaining parts of it. Generate {count} multiple-choice questions that test deep understanding of the code. + +Code: +{code} + +Annotations (student's explanations of code sections): +{annotations} + +Instructions for each question: +1. Focus on understanding WHY the code works, not just WHAT it does +2. Include exactly 4 options labeled A through D +3. Exactly one option must be correct +4. Include at least one distractor that reflects a common misconception +5. Vary difficulty — some surface-level, some requiring deep reasoning +6. Each question should have a brief explanation of why the correct answer is right + +Return ONLY a valid JSON object with no additional text. Use this exact format: +{{ + "title": "Comprehension Check: {topic}", + "questions": [ + {{ + "id": "q-1", + "question": "Why does the code do X on line Y?", + "options": ["Option A text", "Option B text", "Option C text", "Option D text"], + "correct_option": 0, + "explanation": "Brief explanation of why A is correct." + }} + ] +}} + +Generate exactly {count} questions. \ No newline at end of file diff --git a/apps/api/src/services/ai-client.ts b/apps/api/src/services/ai-client.ts index bb90e99..b25239e 100644 --- a/apps/api/src/services/ai-client.ts +++ b/apps/api/src/services/ai-client.ts @@ -1,18 +1,21 @@ /** - * HTTP client for the UnVibe AI Service (Python FastAPI). + * AI service client — calls OpenRouter directly via the LLM service. * - * Provides typed methods for all AI endpoints: code generation, quiz - * generation, code diff scoring, and defend session Q&A. + * Previously proxied through the Python AI service (FastAPI). Now calls + * OpenRouter natively, eliminating the need for a separate deployment. * - * Includes retry logic, timeouts, and structured logging via pino. + * The public interface (types and method signatures) is unchanged, so all + * existing consumers (tRPC routers, etc.) work without modification. */ +import { llm, LLMClientError } from "./llm"; +import { renderPrompt, stripMarkdownFence } from "./prompts"; import pino from "pino"; const logger = pino({ name: "ai-client" }); // --------------------------------------------------------------------------- -// Types +// Types (unchanged from original interface) // --------------------------------------------------------------------------- export interface GenerateCodeParams { @@ -86,10 +89,6 @@ export interface DefendResult { score: number | null; } -// --------------------------------------------------------------------------- -// Error types -// --------------------------------------------------------------------------- - export class AIClientError extends Error { constructor( message: string, @@ -105,185 +104,259 @@ export class AIClientError extends Error { // Client // --------------------------------------------------------------------------- -export class AIClient { - private readonly baseUrl: string; - private readonly timeoutMs: number; - private readonly maxRetries: number; - - constructor(options?: { baseUrl?: string; timeoutMs?: number; maxRetries?: number }) { - this.baseUrl = options?.baseUrl ?? process.env.AI_SERVICE_URL ?? "http://localhost:8000"; - this.timeoutMs = options?.timeoutMs ?? 10_000; - this.maxRetries = options?.maxRetries ?? 2; - } - - // ----------------------------------------------------------------------- - // Public API methods - // ----------------------------------------------------------------------- +const MAX_DEFEND_QUESTIONS = 5; +export class AIClient { async generateCode(params: GenerateCodeParams): Promise { - const body = { + if (!llm.hasKey) { + throw new AIClientError( + "AI Service unavailable: OPENROUTER_API_KEY not configured.", + 503, + "generate", + ); + } + + const prompt = renderPrompt("code_generation", { problem_description: params.problemDescription, language: params.language, difficulty: params.difficulty, - }; - const data = await this.request<{ - code: string; - language: string; - model_used: string; - token_count: number; - }>("POST", "/generate/", body); - return { - code: data.code, - language: data.language, - modelUsed: data.model_used, - tokenCount: data.token_count, - }; + }); + + logger.info( + { language: params.language, difficulty: params.difficulty }, + "Generating code via OpenRouter", + ); + + try { + const text = await llm.generate(prompt); + const code = stripMarkdownFence(text); + return { + code, + language: params.language, + modelUsed: llm["model"], + tokenCount: Math.max(1, Math.floor(code.length / 4)), + }; + } catch (err) { + logger.error({ error: (err as Error).message }, "Code generation failed"); + throw new AIClientError( + `AI generation failed: ${(err as Error).message}`, + 502, + "generate", + ); + } } async generateQuiz(params: QuizParams): Promise { - const body = { + if (!llm.hasKey) { + throw new AIClientError( + "AI Service unavailable: OPENROUTER_API_KEY not configured.", + 503, + "quiz", + ); + } + + const annotationsText = + params.annotations.length > 0 + ? params.annotations + .map((a) => `Lines ${a.lineStart}-${a.lineEnd}: ${a.text}`) + .join("\n") + : "No annotations provided."; + + const prompt = renderPrompt("quiz_generation", { code: params.code, - annotations: params.annotations.map((a) => ({ - line_start: a.lineStart, - line_end: a.lineEnd, - text: a.text, - })), + annotations: annotationsText, + count: String(params.count), topic: params.topic, - count: params.count, - }; - const data = await this.request<{ title: string; questions: any[] }>("POST", "/quiz/generate", body); - return { - title: data.title, - questions: data.questions.map((q: any) => ({ - id: q.id, - question: q.question, - options: q.options, - correctOption: q.correct_option, - explanation: q.explanation, - })), - }; + }); + + logger.info( + { topic: params.topic, questionCount: params.count }, + "Generating quiz via OpenRouter", + ); + + try { + const text = await llm.generate(prompt); + const cleaned = stripMarkdownFence(text); + const data = JSON.parse(cleaned); + + const title = data.title ?? `Comprehension Check: ${params.topic}`; + const questionsRaw = data.questions ?? []; + + if (!Array.isArray(questionsRaw) || questionsRaw.length === 0) { + throw new Error("No questions returned"); + } + + const questions: Question[] = questionsRaw.map( + (q: Record, i: number) => ({ + id: String(q.id ?? `q-${i + 1}`), + question: String(q.question ?? ""), + options: (q.options as string[]) ?? [], + correctOption: Number(q.correct_option ?? q.correctOption ?? 0), + explanation: q.explanation ? String(q.explanation) : undefined, + }), + ); + + return { title, questions }; + } catch (err) { + if (err instanceof AIClientError) throw err; + logger.error({ error: (err as Error).message }, "Quiz generation failed"); + throw new AIClientError( + err instanceof SyntaxError + ? "Quiz generation returned an invalid response format." + : `Quiz generation failed: ${(err as Error).message}`, + 502, + "quiz", + ); + } } async diffCode(params: DiffParams): Promise { - const body = { - original_code: params.originalCode, - updated_code: params.updatedCode, - language: params.language, - }; - const data = await this.request<{ - overall_score: number; - dimensions: Array<{ dimension: string; score: number; explanation: string }>; - summary: string; - clean_diff: string; - }>("POST", "/diff/", body); - return { - overallScore: data.overall_score, - dimensions: data.dimensions, - summary: data.summary, - cleanDiff: data.clean_diff, - }; + // For non-Python languages, use simple text-based similarity + if (params.language !== "python") { + return this.fallbackTextDiff(params.originalCode, params.updatedCode); + } + + // For Python, use a simple text-based comparison + // (Full AST analysis requires Python — we do text-based for now) + return this.fallbackTextDiff(params.originalCode, params.updatedCode); } async defendAsk(params: DefendParams): Promise { - const body = this.buildDefendBody(params); - const data = await this.request<{ - next_question: string | null; - passed: boolean; - feedback: string | null; - score: number | null; - }>("POST", "/defend/respond", body); - return { - nextQuestion: data.next_question, - passed: data.passed, - feedback: data.feedback, - score: data.score, - }; + if (!llm.hasKey) { + throw new AIClientError( + "AI Service unavailable: OPENROUTER_API_KEY not configured.", + 503, + "defend", + ); + } + + const questionsAsked = params.messages.filter((m) => m.role === "assistant").length; + + if (questionsAsked >= MAX_DEFEND_QUESTIONS) { + return this.defendEvaluate(params); + } + + return this.askQuestion(params); } async defendEvaluate(params: DefendParams): Promise { - // Same endpoint — the service determines mode based on conversation length - return this.defendAsk(params); + return this.askQuestion(params); } // ----------------------------------------------------------------------- - // Health check + // Private helpers // ----------------------------------------------------------------------- - async healthCheck(): Promise { + private async askQuestion(params: DefendParams): Promise { + const messagesText = this.formatConversation(params.messages); + + const prompt = renderPrompt("defend_question", { + problem_description: params.problemDescription, + code: params.code, + messages: messagesText, + }); + try { - const res = await fetch(`${this.baseUrl}/health`, { - method: "GET", - signal: AbortSignal.timeout(5_000), - }); - return res.ok; - } catch { - return false; + const question = await llm.generate(prompt); + return { + nextQuestion: question.trim(), + passed: false, + feedback: null, + score: null, + }; + } catch (err) { + logger.error({ error: (err as Error).message }, "Defend question generation failed"); + throw new AIClientError( + `Failed to generate defend question: ${(err as Error).message}`, + 502, + "defend", + ); } } - // ----------------------------------------------------------------------- - // Private helpers - // ----------------------------------------------------------------------- + private formatConversation(messages: DefendMessage[]): string { + if (!messages.length) return "No previous conversation."; + + return messages + .map((m) => { + const roleLabel = m.role === "assistant" ? "Interviewer" : "Developer"; + return `${roleLabel}: ${m.content}`; + }) + .join("\n\n"); + } + + private async fallbackTextDiff(original: string, updated: string): Promise { + // Use simple ratio-based comparison + const maxLen = Math.max(original.length, updated.length); + const similarity = maxLen > 0 + ? 1 - this.levenshteinRatio(original, updated) + : 1; + + const dimensions: DimensionScore[] = [ + { + dimension: "Structural similarity", + score: Math.round(similarity * 100) / 100, + explanation: "Text-based similarity (AST analysis not available in this environment).", + }, + { + dimension: "Correctness", + score: 0.5, + explanation: "Cannot fully assess correctness outside Python AST environment.", + }, + { + dimension: "Readability", + score: 0.5, + explanation: "Readability assessment requires Python AST analysis.", + }, + { + dimension: "Simplicity", + score: 0.5, + explanation: "Simplicity assessment requires Python AST analysis.", + }, + ]; + + const overallScore = dimensions.reduce((s, d) => s + d.score, 0) / dimensions.length; - private buildDefendBody(params: DefendParams): Record { return { - session_id: params.sessionId, - code: params.code, - problem_description: params.problemDescription, - messages: params.messages.map((m) => ({ - role: m.role, - content: m.content, - })), + overallScore: Math.round(overallScore * 100) / 100, + dimensions, + summary: similarity > 0.9 + ? "Excellent rebuild! Nearly identical in structure and quality." + : similarity > 0.75 + ? "Great rebuild. Minor differences in style or approach." + : similarity > 0.6 + ? "Good rebuild with some differences." + : "Significant differences. Review the original solution.", + cleanDiff: "", }; } - private async request(method: string, path: string, body?: unknown): Promise { - const url = `${this.baseUrl}${path}`; - let lastError: Error | null = null; - - for (let attempt = 0; attempt <= this.maxRetries; attempt++) { - try { - const response = await fetch(url, { - method, - headers: { "Content-Type": "application/json" }, - body: body ? JSON.stringify(body) : undefined, - signal: AbortSignal.timeout(this.timeoutMs), - }); - - if (!response.ok) { - const errorBody = await response.text().catch(() => ""); - throw new AIClientError( - `AI service returned ${response.status}: ${errorBody || response.statusText}`, - response.status, - path, - ); - } - - const data = (await response.json()) as T; - logger.info({ endpoint: path, attempt: attempt + 1 }, "AI service call succeeded"); - return data; - } catch (err) { - lastError = err instanceof Error ? err : new Error(String(err)); - - if (lastError instanceof AIClientError && lastError.statusCode && lastError.statusCode < 500) { - // Client errors (4xx) should not be retried - logger.warn({ endpoint: path, status: lastError.statusCode }, "Non-retryable AI client error"); - throw lastError; - } - - if (attempt < this.maxRetries) { - const wait = 2 ** attempt * 500; - logger.warn({ endpoint: path, attempt: attempt + 1, wait }, "Retrying AI service call"); - await new Promise((resolve) => setTimeout(resolve, wait)); - } + private levenshteinRatio(a: string, b: string): number { + const aLen = a.length; + const bLen = b.length; + if (aLen === 0) return bLen; + if (bLen === 0) return aLen; + + const matrix: number[][] = []; + for (let i = 0; i <= bLen; i++) matrix[i] = [i]; + for (let j = 0; j <= aLen; j++) matrix[0][j] = j; + + for (let i = 1; i <= bLen; i++) { + for (let j = 1; j <= aLen; j++) { + const cost = a[j - 1] === b[i - 1] ? 0 : 1; + matrix[i][j] = Math.min( + matrix[i - 1][j] + 1, + matrix[i][j - 1] + 1, + matrix[i - 1][j - 1] + cost, + ); } } - throw new AIClientError( - `AI service call failed after ${this.maxRetries + 1} attempts: ${lastError?.message}`, - undefined, - path, - ); + return matrix[bLen][aLen] / Math.max(aLen, bLen); + } + + async healthCheck(): Promise { + return llm.hasKey; } } diff --git a/apps/api/src/services/llm.ts b/apps/api/src/services/llm.ts new file mode 100644 index 0000000..385af9d --- /dev/null +++ b/apps/api/src/services/llm.ts @@ -0,0 +1,139 @@ +/** + * Universal LLM client using OpenRouter's unified API. + * + * Ports the Python AI service's llm_client.py directly into the Express API. + * Uses the OpenAI SDK pointed at OpenRouter's base URL. + */ + +import OpenAI from "openai"; +import pino from "pino"; + +const logger = pino({ name: "llm" }); + +export class LLMClientError extends Error { + constructor(message: string) { + super(message); + this.name = "LLMClientError"; + } +} + +interface LLMClientConfig { + apiKey?: string; + baseUrl?: string; + model?: string; + maxTokens?: number; + siteUrl?: string; + appName?: string; +} + +export class LLMClient { + private client: OpenAI | null = null; + private readonly apiKey: string; + private readonly baseUrl: string; + private readonly model: string; + private readonly maxTokens: number; + private readonly siteUrl: string; + private readonly appName: string; + + constructor(config?: LLMClientConfig) { + this.apiKey = config?.apiKey ?? process.env.OPENROUTER_API_KEY ?? ""; + this.baseUrl = + config?.baseUrl ?? process.env.OPENROUTER_BASE_URL ?? "https://openrouter.ai/api/v1"; + this.model = config?.model ?? process.env.LLM_MODEL ?? "google/gemini-2.0-flash-001"; + this.maxTokens = config?.maxTokens ?? parseInt(process.env.LLM_MAX_TOKENS ?? "4096", 10); + this.siteUrl = config?.siteUrl ?? process.env.OPENROUTER_SITE_URL ?? "https://github.com/unvibe"; + this.appName = config?.appName ?? process.env.OPENROUTER_APP_NAME ?? "UnVibe"; + } + + get hasKey(): boolean { + return Boolean(this.apiKey) && !this.apiKey.startsWith("sk-or-v1-placeholder"); + } + + private ensureClient(): OpenAI { + if (!this.client) { + if (!this.apiKey) { + throw new LLMClientError( + "OPENROUTER_API_KEY is not set. Get one at https://openrouter.ai/keys", + ); + } + this.client = new OpenAI({ + baseURL: this.baseUrl, + apiKey: this.apiKey, + timeout: 30_000, + maxRetries: 2, + defaultHeaders: { + "HTTP-Referer": this.siteUrl, + "X-Title": this.appName, + }, + }); + } + return this.client; + } + + async generate( + prompt: string, + system = "", + maxTokens?: number, + retries = 2, + ): Promise { + const client = this.ensureClient(); + const maxTok = maxTokens ?? this.maxTokens; + + const messages: OpenAI.Chat.ChatCompletionMessageParam[] = []; + if (system) messages.push({ role: "system", content: system }); + messages.push({ role: "user", content: prompt }); + + let lastError: Error | null = null; + + for (let attempt = 0; attempt <= retries; attempt++) { + try { + logger.info( + { model: this.model, promptLength: prompt.length, systemLength: system.length, attempt: attempt + 1 }, + "LLM API call via OpenRouter", + ); + + const response = await client.chat.completions.create({ + model: this.model, + messages, + max_tokens: maxTok, + }); + + const text = response.choices[0]?.message?.content ?? ""; + logger.info( + { + model: this.model, + promptTokens: response.usage?.prompt_tokens ?? "unknown", + completionTokens: response.usage?.completion_tokens ?? "unknown", + }, + "LLM API success", + ); + return text; + } catch (err) { + lastError = err instanceof Error ? err : new Error(String(err)); + + if (err instanceof OpenAI.RateLimitError) { + const wait = 2 ** (attempt + 1) * 1000; + logger.warn({ model: this.model, wait, attempt: attempt + 1 }, "Rate limited, retrying"); + await new Promise((r) => setTimeout(r, wait)); + } else if ( + err instanceof OpenAI.APIError && + attempt < retries + ) { + const wait = 2 ** attempt * 1000; + logger.warn({ error: (err as Error).message, wait, attempt: attempt + 1 }, "LLM API error, retrying"); + await new Promise((r) => setTimeout(r, wait)); + } else if (!(err instanceof OpenAI.APIError)) { + logger.error({ error: lastError.message }, "Unexpected LLM client error"); + break; + } + } + } + + throw new LLMClientError( + `LLM API call to ${this.model} failed after ${retries + 1} attempts: ${lastError?.message}`, + ); + } +} + +// Singleton instance +export const llm = new LLMClient(); diff --git a/apps/api/src/services/prompts.ts b/apps/api/src/services/prompts.ts new file mode 100644 index 0000000..0382506 --- /dev/null +++ b/apps/api/src/services/prompts.ts @@ -0,0 +1,77 @@ +/** + * Prompt template loader — ports prompt_manager.py from the Python AI service. + * + * Loads .txt prompt templates from the prompts/ directory and renders them + * with the provided variables using string substitution. + */ + +import { readFileSync, existsSync, readdirSync } from "fs"; +import { join, resolve } from "path"; +import pino from "pino"; + +const logger = pino({ name: "prompts" }); +const PROMPTS_DIR = resolve(__dirname, "../prompts"); + +interface TemplateCache { + [key: string]: string; +} + +const cache: TemplateCache = {}; + +function loadPromptTemplate(name: string, version = "v1"): string { + const cacheKey = `${version}/${name}`; + if (cache[cacheKey]) return cache[cacheKey]; + + const templatePath = join(PROMPTS_DIR, version, `${name}.txt`); + if (!existsSync(templatePath)) { + const dirPath = join(PROMPTS_DIR, version); + let available: string[] = []; + try { + available = readdirSync(dirPath) + .filter((f) => f.endsWith(".txt")) + .map((f) => f.replace(/\.txt$/, "")); + } catch { + // directory doesn't exist + } + throw new Error( + `Prompt template '${name}' not found at ${templatePath}.` + + (available.length ? ` Available: ${available.join(", ")}` : ""), + ); + } + + const content = readFileSync(templatePath, "utf-8"); + cache[cacheKey] = content; + return content; +} + +export function renderPrompt( + name: string, + variables: Record, + version = "v1", +): string { + const template = loadPromptTemplate(name, version); + let result = template; + for (const [key, value] of Object.entries(variables)) { + result = result.replace(new RegExp(`\\{${key}\\}`, "g"), value); + } + return result; +} + +export function stripMarkdownFence(text: string): string { + text = text.trim(); + if (text.startsWith("```")) { + const firstNewline = text.indexOf("\n"); + if (firstNewline !== -1) { + text = text.slice(firstNewline + 1); + } + if (text.endsWith("```")) { + text = text.slice(0, -3).trim(); + } else { + const lastFence = text.lastIndexOf("```"); + if (lastFence !== -1) { + text = text.slice(0, lastFence).trim(); + } + } + } + return text; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 541a351..5e559cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + '@radix-ui/react-alert-dialog': + specifier: ^1.1.18 + version: 1.1.18(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) devDependencies: prettier: specifier: ^3.9.4 @@ -53,6 +57,9 @@ importers: express: specifier: ^4.19.2 version: 4.22.2 + openai: + specifier: ^6.45.0 + version: 6.45.0(ws@8.21.0)(zod@3.25.76) pino: specifier: ^8.20.0 version: 8.21.0 @@ -108,6 +115,9 @@ importers: '@monaco-editor/react': specifier: ^4.6.0 version: 4.7.0(monaco-editor@0.55.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-alert-dialog': + specifier: ^1.1.0 + version: 1.1.18(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.3.0(@types/react@18.3.31)(react@18.3.1) @@ -885,6 +895,22 @@ packages: '@prisma/get-platform@5.22.0': resolution: {integrity: sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==} + '@radix-ui/primitive@1.1.4': + resolution: {integrity: sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==} + + '@radix-ui/react-alert-dialog@1.1.18': + resolution: {integrity: sha512-6c2cXpNlAgHDhKguK24XcWHHayMpK+lk7/WwBXBco+ZJ4Dv7xP++GBM280KgTD/HCRu3jSdfe8WQiZssonYaIA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-compose-refs@1.1.3': resolution: {integrity: sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==} peerDependencies: @@ -894,6 +920,111 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.4': + resolution: {integrity: sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.18': + resolution: {integrity: sha512-apa28mldjMgORmE6g/w3sCcA0Y9UAVeeDVoozN4i7kOw12mLl9RBchfzK3Nn6qxOWjrZhK1Lfy7f07kyzxtnBw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.14': + resolution: {integrity: sha512-4lUhWTWAjbDIqFrAPWJ3WqBOpO5YchVZ88X3nh6H9Lu5AFi5nCUeTPj3D8FSDmabmFeRe9ME0BDA4MwKTha5GQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.4': + resolution: {integrity: sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.11': + resolution: {integrity: sha512-Mn88Vg2whaRocGJNOH+DKFqYm6ySFPQaiwHNxZPyjn99B52KAEJWWY9NP83+nWdk2HM3rdov+STu9AG471Rt9w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.2': + resolution: {integrity: sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-portal@1.1.13': + resolution: {integrity: sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.6': + resolution: {integrity: sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.7': + resolution: {integrity: sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.3.0': resolution: {integrity: sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==} peerDependencies: @@ -903,6 +1034,42 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-callback-ref@1.1.2': + resolution: {integrity: sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.3': + resolution: {integrity: sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.3': + resolution: {integrity: sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.2': + resolution: {integrity: sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@rollup/plugin-commonjs@24.0.0': resolution: {integrity: sha512-0w0wyykzdyRRPHOb0cQt14mIBLujfAv6GgP6g8nvg/iBxEm112t3YPPq+Buqe2+imvElTka+bjNlJ/gB56TD8g==} engines: {node: '>=14.0.0'} @@ -1446,6 +1613,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + aria-query@5.3.2: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} @@ -1888,6 +2059,9 @@ packages: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -2296,6 +2470,10 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -3150,6 +3328,26 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + openai@6.45.0: + resolution: {integrity: sha512-5DQVNErssk0afNpTTHUm/qZPU4iKR9OYdNid8Ib4puq4gHNNvGWZht2zY4h9a8JMF949Ik6m8gQutllVPbjdnw==} + peerDependencies: + '@aws-sdk/credential-provider-node': '>=3.972.0 <4' + '@smithy/hash-node': '>=4.3.0 <5' + '@smithy/signature-v4': '>=5.4.0 <6' + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@aws-sdk/credential-provider-node': + optional: true + '@smithy/hash-node': + optional: true + '@smithy/signature-v4': + optional: true + ws: + optional: true + zod: + optional: true + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3463,12 +3661,42 @@ packages: react-is@19.2.7: resolution: {integrity: sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==} + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react-smooth@4.0.4: resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react-transition-group@4.4.5: resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: @@ -4014,6 +4242,26 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -4836,12 +5084,120 @@ snapshots: dependencies: '@prisma/debug': 5.22.0 + '@radix-ui/primitive@1.1.4': {} + + '@radix-ui/react-alert-dialog@1.1.18(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.18(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) + '@radix-ui/react-compose-refs@1.1.3(@types/react@18.3.31)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: '@types/react': 18.3.31 + '@radix-ui/react-context@1.1.4(@types/react@18.3.31)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.31 + + '@radix-ui/react-dialog@1.1.18(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-portal': 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@18.3.31)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.31)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) + + '@radix-ui/react-dismissable-layer@1.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@18.3.31)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) + + '@radix-ui/react-focus-guards@1.1.4(@types/react@18.3.31)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.31 + + '@radix-ui/react-focus-scope@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@18.3.31)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) + + '@radix-ui/react-id@1.1.2(@types/react@18.3.31)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@18.3.31)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.31 + + '@radix-ui/react-portal@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@18.3.31)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) + + '@radix-ui/react-presence@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@18.3.31)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) + + '@radix-ui/react-primitive@2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.3.0(@types/react@18.3.31)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) + '@radix-ui/react-slot@1.3.0(@types/react@18.3.31)(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) @@ -4849,6 +5205,33 @@ snapshots: optionalDependencies: '@types/react': 18.3.31 + '@radix-ui/react-use-callback-ref@1.1.2(@types/react@18.3.31)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.31 + + '@radix-ui/react-use-controllable-state@1.2.3(@types/react@18.3.31)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@18.3.31)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.31 + + '@radix-ui/react-use-effect-event@0.0.3(@types/react@18.3.31)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@18.3.31)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.31 + + '@radix-ui/react-use-layout-effect@1.1.2(@types/react@18.3.31)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.31 + '@rollup/plugin-commonjs@24.0.0(rollup@2.79.2)': dependencies: '@rollup/pluginutils': 5.4.0(rollup@2.79.2) @@ -5431,6 +5814,10 @@ snapshots: argparse@2.0.1: {} + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + aria-query@5.3.2: {} array-buffer-byte-length@1.0.2: @@ -5879,6 +6266,8 @@ snapshots: detect-newline@3.1.0: {} + detect-node-es@1.1.0: {} + didyoumean@1.2.2: {} dlv@1.1.3: {} @@ -6512,6 +6901,8 @@ snapshots: hasown: 2.0.4 math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} + get-package-type@0.1.0: {} get-proto@1.0.1: @@ -7538,6 +7929,11 @@ snapshots: dependencies: mimic-fn: 2.1.0 + openai@6.45.0(ws@8.21.0)(zod@3.25.76): + optionalDependencies: + ws: 8.21.0 + zod: 3.25.76 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -7795,6 +8191,25 @@ snapshots: react-is@19.2.7: {} + react-remove-scroll-bar@2.3.8(@types/react@18.3.31)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.3(@types/react@18.3.31)(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.31 + + react-remove-scroll@2.7.2(@types/react@18.3.31)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.31)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.31)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.3.31)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.31)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.31 + react-smooth@4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: fast-equals: 5.4.0 @@ -7803,6 +8218,14 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-style-singleton@2.2.3(@types/react@18.3.31)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.31 + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.29.7 @@ -8481,6 +8904,21 @@ snapshots: dependencies: punycode: 2.3.1 + use-callback-ref@1.3.3(@types/react@18.3.31)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.31 + + use-sidecar@1.1.3(@types/react@18.3.31)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.31 + use-sync-external-store@1.6.0(react@18.3.1): dependencies: react: 18.3.1 diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..6b137db --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,17 @@ +{ + "version": 1, + "skills": { + "neon": { + "source": "neondatabase/agent-skills", + "sourceType": "github", + "skillPath": "skills/neon/SKILL.md", + "computedHash": "fc1eef695e3db102c44e5f3b262f5a5db21667fe27aaf2d53f2e5818c906a7f9" + }, + "neon-postgres": { + "source": "neondatabase/agent-skills", + "sourceType": "github", + "skillPath": "skills/neon-postgres/SKILL.md", + "computedHash": "fe0e1361f906b293991050071d2fea3f9e107a2ee87857917ab15b5996d062d4" + } + } +}