first commit

This commit is contained in:
mozzie 2024-08-29 16:59:25 +08:00
commit 201619055d
70 changed files with 4467 additions and 0 deletions

View File

@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

30
apps/desktop/README.md Normal file
View File

@ -0,0 +1,30 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list

View File

@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/style/global.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

View File

@ -0,0 +1,43 @@
// @see - https://www.electron.build/configuration/configuration
{
"$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json",
"appId": "YourAppID",
"asar": true,
"productName": "YourAppName",
"directories": {
"output": "release/${version}"
},
"files": [
"dist",
"dist-electron"
],
"mac": {
"target": [
"dmg"
],
"artifactName": "${productName}-Mac-${version}-Installer.${ext}"
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
}
],
"artifactName": "${productName}-Windows-${version}-Setup.${ext}"
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true,
"deleteAppDataOnUninstall": false
},
"linux": {
"target": [
"AppImage"
],
"artifactName": "${productName}-Linux-${version}.${ext}"
}
}

View File

@ -0,0 +1,101 @@
import http from "node:http";
import path from "node:path";
import { spawn, ChildProcess } from "node:child_process";
import { BrowserWindow } from "electron";
class PythonManager {
public flaskProcess: ChildProcess | null = null;
private intervalId: NodeJS.Timeout | null = null;
constructor(
private mainWindow: BrowserWindow | null,
private url: string,
private interval = 5000
) {}
// 启动 Python 服务
public startFlask() {
if (this.flaskProcess) {
console.log("Flask service is already running.");
this.mainWindow?.webContents.send("flask", { running: true });
return;
}
// 使用 spawn 启动 Flask 服务
this.flaskProcess = spawn(path.join(process.env.VITE_PUBLIC!, "flask_app"));
// 实时获取 stdout 日志
this.flaskProcess.stdout?.on("data", (data) => {
const message = data.toString();
console.log(`Flask stdout: ${message}`);
this.mainWindow?.webContents.send("flask", { stdout: message });
});
// 实时获取 stderr 日志
this.flaskProcess.stderr?.on("data", (data) => {
const message = data.toString();
console.error(`Flask stderr: ${message}`);
this.mainWindow?.webContents.send("flask-service:response", { stderr: message });
});
// 监听进程关闭事件
this.flaskProcess.on("close", (code) => {
console.log(`Flask process exited with code ${code}`);
this.flaskProcess = null;
this.mainWindow?.webContents.send("flask-service:response", { exited: true, code });
});
// 开始轮询服务状态
// this.startCheckingFlaskStatus();
}
// 停止 Python 服务
public stopFlask() {
if (this.flaskProcess) {
this.flaskProcess.kill();
console.log("Flask service stopped.");
this.flaskProcess = null;
}
// 停止轮询
this.stopCheckingFlaskStatus();
}
// 检查 Flask 服务状态
private checkFlaskStatus() {
if (!this.mainWindow) return;
http
.get(this.url, (res) => {
const { statusCode } = res;
this.mainWindow?.webContents.send("flask-check", {
running: statusCode === 200,
});
})
.on("error", (err) => {
console.error(`Error checking Flask service: ${err.message}`);
this.mainWindow?.webContents.send("flask-check", {
running: false,
});
});
}
// 开始轮询 Flask 服务状态
private startCheckingFlaskStatus() {
if (this.intervalId) {
console.log("Already checking Flask status.");
return;
}
this.intervalId = setInterval(() => this.checkFlaskStatus(), this.interval);
}
// 停止轮询 Flask 服务状态
private stopCheckingFlaskStatus() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
}
export default PythonManager;

View File

@ -0,0 +1,27 @@
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
const defaultData = { posts: [] };
const db = await JSONFilePreset(
path.join(app.getPath("userData"), "db.json"),
defaultData
);
// Update db.json
await db.update(({ posts }) => posts.push("hello world"));
// Alternatively you can call db.write() explicitely later
// to write to db.json
db.data.posts.push("hello world");
await db.write();
console.log(db);
};
initDb();

View File

@ -0,0 +1,123 @@
import path from "path";
import * as dicomParser from "dicom-parser";
import fs from "fs";
export interface StructuredData {
[StudyInstanceUID: string]: {
[SeriesInstanceUID: string]: ExtractMetadata[];
};
}
export interface ExtractMetadata {
filePath: string;
StudyInstanceUID?: string;
SeriesInstanceUID?: string;
pixelData?: Uint16Array;
}
/**
* .dcm文件
* @param dir
* @param fileList
* @returns
*/
export const findDcmFiles = async (
dir: string,
fileList: string[] = []
): Promise<string[]> => {
const files = await fs.promises.readdir(dir, { withFileTypes: true });
await Promise.all(
files.map(async (file) => {
const filePath = path.join(dir, file.name);
if (file.isDirectory()) {
await findDcmFiles(filePath, fileList); // 递归调用以遍历子目录
} else if (file.name.endsWith(".dcm")) {
fileList.push(filePath); // 如果文件是.dcm文件添加到列表中
}
})
);
return fileList;
};
/**
* dcm文件的metadata信息
*/
export const parseDICOMFile = async (
filePath: string
): Promise<ExtractMetadata | undefined> => {
try {
const arrayBuffer = await fs.promises.readFile(filePath);
const byteArray = new Uint8Array(arrayBuffer);
const options = { TransferSyntaxUID: "1.2.840.10008.1.2" };
const dataSet = dicomParser.parseDicom(byteArray, options);
const StudyInstanceUID = dataSet.string("x0020000d");
const SeriesInstanceUID = dataSet.string("x0020000e");
const pixelDataElement = dataSet.elements.x7fe00010;
const pixelData = new Uint16Array(
dataSet.byteArray.buffer,
pixelDataElement.dataOffset,
pixelDataElement.length / 2
);
return {
filePath,
StudyInstanceUID,
SeriesInstanceUID,
// pixelData,
};
} catch (error) {
console.error(`Error parsing file ${filePath}:`, error);
return undefined;
}
};
/**
*
* @param filePaths
* @param {number} batchSize
* @returns
*/
export const processFilesInBatches = async (
filePaths: string[],
batchSize: number
) => {
const results = [];
for (let i = 0; i < filePaths.length; i += batchSize) {
const batch = filePaths.slice(i, i + batchSize);
const batchResults = await Promise.allSettled(
batch.map((filePath) => parseDICOMFile(filePath))
);
// 只提取状态为 'fulfilled' 的结果的 value
const fulfilledResults = batchResults
.filter((result) => result.status === "fulfilled")
.map(
(result) => (result as PromiseFulfilledResult<ExtractMetadata>).value
);
results.push(...fulfilledResults);
}
return results;
};
export const structureMetadata = (data: ExtractMetadata[]): StructuredData => {
const structured: StructuredData = {};
data.forEach((item) => {
// 确保每个元素都有有效的 StudyInstanceUID 和 SeriesInstanceUID
if (item.StudyInstanceUID && item.SeriesInstanceUID) {
// 如果还没有为这个 StudyInstanceUID 创建记录,则初始化一个空对象
if (!structured[item.StudyInstanceUID]) {
structured[item.StudyInstanceUID] = {};
}
// 如果这个 StudyInstanceUID 下还没有这个 SeriesInstanceUID 的记录,则初始化一个空数组
if (!structured[item.StudyInstanceUID][item.SeriesInstanceUID]) {
structured[item.StudyInstanceUID][item.SeriesInstanceUID] = [];
}
// 将当前元素添加到对应的数组中
structured[item.StudyInstanceUID][item.SeriesInstanceUID].push(item);
}
});
return structured;
};

27
apps/desktop/electron/electron-env.d.ts vendored Normal file
View File

@ -0,0 +1,27 @@
/// <reference types="vite-plugin-electron/electron-env" />
declare namespace NodeJS {
interface ProcessEnv {
/**
* The built directory structure
*
* ```tree
* dist
* index.html
*
* dist-electron
* main.js
* preload.js
*
* ```
*/
APP_ROOT: string
/** /dist/ or /public/ */
VITE_PUBLIC: string
}
}
// Used in Renderer process, expose in `preload.ts`
interface Window {
ipcRenderer: import('electron').IpcRenderer
}

View File

@ -0,0 +1 @@
export const EVENT_PARSE_DICOM = "PARSE_DICOM";

View File

@ -0,0 +1,51 @@
import { dialog, ipcMain } from "electron";
import os from "os";
import {
findDcmFiles,
processFilesInBatches,
structureMetadata,
} from "./core/dicom";
import { EVENT_PARSE_DICOM } from "./ipcEvent";
import PythonManager from "./core/PythonManager";
/**
*
*/
const registerIpcMainHandlers = (
mainWindow: Electron.BrowserWindow | null,
pythonManager: PythonManager
) => {
if (!mainWindow) return;
ipcMain.removeAllListeners();
/**
*
*/
ipcMain.on("ipc-loaded", () => mainWindow.show());
/**
* dicoM
*/
ipcMain.on(EVENT_PARSE_DICOM, async (event, file: string) => {
const dirDialog = await dialog.showOpenDialog(mainWindow, {
properties: ["openDirectory"],
});
if (dirDialog.filePaths.length > 0) {
const filePaths = await findDcmFiles(dirDialog.filePaths[0]);
const batchSize = os.cpus().length * 1 || 10;
console.time("分批处理");
const unraw = await processFilesInBatches(filePaths, batchSize);
console.timeEnd("分批处理");
const result = structureMetadata(unraw);
event.reply(EVENT_PARSE_DICOM + ":RES", result);
}
});
ipcMain.on("python-service", (event, data) => {
const { running } = data;
running ? pythonManager.startFlask() : pythonManager.stopFlask();
});
};
export default registerIpcMainHandlers;

View File

