fix some ui issue
This commit is contained in:
@@ -178,6 +178,8 @@ ccr ui
|
|||||||
|
|
||||||
This will open a web-based interface where you can easily view and edit your `config.json` file.
|
This will open a web-based interface where you can easily view and edit your `config.json` file.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
> **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).
|
> **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.
|
If you encounter any issues, please submit an issue on GitHub.
|
||||||
|
|
||||||
|
|||||||
@@ -174,6 +174,8 @@ ccr ui
|
|||||||
|
|
||||||
这将打开一个基于 Web 的界面,您可以在其中轻松查看和编辑您的 `config.json` 文件。
|
这将打开一个基于 Web 的界面,您可以在其中轻松查看和编辑您的 `config.json` 文件。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
> **注意**: UI 模式目前处于测试阶段。这是一个 100% vibe coding的项目,包括项目的初始化,我只是新建了一个文件夹和一个project.md文档。所有代码均由 ccr + qwen3-coder + gemini(webSearch) 实现。如有问题请提交 issue。
|
> **注意**: UI 模式目前处于测试阶段。这是一个 100% vibe coding的项目,包括项目的初始化,我只是新建了一个文件夹和一个project.md文档。所有代码均由 ccr + qwen3-coder + gemini(webSearch) 实现。如有问题请提交 issue。
|
||||||
|
|
||||||
#### Providers
|
#### Providers
|
||||||
|
|||||||
BIN
blog/images/ui.png
Normal file
BIN
blog/images/ui.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 518 KiB |
@@ -32,6 +32,7 @@ export interface RouterConfig {
|
|||||||
background: string;
|
background: string;
|
||||||
think: string;
|
think: string;
|
||||||
longContext: string;
|
longContext: string;
|
||||||
|
longContextThreshold: number;
|
||||||
webSearch: string;
|
webSearch: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,12 +124,14 @@ export function ConfigProvider({ children }: ConfigProviderProps) {
|
|||||||
background: typeof data.Router.background === 'string' ? data.Router.background : '',
|
background: typeof data.Router.background === 'string' ? data.Router.background : '',
|
||||||
think: typeof data.Router.think === 'string' ? data.Router.think : '',
|
think: typeof data.Router.think === 'string' ? data.Router.think : '',
|
||||||
longContext: typeof data.Router.longContext === 'string' ? data.Router.longContext : '',
|
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 : ''
|
webSearch: typeof data.Router.webSearch === 'string' ? data.Router.webSearch : ''
|
||||||
} : {
|
} : {
|
||||||
default: '',
|
default: '',
|
||||||
background: '',
|
background: '',
|
||||||
think: '',
|
think: '',
|
||||||
longContext: '',
|
longContext: '',
|
||||||
|
longContextThreshold: 60000,
|
||||||
webSearch: ''
|
webSearch: ''
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -153,6 +156,7 @@ export function ConfigProvider({ children }: ConfigProviderProps) {
|
|||||||
background: '',
|
background: '',
|
||||||
think: '',
|
think: '',
|
||||||
longContext: '',
|
longContext: '',
|
||||||
|
longContextThreshold: 60000,
|
||||||
webSearch: ''
|
webSearch: ''
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function Login() {
|
|||||||
try {
|
try {
|
||||||
await api.getConfig();
|
await api.getConfig();
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
} catch (err) {
|
} catch {
|
||||||
// If verification fails, remove the API key
|
// If verification fails, remove the API key
|
||||||
localStorage.removeItem('apiKey');
|
localStorage.removeItem('apiKey');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -69,7 +69,7 @@ export function Login() {
|
|||||||
// Navigate to dashboard
|
// Navigate to dashboard
|
||||||
// The ConfigProvider will handle fetching the config
|
// The ConfigProvider will handle fetching the config
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
} catch (err) {
|
} catch {
|
||||||
// Clear the API key on failure
|
// Clear the API key on failure
|
||||||
api.setApiKey('');
|
api.setApiKey('');
|
||||||
setError(t('login.invalidApiKey'));
|
setError(t('login.invalidApiKey'));
|
||||||
|
|||||||
7
ui/src/components/ProtectedRoute.tsx
Normal file
7
ui/src/components/ProtectedRoute.tsx
Normal 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;
|
||||||
@@ -203,11 +203,11 @@ export function Providers() {
|
|||||||
transformerArray.push(paramsObj);
|
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 {
|
} else {
|
||||||
// Convert to array format with parameters
|
// Convert to array format with parameters
|
||||||
const paramsObj = { [paramName]: paramValue };
|
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);
|
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 {
|
} else {
|
||||||
// Convert to array format with parameters
|
// Convert to array format with parameters
|
||||||
const paramsObj = { [paramName]: paramValue };
|
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}
|
ref={comboInputRef}
|
||||||
options={(editingProvider.models || []).map(model => ({ label: model, value: model }))}
|
options={(editingProvider.models || []).map(model => ({ label: model, value: model }))}
|
||||||
value=""
|
value=""
|
||||||
onChange={(_) => {
|
onChange={() => {
|
||||||
// 只更新输入值,不添加模型
|
// 只更新输入值,不添加模型
|
||||||
}}
|
}}
|
||||||
onEnter={(value) => {
|
onEnter={(value) => {
|
||||||
@@ -436,7 +436,7 @@ export function Providers() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (hasFetchedModels[editingProviderIndex] && comboInputRef.current) {
|
if (hasFetchedModels[editingProviderIndex] && comboInputRef.current) {
|
||||||
// 使用ComboInput的逻辑
|
// 使用ComboInput的逻辑
|
||||||
const comboInput = comboInputRef.current as any;
|
const comboInput = comboInputRef.current as unknown as { getCurrentValue(): string; clearInput(): void };
|
||||||
const currentValue = comboInput.getCurrentValue();
|
const currentValue = comboInput.getCurrentValue();
|
||||||
if (currentValue && currentValue.trim() && editingProviderIndex !== null) {
|
if (currentValue && currentValue.trim() && editingProviderIndex !== null) {
|
||||||
handleAddModel(editingProviderIndex, currentValue.trim());
|
handleAddModel(editingProviderIndex, currentValue.trim());
|
||||||
@@ -506,7 +506,7 @@ export function Providers() {
|
|||||||
{editingProvider.transformer?.use && editingProvider.transformer.use.length > 0 && (
|
{editingProvider.transformer?.use && editingProvider.transformer.use.length > 0 && (
|
||||||
<div className="space-y-2 mt-2">
|
<div className="space-y-2 mt-2">
|
||||||
<div className="text-sm font-medium text-gray-700">{t("providers.selected_transformers")}</div>
|
<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 key={transformerIndex} className="border rounded-md p-3">
|
||||||
<div className="flex gap-2 items-center mb-2">
|
<div className="flex gap-2 items-center mb-2">
|
||||||
<div className="flex-1 bg-gray-50 rounded p-2 text-sm">
|
<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 && (
|
{editingProvider.transformer?.[model]?.use && editingProvider.transformer[model].use.length > 0 && (
|
||||||
<div className="space-y-2 mt-2">
|
<div className="space-y-2 mt-2">
|
||||||
<div className="text-sm font-medium text-gray-700">{t("providers.selected_transformers")}</div>
|
<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 key={transformerIndex} className="border rounded-md p-3">
|
||||||
<div className="flex gap-2 items-center mb-2">
|
<div className="flex gap-2 items-center mb-2">
|
||||||
<div className="flex-1 bg-gray-50 rounded p-2 text-sm">
|
<div className="flex-1 bg-gray-50 rounded p-2 text-sm">
|
||||||
|
|||||||
7
ui/src/components/PublicRoute.tsx
Normal file
7
ui/src/components/PublicRoute.tsx
Normal 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;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import { useConfig } from "./ConfigProvider";
|
import { useConfig } from "./ConfigProvider";
|
||||||
import { Combobox } from "./ui/combobox";
|
import { Combobox } from "./ui/combobox";
|
||||||
|
|
||||||
@@ -28,10 +29,11 @@ export function Router() {
|
|||||||
background: "",
|
background: "",
|
||||||
think: "",
|
think: "",
|
||||||
longContext: "",
|
longContext: "",
|
||||||
|
longContextThreshold: 60000,
|
||||||
webSearch: ""
|
webSearch: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRouterChange = (field: string, value: string) => {
|
const handleRouterChange = (field: string, value: string | number) => {
|
||||||
// Handle case where config.Router might be null or undefined
|
// Handle case where config.Router might be null or undefined
|
||||||
const currentRouter = config.Router || {};
|
const currentRouter = config.Router || {};
|
||||||
const newRouter = { ...currentRouter, [field]: value };
|
const newRouter = { ...currentRouter, [field]: value };
|
||||||
@@ -97,6 +99,8 @@ export function Router() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
<Label>{t("router.longContext")}</Label>
|
<Label>{t("router.longContext")}</Label>
|
||||||
<Combobox
|
<Combobox
|
||||||
options={modelOptions}
|
options={modelOptions}
|
||||||
@@ -107,6 +111,17 @@ export function Router() {
|
|||||||
emptyPlaceholder={t("router.noModelFound")}
|
emptyPlaceholder={t("router.noModelFound")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="space-y-2">
|
||||||
<Label>{t("router.webSearch")}</Label>
|
<Label>{t("router.webSearch")}</Label>
|
||||||
<Combobox
|
<Combobox
|
||||||
|
|||||||
@@ -49,14 +49,32 @@ export function TransformerList({ transformers, onEdit, onRemove }: TransformerL
|
|||||||
// Handle case where transformer.options might be null or undefined
|
// Handle case where transformer.options might be null or undefined
|
||||||
const options = transformer.options || {};
|
const options = transformer.options || {};
|
||||||
|
|
||||||
// Handle case where options.project might be null or undefined
|
// Render parameters as tags in a single line
|
||||||
const project = options.project || "No Project";
|
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 (
|
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 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">
|
<div className="flex-1 space-y-1.5">
|
||||||
<p className="text-md font-semibold text-gray-800">{transformerPath}</p>
|
<p className="text-md font-semibold text-gray-800">{transformerPath}</p>
|
||||||
<p className="text-sm text-gray-500">{project}</p>
|
{renderParameters()}
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4 flex flex-shrink-0 items-center gap-2">
|
<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">
|
<Button variant="ghost" size="icon" onClick={() => onEdit(index)} className="transition-all-ease hover:scale-110">
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
"background": "Background",
|
"background": "Background",
|
||||||
"think": "Think",
|
"think": "Think",
|
||||||
"longContext": "Long Context",
|
"longContext": "Long Context",
|
||||||
|
"longContextThreshold": "Context Threshold",
|
||||||
"webSearch": "Web Search",
|
"webSearch": "Web Search",
|
||||||
"selectModel": "Select a model...",
|
"selectModel": "Select a model...",
|
||||||
"searchModel": "Search model...",
|
"searchModel": "Search model...",
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
"background": "后台",
|
"background": "后台",
|
||||||
"think": "思考",
|
"think": "思考",
|
||||||
"longContext": "长上下文",
|
"longContext": "长上下文",
|
||||||
|
"longContextThreshold": "上下文阈值",
|
||||||
"webSearch": "网络搜索",
|
"webSearch": "网络搜索",
|
||||||
"selectModel": "选择一个模型...",
|
"selectModel": "选择一个模型...",
|
||||||
"searchModel": "搜索模型...",
|
"searchModel": "搜索模型...",
|
||||||
|
|||||||
@@ -1,18 +1,8 @@
|
|||||||
import { createMemoryRouter, Navigate } from 'react-router-dom';
|
import { createMemoryRouter, Navigate } from 'react-router-dom';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import { Login } from '@/components/Login';
|
import { Login } from '@/components/Login';
|
||||||
|
import ProtectedRoute from '@/components/ProtectedRoute';
|
||||||
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
import PublicRoute from '@/components/PublicRoute';
|
||||||
// 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const router = createMemoryRouter([
|
export const router = createMemoryRouter([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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"}
|
||||||
Reference in New Issue
Block a user