fix some ui issue

This commit is contained in:
musistudio
2025-07-30 21:33:58 +08:00
parent aea48239f9
commit 7978f1abae
14 changed files with 83 additions and 36 deletions

View File

@@ -178,6 +178,8 @@ ccr ui
This will open a web-based interface where you can easily view and edit your `config.json` file.
![UI](/blog/images/ui.png)
> **Note**: The UI mode is currently in beta. 100% vibe coding: including project initialization, I just created a folder and a project.md document, and all code was generated by ccr + qwen3-coder + gemini(webSearch).
If you encounter any issues, please submit an issue on GitHub.

View File

@@ -174,6 +174,8 @@ ccr ui
这将打开一个基于 Web 的界面,您可以在其中轻松查看和编辑您的 `config.json` 文件。
![UI](/blog/images/ui.png)
> **注意**: UI 模式目前处于测试阶段。这是一个 100% vibe coding的项目包括项目的初始化我只是新建了一个文件夹和一个project.md文档。所有代码均由 ccr + qwen3-coder + gemini(webSearch) 实现。如有问题请提交 issue。
#### Providers

BIN
blog/images/ui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 KiB

View File

@@ -32,6 +32,7 @@ export interface RouterConfig {
background: string;
think: string;
longContext: string;
longContextThreshold: number;
webSearch: string;
}
@@ -123,12 +124,14 @@ export function ConfigProvider({ children }: ConfigProviderProps) {
background: typeof data.Router.background === 'string' ? data.Router.background : '',
think: typeof data.Router.think === 'string' ? data.Router.think : '',
longContext: typeof data.Router.longContext === 'string' ? data.Router.longContext : '',
longContextThreshold: typeof data.Router.longContextThreshold === 'number' ? data.Router.longContextThreshold : 60000,
webSearch: typeof data.Router.webSearch === 'string' ? data.Router.webSearch : ''
} : {
default: '',
background: '',
think: '',
longContext: '',
longContextThreshold: 60000,
webSearch: ''
}
};
@@ -153,6 +156,7 @@ export function ConfigProvider({ children }: ConfigProviderProps) {
background: '',
think: '',
longContext: '',
longContextThreshold: 60000,
webSearch: ''
}
});

View File

