ux improvement

This commit is contained in:
Muh. Fani Akbar
2026-02-06 21:34:05 +07:00
parent d0a8a46f03
commit 9cdf0ddd5e
3 changed files with 434 additions and 201 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "cc-mate",
"name": "claude-samurai",
"private": true,
"version": "0.2.0",
"type": "module",
+113 -81
View File
@@ -42,68 +42,86 @@ export function Layout() {
const location = useLocation();
const isProjectsRoute = location.pathname.startsWith("/projects");
const navLinks = [
const navSections = [
{
to: "/",
icon: FileJsonIcon,
label: t("navigation.configurations"),
id: "core",
label: t("navigation.section.core", { defaultValue: "Core" }),
items: [
{
to: "/",
icon: FileJsonIcon,
label: t("navigation.configurations"),
},
{
to: "/projects",
icon: FolderIcon,
label: t("navigation.projects"),
},
{
to: "/mcp",
icon: CpuIcon,
label: t("navigation.mcp"),
},
],
},
{
to: "/projects",
icon: FolderIcon,
label: t("navigation.projects"),
id: "automation",
label: t("navigation.section.automation", { defaultValue: "Automation" }),
items: [
{
to: "/hooks",
icon: CpuIcon,
label: t("hooks.title"),
},
{
to: "/commands",
icon: TerminalIcon,
label: t("navigation.commands"),
},
{
to: "/skills",
icon: SparklesIcon,
label: t("navigation.skills"),
},
{
to: "/agents",
icon: BotIcon,
label: "Agents",
},
],
},
{
to: "/mcp",
icon: CpuIcon,
label: t("navigation.mcp"),
id: "system",
label: t("navigation.section.system", { defaultValue: "System" }),
items: [
{
to: "/memory",
icon: BrainIcon,
label: t("navigation.memory"),
},
{
to: "/plugins",
icon: PackageIcon,
label: t("navigation.plugins"),
},
{
to: "/notification",
icon: BellIcon,
label: t("navigation.notifications"),
},
{
to: "/usage",
icon: ActivityIcon,
label: t("navigation.usage"),
},
{
to: "/settings",
icon: SettingsIcon,
label: t("navigation.settings"),
},
],
},
{
to: "/hooks",
icon: CpuIcon,
label: t("hooks.title"),
},
{
to: "/agents",
icon: BotIcon,
label: "Agents",
},
{
to: "/memory",
icon: BrainIcon,
label: t("navigation.memory"),
},
{
to: "/commands",
icon: TerminalIcon,
label: t("navigation.commands"),
},
{
to: "/skills",
icon: SparklesIcon,
label: t("navigation.skills"),
},
{
to: "/plugins",
icon: PackageIcon,
label: t("navigation.plugins"),
},
{
to: "/notification",
icon: BellIcon,
label: t("navigation.notifications"),
},
{
to: "/usage",
icon: ActivityIcon,
label: t("navigation.usage"),
},
{
to: "/settings",
icon: SettingsIcon,
label: t("navigation.settings"),
},
];
] as const;
return (
<div className="min-h-screen bg-background flex flex-col">
@@ -113,37 +131,51 @@ export function Layout() {
<div className="flex flex-1 overflow-hidden ">
<nav
className="w-[200px] bg-background border-r flex flex-col"
aria-label={t("navigation.primary", {
defaultValue: "Primary navigation",
})}
data-tauri-drag-region
>
<MacosDragRegionSpacer className="h-10" />
<div
className="flex flex-col flex-1 justify-between"
data-tauri-drag-region
>
<ul className="px-3 pt-3 space-y-2">
{navLinks.map((link) => (
<li key={link.to}>
<NavLink
to={link.to}
className={({ isActive }) =>
cn(
"flex items-center gap-2 px-3 py-2 rounded-xl cursor-default select-none ",
{
"bg-primary text-primary-foreground": isActive,
"hover:bg-accent hover:text-accent-foreground":
!isActive,
},
)
}
<div className="flex flex-col flex-1 justify-between">
<div className="px-3 pt-3 space-y-4">
{navSections.map((section) => (
<div key={section.id} className="space-y-2">
<p
className="px-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground/80"
role="heading"
aria-level={2}
>
<link.icon size={14} />
{link.label}
</NavLink>
</li>
{section.label}
</p>
<ul className="space-y-1">
{section.items.map((link) => (
<li key={link.to}>
<NavLink
to={link.to}
className={({ isActive }) =>
cn(
"flex items-center gap-2 px-3 py-2 rounded-xl cursor-default select-none text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background",
{
"bg-primary text-primary-foreground":
isActive,
"hover:bg-accent hover:text-accent-foreground":
!isActive,
},
)
}
>
<link.icon size={14} />
{link.label}
</NavLink>
</li>
))}
</ul>
</div>
))}
</ul>
</div>
<div className="space-y-2">
<div className="space-y-2 px-3 pb-3 pt-4 border-t">
<UpdateButton />
</div>
</div>
+320 -119
View File
@@ -5,17 +5,29 @@ import { useNavigate } from "react-router-dom";
import { GLMDialog } from "@/components/GLMBanner";
import { KimiDialog } from "@/components/KimiDialog";
import { MiniMaxDialog } from "@/components/MiniMaxDialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import {
useCreateConfig,
useDeleteConfig,
useResetToOriginalConfig,
useSetCurrentConfig,
useStores,
@@ -23,11 +35,9 @@ import {
export function ConfigSwitcherPage() {
return (
<div className="">
<section>
<ConfigStores />
</section>
</div>
<section>
<ConfigStores />
</section>
);
}
@@ -39,6 +49,10 @@ function ConfigStores() {
const navigate = useNavigate();
const isOriginalConfigActive = !stores.some((store) => store.using);
const activeStore = stores.find((store) => store.using) ?? null;
const otherStores = activeStore
? stores.filter((store) => store.id !== activeStore.id)
: stores;
const handleStoreClick = (storeId: string, isCurrentStore: boolean) => {
if (!isCurrentStore) {
@@ -53,94 +67,106 @@ function ConfigStores() {
};
const createStoreMutation = useCreateConfig();
const deleteStoreMutation = useDeleteConfig();
const onCreateStore = async () => {
const store = await createStoreMutation.mutateAsync({
title: t("configSwitcher.newConfig"),
settings: {},
});
navigate(`/edit/${store.id}`);
if (store) {
navigate(`/edit/${store.id}`);
}
};
if (stores.length === 0) {
return (
<div
className="flex justify-center items-center h-screen"
data-tauri-drag-region
>
<div className="flex flex-col items-center gap-2">
<Button variant="ghost" onClick={onCreateStore} className="">
<PlusIcon size={14} />
{t("configSwitcher.createConfig")}
</Button>
<p className="text-sm text-muted-foreground" data-tauri-drag-region>
<div className="flex h-[calc(100vh-64px)] items-center justify-center px-6">
<div className="flex max-w-md flex-col items-center gap-3 text-center">
<h1 className="text-xl font-semibold">
{t("configSwitcher.emptyTitle", {
defaultValue: "No configurations yet",
})}
</h1>
<p className="text-sm text-muted-foreground">
{t("configSwitcher.description")}
</p>
<div className="mt-4 space-y-2">
<GLMDialog
trigger={
<ButtonGroup className="mt-2">
<Button
onClick={onCreateStore}
size="sm"
disabled={createStoreMutation.isPending}
>
<PlusIcon size={14} />
{t("configSwitcher.createConfig")}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="text-muted-foreground text-sm"
variant="outline"
size="sm"
disabled={createStoreMutation.isPending}
>
<ZAI />
{t("glm.useZhipuGlm")}
<EllipsisVerticalIcon size={14} />
</Button>
}
/>
<MiniMaxDialog
trigger={
<Button
variant="ghost"
className="text-muted-foreground text-sm"
size="sm"
>
<Minimax />
{t("minimax.useMiniMax")}
</Button>
}
/>
<KimiDialog
trigger={
<Button
variant="ghost"
className="text-muted-foreground text-sm"
size="sm"
>
<Kimi />
{t("kimi.useKimi")}
</Button>
}
/>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<GLMDialog
trigger={
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<ZAI />
{t("glm.useZhipuGlm")}
</DropdownMenuItem>
}
/>
<MiniMaxDialog
trigger={
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<Minimax />
{t("minimax.useMiniMax")}
</DropdownMenuItem>
}
/>
<KimiDialog
trigger={
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<Kimi />
{t("kimi.useKimi")}
</DropdownMenuItem>
}
/>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
<p className="mt-4 text-xs text-muted-foreground">
{t("configSwitcher.emptyHelper", {
defaultValue:
"Start from a template like Zhipu GLM, MiniMax, or Kimi, or create a config from scratch.",
})}
</p>
</div>
</div>
);
}
return (
<div className="">
<div
className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10"
data-tauri-drag-region
>
<div data-tauri-drag-region>
<h3 className="font-bold" data-tauri-drag-region>
<div className="space-y-6 p-4">
<header className="flex items-start justify-between gap-4 border-b pb-3">
<div>
<h1 className="text-lg font-semibold">
{t("configSwitcher.title")}
</h3>
<p className="text-sm text-muted-foreground" data-tauri-drag-region>
</h1>
<p className="mt-1 text-sm text-muted-foreground">
{t("configSwitcher.description")}
</p>
</div>
<ButtonGroup>
<Button
variant="outline"
onClick={onCreateStore}
className="text-muted-foreground"
size="sm"
disabled={createStoreMutation.isPending}
>
<PlusIcon size={14} />
{t("configSwitcher.createConfig")}
@@ -149,8 +175,8 @@ function ConfigStores() {
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="text-muted-foreground"
size="sm"
disabled={createStoreMutation.isPending}
>
<EllipsisVerticalIcon size={14} />
</Button>
@@ -183,71 +209,246 @@ function ConfigStores() {
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
</div>
</header>
{/* <GLMBanner className="mx-4 mt-4" /> */}
<div className="grid grid-cols-3 lg:grid-cols-4 gap-3 p-4">
{/* Fixed Claude Original Config Item */}
<div
role="button"
onClick={handleOriginalConfigClick}
className={cn(
"border rounded-xl p-3 h-[100px] flex flex-col justify-between transition-colors",
{
"bg-primary/10 border-primary border-2": isOriginalConfigActive,
},
)}
>
<div>
<div>{t("configSwitcher.originalConfig")}</div>
<div className="text-xs text-muted-foreground mt-1">
{t("configSwitcher.originalConfigDescription")}
</div>
<section className="space-y-3">
<div className="flex items-center justify-between">
<h2 className="text-sm font-medium">
{t("configSwitcher.activeConfigHeading", {
defaultValue: "Active configuration",
})}
</h2>
<div className="flex items-center gap-2">
<Badge variant="success">
{t("configSwitcher.activeBadge", { defaultValue: "Active" })}
</Badge>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground"
onClick={() => navigate("/settings")}
>
{t("configSwitcher.viewHistory", {
defaultValue: "Backups & history",
})}
</Button>
</div>
</div>
{stores.map((store) => {
const isCurrentStore = store.using;
return (
<div
role="button"
key={store.id}
onClick={() => handleStoreClick(store.id, isCurrentStore)}
className={cn(
"border rounded-xl p-3 h-[100px] flex flex-col justify-between transition-colors disabled:opacity-50",
{
"bg-primary/10 border-primary border-2": isCurrentStore,
},
)}
>
<div>
<div>{store.title}</div>
{store.settings.env?.ANTHROPIC_BASE_URL && (
<div
className="text-xs text-muted-foreground mt-1 truncate "
title={store.settings.env.ANTHROPIC_BASE_URL}
<div className="rounded-xl border bg-card p-4">
{activeStore ? (
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2">
<h3 className="font-medium">{activeStore.title}</h3>
<Badge
variant="secondary"
className="text-[11px] font-medium uppercase"
>
{store.settings.env.ANTHROPIC_BASE_URL}
</div>
{t("configSwitcher.tagDailyDriver", {
defaultValue: "Daily driver",
})}
</Badge>
</div>
{activeStore.settings.env?.ANTHROPIC_BASE_URL && (
<p className="text-xs text-muted-foreground">
{activeStore.settings.env.ANTHROPIC_BASE_URL}
</p>
)}
</div>
<div className="flex justify-end">
<button
className="hover:bg-primary/10 rounded-lg p-2 hover:text-primary"
onClick={(e) => {
e.stopPropagation();
navigate(`/edit/${store.id}`);
}}
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => navigate(`/edit/${activeStore.id}`)}
>
<PencilLineIcon className="text-muted-foreground" size={14} />
</button>
<PencilLineIcon size={14} className="mr-1" />
{t("configSwitcher.edit", { defaultValue: "Edit" })}
</Button>
</div>
</div>
);
})}
</div>
) : (
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2">
<h3 className="font-medium">
{t("configSwitcher.originalConfig", {
defaultValue: "Original Claude config",
})}
</h3>
<Badge
variant="secondary"
className="text-[11px] font-medium uppercase"
>
{t("configSwitcher.tagDefault", {
defaultValue: "Default",
})}
</Badge>
</div>
<p className="text-xs text-muted-foreground">
{t("configSwitcher.originalConfigDescription")}
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={handleOriginalConfigClick}
disabled={
isOriginalConfigActive || resetToOriginalMutation.isPending
}
>
{t("configSwitcher.restoreOriginal", {
defaultValue: "Restore original",
})}
</Button>
</div>
)}
</div>
</section>
<section className="space-y-3">
<div className="flex items-center justify-between">
<h2 className="text-sm font-medium">
{t("configSwitcher.otherConfigsHeading", {
defaultValue: "Other configurations",
})}
</h2>
<p className="text-xs text-muted-foreground">
{t("configSwitcher.otherConfigsHelper", {
defaultValue:
"Use these for experiments, different providers, or project-specific setups.",
})}
</p>
</div>
{otherStores.length === 0 ? (
<p className="text-sm text-muted-foreground">
{t("configSwitcher.noOtherConfigs", {
defaultValue: "You only have the active configuration right now.",
})}
</p>
) : (
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
{otherStores.map((store) => (
<div
key={store.id}
className="flex flex-col justify-between rounded-xl border bg-card p-3 transition-colors hover:border-primary/60"
>
<div className="space-y-1">
<div className="flex items-center justify-between gap-2">
<h3 className="truncate text-sm font-medium">
{store.title}
</h3>
</div>
{store.settings.env?.ANTHROPIC_BASE_URL && (
<p
className="truncate text-xs text-muted-foreground"
title={store.settings.env.ANTHROPIC_BASE_URL}
>
{store.settings.env.ANTHROPIC_BASE_URL}
</p>
)}
</div>
<div className="mt-3 flex items-center justify-between gap-2">
<Button
variant="outline"
size="sm"
className="h-8 px-2 text-xs"
onClick={() => navigate(`/edit/${store.id}`)}
>
<PencilLineIcon size={12} className="mr-1" />
{t("configSwitcher.edit", { defaultValue: "Edit" })}
</Button>
<div className="flex gap-1">
<Button
variant="outline"
size="sm"
className="h-8 px-2 text-xs"
onClick={() =>
handleStoreClick(store.id, store.using)
}
disabled={setCurrentStoreMutation.isPending}
>
{t("configSwitcher.activate", {
defaultValue: "Activate",
})}
</Button>
<Button
variant="outline"
size="sm"
className="h-8 px-2 text-xs"
onClick={() =>
createStoreMutation.mutate({
title: `${store.title} (copy)`,
settings: store.settings,
})
}
disabled={createStoreMutation.isPending}
>
{t("configSwitcher.duplicate", {
defaultValue: "Duplicate",
})}
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-8 px-2 text-xs"
disabled={deleteStoreMutation.isPending}
>
{t("configSwitcher.delete", {
defaultValue: "Delete",
})}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{t("configSwitcher.deleteConfirmTitle", {
defaultValue: "Delete configuration?",
})}
</AlertDialogTitle>
<AlertDialogDescription>
{t(
"configSwitcher.deleteConfirmDescription",
{
defaultValue:
"This will permanently remove this configuration from Claude Samurai. This action cannot be undone.",
},
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={deleteStoreMutation.isPending}>
{t("common.cancel", {
defaultValue: "Cancel",
})}
</AlertDialogCancel>
<AlertDialogAction
onClick={() =>
deleteStoreMutation.mutate({
storeId: store.id,
})
}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
disabled={deleteStoreMutation.isPending}
>
{t("configSwitcher.deleteConfirmAction", {
defaultValue: "Delete",
})}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
</div>
))}
</div>
)}
</section>
</div>
);
}