handlePathInputChange(e.target.value)}
placeholder="/path/to/workspace"
- className="w-full rounded-md border border-border/50 bg-background px-3 py-1.5 text-sm pr-8"
+ className="pr-8"
/>
{renderValidationIcon()}
@@ -478,7 +383,7 @@ export function AssistantWorkspaceSection() {
diff --git a/src/components/settings/ProviderManager.tsx b/src/components/settings/ProviderManager.tsx
index c4e927d..21efde5 100644
--- a/src/components/settings/ProviderManager.tsx
+++ b/src/components/settings/ProviderManager.tsx
@@ -1,26 +1,8 @@
"use client";
-import { useState, useEffect, useCallback, type ReactNode } from "react";
+import { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { Textarea } from "@/components/ui/textarea";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogDescription,
- DialogFooter,
-} from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
@@ -31,802 +13,21 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
-import {
- SpinnerGap,
- PencilSimple,
- CaretDown,
- CaretUp,
- HardDrives,
- Gear,
-} from "@phosphor-icons/react";
+import { SpinnerGap, PencilSimple } from "@/components/ui/icon";
import { ProviderForm } from "./ProviderForm";
import type { ProviderFormData } from "./ProviderForm";
+import { PresetConnectDialog } from "./PresetConnectDialog";
+import {
+ QUICK_PRESETS,
+ GEMINI_IMAGE_MODELS,
+ getGeminiImageModel,
+ getProviderIcon,
+ findMatchingPreset,
+ type QuickPreset,
+} from "./provider-presets";
import type { ApiProvider } from "@/types";
import { useTranslation } from "@/hooks/useTranslation";
-import type { TranslationKey } from "@/i18n";
import Anthropic from "@lobehub/icons/es/Anthropic";
-import OpenRouter from "@lobehub/icons/es/OpenRouter";
-import Zhipu from "@lobehub/icons/es/Zhipu";
-import Kimi from "@lobehub/icons/es/Kimi";
-import Moonshot from "@lobehub/icons/es/Moonshot";
-import Minimax from "@lobehub/icons/es/Minimax";
-import Aws from "@lobehub/icons/es/Aws";
-import Bedrock from "@lobehub/icons/es/Bedrock";
-import Google from "@lobehub/icons/es/Google";
-import Volcengine from "@lobehub/icons/es/Volcengine";
-import Bailian from "@lobehub/icons/es/Bailian";
-
-// ---------------------------------------------------------------------------
-// Brand icon resolver
-// ---------------------------------------------------------------------------
-
-/** Map a provider name / base_url to a brand icon */
-function getProviderIcon(name: string, baseUrl: string): ReactNode {
- const lower = name.toLowerCase();
- const url = baseUrl.toLowerCase();
-
- if (lower.includes("openrouter")) return
;
- if (url.includes("bigmodel.cn") || url.includes("z.ai") || lower.includes("glm") || lower.includes("zhipu") || lower.includes("chatglm"))
- return
;
- if (url.includes("kimi.com") || lower.includes("kimi")) return
;
- if (url.includes("moonshot") || lower.includes("moonshot")) return
;
- if (url.includes("minimax") || lower.includes("minimax")) return
;
- if (url.includes("volces.com") || url.includes("volcengine") || lower.includes("volcengine") || lower.includes("火山") || lower.includes("doubao") || lower.includes("豆包"))
- return
;
- if (url.includes("dashscope") || lower.includes("bailian") || lower.includes("百炼") || lower.includes("aliyun"))
- return
;
- if (lower.includes("bedrock")) return
;
- if (lower.includes("vertex") || lower.includes("google")) return
;
- if (lower.includes("aws")) return
;
- if (lower.includes("anthropic") || url.includes("anthropic")) return
;
-
- return
;
-}
-
-// ---------------------------------------------------------------------------
-// Quick-add preset definitions
-// ---------------------------------------------------------------------------
-
-interface QuickPreset {
- key: string; // unique key
- name: string;
- description: string;
- descriptionZh: string;
- icon: ReactNode;
- // Pre-filled provider data
- provider_type: string;
- /** Wire protocol — determines how the provider is dispatched at runtime */
- protocol: string;
- base_url: string;
- extra_env: string;
- // Which fields user must fill
- fields: ("name" | "api_key" | "base_url" | "extra_env" | "model_names" | "model_mapping")[];
- // Category: 'chat' (default) or 'media'
- category?: "chat" | "media";
-}
-
-const QUICK_PRESETS: QuickPreset[] = [
- // ── Custom endpoints ──
- {
- key: "custom-openai",
- name: "Custom API (OpenAI-compatible)",
- description: "OpenAI-compatible custom endpoint",
- descriptionZh: "自定义 OpenAI 兼容 API 端点",
- icon:
,
- provider_type: "custom",
- protocol: "openai-compatible",
- base_url: "",
- extra_env: "{}",
- fields: ["name", "api_key", "base_url", "extra_env"],
- },
- // ── Anthropic-compatible services ──
- {
- key: "anthropic-thirdparty",
- name: "Anthropic Third-party API",
- description: "Anthropic-compatible API — provide URL and Key",
- descriptionZh: "Anthropic 兼容第三方 API — 填写地址和密钥",
- icon:
,
- provider_type: "anthropic",
- protocol: "anthropic",
- base_url: "",
- extra_env: '{"ANTHROPIC_API_KEY":""}',
- fields: ["name", "api_key", "base_url", "model_mapping"],
- },
- {
- key: "anthropic-official",
- name: "Anthropic",
- description: "Official Anthropic API",
- descriptionZh: "Anthropic 官方 API",
- icon:
,
- provider_type: "anthropic",
- protocol: "anthropic",
- base_url: "https://api.anthropic.com",
- extra_env: "{}",
- fields: ["api_key"],
- },
- {
- key: "openrouter",
- name: "OpenRouter",
- description: "Use OpenRouter to access multiple models",
- descriptionZh: "通过 OpenRouter 访问多种模型",
- icon:
,
- provider_type: "openrouter",
- protocol: "openrouter",
- base_url: "https://openrouter.ai/api",
- extra_env: '{"ANTHROPIC_API_KEY":""}',
- fields: ["api_key"],
- },
- {
- key: "glm-cn",
- name: "GLM (CN)",
- description: "Zhipu GLM Code Plan — China region",
- descriptionZh: "智谱 GLM 编程套餐 — 中国区",
- icon:
,
- provider_type: "anthropic",
- protocol: "anthropic",
- base_url: "https://open.bigmodel.cn/api/anthropic",
- extra_env: '{"API_TIMEOUT_MS":"3000000","ANTHROPIC_API_KEY":""}',
- fields: ["api_key"],
- },
- {
- key: "glm-global",
- name: "GLM (Global)",
- description: "Zhipu GLM Code Plan — Global region",
- descriptionZh: "智谱 GLM 编程套餐 — 国际区",
- icon:
,
- provider_type: "anthropic",
- protocol: "anthropic",
- base_url: "https://api.z.ai/api/anthropic",
- extra_env: '{"API_TIMEOUT_MS":"3000000","ANTHROPIC_API_KEY":""}',
- fields: ["api_key"],
- },
- {
- key: "kimi",
- name: "Kimi Coding Plan",
- description: "Kimi Coding Plan API",
- descriptionZh: "Kimi 编程计划 API",
- icon:
,
- provider_type: "anthropic",
- protocol: "anthropic",
- base_url: "https://api.kimi.com/coding/",
- extra_env: '{"ANTHROPIC_AUTH_TOKEN":""}',
- fields: ["api_key"],
- },
- {
- key: "moonshot",
- name: "Moonshot",
- description: "Moonshot AI API",
- descriptionZh: "月之暗面 API",
- icon:
,
- provider_type: "anthropic",
- protocol: "anthropic",
- base_url: "https://api.moonshot.cn/anthropic",
- extra_env: '{"ANTHROPIC_API_KEY":""}',
- fields: ["api_key"],
- },
- {
- key: "minimax-cn",
- name: "MiniMax (CN)",
- description: "MiniMax Code Plan — China region",
- descriptionZh: "MiniMax 编程套餐 — 中国区",
- icon:
,
- provider_type: "anthropic",
- protocol: "anthropic",
- base_url: "https://api.minimaxi.com/anthropic",
- extra_env: '{"API_TIMEOUT_MS":"3000000","CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"1","ANTHROPIC_API_KEY":""}',
- fields: ["api_key"],
- },
- {
- key: "minimax-global",
- name: "MiniMax (Global)",
- description: "MiniMax Code Plan — Global region",
- descriptionZh: "MiniMax 编程套餐 — 国际区",
- icon:
,
- provider_type: "anthropic",
- protocol: "anthropic",
- base_url: "https://api.minimax.io/anthropic",
- extra_env: '{"API_TIMEOUT_MS":"3000000","CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"1","ANTHROPIC_API_KEY":""}',
- fields: ["api_key"],
- },
- {
- key: "volcengine",
- name: "Volcengine Ark",
- description: "Volcengine Ark Coding Plan — Doubao, GLM, DeepSeek, Kimi",
- descriptionZh: "字节火山方舟 Coding Plan — 豆包、GLM、DeepSeek、Kimi",
- icon:
,
- provider_type: "anthropic",
- protocol: "anthropic",
- base_url: "https://ark.cn-beijing.volces.com/api/coding",
- extra_env: '{"ANTHROPIC_AUTH_TOKEN":""}',
- fields: ["api_key", "model_names"],
- },
- {
- key: "bailian",
- name: "Aliyun Bailian",
- description: "Aliyun Bailian Coding Plan — Qwen, GLM, Kimi, MiniMax",
- descriptionZh: "阿里云百炼 Coding Plan — 通义千问、GLM、Kimi、MiniMax",
- icon:
,
- provider_type: "anthropic",
- protocol: "anthropic",
- base_url: "https://coding.dashscope.aliyuncs.com/apps/anthropic",
- extra_env: '{"ANTHROPIC_API_KEY":""}',
- fields: ["api_key"],
- },
- // ── Cloud platform providers ──
- {
- key: "bedrock",
- name: "AWS Bedrock",
- description: "Amazon Bedrock — requires AWS credentials",
- descriptionZh: "Amazon Bedrock — 需要 AWS 凭证",
- icon:
,
- provider_type: "bedrock",
- protocol: "bedrock",
- base_url: "",
- extra_env: '{"CLAUDE_CODE_USE_BEDROCK":"1","AWS_REGION":"us-east-1","CLAUDE_CODE_SKIP_BEDROCK_AUTH":"1"}',
- fields: ["extra_env"],
- },
- {
- key: "vertex",
- name: "Google Vertex",
- description: "Google Vertex AI — requires GCP credentials",
- descriptionZh: "Google Vertex AI — 需要 GCP 凭证",
- icon:
,
- provider_type: "vertex",
- protocol: "vertex",
- base_url: "",
- extra_env: '{"CLAUDE_CODE_USE_VERTEX":"1","CLOUD_ML_REGION":"us-east5","CLAUDE_CODE_SKIP_VERTEX_AUTH":"1"}',
- fields: ["extra_env"],
- },
- // ── Proxy / gateway ──
- {
- key: "litellm",
- name: "LiteLLM",
- description: "LiteLLM proxy — local or remote",
- descriptionZh: "LiteLLM 代理 — 本地或远程",
- icon:
,
- provider_type: "anthropic",
- protocol: "anthropic",
- base_url: "http://localhost:4000",
- extra_env: "{}",
- fields: ["api_key", "base_url"],
- },
- // ── Media providers ──
- {
- key: "gemini-image",
- name: "Google Gemini (Image)",
- description: "Nano Banana Pro — AI image generation by Google Gemini",
- descriptionZh: "Nano Banana Pro — Google Gemini AI 图片生成",
- icon:
,
- provider_type: "gemini-image",
- protocol: "gemini-image",
- base_url: "https://generativelanguage.googleapis.com/v1beta",
- extra_env: '{"GEMINI_API_KEY":""}',
- fields: ["api_key"],
- category: "media",
- },
-];
-
-const GEMINI_IMAGE_MODELS = [
- { value: 'gemini-3.1-flash-image-preview', label: 'Nano Banana 2' },
- { value: 'gemini-3-pro-image-preview', label: 'Nano Banana Pro' },
- { value: 'gemini-2.5-flash-image', label: 'Nano Banana' },
-];
-
-const DEFAULT_GEMINI_IMAGE_MODEL = 'gemini-3.1-flash-image-preview';
-
-function getGeminiImageModel(provider: ApiProvider): string {
- try {
- const env = JSON.parse(provider.extra_env || '{}');
- return env.GEMINI_IMAGE_MODEL || DEFAULT_GEMINI_IMAGE_MODEL;
- } catch {
- return DEFAULT_GEMINI_IMAGE_MODEL;
- }
-}
-
-// ---------------------------------------------------------------------------
-// Preset connect dialog
-// ---------------------------------------------------------------------------
-
-function PresetConnectDialog({
- preset,
- open,
- onOpenChange,
- onSave,
- editProvider,
-}: {
- preset: QuickPreset | null;
- open: boolean;
- onOpenChange: (open: boolean) => void;
- onSave: (data: ProviderFormData) => Promise
;
- /** When set, dialog operates in edit mode (pre-fills from existing provider) */
- editProvider?: ApiProvider | null;
-}) {
- const isEdit = !!editProvider;
- const [apiKey, setApiKey] = useState("");
- const [baseUrl, setBaseUrl] = useState("");
- const [name, setName] = useState("");
- const [extraEnv, setExtraEnv] = useState("{}");
- const [modelName, setModelName] = useState("");
- // Auth style for anthropic-thirdparty: 'api_key' or 'auth_token'
- const [authStyle, setAuthStyle] = useState<"api_key" | "auth_token">("api_key");
- // Track the initial auth style to detect changes
- const [initialAuthStyle, setInitialAuthStyle] = useState<"api_key" | "auth_token">("api_key");
- // Edit-mode advanced fields
- const [headersJson, setHeadersJson] = useState("{}");
- const [envOverridesJson, setEnvOverridesJson] = useState("");
- const [notes, setNotes] = useState("");
- // Model mapping fields (sonnet/opus/haiku → actual API model IDs)
- const [mapSonnet, setMapSonnet] = useState("");
- const [mapOpus, setMapOpus] = useState("");
- const [mapHaiku, setMapHaiku] = useState("");
- const [showAdvanced, setShowAdvanced] = useState(false);
- const [error, setError] = useState(null);
- const [saving, setSaving] = useState(false);
- const { t } = useTranslation();
- const isZh = t('nav.chats') === '对话';
-
- // Reset form when dialog opens
- useEffect(() => {
- if (!open || !preset) return;
- setError(null);
- setSaving(false);
-
- if (isEdit && editProvider) {
- // Edit mode — pre-fill from existing provider
- setName(editProvider.name);
- setBaseUrl(editProvider.base_url);
- setApiKey(editProvider.api_key || "");
- setExtraEnv(editProvider.extra_env || preset.extra_env);
- // Detect auth style from existing extra_env
- try {
- const env = JSON.parse(editProvider.extra_env || "{}");
- const detected = "ANTHROPIC_AUTH_TOKEN" in env ? "auth_token" as const : "api_key" as const;
- setAuthStyle(detected);
- setInitialAuthStyle(detected);
- } catch {
- setAuthStyle("api_key");
- setInitialAuthStyle("api_key");
- }
- // Pre-fill advanced fields
- setHeadersJson(editProvider.headers_json || "{}");
- setEnvOverridesJson(editProvider.env_overrides_json || "");
- setNotes(editProvider.notes || "");
- // Pre-fill model name from role_models_json
- try {
- const rm = JSON.parse(editProvider.role_models_json || "{}");
- setModelName(rm.default || "");
- setMapSonnet(rm.sonnet || "");
- setMapOpus(rm.opus || "");
- setMapHaiku(rm.haiku || "");
- } catch {
- setModelName("");
- setMapSonnet("");
- setMapOpus("");
- setMapHaiku("");
- }
- // Auto-expand advanced if there's meaningful data beyond preset defaults
- const hasModelMapping = (() => {
- try {
- const rm = JSON.parse(editProvider.role_models_json || "{}");
- return !!(rm.sonnet || rm.opus || rm.haiku);
- } catch { return false; }
- })();
- const hasExtraEnvBeyondAuth = (() => {
- try {
- const env = JSON.parse(editProvider.extra_env || "{}");
- const meaningful = Object.keys(env).filter(k =>
- k !== "ANTHROPIC_API_KEY" && k !== "ANTHROPIC_AUTH_TOKEN"
- );
- return meaningful.length > 0;
- } catch { return false; }
- })();
- const hasHeaders = editProvider.headers_json && editProvider.headers_json !== "{}";
- const hasEnvOverrides = !!editProvider.env_overrides_json;
- const hasNotes = !!editProvider.notes;
- setShowAdvanced(hasModelMapping || hasExtraEnvBeyondAuth || !!hasHeaders || hasEnvOverrides || hasNotes);
- } else {
- // Create mode — reset to preset defaults
- setApiKey("");
- setBaseUrl(preset.base_url);
- setName(preset.name);
- setExtraEnv(preset.extra_env);
- setModelName("");
- setAuthStyle("api_key");
- setInitialAuthStyle("api_key");
- setMapSonnet("");
- setMapOpus("");
- setMapHaiku("");
- setHeadersJson("{}");
- setEnvOverridesJson("");
- setNotes("");
- setShowAdvanced(false);
- }
- }, [open, preset, isEdit, editProvider]);
-
- if (!preset) return null;
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- setError(null);
-
- // If auth style changed in edit mode, require a new key
- if (isEdit && authStyle !== initialAuthStyle && (!apiKey || apiKey.startsWith("***"))) {
- setError(isZh
- ? '切换认证方式后需要重新输入密钥'
- : 'Please re-enter the key after changing auth style');
- return;
- }
-
- // For anthropic-thirdparty, inject the correct auth key into extra_env
- // while preserving any other user-specified env vars (e.g. API_TIMEOUT_MS)
- let finalExtraEnv = extraEnv;
- if (preset.key === "anthropic-thirdparty") {
- try {
- const parsed = JSON.parse(extraEnv || "{}");
- // Remove both auth keys, then set the correct one
- delete parsed["ANTHROPIC_API_KEY"];
- delete parsed["ANTHROPIC_AUTH_TOKEN"];
- if (authStyle === "auth_token") {
- parsed["ANTHROPIC_AUTH_TOKEN"] = "";
- } else {
- parsed["ANTHROPIC_API_KEY"] = "";
- }
- finalExtraEnv = JSON.stringify(parsed);
- } catch {
- // If parse fails, fall back to simple replacement
- finalExtraEnv = authStyle === "auth_token"
- ? '{"ANTHROPIC_AUTH_TOKEN":""}'
- : '{"ANTHROPIC_API_KEY":""}';
- }
- }
- // In edit mode, preserve existing role_models_json unless the user modifies mapping fields
- let roleModelsJson = (isEdit && editProvider?.role_models_json) ? editProvider.role_models_json : "{}";
-
- // Model mapping (sonnet/opus/haiku → actual API model IDs)
- // Merge into existing roleModels to preserve roles not shown in this preset.
- // If the preset exposes these fields and user cleared them all, remove those keys.
- if (preset.fields.includes("model_mapping")) {
- const hasAny = mapSonnet.trim() || mapOpus.trim() || mapHaiku.trim();
- if (hasAny) {
- // If user fills any, all 3 are required
- if (!mapSonnet.trim() || !mapOpus.trim() || !mapHaiku.trim()) {
- setError(isZh
- ? '模型映射需要同时填写 Sonnet、Opus、Haiku 三个模型名称'
- : 'Model mapping requires all 3 model names (Sonnet, Opus, Haiku)');
- return;
- }
- const existing = (() => { try { return JSON.parse(roleModelsJson); } catch { return {}; } })();
- roleModelsJson = JSON.stringify({
- ...existing,
- sonnet: mapSonnet.trim(),
- opus: mapOpus.trim(),
- haiku: mapHaiku.trim(),
- });
- } else {
- // All cleared — remove these keys from existing
- const existing = (() => { try { return JSON.parse(roleModelsJson); } catch { return {}; } })();
- delete existing.sonnet;
- delete existing.opus;
- delete existing.haiku;
- roleModelsJson = JSON.stringify(existing);
- }
- }
-
- // Inject model name into role_models_json — merge, don't replace.
- // If the preset exposes model_names and user cleared it, remove the default key.
- if (preset.fields.includes("model_names")) {
- const existing = (() => { try { return JSON.parse(roleModelsJson); } catch { return {}; } })();
- if (modelName.trim()) {
- roleModelsJson = JSON.stringify({ ...existing, default: modelName.trim() });
- } else {
- delete existing.default;
- roleModelsJson = JSON.stringify(existing);
- }
- }
-
- // Validate JSON fields
- for (const [label, val] of [
- ["Extra environment variables", finalExtraEnv],
- ...(isEdit ? [["Headers", headersJson]] : []),
- ] as const) {
- if (val && val.trim()) {
- try { JSON.parse(val); } catch {
- setError(`${label} must be valid JSON`);
- return;
- }
- }
- }
-
- setSaving(true);
- try {
- await onSave({
- name: name.trim() || preset.name,
- provider_type: preset.provider_type,
- protocol: preset.protocol,
- base_url: baseUrl.trim(),
- api_key: apiKey,
- extra_env: finalExtraEnv,
- role_models_json: roleModelsJson,
- headers_json: isEdit ? headersJson.trim() || "{}" : undefined,
- env_overrides_json: isEdit ? envOverridesJson.trim() || "" : undefined,
- notes: isEdit ? notes.trim() : "",
- });
- onOpenChange(false);
- } catch (err) {
- setError(err instanceof Error ? err.message : (isEdit ? "Failed to update provider" : "Failed to add provider"));
- } finally {
- setSaving(false);
- }
- };
-
- return (
-
- );
-}
-
-// ---------------------------------------------------------------------------
-// Preset matcher — find which quick preset a provider was created from
-// ---------------------------------------------------------------------------
-
-function findMatchingPreset(provider: ApiProvider): QuickPreset | undefined {
- // Exact base_url match (most specific)
- if (provider.base_url) {
- const match = QUICK_PRESETS.find(p => p.base_url && p.base_url === provider.base_url);
- if (match) return match;
- }
- // Type-based fallback for known types
- if (provider.provider_type === "bedrock") return QUICK_PRESETS.find(p => p.key === "bedrock");
- if (provider.provider_type === "vertex") return QUICK_PRESETS.find(p => p.key === "vertex");
- if (provider.provider_type === "openrouter") return QUICK_PRESETS.find(p => p.key === "openrouter");
- if (provider.provider_type === "gemini-image") return QUICK_PRESETS.find(p => p.key === "gemini-image");
- if (provider.provider_type === "anthropic" && provider.base_url === "https://api.anthropic.com") {
- return QUICK_PRESETS.find(p => p.key === "anthropic-official");
- }
- // Anthropic-type with custom base_url → anthropic-thirdparty
- if (provider.provider_type === "anthropic" && provider.base_url) {
- return QUICK_PRESETS.find(p => p.key === "anthropic-thirdparty");
- }
- // Custom/OpenAI-compatible
- if (provider.provider_type === "custom" || provider.protocol === "openai-compatible") {
- return QUICK_PRESETS.find(p => p.key === "custom-openai");
- }
- return undefined;
-}
// ---------------------------------------------------------------------------
// Main component
@@ -1002,7 +203,7 @@ export function ProviderManager() {
{t('provider.default')}
{Object.keys(envDetected).length > 0 && (
-
+
ENV
)}
@@ -1061,18 +262,19 @@ export function ProviderManager() {
{GEMINI_IMAGE_MODELS.map((m) => {
const isActive = getGeminiImageModel(provider) === m.value;
return (
-
+
);
})}
diff --git a/src/components/settings/SettingsLayout.tsx b/src/components/settings/SettingsLayout.tsx
index 9fb120e..5c21c04 100644
--- a/src/components/settings/SettingsLayout.tsx
+++ b/src/components/settings/SettingsLayout.tsx
@@ -1,9 +1,9 @@
"use client";
import { useState, useCallback, useSyncExternalStore } from "react";
-import type { Icon } from "@phosphor-icons/react";
-import { Gear, Code, UserCircle, Plug, ChartBar } from "@phosphor-icons/react";
+import { type Icon, Gear, Code, UserCircle, Plug, ChartBar } from "@/components/ui/icon";
import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
import { GeneralSection } from "./GeneralSection";
import { ProviderManager } from "./ProviderManager";
import { CliSettingsSection } from "./CliSettingsSection";
@@ -81,11 +81,12 @@ export function SettingsLayout() {
{/* Sidebar */}
diff --git a/src/components/settings/UsageStatsSection.tsx b/src/components/settings/UsageStatsSection.tsx
index 12d0c03..e440bbc 100644
--- a/src/components/settings/UsageStatsSection.tsx
+++ b/src/components/settings/UsageStatsSection.tsx
@@ -12,6 +12,7 @@ import {
Legend,
ResponsiveContainer,
} from "recharts";
+import { Button } from "@/components/ui/button";
import { useTranslation } from "@/hooks/useTranslation";
// ---------------------------------------------------------------------------
@@ -235,17 +236,17 @@ export function UsageStatsSection() {
{/* Day range selector */}
@@ -290,7 +291,7 @@ export function UsageStatsSection() {
)}
{error && (
-