Update ai validation to use gpt-5 and the new responses api

This commit is contained in:
2025-10-01 22:18:26 -04:00
parent e10df632d8
commit 60875c25a6
7 changed files with 1184 additions and 494 deletions

View File

@@ -20,7 +20,7 @@
"express": "^4.18.2",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.12.0",
"openai": "^4.85.3",
"openai": "^6.0.0",
"pg": "^8.14.1",
"pm2": "^5.3.0",
"ssh2": "^1.16.0",
@@ -399,43 +399,12 @@
"integrity": "sha512-R/BHQFripuhW6XPXy05hIvXJQdQ4540KnTvEFHSLjXfHYM41liOLKgIJEyYYiQe796xpaMHfe4Uj/p7Uvng2vA==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "18.19.76",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.76.tgz",
"integrity": "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw==",
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/node-fetch": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
"license": "MIT",
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.0"
}
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"license": "ISC"
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -458,18 +427,6 @@
"node": ">= 14"
}
},
"node_modules/agentkeepalive": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
"license": "MIT",
"dependencies": {
"humanize-ms": "^1.2.1"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/amp": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz",
@@ -1327,15 +1284,6 @@
"node": ">= 0.6"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/eventemitter2": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz",
@@ -1474,25 +1422,6 @@
"node": ">= 6"
}
},
"node_modules/form-data-encoder": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
"license": "MIT"
},
"node_modules/formdata-node": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
"license": "MIT",
"dependencies": {
"node-domexception": "1.0.0",
"web-streams-polyfill": "4.0.0-beta.3"
},
"engines": {
"node": ">= 12.20"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1853,15 +1782,6 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.0.0"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -2407,25 +2327,6 @@
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
"license": "MIT"
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@@ -2622,25 +2523,16 @@
}
},
"node_modules/openai": {
"version": "4.85.3",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.85.3.tgz",
"integrity": "sha512-KTMXAK6FPd2IvsPtglMt0J1GyVrjMxCYzu/mVbCPabzzquSJoZlYpHtE0p0ScZPyt11XTc757xSO4j39j5g+Xw==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-6.0.0.tgz",
"integrity": "sha512-J7LEmTn3WLZnbyEmMYcMPyT5A0fGzhPwSvVUcNRKy6j2hJIbqSFrJERnUHYNkcoCCalRumypnj9AVoe5bVHd3Q==",
"license": "Apache-2.0",
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
"node-fetch": "^2.6.7"
},
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.23.8"
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
"ws": {
@@ -3941,12 +3833,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"license": "MIT"
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -4017,15 +3903,6 @@
"lodash": "^4.17.14"
}
},
"node_modules/web-streams-polyfill": {
"version": "4.0.0-beta.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",

View File

@@ -29,7 +29,7 @@
"express": "^4.18.2",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.12.0",
"openai": "^4.85.3",
"openai": "^6.0.0",
"pg": "^8.14.1",
"pm2": "^5.3.0",
"ssh2": "^1.16.0",

View File

@@ -6,7 +6,7 @@ const path = require("path");
const dotenv = require("dotenv");
const mysql = require('mysql2/promise');
const { Client } = require('ssh2');
const { getDbConnection } = require('../utils/dbConnection'); // Import the optimized connection function
const { getDbConnection, closeAllConnections } = require('../utils/dbConnection'); // Import the optimized connection function
// Ensure environment variables are loaded
dotenv.config({ path: path.join(__dirname, "../../.env") });
@@ -19,6 +19,121 @@ if (!process.env.OPENAI_API_KEY) {
console.error("Warning: OPENAI_API_KEY is not set in environment variables");
}
async function createResponsesCompletion(payload) {
if (!openai.responses?.create) {
throw new Error(
"OpenAI client does not expose responses.create; please verify the openai SDK version."
);
}
return openai.responses.create(payload);
}
const AI_VALIDATION_SCHEMA_NAME = "ai_validation_response";
const FLEXIBLE_PRIMITIVE_SCHEMAS = [
{ type: "string" },
{ type: "number" },
{ type: "boolean" },
{ type: "null" },
];
const FLEXIBLE_ARRAY_SCHEMA = {
type: "array",
items: {
anyOf: FLEXIBLE_PRIMITIVE_SCHEMAS,
},
};
const FLEXIBLE_OBJECT_SCHEMA = {
type: "object",
properties: {},
patternProperties: {
".+": {
anyOf: [...FLEXIBLE_PRIMITIVE_SCHEMAS, FLEXIBLE_ARRAY_SCHEMA],
},
},
additionalProperties: false,
};
const FLEXIBLE_VALUE_SCHEMA = {
anyOf: [...FLEXIBLE_PRIMITIVE_SCHEMAS, FLEXIBLE_ARRAY_SCHEMA, FLEXIBLE_OBJECT_SCHEMA],
};
const AI_VALIDATION_JSON_SCHEMA = {
type: "object",
additionalProperties: false,
required: [
"correctedData",
"changes",
"warnings",
"summary",
"qualityNotes",
"nextSteps",
"metadata"
],
properties: {
correctedData: {
type: "array",
items: {
type: "object",
properties: {},
patternProperties: {
".+": FLEXIBLE_VALUE_SCHEMA,
},
additionalProperties: false,
},
},
changes: {
type: "array",
items: {
type: "string",
},
default: [],
},
warnings: {
type: "array",
items: {
type: "string",
},
default: [],
},
summary: {
type: "string",
default: "",
},
qualityNotes: {
type: "array",
items: {
type: "string",
},
default: [],
},
nextSteps: {
type: "array",
items: {
type: "string",
},
default: [],
},
metadata: {
type: "object",
properties: {},
patternProperties: {
".+": FLEXIBLE_VALUE_SCHEMA,
},
additionalProperties: false,
},
},
};
const AI_VALIDATION_TEXT_FORMAT = {
type: "json_schema",
name: AI_VALIDATION_SCHEMA_NAME,
strict: true,
schema: AI_VALIDATION_JSON_SCHEMA,
};
// Debug endpoint for viewing prompt
router.post("/debug", async (req, res) => {
try {
@@ -139,6 +254,12 @@ router.post("/debug", async (req, res) => {
code: error.code || null,
name: error.name || null
});
} finally {
try {
await closeAllConnections();
} catch (closeError) {
console.error("⚠️ Failed to close DB connections after debug request:", closeError);
}
}
});
@@ -402,8 +523,9 @@ async function generateDebugResponse(productsToUse, res) {
console.log("Sending response with taxonomy stats:", response.taxonomyStats);
return response;
} finally {
if (promptConnection) await promptConnection.end();
} catch (promptLoadError) {
console.error("Error loading prompt:", promptLoadError);
throw promptLoadError;
}
} catch (error) {
console.error("Error generating debug response:", error);
@@ -883,34 +1005,80 @@ router.post("/validate", async (req, res) => {
console.log("🔄 Loading prompt with filtered taxonomy...");
const promptData = await loadPrompt(connection, products, req.app.locals.pool);
const fullUserPrompt = promptData.userContent + "\n" + JSON.stringify(products);
const promptLength = promptData.systemInstructions.length + fullUserPrompt.length; // Store prompt length for performance metrics
promptLength = promptData.systemInstructions.length + fullUserPrompt.length; // Store prompt length for performance metrics
console.log("📝 Generated prompt length:", promptLength);
console.log("📝 System instructions length:", promptData.systemInstructions.length);
console.log("📝 User content length:", fullUserPrompt.length);
console.log("🤖 Sending request to OpenAI...");
const completion = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
console.log("🤖 Sending request to OpenAI Responses API...");
// GPT-5 Responses API Configuration:
// - Using "gpt-5" (reasoning model) for complex product validation
// - reasoning.effort: "medium" balances quality and speed (minimal, low, medium, high)
// - text.verbosity: "medium" provides balanced output detail (low, medium, high)
// - max_output_tokens: 20000 ensures space for large product batches
// Note: Responses API is the recommended endpoint for GPT-5 models
const completion = await createResponsesCompletion({
model: "gpt-5",
input: [
{
role: "system",
content: promptData.systemInstructions,
role: "developer",
content: `${promptData.systemInstructions}\n\nYou MUST respond with a single valid JSON object containing the following top-level keys: correctedData, changes, warnings, summary, qualityNotes, nextSteps, metadata.\n- correctedData: array of product objects reflecting the updated data.\n- changes: array of human-readable bullet points summarizing the nature of updates.\n- warnings: array of caveats or risks that still require review.\n- summary: a concise paragraph (<=75 words) describing overall data quality and improvements.\n- qualityNotes: array of short comments (<=40 words each) about validation quality or notable observations.\n- nextSteps: array of recommended manual follow-up actions (if none, provide an empty array).\n- metadata: object containing any supplemental machine-readable information (optional fields allowed).\nDo NOT include Markdown code fences or any text outside the JSON object.`,
},
{
role: "user",
content: fullUserPrompt,
},
],
temperature: 0.2,
response_format: { type: "json_object" },
reasoning: {
effort: "medium"
},
text: {
verbosity: "medium",
format: AI_VALIDATION_TEXT_FORMAT,
},
max_output_tokens: 20000,
});
console.log("✅ Received response from OpenAI");
const rawResponse = completion.choices[0].message.content;
console.log("📄 Raw AI response length:", rawResponse.length);
console.log("✅ Received response from OpenAI Responses API");
// Responses API structure: response has 'output' array with message objects
const rawResponse = extractResponseText(completion);
console.log("📄 Raw AI response length:", rawResponse ? rawResponse.length : 0);
if (!rawResponse) {
throw new Error("OpenAI response did not include any text output");
}
const responseModel = completion.model;
const usage = completion.usage || {};
// GPT-5 Responses API provides detailed token usage including reasoning tokens
const tokenUsageSummary = {
prompt: usage.input_tokens ?? usage.prompt_tokens ?? null,
completion: usage.output_tokens ?? usage.completion_tokens ?? null,
total: usage.total_tokens ?? null,
// GPT-5 reasoning tokens are in output_tokens_details
reasoning: usage.output_tokens_details?.reasoning_tokens ?? usage.completion_tokens_details?.reasoning_tokens ?? null,
// Also capture text generation tokens separately from reasoning
textGeneration: usage.output_tokens_details?.text_generation_tokens ?? usage.completion_tokens_details?.text_generation_tokens ?? null,
cachedPrompt: usage.input_tokens_details?.cached_tokens ?? usage.prompt_tokens_details?.cached_tokens ?? null,
// Capture audio tokens if present (future GPT-5 feature)
audioTokens: usage.output_tokens_details?.audio_tokens ?? usage.completion_tokens_details?.audio_tokens ?? null,
};
// Extract reasoning_effort and verbosity that were actually applied
const reasoningEffortApplied = completion.reasoning?.effort || "medium";
const verbosityApplied = completion.text?.verbosity || "medium";
console.log("📊 Token usage summary:", tokenUsageSummary);
console.log("🤖 Model dispatched:", responseModel);
console.log("🧠 Reasoning effort applied:", reasoningEffortApplied);
console.log("📝 Verbosity applied:", verbosityApplied);
try {
const aiResponse = JSON.parse(rawResponse);
const normalizedResponse = normalizeJsonResponse(rawResponse);
const aiResponse = JSON.parse(normalizedResponse);
console.log(
"🔄 Parsed AI response with keys:",
Object.keys(aiResponse)
@@ -975,7 +1143,12 @@ router.post("/validate", async (req, res) => {
const endTime = new Date();
let performanceMetrics = {
promptLength,
productCount: products.length
productCount: products.length,
model: responseModel,
tokenUsage: tokenUsageSummary,
reasoningTokens: tokenUsageSummary.reasoning,
reasoningEffort: reasoningEffortApplied,
verbosity: verbosityApplied,
};
try {
@@ -1040,6 +1213,11 @@ router.post("/validate", async (req, res) => {
let promptSources = null;
try {
// Use the local PostgreSQL pool from the app
const pool = req.app.locals.pool;
if (!pool) {
console.warn("⚠️ Local database pool not available for prompt sources");
} else {
// Get system prompt
const systemPromptResult = await pool.query(`
SELECT * FROM ai_prompts WHERE prompt_type = 'system'
@@ -1070,25 +1248,14 @@ router.post("/validate", async (req, res) => {
companyPrompts = companyPromptsResult.rows;
}
// Find company names from taxonomy for the validation endpoint
const companyPromptsWithNames = companyPrompts.map(prompt => {
let companyName = "Unknown Company";
if (taxonomy.companies && Array.isArray(taxonomy.companies)) {
const companyData = taxonomy.companies.find(company =>
String(company[0]) === String(prompt.company)
);
if (companyData && companyData[1]) {
companyName = companyData[1];
}
}
return {
// Format company prompts for response
// Note: Company names would require re-fetching taxonomy data
// For now, we include company ID only
const companyPromptsWithNames = companyPrompts.map(prompt => ({
id: prompt.id,
company: prompt.company,
companyName: companyName,
prompt_text: prompt.prompt_text
};
});
}));
// Set prompt sources
if (generalPromptResult.rows.length > 0) {
@@ -1118,6 +1285,7 @@ router.post("/validate", async (req, res) => {
companyPrompts: companyPromptsWithNames
};
}
}
} catch (promptSourceError) {
console.error("⚠️ Error getting prompt sources:", promptSourceError);
// Don't fail the entire validation if just prompt sources retrieval fails
@@ -1126,16 +1294,26 @@ router.post("/validate", async (req, res) => {
// Include prompt sources in the response
res.json({
success: true,
changeDetails: changeDetails,
performanceMetrics: performanceMetrics || {
...aiResponse,
changeDetails,
performanceMetrics:
performanceMetrics || {
// Fallback: calculate a simple estimate
promptLength: promptLength,
promptLength,
processingTimeSeconds: Math.max(15, Math.round(promptLength / 1000)),
isEstimate: true,
productCount: products.length
productCount: products.length,
model: responseModel,
tokenUsage: tokenUsageSummary,
reasoningTokens: tokenUsageSummary.reasoning,
reasoningEffort: reasoningEffortApplied,
verbosity: verbosityApplied,
},
promptSources: promptSources,
...aiResponse,
promptSources,
model: responseModel,
tokenUsage: tokenUsageSummary,
reasoningEffort: reasoningEffortApplied,
verbosity: verbosityApplied,
});
} catch (parseError) {
console.error("❌ Error parsing AI response:", parseError);
@@ -1151,10 +1329,6 @@ router.post("/validate", async (req, res) => {
success: false,
error: "OpenAI API Error: " + openaiError.message,
});
} finally {
// Clean up database connection and SSH tunnel
if (connection) await connection.end();
if (ssh) ssh.end();
}
} catch (error) {
console.error("❌ AI Validation Error:", error);
@@ -1167,6 +1341,12 @@ router.post("/validate", async (req, res) => {
success: false,
error: error.message || "Error during AI validation",
});
} finally {
try {
await closeAllConnections();
} catch (closeError) {
console.error("⚠️ Failed to close DB connections after validation request:", closeError);
}
}
});
@@ -1249,8 +1429,11 @@ router.get("/test-taxonomy", async (req, res) => {
timestamp: new Date().toISOString()
});
} finally {
if (connection) await connection.end();
if (ssh) ssh.end();
try {
await closeAllConnections();
} catch (closeError) {
console.error("⚠️ Failed to close DB connections after test-taxonomy request:", closeError);
}
}
} catch (error) {
console.error("Test taxonomy endpoint error:", error);
@@ -1262,3 +1445,99 @@ router.get("/test-taxonomy", async (req, res) => {
});
module.exports = router;
function extractResponseText(response) {
if (!response) return "";
const outputs = [];
if (Array.isArray(response.output)) {
outputs.push(...response.output);
}
if (Array.isArray(response.outputs)) {
outputs.push(...response.outputs);
}
const segments = outputs.flatMap((output) => collectTextSegments(output?.content ?? output));
if (segments.length === 0 && typeof response.output_text === "string") {
segments.push(response.output_text);
}
if (segments.length === 0 && response.choices?.length) {
segments.push(
...collectTextSegments(response.choices?.[0]?.message?.content)
);
}
const text = segments.join("").trim();
return text;
}
function collectTextSegments(node) {
if (node == null) return [];
if (typeof node === "string" || typeof node === "number" || typeof node === "boolean") {
return [String(node)];
}
if (Array.isArray(node)) {
return node.flatMap(collectTextSegments);
}
if (typeof node !== "object") {
return [];
}
const segments = [];
if (typeof node.text === "string") {
segments.push(node.text);
} else if (Array.isArray(node.text)) {
segments.push(...node.text.flatMap(collectTextSegments));
}
if (typeof node.content === "string") {
segments.push(node.content);
} else if (Array.isArray(node.content)) {
segments.push(...node.content.flatMap(collectTextSegments));
}
if (typeof node.output_text === "string") {
segments.push(node.output_text);
} else if (Array.isArray(node.output_text)) {
segments.push(...node.output_text.flatMap(collectTextSegments));
}
if (typeof node.value === "string") {
segments.push(node.value);
}
if (typeof node.data === "string") {
segments.push(node.data);
}
return segments;
}
function normalizeJsonResponse(text) {
if (!text || typeof text !== 'string') return text;
let cleaned = text.trim();
if (cleaned.startsWith('```')) {
const firstLineBreak = cleaned.indexOf('\n');
if (firstLineBreak !== -1) {
cleaned = cleaned.substring(firstLineBreak + 1);
} else {
cleaned = cleaned.replace(/^```/, '');
}
const closingFenceIndex = cleaned.lastIndexOf('```');
if (closingFenceIndex !== -1) {
cleaned = cleaned.substring(0, closingFenceIndex);
}
cleaned = cleaned.trim();
}
return cleaned;
}

View File

@@ -1043,9 +1043,9 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
"integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1085,9 +1085,9 @@
}
},
"node_modules/@eslint/config-array": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz",
"integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==",
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
"integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -1099,10 +1099,20 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/config-helpers": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz",
"integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/core": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz",
"integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==",
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
"integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -1113,9 +1123,9 @@
}
},
"node_modules/@eslint/eslintrc": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
"integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1150,13 +1160,16 @@
}
},
"node_modules/@eslint/js": {
"version": "9.20.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz",
"integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==",
"version": "9.36.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz",
"integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://eslint.org/donate"
}
},
"node_modules/@eslint/object-schema": {
@@ -1170,32 +1183,19 @@
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz",
"integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==",
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
"integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.10.0",
"@eslint/core": "^0.15.2",
"levn": "^0.4.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz",
"integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
@@ -1296,9 +1296,9 @@
}
},
"node_modules/@humanwhocodes/retry": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz",
"integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==",
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -2559,9 +2559,9 @@
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz",
"integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz",
"integrity": "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==",
"cpu": [
"arm"
],
@@ -2573,9 +2573,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz",
"integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.3.tgz",
"integrity": "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==",
"cpu": [
"arm64"
],
@@ -2587,9 +2587,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz",
"integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.3.tgz",
"integrity": "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==",
"cpu": [
"arm64"
],
@@ -2601,9 +2601,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz",
"integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.3.tgz",
"integrity": "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==",
"cpu": [
"x64"
],
@@ -2615,9 +2615,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz",
"integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.3.tgz",
"integrity": "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==",
"cpu": [
"arm64"
],
@@ -2629,9 +2629,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz",
"integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.3.tgz",
"integrity": "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==",
"cpu": [
"x64"
],
@@ -2643,9 +2643,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz",
"integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.3.tgz",
"integrity": "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==",
"cpu": [
"arm"
],
@@ -2657,9 +2657,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz",
"integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.3.tgz",
"integrity": "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==",
"cpu": [
"arm"
],
@@ -2671,9 +2671,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz",
"integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.3.tgz",
"integrity": "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==",
"cpu": [
"arm64"
],
@@ -2685,9 +2685,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz",
"integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.3.tgz",
"integrity": "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==",
"cpu": [
"arm64"
],
@@ -2698,10 +2698,10 @@
"linux"
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz",
"integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==",
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.3.tgz",
"integrity": "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==",
"cpu": [
"loong64"
],
@@ -2712,10 +2712,10 @@
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz",
"integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==",
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.3.tgz",
"integrity": "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==",
"cpu": [
"ppc64"
],
@@ -2727,9 +2727,23 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz",
"integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.3.tgz",
"integrity": "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.3.tgz",
"integrity": "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==",
"cpu": [
"riscv64"
],
@@ -2741,9 +2755,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz",
"integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.3.tgz",
"integrity": "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==",
"cpu": [
"s390x"
],
@@ -2755,9 +2769,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz",
"integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz",
"integrity": "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==",
"cpu": [
"x64"
],
@@ -2769,9 +2783,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz",
"integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz",
"integrity": "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==",
"cpu": [
"x64"
],
@@ -2782,10 +2796,24 @@
"linux"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.3.tgz",
"integrity": "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz",
"integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.3.tgz",
"integrity": "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==",
"cpu": [
"arm64"
],
@@ -2797,9 +2825,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz",
"integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.3.tgz",
"integrity": "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==",
"cpu": [
"ia32"
],
@@ -2810,10 +2838,24 @@
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz",
"integrity": "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz",
"integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.3.tgz",
"integrity": "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==",
"cpu": [
"x64"
],
@@ -3000,12 +3042,6 @@
"@babel/types": "^7.20.7"
}
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"license": "MIT"
},
"node_modules/@types/d3-array": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
@@ -3070,9 +3106,9 @@
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
},
@@ -3306,9 +3342,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"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": {
@@ -3407,9 +3443,9 @@
}
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
@@ -3577,13 +3613,13 @@
}
},
"node_modules/axios": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
"integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -3652,9 +3688,9 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4873,22 +4909,23 @@
}
},
"node_modules/eslint": {
"version": "9.20.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz",
"integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==",
"version": "9.36.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz",
"integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.19.0",
"@eslint/core": "^0.11.0",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "9.20.0",
"@eslint/plugin-kit": "^0.2.5",
"@eslint/config-array": "^0.21.0",
"@eslint/config-helpers": "^0.3.1",
"@eslint/core": "^0.15.2",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.36.0",
"@eslint/plugin-kit": "^0.3.5",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.1",
"@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"ajv": "^6.12.4",
@@ -4896,9 +4933,9 @@
"cross-spawn": "^7.0.6",
"debug": "^4.3.2",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^8.2.0",
"eslint-visitor-keys": "^4.2.0",
"espree": "^10.3.0",
"eslint-scope": "^8.4.0",
"eslint-visitor-keys": "^4.2.1",
"espree": "^10.4.0",
"esquery": "^1.5.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
@@ -4956,9 +4993,9 @@
}
},
"node_modules/eslint-scope": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
"integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
@@ -4973,9 +5010,9 @@
}
},
"node_modules/eslint-visitor-keys": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"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==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -5019,15 +5056,15 @@
}
},
"node_modules/espree": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.14.0",
"acorn": "^8.15.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.2.0"
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -5331,14 +5368,15 @@
}
},
"node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -5545,9 +5583,9 @@
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -7080,15 +7118,13 @@
}
},
"node_modules/react-router": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.5.tgz",
"integrity": "sha512-8BUF+hZEU4/z/JD201yK6S+UYhsf58bzYIDq2NS1iGpwxSXDu7F+DeGSkIXMFBuHZB21FSiCzEcUb18cQNdRkA==",
"version": "7.9.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.3.tgz",
"integrity": "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg==",
"license": "MIT",
"dependencies": {
"@types/cookie": "^0.6.0",
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0",
"turbo-stream": "2.4.0"
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
@@ -7104,12 +7140,12 @@
}
},
"node_modules/react-router-dom": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.1.5.tgz",
"integrity": "sha512-/4f9+up0Qv92D3bB8iN5P1s3oHAepSGa9h5k6tpTFlixTTskJZwKGhJ6vRJ277tLD1zuaZTt95hyGWV1Z37csQ==",
"version": "7.9.3",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.3.tgz",
"integrity": "sha512-1QSbA0TGGFKTAc/aWjpfW/zoEukYfU4dc1dLkT/vvf54JoGMkW+fNA+3oyo2gWVW1GM7BxjJVHz5GnPJv40rvg==",
"license": "MIT",
"dependencies": {
"react-router": "7.1.5"
"react-router": "7.9.3"
},
"engines": {
"node": ">=20.0.0"
@@ -7331,13 +7367,13 @@
}
},
"node_modules/rollup": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz",
"integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz",
"integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.6"
"@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
@@ -7347,25 +7383,28 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.34.8",
"@rollup/rollup-android-arm64": "4.34.8",
"@rollup/rollup-darwin-arm64": "4.34.8",
"@rollup/rollup-darwin-x64": "4.34.8",
"@rollup/rollup-freebsd-arm64": "4.34.8",
"@rollup/rollup-freebsd-x64": "4.34.8",
"@rollup/rollup-linux-arm-gnueabihf": "4.34.8",
"@rollup/rollup-linux-arm-musleabihf": "4.34.8",
"@rollup/rollup-linux-arm64-gnu": "4.34.8",
"@rollup/rollup-linux-arm64-musl": "4.34.8",
"@rollup/rollup-linux-loongarch64-gnu": "4.34.8",
"@rollup/rollup-linux-powerpc64le-gnu": "4.34.8",
"@rollup/rollup-linux-riscv64-gnu": "4.34.8",
"@rollup/rollup-linux-s390x-gnu": "4.34.8",
"@rollup/rollup-linux-x64-gnu": "4.34.8",
"@rollup/rollup-linux-x64-musl": "4.34.8",
"@rollup/rollup-win32-arm64-msvc": "4.34.8",
"@rollup/rollup-win32-ia32-msvc": "4.34.8",
"@rollup/rollup-win32-x64-msvc": "4.34.8",
"@rollup/rollup-android-arm-eabi": "4.52.3",
"@rollup/rollup-android-arm64": "4.52.3",
"@rollup/rollup-darwin-arm64": "4.52.3",
"@rollup/rollup-darwin-x64": "4.52.3",
"@rollup/rollup-freebsd-arm64": "4.52.3",
"@rollup/rollup-freebsd-x64": "4.52.3",
"@rollup/rollup-linux-arm-gnueabihf": "4.52.3",
"@rollup/rollup-linux-arm-musleabihf": "4.52.3",
"@rollup/rollup-linux-arm64-gnu": "4.52.3",
"@rollup/rollup-linux-arm64-musl": "4.52.3",
"@rollup/rollup-linux-loong64-gnu": "4.52.3",
"@rollup/rollup-linux-ppc64-gnu": "4.52.3",
"@rollup/rollup-linux-riscv64-gnu": "4.52.3",
"@rollup/rollup-linux-riscv64-musl": "4.52.3",
"@rollup/rollup-linux-s390x-gnu": "4.52.3",
"@rollup/rollup-linux-x64-gnu": "4.52.3",
"@rollup/rollup-linux-x64-musl": "4.52.3",
"@rollup/rollup-openharmony-arm64": "4.52.3",
"@rollup/rollup-win32-arm64-msvc": "4.52.3",
"@rollup/rollup-win32-ia32-msvc": "4.52.3",
"@rollup/rollup-win32-x64-gnu": "4.52.3",
"@rollup/rollup-win32-x64-msvc": "4.52.3",
"fsevents": "~2.3.2"
}
},
@@ -7806,6 +7845,54 @@
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tinyglobby/node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -7843,12 +7930,6 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/turbo-stream": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
"license": "ISC"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -8054,15 +8135,18 @@
}
},
"node_modules/vite": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.2.tgz",
"integrity": "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==",
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
"picomatch": "^4.0.2",
"postcss": "^8.5.3",
"rollup": "^4.30.1"
"rollup": "^4.34.9",
"tinyglobby": "^0.2.13"
},
"bin": {
"vite": "bin/vite.js"
@@ -8125,6 +8209,37 @@
}
}
},
"node_modules/vite/node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/vite/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",

