From a249fb038c6d754193805a78354c07fb3e987f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AF=92=E5=AF=92?= <2596194220@qq.com> Date: Sun, 12 Apr 2026 23:50:54 +0800 Subject: [PATCH] feat: add recipe translation tool with AI translation and settings management - Add Word document parsing and translation export - Add database configuration management - Add AI/LLM translation configuration - Add translation vocabulary management - Add export settings (colors, page orientation, headers) - Separate database and AI configurations - Add debounced search to prevent database overload --- app/api/ai/translate/route.ts | 68 +++ app/api/config/route.ts | 57 +++ app/api/recipe/route.ts | 280 +++++++++++ app/api/translate/route.ts | 48 ++ app/api/words/[id]/route.ts | 65 +++ app/api/words/route.ts | 125 +++++ app/layout.tsx | 9 +- app/page.tsx | 786 +++++++++++++++++++++++++++--- app/providers.tsx | 7 + app/settings/ai/page.tsx | 175 +++++++ app/settings/database/page.tsx | 152 ++++++ app/settings/export/page.tsx | 18 + app/settings/layout.tsx | 103 ++++ app/settings/page.tsx | 20 + app/settings/translation/page.tsx | 460 +++++++++++++++++ components/ui/alert.tsx | 76 +++ components/ui/toast.tsx | 71 +++ config.json | 14 + config/database.example.env | 9 + lib/ai-translate.ts | 139 ++++++ lib/config.ts | 173 +++++++ lib/database.ts | 67 +++ package.json | 6 + pnpm-lock.yaml | 525 +++++++++++++++++++- 24 files changed, 3383 insertions(+), 70 deletions(-) create mode 100644 app/api/ai/translate/route.ts create mode 100644 app/api/config/route.ts create mode 100644 app/api/recipe/route.ts create mode 100644 app/api/translate/route.ts create mode 100644 app/api/words/[id]/route.ts create mode 100644 app/api/words/route.ts create mode 100644 app/providers.tsx create mode 100644 app/settings/ai/page.tsx create mode 100644 app/settings/database/page.tsx create mode 100644 app/settings/export/page.tsx create mode 100644 app/settings/layout.tsx create mode 100644 app/settings/page.tsx create mode 100644 app/settings/translation/page.tsx create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/toast.tsx create mode 100644 config.json create mode 100644 config/database.example.env create mode 100644 lib/ai-translate.ts create mode 100644 lib/config.ts create mode 100644 lib/database.ts diff --git a/app/api/ai/translate/route.ts b/app/api/ai/translate/route.ts new file mode 100644 index 0000000..c6a8411 --- /dev/null +++ b/app/api/ai/translate/route.ts @@ -0,0 +1,68 @@ +import { NextRequest, NextResponse } from "next/server"; +import { batchTranslateWithAI, translateWithAI } from "@/lib/ai-translate"; + +// 单个翻译 +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const word = searchParams.get("word"); + + if (!word) { + return NextResponse.json({ error: "缺少 word 参数" }, { status: 400 }); + } + + try { + const translation = await translateWithAI(word); + + if (!translation) { + return NextResponse.json({ error: "翻译失败" }, { status: 500 }); + } + + return NextResponse.json({ + word, + english: translation, + }); + } catch (error) { + console.error("AI translation error:", error); + return NextResponse.json( + { error: String(error) }, + { status: 500 } + ); + } +} + +// 批量翻译 +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { words } = body as { words: string[] }; + + if (!Array.isArray(words) || words.length === 0) { + return NextResponse.json({ error: "缺少 words 参数(数组)" }, { status: 400 }); + } + + // 检查 API Key + if (!process.env.AI_API_KEY) { + return NextResponse.json({ error: "AI 配置未完成,请先在设置中配置 AI" }, { status: 500 }); + } + + const results = await batchTranslateWithAI(words); + + // 转换为响应格式 + const translations: Record = {}; + for (const word of words) { + translations[word] = results.get(word) || null; + } + + return NextResponse.json({ + translations, + total: words.length, + success: results.size, + }); + } catch (error) { + console.error("AI batch translation error:", error); + return NextResponse.json( + { error: String(error) }, + { status: 500 } + ); + } +} diff --git a/app/api/config/route.ts b/app/api/config/route.ts new file mode 100644 index 0000000..9d4b50d --- /dev/null +++ b/app/api/config/route.ts @@ -0,0 +1,57 @@ +import { NextResponse } from "next/server"; +import { getConfig, saveDBConfig, saveAIConfig, type DBConfig, type AIConfig } from "@/lib/config"; + +// GET /api/config - 获取完整配置 +export async function GET() { + try { + const config = getConfig(); + return NextResponse.json({ + success: true, + data: config, + }); + } catch (error) { + return NextResponse.json( + { success: false, error: "读取配置失败" }, + { status: 500 } + ); + } +} + +// POST /api/config - 保存配置 +// Body 格式: { db?: DBConfig, ai?: AIConfig } +export async function POST(request: Request) { + try { + const body = await request.json(); + const { db, ai } = body; + + if (db) { + saveDBConfig(db); + } + + if (ai) { + saveAIConfig(ai); + } + + // 如果同时发送了完整配置 + if (!db && !ai && body.db === undefined && body.ai === undefined) { + // 可能是旧的扁平格式,尝试兼容 + if (body.apiKey !== undefined || body.baseUrl !== undefined || body.model !== undefined) { + saveAIConfig({ + apiKey: body.apiKey, + baseUrl: body.baseUrl, + model: body.model, + }); + } + } + + return NextResponse.json({ + success: true, + message: "配置已保存", + }); + } catch (error) { + return NextResponse.json( + { success: false, error: "保存失败" }, + { status: 500 } + ); + } +} diff --git a/app/api/recipe/route.ts b/app/api/recipe/route.ts new file mode 100644 index 0000000..0ed5c67 --- /dev/null +++ b/app/api/recipe/route.ts @@ -0,0 +1,280 @@ +import { NextRequest, NextResponse } from "next/server"; +import mammoth from "mammoth"; +import * as cheerio from "cheerio"; + +// 餐食类型 +enum MealTypeCode { + BREAKFAST = "breakfast", + FRUIT = "fruit", + LUNCH = "lunch", + SNACK = "snack", + SPECIAL = "special", +} + +// 语言名称项 +interface LangName { + zh: string; + en?: string; +} + +// 单个菜品 +interface Dish { + id: string; + name: LangName; +} + +// 单日食谱 +interface DailyMeal { + dayCode: number; + mealCode: MealTypeCode; + dishList: Dish[]; +} + +// 一周食谱 +interface WeeklyMenu { + menuId: string; + weekNumber: string; + title: LangName; + days: { code: number; name: LangName }[]; + mealCategories: { code: MealTypeCode; name: LangName; sort: number }[]; + meals: DailyMeal[]; +} + +// 餐食类型配置 +const MEAL_TYPES = [ + { code: MealTypeCode.BREAKFAST, label: "早餐" }, + { code: MealTypeCode.FRUIT, label: "水果餐" }, + { code: MealTypeCode.LUNCH, label: "中餐" }, + { code: MealTypeCode.SPECIAL, label: "体弱儿专属" }, + { code: MealTypeCode.SNACK, label: "午点" }, +]; + +const WEEK_DAYS = [ + { code: 1, label: "周一" }, + { code: 2, label: "周二" }, + { code: 3, label: "周三" }, + { code: 4, label: "周四" }, + { code: 5, label: "周五" }, +]; + +/** + * 使用 Cheerio 从 HTML 中提取表格原始数据 + */ +function extractRawDataFromHtml(html: string): string[][][] { + const tableData: string[][][] = []; + const $ = cheerio.load(html); + + const rows = $("table tr"); + + rows.each((rowIndex, row) => { + const rowData: string[][] = []; + const cells = $(row).find("td"); + + cells.each((cellIndex, cell) => { + // 直接获取 HTML 并处理 + const rawHtml = $(cell).html() || ""; + + // 替换换行标签为真实换行符 + const processed = rawHtml + .replace(/<\/p>]*>/g, "\n") //

