add biome

This commit is contained in:
Randy Lu
2025-10-29 00:05:40 +08:00
parent 914e4d8464
commit f3f2f7d03d
75 changed files with 8167 additions and 7375 deletions
+19 -20
View File
@@ -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"]
}
+6 -9
View File
@@ -1,11 +1,8 @@
{
"mcpServers": {
"shadcn": {
"command": "npx",
"args": [
"shadcn@latest",
"mcp"
]
}
}
"mcpServers": {
"shadcn": {
"command": "npx",
"args": ["shadcn@latest", "mcp"]
}
}
}
+5 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
}
+91
View File
@@ -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':
+12 -12
View File
@@ -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"
]
}
+5 -13
View File
@@ -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
View File
@@ -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"
]
}
}
}
+80 -81
View File
@@ -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;
}
}
+69 -50
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
);
}
+34 -28
View File
@@ -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>
);
}
+24 -24
View File
@@ -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
View File
@@ -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>
);
}
+41 -40
View File
@@ -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>
);
}
+6 -6
View File
@@ -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>;
}
+43 -43
View File
@@ -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
View File
@@ -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
View File
@@ -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 };
File diff suppressed because it is too large Load Diff
+64 -65
View File
@@ -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,
};
+47 -47
View File
@@ -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
View File
@@ -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
View File
@@ -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,
};
+99 -99
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 };
+36 -36
View File
@@ -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 };
+22 -22
View File
@@ -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 };
+38 -38
View File
@@ -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
View File
@@ -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,
};
+20 -20
View File
@@ -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 };
+10 -10
View File
@@ -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 };
+22 -22
View File
@@ -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 };
+22 -22
View File
@@ -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
View File
@@ -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 };
+17 -17
View File
@@ -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 };
+32 -32
View File
@@ -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>
);
}
+20 -20
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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": "ステップ 2API キーを作成",
"glm.enterConsole": "コンソールに入る",
"glm.step3": "ステップ 3API キーを入力",
"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": "ステップ 1API キーを作成",
"minimax.enterConsole": "コンソールに入る",
"minimax.step2": "ステップ 2API キーを入力",
"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": "ステップ 2API キーを作成",
"glm.enterConsole": "コンソールに入る",
"glm.step3": "ステップ 3API キーを入力",
"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": "ステップ 1API キーを作成",
"minimax.enterConsole": "コンソールに入る",
"minimax.step2": "ステップ 2API キーを入力",
"minimax.apiKeyPlaceholder": "MiniMax API キーを入力してください",
"minimax.creating": "作成中...",
"minimax.createConfig": "設定を作成",
"minimax.useMiniMax": "MiniMax を使用",
"minimax.mainlandTitle": "MiniMax 中国",
"minimax.internationalTitle": "MiniMax 国際"
}
+215 -215
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
};
+7 -7
View File
@@ -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;
}
+14 -14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
);
}
File diff suppressed because it is too large Load Diff
+208 -170
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
);
}
+48 -44
View File
@@ -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
View File
@@ -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;
}
+77 -64
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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/**"],
},
},
}));