View File

@@ -25,6 +25,7 @@ import {
} from "../hooks/useAiValidation";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Protected } from "@/components/auth/Protected";
interface TaxonomyStats {
categories: number;
@@ -96,7 +97,7 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
getFieldDisplayValueWithHighlight,
fields,
}) => {
const [costPerMillionTokens, setCostPerMillionTokens] = useState(2.5); // Default cost
const [costPerMillionTokens, setCostPerMillionTokens] = useState(1.25); // Default cost
// Create our own state to track changes
const [localReversionState, setLocalReversionState] = useState<
@@ -170,8 +171,20 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
return (estimatedTokens / 1_000_000) * costPerMillionTokens * 100; // In cents
};
const formatNumber = (value: number | null | undefined): string => {
if (value === null || value === undefined) {
return "—";
}
return value.toLocaleString();
};
// Use the prompt length from the current prompt
const promptLength = currentPrompt.prompt ? currentPrompt.prompt.length : 0;
const tokenUsage = aiValidationDetails.tokenUsage;
const formattedReasoningEffort = aiValidationDetails.reasoningEffort
? aiValidationDetails.reasoningEffort.charAt(0).toUpperCase() +
aiValidationDetails.reasoningEffort.slice(1)
: null;
return (
<>
@@ -677,30 +690,29 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
aiValidationProgress.estimatedSeconds &&
aiValidationProgress.elapsedSeconds !== undefined &&
aiValidationProgress.step > 0 &&
aiValidationProgress.step < 5 && (
<div className="text-center text-sm">
{(() => {
// Calculate time remaining using the elapsed seconds
const elapsedSeconds =
aiValidationProgress.elapsedSeconds;
const totalEstimatedSeconds =
aiValidationProgress.estimatedSeconds;
aiValidationProgress.step < 5 && (() => {
const elapsedSeconds = aiValidationProgress.elapsedSeconds;
const totalEstimatedSeconds = aiValidationProgress.estimatedSeconds;
const remainingSeconds = Math.max(
0,
totalEstimatedSeconds - elapsedSeconds
);
// Format time remaining
if (remainingSeconds < 60) {
return `Approximately ${Math.round(
remainingSeconds
)} seconds remaining`;
} else {
if (remainingSeconds <= 5) {
return null;
}
const message = remainingSeconds < 60
? `Approximately ${Math.round(remainingSeconds)} seconds remaining`
: (() => {
const minutes = Math.floor(remainingSeconds / 60);
const seconds = Math.round(remainingSeconds % 60);
return `Approximately ${minutes}m ${seconds}s remaining`;
}
})()}
})();
return (
<div className="text-center text-sm">
{message}
{aiValidationProgress.promptLength && (
<p className="mt-1 text-xs text-muted-foreground">
Prompt length:{" "}
@@ -709,7 +721,8 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
</p>
)}
</div>
)
);
})()
);
})()}
</div>
@@ -723,13 +736,139 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
setAiValidationDetails((prev) => ({ ...prev, isOpen: open }))
}
>
<DialogContent className="max-w-6xl w-[90vw]">
<DialogContent className="max-w-6xl w-[90vw] max-h-[90vh] overflow-auto">
<DialogHeader>
<DialogTitle>AI Validation Results</DialogTitle>
<DialogDescription>
Review the changes and warnings suggested by the AI
</DialogDescription>
</DialogHeader>
<Protected permission="admin:debug">
{(aiValidationDetails.model || tokenUsage || formattedReasoningEffort) && (
<div className="mb-4 rounded-md border bg-muted/40 p-4 text-sm">
<div className="flex flex-wrap items-center gap-2">
{aiValidationDetails.model && (
<Badge variant="outline">
Model · {aiValidationDetails.model}
</Badge>
)}
{formattedReasoningEffort && (
<Badge variant="secondary">
Reasoning {formattedReasoningEffort}
</Badge>
)}
</div>
{tokenUsage && (
<div className="mt-3 grid gap-4 text-xs sm:grid-cols-2 lg:grid-cols-5">
<div>
<span className="block text-muted-foreground">
Prompt tokens
</span>
<span className="font-medium">
{formatNumber(tokenUsage.prompt)}
</span>
</div>
<div>
<span className="block text-muted-foreground">
Completion tokens
</span>
<span className="font-medium">
{formatNumber(tokenUsage.completion)}
</span>
</div>
<div>
<span className="block text-muted-foreground">
Total tokens
</span>
<span className="font-medium">
{formatNumber(tokenUsage.total)}
</span>
</div>
<div>
<span className="block text-muted-foreground">
Reasoning tokens
</span>
<span className="font-medium">
{formatNumber(tokenUsage.reasoning)}
</span>
</div>
<div>
<span className="block text-muted-foreground">
Cached prompt tokens
</span>
<span className="font-medium">
{formatNumber(tokenUsage.cachedPrompt)}
</span>
</div>
</div>
)}
</div>
)}
</Protected>
{(aiValidationDetails.summary ||
(aiValidationDetails.changes && aiValidationDetails.changes.length > 0) ||
(aiValidationDetails.warnings && aiValidationDetails.warnings.length > 0) ||
(aiValidationDetails.qualityNotes && aiValidationDetails.qualityNotes.length > 0) ||
(aiValidationDetails.nextSteps && aiValidationDetails.nextSteps.length > 0)) && (
<Card className="mb-4 max-h-[25vh] overflow-auto">
<CardHeader className="pb-2">
<CardTitle className="text-base">Overall Assessment</CardTitle>
</CardHeader>
<CardContent className="space-y-3 text-sm text-muted-foreground">
{aiValidationDetails.changes &&
aiValidationDetails.changes.length > 0 && (
<div>
<h4 className="font-medium text-foreground mb-1">
Key Changes
</h4>
<ul className="list-disc pl-5 space-y-1">
{aiValidationDetails.changes.map((change, idx) => (
<li key={`change-${idx}`}>{change}</li>
))}
</ul>
</div>
)}
{aiValidationDetails.warnings &&
aiValidationDetails.warnings.length > 0 && (
<div>
<h4 className="font-medium text-foreground mb-1">
Warnings
</h4>
<ul className="list-disc pl-5 space-y-1">
{aiValidationDetails.warnings.map((warning, idx) => (
<li key={`warning-${idx}`}>{warning}</li>
))}
</ul>
</div>
)}
{aiValidationDetails.summary && (
<p className="leading-relaxed">{aiValidationDetails.summary}</p>
)}
{aiValidationDetails.qualityNotes &&
aiValidationDetails.qualityNotes.length > 0 && (
<div>
<h4 className="font-medium text-foreground mb-1">Quality Notes</h4>
<ul className="list-disc pl-5 space-y-1">
{aiValidationDetails.qualityNotes.map((note, idx) => (
<li key={`quality-note-${idx}`}>{note}</li>
))}
</ul>
</div>
)}
{aiValidationDetails.nextSteps &&
aiValidationDetails.nextSteps.length > 0 && (
<div>
<h4 className="font-medium text-foreground mb-1">Suggested Next Steps</h4>
<ul className="list-disc pl-5 space-y-1">
{aiValidationDetails.nextSteps.map((step, idx) => (
<li key={`next-step-${idx}`}>{step}</li>
))}
</ul>
</div>
)}
</CardContent>
</Card>
)}
<ScrollArea className="max-h-[70vh]">
{aiValidationDetails.changeDetails &&
aiValidationDetails.changeDetails.length > 0 ? (
@@ -855,23 +994,7 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
</div>
) : (
<div className="py-8 text-center text-muted-foreground">
{aiValidationDetails.warnings &&
aiValidationDetails.warnings.length > 0 ? (
<div>
<p className="mb-4">
No changes were made, but the AI provided some warnings:
</p>
<ul className="list-disc pl-8 text-left">
{aiValidationDetails.warnings.map((warning, i) => (
<li key={`warning-${i}`} className="mb-2">
{warning}
</li>
))}
</ul>
</div>
) : (
<p>No changes or warnings were suggested by the AI.</p>
)}
<p>No field-level changes were suggested by the AI.</p>
</div>
)}
</ScrollArea>