@ -0,0 +1,149 @@
import {
app,
BrowserWindow,
Tray,
Menu,
globalShortcut,
nativeImage,
} from "electron";
import { createRequire } from "node:module";
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));
process.env.APP_ROOT = path.join(__dirname, "..");
export const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"];
export const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron");
export const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist");
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
? path.join(process.env.APP_ROOT, "public")
: RENDERER_DIST;
let win: BrowserWindow | null;
let tray: Tray | null = null;
let pythonManager: PythonManager | null;
const theme: "dark" | "light" = "light";
const themeTitleBarStyles = {
dark: { color: "rgb(32,32,32)", symbolColor: "#fff" },
light: {},
};
function createWindow() {
win = new BrowserWindow({
width: 1280,
height: 800,
show: false, // 先隐藏。等待渲染完成,防止闪烁
icon: path.join(process.env.VITE_PUBLIC, "AI.png"),
// frame: false,
titleBarStyle: "hidden", // customButtonsOnHover || hidden || hiddenInset
titleBarOverlay: { height: 36, ...themeTitleBarStyles[theme] }, // 渲染进程发消息动态改变这个
webPreferences: {
preload: path.join(__dirname, "preload.mjs"),
nodeIntegration: true,
},
});
// Test active push message to Renderer-process.
win.webContents.on("did-finish-load", () => {
win?.webContents.send("main-process-message", {
platform: process.platform === "darwin" ? "macos" : "windows",
theme,
});
});
if (VITE_DEV_SERVER_URL) {
win.loadURL(VITE_DEV_SERVER_URL);
} else {
win.loadFile(path.join(RENDERER_DIST, "index.html"));
}
pythonManager = new PythonManager(win, "http://127.0.0.1:15001", 3000);
registerIpcMainHandlers(win, pythonManager);
}
function createTray() {
if (tray) tray.destroy();
const iconPath = path.join(process.env.VITE_PUBLIC, "AI.png"); // 使用 PNG 图标
const icon = nativeImage
.createFromPath(iconPath)
.resize({ width: 20, height: 20 });
tray = new Tray(icon);
const contextMenu = Menu.buildFromTemplate([
{
label: "Show App",
click: function () {
if (win) {
win.show();
}
},
},
{
label: "Quit",
click: function () {
app.quit();
},
},
]);
tray.setToolTip("My Electron App");
tray.setContextMenu(contextMenu);
tray.on("click", () => {
if (win) {
win.isVisible() ? win.hide() : win.show();
}
});
}
function registerGlobalShortcuts() {
// 注册全局快捷键 'CommandOrControl+Shift+S' 来显示应用窗口
globalShortcut.register("Option+N", () => {
if (win) {
win.isVisible() ? win.hide() : win.show();
}
});
}
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
win = null;
}
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
app.on("before-quit", () => {
if (pythonManager?.flaskProcess) pythonManager?.stopFlask();
});
app.whenReady().then(() => {
createWindow();
createTray();
registerGlobalShortcuts();
// 设置 Dock 图标
if (process.platform === "darwin") {
const dockIconPath = path.join(process.env.VITE_PUBLIC, "girl.png");
const dockIcon = nativeImage.createFromPath(dockIconPath);
app.dock.setIcon(dockIcon);
}
});
// 注销全局快捷键,当应用退出时
app.on("will-quit", () => {
globalShortcut.unregisterAll();
});

View File

@ -0,0 +1,23 @@
import { ipcRenderer, contextBridge } from "electron";
// --------- Expose some API to the Renderer process ---------
contextBridge.exposeInMainWorld("ipcRenderer", {
on(...args: Parameters<typeof ipcRenderer.on>) {
const [channel, listener] = args;
return ipcRenderer.on(channel, (event, ...args) =>
listener(event, ...args)
);
},
off(...args: Parameters<typeof ipcRenderer.off>) {
const [channel, ...omit] = args;
return ipcRenderer.off(channel, ...omit);
},
send(...args: Parameters<typeof ipcRenderer.send>) {
const [channel, ...omit] = args;
return ipcRenderer.send(channel, ...omit);
},
invoke(...args: Parameters<typeof ipcRenderer.invoke>) {
const [channel, ...omit] = args;
return ipcRenderer.invoke(channel, ...omit);
},
});

13
apps/desktop/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

78
apps/desktop/package.json Normal file
View File