→ 换行 + .replace(//gi, "\n") + .replace(/ /g, " "); + + // 移除剩余 HTML 标签 + const text = processed.replace(/<[^>]+>/g, ""); + + // 按换行分割 + const lines = text.split("\n").filter((line) => line.trim() !== ""); + + // 对每行应用顿号/逗号/分号分割 + const finalItems: string[] = []; + lines.forEach((line) => { + finalItems.push(...splitCellContent(line)); + }); + + rowData.push(finalItems); + }); + + if (rowData.length > 0) { + tableData.push(rowData); + } + }); + + return tableData; +} + +/** + * 分割单元格内容为菜品列表 + * 支持分隔符:顿号(、)、逗号(,)、分号(;) + */ +function splitCellContent(content: string): string[] { + if (!content || content.trim() === "") return []; + + // 统一分隔符 + const normalized = content + .replace(/;/g, "|") + .replace(/,/g, "|") + .replace(/、/g, "|") + .trim(); + + const parts = normalized.split("|"); + + return parts + .map((item) => item.trim()) + .filter(Boolean); +} + +/** + * 确定餐食类型 + */ +function getMealType(mealName: string): MealTypeCode | null { + if (mealName.includes("早")) return MealTypeCode.BREAKFAST; + if (mealName.includes("水果")) return MealTypeCode.FRUIT; + if (mealName.includes("中") || mealName.includes("午餐")) return MealTypeCode.LUNCH; + if (mealName.includes("午点") || mealName.includes("点心")) return MealTypeCode.SNACK; + if (mealName.includes("体弱") || mealName.includes("晚")) return MealTypeCode.SPECIAL; + return null; +} + +/** + * 从表格数据转换为一周食谱结构 + */ +function transformTableToWeeklyMenu(tableData: string[][][]): WeeklyMenu { + const meals: DailyMeal[] = []; + + // 校验表格数据完整性 + if (tableData.length < 2 || tableData[0].length < 2) { + console.warn("警告:表格数据不完整"); + return createEmptyMenu(); + } + + // 提取表头数据(星期几) + const weekdays = tableData[0].slice(1).map((cell) => cell[0] || ""); + + // 提取餐型行数据(从第二行开始) + const mealTypeRows = tableData.slice(1); + + // 遍历每一天(列) + for (let dayIndex = 0; dayIndex < weekdays.length; dayIndex++) { + const dayName = weekdays[dayIndex]; + if (!dayName) continue; + + const columnIndex = dayIndex + 1; // 对应星期几的列索引 + + // 遍历每一行餐型 + for (const row of mealTypeRows) { + const mealName = row[0]?.[0] || ""; // 餐型名称 + if (!mealName) continue; + + const mealType = getMealType(mealName); + if (!mealType) continue; + + // 获取当前餐型的菜品列表 + const mealItems = row[columnIndex] || []; + + // 添加到当天餐食列表 + meals.push({ + dayCode: dayIndex + 1, + mealCode: mealType, + dishList: mealItems.map((item, idx) => ({ + id: `${dayIndex + 1}-${mealType}-${idx + 1}`, + name: { zh: item }, + })), + }); + } + } + + // 确保每天每餐都有记录 + for (const day of WEEK_DAYS) { + for (const meal of MEAL_TYPES) { + const exists = meals.find( + (m) => m.dayCode === day.code && m.mealCode === meal.code + ); + if (!exists) { + meals.push({ dayCode: day.code, mealCode: meal.code, dishList: [] }); + } + } + } + + // 构建 days 数组,使用实际解析出的星期 + const days = weekdays + .filter(Boolean) + .map((name, index) => ({ code: index + 1, name: { zh: name } })); + + return { + menuId: `menu-${Date.now()}`, + weekNumber: "", + title: { zh: "食谱", en: "Recipe" }, + days: days, + mealCategories: MEAL_TYPES.map((m, i) => ({ + code: m.code, + name: { zh: m.label }, + sort: i + 1, + })), + meals: meals.sort((a, b) => { + if (a.dayCode !== b.dayCode) return a.dayCode - b.dayCode; + const orderMap: Record = { + [MealTypeCode.BREAKFAST]: 0, + [MealTypeCode.FRUIT]: 1, + [MealTypeCode.LUNCH]: 2, + [MealTypeCode.SNACK]: 3, + [MealTypeCode.SPECIAL]: 4, + }; + return orderMap[a.mealCode] - orderMap[b.mealCode]; + }), + }; +} + +function createEmptyMenu(): WeeklyMenu { + return { + menuId: `menu-${Date.now()}`, + weekNumber: "", + title: { zh: "食谱", en: "Recipe" }, + days: WEEK_DAYS.map((d) => ({ code: d.code, name: { zh: d.label } })), + mealCategories: MEAL_TYPES.map((m, i) => ({ + code: m.code, + name: { zh: m.label }, + sort: i + 1, + })), + meals: [], + }; +} + +export async function POST(request: NextRequest) { + try { + const formData = await request.formData(); + const file = formData.get("file") as File | null; + + if (!file) { + return NextResponse.json({ error: "未上传文件" }, { status: 400 }); + } + + if (!file.name.endsWith(".docx")) { + return NextResponse.json({ error: "仅支持 .docx 格式" }, { status: 400 }); + } + + // 读取文件为 ArrayBuffer + const arrayBuffer = await file.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + // 使用 mammoth 转换为 HTML(保留表格结构) + const { value: htmlContent } = await mammoth.convertToHtml({ buffer }); + + // 使用 cheerio 提取表格数据 + const tableData = extractRawDataFromHtml(htmlContent); + + // 转换为结构化数据 + const weeklyMenu = transformTableToWeeklyMenu(tableData); + + return NextResponse.json({ + success: true, + data: weeklyMenu, + }); + } catch (error) { + console.error("Parse error:", error); + return NextResponse.json( + { error: "文件解析失败", details: String(error) }, + { status: 500 } + ); + } +} diff --git a/app/api/translate/route.ts b/app/api/translate/route.ts new file mode 100644 index 0000000..d2e61ba --- /dev/null +++ b/app/api/translate/route.ts @@ -0,0 +1,48 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getTranslation, getTranslations } from "@/lib/database"; + +// 单个翻译查询 +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const chineseName = searchParams.get("name"); + + if (!chineseName) { + return NextResponse.json({ error: "缺少 name 参数" }, { status: 400 }); + } + + const translation = await getTranslation(chineseName); + + return NextResponse.json({ + chinese: chineseName, + english: translation, + found: translation !== null, + }); +} + +// 批量翻译查询 +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { names } = body as { names: string[] }; + + if (!names || !Array.isArray(names)) { + return NextResponse.json({ error: "缺少 names 参数(数组)" }, { status: 400 }); + } + + const translations = await getTranslations(names); + + const result: Record = {}; + for (const name of names) { + result[name] = translations.get(name) || null; + } + + return NextResponse.json({ + translations: result, + total: names.length, + found: translations.size, + }); + } catch (error) { + console.error("Translation API error:", error); + return NextResponse.json({ error: "请求格式错误" }, { status: 400 }); + } +} diff --git a/app/api/words/[id]/route.ts b/app/api/words/[id]/route.ts new file mode 100644 index 0000000..8b96b90 --- /dev/null +++ b/app/api/words/[id]/route.ts @@ -0,0 +1,65 @@ +import { NextRequest, NextResponse } from "next/server"; +import pool from "@/lib/database"; + +// 更新词组 +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const body = await request.json(); + const { word, english } = body; + + if (!word || !english) { + return NextResponse.json( + { error: "中文和英文翻译不能为空" }, + { status: 400 } + ); + } + + await pool.execute("UPDATE words SET word = ?, english = ? WHERE id = ?", [ + word.trim(), + english.trim(), + parseInt(id), + ]); + + return NextResponse.json({ + success: true, + data: { + id: parseInt(id), + word: word.trim(), + english: english.trim(), + }, + }); + } catch (error) { + console.error("Database error:", error); + return NextResponse.json( + { error: "更新失败", details: String(error) }, + { status: 500 } + ); + } +} + +// 删除词组 +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + + await pool.execute("DELETE FROM words WHERE id = ?", [parseInt(id)]); + + return NextResponse.json({ + success: true, + message: "删除成功", + }); + } catch (error) { + console.error("Database error:", error); + return NextResponse.json( + { error: "删除失败", details: String(error) }, + { status: 500 } + ); + } +} diff --git a/app/api/words/route.ts b/app/api/words/route.ts new file mode 100644 index 0000000..4e34214 --- /dev/null +++ b/app/api/words/route.ts @@ -0,0 +1,125 @@ +import { NextRequest, NextResponse } from "next/server"; +import pool from "@/lib/database"; + +// 获取所有词组(分页) +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const page = Math.max(1, parseInt(searchParams.get("page") || "1") || 1); + const pageSize = Math.min(100, Math.max(1, parseInt(searchParams.get("pageSize") || "20") || 20)); + const search = searchParams.get("search") || ""; + + try { + const offset = (page - 1) * pageSize; + + // 构建查询条件 + let whereClause = ""; + let params: (string | number)[] = []; + + if (search) { + whereClause = "WHERE word LIKE ? OR english LIKE ?"; + params = [`%${search}%`, `%${search}%`]; + } + + // 获取总数 + const [countResult] = await pool.query( + `SELECT COUNT(*) as total FROM words ${whereClause}`, + params + ); + const total = (countResult as any)[0]?.total || 0; + + // 获取列表 - LIMIT 和 OFFSET 直接使用数字,不使用占位符 + const [rows] = await pool.query( + `SELECT id, word, english, createTime, updateTime FROM words ${whereClause} ORDER BY id DESC LIMIT ${pageSize} OFFSET ${offset}`, + params + ); + + return NextResponse.json({ + success: true, + data: rows, + pagination: { + page, + pageSize, + total, + totalPages: Math.ceil(total / pageSize) || 1, + }, + }); + } catch (error) { + console.error("Database error:", error); + return NextResponse.json( + { error: "获取数据失败", details: String(error) }, + { status: 500 } + ); + } +} + +// 添加词组 +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { word, english } = body; + + if (!word || !english) { + return NextResponse.json( + { error: "中文和英文翻译不能为空" }, + { status: 400 } + ); + } + + const [result] = await pool.execute( + "INSERT INTO words (word, english) VALUES (?, ?)", + [word.trim(), english.trim()] + ); + + const insertResult = result as any; + + return NextResponse.json({ + success: true, + data: { + id: insertResult.insertId, + word: word.trim(), + english: english.trim(), + }, + }); + } catch (error) { + console.error("Database error:", error); + return NextResponse.json( + { error: "添加失败", details: String(error) }, + { status: 500 } + ); + } +} + +// 批量添加词组 +export async function PUT(request: NextRequest) { + try { + const body = await request.json(); + const { words } = body; + + if (!Array.isArray(words) || words.length === 0) { + return NextResponse.json( + { error: "请提供词组数组" }, + { status: 400 } + ); + } + + const values = words.map((w: { word: string; english: string }) => [w.word.trim(), w.english.trim()]); + const placeholders = values.map(() => "(?, ?)").join(", "); + const flatValues = values.flat(); + + await pool.execute( + `INSERT INTO words (word, english) VALUES ${placeholders} ON DUPLICATE KEY UPDATE english = VALUES(english)`, + flatValues + ); + + return NextResponse.json({ + success: true, + message: `成功添加 ${words.length} 个词组`, + }); + } catch (error) { + console.error("Database error:", error); + return NextResponse.json( + { error: "批量添加失败", details: String(error) }, + { status: 500 } + ); + } +} diff --git a/app/layout.tsx b/app/layout.tsx index e159121..f938da0 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono, Inter } from "next/font/google"; import "./globals.css"; import { cn } from "@/lib/utils"; +import Providers from "./providers"; const inter = Inter({subsets:['latin'],variable:'--font-sans'}); @@ -16,8 +17,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "食谱翻译工具 | 食谱解析工具,解放你牛马的双手", + description: "食谱解析工具,解放你牛马的双手", }; export default function RootLayout({ @@ -30,7 +31,9 @@ export default function RootLayout({ lang="en" className={cn("h-full", "antialiased", geistSans.variable, geistMono.variable, "font-sans", inter.variable)} > - {children} + + {children} + ); } diff --git a/app/page.tsx b/app/page.tsx index 3f36f7c..7425a28 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,65 +1,725 @@ -import Image from "next/image"; +"use client"; -export default function Home() { - return ( -

-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
-
- - Vercel logomark - Deploy Now - - - Documentation - -
-
-
- ); +import {useState, useCallback} from "react"; +import { + Upload, + Sun, + Apple, + UtensilsCrossed, + Cookie, + Heart, + Languages, + CheckCircle2, + Loader2, + X, + AlertCircle, + Download, Settings, +} from "lucide-react"; +import Link from 'next/link' +import {Button} from "@/components/ui/button"; +import { + Document, + Packer, + Table, + TableRow, + TableCell, + TextRun, + Paragraph, + AlignmentType, + WidthType, + PageOrientation, + TableLayoutType +} from "docx"; + +// 餐食类型 +enum MealTypeCode { + BREAKFAST = "breakfast", + FRUIT = "fruit", + LUNCH = "lunch", + SNACK = "snack", + SPECIAL = "special", +} + +// 语言名称项 +interface LangName { + zh: string; + en?: string; +} + +// 单个菜品 +interface Dish { + id?: string; + name: LangName; + translated?: boolean; // 是否已翻译 +} + +// 单日食谱 +interface DailyMeal { + dayCode: number; + mealCode: MealTypeCode; + dishList: Dish[]; +} + +// 一周食谱 +interface WeeklyMenu { + menuId?: string; + weekNumber?: string; + title: LangName; + days: { code: number; name: LangName }[]; + mealCategories: { code: MealTypeCode; name: LangName; sort: number }[]; + meals: DailyMeal[]; +} + +// 餐食类型配置 (体弱儿在第四位) +const MEAL_TYPES = [ + {code: MealTypeCode.BREAKFAST, label: "早餐", icon: Sun}, + {code: MealTypeCode.FRUIT, label: "水果餐", icon: Apple}, + {code: MealTypeCode.LUNCH, label: "中餐", icon: UtensilsCrossed}, + {code: MealTypeCode.SPECIAL, label: "体弱儿餐", icon: Heart}, + {code: MealTypeCode.SNACK, label: "午点", icon: Cookie}, +]; + +type UploadState = "idle" | "uploading" | "parsing" | "done" | "error"; +type TranslateState = "idle" | "translating" | "done"; + +/** + * 头部组件 + * */ +function HarderCompose() { + return ( +
+
+
+
+ +
+
+

