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, "private": true,
"version": "0.2.0", "version": "0.2.0",
"type": "module", "type": "module",
+113 -81
View File
@@ -42,68 +42,86 @@ export function Layout() {
const location = useLocation(); const location = useLocation();
const isProjectsRoute = location.pathname.startsWith("/projects"); const isProjectsRoute = location.pathname.startsWith("/projects");
const navLinks = [ const navSections = [
{ {
to: "/", id: "core",
icon: FileJsonIcon, label: t("navigation.section.core", { defaultValue: "Core" }),
label: t("navigation.configurations"), 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", id: "automation",
icon: FolderIcon, label: t("navigation.section.automation", { defaultValue: "Automation" }),
label: t("navigation.projects"), 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", id: "system",
icon: CpuIcon, label: t("navigation.section.system", { defaultValue: "System" }),
label: t("navigation.mcp"), 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"),
},
],
}, },
{ ] as const;
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"),
},
];
return ( return (
<div className="min-h-screen bg-background flex flex-col"> <div className="min-h-screen bg-background flex flex-col">
@@ -113,37 +131,51 @@ export function Layout() {
<div className="flex flex-1 overflow-hidden "> <div className="flex flex-1 overflow-hidden ">
<nav <nav
className="w-[200px] bg-background border-r flex flex-col" className="w-[200px] bg-background border-r flex flex-col"
aria-label={t("navigation.primary", {
defaultValue: "Primary navigation",
})}
data-tauri-drag-region data-tauri-drag-region
> >
<MacosDragRegionSpacer className="h-10" /> <MacosDragRegionSpacer className="h-10" />
<div <div className="flex flex-col flex-1 justify-between">
className="flex flex-col flex-1 justify-between" <div className="px-3 pt-3 space-y-4">
data-tauri-drag-region {navSections.map((section) => (
> <div key={section.id} className="space-y-2">
<ul className="px-3 pt-3 space-y-2"> <p
{navLinks.map((link) => ( className="px-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground/80"
<li key={link.to}> role="heading"
<NavLink aria-level={2}
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,
},
)
}
> >
<link.icon size={14} /> {section.label}
{link.label} </p>
</NavLink> <ul className="space-y-1">
</li> {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 /> <UpdateButton />
</div> </div>
</div> </div>
+320 -119
View File
@@ -5,17 +5,29 @@ import { useNavigate } from "react-router-dom";
import { GLMDialog } from "@/components/GLMBanner"; import { GLMDialog } from "@/components/GLMBanner";
import { KimiDialog } from "@/components/KimiDialog"; import { KimiDialog } from "@/components/KimiDialog";
import { MiniMaxDialog } from "@/components/MiniMaxDialog"; import { MiniMaxDialog } from "@/components/MiniMaxDialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group"; import { ButtonGroup } from "@/components/ui/button-group";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { import {
useCreateConfig, useCreateConfig,
useDeleteConfig,
useResetToOriginalConfig, useResetToOriginalConfig,
useSetCurrentConfig, useSetCurrentConfig,
useStores, useStores,
@@ -23,11 +35,9 @@ import {
export function ConfigSwitcherPage() { export function ConfigSwitcherPage() {
return ( return (
<div className=""> <section>
<section> <ConfigStores />
<ConfigStores /> </section>
</section>
</div>
); );
} }
@@ -39,6 +49,10 @@ function ConfigStores() {
const navigate = useNavigate(); const navigate = useNavigate();
const isOriginalConfigActive = !stores.some((store) => store.using); 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) => { const handleStoreClick = (storeId: string, isCurrentStore: boolean) => {
if (!isCurrentStore) { if (!isCurrentStore) {
@@ -53,94 +67,106 @@ function ConfigStores() {
}; };
const createStoreMutation = useCreateConfig(); const createStoreMutation = useCreateConfig();
const deleteStoreMutation = useDeleteConfig();
const onCreateStore = async () => { const onCreateStore = async () => {
const store = await createStoreMutation.mutateAsync({ const store = await createStoreMutation.mutateAsync({
title: t("configSwitcher.newConfig"), title: t("configSwitcher.newConfig"),
settings: {}, settings: {},
}); });
navigate(`/edit/${store.id}`); if (store) {
navigate(`/edit/${store.id}`);
}
}; };
if (stores.length === 0) { if (stores.length === 0) {
return ( return (
<div <div className="flex h-[calc(100vh-64px)] items-center justify-center px-6">
className="flex justify-center items-center h-screen" <div className="flex max-w-md flex-col items-center gap-3 text-center">
data-tauri-drag-region <h1 className="text-xl font-semibold">
> {t("configSwitcher.emptyTitle", {
<div className="flex flex-col items-center gap-2"> defaultValue: "No configurations yet",
<Button variant="ghost" onClick={onCreateStore} className=""> })}
<PlusIcon size={14} /> </h1>
{t("configSwitcher.createConfig")} <p className="text-sm text-muted-foreground">
</Button>
<p className="text-sm text-muted-foreground" data-tauri-drag-region>
{t("configSwitcher.description")} {t("configSwitcher.description")}
</p> </p>
<div className="mt-4 space-y-2"> <ButtonGroup className="mt-2">
<GLMDialog <Button
trigger={ onClick={onCreateStore}
size="sm"
disabled={createStoreMutation.isPending}
>
<PlusIcon size={14} />
{t("configSwitcher.createConfig")}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button <Button
variant="ghost" variant="outline"
className="text-muted-foreground text-sm"
size="sm" size="sm"
disabled={createStoreMutation.isPending}
> >
<ZAI /> <EllipsisVerticalIcon size={14} />
{t("glm.useZhipuGlm")}
</Button> </Button>
} </DropdownMenuTrigger>
/> <DropdownMenuContent align="end">
<MiniMaxDialog <GLMDialog
trigger={ trigger={
<Button <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
variant="ghost" <ZAI />
className="text-muted-foreground text-sm" {t("glm.useZhipuGlm")}
size="sm" </DropdownMenuItem>
> }
<Minimax /> />
{t("minimax.useMiniMax")} <MiniMaxDialog
</Button> trigger={
} <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
/> <Minimax />
<KimiDialog {t("minimax.useMiniMax")}
trigger={ </DropdownMenuItem>
<Button }
variant="ghost" />
className="text-muted-foreground text-sm" <KimiDialog
size="sm" trigger={
> <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<Kimi /> <Kimi />
{t("kimi.useKimi")} {t("kimi.useKimi")}
</Button> </DropdownMenuItem>
} }
/> />
</div> </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>
</div> </div>
); );
} }
return ( return (
<div className=""> <div className="space-y-6 p-4">
<div <header className="flex items-start justify-between gap-4 border-b pb-3">
className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10" <div>
data-tauri-drag-region <h1 className="text-lg font-semibold">
>
<div data-tauri-drag-region>
<h3 className="font-bold" data-tauri-drag-region>
{t("configSwitcher.title")} {t("configSwitcher.title")}
</h3> </h1>
<p className="text-sm text-muted-foreground" data-tauri-drag-region> <p className="mt-1 text-sm text-muted-foreground">
{t("configSwitcher.description")} {t("configSwitcher.description")}
</p> </p>
</div> </div>
<ButtonGroup> <ButtonGroup>
<Button <Button
variant="outline"
onClick={onCreateStore} onClick={onCreateStore}
className="text-muted-foreground"
size="sm" size="sm"
disabled={createStoreMutation.isPending}
> >
<PlusIcon size={14} /> <PlusIcon size={14} />
{t("configSwitcher.createConfig")} {t("configSwitcher.createConfig")}
@@ -149,8 +175,8 @@ function ConfigStores() {
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
variant="outline" variant="outline"
className="text-muted-foreground"
size="sm" size="sm"
disabled={createStoreMutation.isPending}
> >
<EllipsisVerticalIcon size={14} /> <EllipsisVerticalIcon size={14} />
</Button> </Button>
@@ -183,71 +209,246 @@ function ConfigStores() {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</ButtonGroup> </ButtonGroup>
</div> </header>
{/* <GLMBanner className="mx-4 mt-4" /> */} <section className="space-y-3">
<div className="flex items-center justify-between">
<div className="grid grid-cols-3 lg:grid-cols-4 gap-3 p-4"> <h2 className="text-sm font-medium">
{/* Fixed Claude Original Config Item */} {t("configSwitcher.activeConfigHeading", {
<div defaultValue: "Active configuration",
role="button" })}
onClick={handleOriginalConfigClick} </h2>
className={cn( <div className="flex items-center gap-2">
"border rounded-xl p-3 h-[100px] flex flex-col justify-between transition-colors", <Badge variant="success">
{ {t("configSwitcher.activeBadge", { defaultValue: "Active" })}
"bg-primary/10 border-primary border-2": isOriginalConfigActive, </Badge>
}, <Button
)} variant="ghost"
> size="sm"
<div> className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground"
<div>{t("configSwitcher.originalConfig")}</div> onClick={() => navigate("/settings")}
<div className="text-xs text-muted-foreground mt-1"> >
{t("configSwitcher.originalConfigDescription")} {t("configSwitcher.viewHistory", {
</div> defaultValue: "Backups & history",
})}
</Button>
</div> </div>
</div> </div>
{stores.map((store) => { <div className="rounded-xl border bg-card p-4">
const isCurrentStore = store.using; {activeStore ? (
return ( <div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div <div className="space-y-1">
role="button" <div className="flex items-center gap-2">
key={store.id} <h3 className="font-medium">{activeStore.title}</h3>
onClick={() => handleStoreClick(store.id, isCurrentStore)} <Badge
className={cn( variant="secondary"
"border rounded-xl p-3 h-[100px] flex flex-col justify-between transition-colors disabled:opacity-50", className="text-[11px] font-medium uppercase"
{
"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}
> >
{store.settings.env.ANTHROPIC_BASE_URL} {t("configSwitcher.tagDailyDriver", {
</div> 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>
<div className="flex gap-2">
<div className="flex justify-end"> <Button
<button variant="outline"
className="hover:bg-primary/10 rounded-lg p-2 hover:text-primary" size="sm"
onClick={(e) => { onClick={() => navigate(`/edit/${activeStore.id}`)}
e.stopPropagation();
navigate(`/edit/${store.id}`);
}}
> >
<PencilLineIcon className="text-muted-foreground" size={14} /> <PencilLineIcon size={14} className="mr-1" />
</button> {t("configSwitcher.edit", { defaultValue: "Edit" })}
</Button>
</div> </div>
</div> </div>
); ) : (
})} <div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
</div> <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> </div>
); );
} }