@ -0,0 +1,78 @@
{
"name": "@cvpilot/desktop",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "dist-electron/main.js",
"scripts": {
"dev": "vite",
"build": "tsc && vite build && electron-builder",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons": "^5.4.0",
"@google-cloud/spanner": "^7.12.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-scroll-area": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.2",
"@tanstack/react-table": "^8.20.5",
"@types/react-icons": "^3.0.0",
"@xenova/transformers": "^2.17.2",
"antd": "^5.20.0",
"better-sqlite3": "^11.1.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"custom-electron-titlebar": "^4.2.8",
"date-fns": "^3.6.0",
"dexie": "^4.0.8",
"dicom-parser": "1.8.21",
"dockview": "^1.15.2",
"electron-store": "^10.0.0",
"flexlayout-react": "^0.7.15",
"framer-motion": "^11.3.24",
"lowdb": "^7.0.1",
"lucide-react": "^0.408.0",
"object-hash": "^3.0.0",
"onnxruntime-node": "^1.18.0",
"openvino-node": "2024.3.0",
"react": "^18.2.0",
"react-day-picker": "^8.10.1",
"react-desktop": "^0.3.9",
"react-dom": "^18.2.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"
},
"devDependencies": {
"@radix-ui/react-icons": "^1.3.0",
"@types/react": "^18.2.64",
"@types/react-dom": "^18.2.21",
"@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.19",
"electron": "30.4.0",
"electron-builder": "^24.13.3",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"postcss": "^8.4.39",
"tailwindcss": "^3.4.4",
"typescript": "^5.2.2",
"vite": "^5.1.6",
"vite-plugin-electron": "^0.28.6",
"vite-plugin-electron-renderer": "^0.14.5"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
apps/desktop/public/AI.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 MiB

BIN
apps/desktop/public/flask_app Executable file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

View File

@ -0,0 +1,233 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2841 4814 c-30 -8 -62 -20 -70 -26 -72 -54 -117 -123 -108 -167 l6
-26 -138 3 c-389 9 -757 -142 -1042 -427 -121 -122 -178 -200 -259 -354 -95
-179 -128 -295 -160 -562 -55 -456 -122 -632 -314 -832 -65 -68 -96 -107 -89
-114 5 -5 24 -14 42 -19 29 -8 43 -4 119 36 48 25 113 63 145 84 l58 39 -6
-37 c-4 -20 -25 -111 -47 -202 -27 -109 -40 -185 -39 -225 1 -44 -2 -56 -9
-45 -8 11 -9 7 -4 -15 4 -16 11 -34 15 -39 5 -6 8 -21 8 -35 -1 -25 -2 -25 -9
4 -5 19 -7 2 -4 -45 5 -86 21 -145 57 -217 27 -53 73 -113 87 -113 18 0 19 33
1 68 -10 20 -22 53 -26 72 -7 30 -9 32 -16 15 -11 -28 -11 -37 1 -30 5 3 10
-1 10 -10 0 -9 7 -29 15 -45 9 -17 11 -30 5 -30 -18 0 -99 176 -108 237 -13
84 -5 297 13 378 17 76 35 116 35 79 0 -14 8 -24 21 -27 13 -4 18 -10 14 -18
-5 -6 -9 -61 -10 -120 -2 -60 -8 -132 -15 -160 -14 -59 -8 -153 14 -195 56
-110 87 -192 85 -224 -3 -43 6 -60 32 -60 22 0 24 15 4 50 -8 14 -14 37 -15
51 0 15 -11 57 -25 93 -21 57 -25 83 -25 179 1 62 11 179 23 264 22 149 37
201 37 131 0 -20 7 -49 15 -64 8 -16 15 -37 15 -47 0 -20 32 -37 38 -19 2 6
10 8 18 5 10 -4 14 -23 14 -72 0 -199 31 -378 100 -576 17 -49 32 -98 34 -108
1 -9 7 -22 14 -29 24 -24 23 38 -4 117 -33 100 -45 268 -21 300 14 18 16 13
32 -60 43 -204 88 -300 172 -373 l54 -47 -25 -22 c-15 -12 -42 -25 -61 -29
-20 -3 -35 -12 -35 -20 0 -21 43 -17 70 6 13 11 29 20 35 20 7 0 23 8 36 18
l22 19 -52 79 c-31 46 -48 80 -41 82 6 2 12 2 14 1 1 -2 28 -34 59 -70 40 -47
63 -66 75 -62 10 2 26 -5 36 -16 17 -18 17 -21 2 -43 -21 -30 -20 -41 3 -27
15 10 26 6 67 -21 27 -19 50 -37 52 -41 2 -4 9 -6 15 -4 7 3 18 1 25 -4 11 -7
12 -12 1 -29 -15 -25 -106 -75 -113 -63 -3 5 1 11 9 15 8 3 15 12 15 21 0 12
-4 11 -20 -5 -23 -23 -50 -15 -69 18 -9 15 -10 15 -11 0 0 -20 55 -68 78 -68
55 1 110 24 151 63 l44 42 -27 -47 c-25 -44 -26 -49 -12 -75 8 -16 42 -48 75
-71 47 -33 63 -51 71 -80 5 -20 10 -42 10 -49 0 -35 54 -175 86 -221 47 -71
96 -94 175 -83 64 8 113 34 143 74 16 22 21 48 26 130 3 56 9 107 13 113 5 8
16 7 41 -5 41 -19 56 -52 56 -124 0 -99 53 -164 150 -186 93 -20 175 20 219
109 21 42 56 158 66 218 6 36 15 48 64 86 91 70 105 101 76 170 -8 20 -12 36
-9 36 4 0 25 -15 48 -34 51 -41 98 -66 126 -66 12 0 28 -7 36 -15 21 -21 70
-19 93 4 20 20 38 87 27 98 -3 4 -6 -5 -6 -19 0 -14 -8 -36 -19 -49 -18 -22
-19 -22 -36 -5 -13 13 -15 20 -6 29 6 6 11 20 11 32 0 15 3 16 9 7 7 -11 10
-10 15 3 8 22 8 25 -5 25 -6 0 -20 10 -31 22 l-19 23 5 -33 c7 -41 -8 -82 -31
-82 -9 0 -32 14 -50 32 l-32 31 20 29 c11 16 38 37 59 48 21 11 47 31 58 45
11 14 22 25 25 25 2 0 10 -14 17 -30 7 -17 14 -29 17 -27 2 2 -6 26 -18 53
-11 27 -25 60 -30 74 l-10 24 22 -19 21 -20 35 40 c50 57 103 141 130 205 l23
55 0 -52 c0 -32 -10 -78 -25 -119 -14 -37 -23 -69 -20 -72 10 -10 94 81 146
155 27 40 72 119 99 175 40 86 49 98 49 72 2 -80 -103 -320 -160 -368 -27 -23
-38 -46 -22 -46 4 0 17 10 30 22 13 12 25 19 29 16 3 -3 -4 -15 -16 -26 -11
-10 -29 -36 -39 -56 -30 -58 2 -46 77 31 56 57 63 69 53 84 -15 25 -14 46 3
56 21 12 63 101 81 168 8 33 15 101 15 154 0 76 2 92 13 82 17 -17 13 -224 -7
-325 -9 -44 -13 -82 -10 -85 15 -15 108 108 154 204 60 123 87 229 101 395 10
128 9 157 -17 318 -9 52 -13 96 -11 99 3 3 26 -9 51 -26 100 -69 190 -97 215
-67 15 18 6 41 -37 94 -72 90 -151 244 -197 383 -48 144 -95 369 -95 450 0 27
-6 68 -14 91 -8 24 -16 63 -19 88 -19 148 -124 404 -231 565 -92 137 -187 229
-331 325 -103 69 -286 163 -365 189 -22 7 -36 18 -37 29 -11 124 -76 211 -184
245 -62 20 -106 21 -178 1z m175 -29 c62 -18 115 -62 148 -122 l26 -46 -54 51
c-54 51 -92 69 -121 58 -11 -4 -14 -2 -9 6 5 8 -2 9 -26 4 -37 -7 -85 -34 -77
-43 3 -3 16 2 29 11 26 19 53 21 42 4 -5 -8 -2 -9 9 -5 29 11 19 -2 -15 -19
-30 -16 -32 -16 -19 -1 7 10 9 17 3 17 -15 0 -33 -26 -24 -35 4 -5 3 -6 -3 -3
-5 4 -26 -10 -46 -30 -26 -27 -43 -58 -59 -112 -13 -41 -29 -84 -37 -95 -12
-18 -13 -17 -13 15 1 46 23 145 43 193 9 21 13 36 8 33 -7 -5 -25 -43 -55
-121 -8 -22 -15 -32 -15 -22 -1 18 -21 25 -21 7 0 -5 -4 -10 -9 -10 -14 0 -33
91 -26 129 19 110 181 178 321 136z m83 -166 c17 -17 31 -34 31 -37 0 -4 -47
-7 -105 -7 -58 0 -105 3 -105 6 0 3 10 15 23 26 59 53 111 58 156 12z m69 -45
c-4 -3 -12 5 -18 18 -12 22 -12 22 6 6 10 -10 15 -20 12 -24z m-502 -22 c11
-6 5 -12 -24 -20 -22 -7 -45 -19 -52 -27 -7 -8 -17 -12 -24 -9 -6 3 0 -4 14
-15 14 -11 33 -21 43 -21 9 0 17 -4 17 -10 0 -13 -1 -13 -57 11 -62 28 -240
38 -291 18 -31 -13 -23 -14 95 -14 119 0 132 -2 188 -28 76 -36 60 -47 -44
-32 -44 6 -104 10 -135 8 -75 -4 -197 -44 -273 -89 -82 -49 -91 -73 -13 -34
33 17 100 40 148 51 75 18 106 20 217 15 72 -4 261 -8 420 -10 282 -4 292 -5
364 -30 41 -14 76 -24 78 -22 5 4 -173 90 -209 101 -13 4 -31 15 -41 23 -13
11 -46 18 -105 22 -87 6 -87 6 -47 18 47 14 124 15 178 3 20 -5 37 -5 37 -1 0
12 -77 39 -138 49 -95 15 -87 31 14 31 115 0 164 -15 341 -101 211 -103 349
-213 495 -397 155 -196 249 -437 293 -759 37 -271 87 -460 160 -603 40 -82
139 -223 179 -257 28 -24 32 -38 16 -48 -13 -8 -107 25 -122 42 -8 10 -7 12 6
7 25 -10 19 4 -8 18 -14 7 -74 56 -134 110 -89 79 -115 108 -144 165 -41 78
-103 241 -112 293 -3 19 7 -1 23 -45 36 -101 49 -135 55 -135 2 0 3 8 1 18 -2
9 -4 34 -4 55 -1 26 -5 36 -12 31 -8 -5 -9 -2 -2 11 17 29 -16 165 -39 165 -6
0 -7 7 -4 16 3 8 2 13 -3 9 -5 -3 -12 1 -14 7 -4 10 -8 10 -15 -1 -7 -9 -15
15 -26 80 -25 137 -82 332 -123 422 -20 42 -38 75 -40 73 -3 -2 5 -22 16 -43
52 -103 86 -250 95 -419 5 -86 4 -95 -7 -72 -10 22 -16 25 -25 16 -18 -18 -52
-20 -52 -4 0 17 -39 65 -40 49 0 -20 -20 -15 -20 5 0 14 -2 15 -10 2 -8 -12
-10 -12 -10 5 0 38 -81 300 -114 371 -18 38 -46 86 -62 105 -16 18 -56 70 -89
114 -62 85 -305 333 -376 384 -23 17 -54 31 -68 31 l-26 -1 23 -12 c32 -18
270 -220 271 -231 1 -4 21 -30 45 -57 49 -54 139 -200 186 -300 50 -109 103
-334 62 -268 -8 12 -9 18 -2 14 13 -8 2 45 -13 60 -6 7 -8 2 -4 -15 l6 -25
-17 22 c-9 12 -51 77 -92 144 -79 129 -102 140 -49 23 36 -80 63 -151 53 -141
-10 9 -44 -22 -44 -40 0 -20 -21 -16 -62 12 -20 14 -44 25 -52 25 -21 0 -90
84 -122 149 -16 31 -43 79 -61 106 -17 28 -50 100 -71 161 -85 238 -157 345
-274 409 -60 33 -62 22 -12 -78 20 -39 43 -88 50 -107 l13 -35 0 30 c1 17 -12
58 -29 93 -33 70 -27 71 48 7 52 -45 113 -165 143 -285 26 -102 28 -255 6
-370 l-15 -75 -2 85 c-1 72 -15 187 -19 155 -7 -58 -31 -186 -40 -207 -16 -43
-31 -33 -31 20 0 62 -10 98 -21 80 -5 -7 -9 -23 -9 -35 0 -36 -41 -135 -66
-160 -36 -36 -54 -30 -55 20 -1 23 -4 53 -9 67 l-7 25 -12 -30 c-20 -50 -93
-156 -105 -152 -6 2 -35 -9 -65 -24 -52 -28 -88 -30 -117 -8 -6 5 -14 6 -17 2
-4 -3 -7 -1 -7 5 0 6 -7 9 -16 5 -8 -3 -13 -12 -10 -19 3 -8 3 -14 1 -14 -10
0 -46 82 -64 147 -11 37 -20 62 -20 55 -1 -7 -6 -10 -13 -6 -8 5 -9 54 -5 178
5 140 11 187 31 256 31 105 59 171 99 228 23 35 25 42 10 34 -32 -18 -71 -83
-102 -172 -31 -91 -44 -165 -61 -355 -17 -190 -21 -210 -31 -194 -7 11 -9 10
-10 -6 -4 -52 -7 -67 -17 -77 -7 -7 -15 -8 -19 -4 -5 4 -8 98 -7 209 0 193 2
207 31 317 18 63 35 124 38 135 4 12 -1 8 -14 -10 -26 -37 -64 -156 -82 -253
-10 -59 -17 -76 -33 -82 -36 -14 -106 -86 -438 -453 -62 -70 -74 -59 -52 53 8
46 27 119 40 163 14 43 23 81 21 84 -6 5 -47 -81 -88 -185 -28 -70 -59 -121
-59 -97 0 29 53 201 85 274 19 46 34 89 32 95 -5 15 -111 -203 -136 -279 -29
-92 -51 -237 -52 -359 -1 -61 -4 -97 -6 -81 -6 53 -13 75 -23 75 -6 0 -10 -6
-10 -12 0 -18 -49 -76 -50 -60 0 7 -4 11 -9 8 -5 -3 -15 1 -22 10 -22 27 -37
-6 -28 -63 12 -76 8 -94 -6 -28 -29 128 -37 241 -25 355 12 121 45 265 83 359
14 33 23 61 20 61 -2 0 -22 -37 -43 -82 -67 -140 -109 -335 -126 -578 l-7
-105 -17 53 c-17 56 -29 56 -45 3 l-7 -24 -12 22 c-11 20 -13 18 -30 -36 -58
-186 -172 -407 -239 -464 -72 -61 -187 -112 -187 -82 0 5 23 26 50 46 67 50
170 182 224 287 78 151 113 299 151 635 26 238 80 402 192 590 87 145 133 204
237 302 267 254 669 405 1029 387 49 -2 95 -8 103 -12z m446 -999 c52 -56 283
-346 291 -367 4 -10 5 -22 1 -27 -3 -5 4 -9 15 -9 12 0 73 -50 151 -122 l131
-123 -3 -112 c-1 -62 -5 -113 -8 -113 -4 0 -5 -14 -2 -31 5 -33 -21 -252 -45
-369 -36 -185 -89 -258 -237 -330 -226 -110 -683 -161 -1143 -130 -228 16
-357 47 -453 111 -68 46 -104 93 -126 166 -22 71 -114 431 -114 445 0 7 37
-24 83 -70 45 -45 85 -82 89 -82 4 0 -2 25 -13 55 -22 62 -23 85 -9 137 19 66
124 178 146 155 4 -3 -6 -21 -21 -39 -70 -83 -83 -175 -38 -280 55 -130 104
-165 217 -156 104 9 161 51 192 142 22 66 15 184 -14 245 -25 50 -22 63 8 36
11 -10 20 -13 20 -7 0 5 -23 31 -50 56 -93 83 -225 114 -348 81 -37 -10 -77
-26 -90 -36 -13 -10 -36 -21 -53 -24 -16 -4 -27 -11 -23 -16 3 -5 9 -7 13 -5
4 3 13 1 21 -4 12 -7 9 -15 -15 -40 -16 -16 -33 -30 -39 -30 -5 0 -18 -7 -28
-16 -17 -16 -17 -18 -1 -30 16 -11 15 -13 -10 -25 -15 -7 -30 -9 -33 -6 -4 3
-10 37 -14 74 -9 80 -1 103 81 233 30 47 79 132 109 188 65 122 177 275 287
392 l80 84 7 -94 c5 -83 27 -211 51 -304 7 -27 6 -29 -16 -22 -93 27 -237 10
-329 -39 -52 -28 -37 -32 35 -9 59 18 214 20 274 4 44 -12 65 -7 55 13 -15 31
25 -30 56 -84 37 -65 109 -149 127 -149 8 0 7 9 -2 28 -16 32 -45 191 -44 242
0 32 2 30 23 -28 30 -79 109 -212 153 -254 18 -17 34 -30 36 -27 93 109 193
246 227 308 24 45 45 79 47 77 2 -2 -12 -52 -33 -110 -20 -59 -34 -110 -30
-113 17 -17 118 115 179 235 46 88 89 212 102 290 4 24 14 46 22 49 12 5 12 7
1 14 -10 7 -10 9 2 9 9 0 32 -17 52 -37z m-1635 -740 c-3 -10 -5 -4 -5 12 0
17 2 24 5 18 2 -7 2 -21 0 -30z m2293 -38 c0 -19 5 -35 10 -35 6 0 10 14 10
31 0 26 2 29 14 20 8 -7 13 -23 10 -37 -2 -13 0 -24 6 -24 6 0 8 -6 4 -12 -4
-7 2 -4 14 6 26 23 26 32 6 -140 -17 -153 -28 -198 -55 -228 -11 -11 -19 -30
-19 -43 0 -15 -6 -23 -16 -23 -12 0 -15 6 -10 23 4 12 8 60 11 107 l5 85 -15
-60 c-19 -81 -63 -205 -64 -180 0 11 6 47 14 80 19 79 55 346 55 413 0 28 5
52 10 52 6 0 10 -16 10 -35z m-2254 -195 c14 -74 32 -158 39 -186 12 -48 10
-104 -4 -104 -4 0 -13 16 -20 35 -20 55 -31 31 -15 -33 30 -121 29 -119 10
-102 -10 8 -22 24 -27 37 -10 28 -33 31 -24 4 3 -11 2 -22 -4 -25 -5 -3 -7
-16 -5 -29 4 -20 4 -20 -6 2 -6 14 -37 50 -69 82 -33 32 -70 81 -86 113 -24
49 -27 65 -22 118 5 54 10 66 40 94 l35 33 6 -31 c4 -18 1 -37 -6 -45 -7 -9
-8 -17 -2 -21 9 -7 23 -44 43 -122 16 -60 13 8 -3 87 -12 59 -20 130 -17 156
1 4 10 3 22 -3 11 -7 18 -17 16 -24 -3 -7 3 -17 13 -24 24 -15 50 -68 50 -104
0 -15 4 -28 9 -28 15 0 20 39 10 73 -5 18 -11 77 -14 132 -2 55 -2 89 1 75 3
-14 16 -86 30 -160z m2418 150 c77 -92 51 -272 -58 -395 -40 -45 -47 -32 -20
35 14 34 14 45 4 65 -15 28 -4 139 16 159 10 11 11 20 1 45 -10 27 -9 34 3 41
8 6 22 7 30 4 8 -3 5 5 -8 19 -25 26 -29 57 -8 57 8 0 26 -14 40 -30z m-1936
-12 c19 -19 15 -66 -7 -78 -44 -23 -91 -1 -91 44 0 31 18 46 55 46 17 0 36 -5
43 -12z m-651 -40 c-3 -7 -5 -2 -5 12 0 14 2 19 5 13 2 -7 2 -19 0 -25z m2822
-123 c25 -43 87 -268 103 -374 18 -123 -16 -322 -79 -461 -33 -72 -122 -224
-128 -218 -2 2 6 37 18 78 17 64 20 101 19 248 -1 107 2 172 9 172 5 0 7 11 3
24 -5 21 -3 24 14 19 15 -3 21 2 26 22 9 34 10 237 2 290 -4 29 -1 23 12 -20
l17 -60 3 51 c3 45 -24 199 -43 247 -12 29 2 19 24 -18z m-2219 -105 c7 -18
22 -33 36 -36 32 -8 79 12 99 42 12 18 22 23 46 20 45 -5 52 -17 39 -66 -17
-63 -46 -96 -95 -110 -24 -7 -48 -9 -53 -6 -5 3 -16 -1 -23 -10 -12 -14 -14
-14 -21 5 -4 12 -18 25 -31 30 -38 15 -87 61 -87 83 0 17 3 18 14 9 11 -10 19
-7 36 11 11 13 19 31 16 40 -7 30 11 20 24 -12z m1857 -197 c-3 -10 -5 -2 -5
17 0 19 2 27 5 18 2 -10 2 -26 0 -35z m-1137 -503 c-11 -22 -22 -40 -23 -40
-2 0 -21 18 -42 40 l-39 40 62 0 62 0 -20 -40z m276 -53 c-8 -44 -60 -121 -99
-149 -28 -19 -27 -16 11 26 45 49 81 116 82 150 0 11 3 17 6 13 4 -3 4 -21 0
-40z m-319 -31 c-7 -22 -26 -15 -52 20 l-28 37 41 -23 c24 -13 41 -28 39 -34z
m103 36 c0 -5 -9 -17 -20 -27 -23 -21 -26 -14 -8 15 13 20 28 27 28 12z m-365
-82 c10 -19 48 -66 84 -104 40 -43 54 -61 34 -48 -71 50 -154 166 -152 214 1
22 1 22 8 -2 5 -14 16 -41 26 -60z m205 -12 c0 -5 -9 -7 -19 -3 -11 3 -22 1
-26 -5 -3 -5 -16 -10 -28 -10 -18 0 -16 3 13 24 34 23 60 20 60 -6z m112 -11
c-12 -13 -22 -27 -22 -31 0 -4 17 -27 38 -51 l37 -43 35 25 c29 22 26 17 -15
-26 -27 -29 -67 -66 -87 -83 -36 -28 -38 -28 -38 -9 0 12 6 21 13 21 7 0 28
13 47 29 l35 30 -47 47 -46 47 -50 -37 c-48 -34 -73 -45 -71 -28 1 13 103 87
121 87 9 0 26 13 38 28 12 15 25 26 28 22 3 -3 -4 -15 -16 -28z m108 12 c0 -5
-7 -9 -15 -9 -15 0 -20 12 -9 23 8 8 24 -1 24 -14z m695 -166 c-4 -43 -9 -80
-11 -82 -8 -9 -35 24 -42 53 -6 21 -15 32 -30 34 -15 2 -22 9 -20 20 6 33 23
42 96 51 11 1 12 -15 7 -76z m-855 -14 c0 -11 -4 -17 -10 -14 -5 3 -10 13 -10
21 0 8 5 14 10 14 6 0 10 -9 10 -21z m-20 -94 c0 -18 -5 -25 -20 -25 -22 0
-26 20 -8 38 19 19 28 14 28 -13z m828 -17 c19 -19 14 -30 -8 -18 -11 6 -20
15 -20 20 0 14 14 12 28 -2z m-816 -143 c-5 -28 -12 -31 -30 -13 -17 17 -15
31 3 25 8 -4 17 -2 20 3 10 16 12 11 7 -15z m763 -147 c19 -17 35 -35 35 -40
0 -24 -84 12 -95 41 -5 11 9 30 23 31 1 0 18 -14 37 -32z m113 -60 c7 -7 12
-16 12 -20 0 -11 -32 -10 -50 2 -13 9 -13 11 0 20 19 13 24 12 38 -2z m-542
-453 c-15 -23 -61 -44 -72 -32 -3 3 7 12 23 21 16 8 35 22 43 31 20 24 25 9 6
-20z"/>
<path d="M2438 4433 c6 -2 18 -2 25 0 6 3 1 5 -13 5 -14 0 -19 -2 -12 -5z"/>
<path d="M2500 4426 c0 -2 11 -6 25 -8 14 -3 25 -1 25 3 0 5 -11 9 -25 9 -14
0 -25 -2 -25 -4z"/>
<path d="M2871 3676 c-1 -29 2 -37 10 -29 8 8 8 19 1 40 -10 28 -10 28 -11
-11z"/>
<path d="M4050 3233 c0 -40 30 -185 40 -196 12 -13 20 55 11 93 l-9 35 -2 -30
c-1 -27 -2 -28 -10 -9 -4 12 -11 45 -15 75 -8 62 -15 77 -15 32z"/>
<path d="M4108 3038 c-3 -15 -1 -25 3 -22 5 3 9 0 9 -7 0 -7 2 -10 5 -8 2 3 1
18 -4 35 l-8 29 -5 -27z"/>
<path d="M3108 3220 c-67 -11 -113 -32 -128 -60 -13 -25 -7 -25 66 0 103 36
231 37 319 3 40 -15 1 19 -43 38 -55 24 -137 31 -214 19z"/>
<path d="M3081 2884 c-17 -14 -40 -40 -52 -57 -20 -31 -20 -31 9 3 16 19 43
45 58 57 16 13 26 23 22 23 -3 0 -20 -12 -37 -26z"/>
<path d="M3237 2899 c-57 -13 -89 -33 -134 -82 -53 -57 -66 -107 -61 -222 3
-77 8 -98 33 -142 17 -29 36 -53 43 -53 7 0 24 -12 38 -27 23 -24 33 -27 112
-31 106 -4 133 9 189 94 35 52 38 64 42 137 4 98 -8 140 -55 195 -20 24 -34
46 -31 49 11 11 72 -33 104 -75 46 -61 59 -111 46 -175 -6 -29 -10 -53 -9 -54
1 -2 26 32 55 73 48 70 51 77 37 93 -21 23 -20 29 6 50 l21 18 -44 25 c-24 13
-43 30 -41 36 1 7 -18 22 -43 34 -42 21 -58 38 -35 38 16 0 11 17 -7 24 -9 4
-20 1 -24 -6 -6 -9 -13 -9 -34 0 -30 14 -152 14 -208 1z m44 -125 c14 -38 -12
-64 -62 -64 -29 0 -39 5 -44 20 -19 60 83 102 106 44z m-130 -227 c14 -32 60
-57 102 -57 34 0 73 26 93 63 9 16 13 16 53 -5 38 -21 42 -26 36 -53 -10 -57
-69 -111 -125 -115 -64 -5 -108 1 -123 18 -53 54 -77 86 -77 101 0 9 -5 22
-12 29 -8 8 -8 15 2 27 18 22 39 19 51 -8z"/>
<path d="M2121 2846 c2 -2 15 -9 29 -15 24 -11 24 -11 6 3 -16 13 -49 24 -35
12z"/>
<path d="M2589 2153 c-29 -3 -53 -11 -64 -23 -15 -17 -15 -21 0 -49 19 -37 24
-39 18 -8 -7 38 23 54 118 62 81 7 88 6 109 -15 l21 -21 -3 28 c-3 28 -3 28
-78 29 -41 0 -96 -1 -121 -3z"/>
<path d="M991 2384 c0 -5 -22 -26 -47 -45 -38 -28 -74 -78 -74 -104 0 -3 18
-5 39 -5 22 0 41 -5 43 -12 6 -17 50 139 45 159 -3 10 -5 13 -6 7z"/>
<path d="M4500 2285 c-25 -15 -26 -14 24 -29 19 -6 51 18 42 32 -9 15 -34 14
-66 -3z"/>
<path d="M4313 2025 c-1 -170 -20 -250 -93 -403 -28 -58 -49 -107 -47 -109 5
-4 69 92 96 145 52 102 73 289 52 452 -6 41 -8 20 -8 -85z"/>
<path d="M1125 1886 c-5 -36 -3 -93 5 -150 10 -75 21 -110 65 -201 52 -106
137 -226 169 -238 25 -10 19 3 -40 83 -107 146 -154 267 -178 463 l-13 102 -8
-59z"/>
<path d="M972 1795 c0 -11 3 -29 8 -40 11 -25 11 1 0 35 -6 20 -8 21 -8 5z"/>
<path d="M996 1710 c3 -14 11 -32 16 -40 6 -10 7 -7 2 10 -3 14 -11 32 -16 40
-6 10 -7 7 -2 -10z"/>
<path d="M1795 940 c3 -5 16 -10 28 -10 18 0 19 2 7 10 -20 13 -43 13 -35 0z"/>
<path d="M3277 873 c-21 -30 -49 -58 -62 -63 -22 -8 -27 -22 -51 -137 -6 -32
7 -29 47 12 23 23 57 42 99 55 66 21 100 44 100 67 0 17 -6 17 -49 -6 -20 -11
-56 -29 -81 -40 -25 -11 -53 -26 -62 -33 -20 -14 -33 -6 -25 17 4 8 18 15 34
15 39 0 163 76 163 100 0 27 18 25 31 -4 9 -20 15 -22 38 -15 14 4 44 11 66
16 25 5 41 14 43 25 3 13 -7 17 -55 20 -32 2 -76 1 -98 -2 -36 -6 -82 5 -95
21 -3 4 -22 -18 -43 -48z"/>
<path d="M1870 905 c-14 -8 -37 -14 -52 -15 -16 0 -28 -4 -28 -8 0 -19 61 -63
133 -96 42 -19 77 -33 77 -31 0 2 -20 18 -44 34 -68 47 -65 65 5 29 33 -17 63
-28 66 -24 8 8 -107 126 -122 126 -5 -1 -21 -7 -35 -15z"/>
<path d="M2623 570 c0 -30 2 -43 4 -27 2 15 2 39 0 55 -2 15 -4 2 -4 -28z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="27"
height="27"
viewBox="0 0 45 47"
fill="none"
>
<path
d="M24.273 7.7653L19.6087 11.3716L15.3793 5.98863L11 7L30 35.5L34.1633 30.0428L32.7808 28.1213C32.1142 27.1213 31.6711 25.3464 33.2711 24.1464C34.8711 22.9464 36.6061 23.6397 37.4394 24.473L40.7354 28.7305L30 47L0 0H18.0882L24.273 7.7653Z"
fill="rgb(3, 148, 130)"
></path>
<path
d="M43.645 9.14625C42.6195 7.84489 40.7332 7.62124 39.4319 8.64673L30.4992 15.6858L28.646 13.3226C27.6235 12.0188 25.7378 11.7907 24.434 12.8132C23.1302 13.8356 22.9022 15.7213 23.9246 17.0251L29.4784 24.1071L29.4954 24.0939L29.506 24.1074L43.1455 13.3594C44.4468 12.3339 44.6705 10.4476 43.645 9.14625Z"
fill="rgb(3, 148, 130)"
></path>
</svg>

After

Width:  |  Height:  |  Size: 850 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 B

View File

@ -0,0 +1 @@
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 256 256" height="44px" width="44px" xmlns="http://www.w3.org/2000/svg"><path d="M48.07,104H207.93a16,16,0,0,0,15.72-19.38C216.22,49.5,176,24,128,24S39.78,49.5,32.35,84.62A16,16,0,0,0,48.07,104ZM128,40c39.82,0,74.21,20.61,79.93,48H48.07L48,87.93C53.79,60.61,88.18,40,128,40ZM229.26,152.48l-41.13,15L151,152.57a8,8,0,0,0-5.94,0l-37,14.81L71,152.57a8,8,0,0,0-5.7-.09l-44,16a8,8,0,0,0,5.47,15L40,178.69V184a40,40,0,0,0,40,40h96a40,40,0,0,0,40-40v-9.67l18.73-6.81a8,8,0,1,0-5.47-15ZM200,184a24,24,0,0,1-24,24H80a24,24,0,0,1-24-24V172.88l11.87-4.32L105,183.43a8,8,0,0,0,5.94,0l37-14.81,37,14.81a8,8,0,0,0,5.7.09l9.27-3.37ZM16,128a8,8,0,0,1,8-8H232a8,8,0,0,1,0,16H24A8,8,0,0,1,16,128Z"></path></svg>

After

Width:  |  Height:  |  Size: 769 B

37
apps/desktop/src/App.tsx Normal file
View File

@ -0,0 +1,37 @@
import { ThemeProvider } from "@/components/theme-provider";
import LayoutMain from "@/pages/Layout";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Boot from "@/pages/Boot";
import { Setting } from "./pages/Setting";
import { useEffect } from "react";
import { Models } from "./pages/Models";
import { Tools } from "./pages/Tools";
import { Datasource } from "./pages/Datasource";
function App() {
const theme = document.querySelector("html")!.getAttribute("theme") as
| "dark"
| "light";
useEffect(() => {
window.ipcRenderer.send("ipc-loaded");
}, []);
return (
<ThemeProvider defaultTheme={theme} storageKey="vite-ui-theme">
<Router>
<Routes>
<Route path="/" element={<LayoutMain />}>
<Route index element={<Boot />} />
<Route path="models" element={<Models />} />
<Route path="datasource" element={<Datasource />} />
<Route path="tools" element={<Tools />} />
<Route path="setting" element={<Setting />} />
</Route>
</Routes>
</Router>
</ThemeProvider>
);
}
export default App;

View File

@ -0,0 +1,73 @@
import { createContext, useContext, useEffect, useState } from "react";
type Theme = "dark" | "light" | "system";
type ThemeProviderProps = {
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
};
type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
const initialState: ThemeProviderState = {
theme: "dark",
setTheme: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
);
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
root.classList.add(systemTheme);
return;
}
root.classList.add(theme);
}, [theme]);
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider");
return context;
};

