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:
@@ -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()
|
||||||
|
|||||||
@@ -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}")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
13
src/App.tsx
13
src/App.tsx
@@ -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()}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user