feat(frontend): 技能/专家广场展示调整 + 通知合并入设置菜单

- 技能与专家「广场」标签只展示内置和已发布项,Tab 顺序调为「我的」在前并默认选中
- 「智能体」全量更名为「专家」
- 通知由侧边栏铃铛改为「设置和更多」菜单内的弹窗入口,合并通知列表与通知设置

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xuhdandClaude Opus 4.7 committed 2026-05-15 15:08:56 +08:00
1 parent aeba15eb16
commit ebb6be816b
10 files changed
+395 -359

No files matched your search

@@ -16,7 +16,7 @@ export function AgentGallery() {
const { t } = useI18n();
const { agents, isLoading } = useAgents();
const router = useRouter();
const [activeTab, setActiveTab] = useState<"all" | "mine">("all");
const [activeTab, setActiveTab] = useState<"all" | "mine">("mine");
const storedAuth = useMemo(() => getStoredAuth(), []);
const currentUserId = storedAuth?.user_id ?? null;
@@ -84,8 +84,8 @@ export function AgentGallery() {
className="flex flex-1 flex-col overflow-hidden"
>
<TabsList className="mx-6 mt-4 w-auto self-start">
<TabsTrigger value="all">全部</TabsTrigger>
<TabsTrigger value="mine">自己的</TabsTrigger>
<TabsTrigger value="mine">我的</TabsTrigger>
<TabsTrigger value="all">广场</TabsTrigger>
</TabsList>
<TabsContent value="all" className="flex-1 overflow-y-auto p-6 mt-0">
@@ -1,212 +0,0 @@
"use client";
import { BellIcon, CheckCheckIcon, Loader2, Trash2Icon } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
SidebarMenuButton,
useSidebar,
} from "@/components/ui/sidebar";
import { useI18n } from "@/core/i18n/hooks";
import {
useDeleteNotification,
useMarkAllNotificationsRead,
useMarkNotificationRead,
useNotifications,
useUnreadCount,
} from "@/core/notifications";
import type { NotificationItem } from "@/core/notifications";
import { formatTimeAgo } from "@/core/utils/datetime";
import { cn } from "@/lib/utils";
const BELL_LIMIT = 20;
export function NotificationBell() {
const { t } = useI18n();
const { open: isSidebarOpen } = useSidebar();
const [open, setOpen] = useState(false);
// Render badge from the lightweight count endpoint so the bell stays cheap
// when nobody opens it. Only when the user actually opens the dropdown do
// we pay for the full list query.
const { data: unreadCount = 0 } = useUnreadCount();
const {
data: notifications,
isLoading,
isFetching,
} = useNotifications({ limit: BELL_LIMIT });
const markRead = useMarkNotificationRead();
const markAllRead = useMarkAllNotificationsRead();
const removeNotification = useDeleteNotification();
const hasUnread = unreadCount > 0;
const items = notifications ?? [];
async function handleClickItem(notification: NotificationItem) {
if (notification.read) return;
try {
await markRead.mutateAsync(notification.id);
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
}
async function handleMarkAllRead() {
if (!hasUnread) return;
try {
await markAllRead.mutateAsync();
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
}
async function handleDelete(notification: NotificationItem) {
try {
await removeNotification.mutateAsync(notification.id);
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
}
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
aria-label={t.notifications.bellAria}
>
{isSidebarOpen ? (
<div className="text-sidebar-foreground/85 flex w-full items-center gap-2 text-left text-sm">
<div className="relative">
<BellIcon className="size-4" />
{hasUnread && (
<span className="absolute -right-1 -top-1 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-red-500 px-1 text-[10px] font-medium leading-none text-white">
{unreadCount > 99 ? "99+" : unreadCount}
</span>
)}
</div>
<span>{t.notifications.title}</span>
{hasUnread && (
<span className="text-muted-foreground ml-auto text-xs">
{t.notifications.unreadShort.replace("{count}", String(unreadCount))}
</span>
)}
</div>
) : (
<div className="flex size-full items-center justify-center">
<div className="relative">
<BellIcon className="text-sidebar-foreground/80 size-4" />
{hasUnread && (
<span className="absolute -right-1 -top-1 size-2 rounded-full bg-red-500" />
)}
</div>
</div>
)}
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
side="right"
sideOffset={8}
className="w-96 p-0"
>
<div className="flex items-center justify-between gap-2 border-b px-3 py-2">
<div className="flex items-center gap-2">
<BellIcon className="size-4" />
<span className="text-sm font-semibold">
{t.notifications.title}
</span>
{isFetching && !isLoading && (
<Loader2 className="text-muted-foreground size-3 animate-spin" />
)}
</div>
<Button
size="sm"
variant="ghost"
className="h-7 px-2 text-xs"
disabled={!hasUnread || markAllRead.isPending}
onClick={() => void handleMarkAllRead()}
>
<CheckCheckIcon className="mr-1 size-3.5" />
{t.notifications.markAllRead}
</Button>
</div>
<ScrollArea className="max-h-[60vh]">
{isLoading ? (
<div className="text-muted-foreground flex items-center justify-center gap-2 py-8 text-sm">
<Loader2 className="size-4 animate-spin" />
{t.common.loading}
</div>
) : items.length === 0 ? (
<div className="text-muted-foreground py-10 text-center text-sm">
{t.notifications.empty}
</div>
) : (
<ul className="divide-y">
{items.map((notification) => (
<li
key={notification.id}
className={cn(
"group cursor-pointer px-3 py-2 transition-colors hover:bg-accent/40",
!notification.read && "bg-blue-500/5",
)}
onClick={() => void handleClickItem(notification)}
>
<div className="flex items-start gap-2">
{!notification.read && (
<span className="mt-1.5 size-2 shrink-0 rounded-full bg-blue-500" />
)}
<div className="min-w-0 flex-1">
<div className="flex items-baseline justify-between gap-2">
<p
className={cn(
"truncate text-sm",
!notification.read && "font-medium",
)}
title={notification.title}
>
{notification.title || notification.type}
</p>
{notification.created_at && (
<span className="text-muted-foreground shrink-0 text-[10px]">
{formatTimeAgo(notification.created_at)}
</span>
)}
</div>
{notification.body && (
<p className="text-muted-foreground line-clamp-2 text-xs">
{notification.body}
</p>
)}
</div>
<Button
size="icon"
variant="ghost"
className="text-muted-foreground hover:text-destructive size-6 shrink-0 opacity-0 group-hover:opacity-100"
onClick={(event) => {
event.stopPropagation();
void handleDelete(notification);
}}
aria-label={t.notifications.deleteAria}
>
<Trash2Icon className="size-3" />
</Button>
</div>
</li>
))}
</ul>
)}
</ScrollArea>
</DropdownMenuContent>
</DropdownMenu>
);
}
@@ -0,0 +1,329 @@
"use client";
import {
BellIcon,
CheckCheckIcon,
ChevronLeftIcon,
Loader2,
SettingsIcon,
Trash2Icon,
XIcon,
} from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogTitle,
} from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Switch } from "@/components/ui/switch";
import { useI18n } from "@/core/i18n/hooks";
import { useNotification } from "@/core/notification/hooks";
import {
useDeleteNotification,
useMarkAllNotificationsRead,
useMarkNotificationRead,
useNotifications,
useUnreadCount,
} from "@/core/notifications";
import type { NotificationItem } from "@/core/notifications";
import { useLocalSettings } from "@/core/settings";
import { formatTimeAgo } from "@/core/utils/datetime";
import { cn } from "@/lib/utils";
const BELL_LIMIT = 20;
type NotificationView = "list" | "settings";
export function NotificationDialog({
open,
onOpenChange,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
const { t } = useI18n();
const [view, setView] = useState<NotificationView>("list");
const {
data: notifications,
isLoading,
isFetching,
} = useNotifications({ limit: BELL_LIMIT });
const { data: unreadCount = 0 } = useUnreadCount();
const markRead = useMarkNotificationRead();
const markAllRead = useMarkAllNotificationsRead();
const removeNotification = useDeleteNotification();
const hasUnread = unreadCount > 0;
const items = notifications ?? [];
function handleOpenChange(next: boolean) {
onOpenChange(next);
// 关闭弹窗时回到列表视图,下次打开默认看到通知列表。
if (!next) setView("list");
}
async function handleClickItem(notification: NotificationItem) {
if (notification.read) return;
try {
await markRead.mutateAsync(notification.id);
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
}
async function handleMarkAllRead() {
if (!hasUnread) return;
try {
await markAllRead.mutateAsync();
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
}
async function handleDelete(notification: NotificationItem) {
try {
await removeNotification.mutateAsync(notification.id);
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent
showCloseButton={false}
className="flex max-h-[80vh] flex-col gap-0 overflow-hidden p-0 sm:max-w-xl"
aria-describedby={undefined}
>
{view === "list" ? (
<>
<div className="flex items-center justify-between gap-2 border-b px-3 py-3">
<div className="flex items-center gap-2">
<BellIcon className="size-4" />
<DialogTitle className="text-base font-semibold">
{t.notifications.title}
</DialogTitle>
{isFetching && !isLoading && (
<Loader2 className="text-muted-foreground size-3 animate-spin" />
)}
</div>
<div className="flex items-center gap-1">
<Button
size="sm"
variant="ghost"
className="h-7 px-2 text-xs"
disabled={!hasUnread || markAllRead.isPending}
onClick={() => void handleMarkAllRead()}
>
<CheckCheckIcon className="mr-1 size-3.5" />
{t.notifications.markAllRead}
</Button>
<Button
size="icon"
variant="ghost"
className="text-muted-foreground size-7"
aria-label={t.notifications.settingsAria}
onClick={() => setView("settings")}
>
<SettingsIcon className="size-4" />
</Button>
<DialogClose asChild>
<Button
size="icon"
variant="ghost"
className="text-muted-foreground size-7"
aria-label={t.common.close}
>
<XIcon className="size-4" />
</Button>
</DialogClose>
</div>
</div>
<ScrollArea className="min-h-0 flex-1">
<div className="w-full px-3 py-3">
{isLoading ? (
<div className="text-muted-foreground flex items-center justify-center gap-2 py-20 text-sm">
<Loader2 className="size-4 animate-spin" />
{t.common.loading}
</div>
) : items.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
<div className="bg-muted flex size-14 items-center justify-center rounded-full">
<BellIcon className="text-muted-foreground size-6" />
</div>
<p className="text-muted-foreground text-sm">
{t.notifications.empty}
</p>
</div>
) : (
<ul className="flex flex-col gap-2">
{items.map((notification) => (
<li
key={notification.id}
className={cn(
"group cursor-pointer rounded-lg border px-4 py-3 transition-colors hover:bg-accent/50",
!notification.read
? "border-blue-500/30 bg-blue-500/5"
: "border-border",
)}
onClick={() => void handleClickItem(notification)}
>
<div className="flex items-start gap-3">
<span
className={cn(
"mt-1.5 size-2 shrink-0 rounded-full",
notification.read ? "bg-transparent" : "bg-blue-500",
)}
/>
<div className="min-w-0 flex-1">
<div className="flex items-baseline justify-between gap-3">
<p
className={cn(
"truncate text-sm",
notification.read
? "font-medium"
: "font-semibold",
)}
title={notification.title}
>
{notification.title || notification.type}
</p>
{notification.created_at && (
<span className="text-muted-foreground shrink-0 text-xs">
{formatTimeAgo(notification.created_at)}
</span>
)}
</div>
{notification.body && (
<p className="text-muted-foreground mt-1 line-clamp-2 text-xs leading-relaxed">
{notification.body}
</p>
)}
</div>
<Button
size="icon"
variant="ghost"
className="text-muted-foreground hover:text-destructive size-7 shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
onClick={(event) => {
event.stopPropagation();
void handleDelete(notification);
}}
aria-label={t.notifications.deleteAria}
>
<Trash2Icon className="size-3.5" />
</Button>
</div>
</li>
))}
</ul>
)}
</div>
</ScrollArea>
</>
) : (
<NotificationSettingsView onBack={() => setView("list")} />
)}
</DialogContent>
</Dialog>
);
}
function NotificationSettingsView({ onBack }: { onBack: () => void }) {
const { t } = useI18n();
const { permission, isSupported, requestPermission, showNotification } =
useNotification();
const [settings, setSettings] = useLocalSettings();
const handleRequestPermission = async () => {
await requestPermission();
};
const handleTestNotification = () => {
showNotification(t.settings.notification.testTitle, {
body: t.settings.notification.testBody,
});
};
const handleEnableNotification = (enabled: boolean) => {
setSettings("notification", { enabled });
};
return (
<>
<div className="flex items-center justify-between gap-2 border-b px-3 py-3">
<div className="flex items-center gap-2">
<Button
size="icon"
variant="ghost"
className="text-muted-foreground size-7"
aria-label={t.notifications.backAria}
onClick={onBack}
>
<ChevronLeftIcon className="size-4" />
</Button>
<DialogTitle className="text-base font-semibold">
{t.notifications.settingsTitle}
</DialogTitle>
</div>
<DialogClose asChild>
<Button
size="icon"
variant="ghost"
className="text-muted-foreground size-7"
aria-label={t.common.close}
>
<XIcon className="size-4" />
</Button>
</DialogClose>
</div>
<div className="mx-auto flex w-full max-w-md flex-col gap-4 px-3 py-6">
{!isSupported ? (
<p className="text-muted-foreground text-sm">
{t.settings.notification.notSupported}
</p>
) : (
<>
<div className="flex items-start justify-between gap-3">
<p className="text-muted-foreground text-sm">
{t.settings.notification.description}
</p>
<Switch
disabled={permission !== "granted"}
checked={
permission === "granted" && settings.notification.enabled
}
onCheckedChange={handleEnableNotification}
/>
</div>
{permission === "default" && (
<Button onClick={handleRequestPermission} variant="default">
<BellIcon className="mr-2 size-4" />
{t.settings.notification.requestPermission}
</Button>
)}
{permission === "denied" && (
<p className="text-muted-foreground rounded-md border border-amber-200 bg-amber-50 p-3 text-sm dark:border-amber-800 dark:bg-amber-950/50">
{t.settings.notification.deniedHint}
</p>
)}
{permission === "granted" && settings.notification.enabled && (
<Button onClick={handleTestNotification} variant="outline">
<BellIcon className="mr-2 size-4" />
{t.settings.notification.testButton}
</Button>
)}
</>
)}
</div>
</>
);
}
@@ -1,92 +0,0 @@
"use client";
import { BellIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { useI18n } from "@/core/i18n/hooks";
import { useNotification } from "@/core/notification/hooks";
import { useLocalSettings } from "@/core/settings";
import { SettingsSection } from "./settings-section";
export function NotificationSettingsPage() {
const { t } = useI18n();
const { permission, isSupported, requestPermission, showNotification } =
useNotification();
const [settings, setSettings] = useLocalSettings();
const handleRequestPermission = async () => {
await requestPermission();
};
const handleTestNotification = () => {
showNotification(t.settings.notification.testTitle, {
body: t.settings.notification.testBody,
});
};
const handleEnableNotification = async (enabled: boolean) => {
setSettings("notification", {
enabled,
});
};
if (!isSupported) {
return (
<SettingsSection
title={t.settings.notification.title}
description={t.settings.notification.description}
>
<p className="text-muted-foreground text-sm">
{t.settings.notification.notSupported}
</p>
</SettingsSection>
);
}
return (
<SettingsSection
title={t.settings.notification.title}
description={
<div className="flex items-center gap-2">
<div>{t.settings.notification.description}</div>
<div>
<Switch
disabled={permission !== "granted"}
checked={
permission === "granted" && settings.notification.enabled
}
onCheckedChange={handleEnableNotification}
/>
</div>
</div>
}
>
<div className="flex flex-col gap-4">
{permission === "default" && (
<Button onClick={handleRequestPermission} variant="default">
<BellIcon className="mr-2 size-4" />
{t.settings.notification.requestPermission}
</Button>
)}
{permission === "denied" && (
<p className="text-muted-foreground rounded-md border border-amber-200 bg-amber-50 p-3 text-sm dark:border-amber-800 dark:bg-amber-950/50">
{t.settings.notification.deniedHint}
</p>
)}
{permission === "granted" && settings.notification.enabled && (
<div className="flex flex-col gap-4">
<Button onClick={handleTestNotification} variant="outline">
<BellIcon className="mr-2 size-4" />
{t.settings.notification.testButton}
</Button>
</div>
)}
</div>
</SettingsSection>
);
}
@@ -1,7 +1,6 @@
"use client";
import {
BellIcon,
BrainIcon,
PaletteIcon,
SparklesIcon,
@@ -21,7 +20,6 @@ import { AboutSettingsPage } from "@/components/workspace/settings/about-setting
import { AccountSettingsPage } from "@/components/workspace/settings/account-settings-page";
import { AppearanceSettingsPage } from "@/components/workspace/settings/appearance-settings-page";
import { MemorySettingsPage } from "@/components/workspace/settings/memory-settings-page";
import { NotificationSettingsPage } from "@/components/workspace/settings/notification-settings-page";
import { SkillSettingsPage } from "@/components/workspace/settings/skill-settings-page";
import { ToolSettingsPage } from "@/components/workspace/settings/tool-settings-page";
import { useI18n } from "@/core/i18n/hooks";
@@ -33,7 +31,6 @@ type SettingsSection =
| "memory"
| "tools"
| "skills"
| "notification"
| "about";
type SettingsDialogProps = React.ComponentProps<typeof Dialog> & {
@@ -61,11 +58,6 @@ export function SettingsDialog(props: SettingsDialogProps) {
label: t.settings.sections.appearance,
icon: PaletteIcon,
},
{
id: "notification",
label: t.settings.sections.notification,
icon: BellIcon,
},
{
id: "memory",
label: t.settings.sections.memory,
@@ -85,7 +77,6 @@ export function SettingsDialog(props: SettingsDialogProps) {
t.settings.sections.memory,
t.settings.sections.tools,
t.settings.sections.skills,
t.settings.sections.notification,
t.settings.sections.about,
],
);
@@ -135,7 +126,6 @@ export function SettingsDialog(props: SettingsDialogProps) {
{activeSection === "appearance" && <AppearanceSettingsPage />}
{activeSection === "memory" && <MemorySettingsPage />}
{activeSection === "tools" && <ToolSettingsPage />}
{activeSection === "notification" && <NotificationSettingsPage />}
{activeSection === "skills" && <SkillSettingsPage />}
{activeSection === "about" && <AboutSettingsPage />}
</div>
@@ -92,7 +92,7 @@ function SkillSettingsList({
}) {
const { t } = useI18n();
const router = useRouter();
const [filter, setFilter] = useState<string>("all");
const [filter, setFilter] = useState<string>("mine");
const [installOpen, setInstallOpen] = useState(false);
const [staleSheetOpen, setStaleSheetOpen] = useState(false);
const [archiveSheetOpen, setArchiveSheetOpen] = useState(false);
@@ -101,9 +101,9 @@ function SkillSettingsList({
const currentUserId = storedAuth?.user_id ?? null;
const isAdmin = storedAuth?.system_role === "admin";
// Visible-to-me skills, classified into the three buckets the UI cares about.
// "All" = built-in + anyone-published-custom + mine (active state only).
// "Mine" = strictly my own custom skills (active state only).
// 把可见技能分类成 UI 关心的几个桶。
// "All" = 内置 + 已发布的技能(仅 active 状态);自己的技能也要自己发布了才出现。
// "Mine" = 严格属于我自己的自定义技能(仅 active 状态)。
const { activeAll, activeMine, staleVisible, archivedVisible } = useMemo(() => {
const all: Skill[] = [];
const mine: Skill[] = [];
@@ -115,8 +115,6 @@ function SkillSettingsList({
const isOwnedByMe =
isCustom && !!skill.owner_user_id && skill.owner_user_id === currentUserId;
const isBuiltin = skill.builtin ?? !skill.owner_user_id;
const isPublishedByOthers =
isCustom && !isOwnedByMe && !isBuiltin && !!skill.published;
if (state === "stale") {
stale.push(skill);
@@ -127,7 +125,7 @@ function SkillSettingsList({
continue;
}
// state === active
if (!isCustom || isBuiltin || isOwnedByMe || isPublishedByOthers) {
if (isBuiltin || !!skill.published) {
all.push(skill);
}
if (isOwnedByMe) {
@@ -156,8 +154,8 @@ function SkillSettingsList({
<div className="flex flex-wrap items-center gap-2">
<Tabs value={filter} onValueChange={setFilter}>
<TabsList variant="line">
<TabsTrigger value="all">{t.settings.skills.tabAll}</TabsTrigger>
<TabsTrigger value="mine">{t.settings.skills.tabMine}</TabsTrigger>
<TabsTrigger value="all">{t.settings.skills.tabAll}</TabsTrigger>
</TabsList>
</Tabs>
<Button
@@ -1,6 +1,7 @@
"use client";
import {
BellIcon,
ChevronsUpDown,
GaugeIcon,
MessageSquareCodeIcon,
@@ -26,8 +27,9 @@ import {
} from "@/components/ui/sidebar";
import { getStoredAuth } from "@/core/auth";
import { useI18n } from "@/core/i18n/hooks";
import { useUnreadCount } from "@/core/notifications";
import { NotificationBell } from "./notification-bell";
import { NotificationDialog } from "./notification-dialog";
import { useNavigate } from "react-router-dom";
import { SettingsDialog } from "./settings";
@@ -57,11 +59,13 @@ export function WorkspaceNavMenu() {
const isAdmin = getStoredAuth()?.system_role === "admin";
const [settingsOpen, setSettingsOpen] = useState(false);
const [settingsDefaultSection, setSettingsDefaultSection] = useState<
"appearance" | "memory" | "tools" | "notification" | "about"
"appearance" | "memory" | "tools" | "about"
>("appearance");
const [notificationOpen, setNotificationOpen] = useState(false);
const [mounted, setMounted] = useState(false);
const { open: isSidebarOpen } = useSidebar();
const { t } = useI18n();
const { data: unreadCount = 0 } = useUnreadCount();
useEffect(() => {
setMounted(true);
@@ -74,10 +78,11 @@ export function WorkspaceNavMenu() {
onOpenChange={setSettingsOpen}
defaultSection={settingsDefaultSection}
/>
<NotificationDialog
open={notificationOpen}
onOpenChange={setNotificationOpen}
/>
<SidebarMenu className="w-full">
<SidebarMenuItem>
<NotificationBell />
</SidebarMenuItem>
<SidebarMenuItem>
{mounted ? (
<DropdownMenu>
@@ -95,6 +100,15 @@ export function WorkspaceNavMenu() {
sideOffset={4}
>
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => setNotificationOpen(true)}>
<BellIcon />
{t.notifications.title}
{unreadCount > 0 && (
<span className="ml-auto flex h-4 min-w-4 items-center justify-center rounded-full bg-red-500 px-1 text-[10px] font-medium leading-none text-white">
{unreadCount > 99 ? "99+" : unreadCount}
</span>
)}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setSettingsDefaultSection("appearance");
+7 -4
View File
@@ -177,10 +177,10 @@ export const enUS: Translations = {
// Agents
agents: {
title: "Agents",
title: "Experts",
description:
"Create and manage custom agents with specialized prompts and capabilities.",
newAgent: "New Agent",
"Create and manage custom experts with specialized prompts and capabilities.",
newAgent: "New Expert",
emptyTitle: "No custom agents yet",
emptyDescription:
"Create your first custom agent with a specialized system prompt.",
@@ -456,7 +456,7 @@ export const enUS: Translations = {
badgeSourceAgentTitle: "Auto-generated by an agent during a conversation",
badgeSourceUpload: "Uploaded",
badgeSourceUploadTitle: "Manually uploaded or edited by a user",
tabAll: "All",
tabAll: "Square",
tabMine: "Mine",
entryStale: "Stale {count}",
entryArchived: "Archived {count}",
@@ -632,5 +632,8 @@ export const enUS: Translations = {
markAllRead: "Mark all read",
empty: "No notifications",
deleteAria: "Delete notification",
settingsAria: "Notification settings",
backAria: "Back",
settingsTitle: "Notification settings",
},
};
@@ -7,6 +7,9 @@ export interface NotificationsTranslations {
markAllRead: string;
empty: string;
deleteAria: string;
settingsAria: string;
backAria: string;
settingsTitle: string;
}
export interface Translations {
+28 -25
View File
@@ -1,4 +1,4 @@
import {
import {
CompassIcon,
GraduationCapIcon,
ImageIcon,
@@ -89,7 +89,7 @@ export const zhCN: Translations = {
reasoningModeDescription: "思考后再行动,在时间与准确性之间取得平衡",
proMode: "专业问答",
proModeDescription: "思考、计划再执行,获得更精准的结果,可能需要更多时间",
ultraMode: "多智能体问答",
ultraMode: "多专家问答",
ultraModeDescription:
"继承自专业问答模式,可调用子代理分工协作,适合复杂多步骤任务,能力最强",
reasoningEffort: "推理深度",
@@ -165,47 +165,47 @@ export const zhCN: Translations = {
chats: "对话",
recentChats: "最近的对话",
demoChats: "演示对话",
agents: "智能体",
agents: "专家",
users: "用户管理",
},
// Agents
agents: {
title: "智能体",
description: "创建和管理具有专属 Prompt 与能力的自定义智能体。",
newAgent: "新建智能体",
emptyTitle: "还没有自定义智能体",
emptyDescription: "创建你的第一个自定义智能体,设置专属系统提示词。",
title: "专家",
description: "创建和管理具有专属 Prompt 与能力的自定义专家。",
newAgent: "新建专家",
emptyTitle: "还没有自定义专家",
emptyDescription: "创建你的第一个自定义专家,设置专属系统提示词。",
chat: "对话",
delete: "删除",
deleteConfirm: "确定要删除该智能体吗?此操作不可撤销。",
deleteSuccess: "智能体已删除",
deleteConfirm: "确定要删除该专家吗?此操作不可撤销。",
deleteSuccess: "专家已删除",
newChat: "新对话",
createPageTitle: "设计你的智能体",
createPageSubtitle: "描述你想要的智能体,我来帮你通过对话创建。",
nameStepTitle: "给新智能体起个名字",
createPageTitle: "设计你的专家",
createPageSubtitle: "描述你想要的专家,我来帮你通过对话创建。",
nameStepTitle: "给新专家起个名字",
nameStepHint:
"只允许字母、数字和连字符,存储时自动转为小写(例如 code-reviewer)",
nameStepPlaceholder: "例如 code-reviewer",
nameStepContinue: "继续",
nameStepInvalidError: "名称无效,只允许字母、数字和连字符",
nameStepAlreadyExistsError: "已存在同名智能体",
nameStepAlreadyExistsError: "已存在同名专家",
nameStepNetworkError: "网络请求失败,请检查网络或后端连接",
nameStepCheckError: "无法验证名称可用性,请稍后重试",
nameStepBootstrapMessage:
"新智能体的名称是 {name},现在开始为它生成 **SOUL**。",
save: "保存智能体",
saving: "正在保存智能体...",
"新专家的名称是 {name},现在开始为它生成 **SOUL**。",
save: "保存专家",
saving: "正在保存专家...",
saveRequested:
"已提交保存请求,CM 助手 正在根据当前对话生成并保存初版智能体。",
"已提交保存请求,CM 助手 正在根据当前对话生成并保存初版专家。",
saveHint:
"你可以在右上角的菜单里随时保存这个智能体,就算目前还只是初稿也可以。",
"你可以在右上角的菜单里随时保存这个专家,就算目前还只是初稿也可以。",
saveCommandMessage:
"请现在根据我们目前已经讨论的全部内容保存这个自定义智能体。这就是我明确的保存确认。如果仍有少量细节缺失,请根据上下文做出合理假设,生成一份简洁的英文初始 SOUL.md,并直接调用 setup_agent,不要再向我索要额外确认。",
"请现在根据我们目前已经讨论的全部内容保存这个自定义专家。这就是我明确的保存确认。如果仍有少量细节缺失,请根据上下文做出合理假设,生成一份简洁的英文初始 SOUL.md,并直接调用 setup_agent,不要再向我索要额外确认。",
agentCreatedPendingRefresh:
"智能体已创建,但 CM 助手 暂时还无法读取到它。请稍后刷新当前页面。",
"专家已创建,但 CM 助手 暂时还无法读取到它。请稍后刷新当前页面。",
more: "更多操作",
agentCreated: "智能体已创建!",
agentCreated: "专家已创建!",
startChatting: "开始对话",
backToGallery: "返回 Gallery",
},
@@ -437,7 +437,7 @@ export const zhCN: Translations = {
badgeSourceAgentTitle: "在对话中由 Agent 自动生成",
badgeSourceUpload: "用户上传",
badgeSourceUploadTitle: "由用户手动上传或编辑",
tabAll: "全部",
tabAll: "广场",
tabMine: "我的",
entryStale: "过时 {count}",
entryArchived: "归档 {count}",
@@ -583,12 +583,12 @@ export const zhCN: Translations = {
adminUsers: {
title: "用户管理",
description: "查看所有注册用户及其关联的技能和智能体数量。",
description: "查看所有注册用户及其关联的技能和专家数量。",
email: "邮箱",
role: "角色",
userId: "用户 ID",
skillCount: "技能数",
agentCount: "智能体数",
agentCount: "专家数",
roleAdmin: "管理员",
roleUser: "普通用户",
empty: "暂无用户",
@@ -601,5 +601,8 @@ export const zhCN: Translations = {
markAllRead: "全部已读",
empty: "暂无通知",
deleteAria: "删除通知",
settingsAria: "通知设置",
backAria: "返回",
settingsTitle: "通知设置",
},
};