View File

@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,72 @@
"use client"
import * as React from "react"
import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
export type CalendarProps = React.ComponentProps<typeof DayPicker>
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-slate-500 rounded-md w-8 font-normal text-[0.8rem] dark:text-slate-400",
row: "flex w-full mt-2",
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-slate-100 [&:has([aria-selected].day-outside)]:bg-slate-100/50 [&:has([aria-selected].day-range-end)]:rounded-r-md dark:[&:has([aria-selected])]:bg-slate-800 dark:[&:has([aria-selected].day-outside)]:bg-slate-800/50",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md"
),
day: cn(
buttonVariants({ variant: "ghost" }),
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
),
day_range_start: "day-range-start",
day_range_end: "day-range-end",
day_selected:
"bg-slate-900 text-slate-50 hover:bg-slate-900 hover:text-slate-50 focus:bg-slate-900 focus:text-slate-50 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50 dark:hover:text-slate-900 dark:focus:bg-slate-50 dark:focus:text-slate-900",
day_today: "bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-slate-50",
day_outside:
"day-outside text-slate-500 opacity-50 aria-selected:bg-slate-100/50 aria-selected:text-slate-500 aria-selected:opacity-30 dark:text-slate-400 dark:aria-selected:bg-slate-800/50 dark:aria-selected:text-slate-400",
day_disabled: "text-slate-500 opacity-50 dark:text-slate-400",
day_range_middle:
"aria-selected:bg-slate-100 aria-selected:text-slate-900 dark:aria-selected:bg-slate-800 dark:aria-selected:text-slate-50",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeftIcon className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRightIcon className="h-4 w-4" />,
}}
{...props}
/>
)
}
Calendar.displayName = "Calendar"
export { Calendar }

