diff --git a/README.md b/README.md index 55d92c2..38e0be3 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/README_zh.md b/README_zh.md index 727ba38..f069032 100644 --- a/README_zh.md +++ b/README_zh.md @@ -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 diff --git a/blog/images/ui.png b/blog/images/ui.png new file mode 100644 index 0000000..2322b4a Binary files /dev/null and b/blog/images/ui.png differ diff --git a/ui/src/components/ConfigProvider.tsx b/ui/src/components/ConfigProvider.tsx index 05f954d..8aa38ce 100644 --- a/ui/src/components/ConfigProvider.tsx +++ b/ui/src/components/ConfigProvider.tsx @@ -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: '' } }); diff --git a/ui/src/components/Login.tsx b/ui/src/components/Login.tsx index 61ccdbe..d3dac51 100644 --- a/ui/src/components/Login.tsx +++ b/ui/src/components/Login.tsx @@ -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')); diff --git a/ui/src/components/ProtectedRoute.tsx b/ui/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..b76bd6d --- /dev/null +++ b/ui/src/components/ProtectedRoute.tsx @@ -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; \ No newline at end of file diff --git a/ui/src/components/Providers.tsx b/ui/src/components/Providers.tsx index c82a2f5..1dbcd94 100644 --- a/ui/src/components/Providers.tsx +++ b/ui/src/components/Providers.tsx @@ -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 | { 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 | { 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 && (
{t("providers.selected_transformers")}
- {editingProvider.transformer.use.map((transformer: any, transformerIndex: number) => ( + {editingProvider.transformer.use.map((transformer: string | (string | Record | { max_tokens: number })[], transformerIndex: number) => (
@@ -660,7 +660,7 @@ export function Providers() { {editingProvider.transformer?.[model]?.use && editingProvider.transformer[model].use.length > 0 && (
{t("providers.selected_transformers")}
- {editingProvider.transformer[model].use.map((transformer: any, transformerIndex: number) => ( + {editingProvider.transformer[model].use.map((transformer: string | (string | Record | { max_tokens: number })[], transformerIndex: number) => (
diff --git a/ui/src/components/PublicRoute.tsx b/ui/src/components/PublicRoute.tsx new file mode 100644 index 0000000..fb3f811 --- /dev/null +++ b/ui/src/components/PublicRoute.tsx @@ -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; \ No newline at end of file diff --git a/ui/src/components/Router.tsx b/ui/src/components/Router.tsx index 2a77826..8db3e19 100644 --- a/ui/src/components/Router.tsx +++ b/ui/src/components/Router.tsx @@ -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,15 +99,28 @@ export function Router() { />
- - handleRouterChange("longContext", value)} - placeholder={t("router.selectModel")} - searchPlaceholder={t("router.searchModel")} - emptyPlaceholder={t("router.noModelFound")} - /> +
+
+ + handleRouterChange("longContext", value)} + placeholder={t("router.selectModel")} + searchPlaceholder={t("router.searchModel")} + emptyPlaceholder={t("router.noModelFound")} + /> +
+
+ + handleRouterChange("longContextThreshold", parseInt(e.target.value) || 60000)} + placeholder="60000" + /> +
+
diff --git a/ui/src/components/TransformerList.tsx b/ui/src/components/TransformerList.tsx index a1322b8..42300a5 100644 --- a/ui/src/components/TransformerList.tsx +++ b/ui/src/components/TransformerList.tsx @@ -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

No parameters configured

; + } + + return ( +
+ {Object.entries(options).map(([key, value]) => ( + + {key}: + {String(value)} + + ))} +
+ ); + }; return (

{transformerPath}

-

{project}

+ {renderParameters()}