mirror of
https://github.com/mfakbar127/Claude-Samurai
synced 2026-06-21 13:57:42 +00:00
add biome
This commit is contained in:
+19
-20
@@ -1,22 +1,21 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(pnpm tsc:*)",
|
||||
"mcp__exa__get_code_context_exa",
|
||||
"mcp__context7__resolve-library-id",
|
||||
"mcp__context7__get-library-docs",
|
||||
"mcp__exa__web_search_exa",
|
||||
"Bash(pnpm dlx shadcn@latest add native-select)",
|
||||
"mcp__shadcn__get_project_registries",
|
||||
"mcp__shadcn__search_items_in_registries",
|
||||
"mcp__shadcn__get_add_command_for_items",
|
||||
"Bash(pnpm dlx shadcn@latest add:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
},
|
||||
"enableAllProjectMcpServers": true,
|
||||
"enabledMcpjsonServers": [
|
||||
"shadcn"
|
||||
]
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(pnpm tsc:*)",
|
||||
"mcp__exa__get_code_context_exa",
|
||||
"mcp__context7__resolve-library-id",
|
||||
"mcp__context7__get-library-docs",
|
||||
"mcp__exa__web_search_exa",
|
||||
"Bash(pnpm dlx shadcn@latest add native-select)",
|
||||
"mcp__shadcn__get_project_registries",
|
||||
"mcp__shadcn__search_items_in_registries",
|
||||
"mcp__shadcn__get_add_command_for_items",
|
||||
"Bash(pnpm dlx shadcn@latest add:*)",
|
||||
"Bash(pnpm exec biome check:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
},
|
||||
"enableAllProjectMcpServers": true,
|
||||
"enabledMcpjsonServers": ["shadcn"]
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"shadcn": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"shadcn@latest",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
"mcpServers": {
|
||||
"shadcn": {
|
||||
"command": "npx",
|
||||
"args": ["shadcn@latest", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+5
-1
@@ -1,3 +1,7 @@
|
||||
{
|
||||
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer", "Lokalise.i18n-ally"]
|
||||
"recommendations": [
|
||||
"tauri-apps.tauri-vscode",
|
||||
"rust-lang.rust-analyzer",
|
||||
"Lokalise.i18n-ally"
|
||||
]
|
||||
}
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.1/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "tab"
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"parser": {
|
||||
"allowWrongLineComments": true,
|
||||
"cssModules": false,
|
||||
"tailwindDirectives": true
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
}
|
||||
},
|
||||
"assist": {
|
||||
"enabled": true,
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": "on"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-20
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/App.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"registries": {}
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/App.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"registries": {}
|
||||
}
|
||||
|
||||
+81
-80
@@ -1,82 +1,83 @@
|
||||
{
|
||||
"name": "cc-mate",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/fold": "^0.19.4",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.4.0",
|
||||
"@codemirror/language": "^6.11.3",
|
||||
"@codemirror/language-data": "^6.5.1",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@lobehub/icons": "^2.38.0",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@remixicon/react": "^4.6.0",
|
||||
"@tanstack/react-query": "^5.90.2",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.0",
|
||||
"@tauri-apps/plugin-notification": "^2.3.1",
|
||||
"@tauri-apps/plugin-opener": "^2.5.0",
|
||||
"@tauri-apps/plugin-os": "^2.3.1",
|
||||
"@tauri-apps/plugin-updater": "~2",
|
||||
"@tremor/react": "^3.18.7",
|
||||
"@uiw/codemirror-theme-vscode": "^4.25.2",
|
||||
"@uiw/react-codemirror": "^4.25.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"i18next": "^25.5.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "^0.544.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-hook-form": "^7.64.0",
|
||||
"react-i18next": "^16.0.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^7.9.3",
|
||||
"recharts": "^3.2.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-variants": "^3.1.1",
|
||||
"ts-pattern": "^5.8.0",
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
||||
"shadcn": "^3.5.0",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.0.4"
|
||||
}
|
||||
"name": "cc-mate",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/fold": "^0.19.4",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.4.0",
|
||||
"@codemirror/language": "^6.11.3",
|
||||
"@codemirror/language-data": "^6.5.1",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@lobehub/icons": "^2.38.0",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@remixicon/react": "^4.6.0",
|
||||
"@tanstack/react-query": "^5.90.2",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.0",
|
||||
"@tauri-apps/plugin-notification": "^2.3.1",
|
||||
"@tauri-apps/plugin-opener": "^2.5.0",
|
||||
"@tauri-apps/plugin-os": "^2.3.1",
|
||||
"@tauri-apps/plugin-updater": "~2",
|
||||
"@tremor/react": "^3.18.7",
|
||||
"@uiw/codemirror-theme-vscode": "^4.25.2",
|
||||
"@uiw/react-codemirror": "^4.25.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"i18next": "^25.5.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "^0.544.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-hook-form": "^7.64.0",
|
||||
"react-i18next": "^16.0.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^7.9.3",
|
||||
"recharts": "^3.2.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-variants": "^3.1.1",
|
||||
"ts-pattern": "^5.8.0",
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.3.1",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
||||
"shadcn": "^3.5.0",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+91
@@ -171,6 +171,9 @@ importers:
|
||||
specifier: ^4.1.11
|
||||
version: 4.1.11
|
||||
devDependencies:
|
||||
'@biomejs/biome':
|
||||
specifier: 2.3.1
|
||||
version: 2.3.1
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.1.14
|
||||
version: 4.1.14(vite@7.1.9(jiti@2.6.1)(lightningcss@1.30.1))
|
||||
@@ -431,6 +434,59 @@ packages:
|
||||
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@biomejs/biome@2.3.1':
|
||||
resolution: {integrity: sha512-A29evf1R72V5bo4o2EPxYMm5mtyGvzp2g+biZvRFx29nWebGyyeOSsDWGx3tuNNMFRepGwxmA9ZQ15mzfabK2w==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
hasBin: true
|
||||
|
||||
'@biomejs/cli-darwin-arm64@2.3.1':
|
||||
resolution: {integrity: sha512-ombSf3MnTUueiYGN1SeI9tBCsDUhpWzOwS63Dove42osNh0PfE1cUtHFx6eZ1+MYCCLwXzlFlYFdrJ+U7h6LcA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@biomejs/cli-darwin-x64@2.3.1':
|
||||
resolution: {integrity: sha512-pcOfwyoQkrkbGvXxRvZNe5qgD797IowpJPovPX5biPk2FwMEV+INZqfCaz4G5bVq9hYnjwhRMamg11U4QsRXrQ==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@biomejs/cli-linux-arm64-musl@2.3.1':
|
||||
resolution: {integrity: sha512-+DZYv8l7FlUtTrWs1Tdt1KcNCAmRO87PyOnxKGunbWm5HKg1oZBSbIIPkjrCtDZaeqSG1DiGx7qF+CPsquQRcg==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-linux-arm64@2.3.1':
|
||||
resolution: {integrity: sha512-td5O8pFIgLs8H1sAZsD6v+5quODihyEw4nv2R8z7swUfIK1FKk+15e4eiYVLcAE4jUqngvh4j3JCNgg0Y4o4IQ==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-linux-x64-musl@2.3.1':
|
||||
resolution: {integrity: sha512-Y3Ob4nqgv38Mh+6EGHltuN+Cq8aj/gyMTJYzkFZV2AEj+9XzoXB9VNljz9pjfFNHUxvLEV4b55VWyxozQTBaUQ==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-linux-x64@2.3.1':
|
||||
resolution: {integrity: sha512-PYWgEO7up7XYwSAArOpzsVCiqxBCXy53gsReAb1kKYIyXaoAlhBaBMvxR/k2Rm9aTuZ662locXUmPk/Aj+Xu+Q==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-win32-arm64@2.3.1':
|
||||
resolution: {integrity: sha512-RHIG/zgo+69idUqVvV3n8+j58dKYABRpMyDmfWu2TITC+jwGPiEaT0Q3RKD+kQHiS80mpBrST0iUGeEXT0bU9A==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@biomejs/cli-win32-x64@2.3.1':
|
||||
resolution: {integrity: sha512-izl30JJ5Dp10mi90Eko47zhxE6pYyWPcnX1NQxKpL/yMhXxf95oLTzfpu4q+MDBh/gemNqyJEwjBpe0MT5iWPA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@braintree/sanitize-url@7.1.1':
|
||||
resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==}
|
||||
|
||||
@@ -5653,6 +5709,41 @@ snapshots:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
|
||||
'@biomejs/biome@2.3.1':
|
||||
optionalDependencies:
|
||||
'@biomejs/cli-darwin-arm64': 2.3.1
|
||||
'@biomejs/cli-darwin-x64': 2.3.1
|
||||
'@biomejs/cli-linux-arm64': 2.3.1
|
||||
'@biomejs/cli-linux-arm64-musl': 2.3.1
|
||||
'@biomejs/cli-linux-x64': 2.3.1
|
||||
'@biomejs/cli-linux-x64-musl': 2.3.1
|
||||
'@biomejs/cli-win32-arm64': 2.3.1
|
||||
'@biomejs/cli-win32-x64': 2.3.1
|
||||
|
||||
'@biomejs/cli-darwin-arm64@2.3.1':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-darwin-x64@2.3.1':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-arm64-musl@2.3.1':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-arm64@2.3.1':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-x64-musl@2.3.1':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-x64@2.3.1':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-win32-arm64@2.3.1':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-win32-x64@2.3.1':
|
||||
optional: true
|
||||
|
||||
'@braintree/sanitize-url@7.1.1': {}
|
||||
|
||||
'@chevrotain/cst-dts-gen@11.0.3':
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"dialog:allow-ask",
|
||||
"dialog:allow-message",
|
||||
"os:allow-platform"
|
||||
]
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"dialog:allow-ask",
|
||||
"dialog:allow-message",
|
||||
"os:allow-platform"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
{
|
||||
"identifier": "desktop-capability",
|
||||
"platforms": [
|
||||
"macOS",
|
||||
"windows",
|
||||
"linux"
|
||||
],
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"updater:default"
|
||||
]
|
||||
}
|
||||
"identifier": "desktop-capability",
|
||||
"platforms": ["macOS", "windows", "linux"],
|
||||
"windows": ["main"],
|
||||
"permissions": ["updater:default"]
|
||||
}
|
||||
|
||||
+43
-46
@@ -1,48 +1,45 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "CC Mate",
|
||||
"version": "0.2.9",
|
||||
"identifier": "org.randynamic.ccconfig",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "pnpm build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"macOSPrivateApi": true,
|
||||
"windows": [
|
||||
{
|
||||
"title": "CC Mate",
|
||||
"width": 860,
|
||||
"height": 660,
|
||||
"minWidth": 860,
|
||||
"minHeight": 660,
|
||||
"decorations": true,
|
||||
"titleBarStyle": "Overlay",
|
||||
"hiddenTitle": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null,
|
||||
"capabilities": ["default"]
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"createUpdaterArtifacts": true,
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5NjU1MUZBRDYxQTY1ODcKUldTSFpSclcrbEZsR1ptZ3Z0a3hJNEx0RHpGQ29ZQVhJWDZZN3JOMnBBd29YdzA5ZmRsNzloRlEK",
|
||||
"endpoints": [
|
||||
"https://github.com/djyde/ccmate-release/releases/latest/download/latest.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "CC Mate",
|
||||
"version": "0.2.9",
|
||||
"identifier": "org.randynamic.ccconfig",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "pnpm build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"macOSPrivateApi": true,
|
||||
"windows": [
|
||||
{
|
||||
"title": "CC Mate",
|
||||
"width": 860,
|
||||
"height": 660,
|
||||
"minWidth": 860,
|
||||
"minHeight": 660,
|
||||
"decorations": true,
|
||||
"titleBarStyle": "Overlay",
|
||||
"hiddenTitle": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null,
|
||||
"capabilities": ["default"]
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"createUpdaterArtifacts": true,
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": ["icons/icon.icns", "icons/icon.ico"]
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5NjU1MUZBRDYxQTY1ODcKUldTSFpSclcrbEZsR1ptZ3Z0a3hJNEx0RHpGQ29ZQVhJWDZZN3JOMnBBd29YdzA5ZmRsNzloRlEK",
|
||||
"endpoints": [
|
||||
"https://github.com/djyde/ccmate-release/releases/latest/download/latest.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,99 +1,98 @@
|
||||
import { Component, ErrorInfo, ReactNode } from "react";
|
||||
import { Button } from "./ui/button";
|
||||
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Component, type ErrorInfo, type ReactNode } from "react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
errorInfo: ErrorInfo | null;
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
errorInfo: ErrorInfo | null;
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
};
|
||||
}
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return {
|
||||
hasError: true,
|
||||
error,
|
||||
errorInfo: null,
|
||||
};
|
||||
}
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return {
|
||||
hasError: true,
|
||||
error,
|
||||
errorInfo: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
console.error("ErrorBoundary caught an error:", error, errorInfo);
|
||||
this.setState({
|
||||
error,
|
||||
errorInfo,
|
||||
});
|
||||
}
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
console.error("ErrorBoundary caught an error:", error, errorInfo);
|
||||
this.setState({
|
||||
error,
|
||||
errorInfo,
|
||||
});
|
||||
}
|
||||
|
||||
handleReset = () => {
|
||||
this.setState({
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
});
|
||||
window.location.href = "/";
|
||||
};
|
||||
handleReset = () => {
|
||||
this.setState({
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
});
|
||||
window.location.href = "/";
|
||||
};
|
||||
|
||||
handleReload = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
handleReload = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background p-4">
|
||||
<div className="max-w-2xl w-full space-y-4">
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Something went wrong</AlertTitle>
|
||||
<AlertDescription className="mt-2">
|
||||
<div className="font-medium mb-1 select-text">
|
||||
{this.state.error?.message || "An unexpected error occurred"}
|
||||
</div>
|
||||
<div className="text-sm opacity-90 select-text">
|
||||
Please try reloading the page or return to the home page.
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background p-4">
|
||||
<div className="max-w-2xl w-full space-y-4">
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Something went wrong</AlertTitle>
|
||||
<AlertDescription className="mt-2">
|
||||
<div className="font-medium mb-1 select-text">
|
||||
{this.state.error?.message || "An unexpected error occurred"}
|
||||
</div>
|
||||
<div className="text-sm opacity-90 select-text">
|
||||
Please try reloading the page or return to the home page.
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{this.state.error && (
|
||||
<div className="bg-muted p-4 rounded-lg">
|
||||
<h3 className="font-semibold mb-2 text-sm">Stack Trace:</h3>
|
||||
<pre className="text-xs overflow-auto max-h-60 text-muted-foreground whitespace-pre-wrap break-words select-text cursor-text">
|
||||
{this.state.error.stack || this.state.error.toString()}
|
||||
{this.state.errorInfo?.componentStack}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{this.state.error && (
|
||||
<div className="bg-muted p-4 rounded-lg">
|
||||
<h3 className="font-semibold mb-2 text-sm">Stack Trace:</h3>
|
||||
<pre className="text-xs overflow-auto max-h-60 text-muted-foreground whitespace-pre-wrap break-words select-text cursor-text">
|
||||
{this.state.error.stack || this.state.error.toString()}
|
||||
{this.state.errorInfo?.componentStack}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button onClick={this.handleReset} variant="default">
|
||||
Go to Home
|
||||
</Button>
|
||||
<Button onClick={this.handleReload} variant="outline">
|
||||
Reload Page
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<div className="flex gap-3">
|
||||
<Button onClick={this.handleReset} variant="default">
|
||||
Go to Home
|
||||
</Button>
|
||||
<Button onClick={this.handleReload} variant="outline">
|
||||
Reload Page
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,58 +1,77 @@
|
||||
import { useState } from "react"
|
||||
import { Button } from "./ui/button"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip"
|
||||
import { CircleQuestionMarkIcon } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { GLMDialog } from "./GLMDialog"
|
||||
import { CircleQuestionMarkIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { GLMDialog } from "./GLMDialog";
|
||||
import { Button } from "./ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "./ui/tooltip";
|
||||
|
||||
export { GLMDialog } from "./GLMDialog"
|
||||
export { GLMDialog } from "./GLMDialog";
|
||||
|
||||
export function GLMBanner(props: {
|
||||
className?: string,
|
||||
hideCloseButton?: boolean
|
||||
className?: string;
|
||||
hideCloseButton?: boolean;
|
||||
}) {
|
||||
const { t, i18n } = useTranslation()
|
||||
const [isDismissed, setIsDismissed] = useState(localStorage.getItem('glm-banner-dismissed') === 'true')
|
||||
const { t, i18n } = useTranslation();
|
||||
const [isDismissed, setIsDismissed] = useState(
|
||||
localStorage.getItem("glm-banner-dismissed") === "true",
|
||||
);
|
||||
|
||||
const handleDismiss = () => {
|
||||
localStorage.setItem('glm-banner-dismissed', 'true')
|
||||
setIsDismissed(true)
|
||||
}
|
||||
const handleDismiss = () => {
|
||||
localStorage.setItem("glm-banner-dismissed", "true");
|
||||
setIsDismissed(true);
|
||||
};
|
||||
|
||||
// Only show banner when locale is Chinese
|
||||
if (i18n.language !== 'zh' || isDismissed) {
|
||||
return null
|
||||
}
|
||||
// Only show banner when locale is Chinese
|
||||
if (i18n.language !== "zh" || isDismissed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("bg-card rounded-md p-2 border space-y-2", props.className)}>
|
||||
<h3 className="text-card-foreground text-sm font-medium flex items-center gap-2">{t('glm.title')}
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger>
|
||||
<CircleQuestionMarkIcon size={14} className="text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="w-[200px]">
|
||||
<p className="font-normal">{t('glm.tooltip')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</h3>
|
||||
<div className="flex items-center gap-1">
|
||||
<GLMDialog
|
||||
trigger={
|
||||
<Button size="sm" variant="outline" className="text-sm">
|
||||
{t('glm.startConfig')}
|
||||
</Button>
|
||||
}
|
||||
onSuccess={handleDismiss}
|
||||
/>
|
||||
{!props.hideCloseButton && <Button size="sm" variant="ghost" className="text-sm" onClick={handleDismiss}>
|
||||
{t('glm.close')}
|
||||
</Button>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
className={cn("bg-card rounded-md p-2 border space-y-2", props.className)}
|
||||
>
|
||||
<h3 className="text-card-foreground text-sm font-medium flex items-center gap-2">
|
||||
{t("glm.title")}
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger>
|
||||
<CircleQuestionMarkIcon
|
||||
size={14}
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="w-[200px]">
|
||||
<p className="font-normal">{t("glm.tooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</h3>
|
||||
<div className="flex items-center gap-1">
|
||||
<GLMDialog
|
||||
trigger={
|
||||
<Button size="sm" variant="outline" className="text-sm">
|
||||
{t("glm.startConfig")}
|
||||
</Button>
|
||||
}
|
||||
onSuccess={handleDismiss}
|
||||
/>
|
||||
{!props.hideCloseButton && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-sm"
|
||||
onClick={handleDismiss}
|
||||
>
|
||||
{t("glm.close")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+169
-135
@@ -1,148 +1,182 @@
|
||||
import { useState } from "react"
|
||||
import { openUrl } from "@tauri-apps/plugin-opener"
|
||||
import { Button } from "./ui/button"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog"
|
||||
import { ExternalLinkIcon } from "lucide-react"
|
||||
import { Input } from "./ui/input"
|
||||
import { NativeSelect, NativeSelectOption } from "./ui/native-select"
|
||||
import { useCreateConfig, useSetCurrentConfig } from "@/lib/query"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useCreateConfig, useSetCurrentConfig } from "@/lib/query";
|
||||
import { Button } from "./ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "./ui/dialog";
|
||||
import { Input } from "./ui/input";
|
||||
import { NativeSelect, NativeSelectOption } from "./ui/native-select";
|
||||
|
||||
export function GLMDialog(props: {
|
||||
trigger: React.ReactNode
|
||||
onSuccess?: () => void
|
||||
trigger: React.ReactNode;
|
||||
onSuccess?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const [apiKey, setApiKey] = useState("")
|
||||
const [selectedRegion, setSelectedRegion] = useState("china-mainland")
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const createConfigMutation = useCreateConfig()
|
||||
const setCurrentConfigMutation = useSetCurrentConfig()
|
||||
const { t } = useTranslation();
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [selectedRegion, setSelectedRegion] = useState("china-mainland");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const createConfigMutation = useCreateConfig();
|
||||
const setCurrentConfigMutation = useSetCurrentConfig();
|
||||
|
||||
const handleCreateConfig = async () => {
|
||||
if (!apiKey.trim()) {
|
||||
return
|
||||
}
|
||||
const handleCreateConfig = async () => {
|
||||
if (!apiKey.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const baseUrl = selectedRegion === "z-ai"
|
||||
? 'https://api.z.ai/api/anthropic'
|
||||
: 'https://open.bigmodel.cn/api/anthropic'
|
||||
try {
|
||||
const baseUrl =
|
||||
selectedRegion === "z-ai"
|
||||
? "https://api.z.ai/api/anthropic"
|
||||
: "https://open.bigmodel.cn/api/anthropic";
|
||||
|
||||
const store = await createConfigMutation.mutateAsync({
|
||||
title: selectedRegion === "z-ai" ? t('glm.zaiTitle') : t('glm.zhipuTitle'),
|
||||
settings: {
|
||||
env: {
|
||||
ANTHROPIC_AUTH_TOKEN: apiKey.trim(),
|
||||
ANTHROPIC_BASE_URL: baseUrl,
|
||||
ANTHROPIC_MODEL: 'GLM-4.6',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'GLM-4.6',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'GLM-4.6',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'GLM-4.5-Air'
|
||||
}
|
||||
}
|
||||
})
|
||||
const store = await createConfigMutation.mutateAsync({
|
||||
title:
|
||||
selectedRegion === "z-ai" ? t("glm.zaiTitle") : t("glm.zhipuTitle"),
|
||||
settings: {
|
||||
env: {
|
||||
ANTHROPIC_AUTH_TOKEN: apiKey.trim(),
|
||||
ANTHROPIC_BASE_URL: baseUrl,
|
||||
ANTHROPIC_MODEL: "GLM-4.6",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "GLM-4.6",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "GLM-4.6",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "GLM-4.5-Air",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Set the newly created config as the current/active config
|
||||
await setCurrentConfigMutation.mutateAsync(store.id)
|
||||
// Set the newly created config as the current/active config
|
||||
await setCurrentConfigMutation.mutateAsync(store.id);
|
||||
|
||||
setIsOpen(false)
|
||||
setApiKey("")
|
||||
setSelectedRegion("china-mainland")
|
||||
setIsOpen(false);
|
||||
setApiKey("");
|
||||
setSelectedRegion("china-mainland");
|
||||
|
||||
// Call onSuccess callback to dismiss the banner
|
||||
props.onSuccess?.()
|
||||
} catch (error) {
|
||||
console.error("Failed to create GLM config:", error)
|
||||
}
|
||||
}
|
||||
// Call onSuccess callback to dismiss the banner
|
||||
props.onSuccess?.();
|
||||
} catch (error) {
|
||||
console.error("Failed to create GLM config:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{props.trigger}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{selectedRegion === "z-ai" ? t('glm.configZai') : t('glm.configZhipu')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('glm.description', { provider: selectedRegion === "z-ai" ? "Z.ai" : t('glm.zhipu') })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="mt-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="my-4 flex items-center gap-3">
|
||||
<NativeSelect
|
||||
value={selectedRegion}
|
||||
onChange={(e) => setSelectedRegion(e.target.value)}
|
||||
className="w-full mt-1"
|
||||
>
|
||||
<NativeSelectOption value="china-mainland">{t('glm.chinaMainland')}</NativeSelectOption>
|
||||
<NativeSelectOption value="z-ai">{t('glm.international')}</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</div>
|
||||
<h2 className="text-card-foreground text-sm font-medium flex items-center gap-2">
|
||||
{t('glm.step1')}
|
||||
</h2>
|
||||
<div className="space-y-2 bg-secondary p-3 rounded-lg m-2">
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>{props.trigger}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{selectedRegion === "z-ai"
|
||||
? t("glm.configZai")
|
||||
: t("glm.configZhipu")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("glm.description", {
|
||||
provider: selectedRegion === "z-ai" ? "Z.ai" : t("glm.zhipu"),
|
||||
})}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="mt-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="my-4 flex items-center gap-3">
|
||||
<NativeSelect
|
||||
value={selectedRegion}
|
||||
onChange={(e) => setSelectedRegion(e.target.value)}
|
||||
className="w-full mt-1"
|
||||
>
|
||||
<NativeSelectOption value="china-mainland">
|
||||
{t("glm.chinaMainland")}
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="z-ai">
|
||||
{t("glm.international")}
|
||||
</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</div>
|
||||
<h2 className="text-card-foreground text-sm font-medium flex items-center gap-2">
|
||||
{t("glm.step1")}
|
||||
</h2>
|
||||
<div className="space-y-2 bg-secondary p-3 rounded-lg m-2">
|
||||
<Button
|
||||
onClick={(_) => {
|
||||
const url =
|
||||
selectedRegion === "z-ai"
|
||||
? "https://z.ai/subscribe?ic=EBGYZCJRYJ"
|
||||
: "https://www.bigmodel.cn/claude-code?ic=UP1VEQEATH";
|
||||
openUrl(url);
|
||||
}}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-sm"
|
||||
>
|
||||
<ExternalLinkIcon />
|
||||
{t("glm.buyFromOfficial")}
|
||||
</Button>
|
||||
<p className="text-muted-foreground text-sm flex items-center gap-1">
|
||||
{t("glm.discount")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button onClick={_ => {
|
||||
const url = selectedRegion === "z-ai"
|
||||
? "https://z.ai/subscribe?ic=EBGYZCJRYJ"
|
||||
: "https://www.bigmodel.cn/claude-code?ic=UP1VEQEATH"
|
||||
openUrl(url)
|
||||
}} size="sm" variant="outline" className="text-sm">
|
||||
<ExternalLinkIcon />
|
||||
{t('glm.buyFromOfficial')}
|
||||
</Button>
|
||||
<p className="text-muted-foreground text-sm flex items-center gap-1">{t('glm.discount')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-card-foreground text-sm font-medium flex items-center gap-2">
|
||||
{t("glm.step2")}
|
||||
</h2>
|
||||
<div className="space-y-2 bg-secondary p-3 rounded-lg m-2">
|
||||
<Button
|
||||
onClick={(_) => {
|
||||
const url =
|
||||
selectedRegion === "z-ai"
|
||||
? "https://z.ai/manage-apikey/apikey-list"
|
||||
: "https://bigmodel.cn/usercenter/proj-mgmt/apikeys";
|
||||
openUrl(url);
|
||||
}}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-sm"
|
||||
>
|
||||
<ExternalLinkIcon />
|
||||
{t("glm.enterConsole")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-card-foreground text-sm font-medium flex items-center gap-2">
|
||||
{t('glm.step2')}
|
||||
</h2>
|
||||
<div className="space-y-2 bg-secondary p-3 rounded-lg m-2">
|
||||
<Button onClick={_ => {
|
||||
const url = selectedRegion === "z-ai"
|
||||
? "https://z.ai/manage-apikey/apikey-list"
|
||||
: "https://bigmodel.cn/usercenter/proj-mgmt/apikeys"
|
||||
openUrl(url)
|
||||
}} size="sm" variant="outline" className="text-sm">
|
||||
<ExternalLinkIcon />
|
||||
{t('glm.enterConsole')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-card-foreground text-sm font-medium flex items-center gap-2">
|
||||
{t("glm.step3")}
|
||||
</h2>
|
||||
<div className="space-y-2 bg-secondary p-3 rounded-lg m-2">
|
||||
<Input
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder={
|
||||
selectedRegion === "z-ai"
|
||||
? t("glm.zaiApiKeyPlaceholder")
|
||||
: t("glm.zhipuApiKeyPlaceholder")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-card-foreground text-sm font-medium flex items-center gap-2">
|
||||
{t('glm.step3')}
|
||||
</h2>
|
||||
<div className="space-y-2 bg-secondary p-3 rounded-lg m-2">
|
||||
<Input
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder={selectedRegion === "z-ai" ? t('glm.zaiApiKeyPlaceholder') : t('glm.zhipuApiKeyPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mx-2 mt-2">
|
||||
<Button
|
||||
onClick={handleCreateConfig}
|
||||
disabled={!apiKey.trim() || createConfigMutation.isPending}
|
||||
>
|
||||
{createConfigMutation.isPending ? t('glm.creating') : t('glm.createConfig')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
<div className="flex justify-end mx-2 mt-2">
|
||||
<Button
|
||||
onClick={handleCreateConfig}
|
||||
disabled={!apiKey.trim() || createConfigMutation.isPending}
|
||||
>
|
||||
{createConfigMutation.isPending
|
||||
? t("glm.creating")
|
||||
: t("glm.createConfig")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
+139
-113
@@ -1,122 +1,148 @@
|
||||
import React from "react";
|
||||
import { Outlet, NavLink, useLocation } from "react-router-dom";
|
||||
import {
|
||||
ActivityIcon,
|
||||
BellIcon,
|
||||
BrainIcon,
|
||||
CpuIcon,
|
||||
FileJsonIcon,
|
||||
FolderIcon,
|
||||
SettingsIcon,
|
||||
TerminalIcon,
|
||||
} from "lucide-react";
|
||||
import type React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { NavLink, Outlet, useLocation } from "react-router-dom";
|
||||
import { cn, isMacOS } from "../lib/utils";
|
||||
import { FileJsonIcon, SettingsIcon, CpuIcon, ActivityIcon, BrainIcon, FolderIcon, BellIcon, TerminalIcon } from "lucide-react";
|
||||
import { ScrollArea } from "./ui/scroll-area";
|
||||
import { UpdateButton } from "./UpdateButton";
|
||||
import { ScrollArea } from "./ui/scroll-area";
|
||||
|
||||
export function Layout() {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const isProjectsRoute = location.pathname.startsWith('/projects');
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const isProjectsRoute = location.pathname.startsWith("/projects");
|
||||
|
||||
const navLinks = [
|
||||
{
|
||||
to: "/",
|
||||
icon: FileJsonIcon,
|
||||
label: t("navigation.configurations")
|
||||
},
|
||||
{
|
||||
to: "/projects",
|
||||
icon: FolderIcon,
|
||||
label: t("navigation.projects")
|
||||
},
|
||||
{
|
||||
to: "/mcp",
|
||||
icon: CpuIcon,
|
||||
label: t("navigation.mcp")
|
||||
},
|
||||
{
|
||||
to: "/memory",
|
||||
icon: BrainIcon,
|
||||
label: t("navigation.memory")
|
||||
},
|
||||
{
|
||||
to: "/commands",
|
||||
icon: TerminalIcon,
|
||||
label: t("navigation.commands")
|
||||
},
|
||||
{
|
||||
to: "/notification",
|
||||
icon: BellIcon,
|
||||
label: t("navigation.notifications")
|
||||
},
|
||||
{
|
||||
to: "/usage",
|
||||
icon: ActivityIcon,
|
||||
label: t("navigation.usage")
|
||||
},
|
||||
{
|
||||
to: "/settings",
|
||||
icon: SettingsIcon,
|
||||
label: t("navigation.settings")
|
||||
},
|
||||
const navLinks = [
|
||||
{
|
||||
to: "/",
|
||||
icon: FileJsonIcon,
|
||||
label: t("navigation.configurations"),
|
||||
},
|
||||
{
|
||||
to: "/projects",
|
||||
icon: FolderIcon,
|
||||
label: t("navigation.projects"),
|
||||
},
|
||||
{
|
||||
to: "/mcp",
|
||||
icon: CpuIcon,
|
||||
label: t("navigation.mcp"),
|
||||
},
|
||||
{
|
||||
to: "/memory",
|
||||
icon: BrainIcon,
|
||||
label: t("navigation.memory"),
|
||||
},
|
||||
{
|
||||
to: "/commands",
|
||||
icon: TerminalIcon,
|
||||
label: t("navigation.commands"),
|
||||
},
|
||||
{
|
||||
to: "/notification",
|
||||
icon: BellIcon,
|
||||
label: t("navigation.notifications"),
|
||||
},
|
||||
{
|
||||
to: "/usage",
|
||||
icon: ActivityIcon,
|
||||
label: t("navigation.usage"),
|
||||
},
|
||||
{
|
||||
to: "/settings",
|
||||
icon: SettingsIcon,
|
||||
label: t("navigation.settings"),
|
||||
},
|
||||
];
|
||||
|
||||
]
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex flex-col">
|
||||
{/* Custom Title Bar - Draggable Region with traffic lights space (macOS only) */}
|
||||
{isMacOS && (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className=""
|
||||
style={
|
||||
{
|
||||
WebkitUserSelect: "none",
|
||||
WebkitAppRegion: "drag",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
></div>
|
||||
)}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex flex-col">
|
||||
{/* Custom Title Bar - Draggable Region with traffic lights space (macOS only) */}
|
||||
{isMacOS && (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className=""
|
||||
style={{ WebkitUserSelect: 'none', WebkitAppRegion: 'drag' } as React.CSSProperties}
|
||||
>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-1 overflow-hidden ">
|
||||
<nav
|
||||
className="w-[200px] bg-background border-r flex flex-col"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
{isMacOS && (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="h-10"
|
||||
style={
|
||||
{
|
||||
WebkitUserSelect: "none",
|
||||
WebkitAppRegion: "drag",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
></div>
|
||||
)}
|
||||
<div
|
||||
className="flex flex-col flex-1 justify-between"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<ul className="px-3 pt-3 space-y-2">
|
||||
{navLinks.map((link) => (
|
||||
<li key={link.to}>
|
||||
<NavLink
|
||||
to={link.to}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"flex items-center gap-2 px-3 py-2 rounded-xl cursor-default select-none ",
|
||||
{
|
||||
"bg-primary text-primary-foreground": isActive,
|
||||
"hover:bg-accent hover:text-accent-foreground":
|
||||
!isActive,
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
<link.icon size={14} />
|
||||
{link.label}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="flex flex-1 overflow-hidden ">
|
||||
<nav className="w-[200px] bg-background border-r flex flex-col" data-tauri-drag-region >
|
||||
{isMacOS && (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="h-10"
|
||||
style={{ WebkitUserSelect: 'none', WebkitAppRegion: 'drag' } as React.CSSProperties}
|
||||
>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col flex-1 justify-between" data-tauri-drag-region>
|
||||
<ul className="px-3 pt-3 space-y-2">
|
||||
{navLinks.map((link) => (
|
||||
<li key={link.to}>
|
||||
<NavLink
|
||||
to={link.to}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"flex items-center gap-2 px-3 py-2 rounded-xl cursor-default select-none ",
|
||||
{
|
||||
"bg-primary text-primary-foreground": isActive,
|
||||
"hover:bg-accent hover:text-accent-foreground": !isActive
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
<link.icon size={14} />
|
||||
{link.label}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="space-y-2">
|
||||
<UpdateButton />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{isProjectsRoute ? (
|
||||
<main className="flex-1 h-screen overflow-hidden" data-tauri-drag-region>
|
||||
<Outlet />
|
||||
</main>
|
||||
) : (
|
||||
<ScrollArea className="flex-1 h-screen [&>div>div]:!block">
|
||||
<main className="" data-tauri-drag-region>
|
||||
<Outlet />
|
||||
</main>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<div className="space-y-2">
|
||||
<UpdateButton />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{isProjectsRoute ? (
|
||||
<main
|
||||
className="flex-1 h-screen overflow-hidden"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<Outlet />
|
||||
</main>
|
||||
) : (
|
||||
<ScrollArea className="flex-1 h-screen [&>div>div]:!block">
|
||||
<main className="" data-tauri-drag-region>
|
||||
<Outlet />
|
||||
</main>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+152
-124
@@ -1,136 +1,164 @@
|
||||
import { useState } from "react"
|
||||
import { openUrl } from "@tauri-apps/plugin-opener"
|
||||
import { Button } from "./ui/button"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog"
|
||||
import { CircleQuestionMarkIcon, ExternalLinkIcon } from "lucide-react"
|
||||
import { Input } from "./ui/input"
|
||||
import { NativeSelect, NativeSelectOption } from "./ui/native-select"
|
||||
import { useCreateConfig, useSetCurrentConfig } from "@/lib/query"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useCreateConfig, useSetCurrentConfig } from "@/lib/query";
|
||||
import { Button } from "./ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "./ui/dialog";
|
||||
import { Input } from "./ui/input";
|
||||
import { NativeSelect, NativeSelectOption } from "./ui/native-select";
|
||||
|
||||
export function MiniMaxDialog(props: {
|
||||
trigger: React.ReactNode
|
||||
onSuccess?: () => void
|
||||
trigger: React.ReactNode;
|
||||
onSuccess?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const [apiKey, setApiKey] = useState("")
|
||||
const [selectedRegion, setSelectedRegion] = useState("china-mainland")
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const createConfigMutation = useCreateConfig()
|
||||
const setCurrentConfigMutation = useSetCurrentConfig()
|
||||
const { t } = useTranslation();
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [selectedRegion, setSelectedRegion] = useState("china-mainland");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const createConfigMutation = useCreateConfig();
|
||||
const setCurrentConfigMutation = useSetCurrentConfig();
|
||||
|
||||
const handleCreateConfig = async () => {
|
||||
if (!apiKey.trim()) {
|
||||
return
|
||||
}
|
||||
const handleCreateConfig = async () => {
|
||||
if (!apiKey.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const baseUrl = selectedRegion === "international"
|
||||
? 'https://api.minimax.io/anthropic'
|
||||
: 'https://api.minimaxi.com/anthropic'
|
||||
try {
|
||||
const baseUrl =
|
||||
selectedRegion === "international"
|
||||
? "https://api.minimax.io/anthropic"
|
||||
: "https://api.minimaxi.com/anthropic";
|
||||
|
||||
const store = await createConfigMutation.mutateAsync({
|
||||
title: selectedRegion === "international" ? t('minimax.internationalTitle') : t('minimax.mainlandTitle'),
|
||||
settings: {
|
||||
env: {
|
||||
ANTHROPIC_AUTH_TOKEN: apiKey.trim(),
|
||||
ANTHROPIC_BASE_URL: baseUrl,
|
||||
API_TIMEOUT_MS: "3000000",
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1,
|
||||
ANTHROPIC_MODEL: 'MiniMax-M2',
|
||||
ANTHROPIC_SMALL_FAST_MODEL: 'MiniMax-M2',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'MiniMax-M2',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'MiniMax-M2',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'MiniMax-M2'
|
||||
}
|
||||
}
|
||||
})
|
||||
const store = await createConfigMutation.mutateAsync({
|
||||
title:
|
||||
selectedRegion === "international"
|
||||
? t("minimax.internationalTitle")
|
||||
: t("minimax.mainlandTitle"),
|
||||
settings: {
|
||||
env: {
|
||||
ANTHROPIC_AUTH_TOKEN: apiKey.trim(),
|
||||
ANTHROPIC_BASE_URL: baseUrl,
|
||||
API_TIMEOUT_MS: "3000000",
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1,
|
||||
ANTHROPIC_MODEL: "MiniMax-M2",
|
||||
ANTHROPIC_SMALL_FAST_MODEL: "MiniMax-M2",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "MiniMax-M2",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "MiniMax-M2",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "MiniMax-M2",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Set the newly created config as the current/active config
|
||||
await setCurrentConfigMutation.mutateAsync(store.id)
|
||||
// Set the newly created config as the current/active config
|
||||
await setCurrentConfigMutation.mutateAsync(store.id);
|
||||
|
||||
setIsOpen(false)
|
||||
setApiKey("")
|
||||
setSelectedRegion("china-mainland")
|
||||
setIsOpen(false);
|
||||
setApiKey("");
|
||||
setSelectedRegion("china-mainland");
|
||||
|
||||
// Call onSuccess callback to dismiss the banner
|
||||
props.onSuccess?.()
|
||||
} catch (error) {
|
||||
console.error("Failed to create MiniMax config:", error)
|
||||
}
|
||||
}
|
||||
// Call onSuccess callback to dismiss the banner
|
||||
props.onSuccess?.();
|
||||
} catch (error) {
|
||||
console.error("Failed to create MiniMax config:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{props.trigger}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{selectedRegion === "international" ? t('minimax.configInternational') : t('minimax.configMainland')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div className="flex items-center gap-2">
|
||||
<p>
|
||||
{t('minimax.description', { provider: selectedRegion === "international" ? "MiniMax" : t('minimax.minimax') })}
|
||||
</p>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="mt-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="my-4 flex items-center gap-3">
|
||||
<NativeSelect
|
||||
value={selectedRegion}
|
||||
onChange={(e) => setSelectedRegion(e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
<NativeSelectOption value="china-mainland">{t('minimax.chinaMainland')}</NativeSelectOption>
|
||||
<NativeSelectOption value="international">{t('minimax.international')}</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</div>
|
||||
<h2 className="text-card-foreground text-sm font-medium flex items-center gap-2">
|
||||
{t('minimax.step1')}
|
||||
</h2>
|
||||
<div className="space-y-2 bg-secondary p-3 rounded-lg m-2">
|
||||
<Button onClick={_ => {
|
||||
const url = selectedRegion === "international"
|
||||
? "https://platform.minimax.io/user-center/basic-information/interface-key"
|
||||
: "https://platform.minimaxi.com/user-center/basic-information/interface-key"
|
||||
openUrl(url)
|
||||
}} size="sm" variant="outline" className="text-sm">
|
||||
<ExternalLinkIcon />
|
||||
{t('minimax.enterConsole')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>{props.trigger}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{selectedRegion === "international"
|
||||
? t("minimax.configInternational")
|
||||
: t("minimax.configMainland")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div className="flex items-center gap-2">
|
||||
<p>
|
||||
{t("minimax.description", {
|
||||
provider:
|
||||
selectedRegion === "international"
|
||||
? "MiniMax"
|
||||
: t("minimax.minimax"),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="mt-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="my-4 flex items-center gap-3">
|
||||
<NativeSelect
|
||||
value={selectedRegion}
|
||||
onChange={(e) => setSelectedRegion(e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
<NativeSelectOption value="china-mainland">
|
||||
{t("minimax.chinaMainland")}
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="international">
|
||||
{t("minimax.international")}
|
||||
</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</div>
|
||||
<h2 className="text-card-foreground text-sm font-medium flex items-center gap-2">
|
||||
{t("minimax.step1")}
|
||||
</h2>
|
||||
<div className="space-y-2 bg-secondary p-3 rounded-lg m-2">
|
||||
<Button
|
||||
onClick={(_) => {
|
||||
const url =
|
||||
selectedRegion === "international"
|
||||
? "https://platform.minimax.io/user-center/basic-information/interface-key"
|
||||
: "https://platform.minimaxi.com/user-center/basic-information/interface-key";
|
||||
openUrl(url);
|
||||
}}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-sm"
|
||||
>
|
||||
<ExternalLinkIcon />
|
||||
{t("minimax.enterConsole")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-card-foreground text-sm font-medium flex items-center gap-2">
|
||||
{t('minimax.step2')}
|
||||
</h2>
|
||||
<div className="space-y-2 bg-secondary p-3 rounded-lg m-2">
|
||||
<Input
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder={t('minimax.apiKeyPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-card-foreground text-sm font-medium flex items-center gap-2">
|
||||
{t("minimax.step2")}
|
||||
</h2>
|
||||
<div className="space-y-2 bg-secondary p-3 rounded-lg m-2">
|
||||
<Input
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder={t("minimax.apiKeyPlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mx-2 mt-2">
|
||||
<Button
|
||||
onClick={handleCreateConfig}
|
||||
disabled={!apiKey.trim() || createConfigMutation.isPending}
|
||||
>
|
||||
{createConfigMutation.isPending ? t('minimax.creating') : t('minimax.createConfig')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
<div className="flex justify-end mx-2 mt-2">
|
||||
<Button
|
||||
onClick={handleCreateConfig}
|
||||
disabled={!apiKey.trim() || createConfigMutation.isPending}
|
||||
>
|
||||
{createConfigMutation.isPending
|
||||
? t("minimax.creating")
|
||||
: t("minimax.createConfig")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,36 +1,42 @@
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
interface QueryErrorFallbackProps {
|
||||
error: Error;
|
||||
resetErrorBoundary?: () => void;
|
||||
error: Error;
|
||||
resetErrorBoundary?: () => void;
|
||||
}
|
||||
|
||||
export function QueryErrorFallback({ error, resetErrorBoundary }: QueryErrorFallbackProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen p-4">
|
||||
<div className="max-w-md w-full space-y-4">
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Failed to load data</AlertTitle>
|
||||
<AlertDescription className="select-text">
|
||||
{error.message || "An unexpected error occurred while fetching data."}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
export function QueryErrorFallback({
|
||||
error,
|
||||
resetErrorBoundary,
|
||||
}: QueryErrorFallbackProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen p-4">
|
||||
<div className="max-w-md w-full space-y-4">
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Failed to load data</AlertTitle>
|
||||
<AlertDescription className="select-text">
|
||||
{error.message ||
|
||||
"An unexpected error occurred while fetching data."}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{resetErrorBoundary && (
|
||||
<div className="flex gap-3">
|
||||
<Button onClick={resetErrorBoundary} variant="default">
|
||||
Try Again
|
||||
</Button>
|
||||
<Button onClick={() => window.location.href = "/"} variant="outline">
|
||||
Go to Home
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
{resetErrorBoundary && (
|
||||
<div className="flex gap-3">
|
||||
<Button onClick={resetErrorBoundary} variant="default">
|
||||
Try Again
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => (window.location.href = "/")}
|
||||
variant="outline"
|
||||
>
|
||||
Go to Home
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
import { Suspense, ReactNode } from "react";
|
||||
import { QueryErrorResetBoundary } from "@tanstack/react-query";
|
||||
import { type ReactNode, Suspense } from "react";
|
||||
import { ErrorBoundary, type FallbackProps } from "react-error-boundary";
|
||||
import { QueryErrorFallback } from "./QueryErrorFallback";
|
||||
|
||||
function LoadingFallback() {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-muted-foreground">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-muted-foreground">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface RouteWrapperProps {
|
||||
children: ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function RouteWrapper({ children }: RouteWrapperProps) {
|
||||
return (
|
||||
<QueryErrorResetBoundary>
|
||||
{({ reset }) => (
|
||||
<ErrorBoundary
|
||||
onReset={reset}
|
||||
fallbackRender={({ error, resetErrorBoundary }: FallbackProps) => (
|
||||
<QueryErrorFallback error={error} resetErrorBoundary={resetErrorBoundary} />
|
||||
)}
|
||||
>
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
{children}
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
</QueryErrorResetBoundary>
|
||||
);
|
||||
return (
|
||||
<QueryErrorResetBoundary>
|
||||
{({ reset }) => (
|
||||
<ErrorBoundary
|
||||
onReset={reset}
|
||||
fallbackRender={({ error, resetErrorBoundary }: FallbackProps) => (
|
||||
<QueryErrorFallback
|
||||
error={error}
|
||||
resetErrorBoundary={resetErrorBoundary}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<Suspense fallback={<LoadingFallback />}>{children}</Suspense>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
</QueryErrorResetBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+380
-314
@@ -1,356 +1,422 @@
|
||||
import { AreaChart } from "@/components/ui/area-chart";
|
||||
import { format, startOfDay, startOfWeek, startOfMonth, subHours, subDays } from "date-fns";
|
||||
import { ProjectUsageRecord } from "@/lib/query";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import { formatLargeNumber } from "@/lib/utils";
|
||||
import {
|
||||
format,
|
||||
startOfDay,
|
||||
startOfMonth,
|
||||
startOfWeek,
|
||||
subDays,
|
||||
subHours,
|
||||
} from "date-fns";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AreaChart } from "@/components/ui/area-chart";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import type { ProjectUsageRecord } from "@/lib/query";
|
||||
import { formatLargeNumber } from "@/lib/utils";
|
||||
|
||||
interface TokenUsageChartProps {
|
||||
data: ProjectUsageRecord[];
|
||||
onFilteredDataChange?: (filteredData: ProjectUsageRecord[]) => void;
|
||||
data: ProjectUsageRecord[];
|
||||
onFilteredDataChange?: (filteredData: ProjectUsageRecord[]) => void;
|
||||
}
|
||||
|
||||
type TimeRange = "5h" | "today" | "7d" | "week" | "month" | "all";
|
||||
|
||||
export function TokenUsageChart({ data, onFilteredDataChange }: TokenUsageChartProps) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedModel, setSelectedModel] = useState<string>("all");
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>("5h");
|
||||
const [activeCategories, setActiveCategories] = useState<string[]>(["Input Tokens", "Output Tokens"]);
|
||||
export function TokenUsageChart({
|
||||
data,
|
||||
onFilteredDataChange,
|
||||
}: TokenUsageChartProps) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedModel, setSelectedModel] = useState<string>("all");
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>("5h");
|
||||
const [activeCategories, setActiveCategories] = useState<string[]>([
|
||||
"Input Tokens",
|
||||
"Output Tokens",
|
||||
]);
|
||||
|
||||
// Get unique models from data
|
||||
const availableModels = useMemo(() => {
|
||||
const models = new Set<string>();
|
||||
data.forEach((record) => {
|
||||
if (record.model) {
|
||||
models.add(record.model);
|
||||
}
|
||||
});
|
||||
return Array.from(models).sort();
|
||||
}, [data]);
|
||||
// Get unique models from data
|
||||
const availableModels = useMemo(() => {
|
||||
const models = new Set<string>();
|
||||
data.forEach((record) => {
|
||||
if (record.model) {
|
||||
models.add(record.model);
|
||||
}
|
||||
});
|
||||
return Array.from(models).sort();
|
||||
}, [data]);
|
||||
|
||||
// Filter data based on selected model and time range
|
||||
const filteredData = useMemo(() => {
|
||||
let filtered = data;
|
||||
// Filter data based on selected model and time range
|
||||
const filteredData = useMemo(() => {
|
||||
let filtered = data;
|
||||
|
||||
// Filter by model
|
||||
if (selectedModel !== "all") {
|
||||
filtered = filtered.filter((record) => record.model === selectedModel);
|
||||
}
|
||||
// Filter by model
|
||||
if (selectedModel !== "all") {
|
||||
filtered = filtered.filter((record) => record.model === selectedModel);
|
||||
}
|
||||
|
||||
// Filter by time range
|
||||
const now = new Date();
|
||||
let startTime: Date;
|
||||
// Filter by time range
|
||||
const now = new Date();
|
||||
let startTime: Date;
|
||||
|
||||
switch (timeRange) {
|
||||
case "5h":
|
||||
startTime = subHours(now, 5);
|
||||
break;
|
||||
case "today":
|
||||
startTime = startOfDay(now);
|
||||
break;
|
||||
case "7d":
|
||||
startTime = subDays(now, 6);
|
||||
break;
|
||||
case "week":
|
||||
startTime = startOfWeek(now);
|
||||
break;
|
||||
case "month":
|
||||
startTime = startOfMonth(now);
|
||||
break;
|
||||
case "all":
|
||||
// For "all time", find the earliest timestamp in the data
|
||||
if (filtered.length > 0) {
|
||||
const earliestTime = new Date(
|
||||
Math.min(...filtered.map((record) => new Date(record.timestamp).getTime()))
|
||||
);
|
||||
startTime = earliestTime;
|
||||
} else {
|
||||
startTime = new Date(0);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
startTime = subHours(now, 5);
|
||||
}
|
||||
switch (timeRange) {
|
||||
case "5h":
|
||||
startTime = subHours(now, 5);
|
||||
break;
|
||||
case "today":
|
||||
startTime = startOfDay(now);
|
||||
break;
|
||||
case "7d":
|
||||
startTime = subDays(now, 6);
|
||||
break;
|
||||
case "week":
|
||||
startTime = startOfWeek(now);
|
||||
break;
|
||||
case "month":
|
||||
startTime = startOfMonth(now);
|
||||
break;
|
||||
case "all":
|
||||
// For "all time", find the earliest timestamp in the data
|
||||
if (filtered.length > 0) {
|
||||
const earliestTime = new Date(
|
||||
Math.min(
|
||||
...filtered.map((record) => new Date(record.timestamp).getTime()),
|
||||
),
|
||||
);
|
||||
startTime = earliestTime;
|
||||
} else {
|
||||
startTime = new Date(0);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
startTime = subHours(now, 5);
|
||||
}
|
||||
|
||||
filtered = filtered.filter((record) => new Date(record.timestamp) >= startTime);
|
||||
filtered = filtered.filter(
|
||||
(record) => new Date(record.timestamp) >= startTime,
|
||||
);
|
||||
|
||||
return filtered;
|
||||
}, [data, selectedModel, timeRange]);
|
||||
return filtered;
|
||||
}, [data, selectedModel, timeRange]);
|
||||
|
||||
// Notify parent component when filtered data changes
|
||||
useEffect(() => {
|
||||
if (onFilteredDataChange) {
|
||||
onFilteredDataChange(filteredData);
|
||||
}
|
||||
}, [filteredData, onFilteredDataChange]);
|
||||
// Notify parent component when filtered data changes
|
||||
useEffect(() => {
|
||||
if (onFilteredDataChange) {
|
||||
onFilteredDataChange(filteredData);
|
||||
}
|
||||
}, [filteredData, onFilteredDataChange]);
|
||||
|
||||
// Handle category toggling
|
||||
const handleCategoryToggle = (value: any) => {
|
||||
if (value && value.categoryClicked) {
|
||||
setActiveCategories(prev => {
|
||||
if (prev.includes(value.categoryClicked)) {
|
||||
return prev.filter(cat => cat !== value.categoryClicked);
|
||||
} else {
|
||||
return [...prev, value.categoryClicked];
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
// Handle category toggling
|
||||
const handleCategoryToggle = (value: any) => {
|
||||
if (value && value.categoryClicked) {
|
||||
setActiveCategories((prev) => {
|
||||
if (prev.includes(value.categoryClicked)) {
|
||||
return prev.filter((cat) => cat !== value.categoryClicked);
|
||||
} else {
|
||||
return [...prev, value.categoryClicked];
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle category function for direct use
|
||||
const toggleCategory = (category: string) => {
|
||||
handleCategoryToggle({ categoryClicked: category });
|
||||
};
|
||||
// Group data based on time range
|
||||
const groupDataByInterval = (records: ProjectUsageRecord[]) => {
|
||||
const intervals: { [key: string]: { input: number; output: number; cache: number } } = {};
|
||||
const now = new Date();
|
||||
// Toggle category function for direct use
|
||||
const toggleCategory = (category: string) => {
|
||||
handleCategoryToggle({ categoryClicked: category });
|
||||
};
|
||||
// Group data based on time range
|
||||
const groupDataByInterval = (records: ProjectUsageRecord[]) => {
|
||||
const intervals: {
|
||||
[key: string]: { input: number; output: number; cache: number };
|
||||
} = {};
|
||||
const now = new Date();
|
||||
|
||||
if (timeRange === "all") {
|
||||
// For all time, group by week
|
||||
const earliestTime = records.length > 0
|
||||
? new Date(Math.min(...records.map((record) => new Date(record.timestamp).getTime())))
|
||||
: new Date();
|
||||
if (timeRange === "all") {
|
||||
// For all time, group by week
|
||||
const earliestTime =
|
||||
records.length > 0
|
||||
? new Date(
|
||||
Math.min(
|
||||
...records.map((record) =>
|
||||
new Date(record.timestamp).getTime(),
|
||||
),
|
||||
),
|
||||
)
|
||||
: new Date();
|
||||
|
||||
// Get start of the week for earliest time
|
||||
let currentWeekStart = startOfWeek(earliestTime);
|
||||
const nowWeekStart = startOfWeek(now);
|
||||
// Get start of the week for earliest time
|
||||
let currentWeekStart = startOfWeek(earliestTime);
|
||||
const nowWeekStart = startOfWeek(now);
|
||||
|
||||
// Generate weekly intervals from earliest week to current week
|
||||
while (currentWeekStart <= nowWeekStart) {
|
||||
intervals[currentWeekStart.getTime()] = { input: 0, output: 0, cache: 0 };
|
||||
// Move to next week (7 days)
|
||||
currentWeekStart = new Date(currentWeekStart.getTime() + 7 * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
// Generate weekly intervals from earliest week to current week
|
||||
while (currentWeekStart <= nowWeekStart) {
|
||||
intervals[currentWeekStart.getTime()] = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cache: 0,
|
||||
};
|
||||
// Move to next week (7 days)
|
||||
currentWeekStart = new Date(
|
||||
currentWeekStart.getTime() + 7 * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
// Group records into weekly intervals
|
||||
records.forEach((record) => {
|
||||
const recordTime = new Date(record.timestamp);
|
||||
const weekStart = startOfWeek(recordTime);
|
||||
const weekKey = weekStart.getTime();
|
||||
// Group records into weekly intervals
|
||||
records.forEach((record) => {
|
||||
const recordTime = new Date(record.timestamp);
|
||||
const weekStart = startOfWeek(recordTime);
|
||||
const weekKey = weekStart.getTime();
|
||||
|
||||
if (intervals[weekKey]) {
|
||||
intervals[weekKey].input += record.usage?.input_tokens || 0;
|
||||
intervals[weekKey].output += record.usage?.output_tokens || 0;
|
||||
intervals[weekKey].cache += record.usage?.cache_read_input_tokens || 0;
|
||||
}
|
||||
});
|
||||
} else if (timeRange === "5h") {
|
||||
// Group by 30-minute intervals for 5h time range
|
||||
const intervalMs = 30 * 60 * 1000; // 30 minutes in milliseconds
|
||||
if (intervals[weekKey]) {
|
||||
intervals[weekKey].input += record.usage?.input_tokens || 0;
|
||||
intervals[weekKey].output += record.usage?.output_tokens || 0;
|
||||
intervals[weekKey].cache +=
|
||||
record.usage?.cache_read_input_tokens || 0;
|
||||
}
|
||||
});
|
||||
} else if (timeRange === "5h") {
|
||||
// Group by 30-minute intervals for 5h time range
|
||||
const intervalMs = 30 * 60 * 1000; // 30 minutes in milliseconds
|
||||
|
||||
// Round current time down to nearest 30-minute boundary (epoch-based)
|
||||
const currentIntervalKey = Math.floor(now.getTime() / intervalMs) * intervalMs;
|
||||
// Round current time down to nearest 30-minute boundary (epoch-based)
|
||||
const currentIntervalKey =
|
||||
Math.floor(now.getTime() / intervalMs) * intervalMs;
|
||||
|
||||
// Generate intervals (10 intervals for 5 hours)
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const intervalKey = currentIntervalKey - i * intervalMs;
|
||||
intervals[intervalKey] = { input: 0, output: 0, cache: 0 };
|
||||
}
|
||||
// Generate intervals (10 intervals for 5 hours)
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const intervalKey = currentIntervalKey - i * intervalMs;
|
||||
intervals[intervalKey] = { input: 0, output: 0, cache: 0 };
|
||||
}
|
||||
|
||||
// Group records into 30-minute intervals
|
||||
records.forEach((record) => {
|
||||
const recordTime = new Date(record.timestamp);
|
||||
const recordIntervalKey = Math.floor(recordTime.getTime() / intervalMs) * intervalMs;
|
||||
// Group records into 30-minute intervals
|
||||
records.forEach((record) => {
|
||||
const recordTime = new Date(record.timestamp);
|
||||
const recordIntervalKey =
|
||||
Math.floor(recordTime.getTime() / intervalMs) * intervalMs;
|
||||
|
||||
if (intervals[recordIntervalKey]) {
|
||||
intervals[recordIntervalKey].input += record.usage?.input_tokens || 0;
|
||||
intervals[recordIntervalKey].output += record.usage?.output_tokens || 0;
|
||||
intervals[recordIntervalKey].cache += record.usage?.cache_read_input_tokens || 0;
|
||||
}
|
||||
});
|
||||
} else if (timeRange === "today") {
|
||||
// Group by hour for today
|
||||
const startOfToday = startOfDay(now);
|
||||
const currentHour = now.getHours();
|
||||
if (intervals[recordIntervalKey]) {
|
||||
intervals[recordIntervalKey].input += record.usage?.input_tokens || 0;
|
||||
intervals[recordIntervalKey].output +=
|
||||
record.usage?.output_tokens || 0;
|
||||
intervals[recordIntervalKey].cache +=
|
||||
record.usage?.cache_read_input_tokens || 0;
|
||||
}
|
||||
});
|
||||
} else if (timeRange === "today") {
|
||||
// Group by hour for today
|
||||
const startOfToday = startOfDay(now);
|
||||
const currentHour = now.getHours();
|
||||
|
||||
for (let i = 0; i <= currentHour; i++) {
|
||||
const hourTime = new Date(startOfToday.getTime() + i * 60 * 60 * 1000);
|
||||
intervals[hourTime.getTime()] = { input: 0, output: 0, cache: 0 };
|
||||
}
|
||||
for (let i = 0; i <= currentHour; i++) {
|
||||
const hourTime = new Date(startOfToday.getTime() + i * 60 * 60 * 1000);
|
||||
intervals[hourTime.getTime()] = { input: 0, output: 0, cache: 0 };
|
||||
}
|
||||
|
||||
records.forEach((record) => {
|
||||
const recordTime = new Date(record.timestamp);
|
||||
const hourStart = new Date(recordTime);
|
||||
hourStart.setMinutes(0, 0, 0);
|
||||
const hourKey = hourStart.getTime();
|
||||
records.forEach((record) => {
|
||||
const recordTime = new Date(record.timestamp);
|
||||
const hourStart = new Date(recordTime);
|
||||
hourStart.setMinutes(0, 0, 0);
|
||||
const hourKey = hourStart.getTime();
|
||||
|
||||
if (intervals[hourKey]) {
|
||||
intervals[hourKey].input += record.usage?.input_tokens || 0;
|
||||
intervals[hourKey].output += record.usage?.output_tokens || 0;
|
||||
intervals[hourKey].cache += record.usage?.cache_read_input_tokens || 0;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Group by day for longer periods (7d, week, month)
|
||||
let startDate: Date;
|
||||
let days: number;
|
||||
if (intervals[hourKey]) {
|
||||
intervals[hourKey].input += record.usage?.input_tokens || 0;
|
||||
intervals[hourKey].output += record.usage?.output_tokens || 0;
|
||||
intervals[hourKey].cache +=
|
||||
record.usage?.cache_read_input_tokens || 0;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Group by day for longer periods (7d, week, month)
|
||||
let startDate: Date;
|
||||
let days: number;
|
||||
|
||||
if (timeRange === "week") {
|
||||
startDate = startOfWeek(now);
|
||||
// Calculate actual days in the current week so far (from start of week to today)
|
||||
const todayStart = startOfDay(now);
|
||||
days = Math.floor((todayStart.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000)) + 1;
|
||||
} else if (timeRange === "month") {
|
||||
startDate = startOfMonth(now);
|
||||
// Calculate actual days in the current month so far (from start of month to today)
|
||||
const todayStart = startOfDay(now);
|
||||
days = Math.floor((todayStart.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000)) + 1;
|
||||
} else {
|
||||
// For 7d, start from (days-1) days ago to include today
|
||||
days = 7;
|
||||
startDate = startOfDay(subDays(now, days - 1));
|
||||
}
|
||||
if (timeRange === "week") {
|
||||
startDate = startOfWeek(now);
|
||||
// Calculate actual days in the current week so far (from start of week to today)
|
||||
const todayStart = startOfDay(now);
|
||||
days =
|
||||
Math.floor(
|
||||
(todayStart.getTime() - startDate.getTime()) /
|
||||
(24 * 60 * 60 * 1000),
|
||||
) + 1;
|
||||
} else if (timeRange === "month") {
|
||||
startDate = startOfMonth(now);
|
||||
// Calculate actual days in the current month so far (from start of month to today)
|
||||
const todayStart = startOfDay(now);
|
||||
days =
|
||||
Math.floor(
|
||||
(todayStart.getTime() - startDate.getTime()) /
|
||||
(24 * 60 * 60 * 1000),
|
||||
) + 1;
|
||||
} else {
|
||||
// For 7d, start from (days-1) days ago to include today
|
||||
days = 7;
|
||||
startDate = startOfDay(subDays(now, days - 1));
|
||||
}
|
||||
|
||||
for (let i = 0; i < days; i++) {
|
||||
const dayTime = startOfDay(new Date(startDate.getTime() + i * 24 * 60 * 60 * 1000));
|
||||
intervals[dayTime.getTime()] = { input: 0, output: 0, cache: 0 };
|
||||
}
|
||||
for (let i = 0; i < days; i++) {
|
||||
const dayTime = startOfDay(
|
||||
new Date(startDate.getTime() + i * 24 * 60 * 60 * 1000),
|
||||
);
|
||||
intervals[dayTime.getTime()] = { input: 0, output: 0, cache: 0 };
|
||||
}
|
||||
|
||||
records.forEach((record) => {
|
||||
const recordTime = new Date(record.timestamp);
|
||||
const dayStart = new Date(recordTime);
|
||||
dayStart.setHours(0, 0, 0, 0);
|
||||
const dayKey = dayStart.getTime();
|
||||
records.forEach((record) => {
|
||||
const recordTime = new Date(record.timestamp);
|
||||
const dayStart = new Date(recordTime);
|
||||
dayStart.setHours(0, 0, 0, 0);
|
||||
const dayKey = dayStart.getTime();
|
||||
|
||||
if (intervals[dayKey]) {
|
||||
intervals[dayKey].input += record.usage?.input_tokens || 0;
|
||||
intervals[dayKey].output += record.usage?.output_tokens || 0;
|
||||
intervals[dayKey].cache += record.usage?.cache_read_input_tokens || 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (intervals[dayKey]) {
|
||||
intervals[dayKey].input += record.usage?.input_tokens || 0;
|
||||
intervals[dayKey].output += record.usage?.output_tokens || 0;
|
||||
intervals[dayKey].cache += record.usage?.cache_read_input_tokens || 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return intervals;
|
||||
};
|
||||
return intervals;
|
||||
};
|
||||
|
||||
const groupedData = groupDataByInterval(filteredData);
|
||||
const groupedData = groupDataByInterval(filteredData);
|
||||
|
||||
// Prepare chart data for Recharts
|
||||
const chartData = Object.keys(groupedData)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b)
|
||||
.map((timestamp) => {
|
||||
const date = new Date(timestamp);
|
||||
let label: string;
|
||||
if (timeRange === "all") {
|
||||
label = format(date, "MMM dd, yyyy");
|
||||
} else if (timeRange === "today") {
|
||||
label = format(date, "HH:mm");
|
||||
} else if (timeRange === "5h") {
|
||||
label = format(date, "HH:mm");
|
||||
} else {
|
||||
label = format(date, "MMM dd");
|
||||
}
|
||||
// Prepare chart data for Recharts
|
||||
const chartData = Object.keys(groupedData)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b)
|
||||
.map((timestamp) => {
|
||||
const date = new Date(timestamp);
|
||||
let label: string;
|
||||
if (timeRange === "all") {
|
||||
label = format(date, "MMM dd, yyyy");
|
||||
} else if (timeRange === "today") {
|
||||
label = format(date, "HH:mm");
|
||||
} else if (timeRange === "5h") {
|
||||
label = format(date, "HH:mm");
|
||||
} else {
|
||||
label = format(date, "MMM dd");
|
||||
}
|
||||
|
||||
return {
|
||||
time: label,
|
||||
timestamp,
|
||||
"Input Tokens": groupedData[timestamp].input,
|
||||
"Output Tokens": groupedData[timestamp].output,
|
||||
"Cache Read Tokens": groupedData[timestamp].cache,
|
||||
};
|
||||
});
|
||||
return {
|
||||
time: label,
|
||||
timestamp,
|
||||
"Input Tokens": groupedData[timestamp].input,
|
||||
"Output Tokens": groupedData[timestamp].output,
|
||||
"Cache Read Tokens": groupedData[timestamp].cache,
|
||||
};
|
||||
});
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<div className="h-96 flex items-center justify-center border rounded-lg bg-muted/20">
|
||||
<p className="text-muted-foreground">{t("usageChart.noData")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<div className="h-96 flex items-center justify-center border rounded-lg bg-muted/20">
|
||||
<p className="text-muted-foreground">{t("usageChart.noData")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 w-full min-w-0">
|
||||
{/* Filter Controls */}
|
||||
<div className="flex gap-4 items-center flex-wrap pb-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor="model-filter" className="text-sm font-medium">
|
||||
{t("usageChart.modelFilter")}
|
||||
</label>
|
||||
<Select value={selectedModel} onValueChange={setSelectedModel}>
|
||||
<SelectTrigger id="model-filter" className="w-48">
|
||||
<SelectValue placeholder={t("usageChart.allModels")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{t("usageChart.allModels")}</SelectItem>
|
||||
{availableModels.map((model) => (
|
||||
<SelectItem key={model} value={model}>
|
||||
{model}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
return (
|
||||
<div className="space-y-4 w-full min-w-0">
|
||||
{/* Filter Controls */}
|
||||
<div className="flex gap-4 items-center flex-wrap pb-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor="model-filter" className="text-sm font-medium">
|
||||
{t("usageChart.modelFilter")}
|
||||
</label>
|
||||
<Select value={selectedModel} onValueChange={setSelectedModel}>
|
||||
<SelectTrigger id="model-filter" className="w-48">
|
||||
<SelectValue placeholder={t("usageChart.allModels")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{t("usageChart.allModels")}</SelectItem>
|
||||
{availableModels.map((model) => (
|
||||
<SelectItem key={model} value={model}>
|
||||
{model}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor="time-range" className="text-sm font-medium">
|
||||
{t("usageChart.timeRange")}
|
||||
</label>
|
||||
<Select value={timeRange} onValueChange={(value: TimeRange) => setTimeRange(value)}>
|
||||
<SelectTrigger id="time-range" className="w-48">
|
||||
<SelectValue placeholder={t("usageChart.selectTimeRange")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="5h">{t("usageChart.last5Hours")}</SelectItem>
|
||||
<SelectItem value="today">{t("usageChart.startOfToday")}</SelectItem>
|
||||
<SelectItem value="7d">{t("usageChart.last7Days")}</SelectItem>
|
||||
<SelectItem value="week">{t("usageChart.startOfWeek")}</SelectItem>
|
||||
<SelectItem value="month">{t("usageChart.startOfMonth")}</SelectItem>
|
||||
<SelectItem value="all">{t("usageChart.allTime")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor="time-range" className="text-sm font-medium">
|
||||
{t("usageChart.timeRange")}
|
||||
</label>
|
||||
<Select
|
||||
value={timeRange}
|
||||
onValueChange={(value: TimeRange) => setTimeRange(value)}
|
||||
>
|
||||
<SelectTrigger id="time-range" className="w-48">
|
||||
<SelectValue placeholder={t("usageChart.selectTimeRange")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="5h">{t("usageChart.last5Hours")}</SelectItem>
|
||||
<SelectItem value="today">
|
||||
{t("usageChart.startOfToday")}
|
||||
</SelectItem>
|
||||
<SelectItem value="7d">{t("usageChart.last7Days")}</SelectItem>
|
||||
<SelectItem value="week">
|
||||
{t("usageChart.startOfWeek")}
|
||||
</SelectItem>
|
||||
<SelectItem value="month">
|
||||
{t("usageChart.startOfMonth")}
|
||||
</SelectItem>
|
||||
<SelectItem value="all">{t("usageChart.allTime")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom Legend */}
|
||||
<div className="flex gap-6 items-center justify-center pb-4">
|
||||
{[
|
||||
{ key: "Input Tokens", label: t("usage.inputTokens") },
|
||||
{ key: "Output Tokens", label: t("usage.outputTokens") },
|
||||
{ key: "Cache Read Tokens", label: t("usage.cacheReadTokens") }
|
||||
].map(({ key, label }) => {
|
||||
const isActive = activeCategories.includes(key);
|
||||
const color = key === "Input Tokens" ? "bg-blue-500" :
|
||||
key === "Output Tokens" ? "bg-emerald-500" :
|
||||
"bg-amber-500";
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => toggleCategory(key)}
|
||||
className={`flex items-center gap-2 px-3 py-1 rounded-md text-sm transition-all ${
|
||||
isActive
|
||||
? 'opacity-100 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||
: 'opacity-40 hover:opacity-60'
|
||||
}`}
|
||||
>
|
||||
<span className={`w-3 h-3 rounded-full ${color}`} />
|
||||
<span className="text-gray-700 dark:text-gray-300">{label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* Custom Legend */}
|
||||
<div className="flex gap-6 items-center justify-center pb-4">
|
||||
{[
|
||||
{ key: "Input Tokens", label: t("usage.inputTokens") },
|
||||
{ key: "Output Tokens", label: t("usage.outputTokens") },
|
||||
{ key: "Cache Read Tokens", label: t("usage.cacheReadTokens") },
|
||||
].map(({ key, label }) => {
|
||||
const isActive = activeCategories.includes(key);
|
||||
const color =
|
||||
key === "Input Tokens"
|
||||
? "bg-blue-500"
|
||||
: key === "Output Tokens"
|
||||
? "bg-emerald-500"
|
||||
: "bg-amber-500";
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => toggleCategory(key)}
|
||||
className={`flex items-center gap-2 px-3 py-1 rounded-md text-sm transition-all ${
|
||||
isActive
|
||||
? "opacity-100 hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
: "opacity-40 hover:opacity-60"
|
||||
}`}
|
||||
>
|
||||
<span className={`w-3 h-3 rounded-full ${color}`} />
|
||||
<span className="text-gray-700 dark:text-gray-300">{label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<div className="h-[320px] w-full min-w-0">
|
||||
<AreaChart
|
||||
data={chartData}
|
||||
index="time"
|
||||
categories={activeCategories}
|
||||
colors={activeCategories.map(cat => {
|
||||
if (cat === "Input Tokens") return "blue";
|
||||
if (cat === "Output Tokens") return "emerald";
|
||||
if (cat === "Cache Read Tokens") return "amber";
|
||||
return "blue";
|
||||
})}
|
||||
valueFormatter={formatLargeNumber}
|
||||
fill="gradient"
|
||||
className="h-full"
|
||||
showLegend={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
{/* Chart */}
|
||||
<div className="h-[320px] w-full min-w-0">
|
||||
<AreaChart
|
||||
data={chartData}
|
||||
index="time"
|
||||
categories={activeCategories}
|
||||
colors={activeCategories.map((cat) => {
|
||||
if (cat === "Input Tokens") return "blue";
|
||||
if (cat === "Output Tokens") return "emerald";
|
||||
if (cat === "Cache Read Tokens") return "amber";
|
||||
return "blue";
|
||||
})}
|
||||
valueFormatter={formatLargeNumber}
|
||||
fill="gradient"
|
||||
className="h-full"
|
||||
showLegend={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,46 +1,47 @@
|
||||
import { useCheckForUpdates, useInstallAndRestart } from "../lib/query";
|
||||
import { RotateCwIcon, RefreshCcwIcon } from "lucide-react";
|
||||
import { cn } from "../lib/utils";
|
||||
import { RefreshCcwIcon, RotateCwIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useCheckForUpdates, useInstallAndRestart } from "../lib/query";
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
export function UpdateButton() {
|
||||
const { t } = useTranslation();
|
||||
const { data: updateInfo, isLoading, error } = useCheckForUpdates();
|
||||
const installAndRestart = useInstallAndRestart();
|
||||
const { t } = useTranslation();
|
||||
const { data: updateInfo, isLoading, error } = useCheckForUpdates();
|
||||
const installAndRestart = useInstallAndRestart();
|
||||
|
||||
if (isLoading || error) {
|
||||
return null;
|
||||
}
|
||||
if (isLoading || error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!updateInfo || !updateInfo.available) {
|
||||
return null;
|
||||
}
|
||||
if (!updateInfo || !updateInfo.available) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-3 py-2">
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={() => installAndRestart.mutate()}
|
||||
disabled={installAndRestart.isPending}
|
||||
className={cn("flex items-center justify-center text-sm gap-2 bg-secondary hover:bg-secondary/80 rounded-md px-2 py-2 w-full",
|
||||
{
|
||||
"opacity-50": installAndRestart.isPending,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{installAndRestart.isPending ? (
|
||||
<>
|
||||
<RotateCwIcon className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t("updateButton.installing")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCcwIcon className="h-4 w-4" />
|
||||
{t("updateButton.newVersionAvailable")}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="px-3 py-2">
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={() => installAndRestart.mutate()}
|
||||
disabled={installAndRestart.isPending}
|
||||
className={cn(
|
||||
"flex items-center justify-center text-sm gap-2 bg-secondary hover:bg-secondary/80 rounded-md px-2 py-2 w-full",
|
||||
{
|
||||
"opacity-50": installAndRestart.isPending,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{installAndRestart.isPending ? (
|
||||
<>
|
||||
<RotateCwIcon className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t("updateButton.installing")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCcwIcon className="h-4 w-4" />
|
||||
{t("updateButton.newVersionAvailable")}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import type * as React from "react";
|
||||
|
||||
type ThemeProviderProps = React.ComponentProps<typeof NextThemesProvider>
|
||||
type ThemeProviderProps = React.ComponentProps<typeof NextThemesProvider>;
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AccordionItem.displayName = "AccordionItem";
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
|
||||
+108
-109
@@ -1,139 +1,138 @@
|
||||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||
import * as React from "react";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
const AlertDialog = AlertDialogPrimitive.Root;
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal;
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
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}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
<AlertDialogPrimitive.Overlay
|
||||
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}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.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}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.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}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
));
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
|
||||
|
||||
const AlertDialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader";
|
||||
|
||||
const AlertDialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter";
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogDescription.displayName =
|
||||
AlertDialogPrimitive.Description.displayName
|
||||
AlertDialogPrimitive.Description.displayName;
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action
|
||||
ref={ref}
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
<AlertDialogPrimitive.Action
|
||||
ref={ref}
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"mt-2 sm:mt-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"mt-2 sm:mt-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
};
|
||||
|
||||
+46
-46
@@ -1,59 +1,59 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Alert.displayName = "Alert"
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Alert.displayName = "Alert";
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertTitle.displayName = "AlertTitle"
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertTitle.displayName = "AlertTitle";
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDescription.displayName = "AlertDescription"
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDescription.displayName = "AlertDescription";
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
|
||||
+920
-916
File diff suppressed because it is too large
Load Diff
@@ -1,83 +1,82 @@
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonGroupVariants = cva(
|
||||
"flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
||||
{
|
||||
variants: {
|
||||
orientation: {
|
||||
horizontal:
|
||||
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
|
||||
vertical:
|
||||
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: "horizontal",
|
||||
},
|
||||
}
|
||||
)
|
||||
"flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
||||
{
|
||||
variants: {
|
||||
orientation: {
|
||||
horizontal:
|
||||
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
|
||||
vertical:
|
||||
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: "horizontal",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function ButtonGroup({
|
||||
className,
|
||||
orientation,
|
||||
...props
|
||||
className,
|
||||
orientation,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="button-group"
|
||||
data-orientation={orientation}
|
||||
className={cn(buttonGroupVariants({ orientation }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="button-group"
|
||||
data-orientation={orientation}
|
||||
className={cn(buttonGroupVariants({ orientation }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ButtonGroupText({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
asChild?: boolean
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "div"
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
className={cn(
|
||||
"bg-muted shadow-xs flex items-center gap-2 rounded-md border px-4 text-sm font-medium [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<Comp
|
||||
className={cn(
|
||||
"bg-muted shadow-xs flex items-center gap-2 rounded-md border px-4 text-sm font-medium [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ButtonGroupSeparator({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Separator>) {
|
||||
return (
|
||||
<Separator
|
||||
data-slot="button-group-separator"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<Separator
|
||||
data-slot="button-group-separator"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
ButtonGroup,
|
||||
ButtonGroupSeparator,
|
||||
ButtonGroupText,
|
||||
buttonGroupVariants,
|
||||
}
|
||||
ButtonGroup,
|
||||
ButtonGroupSeparator,
|
||||
ButtonGroupText,
|
||||
buttonGroupVariants,
|
||||
};
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
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-8 rounded-md px-2 text-sm",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
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-8 rounded-md px-2 text-sm",
|
||||
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
|
||||
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"
|
||||
({ 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 }
|
||||
export { Button, buttonVariants };
|
||||
|
||||
+52
-37
@@ -2,69 +2,84 @@ import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
||||
<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>
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = "CardContent";
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = "CardFooter";
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
|
||||
+118
-119
@@ -1,153 +1,152 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import { type DialogProps } from "@radix-ui/react-dialog"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { Search } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||
import type { DialogProps } from "@radix-ui/react-dialog";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { Search } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
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
|
||||
<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;
|
||||
|
||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<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>
|
||||
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="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 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>
|
||||
))
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 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
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
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}
|
||||
/>
|
||||
))
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName
|
||||
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
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}
|
||||
/>
|
||||
))
|
||||
<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
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
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
|
||||
<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>
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
CommandShortcut.displayName = "CommandShortcut"
|
||||
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,
|
||||
}
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
};
|
||||
|
||||
@@ -1,124 +1,124 @@
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
data-tauri-drag-region
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/20 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
|
||||
<DialogPrimitive.Overlay
|
||||
data-tauri-drag-region
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/20 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>
|
||||
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 w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border-zinc-100 bg-background p-6 sm:rounded-md",
|
||||
className
|
||||
)}
|
||||
onPointerDownOutside={(event) => {
|
||||
event.preventDefault()
|
||||
}}
|
||||
{...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">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border-zinc-100 bg-background p-6 sm:rounded-md",
|
||||
className,
|
||||
)}
|
||||
onPointerDownOutside={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
{...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">
|
||||
<X 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
|
||||
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"
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
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"
|
||||
<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>
|
||||
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
|
||||
<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>
|
||||
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
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
};
|
||||
|
||||
+159
-159
@@ -1,198 +1,198 @@
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
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 gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
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 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
<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 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
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 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-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 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-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 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
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
|
||||
}
|
||||
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 gap-2 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
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>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
<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>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
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>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
<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>
|
||||
<Circle className="h-2 w-2 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
|
||||
}
|
||||
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
|
||||
<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>
|
||||
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
|
||||
<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
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
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,
|
||||
}
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
};
|
||||
|
||||
+18
-18
@@ -1,22 +1,22 @@
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input }
|
||||
export { Input };
|
||||
|
||||
+17
-17
@@ -1,24 +1,24 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
"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>
|
||||
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
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label }
|
||||
export { Label };
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
import * as React from "react"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function NativeSelect({ className, ...props }: React.ComponentProps<"select">) {
|
||||
return (
|
||||
<div
|
||||
className="group/native-select relative w-fit has-[select:disabled]:opacity-50"
|
||||
data-slot="native-select-wrapper"
|
||||
>
|
||||
<select
|
||||
data-slot="native-select"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 h-9 w-full min-w-0 appearance-none rounded-md border bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<ChevronDownIcon
|
||||
className="text-muted-foreground pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 opacity-50 select-none"
|
||||
aria-hidden="true"
|
||||
data-slot="native-select-icon"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
className="group/native-select relative w-fit has-[select:disabled]:opacity-50"
|
||||
data-slot="native-select-wrapper"
|
||||
>
|
||||
<select
|
||||
data-slot="native-select"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 h-9 w-full min-w-0 appearance-none rounded-md border bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<ChevronDownIcon
|
||||
className="text-muted-foreground pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 opacity-50 select-none"
|
||||
aria-hidden="true"
|
||||
data-slot="native-select-icon"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NativeSelectOption({ ...props }: React.ComponentProps<"option">) {
|
||||
return <option data-slot="native-select-option" {...props} />
|
||||
return <option data-slot="native-select-option" {...props} />;
|
||||
}
|
||||
|
||||
function NativeSelectOptGroup({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"optgroup">) {
|
||||
return (
|
||||
<optgroup
|
||||
data-slot="native-select-optgroup"
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<optgroup
|
||||
data-slot="native-select-optgroup"
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { NativeSelect, NativeSelectOptGroup, NativeSelectOption }
|
||||
export { NativeSelect, NativeSelectOptGroup, NativeSelectOption };
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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 origin-[--radix-popover-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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 origin-[--radix-popover-content-transform-origin]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent }
|
||||
export { Popover, PopoverTrigger, PopoverContent };
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
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
|
||||
<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>
|
||||
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
|
||||
<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 }
|
||||
export { ScrollArea, ScrollBar };
|
||||
|
||||
+129
-129
@@ -1,158 +1,158 @@
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-7 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-xs ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-7 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-xs ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
));
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName
|
||||
SelectPrimitive.ScrollDownButton.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover 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 origin-[--radix-select-content-transform-origin]",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover 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 origin-[--radix-select-content-transform-origin]",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none 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">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none 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">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
};
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
export { Separator };
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
export { Skeleton };
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { Toaster as Sonner } from "sonner"
|
||||
import { Toaster as Sonner } from "sonner";
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>;
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
return (
|
||||
<Sonner
|
||||
theme="light"
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Sonner
|
||||
theme="light"
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster }
|
||||
export { Toaster };
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
));
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||
|
||||
export { Switch }
|
||||
export { Switch };
|
||||
|
||||
+41
-41
@@ -1,53 +1,53 @@
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md 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>
|
||||
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-sm px-3 py-1.5 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-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 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-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
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
|
||||
<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 }
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Textarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
React.ComponentProps<"textarea">
|
||||
HTMLTextAreaElement,
|
||||
React.ComponentProps<"textarea">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Textarea.displayName = "Textarea"
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Textarea.displayName = "Textarea";
|
||||
|
||||
export { Textarea }
|
||||
export { Textarea };
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { setTheme } = useTheme()
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
浅色
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
深色
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
跟随系统
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
浅色
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
深色
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
跟随系统
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
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 border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md 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 origin-[--radix-tooltip-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md 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 origin-[--radix-tooltip-content-transform-origin]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
||||
+35
-35
@@ -1,41 +1,41 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import enTranslations from './locales/en.json';
|
||||
import zhTranslations from './locales/zh.json';
|
||||
import frTranslations from './locales/fr.json';
|
||||
import jaTranslations from './locales/ja.json';
|
||||
import i18n from "i18next";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import enTranslations from "./locales/en.json";
|
||||
import frTranslations from "./locales/fr.json";
|
||||
import jaTranslations from "./locales/ja.json";
|
||||
import zhTranslations from "./locales/zh.json";
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
translation: enTranslations
|
||||
},
|
||||
zh: {
|
||||
translation: zhTranslations
|
||||
},
|
||||
fr: {
|
||||
translation: frTranslations
|
||||
},
|
||||
ja: {
|
||||
translation: jaTranslations
|
||||
}
|
||||
en: {
|
||||
translation: enTranslations,
|
||||
},
|
||||
zh: {
|
||||
translation: zhTranslations,
|
||||
},
|
||||
fr: {
|
||||
translation: frTranslations,
|
||||
},
|
||||
ja: {
|
||||
translation: jaTranslations,
|
||||
},
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: 'en',
|
||||
debug: import.meta.env.DEV,
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
},
|
||||
detection: {
|
||||
order: ['localStorage', 'navigator', 'htmlTag'],
|
||||
caches: ['localStorage'],
|
||||
lookupLocalStorage: 'i18nextLng',
|
||||
}
|
||||
});
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: "en",
|
||||
debug: import.meta.env.DEV,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
detection: {
|
||||
order: ["localStorage", "navigator", "htmlTag"],
|
||||
caches: ["localStorage"],
|
||||
lookupLocalStorage: "i18nextLng",
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
export default i18n;
|
||||
|
||||
+215
-215
@@ -1,216 +1,216 @@
|
||||
{
|
||||
"app.title": "CC Mate",
|
||||
"navigation.configurations": "Config files",
|
||||
"navigation.projects": "Projects",
|
||||
"navigation.mcp": "MCP",
|
||||
"navigation.usage": "Usage",
|
||||
"navigation.settings": "Settings",
|
||||
"config.type.user": "User Configuration",
|
||||
"config.type.enterprise": "Enterprise Configuration",
|
||||
"config.type.mcp": "MCP Configuration",
|
||||
"config.save": "Save Configuration",
|
||||
"config.backup": "Backup Configuration",
|
||||
"config.restore": "Restore Configuration",
|
||||
"error.validation": "Invalid JSON format",
|
||||
"error.save": "Failed to save configuration",
|
||||
"success.save": "Configuration saved successfully",
|
||||
"loading": "Loading...",
|
||||
"settings.title": "Settings",
|
||||
"settings.language": "Language",
|
||||
"settings.theme": "Theme",
|
||||
"settings.theme.system": "Follow System",
|
||||
"settings.theme.light": "Light",
|
||||
"settings.theme.dark": "Dark",
|
||||
"settings.version": "Version",
|
||||
"settings.checkingUpdate": "Checking for updates...",
|
||||
"settings.newVersionAvailable": "New version v{{version}} available",
|
||||
"settings.installing": "Installing...",
|
||||
"settings.installAndRestart": "Install and Restart",
|
||||
"settings.upToDate": "Up to date",
|
||||
"settings.contact": "Contact",
|
||||
"settings.reportIssue": "Report Issue",
|
||||
"configSwitcher.newConfig": "New Configuration",
|
||||
"configSwitcher.createConfig": "New Config",
|
||||
"configSwitcher.allConfigs": "All Configurations",
|
||||
"configSwitcher.title": "Configurations",
|
||||
"configSwitcher.description": "Create and switch between multiple Claude Code configurations",
|
||||
"configSwitcher.originalConfig": "Claude",
|
||||
"configSwitcher.originalConfigDescription": "Claude Code Original Configuration",
|
||||
"configEditor.save": "Save",
|
||||
"configEditor.configName": "Configuration Name",
|
||||
"configEditor.deleteConfirm": "Are you sure you want to delete configuration \"{{name}}\"? This action cannot be undone.",
|
||||
"configEditor.deleteTitle": "Delete Configuration",
|
||||
"configEditor.sections.common": "Common",
|
||||
"configEditor.sections.generalSettings": "General Settings",
|
||||
"configEditor.sections.authLogin": "Authentication & Login",
|
||||
"configEditor.sections.mcpConfig": "MCP Configuration",
|
||||
"configEditor.sections.awsConfig": "AWS Configuration",
|
||||
"configEditor.sections.environmentVars": "Environment Variables",
|
||||
"updateButton.installing": "Installing...",
|
||||
"updateButton.newVersionAvailable": "New version available",
|
||||
"toast.configSaved": "Configuration \"{{configType}}\" saved successfully",
|
||||
"toast.configSaveFailed": "Failed to save configuration: {{error}}",
|
||||
"toast.backupSuccess": "Claude configurations backed up successfully",
|
||||
"toast.backupFailed": "Failed to backup configurations: {{error}}",
|
||||
"toast.storeCreated": "Config created successfully",
|
||||
"toast.storeCreateFailed": "Failed to create config: {{error}}",
|
||||
"toast.storeDeleted": "Config deleted successfully",
|
||||
"toast.storeDeleteFailed": "Failed to delete config: {{error}}",
|
||||
"toast.storeActivated": "Config activated successfully",
|
||||
"toast.storeActivateFailed": "Failed to switch config: {{error}}",
|
||||
"toast.storeSaved": "Config \"{{title}}\" saved successfully",
|
||||
"toast.storeSavedAndActive": "Config \"{{title}}\" saved successfully. Config has applied, please restart the Claude Code session",
|
||||
"toast.storeSaveFailed": "Failed to save config: {{error}}",
|
||||
"toast.updateInstalled": "Update installed successfully. Restarting...",
|
||||
"toast.updateInstallFailed": "Failed to install update: {{error}}",
|
||||
"mcp.title": "MCP",
|
||||
"mcp.description": "Claude Code Global MCP Service Configuration",
|
||||
"mcp.addServer": "Add MCP Server",
|
||||
"mcp.addServerTitle": "Add MCP Server",
|
||||
"mcp.addServerDescription": "Add global MCP services available across projects",
|
||||
"mcp.noServersConfigured": "No MCP servers configured. Click \"Add MCP Server\" to add one.",
|
||||
"mcp.save": "Save",
|
||||
"mcp.saving": "Saving...",
|
||||
"mcp.recommend": "Recommended",
|
||||
"mcp.custom": "Custom",
|
||||
"mcp.source": "Source",
|
||||
"mcp.add": "Add",
|
||||
"mcp.invalidJsonError": "Invalid JSON configuration for {{serverName}}",
|
||||
"mcp.invalidJsonTitle": "Invalid JSON",
|
||||
"mcp.serverExistsError": "MCP server \"{{serverName}}\" already exists",
|
||||
"mcp.serverExistsTitle": "MCP Server Exists",
|
||||
"mcp.addServerConfirm": "Do you want to add the {{serverName}} MCP server?",
|
||||
"mcp.addCustomServerError": "Invalid JSON format. Please enter a valid JSON configuration.",
|
||||
"mcp.invalidConfigError": "Configuration must be a JSON object.",
|
||||
"mcp.noServersError": "Configuration must contain at least one MCP server.",
|
||||
"mcp.duplicateServersError": "MCP server(s) already exist: {{servers}}",
|
||||
"mcp.duplicateServersTitle": "Duplicate MCP Servers",
|
||||
"mcp.addCustomServersConfirm": "Do you want to add {{count}} MCP server(s)?",
|
||||
"mcp.addCustomServersTitle": "Add Custom MCP Servers",
|
||||
"mcp.addServerError": "Failed to add MCP server",
|
||||
"mcp.deleteServerConfirm": "Are you sure you want to delete the MCP server \"{{serverName}}\"? This action cannot be undone.",
|
||||
"mcp.deleteServerTitle": "Delete MCP Server",
|
||||
"mcp.customPlaceholder": "example:\n\n{\n \"postgres\": {\n \"command\": \"npx\",\n \"args\": [\n \"-y\",\n \"@modelcontextprotocol/server-postgres\",\n \"postgresql://localhost/mydb\"\n ]\n }\n}",
|
||||
"usage.title": "Usage",
|
||||
"usage.description": "Monitor your token usage",
|
||||
"usage.loading": "Loading usage data...",
|
||||
"usage.error": "Error loading usage data: {{error}}",
|
||||
"usage.noData": "No usage data found.",
|
||||
"usage.inputTokens": "Input Tokens",
|
||||
"usage.outputTokens": "Output Tokens",
|
||||
"usage.cacheReadTokens": "Cache Read Tokens",
|
||||
"usage.cost": "Cost",
|
||||
"usage.refresh": "Refresh",
|
||||
"usage.refreshing": "Refreshing",
|
||||
"usageChart.noData": "No usage data available",
|
||||
"usageChart.modelFilter": "Model:",
|
||||
"usageChart.timeRange": "Time Range:",
|
||||
"usageChart.allModels": "All models",
|
||||
"usageChart.selectTimeRange": "Select time range",
|
||||
"usageChart.last5Hours": "Last 5 hours",
|
||||
"usageChart.startOfToday": "Start of today",
|
||||
"usageChart.last7Days": "Last 7 days",
|
||||
"usageChart.startOfWeek": "Start of this week",
|
||||
"usageChart.startOfMonth": "Start of this month",
|
||||
"usageChart.allTime": "All time",
|
||||
"navigation.memory": "Memory",
|
||||
"navigation.notifications": "Notifications",
|
||||
"navigation.commands": "Commands",
|
||||
"memory.title": "Memory",
|
||||
"memory.description": "Manage Claude Code global memory",
|
||||
"memory.save": "Save",
|
||||
"memory.saving": "Saving...",
|
||||
"commands.title": "Commands",
|
||||
"commands.description": "View your Claude Code custom commands",
|
||||
"commands.noCommands": "No custom commands found. Click \"Add Command\" to create one.",
|
||||
"commands.error": "Error loading commands: {{error}}",
|
||||
"commands.viewInClaude": "View in Claude",
|
||||
"commands.addCommand": "Add Command",
|
||||
"commands.addCommandTitle": "Add Command",
|
||||
"commands.addCommandDescription": "Create a new custom command for Claude Code",
|
||||
"commands.commandName": "Command Name",
|
||||
"commands.commandNamePlaceholder": "e.g., write-tests",
|
||||
"commands.commandContent": "Command Content",
|
||||
"commands.contentPlaceholder": "Enter your command instructions...",
|
||||
"commands.save": "Save",
|
||||
"commands.saving": "Saving...",
|
||||
"commands.create": "Create",
|
||||
"commands.creating": "Creating...",
|
||||
"commands.deleteConfirm": "Are you sure you want to delete the command \"{{commandName}}\"? This action cannot be undone.",
|
||||
"commands.deleteTitle": "Delete Command",
|
||||
"commands.emptyNameError": "Command name cannot be empty",
|
||||
"commands.emptyContentError": "Command content cannot be empty",
|
||||
"commands.validationError": "Validation Error",
|
||||
"commands.commandExistsError": "Command \"{{commandName}}\" already exists",
|
||||
"commands.commandExistsTitle": "Command Exists",
|
||||
"toast.commandSaved": "Command saved successfully",
|
||||
"toast.commandSaveFailed": "Failed to save command: {{error}}",
|
||||
"toast.commandDeleted": "Command deleted successfully",
|
||||
"toast.commandDeleteFailed": "Failed to delete command: {{error}}",
|
||||
"projects.title": "Projects",
|
||||
"projects.detail.editor": "Project Configuration Editor",
|
||||
"projects.detail.projectEditor": "Project Editor",
|
||||
"projects.detail.loading": "Loading project configuration...",
|
||||
"projects.detail.loadError": "Failed to load configuration:",
|
||||
"projects.detail.noProjectSelected": "No project selected or configuration not loaded",
|
||||
"projects.detail.invalidJson": "Invalid JSON format. Please fix the errors before saving.",
|
||||
"projects.detail.unsavedChanges": "You have unsaved changes. Do you want to save before switching projects?",
|
||||
"projects.detail.projectNotFound": "Project with path \"{{path}}\" not found.",
|
||||
"projects.detail.backToProjects": "Back to Projects",
|
||||
"projects.detail.save": "Save",
|
||||
"projects.detail.saving": "Saving...",
|
||||
"projects.detail.selectProject": "Select Project:",
|
||||
"projects.detail.searchProject": "Search project...",
|
||||
"projects.detail.noProjectFound": "No project found.",
|
||||
"projects.detail.noProjectsMessage": "No open Claude Code projects. Projects will automatically appear here after running the `claude` command in a project.",
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.description": "Receive notifications from Claude Code",
|
||||
"notifications.general": "General Notifications",
|
||||
"notifications.generalDescription": "Notify when Claude Code sends notifications",
|
||||
"notifications.toolUse": "Tool Use Notifications",
|
||||
"notifications.toolUseDescription": "Notify when Claude Code is using tools",
|
||||
"notifications.completion": "Completion Notifications",
|
||||
"notifications.completionDescription": "Notify when Claude Code completes tasks",
|
||||
"notifications.sending": "Sending...",
|
||||
"notifications.testGeneral": "Test Notification",
|
||||
"notifications.testToolUse": "Test Tool Use",
|
||||
"notifications.testCompletion": "Test Completion",
|
||||
"glm.title": "Use GLM 4.6 in Claude Code",
|
||||
"glm.tooltip": "\"In public benchmarks and real programming tasks, GLM-4.6's coding capabilities align with Claude Sonnet 4, making it the best known domestic Coding model\" — Zhipu Official Documentation",
|
||||
"glm.startConfig": "Start Configuration",
|
||||
"glm.close": "Close",
|
||||
"glm.zaiTitle": "Z.ai GLM",
|
||||
"glm.zhipuTitle": "Zhipu GLM",
|
||||
"glm.configZai": "Configure Z.ai GLM",
|
||||
"glm.configZhipu": "Configure Zhipu GLM",
|
||||
"glm.description": "Use {{provider}} GLM in Claude Code",
|
||||
"glm.zhipu": "Zhipu",
|
||||
"glm.chinaMainland": "China Mainland (bigmodel.cn)",
|
||||
"glm.international": "International (z.ai)",
|
||||
"glm.step1": "Step 1: Buy GLM Coding Plan",
|
||||
"glm.buyFromOfficial": "Buy from Official Website",
|
||||
"glm.discount": "Save 10% on your order when using this button (official promotion)",
|
||||
"glm.step2": "Step 2: Create API Key",
|
||||
"glm.enterConsole": "Enter Console",
|
||||
"glm.step3": "Step 3: Enter API Key",
|
||||
"glm.zaiApiKeyPlaceholder": "Enter your Z.ai API Key",
|
||||
"glm.zhipuApiKeyPlaceholder": "Enter your Zhipu API Key",
|
||||
"glm.creating": "Creating...",
|
||||
"glm.createConfig": "Create Configuration",
|
||||
"glm.useZhipuGlm": "Use Z.ai GLM",
|
||||
"minimax.configMainland": "Configure MiniMax (China)",
|
||||
"minimax.configInternational": "Configure MiniMax (International)",
|
||||
"minimax.description": "Use MiniMax in Claude Code",
|
||||
"minimax.minimax": "MiniMax",
|
||||
"minimax.chinaMainland": "China Mainland (minimaxi.com)",
|
||||
"minimax.international": "International (minimax.io)",
|
||||
"minimax.step1": "Step 1: Create API Key",
|
||||
"minimax.enterConsole": "Enter Console",
|
||||
"minimax.step2": "Step 2: Enter API Key",
|
||||
"minimax.apiKeyPlaceholder": "Enter your MiniMax API Key",
|
||||
"minimax.creating": "Creating...",
|
||||
"minimax.createConfig": "Create Configuration",
|
||||
"minimax.useMiniMax": "Use MiniMax",
|
||||
"minimax.mainlandTitle": "MiniMax China",
|
||||
"minimax.internationalTitle": "MiniMax International"
|
||||
}
|
||||
"app.title": "CC Mate",
|
||||
"navigation.configurations": "Config files",
|
||||
"navigation.projects": "Projects",
|
||||
"navigation.mcp": "MCP",
|
||||
"navigation.usage": "Usage",
|
||||
"navigation.settings": "Settings",
|
||||
"config.type.user": "User Configuration",
|
||||
"config.type.enterprise": "Enterprise Configuration",
|
||||
"config.type.mcp": "MCP Configuration",
|
||||
"config.save": "Save Configuration",
|
||||
"config.backup": "Backup Configuration",
|
||||
"config.restore": "Restore Configuration",
|
||||
"error.validation": "Invalid JSON format",
|
||||
"error.save": "Failed to save configuration",
|
||||
"success.save": "Configuration saved successfully",
|
||||
"loading": "Loading...",
|
||||
"settings.title": "Settings",
|
||||
"settings.language": "Language",
|
||||
"settings.theme": "Theme",
|
||||
"settings.theme.system": "Follow System",
|
||||
"settings.theme.light": "Light",
|
||||
"settings.theme.dark": "Dark",
|
||||
"settings.version": "Version",
|
||||
"settings.checkingUpdate": "Checking for updates...",
|
||||
"settings.newVersionAvailable": "New version v{{version}} available",
|
||||
"settings.installing": "Installing...",
|
||||
"settings.installAndRestart": "Install and Restart",
|
||||
"settings.upToDate": "Up to date",
|
||||
"settings.contact": "Contact",
|
||||
"settings.reportIssue": "Report Issue",
|
||||
"configSwitcher.newConfig": "New Configuration",
|
||||
"configSwitcher.createConfig": "New Config",
|
||||
"configSwitcher.allConfigs": "All Configurations",
|
||||
"configSwitcher.title": "Configurations",
|
||||
"configSwitcher.description": "Create and switch between multiple Claude Code configurations",
|
||||
"configSwitcher.originalConfig": "Claude",
|
||||
"configSwitcher.originalConfigDescription": "Claude Code Original Configuration",
|
||||
"configEditor.save": "Save",
|
||||
"configEditor.configName": "Configuration Name",
|
||||
"configEditor.deleteConfirm": "Are you sure you want to delete configuration \"{{name}}\"? This action cannot be undone.",
|
||||
"configEditor.deleteTitle": "Delete Configuration",
|
||||
"configEditor.sections.common": "Common",
|
||||
"configEditor.sections.generalSettings": "General Settings",
|
||||
"configEditor.sections.authLogin": "Authentication & Login",
|
||||
"configEditor.sections.mcpConfig": "MCP Configuration",
|
||||
"configEditor.sections.awsConfig": "AWS Configuration",
|
||||
"configEditor.sections.environmentVars": "Environment Variables",
|
||||
"updateButton.installing": "Installing...",
|
||||
"updateButton.newVersionAvailable": "New version available",
|
||||
"toast.configSaved": "Configuration \"{{configType}}\" saved successfully",
|
||||
"toast.configSaveFailed": "Failed to save configuration: {{error}}",
|
||||
"toast.backupSuccess": "Claude configurations backed up successfully",
|
||||
"toast.backupFailed": "Failed to backup configurations: {{error}}",
|
||||
"toast.storeCreated": "Config created successfully",
|
||||
"toast.storeCreateFailed": "Failed to create config: {{error}}",
|
||||
"toast.storeDeleted": "Config deleted successfully",
|
||||
"toast.storeDeleteFailed": "Failed to delete config: {{error}}",
|
||||
"toast.storeActivated": "Config activated successfully",
|
||||
"toast.storeActivateFailed": "Failed to switch config: {{error}}",
|
||||
"toast.storeSaved": "Config \"{{title}}\" saved successfully",
|
||||
"toast.storeSavedAndActive": "Config \"{{title}}\" saved successfully. Config has applied, please restart the Claude Code session",
|
||||
"toast.storeSaveFailed": "Failed to save config: {{error}}",
|
||||
"toast.updateInstalled": "Update installed successfully. Restarting...",
|
||||
"toast.updateInstallFailed": "Failed to install update: {{error}}",
|
||||
"mcp.title": "MCP",
|
||||
"mcp.description": "Claude Code Global MCP Service Configuration",
|
||||
"mcp.addServer": "Add MCP Server",
|
||||
"mcp.addServerTitle": "Add MCP Server",
|
||||
"mcp.addServerDescription": "Add global MCP services available across projects",
|
||||
"mcp.noServersConfigured": "No MCP servers configured. Click \"Add MCP Server\" to add one.",
|
||||
"mcp.save": "Save",
|
||||
"mcp.saving": "Saving...",
|
||||
"mcp.recommend": "Recommended",
|
||||
"mcp.custom": "Custom",
|
||||
"mcp.source": "Source",
|
||||
"mcp.add": "Add",
|
||||
"mcp.invalidJsonError": "Invalid JSON configuration for {{serverName}}",
|
||||
"mcp.invalidJsonTitle": "Invalid JSON",
|
||||
"mcp.serverExistsError": "MCP server \"{{serverName}}\" already exists",
|
||||
"mcp.serverExistsTitle": "MCP Server Exists",
|
||||
"mcp.addServerConfirm": "Do you want to add the {{serverName}} MCP server?",
|
||||
"mcp.addCustomServerError": "Invalid JSON format. Please enter a valid JSON configuration.",
|
||||
"mcp.invalidConfigError": "Configuration must be a JSON object.",
|
||||
"mcp.noServersError": "Configuration must contain at least one MCP server.",
|
||||
"mcp.duplicateServersError": "MCP server(s) already exist: {{servers}}",
|
||||
"mcp.duplicateServersTitle": "Duplicate MCP Servers",
|
||||
"mcp.addCustomServersConfirm": "Do you want to add {{count}} MCP server(s)?",
|
||||
"mcp.addCustomServersTitle": "Add Custom MCP Servers",
|
||||
"mcp.addServerError": "Failed to add MCP server",
|
||||
"mcp.deleteServerConfirm": "Are you sure you want to delete the MCP server \"{{serverName}}\"? This action cannot be undone.",
|
||||
"mcp.deleteServerTitle": "Delete MCP Server",
|
||||
"mcp.customPlaceholder": "example:\n\n{\n \"postgres\": {\n \"command\": \"npx\",\n \"args\": [\n \"-y\",\n \"@modelcontextprotocol/server-postgres\",\n \"postgresql://localhost/mydb\"\n ]\n }\n}",
|
||||
"usage.title": "Usage",
|
||||
"usage.description": "Monitor your token usage",
|
||||
"usage.loading": "Loading usage data...",
|
||||
"usage.error": "Error loading usage data: {{error}}",
|
||||
"usage.noData": "No usage data found.",
|
||||
"usage.inputTokens": "Input Tokens",
|
||||
"usage.outputTokens": "Output Tokens",
|
||||
"usage.cacheReadTokens": "Cache Read Tokens",
|
||||
"usage.cost": "Cost",
|
||||
"usage.refresh": "Refresh",
|
||||
"usage.refreshing": "Refreshing",
|
||||
"usageChart.noData": "No usage data available",
|
||||
"usageChart.modelFilter": "Model:",
|
||||
"usageChart.timeRange": "Time Range:",
|
||||
"usageChart.allModels": "All models",
|
||||
"usageChart.selectTimeRange": "Select time range",
|
||||
"usageChart.last5Hours": "Last 5 hours",
|
||||
"usageChart.startOfToday": "Start of today",
|
||||
"usageChart.last7Days": "Last 7 days",
|
||||
"usageChart.startOfWeek": "Start of this week",
|
||||
"usageChart.startOfMonth": "Start of this month",
|
||||
"usageChart.allTime": "All time",
|
||||
"navigation.memory": "Memory",
|
||||
"navigation.notifications": "Notifications",
|
||||
"navigation.commands": "Commands",
|
||||
"memory.title": "Memory",
|
||||
"memory.description": "Manage Claude Code global memory",
|
||||
"memory.save": "Save",
|
||||
"memory.saving": "Saving...",
|
||||
"commands.title": "Commands",
|
||||
"commands.description": "View your Claude Code custom commands",
|
||||
"commands.noCommands": "No custom commands found. Click \"Add Command\" to create one.",
|
||||
"commands.error": "Error loading commands: {{error}}",
|
||||
"commands.viewInClaude": "View in Claude",
|
||||
"commands.addCommand": "Add Command",
|
||||
"commands.addCommandTitle": "Add Command",
|
||||
"commands.addCommandDescription": "Create a new custom command for Claude Code",
|
||||
"commands.commandName": "Command Name",
|
||||
"commands.commandNamePlaceholder": "e.g., write-tests",
|
||||
"commands.commandContent": "Command Content",
|
||||
"commands.contentPlaceholder": "Enter your command instructions...",
|
||||
"commands.save": "Save",
|
||||
"commands.saving": "Saving...",
|
||||
"commands.create": "Create",
|
||||
"commands.creating": "Creating...",
|
||||
"commands.deleteConfirm": "Are you sure you want to delete the command \"{{commandName}}\"? This action cannot be undone.",
|
||||
"commands.deleteTitle": "Delete Command",
|
||||
"commands.emptyNameError": "Command name cannot be empty",
|
||||
"commands.emptyContentError": "Command content cannot be empty",
|
||||
"commands.validationError": "Validation Error",
|
||||
"commands.commandExistsError": "Command \"{{commandName}}\" already exists",
|
||||
"commands.commandExistsTitle": "Command Exists",
|
||||
"toast.commandSaved": "Command saved successfully",
|
||||
"toast.commandSaveFailed": "Failed to save command: {{error}}",
|
||||
"toast.commandDeleted": "Command deleted successfully",
|
||||
"toast.commandDeleteFailed": "Failed to delete command: {{error}}",
|
||||
"projects.title": "Projects",
|
||||
"projects.detail.editor": "Project Configuration Editor",
|
||||
"projects.detail.projectEditor": "Project Editor",
|
||||
"projects.detail.loading": "Loading project configuration...",
|
||||
"projects.detail.loadError": "Failed to load configuration:",
|
||||
"projects.detail.noProjectSelected": "No project selected or configuration not loaded",
|
||||
"projects.detail.invalidJson": "Invalid JSON format. Please fix the errors before saving.",
|
||||
"projects.detail.unsavedChanges": "You have unsaved changes. Do you want to save before switching projects?",
|
||||
"projects.detail.projectNotFound": "Project with path \"{{path}}\" not found.",
|
||||
"projects.detail.backToProjects": "Back to Projects",
|
||||
"projects.detail.save": "Save",
|
||||
"projects.detail.saving": "Saving...",
|
||||
"projects.detail.selectProject": "Select Project:",
|
||||
"projects.detail.searchProject": "Search project...",
|
||||
"projects.detail.noProjectFound": "No project found.",
|
||||
"projects.detail.noProjectsMessage": "No open Claude Code projects. Projects will automatically appear here after running the `claude` command in a project.",
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.description": "Receive notifications from Claude Code",
|
||||
"notifications.general": "General Notifications",
|
||||
"notifications.generalDescription": "Notify when Claude Code sends notifications",
|
||||
"notifications.toolUse": "Tool Use Notifications",
|
||||
"notifications.toolUseDescription": "Notify when Claude Code is using tools",
|
||||
"notifications.completion": "Completion Notifications",
|
||||
"notifications.completionDescription": "Notify when Claude Code completes tasks",
|
||||
"notifications.sending": "Sending...",
|
||||
"notifications.testGeneral": "Test Notification",
|
||||
"notifications.testToolUse": "Test Tool Use",
|
||||
"notifications.testCompletion": "Test Completion",
|
||||
"glm.title": "Use GLM 4.6 in Claude Code",
|
||||
"glm.tooltip": "\"In public benchmarks and real programming tasks, GLM-4.6's coding capabilities align with Claude Sonnet 4, making it the best known domestic Coding model\" — Zhipu Official Documentation",
|
||||
"glm.startConfig": "Start Configuration",
|
||||
"glm.close": "Close",
|
||||
"glm.zaiTitle": "Z.ai GLM",
|
||||
"glm.zhipuTitle": "Zhipu GLM",
|
||||
"glm.configZai": "Configure Z.ai GLM",
|
||||
"glm.configZhipu": "Configure Zhipu GLM",
|
||||
"glm.description": "Use {{provider}} GLM in Claude Code",
|
||||
"glm.zhipu": "Zhipu",
|
||||
"glm.chinaMainland": "China Mainland (bigmodel.cn)",
|
||||
"glm.international": "International (z.ai)",
|
||||
"glm.step1": "Step 1: Buy GLM Coding Plan",
|
||||
"glm.buyFromOfficial": "Buy from Official Website",
|
||||
"glm.discount": "Save 10% on your order when using this button (official promotion)",
|
||||
"glm.step2": "Step 2: Create API Key",
|
||||
"glm.enterConsole": "Enter Console",
|
||||
"glm.step3": "Step 3: Enter API Key",
|
||||
"glm.zaiApiKeyPlaceholder": "Enter your Z.ai API Key",
|
||||
"glm.zhipuApiKeyPlaceholder": "Enter your Zhipu API Key",
|
||||
"glm.creating": "Creating...",
|
||||
"glm.createConfig": "Create Configuration",
|
||||
"glm.useZhipuGlm": "Use Z.ai GLM",
|
||||
"minimax.configMainland": "Configure MiniMax (China)",
|
||||
"minimax.configInternational": "Configure MiniMax (International)",
|
||||
"minimax.description": "Use MiniMax in Claude Code",
|
||||
"minimax.minimax": "MiniMax",
|
||||
"minimax.chinaMainland": "China Mainland (minimaxi.com)",
|
||||
"minimax.international": "International (minimax.io)",
|
||||
"minimax.step1": "Step 1: Create API Key",
|
||||
"minimax.enterConsole": "Enter Console",
|
||||
"minimax.step2": "Step 2: Enter API Key",
|
||||
"minimax.apiKeyPlaceholder": "Enter your MiniMax API Key",
|
||||
"minimax.creating": "Creating...",
|
||||
"minimax.createConfig": "Create Configuration",
|
||||
"minimax.useMiniMax": "Use MiniMax",
|
||||
"minimax.mainlandTitle": "MiniMax China",
|
||||
"minimax.internationalTitle": "MiniMax International"
|
||||
}
|
||||
|
||||
+215
-215
@@ -1,216 +1,216 @@
|
||||
{
|
||||
"app.title": "CC Mate",
|
||||
"navigation.configurations": "Fichiers de configuration",
|
||||
"navigation.projects": "Projets",
|
||||
"navigation.mcp": "MCP",
|
||||
"navigation.usage": "Utilisation",
|
||||
"navigation.settings": "Paramètres",
|
||||
"config.type.user": "Configuration utilisateur",
|
||||
"config.type.enterprise": "Configuration entreprise",
|
||||
"config.type.mcp": "Configuration MCP",
|
||||
"config.save": "Sauvegarder la configuration",
|
||||
"config.backup": "Sauvegarder la configuration",
|
||||
"config.restore": "Restaurer la configuration",
|
||||
"error.validation": "Format JSON invalide",
|
||||
"error.save": "Échec de la sauvegarde de la configuration",
|
||||
"success.save": "Configuration sauvegardée avec succès",
|
||||
"loading": "Chargement...",
|
||||
"settings.title": "Paramètres",
|
||||
"settings.language": "Langue",
|
||||
"settings.theme": "Thème",
|
||||
"settings.theme.system": "Suivre le système",
|
||||
"settings.theme.light": "Clair",
|
||||
"settings.theme.dark": "Sombre",
|
||||
"settings.version": "Version",
|
||||
"settings.checkingUpdate": "Vérification des mises à jour...",
|
||||
"settings.newVersionAvailable": "Nouvelle version v{{version}} disponible",
|
||||
"settings.installing": "Installation...",
|
||||
"settings.installAndRestart": "Installer et redémarrer",
|
||||
"settings.upToDate": "À jour",
|
||||
"settings.contact": "Contact",
|
||||
"settings.reportIssue": "Signaler un problème",
|
||||
"configSwitcher.newConfig": "Nouvelle configuration",
|
||||
"configSwitcher.createConfig": "Nouvelle configuration",
|
||||
"configSwitcher.allConfigs": "Toutes les configurations",
|
||||
"configSwitcher.title": "Configurations",
|
||||
"configSwitcher.description": "Créer et basculer entre plusieurs configurations Claude Code",
|
||||
"configSwitcher.originalConfig": "Claude",
|
||||
"configSwitcher.originalConfigDescription": "Configuration d'origine de Claude Code",
|
||||
"configEditor.save": "Sauvegarder",
|
||||
"configEditor.configName": "Nom de la configuration",
|
||||
"configEditor.deleteConfirm": "Êtes-vous sûr de vouloir supprimer la configuration \"{{name}}\" ? Cette action ne peut pas être annulée.",
|
||||
"configEditor.deleteTitle": "Supprimer la configuration",
|
||||
"configEditor.sections.common": "Commun",
|
||||
"configEditor.sections.generalSettings": "Paramètres généraux",
|
||||
"configEditor.sections.authLogin": "Authentification et connexion",
|
||||
"configEditor.sections.mcpConfig": "Configuration MCP",
|
||||
"configEditor.sections.awsConfig": "Configuration AWS",
|
||||
"configEditor.sections.environmentVars": "Variables d'environnement",
|
||||
"updateButton.installing": "Installation...",
|
||||
"updateButton.newVersionAvailable": "Nouvelle version disponible",
|
||||
"toast.configSaved": "Configuration \"{{configType}}\" sauvegardée avec succès",
|
||||
"toast.configSaveFailed": "Échec de la sauvegarde de la configuration : {{error}}",
|
||||
"toast.backupSuccess": "Configurations Claude sauvegardées avec succès",
|
||||
"toast.backupFailed": "Échec de la sauvegarde des configurations : {{error}}",
|
||||
"toast.storeCreated": "Configuration créée avec succès",
|
||||
"toast.storeCreateFailed": "Échec de la création de la configuration : {{error}}",
|
||||
"toast.storeDeleted": "Configuration supprimée avec succès",
|
||||
"toast.storeDeleteFailed": "Échec de la suppression de la configuration : {{error}}",
|
||||
"toast.storeActivated": "Configuration activée avec succès",
|
||||
"toast.storeActivateFailed": "Échec du basculement de la configuration : {{error}}",
|
||||
"toast.storeSaved": "Configuration \"{{title}}\" sauvegardée avec succès",
|
||||
"toast.storeSavedAndActive": "Configuration \"{{title}}\" sauvegardée avec succès. La configuration a été appliquée, veuillez redémarrer la session Claude Code",
|
||||
"toast.storeSaveFailed": "Échec de la sauvegarde de la configuration : {{error}}",
|
||||
"toast.updateInstalled": "Mise à jour installée avec succès. Redémarrage...",
|
||||
"toast.updateInstallFailed": "Échec de l'installation de la mise à jour : {{error}}",
|
||||
"mcp.title": "MCP",
|
||||
"mcp.description": "Configuration du service MCP global Claude Code",
|
||||
"mcp.addServer": "Ajouter un serveur MCP",
|
||||
"mcp.addServerTitle": "Ajouter un serveur MCP",
|
||||
"mcp.addServerDescription": "Ajouter des services MCP globaux disponibles à travers les projets",
|
||||
"mcp.noServersConfigured": "Aucun serveur MCP configuré. Cliquez sur \"Ajouter un serveur MCP\" pour en ajouter un.",
|
||||
"mcp.save": "Sauvegarder",
|
||||
"mcp.saving": "Sauvegarde...",
|
||||
"mcp.recommend": "Recommandé",
|
||||
"mcp.custom": "Personnalisé",
|
||||
"mcp.source": "Source",
|
||||
"mcp.add": "Ajouter",
|
||||
"mcp.invalidJsonError": "Configuration JSON invalide pour {{serverName}}",
|
||||
"mcp.invalidJsonTitle": "JSON invalide",
|
||||
"mcp.serverExistsError": "Le serveur MCP \"{{serverName}}\" existe déjà",
|
||||
"mcp.serverExistsTitle": "Le serveur MCP existe déjà",
|
||||
"mcp.addServerConfirm": "Voulez-vous ajouter le serveur MCP {{serverName}} ?",
|
||||
"mcp.addServerError": "Échec de l'ajout du serveur MCP",
|
||||
"mcp.addCustomServerError": "Format JSON invalide. Veuillez entrer une configuration JSON valide.",
|
||||
"mcp.invalidConfigError": "La configuration doit être un objet JSON.",
|
||||
"mcp.noServersError": "La configuration doit contenir au moins un serveur MCP.",
|
||||
"mcp.duplicateServersError": "Serveur(s) MCP existe déjà : {{servers}}",
|
||||
"mcp.duplicateServersTitle": "Serveurs MCP en double",
|
||||
"mcp.addCustomServersConfirm": "Voulez-vous ajouter {{count}} serveur(s) MCP ?",
|
||||
"mcp.addCustomServersTitle": "Ajouter des serveurs MCP personnalisés",
|
||||
"mcp.deleteServerConfirm": "Êtes-vous sûr de vouloir supprimer le serveur MCP \"{{serverName}}\" ? Cette action ne peut pas être annulée.",
|
||||
"mcp.deleteServerTitle": "Supprimer le serveur MCP",
|
||||
"mcp.customPlaceholder": "exemple:\n\n{\n \"postgres\": {\n \"command\": \"npx\",\n \"args\": [\n \"-y\",\n \"@modelcontextprotocol/server-postgres\",\n \"postgresql://localhost/mydb\"\n ]\n }\n}",
|
||||
"usage.title": "Utilisation",
|
||||
"usage.description": "Surveillez votre utilisation de jetons",
|
||||
"usage.loading": "Chargement des données d'utilisation...",
|
||||
"usage.error": "Erreur lors du chargement des données d'utilisation : {{error}}",
|
||||
"usage.noData": "Aucune donnée d'utilisation trouvée.",
|
||||
"usage.inputTokens": "Jetons d'entrée",
|
||||
"usage.outputTokens": "Jetons de sortie",
|
||||
"usage.cacheReadTokens": "Jetons de lecture du cache",
|
||||
"usage.cost": "Coût",
|
||||
"usage.refresh": "Actualiser",
|
||||
"usage.refreshing": "Actualisation",
|
||||
"usageChart.noData": "Aucune donnée d'utilisation disponible",
|
||||
"usageChart.modelFilter": "Modèle :",
|
||||
"usageChart.timeRange": "Plage de temps :",
|
||||
"usageChart.allModels": "Tous les modèles",
|
||||
"usageChart.selectTimeRange": "Sélectionner une plage de temps",
|
||||
"usageChart.last5Hours": "Dernières 5 heures",
|
||||
"usageChart.startOfToday": "Début de la journée",
|
||||
"usageChart.last7Days": "Derniers 7 jours",
|
||||
"usageChart.startOfWeek": "Début de la semaine",
|
||||
"usageChart.startOfMonth": "Début du mois",
|
||||
"usageChart.allTime": "Toutes les périodes",
|
||||
"navigation.memory": "Mémoire",
|
||||
"navigation.notifications": "Notifications",
|
||||
"navigation.commands": "Commandes",
|
||||
"memory.title": "Mémoire",
|
||||
"memory.description": "Gérer la mémoire globale de Claude Code",
|
||||
"memory.save": "Sauvegarder",
|
||||
"memory.saving": "Sauvegarde...",
|
||||
"commands.title": "Commandes",
|
||||
"commands.description": "Afficher vos commandes personnalisées Claude Code",
|
||||
"commands.noCommands": "Aucune commande personnalisée trouvée. Cliquez sur \"Ajouter une commande\" pour en créer une.",
|
||||
"commands.error": "Erreur lors du chargement des commandes : {{error}}",
|
||||
"commands.viewInClaude": "Voir dans Claude",
|
||||
"commands.addCommand": "Ajouter une commande",
|
||||
"commands.addCommandTitle": "Ajouter une commande",
|
||||
"commands.addCommandDescription": "Créer une nouvelle commande personnalisée pour Claude Code",
|
||||
"commands.commandName": "Nom de la commande",
|
||||
"commands.commandNamePlaceholder": "ex: write-tests",
|
||||
"commands.commandContent": "Contenu de la commande",
|
||||
"commands.contentPlaceholder": "Entrez vos instructions de commande...",
|
||||
"commands.save": "Sauvegarder",
|
||||
"commands.saving": "Sauvegarde...",
|
||||
"commands.create": "Créer",
|
||||
"commands.creating": "Création...",
|
||||
"commands.deleteConfirm": "Êtes-vous sûr de vouloir supprimer la commande \"{{commandName}}\" ? Cette action ne peut pas être annulée.",
|
||||
"commands.deleteTitle": "Supprimer la commande",
|
||||
"commands.emptyNameError": "Le nom de la commande ne peut pas être vide",
|
||||
"commands.emptyContentError": "Le contenu de la commande ne peut pas être vide",
|
||||
"commands.validationError": "Erreur de validation",
|
||||
"commands.commandExistsError": "La commande \"{{commandName}}\" existe déjà",
|
||||
"commands.commandExistsTitle": "La commande existe déjà",
|
||||
"toast.commandSaved": "Commande sauvegardée avec succès",
|
||||
"toast.commandSaveFailed": "Échec de la sauvegarde de la commande : {{error}}",
|
||||
"toast.commandDeleted": "Commande supprimée avec succès",
|
||||
"toast.commandDeleteFailed": "Échec de la suppression de la commande : {{error}}",
|
||||
"projects.title": "Projets",
|
||||
"projects.detail.editor": "Éditeur de configuration de projet",
|
||||
"projects.detail.projectEditor": "Éditeur de projet",
|
||||
"projects.detail.loading": "Chargement de la configuration du projet...",
|
||||
"projects.detail.loadError": "Échec du chargement de la configuration :",
|
||||
"projects.detail.noProjectSelected": "Aucun projet sélectionné ou configuration non chargée",
|
||||
"projects.detail.invalidJson": "Format JSON invalide. Veuillez corriger les erreurs avant de sauvegarder.",
|
||||
"projects.detail.unsavedChanges": "Vous avez des modifications non sauvegardées. Voulez-vous sauvegarder avant de changer de projet ?",
|
||||
"projects.detail.projectNotFound": "Projet avec le chemin \"{{path}}\" non trouvé.",
|
||||
"projects.detail.backToProjects": "Retour aux projets",
|
||||
"projects.detail.save": "Sauvegarder",
|
||||
"projects.detail.saving": "Sauvegarde...",
|
||||
"projects.detail.selectProject": "Sélectionner le projet :",
|
||||
"projects.detail.searchProject": "Rechercher un projet...",
|
||||
"projects.detail.noProjectFound": "Aucun projet trouvé.",
|
||||
"projects.detail.noProjectsMessage": "Aucun projet Claude Code ouvert. Les projets apparaîtront automatiquement ici après avoir exécuté la commande `claude` dans un projet.",
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.description": "Recevoir les notifications de Claude Code",
|
||||
"notifications.general": "Notifications générales",
|
||||
"notifications.generalDescription": "Notifier lorsque Claude Code envoie des notifications",
|
||||
"notifications.toolUse": "Notifications d'utilisation d'outils",
|
||||
"notifications.toolUseDescription": "Notifier lorsque Claude Code utilise des outils",
|
||||
"notifications.completion": "Notifications de complétion",
|
||||
"notifications.completionDescription": "Notifier lorsque Claude Code termine des tâches",
|
||||
"notifications.sending": "Envoi en cours...",
|
||||
"notifications.testGeneral": "Tester la notification",
|
||||
"notifications.testToolUse": "Tester l'utilisation d'outils",
|
||||
"notifications.testCompletion": "Tester la complétion",
|
||||
"glm.title": "Utiliser GLM 4.6 dans Claude Code",
|
||||
"glm.tooltip": "\"Dans les benchmarks publics et les tâches de programmation réelles, les capacités de codage de GLM-4.6 s'alignent avec Claude Sonnet 4, ce qui en fait le meilleur modèle de codage domestique connu\" — Documentation officielle Zhipu",
|
||||
"glm.startConfig": "Commencer la configuration",
|
||||
"glm.close": "Fermer",
|
||||
"glm.zaiTitle": "Z.ai GLM",
|
||||
"glm.zhipuTitle": "Zhipu GLM",
|
||||
"glm.configZai": "Configurer Z.ai GLM",
|
||||
"glm.configZhipu": "Configurer Zhipu GLM",
|
||||
"glm.description": "Utiliser {{provider}} GLM dans Claude Code",
|
||||
"glm.zhipu": "Zhipu",
|
||||
"glm.chinaMainland": "Chine continentale (bigmodel.cn)",
|
||||
"glm.international": "International (z.ai)",
|
||||
"glm.step1": "Étape 1 : Acheter le plan de codage GLM",
|
||||
"glm.buyFromOfficial": "Acheter sur le site officiel",
|
||||
"glm.discount": "Économisez 10% sur votre commande en utilisant ce bouton (promotion officielle)",
|
||||
"glm.step2": "Étape 2 : Créer une clé API",
|
||||
"glm.enterConsole": "Entrer dans la console",
|
||||
"glm.step3": "Étape 3 : Entrer la clé API",
|
||||
"glm.zaiApiKeyPlaceholder": "Entrez votre clé API Z.ai",
|
||||
"glm.zhipuApiKeyPlaceholder": "Entrez votre clé API Zhipu",
|
||||
"glm.creating": "Création...",
|
||||
"glm.createConfig": "Créer la configuration",
|
||||
"glm.useZhipuGlm": "Utiliser Zhipu GLM",
|
||||
"minimax.configMainland": "Configurer MiniMax (Chine)",
|
||||
"minimax.configInternational": "Configurer MiniMax (International)",
|
||||
"minimax.description": "Utiliser MiniMax dans Claude Code",
|
||||
"minimax.minimax": "MiniMax",
|
||||
"minimax.chinaMainland": "Chine continentale (minimaxi.com)",
|
||||
"minimax.international": "International (minimax.io)",
|
||||
"minimax.step1": "Étape 1 : Créer une clé API",
|
||||
"minimax.enterConsole": "Entrer dans la console",
|
||||
"minimax.step2": "Étape 2 : Entrer la clé API",
|
||||
"minimax.apiKeyPlaceholder": "Entrez votre clé API MiniMax",
|
||||
"minimax.creating": "Création...",
|
||||
"minimax.createConfig": "Créer la configuration",
|
||||
"minimax.useMiniMax": "Utiliser MiniMax",
|
||||
"minimax.mainlandTitle": "MiniMax Chine",
|
||||
"minimax.internationalTitle": "MiniMax International"
|
||||
}
|
||||
"app.title": "CC Mate",
|
||||
"navigation.configurations": "Fichiers de configuration",
|
||||
"navigation.projects": "Projets",
|
||||
"navigation.mcp": "MCP",
|
||||
"navigation.usage": "Utilisation",
|
||||
"navigation.settings": "Paramètres",
|
||||
"config.type.user": "Configuration utilisateur",
|
||||
"config.type.enterprise": "Configuration entreprise",
|
||||
"config.type.mcp": "Configuration MCP",
|
||||
"config.save": "Sauvegarder la configuration",
|
||||
"config.backup": "Sauvegarder la configuration",
|
||||
"config.restore": "Restaurer la configuration",
|
||||
"error.validation": "Format JSON invalide",
|
||||
"error.save": "Échec de la sauvegarde de la configuration",
|
||||
"success.save": "Configuration sauvegardée avec succès",
|
||||
"loading": "Chargement...",
|
||||
"settings.title": "Paramètres",
|
||||
"settings.language": "Langue",
|
||||
"settings.theme": "Thème",
|
||||
"settings.theme.system": "Suivre le système",
|
||||
"settings.theme.light": "Clair",
|
||||
"settings.theme.dark": "Sombre",
|
||||
"settings.version": "Version",
|
||||
"settings.checkingUpdate": "Vérification des mises à jour...",
|
||||
"settings.newVersionAvailable": "Nouvelle version v{{version}} disponible",
|
||||
"settings.installing": "Installation...",
|
||||
"settings.installAndRestart": "Installer et redémarrer",
|
||||
"settings.upToDate": "À jour",
|
||||
"settings.contact": "Contact",
|
||||
"settings.reportIssue": "Signaler un problème",
|
||||
"configSwitcher.newConfig": "Nouvelle configuration",
|
||||
"configSwitcher.createConfig": "Nouvelle configuration",
|
||||
"configSwitcher.allConfigs": "Toutes les configurations",
|
||||
"configSwitcher.title": "Configurations",
|
||||
"configSwitcher.description": "Créer et basculer entre plusieurs configurations Claude Code",
|
||||
"configSwitcher.originalConfig": "Claude",
|
||||
"configSwitcher.originalConfigDescription": "Configuration d'origine de Claude Code",
|
||||
"configEditor.save": "Sauvegarder",
|
||||
"configEditor.configName": "Nom de la configuration",
|
||||
"configEditor.deleteConfirm": "Êtes-vous sûr de vouloir supprimer la configuration \"{{name}}\" ? Cette action ne peut pas être annulée.",
|
||||
"configEditor.deleteTitle": "Supprimer la configuration",
|
||||
"configEditor.sections.common": "Commun",
|
||||
"configEditor.sections.generalSettings": "Paramètres généraux",
|
||||
"configEditor.sections.authLogin": "Authentification et connexion",
|
||||
"configEditor.sections.mcpConfig": "Configuration MCP",
|
||||
"configEditor.sections.awsConfig": "Configuration AWS",
|
||||
"configEditor.sections.environmentVars": "Variables d'environnement",
|
||||
"updateButton.installing": "Installation...",
|
||||
"updateButton.newVersionAvailable": "Nouvelle version disponible",
|
||||
"toast.configSaved": "Configuration \"{{configType}}\" sauvegardée avec succès",
|
||||
"toast.configSaveFailed": "Échec de la sauvegarde de la configuration : {{error}}",
|
||||
"toast.backupSuccess": "Configurations Claude sauvegardées avec succès",
|
||||
"toast.backupFailed": "Échec de la sauvegarde des configurations : {{error}}",
|
||||
"toast.storeCreated": "Configuration créée avec succès",
|
||||
"toast.storeCreateFailed": "Échec de la création de la configuration : {{error}}",
|
||||
"toast.storeDeleted": "Configuration supprimée avec succès",
|
||||
"toast.storeDeleteFailed": "Échec de la suppression de la configuration : {{error}}",
|
||||
"toast.storeActivated": "Configuration activée avec succès",
|
||||
"toast.storeActivateFailed": "Échec du basculement de la configuration : {{error}}",
|
||||
"toast.storeSaved": "Configuration \"{{title}}\" sauvegardée avec succès",
|
||||
"toast.storeSavedAndActive": "Configuration \"{{title}}\" sauvegardée avec succès. La configuration a été appliquée, veuillez redémarrer la session Claude Code",
|
||||
"toast.storeSaveFailed": "Échec de la sauvegarde de la configuration : {{error}}",
|
||||
"toast.updateInstalled": "Mise à jour installée avec succès. Redémarrage...",
|
||||
"toast.updateInstallFailed": "Échec de l'installation de la mise à jour : {{error}}",
|
||||
"mcp.title": "MCP",
|
||||
"mcp.description": "Configuration du service MCP global Claude Code",
|
||||
"mcp.addServer": "Ajouter un serveur MCP",
|
||||
"mcp.addServerTitle": "Ajouter un serveur MCP",
|
||||
"mcp.addServerDescription": "Ajouter des services MCP globaux disponibles à travers les projets",
|
||||
"mcp.noServersConfigured": "Aucun serveur MCP configuré. Cliquez sur \"Ajouter un serveur MCP\" pour en ajouter un.",
|
||||
"mcp.save": "Sauvegarder",
|
||||
"mcp.saving": "Sauvegarde...",
|
||||
"mcp.recommend": "Recommandé",
|
||||
"mcp.custom": "Personnalisé",
|
||||
"mcp.source": "Source",
|
||||
"mcp.add": "Ajouter",
|
||||
"mcp.invalidJsonError": "Configuration JSON invalide pour {{serverName}}",
|
||||
"mcp.invalidJsonTitle": "JSON invalide",
|
||||
"mcp.serverExistsError": "Le serveur MCP \"{{serverName}}\" existe déjà",
|
||||
"mcp.serverExistsTitle": "Le serveur MCP existe déjà",
|
||||
"mcp.addServerConfirm": "Voulez-vous ajouter le serveur MCP {{serverName}} ?",
|
||||
"mcp.addServerError": "Échec de l'ajout du serveur MCP",
|
||||
"mcp.addCustomServerError": "Format JSON invalide. Veuillez entrer une configuration JSON valide.",
|
||||
"mcp.invalidConfigError": "La configuration doit être un objet JSON.",
|
||||
"mcp.noServersError": "La configuration doit contenir au moins un serveur MCP.",
|
||||
"mcp.duplicateServersError": "Serveur(s) MCP existe déjà : {{servers}}",
|
||||
"mcp.duplicateServersTitle": "Serveurs MCP en double",
|
||||
"mcp.addCustomServersConfirm": "Voulez-vous ajouter {{count}} serveur(s) MCP ?",
|
||||
"mcp.addCustomServersTitle": "Ajouter des serveurs MCP personnalisés",
|
||||
"mcp.deleteServerConfirm": "Êtes-vous sûr de vouloir supprimer le serveur MCP \"{{serverName}}\" ? Cette action ne peut pas être annulée.",
|
||||
"mcp.deleteServerTitle": "Supprimer le serveur MCP",
|
||||
"mcp.customPlaceholder": "exemple:\n\n{\n \"postgres\": {\n \"command\": \"npx\",\n \"args\": [\n \"-y\",\n \"@modelcontextprotocol/server-postgres\",\n \"postgresql://localhost/mydb\"\n ]\n }\n}",
|
||||
"usage.title": "Utilisation",
|
||||
"usage.description": "Surveillez votre utilisation de jetons",
|
||||
"usage.loading": "Chargement des données d'utilisation...",
|
||||
"usage.error": "Erreur lors du chargement des données d'utilisation : {{error}}",
|
||||
"usage.noData": "Aucune donnée d'utilisation trouvée.",
|
||||
"usage.inputTokens": "Jetons d'entrée",
|
||||
"usage.outputTokens": "Jetons de sortie",
|
||||
"usage.cacheReadTokens": "Jetons de lecture du cache",
|
||||
"usage.cost": "Coût",
|
||||
"usage.refresh": "Actualiser",
|
||||
"usage.refreshing": "Actualisation",
|
||||
"usageChart.noData": "Aucune donnée d'utilisation disponible",
|
||||
"usageChart.modelFilter": "Modèle :",
|
||||
"usageChart.timeRange": "Plage de temps :",
|
||||
"usageChart.allModels": "Tous les modèles",
|
||||
"usageChart.selectTimeRange": "Sélectionner une plage de temps",
|
||||
"usageChart.last5Hours": "Dernières 5 heures",
|
||||
"usageChart.startOfToday": "Début de la journée",
|
||||
"usageChart.last7Days": "Derniers 7 jours",
|
||||
"usageChart.startOfWeek": "Début de la semaine",
|
||||
"usageChart.startOfMonth": "Début du mois",
|
||||
"usageChart.allTime": "Toutes les périodes",
|
||||
"navigation.memory": "Mémoire",
|
||||
"navigation.notifications": "Notifications",
|
||||
"navigation.commands": "Commandes",
|
||||
"memory.title": "Mémoire",
|
||||
"memory.description": "Gérer la mémoire globale de Claude Code",
|
||||
"memory.save": "Sauvegarder",
|
||||
"memory.saving": "Sauvegarde...",
|
||||
"commands.title": "Commandes",
|
||||
"commands.description": "Afficher vos commandes personnalisées Claude Code",
|
||||
"commands.noCommands": "Aucune commande personnalisée trouvée. Cliquez sur \"Ajouter une commande\" pour en créer une.",
|
||||
"commands.error": "Erreur lors du chargement des commandes : {{error}}",
|
||||
"commands.viewInClaude": "Voir dans Claude",
|
||||
"commands.addCommand": "Ajouter une commande",
|
||||
"commands.addCommandTitle": "Ajouter une commande",
|
||||
"commands.addCommandDescription": "Créer une nouvelle commande personnalisée pour Claude Code",
|
||||
"commands.commandName": "Nom de la commande",
|
||||
"commands.commandNamePlaceholder": "ex: write-tests",
|
||||
"commands.commandContent": "Contenu de la commande",
|
||||
"commands.contentPlaceholder": "Entrez vos instructions de commande...",
|
||||
"commands.save": "Sauvegarder",
|
||||
"commands.saving": "Sauvegarde...",
|
||||
"commands.create": "Créer",
|
||||
"commands.creating": "Création...",
|
||||
"commands.deleteConfirm": "Êtes-vous sûr de vouloir supprimer la commande \"{{commandName}}\" ? Cette action ne peut pas être annulée.",
|
||||
"commands.deleteTitle": "Supprimer la commande",
|
||||
"commands.emptyNameError": "Le nom de la commande ne peut pas être vide",
|
||||
"commands.emptyContentError": "Le contenu de la commande ne peut pas être vide",
|
||||
"commands.validationError": "Erreur de validation",
|
||||
"commands.commandExistsError": "La commande \"{{commandName}}\" existe déjà",
|
||||
"commands.commandExistsTitle": "La commande existe déjà",
|
||||
"toast.commandSaved": "Commande sauvegardée avec succès",
|
||||
"toast.commandSaveFailed": "Échec de la sauvegarde de la commande : {{error}}",
|
||||
"toast.commandDeleted": "Commande supprimée avec succès",
|
||||
"toast.commandDeleteFailed": "Échec de la suppression de la commande : {{error}}",
|
||||
"projects.title": "Projets",
|
||||
"projects.detail.editor": "Éditeur de configuration de projet",
|
||||
"projects.detail.projectEditor": "Éditeur de projet",
|
||||
"projects.detail.loading": "Chargement de la configuration du projet...",
|
||||
"projects.detail.loadError": "Échec du chargement de la configuration :",
|
||||
"projects.detail.noProjectSelected": "Aucun projet sélectionné ou configuration non chargée",
|
||||
"projects.detail.invalidJson": "Format JSON invalide. Veuillez corriger les erreurs avant de sauvegarder.",
|
||||
"projects.detail.unsavedChanges": "Vous avez des modifications non sauvegardées. Voulez-vous sauvegarder avant de changer de projet ?",
|
||||
"projects.detail.projectNotFound": "Projet avec le chemin \"{{path}}\" non trouvé.",
|
||||
"projects.detail.backToProjects": "Retour aux projets",
|
||||
"projects.detail.save": "Sauvegarder",
|
||||
"projects.detail.saving": "Sauvegarde...",
|
||||
"projects.detail.selectProject": "Sélectionner le projet :",
|
||||
"projects.detail.searchProject": "Rechercher un projet...",
|
||||
"projects.detail.noProjectFound": "Aucun projet trouvé.",
|
||||
"projects.detail.noProjectsMessage": "Aucun projet Claude Code ouvert. Les projets apparaîtront automatiquement ici après avoir exécuté la commande `claude` dans un projet.",
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.description": "Recevoir les notifications de Claude Code",
|
||||
"notifications.general": "Notifications générales",
|
||||
"notifications.generalDescription": "Notifier lorsque Claude Code envoie des notifications",
|
||||
"notifications.toolUse": "Notifications d'utilisation d'outils",
|
||||
"notifications.toolUseDescription": "Notifier lorsque Claude Code utilise des outils",
|
||||
"notifications.completion": "Notifications de complétion",
|
||||
"notifications.completionDescription": "Notifier lorsque Claude Code termine des tâches",
|
||||
"notifications.sending": "Envoi en cours...",
|
||||
"notifications.testGeneral": "Tester la notification",
|
||||
"notifications.testToolUse": "Tester l'utilisation d'outils",
|
||||
"notifications.testCompletion": "Tester la complétion",
|
||||
"glm.title": "Utiliser GLM 4.6 dans Claude Code",
|
||||
"glm.tooltip": "\"Dans les benchmarks publics et les tâches de programmation réelles, les capacités de codage de GLM-4.6 s'alignent avec Claude Sonnet 4, ce qui en fait le meilleur modèle de codage domestique connu\" — Documentation officielle Zhipu",
|
||||
"glm.startConfig": "Commencer la configuration",
|
||||
"glm.close": "Fermer",
|
||||
"glm.zaiTitle": "Z.ai GLM",
|
||||
"glm.zhipuTitle": "Zhipu GLM",
|
||||
"glm.configZai": "Configurer Z.ai GLM",
|
||||
"glm.configZhipu": "Configurer Zhipu GLM",
|
||||
"glm.description": "Utiliser {{provider}} GLM dans Claude Code",
|
||||
"glm.zhipu": "Zhipu",
|
||||
"glm.chinaMainland": "Chine continentale (bigmodel.cn)",
|
||||
"glm.international": "International (z.ai)",
|
||||
"glm.step1": "Étape 1 : Acheter le plan de codage GLM",
|
||||
"glm.buyFromOfficial": "Acheter sur le site officiel",
|
||||
"glm.discount": "Économisez 10% sur votre commande en utilisant ce bouton (promotion officielle)",
|
||||
"glm.step2": "Étape 2 : Créer une clé API",
|
||||
"glm.enterConsole": "Entrer dans la console",
|
||||
"glm.step3": "Étape 3 : Entrer la clé API",
|
||||
"glm.zaiApiKeyPlaceholder": "Entrez votre clé API Z.ai",
|
||||
"glm.zhipuApiKeyPlaceholder": "Entrez votre clé API Zhipu",
|
||||
"glm.creating": "Création...",
|
||||
"glm.createConfig": "Créer la configuration",
|
||||
"glm.useZhipuGlm": "Utiliser Zhipu GLM",
|
||||
"minimax.configMainland": "Configurer MiniMax (Chine)",
|
||||
"minimax.configInternational": "Configurer MiniMax (International)",
|
||||
"minimax.description": "Utiliser MiniMax dans Claude Code",
|
||||
"minimax.minimax": "MiniMax",
|
||||
"minimax.chinaMainland": "Chine continentale (minimaxi.com)",
|
||||
"minimax.international": "International (minimax.io)",
|
||||
"minimax.step1": "Étape 1 : Créer une clé API",
|
||||
"minimax.enterConsole": "Entrer dans la console",
|
||||
"minimax.step2": "Étape 2 : Entrer la clé API",
|
||||
"minimax.apiKeyPlaceholder": "Entrez votre clé API MiniMax",
|
||||
"minimax.creating": "Création...",
|
||||
"minimax.createConfig": "Créer la configuration",
|
||||
"minimax.useMiniMax": "Utiliser MiniMax",
|
||||
"minimax.mainlandTitle": "MiniMax Chine",
|
||||
"minimax.internationalTitle": "MiniMax International"
|
||||
}
|
||||
|
||||
+215
-215
@@ -1,216 +1,216 @@
|
||||
{
|
||||
"app.title": "CC Mate",
|
||||
"navigation.configurations": "設定ファイル",
|
||||
"navigation.projects": "プロジェクト",
|
||||
"navigation.mcp": "MCP",
|
||||
"navigation.usage": "使用量",
|
||||
"navigation.settings": "設定",
|
||||
"config.type.user": "ユーザー設定",
|
||||
"config.type.enterprise": "エンタープライズ設定",
|
||||
"config.type.mcp": "MCP 設定",
|
||||
"config.save": "設定を保存",
|
||||
"config.backup": "設定をバックアップ",
|
||||
"config.restore": "設定を復元",
|
||||
"error.validation": "JSON 形式が無効です",
|
||||
"error.save": "設定の保存に失敗しました",
|
||||
"success.save": "設定が正常に保存されました",
|
||||
"loading": "読み込み中...",
|
||||
"settings.title": "設定",
|
||||
"settings.language": "言語",
|
||||
"settings.theme": "テーマ",
|
||||
"settings.theme.system": "システムに従う",
|
||||
"settings.theme.light": "ライト",
|
||||
"settings.theme.dark": "ダーク",
|
||||
"settings.version": "バージョン",
|
||||
"settings.checkingUpdate": "更新を確認中...",
|
||||
"settings.newVersionAvailable": "新しいバージョン v{{version}} が利用可能です",
|
||||
"settings.installing": "インストール中...",
|
||||
"settings.installAndRestart": "インストールして再起動",
|
||||
"settings.upToDate": "最新版です",
|
||||
"settings.contact": "連絡先",
|
||||
"settings.reportIssue": "問題を報告",
|
||||
"configSwitcher.newConfig": "新しい設定",
|
||||
"configSwitcher.createConfig": "新しい設定",
|
||||
"configSwitcher.allConfigs": "すべての設定",
|
||||
"configSwitcher.title": "設定",
|
||||
"configSwitcher.description": "複数の Claude Code 設定を作成して切り替え",
|
||||
"configSwitcher.originalConfig": "Claude",
|
||||
"configSwitcher.originalConfigDescription": "Claude Code の元の設定",
|
||||
"configEditor.save": "保存",
|
||||
"configEditor.configName": "設定名",
|
||||
"configEditor.deleteConfirm": "設定 \"{{name}}\" を削除してもよろしいですか?この操作は元に戻せません。",
|
||||
"configEditor.deleteTitle": "設定を削除",
|
||||
"configEditor.sections.common": "共通",
|
||||
"configEditor.sections.generalSettings": "一般設定",
|
||||
"configEditor.sections.authLogin": "認証とログイン",
|
||||
"configEditor.sections.mcpConfig": "MCP 設定",
|
||||
"configEditor.sections.awsConfig": "AWS 設定",
|
||||
"configEditor.sections.environmentVars": "環境変数",
|
||||
"updateButton.installing": "インストール中...",
|
||||
"updateButton.newVersionAvailable": "新しいバージョンが利用可能です",
|
||||
"toast.configSaved": "設定 \"{{configType}}\" が正常に保存されました",
|
||||
"toast.configSaveFailed": "設定の保存に失敗しました:{{error}}",
|
||||
"toast.backupSuccess": "Claude 設定が正常にバックアップされました",
|
||||
"toast.backupFailed": "設定のバックアップに失敗しました:{{error}}",
|
||||
"toast.storeCreated": "設定が正常に作成されました",
|
||||
"toast.storeCreateFailed": "設定の作成に失敗しました:{{error}}",
|
||||
"toast.storeDeleted": "設定が正常に削除されました",
|
||||
"toast.storeDeleteFailed": "設定の削除に失敗しました:{{error}}",
|
||||
"toast.storeActivated": "設定が正常にアクティベートされました",
|
||||
"toast.storeActivateFailed": "設定の切り替えに失敗しました:{{error}}",
|
||||
"toast.storeSaved": "設定 \"{{title}}\" が正常に保存されました",
|
||||
"toast.storeSavedAndActive": "設定 \"{{title}}\" が正常に保存されました。設定が適用されました。Claude Code セッションを再起動してください",
|
||||
"toast.storeSaveFailed": "設定の保存に失敗しました:{{error}}",
|
||||
"toast.updateInstalled": "更新が正常にインストールされました。再起動中...",
|
||||
"toast.updateInstallFailed": "更新のインストールに失敗しました:{{error}}",
|
||||
"mcp.title": "MCP",
|
||||
"mcp.description": "Claude Code グローバル MCP サービス設定",
|
||||
"mcp.addServer": "MCP サーバーを追加",
|
||||
"mcp.addServerTitle": "MCP サーバーを追加",
|
||||
"mcp.addServerDescription": "プロジェクト間で利用可能なグローバル MCP サービスを追加",
|
||||
"mcp.noServersConfigured": "MCP サーバーが設定されていません。「MCP サーバーを追加」をクリックして追加してください。",
|
||||
"mcp.save": "保存",
|
||||
"mcp.saving": "保存中...",
|
||||
"mcp.recommend": "推奨",
|
||||
"mcp.custom": "カスタム",
|
||||
"mcp.source": "ソース",
|
||||
"mcp.add": "追加",
|
||||
"mcp.invalidJsonError": "{{serverName}} の JSON 設定が無効です",
|
||||
"mcp.invalidJsonTitle": "JSON が無効です",
|
||||
"mcp.serverExistsError": "MCP サーバー \"{{serverName}}\" は既に存在します",
|
||||
"mcp.serverExistsTitle": "MCP サーバーが既に存在します",
|
||||
"mcp.addServerConfirm": "{{serverName}} MCP サーバーを追加してもよろしいですか?",
|
||||
"mcp.addServerError": "MCP サーバーの追加に失敗しました",
|
||||
"mcp.addCustomServerError": "JSON 形式が無効です。有効な JSON 設定を入力してください。",
|
||||
"mcp.invalidConfigError": "設定は JSON オブジェクトである必要があります。",
|
||||
"mcp.noServersError": "設定には少なくとも 1 つの MCP サーバーが含まれている必要があります。",
|
||||
"mcp.duplicateServersError": "MCP サーバーは既に存在します:{{servers}}",
|
||||
"mcp.duplicateServersTitle": "重複する MCP サーバー",
|
||||
"mcp.addCustomServersConfirm": "{{count}} 個の MCP サーバーを追加してもよろしいですか?",
|
||||
"mcp.addCustomServersTitle": "カスタム MCP サーバーを追加",
|
||||
"mcp.deleteServerConfirm": "MCP サーバー \"{{serverName}}\" を削除してもよろしいですか?この操作は元に戻せません。",
|
||||
"mcp.deleteServerTitle": "MCP サーバーを削除",
|
||||
"mcp.customPlaceholder": "例:\n\n{\n \"postgres\": {\n \"command\": \"npx\",\n \"args\": [\n \"-y\",\n \"@modelcontextprotocol/server-postgres\",\n \"postgresql://localhost/mydb\"\n ]\n }\n}",
|
||||
"usage.title": "使用量",
|
||||
"usage.description": "トークン使用量を監視",
|
||||
"usage.loading": "使用データを読み込み中...",
|
||||
"usage.error": "使用データの読み込みエラー:{{error}}",
|
||||
"usage.noData": "使用データが見つかりません。",
|
||||
"usage.inputTokens": "入力トークン",
|
||||
"usage.outputTokens": "出力トークン",
|
||||
"usage.cacheReadTokens": "キャッシュ読み取りトークン",
|
||||
"usage.cost": "コスト",
|
||||
"usage.refresh": "更新",
|
||||
"usage.refreshing": "更新中",
|
||||
"usageChart.noData": "使用データがありません",
|
||||
"usageChart.modelFilter": "モデル:",
|
||||
"usageChart.timeRange": "時間範囲:",
|
||||
"usageChart.allModels": "すべてのモデル",
|
||||
"usageChart.selectTimeRange": "時間範囲を選択",
|
||||
"usageChart.last5Hours": "過去 5 時間",
|
||||
"usageChart.startOfToday": "今日の開始",
|
||||
"usageChart.last7Days": "過去 7 日間",
|
||||
"usageChart.startOfWeek": "今週の開始",
|
||||
"usageChart.startOfMonth": "今月の開始",
|
||||
"usageChart.allTime": "全期間",
|
||||
"navigation.memory": "メモリ",
|
||||
"navigation.notifications": "通知",
|
||||
"navigation.commands": "コマンド",
|
||||
"memory.title": "メモリ",
|
||||
"memory.description": "Claude Code グローバルメモリを管理",
|
||||
"memory.save": "保存",
|
||||
"memory.saving": "保存中...",
|
||||
"commands.title": "コマンド",
|
||||
"commands.description": "Claude Code カスタムコマンドを表示",
|
||||
"commands.noCommands": "カスタムコマンドが見つかりません。「コマンドを追加」をクリックして作成してください。",
|
||||
"commands.error": "コマンドの読み込みエラー:{{error}}",
|
||||
"commands.viewInClaude": "Claude で表示",
|
||||
"commands.addCommand": "コマンドを追加",
|
||||
"commands.addCommandTitle": "コマンドを追加",
|
||||
"commands.addCommandDescription": "Claude Code の新しいカスタムコマンドを作成",
|
||||
"commands.commandName": "コマンド名",
|
||||
"commands.commandNamePlaceholder": "例:write-tests",
|
||||
"commands.commandContent": "コマンド内容",
|
||||
"commands.contentPlaceholder": "コマンドの指示を入力...",
|
||||
"commands.save": "保存",
|
||||
"commands.saving": "保存中...",
|
||||
"commands.create": "作成",
|
||||
"commands.creating": "作成中...",
|
||||
"commands.deleteConfirm": "コマンド \"{{commandName}}\" を削除してもよろしいですか?この操作は元に戻せません。",
|
||||
"commands.deleteTitle": "コマンドを削除",
|
||||
"commands.emptyNameError": "コマンド名は空にできません",
|
||||
"commands.emptyContentError": "コマンド内容は空にできません",
|
||||
"commands.validationError": "検証エラー",
|
||||
"commands.commandExistsError": "コマンド \"{{commandName}}\" は既に存在します",
|
||||
"commands.commandExistsTitle": "コマンドが既に存在します",
|
||||
"toast.commandSaved": "コマンドが正常に保存されました",
|
||||
"toast.commandSaveFailed": "コマンドの保存に失敗しました:{{error}}",
|
||||
"toast.commandDeleted": "コマンドが正常に削除されました",
|
||||
"toast.commandDeleteFailed": "コマンドの削除に失敗しました:{{error}}",
|
||||
"projects.title": "プロジェクト",
|
||||
"projects.detail.editor": "プロジェクト設定エディター",
|
||||
"projects.detail.projectEditor": "プロジェクトエディター",
|
||||
"projects.detail.loading": "プロジェクト設定を読み込み中...",
|
||||
"projects.detail.loadError": "設定の読み込みに失敗しました:",
|
||||
"projects.detail.noProjectSelected": "プロジェクトが選択されていないか、設定が読み込まれていません",
|
||||
"projects.detail.invalidJson": "JSON 形式が無効です。保存前にエラーを修正してください。",
|
||||
"projects.detail.unsavedChanges": "保存されていない変更があります。プロジェクトを切り替える前に保存しますか?",
|
||||
"projects.detail.projectNotFound": "パス \"{{path}}\" のプロジェクトが見つかりません。",
|
||||
"projects.detail.backToProjects": "プロジェクトに戻る",
|
||||
"projects.detail.save": "保存",
|
||||
"projects.detail.saving": "保存中...",
|
||||
"projects.detail.selectProject": "プロジェクトを選択:",
|
||||
"projects.detail.searchProject": "プロジェクトを検索...",
|
||||
"projects.detail.noProjectFound": "プロジェクトが見つかりません。",
|
||||
"projects.detail.noProjectsMessage": "開いている Claude Code プロジェクトがありません。プロジェクトで `claude` コマンドを実行すると、プロジェクトがここに自動的に表示されます。",
|
||||
"notifications.title": "通知",
|
||||
"notifications.description": "Claude Code の通知を受け取る",
|
||||
"notifications.general": "一般通知",
|
||||
"notifications.generalDescription": "Claude Code が通知を送信する際に通知",
|
||||
"notifications.toolUse": "ツール使用通知",
|
||||
"notifications.toolUseDescription": "Claude Code がツールを使用する際に通知",
|
||||
"notifications.completion": "完了通知",
|
||||
"notifications.completionDescription": "Claude Code がタスクを完了した際に通知",
|
||||
"notifications.sending": "送信中...",
|
||||
"notifications.testGeneral": "テスト通知",
|
||||
"notifications.testToolUse": "ツール使用テスト",
|
||||
"notifications.testCompletion": "完了テスト",
|
||||
"glm.title": "Claude Code で GLM 4.6 を使用",
|
||||
"glm.tooltip": "\"公開ベンチマークと実際のプログラミングタスクにおいて、GLM-4.6 のコーディング能力は Claude Sonnet 4 と同等であり、国内で知られている最高のコーディングモデルです」 — 智譜公式ドキュメント",
|
||||
"glm.startConfig": "設定を開始",
|
||||
"glm.close": "閉じる",
|
||||
"glm.zaiTitle": "Z.ai GLM",
|
||||
"glm.zhipuTitle": "智譜 GLM",
|
||||
"glm.configZai": "Z.ai GLM を設定",
|
||||
"glm.configZhipu": "智譜 GLM を設定",
|
||||
"glm.description": "Claude Code で {{provider}} GLM を使用",
|
||||
"glm.zhipu": "智譜",
|
||||
"glm.chinaMainland": "中国大陸 (bigmodel.cn)",
|
||||
"glm.international": "国際 (z.ai)",
|
||||
"glm.step1": "ステップ 1:GLM コーディングプランを購入",
|
||||
"glm.buyFromOfficial": "公式サイトで購入",
|
||||
"glm.discount": "このボタンを使用して注文すると、即時に10%割引(公式キャンペーン)",
|
||||
"glm.step2": "ステップ 2:API キーを作成",
|
||||
"glm.enterConsole": "コンソールに入る",
|
||||
"glm.step3": "ステップ 3:API キーを入力",
|
||||
"glm.zaiApiKeyPlaceholder": "Z.ai API キーを入力してください",
|
||||
"glm.zhipuApiKeyPlaceholder": "智譜 API キーを入力してください",
|
||||
"glm.creating": "作成中...",
|
||||
"glm.createConfig": "設定を作成",
|
||||
"glm.useZhipuGlm": "智譜 GLM を使用",
|
||||
"minimax.configMainland": "MiniMax (中国) を設定",
|
||||
"minimax.configInternational": "MiniMax (国際) を設定",
|
||||
"minimax.description": "Claude Code で MiniMax を使用",
|
||||
"minimax.minimax": "MiniMax",
|
||||
"minimax.chinaMainland": "中国大陸 (minimaxi.com)",
|
||||
"minimax.international": "国際 (minimax.io)",
|
||||
"minimax.step1": "ステップ 1:API キーを作成",
|
||||
"minimax.enterConsole": "コンソールに入る",
|
||||
"minimax.step2": "ステップ 2:API キーを入力",
|
||||
"minimax.apiKeyPlaceholder": "MiniMax API キーを入力してください",
|
||||
"minimax.creating": "作成中...",
|
||||
"minimax.createConfig": "設定を作成",
|
||||
"minimax.useMiniMax": "MiniMax を使用",
|
||||
"minimax.mainlandTitle": "MiniMax 中国",
|
||||
"minimax.internationalTitle": "MiniMax 国際"
|
||||
}
|
||||
"app.title": "CC Mate",
|
||||
"navigation.configurations": "設定ファイル",
|
||||
"navigation.projects": "プロジェクト",
|
||||
"navigation.mcp": "MCP",
|
||||
"navigation.usage": "使用量",
|
||||
"navigation.settings": "設定",
|
||||
"config.type.user": "ユーザー設定",
|
||||
"config.type.enterprise": "エンタープライズ設定",
|
||||
"config.type.mcp": "MCP 設定",
|
||||
"config.save": "設定を保存",
|
||||
"config.backup": "設定をバックアップ",
|
||||
"config.restore": "設定を復元",
|
||||
"error.validation": "JSON 形式が無効です",
|
||||
"error.save": "設定の保存に失敗しました",
|
||||
"success.save": "設定が正常に保存されました",
|
||||
"loading": "読み込み中...",
|
||||
"settings.title": "設定",
|
||||
"settings.language": "言語",
|
||||
"settings.theme": "テーマ",
|
||||
"settings.theme.system": "システムに従う",
|
||||
"settings.theme.light": "ライト",
|
||||
"settings.theme.dark": "ダーク",
|
||||
"settings.version": "バージョン",
|
||||
"settings.checkingUpdate": "更新を確認中...",
|
||||
"settings.newVersionAvailable": "新しいバージョン v{{version}} が利用可能です",
|
||||
"settings.installing": "インストール中...",
|
||||
"settings.installAndRestart": "インストールして再起動",
|
||||
"settings.upToDate": "最新版です",
|
||||
"settings.contact": "連絡先",
|
||||
"settings.reportIssue": "問題を報告",
|
||||
"configSwitcher.newConfig": "新しい設定",
|
||||
"configSwitcher.createConfig": "新しい設定",
|
||||
"configSwitcher.allConfigs": "すべての設定",
|
||||
"configSwitcher.title": "設定",
|
||||
"configSwitcher.description": "複数の Claude Code 設定を作成して切り替え",
|
||||
"configSwitcher.originalConfig": "Claude",
|
||||
"configSwitcher.originalConfigDescription": "Claude Code の元の設定",
|
||||
"configEditor.save": "保存",
|
||||
"configEditor.configName": "設定名",
|
||||
"configEditor.deleteConfirm": "設定 \"{{name}}\" を削除してもよろしいですか?この操作は元に戻せません。",
|
||||
"configEditor.deleteTitle": "設定を削除",
|
||||
"configEditor.sections.common": "共通",
|
||||
"configEditor.sections.generalSettings": "一般設定",
|
||||
"configEditor.sections.authLogin": "認証とログイン",
|
||||
"configEditor.sections.mcpConfig": "MCP 設定",
|
||||
"configEditor.sections.awsConfig": "AWS 設定",
|
||||
"configEditor.sections.environmentVars": "環境変数",
|
||||
"updateButton.installing": "インストール中...",
|
||||
"updateButton.newVersionAvailable": "新しいバージョンが利用可能です",
|
||||
"toast.configSaved": "設定 \"{{configType}}\" が正常に保存されました",
|
||||
"toast.configSaveFailed": "設定の保存に失敗しました:{{error}}",
|
||||
"toast.backupSuccess": "Claude 設定が正常にバックアップされました",
|
||||
"toast.backupFailed": "設定のバックアップに失敗しました:{{error}}",
|
||||
"toast.storeCreated": "設定が正常に作成されました",
|
||||
"toast.storeCreateFailed": "設定の作成に失敗しました:{{error}}",
|
||||
"toast.storeDeleted": "設定が正常に削除されました",
|
||||
"toast.storeDeleteFailed": "設定の削除に失敗しました:{{error}}",
|
||||
"toast.storeActivated": "設定が正常にアクティベートされました",
|
||||
"toast.storeActivateFailed": "設定の切り替えに失敗しました:{{error}}",
|
||||
"toast.storeSaved": "設定 \"{{title}}\" が正常に保存されました",
|
||||
"toast.storeSavedAndActive": "設定 \"{{title}}\" が正常に保存されました。設定が適用されました。Claude Code セッションを再起動してください",
|
||||
"toast.storeSaveFailed": "設定の保存に失敗しました:{{error}}",
|
||||
"toast.updateInstalled": "更新が正常にインストールされました。再起動中...",
|
||||
"toast.updateInstallFailed": "更新のインストールに失敗しました:{{error}}",
|
||||
"mcp.title": "MCP",
|
||||
"mcp.description": "Claude Code グローバル MCP サービス設定",
|
||||
"mcp.addServer": "MCP サーバーを追加",
|
||||
"mcp.addServerTitle": "MCP サーバーを追加",
|
||||
"mcp.addServerDescription": "プロジェクト間で利用可能なグローバル MCP サービスを追加",
|
||||
"mcp.noServersConfigured": "MCP サーバーが設定されていません。「MCP サーバーを追加」をクリックして追加してください。",
|
||||
"mcp.save": "保存",
|
||||
"mcp.saving": "保存中...",
|
||||
"mcp.recommend": "推奨",
|
||||
"mcp.custom": "カスタム",
|
||||
"mcp.source": "ソース",
|
||||
"mcp.add": "追加",
|
||||
"mcp.invalidJsonError": "{{serverName}} の JSON 設定が無効です",
|
||||
"mcp.invalidJsonTitle": "JSON が無効です",
|
||||
"mcp.serverExistsError": "MCP サーバー \"{{serverName}}\" は既に存在します",
|
||||
"mcp.serverExistsTitle": "MCP サーバーが既に存在します",
|
||||
"mcp.addServerConfirm": "{{serverName}} MCP サーバーを追加してもよろしいですか?",
|
||||
"mcp.addServerError": "MCP サーバーの追加に失敗しました",
|
||||
"mcp.addCustomServerError": "JSON 形式が無効です。有効な JSON 設定を入力してください。",
|
||||
"mcp.invalidConfigError": "設定は JSON オブジェクトである必要があります。",
|
||||
"mcp.noServersError": "設定には少なくとも 1 つの MCP サーバーが含まれている必要があります。",
|
||||
"mcp.duplicateServersError": "MCP サーバーは既に存在します:{{servers}}",
|
||||
"mcp.duplicateServersTitle": "重複する MCP サーバー",
|
||||
"mcp.addCustomServersConfirm": "{{count}} 個の MCP サーバーを追加してもよろしいですか?",
|
||||
"mcp.addCustomServersTitle": "カスタム MCP サーバーを追加",
|
||||
"mcp.deleteServerConfirm": "MCP サーバー \"{{serverName}}\" を削除してもよろしいですか?この操作は元に戻せません。",
|
||||
"mcp.deleteServerTitle": "MCP サーバーを削除",
|
||||
"mcp.customPlaceholder": "例:\n\n{\n \"postgres\": {\n \"command\": \"npx\",\n \"args\": [\n \"-y\",\n \"@modelcontextprotocol/server-postgres\",\n \"postgresql://localhost/mydb\"\n ]\n }\n}",
|
||||
"usage.title": "使用量",
|
||||
"usage.description": "トークン使用量を監視",
|
||||
"usage.loading": "使用データを読み込み中...",
|
||||
"usage.error": "使用データの読み込みエラー:{{error}}",
|
||||
"usage.noData": "使用データが見つかりません。",
|
||||
"usage.inputTokens": "入力トークン",
|
||||
"usage.outputTokens": "出力トークン",
|
||||
"usage.cacheReadTokens": "キャッシュ読み取りトークン",
|
||||
"usage.cost": "コスト",
|
||||
"usage.refresh": "更新",
|
||||
"usage.refreshing": "更新中",
|
||||
"usageChart.noData": "使用データがありません",
|
||||
"usageChart.modelFilter": "モデル:",
|
||||
"usageChart.timeRange": "時間範囲:",
|
||||
"usageChart.allModels": "すべてのモデル",
|
||||
"usageChart.selectTimeRange": "時間範囲を選択",
|
||||
"usageChart.last5Hours": "過去 5 時間",
|
||||
"usageChart.startOfToday": "今日の開始",
|
||||
"usageChart.last7Days": "過去 7 日間",
|
||||
"usageChart.startOfWeek": "今週の開始",
|
||||
"usageChart.startOfMonth": "今月の開始",
|
||||
"usageChart.allTime": "全期間",
|
||||
"navigation.memory": "メモリ",
|
||||
"navigation.notifications": "通知",
|
||||
"navigation.commands": "コマンド",
|
||||
"memory.title": "メモリ",
|
||||
"memory.description": "Claude Code グローバルメモリを管理",
|
||||
"memory.save": "保存",
|
||||
"memory.saving": "保存中...",
|
||||
"commands.title": "コマンド",
|
||||
"commands.description": "Claude Code カスタムコマンドを表示",
|
||||
"commands.noCommands": "カスタムコマンドが見つかりません。「コマンドを追加」をクリックして作成してください。",
|
||||
"commands.error": "コマンドの読み込みエラー:{{error}}",
|
||||
"commands.viewInClaude": "Claude で表示",
|
||||
"commands.addCommand": "コマンドを追加",
|
||||
"commands.addCommandTitle": "コマンドを追加",
|
||||
"commands.addCommandDescription": "Claude Code の新しいカスタムコマンドを作成",
|
||||
"commands.commandName": "コマンド名",
|
||||
"commands.commandNamePlaceholder": "例:write-tests",
|
||||
"commands.commandContent": "コマンド内容",
|
||||
"commands.contentPlaceholder": "コマンドの指示を入力...",
|
||||
"commands.save": "保存",
|
||||
"commands.saving": "保存中...",
|
||||
"commands.create": "作成",
|
||||
"commands.creating": "作成中...",
|
||||
"commands.deleteConfirm": "コマンド \"{{commandName}}\" を削除してもよろしいですか?この操作は元に戻せません。",
|
||||
"commands.deleteTitle": "コマンドを削除",
|
||||
"commands.emptyNameError": "コマンド名は空にできません",
|
||||
"commands.emptyContentError": "コマンド内容は空にできません",
|
||||
"commands.validationError": "検証エラー",
|
||||
"commands.commandExistsError": "コマンド \"{{commandName}}\" は既に存在します",
|
||||
"commands.commandExistsTitle": "コマンドが既に存在します",
|
||||
"toast.commandSaved": "コマンドが正常に保存されました",
|
||||
"toast.commandSaveFailed": "コマンドの保存に失敗しました:{{error}}",
|
||||
"toast.commandDeleted": "コマンドが正常に削除されました",
|
||||
"toast.commandDeleteFailed": "コマンドの削除に失敗しました:{{error}}",
|
||||
"projects.title": "プロジェクト",
|
||||
"projects.detail.editor": "プロジェクト設定エディター",
|
||||
"projects.detail.projectEditor": "プロジェクトエディター",
|
||||
"projects.detail.loading": "プロジェクト設定を読み込み中...",
|
||||
"projects.detail.loadError": "設定の読み込みに失敗しました:",
|
||||
"projects.detail.noProjectSelected": "プロジェクトが選択されていないか、設定が読み込まれていません",
|
||||
"projects.detail.invalidJson": "JSON 形式が無効です。保存前にエラーを修正してください。",
|
||||
"projects.detail.unsavedChanges": "保存されていない変更があります。プロジェクトを切り替える前に保存しますか?",
|
||||
"projects.detail.projectNotFound": "パス \"{{path}}\" のプロジェクトが見つかりません。",
|
||||
"projects.detail.backToProjects": "プロジェクトに戻る",
|
||||
"projects.detail.save": "保存",
|
||||
"projects.detail.saving": "保存中...",
|
||||
"projects.detail.selectProject": "プロジェクトを選択:",
|
||||
"projects.detail.searchProject": "プロジェクトを検索...",
|
||||
"projects.detail.noProjectFound": "プロジェクトが見つかりません。",
|
||||
"projects.detail.noProjectsMessage": "開いている Claude Code プロジェクトがありません。プロジェクトで `claude` コマンドを実行すると、プロジェクトがここに自動的に表示されます。",
|
||||
"notifications.title": "通知",
|
||||
"notifications.description": "Claude Code の通知を受け取る",
|
||||
"notifications.general": "一般通知",
|
||||
"notifications.generalDescription": "Claude Code が通知を送信する際に通知",
|
||||
"notifications.toolUse": "ツール使用通知",
|
||||
"notifications.toolUseDescription": "Claude Code がツールを使用する際に通知",
|
||||
"notifications.completion": "完了通知",
|
||||
"notifications.completionDescription": "Claude Code がタスクを完了した際に通知",
|
||||
"notifications.sending": "送信中...",
|
||||
"notifications.testGeneral": "テスト通知",
|
||||
"notifications.testToolUse": "ツール使用テスト",
|
||||
"notifications.testCompletion": "完了テスト",
|
||||
"glm.title": "Claude Code で GLM 4.6 を使用",
|
||||
"glm.tooltip": "\"公開ベンチマークと実際のプログラミングタスクにおいて、GLM-4.6 のコーディング能力は Claude Sonnet 4 と同等であり、国内で知られている最高のコーディングモデルです」 — 智譜公式ドキュメント",
|
||||
"glm.startConfig": "設定を開始",
|
||||
"glm.close": "閉じる",
|
||||
"glm.zaiTitle": "Z.ai GLM",
|
||||
"glm.zhipuTitle": "智譜 GLM",
|
||||
"glm.configZai": "Z.ai GLM を設定",
|
||||
"glm.configZhipu": "智譜 GLM を設定",
|
||||
"glm.description": "Claude Code で {{provider}} GLM を使用",
|
||||
"glm.zhipu": "智譜",
|
||||
"glm.chinaMainland": "中国大陸 (bigmodel.cn)",
|
||||
"glm.international": "国際 (z.ai)",
|
||||
"glm.step1": "ステップ 1:GLM コーディングプランを購入",
|
||||
"glm.buyFromOfficial": "公式サイトで購入",
|
||||
"glm.discount": "このボタンを使用して注文すると、即時に10%割引(公式キャンペーン)",
|
||||
"glm.step2": "ステップ 2:API キーを作成",
|
||||
"glm.enterConsole": "コンソールに入る",
|
||||
"glm.step3": "ステップ 3:API キーを入力",
|
||||
"glm.zaiApiKeyPlaceholder": "Z.ai API キーを入力してください",
|
||||
"glm.zhipuApiKeyPlaceholder": "智譜 API キーを入力してください",
|
||||
"glm.creating": "作成中...",
|
||||
"glm.createConfig": "設定を作成",
|
||||
"glm.useZhipuGlm": "智譜 GLM を使用",
|
||||
"minimax.configMainland": "MiniMax (中国) を設定",
|
||||
"minimax.configInternational": "MiniMax (国際) を設定",
|
||||
"minimax.description": "Claude Code で MiniMax を使用",
|
||||
"minimax.minimax": "MiniMax",
|
||||
"minimax.chinaMainland": "中国大陸 (minimaxi.com)",
|
||||
"minimax.international": "国際 (minimax.io)",
|
||||
"minimax.step1": "ステップ 1:API キーを作成",
|
||||
"minimax.enterConsole": "コンソールに入る",
|
||||
"minimax.step2": "ステップ 2:API キーを入力",
|
||||
"minimax.apiKeyPlaceholder": "MiniMax API キーを入力してください",
|
||||
"minimax.creating": "作成中...",
|
||||
"minimax.createConfig": "設定を作成",
|
||||
"minimax.useMiniMax": "MiniMax を使用",
|
||||
"minimax.mainlandTitle": "MiniMax 中国",
|
||||
"minimax.internationalTitle": "MiniMax 国際"
|
||||
}
|
||||
|
||||
+215
-215
@@ -1,216 +1,216 @@
|
||||
{
|
||||
"app.title": "CC Mate",
|
||||
"navigation.configurations": "配置文件",
|
||||
"navigation.projects": "项目",
|
||||
"navigation.mcp": "MCP",
|
||||
"navigation.usage": "用量",
|
||||
"navigation.settings": "设置",
|
||||
"config.type.user": "用户配置",
|
||||
"config.type.enterprise": "企业配置",
|
||||
"config.type.mcp": "MCP 配置",
|
||||
"config.save": "保存配置",
|
||||
"config.backup": "备份配置",
|
||||
"config.restore": "恢复配置",
|
||||
"error.validation": "JSON 格式无效",
|
||||
"error.save": "保存配置失败",
|
||||
"success.save": "配置保存成功",
|
||||
"loading": "加载中...",
|
||||
"settings.title": "设置",
|
||||
"settings.language": "语言",
|
||||
"settings.theme": "主题",
|
||||
"settings.theme.system": "跟随系统",
|
||||
"settings.theme.light": "浅色",
|
||||
"settings.theme.dark": "深色",
|
||||
"settings.version": "软件版本",
|
||||
"settings.checkingUpdate": "检查更新中...",
|
||||
"settings.newVersionAvailable": "新版本 v{{version}} 可用",
|
||||
"settings.installing": "安装中...",
|
||||
"settings.installAndRestart": "安装并重启",
|
||||
"settings.upToDate": "已是最新版本",
|
||||
"settings.contact": "联系",
|
||||
"settings.reportIssue": "反馈问题",
|
||||
"configSwitcher.newConfig": "新配置",
|
||||
"configSwitcher.createConfig": "新建配置",
|
||||
"configSwitcher.allConfigs": "所有配置",
|
||||
"configSwitcher.title": "配置",
|
||||
"configSwitcher.description": "创建并切换多个 Claude Code 配置",
|
||||
"configSwitcher.originalConfig": "Claude",
|
||||
"configSwitcher.originalConfigDescription": "Claude Code 原有配置",
|
||||
"configEditor.save": "保存",
|
||||
"configEditor.configName": "配置名",
|
||||
"configEditor.deleteConfirm": "确定要删除配置 \"{{name}}\" 吗?此操作无法撤销。",
|
||||
"configEditor.deleteTitle": "删除配置",
|
||||
"configEditor.sections.common": "常用",
|
||||
"configEditor.sections.generalSettings": "通用设置",
|
||||
"configEditor.sections.authLogin": "认证和登录",
|
||||
"configEditor.sections.mcpConfig": "MCP 配置",
|
||||
"configEditor.sections.awsConfig": "AWS 配置",
|
||||
"configEditor.sections.environmentVars": "环境变量",
|
||||
"updateButton.installing": "安装中...",
|
||||
"updateButton.newVersionAvailable": "有新版本可更新",
|
||||
"toast.configSaved": "配置 \"{{configType}}\" 保存成功",
|
||||
"toast.configSaveFailed": "保存配置失败:{{error}}",
|
||||
"toast.backupSuccess": "Claude 配置备份成功",
|
||||
"toast.backupFailed": "备份配置失败:{{error}}",
|
||||
"toast.storeCreated": "配置创建成功",
|
||||
"toast.storeCreateFailed": "创建配置失败:{{error}}",
|
||||
"toast.storeDeleted": "配置删除成功",
|
||||
"toast.storeDeleteFailed": "删除配置失败:{{error}}",
|
||||
"toast.storeActivated": "配置激活成功",
|
||||
"toast.storeActivateFailed": "切换配置失败:{{error}}",
|
||||
"toast.storeSaved": "配置 \"{{title}}\" 保存成功",
|
||||
"toast.storeSavedAndActive": "配置 \"{{title}}\" 保存成功。配置已应用,请重启 Claude Code 会话",
|
||||
"toast.storeSaveFailed": "保存配置失败:{{error}}",
|
||||
"toast.updateInstalled": "更新安装成功,正在重启...",
|
||||
"toast.updateInstallFailed": "安装更新失败:{{error}}",
|
||||
"mcp.title": "MCP",
|
||||
"mcp.description": "Claude Code 全局 MCP 服务配置",
|
||||
"mcp.addServer": "添加 MCP 服务",
|
||||
"mcp.addServerTitle": "添加 MCP 服务",
|
||||
"mcp.addServerDescription": "添加全局 MCP 服务,可跨项目使用",
|
||||
"mcp.noServersConfigured": "尚未配置 MCP 服务器。点击\"添加 MCP 服务\"来添加一个。",
|
||||
"mcp.save": "保存",
|
||||
"mcp.saving": "保存中...",
|
||||
"mcp.recommend": "推荐",
|
||||
"mcp.custom": "自定义",
|
||||
"mcp.source": "源码",
|
||||
"mcp.add": "添加",
|
||||
"mcp.invalidJsonError": "{{serverName}} 的 JSON 配置无效",
|
||||
"mcp.invalidJsonTitle": "JSON 无效",
|
||||
"mcp.serverExistsError": "MCP 服务 \"{{serverName}}\" 已存在",
|
||||
"mcp.serverExistsTitle": "MCP 服务已存在",
|
||||
"mcp.addServerConfirm": "确定要添加 {{serverName}} MCP 服务吗?",
|
||||
"mcp.addCustomServerError": "JSON 格式无效。请输入有效的 JSON 配置。",
|
||||
"mcp.invalidConfigError": "配置必须是 JSON 对象。",
|
||||
"mcp.noServersError": "配置必须至少包含一个 MCP 服务。",
|
||||
"mcp.duplicateServersError": "MCP 服务已存在:{{servers}}",
|
||||
"mcp.duplicateServersTitle": "重复的 MCP 服务",
|
||||
"mcp.addCustomServersConfirm": "确定要添加 {{count}} 个 MCP 服务吗?",
|
||||
"mcp.addCustomServersTitle": "添加自定义 MCP 服务",
|
||||
"mcp.addServerError": "添加 MCP 服务失败",
|
||||
"mcp.deleteServerConfirm": "确定要删除 MCP 服务 \"{{serverName}}\" 吗?此操作无法撤销。",
|
||||
"mcp.deleteServerTitle": "删除 MCP 服务",
|
||||
"mcp.customPlaceholder": "例如:\n\n{\n \"postgres\": {\n \"command\": \"npx\",\n \"args\": [\n \"-y\",\n \"@modelcontextprotocol/server-postgres\",\n \"postgresql://localhost/mydb\"\n ]\n }\n}",
|
||||
"usage.title": "用量",
|
||||
"usage.description": "查看您的 token 使用情况",
|
||||
"usage.loading": "加载使用数据中...",
|
||||
"usage.error": "加载使用数据出错:{{error}}",
|
||||
"usage.noData": "未找到使用数据。",
|
||||
"usage.inputTokens": "输入 token",
|
||||
"usage.outputTokens": "输出 token",
|
||||
"usage.cacheReadTokens": "缓存读取 token",
|
||||
"usage.cost": "费用",
|
||||
"usage.refresh": "刷新",
|
||||
"usage.refreshing": "正在刷新",
|
||||
"usageChart.noData": "暂无使用数据",
|
||||
"usageChart.modelFilter": "模型:",
|
||||
"usageChart.timeRange": "时间范围:",
|
||||
"usageChart.allModels": "所有模型",
|
||||
"usageChart.selectTimeRange": "选择时间范围",
|
||||
"usageChart.last5Hours": "最近 5 小时",
|
||||
"usageChart.startOfToday": "今天开始",
|
||||
"usageChart.last7Days": "最近 7 天",
|
||||
"usageChart.startOfWeek": "本周开始",
|
||||
"usageChart.startOfMonth": "本月开始",
|
||||
"usageChart.allTime": "全部时间",
|
||||
"navigation.memory": "记忆",
|
||||
"navigation.notifications": "通知",
|
||||
"navigation.commands": "命令",
|
||||
"memory.title": "记忆",
|
||||
"memory.description": "管理 Claude Code 全局记忆",
|
||||
"memory.save": "保存",
|
||||
"memory.saving": "保存中...",
|
||||
"commands.title": "命令",
|
||||
"commands.description": "查看您的 Claude Code 自定义命令",
|
||||
"commands.noCommands": "未找到自定义命令。点击「添加命令」来创建一个。",
|
||||
"commands.error": "加载命令时出错:{{error}}",
|
||||
"commands.viewInClaude": "在 Claude 中查看",
|
||||
"commands.addCommand": "添加命令",
|
||||
"commands.addCommandTitle": "添加命令",
|
||||
"commands.addCommandDescription": "为 Claude Code 创建新的自定义命令",
|
||||
"commands.commandName": "命令名称",
|
||||
"commands.commandNamePlaceholder": "例如:write-tests",
|
||||
"commands.commandContent": "命令内容",
|
||||
"commands.contentPlaceholder": "输入您的命令说明...",
|
||||
"commands.save": "保存",
|
||||
"commands.saving": "保存中...",
|
||||
"commands.create": "创建",
|
||||
"commands.creating": "创建中...",
|
||||
"commands.deleteConfirm": "确定要删除命令「{{commandName}}」吗?此操作无法撤销。",
|
||||
"commands.deleteTitle": "删除命令",
|
||||
"commands.emptyNameError": "命令名称不能为空",
|
||||
"commands.emptyContentError": "命令内容不能为空",
|
||||
"commands.validationError": "验证错误",
|
||||
"commands.commandExistsError": "命令「{{commandName}}」已存在",
|
||||
"commands.commandExistsTitle": "命令已存在",
|
||||
"toast.commandSaved": "命令保存成功",
|
||||
"toast.commandSaveFailed": "保存命令失败:{{error}}",
|
||||
"toast.commandDeleted": "命令删除成功",
|
||||
"toast.commandDeleteFailed": "删除命令失败:{{error}}",
|
||||
"projects.title": "项目",
|
||||
"projects.detail.editor": "项目配置编辑器",
|
||||
"projects.detail.projectEditor": "项目编辑器",
|
||||
"projects.detail.loading": "加载项目配置中...",
|
||||
"projects.detail.loadError": "加载配置失败:",
|
||||
"projects.detail.noProjectSelected": "未选择项目或配置未加载",
|
||||
"projects.detail.invalidJson": "JSON 格式无效。请在保存前修复错误。",
|
||||
"projects.detail.unsavedChanges": "您有未保存的更改。要在切换项目前保存吗?",
|
||||
"projects.detail.projectNotFound": "未找到路径为 \"{{path}}\" 的项目。",
|
||||
"projects.detail.backToProjects": "返回项目列表",
|
||||
"projects.detail.save": "保存",
|
||||
"projects.detail.saving": "保存中...",
|
||||
"projects.detail.selectProject": "选择项目:",
|
||||
"projects.detail.searchProject": "搜索项目...",
|
||||
"projects.detail.noProjectFound": "未找到项目。",
|
||||
"projects.detail.noProjectsMessage": "没有已打开的 Claude Code 项目。在项目中运行 `claude` 命令后,这个项目会自动出现在这里。",
|
||||
"notifications.title": "通知",
|
||||
"notifications.description": "接收 Claude Code 的通知",
|
||||
"notifications.general": "一般通知",
|
||||
"notifications.generalDescription": "当 Claude Code 发送通知时提醒",
|
||||
"notifications.toolUse": "工具使用通知",
|
||||
"notifications.toolUseDescription": "当 Claude Code 使用工具时提醒",
|
||||
"notifications.completion": "完成时通知",
|
||||
"notifications.completionDescription": "当 Claude Code 完成任务时提醒",
|
||||
"notifications.sending": "发送中...",
|
||||
"notifications.testGeneral": "测试通知",
|
||||
"notifications.testToolUse": "测试工具使用",
|
||||
"notifications.testCompletion": "测试完成",
|
||||
"glm.title": "在 Claude Code 中使用 GLM 4.6",
|
||||
"glm.tooltip": "「在公开基准与真实编程任务中,GLM-4.6 的代码能力对齐 Claude Sonnet 4,是国内已知的最好的 Coding 模型」 —— 智谱官方文档",
|
||||
"glm.startConfig": "开始配置",
|
||||
"glm.close": "关闭",
|
||||
"glm.zaiTitle": "Z.ai GLM",
|
||||
"glm.zhipuTitle": "智谱 GLM",
|
||||
"glm.configZai": "配置 Z.ai GLM",
|
||||
"glm.configZhipu": "配置智谱 GLM",
|
||||
"glm.description": "在 Claude Code 中使用{{provider}} GLM",
|
||||
"glm.zhipu": "智谱",
|
||||
"glm.chinaMainland": "中国大陆 (bigmodel.cn)",
|
||||
"glm.international": "国际 (z.ai)",
|
||||
"glm.step1": "第 1 步:购买 GLM 编码畅玩套餐",
|
||||
"glm.buyFromOfficial": "前往官网购买",
|
||||
"glm.discount": "使用此按钮购买时,下单立减10%金额(官方活动)",
|
||||
"glm.step2": "第 2 步:创建 API Key",
|
||||
"glm.enterConsole": "进入控制台",
|
||||
"glm.step3": "第 3 步:输入 API Key",
|
||||
"glm.zaiApiKeyPlaceholder": "请输入您的 Z.ai API Key",
|
||||
"glm.zhipuApiKeyPlaceholder": "请输入您的智谱 API Key",
|
||||
"glm.creating": "创建中...",
|
||||
"glm.createConfig": "创建配置",
|
||||
"glm.useZhipuGlm": "使用智谱GLM",
|
||||
"minimax.configMainland": "配置 MiniMax (中国)",
|
||||
"minimax.configInternational": "配置 MiniMax (国际)",
|
||||
"minimax.description": "在 Claude Code 中使用 MiniMax",
|
||||
"minimax.minimax": "MiniMax",
|
||||
"minimax.chinaMainland": "中国大陆 (minimaxi.com)",
|
||||
"minimax.international": "国际 (minimax.io)",
|
||||
"minimax.step1": "第 1 步:创建 API Key",
|
||||
"minimax.enterConsole": "进入控制台",
|
||||
"minimax.step2": "第 2 步:输入 API Key",
|
||||
"minimax.apiKeyPlaceholder": "请输入您的 MiniMax API Key",
|
||||
"minimax.creating": "创建中...",
|
||||
"minimax.createConfig": "创建配置",
|
||||
"minimax.useMiniMax": "使用 MiniMax",
|
||||
"minimax.mainlandTitle": "MiniMax 中国",
|
||||
"minimax.internationalTitle": "MiniMax 国际"
|
||||
}
|
||||
"app.title": "CC Mate",
|
||||
"navigation.configurations": "配置文件",
|
||||
"navigation.projects": "项目",
|
||||
"navigation.mcp": "MCP",
|
||||
"navigation.usage": "用量",
|
||||
"navigation.settings": "设置",
|
||||
"config.type.user": "用户配置",
|
||||
"config.type.enterprise": "企业配置",
|
||||
"config.type.mcp": "MCP 配置",
|
||||
"config.save": "保存配置",
|
||||
"config.backup": "备份配置",
|
||||
"config.restore": "恢复配置",
|
||||
"error.validation": "JSON 格式无效",
|
||||
"error.save": "保存配置失败",
|
||||
"success.save": "配置保存成功",
|
||||
"loading": "加载中...",
|
||||
"settings.title": "设置",
|
||||
"settings.language": "语言",
|
||||
"settings.theme": "主题",
|
||||
"settings.theme.system": "跟随系统",
|
||||
"settings.theme.light": "浅色",
|
||||
"settings.theme.dark": "深色",
|
||||
"settings.version": "软件版本",
|
||||
"settings.checkingUpdate": "检查更新中...",
|
||||
"settings.newVersionAvailable": "新版本 v{{version}} 可用",
|
||||
"settings.installing": "安装中...",
|
||||
"settings.installAndRestart": "安装并重启",
|
||||
"settings.upToDate": "已是最新版本",
|
||||
"settings.contact": "联系",
|
||||
"settings.reportIssue": "反馈问题",
|
||||
"configSwitcher.newConfig": "新配置",
|
||||
"configSwitcher.createConfig": "新建配置",
|
||||
"configSwitcher.allConfigs": "所有配置",
|
||||
"configSwitcher.title": "配置",
|
||||
"configSwitcher.description": "创建并切换多个 Claude Code 配置",
|
||||
"configSwitcher.originalConfig": "Claude",
|
||||
"configSwitcher.originalConfigDescription": "Claude Code 原有配置",
|
||||
"configEditor.save": "保存",
|
||||
"configEditor.configName": "配置名",
|
||||
"configEditor.deleteConfirm": "确定要删除配置 \"{{name}}\" 吗?此操作无法撤销。",
|
||||
"configEditor.deleteTitle": "删除配置",
|
||||
"configEditor.sections.common": "常用",
|
||||
"configEditor.sections.generalSettings": "通用设置",
|
||||
"configEditor.sections.authLogin": "认证和登录",
|
||||
"configEditor.sections.mcpConfig": "MCP 配置",
|
||||
"configEditor.sections.awsConfig": "AWS 配置",
|
||||
"configEditor.sections.environmentVars": "环境变量",
|
||||
"updateButton.installing": "安装中...",
|
||||
"updateButton.newVersionAvailable": "有新版本可更新",
|
||||
"toast.configSaved": "配置 \"{{configType}}\" 保存成功",
|
||||
"toast.configSaveFailed": "保存配置失败:{{error}}",
|
||||
"toast.backupSuccess": "Claude 配置备份成功",
|
||||
"toast.backupFailed": "备份配置失败:{{error}}",
|
||||
"toast.storeCreated": "配置创建成功",
|
||||
"toast.storeCreateFailed": "创建配置失败:{{error}}",
|
||||
"toast.storeDeleted": "配置删除成功",
|
||||
"toast.storeDeleteFailed": "删除配置失败:{{error}}",
|
||||
"toast.storeActivated": "配置激活成功",
|
||||
"toast.storeActivateFailed": "切换配置失败:{{error}}",
|
||||
"toast.storeSaved": "配置 \"{{title}}\" 保存成功",
|
||||
"toast.storeSavedAndActive": "配置 \"{{title}}\" 保存成功。配置已应用,请重启 Claude Code 会话",
|
||||
"toast.storeSaveFailed": "保存配置失败:{{error}}",
|
||||
"toast.updateInstalled": "更新安装成功,正在重启...",
|
||||
"toast.updateInstallFailed": "安装更新失败:{{error}}",
|
||||
"mcp.title": "MCP",
|
||||
"mcp.description": "Claude Code 全局 MCP 服务配置",
|
||||
"mcp.addServer": "添加 MCP 服务",
|
||||
"mcp.addServerTitle": "添加 MCP 服务",
|
||||
"mcp.addServerDescription": "添加全局 MCP 服务,可跨项目使用",
|
||||
"mcp.noServersConfigured": "尚未配置 MCP 服务器。点击\"添加 MCP 服务\"来添加一个。",
|
||||
"mcp.save": "保存",
|
||||
"mcp.saving": "保存中...",
|
||||
"mcp.recommend": "推荐",
|
||||
"mcp.custom": "自定义",
|
||||
"mcp.source": "源码",
|
||||
"mcp.add": "添加",
|
||||
"mcp.invalidJsonError": "{{serverName}} 的 JSON 配置无效",
|
||||
"mcp.invalidJsonTitle": "JSON 无效",
|
||||
"mcp.serverExistsError": "MCP 服务 \"{{serverName}}\" 已存在",
|
||||
"mcp.serverExistsTitle": "MCP 服务已存在",
|
||||
"mcp.addServerConfirm": "确定要添加 {{serverName}} MCP 服务吗?",
|
||||
"mcp.addCustomServerError": "JSON 格式无效。请输入有效的 JSON 配置。",
|
||||
"mcp.invalidConfigError": "配置必须是 JSON 对象。",
|
||||
"mcp.noServersError": "配置必须至少包含一个 MCP 服务。",
|
||||
"mcp.duplicateServersError": "MCP 服务已存在:{{servers}}",
|
||||
"mcp.duplicateServersTitle": "重复的 MCP 服务",
|
||||
"mcp.addCustomServersConfirm": "确定要添加 {{count}} 个 MCP 服务吗?",
|
||||
"mcp.addCustomServersTitle": "添加自定义 MCP 服务",
|
||||
"mcp.addServerError": "添加 MCP 服务失败",
|
||||
"mcp.deleteServerConfirm": "确定要删除 MCP 服务 \"{{serverName}}\" 吗?此操作无法撤销。",
|
||||
"mcp.deleteServerTitle": "删除 MCP 服务",
|
||||
"mcp.customPlaceholder": "例如:\n\n{\n \"postgres\": {\n \"command\": \"npx\",\n \"args\": [\n \"-y\",\n \"@modelcontextprotocol/server-postgres\",\n \"postgresql://localhost/mydb\"\n ]\n }\n}",
|
||||
"usage.title": "用量",
|
||||
"usage.description": "查看您的 token 使用情况",
|
||||
"usage.loading": "加载使用数据中...",
|
||||
"usage.error": "加载使用数据出错:{{error}}",
|
||||
"usage.noData": "未找到使用数据。",
|
||||
"usage.inputTokens": "输入 token",
|
||||
"usage.outputTokens": "输出 token",
|
||||
"usage.cacheReadTokens": "缓存读取 token",
|
||||
"usage.cost": "费用",
|
||||
"usage.refresh": "刷新",
|
||||
"usage.refreshing": "正在刷新",
|
||||
"usageChart.noData": "暂无使用数据",
|
||||
"usageChart.modelFilter": "模型:",
|
||||
"usageChart.timeRange": "时间范围:",
|
||||
"usageChart.allModels": "所有模型",
|
||||
"usageChart.selectTimeRange": "选择时间范围",
|
||||
"usageChart.last5Hours": "最近 5 小时",
|
||||
"usageChart.startOfToday": "今天开始",
|
||||
"usageChart.last7Days": "最近 7 天",
|
||||
"usageChart.startOfWeek": "本周开始",
|
||||
"usageChart.startOfMonth": "本月开始",
|
||||
"usageChart.allTime": "全部时间",
|
||||
"navigation.memory": "记忆",
|
||||
"navigation.notifications": "通知",
|
||||
"navigation.commands": "命令",
|
||||
"memory.title": "记忆",
|
||||
"memory.description": "管理 Claude Code 全局记忆",
|
||||
"memory.save": "保存",
|
||||
"memory.saving": "保存中...",
|
||||
"commands.title": "命令",
|
||||
"commands.description": "查看您的 Claude Code 自定义命令",
|
||||
"commands.noCommands": "未找到自定义命令。点击「添加命令」来创建一个。",
|
||||
"commands.error": "加载命令时出错:{{error}}",
|
||||
"commands.viewInClaude": "在 Claude 中查看",
|
||||
"commands.addCommand": "添加命令",
|
||||
"commands.addCommandTitle": "添加命令",
|
||||
"commands.addCommandDescription": "为 Claude Code 创建新的自定义命令",
|
||||
"commands.commandName": "命令名称",
|
||||
"commands.commandNamePlaceholder": "例如:write-tests",
|
||||
"commands.commandContent": "命令内容",
|
||||
"commands.contentPlaceholder": "输入您的命令说明...",
|
||||
"commands.save": "保存",
|
||||
"commands.saving": "保存中...",
|
||||
"commands.create": "创建",
|
||||
"commands.creating": "创建中...",
|
||||
"commands.deleteConfirm": "确定要删除命令「{{commandName}}」吗?此操作无法撤销。",
|
||||
"commands.deleteTitle": "删除命令",
|
||||
"commands.emptyNameError": "命令名称不能为空",
|
||||
"commands.emptyContentError": "命令内容不能为空",
|
||||
"commands.validationError": "验证错误",
|
||||
"commands.commandExistsError": "命令「{{commandName}}」已存在",
|
||||
"commands.commandExistsTitle": "命令已存在",
|
||||
"toast.commandSaved": "命令保存成功",
|
||||
"toast.commandSaveFailed": "保存命令失败:{{error}}",
|
||||
"toast.commandDeleted": "命令删除成功",
|
||||
"toast.commandDeleteFailed": "删除命令失败:{{error}}",
|
||||
"projects.title": "项目",
|
||||
"projects.detail.editor": "项目配置编辑器",
|
||||
"projects.detail.projectEditor": "项目编辑器",
|
||||
"projects.detail.loading": "加载项目配置中...",
|
||||
"projects.detail.loadError": "加载配置失败:",
|
||||
"projects.detail.noProjectSelected": "未选择项目或配置未加载",
|
||||
"projects.detail.invalidJson": "JSON 格式无效。请在保存前修复错误。",
|
||||
"projects.detail.unsavedChanges": "您有未保存的更改。要在切换项目前保存吗?",
|
||||
"projects.detail.projectNotFound": "未找到路径为 \"{{path}}\" 的项目。",
|
||||
"projects.detail.backToProjects": "返回项目列表",
|
||||
"projects.detail.save": "保存",
|
||||
"projects.detail.saving": "保存中...",
|
||||
"projects.detail.selectProject": "选择项目:",
|
||||
"projects.detail.searchProject": "搜索项目...",
|
||||
"projects.detail.noProjectFound": "未找到项目。",
|
||||
"projects.detail.noProjectsMessage": "没有已打开的 Claude Code 项目。在项目中运行 `claude` 命令后,这个项目会自动出现在这里。",
|
||||
"notifications.title": "通知",
|
||||
"notifications.description": "接收 Claude Code 的通知",
|
||||
"notifications.general": "一般通知",
|
||||
"notifications.generalDescription": "当 Claude Code 发送通知时提醒",
|
||||
"notifications.toolUse": "工具使用通知",
|
||||
"notifications.toolUseDescription": "当 Claude Code 使用工具时提醒",
|
||||
"notifications.completion": "完成时通知",
|
||||
"notifications.completionDescription": "当 Claude Code 完成任务时提醒",
|
||||
"notifications.sending": "发送中...",
|
||||
"notifications.testGeneral": "测试通知",
|
||||
"notifications.testToolUse": "测试工具使用",
|
||||
"notifications.testCompletion": "测试完成",
|
||||
"glm.title": "在 Claude Code 中使用 GLM 4.6",
|
||||
"glm.tooltip": "「在公开基准与真实编程任务中,GLM-4.6 的代码能力对齐 Claude Sonnet 4,是国内已知的最好的 Coding 模型」 —— 智谱官方文档",
|
||||
"glm.startConfig": "开始配置",
|
||||
"glm.close": "关闭",
|
||||
"glm.zaiTitle": "Z.ai GLM",
|
||||
"glm.zhipuTitle": "智谱 GLM",
|
||||
"glm.configZai": "配置 Z.ai GLM",
|
||||
"glm.configZhipu": "配置智谱 GLM",
|
||||
"glm.description": "在 Claude Code 中使用{{provider}} GLM",
|
||||
"glm.zhipu": "智谱",
|
||||
"glm.chinaMainland": "中国大陆 (bigmodel.cn)",
|
||||
"glm.international": "国际 (z.ai)",
|
||||
"glm.step1": "第 1 步:购买 GLM 编码畅玩套餐",
|
||||
"glm.buyFromOfficial": "前往官网购买",
|
||||
"glm.discount": "使用此按钮购买时,下单立减10%金额(官方活动)",
|
||||
"glm.step2": "第 2 步:创建 API Key",
|
||||
"glm.enterConsole": "进入控制台",
|
||||
"glm.step3": "第 3 步:输入 API Key",
|
||||
"glm.zaiApiKeyPlaceholder": "请输入您的 Z.ai API Key",
|
||||
"glm.zhipuApiKeyPlaceholder": "请输入您的智谱 API Key",
|
||||
"glm.creating": "创建中...",
|
||||
"glm.createConfig": "创建配置",
|
||||
"glm.useZhipuGlm": "使用智谱GLM",
|
||||
"minimax.configMainland": "配置 MiniMax (中国)",
|
||||
"minimax.configInternational": "配置 MiniMax (国际)",
|
||||
"minimax.description": "在 Claude Code 中使用 MiniMax",
|
||||
"minimax.minimax": "MiniMax",
|
||||
"minimax.chinaMainland": "中国大陆 (minimaxi.com)",
|
||||
"minimax.international": "国际 (minimax.io)",
|
||||
"minimax.step1": "第 1 步:创建 API Key",
|
||||
"minimax.enterConsole": "进入控制台",
|
||||
"minimax.step2": "第 2 步:输入 API Key",
|
||||
"minimax.apiKeyPlaceholder": "请输入您的 MiniMax API Key",
|
||||
"minimax.creating": "创建中...",
|
||||
"minimax.createConfig": "创建配置",
|
||||
"minimax.useMiniMax": "使用 MiniMax",
|
||||
"minimax.mainlandTitle": "MiniMax 中国",
|
||||
"minimax.internationalTitle": "MiniMax 国际"
|
||||
}
|
||||
|
||||
+22
-21
@@ -1,36 +1,37 @@
|
||||
export const builtInMcpServers = [
|
||||
{
|
||||
name: "exa",
|
||||
source: "https://docs.exa.ai/reference/exa-mcp",
|
||||
description: "fast, efficient web context for coding agents",
|
||||
prefill: `"exa": {
|
||||
{
|
||||
name: "exa",
|
||||
source: "https://docs.exa.ai/reference/exa-mcp",
|
||||
description: "fast, efficient web context for coding agents",
|
||||
prefill: `"exa": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.exa.ai/mcp",
|
||||
"headers": {}
|
||||
}`
|
||||
},
|
||||
{
|
||||
name: "context7",
|
||||
source: "https://github.com/upstash/context7",
|
||||
description: "Up-to-date code documentation for LLMs and AI code editors",
|
||||
prefill: `"context7": {
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "context7",
|
||||
source: "https://github.com/upstash/context7",
|
||||
description: "Up-to-date code documentation for LLMs and AI code editors",
|
||||
prefill: `"context7": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.context7.com/mcp",
|
||||
"headers": {
|
||||
"CONTEXT7_API_KEY": ""
|
||||
}
|
||||
}`
|
||||
},
|
||||
{
|
||||
name: "github",
|
||||
source: "https://github.com/github/github-mcp-server/blob/main/docs/installation-guides/install-claude.md",
|
||||
description: "GitHub's official MCP Server",
|
||||
prefill: `"github": {
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "github",
|
||||
source:
|
||||
"https://github.com/github/github-mcp-server/blob/main/docs/installation-guides/install-claude.md",
|
||||
description: "GitHub's official MCP Server",
|
||||
prefill: `"github": {
|
||||
"type": "http",
|
||||
"url": "https://api.githubcopilot.com/mcp/",
|
||||
"headers": {
|
||||
"Authorization": "Bearer <YOUR_GITHUB_TOKEN>"
|
||||
}
|
||||
}`
|
||||
}
|
||||
}`,
|
||||
},
|
||||
];
|
||||
|
||||
+91
-91
@@ -1,110 +1,110 @@
|
||||
// Tremor Raw chartColors [v0.1.0]
|
||||
export type ColorUtility = "bg" | "stroke" | "fill" | "text"
|
||||
export type ColorUtility = "bg" | "stroke" | "fill" | "text";
|
||||
|
||||
export const chartColors = {
|
||||
blue: {
|
||||
bg: "bg-blue-500",
|
||||
stroke: "stroke-blue-500",
|
||||
fill: "fill-blue-500",
|
||||
text: "text-blue-500",
|
||||
},
|
||||
emerald: {
|
||||
bg: "bg-emerald-500",
|
||||
stroke: "stroke-emerald-500",
|
||||
fill: "fill-emerald-500",
|
||||
text: "text-emerald-500",
|
||||
},
|
||||
violet: {
|
||||
bg: "bg-violet-500",
|
||||
stroke: "stroke-violet-500",
|
||||
fill: "fill-violet-500",
|
||||
text: "text-violet-500",
|
||||
},
|
||||
amber: {
|
||||
bg: "bg-amber-500",
|
||||
stroke: "stroke-amber-500",
|
||||
fill: "fill-amber-500",
|
||||
text: "text-amber-500",
|
||||
},
|
||||
gray: {
|
||||
bg: "bg-gray-500",
|
||||
stroke: "stroke-gray-500",
|
||||
fill: "fill-gray-500",
|
||||
text: "text-gray-500",
|
||||
},
|
||||
cyan: {
|
||||
bg: "bg-cyan-500",
|
||||
stroke: "stroke-cyan-500",
|
||||
fill: "fill-cyan-500",
|
||||
text: "text-cyan-500",
|
||||
},
|
||||
pink: {
|
||||
bg: "bg-pink-500",
|
||||
stroke: "stroke-pink-500",
|
||||
fill: "fill-pink-500",
|
||||
text: "text-pink-500",
|
||||
},
|
||||
lime: {
|
||||
bg: "bg-lime-500",
|
||||
stroke: "stroke-lime-500",
|
||||
fill: "fill-lime-500",
|
||||
text: "text-lime-500",
|
||||
},
|
||||
fuchsia: {
|
||||
bg: "bg-fuchsia-500",
|
||||
stroke: "stroke-fuchsia-500",
|
||||
fill: "fill-fuchsia-500",
|
||||
text: "text-fuchsia-500",
|
||||
},
|
||||
blue: {
|
||||
bg: "bg-blue-500",
|
||||
stroke: "stroke-blue-500",
|
||||
fill: "fill-blue-500",
|
||||
text: "text-blue-500",
|
||||
},
|
||||
emerald: {
|
||||
bg: "bg-emerald-500",
|
||||
stroke: "stroke-emerald-500",
|
||||
fill: "fill-emerald-500",
|
||||
text: "text-emerald-500",
|
||||
},
|
||||
violet: {
|
||||
bg: "bg-violet-500",
|
||||
stroke: "stroke-violet-500",
|
||||
fill: "fill-violet-500",
|
||||
text: "text-violet-500",
|
||||
},
|
||||
amber: {
|
||||
bg: "bg-amber-500",
|
||||
stroke: "stroke-amber-500",
|
||||
fill: "fill-amber-500",
|
||||
text: "text-amber-500",
|
||||
},
|
||||
gray: {
|
||||
bg: "bg-gray-500",
|
||||
stroke: "stroke-gray-500",
|
||||
fill: "fill-gray-500",
|
||||
text: "text-gray-500",
|
||||
},
|
||||
cyan: {
|
||||
bg: "bg-cyan-500",
|
||||
stroke: "stroke-cyan-500",
|
||||
fill: "fill-cyan-500",
|
||||
text: "text-cyan-500",
|
||||
},
|
||||
pink: {
|
||||
bg: "bg-pink-500",
|
||||
stroke: "stroke-pink-500",
|
||||
fill: "fill-pink-500",
|
||||
text: "text-pink-500",
|
||||
},
|
||||
lime: {
|
||||
bg: "bg-lime-500",
|
||||
stroke: "stroke-lime-500",
|
||||
fill: "fill-lime-500",
|
||||
text: "text-lime-500",
|
||||
},
|
||||
fuchsia: {
|
||||
bg: "bg-fuchsia-500",
|
||||
stroke: "stroke-fuchsia-500",
|
||||
fill: "fill-fuchsia-500",
|
||||
text: "text-fuchsia-500",
|
||||
},
|
||||
} as const satisfies {
|
||||
[color: string]: {
|
||||
[key in ColorUtility]: string
|
||||
}
|
||||
}
|
||||
[color: string]: {
|
||||
[key in ColorUtility]: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type AvailableChartColorsKeys = keyof typeof chartColors
|
||||
export type AvailableChartColorsKeys = keyof typeof chartColors;
|
||||
|
||||
export const AvailableChartColors: AvailableChartColorsKeys[] = Object.keys(
|
||||
chartColors,
|
||||
) as Array<AvailableChartColorsKeys>
|
||||
chartColors,
|
||||
) as Array<AvailableChartColorsKeys>;
|
||||
|
||||
export const constructCategoryColors = (
|
||||
categories: string[],
|
||||
colors: AvailableChartColorsKeys[],
|
||||
categories: string[],
|
||||
colors: AvailableChartColorsKeys[],
|
||||
): Map<string, AvailableChartColorsKeys> => {
|
||||
const categoryColors = new Map<string, AvailableChartColorsKeys>()
|
||||
categories.forEach((category, index) => {
|
||||
categoryColors.set(category, colors[index % colors.length])
|
||||
})
|
||||
return categoryColors
|
||||
}
|
||||
const categoryColors = new Map<string, AvailableChartColorsKeys>();
|
||||
categories.forEach((category, index) => {
|
||||
categoryColors.set(category, colors[index % colors.length]);
|
||||
});
|
||||
return categoryColors;
|
||||
};
|
||||
|
||||
export const getColorClassName = (
|
||||
color: AvailableChartColorsKeys,
|
||||
type: ColorUtility,
|
||||
color: AvailableChartColorsKeys,
|
||||
type: ColorUtility,
|
||||
): string => {
|
||||
const fallbackColor = {
|
||||
bg: "bg-gray-500",
|
||||
stroke: "stroke-gray-500",
|
||||
fill: "fill-gray-500",
|
||||
text: "text-gray-500",
|
||||
}
|
||||
return chartColors[color]?.[type] ?? fallbackColor[type]
|
||||
}
|
||||
const fallbackColor = {
|
||||
bg: "bg-gray-500",
|
||||
stroke: "stroke-gray-500",
|
||||
fill: "fill-gray-500",
|
||||
text: "text-gray-500",
|
||||
};
|
||||
return chartColors[color]?.[type] ?? fallbackColor[type];
|
||||
};
|
||||
|
||||
export const getYAxisDomain = (
|
||||
autoMinValue: boolean,
|
||||
minValue: number | undefined,
|
||||
maxValue: number | undefined,
|
||||
autoMinValue: boolean,
|
||||
minValue: number | undefined,
|
||||
maxValue: number | undefined,
|
||||
) => {
|
||||
const minDomain = autoMinValue ? "auto" : minValue ?? 0
|
||||
const maxDomain = maxValue ?? "auto"
|
||||
return [minDomain, maxDomain]
|
||||
}
|
||||
const minDomain = autoMinValue ? "auto" : (minValue ?? 0);
|
||||
const maxDomain = maxValue ?? "auto";
|
||||
return [minDomain, maxDomain];
|
||||
};
|
||||
|
||||
export function hasOnlyOneValueForKey(data: any[], key: string): boolean {
|
||||
if (data.length === 0) return false
|
||||
if (data.length === 0) return false;
|
||||
|
||||
const firstValue = data[0][key]
|
||||
return data.every((item) => item[key] === firstValue)
|
||||
}
|
||||
const firstValue = data[0][key];
|
||||
return data.every((item) => item[key] === firstValue);
|
||||
}
|
||||
|
||||
+10
-10
@@ -1,15 +1,15 @@
|
||||
// Tremor useOnWindowResize [v0.0.2]
|
||||
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
export const useOnWindowResize = (handler: () => void) => {
|
||||
React.useEffect(() => {
|
||||
const handleResize = () => {
|
||||
handler()
|
||||
}
|
||||
handleResize()
|
||||
window.addEventListener("resize", handleResize)
|
||||
React.useEffect(() => {
|
||||
const handleResize = () => {
|
||||
handler();
|
||||
};
|
||||
handleResize();
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
return () => window.removeEventListener("resize", handleResize)
|
||||
}, [handler])
|
||||
}
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, [handler]);
|
||||
};
|
||||
|
||||
+417
-352
@@ -1,513 +1,578 @@
|
||||
import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
useSuspenseQuery,
|
||||
} from "@tanstack/react-query";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { toast } from "sonner";
|
||||
import { nanoid } from "nanoid";
|
||||
import { toast } from "sonner";
|
||||
import i18n from "../i18n";
|
||||
|
||||
export type ConfigType =
|
||||
| "user"
|
||||
| "enterprise_macos"
|
||||
| "enterprise_linux"
|
||||
| "enterprise_windows"
|
||||
| "mcp_macos"
|
||||
| "mcp_linux"
|
||||
| "mcp_windows";
|
||||
| "user"
|
||||
| "enterprise_macos"
|
||||
| "enterprise_linux"
|
||||
| "enterprise_windows"
|
||||
| "mcp_macos"
|
||||
| "mcp_linux"
|
||||
| "mcp_windows";
|
||||
|
||||
export interface ConfigFile {
|
||||
path: string;
|
||||
content: unknown;
|
||||
exists: boolean;
|
||||
path: string;
|
||||
content: unknown;
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
export interface ClaudeSettings {
|
||||
model?: string;
|
||||
permissions?: Record<string, any>;
|
||||
env?: Record<string, string>;
|
||||
[key: string]: any;
|
||||
model?: string;
|
||||
permissions?: Record<string, any>;
|
||||
env?: Record<string, string>;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ConfigStore {
|
||||
id: string; // nanoid(6)
|
||||
title: string;
|
||||
createdAt: number;
|
||||
settings: ClaudeSettings;
|
||||
using: boolean;
|
||||
id: string; // nanoid(6)
|
||||
title: string;
|
||||
createdAt: number;
|
||||
settings: ClaudeSettings;
|
||||
using: boolean;
|
||||
}
|
||||
|
||||
export interface McpServer {
|
||||
config: Record<string, any>;
|
||||
config: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface NotificationSettings {
|
||||
enable: boolean;
|
||||
enabled_hooks: string[];
|
||||
enable: boolean;
|
||||
enabled_hooks: string[];
|
||||
}
|
||||
|
||||
export interface CommandFile {
|
||||
name: string;
|
||||
content: string;
|
||||
exists: boolean;
|
||||
name: string;
|
||||
content: string;
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
export const useConfigFiles = () => {
|
||||
return useQuery({
|
||||
queryKey: ["config-files"],
|
||||
queryFn: () => invoke<ConfigType[]>("list_config_files"),
|
||||
});
|
||||
return useQuery({
|
||||
queryKey: ["config-files"],
|
||||
queryFn: () => invoke<ConfigType[]>("list_config_files"),
|
||||
});
|
||||
};
|
||||
|
||||
export const useConfigFile = (configType: ConfigType) => {
|
||||
return useQuery({
|
||||
queryKey: ["config-file", configType],
|
||||
queryFn: () => invoke<ConfigFile>("read_config_file", { configType }),
|
||||
enabled: !!configType,
|
||||
});
|
||||
return useQuery({
|
||||
queryKey: ["config-file", configType],
|
||||
queryFn: () => invoke<ConfigFile>("read_config_file", { configType }),
|
||||
enabled: !!configType,
|
||||
});
|
||||
};
|
||||
|
||||
export const useWriteConfigFile = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ configType, content }: { configType: ConfigType; content: unknown }) =>
|
||||
invoke<void>("write_config_file", { configType, content }),
|
||||
onSuccess: (_, variables) => {
|
||||
toast.success(i18n.t("toast.configSaved", { configType: variables.configType }));
|
||||
queryClient.invalidateQueries({ queryKey: ["config-file", variables.configType] });
|
||||
queryClient.invalidateQueries({ queryKey: ["config-files"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
toast.error(i18n.t("toast.configSaveFailed", { error: errorMessage }));
|
||||
},
|
||||
});
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
configType,
|
||||
content,
|
||||
}: {
|
||||
configType: ConfigType;
|
||||
content: unknown;
|
||||
}) => invoke<void>("write_config_file", { configType, content }),
|
||||
onSuccess: (_, variables) => {
|
||||
toast.success(
|
||||
i18n.t("toast.configSaved", { configType: variables.configType }),
|
||||
);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["config-file", variables.configType],
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ["config-files"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(i18n.t("toast.configSaveFailed", { error: errorMessage }));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const useBackupClaudeConfigs = () => {
|
||||
return useMutation({
|
||||
mutationFn: () => invoke<void>("backup_claude_configs"),
|
||||
onSuccess: () => {
|
||||
toast.success(i18n.t("toast.backupSuccess"));
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
toast.error(i18n.t("toast.backupFailed", { error: errorMessage }));
|
||||
},
|
||||
});
|
||||
return useMutation({
|
||||
mutationFn: () => invoke<void>("backup_claude_configs"),
|
||||
onSuccess: () => {
|
||||
toast.success(i18n.t("toast.backupSuccess"));
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(i18n.t("toast.backupFailed", { error: errorMessage }));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Store management hooks
|
||||
|
||||
export const useStores = (options?: {
|
||||
storeId?: string;
|
||||
}) => {
|
||||
return useSuspenseQuery({
|
||||
queryKey: ["stores", options?.storeId],
|
||||
queryFn: () => invoke<ConfigStore[]>("get_stores"),
|
||||
});
|
||||
export const useStores = (options?: { storeId?: string }) => {
|
||||
return useSuspenseQuery({
|
||||
queryKey: ["stores", options?.storeId],
|
||||
queryFn: () => invoke<ConfigStore[]>("get_stores"),
|
||||
});
|
||||
};
|
||||
|
||||
export const useStore = (storeId: string) => {
|
||||
return useSuspenseQuery({
|
||||
queryKey: ["store", storeId],
|
||||
queryFn: () => invoke<ConfigStore>("get_store", { storeId }),
|
||||
});
|
||||
return useSuspenseQuery({
|
||||
queryKey: ["store", storeId],
|
||||
queryFn: () => invoke<ConfigStore>("get_store", { storeId }),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const useCurrentStore = () => {
|
||||
return useSuspenseQuery({
|
||||
queryKey: ["current-store"],
|
||||
queryFn: () => invoke<ConfigStore | null>("get_current_store"),
|
||||
});
|
||||
return useSuspenseQuery({
|
||||
queryKey: ["current-store"],
|
||||
queryFn: () => invoke<ConfigStore | null>("get_current_store"),
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ title, settings }: { title: string; settings: unknown }) => {
|
||||
const id = nanoid(6);
|
||||
return invoke<ConfigStore>("create_config", { id, title, settings });
|
||||
},
|
||||
onSuccess: async () => {
|
||||
toast.success(i18n.t("toast.storeCreated"));
|
||||
queryClient.invalidateQueries({ queryKey: ["stores"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["current-store"] });
|
||||
await rebuildTrayMenu();
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
toast.error(i18n.t("toast.storeCreateFailed", { error: errorMessage }));
|
||||
},
|
||||
});
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
title,
|
||||
settings,
|
||||
}: {
|
||||
title: string;
|
||||
settings: unknown;
|
||||
}) => {
|
||||
const id = nanoid(6);
|
||||
return invoke<ConfigStore>("create_config", { id, title, settings });
|
||||
},
|
||||
onSuccess: async () => {
|
||||
toast.success(i18n.t("toast.storeCreated"));
|
||||
queryClient.invalidateQueries({ queryKey: ["stores"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["current-store"] });
|
||||
await rebuildTrayMenu();
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(i18n.t("toast.storeCreateFailed", { error: errorMessage }));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (body: {
|
||||
storeId: string;
|
||||
}) => invoke<void>("delete_config", {
|
||||
storeId: body.storeId,
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
toast.success(i18n.t("toast.storeDeleted"));
|
||||
queryClient.invalidateQueries({ queryKey: ["stores"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["current-store"] });
|
||||
await rebuildTrayMenu();
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
toast.error(i18n.t("toast.storeDeleteFailed", { error: errorMessage }));
|
||||
},
|
||||
});
|
||||
return useMutation({
|
||||
mutationFn: (body: { storeId: string }) =>
|
||||
invoke<void>("delete_config", {
|
||||
storeId: body.storeId,
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
toast.success(i18n.t("toast.storeDeleted"));
|
||||
queryClient.invalidateQueries({ queryKey: ["stores"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["current-store"] });
|
||||
await rebuildTrayMenu();
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(i18n.t("toast.storeDeleteFailed", { error: errorMessage }));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useSetUsingConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (storeId: string) => invoke<void>("set_using_config", { storeId }),
|
||||
onSuccess: () => {
|
||||
toast.success(i18n.t("toast.storeActivated"));
|
||||
queryClient.invalidateQueries({ queryKey: ["stores"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["current-store"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["config-file", "user"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
toast.error(i18n.t("toast.storeActivateFailed", { error: errorMessage }));
|
||||
},
|
||||
});
|
||||
return useMutation({
|
||||
mutationFn: (storeId: string) =>
|
||||
invoke<void>("set_using_config", { storeId }),
|
||||
onSuccess: () => {
|
||||
toast.success(i18n.t("toast.storeActivated"));
|
||||
queryClient.invalidateQueries({ queryKey: ["stores"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["current-store"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["config-file", "user"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(i18n.t("toast.storeActivateFailed", { error: errorMessage }));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useSetCurrentConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (storeId: string) => invoke<void>("set_using_config", { storeId }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["stores"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["current-store"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["config-file", "user"] });
|
||||
},
|
||||
});
|
||||
return useMutation({
|
||||
mutationFn: (storeId: string) =>
|
||||
invoke<void>("set_using_config", { storeId }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["stores"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["current-store"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["config-file", "user"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useResetToOriginalConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => invoke<void>("reset_to_original_config"),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["stores"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["current-store"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["config-file", "user"] });
|
||||
},
|
||||
});
|
||||
return useMutation({
|
||||
mutationFn: () => invoke<void>("reset_to_original_config"),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["stores"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["current-store"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["config-file", "user"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ storeId, title, settings }: { storeId: string; title: string; settings: unknown }) =>
|
||||
invoke<ConfigStore>("update_config", { storeId, title, settings }),
|
||||
onSuccess: async (data) => {
|
||||
if (data.using) {
|
||||
toast.success(i18n.t("toast.storeSavedAndActive", { title: data.title }));
|
||||
} else {
|
||||
toast.success(i18n.t("toast.storeSaved", { title: data.title }));
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ["stores"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["store", data.id] });
|
||||
queryClient.invalidateQueries({ queryKey: ["current-store"] });
|
||||
if (data.using) {
|
||||
queryClient.invalidateQueries({ queryKey: ["config-file", "user"] });
|
||||
}
|
||||
await rebuildTrayMenu();
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
toast.error(i18n.t("toast.storeSaveFailed", { error: errorMessage }));
|
||||
},
|
||||
});
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
storeId,
|
||||
title,
|
||||
settings,
|
||||
}: {
|
||||
storeId: string;
|
||||
title: string;
|
||||
settings: unknown;
|
||||
}) => invoke<ConfigStore>("update_config", { storeId, title, settings }),
|
||||
onSuccess: async (data) => {
|
||||
if (data.using) {
|
||||
toast.success(
|
||||
i18n.t("toast.storeSavedAndActive", { title: data.title }),
|
||||
);
|
||||
} else {
|
||||
toast.success(i18n.t("toast.storeSaved", { title: data.title }));
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ["stores"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["store", data.id] });
|
||||
queryClient.invalidateQueries({ queryKey: ["current-store"] });
|
||||
if (data.using) {
|
||||
queryClient.invalidateQueries({ queryKey: ["config-file", "user"] });
|
||||
}
|
||||
await rebuildTrayMenu();
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(i18n.t("toast.storeSaveFailed", { error: errorMessage }));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export interface UpdateInfo {
|
||||
available: boolean;
|
||||
version?: string;
|
||||
body?: string;
|
||||
date?: string;
|
||||
available: boolean;
|
||||
version?: string;
|
||||
body?: string;
|
||||
date?: string;
|
||||
}
|
||||
|
||||
export const useCheckForUpdates = () => {
|
||||
return useQuery({
|
||||
queryKey: ["check-updates"],
|
||||
queryFn: () => invoke<UpdateInfo>("check_for_updates"),
|
||||
refetchInterval: 1000 * 60 * 30, // Check every 30 minutes
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
return useQuery({
|
||||
queryKey: ["check-updates"],
|
||||
queryFn: () => invoke<UpdateInfo>("check_for_updates"),
|
||||
refetchInterval: 1000 * 60 * 30, // Check every 30 minutes
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
};
|
||||
|
||||
export const useInstallAndRestart = () => {
|
||||
return useMutation({
|
||||
mutationFn: () => {
|
||||
console.log("🚀 Frontend: Starting update installation");
|
||||
return invoke<void>("install_and_restart");
|
||||
},
|
||||
onSuccess: () => {
|
||||
console.log("✅ Frontend: Update installation completed, preparing to restart");
|
||||
toast.success(i18n.t("toast.updateInstalled"));
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log("❌ Frontend: Update installation failed", error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
toast.error(i18n.t("toast.updateInstallFailed", { error: errorMessage }));
|
||||
},
|
||||
});
|
||||
return useMutation({
|
||||
mutationFn: () => {
|
||||
console.log("🚀 Frontend: Starting update installation");
|
||||
return invoke<void>("install_and_restart");
|
||||
},
|
||||
onSuccess: () => {
|
||||
console.log(
|
||||
"✅ Frontend: Update installation completed, preparing to restart",
|
||||
);
|
||||
toast.success(i18n.t("toast.updateInstalled"));
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log("❌ Frontend: Update installation failed", error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(i18n.t("toast.updateInstallFailed", { error: errorMessage }));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// MCP Server management hooks
|
||||
|
||||
export const useGlobalMcpServers = () => {
|
||||
return useSuspenseQuery({
|
||||
queryKey: ["global-mcp-servers"],
|
||||
queryFn: () => invoke<Record<string, McpServer>>("get_global_mcp_servers"),
|
||||
});
|
||||
return useSuspenseQuery({
|
||||
queryKey: ["global-mcp-servers"],
|
||||
queryFn: () => invoke<Record<string, McpServer>>("get_global_mcp_servers"),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateGlobalMcpServer = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ serverName, serverConfig }: { serverName: string; serverConfig: Record<string, any> }) =>
|
||||
invoke<void>("update_global_mcp_server", { serverName, serverConfig }),
|
||||
onSuccess: () => {
|
||||
toast.success("MCP server configuration updated successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ["global-mcp-servers"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to update MCP server: ${errorMessage}`);
|
||||
},
|
||||
});
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
serverName,
|
||||
serverConfig,
|
||||
}: {
|
||||
serverName: string;
|
||||
serverConfig: Record<string, any>;
|
||||
}) =>
|
||||
invoke<void>("update_global_mcp_server", { serverName, serverConfig }),
|
||||
onSuccess: () => {
|
||||
toast.success("MCP server configuration updated successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ["global-mcp-servers"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to update MCP server: ${errorMessage}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddGlobalMcpServer = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ serverName, serverConfig }: { serverName: string; serverConfig: Record<string, any> }) =>
|
||||
invoke<void>("update_global_mcp_server", { serverName, serverConfig }),
|
||||
onSuccess: () => {
|
||||
toast.success("MCP server added successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ["global-mcp-servers"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to add MCP server: ${errorMessage}`);
|
||||
},
|
||||
});
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
serverName,
|
||||
serverConfig,
|
||||
}: {
|
||||
serverName: string;
|
||||
serverConfig: Record<string, any>;
|
||||
}) =>
|
||||
invoke<void>("update_global_mcp_server", { serverName, serverConfig }),
|
||||
onSuccess: () => {
|
||||
toast.success("MCP server added successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ["global-mcp-servers"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to add MCP server: ${errorMessage}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useCheckMcpServerExists = (serverName: string, options?: { enabled?: boolean }) => {
|
||||
return useQuery({
|
||||
queryKey: ["check-mcp-server-exists", serverName],
|
||||
queryFn: () => invoke<boolean>("check_mcp_server_exists", { serverName }),
|
||||
enabled: options?.enabled !== false && !!serverName, // Only run when serverName is provided
|
||||
});
|
||||
export const useCheckMcpServerExists = (
|
||||
serverName: string,
|
||||
options?: { enabled?: boolean },
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: ["check-mcp-server-exists", serverName],
|
||||
queryFn: () => invoke<boolean>("check_mcp_server_exists", { serverName }),
|
||||
enabled: options?.enabled !== false && !!serverName, // Only run when serverName is provided
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteGlobalMcpServer = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (serverName: string) => invoke<void>("delete_global_mcp_server", { serverName }),
|
||||
onSuccess: () => {
|
||||
toast.success("MCP server deleted successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ["global-mcp-servers"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to delete MCP server: ${errorMessage}`);
|
||||
},
|
||||
});
|
||||
return useMutation({
|
||||
mutationFn: (serverName: string) =>
|
||||
invoke<void>("delete_global_mcp_server", { serverName }),
|
||||
onSuccess: () => {
|
||||
toast.success("MCP server deleted successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ["global-mcp-servers"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to delete MCP server: ${errorMessage}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export interface UsageData {
|
||||
input_tokens?: number;
|
||||
cache_read_input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
input_tokens?: number;
|
||||
cache_read_input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
}
|
||||
|
||||
export interface ProjectUsageRecord {
|
||||
uuid: string;
|
||||
timestamp: string;
|
||||
model?: string;
|
||||
usage?: UsageData;
|
||||
uuid: string;
|
||||
timestamp: string;
|
||||
model?: string;
|
||||
usage?: UsageData;
|
||||
}
|
||||
|
||||
export const useProjectUsageFiles = () => {
|
||||
return useQuery({
|
||||
queryKey: ["project-usage-files"],
|
||||
queryFn: () => invoke<ProjectUsageRecord[]>("read_project_usage_files"),
|
||||
});
|
||||
return useQuery({
|
||||
queryKey: ["project-usage-files"],
|
||||
queryFn: () => invoke<ProjectUsageRecord[]>("read_project_usage_files"),
|
||||
});
|
||||
};
|
||||
|
||||
// Memory management hooks
|
||||
|
||||
export interface MemoryFile {
|
||||
path: string;
|
||||
content: string;
|
||||
exists: boolean;
|
||||
path: string;
|
||||
content: string;
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
export const useClaudeMemory = () => {
|
||||
return useSuspenseQuery({
|
||||
queryKey: ["claude-memory"],
|
||||
queryFn: () => invoke<MemoryFile>("read_claude_memory"),
|
||||
});
|
||||
return useSuspenseQuery({
|
||||
queryKey: ["claude-memory"],
|
||||
queryFn: () => invoke<MemoryFile>("read_claude_memory"),
|
||||
});
|
||||
};
|
||||
|
||||
export const useWriteClaudeMemory = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (content: string) => invoke<void>("write_claude_memory", { content }),
|
||||
onSuccess: () => {
|
||||
toast.success("Memory saved successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-memory"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to save memory: ${errorMessage}`);
|
||||
},
|
||||
});
|
||||
return useMutation({
|
||||
mutationFn: (content: string) =>
|
||||
invoke<void>("write_claude_memory", { content }),
|
||||
onSuccess: () => {
|
||||
toast.success("Memory saved successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-memory"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to save memory: ${errorMessage}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Projects management hooks
|
||||
|
||||
export interface ProjectConfig {
|
||||
path: string;
|
||||
config: Record<string, any>;
|
||||
path: string;
|
||||
config: Record<string, any>;
|
||||
}
|
||||
|
||||
export const useClaudeProjects = () => {
|
||||
return useQuery({
|
||||
queryKey: ["claude-projects"],
|
||||
queryFn: () => invoke<ProjectConfig[]>("read_claude_projects"),
|
||||
});
|
||||
return useQuery({
|
||||
queryKey: ["claude-projects"],
|
||||
queryFn: () => invoke<ProjectConfig[]>("read_claude_projects"),
|
||||
});
|
||||
};
|
||||
|
||||
export interface ClaudeConfigFile {
|
||||
path: string;
|
||||
content: unknown;
|
||||
exists: boolean;
|
||||
path: string;
|
||||
content: unknown;
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
export const useClaudeConfigFile = () => {
|
||||
return useQuery({
|
||||
queryKey: ["claude-config-file"],
|
||||
queryFn: () => invoke<ClaudeConfigFile>("read_claude_config_file"),
|
||||
});
|
||||
return useQuery({
|
||||
queryKey: ["claude-config-file"],
|
||||
queryFn: () => invoke<ClaudeConfigFile>("read_claude_config_file"),
|
||||
});
|
||||
};
|
||||
|
||||
export const useWriteClaudeConfigFile = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (content: unknown) => invoke<void>("write_claude_config_file", { content }),
|
||||
onSuccess: () => {
|
||||
toast.success("Claude configuration saved successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-config-file"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-projects"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to save Claude configuration: ${errorMessage}`);
|
||||
},
|
||||
});
|
||||
return useMutation({
|
||||
mutationFn: (content: unknown) =>
|
||||
invoke<void>("write_claude_config_file", { content }),
|
||||
onSuccess: () => {
|
||||
toast.success("Claude configuration saved successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-config-file"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-projects"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to save Claude configuration: ${errorMessage}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Notification settings hooks
|
||||
|
||||
export const useNotificationSettings = () => {
|
||||
return useQuery({
|
||||
queryKey: ["notification-settings"],
|
||||
queryFn: () => invoke<NotificationSettings | null>("get_notification_settings"),
|
||||
});
|
||||
return useQuery({
|
||||
queryKey: ["notification-settings"],
|
||||
queryFn: () =>
|
||||
invoke<NotificationSettings | null>("get_notification_settings"),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateNotificationSettings = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (settings: NotificationSettings) => {
|
||||
return invoke<void>("update_notification_settings", { settings });
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Notification settings updated successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ["notification-settings"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to update notification settings: ${errorMessage}`);
|
||||
},
|
||||
});
|
||||
return useMutation({
|
||||
mutationFn: (settings: NotificationSettings) => {
|
||||
return invoke<void>("update_notification_settings", { settings });
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Notification settings updated successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ["notification-settings"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to update notification settings: ${errorMessage}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Command management hooks
|
||||
export const useClaudeCommands = () =>
|
||||
useQuery({
|
||||
queryKey: ["claude-commands"],
|
||||
queryFn: () => invoke<CommandFile[]>("read_claude_commands"),
|
||||
});
|
||||
useQuery({
|
||||
queryKey: ["claude-commands"],
|
||||
queryFn: () => invoke<CommandFile[]>("read_claude_commands"),
|
||||
});
|
||||
|
||||
export const useWriteClaudeCommand = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ commandName, content }: { commandName: string; content: string }) =>
|
||||
invoke<void>("write_claude_command", { commandName, content }),
|
||||
onSuccess: () => {
|
||||
toast.success(i18n.t("toast.commandSaved"));
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-commands"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
toast.error(i18n.t("toast.commandSaveFailed", { error: errorMessage }));
|
||||
},
|
||||
});
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
commandName,
|
||||
content,
|
||||
}: {
|
||||
commandName: string;
|
||||
content: string;
|
||||
}) => invoke<void>("write_claude_command", { commandName, content }),
|
||||
onSuccess: () => {
|
||||
toast.success(i18n.t("toast.commandSaved"));
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-commands"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(i18n.t("toast.commandSaveFailed", { error: errorMessage }));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteClaudeCommand = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (commandName: string) =>
|
||||
invoke<void>("delete_claude_command", { commandName }),
|
||||
onSuccess: () => {
|
||||
toast.success(i18n.t("toast.commandDeleted"));
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-commands"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
toast.error(i18n.t("toast.commandDeleteFailed", { error: errorMessage }));
|
||||
},
|
||||
});
|
||||
return useMutation({
|
||||
mutationFn: (commandName: string) =>
|
||||
invoke<void>("delete_claude_command", { commandName }),
|
||||
onSuccess: () => {
|
||||
toast.success(i18n.t("toast.commandDeleted"));
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-commands"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(i18n.t("toast.commandDeleteFailed", { error: errorMessage }));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to rebuild tray menu
|
||||
const rebuildTrayMenu = async () => {
|
||||
try {
|
||||
await invoke<void>("rebuild_tray_menu_command");
|
||||
} catch (error) {
|
||||
console.error("Failed to rebuild tray menu:", error);
|
||||
}
|
||||
try {
|
||||
await invoke<void>("rebuild_tray_menu_command");
|
||||
} catch (error) {
|
||||
console.error("Failed to rebuild tray menu:", error);
|
||||
}
|
||||
};
|
||||
|
||||
+14
-11
@@ -1,17 +1,20 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export enum TrackEvent {
|
||||
AppLaunched = "app_launched",
|
||||
AppLaunched = "app_launched",
|
||||
}
|
||||
|
||||
// Analytics tracking function
|
||||
export const track = async (event: string, properties: Record<string, any> = {}) => {
|
||||
if (!import.meta.env.PROD) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await invoke<void>("track", { event, properties });
|
||||
} catch (error) {
|
||||
console.error("Failed to track event:", error);
|
||||
}
|
||||
};
|
||||
export const track = async (
|
||||
event: string,
|
||||
properties: Record<string, any> = {},
|
||||
) => {
|
||||
if (!import.meta.env.PROD) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await invoke<void>("track", { event, properties });
|
||||
} catch (error) {
|
||||
console.error("Failed to track event:", error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useTheme } from "next-themes";
|
||||
import { vscodeDark, vscodeLight } from "@uiw/codemirror-theme-vscode";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
export function useCodeMirrorTheme() {
|
||||
const { theme, systemTheme } = useTheme();
|
||||
const { theme, systemTheme } = useTheme();
|
||||
|
||||
// Determine the actual theme being applied
|
||||
// If theme is "system", use the systemTheme value
|
||||
const actualTheme = theme === "system" ? systemTheme : theme;
|
||||
// Determine the actual theme being applied
|
||||
// If theme is "system", use the systemTheme value
|
||||
const actualTheme = theme === "system" ? systemTheme : theme;
|
||||
|
||||
return actualTheme === "dark" ? vscodeDark : vscodeLight;
|
||||
}
|
||||
return actualTheme === "dark" ? vscodeDark : vscodeLight;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { useEffect, useRef } from "react"
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export function useOnWindowResize(callback: () => void) {
|
||||
const callbackRef = useRef(callback)
|
||||
callbackRef.current = callback
|
||||
const callbackRef = useRef(callback);
|
||||
callbackRef.current = callback;
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
callbackRef.current()
|
||||
}
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
callbackRef.current();
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize)
|
||||
handleResize() // Call once on mount
|
||||
window.addEventListener("resize", handleResize);
|
||||
handleResize(); // Call once on mount
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize)
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
+14
-14
@@ -1,24 +1,24 @@
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function cx(...inputs: ClassValue[]) {
|
||||
return clsx(inputs)
|
||||
return clsx(inputs);
|
||||
}
|
||||
|
||||
export const isMacOS = platform() === 'macos';
|
||||
export const isMacOS = platform() === "macos";
|
||||
|
||||
export function formatLargeNumber(num: number): string {
|
||||
if (num >= 1e9) {
|
||||
return (num / 1e9).toFixed(1) + 'B';
|
||||
} else if (num >= 1e6) {
|
||||
return (num / 1e6).toFixed(1) + 'M';
|
||||
} else if (num >= 1e3) {
|
||||
return (num / 1e3).toFixed(1) + 'K';
|
||||
}
|
||||
return num.toString();
|
||||
}
|
||||
if (num >= 1e9) {
|
||||
return (num / 1e9).toFixed(1) + "B";
|
||||
} else if (num >= 1e6) {
|
||||
return (num / 1e6).toFixed(1) + "M";
|
||||
} else if (num >= 1e3) {
|
||||
return (num / 1e3).toFixed(1) + "K";
|
||||
}
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
+26
-28
@@ -1,40 +1,38 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Router } from "./router";
|
||||
import { Toaster } from "./components/ui/sonner";
|
||||
import { ErrorBoundary } from "./components/ErrorBoundary";
|
||||
import { ThemeProvider } from "./components/theme-provider";
|
||||
import './i18n';
|
||||
import './tw.css'
|
||||
import { track } from "./lib/tracker";
|
||||
import { TrackEvent } from "./lib/tracker";
|
||||
|
||||
import { Toaster } from "./components/ui/sonner";
|
||||
import { Router } from "./router";
|
||||
import "./i18n";
|
||||
import "./tw.css";
|
||||
import { TrackEvent, track } from "./lib/tracker";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
track(TrackEvent.AppLaunched);
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<ErrorBoundary>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<Router />
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</ErrorBoundary>
|
||||
</React.StrictMode>,
|
||||
<React.StrictMode>
|
||||
<ErrorBoundary>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<Router />
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</ErrorBoundary>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
+321
-272
@@ -1,300 +1,349 @@
|
||||
import { useState, Suspense } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { TerminalIcon, PlusIcon, SaveIcon, TrashIcon } from "lucide-react";
|
||||
import { useClaudeCommands, useWriteClaudeCommand, useDeleteClaudeCommand } from "@/lib/query";
|
||||
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
|
||||
import { ask, message } from "@tauri-apps/plugin-dialog";
|
||||
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
|
||||
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
|
||||
import { PlusIcon, SaveIcon, TerminalIcon, TrashIcon } from "lucide-react";
|
||||
import { Suspense, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
useClaudeCommands,
|
||||
useDeleteClaudeCommand,
|
||||
useWriteClaudeCommand,
|
||||
} from "@/lib/query";
|
||||
import { useCodeMirrorTheme } from "@/lib/use-codemirror-theme";
|
||||
|
||||
function CommandsPageContent() {
|
||||
const { t } = useTranslation();
|
||||
const { data: commands, isLoading, error } = useClaudeCommands();
|
||||
const writeCommand = useWriteClaudeCommand();
|
||||
const deleteCommand = useDeleteClaudeCommand();
|
||||
const [commandEdits, setCommandEdits] = useState<Record<string, string>>({});
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const codeMirrorTheme = useCodeMirrorTheme();
|
||||
const { t } = useTranslation();
|
||||
const { data: commands, isLoading, error } = useClaudeCommands();
|
||||
const writeCommand = useWriteClaudeCommand();
|
||||
const deleteCommand = useDeleteClaudeCommand();
|
||||
const [commandEdits, setCommandEdits] = useState<Record<string, string>>({});
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const codeMirrorTheme = useCodeMirrorTheme();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">{t('loading')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">{t("loading")}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center text-red-500">
|
||||
{t('commands.error', { error: error.message })}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center text-red-500">
|
||||
{t("commands.error", { error: error.message })}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleContentChange = (commandName: string, content: string) => {
|
||||
setCommandEdits(prev => ({
|
||||
...prev,
|
||||
[commandName]: content
|
||||
}));
|
||||
};
|
||||
const handleContentChange = (commandName: string, content: string) => {
|
||||
setCommandEdits((prev) => ({
|
||||
...prev,
|
||||
[commandName]: content,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSaveCommand = async (commandName: string) => {
|
||||
const content = commandEdits[commandName];
|
||||
if (content === undefined) return;
|
||||
const handleSaveCommand = async (commandName: string) => {
|
||||
const content = commandEdits[commandName];
|
||||
if (content === undefined) return;
|
||||
|
||||
writeCommand.mutate({
|
||||
commandName,
|
||||
content
|
||||
});
|
||||
};
|
||||
writeCommand.mutate({
|
||||
commandName,
|
||||
content,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteCommand = async (commandName: string) => {
|
||||
const confirmed = await ask(
|
||||
t('commands.deleteConfirm', { commandName }),
|
||||
{ title: t('commands.deleteTitle'), kind: "warning" }
|
||||
);
|
||||
const handleDeleteCommand = async (commandName: string) => {
|
||||
const confirmed = await ask(t("commands.deleteConfirm", { commandName }), {
|
||||
title: t("commands.deleteTitle"),
|
||||
kind: "warning",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
deleteCommand.mutate(commandName);
|
||||
}
|
||||
};
|
||||
if (confirmed) {
|
||||
deleteCommand.mutate(commandName);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10" data-tauri-drag-region>
|
||||
<div data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>{t('commands.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground" data-tauri-drag-region>
|
||||
{t('commands.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" className="text-muted-foreground" size="sm">
|
||||
<PlusIcon size={14} />
|
||||
{t('commands.addCommand')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="">{t('commands.addCommandTitle')}</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground text-sm">
|
||||
{t('commands.addCommandDescription')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<CreateCommandPanel onClose={() => setIsDialogOpen(false)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
<div className="">
|
||||
{!commands || commands.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
{t('commands.noCommands')}
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="h-full">
|
||||
<div className="">
|
||||
<Accordion type="multiple" className="">
|
||||
{commands.map((command) => (
|
||||
<AccordionItem key={command.name} value={command.name} className="bg-card">
|
||||
<AccordionTrigger className="hover:no-underline px-4 py-2 bg-card hover:bg-accent duration-150">
|
||||
<div className="flex items-center gap-2">
|
||||
<TerminalIcon size={12} />
|
||||
<span className="font-medium">{command.name}</span>
|
||||
<span className="text-sm text-muted-foreground font-normal">
|
||||
{`~/.claude/commands/${command.name}.md`}
|
||||
</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-3">
|
||||
<div className="px-3 pt-3 space-y-3">
|
||||
<div className="rounded-lg overflow-hidden border">
|
||||
<CodeMirror
|
||||
value={commandEdits[command.name] !== undefined ? commandEdits[command.name] : command.content}
|
||||
height="180px"
|
||||
theme={codeMirrorTheme}
|
||||
onChange={(value) => handleContentChange(command.name, value)}
|
||||
placeholder={t('commands.contentPlaceholder')}
|
||||
extensions={[
|
||||
markdown({
|
||||
base: markdownLanguage,
|
||||
}),
|
||||
EditorView.lineWrapping
|
||||
]}
|
||||
basicSetup={{
|
||||
lineNumbers: false,
|
||||
highlightActiveLineGutter: true,
|
||||
foldGutter: false,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: true,
|
||||
bracketMatching: true,
|
||||
closeBrackets: true,
|
||||
autocompletion: true,
|
||||
highlightActiveLine: true,
|
||||
highlightSelectionMatches: true,
|
||||
searchKeymap: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between bg-card">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleSaveCommand(command.name)}
|
||||
disabled={writeCommand.isPending || commandEdits[command.name] === undefined}
|
||||
size="sm"
|
||||
>
|
||||
<SaveIcon size={14} className="" />
|
||||
{writeCommand.isPending ? t('commands.saving') : t('commands.save')}
|
||||
</Button>
|
||||
return (
|
||||
<div className="">
|
||||
<div
|
||||
className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>
|
||||
{t("commands.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground" data-tauri-drag-region>
|
||||
{t("commands.description")}
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" className="text-muted-foreground" size="sm">
|
||||
<PlusIcon size={14} />
|
||||
{t("commands.addCommand")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="">
|
||||
{t("commands.addCommandTitle")}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground text-sm">
|
||||
{t("commands.addCommandDescription")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<CreateCommandPanel onClose={() => setIsDialogOpen(false)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
<div className="">
|
||||
{!commands || commands.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
{t("commands.noCommands")}
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="h-full">
|
||||
<div className="">
|
||||
<Accordion type="multiple" className="">
|
||||
{commands.map((command) => (
|
||||
<AccordionItem
|
||||
key={command.name}
|
||||
value={command.name}
|
||||
className="bg-card"
|
||||
>
|
||||
<AccordionTrigger className="hover:no-underline px-4 py-2 bg-card hover:bg-accent duration-150">
|
||||
<div className="flex items-center gap-2">
|
||||
<TerminalIcon size={12} />
|
||||
<span className="font-medium">{command.name}</span>
|
||||
<span className="text-sm text-muted-foreground font-normal">
|
||||
{`~/.claude/commands/${command.name}.md`}
|
||||
</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-3">
|
||||
<div className="px-3 pt-3 space-y-3">
|
||||
<div className="rounded-lg overflow-hidden border">
|
||||
<CodeMirror
|
||||
value={
|
||||
commandEdits[command.name] !== undefined
|
||||
? commandEdits[command.name]
|
||||
: command.content
|
||||
}
|
||||
height="180px"
|
||||
theme={codeMirrorTheme}
|
||||
onChange={(value) =>
|
||||
handleContentChange(command.name, value)
|
||||
}
|
||||
placeholder={t("commands.contentPlaceholder")}
|
||||
extensions={[
|
||||
markdown({
|
||||
base: markdownLanguage,
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
]}
|
||||
basicSetup={{
|
||||
lineNumbers: false,
|
||||
highlightActiveLineGutter: true,
|
||||
foldGutter: false,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: true,
|
||||
bracketMatching: true,
|
||||
closeBrackets: true,
|
||||
autocompletion: true,
|
||||
highlightActiveLine: true,
|
||||
highlightSelectionMatches: true,
|
||||
searchKeymap: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between bg-card">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleSaveCommand(command.name)}
|
||||
disabled={
|
||||
writeCommand.isPending ||
|
||||
commandEdits[command.name] === undefined
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
<SaveIcon size={14} className="" />
|
||||
{writeCommand.isPending
|
||||
? t("commands.saving")
|
||||
: t("commands.save")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteCommand(command.name)}
|
||||
disabled={deleteCommand.isPending}
|
||||
>
|
||||
<TrashIcon size={14} className="" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteCommand(command.name)}
|
||||
disabled={deleteCommand.isPending}
|
||||
>
|
||||
<TrashIcon size={14} className="" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CommandsPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">{t('loading')}</div>
|
||||
</div>
|
||||
}>
|
||||
<CommandsPageContent />
|
||||
</Suspense>
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">{t("loading")}</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<CommandsPageContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateCommandPanel({ onClose }: { onClose?: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const [commandName, setCommandName] = useState("");
|
||||
const [commandContent, setCommandContent] = useState("");
|
||||
const writeCommand = useWriteClaudeCommand();
|
||||
const { data: commands } = useClaudeCommands();
|
||||
const codeMirrorTheme = useCodeMirrorTheme();
|
||||
const { t } = useTranslation();
|
||||
const [commandName, setCommandName] = useState("");
|
||||
const [commandContent, setCommandContent] = useState("");
|
||||
const writeCommand = useWriteClaudeCommand();
|
||||
const { data: commands } = useClaudeCommands();
|
||||
const codeMirrorTheme = useCodeMirrorTheme();
|
||||
|
||||
const handleCreateCommand = async () => {
|
||||
// Validate command name
|
||||
if (!commandName.trim()) {
|
||||
await message(t('commands.emptyNameError'), {
|
||||
title: t('commands.validationError'),
|
||||
kind: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
const handleCreateCommand = async () => {
|
||||
// Validate command name
|
||||
if (!commandName.trim()) {
|
||||
await message(t("commands.emptyNameError"), {
|
||||
title: t("commands.validationError"),
|
||||
kind: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if command already exists
|
||||
const exists = commands && commands.some(cmd => cmd.name === commandName);
|
||||
if (exists) {
|
||||
await message(t('commands.commandExistsError', { commandName }), {
|
||||
title: t('commands.commandExistsTitle'),
|
||||
kind: "info"
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Check if command already exists
|
||||
const exists = commands && commands.some((cmd) => cmd.name === commandName);
|
||||
if (exists) {
|
||||
await message(t("commands.commandExistsError", { commandName }), {
|
||||
title: t("commands.commandExistsTitle"),
|
||||
kind: "info",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate content
|
||||
if (!commandContent.trim()) {
|
||||
await message(t('commands.emptyContentError'), {
|
||||
title: t('commands.validationError'),
|
||||
kind: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Validate content
|
||||
if (!commandContent.trim()) {
|
||||
await message(t("commands.emptyContentError"), {
|
||||
title: t("commands.validationError"),
|
||||
kind: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
writeCommand.mutate({
|
||||
commandName,
|
||||
content: commandContent
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
setCommandName("");
|
||||
setCommandContent("");
|
||||
onClose?.();
|
||||
}
|
||||
});
|
||||
};
|
||||
writeCommand.mutate(
|
||||
{
|
||||
commandName,
|
||||
content: commandContent,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setCommandName("");
|
||||
setCommandContent("");
|
||||
onClose?.();
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="command-name">{t('commands.commandName')}</Label>
|
||||
<Input
|
||||
id="command-name"
|
||||
value={commandName}
|
||||
onChange={(e) => setCommandName(e.target.value)}
|
||||
placeholder={t('commands.commandNamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
return (
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="command-name">{t("commands.commandName")}</Label>
|
||||
<Input
|
||||
id="command-name"
|
||||
value={commandName}
|
||||
onChange={(e) => setCommandName(e.target.value)}
|
||||
placeholder={t("commands.commandNamePlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="command-content">{t('commands.commandContent')}</Label>
|
||||
<div className="rounded-lg overflow-hidden border">
|
||||
<CodeMirror
|
||||
value={commandContent}
|
||||
onChange={(value) => setCommandContent(value)}
|
||||
height="200px"
|
||||
theme={codeMirrorTheme}
|
||||
placeholder={t('commands.contentPlaceholder')}
|
||||
extensions={[
|
||||
markdown({
|
||||
base: markdownLanguage,
|
||||
}),
|
||||
EditorView.lineWrapping
|
||||
]}
|
||||
basicSetup={{
|
||||
lineNumbers: false,
|
||||
highlightActiveLineGutter: true,
|
||||
foldGutter: false,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: true,
|
||||
bracketMatching: true,
|
||||
closeBrackets: true,
|
||||
autocompletion: true,
|
||||
highlightActiveLine: true,
|
||||
highlightSelectionMatches: true,
|
||||
searchKeymap: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="command-content">{t("commands.commandContent")}</Label>
|
||||
<div className="rounded-lg overflow-hidden border">
|
||||
<CodeMirror
|
||||
value={commandContent}
|
||||
onChange={(value) => setCommandContent(value)}
|
||||
height="200px"
|
||||
theme={codeMirrorTheme}
|
||||
placeholder={t("commands.contentPlaceholder")}
|
||||
extensions={[
|
||||
markdown({
|
||||
base: markdownLanguage,
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
]}
|
||||
basicSetup={{
|
||||
lineNumbers: false,
|
||||
highlightActiveLineGutter: true,
|
||||
foldGutter: false,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: true,
|
||||
bracketMatching: true,
|
||||
closeBrackets: true,
|
||||
autocompletion: true,
|
||||
highlightActiveLine: true,
|
||||
highlightSelectionMatches: true,
|
||||
searchKeymap: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={handleCreateCommand}
|
||||
disabled={!commandName.trim() || !commandContent.trim() || writeCommand.isPending}
|
||||
>
|
||||
{writeCommand.isPending ? t('commands.creating') : t('commands.create')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={handleCreateCommand}
|
||||
disabled={
|
||||
!commandName.trim() ||
|
||||
!commandContent.trim() ||
|
||||
writeCommand.isPending
|
||||
}
|
||||
>
|
||||
{writeCommand.isPending
|
||||
? t("commands.creating")
|
||||
: t("commands.create")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+855
-794
File diff suppressed because it is too large
Load Diff
+208
-170
@@ -1,194 +1,232 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useStores, useSetCurrentConfig, useCreateConfig, useResetToOriginalConfig } from "../lib/query";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Minimax, ZAI } from "@lobehub/icons";
|
||||
import { EllipsisVerticalIcon, PencilLineIcon, PlusIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { GLMDialog } from "@/components/GLMBanner";
|
||||
import { MiniMaxDialog } from "@/components/MiniMaxDialog";
|
||||
import { Minimax, ZAI } from "@lobehub/icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ButtonGroup } from "@/components/ui/button-group";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
useCreateConfig,
|
||||
useResetToOriginalConfig,
|
||||
useSetCurrentConfig,
|
||||
useStores,
|
||||
} from "../lib/query";
|
||||
|
||||
export function ConfigSwitcherPage() {
|
||||
return (
|
||||
<div className="">
|
||||
<section>
|
||||
<ConfigStores />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="">
|
||||
<section>
|
||||
<ConfigStores />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfigStores() {
|
||||
const { t } = useTranslation();
|
||||
const { data: stores } = useStores();
|
||||
const setCurrentStoreMutation = useSetCurrentConfig();
|
||||
const resetToOriginalMutation = useResetToOriginalConfig();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const isOriginalConfigActive = !stores.some(store => store.using);
|
||||
|
||||
const handleStoreClick = (storeId: string, isCurrentStore: boolean) => {
|
||||
if (!isCurrentStore) {
|
||||
setCurrentStoreMutation.mutate(storeId);
|
||||
}
|
||||
};
|
||||
const { t } = useTranslation();
|
||||
const { data: stores } = useStores();
|
||||
const setCurrentStoreMutation = useSetCurrentConfig();
|
||||
const resetToOriginalMutation = useResetToOriginalConfig();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleOriginalConfigClick = () => {
|
||||
if (!isOriginalConfigActive) {
|
||||
resetToOriginalMutation.mutate();
|
||||
}
|
||||
};
|
||||
const isOriginalConfigActive = !stores.some((store) => store.using);
|
||||
|
||||
const createStoreMutation = useCreateConfig();
|
||||
const handleStoreClick = (storeId: string, isCurrentStore: boolean) => {
|
||||
if (!isCurrentStore) {
|
||||
setCurrentStoreMutation.mutate(storeId);
|
||||
}
|
||||
};
|
||||
|
||||
const onCreateStore = async () => {
|
||||
const store = await createStoreMutation.mutateAsync({
|
||||
title: t("configSwitcher.newConfig"),
|
||||
settings: {},
|
||||
});
|
||||
navigate(`/edit/${store.id}`);
|
||||
};
|
||||
const handleOriginalConfigClick = () => {
|
||||
if (!isOriginalConfigActive) {
|
||||
resetToOriginalMutation.mutate();
|
||||
}
|
||||
};
|
||||
|
||||
if (stores.length === 0) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-screen" data-tauri-drag-region>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Button variant="ghost" onClick={onCreateStore} className="">
|
||||
<PlusIcon size={14} />
|
||||
{t("configSwitcher.createConfig")}
|
||||
</Button>
|
||||
const createStoreMutation = useCreateConfig();
|
||||
|
||||
<p className="text-sm text-muted-foreground" data-tauri-drag-region>
|
||||
{t("configSwitcher.description")}
|
||||
</p>
|
||||
const onCreateStore = async () => {
|
||||
const store = await createStoreMutation.mutateAsync({
|
||||
title: t("configSwitcher.newConfig"),
|
||||
settings: {},
|
||||
});
|
||||
navigate(`/edit/${store.id}`);
|
||||
};
|
||||
|
||||
<div className="mt-4 space-y-2">
|
||||
<GLMDialog
|
||||
trigger={
|
||||
<Button variant="ghost" className="text-muted-foreground text-sm" size="sm">
|
||||
<ZAI />
|
||||
{t('glm.useZhipuGlm')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<MiniMaxDialog
|
||||
trigger={
|
||||
<Button variant="ghost" className="text-muted-foreground text-sm" size="sm">
|
||||
<Minimax />
|
||||
{t('minimax.useMiniMax')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (stores.length === 0) {
|
||||
return (
|
||||
<div
|
||||
className="flex justify-center items-center h-screen"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Button variant="ghost" onClick={onCreateStore} className="">
|
||||
<PlusIcon size={14} />
|
||||
{t("configSwitcher.createConfig")}
|
||||
</Button>
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10" data-tauri-drag-region>
|
||||
<div data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>{t("configSwitcher.title")}</h3>
|
||||
<p className="text-sm text-muted-foreground" data-tauri-drag-region>
|
||||
{t("configSwitcher.description")}
|
||||
</p>
|
||||
</div>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" onClick={onCreateStore} className="text-muted-foreground" size="sm">
|
||||
<PlusIcon size={14} />
|
||||
{t("configSwitcher.createConfig")}
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="text-muted-foreground" size="sm">
|
||||
<EllipsisVerticalIcon size={14} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<GLMDialog
|
||||
trigger={
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<ZAI />
|
||||
{t('glm.useZhipuGlm')}
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
/>
|
||||
<MiniMaxDialog
|
||||
trigger={
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<Minimax />
|
||||
{t('minimax.useMiniMax')}
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
<p className="text-sm text-muted-foreground" data-tauri-drag-region>
|
||||
{t("configSwitcher.description")}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<div className="mt-4 space-y-2">
|
||||
<GLMDialog
|
||||
trigger={
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-muted-foreground text-sm"
|
||||
size="sm"
|
||||
>
|
||||
<ZAI />
|
||||
{t("glm.useZhipuGlm")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<MiniMaxDialog
|
||||
trigger={
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-muted-foreground text-sm"
|
||||
size="sm"
|
||||
>
|
||||
<Minimax />
|
||||
{t("minimax.useMiniMax")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
{/* <GLMBanner className="mx-4 mt-4" /> */}
|
||||
return (
|
||||
<div className="">
|
||||
<div
|
||||
className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>
|
||||
{t("configSwitcher.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground" data-tauri-drag-region>
|
||||
{t("configSwitcher.description")}
|
||||
</p>
|
||||
</div>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onCreateStore}
|
||||
className="text-muted-foreground"
|
||||
size="sm"
|
||||
>
|
||||
<PlusIcon size={14} />
|
||||
{t("configSwitcher.createConfig")}
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-muted-foreground"
|
||||
size="sm"
|
||||
>
|
||||
<EllipsisVerticalIcon size={14} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<GLMDialog
|
||||
trigger={
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<ZAI />
|
||||
{t("glm.useZhipuGlm")}
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
/>
|
||||
<MiniMaxDialog
|
||||
trigger={
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<Minimax />
|
||||
{t("minimax.useMiniMax")}
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 lg:grid-cols-4 gap-3 p-4">
|
||||
{/* Fixed Claude Original Config Item */}
|
||||
<div
|
||||
role="button"
|
||||
onClick={handleOriginalConfigClick}
|
||||
className={cn("border rounded-xl p-3 h-[100px] flex flex-col justify-between transition-colors", {
|
||||
"bg-primary/10 border-primary border-2": isOriginalConfigActive,
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
<div>{t("configSwitcher.originalConfig")}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
{t("configSwitcher.originalConfigDescription")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <GLMBanner className="mx-4 mt-4" /> */}
|
||||
|
||||
{stores.map((store) => {
|
||||
const isCurrentStore = store.using
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
key={store.id}
|
||||
onClick={() => handleStoreClick(store.id, isCurrentStore)}
|
||||
className={cn("border rounded-xl p-3 h-[100px] flex flex-col justify-between transition-colors disabled:opacity-50", {
|
||||
"bg-primary/10 border-primary border-2": isCurrentStore,
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
<div>{store.title}</div>
|
||||
{store.settings.env?.ANTHROPIC_BASE_URL && (
|
||||
<div className="text-xs text-muted-foreground mt-1 truncate " title={store.settings.env.ANTHROPIC_BASE_URL}>
|
||||
{store.settings.env.ANTHROPIC_BASE_URL}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 lg:grid-cols-4 gap-3 p-4">
|
||||
{/* Fixed Claude Original Config Item */}
|
||||
<div
|
||||
role="button"
|
||||
onClick={handleOriginalConfigClick}
|
||||
className={cn(
|
||||
"border rounded-xl p-3 h-[100px] flex flex-col justify-between transition-colors",
|
||||
{
|
||||
"bg-primary/10 border-primary border-2": isOriginalConfigActive,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<div>{t("configSwitcher.originalConfig")}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
{t("configSwitcher.originalConfigDescription")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button className="hover:bg-primary/10 rounded-lg p-2 hover:text-primary" onClick={e => {
|
||||
e.stopPropagation()
|
||||
navigate(`/edit/${store.id}`)
|
||||
}}>
|
||||
<PencilLineIcon
|
||||
className="text-muted-foreground"
|
||||
size={14}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{stores.map((store) => {
|
||||
const isCurrentStore = store.using;
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
key={store.id}
|
||||
onClick={() => handleStoreClick(store.id, isCurrentStore)}
|
||||
className={cn(
|
||||
"border rounded-xl p-3 h-[100px] flex flex-col justify-between transition-colors disabled:opacity-50",
|
||||
{
|
||||
"bg-primary/10 border-primary border-2": isCurrentStore,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<div>{store.title}</div>
|
||||
{store.settings.env?.ANTHROPIC_BASE_URL && (
|
||||
<div
|
||||
className="text-xs text-muted-foreground mt-1 truncate "
|
||||
title={store.settings.env.ANTHROPIC_BASE_URL}
|
||||
>
|
||||
{store.settings.env.ANTHROPIC_BASE_URL}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
className="hover:bg-primary/10 rounded-lg p-2 hover:text-primary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/edit/${store.id}`);
|
||||
}}
|
||||
>
|
||||
<PencilLineIcon className="text-muted-foreground" size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+416
-354
@@ -1,99 +1,132 @@
|
||||
import { useState, Suspense } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { ExternalLinkIcon, HammerIcon, PlusIcon, SaveIcon, TrashIcon } from "lucide-react";
|
||||
import { useGlobalMcpServers, useUpdateGlobalMcpServer, useAddGlobalMcpServer, useDeleteGlobalMcpServer, type McpServer } from "@/lib/query";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { ask, message } from "@tauri-apps/plugin-dialog";
|
||||
import { match } from "ts-pattern";
|
||||
import CodeMirror from "@uiw/react-codemirror";
|
||||
import { json } from "@codemirror/lang-json";
|
||||
import { useCodeMirrorTheme } from "@/lib/use-codemirror-theme";
|
||||
import { ask, message } from "@tauri-apps/plugin-dialog";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import CodeMirror from "@uiw/react-codemirror";
|
||||
import {
|
||||
ExternalLinkIcon,
|
||||
HammerIcon,
|
||||
PlusIcon,
|
||||
SaveIcon,
|
||||
TrashIcon,
|
||||
} from "lucide-react";
|
||||
import { Suspense, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { match } from "ts-pattern";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { builtInMcpServers } from "@/lib/builtInMCP";
|
||||
|
||||
import {
|
||||
type McpServer,
|
||||
useAddGlobalMcpServer,
|
||||
useDeleteGlobalMcpServer,
|
||||
useGlobalMcpServers,
|
||||
useUpdateGlobalMcpServer,
|
||||
} from "@/lib/query";
|
||||
import { useCodeMirrorTheme } from "@/lib/use-codemirror-theme";
|
||||
|
||||
function MCPPageContent() {
|
||||
const { t } = useTranslation();
|
||||
const { data: mcpServers } = useGlobalMcpServers();
|
||||
const updateMcpServer = useUpdateGlobalMcpServer();
|
||||
const deleteMcpServer = useDeleteGlobalMcpServer();
|
||||
const [serverConfigs, setServerConfigs] = useState<Record<string, string>>({});
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const codeMirrorTheme = useCodeMirrorTheme();
|
||||
const { t } = useTranslation();
|
||||
const { data: mcpServers } = useGlobalMcpServers();
|
||||
const updateMcpServer = useUpdateGlobalMcpServer();
|
||||
const deleteMcpServer = useDeleteGlobalMcpServer();
|
||||
const [serverConfigs, setServerConfigs] = useState<Record<string, string>>(
|
||||
{},
|
||||
);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const codeMirrorTheme = useCodeMirrorTheme();
|
||||
|
||||
const handleConfigChange = (serverName: string, configText: string) => {
|
||||
setServerConfigs(prev => ({
|
||||
...prev,
|
||||
[serverName]: configText
|
||||
}));
|
||||
};
|
||||
const handleConfigChange = (serverName: string, configText: string) => {
|
||||
setServerConfigs((prev) => ({
|
||||
...prev,
|
||||
[serverName]: configText,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSaveConfig = async (serverName: string) => {
|
||||
const configText = serverConfigs[serverName];
|
||||
if (!configText) return;
|
||||
const handleSaveConfig = async (serverName: string) => {
|
||||
const configText = serverConfigs[serverName];
|
||||
if (!configText) return;
|
||||
|
||||
try {
|
||||
const configObject = JSON.parse(configText);
|
||||
updateMcpServer.mutate({
|
||||
serverName,
|
||||
serverConfig: configObject
|
||||
});
|
||||
} catch (error) {
|
||||
await message(t('mcp.invalidJsonError', { serverName }), {
|
||||
title: t('mcp.invalidJsonTitle'),
|
||||
kind: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
try {
|
||||
const configObject = JSON.parse(configText);
|
||||
updateMcpServer.mutate({
|
||||
serverName,
|
||||
serverConfig: configObject,
|
||||
});
|
||||
} catch (error) {
|
||||
await message(t("mcp.invalidJsonError", { serverName }), {
|
||||
title: t("mcp.invalidJsonTitle"),
|
||||
kind: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteServer = async (serverName: string) => {
|
||||
// Show confirmation dialog
|
||||
const confirmed = await ask(
|
||||
t('mcp.deleteServerConfirm', { serverName }),
|
||||
{ title: t('mcp.deleteServerTitle'), kind: "warning" }
|
||||
);
|
||||
const handleDeleteServer = async (serverName: string) => {
|
||||
// Show confirmation dialog
|
||||
const confirmed = await ask(t("mcp.deleteServerConfirm", { serverName }), {
|
||||
title: t("mcp.deleteServerTitle"),
|
||||
kind: "warning",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
deleteMcpServer.mutate(serverName);
|
||||
}
|
||||
};
|
||||
if (confirmed) {
|
||||
deleteMcpServer.mutate(serverName);
|
||||
}
|
||||
};
|
||||
|
||||
const formatConfigForDisplay = (server: McpServer): string => {
|
||||
return JSON.stringify(server, null, 2);
|
||||
};
|
||||
|
||||
const formatConfigForDisplay = (server: McpServer): string => {
|
||||
return JSON.stringify(server, null, 2);
|
||||
};
|
||||
const serverEntries = Object.entries(mcpServers || {}).sort(([a], [b]) =>
|
||||
a.localeCompare(b),
|
||||
);
|
||||
|
||||
const serverEntries = Object.entries(mcpServers || {}).sort(([a], [b]) => a.localeCompare(b));
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10" data-tauri-drag-region>
|
||||
<div data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>{t('mcp.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground" data-tauri-drag-region>
|
||||
{t('mcp.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" className="text-muted-foreground" size="sm">
|
||||
<PlusIcon size={14} />
|
||||
{t('mcp.addServer')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[700px] h-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-primary text-sm">{t('mcp.addServerTitle')}</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground text-sm">
|
||||
{t('mcp.addServerDescription')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-3 mt-3">
|
||||
<MCPCreatePanel onClose={() => setIsDialogOpen(false)} />
|
||||
</div>
|
||||
{/* <div className="flex justify-end">
|
||||
return (
|
||||
<div className="">
|
||||
<div
|
||||
className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>
|
||||
{t("mcp.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground" data-tauri-drag-region>
|
||||
{t("mcp.description")}
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" className="text-muted-foreground" size="sm">
|
||||
<PlusIcon size={14} />
|
||||
{t("mcp.addServer")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[700px] h-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-primary text-sm">
|
||||
{t("mcp.addServerTitle")}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground text-sm">
|
||||
{t("mcp.addServerDescription")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-3 mt-3">
|
||||
<MCPCreatePanel onClose={() => setIsDialogOpen(false)} />
|
||||
</div>
|
||||
{/* <div className="flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
@@ -101,307 +134,336 @@ function MCPPageContent() {
|
||||
关闭
|
||||
</Button>
|
||||
</div> */}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
<div className="">
|
||||
{serverEntries.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
{t('mcp.noServersConfigured')}
|
||||
</div>
|
||||
) : (
|
||||
<Accordion type="multiple" className="">
|
||||
{serverEntries.map(([serverName, serverConfig]) => (
|
||||
<AccordionItem key={serverName} value={serverName} className="bg-card">
|
||||
<AccordionTrigger className="hover:no-underline px-4 py-2 bg-card hover:bg-accent duration-150">
|
||||
<div className="flex items-center gap-2">
|
||||
<HammerIcon size={12} />
|
||||
<span className="font-medium">{serverName}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-3">
|
||||
<div className="px-3 pt-3 space-y-3">
|
||||
<div className="rounded-lg overflow-hidden border">
|
||||
<CodeMirror
|
||||
value={serverConfigs[serverName] || formatConfigForDisplay(serverConfig)}
|
||||
height="180px"
|
||||
theme={codeMirrorTheme}
|
||||
extensions={[json()]}
|
||||
onChange={(value) => handleConfigChange(serverName, value)}
|
||||
placeholder="Enter MCP server configuration as JSON"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between bg-card">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleSaveConfig(serverName)}
|
||||
disabled={updateMcpServer.isPending}
|
||||
size="sm"
|
||||
>
|
||||
<SaveIcon size={14} className="" />
|
||||
{updateMcpServer.isPending ? t('mcp.saving') : t('mcp.save')}
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
<div className="">
|
||||
{serverEntries.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
{t("mcp.noServersConfigured")}
|
||||
</div>
|
||||
) : (
|
||||
<Accordion type="multiple" className="">
|
||||
{serverEntries.map(([serverName, serverConfig]) => (
|
||||
<AccordionItem
|
||||
key={serverName}
|
||||
value={serverName}
|
||||
className="bg-card"
|
||||
>
|
||||
<AccordionTrigger className="hover:no-underline px-4 py-2 bg-card hover:bg-accent duration-150">
|
||||
<div className="flex items-center gap-2">
|
||||
<HammerIcon size={12} />
|
||||
<span className="font-medium">{serverName}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-3">
|
||||
<div className="px-3 pt-3 space-y-3">
|
||||
<div className="rounded-lg overflow-hidden border">
|
||||
<CodeMirror
|
||||
value={
|
||||
serverConfigs[serverName] ||
|
||||
formatConfigForDisplay(serverConfig)
|
||||
}
|
||||
height="180px"
|
||||
theme={codeMirrorTheme}
|
||||
extensions={[json()]}
|
||||
onChange={(value) =>
|
||||
handleConfigChange(serverName, value)
|
||||
}
|
||||
placeholder="Enter MCP server configuration as JSON"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between bg-card">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleSaveConfig(serverName)}
|
||||
disabled={updateMcpServer.isPending}
|
||||
size="sm"
|
||||
>
|
||||
<SaveIcon size={14} className="" />
|
||||
{updateMcpServer.isPending
|
||||
? t("mcp.saving")
|
||||
: t("mcp.save")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteServer(serverName)}
|
||||
disabled={deleteMcpServer.isPending}
|
||||
>
|
||||
<TrashIcon size={14} className="" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteServer(serverName)}
|
||||
disabled={deleteMcpServer.isPending}
|
||||
>
|
||||
<TrashIcon size={14} className="" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MCPPage() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">Loading MCP servers...</div>
|
||||
</div>
|
||||
}>
|
||||
<MCPPageContent />
|
||||
</Suspense>
|
||||
);
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">Loading MCP servers...</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<MCPPageContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function MCPCreatePanel({ onClose }: { onClose?: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const [currentTab, setCurrentTab] = useState<("recommend" | "manual")>("recommend");
|
||||
const { t } = useTranslation();
|
||||
const [currentTab, setCurrentTab] = useState<"recommend" | "manual">(
|
||||
"recommend",
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="flex mb-3 gap-1">
|
||||
<Button size="sm" variant={
|
||||
currentTab === "recommend" ? "secondary" : "ghost"
|
||||
} className="text-sm" onClick={() => setCurrentTab("recommend")}>
|
||||
{t('mcp.recommend')}
|
||||
</Button>
|
||||
<Button size="sm" variant={
|
||||
currentTab === "manual" ? "secondary" : "ghost"
|
||||
} className="text-sm" onClick={() => setCurrentTab("manual")}>
|
||||
{t('mcp.custom')}
|
||||
</Button>
|
||||
</div>
|
||||
return (
|
||||
<div className="">
|
||||
<div className="flex mb-3 gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={currentTab === "recommend" ? "secondary" : "ghost"}
|
||||
className="text-sm"
|
||||
onClick={() => setCurrentTab("recommend")}
|
||||
>
|
||||
{t("mcp.recommend")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={currentTab === "manual" ? "secondary" : "ghost"}
|
||||
className="text-sm"
|
||||
onClick={() => setCurrentTab("manual")}
|
||||
>
|
||||
{t("mcp.custom")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{match(currentTab)
|
||||
.with("recommend", () => {
|
||||
return (
|
||||
<RecommendMCPPanel onClose={onClose} />
|
||||
)
|
||||
})
|
||||
.with("manual", () => {
|
||||
return (
|
||||
<CustomMCPPanel onClose={onClose} />
|
||||
)
|
||||
})
|
||||
.exhaustive()
|
||||
}
|
||||
</div>
|
||||
);
|
||||
{match(currentTab)
|
||||
.with("recommend", () => {
|
||||
return <RecommendMCPPanel onClose={onClose} />;
|
||||
})
|
||||
.with("manual", () => {
|
||||
return <CustomMCPPanel onClose={onClose} />;
|
||||
})
|
||||
.exhaustive()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RecommendMCPPanel({ onClose }: { onClose?: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const addMcpServer = useAddGlobalMcpServer();
|
||||
const { data: mcpServers } = useGlobalMcpServers();
|
||||
const { t } = useTranslation();
|
||||
const addMcpServer = useAddGlobalMcpServer();
|
||||
const { data: mcpServers } = useGlobalMcpServers();
|
||||
|
||||
const handleAddMcpServer = async (mcpServer: typeof builtInMcpServers[0]) => {
|
||||
try {
|
||||
// Check if MCP server already exists using cached data
|
||||
const exists = mcpServers && Object.keys(mcpServers).includes(mcpServer.name);
|
||||
const handleAddMcpServer = async (
|
||||
mcpServer: (typeof builtInMcpServers)[0],
|
||||
) => {
|
||||
try {
|
||||
// Check if MCP server already exists using cached data
|
||||
const exists =
|
||||
mcpServers && Object.keys(mcpServers).includes(mcpServer.name);
|
||||
|
||||
if (exists) {
|
||||
await message(t('mcp.serverExistsError', { serverName: mcpServer.name }), {
|
||||
title: t('mcp.serverExistsTitle'),
|
||||
kind: "info"
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (exists) {
|
||||
await message(
|
||||
t("mcp.serverExistsError", { serverName: mcpServer.name }),
|
||||
{
|
||||
title: t("mcp.serverExistsTitle"),
|
||||
kind: "info",
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show confirmation dialog
|
||||
const confirmed = await ask(
|
||||
t('mcp.addServerConfirm', { serverName: mcpServer.name }),
|
||||
{ title: t('mcp.addServerTitle'), kind: "info" }
|
||||
);
|
||||
// Show confirmation dialog
|
||||
const confirmed = await ask(
|
||||
t("mcp.addServerConfirm", { serverName: mcpServer.name }),
|
||||
{ title: t("mcp.addServerTitle"), kind: "info" },
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
// Parse the prefill JSON to get the config object
|
||||
const configObject = JSON.parse(`{${mcpServer.prefill}}`);
|
||||
if (confirmed) {
|
||||
// Parse the prefill JSON to get the config object
|
||||
const configObject = JSON.parse(`{${mcpServer.prefill}}`);
|
||||
|
||||
addMcpServer.mutate({
|
||||
serverName: mcpServer.name,
|
||||
serverConfig: configObject[mcpServer.name]
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
// Close dialog after successful addition
|
||||
onClose?.();
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to add MCP server:", error);
|
||||
await message(t('mcp.addServerError'), {
|
||||
title: "Error",
|
||||
kind: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
addMcpServer.mutate(
|
||||
{
|
||||
serverName: mcpServer.name,
|
||||
serverConfig: configObject[mcpServer.name],
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
// Close dialog after successful addition
|
||||
onClose?.();
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to add MCP server:", error);
|
||||
await message(t("mcp.addServerError"), {
|
||||
title: "Error",
|
||||
kind: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-5">
|
||||
{builtInMcpServers.map((mcpServer) => (
|
||||
<div
|
||||
key={mcpServer.name}
|
||||
className="border p-3 rounded-md h-[120px] flex justify-between flex-col hover:bg-primary/10 hover:border-primary/20 hover:text-primary cursor-default"
|
||||
onClick={() => handleAddMcpServer(mcpServer)}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="font-bold text-primary">{mcpServer.name}</h3>
|
||||
<a
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
openUrl(mcpServer.source)
|
||||
}}
|
||||
className="text-sm text-muted-foreground flex items-center gap-1 hover:underline"
|
||||
>
|
||||
<ExternalLinkIcon size={12} />
|
||||
{t('mcp.source')}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-muted-foreground">{mcpServer.description}</p>
|
||||
{/* <Button size="sm" variant="outline" className="w-full text-sm">
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-5">
|
||||
{builtInMcpServers.map((mcpServer) => (
|
||||
<div
|
||||
key={mcpServer.name}
|
||||
className="border p-3 rounded-md h-[120px] flex justify-between flex-col hover:bg-primary/10 hover:border-primary/20 hover:text-primary cursor-default"
|
||||
onClick={() => handleAddMcpServer(mcpServer)}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="font-bold text-primary">{mcpServer.name}</h3>
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openUrl(mcpServer.source);
|
||||
}}
|
||||
className="text-sm text-muted-foreground flex items-center gap-1 hover:underline"
|
||||
>
|
||||
<ExternalLinkIcon size={12} />
|
||||
{t("mcp.source")}
|
||||
</a>
|
||||
</div>
|
||||
<div></div>
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{mcpServer.description}
|
||||
</p>
|
||||
{/* <Button size="sm" variant="outline" className="w-full text-sm">
|
||||
<PlusIcon />
|
||||
添加
|
||||
</Button> */}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CustomMCPPanel({ onClose }: { onClose?: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const [customConfig, setCustomConfig] = useState("");
|
||||
const addMcpServer = useAddGlobalMcpServer();
|
||||
const { data: mcpServers } = useGlobalMcpServers();
|
||||
const codeMirrorTheme = useCodeMirrorTheme();
|
||||
const { t } = useTranslation();
|
||||
const [customConfig, setCustomConfig] = useState("");
|
||||
const addMcpServer = useAddGlobalMcpServer();
|
||||
const { data: mcpServers } = useGlobalMcpServers();
|
||||
const codeMirrorTheme = useCodeMirrorTheme();
|
||||
|
||||
const handleAddCustomMcpServer = async () => {
|
||||
try {
|
||||
// Validate JSON format
|
||||
let configObject;
|
||||
try {
|
||||
configObject = JSON.parse(customConfig);
|
||||
} catch (error) {
|
||||
await message(t('mcp.addCustomServerError'), {
|
||||
title: t('mcp.invalidJsonTitle'),
|
||||
kind: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
const handleAddCustomMcpServer = async () => {
|
||||
try {
|
||||
// Validate JSON format
|
||||
let configObject;
|
||||
try {
|
||||
configObject = JSON.parse(customConfig);
|
||||
} catch (error) {
|
||||
await message(t("mcp.addCustomServerError"), {
|
||||
title: t("mcp.invalidJsonTitle"),
|
||||
kind: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's an object with at least one server
|
||||
if (typeof configObject !== "object" || configObject === null) {
|
||||
await message(t('mcp.invalidConfigError'), {
|
||||
title: "Invalid Configuration",
|
||||
kind: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Check if it's an object with at least one server
|
||||
if (typeof configObject !== "object" || configObject === null) {
|
||||
await message(t("mcp.invalidConfigError"), {
|
||||
title: "Invalid Configuration",
|
||||
kind: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const serverNames = Object.keys(configObject);
|
||||
if (serverNames.length === 0) {
|
||||
await message(t('mcp.noServersError'), {
|
||||
title: "Invalid Configuration",
|
||||
kind: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
const serverNames = Object.keys(configObject);
|
||||
if (serverNames.length === 0) {
|
||||
await message(t("mcp.noServersError"), {
|
||||
title: "Invalid Configuration",
|
||||
kind: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for duplicate server names
|
||||
const existingNames = mcpServers ? Object.keys(mcpServers) : [];
|
||||
const duplicateNames = serverNames.filter(name => existingNames.includes(name));
|
||||
// Check for duplicate server names
|
||||
const existingNames = mcpServers ? Object.keys(mcpServers) : [];
|
||||
const duplicateNames = serverNames.filter((name) =>
|
||||
existingNames.includes(name),
|
||||
);
|
||||
|
||||
if (duplicateNames.length > 0) {
|
||||
await message(t('mcp.duplicateServersError', { servers: duplicateNames.join(", ") }), {
|
||||
title: t('mcp.duplicateServersTitle'),
|
||||
kind: "warning"
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (duplicateNames.length > 0) {
|
||||
await message(
|
||||
t("mcp.duplicateServersError", {
|
||||
servers: duplicateNames.join(", "),
|
||||
}),
|
||||
{
|
||||
title: t("mcp.duplicateServersTitle"),
|
||||
kind: "warning",
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show confirmation dialog
|
||||
const confirmed = await ask(
|
||||
t('mcp.addCustomServersConfirm', { count: serverNames.length }),
|
||||
{ title: t('mcp.addCustomServersTitle'), kind: "info" }
|
||||
);
|
||||
// Show confirmation dialog
|
||||
const confirmed = await ask(
|
||||
t("mcp.addCustomServersConfirm", { count: serverNames.length }),
|
||||
{ title: t("mcp.addCustomServersTitle"), kind: "info" },
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
// Add each server
|
||||
for (const [serverName, serverConfig] of Object.entries(configObject)) {
|
||||
addMcpServer.mutate({
|
||||
serverName,
|
||||
serverConfig: serverConfig as Record<string, any>
|
||||
});
|
||||
}
|
||||
if (confirmed) {
|
||||
// Add each server
|
||||
for (const [serverName, serverConfig] of Object.entries(configObject)) {
|
||||
addMcpServer.mutate({
|
||||
serverName,
|
||||
serverConfig: serverConfig as Record<string, any>,
|
||||
});
|
||||
}
|
||||
|
||||
// Clear input and close dialog
|
||||
setCustomConfig("");
|
||||
onClose?.();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to add custom MCP servers:", error);
|
||||
await message(t('mcp.addServerError'), {
|
||||
title: "Error",
|
||||
kind: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
// Clear input and close dialog
|
||||
setCustomConfig("");
|
||||
onClose?.();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to add custom MCP servers:", error);
|
||||
await message(t("mcp.addServerError"), {
|
||||
title: "Error",
|
||||
kind: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-lg overflow-hidden border">
|
||||
<CodeMirror
|
||||
value={customConfig}
|
||||
onChange={(value) => setCustomConfig(value)}
|
||||
height="240px"
|
||||
theme={codeMirrorTheme}
|
||||
extensions={[json()]}
|
||||
placeholder={t('mcp.customPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
return (
|
||||
<div className="">
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-lg overflow-hidden border">
|
||||
<CodeMirror
|
||||
value={customConfig}
|
||||
onChange={(value) => setCustomConfig(value)}
|
||||
height="240px"
|
||||
theme={codeMirrorTheme}
|
||||
extensions={[json()]}
|
||||
placeholder={t("mcp.customPlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="w-full text-sm"
|
||||
onClick={handleAddCustomMcpServer}
|
||||
disabled={!customConfig.trim()}
|
||||
>
|
||||
{t('mcp.add')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="w-full text-sm"
|
||||
onClick={handleAddCustomMcpServer}
|
||||
disabled={!customConfig.trim()}
|
||||
>
|
||||
{t("mcp.add")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+135
-119
@@ -1,141 +1,157 @@
|
||||
import { useState, useEffect, Suspense } from "react";
|
||||
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
|
||||
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
|
||||
import { SaveIcon } from "lucide-react";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { SaveIcon } from "lucide-react";
|
||||
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
|
||||
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
|
||||
import { useClaudeMemory, useWriteClaudeMemory } from "@/lib/query";
|
||||
import { useCodeMirrorTheme } from "@/lib/use-codemirror-theme";
|
||||
|
||||
function MemoryPageHeader({ onSave, saving }: { onSave: () => void; saving: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
function MemoryPageHeader({
|
||||
onSave,
|
||||
saving,
|
||||
}: {
|
||||
onSave: () => void;
|
||||
saving: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10" data-tauri-drag-region>
|
||||
<div data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>{t("memory.title")}</h3>
|
||||
<p className="text-sm text-muted-foreground">{t("memory.description")}</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={onSave}
|
||||
disabled={saving}
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<SaveIcon className="w-4 h-4" />
|
||||
{saving ? t("memory.saving") : t("memory.save")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>
|
||||
{t("memory.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("memory.description")}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={onSave}
|
||||
disabled={saving}
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<SaveIcon className="w-4 h-4" />
|
||||
{saving ? t("memory.saving") : t("memory.save")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MemoryPageSkeleton() {
|
||||
return (
|
||||
<div className="flex flex-col h-screen">
|
||||
<div className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10" data-tauri-drag-region>
|
||||
<div data-tauri-drag-region>
|
||||
<Skeleton className="h-6 w-16 mb-2" />
|
||||
<Skeleton className="h-4 w-64" />
|
||||
</div>
|
||||
<Skeleton className="h-8 w-16" />
|
||||
</div>
|
||||
<div className="flex-1 p-4 overflow-hidden">
|
||||
<div className="rounded-lg overflow-hidden border h-full">
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="space-y-2 w-full max-w-2xl">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="flex flex-col h-screen">
|
||||
<div
|
||||
className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div data-tauri-drag-region>
|
||||
<Skeleton className="h-6 w-16 mb-2" />
|
||||
<Skeleton className="h-4 w-64" />
|
||||
</div>
|
||||
<Skeleton className="h-8 w-16" />
|
||||
</div>
|
||||
<div className="flex-1 p-4 overflow-hidden">
|
||||
<div className="rounded-lg overflow-hidden border h-full">
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="space-y-2 w-full max-w-2xl">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MemoryPageContent() {
|
||||
const { data: memoryData } = useClaudeMemory();
|
||||
const { mutate: saveMemory, isPending: saving } = useWriteClaudeMemory();
|
||||
const [content, setContent] = useState<string>("");
|
||||
const codeMirrorTheme = useCodeMirrorTheme();
|
||||
const { data: memoryData } = useClaudeMemory();
|
||||
const { mutate: saveMemory, isPending: saving } = useWriteClaudeMemory();
|
||||
const [content, setContent] = useState<string>("");
|
||||
const codeMirrorTheme = useCodeMirrorTheme();
|
||||
|
||||
// Update local content when memory data loads
|
||||
useEffect(() => {
|
||||
if (memoryData?.content) {
|
||||
setContent(memoryData.content);
|
||||
}
|
||||
}, [memoryData]);
|
||||
// Update local content when memory data loads
|
||||
useEffect(() => {
|
||||
if (memoryData?.content) {
|
||||
setContent(memoryData.content);
|
||||
}
|
||||
}, [memoryData]);
|
||||
|
||||
const handleSave = () => {
|
||||
saveMemory(content);
|
||||
};
|
||||
const handleSave = () => {
|
||||
saveMemory(content);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Cmd+S or Ctrl+S to save
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
}
|
||||
};
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Cmd+S or Ctrl+S to save
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
}
|
||||
};
|
||||
|
||||
// Add keyboard event listener
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [content]);
|
||||
// Add keyboard event listener
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [content]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen">
|
||||
<MemoryPageHeader onSave={handleSave} saving={saving} />
|
||||
return (
|
||||
<div className="flex flex-col h-screen">
|
||||
<MemoryPageHeader onSave={handleSave} saving={saving} />
|
||||
|
||||
<div className="flex-1 p-4 overflow-hidden">
|
||||
<div className="rounded-lg overflow-hidden border h-full">
|
||||
<CodeMirror
|
||||
value={content}
|
||||
height="100%"
|
||||
extensions={[
|
||||
markdown({
|
||||
base: markdownLanguage,
|
||||
}),
|
||||
EditorView.lineWrapping
|
||||
]}
|
||||
placeholder="~/.claude/CLAUDE.md"
|
||||
onChange={(value) => setContent(value)}
|
||||
theme={codeMirrorTheme}
|
||||
basicSetup={{
|
||||
lineNumbers: false,
|
||||
highlightActiveLineGutter: true,
|
||||
foldGutter: false,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: true,
|
||||
bracketMatching: true,
|
||||
closeBrackets: true,
|
||||
autocompletion: true,
|
||||
highlightActiveLine: true,
|
||||
highlightSelectionMatches: true,
|
||||
searchKeymap: false,
|
||||
}}
|
||||
className="h-full"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className="flex-1 p-4 overflow-hidden">
|
||||
<div className="rounded-lg overflow-hidden border h-full">
|
||||
<CodeMirror
|
||||
value={content}
|
||||
height="100%"
|
||||
extensions={[
|
||||
markdown({
|
||||
base: markdownLanguage,
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
]}
|
||||
placeholder="~/.claude/CLAUDE.md"
|
||||
onChange={(value) => setContent(value)}
|
||||
theme={codeMirrorTheme}
|
||||
basicSetup={{
|
||||
lineNumbers: false,
|
||||
highlightActiveLineGutter: true,
|
||||
foldGutter: false,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: true,
|
||||
bracketMatching: true,
|
||||
closeBrackets: true,
|
||||
autocompletion: true,
|
||||
highlightActiveLine: true,
|
||||
highlightSelectionMatches: true,
|
||||
searchKeymap: false,
|
||||
}}
|
||||
className="h-full"
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MemoryPage() {
|
||||
return (
|
||||
<Suspense fallback={<MemoryPageSkeleton />}>
|
||||
<MemoryPageContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Suspense fallback={<MemoryPageSkeleton />}>
|
||||
<MemoryPageContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
+143
-120
@@ -1,132 +1,155 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNotificationSettings, useUpdateNotificationSettings } from "@/lib/query";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
useNotificationSettings,
|
||||
useUpdateNotificationSettings,
|
||||
} from "@/lib/query";
|
||||
|
||||
export function NotificationPage() {
|
||||
const { t } = useTranslation();
|
||||
const { data: notificationSettings, isLoading } = useNotificationSettings();
|
||||
const updateNotificationSettings = useUpdateNotificationSettings();
|
||||
const { t } = useTranslation();
|
||||
const { data: notificationSettings, isLoading } = useNotificationSettings();
|
||||
const updateNotificationSettings = useUpdateNotificationSettings();
|
||||
|
||||
const handleGeneralToggle = (checked: boolean) => {
|
||||
if (!notificationSettings) return;
|
||||
const handleGeneralToggle = (checked: boolean) => {
|
||||
if (!notificationSettings) return;
|
||||
|
||||
const newSettings = {
|
||||
enable: checked,
|
||||
enabled_hooks: checked ? notificationSettings.enabled_hooks : []
|
||||
};
|
||||
updateNotificationSettings.mutate(newSettings);
|
||||
};
|
||||
const newSettings = {
|
||||
enable: checked,
|
||||
enabled_hooks: checked ? notificationSettings.enabled_hooks : [],
|
||||
};
|
||||
updateNotificationSettings.mutate(newSettings);
|
||||
};
|
||||
|
||||
const handleHookToggle = (hookName: string, checked: boolean) => {
|
||||
if (!notificationSettings) return;
|
||||
const handleHookToggle = (hookName: string, checked: boolean) => {
|
||||
if (!notificationSettings) return;
|
||||
|
||||
let newHooks: string[];
|
||||
if (checked) {
|
||||
newHooks = [...notificationSettings.enabled_hooks, hookName];
|
||||
} else {
|
||||
newHooks = notificationSettings.enabled_hooks.filter(hook => hook !== hookName);
|
||||
}
|
||||
let newHooks: string[];
|
||||
if (checked) {
|
||||
newHooks = [...notificationSettings.enabled_hooks, hookName];
|
||||
} else {
|
||||
newHooks = notificationSettings.enabled_hooks.filter(
|
||||
(hook) => hook !== hookName,
|
||||
);
|
||||
}
|
||||
|
||||
const newSettings = {
|
||||
enable: notificationSettings.enable,
|
||||
enabled_hooks: newHooks
|
||||
};
|
||||
updateNotificationSettings.mutate(newSettings);
|
||||
};
|
||||
const newSettings = {
|
||||
enable: notificationSettings.enable,
|
||||
enabled_hooks: newHooks,
|
||||
};
|
||||
updateNotificationSettings.mutate(newSettings);
|
||||
};
|
||||
|
||||
const isHookEnabled = (hookName: string) => {
|
||||
return notificationSettings?.enabled_hooks.includes(hookName) || false;
|
||||
};
|
||||
const isHookEnabled = (hookName: string) => {
|
||||
return notificationSettings?.enabled_hooks.includes(hookName) || false;
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="">
|
||||
<div className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10 mb-4" data-tauri-drag-region>
|
||||
<div data-tauri-drag-region>
|
||||
<Skeleton className="h-6 w-32 mb-1" />
|
||||
<Skeleton className="h-4 w-48" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 flex flex-col bg-card mx-4 rounded-lg py-1 border space-y-4">
|
||||
<div className="border-b px-1 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-6 w-11" />
|
||||
</div>
|
||||
<Skeleton className="h-3 w-64 mt-2" />
|
||||
</div>
|
||||
<div className="border-b px-1 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-6 w-11" />
|
||||
</div>
|
||||
<Skeleton className="h-3 w-64 mt-2" />
|
||||
</div>
|
||||
<div className="px-1 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-6 w-11" />
|
||||
</div>
|
||||
<Skeleton className="h-3 w-64 mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="">
|
||||
<div
|
||||
className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10 mb-4"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div data-tauri-drag-region>
|
||||
<Skeleton className="h-6 w-32 mb-1" />
|
||||
<Skeleton className="h-4 w-48" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 flex flex-col bg-card mx-4 rounded-lg py-1 border space-y-4">
|
||||
<div className="border-b px-1 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-6 w-11" />
|
||||
</div>
|
||||
<Skeleton className="h-3 w-64 mt-2" />
|
||||
</div>
|
||||
<div className="border-b px-1 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-6 w-11" />
|
||||
</div>
|
||||
<Skeleton className="h-3 w-64 mt-2" />
|
||||
</div>
|
||||
<div className="px-1 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-6 w-11" />
|
||||
</div>
|
||||
<Skeleton className="h-3 w-64 mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10 mb-4" data-tauri-drag-region>
|
||||
<div data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>{t("notifications.title")}</h3>
|
||||
<p className="text-sm text-muted-foreground" data-tauri-drag-region>{t("notifications.description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 flex flex-col bg-card mx-4 rounded-lg py-1 border">
|
||||
<div className="border-b px-1 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="notification" className="">{t("notifications.general")}</Label>
|
||||
<Switch
|
||||
id="notification"
|
||||
checked={notificationSettings?.enable || false}
|
||||
onCheckedChange={handleGeneralToggle}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{t("notifications.generalDescription")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-b px-1 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="preToolUse" className="">{t("notifications.toolUse")}</Label>
|
||||
<Switch
|
||||
id="preToolUse"
|
||||
checked={isHookEnabled("PreToolUse")}
|
||||
onCheckedChange={(checked) => handleHookToggle("PreToolUse", checked)}
|
||||
disabled={!notificationSettings?.enable}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{t("notifications.toolUseDescription")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-1 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="stop" className="">{t("notifications.completion")}</Label>
|
||||
<Switch
|
||||
id="stop"
|
||||
checked={isHookEnabled("Stop")}
|
||||
onCheckedChange={(checked) => handleHookToggle("Stop", checked)}
|
||||
disabled={!notificationSettings?.enable}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{t("notifications.completionDescription")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="">
|
||||
<div
|
||||
className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10 mb-4"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>
|
||||
{t("notifications.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground" data-tauri-drag-region>
|
||||
{t("notifications.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 flex flex-col bg-card mx-4 rounded-lg py-1 border">
|
||||
<div className="border-b px-1 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="notification" className="">
|
||||
{t("notifications.general")}
|
||||
</Label>
|
||||
<Switch
|
||||
id="notification"
|
||||
checked={notificationSettings?.enable || false}
|
||||
onCheckedChange={handleGeneralToggle}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{t("notifications.generalDescription")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-b px-1 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="preToolUse" className="">
|
||||
{t("notifications.toolUse")}
|
||||
</Label>
|
||||
<Switch
|
||||
id="preToolUse"
|
||||
checked={isHookEnabled("PreToolUse")}
|
||||
onCheckedChange={(checked) =>
|
||||
handleHookToggle("PreToolUse", checked)
|
||||
}
|
||||
disabled={!notificationSettings?.enable}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{t("notifications.toolUseDescription")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-1 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="stop" className="">
|
||||
{t("notifications.completion")}
|
||||
</Label>
|
||||
<Switch
|
||||
id="stop"
|
||||
checked={isHookEnabled("Stop")}
|
||||
onCheckedChange={(checked) => handleHookToggle("Stop", checked)}
|
||||
disabled={!notificationSettings?.enable}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{t("notifications.completionDescription")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+155
-125
@@ -1,139 +1,169 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "next-themes";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ExternalLinkIcon, DownloadIcon, RotateCwIcon } from "lucide-react";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { useCheckForUpdates, useInstallAndRestart } from "@/lib/query";
|
||||
import { DownloadIcon, ExternalLinkIcon, RotateCwIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaXTwitter } from "react-icons/fa6";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useCheckForUpdates, useInstallAndRestart } from "@/lib/query";
|
||||
|
||||
export function SettingsPage() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { t, i18n } = useTranslation();
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const { data: updateInfo, isLoading: checkingUpdate } = useCheckForUpdates();
|
||||
const { mutate: installUpdate, isPending: installingUpdate } = useInstallAndRestart();
|
||||
const [version, setVersion] = useState<string>("");
|
||||
const { data: updateInfo, isLoading: checkingUpdate } = useCheckForUpdates();
|
||||
const { mutate: installUpdate, isPending: installingUpdate } =
|
||||
useInstallAndRestart();
|
||||
const [version, setVersion] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
getVersion().then(setVersion);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
getVersion().then(setVersion);
|
||||
}, []);
|
||||
|
||||
const handleLanguageChange = (language: string) => {
|
||||
i18n.changeLanguage(language);
|
||||
};
|
||||
const handleLanguageChange = (language: string) => {
|
||||
i18n.changeLanguage(language);
|
||||
};
|
||||
|
||||
const handleInstallUpdate = () => {
|
||||
installUpdate();
|
||||
};
|
||||
const handleInstallUpdate = () => {
|
||||
installUpdate();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="" >
|
||||
<div className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10 mb-4" data-tauri-drag-region>
|
||||
<div data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>{t("settings.title")}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6 px-4 ">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 mx-2">{t("settings.language")}</label>
|
||||
<Select value={i18n.language} onValueChange={handleLanguageChange}>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue placeholder={t("settings.language")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
<SelectItem value="zh-CN">中文</SelectItem>
|
||||
<SelectItem value="fr">Français</SelectItem>
|
||||
<SelectItem value="ja">日本語</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
return (
|
||||
<div className="">
|
||||
<div
|
||||
className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10 mb-4"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>
|
||||
{t("settings.title")}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6 px-4 ">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 mx-2">
|
||||
{t("settings.language")}
|
||||
</label>
|
||||
<Select value={i18n.language} onValueChange={handleLanguageChange}>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue placeholder={t("settings.language")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
<SelectItem value="zh-CN">中文</SelectItem>
|
||||
<SelectItem value="fr">Français</SelectItem>
|
||||
<SelectItem value="ja">日本語</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 mx-2">{t("settings.theme")}</label>
|
||||
<Select value={theme || "system"} onValueChange={setTheme}>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue placeholder={t("settings.theme")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="system">{t("settings.theme.system")}</SelectItem>
|
||||
<SelectItem value="light">{t("settings.theme.light")}</SelectItem>
|
||||
<SelectItem value="dark">{t("settings.theme.dark")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 mx-2">
|
||||
{t("settings.theme")}
|
||||
</label>
|
||||
<Select value={theme || "system"} onValueChange={setTheme}>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue placeholder={t("settings.theme")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="system">
|
||||
{t("settings.theme.system")}
|
||||
</SelectItem>
|
||||
<SelectItem value="light">{t("settings.theme.light")}</SelectItem>
|
||||
<SelectItem value="dark">{t("settings.theme.dark")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 mx-2">{t("settings.version")}</label>
|
||||
<div className="flex items-center gap-2 mx-2">
|
||||
<p className="text-sm text-muted-foreground">v{version}</p>
|
||||
{checkingUpdate ? (
|
||||
<Button variant="outline" size="sm" disabled>
|
||||
<RotateCwIcon className="w-4 h-4 animate-spin" />
|
||||
{t("settings.checkingUpdate")}
|
||||
</Button>
|
||||
) : updateInfo?.available ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-green-600 font-medium">
|
||||
{t("settings.newVersionAvailable", { version: updateInfo.version })}
|
||||
</span>
|
||||
<Button
|
||||
onClick={handleInstallUpdate}
|
||||
disabled={installingUpdate}
|
||||
variant="default"
|
||||
size="sm"
|
||||
>
|
||||
{installingUpdate ? (
|
||||
<>
|
||||
<RotateCwIcon className="w-4 h-4 animate-spin mr-1" />
|
||||
{t("settings.installing")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DownloadIcon className="w-4 h-4 mr-1" />
|
||||
{t("settings.installAndRestart")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">{t("settings.upToDate")}</span>
|
||||
)}
|
||||
</div>
|
||||
{updateInfo?.body && (
|
||||
<p className="text-xs text-muted-foreground mx-2 mt-1">
|
||||
{updateInfo.body}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 mx-2">
|
||||
{t("settings.version")}
|
||||
</label>
|
||||
<div className="flex items-center gap-2 mx-2">
|
||||
<p className="text-sm text-muted-foreground">v{version}</p>
|
||||
{checkingUpdate ? (
|
||||
<Button variant="outline" size="sm" disabled>
|
||||
<RotateCwIcon className="w-4 h-4 animate-spin" />
|
||||
{t("settings.checkingUpdate")}
|
||||
</Button>
|
||||
) : updateInfo?.available ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-green-600 font-medium">
|
||||
{t("settings.newVersionAvailable", {
|
||||
version: updateInfo.version,
|
||||
})}
|
||||
</span>
|
||||
<Button
|
||||
onClick={handleInstallUpdate}
|
||||
disabled={installingUpdate}
|
||||
variant="default"
|
||||
size="sm"
|
||||
>
|
||||
{installingUpdate ? (
|
||||
<>
|
||||
<RotateCwIcon className="w-4 h-4 animate-spin mr-1" />
|
||||
{t("settings.installing")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DownloadIcon className="w-4 h-4 mr-1" />
|
||||
{t("settings.installAndRestart")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("settings.upToDate")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{updateInfo?.body && (
|
||||
<p className="text-xs text-muted-foreground mx-2 mt-1">
|
||||
{updateInfo.body}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 mx-2">{t("settings.contact")}</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button onClick={_ => {
|
||||
openUrl("https://github.com/djyde/ccmate-release/issues")
|
||||
}} variant="outline" size="sm" className="text-sm">
|
||||
<ExternalLinkIcon />
|
||||
{t("settings.reportIssue")}
|
||||
</Button>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 mx-2">
|
||||
{t("settings.contact")}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={(_) => {
|
||||
openUrl("https://github.com/djyde/ccmate-release/issues");
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm"
|
||||
>
|
||||
<ExternalLinkIcon />
|
||||
{t("settings.reportIssue")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={_ => {
|
||||
openUrl("https://x.com/randyloop")
|
||||
}} variant="ghost" size="sm" className="text-sm">
|
||||
<FaXTwitter />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<Button
|
||||
onClick={(_) => {
|
||||
openUrl("https://x.com/randyloop");
|
||||
}}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-sm"
|
||||
>
|
||||
<FaXTwitter />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+144
-97
@@ -1,96 +1,140 @@
|
||||
import { useProjectUsageFiles } from "@/lib/query";
|
||||
import { TokenUsageChart } from "@/components/TokenUsageChart";
|
||||
import { useState, useEffect } from "react";
|
||||
import { ProjectUsageRecord } from "@/lib/query";
|
||||
import { cn, formatLargeNumber } from "@/lib/utils";
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
CircleDotDashedIcon,
|
||||
RefreshCwIcon,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TokenUsageChart } from "@/components/TokenUsageChart";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowDownIcon, ArrowUpIcon, CircleDotDashedIcon, RefreshCwIcon } from "lucide-react";
|
||||
import { type ProjectUsageRecord, useProjectUsageFiles } from "@/lib/query";
|
||||
import { cn, formatLargeNumber } from "@/lib/utils";
|
||||
|
||||
export function UsagePage() {
|
||||
const { t } = useTranslation();
|
||||
const { data: usageData, isLoading, error, refetch, isRefetching } = useProjectUsageFiles();
|
||||
const [filteredUsageData, setFilteredUsageData] = useState<ProjectUsageRecord[]>([]);
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data: usageData,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
isRefetching,
|
||||
} = useProjectUsageFiles();
|
||||
const [filteredUsageData, setFilteredUsageData] = useState<
|
||||
ProjectUsageRecord[]
|
||||
>([]);
|
||||
|
||||
// Initialize filtered data with full data
|
||||
useEffect(() => {
|
||||
if (usageData) {
|
||||
setFilteredUsageData(usageData);
|
||||
}
|
||||
}, [usageData]);
|
||||
// Initialize filtered data with full data
|
||||
useEffect(() => {
|
||||
if (usageData) {
|
||||
setFilteredUsageData(usageData);
|
||||
}
|
||||
}, [usageData]);
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10 mb-4" data-tauri-drag-region>
|
||||
<div data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>{t("usage.title")}</h3>
|
||||
<p className="text-sm text-muted-foreground" data-tauri-drag-region>
|
||||
{t("usage.description")}
|
||||
</p>
|
||||
</div>
|
||||
return (
|
||||
<div className="">
|
||||
<div
|
||||
className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10 mb-4"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>
|
||||
{t("usage.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground" data-tauri-drag-region>
|
||||
{t("usage.description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
disabled={isRefetching || isLoading}
|
||||
onClick={_ => {
|
||||
refetch();
|
||||
}} variant="ghost" size="sm" className="text-muted-foreground">
|
||||
<RefreshCwIcon className={cn({
|
||||
"animate-spin": isRefetching || isLoading,
|
||||
})} />
|
||||
{isRefetching || isLoading ? t("usage.refreshing") : t("usage.refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="bg-card p-4 rounded-lg space-y-2 border">
|
||||
<div className="h-4 rounded animate-pulse"></div>
|
||||
<div className="h-8 rounded animate-pulse"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 bg-card p-6 rounded-lg w-full min-w-0 border">
|
||||
<div className="h-64 rounded animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
) : error ? (
|
||||
<p>{t("usage.error", { error: error.message })}</p>
|
||||
) : usageData && usageData.length > 0 ? (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-blue-50 border-blue-100 text-blue-700 border-2 p-4 rounded-lg space-y-2 dark:bg-blue-950/20 dark:border-blue-900/30 dark:text-blue-300">
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowDownIcon size={12} />
|
||||
<h3 className="font-medium">{t("usage.inputTokens")}</h3>
|
||||
</div>
|
||||
<p className="text-2xl font-bold">
|
||||
{formatLargeNumber(filteredUsageData.reduce((sum, record) => sum + (record.usage?.input_tokens || 0), 0))}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-emerald-50 border-emerald-100 text-emerald-700 border-2 p-4 rounded-lg space-y-2 dark:bg-emerald-950/20 dark:border-emerald-900/30 dark:text-emerald-300">
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowUpIcon size={12} />
|
||||
<h3 className="font-medium">{t("usage.outputTokens")}</h3>
|
||||
</div>
|
||||
<p className="text-2xl font-bold">
|
||||
{formatLargeNumber(filteredUsageData.reduce((sum, record) => sum + (record.usage?.output_tokens || 0), 0))}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-amber-50 border-amber-100 text-amber-700 border-2 p-4 rounded-lg space-y-2 dark:bg-amber-950/20 dark:border-amber-900/30 dark:text-amber-300">
|
||||
<div className="flex items-center gap-2">
|
||||
<CircleDotDashedIcon size={12} />
|
||||
<h3 className="font-medium">{t("usage.cacheReadTokens")}</h3>
|
||||
</div>
|
||||
<p className="text-2xl font-bold">
|
||||
{formatLargeNumber(filteredUsageData.reduce((sum, record) => sum + (record.usage?.cache_read_input_tokens || 0), 0))}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
disabled={isRefetching || isLoading}
|
||||
onClick={(_) => {
|
||||
refetch();
|
||||
}}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<RefreshCwIcon
|
||||
className={cn({
|
||||
"animate-spin": isRefetching || isLoading,
|
||||
})}
|
||||
/>
|
||||
{isRefetching || isLoading
|
||||
? t("usage.refreshing")
|
||||
: t("usage.refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-card p-4 rounded-lg space-y-2 border"
|
||||
>
|
||||
<div className="h-4 rounded animate-pulse"></div>
|
||||
<div className="h-8 rounded animate-pulse"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 bg-card p-6 rounded-lg w-full min-w-0 border">
|
||||
<div className="h-64 rounded animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
) : error ? (
|
||||
<p>{t("usage.error", { error: error.message })}</p>
|
||||
) : usageData && usageData.length > 0 ? (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-blue-50 border-blue-100 text-blue-700 border-2 p-4 rounded-lg space-y-2 dark:bg-blue-950/20 dark:border-blue-900/30 dark:text-blue-300">
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowDownIcon size={12} />
|
||||
<h3 className="font-medium">{t("usage.inputTokens")}</h3>
|
||||
</div>
|
||||
<p className="text-2xl font-bold">
|
||||
{formatLargeNumber(
|
||||
filteredUsageData.reduce(
|
||||
(sum, record) => sum + (record.usage?.input_tokens || 0),
|
||||
0,
|
||||
),
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-emerald-50 border-emerald-100 text-emerald-700 border-2 p-4 rounded-lg space-y-2 dark:bg-emerald-950/20 dark:border-emerald-900/30 dark:text-emerald-300">
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowUpIcon size={12} />
|
||||
<h3 className="font-medium">{t("usage.outputTokens")}</h3>
|
||||
</div>
|
||||
<p className="text-2xl font-bold">
|
||||
{formatLargeNumber(
|
||||
filteredUsageData.reduce(
|
||||
(sum, record) => sum + (record.usage?.output_tokens || 0),
|
||||
0,
|
||||
),
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-amber-50 border-amber-100 text-amber-700 border-2 p-4 rounded-lg space-y-2 dark:bg-amber-950/20 dark:border-amber-900/30 dark:text-amber-300">
|
||||
<div className="flex items-center gap-2">
|
||||
<CircleDotDashedIcon size={12} />
|
||||
<h3 className="font-medium">{t("usage.cacheReadTokens")}</h3>
|
||||
</div>
|
||||
<p className="text-2xl font-bold">
|
||||
{formatLargeNumber(
|
||||
filteredUsageData.reduce(
|
||||
(sum, record) =>
|
||||
sum + (record.usage?.cache_read_input_tokens || 0),
|
||||
0,
|
||||
),
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* <div className="bg-zinc-50 p-4 rounded-lg space-y-2">
|
||||
{/* <div className="bg-zinc-50 p-4 rounded-lg space-y-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<h3 className="font-medium">Cost</h3>
|
||||
<Button variant="ghost" size="icon" className="-my-2 -mr-2">
|
||||
@@ -101,16 +145,19 @@ export function UsagePage() {
|
||||
$0.00
|
||||
</p>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className=" bg-card p-6 rounded-lg w-full min-w-0 border">
|
||||
<TokenUsageChart data={usageData} onFilteredDataChange={setFilteredUsageData} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p>{t("usage.noData")}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<div className=" bg-card p-6 rounded-lg w-full min-w-0 border">
|
||||
<TokenUsageChart
|
||||
data={usageData}
|
||||
onFilteredDataChange={setFilteredUsageData}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p>{t("usage.noData")}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+289
-228
@@ -1,253 +1,314 @@
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useClaudeProjects, useClaudeConfigFile, useWriteClaudeConfigFile } from "../../lib/query";
|
||||
import { json } from "@codemirror/lang-json";
|
||||
import { codeFolding } from "@codemirror/language";
|
||||
import CodeMirror, { EditorView, keymap } from "@uiw/react-codemirror";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
Check,
|
||||
ChevronsUpDown,
|
||||
FolderIcon,
|
||||
SaveIcon,
|
||||
} from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { Alert, AlertDescription } from "../../components/ui/alert";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { ArrowLeftIcon, Check, ChevronsUpDown, FolderIcon, SaveIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import CodeMirror, { EditorView, keymap } from '@uiw/react-codemirror';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "../../components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "../../components/ui/popover";
|
||||
import {
|
||||
useClaudeConfigFile,
|
||||
useClaudeProjects,
|
||||
useWriteClaudeConfigFile,
|
||||
} from "../../lib/query";
|
||||
import { useCodeMirrorTheme } from "../../lib/use-codemirror-theme";
|
||||
import { codeFolding } from '@codemirror/language';
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../../components/ui/popover";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "../../components/ui/command";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export function Detail() {
|
||||
const { t } = useTranslation();
|
||||
const { path } = useParams<{ path: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { data: projects, isLoading: isLoadingProjects, error: projectsError } = useClaudeProjects();
|
||||
const { data: claudeConfig, isLoading: isLoadingConfig, error: configError } = useClaudeConfigFile();
|
||||
const writeClaudeConfig = useWriteClaudeConfigFile();
|
||||
const [jsonContent, setJsonContent] = useState("");
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [comboboxOpen, setComboboxOpen] = useState(false);
|
||||
const codeMirrorTheme = useCodeMirrorTheme();
|
||||
const { t } = useTranslation();
|
||||
const { path } = useParams<{ path: string }>();
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
data: projects,
|
||||
isLoading: isLoadingProjects,
|
||||
error: projectsError,
|
||||
} = useClaudeProjects();
|
||||
const {
|
||||
data: claudeConfig,
|
||||
isLoading: isLoadingConfig,
|
||||
error: configError,
|
||||
} = useClaudeConfigFile();
|
||||
const writeClaudeConfig = useWriteClaudeConfigFile();
|
||||
const [jsonContent, setJsonContent] = useState("");
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [comboboxOpen, setComboboxOpen] = useState(false);
|
||||
const codeMirrorTheme = useCodeMirrorTheme();
|
||||
|
||||
// Update JSON content when project data loads or path changes
|
||||
useEffect(() => {
|
||||
if (path && projects && !isLoadingProjects && !isLoadingConfig) {
|
||||
const decodedPath = decodeURIComponent(path);
|
||||
const project = projects.find(p => p.path === decodedPath);
|
||||
// Update JSON content when project data loads or path changes
|
||||
useEffect(() => {
|
||||
if (path && projects && !isLoadingProjects && !isLoadingConfig) {
|
||||
const decodedPath = decodeURIComponent(path);
|
||||
const project = projects.find((p) => p.path === decodedPath);
|
||||
|
||||
if (project) {
|
||||
setJsonContent(JSON.stringify(project.config, null, 2));
|
||||
setHasChanges(false);
|
||||
}
|
||||
}
|
||||
}, [path, projects, isLoadingProjects, isLoadingConfig]);
|
||||
if (project) {
|
||||
setJsonContent(JSON.stringify(project.config, null, 2));
|
||||
setHasChanges(false);
|
||||
}
|
||||
}
|
||||
}, [path, projects, isLoadingProjects, isLoadingConfig]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
try {
|
||||
const parsedContent = JSON.parse(jsonContent);
|
||||
const handleSave = useCallback(() => {
|
||||
try {
|
||||
const parsedContent = JSON.parse(jsonContent);
|
||||
|
||||
if (!path || !claudeConfig) {
|
||||
toast.error(t('projects.detail.noProjectSelected'));
|
||||
return;
|
||||
}
|
||||
if (!path || !claudeConfig) {
|
||||
toast.error(t("projects.detail.noProjectSelected"));
|
||||
return;
|
||||
}
|
||||
|
||||
const decodedPath = decodeURIComponent(path);
|
||||
const updatedConfig = JSON.parse(JSON.stringify(claudeConfig.content));
|
||||
const decodedPath = decodeURIComponent(path);
|
||||
const updatedConfig = JSON.parse(JSON.stringify(claudeConfig.content));
|
||||
|
||||
// Update the specific project in the projects object
|
||||
if (!updatedConfig.projects) {
|
||||
updatedConfig.projects = {};
|
||||
}
|
||||
updatedConfig.projects[decodedPath] = parsedContent;
|
||||
// Update the specific project in the projects object
|
||||
if (!updatedConfig.projects) {
|
||||
updatedConfig.projects = {};
|
||||
}
|
||||
updatedConfig.projects[decodedPath] = parsedContent;
|
||||
|
||||
writeClaudeConfig.mutate(updatedConfig);
|
||||
setHasChanges(false);
|
||||
} catch (error) {
|
||||
toast.error(t('projects.detail.invalidJson'));
|
||||
}
|
||||
}, [jsonContent, path, claudeConfig, writeClaudeConfig, t]);
|
||||
writeClaudeConfig.mutate(updatedConfig);
|
||||
setHasChanges(false);
|
||||
} catch (error) {
|
||||
toast.error(t("projects.detail.invalidJson"));
|
||||
}
|
||||
}, [jsonContent, path, claudeConfig, writeClaudeConfig, t]);
|
||||
|
||||
const handleContentChange = useCallback((value: string) => {
|
||||
setJsonContent(value);
|
||||
setHasChanges(true);
|
||||
}, []);
|
||||
const handleContentChange = useCallback((value: string) => {
|
||||
setJsonContent(value);
|
||||
setHasChanges(true);
|
||||
}, []);
|
||||
|
||||
const handleProjectChange = useCallback((newPath: string) => {
|
||||
if (hasChanges) {
|
||||
// Check if user wants to save before switching
|
||||
const shouldSave = window.confirm(t('projects.detail.unsavedChanges'));
|
||||
if (shouldSave) {
|
||||
handleSave();
|
||||
}
|
||||
}
|
||||
navigate(`/projects/${encodeURIComponent(newPath)}`);
|
||||
}, [hasChanges, handleSave, navigate, t]);
|
||||
const handleProjectChange = useCallback(
|
||||
(newPath: string) => {
|
||||
if (hasChanges) {
|
||||
// Check if user wants to save before switching
|
||||
const shouldSave = window.confirm(t("projects.detail.unsavedChanges"));
|
||||
if (shouldSave) {
|
||||
handleSave();
|
||||
}
|
||||
}
|
||||
navigate(`/projects/${encodeURIComponent(newPath)}`);
|
||||
},
|
||||
[hasChanges, handleSave, navigate, t],
|
||||
);
|
||||
|
||||
// Create save keymap for CodeMirror
|
||||
const saveKeymap = keymap.of([
|
||||
{
|
||||
key: "Mod-s",
|
||||
run: () => {
|
||||
handleSave();
|
||||
return true; // Prevent further handling
|
||||
}
|
||||
}
|
||||
]);
|
||||
// Create save keymap for CodeMirror
|
||||
const saveKeymap = keymap.of([
|
||||
{
|
||||
key: "Mod-s",
|
||||
run: () => {
|
||||
handleSave();
|
||||
return true; // Prevent further handling
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// Create word wrap extension
|
||||
const wordWrapExtension = EditorView.lineWrapping;
|
||||
// Create word wrap extension
|
||||
const wordWrapExtension = EditorView.lineWrapping;
|
||||
|
||||
if (isLoadingConfig || isLoadingProjects) {
|
||||
return (
|
||||
<div className="">
|
||||
<div className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10 mb-4" data-tauri-drag-region>
|
||||
<div data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>{t('projects.detail.editor')}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6 px-4">
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="text-sm text-muted-foreground">{t('projects.detail.loading')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (isLoadingConfig || isLoadingProjects) {
|
||||
return (
|
||||
<div className="">
|
||||
<div
|
||||
className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10 mb-4"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>
|
||||
{t("projects.detail.editor")}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6 px-4">
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("projects.detail.loading")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (configError || projectsError) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<div className="space-y-6 px-4">
|
||||
{t('projects.detail.loadError')} {(configError || projectsError) instanceof Error ? (configError || projectsError)?.message : String(configError || projectsError)}
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
if (configError || projectsError) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<div className="space-y-6 px-4">
|
||||
{t("projects.detail.loadError")}{" "}
|
||||
{(configError || projectsError) instanceof Error
|
||||
? (configError || projectsError)?.message
|
||||
: String(configError || projectsError)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!path || !projects || projects.length === 0) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<div className="space-y-6 px-4">
|
||||
{t('projects.detail.noProjectsMessage')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!path || !projects || projects.length === 0) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<div className="space-y-6 px-4">
|
||||
{t("projects.detail.noProjectsMessage")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const decodedPath = decodeURIComponent(path);
|
||||
const project = projects.find(p => p.path === decodedPath);
|
||||
const decodedPath = decodeURIComponent(path);
|
||||
const project = projects.find((p) => p.path === decodedPath);
|
||||
|
||||
if (!project) {
|
||||
return (
|
||||
<div className="">
|
||||
<div className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10 mb-4" data-tauri-drag-region>
|
||||
<div data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>{t('projects.detail.editor')}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6 px-4">
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
{t('projects.detail.projectNotFound', { path: decodedPath })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Button onClick={() => navigate("/projects")} variant="outline">
|
||||
<ArrowLeftIcon className="h-4 w-4 mr-2" />
|
||||
{t('projects.detail.backToProjects')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!project) {
|
||||
return (
|
||||
<div className="">
|
||||
<div
|
||||
className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10 mb-4"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>
|
||||
{t("projects.detail.editor")}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6 px-4">
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
{t("projects.detail.projectNotFound", { path: decodedPath })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Button onClick={() => navigate("/projects")} variant="outline">
|
||||
<ArrowLeftIcon className="h-4 w-4 mr-2" />
|
||||
{t("projects.detail.backToProjects")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen">
|
||||
<div className="flex items-center px-4 justify-between py-3 border-b" data-tauri-drag-region>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-medium text-sm text-muted-foreground">{t('projects.detail.projectEditor')}</h3>
|
||||
<span className="text-muted-foreground text-xs">/</span>
|
||||
<Popover open={comboboxOpen} onOpenChange={setComboboxOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
role="combobox"
|
||||
aria-expanded={comboboxOpen}
|
||||
className="justify-between min-w-[200px]"
|
||||
>
|
||||
<span className="truncate">{project.path}</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder={t('projects.detail.searchProject')} className="h-9" />
|
||||
<CommandList>
|
||||
<CommandEmpty>{t('projects.detail.noProjectFound')}</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{projects.map((proj) => (
|
||||
<CommandItem
|
||||
key={proj.path}
|
||||
value={proj.path}
|
||||
onSelect={() => {
|
||||
handleProjectChange(proj.path);
|
||||
setComboboxOpen(false);
|
||||
}}
|
||||
>
|
||||
<FolderIcon className="mr-2 h-4 w-4" />
|
||||
<span className="truncate">{proj.path}</span>
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
project.path === proj.path ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
return (
|
||||
<div className="flex flex-col h-screen">
|
||||
<div
|
||||
className="flex items-center px-4 justify-between py-3 border-b"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-medium text-sm text-muted-foreground">
|
||||
{t("projects.detail.projectEditor")}
|
||||
</h3>
|
||||
<span className="text-muted-foreground text-xs">/</span>
|
||||
<Popover open={comboboxOpen} onOpenChange={setComboboxOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
role="combobox"
|
||||
aria-expanded={comboboxOpen}
|
||||
className="justify-between min-w-[200px]"
|
||||
>
|
||||
<span className="truncate">{project.path}</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder={t("projects.detail.searchProject")}
|
||||
className="h-9"
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{t("projects.detail.noProjectFound")}
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{projects.map((proj) => (
|
||||
<CommandItem
|
||||
key={proj.path}
|
||||
value={proj.path}
|
||||
onSelect={() => {
|
||||
handleProjectChange(proj.path);
|
||||
setComboboxOpen(false);
|
||||
}}
|
||||
>
|
||||
<FolderIcon className="mr-2 h-4 w-4" />
|
||||
<span className="truncate">{proj.path}</span>
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
project.path === proj.path
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges || writeClaudeConfig.isPending}
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<SaveIcon className="h-4 w-4" />
|
||||
{writeClaudeConfig.isPending ? t('projects.detail.saving') : t('projects.detail.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges || writeClaudeConfig.isPending}
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<SaveIcon className="h-4 w-4" />
|
||||
{writeClaudeConfig.isPending
|
||||
? t("projects.detail.saving")
|
||||
: t("projects.detail.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="overflow-hidden h-full">
|
||||
<CodeMirror
|
||||
value={jsonContent}
|
||||
height="100%"
|
||||
theme={codeMirrorTheme}
|
||||
extensions={[json(), codeFolding(), wordWrapExtension, saveKeymap]}
|
||||
onChange={handleContentChange}
|
||||
basicSetup={{
|
||||
lineNumbers: true,
|
||||
highlightActiveLineGutter: true,
|
||||
foldGutter: true,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: true,
|
||||
bracketMatching: true,
|
||||
closeBrackets: true,
|
||||
autocompletion: true,
|
||||
highlightActiveLine: true,
|
||||
highlightSelectionMatches: true,
|
||||
searchKeymap: true,
|
||||
}}
|
||||
className="h-full text-sm"
|
||||
style={{ width: '100%', fontSize: '12px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="overflow-hidden h-full">
|
||||
<CodeMirror
|
||||
value={jsonContent}
|
||||
height="100%"
|
||||
theme={codeMirrorTheme}
|
||||
extensions={[json(), codeFolding(), wordWrapExtension, saveKeymap]}
|
||||
onChange={handleContentChange}
|
||||
basicSetup={{
|
||||
lineNumbers: true,
|
||||
highlightActiveLineGutter: true,
|
||||
foldGutter: true,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: true,
|
||||
bracketMatching: true,
|
||||
closeBrackets: true,
|
||||
autocompletion: true,
|
||||
highlightActiveLine: true,
|
||||
highlightSelectionMatches: true,
|
||||
searchKeymap: true,
|
||||
}}
|
||||
className="h-full text-sm"
|
||||
style={{ width: "100%", fontSize: "12px" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,52 +1,56 @@
|
||||
import { FolderOpenIcon, Loader2 } from "lucide-react";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { useClaudeProjects } from "../../lib/query";
|
||||
import { Alert, AlertDescription } from "../../components/ui/alert";
|
||||
import { Loader2, FolderOpenIcon } from "lucide-react";
|
||||
import { useClaudeProjects } from "../../lib/query";
|
||||
|
||||
export function ProjectsLayout() {
|
||||
const { data: projects, isLoading, error } = useClaudeProjects();
|
||||
const { data: projects, isLoading, error } = useClaudeProjects();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
<span className="ml-2 text-sm text-muted-foreground">Loading projects...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
<span className="ml-2 text-sm text-muted-foreground">
|
||||
Loading projects...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
Failed to load projects: {error instanceof Error ? error.message : String(error)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
Failed to load projects:{" "}
|
||||
{error instanceof Error ? error.message : String(error)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!projects || projects.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||
<FolderOpenIcon className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">No Projects Found</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-md">
|
||||
There are no Claude projects configured. Projects appear here when you use Claude Code in different project folders.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!projects || projects.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||
<FolderOpenIcon className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">No Projects Found</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-md">
|
||||
There are no Claude projects configured. Projects appear here when
|
||||
you use Claude Code in different project folders.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* <ProjectSelector projects={projects} /> */}
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* <ProjectSelector projects={projects} /> */}
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+13
-13
@@ -3,18 +3,18 @@ import { useNavigate } from "react-router-dom";
|
||||
import { useClaudeProjects } from "../../lib/query";
|
||||
|
||||
export function List() {
|
||||
const navigate = useNavigate();
|
||||
const { data: projects } = useClaudeProjects();
|
||||
const navigate = useNavigate();
|
||||
const { data: projects } = useClaudeProjects();
|
||||
|
||||
useEffect(() => {
|
||||
if (projects && projects.length > 0) {
|
||||
// Navigate to the first project, URL-encoding the path
|
||||
const firstProjectPath = encodeURIComponent(projects[0].path);
|
||||
navigate(`/projects/${firstProjectPath}`, { replace: true });
|
||||
}
|
||||
}, [projects, navigate]);
|
||||
useEffect(() => {
|
||||
if (projects && projects.length > 0) {
|
||||
// Navigate to the first project, URL-encoding the path
|
||||
const firstProjectPath = encodeURIComponent(projects[0].path);
|
||||
navigate(`/projects/${firstProjectPath}`, { replace: true });
|
||||
}
|
||||
}, [projects, navigate]);
|
||||
|
||||
// This component is mainly used for redirection now
|
||||
// The loading, error, and empty states are handled by the layout
|
||||
return null;
|
||||
}
|
||||
// This component is mainly used for redirection now
|
||||
// The loading, error, and empty states are handled by the layout
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,74 +1,87 @@
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { ProjectConfig } from "../../lib/query";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "../../components/ui/dropdown-menu";
|
||||
import { ChevronDownIcon, FolderIcon } from "lucide-react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { FolderIcon, ChevronDownIcon } from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "../../components/ui/dropdown-menu";
|
||||
import type { ProjectConfig } from "../../lib/query";
|
||||
|
||||
interface ProjectSelectorProps {
|
||||
projects: ProjectConfig[];
|
||||
projects: ProjectConfig[];
|
||||
}
|
||||
|
||||
export function ProjectSelector({ projects }: ProjectSelectorProps) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// Get the current project path from the URL
|
||||
const currentProjectPath = location.pathname.startsWith("/projects/")
|
||||
? decodeURIComponent(location.pathname.replace("/projects/", ""))
|
||||
: null;
|
||||
// Get the current project path from the URL
|
||||
const currentProjectPath = location.pathname.startsWith("/projects/")
|
||||
? decodeURIComponent(location.pathname.replace("/projects/", ""))
|
||||
: null;
|
||||
|
||||
// Find the current project
|
||||
const currentProject = currentProjectPath
|
||||
? projects.find(p => p.path === currentProjectPath)
|
||||
: null;
|
||||
// Find the current project
|
||||
const currentProject = currentProjectPath
|
||||
? projects.find((p) => p.path === currentProjectPath)
|
||||
: null;
|
||||
|
||||
const handleProjectSelect = (projectPath: string) => {
|
||||
const encodedPath = encodeURIComponent(projectPath);
|
||||
navigate(`/projects/${encodedPath}`);
|
||||
};
|
||||
const handleProjectSelect = (projectPath: string) => {
|
||||
const encodedPath = encodeURIComponent(projectPath);
|
||||
navigate(`/projects/${encodedPath}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center p-3 border-b bg-background sticky top-0 z-10" data-tauri-drag-region>
|
||||
<div className="flex items-center gap-2 flex-1" data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>Projects</h3>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="flex items-center gap-2 min-w-[200px] justify-between">
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
<FolderIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="truncate">
|
||||
{currentProject ? currentProject.path.split("/").pop() || currentProject.path : "Select Project"}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDownIcon className="h-4 w-4 flex-shrink-0" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="min-w-[250px] max-h-[300px] overflow-y-auto">
|
||||
{projects.map((project) => (
|
||||
<DropdownMenuItem
|
||||
key={project.path}
|
||||
onClick={() => handleProjectSelect(project.path)}
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
>
|
||||
<FolderIcon className="h-4 w-4" />
|
||||
<div className="flex flex-col truncate">
|
||||
<span className="truncate font-medium">
|
||||
{project.path.split("/").pop() || project.path}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground truncate">
|
||||
{project.path}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="flex items-center p-3 border-b bg-background sticky top-0 z-10"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1" data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>
|
||||
Projects
|
||||
</h3>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-2 min-w-[200px] justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
<FolderIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="truncate">
|
||||
{currentProject
|
||||
? currentProject.path.split("/").pop() || currentProject.path
|
||||
: "Select Project"}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDownIcon className="h-4 w-4 flex-shrink-0" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="min-w-[250px] max-h-[300px] overflow-y-auto"
|
||||
>
|
||||
{projects.map((project) => (
|
||||
<DropdownMenuItem
|
||||
key={project.path}
|
||||
onClick={() => handleProjectSelect(project.path)}
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
>
|
||||
<FolderIcon className="h-4 w-4" />
|
||||
<div className="flex flex-col truncate">
|
||||
<span className="truncate font-medium">
|
||||
{project.path.split("/").pop() || project.path}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground truncate">
|
||||
{project.path}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+111
-111
@@ -1,121 +1,121 @@
|
||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
import { RouteWrapper } from "./components/RouteWrapper";
|
||||
import { Layout } from "./components/Layout";
|
||||
import { ConfigSwitcherPage } from "./pages/ConfigSwitcherPage";
|
||||
import { ConfigEditorPage } from "./pages/ConfigEditorPage";
|
||||
import { SettingsPage } from "./pages/SettingsPage";
|
||||
import { MCPPage } from "./pages/MCPPage";
|
||||
import { UsagePage } from "./pages/UsagePage";
|
||||
import { MemoryPage } from "./pages/MemoryPage";
|
||||
import { RouteWrapper } from "./components/RouteWrapper";
|
||||
import { CommandsPage } from "./pages/CommandsPage";
|
||||
import { List } from "./pages/projects/List";
|
||||
import { ConfigEditorPage } from "./pages/ConfigEditorPage";
|
||||
import { ConfigSwitcherPage } from "./pages/ConfigSwitcherPage";
|
||||
import { MCPPage } from "./pages/MCPPage";
|
||||
import { MemoryPage } from "./pages/MemoryPage";
|
||||
import { NotificationPage } from "./pages/NotificationPage";
|
||||
import { Detail } from "./pages/projects/Detail";
|
||||
import { ProjectsLayout } from "./pages/projects/Layout";
|
||||
import { NotificationPage } from "./pages/NotificationPage";
|
||||
import { List } from "./pages/projects/List";
|
||||
import { SettingsPage } from "./pages/SettingsPage";
|
||||
import { UsagePage } from "./pages/UsagePage";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<Layout />
|
||||
</RouteWrapper>
|
||||
),
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<ConfigSwitcherPage />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "edit/:storeId",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<ConfigEditorPage />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "settings",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<SettingsPage />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "mcp",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<MCPPage />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "usage",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<UsagePage />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "memory",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<MemoryPage />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "notification",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<NotificationPage />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "commands",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<CommandsPage />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "projects",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<ProjectsLayout />
|
||||
</RouteWrapper>
|
||||
),
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<List />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: ":path",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<Detail />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<Layout />
|
||||
</RouteWrapper>
|
||||
),
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<ConfigSwitcherPage />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "edit/:storeId",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<ConfigEditorPage />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "settings",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<SettingsPage />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "mcp",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<MCPPage />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "usage",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<UsagePage />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "memory",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<MemoryPage />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "notification",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<NotificationPage />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "commands",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<CommandsPage />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "projects",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<ProjectsLayout />
|
||||
</RouteWrapper>
|
||||
),
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<List />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: ":path",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<Detail />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
export function Router() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
+153
-152
@@ -4,174 +4,175 @@
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: #D97757;
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: #d97757;
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.11 0.006 285.82);
|
||||
--foreground: oklch(0.915 0.013 285.82);
|
||||
--card: oklch(0.15 0.008 285.82);
|
||||
--card-foreground: oklch(0.915 0.013 285.82);
|
||||
--popover: oklch(0.15 0.008 285.82);
|
||||
--popover-foreground: oklch(0.915 0.013 285.82);
|
||||
--primary: #D97757;
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.18 0.007 285.82);
|
||||
--secondary-foreground: oklch(0.915 0.013 285.82);
|
||||
--muted: oklch(0.18 0.007 285.82);
|
||||
--muted-foreground: oklch(0.65 0.03 285.82);
|
||||
--accent: oklch(0.18 0.007 285.82);
|
||||
--accent-foreground: oklch(0.915 0.013 285.82);
|
||||
--destructive: oklch(0.62 0.20 28.25);
|
||||
--border: oklch(0.25 0.006 285.82);
|
||||
--input: oklch(0.18 0.007 285.82);
|
||||
--ring: oklch(0.55 0.016 285.82);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.15 0.008 285.82);
|
||||
--sidebar-foreground: oklch(0.915 0.013 285.82);
|
||||
--sidebar-primary: #D97757;
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.18 0.007 285.82);
|
||||
--sidebar-accent-foreground: oklch(0.915 0.013 285.82);
|
||||
--sidebar-border: oklch(0.25 0.006 285.82);
|
||||
--sidebar-ring: oklch(0.55 0.016 285.82);
|
||||
--background: oklch(0.11 0.006 285.82);
|
||||
--foreground: oklch(0.915 0.013 285.82);
|
||||
--card: oklch(0.15 0.008 285.82);
|
||||
--card-foreground: oklch(0.915 0.013 285.82);
|
||||
--popover: oklch(0.15 0.008 285.82);
|
||||
--popover-foreground: oklch(0.915 0.013 285.82);
|
||||
--primary: #d97757;
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.18 0.007 285.82);
|
||||
--secondary-foreground: oklch(0.915 0.013 285.82);
|
||||
--muted: oklch(0.18 0.007 285.82);
|
||||
--muted-foreground: oklch(0.65 0.03 285.82);
|
||||
--accent: oklch(0.18 0.007 285.82);
|
||||
--accent-foreground: oklch(0.915 0.013 285.82);
|
||||
--destructive: oklch(0.62 0.2 28.25);
|
||||
--border: oklch(0.25 0.006 285.82);
|
||||
--input: oklch(0.18 0.007 285.82);
|
||||
--ring: oklch(0.55 0.016 285.82);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.15 0.008 285.82);
|
||||
--sidebar-foreground: oklch(0.915 0.013 285.82);
|
||||
--sidebar-primary: #d97757;
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.18 0.007 285.82);
|
||||
--sidebar-accent-foreground: oklch(0.915 0.013 285.82);
|
||||
--sidebar-border: oklch(0.25 0.006 285.82);
|
||||
--sidebar-ring: oklch(0.55 0.016 285.82);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50 box-border;
|
||||
}
|
||||
* {
|
||||
@apply border-border outline-ring/50 box-border;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
@apply text-foreground text-[14px];
|
||||
/* background: transparent;
|
||||
html,
|
||||
body {
|
||||
@apply text-foreground text-[14px];
|
||||
/* background: transparent;
|
||||
overflow: hidden; */
|
||||
|
||||
/* Native-like scrolling behavior */
|
||||
scroll-behavior: smooth;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
/* Native-like scrolling behavior */
|
||||
scroll-behavior: smooth;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
/* Native text selection */
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
/* Native text selection */
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
/* Prevent text selection during drag operations */
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
/* Prevent text selection during drag operations */
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#root {
|
||||
/* @apply rounded-xl overflow-hidden shadow-2xl; */
|
||||
/* min-height: 100vh; */
|
||||
}
|
||||
#root {
|
||||
/* @apply rounded-xl overflow-hidden shadow-2xl; */
|
||||
/* min-height: 100vh; */
|
||||
}
|
||||
|
||||
@keyframes blink-highlight {
|
||||
0% {
|
||||
background-color: rgb(254 249 195);
|
||||
box-shadow: 0 0 0 3px rgb(254 240 138 / 0.5);
|
||||
}
|
||||
50% {
|
||||
background-color: rgb(254 240 138);
|
||||
box-shadow: 0 0 0 4px rgb(254 240 138);
|
||||
}
|
||||
100% {
|
||||
background-color: rgb(254 249 195);
|
||||
box-shadow: 0 0 0 3px rgb(254 240 138 / 0.5);
|
||||
}
|
||||
}
|
||||
@keyframes blink-highlight {
|
||||
0% {
|
||||
background-color: rgb(254 249 195);
|
||||
box-shadow: 0 0 0 3px rgb(254 240 138 / 0.5);
|
||||
}
|
||||
50% {
|
||||
background-color: rgb(254 240 138);
|
||||
box-shadow: 0 0 0 4px rgb(254 240 138);
|
||||
}
|
||||
100% {
|
||||
background-color: rgb(254 249 195);
|
||||
box-shadow: 0 0 0 3px rgb(254 240 138 / 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
input.animate-blink,
|
||||
textarea.animate-blink,
|
||||
button.animate-blink {
|
||||
animation-name: blink-highlight !important;
|
||||
animation-duration: 0.5s !important;
|
||||
animation-timing-function: ease-in-out !important;
|
||||
animation-iteration-count: 3 !important;
|
||||
animation-fill-mode: forwards !important;
|
||||
}
|
||||
input.animate-blink,
|
||||
textarea.animate-blink,
|
||||
button.animate-blink {
|
||||
animation-name: blink-highlight !important;
|
||||
animation-duration: 0.5s !important;
|
||||
animation-timing-function: ease-in-out !important;
|
||||
animation-iteration-count: 3 !important;
|
||||
animation-fill-mode: forwards !important;
|
||||
}
|
||||
|
||||
a, button {
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
a,
|
||||
button {
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html, body {
|
||||
@apply overscroll-none;
|
||||
}
|
||||
|
||||
}
|
||||
html,
|
||||
body {
|
||||
@apply overscroll-none;
|
||||
}
|
||||
}
|
||||
|
||||
+26
-26
@@ -1,31 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"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",
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Path mapping */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
/* Path mapping */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
||||
+10
-10
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
+36
-33
@@ -1,42 +1,45 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// @ts-expect-error process is a nodejs global
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [react({
|
||||
babel: {
|
||||
plugins: ['babel-plugin-react-compiler'],
|
||||
},
|
||||
}), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": "/src",
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
react({
|
||||
babel: {
|
||||
plugins: ["babel-plugin-react-compiler"],
|
||||
},
|
||||
}),
|
||||
tailwindcss(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": "/src",
|
||||
},
|
||||
},
|
||||
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
//
|
||||
// 1. prevent Vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
// 2. tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
host: host || false,
|
||||
hmr: host
|
||||
? {
|
||||
protocol: "ws",
|
||||
host,
|
||||
port: 1421,
|
||||
}
|
||||
: undefined,
|
||||
watch: {
|
||||
// 3. tell Vite to ignore watching `src-tauri`
|
||||
ignored: ["**/src-tauri/**"],
|
||||
},
|
||||
},
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
//
|
||||
// 1. prevent Vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
// 2. tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
host: host || false,
|
||||
hmr: host
|
||||
? {
|
||||
protocol: "ws",
|
||||
host,
|
||||
port: 1421,
|
||||
}
|
||||
: undefined,
|
||||
watch: {
|
||||
// 3. tell Vite to ignore watching `src-tauri`
|
||||
ignored: ["**/src-tauri/**"],
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user