View File

@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@ -0,0 +1,155 @@
"use client"
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { MagnifyingGlassIcon } from "@radix-ui/react-icons"
import { Command as CommandPrimitive } from "cmdk"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<MagnifyingGlassIcon className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -0,0 +1,205 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View File

@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@ -0,0 +1,240 @@
"use client"
import * as React from "react"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { cn } from "@/lib/utils"
const MenubarMenu = MenubarPrimitive.Menu
const MenubarGroup = MenubarPrimitive.Group
const MenubarPortal = MenubarPrimitive.Portal
const MenubarSub = MenubarPrimitive.Sub
const MenubarRadioGroup = MenubarPrimitive.RadioGroup
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn(
"flex h-9 items-center space-x-1 rounded-md border border-slate-200 bg-white p-1 shadow-sm dark:border-slate-800 dark:bg-slate-950",
className
)}
{...props}
/>
))
Menubar.displayName = MenubarPrimitive.Root.displayName
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-slate-100 focus:text-slate-900 data-[state=open]:bg-slate-100 data-[state=open]:text-slate-900 dark:focus:bg-slate-800 dark:focus:text-slate-50 dark:data-[state=open]:bg-slate-800 dark:data-[state=open]:text-slate-50",
className
)}
{...props}
/>
))
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-slate-100 focus:text-slate-900 data-[state=open]:bg-slate-100 data-[state=open]:text-slate-900 dark:focus:bg-slate-800 dark:focus:text-slate-50 dark:data-[state=open]:bg-slate-800 dark:data-[state=open]:text-slate-50",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
))
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-200 bg-white p-1 text-slate-950 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50",
className
)}
{...props}
/>
))
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(
(
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
ref
) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border border-slate-200 bg-white p-1 text-slate-950 shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50",
className
)}
{...props}
/>
</MenubarPrimitive.Portal>
)
)
MenubarContent.displayName = MenubarPrimitive.Content.displayName
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-800 dark:focus:text-slate-50",
inset && "pl-8",
className
)}
{...props}
/>
))
MenubarItem.displayName = MenubarPrimitive.Item.displayName
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-800 dark:focus:text-slate-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
))
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-800 dark:focus:text-slate-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
))
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-slate-100 dark:bg-slate-800", className)}
{...props}
/>
))
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
const MenubarShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-slate-500 dark:text-slate-400",
className
)}
{...props}
/>
)
}
MenubarShortcut.displayname = "MenubarShortcut"
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
}

