refactor(ui): simplify AppSwitcher styles and migrate to local SVG icons

- Replace complex gradient animations with clean, minimal tab design
- Migrate from @lobehub/icons CDN to local SVG assets for better reliability
- Fix clippy warning in error.rs (use inline format args)
- Improve code formatting in skill service and commands
- Reduce CSS complexity in AppSwitcher component (removed blur effects and gradients)
- Update BrandIcons to use imported local SVG files instead of dynamic image loading

This improves performance, reduces external dependencies, and provides a cleaner UI experience.
This commit is contained in:
YoVinchen
2025-11-22 01:20:21 +08:00
parent e7545f8cdf
commit de7f93d513
8 changed files with 64 additions and 123 deletions

View File

@@ -56,26 +56,20 @@ pub async fn install_skill(
if !skill.installed { if !skill.installed {
let repo = SkillRepo { let repo = SkillRepo {
owner: skill owner: skill.repo_owner.clone().ok_or_else(|| {
.repo_owner format_skill_error(
.clone() "MISSING_REPO_INFO",
.ok_or_else(|| { &[("directory", &directory), ("field", "owner")],
format_skill_error( None,
"MISSING_REPO_INFO", )
&[("directory", &directory), ("field", "owner")], })?,
None, name: skill.repo_name.clone().ok_or_else(|| {
) format_skill_error(
})?, "MISSING_REPO_INFO",
name: skill &[("directory", &directory), ("field", "name")],
.repo_name None,
.clone() )
.ok_or_else(|| { })?,
format_skill_error(
"MISSING_REPO_INFO",
&[("directory", &directory), ("field", "name")],
None,
)
})?,
branch: skill branch: skill
.repo_branch .repo_branch
.clone() .clone()

View File

@@ -116,6 +116,6 @@ pub fn format_skill_error(
serde_json::to_string(&error_obj).unwrap_or_else(|_| { serde_json::to_string(&error_obj).unwrap_or_else(|_| {
// 如果 JSON 序列化失败,返回简单格式 // 如果 JSON 序列化失败,返回简单格式
format!("ERROR:{}", code) format!("ERROR:{code}")
}) })
} }

View File

@@ -490,7 +490,9 @@ impl SkillService {
// 根据 skills_path 确定源目录路径 // 根据 skills_path 确定源目录路径
let source = if let Some(ref skills_path) = repo.skills_path { let source = if let Some(ref skills_path) = repo.skills_path {
// 如果指定了 skills_path源路径为: temp_dir/skills_path/directory // 如果指定了 skills_path源路径为: temp_dir/skills_path/directory
temp_dir.join(skills_path.trim_matches('/')).join(&directory) temp_dir
.join(skills_path.trim_matches('/'))
.join(&directory)
} else { } else {
// 否则源路径为: temp_dir/directory // 否则源路径为: temp_dir/directory
temp_dir.join(&directory) temp_dir.join(&directory)

View File

@@ -58,7 +58,7 @@ function App() {
const mcpPanelRef = useRef<any>(null); const mcpPanelRef = useRef<any>(null);
const skillsPageRef = useRef<any>(null); const skillsPageRef = useRef<any>(null);
const addActionButtonClass = const addActionButtonClass =
"bg-primary hover:bg-primary/90 text-primary-foreground shadow-lg shadow-primary/20 rounded-full w-8 h-8"; "bg-orange-500 hover:bg-orange-600 dark:bg-orange-500 dark:hover:bg-orange-600 text-white shadow-lg shadow-orange-500/30 dark:shadow-orange-500/40 rounded-full w-8 h-8";
const { data, isLoading, refetch } = useProvidersQuery(activeApp); const { data, isLoading, refetch } = useProvidersQuery(activeApp);
const providers = useMemo(() => data?.providers ?? {}, [data]); const providers = useMemo(() => data?.providers ?? {}, [data]);
@@ -281,7 +281,7 @@ function App() {
return <AgentsPanel onOpenChange={() => setCurrentView("providers")} />; return <AgentsPanel onOpenChange={() => setCurrentView("providers")} />;
default: default:
return ( return (
<div className="mx-auto max-w-5xl space-y-4"> <div className="mx-auto max-w-4xl space-y-4">
<ProviderList <ProviderList
providers={providers} providers={providers}
currentProviderId={currentProviderId} currentProviderId={currentProviderId}
@@ -509,9 +509,8 @@ function App() {
</header> </header>
<main <main
className={`flex-1 overflow-y-auto pb-12 px-6 animate-fade-in scroll-overlay ${ className={`flex-1 overflow-y-auto pb-12 px-6 animate-fade-in scroll-overlay ${currentView === "providers" ? "pt-24" : "pt-20"
currentView === "providers" ? "pt-24" : "pt-20" }`}
}`}
style={{ overflowX: "hidden" }} style={{ overflowX: "hidden" }}
> >
{renderContent()} {renderContent()}
@@ -554,8 +553,8 @@ function App() {
message={ message={
confirmDelete confirmDelete
? t("confirm.deleteProviderMessage", { ? t("confirm.deleteProviderMessage", {
name: confirmDelete.name, name: confirmDelete.name,
}) })
: "" : ""
} }
onConfirm={() => void handleConfirmDelete()} onConfirm={() => void handleConfirmDelete()}

View File

@@ -13,28 +13,22 @@ export function AppSwitcher({ activeApp, onSwitch }: AppSwitcherProps) {
}; };
return ( return (
<div className="glass p-1.5 rounded-full flex items-center gap-1.5"> <div className="inline-flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1 gap-1">
<button <button
type="button" type="button"
onClick={() => handleSwitch("claude")} onClick={() => handleSwitch("claude")}
className={`group relative flex items-center gap-2 px-4 py-2.5 rounded-full text-sm font-semibold overflow-hidden transition-all duration-300 ease-out ${ className={`group inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
activeApp === "claude" activeApp === "claude"
? "text-white scale-[1.02] shadow-[0_12px_35px_-15px_rgba(20,184,166,0.6)] ring-1 ring-white/10" ? "bg-white text-gray-900 shadow-sm dark:bg-gray-900 dark:text-gray-100"
: "text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5" : "text-gray-500 hover:text-gray-900 hover:bg-white/50 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800/60"
}`} }`}
> >
{activeApp === "claude" && (
<div className="absolute inset-0 bg-gradient-to-r from-teal-500 via-emerald-500 to-green-600 rounded-full opacity-90 blur-[1px] transition-all duration-500 -z-10 scale-100" />
)}
{activeApp !== "claude" && (
<div className="absolute inset-0 rounded-full bg-white/0 transition-all duration-300 -z-10" />
)}
<ClaudeIcon <ClaudeIcon
size={16} size={16}
className={ className={
activeApp === "claude" activeApp === "claude"
? "text-white" ? "text-foreground"
: "text-muted-foreground group-hover:text-teal-500 transition-colors" : "text-gray-500 dark:text-gray-400 group-hover:text-foreground transition-colors"
} }
/> />
<span>Claude</span> <span>Claude</span>
@@ -43,24 +37,18 @@ export function AppSwitcher({ activeApp, onSwitch }: AppSwitcherProps) {
<button <button
type="button" type="button"
onClick={() => handleSwitch("codex")} onClick={() => handleSwitch("codex")}
className={`group relative flex items-center gap-2 px-4 py-2.5 rounded-full text-sm font-semibold overflow-hidden transition-all duration-300 ease-out ${ className={`inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
activeApp === "codex" activeApp === "codex"
? "text-white scale-[1.02] shadow-[0_12px_35px_-15px_rgba(59,130,246,0.8)] ring-1 ring-white/10" ? "bg-white text-gray-900 shadow-sm dark:bg-gray-900 dark:text-gray-100"
: "text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5" : "text-gray-500 hover:text-gray-900 hover:bg-white/50 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800/60"
}`} }`}
> >
{activeApp === "codex" && (
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 via-sky-500 to-cyan-500 rounded-full opacity-90 blur-[1px] transition-all duration-500 -z-10 scale-100" />
)}
{activeApp !== "codex" && (
<div className="absolute inset-0 rounded-full bg-white/0 transition-all duration-300 -z-10" />
)}
<CodexIcon <CodexIcon
size={16} size={16}
className={ className={
activeApp === "codex" activeApp === "codex"
? "text-white" ? "text-foreground"
: "text-muted-foreground group-hover:text-blue-500 transition-colors" : "text-gray-500 dark:text-gray-400 group-hover:text-foreground transition-colors"
} }
/> />
<span>Codex</span> <span>Codex</span>
@@ -69,24 +57,18 @@ export function AppSwitcher({ activeApp, onSwitch }: AppSwitcherProps) {
<button <button
type="button" type="button"
onClick={() => handleSwitch("gemini")} onClick={() => handleSwitch("gemini")}
className={`group relative flex items-center gap-2 px-4 py-2.5 rounded-full text-sm font-semibold overflow-hidden transition-all duration-300 ease-out ${ className={`inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
activeApp === "gemini" activeApp === "gemini"
? "text-white scale-[1.02] shadow-[0_12px_35px_-15px_rgba(99,102,241,0.8)] ring-1 ring-white/10" ? "bg-white text-gray-900 shadow-sm dark:bg-gray-900 dark:text-gray-100"
: "text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5" : "text-gray-500 hover:text-gray-900 hover:bg-white/50 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800/60"
}`} }`}
> >
{activeApp === "gemini" && (
<div className="absolute inset-0 bg-gradient-to-r from-indigo-500 via-violet-500 to-purple-600 rounded-full opacity-90 blur-[1px] transition-all duration-500 -z-10 scale-100" />
)}
{activeApp !== "gemini" && (
<div className="absolute inset-0 rounded-full bg-white/0 transition-all duration-300 -z-10" />
)}
<GeminiIcon <GeminiIcon
size={16} size={16}
className={ className={
activeApp === "gemini" activeApp === "gemini"
? "text-white" ? "text-foreground"
: "text-muted-foreground group-hover:text-indigo-500 transition-colors" : "text-gray-500 dark:text-gray-400 group-hover:text-foreground transition-colors"
} }
/> />
<span>Gemini</span> <span>Gemini</span>

View File

@@ -1,84 +1,48 @@
import { useEffect, useState } from "react";
interface IconProps { interface IconProps {
size?: number; size?: number;
className?: string; className?: string;
} }
const LOBE_ICONS_VERSION = "latest"; // pin if needed, e.g. "1.4.0" // 导入本地 SVG 图标
const LOBE_BASE = `https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@${LOBE_ICONS_VERSION}/icons`; import ClaudeSvg from "@/icons/extracted/claude.svg?url";
import OpenAISvg from "@/icons/extracted/openai.svg?url";
function IconImage({ import GeminiSvg from "@/icons/extracted/gemini.svg?url";
urls,
alt,
size,
className,
}: {
urls: string[];
alt: string;
size: number;
className?: string;
}) {
const [index, setIndex] = useState(0);
useEffect(() => {
setIndex(0);
}, [urls.join("|")]);
const src = urls[index] ?? urls[urls.length - 1];
return (
<img
src={src}
width={size}
height={size}
className={className}
alt={alt}
loading="lazy"
referrerPolicy="no-referrer"
onError={() => {
if (index < urls.length - 1) {
setIndex((i) => i + 1);
}
}}
/>
);
}
export function ClaudeIcon({ size = 16, className = "" }: IconProps) { export function ClaudeIcon({ size = 16, className = "" }: IconProps) {
return ( return (
<IconImage <img
urls={[`${LOBE_BASE}/claude-color.svg`, `${LOBE_BASE}/claude.svg`]} src={ClaudeSvg}
size={size} width={size}
height={size}
className={className} className={className}
alt="Claude" alt="Claude"
loading="lazy"
/> />
); );
} }
export function CodexIcon({ size = 16, className = "" }: IconProps) { export function CodexIcon({ size = 16, className = "" }: IconProps) {
return ( return (
<IconImage <img
urls={[ src={OpenAISvg}
`${LOBE_BASE}/openai-color.svg`, width={size}
`${LOBE_BASE}/chatgpt-color.svg`, height={size}
`${LOBE_BASE}/openai.svg`, className={`dark:brightness-0 dark:invert ${className}`}
`${LOBE_BASE}/chatgpt.svg`,
]}
size={size}
className={className}
alt="Codex" alt="Codex"
loading="lazy"
/> />
); );
} }
export function GeminiIcon({ size = 16, className = "" }: IconProps) { export function GeminiIcon({ size = 16, className = "" }: IconProps) {
return ( return (
<IconImage <img
urls={[`${LOBE_BASE}/gemini-color.svg`, `${LOBE_BASE}/gemini.svg`]} src={GeminiSvg}
size={size} width={size}
height={size}
className={className} className={className}
alt="Gemini" alt="Gemini"
loading="lazy"
/> />
); );
} }

View File

@@ -120,7 +120,7 @@ export function ProviderCard({
? "border-primary/50 bg-primary/5 shadow-[0_0_20px_rgba(59,130,246,0.15)]" ? "border-primary/50 bg-primary/5 shadow-[0_0_20px_rgba(59,130,246,0.15)]"
: "hover:scale-[1.01]", : "hover:scale-[1.01]",
dragHandleProps?.isDragging && dragHandleProps?.isDragging &&
"cursor-grabbing border-primary shadow-lg scale-105 z-10", "cursor-grabbing border-primary shadow-lg scale-105 z-10",
)} )}
> >
<div className="absolute inset-0 bg-gradient-to-r from-primary/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" /> <div className="absolute inset-0 bg-gradient-to-r from-primary/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" />
@@ -141,7 +141,7 @@ export function ProviderCard({
</button> </button>
{/* 供应商图标 */} {/* 供应商图标 */}
<div className="w-10 h-10 rounded-lg bg-white/5 flex items-center justify-center border border-white/10 group-hover:scale-105 transition-transform duration-300"> <div className="w-10 h-10 rounded-lg bg-white/5 flex items-center justify-center border border-gray-200 dark:border-white/10 group-hover:scale-105 transition-transform duration-300">
<ProviderIcon <ProviderIcon
icon={provider.icon} icon={provider.icon}
name={provider.name} name={provider.name}
@@ -196,7 +196,7 @@ export function ProviderCard({
</div> </div>
<div className="relative flex items-center ml-auto"> <div className="relative flex items-center ml-auto">
<div className="ml-auto transition-transform duration-200 group-hover:-translate-x-[14rem] group-focus-within:-translate-x-[14rem] sm:group-hover:-translate-x-[16rem] sm:group-focus-within:-translate-x-[16rem]"> <div className="ml-auto transition-transform duration-200 group-hover:-translate-x-[11rem] group-focus-within:-translate-x-[11rem] sm:group-hover:-translate-x-[13rem] sm:group-focus-within:-translate-x-[13rem]">
<UsageFooter <UsageFooter
provider={provider} provider={provider}
providerId={provider.id} providerId={provider.id}

View File

@@ -70,7 +70,7 @@ function getSuggestionI18nKey(suggestion: string): string {
export function formatSkillError( export function formatSkillError(
errorString: string, errorString: string,
t: TFunction, t: TFunction,
defaultTitle: string = "skills.installFailed" defaultTitle: string = "skills.installFailed",
): { title: string; description: string } { ): { title: string; description: string } {
const parsedError = parseSkillError(errorString); const parsedError = parseSkillError(errorString);