feat(frontend): 技能/专家广场展示调整 + 通知合并入设置菜单
- 技能与专家「广场」标签只展示内置和已发布项,Tab 顺序调为「我的」在前并默认选中 - 「智能体」全量更名为「专家」 - 通知由侧边栏铃铛改为「设置和更多」菜单内的弹窗入口,合并通知列表与通知设置 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
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 { t } = useI18n();
|
||||||
const { agents, isLoading } = useAgents();
|
const { agents, isLoading } = useAgents();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [activeTab, setActiveTab] = useState<"all" | "mine">("all");
|
const [activeTab, setActiveTab] = useState<"all" | "mine">("mine");
|
||||||
|
|
||||||
const storedAuth = useMemo(() => getStoredAuth(), []);
|
const storedAuth = useMemo(() => getStoredAuth(), []);
|
||||||
const currentUserId = storedAuth?.user_id ?? null;
|
const currentUserId = storedAuth?.user_id ?? null;
|
||||||
@@ -84,8 +84,8 @@ export function AgentGallery() {
|
|||||||
className="flex flex-1 flex-col overflow-hidden"
|
className="flex flex-1 flex-col overflow-hidden"
|
||||||
>
|
>
|
||||||
<TabsList className="mx-6 mt-4 w-auto self-start">
|
<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>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="all" className="flex-1 overflow-y-auto p-6 mt-0">
|
<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";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BellIcon,
|
|
||||||
BrainIcon,
|
BrainIcon,
|
||||||
PaletteIcon,
|
PaletteIcon,
|
||||||
SparklesIcon,
|
SparklesIcon,
|
||||||
@@ -21,7 +20,6 @@ import { AboutSettingsPage } from "@/components/workspace/settings/about-setting
|
|||||||
import { AccountSettingsPage } from "@/components/workspace/settings/account-settings-page";
|
import { AccountSettingsPage } from "@/components/workspace/settings/account-settings-page";
|
||||||
import { AppearanceSettingsPage } from "@/components/workspace/settings/appearance-settings-page";
|
import { AppearanceSettingsPage } from "@/components/workspace/settings/appearance-settings-page";
|
||||||
import { MemorySettingsPage } from "@/components/workspace/settings/memory-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 { SkillSettingsPage } from "@/components/workspace/settings/skill-settings-page";
|
||||||
import { ToolSettingsPage } from "@/components/workspace/settings/tool-settings-page";
|
import { ToolSettingsPage } from "@/components/workspace/settings/tool-settings-page";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
@@ -33,7 +31,6 @@ type SettingsSection =
|
|||||||
| "memory"
|
| "memory"
|
||||||
| "tools"
|
| "tools"
|
||||||
| "skills"
|
| "skills"
|
||||||
| "notification"
|
|
||||||
| "about";
|
| "about";
|
||||||
|
|
||||||
type SettingsDialogProps = React.ComponentProps<typeof Dialog> & {
|
type SettingsDialogProps = React.ComponentProps<typeof Dialog> & {
|
||||||
@@ -61,11 +58,6 @@ export function SettingsDialog(props: SettingsDialogProps) {
|
|||||||
label: t.settings.sections.appearance,
|
label: t.settings.sections.appearance,
|
||||||
icon: PaletteIcon,
|
icon: PaletteIcon,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "notification",
|
|
||||||
label: t.settings.sections.notification,
|
|
||||||
icon: BellIcon,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "memory",
|
id: "memory",
|
||||||
label: t.settings.sections.memory,
|
label: t.settings.sections.memory,
|
||||||
@@ -85,7 +77,6 @@ export function SettingsDialog(props: SettingsDialogProps) {
|
|||||||
t.settings.sections.memory,
|
t.settings.sections.memory,
|
||||||
t.settings.sections.tools,
|
t.settings.sections.tools,
|
||||||
t.settings.sections.skills,
|
t.settings.sections.skills,
|
||||||
t.settings.sections.notification,
|
|
||||||
t.settings.sections.about,
|
t.settings.sections.about,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -135,7 +126,6 @@ export function SettingsDialog(props: SettingsDialogProps) {
|
|||||||
{activeSection === "appearance" && <AppearanceSettingsPage />}
|
{activeSection === "appearance" && <AppearanceSettingsPage />}
|
||||||
{activeSection === "memory" && <MemorySettingsPage />}
|
{activeSection === "memory" && <MemorySettingsPage />}
|
||||||
{activeSection === "tools" && <ToolSettingsPage />}
|
{activeSection === "tools" && <ToolSettingsPage />}
|
||||||
{activeSection === "notification" && <NotificationSettingsPage />}
|
|
||||||
{activeSection === "skills" && <SkillSettingsPage />}
|
{activeSection === "skills" && <SkillSettingsPage />}
|
||||||
{activeSection === "about" && <AboutSettingsPage />}
|
{activeSection === "about" && <AboutSettingsPage />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ function SkillSettingsList({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [filter, setFilter] = useState<string>("all");
|
const [filter, setFilter] = useState<string>("mine");
|
||||||
const [installOpen, setInstallOpen] = useState(false);
|
const [installOpen, setInstallOpen] = useState(false);
|
||||||
const [staleSheetOpen, setStaleSheetOpen] = useState(false);
|
const [staleSheetOpen, setStaleSheetOpen] = useState(false);
|
||||||
const [archiveSheetOpen, setArchiveSheetOpen] = useState(false);
|
const [archiveSheetOpen, setArchiveSheetOpen] = useState(false);
|
||||||
@@ -101,9 +101,9 @@ function SkillSettingsList({
|
|||||||
const currentUserId = storedAuth?.user_id ?? null;
|
const currentUserId = storedAuth?.user_id ?? null;
|
||||||
const isAdmin = storedAuth?.system_role === "admin";
|
const isAdmin = storedAuth?.system_role === "admin";
|
||||||
|
|
||||||
// Visible-to-me skills, classified into the three buckets the UI cares about.
|
// 把可见技能分类成 UI 关心的几个桶。
|
||||||
// "All" = built-in + anyone-published-custom + mine (active state only).
|
// "All" = 内置 + 已发布的技能(仅 active 状态);自己的技能也要自己发布了才出现。
|
||||||
// "Mine" = strictly my own custom skills (active state only).
|
// "Mine" = 严格属于我自己的自定义技能(仅 active 状态)。
|
||||||
const { activeAll, activeMine, staleVisible, archivedVisible } = useMemo(() => {
|
const { activeAll, activeMine, staleVisible, archivedVisible } = useMemo(() => {
|
||||||
const all: Skill[] = [];
|
const all: Skill[] = [];
|
||||||
const mine: Skill[] = [];
|
const mine: Skill[] = [];
|
||||||
@@ -115,8 +115,6 @@ function SkillSettingsList({
|
|||||||
const isOwnedByMe =
|
const isOwnedByMe =
|
||||||
isCustom && !!skill.owner_user_id && skill.owner_user_id === currentUserId;
|
isCustom && !!skill.owner_user_id && skill.owner_user_id === currentUserId;
|
||||||
const isBuiltin = skill.builtin ?? !skill.owner_user_id;
|
const isBuiltin = skill.builtin ?? !skill.owner_user_id;
|
||||||
const isPublishedByOthers =
|
|
||||||
isCustom && !isOwnedByMe && !isBuiltin && !!skill.published;
|
|
||||||
|
|
||||||
if (state === "stale") {
|
if (state === "stale") {
|
||||||
stale.push(skill);
|
stale.push(skill);
|
||||||
@@ -127,7 +125,7 @@ function SkillSettingsList({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// state === active
|
// state === active
|
||||||
if (!isCustom || isBuiltin || isOwnedByMe || isPublishedByOthers) {
|
if (isBuiltin || !!skill.published) {
|
||||||
all.push(skill);
|
all.push(skill);
|
||||||
}
|
}
|
||||||
if (isOwnedByMe) {
|
if (isOwnedByMe) {
|
||||||
@@ -156,8 +154,8 @@ function SkillSettingsList({
|
|||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Tabs value={filter} onValueChange={setFilter}>
|
<Tabs value={filter} onValueChange={setFilter}>
|
||||||
<TabsList variant="line">
|
<TabsList variant="line">
|
||||||
<TabsTrigger value="all">{t.settings.skills.tabAll}</TabsTrigger>
|
|
||||||
<TabsTrigger value="mine">{t.settings.skills.tabMine}</TabsTrigger>
|
<TabsTrigger value="mine">{t.settings.skills.tabMine}</TabsTrigger>
|
||||||
|
<TabsTrigger value="all">{t.settings.skills.tabAll}</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
BellIcon,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
GaugeIcon,
|
GaugeIcon,
|
||||||
MessageSquareCodeIcon,
|
MessageSquareCodeIcon,
|
||||||
@@ -26,8 +27,9 @@ import {
|
|||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { getStoredAuth } from "@/core/auth";
|
import { getStoredAuth } from "@/core/auth";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
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 { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { SettingsDialog } from "./settings";
|
import { SettingsDialog } from "./settings";
|
||||||
@@ -57,11 +59,13 @@ export function WorkspaceNavMenu() {
|
|||||||
const isAdmin = getStoredAuth()?.system_role === "admin";
|
const isAdmin = getStoredAuth()?.system_role === "admin";
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
const [settingsDefaultSection, setSettingsDefaultSection] = useState<
|
const [settingsDefaultSection, setSettingsDefaultSection] = useState<
|
||||||
"appearance" | "memory" | "tools" | "notification" | "about"
|
"appearance" | "memory" | "tools" | "about"
|
||||||
>("appearance");
|
>("appearance");
|
||||||
|
const [notificationOpen, setNotificationOpen] = useState(false);
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const { open: isSidebarOpen } = useSidebar();
|
const { open: isSidebarOpen } = useSidebar();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { data: unreadCount = 0 } = useUnreadCount();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
@@ -74,10 +78,11 @@ export function WorkspaceNavMenu() {
|
|||||||
onOpenChange={setSettingsOpen}
|
onOpenChange={setSettingsOpen}
|
||||||
defaultSection={settingsDefaultSection}
|
defaultSection={settingsDefaultSection}
|
||||||
/>
|
/>
|
||||||
|
<NotificationDialog
|
||||||
|
open={notificationOpen}
|
||||||
|
onOpenChange={setNotificationOpen}
|
||||||
|
/>
|
||||||
<SidebarMenu className="w-full">
|
<SidebarMenu className="w-full">
|
||||||
<SidebarMenuItem>
|
|
||||||
<NotificationBell />
|
|
||||||
</SidebarMenuItem>
|
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
{mounted ? (
|
{mounted ? (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -95,6 +100,15 @@ export function WorkspaceNavMenu() {
|
|||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
>
|
>
|
||||||
<DropdownMenuGroup>
|
<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
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSettingsDefaultSection("appearance");
|
setSettingsDefaultSection("appearance");
|
||||||
|
|||||||
@@ -177,10 +177,10 @@ export const enUS: Translations = {
|
|||||||
|
|
||||||
// Agents
|
// Agents
|
||||||
agents: {
|
agents: {
|
||||||
title: "Agents",
|
title: "Experts",
|
||||||
description:
|
description:
|
||||||
"Create and manage custom agents with specialized prompts and capabilities.",
|
"Create and manage custom experts with specialized prompts and capabilities.",
|
||||||
newAgent: "New Agent",
|
newAgent: "New Expert",
|
||||||
emptyTitle: "No custom agents yet",
|
emptyTitle: "No custom agents yet",
|
||||||
emptyDescription:
|
emptyDescription:
|
||||||
"Create your first custom agent with a specialized system prompt.",
|
"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",
|
badgeSourceAgentTitle: "Auto-generated by an agent during a conversation",
|
||||||
badgeSourceUpload: "Uploaded",
|
badgeSourceUpload: "Uploaded",
|
||||||
badgeSourceUploadTitle: "Manually uploaded or edited by a user",
|
badgeSourceUploadTitle: "Manually uploaded or edited by a user",
|
||||||
tabAll: "All",
|
tabAll: "Square",
|
||||||
tabMine: "Mine",
|
tabMine: "Mine",
|
||||||
entryStale: "Stale {count}",
|
entryStale: "Stale {count}",
|
||||||
entryArchived: "Archived {count}",
|
entryArchived: "Archived {count}",
|
||||||
@@ -632,5 +632,8 @@ export const enUS: Translations = {
|
|||||||
markAllRead: "Mark all read",
|
markAllRead: "Mark all read",
|
||||||
empty: "No notifications",
|
empty: "No notifications",
|
||||||
deleteAria: "Delete notification",
|
deleteAria: "Delete notification",
|
||||||
|
settingsAria: "Notification settings",
|
||||||
|
backAria: "Back",
|
||||||
|
settingsTitle: "Notification settings",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -7,6 +7,9 @@ export interface NotificationsTranslations {
|
|||||||
markAllRead: string;
|
markAllRead: string;
|
||||||
empty: string;
|
empty: string;
|
||||||
deleteAria: string;
|
deleteAria: string;
|
||||||
|
settingsAria: string;
|
||||||
|
backAria: string;
|
||||||
|
settingsTitle: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Translations {
|
export interface Translations {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
CompassIcon,
|
CompassIcon,
|
||||||
GraduationCapIcon,
|
GraduationCapIcon,
|
||||||
ImageIcon,
|
ImageIcon,
|
||||||
@@ -89,7 +89,7 @@ export const zhCN: Translations = {
|
|||||||
reasoningModeDescription: "思考后再行动,在时间与准确性之间取得平衡",
|
reasoningModeDescription: "思考后再行动,在时间与准确性之间取得平衡",
|
||||||
proMode: "专业问答",
|
proMode: "专业问答",
|
||||||
proModeDescription: "思考、计划再执行,获得更精准的结果,可能需要更多时间",
|
proModeDescription: "思考、计划再执行,获得更精准的结果,可能需要更多时间",
|
||||||
ultraMode: "多智能体问答",
|
ultraMode: "多专家问答",
|
||||||
ultraModeDescription:
|
ultraModeDescription:
|
||||||
"继承自专业问答模式,可调用子代理分工协作,适合复杂多步骤任务,能力最强",
|
"继承自专业问答模式,可调用子代理分工协作,适合复杂多步骤任务,能力最强",
|
||||||
reasoningEffort: "推理深度",
|
reasoningEffort: "推理深度",
|
||||||
@@ -165,47 +165,47 @@ export const zhCN: Translations = {
|
|||||||
chats: "对话",
|
chats: "对话",
|
||||||
recentChats: "最近的对话",
|
recentChats: "最近的对话",
|
||||||
demoChats: "演示对话",
|
demoChats: "演示对话",
|
||||||
agents: "智能体",
|
agents: "专家",
|
||||||
users: "用户管理",
|
users: "用户管理",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Agents
|
// Agents
|
||||||
agents: {
|
agents: {
|
||||||
title: "智能体",
|
title: "专家",
|
||||||
description: "创建和管理具有专属 Prompt 与能力的自定义智能体。",
|
description: "创建和管理具有专属 Prompt 与能力的自定义专家。",
|
||||||
newAgent: "新建智能体",
|
newAgent: "新建专家",
|
||||||
emptyTitle: "还没有自定义智能体",
|
emptyTitle: "还没有自定义专家",
|
||||||
emptyDescription: "创建你的第一个自定义智能体,设置专属系统提示词。",
|
emptyDescription: "创建你的第一个自定义专家,设置专属系统提示词。",
|
||||||
chat: "对话",
|
chat: "对话",
|
||||||
delete: "删除",
|
delete: "删除",
|
||||||
deleteConfirm: "确定要删除该智能体吗?此操作不可撤销。",
|
deleteConfirm: "确定要删除该专家吗?此操作不可撤销。",
|
||||||
deleteSuccess: "智能体已删除",
|
deleteSuccess: "专家已删除",
|
||||||
newChat: "新对话",
|
newChat: "新对话",
|
||||||
createPageTitle: "设计你的智能体",
|
createPageTitle: "设计你的专家",
|
||||||
createPageSubtitle: "描述你想要的智能体,我来帮你通过对话创建。",
|
createPageSubtitle: "描述你想要的专家,我来帮你通过对话创建。",
|
||||||
nameStepTitle: "给新智能体起个名字",
|
nameStepTitle: "给新专家起个名字",
|
||||||
nameStepHint:
|
nameStepHint:
|
||||||
"只允许字母、数字和连字符,存储时自动转为小写(例如 code-reviewer)",
|
"只允许字母、数字和连字符,存储时自动转为小写(例如 code-reviewer)",
|
||||||
nameStepPlaceholder: "例如 code-reviewer",
|
nameStepPlaceholder: "例如 code-reviewer",
|
||||||
nameStepContinue: "继续",
|
nameStepContinue: "继续",
|
||||||
nameStepInvalidError: "名称无效,只允许字母、数字和连字符",
|
nameStepInvalidError: "名称无效,只允许字母、数字和连字符",
|
||||||
nameStepAlreadyExistsError: "已存在同名智能体",
|
nameStepAlreadyExistsError: "已存在同名专家",
|
||||||
nameStepNetworkError: "网络请求失败,请检查网络或后端连接",
|
nameStepNetworkError: "网络请求失败,请检查网络或后端连接",
|
||||||
nameStepCheckError: "无法验证名称可用性,请稍后重试",
|
nameStepCheckError: "无法验证名称可用性,请稍后重试",
|
||||||
nameStepBootstrapMessage:
|
nameStepBootstrapMessage:
|
||||||
"新智能体的名称是 {name},现在开始为它生成 **SOUL**。",
|
"新专家的名称是 {name},现在开始为它生成 **SOUL**。",
|
||||||
save: "保存智能体",
|
save: "保存专家",
|
||||||
saving: "正在保存智能体...",
|
saving: "正在保存专家...",
|
||||||
saveRequested:
|
saveRequested:
|
||||||
"已提交保存请求,CM 助手 正在根据当前对话生成并保存初版智能体。",
|
"已提交保存请求,CM 助手 正在根据当前对话生成并保存初版专家。",
|
||||||
saveHint:
|
saveHint:
|
||||||
"你可以在右上角的菜单里随时保存这个智能体,就算目前还只是初稿也可以。",
|
"你可以在右上角的菜单里随时保存这个专家,就算目前还只是初稿也可以。",
|
||||||
saveCommandMessage:
|
saveCommandMessage:
|
||||||
"请现在根据我们目前已经讨论的全部内容保存这个自定义智能体。这就是我明确的保存确认。如果仍有少量细节缺失,请根据上下文做出合理假设,生成一份简洁的英文初始 SOUL.md,并直接调用 setup_agent,不要再向我索要额外确认。",
|
"请现在根据我们目前已经讨论的全部内容保存这个自定义专家。这就是我明确的保存确认。如果仍有少量细节缺失,请根据上下文做出合理假设,生成一份简洁的英文初始 SOUL.md,并直接调用 setup_agent,不要再向我索要额外确认。",
|
||||||
agentCreatedPendingRefresh:
|
agentCreatedPendingRefresh:
|
||||||
"智能体已创建,但 CM 助手 暂时还无法读取到它。请稍后刷新当前页面。",
|
"专家已创建,但 CM 助手 暂时还无法读取到它。请稍后刷新当前页面。",
|
||||||
more: "更多操作",
|
more: "更多操作",
|
||||||
agentCreated: "智能体已创建!",
|
agentCreated: "专家已创建!",
|
||||||
startChatting: "开始对话",
|
startChatting: "开始对话",
|
||||||
backToGallery: "返回 Gallery",
|
backToGallery: "返回 Gallery",
|
||||||
},
|
},
|
||||||
@@ -437,7 +437,7 @@ export const zhCN: Translations = {
|
|||||||
badgeSourceAgentTitle: "在对话中由 Agent 自动生成",
|
badgeSourceAgentTitle: "在对话中由 Agent 自动生成",
|
||||||
badgeSourceUpload: "用户上传",
|
badgeSourceUpload: "用户上传",
|
||||||
badgeSourceUploadTitle: "由用户手动上传或编辑",
|
badgeSourceUploadTitle: "由用户手动上传或编辑",
|
||||||
tabAll: "全部",
|
tabAll: "广场",
|
||||||
tabMine: "我的",
|
tabMine: "我的",
|
||||||
entryStale: "过时 {count}",
|
entryStale: "过时 {count}",
|
||||||
entryArchived: "归档 {count}",
|
entryArchived: "归档 {count}",
|
||||||
@@ -583,12 +583,12 @@ export const zhCN: Translations = {
|
|||||||
|
|
||||||
adminUsers: {
|
adminUsers: {
|
||||||
title: "用户管理",
|
title: "用户管理",
|
||||||
description: "查看所有注册用户及其关联的技能和智能体数量。",
|
description: "查看所有注册用户及其关联的技能和专家数量。",
|
||||||
email: "邮箱",
|
email: "邮箱",
|
||||||
role: "角色",
|
role: "角色",
|
||||||
userId: "用户 ID",
|
userId: "用户 ID",
|
||||||
skillCount: "技能数",
|
skillCount: "技能数",
|
||||||
agentCount: "智能体数",
|
agentCount: "专家数",
|
||||||
roleAdmin: "管理员",
|
roleAdmin: "管理员",
|
||||||
roleUser: "普通用户",
|
roleUser: "普通用户",
|
||||||
empty: "暂无用户",
|
empty: "暂无用户",
|
||||||
@@ -601,5 +601,8 @@ export const zhCN: Translations = {
|
|||||||
markAllRead: "全部已读",
|
markAllRead: "全部已读",
|
||||||
empty: "暂无通知",
|
empty: "暂无通知",
|
||||||
deleteAria: "删除通知",
|
deleteAria: "删除通知",
|
||||||
|
settingsAria: "通知设置",
|
||||||
|
backAria: "返回",
|
||||||
|
settingsTitle: "通知设置",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
Reference in new issue
Block a user