@@ -24,7 +24,7 @@ export function Login() {
try {
await api.getConfig();
navigate('/dashboard');
} catch (err) {
} catch {
// If verification fails, remove the API key
localStorage.removeItem('apiKey');
} finally {
@@ -69,7 +69,7 @@ export function Login() {
// Navigate to dashboard
// The ConfigProvider will handle fetching the config
navigate('/dashboard');
} catch (err) {
} catch {
// Clear the API key on failure
api.setApiKey('');
setError(t('login.invalidApiKey'));

View File

@@ -0,0 +1,7 @@
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
// For this application, we allow access without an API key
// The App component will handle loading and error states
return children;
};
export default ProtectedRoute;

View File

@@ -203,11 +203,11 @@ export function Providers() {
transformerArray.push(paramsObj);
}
newProviders[providerIndex].transformer!.use[transformerIndex] = transformerArray as any;
newProviders[providerIndex].transformer!.use[transformerIndex] = transformerArray as string | (string | Record<string, unknown> | { max_tokens: number })[];
} else {
// Convert to array format with parameters
const paramsObj = { [paramName]: paramValue };
newProviders[providerIndex].transformer!.use[transformerIndex] = [targetTransformer as string, paramsObj] as any;
newProviders[providerIndex].transformer!.use[transformerIndex] = [targetTransformer as string, paramsObj];
}
}
@@ -277,11 +277,11 @@ export function Providers() {
transformerArray.push(paramsObj);
}
newProviders[providerIndex].transformer![model].use[transformerIndex] = transformerArray as any;
newProviders[providerIndex].transformer![model].use[transformerIndex] = transformerArray as string | (string | Record<string, unknown> | { max_tokens: number })[];
} else {
// Convert to array format with parameters
const paramsObj = { [paramName]: paramValue };
newProviders[providerIndex].transformer![model].use[transformerIndex] = [targetTransformer as string, paramsObj] as any;
newProviders[providerIndex].transformer![model].use[transformerIndex] = [targetTransformer as string, paramsObj];
}
}
@@ -409,7 +409,7 @@ export function Providers() {
ref={comboInputRef}
options={(editingProvider.models || []).map(model => ({ label: model, value: model }))}
value=""
onChange={(_) => {
onChange={() => {
// 只更新输入值,不添加模型
}}
onEnter={(value) => {
@@ -436,7 +436,7 @@ export function Providers() {
onClick={() => {
if (hasFetchedModels[editingProviderIndex] && comboInputRef.current) {
// 使用ComboInput的逻辑
const comboInput = comboInputRef.current as any;
const comboInput = comboInputRef.current as unknown as { getCurrentValue(): string; clearInput(): void };
const currentValue = comboInput.getCurrentValue();
if (currentValue && currentValue.trim() && editingProviderIndex !== null) {
handleAddModel(editingProviderIndex, currentValue.trim());
@@ -506,7 +506,7 @@ export function Providers() {
{editingProvider.transformer?.use && editingProvider.transformer.use.length > 0 && (
<div className="space-y-2 mt-2">
<div className="text-sm font-medium text-gray-700">{t("providers.selected_transformers")}</div>
{editingProvider.transformer.use.map((transformer: any, transformerIndex: number) => (
{editingProvider.transformer.use.map((transformer: string | (string | Record<string, unknown> | { max_tokens: number })[], transformerIndex: number) => (
<div key={transformerIndex} className="border rounded-md p-3">
<div className="flex gap-2 items-center mb-2">
<div className="flex-1 bg-gray-50 rounded p-2 text-sm">
@@ -660,7 +660,7 @@ export function Providers() {
{editingProvider.transformer?.[model]?.use && editingProvider.transformer[model].use.length > 0 && (
<div className="space-y-2 mt-2">
<div className="text-sm font-medium text-gray-700">{t("providers.selected_transformers")}</div>
{editingProvider.transformer[model].use.map((transformer: any, transformerIndex: number) => (
{editingProvider.transformer[model].use.map((transformer: string | (string | Record<string, unknown> | { max_tokens: number })[], transformerIndex: number) => (
<div key={transformerIndex} className="border rounded-md p-3">
<div className="flex gap-2 items-center mb-2">
<div className="flex-1 bg-gray-50 rounded p-2 text-sm">

View File

@@ -0,0 +1,7 @@
const PublicRoute = ({ children }: { children: React.ReactNode }) => {
// Always show login page
// The login page will handle empty API keys appropriately
return children;
};
export default PublicRoute;

View File

@@ -1,6 +1,7 @@
import { useTranslation } from "react-i18next";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { useConfig } from "./ConfigProvider";
import { Combobox } from "./ui/combobox";
@@ -28,10 +29,11 @@ export function Router() {
background: "",
think: "",
longContext: "",
longContextThreshold: 60000,
webSearch: ""
};
const handleRouterChange = (field: string, value: string) => {
const handleRouterChange = (field: string, value: string | number) => {
// Handle case where config.Router might be null or undefined
const currentRouter = config.Router || {};
const newRouter = { ...currentRouter, [field]: value };
@@ -97,6 +99,8 @@ export function Router() {
/>
</div>
<div className="space-y-2">
<div className="flex items-center gap-4">
<div className="flex-1">
<Label>{t("router.longContext")}</Label>
<Combobox
options={modelOptions}
@@ -107,6 +111,17 @@ export function Router() {
emptyPlaceholder={t("router.noModelFound")}
/>
</div>
<div className="w-48">
<Label>{t("router.longContextThreshold")}</Label>
<Input
type="number"
value={routerConfig.longContextThreshold || 60000}
onChange={(e) => handleRouterChange("longContextThreshold", parseInt(e.target.value) || 60000)}
placeholder="60000"
/>
</div>
</div>
</div>
<div className="space-y-2">
<Label>{t("router.webSearch")}</Label>
<Combobox

View File

@@ -49,14 +49,32 @@ export function TransformerList({ transformers, onEdit, onRemove }: TransformerL
// Handle case where transformer.options might be null or undefined
const options = transformer.options || {};
// Handle case where options.project might be null or undefined
const project = options.project || "No Project";
// Render parameters as tags in a single line
const renderParameters = () => {
if (!options || Object.keys(options).length === 0) {
return <p className="text-sm text-gray-500">No parameters configured</p>;
}
return (
<div className="flex flex-wrap gap-2 max-h-8 overflow-hidden">
{Object.entries(options).map(([key, value]) => (
<span
key={key}
className="inline-flex items-center px-2 py-1 rounded-md bg-gray-100 text-xs font-medium text-gray-700 border"
>
<span className="text-gray-600">{key}:</span>
<span className="ml-1 text-gray-800">{String(value)}</span>
</span>
))}
</div>
);
};
return (
<div key={index} className="flex items-start justify-between rounded-md border bg-white p-4 transition-all hover:shadow-md animate-slide-in hover:scale-[1.01]">
<div className="flex-1 space-y-1.5">
<p className="text-md font-semibold text-gray-800">{transformerPath}</p>
<p className="text-sm text-gray-500">{project}</p>
{renderParameters()}
</div>
<div className="ml-4 flex flex-shrink-0 items-center gap-2">
<Button variant="ghost" size="icon" onClick={() => onEdit(index)} className="transition-all-ease hover:scale-110">

View File

@@ -83,6 +83,7 @@
"background": "Background",
"think": "Think",
"longContext": "Long Context",
"longContextThreshold": "Context Threshold",
"webSearch": "Web Search",
"selectModel": "Select a model...",
"searchModel": "Search model...",

View File

@@ -83,6 +83,7 @@
"background": "后台",
"think": "思考",
"longContext": "长上下文",
"longContextThreshold": "上下文阈值",
"webSearch": "网络搜索",
"selectModel": "选择一个模型...",
"searchModel": "搜索模型...",

View File

@@ -1,18 +1,8 @@
import { createMemoryRouter, Navigate } from 'react-router-dom';
import App from './App';
import { Login } from '@/components/Login';
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
// For this application, we allow access without an API key
// The App component will handle loading and error states
return children;
};
const PublicRoute = ({ children }: { children: React.ReactNode }) => {
// Always show login page
// The login page will handle empty API keys appropriately
return children;
};
import ProtectedRoute from '@/components/ProtectedRoute';
import PublicRoute from '@/components/PublicRoute';
export const router = createMemoryRouter([
{

View File

@@ -1 +1 @@
{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/jsoneditor.tsx","./src/components/login.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/utils.ts"],"version":"5.8.3"}
{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/jsoneditor.tsx","./src/components/login.tsx","./src/components/protectedroute.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/publicroute.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/utils.ts"],"version":"5.8.3"}