食谱翻译工具

+

Word 文档解析 · 智能翻译

+
+
+ + + +
+
+ ); +} + +export default function RecipeTranslationPage() { + const [uploadState, setUploadState] = useState("idle"); + const [translateState, setTranslateState] = useState("idle"); + const [selectedMealType, setSelectedMealType] = useState("all"); + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [fileName, setFileName] = useState(""); + + // 收集所有未翻译的菜品名称 + const getAllDishNames = useCallback((menu: WeeklyMenu): string[] => { + const names: string[] = []; + for (const meal of menu.meals) { + for (const dish of meal.dishList) { + if (dish.name.zh && !dish.name.en) { + names.push(dish.name.zh); + } + } + } + return [...new Set(names)]; // 去重 + }, []); + + // 批量翻译 + const handleTranslateAll = useCallback(async () => { + if (!data) return; + + const dishNames = getAllDishNames(data); + if (dishNames.length === 0) { + alert("所有菜品都已翻译!"); + return; + } + + setTranslateState("translating"); + + try { + const response = await fetch("/api/translate", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({names: dishNames}), + }); + + const result = await response.json(); + + if (!response.ok) { + throw new Error(result.error || "翻译失败"); + } + + // 更新 data 中的翻译结果 + const translations = result.translations as Record; + + setData((prevData) => { + if (!prevData) return null; + + const newMeals = prevData.meals.map((meal) => ({ + ...meal, + dishList: meal.dishList.map((dish) => { + const translation = dish.name.zh ? translations[dish.name.zh] : null; + if (translation) { + return { + ...dish, + name: {...dish.name, en: translation}, + translated: true, + }; + } + return { + ...dish, + translated: dish.translated || false, + }; + }), + })); + + return {...prevData, meals: newMeals}; + }); + + setTranslateState("done"); + } catch (err) { + console.error("Translation error:", err); + alert("翻译失败,请重试"); + setTranslateState("idle"); + } + }, [data, getAllDishNames]); + + // 导出 Word 文档 + const handleExport = useCallback(async () => { + if (!data) return; + + try { + // 1. 收集所有需要翻译的内容(星期、餐食类型、菜品) + const allNames: string[] = []; + + // 收集星期名称(使用实际数据中的星期) + for (const day of data.days) { + if (day.name.zh) { + allNames.push(day.name.zh); + } + } + + // 收集餐食类型名称 + for (const meal of MEAL_TYPES) { + allNames.push(meal.label); + } + + // 收集所有菜品名称(去重) + const dishNamesSet = new Set(); + for (const meal of data.meals) { + for (const dish of meal.dishList) { + if (dish.name.zh) { + dishNamesSet.add(dish.name.zh); + } + } + } + allNames.push(...dishNamesSet); + + // 2. 调用翻译 API + const response = await fetch("/api/translate", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({names: [...new Set(allNames)]}), + }); + + const result = await response.json(); + + if (!response.ok) { + throw new Error(result.error || "翻译失败"); + } + + const translations = result.translations as Record; + + // 3. 构建翻译映射函数 + const t = (zh: string): string => translations[zh] || ""; + + // 4. 构建表格数据 + const tableRows: TableRow[] = []; + + // 表头行 - 星期列头 + 餐食类型都要上中下英 + const headerCells: TableCell[] = []; + const weekEnglish = translations["week"] || translations["Week"] || "Day"; + headerCells.push( + new TableCell({ + children: [ + new Paragraph({children: [new TextRun({text: "星期 / Week", bold: true})]}), + ].filter((p): p is Paragraph => p !== null) as unknown as Paragraph[], + width: {size: 12, type: WidthType.PERCENTAGE}, + }), + ); + + for (const mealType of MEAL_TYPES) { + const zhText = mealType.label; + const enText = t(zhText); + headerCells.push( + new TableCell({ + children: [ + new Paragraph({children: [new TextRun({text: enText || zhText, bold: true})]}), + enText ? new Paragraph({ + children: [new TextRun({ + text: zhText, + color: "666666", + size: 18 + })] + }) : null, + ].filter((p): p is Paragraph => p !== null) as unknown as Paragraph[], + width: {size: 17.6, type: WidthType.PERCENTAGE}, + }) + ); + } + + tableRows.push(new TableRow({children: headerCells, tableHeader: true})); + + // 数据行(使用实际数据中的星期)- 上中下英 + for (const day of data.days) { + const dayZh = day.name.zh || ""; + const dayEn = t(dayZh) || translations[dayZh] || ""; + const dayCells: TableCell[] = [ + new TableCell({ + children: [ + new Paragraph({children: [new TextRun({text: dayZh})]}), + dayEn ? new Paragraph({ + children: [new TextRun({ + text: dayEn, + color: "666666", + size: 18 + })] + }) : null, + ].filter((p): p is Paragraph => p !== null) as unknown as Paragraph[], + }), + ]; + + for (const mealType of MEAL_TYPES) { + const mealData = data.meals.find( + (m) => m.dayCode === day.code && m.mealCode === mealType.code + ); + + const cellContent: Paragraph[] = []; + + if (mealData?.dishList && mealData.dishList.length > 0) { + for (const dish of mealData.dishList) { + const zhText = dish.name.zh; + const enText = dish.name.en || t(zhText); + + if (enText) { + cellContent.push( + new Paragraph({ + children: [new TextRun({text: zhText})], + }) + ); + cellContent.push( + new Paragraph({ + children: [new TextRun({text: enText, color: "666666", size: 18})], + }) + ); + } else { + cellContent.push( + new Paragraph({ + children: [new TextRun({text: zhText, color: "CC0000"})], + }) + ); + } + } + } else { + cellContent.push(new Paragraph({children: [new TextRun({text: "—", color: "999999"})]})); + } + + dayCells.push( + new TableCell({ + children: cellContent, + }) + ); + } + + tableRows.push(new TableRow({children: dayCells})); + } + + // 5. 创建文档 - 横向 A4 + const doc = new Document({ + sections: [ + { + properties: { + page: { + size: { + orientation: PageOrientation.LANDSCAPE, + width: 11906, + height: 16838, + }, + margin: { + top: 700, // 约1.27厘米 + bottom: 700, + left: 900, // 约1.9厘米 + right: 900, + }, + }, + }, + children: [ + new Paragraph({ + children: [ + new TextRun({ + text: t(data.title?.zh || "食谱") || data.title?.zh || "食谱", + bold: true, + size: 32, + }), + ], + alignment: AlignmentType.CENTER, + }), + new Paragraph({children: []}), + new Table({ + rows: tableRows, + width: {size: 100, type: WidthType.PERCENTAGE}, + layout: TableLayoutType.FIXED, + }), + ], + }, + ], + }); + + // 6. 生成并下载 + const blob = await Packer.toBlob(doc); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = `${fileName || data.title?.zh || "食谱翻译"}.docx`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + } catch (err) { + console.error("Export error:", err); + alert("导出失败,请重试"); + } + }, [data, fileName]); + + const handleFileUpload = useCallback( + async (file: File) => { + if (!file.name.endsWith(".docx")) { + setError("请上传 .docx 格式的文件"); + setUploadState("error"); + return; + } + + setUploadState("uploading"); + setError(null); + setTranslateState("idle"); + + try { + const formData = new FormData(); + formData.append("file", file); + + setUploadState("parsing"); + + const response = await fetch("/api/recipe", { + method: "POST", + body: formData, + }); + + const result = await response.json(); + + if (!response.ok) { + throw new Error(result.error || "解析失败"); + } + + setData(result.data); + setFileName(file.name.replace(/\.docx$/i, "")); + setUploadState("done"); + } catch (err) { + setError(err instanceof Error ? err.message : "文件解析失败,请检查格式"); + setUploadState("error"); + } + }, + [] + ); + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + const file = e.dataTransfer.files[0]; + if (file) handleFileUpload(file); + }, + [handleFileUpload] + ); + + const handleFileInput = useCallback( + (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) handleFileUpload(file); + }, + [handleFileUpload] + ); + + const reset = () => { + setUploadState("idle"); + setData(null); + setError(null); + setTranslateState("idle"); + setSelectedMealType("all"); + setFileName(""); + }; + + // 统计翻译状态 + const untranslatedCount = data ? getAllDishNames(data).length : 0; + + return ( +
+ {/* Header */} +
+ +
+ +
+ {/* Upload Zone - 闲置状态 */} + {uploadState === "idle" && ( +
e.preventDefault()} + className="group relative flex flex-col items-center justify-center rounded-2xl border-2 border-dashed border-border bg-muted/50 p-12 transition-all hover:border-primary/50 hover:bg-muted" + > + +
+ +
+

点击上传或拖拽 Word 文档

+

支持 .docx 格式

+
+ )} + + {/* 上传中 */} + {uploadState === "uploading" && ( +
+ +

正在上传文件...

+
+ )} + + {/* 解析中 */} + {uploadState === "parsing" && ( +
+ +

正在解析文档内容...

+

提取文字并识别食谱结构

+
+ )} + + {/* 错误状态 */} + {uploadState === "error" && ( +
+
+ +
+

{error}

+ +
+ )} + + {/* 完成 - 显示结果 */} + {uploadState === "done" && data && ( +
+ {/* 成功提示 + 重新上传 */} +
+
+ + 文档解析完成 + {data.title.zh} + {untranslatedCount > 0 && translateState !== "translating" && ( + + + {untranslatedCount} 个菜品待翻译 + + )} + {translateState === "done" && untranslatedCount === 0 && ( + + + 全部翻译完成 + + )} +
+ +
+ + {/* 餐食类型筛选 + 操作按钮 */} +
+ {/* 餐食类型筛选 */} +
+ + {MEAL_TYPES.map((meal) => { + const Icon = meal.icon; + return ( + + ); + })} +
+ + {/* 操作按钮 */} +
+ + +
+
+ + {/* 食谱表格 */} +
+ + + + + {selectedMealType === "all" + ? MEAL_TYPES.map((meal) => { + const Icon = meal.icon; + return ( + + ); + }) + : MEAL_TYPES.find((m) => m.code === selectedMealType)?.icon && ( + + )} + + + + {data.days.map((day) => ( + + + {selectedMealType === "all" ? ( + MEAL_TYPES.map((meal) => { + const mealData = data.meals.find( + (m) => m.dayCode === day.code && m.mealCode === meal.code + ); + return ( + + ); + }) + ) : ( + + )} + + ))} + +
星期 +
+ + {meal.label} +
+
+ { + MEAL_TYPES.find((m) => m.code === selectedMealType) + ?.label + } +
{day.name.zh} +
+ {mealData?.dishList.length ? ( + mealData.dishList.map((dish) => ( +
+ + {dish.name.zh} + + {dish.name.en && ( + + {dish.name.en} + + )} +
+ )) + ) : ( + + )} +
+
+ {(() => { + const mealData = data.meals.find( + (m) => + m.dayCode === day.code && + m.mealCode === selectedMealType + ); + return mealData?.dishList.length ? ( +
+ {mealData.dishList.map((dish) => ( +
+ + {dish.name.zh} + + {dish.name.en && ( + + {dish.name.en} + + )} +
+ ))} +
+ ) : ( + + ); + })()} +
+
+
+ )} +
+
+ ); } diff --git a/app/providers.tsx b/app/providers.tsx new file mode 100644 index 0000000..f7d9d1b --- /dev/null +++ b/app/providers.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ToastProvider } from "@/components/ui/toast"; + +export default function Providers({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/app/settings/ai/page.tsx b/app/settings/ai/page.tsx new file mode 100644 index 0000000..af47c2b --- /dev/null +++ b/app/settings/ai/page.tsx @@ -0,0 +1,175 @@ +"use client"; +import {useEffect, useState} from "react"; +import {Loader2} from "lucide-react"; +import {Button} from "@/components/ui/button"; + +interface AIConfig { + apiKey: string; + baseUrl: string; + model: string; +} + +const defaultAIConfig: AIConfig = { + apiKey: "", + baseUrl: "https://api.openai.com/v1", + model: "gpt-3.5-turbo", +}; + +export default function AISettingsPage() { + const [config, setConfig] = useState(defaultAIConfig); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [testing, setTesting] = useState(false); + const [testResult, setTestResult] = useState<"success" | "error" | null>(null); + + // 加载配置 + useEffect(() => { + fetch("/api/config") + .then((res) => res.json()) + .then((result) => { + if (result.success && result.data) { + setConfig({ + apiKey: result.data.ai?.apiKey || defaultAIConfig.apiKey, + baseUrl: result.data.ai?.baseUrl || defaultAIConfig.baseUrl, + model: result.data.ai?.model || defaultAIConfig.model, + }); + } + }) + .catch(console.error) + .finally(() => setLoading(false)); + }, []); + + // 保存配置 + const handleSave = async () => { + setSaving(true); + try { + const response = await fetch("/api/config", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({ai: config}), + }); + const result = await response.json(); + if (result.success) { + setTestResult("success"); + setTimeout(() => setTestResult(null), 3000); + } else { + setTestResult("error"); + } + } catch (error) { + console.error("Failed to save config:", error); + setTestResult("error"); + } finally { + setSaving(false); + } + }; + + // 测试 AI 连接 + const handleTestAI = async () => { + if (!config.apiKey) return; + + // 先保存配置 + await handleSave(); + + setTesting(true); + setTestResult(null); + + try { + const response = await fetch(`/api/ai/translate?word=${encodeURIComponent("早餐")}`); + const result = await response.json(); + + if (result.english) { + setTestResult("success"); + } else { + setTestResult("error"); + } + } catch { + setTestResult("error"); + } finally { + setTesting(false); + } + }; + + if (loading) { + return ( +
+
+

大模型设置

+

配置 AI 翻译服务

+
+
+
+ ); + } + + return ( +
+
+
+

大模型设置

+

配置 AI 翻译服务

+
+
+
+ + {testResult === "success" && ( + 配置成功! + )} + {testResult === "error" && ( + 配置失败,请检查 API Key + )} +
+ +
+ +
+
+
+ +
+
+ + setConfig({...config, apiKey: e.target.value})} + className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm" + placeholder="sk-..." + /> +
+
+ + setConfig({...config, baseUrl: e.target.value})} + className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm" + placeholder="https://api.openai.com/v1" + /> +
+
+ + setConfig({...config, model: e.target.value})} + className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm" + placeholder="gpt-3.5-turbo" + /> +
+
+ + +
+ ); +} diff --git a/app/settings/database/page.tsx b/app/settings/database/page.tsx new file mode 100644 index 0000000..5f664bf --- /dev/null +++ b/app/settings/database/page.tsx @@ -0,0 +1,152 @@ +"use client"; + +import {useState, useEffect} from "react"; +import {useToast} from "@/components/ui/toast"; +import {Loader2} from "lucide-react"; +import {Button} from "@/components/ui/button"; + +interface DBConfig { + host: string; + port: number; + user: string; + password: string; + database: string; +} + +const defaultDBConfig: DBConfig = { + host: "127.0.0.1", + port: 3306, + user: "root", + password: "", + database: "recipe_tools", +}; + +export default function DatabasePage() { + const [config, setConfig] = useState(defaultDBConfig); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const {addToast} = useToast(); + + useEffect(() => { + fetch("/api/config") + .then((res) => res.json()) + .then((result) => { + if (result.success && result.data) { + setConfig({ + host: result.data.db?.host || defaultDBConfig.host, + port: result.data.db?.port || defaultDBConfig.port, + user: result.data.db?.user || defaultDBConfig.user, + password: result.data.db?.password || defaultDBConfig.password, + database: result.data.db?.database || defaultDBConfig.database, + }); + } + }) + .catch(console.error) + .finally(() => setLoading(false)); + }, []); + + const handleSave = async () => { + setSaving(true); + try { + const response = await fetch("/api/config", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({db: config}), + }); + const result = await response.json(); + if (result.success) { + addToast({type: "success", title: "数据库配置已保存"}); + } else { + addToast({type: "error", title: "保存失败", description: result.error}); + } + } catch { + addToast({type: "error", title: "保存失败"}); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( +
+
+

数据库配置

+

配置 MySQL 数据库连接信息

+
+
+
+ ); + } + + return ( +
+
+
+

数据库配置

+

配置 MySQL 数据库连接信息

+
+
+ +
+
+ +
+
+ + setConfig({...config, host: e.target.value})} + className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm" + placeholder="127.0.0.1" + /> +
+
+ + setConfig({...config, port: parseInt(e.target.value) || 3306})} + className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm" + placeholder="3306" + /> +
+
+ + setConfig({...config, user: e.target.value})} + className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm" + placeholder="root" + /> +
+
+ + setConfig({...config, password: e.target.value})} + className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm" + placeholder="******" + /> +
+
+ + setConfig({...config, database: e.target.value})} + className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm" + placeholder="recipe_tools" + /> +
+
+ + +
+ ); +} diff --git a/app/settings/export/page.tsx b/app/settings/export/page.tsx new file mode 100644 index 0000000..78fc47f --- /dev/null +++ b/app/settings/export/page.tsx @@ -0,0 +1,18 @@ +import {FileText} from "lucide-react"; + +export default function ExportPage() { + return ( +
+
+

导出设置

+

配置导出 Word 文档的格式

+
+
+ +

+ 导出设置功能开发中... +

+
+
+ ); +} diff --git a/app/settings/layout.tsx b/app/settings/layout.tsx new file mode 100644 index 0000000..f40f68f --- /dev/null +++ b/app/settings/layout.tsx @@ -0,0 +1,103 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { + Database, + Globe, + FileText, + ChevronRight, + UtensilsCrossed, + Bandage +} from "lucide-react"; +import { Button } from "@/components/ui/button"; + +const settingsMenu = [ + { + title: "数据库配置", + icon: Database, + href: "/settings/database", + }, + { + title: "翻译词库", + icon: Globe, + href: "/settings/translation", + }, + { + title: "导出设置", + icon: FileText, + href: "/settings/export", + }, + { + title: "大模型设置", + icon: Bandage, + href: "/settings/ai", + }, +]; + +export default function SettingsLayout({ + children, +}: { + children: React.ReactNode; +}) { + const pathname = usePathname(); + + return ( +
+
+
+
+
+
+ +
+
+

食谱翻译工具

+

设置

+
+
+ + + +
+
+
+ +
+
+ {/* 左侧菜单 */} + + + {/* 右侧内容 */} +
+ {children} +
+
+
+
+ ); +} diff --git a/app/settings/page.tsx b/app/settings/page.tsx new file mode 100644 index 0000000..9a7405b --- /dev/null +++ b/app/settings/page.tsx @@ -0,0 +1,20 @@ +"use client"; + +import {useEffect} from "react"; +import {useRouter} from "next/navigation"; +import {Loader2} from "lucide-react"; + +// 主设置页面 - 重定向到数据库设置 +export default function SettingsPage() { + const router = useRouter(); + + useEffect(() => { + router.replace("/settings/database"); + }, [router]); + + return ( +
+ +
+ ); +} diff --git a/app/settings/translation/page.tsx b/app/settings/translation/page.tsx new file mode 100644 index 0000000..009400e --- /dev/null +++ b/app/settings/translation/page.tsx @@ -0,0 +1,460 @@ +"use client"; + +import {useState, useEffect, useCallback, useRef} from "react"; +import { + Plus, + Search, + Loader2, + Pencil, + Trash2, + Sparkles, + ChevronLeft, + ChevronRight, + X, +} from "lucide-react"; +import {Button} from "@/components/ui/button"; +import {useToast} from "@/components/ui/toast"; + +interface WordItem { + id: number; + word: string; + english: string; + createTime?: string; + updateTime?: string; +} + +interface Pagination { + page: number; + pageSize: number; + total: number; + totalPages: number; +} + +// Modal 组件 +function Modal({ + open, + onClose, + title, + children, + }: { + open: boolean; + onClose: () => void; + title: string; + children: React.ReactNode; +}) { + if (!open) return null; + + return ( +
+ {/* Backdrop */} +
+ {/* Content */} +
+
+

