diff --git a/apps/desktop/electron/core/db.ts b/apps/desktop/electron/core/db.ts index 4a5a758..918d3a8 100644 --- a/apps/desktop/electron/core/db.ts +++ b/apps/desktop/electron/core/db.ts @@ -1,9 +1,6 @@ import path from "node:path"; import { JSONFilePreset } from "lowdb/node"; import { app } from "electron"; -import { fileURLToPath } from "node:url"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); const initDb = async () => { // Read or create db.json diff --git a/apps/desktop/electron/core/dicom.ts b/apps/desktop/electron/core/dicom.ts index d74f1a2..e3e90ab 100644 --- a/apps/desktop/electron/core/dicom.ts +++ b/apps/desktop/electron/core/dicom.ts @@ -3,18 +3,41 @@ import * as dicomParser from "dicom-parser"; import fs from "fs"; export interface StructuredData { - [StudyInstanceUID: string]: { - [SeriesInstanceUID: string]: ExtractMetadata[]; - }; + [SeriesInstanceUID: string]: ExtractMetadata[]; } export interface ExtractMetadata { filePath: string; StudyInstanceUID?: string; SeriesInstanceUID?: string; + PatientName?: string; pixelData?: Uint16Array; } +/** + * 检查文件是否为 DICOM 文件(通过 Magic Number 判断) + * @param filePath 文件路径 + * @returns 是否为 DICOM 文件 + */ +const isDICOMFile = async (filePath: string) => { + try { + // 打开文件以进行读取 + const fileHandle = await fs.promises.open(filePath, "r"); + const buffer = Buffer.alloc(132); // 创建一个 132 字节的缓冲区 + + // 从文件中读取前 132 个字节 + await fileHandle.read(buffer, 0, 132, 0); + await fileHandle.close(); // 关闭文件 + + // 检查 "DICM" 标识 (偏移 128-131 字节) + const magicNumber = buffer.toString("utf-8", 128, 132); + return magicNumber === "DICM"; + } catch (error) { + console.error(`Error reading file ${filePath}:`, error); + return false; + } +}; + /** * 定义一个异步函数来递归地查找.dcm文件 * @param dir @@ -31,8 +54,8 @@ export const findDcmFiles = async ( const filePath = path.join(dir, file.name); if (file.isDirectory()) { await findDcmFiles(filePath, fileList); // 递归调用以遍历子目录 - } else if (file.name.endsWith(".dcm")) { - fileList.push(filePath); // 如果文件是.dcm文件,添加到列表中 + } else if (await isDICOMFile(filePath)) { + fileList.push(filePath); // 如果文件是 DICOM 文件,添加到列表中 } }) ); @@ -52,6 +75,7 @@ export const parseDICOMFile = async ( const dataSet = dicomParser.parseDicom(byteArray, options); const StudyInstanceUID = dataSet.string("x0020000d"); const SeriesInstanceUID = dataSet.string("x0020000e"); + const PatientName = dataSet.string("x00100030"); const pixelDataElement = dataSet.elements.x7fe00010; const pixelData = new Uint16Array( dataSet.byteArray.buffer, @@ -63,6 +87,7 @@ export const parseDICOMFile = async ( filePath, StudyInstanceUID, SeriesInstanceUID, + PatientName, // pixelData, }; } catch (error) { @@ -98,26 +123,40 @@ export const processFilesInBatches = async ( return results; }; -export const structureMetadata = (data: ExtractMetadata[]): StructuredData => { - const structured: StructuredData = {}; +export interface StructuredMetadata { + filePaths: string[]; + StudyInstanceUID?: string; + SeriesInstanceUID?: string; + PatientName?: string; +} + +export const structureMetadata = ( + data: ExtractMetadata[] +): StructuredMetadata[] => { + const result: StructuredMetadata[] = []; data.forEach((item) => { - // 确保每个元素都有有效的 StudyInstanceUID 和 SeriesInstanceUID - if (item.StudyInstanceUID && item.SeriesInstanceUID) { - // 如果还没有为这个 StudyInstanceUID 创建记录,则初始化一个空对象 - if (!structured[item.StudyInstanceUID]) { - structured[item.StudyInstanceUID] = {}; - } + // 查找是否已经有相同 UID 和 PatientName 的条目 + const existingEntry = result.find( + (entry) => + entry.StudyInstanceUID === item.StudyInstanceUID && + entry.SeriesInstanceUID === item.SeriesInstanceUID && + entry.PatientName === item.PatientName + ); - // 如果这个 StudyInstanceUID 下还没有这个 SeriesInstanceUID 的记录,则初始化一个空数组 - if (!structured[item.StudyInstanceUID][item.SeriesInstanceUID]) { - structured[item.StudyInstanceUID][item.SeriesInstanceUID] = []; - } - - // 将当前元素添加到对应的数组中 - structured[item.StudyInstanceUID][item.SeriesInstanceUID].push(item); + if (existingEntry) { + // 如果找到了相同的条目,合并 filePath + existingEntry.filePaths.push(item.filePath); + } else { + // 如果没有找到,创建一个新的条目 + result.push({ + filePaths: [item.filePath], + StudyInstanceUID: item.StudyInstanceUID, + SeriesInstanceUID: item.SeriesInstanceUID, + PatientName: item.PatientName, + }); } }); - return structured; + return result; }; diff --git a/apps/desktop/electron/ipcMainHandlers.ts b/apps/desktop/electron/ipcMainHandlers.ts index 53d30a2..af69e87 100644 --- a/apps/desktop/electron/ipcMainHandlers.ts +++ b/apps/desktop/electron/ipcMainHandlers.ts @@ -46,6 +46,19 @@ const registerIpcMainHandlers = ( const { running } = data; running ? pythonManager.startFlask() : pythonManager.stopFlask(); }); + + ipcMain.on("scan-dicom", async () => { + const result = await dialog.showOpenDialog({ + properties: ["openDirectory"], + }); + if (result.canceled) return null; + const filePaths = result.filePaths[0]; + const dcmPaths = await findDcmFiles(filePaths); + const items = await processFilesInBatches(dcmPaths, 4); + const structDicom = await structureMetadata(items); + Database.getDb().data.posts.push("hello world"); + return structDicom; + }); }; export default registerIpcMainHandlers; diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 452abe4..f43954d 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -11,7 +11,6 @@ import { fileURLToPath } from "node:url"; import path from "node:path"; import registerIpcMainHandlers from "./ipcMainHandlers"; import PythonManager from "./core/PythonManager"; -import "./core/db"; const require = createRequire(import.meta.url); const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 78fe14a..a671feb 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -13,12 +13,15 @@ "dependencies": { "@ant-design/icons": "^5.4.0", "@google-cloud/spanner": "^7.12.0", + "@hookform/resolvers": "3.9.0", "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-menubar": "^1.1.1", + "@radix-ui/react-radio-group": "^1.2.0", "@radix-ui/react-scroll-area": "^1.1.0", + "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", @@ -48,12 +51,14 @@ "react-day-picker": "^8.10.1", "react-desktop": "^0.3.9", "react-dom": "^18.2.0", + "react-dropzone": "14.2.3", + "react-hook-form": "7.53.0", "react-icons": "^5.2.1", "react-resizable-panels": "^2.0.20", "react-router-dom": "^6.26.0", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", - "react-dropzone": "14.2.3" + "zod": "3.23.8" }, "devDependencies": { "@radix-ui/react-icons": "^1.3.0", @@ -75,4 +80,4 @@ "vite-plugin-electron": "^0.28.6", "vite-plugin-electron-renderer": "^0.14.5" } -} \ No newline at end of file +} diff --git a/apps/desktop/src/components/ui/alert.tsx b/apps/desktop/src/components/ui/alert.tsx new file mode 100644 index 0000000..5afd41d --- /dev/null +++ b/apps/desktop/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/apps/desktop/src/components/ui/form.tsx b/apps/desktop/src/components/ui/form.tsx new file mode 100644 index 0000000..b6daa65 --- /dev/null +++ b/apps/desktop/src/components/ui/form.tsx @@ -0,0 +1,178 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +
); }; diff --git a/apps/desktop/src/pages/Datasource/index.tsx b/apps/desktop/src/pages/Datasource/index.tsx index d9bad78..dfbd93c 100644 --- a/apps/desktop/src/pages/Datasource/index.tsx +++ b/apps/desktop/src/pages/Datasource/index.tsx @@ -1,9 +1,13 @@ -import FileUpload from "./FileUpload"; +import { Button } from "@/components/ui/button"; export const Datasource = () => { return (
- +
+ +
); }; diff --git a/apps/desktop/src/pages/Layout.tsx b/apps/desktop/src/pages/Layout.tsx index 136d9af..f5e707f 100644 --- a/apps/desktop/src/pages/Layout.tsx +++ b/apps/desktop/src/pages/Layout.tsx @@ -1,3 +1,4 @@ +import { Toaster } from "@/components/ui/toaster"; import { LeftDocker } from "./LeftDocker"; import { Outlet } from "react-router-dom"; @@ -6,8 +7,7 @@ const LayoutMain = () => { document.querySelector("html")?.getAttribute("platform") ?? "macos"; // const titleBarStyles = // platform === "macos" ? "pl-[5rem] pr-[.5rem]" : "pl-[.5rem]"; - const titleBarStyles = - platform === "macos" ? "px-[.5rem]" : "pl-[.5rem]"; + const titleBarStyles = platform === "macos" ? "px-[.5rem]" : "pl-[.5rem]"; return (
@@ -35,6 +35,7 @@ const LayoutMain = () => {
+ ); }; diff --git a/apps/desktop/src/pages/Models/index.tsx b/apps/desktop/src/pages/Models/index.tsx index d14fe80..35ac5fb 100644 --- a/apps/desktop/src/pages/Models/index.tsx +++ b/apps/desktop/src/pages/Models/index.tsx @@ -1,5 +1,15 @@ +import { motion } from "framer-motion"; import { ModelTable } from "./ModelTable"; export const Models = () => { - return ; + return ( + + + + ); }; diff --git a/apps/desktop/src/pages/Setting/index.tsx b/apps/desktop/src/pages/Setting/index.tsx index e470494..b91dadb 100644 --- a/apps/desktop/src/pages/Setting/index.tsx +++ b/apps/desktop/src/pages/Setting/index.tsx @@ -1,12 +1,98 @@ -import { motion } from 'framer-motion'; +import { motion } from "framer-motion"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { toast } from "@/components/ui/use-toast"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; + +const FormSchema = z.object({ + inferEngine: z.string({ + required_error: "Please select an email to display.", + }), + workspace: z.string({ required_error: "请设置工作空间" }), +}); export const Setting = () => { - return -
设置
-
-} \ No newline at end of file + const form = useForm>({ + resolver: zodResolver(FormSchema), + }); + + const onSubmit = (data: z.infer) => { + toast({ + title: "You submitted the following values:", + description: ( +
+          {JSON.stringify(data, null, 2)}
+        
+ ), + }); + }; + + return ( + +
+
+ + ( + +
+ 生成引擎 + 选择推理的硬件设备 +
+
+ +
+
+ )} + /> + + +
+
+ ); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e85955..669fdd3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,6 +16,9 @@ importers: '@google-cloud/spanner': specifier: ^7.12.0 version: 7.14.0 + '@hookform/resolvers': + specifier: 3.9.0 + version: 3.9.0(react-hook-form@7.53.0(react@18.3.1)) '@radix-ui/react-checkbox': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -31,9 +34,15 @@ importers: '@radix-ui/react-menubar': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-radio-group': + specifier: ^1.2.0 + version: 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-scroll-area': specifier: ^1.1.0 version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': + specifier: ^2.1.1 + version: 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': specifier: ^1.1.0 version: 1.1.0(@types/react@18.3.4)(react@18.3.1) @@ -124,6 +133,9 @@ importers: react-dropzone: specifier: 14.2.3 version: 14.2.3(react@18.3.1) + react-hook-form: + specifier: 7.53.0 + version: 7.53.0(react@18.3.1) react-icons: specifier: ^5.2.1 version: 5.3.0(react@18.3.1) @@ -139,6 +151,9 @@ importers: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.10) + zod: + specifier: 3.23.8 + version: 3.23.8 devDependencies: '@radix-ui/react-icons': specifier: ^1.3.0 @@ -569,6 +584,11 @@ packages: engines: {node: '>=6'} hasBin: true + '@hookform/resolvers@3.9.0': + resolution: {integrity: sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==} + peerDependencies: + react-hook-form: ^7.0.0 + '@huggingface/jinja@0.2.2': resolution: {integrity: sha512-/KPde26khDUIPkTGU82jdtTW9UAuvUTumCAbFs/7giR0SxsvZC4hru51PBvpijH6BVkHcROcvZM/lpy5h1jRRA==} engines: {node: '>=18'} @@ -1024,6 +1044,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-radio-group@1.2.0': + resolution: {integrity: sha512-yv+oiLaicYMBpqgfpSPw6q+RyXlLdIpQWDHZbUKURxe+nEh53hFXPPlfhfQQtYkS5MMK/5IWIa76SksleQZSzw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.1.0': resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==} peerDependencies: @@ -1050,6 +1083,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-select@2.1.1': + resolution: {integrity: sha512-8iRDfyLtzxlprOo9IicnzvpsO1wNCkuwzzCM+Z5Rb5tNOpCdMvcc2AkzX0Fz+Tz9v6NJ5B/7EEgyZveo4FBRfQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.0.2': resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: @@ -3418,6 +3464,12 @@ packages: peerDependencies: react: '>= 16.8 || 18.0.0' + react-hook-form@7.53.0: + resolution: {integrity: sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-icons@5.3.0: resolution: {integrity: sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==} peerDependencies: @@ -4058,6 +4110,9 @@ packages: resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} engines: {node: '>= 10'} + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + snapshots: 7zip-bin@5.2.0: {} @@ -4483,6 +4538,10 @@ snapshots: protobufjs: 7.4.0 yargs: 17.7.2 + '@hookform/resolvers@3.9.0(react-hook-form@7.53.0(react@18.3.1))': + dependencies: + react-hook-form: 7.53.0(react@18.3.1) + '@huggingface/jinja@0.2.2': {} '@humanwhocodes/config-array@0.11.14': @@ -4930,6 +4989,24 @@ snapshots: '@types/react': 18.3.4 '@types/react-dom': 18.3.0 + '@radix-ui/react-radio-group@1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.4)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.4)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.4)(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.4)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.4)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.4)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.4 + '@types/react-dom': 18.3.0 + '@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -4964,6 +5041,35 @@ snapshots: '@types/react': 18.3.4 '@types/react-dom': 18.3.0 + '@radix-ui/react-select@2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.4)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.4)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.4)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.4)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.4)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.4)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.4)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.4)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.4)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.4)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.3.4)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.4 + '@types/react-dom': 18.3.0 + '@radix-ui/react-slot@1.0.2(@types/react@18.3.4)(react@18.3.1)': dependencies: '@babel/runtime': 7.25.4 @@ -7666,6 +7772,10 @@ snapshots: prop-types: 15.8.1 react: 18.3.1 + react-hook-form@7.53.0(react@18.3.1): + dependencies: + react: 18.3.1 + react-icons@5.3.0(react@18.3.1): dependencies: react: 18.3.1 @@ -8332,3 +8442,5 @@ snapshots: archiver-utils: 3.0.4 compress-commons: 4.1.2 readable-stream: 3.6.2 + + zod@3.23.8: {}