View File

@ -0,0 +1,45 @@
"use client"
import { DragHandleDots2Icon } from "@radix-ui/react-icons"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
const ResizablePanel = ResizablePrimitive.Panel
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
"relative flex w-px items-center justify-center bg-slate-200 after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90 dark:bg-slate-800 dark:focus-visible:ring-slate-300",
className
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border border-slate-200 bg-slate-200 dark:border-slate-800 dark:bg-slate-800">
<DragHandleDots2Icon className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

View File

@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@ -0,0 +1,120 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@ -0,0 +1,129 @@
"use client"
import * as React from "react"
import { Cross2Icon } from "@radix-ui/react-icons"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border border-slate-200 p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full dark:border-slate-800",
{
variants: {
variant: {
default: "border bg-white text-slate-950 dark:bg-slate-950 dark:text-slate-50",
destructive:
"destructive group border-red-500 bg-red-500 text-slate-50 dark:border-red-900 dark:bg-red-900 dark:text-slate-50",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border border-slate-200 bg-transparent px-3 text-sm font-medium transition-colors hover:bg-slate-100 focus:outline-none focus:ring-1 focus:ring-slate-950 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-slate-100/40 group-[.destructive]:hover:border-red-500/30 group-[.destructive]:hover:bg-red-500 group-[.destructive]:hover:text-slate-50 group-[.destructive]:focus:ring-red-500 dark:border-slate-800 dark:hover:bg-slate-800 dark:focus:ring-slate-300 dark:group-[.destructive]:border-slate-800/40 dark:group-[.destructive]:hover:border-red-900/30 dark:group-[.destructive]:hover:bg-red-900 dark:group-[.destructive]:hover:text-slate-50 dark:group-[.destructive]:focus:ring-red-900",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-1 top-1 rounded-md p-1 text-slate-950/50 opacity-0 transition-opacity hover:text-slate-950 focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 dark:text-slate-50/50 dark:hover:text-slate-50",
className
)}
toast-close=""
{...props}
>
<Cross2Icon className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

View File

@ -0,0 +1,35 @@
"use client"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

View File

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@ -0,0 +1,194 @@
"use client"
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }

View File

@ -0,0 +1,30 @@
// database.ts
import Dexie from "dexie";
import { IFriend } from "./models/User";
class Database extends Dexie {
public friends: Dexie.Table<IFriend, number>; // `number` 是主键的类型
constructor() {
super("Database");
this.version(1).stores({
friends: "++id, name, age",
});
this.friends = this.table("friends");
}
async addFriend(friend: IFriend): Promise<number> {
return await this.friends.add(friend);
}
async getAllFriends(): Promise<IFriend[]> {
return await this.friends.toArray();
}
async getFriendsYoungerThan(ageLimit: number): Promise<IFriend[]> {
return await this.friends.where("age").below(ageLimit).toArray();
}
}
const db = new Database();
export default db;

View File

@ -0,0 +1,5 @@
export interface IFriend {
id?: number; // 可选,自增主键
name: string;
age: number;
}

View File

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

11
apps/desktop/src/main.tsx Normal file
View File

@ -0,0 +1,11 @@
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "@/style/global.css";
window.ipcRenderer.on("main-process-message", (_event, message) => {
const { platform, theme } = message
document.querySelector('html')?.setAttribute('platform', platform)
document.querySelector('html')?.setAttribute('theme', theme)
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
});

View File

@ -0,0 +1,83 @@
import { Button } from "@/components/ui/button";
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { IoCheckmarkCircleSharp } from "react-icons/io5";
const Boot = () => {
const [flaskWaiting, setFlaskWaiting] = useState(false);
const [flaskRunning, setflaskRunning] = useState(false);
const [flaskInfo, setFlaskInfo] = useState("");
const handleBootPythonServer = () => {
if (!flaskRunning) setFlaskWaiting(true);
window.ipcRenderer.send("python-service", { running: !flaskRunning });
};
useEffect(() => {
window.ipcRenderer.on("flask-service:response", (event, data) => {
console.log(data);
if (data.running) {
setflaskRunning(true);
setFlaskWaiting(false);
}
if (data.stdout || data.stderr) {
setflaskRunning(true);
setFlaskWaiting(false);
setFlaskInfo(data.stderr);
}
if (data.exited) {
setFlaskInfo("");
setflaskRunning(false);
}
});
}, []);
return (
<motion.div
initial={{ y: 10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 0, opacity: 0 }}
transition={{ duration: 0.25 }}
className="h-full"
>
<div className="p-4 h-full flex flex-col">
<div
className="w-full h-[250px] rounded-md"
style={{
backgroundImage: `url('/banner.png')`,
backgroundSize: "cover",
}}
></div>
<main className="flex-1 flex flex-col">
<div className="mt-4 flex-1">
<pre className="text-xs">{flaskInfo}</pre>
</div>
<footer className="flex justify-between">
<div className="left"></div>
<Button
disabled={flaskWaiting}
className={`bg-teal-500 hover:bg-teal-400 ${
flaskWaiting ? "bg-teal-700" : ""
}`}
size={"lg"}
onClick={handleBootPythonServer}
>
<IoCheckmarkCircleSharp className={`mr-2 h-4 w-4}`} />
{flaskRunning ? "运行中" : "启动"} python
</Button>
</footer>
</main>
</div>
</motion.div>
);
};
export default Boot;

View File

@ -0,0 +1,88 @@
"use client";
import * as React from "react";
import {
CalendarIcon,
EnvelopeClosedIcon,
FaceIcon,
GearIcon,
PersonIcon,
RocketIcon,
} from "@radix-ui/react-icons";
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command";
import { DialogTitle } from "@radix-ui/react-dialog";
export function CommandDialogDemo() {
const [open, setOpen] = React.useState(false);
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "j" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((open) => !open);
}
};
document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down);
}, []);
return (
<>
<p className="text-sm text-muted-foreground">
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
<span className="text-xs"></span>J
</kbd>
</p>
<CommandDialog open={open} onOpenChange={setOpen}>
<DialogTitle className="hidden"></DialogTitle>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem>
<CalendarIcon className="mr-2 h-4 w-4" />
<span>Calendar</span>
</CommandItem>
<CommandItem>
<FaceIcon className="mr-2 h-4 w-4" />
<span>Search Emoji</span>
</CommandItem>
<CommandItem>
<RocketIcon className="mr-2 h-4 w-4" />
<span>Launch</span>
</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Settings">
<CommandItem>
<PersonIcon className="mr-2 h-4 w-4" />
<span>Profile</span>
<CommandShortcut>P</CommandShortcut>
</CommandItem>
<CommandItem>
<EnvelopeClosedIcon className="mr-2 h-4 w-4" />
<span>Mail</span>
<CommandShortcut>B</CommandShortcut>
</CommandItem>
<CommandItem>
<GearIcon className="mr-2 h-4 w-4" />
<span>Settings</span>
<CommandShortcut>S</CommandShortcut>
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
</>
);
}

View File

@ -0,0 +1,46 @@
import { Button } from "@/components/ui/button";
import React from "react";
import { useDropzone } from "react-dropzone";
interface FileUploadProps {
onFilesSelected?: (files: File[]) => void;
}
const FileUpload: React.FC<FileUploadProps> = ({ onFilesSelected }) => {
const onDrop = (acceptedFiles: File[]) => {
console.log("Accepted files:", acceptedFiles);
// 调用父组件传入的回调函数处理文件
if (onFilesSelected) {
onFilesSelected(acceptedFiles);
}
};
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
multiple: true,
accept: {
"file/dicom": [".dcm"],
"file/model": [".stl"],
},
noClick: false, // 启用点击选择文件
noKeyboard: true, // 禁用键盘选择(可选)
directory: true, // 启用文件夹上传支持
});
return (
<div
{...getRootProps()}
className="border-dashed border-2 p-4 py-8 rounded-md text-center cursor-pointer"
>
<input {...getInputProps()} webkitdirectory="" directory />
{isDragActive ? (
<p>/</p>
) : (
<p>/</p>
)}
<Button className="mt-2"></Button>
</div>
);
};
export default FileUpload;

View File