{title}

+ +
+ {children} +
+
+ ); +} + +export default function TranslationPage() { + const [words, setWords] = useState([]); + const [pagination, setPagination] = useState({ + page: 1, + pageSize: 20, + total: 0, + totalPages: 0, + }); + const [search, setSearch] = useState(""); + const [loading, setLoading] = useState(false); + const [showAddModal, setShowAddModal] = useState(false); + const [showEditModal, setShowEditModal] = useState(false); + const [editingWord, setEditingWord] = useState(null); + const [newWord, setNewWord] = useState({word: "", english: ""}); + const [editForm, setEditForm] = useState({word: "", english: ""}); + const [aiTranslating, setAiTranslating] = useState(false); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const {addToast} = useToast(); + + // 搜索防抖定时器 + const searchTimerRef = useRef(null); + + // 禁止背景滚动 + useEffect(() => { + if (showAddModal || showEditModal) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = ""; + } + return () => { + document.body.style.overflow = ""; + }; + }, [showAddModal, showEditModal]); + + // 获取词组列表 + const fetchWords = useCallback(async () => { + setLoading(true); + setError(null); + try { + const params = new URLSearchParams({ + page: pagination.page.toString(), + pageSize: pagination.pageSize.toString(), + }); + if (search) params.set("search", search); + + const response = await fetch(`/api/words?${params}`); + const result = await response.json(); + + if (result.success) { + setWords(result.data); + setPagination(result.pagination); + } else { + setError(result.error || "获取数据失败"); + } + } catch (error) { + console.error("Failed to fetch words:", error); + setError("网络错误,请检查连接"); + } finally { + setLoading(false); + } + }, [pagination.page, pagination.pageSize, search]); + + useEffect(() => { + fetchWords(); + }, [fetchWords]); + + // 搜索(防抖 300ms) + const handleSearch = () => { + if (searchTimerRef.current) { + clearTimeout(searchTimerRef.current); + } + searchTimerRef.current = setTimeout(() => { + setPagination((prev) => ({...prev, page: 1})); + fetchWords(); + }, 300); + }; + + // 添加词组 + const handleAdd = async () => { + if (!newWord.word || !newWord.english) return; + + setSaving(true); + try { + const response = await fetch("/api/words", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify(newWord), + }); + + if (response.ok) { + setNewWord({word: "", english: ""}); + setShowAddModal(false); + fetchWords(); + } + } catch (error) { + console.error("Failed to add word:", error); + } finally { + setSaving(false); + } + }; + + // 打开编辑弹窗 + const handleEdit = (word: WordItem) => { + setEditingWord(word); + setEditForm({word: word.word, english: word.english}); + setShowEditModal(true); + }; + + // 保存编辑 + const handleSave = async () => { + if (!editingWord || !editForm.word || !editForm.english) return; + + setSaving(true); + try { + const response = await fetch(`/api/words/${editingWord.id}`, { + method: "PATCH", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify(editForm), + }); + + if (response.ok) { + setShowEditModal(false); + setEditingWord(null); + fetchWords(); + } + } catch (error) { + console.error("Failed to update word:", error); + } finally { + setSaving(false); + } + }; + + // 删除词组 + const handleDelete = async (id: number) => { + if (!confirm("确定要删除这个词组吗?")) return; + + try { + const response = await fetch(`/api/words/${id}`, { + method: "DELETE", + }); + + if (response.ok) { + fetchWords(); + } + } catch (error) { + console.error("Failed to delete word:", error); + } + }; + + // AI 翻译 + const handleAiTranslate = async (target: "add" | "edit") => { + const source = target === "add" ? newWord.word : editForm.word; + if (!source.trim()) return; + + setAiTranslating(true); + try { + // 使用 AI 翻译 API + const response = await fetch(`/api/ai/translate?word=${encodeURIComponent(source.trim())}`); + const result = await response.json(); + + if (result.english) { + if (target === "add") { + setNewWord((prev) => ({...prev, english: result.english})); + } else { + setEditForm((prev) => ({...prev, english: result.english})); + } + } else { + addToast({type: "error", title: "翻译失败", description: result.error || "未找到翻译结果"}); + } + } catch (error) { + console.error("AI translation error:", error); + addToast({type: "error", title: "翻译失败", description: "请检查 AI 配置"}); + } finally { + setAiTranslating(false); + } + }; + + return ( +
+ {/*标题和按钮*/} +
+
+

翻译词库

+

管理翻译词组对照表

+
+ +
+ {/* 搜索输入框 */} +
+
+ + setSearch(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleSearch()} + placeholder="搜索中文或英文..." + className="w-full rounded-md border border-input bg-background py-2 pl-10 pr-4 text-sm" + /> +
+ +
+ + {/* 错误提示 */} + {error && ( +
+

{error}

+ +
+ )} + + {/* 词组列表 */} +
+
+ + + + + + + + + + {loading ? ( + + + + ) : words.length === 0 ? ( + + + + ) : ( + words.map((word) => ( + + + + + + )) + )} + +
中文英文操作
+ +
+ 暂无词组,点击上方添加按钮新建 +
{word.word}{word.english} +
+ + +
+
+
+ + {/* 分页 */} + {pagination.totalPages > 1 && ( +
+ + 共 {pagination.total} 条,第 {pagination.page}/{pagination.totalPages} 页 + +
+ + + {pagination.page} / {pagination.totalPages} + + +
+
+ )} +
+ + {/* 添加弹窗 */} + setShowAddModal(false)} title="添加词组"> +
+
+ + setNewWord({...newWord, word: e.target.value})} + placeholder="输入中文" + className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm" + /> +
+
+ +
+ setNewWord({...newWord, english: e.target.value})} + placeholder="输入英文" + className="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm" + /> + +
+
+
+ + +
+
+
+ + {/* 编辑弹窗 */} + setShowEditModal(false)} title="编辑词组"> +
+
+ + setEditForm({...editForm, word: e.target.value})} + className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm" + /> +
+
+ +
+ setEditForm({...editForm, english: e.target.value})} + className="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm" + /> + +
+
+
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx new file mode 100644 index 0000000..37e13c0 --- /dev/null +++ b/components/ui/alert.tsx @@ -0,0 +1,76 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "group/alert relative grid w-full gap-0.5 rounded-2xl border px-4 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground", + className + )} + {...props} + /> + ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription, AlertAction } diff --git a/components/ui/toast.tsx b/components/ui/toast.tsx new file mode 100644 index 0000000..a4b3b29 --- /dev/null +++ b/components/ui/toast.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { createContext, useContext, useState, useCallback, ReactNode } from "react"; +import { X } from "lucide-react"; +import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"; + +interface Toast { + id: string; + type: "success" | "error" | "info"; + title: string; + description?: string; +} + +interface ToastContextType { + toasts: Toast[]; + addToast: (toast: Omit) => void; + removeToast: (id: string) => void; +} + +const ToastContext = createContext(null); + +export function ToastProvider({ children }: { children: ReactNode }) { + const [toasts, setToasts] = useState([]); + + const addToast = useCallback((toast: Omit) => { + const id = Math.random().toString(36).substring(2, 9); + setToasts((prev) => [...prev, { ...toast, id }]); + + // Auto remove after 4 seconds + setTimeout(() => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }, 4000); + }, []); + + const removeToast = useCallback((id: string) => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }, []); + + return ( + + {children} + {/* Toast Container */} +
+ {toasts.map((toast) => ( + + {toast.title} + {toast.description && ( + {toast.description} + )} +
+ +
+
+ ))} +
+
+ ); +} + +export function useToast() { + const context = useContext(ToastContext); + if (!context) { + throw new Error("useToast must be used within a ToastProvider"); + } + return context; +} diff --git a/config.json b/config.json new file mode 100644 index 0000000..2991019 --- /dev/null +++ b/config.json @@ -0,0 +1,14 @@ +{ + "db": { + "host": "127.0.0.1", + "port": 3306, + "user": "root", + "password": "", + "database": "recipe_tools" + }, + "ai": { + "apiKey": "6a81b361620a44fd86aa3c8fcd833800.PjPyiU8kSzCTviYa", + "baseUrl": "https://open.bigmodel.cn/api/paas/v4/", + "model": "GLM-4.7-Flash" + } +} \ No newline at end of file diff --git a/config/database.example.env b/config/database.example.env new file mode 100644 index 0000000..13263be --- /dev/null +++ b/config/database.example.env @@ -0,0 +1,9 @@ +# 数据库配置 +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_USER=root +DB_PASSWORD=123456 +DB_NAME=recipe_tools + +# 服务器配置 +PORT=3000 diff --git a/lib/ai-translate.ts b/lib/ai-translate.ts new file mode 100644 index 0000000..1e7a5e5 --- /dev/null +++ b/lib/ai-translate.ts @@ -0,0 +1,139 @@ +import OpenAI from "openai"; + +// AI 翻译配置 +interface AIConfig { + apiKey: string; + baseUrl?: string; + model?: string; +} + +// 默认配置(从环境变量) +function getDefaultConfig(): AIConfig { + return { + apiKey: process.env.AI_API_KEY || "", + baseUrl: process.env.AI_BASE_URL || undefined, + model: process.env.AI_MODEL || "gpt-3.5-turbo", + }; +} + +// 模块级配置存储 +let savedConfig: AIConfig | null = null; + +// 设置保存的配置 +export function setAIConfig(config: AIConfig) { + savedConfig = config; +} + +// 获取配置(优先使用保存的配置) +export function getAIConfig(): AIConfig { + return savedConfig || getDefaultConfig(); +} + +// 解析翻译结果 +function parseTranslation(content: string): string | null { + const patterns = [ + /english\s*:\s*['"]([^'"]+)['"]/i, + /english\s*[:\s]+([^,'"\n]+)/i, + /['"]([^'"]+)['"]\s*[,}].*english/i, + /^([^,]+)\s*,\s*english\s*:\s*['"]([^'"]+)['"]/i, + ]; + + for (const pattern of patterns) { + const match = content.match(pattern); + if (match) { + return match[1]?.trim() || null; + } + } + + return null; +} + +// 翻译单个词组(带重试) +export async function translateWithAI( + word: string, + config?: AIConfig, + maxRetries: number = 3 +): Promise { + const aiConfig = config || getAIConfig(); + + if (!aiConfig.apiKey) { + throw new Error("AI API Key 未配置"); + } + + const client = new OpenAI({ + apiKey: aiConfig.apiKey, + baseURL: aiConfig.baseUrl, + }); + + const prompt = `你是一个专业的中文到英文翻译助手。 + +任务:将给定的中文词组翻译成英文。 + +严格要求: +1. 必须严格返回 JSON 格式:{"word":"中文原文","english":"英文翻译"} +2. 不要返回任何其他内容,不要解释 +3. 只返回这一行 JSON +4. 翻译要简洁、准确 + +中文词组:${word} + +请立即返回 JSON 结果:`; + + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + const completion = await client.chat.completions.create({ + model: aiConfig.model || "gpt-3.5-turbo", + messages: [ + { + role: "system", + content: "你是一个严格遵循指令的翻译助手。必须只返回要求的 JSON 格式,不要返回任何其他内容。", + }, + { role: "user", content: prompt }, + ], + temperature: 0, + max_tokens: 50, + }); + + const content = completion.choices[0]?.message?.content || ""; + const translation = parseTranslation(content); + + if (translation && translation.length > 0) { + return translation; + } + + // Fallback: 提取英文单词 + const fallbackMatch = content.match(/[a-zA-Z][a-zA-Z\s-]*/); + if (fallbackMatch) { + const fallback = fallbackMatch[0].trim(); + if (fallback.length > 1) { + return fallback; + } + } + } catch (error) { + console.error(`Attempt ${attempt + 1} error:`, error); + } + } + + return null; +} + +// 批量翻译词组 +export async function batchTranslateWithAI( + words: string[], + config?: AIConfig +): Promise> { + const results = new Map(); + + for (const word of words) { + try { + const translation = await translateWithAI(word, config); + if (translation) { + results.set(word, translation); + } + } catch (error) { + console.error(`Failed to translate "${word}":`, error); + } + } + + return results; +} diff --git a/lib/config.ts b/lib/config.ts new file mode 100644 index 0000000..4f8f45e --- /dev/null +++ b/lib/config.ts @@ -0,0 +1,173 @@ +import fs from "fs"; +import path from "path"; + +export interface DBConfig { + host: string; + port: number; + user: string; + password: string; + database: string; +} + +export interface AIConfig { + apiKey: string; + baseUrl: string; + model: string; +} + +export interface AppConfig { + db: DBConfig; + ai: AIConfig; +} + +// 配置文件路径(单一文件) +const configPath = path.join(process.cwd(), "config.json"); + +/** + * 读取完整配置 + */ +function readConfigFile(): Partial { + try { + if (fs.existsSync(configPath)) { + const content = fs.readFileSync(configPath, "utf-8"); + return JSON.parse(content); + } + } catch (e) { + console.error("Config read error:", e); + } + return {}; +} + +/** + * 写入完整配置 + */ +function writeConfigFile(config: AppConfig): void { + fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8"); +} + +/** + * 获取数据库配置 + * 优先级:环境变量 > config.json + */ +export function getDBConfig(): DBConfig { + let host = "127.0.0.1"; + let port = 3306; + let user = "root"; + let password = ""; + let database = "recipe_tools"; + + // 环境变量优先级最高 + if (process.env.DB_HOST) host = process.env.DB_HOST; + if (process.env.DB_PORT) port = parseInt(process.env.DB_PORT); + if (process.env.DB_USER) user = process.env.DB_USER; + if (process.env.DB_PASSWORD) password = process.env.DB_PASSWORD; + if (process.env.DB_NAME) database = process.env.DB_NAME; + + // config.json 次之(仅当环境变量未设置时) + const fileConfig = readConfigFile(); + if (fileConfig.db) { + if (fileConfig.db.host && !process.env.DB_HOST) host = fileConfig.db.host; + if (fileConfig.db.port && !process.env.DB_PORT) port = fileConfig.db.port; + if (fileConfig.db.user && !process.env.DB_USER) user = fileConfig.db.user; + if (fileConfig.db.password && !process.env.DB_PASSWORD) password = fileConfig.db.password; + if (fileConfig.db.database && !process.env.DB_NAME) database = fileConfig.db.database; + } + + return { host, port, user, password, database }; +} + +/** + * 获取AI翻译配置 + * 优先级:环境变量 > config.json + */ +export function getAIConfig(): AIConfig { + let apiKey = ""; + let baseUrl = "https://api.openai.com/v1"; + let model = "gpt-3.5-turbo"; + + // 环境变量优先级最高 + if (process.env.AI_API_KEY) apiKey = process.env.AI_API_KEY; + if (process.env.AI_BASE_URL) baseUrl = process.env.AI_BASE_URL; + if (process.env.AI_MODEL) model = process.env.AI_MODEL; + + // config.json 次之(仅当环境变量未设置时) + const fileConfig = readConfigFile(); + if (fileConfig.ai) { + if (fileConfig.ai.apiKey && !process.env.AI_API_KEY) apiKey = fileConfig.ai.apiKey; + if (fileConfig.ai.baseUrl && !process.env.AI_BASE_URL) baseUrl = fileConfig.ai.baseUrl; + if (fileConfig.ai.model && !process.env.AI_MODEL) model = fileConfig.ai.model; + } + + return { apiKey, baseUrl, model }; +} + +/** + * 获取完整配置(兼容旧接口) + */ +export function getConfig(): AppConfig { + return { + db: getDBConfig(), + ai: getAIConfig(), + }; +} + +/** + * 保存数据库配置 + */ +export function saveDBConfig(config: Partial): DBConfig { + const current = getDBConfig(); + + const newConfig: DBConfig = { + host: config.host ?? current.host, + port: config.port ?? current.port, + user: config.user ?? current.user, + password: config.password ?? current.password, + database: config.database ?? current.database, + }; + + // 读取现有配置,更新 db 部分 + const fileConfig = readConfigFile(); + const updatedConfig: AppConfig = { + ai: fileConfig.ai ?? { apiKey: "", baseUrl: "https://api.openai.com/v1", model: "gpt-3.5-turbo" }, + db: newConfig, + }; + writeConfigFile(updatedConfig); + + return newConfig; +} + +/** + * 保存AI翻译配置 + */ +export function saveAIConfig(config: Partial): AIConfig { + const current = getAIConfig(); + + const newConfig: AIConfig = { + apiKey: config.apiKey ?? current.apiKey, + baseUrl: config.baseUrl ?? current.baseUrl, + model: config.model ?? current.model, + }; + + // 读取现有配置,更新 ai 部分 + const fileConfig = readConfigFile(); + const updatedConfig: AppConfig = { + db: fileConfig.db ?? { host: "127.0.0.1", port: 3306, user: "root", password: "", database: "recipe_tools" }, + ai: newConfig, + }; + writeConfigFile(updatedConfig); + + return newConfig; +} + +/** + * 保存完整配置(兼容旧接口) + */ +export function saveConfig(config: Partial): AppConfig { + const current = getConfig(); + const fileConfig = readConfigFile(); + + return { + db: config.db ? saveDBConfig(config.db) : current.db, + ai: config.ai ? saveAIConfig(config.ai) : current.ai, + }; +} diff --git a/lib/database.ts b/lib/database.ts new file mode 100644 index 0000000..883faa6 --- /dev/null +++ b/lib/database.ts @@ -0,0 +1,67 @@ +import mysql from "mysql2/promise"; + +const pool = mysql.createPool({ + host: process.env.DB_HOST || "127.0.0.1", + port: parseInt(process.env.DB_PORT || "3306"), + user: process.env.DB_USER || "root", + password: process.env.DB_PASSWORD || "", + database: process.env.DB_NAME || "recipe_tools", + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0, +}); + +export default pool; + +/** + * 根据中文菜名查询英文翻译 + * @param chineseName 中文菜名 + * @returns 翻译结果或 null + */ +export async function getTranslation(chineseName: string): Promise { + try { + const [rows] = await pool.execute( + "SELECT english FROM words WHERE word = ? LIMIT 1", + [chineseName] + ); + + if (rows.length > 0 && rows[0].english) { + return rows[0].english as string; + } + return null; + } catch (error) { + console.error("Database query error:", error); + return null; + } +} + +/** + * 批量查询翻译 + * @param chineseNames 中文菜名数组 + * @returns 翻译结果映射 { chineseName: englishName } + */ +export async function getTranslations( + chineseNames: string[] +): Promise> { + const result = new Map(); + + if (chineseNames.length === 0) return result; + + try { + const placeholders = chineseNames.map(() => "?").join(","); + const [rows] = await pool.execute( + `SELECT word, english FROM words WHERE word IN (${placeholders})`, + chineseNames + ); + + for (const row of rows) { + if (row.word && row.english) { + result.set(row.word as string, row.english as string); + } + } + } catch (error) { + console.error("Database batch query error:", error); + } + + return result; +} diff --git a/package.json b/package.json index c8b1659..51d1f92 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,16 @@ "lint": "eslint" }, "dependencies": { + "adm-zip": "^0.5.17", + "cheerio": "^1.2.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "docx": "^9.6.1", "lucide-react": "^1.8.0", + "mammoth": "^1.12.0", + "mysql2": "^3.22.0", "next": "16.2.3", + "openai": "^6.34.0", "radix-ui": "^1.4.3", "react": "19.2.4", "react-dom": "19.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f3df4da..b32bac9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,18 +8,36 @@ importers: .: dependencies: + adm-zip: + specifier: ^0.5.17 + version: 0.5.17 + cheerio: + specifier: ^1.2.0 + version: 1.2.0 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 clsx: specifier: ^2.1.1 version: 2.1.1 + docx: + specifier: ^9.6.1 + version: 9.6.1 lucide-react: specifier: ^1.8.0 version: 1.8.0(react@19.2.4) + mammoth: + specifier: ^1.12.0 + version: 1.12.0 + mysql2: + specifier: ^3.22.0 + version: 3.22.0(@types/node@20.19.39) next: specifier: 16.2.3 version: 16.2.3(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + openai: + specifier: ^6.34.0 + version: 6.34.0(zod@3.25.76) radix-ui: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -31,7 +49,7 @@ importers: version: 19.2.4(react@19.2.4) shadcn: specifier: ^4.2.0 - version: 4.2.0(@types/node@20.19.39)(typescript@5.9.3) + version: 4.2.0(@cfworker/json-schema@4.1.1)(@types/node@20.19.39)(typescript@5.9.3) tailwind-merge: specifier: ^3.5.0 version: 3.5.0 @@ -199,6 +217,9 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@cfworker/json-schema@4.1.1': + resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} + '@dotenvx/dotenvx@1.61.0': resolution: {integrity: sha512-utL3cpZoFzflyqUkjYbxYujI6STBTmO5LFn4bbin/NZnRWN6wQ7eErhr3/Vpa5h/jicPFC6kTa42r940mQftJQ==} hasBin: true @@ -1422,6 +1443,9 @@ packages: '@types/node@20.19.39': resolution: {integrity: sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==} + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -1598,6 +1622,10 @@ packages: cpu: [x64] os: [win32] + '@xmldom/xmldom@0.8.12': + resolution: {integrity: sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==} + engines: {node: '>=10.0.0'} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -1612,6 +1640,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + adm-zip@0.5.17: + resolution: {integrity: sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==} + engines: {node: '>=12.0'} + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -1642,6 +1674,9 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1700,6 +1735,10 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + axe-core@4.11.2: resolution: {integrity: sha512-byD6KPdvo72y/wj2T/4zGEvvlis+PsZsn/yPS3pEO+sFpcrqRpX/TJCxvVaEsNeMrfQbCr7w163YqoD9IYwHXw==} engines: {node: '>=4'} @@ -1715,15 +1754,24 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.18: resolution: {integrity: sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==} engines: {node: '>=6.0.0'} hasBin: true + bluebird@3.4.7: + resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + brace-expansion@1.1.14: resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} @@ -1775,6 +1823,13 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.2.0: + resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==} + engines: {node: '>=20.18.1'} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -1845,6 +1900,9 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.6: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} @@ -1862,6 +1920,13 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -1941,6 +2006,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1956,14 +2025,37 @@ packages: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} + dingbat-to-unicode@1.0.1: + resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + docx@9.6.1: + resolution: {integrity: sha512-ZJja9/KBUuFC109sCMzovoq2GR2wCG/AuxivjA+OHj/q0TEgJIm3S7yrlUxIy3B+bV8YDj/BiHfWyrRFmyWpDQ==} + engines: {node: '>=10'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dotenv@17.4.1: resolution: {integrity: sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==} engines: {node: '>=12'} + duck@0.1.12: + resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1991,10 +2083,25 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + encoding-sniffer@0.2.1: + resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} + enhanced-resolve@5.20.1: resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -2296,6 +2403,9 @@ packages: fuzzysort@3.1.0: resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==} + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -2397,6 +2507,9 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -2414,6 +2527,9 @@ packages: resolution: {integrity: sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==} engines: {node: '>=16.9.0'} + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -2430,6 +2546,10 @@ packages: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -2442,6 +2562,9 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -2571,6 +2694,9 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -2631,6 +2757,9 @@ packages: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -2698,6 +2827,9 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2720,6 +2852,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -2808,13 +2943,23 @@ packages: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lop@0.4.2: + resolution: {integrity: sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru.min@1.1.4: + resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + lucide-react@1.8.0: resolution: {integrity: sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==} peerDependencies: @@ -2823,6 +2968,11 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + mammoth@1.12.0: + resolution: {integrity: sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==} + engines: {node: '>=12.0.0'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2862,6 +3012,9 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -2889,11 +3042,26 @@ packages: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} + mysql2@3.22.0: + resolution: {integrity: sha512-4jaJYBObj7FhD3lnZhqX1yDMuZN4mQNz+IolDySDXT7fbozMBpeGQNcuWXKUqo4ahkAEfkjUHPjnwuDI0/6VKw==} + engines: {node: '>= 8.0'} + peerDependencies: + '@types/node': '>= 8' + + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@5.1.7: + resolution: {integrity: sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==} + engines: {node: ^18 || >=20} + hasBin: true + napi-postinstall@0.3.4: resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -2951,6 +3119,9 @@ packages: resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} engines: {node: '>=18'} + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3006,6 +3177,21 @@ packages: resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} engines: {node: '>=20'} + openai@6.34.0: + resolution: {integrity: sha512-yEr2jdGf4tVFYG6ohmr3pF6VJuveP0EA/sS8TBx+4Eq5NT10alu5zg2dmxMXMgqpihRDQlFGpRt2XwsGj+Fyxw==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + option@0.2.4: + resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3029,6 +3215,9 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -3041,6 +3230,15 @@ packages: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -3052,6 +3250,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -3112,6 +3314,9 @@ packages: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -3197,6 +3402,9 @@ packages: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + recast@0.23.11: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} @@ -3255,6 +3463,9 @@ packages: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -3266,6 +3477,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -3298,6 +3513,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -3351,6 +3569,13 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + sql-escaper@1.3.3: + resolution: {integrity: sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==} + engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'} + stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -3400,6 +3625,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + stringify-object@5.0.0: resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==} engines: {node: '>=14.16'} @@ -3555,9 +3783,19 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + underscore@1.13.8: + resolution: {integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + + undici@7.24.7: + resolution: {integrity: sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==} + engines: {node: '>=20.18.1'} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -3625,6 +3863,15 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -3670,6 +3917,17 @@ packages: resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} engines: {node: '>=20'} + xml-js@1.6.11: + resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} + hasBin: true + + xml@1.0.1: + resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} + + xmlbuilder@10.1.1: + resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==} + engines: {node: '>=4.0'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -3908,6 +4166,9 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@cfworker/json-schema@4.1.1': + optional: true + '@dotenvx/dotenvx@1.61.0': dependencies: commander: 11.1.0 @@ -4163,7 +4424,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.13(hono@4.12.12) ajv: 8.18.0 @@ -4182,6 +4443,8 @@ snapshots: raw-body: 3.0.2 zod: 3.25.76 zod-to-json-schema: 3.25.2(zod@3.25.76) + optionalDependencies: + '@cfworker/json-schema': 4.1.1 transitivePeerDependencies: - supports-color @@ -5109,6 +5372,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@25.6.0': + dependencies: + undici-types: 7.19.2 + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: '@types/react': 19.2.14 @@ -5271,6 +5538,8 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@xmldom/xmldom@0.8.12': {} + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -5282,6 +5551,8 @@ snapshots: acorn@8.16.0: {} + adm-zip@0.5.17: {} + agent-base@7.1.4: {} ajv-formats@3.0.1(ajv@8.18.0): @@ -5310,6 +5581,10 @@ snapshots: dependencies: color-convert: 2.0.1 + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} aria-hidden@1.2.6: @@ -5397,6 +5672,8 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + aws-ssl-profiles@1.1.2: {} + axe-core@4.11.2: {} axobject-query@4.1.0: {} @@ -5405,8 +5682,12 @@ snapshots: balanced-match@4.0.4: {} + base64-js@1.5.1: {} + baseline-browser-mapping@2.10.18: {} + bluebird@3.4.7: {} + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -5421,6 +5702,8 @@ snapshots: transitivePeerDependencies: - supports-color + boolbase@1.0.0: {} + brace-expansion@1.1.14: dependencies: balanced-match: 1.0.2 @@ -5476,6 +5759,29 @@ snapshots: chalk@5.6.2: {} + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.2.2 + css-what: 6.2.2 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + + cheerio@1.2.0: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + encoding-sniffer: 0.2.1 + htmlparser2: 10.1.0 + parse5: 7.3.0 + parse5-htmlparser2-tree-adapter: 7.1.0 + parse5-parser-stream: 7.1.2 + undici: 7.24.7 + whatwg-mimetype: 4.0.0 + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -5524,6 +5830,8 @@ snapshots: cookie@1.1.1: {} + core-util-is@1.0.3: {} + cors@2.8.6: dependencies: object-assign: 4.1.1 @@ -5544,6 +5852,16 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-what@6.2.2: {} + cssesc@3.0.0: {} csstype@3.2.3: {} @@ -5605,6 +5923,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + denque@2.1.0: {} + depd@2.0.0: {} detect-libc@2.1.2: {} @@ -5613,12 +5933,45 @@ snapshots: diff@8.0.4: {} + dingbat-to-unicode@1.0.1: {} + doctrine@2.1.0: dependencies: esutils: 2.0.3 + docx@9.6.1: + dependencies: + '@types/node': 25.6.0 + hash.js: 1.1.7 + jszip: 3.10.1 + nanoid: 5.1.7 + xml: 1.0.1 + xml-js: 1.6.11 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dotenv@17.4.1: {} + duck@0.1.12: + dependencies: + underscore: 1.13.8 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -5644,11 +5997,22 @@ snapshots: encodeurl@2.0.0: {} + encoding-sniffer@0.2.1: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 + enhanced-resolve@5.20.1: dependencies: graceful-fs: 4.2.11 tapable: 2.3.2 + entities@4.5.0: {} + + entities@6.0.1: {} + + entities@7.0.1: {} + env-paths@2.2.1: {} error-ex@1.3.4: @@ -6147,6 +6511,10 @@ snapshots: fuzzysort@3.1.0: {} + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + generator-function@2.0.1: {} gensync@1.0.0-beta.2: {} @@ -6235,6 +6603,11 @@ snapshots: dependencies: has-symbols: 1.1.0 + hash.js@1.1.7: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -6249,6 +6622,13 @@ snapshots: hono@4.12.12: {} + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -6268,6 +6648,10 @@ snapshots: human-signals@8.0.1: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -6276,6 +6660,8 @@ snapshots: ignore@7.0.5: {} + immediate@3.0.6: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -6390,6 +6776,8 @@ snapshots: is-promise@4.0.0: {} + is-property@1.0.2: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -6443,6 +6831,8 @@ snapshots: dependencies: is-inside-container: 1.0.0 + isarray@1.0.0: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -6501,6 +6891,13 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -6520,6 +6917,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lightningcss-android-arm64@1.32.0: optional: true @@ -6582,14 +6983,24 @@ snapshots: chalk: 5.6.2 is-unicode-supported: 1.3.0 + long@5.3.2: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 + lop@0.4.2: + dependencies: + duck: 0.1.12 + option: 0.2.4 + underscore: 1.13.8 + lru-cache@5.1.1: dependencies: yallist: 3.1.1 + lru.min@1.1.4: {} + lucide-react@1.8.0(react@19.2.4): dependencies: react: 19.2.4 @@ -6598,6 +7009,19 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + mammoth@1.12.0: + dependencies: + '@xmldom/xmldom': 0.8.12 + argparse: 1.0.10 + base64-js: 1.5.1 + bluebird: 3.4.7 + dingbat-to-unicode: 1.0.1 + jszip: 3.10.1 + lop: 0.4.2 + path-is-absolute: 1.0.1 + underscore: 1.13.8 + xmlbuilder: 10.1.1 + math-intrinsics@1.1.0: {} media-typer@1.1.0: {} @@ -6623,6 +7047,8 @@ snapshots: mimic-function@5.0.1: {} + minimalistic-assert@1.0.1: {} + minimatch@10.2.5: dependencies: brace-expansion: 5.0.5 @@ -6662,8 +7088,26 @@ snapshots: mute-stream@2.0.0: {} + mysql2@3.22.0(@types/node@20.19.39): + dependencies: + '@types/node': 20.19.39 + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.2 + long: 5.3.2 + lru.min: 1.1.4 + named-placeholders: 1.1.6 + sql-escaper: 1.3.3 + + named-placeholders@1.1.6: + dependencies: + lru.min: 1.1.4 + nanoid@3.3.11: {} + nanoid@5.1.7: {} + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} @@ -6720,6 +7164,10 @@ snapshots: path-key: 4.0.0 unicorn-magic: 0.3.0 + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -6789,6 +7237,12 @@ snapshots: powershell-utils: 0.1.0 wsl-utils: 0.3.1 + openai@6.34.0(zod@3.25.76): + optionalDependencies: + zod: 3.25.76 + + option@0.2.4: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -6826,6 +7280,8 @@ snapshots: dependencies: p-limit: 3.1.0 + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -6839,12 +7295,27 @@ snapshots: parse-ms@4.0.0: {} + parse5-htmlparser2-tree-adapter@7.1.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.3.0 + + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.3.0 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + parseurl@1.3.3: {} path-browserify@1.0.1: {} path-exists@4.0.0: {} + path-is-absolute@1.0.1: {} + path-key@3.1.1: {} path-key@4.0.0: {} @@ -6890,6 +7361,8 @@ snapshots: dependencies: parse-ms: 4.0.0 + process-nextick-args@2.0.1: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -7022,6 +7495,16 @@ snapshots: react@19.2.4: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + recast@0.23.11: dependencies: ast-types: 0.16.1 @@ -7100,6 +7583,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.1.2: {} + safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 @@ -7113,6 +7598,8 @@ snapshots: safer-buffer@2.1.2: {} + sax@1.6.0: {} + scheduler@0.27.0: {} semver@6.3.1: {} @@ -7166,16 +7653,18 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + setimmediate@1.0.5: {} + setprototypeof@1.2.0: {} - shadcn@4.2.0(@types/node@20.19.39)(typescript@5.9.3): + shadcn@4.2.0(@cfworker/json-schema@4.1.1)(@types/node@20.19.39)(typescript@5.9.3): dependencies: '@babel/core': 7.29.0 '@babel/parser': 7.29.2 '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) '@dotenvx/dotenvx': 1.61.0 - '@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76) + '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) '@types/validate-npm-package-name': 4.0.2 browserslist: 4.28.2 commander: 14.0.3 @@ -7287,6 +7776,10 @@ snapshots: source-map@0.6.1: {} + sprintf-js@1.0.3: {} + + sql-escaper@1.3.3: {} + stable-hash@0.0.5: {} statuses@2.0.2: {} @@ -7362,6 +7855,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + stringify-object@5.0.0: dependencies: get-own-enumerable-keys: 1.0.0 @@ -7521,8 +8018,14 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + underscore@1.13.8: {} + undici-types@6.21.0: {} + undici-types@7.19.2: {} + + undici@7.24.7: {} + unicorn-magic@0.3.0: {} universalify@2.0.1: {} @@ -7592,6 +8095,12 @@ snapshots: web-streams-polyfill@3.3.3: {} + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -7662,6 +8171,14 @@ snapshots: is-wsl: 3.1.1 powershell-utils: 0.1.0 + xml-js@1.6.11: + dependencies: + sax: 1.6.0 + + xml@1.0.1: {} + + xmlbuilder@10.1.1: {} + y18n@5.0.8: {} yallist@3.1.1: {}