View File

@@ -20,12 +20,28 @@ export interface ProductChangeDetail {
changes: ChangeDetail[];
}
export type ReasoningEffortLevel = 'minimal' | 'low' | 'medium' | 'high';
export interface AiValidationTokenUsage {
prompt: number | null;
completion: number | null;
total: number | null;
reasoning?: number | null;
cachedPrompt?: number | null;
}
export interface AiValidationDetails {
changes: string[];
warnings: string[];
changeDetails: ProductChangeDetail[];
isOpen: boolean;
originalData?: RowData<string>[];
originalData?: RowData<any>[];
model?: string;
reasoningEffort?: ReasoningEffortLevel;
tokenUsage?: AiValidationTokenUsage;
summary?: string;
qualityNotes?: string[];
nextSteps?: string[];
}
export interface AiValidationProgress {
@@ -37,8 +53,119 @@ export interface AiValidationProgress {
promptLength?: number;
elapsedSeconds?: number;
progressPercent?: number;
model?: string;
reasoningEffort?: ReasoningEffortLevel;
tokenUsage?: AiValidationTokenUsage;
}
const toNumberOrNull = (value: unknown): number | null => {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'string') {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
};
const normalizeReasoningEffort = (
value: unknown
): ReasoningEffortLevel | undefined => {
if (typeof value !== 'string') return undefined;
const normalized = value.toLowerCase();
if (
normalized === 'minimal' ||
normalized === 'low' ||
normalized === 'medium' ||
normalized === 'high'
) {
return normalized as ReasoningEffortLevel;
}
return undefined;
};
const isRecord = (value: unknown): value is Record<string, any> => {
return value !== null && typeof value === 'object' && !Array.isArray(value);
};
const valuesAreEqual = (a: unknown, b: unknown): boolean => {
if (a === b) {
// Handle NaN equality manually
return a === a || b === b;
}
try {
return JSON.stringify(a) === JSON.stringify(b);
} catch {
return false;
}
};
const buildChangeDetailsFromData = <T extends string>(
originalRows: RowData<T>[],
correctedRows: unknown[]
): ProductChangeDetail[] => {
if (!Array.isArray(correctedRows)) {
return [];
}
const details: ProductChangeDetail[] = [];
const maxLength = Math.max(originalRows.length, correctedRows.length);
for (let index = 0; index < maxLength; index++) {
const original = (originalRows[index] as Record<string, any> | undefined) ?? undefined;
const correctedCandidate = correctedRows[index];
if (!isRecord(correctedCandidate)) {
continue;
}
const keys = new Set<string>([
...Object.keys(original ?? {}),
...Object.keys(correctedCandidate),
]);
const changes: ChangeDetail[] = [];
keys.forEach((key) => {
if (key.startsWith('__')) {
return;
}
const originalValue = original ? original[key] : undefined;
const correctedValue = correctedCandidate[key];
if (!valuesAreEqual(originalValue, correctedValue)) {
changes.push({
field: key,
original: originalValue,
corrected: correctedValue,
});
}
});
if (changes.length === 0) {
continue;
}
const titleCandidate =
(original && (original.name ?? original.title)) ??
(correctedCandidate.name ?? correctedCandidate.title);
details.push({
productIndex: index,
title:
typeof titleCandidate === 'string'
? titleCandidate
: `Product ${index + 1}`,
changes,
});
}
return details;
};
export interface CurrentPrompt {
isOpen: boolean;
prompt: string | null;
@@ -374,6 +501,9 @@ export const useAiValidation = <T extends string>(
startTime,
elapsedSeconds: 0,
progressPercent: 0,
model: undefined,
reasoningEffort: undefined,
tokenUsage: undefined,
...(aiValidationProgress.estimatedSeconds ? {
estimatedSeconds: aiValidationProgress.estimatedSeconds,
promptLength: aiValidationProgress.promptLength
@@ -476,14 +606,25 @@ export const useAiValidation = <T extends string>(
promptLength: prev.promptLength
}));
const response = await fetch(`${getApiUrl()}/ai-validation/validate`, {
const requestBody = JSON.stringify({ products: cleanedData });
const fetchPromise = fetch(`${getApiUrl()}/ai-validation/validate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ products: cleanedData }),
body: requestBody,
});
setAiValidationProgress(prev => ({
...prev,
status: "Waiting on AI response...",
step: Math.max(prev.step, 2),
estimatedSeconds: prev.estimatedSeconds,
promptLength: prev.promptLength
}));
const response = await fetchPromise;
if (!response.ok) {
const errorText = await response.text();
console.error('AI validation error response:', {
@@ -501,6 +642,111 @@ export const useAiValidation = <T extends string>(
throw new Error(result.error || 'AI validation failed');
}
const usageSource: any =
result.tokenUsage ||
result.performanceMetrics?.tokenUsage ||
result.usage;
const modelUsed: string | undefined =
typeof result.model === 'string'
? result.model
: typeof result.modelName === 'string'
? result.modelName
: typeof result.modelId === 'string'
? result.modelId
: typeof result.performanceMetrics?.model === 'string'
? result.performanceMetrics.model
: undefined;
const reasoningEffort = normalizeReasoningEffort(
result.reasoningEffort ||
result.reasoning_effort ||
result.performanceMetrics?.reasoningEffort
);
let normalizedTokenUsage: AiValidationTokenUsage | undefined;
if (usageSource) {
const promptTokensRaw =
usageSource.prompt ??
usageSource.promptTokens ??
usageSource.prompt_tokens ??
usageSource.input;
const completionTokensRaw =
usageSource.completion ??
usageSource.completionTokens ??
usageSource.completion_tokens ??
usageSource.output;
const totalTokensRaw =
usageSource.total ??
usageSource.totalTokens ??
usageSource.total_tokens ??
usageSource.tokenTotal;
const reasoningTokensRaw =
usageSource.reasoning ??
usageSource.reasoningTokens ??
usageSource.reasoning_tokens;
const cachedPromptRaw =
usageSource.cachedPrompt ??
usageSource.cachedTokens ??
usageSource.cached_prompt ??
usageSource.cached_tokens ??
usageSource.cached;
const promptTokens = toNumberOrNull(promptTokensRaw);
const completionTokens = toNumberOrNull(completionTokensRaw);
let totalTokens = toNumberOrNull(totalTokensRaw);
if (
totalTokens === null &&
promptTokens !== null &&
completionTokens !== null
) {
totalTokens = promptTokens + completionTokens;
}
const reasoningTokens = toNumberOrNull(reasoningTokensRaw);
const cachedPrompt = toNumberOrNull(cachedPromptRaw);
if (
promptTokens !== null ||
completionTokens !== null ||
totalTokens !== null ||
reasoningTokens !== null ||
cachedPrompt !== null
) {
normalizedTokenUsage = {
prompt: promptTokens,
completion: completionTokens,
total: totalTokens,
reasoning: reasoningTokens,
cachedPrompt,
};
}
}
const changesFromResult = Array.isArray(result.changes)
? result.changes
: [];
const warningsFromResult = Array.isArray(result.warnings)
? result.warnings
: [];
const summaryFromResult = typeof result.summary === 'string'
? result.summary
: typeof result.overallSummary === 'string'
? result.overallSummary
: '';
const qualityNotesFromResult = Array.isArray(result.qualityNotes)
? result.qualityNotes
: Array.isArray(result.quality_notes)
? result.quality_notes
: Array.isArray(result.qualitySummary)
? result.qualitySummary
: [];
const nextStepsFromResult = Array.isArray(result.nextSteps)
? result.nextSteps
: Array.isArray(result.recommendedNextSteps)
? result.recommendedNextSteps
: Array.isArray(result.followUpActions)
? result.followUpActions
: [];
// Store the prompt sources if they exist
if (result.promptSources) {
setCurrentPrompt(prev => {
@@ -534,14 +780,23 @@ export const useAiValidation = <T extends string>(
result.performanceMetrics.processingTimeSeconds ||
prev.estimatedSeconds,
promptLength: result.performanceMetrics.promptLength || prev.promptLength,
progressPercent: 75 // 75% complete when we're processing the AI response
progressPercent: 75, // 75% complete when we're processing the AI response
model: modelUsed ?? prev.model,
reasoningEffort: reasoningEffort ?? prev.reasoningEffort,
tokenUsage:
normalizedTokenUsage ??
result.performanceMetrics.tokenUsage ??
prev.tokenUsage,
}));
} else {
setAiValidationProgress(prev => ({
...prev,
status: "Processing AI response...",
step: 3,
progressPercent: 75
progressPercent: 75,
model: modelUsed ?? prev.model,
reasoningEffort: reasoningEffort ?? prev.reasoningEffort,
tokenUsage: normalizedTokenUsage ?? prev.tokenUsage,
}));
}
@@ -620,7 +875,9 @@ export const useAiValidation = <T extends string>(
console.log('About to update data with AI corrections:', {
originalDataSample: data.slice(0, 2),
processedDataSample: processedData.slice(0, 2),
correctionCount: result.changes?.length || 0
correctionCount: result.changes?.length || 0,
changeDetailCount: 0,
changeDetailsPreview: []
});
// First validate the new data to ensure all validation rules are applied
@@ -644,27 +901,63 @@ export const useAiValidation = <T extends string>(
hasErrors: false // We no longer check row.__errors
});
// Show changes and warnings in dialog after data is updated
setAiValidationDetails({
changes: result.changes || [],
warnings: result.warnings || [],
changeDetails: result.changeDetails || [],
isOpen: true,
originalData: originalDataCopy // Use the stored original data
const changeDetailsForDialog = buildChangeDetailsFromData(
originalDataCopy,
validatedData as RowData<T>[]
);
console.log('Computed change details after validation:', {
changeDetailCount: changeDetailsForDialog.length,
changeDetailsPreview: changeDetailsForDialog.slice(0, 2)
});
const detailPayload: AiValidationDetails = {
changes: changesFromResult,
warnings: warningsFromResult,
changeDetails: changeDetailsForDialog,
isOpen: true,
originalData: originalDataCopy,
model: modelUsed,
reasoningEffort,
tokenUsage: normalizedTokenUsage,
summary: summaryFromResult,
qualityNotes: qualityNotesFromResult,
nextSteps: nextStepsFromResult,
};
// Show changes and warnings in dialog after data is updated
setAiValidationDetails(detailPayload);
} catch (error) {
console.error('Error validating AI corrections:', error);
// Fall back to basic update without validation
setData(processedData);
// Still show the result dialog even if validation failed
setAiValidationDetails({
changes: result.changes || [],
warnings: result.warnings || [],
changeDetails: result.changeDetails || [],
isOpen: true,
originalData: originalDataCopy, // Use the stored original data
const changeDetailsForDialog = buildChangeDetailsFromData(
originalDataCopy,
processedData as RowData<T>[]
);
console.log('Computed change details after fallback:', {
changeDetailCount: changeDetailsForDialog.length,
changeDetailsPreview: changeDetailsForDialog.slice(0, 2)
});
const detailPayload: AiValidationDetails = {
changes: changesFromResult,
warnings: warningsFromResult,
changeDetails: changeDetailsForDialog,
isOpen: true,
originalData: originalDataCopy,
model: modelUsed,
reasoningEffort,
tokenUsage: normalizedTokenUsage,
summary: summaryFromResult,
qualityNotes: qualityNotesFromResult,
nextSteps: nextStepsFromResult,
};
// Still show the result dialog even if validation failed
setAiValidationDetails(detailPayload);
}
}
@@ -673,7 +966,10 @@ export const useAiValidation = <T extends string>(
status: "Validation complete!",
step: 5,
estimatedSeconds: prev.estimatedSeconds,
promptLength: prev.promptLength
promptLength: prev.promptLength,
model: modelUsed ?? prev.model,
reasoningEffort: reasoningEffort ?? prev.reasoningEffort,
tokenUsage: normalizedTokenUsage ?? prev.tokenUsage,
}));
setTimeout(() => {

View File

@@ -294,12 +294,12 @@ export const useValidationState = <T extends string>({
// Track initialization task statuses for the progress UI
const initializationTasks = {
upcValidation: {
label: 'Validating UPCs and generating item numbers',
label: 'Generating item numbers',
status: upcValidation.initialValidationDone ? 'completed' :
hasPendingUpcValidation ? 'in_progress' : 'pending'
},
fieldValidation: {
label: 'Validating field requirements and formats',
label: 'Checking for field errors',
status: initialValidationComplete ? 'completed' :
isInitialValidationRunning ? 'in_progress' : 'pending'
},