@ -0,0 +1,316 @@
"use client";
import * as React from "react";
import {
CaretSortIcon,
ChevronDownIcon,
DotsHorizontalIcon,
} from "@radix-ui/react-icons";
import {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
const data: Payment[] = [
{
id: "m5gr84i9",
amount: 316,
status: "success",
email: "ken99@yahoo.com",
},
{
id: "3u1reuv4",
amount: 242,
status: "success",
email: "Abe45@gmail.com",
},
{
id: "derv1ws0",
amount: 837,
status: "processing",
email: "Monserrat44@gmail.com",
},
{
id: "5kma53ae",
amount: 874,
status: "success",
email: "Silas22@gmail.com",
},
{
id: "bhqecj4p",
amount: 721,
status: "failed",
email: "carmella@hotmail.com",
},
];
export type Payment = {
id: string;
amount: number;
status: "pending" | "processing" | "success" | "failed";
email: string;
};
export const columns: ColumnDef<Payment>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => (
<div className="capitalize">{row.getValue("status")}</div>
),
},
{
accessorKey: "email",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Email
<CaretSortIcon className="ml-2 h-4 w-4" />
</Button>
);
},
cell: ({ row }) => <div className="lowercase">{row.getValue("email")}</div>,
},
{
accessorKey: "amount",
header: () => <div className="text-right">Amount</div>,
cell: ({ row }) => {
const amount = parseFloat(row.getValue("amount"));
// Format the amount as a dollar amount
const formatted = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
return <div className="text-right font-medium">{formatted}</div>;
},
},
{
id: "actions",
enableHiding: false,
cell: ({ row }) => {
const payment = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<DotsHorizontalIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(payment.id)}
>
Copy payment ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>View customer</DropdownMenuItem>
<DropdownMenuItem>View payment details</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
export function SeriesTable() {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
});
return (
<div className="w-full">
<div className="flex items-center py-4">
<Input
placeholder="Filter emails..."
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("email")?.setFilterValue(event.target.value)
}
className="max-w-sm"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto">
Columns <ChevronDownIcon className="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,9 @@
import FileUpload from "./FileUpload";
export const Datasource = () => {
return (
<div className="p-4">
<FileUpload />
</div>
);
};

View File

@ -0,0 +1,122 @@
import {
DockviewApi,
DockviewReact,
DockviewReadyEvent,
IDockviewPanelProps,
} from "dockview";
import { useEffect, useState } from "react";
import "dockview/dist/styles/dockview.css";
import "./theme.reset.css";
const Default = (props: IDockviewPanelProps) => {
return (
<div style={{ height: "100%" }}>
<div>{props.api.title}</div>
</div>
);
};
const components = {
default: Default,
};
const Dockview = (props: { theme?: string }) => {
const [disablePanelDrag, setDisablePanelDrag] = useState<boolean>(false);
const [disableGroupDrag, setDisableGroupDrag] = useState<boolean>(false);
const [disableOverlay, setDisableOverlay] = useState<boolean>(false);
const [api, setApi] = useState<DockviewApi>();
useEffect(() => {
if (!api) return;
const disposables = [
api.onWillDragPanel((e) => {
if (!disablePanelDrag) {
return;
}
e.nativeEvent.preventDefault();
}),
api.onWillDragGroup((e) => {
if (!disableGroupDrag) {
return;
}
e.nativeEvent.preventDefault();
}),
api.onWillShowOverlay((e) => {
console.log(e);
if (!disableOverlay) {
return;
}
e.preventDefault();
}),
api.onWillDrop((e) => {
//
}),
api.onDidDrop((e) => {
//
}),
];
return () => {
disposables.forEach((disposable) => {
disposable.dispose();
});
};
}, [api, disablePanelDrag, disableGroupDrag, disableOverlay]);
const onReady = (event: DockviewReadyEvent) => {
setApi(event.api);
event.api.addPanel({
id: "panel_1",
component: "default",
});
event.api.addPanel({
id: "panel_2",
component: "default",
position: {
direction: "right",
referencePanel: "panel_1",
},
});
event.api.addPanel({
id: "panel_3",
component: "default",
position: {
direction: "below",
referencePanel: "panel_1",
},
});
event.api.addPanel({
id: "panel_4",
component: "default",
});
event.api.addPanel({
id: "panel_5",
component: "default",
});
};
return (
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
<div style={{ flexGrow: 1 }}>
<DockviewReact
className={`${props.theme || "dockview-theme-light"}`}
onReady={onReady}
components={components}
/>
</div>
</div>
);
};
export default Dockview;

View File

@ -0,0 +1,28 @@
.dockview-theme-light {
--dv-background-color: black;
--dv-paneview-active-outline-color: dodgerblue;
--dv-tabs-and-actions-container-font-size: 13px;
--dv-tabs-and-actions-container-height: 35px;
--dv-drag-over-background-color: hsl(var(--accent));
--dv-drag-over-border-color: white;
--dv-tabs-container-scrollbar-color: #888;
--dv-icon-hover-background-color: rgba(90, 93, 94, 0.31);
--dv-floating-box-shadow: 8px 8px 8px 0px rgba(83, 89, 93, 0.5);
--dv-group-view-background-color: white;
--dv-tabs-and-actions-container-background-color: #f3f3f3;
--dv-activegroup-visiblepanel-tab-background-color: white;
--dv-activegroup-hiddenpanel-tab-background-color: #ececec;
--dv-inactivegroup-visiblepanel-tab-background-color: white;
--dv-inactivegroup-hiddenpanel-tab-background-color: #ececec;
--dv-tab-divider-color: white;
--dv-activegroup-visiblepanel-tab-color: rgb(51, 51, 51);
--dv-activegroup-hiddenpanel-tab-color: rgba(51, 51, 51, 0.7);
--dv-inactivegroup-visiblepanel-tab-color: rgba(51, 51, 51, 0.7);
--dv-inactivegroup-hiddenpanel-tab-color: rgba(51, 51, 51, 0.35);
--dv-separator-border: hsl(var(--border));
--dv-paneview-header-border-color: rgb(51, 51, 51);
}
.tabs-container .tab{
border-radius: 5px 5px 0 0;
}

View File

@ -0,0 +1,150 @@
import React from "react";
import {
Actions,
BorderNode,
DockLocation,
IJsonModel,
ITabRenderValues,
ITabSetRenderValues,
Layout,
Model,
TabNode,
TabSetNode,
} from "flexlayout-react";
import "flexlayout-react/style/light.css";
import "./Layout.css";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { VscAdd } from "react-icons/vsc";
const json: IJsonModel = {
global: {
tabEnableFloat: false,
tabSetMinWidth: 150,
tabSetMinHeight: 200,
/**
*
*/
tabSetTabStripHeight: 32, //tab栏高度
/**
*
*/
enableEdgeDock: false,
borderMinSize: 100,
splitterSize: 2,
tabSetClassNameTabStrip: "custom-tab-strip",
},
borders: [],
layout: {
type: "row",
weight: 100,
children: [
{
type: "tabset",
weight: 50,
children: [
{
type: "tab",
id: "tab1",
name: "测试22222222222",
component: "tabComponent",
config: { icon: "📘" },
},
],
},
{
type: "tabset",
weight: 50,
children: [
{
type: "tab",
id: "tab2",
name: "Two",
component: "tabComponent",
config: { icon: "📘" },
},
],
},
],
},
};
const FlexLayoutComponent: React.FC = () => {
const model = Model.fromJson(json);
const factory = (node: TabNode) => {
const component = node.getComponent();
if (component === "tabComponent") {
return <div>{node.getName()}11</div>;
}
};
const onRenderTab = (node: TabNode, renderValues: ITabRenderValues) => {
renderValues.content = (
<div className="text-xs">
<span>{node.getConfig().icon}</span>
{renderValues.content}
</div>
);
};
const addTab = (node: TabSetNode | BorderNode) => {
// 创建一个新的 tab 对象
const newTab = {
type: "tab",
name: "New Tab",
component: "tabComponent",
config: { icon: "" },
};
// 使用 FlexLayout 的 addAction 执行添加 tab 的动作
const addAction = Actions.addNode(
newTab,
node.getId(),
DockLocation.CENTER,
-1
);
node.getModel().doAction(addAction);
};
const onRenderTabSet = (
node: TabSetNode | BorderNode,
renderValues: ITabSetRenderValues
) => {
const createTabButton = (
<TooltipProvider key={node.getId() + "create"}>
<Tooltip>
<TooltipTrigger asChild>
<button
className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-6 w-6"
onClick={() => addTab(node)}
>
<VscAdd />
</button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
renderValues.stickyButtons.push(createTabButton);
};
return (
<Layout
model={model}
realtimeResize
factory={factory}
classNameMapper={(defaultClass) => `${defaultClass} custom-layout-class`}
onRenderTab={onRenderTab}
onRenderTabSet={onRenderTabSet}
/>
);
};
export default FlexLayoutComponent;

View File

@ -0,0 +1,18 @@
/* Layout.css */
.custom-tab-strip {
border-bottom-color: hsl(var(--border));
padding-left: 0;
.flexlayout__tabset_tabbar_inner_tab_container {
border-top: 0;
padding-left: 0;
}
.flexlayout__tab_button--selected {
background: hsl(var(--secondary));
}
.flexlayout__tabset-selected {
background: transparent;
}
}

View File

@ -0,0 +1,42 @@
import { LeftDocker } from "./LeftDocker";
import { Outlet } from "react-router-dom";
const LayoutMain = () => {
const platform =
document.querySelector("html")?.getAttribute("platform") ?? "macos";
// const titleBarStyles =
// platform === "macos" ? "pl-[5rem] pr-[.5rem]" : "pl-[.5rem]";
const titleBarStyles =
platform === "macos" ? "px-[.5rem]" : "pl-[.5rem]";
return (
<div className="h-full">
<div
className={`title-bar drag h-[36px] flex ${
platform === "macos" ? "justify-center" : ""
}`}
>
<div
className={`inline-flex no-drag items-center justify-between ${titleBarStyles}`}
>
<img src="logo.svg" className="h-[20px]" />
<span className="pl-2 text-xs">CVPilot Tool</span>
</div>
</div>
<div className="relative h-[calc(100%-36px)]">
<div className="flex h-full">
<div className="workspace w-[80px] drag">
<LeftDocker />
</div>
<div className="relative w-[calc(100%-80px)]">
<div className="bg-card rounded-l-lg h-full">
<Outlet />
</div>
</div>
</div>
</div>
</div>
);
};
export default LayoutMain;

View File

View File

@ -0,0 +1,55 @@
import { Link, useLocation } from "react-router-dom";
import {
IoCubeOutline,
IoHammerOutline,
IoHandLeftOutline,
IoLayersOutline,
IoListOutline,
IoPlayOutline,
IoSettingsOutline
} from "react-icons/io5";
import './LeftDocker.css'
import { ReactNode } from 'react';
type MenuItem = {
to: string;
name: string;
icon: ReactNode;
};
const menuItems: MenuItem[] = [
{ to: "/", name: "一键启动", icon: <IoPlayOutline size={24} /> },
{ to: "/datasource", name: "数据列表", icon: <IoListOutline size={24} /> },
{ to: "/models", name: "模型管理", icon: <IoCubeOutline size={24} /> },
{ to: "/tools", name: "小工具", icon: <IoHammerOutline size={24} /> },
// { to: "/help", name: "帮助", icon: <IoHandLeftOutline size={24} /> },
{ to: "/setting", name: "设置", icon: <IoSettingsOutline size={24} /> },
];
export const LeftDocker = () => {
const location = useLocation();
const handleClick = (item: MenuItem) => {
console.log(`Clicked on ${item.name}`);
};
return (
<ul className="no-drag flex flex-col items-center pt-2 gap-4">
{menuItems.map((item) => (
<li
key={item.name}
className={`w-full flex flex-col items-center justify-center ${location.pathname === item.to ? "active text-teal-500" : ""
}`}
onClick={() => handleClick(item)}
>
<Link to={item.to} className="w-[60px] h-[60px] flex items-center justify-center flex-col gap-2">
{item.icon}
<span className="text-xs">{item.name}</span>
</Link>
</li>
))}
</ul>
);
};

View File

@ -0,0 +1,126 @@
import {
Menubar,
MenubarCheckboxItem,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSeparator,
MenubarShortcut,
MenubarSub,
MenubarSubContent,
MenubarSubTrigger,
MenubarTrigger,
} from "@/components/ui/menubar";
import { EVENT_PARSE_DICOM } from "../../electron/ipcEvent";
import { useEffect } from "react";
export const MenuBar = () => {
useEffect(() => {
window.ipcRenderer.on(EVENT_PARSE_DICOM + ":RES", (event, data) => {
console.log(data);
if (data.error) return;
});
return () => {
window.ipcRenderer.off(EVENT_PARSE_DICOM + ":RES", () => { });
};
}, []);
const handleImportDicom = () => {
window.ipcRenderer.send(EVENT_PARSE_DICOM);
};
return (
<Menubar
style={{ background: "transparent", border: 0, boxShadow: "none" }}
>
<MenubarMenu>
<MenubarTrigger></MenubarTrigger>
<MenubarContent>
<MenubarItem onSelect={handleImportDicom}>
Dicom<MenubarShortcut>T</MenubarShortcut>
</MenubarItem>
<MenubarItem>
New Window <MenubarShortcut>N</MenubarShortcut>
</MenubarItem>
<MenubarItem disabled>New Incognito Window</MenubarItem>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>Share</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem>Email link</MenubarItem>
<MenubarItem>Messages</MenubarItem>
<MenubarItem>Notes</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSeparator />
<MenubarItem>
Print... <MenubarShortcut>P</MenubarShortcut>
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Edit</MenubarTrigger>
<MenubarContent>
<MenubarItem>
Undo <MenubarShortcut>Z</MenubarShortcut>
</MenubarItem>
<MenubarItem>
Redo <MenubarShortcut>Z</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>Find</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem>Search the web</MenubarItem>
<MenubarSeparator />
<MenubarItem>Find...</MenubarItem>
<MenubarItem>Find Next</MenubarItem>
<MenubarItem>Find Previous</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSeparator />
<MenubarItem>Cut</MenubarItem>
<MenubarItem>Copy</MenubarItem>
<MenubarItem>Paste</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>View</MenubarTrigger>
<MenubarContent>
<MenubarCheckboxItem>Always Show Bookmarks Bar</MenubarCheckboxItem>
<MenubarCheckboxItem checked>
Always Show Full URLs
</MenubarCheckboxItem>
<MenubarSeparator />
<MenubarItem inset>
Reload <MenubarShortcut>R</MenubarShortcut>
</MenubarItem>
<MenubarItem disabled inset>
Force Reload <MenubarShortcut>R</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem inset>Toggle Fullscreen</MenubarItem>
<MenubarSeparator />
<MenubarItem inset>Hide Sidebar</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Profiles</MenubarTrigger>
<MenubarContent>
<MenubarRadioGroup value="benoit">
<MenubarRadioItem value="andy">Andy</MenubarRadioItem>
<MenubarRadioItem value="benoit">Benoit</MenubarRadioItem>
<MenubarRadioItem value="Luis">Luis</MenubarRadioItem>
</MenubarRadioGroup>
<MenubarSeparator />
<MenubarItem inset>Edit...</MenubarItem>
<MenubarSeparator />
<MenubarItem inset>Add Profile...</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>
);
};

View File

@ -0,0 +1,328 @@
import { CaretSortIcon, DotsHorizontalIcon } from "@radix-ui/react-icons";
import {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useState } from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { IoAdd, IoRefreshCircleOutline } from "react-icons/io5";
export type Payment = {
id: string;
filename: string;
modelname: string;
author: string;
homepage: string;
hash: string;
filesize: string;
createtime: string;
};
const data: Payment[] = [
{
id: "m5gr84i9",
filename: "waizhou111",
modelname: "外周111",
author: "ken99@yahoo.com",
homepage: "",
hash: "m5gr84i9",
filesize: "500mb",
createtime: "2024年08月29日",
},
{
id: "3u1reuv4",
filename: "waizhou222",
modelname: "外周222",
author: "ken99@yahoo.com",
homepage: "",
hash: "m5gr84i9",
filesize: "500mb",
createtime: "2024年01月29日",
},
{
id: "derv1ws0",
filename: "waizhou333",
modelname: "外周333",
author: "ken99@yahoo.com",
homepage: "",
hash: "m5gr84i9",
filesize: "500mb",
createtime: "2024年08月29日",
},
{
id: "5kma53ae",
filename: "waizhou1",
modelname: "外周1",
author: "ken99@yahoo.com",
homepage: "",
hash: "m5gr84i9",
filesize: "500mb",
createtime: "2024年08月29日",
},
{
id: "bhqecj4p",
filename: "waizhou2",
modelname: "外周2",
author: "ken99@yahoo.com",
homepage: "",
hash: "m5gr84i9",
filesize: "500mb",
createtime: "2024年08月29日",
},
{
id: "1bhqecj4p",
filename: "waizhou3",
modelname: "外周3",
author: "ken99@yahoo.com",
homepage: "",
hash: "m5gr84i9",
filesize: "500mb",
createtime: "2024年08月29日",
},
{
id: "2bhqecj4p",
filename: "waizhou6",
modelname: "外周6",
author: "ken99@yahoo.com",
homepage: "",
hash: "m5gr84i9",
filesize: "500mb",
createtime: "2024年08月29日",
},
{
id: "3bhqecj4p",
filename: "waizhou9",
modelname: "外周9",
author: "ken99@yahoo.com",
homepage: "",
hash: "m5gr84i9",
filesize: "500mb",
createtime: "2024年08月29日",
},
];
export const columns: ColumnDef<Payment>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "filename",
header: "文件名",
},
{
accessorKey: "modelname",
header: "模型名",
},
{
accessorKey: "author",
header: "作者",
},
{
accessorKey: "createtime",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
<CaretSortIcon className="ml-2 h-4 w-4" />
</Button>
);
},
cell: ({ row }) => (
<div className="lowercase">{row.getValue("createtime")}</div>
),
},
{
accessorKey: "filesize",
header: "文件大小",
},
{
accessorKey: "homepage",
header: "主页",
},
{
accessorKey: "hash",
header: "短哈希",
},
{
id: "actions",
header: "操作",
enableHiding: false,
cell: ({ row }) => {
const payment = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<DotsHorizontalIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(payment.id)}
>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem></DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
export function ModelTable() {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [rowSelection, setRowSelection] = useState({});
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
});
return (
<div className="w-full h-full flex flex-col">
<div className="flex-shrink-0 flex items-center justify-between p-4">
<Input
placeholder="根据名称查询模型"
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("email")?.setFilterValue(event.target.value)
}
className="max-w-sm"
/>
<div className="flex items-center space-x-2">
<Button className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 px-4 py-2">
<IoRefreshCircleOutline className="mr-2 h-4 w-4" />
</Button>
<Button className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 px-4 py-2">
<IoAdd className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
<ScrollArea className="flex-grow pl-4 pr-4">
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="text-right space-x-2 py-4">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length}
</div>
</div>
</ScrollArea>
</div>
);
}

View File

@ -0,0 +1,5 @@
import { ModelTable } from "./ModelTable";
export const Models = () => {
return <ModelTable />;
};

View File

@ -0,0 +1,12 @@
import { motion } from 'framer-motion';
export const Setting = () => {
return <motion.div
initial={{ y: 10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 0, opacity: 0 }}
transition={{ duration: 0.25 }}
>
<div></div>
</motion.div>
}

View File

@ -0,0 +1,5 @@
import React from "react";
export const Tools = () => {
return <div>Tools</div>;
};

View File

@ -0,0 +1,78 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body,
#root {
height: 100%;
margin: 0;
overflow: hidden;
}
@layer base {
:root {
--background: 0 0% 94.12%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--background: 0deg 0% 12.55%;
--foreground: 0 0% 100%;
--card: 0 0% 14.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

1
apps/desktop/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,86 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate"), function ({ addUtilities }) {
addUtilities({
'.drag': {
'-webkit-app-region': 'drag',
},
'.no-drag': {
'-webkit-app-region': 'no-drag',
},
});
},],
}

View File

@ -0,0 +1,42 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"experimentalDecorators": true,
"strictPropertyInitialization": false,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"src",
"electron"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,35 @@
import { defineConfig } from "vite";
import path from "node:path";
import electron from "vite-plugin-electron/simple";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
electron({
main: {
// Shortcut of `build.lib.entry`.
entry: "electron/main.ts",
},
preload: {
// Shortcut of `build.rollupOptions.input`.
// Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`.
input: path.join(__dirname, "electron/preload.ts"),
},
// Ployfill the Electron and Node.js API for Renderer process.
// If you want use Node.js in Renderer process, the `nodeIntegration` needs to be enabled in the Main process.
// See 👉 https://github.com/electron-vite/vite-plugin-electron-renderer
renderer:
process.env.NODE_ENV === "test"
? // https://github.com/electron-vite/vite-plugin-electron-renderer/issues/78#issuecomment-2053600808
undefined
: {},
}),
],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});