Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into gzy_iframedf
This commit is contained in:
commit
a5b65f4087
91 files changed
+3731
-2141
No files matched your search
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { BotIcon, RefreshCwIcon, SearchIcon } from "lucide-react";
|
||||
import { AlertCircle, BotIcon, CheckCircle2, RefreshCwIcon, SearchIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -22,9 +22,37 @@ import {
|
||||
import { AgentCard } from "@/components/workspace/agents/agent-card";
|
||||
import { useAdminSyncAgentSkills, useAllAgentsAdmin } from "@/core/agents";
|
||||
import type { AgentSkillsSyncResult } from "@/core/agents/api";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
|
||||
/** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||
const successToastDarkOptions = {
|
||||
classNames: {
|
||||
toast:
|
||||
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
|
||||
title: "!text-[#FFFFFF]",
|
||||
description: "!text-[#FFFFFF]",
|
||||
},
|
||||
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
|
||||
} as const;
|
||||
|
||||
/** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||
const errorToastDarkOptions = {
|
||||
classNames: {
|
||||
toast:
|
||||
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
|
||||
title: "!text-[#FFFFFF]",
|
||||
description: "!text-[#FFFFFF]",
|
||||
},
|
||||
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
|
||||
} as const;
|
||||
|
||||
export default function AdminAgentsPage() {
|
||||
const { theme } = useTheme();
|
||||
const darkToastOpts =
|
||||
theme === "dark"
|
||||
? { success: successToastDarkOptions, error: errorToastDarkOptions }
|
||||
: { success: undefined, error: undefined };
|
||||
const [search, setSearch] = useState("");
|
||||
const { agents, isLoading } = useAllAgentsAdmin(true);
|
||||
const syncSkills = useAdminSyncAgentSkills();
|
||||
@@ -39,14 +67,21 @@ export default function AdminAgentsPage() {
|
||||
setSyncResult(result);
|
||||
setSyncDialogOpen(true);
|
||||
if (result.modified === 0) {
|
||||
toast.success("所有智能体的技能均有效,无需清理");
|
||||
toast.success(
|
||||
"所有智能体的技能均有效,无需清理",
|
||||
darkToastOpts.success,
|
||||
);
|
||||
} else {
|
||||
toast.success(
|
||||
`已清理 ${result.modified} 个智能体中的失效技能引用`,
|
||||
darkToastOpts.success,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : String(err));
|
||||
toast.error(
|
||||
err instanceof Error ? err.message : String(err),
|
||||
darkToastOpts.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import { toast } from "sonner";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -70,6 +71,7 @@ const emptyForm = {
|
||||
};
|
||||
|
||||
export default function AdminScheduledTasksPage() {
|
||||
const { theme } = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
// ?task= carries the task to re-select when returning from a run detail.
|
||||
@@ -227,7 +229,10 @@ export default function AdminScheduledTasksPage() {
|
||||
onChange={(event) => setSearchInput(event.target.value)}
|
||||
placeholder="按任务名称搜索"
|
||||
/>
|
||||
<Button type="submit" variant="outline">
|
||||
<Button
|
||||
type="submit"
|
||||
variant={theme === "dark" ? "default" : "outline"}
|
||||
>
|
||||
<Search className="size-4" />
|
||||
搜索
|
||||
</Button>
|
||||
|
||||
@@ -84,10 +84,10 @@ export default function AdminSkillsPage() {
|
||||
)}
|
||||
{!isLoading && !error && filteredSkills.length === 0 && (
|
||||
<div className="flex h-64 flex-col items-center justify-center gap-3 text-center">
|
||||
<div className="bg-muted flex h-14 w-14 items-center justify-center rounded-full">
|
||||
<SparklesIcon className="text-muted-foreground h-7 w-7" />
|
||||
<div className="bg-muted flex h-14 w-14 items-center justify-center rounded-full dark:bg-[#022248]">
|
||||
<SparklesIcon className="text-muted-foreground h-7 w-7 dark:text-[#AACEFF]" />
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<p className="text-muted-foreground text-sm dark:text-[#AACEFF]">
|
||||
当前没有可管理的技能。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Download, Loader2 } from "lucide-react";
|
||||
import { AlertCircle,CheckCircle2, Download, Loader2 } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from "@/core/llm-metrics/api";
|
||||
import { useLlmMetrics } from "@/core/llm-metrics/hooks";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
@@ -74,6 +75,7 @@ function speedColor(tps?: number | null): string {
|
||||
|
||||
/** The original LLM-call metrics view, now one tab of the metrics page. */
|
||||
function LlmMetricsPanel() {
|
||||
const { theme } = useTheme();
|
||||
const [page, setPage] = useState(0);
|
||||
const [model, setModel] = useState("");
|
||||
const [user, setUser] = useState("");
|
||||
@@ -81,6 +83,31 @@ function LlmMetricsPanel() {
|
||||
const [since, setSince] = useState("");
|
||||
const [until, setUntil] = useState("");
|
||||
const [exporting, setExporting] = useState(false);
|
||||
/** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||
const successToastDarkOptions = {
|
||||
classNames: {
|
||||
toast:
|
||||
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
|
||||
title: "!text-[#FFFFFF]",
|
||||
description: "!text-[#FFFFFF]",
|
||||
},
|
||||
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
|
||||
} as const;
|
||||
|
||||
/** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||
const errorToastDarkOptions = {
|
||||
classNames: {
|
||||
toast:
|
||||
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
|
||||
title: "!text-[#FFFFFF]",
|
||||
description: "!text-[#FFFFFF]",
|
||||
},
|
||||
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
|
||||
} as const;
|
||||
const darkToastOpts =
|
||||
theme === "dark"
|
||||
? { success: successToastDarkOptions, error: errorToastDarkOptions }
|
||||
: { success: undefined, error: undefined };
|
||||
|
||||
const filters = useMemo<LlmMetricsFilters>(() => {
|
||||
const out: LlmMetricsFilters = {};
|
||||
@@ -103,9 +130,9 @@ function LlmMetricsPanel() {
|
||||
setExporting(true);
|
||||
try {
|
||||
await downloadLlmMetricsExcel(filters);
|
||||
toast.success("已导出为 Excel");
|
||||
toast.success("已导出为 Excel",darkToastOpts.success);
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "导出失败");
|
||||
toast.error(e instanceof Error ? e.message : "导出失败",darkToastOpts.error);
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
@@ -267,9 +294,9 @@ function LlmMetricsPanel() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 rounded-xl border bg-white p-4 md:grid-cols-5">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-muted-foreground text-xs">模型</label>
|
||||
<div className="grid gap-3 rounded-xl border bg-white p-4 md:grid-cols-5 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
|
||||
<div className="flex flex-col gap-1 ">
|
||||
<label className="text-muted-foreground text-xs dark:text-white">模型</label>
|
||||
<Input
|
||||
value={model}
|
||||
placeholder="按模型名筛选"
|
||||
@@ -280,7 +307,7 @@ function LlmMetricsPanel() {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-muted-foreground text-xs">用户名</label>
|
||||
<label className="text-muted-foreground text-xs dark:text-white">用户名</label>
|
||||
<Input
|
||||
value={user}
|
||||
placeholder="按用户名匹配(@ 前部分)"
|
||||
@@ -291,7 +318,7 @@ function LlmMetricsPanel() {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-muted-foreground text-xs">状态</label>
|
||||
<label className="text-muted-foreground text-xs dark:text-white">状态</label>
|
||||
<Select
|
||||
value={status}
|
||||
onValueChange={(value) => {
|
||||
@@ -299,10 +326,10 @@ function LlmMetricsPanel() {
|
||||
setPage(0);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="w-full dark:bg-[#0D2D54] dark:hover:bg-[#0D2D54]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent position="popper" side="bottom" sideOffset={4}>
|
||||
<SelectItem value="all">全部</SelectItem>
|
||||
<SelectItem value="success">成功</SelectItem>
|
||||
<SelectItem value="error">失败</SelectItem>
|
||||
@@ -310,7 +337,7 @@ function LlmMetricsPanel() {
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-muted-foreground text-xs">开始时间</label>
|
||||
<label className="text-muted-foreground text-xs dark:text-white">开始时间</label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={since}
|
||||
@@ -321,7 +348,7 @@ function LlmMetricsPanel() {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-muted-foreground text-xs">结束时间</label>
|
||||
<label className="text-muted-foreground text-xs dark:text-white">结束时间</label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={until}
|
||||
@@ -338,10 +365,10 @@ function LlmMetricsPanel() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-white">
|
||||
<div className="rounded-xl border bg-white dark:border-[#213D5E] dark:bg-[#02244C] ">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[1080px] text-sm">
|
||||
<thead className="bg-muted/60 text-muted-foreground">
|
||||
<thead className="bg-muted/60 text-muted-foreground dark:text-white">
|
||||
<tr>
|
||||
{visibleColumns.map((col) => (
|
||||
<Th
|
||||
|
||||
@@ -4,12 +4,13 @@ import { useParams, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import { loginByUsername } from '@/core/auth/api';
|
||||
import { loginByToken, loginByUsername } from '@/core/auth/api';
|
||||
import { setStoredAuth } from '@/core/auth';
|
||||
import {
|
||||
persistExternalLoginParams,
|
||||
resolveThemeFromQuery,
|
||||
} from '@/core/auth/external-login';
|
||||
import { isWujieEnv, useWujieProps } from '@/core/embed/wujie';
|
||||
import {
|
||||
GENERAL_QA_ROUTE,
|
||||
loginByYUserId,
|
||||
@@ -50,6 +51,10 @@ export default function LoginPage() {
|
||||
// 外部跳转参数:theme 直接调 setTheme;ticket/userId/env 持久化到 localStorage
|
||||
const yUserId = searchParams.get('yUserId')?.trim() ?? '';
|
||||
const toolToken = searchParams.get('token')?.trim() ?? '';
|
||||
// 用户名直登口令门:开启后端开关时,口令从 ?password= 透传
|
||||
const gatePassword = searchParams.get('password') ?? '';
|
||||
// token 换登录:?authToken= 传上游 token(与 tools_token 的 ?token= 区分开)
|
||||
const authToken = searchParams.get('authToken')?.trim() ?? '';
|
||||
const externalParams = useMemo(
|
||||
() => ({
|
||||
theme: searchParams.get('theme') ?? '',
|
||||
@@ -62,11 +67,23 @@ export default function LoginPage() {
|
||||
const [status, setStatus] = useState<LoginStatus>('loading');
|
||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||
|
||||
// 主题切换:theme=dark-blue → 深色,其他值或缺省 → 浅色
|
||||
// 无界(wujie)父级通过 $wujie.props 注入的参数(ticket / userId / theme / env)。
|
||||
// hook 内部已把 ticket/userId/env 持久化到 localStorage(复用 external-login 存储)。
|
||||
const wujieProps = useWujieProps();
|
||||
|
||||
// 调试:把无界注入的 props 打印出来(仅在无界子应用环境下,避免独立访问时刷日志)。
|
||||
useEffect(() => {
|
||||
if (!externalParams.theme) return;
|
||||
setTheme(resolveThemeFromQuery(externalParams.theme));
|
||||
}, [externalParams.theme, setTheme]);
|
||||
if (!isWujieEnv()) return;
|
||||
console.log('[wujie] sub-app props:', wujieProps);
|
||||
}, [wujieProps]);
|
||||
|
||||
// 主题切换:无界 props.theme 优先,其次 URL query theme。两者取值约定一致
|
||||
// (dark-blue → 深色;red → 浅色;blue 暂无该主题,先用浅色代替),统一走 resolveThemeFromQuery。
|
||||
useEffect(() => {
|
||||
const themeValue = wujieProps.theme || externalParams.theme;
|
||||
if (!themeValue) return;
|
||||
setTheme(resolveThemeFromQuery(themeValue));
|
||||
}, [wujieProps.theme, externalParams.theme, setTheme]);
|
||||
|
||||
// ticket / userId / env 持久化(与 login 请求并行做,互不阻塞)
|
||||
useEffect(() => {
|
||||
@@ -84,13 +101,19 @@ export default function LoginPage() {
|
||||
setStatus('loading');
|
||||
setErrorMessage('');
|
||||
try {
|
||||
if (yUserId) {
|
||||
await loginByYUserId(yUserId, toolToken || null);
|
||||
} else if (toolToken) {
|
||||
persistToolsToken(toolToken);
|
||||
}
|
||||
let auth;
|
||||
if (authToken) {
|
||||
// token 换登录:后端用 authToken 调上游接口换出用户名再登录
|
||||
auth = await loginByToken(authToken);
|
||||
} else {
|
||||
if (yUserId) {
|
||||
await loginByYUserId(yUserId, toolToken || null);
|
||||
} else if (toolToken) {
|
||||
persistToolsToken(toolToken);
|
||||
}
|
||||
|
||||
const auth = await loginByUsername(resolvedUsername);
|
||||
auth = await loginByUsername(resolvedUsername, gatePassword);
|
||||
}
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
@@ -114,12 +137,13 @@ export default function LoginPage() {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
redirectPath,
|
||||
resolvedUsername,
|
||||
router,
|
||||
sidebarEnabled,
|
||||
yUserId,
|
||||
toolToken,
|
||||
authToken,
|
||||
gatePassword,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -18,8 +18,11 @@ function PageLayout() {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
|
||||
// h-full(100%)而非 h-screen(100vh):配合 globals.css 的 html/body/#root height:100%
|
||||
// 高度链,根布局跟随容器高度。全屏下 100% 解析到视口,与 h-screen 等价;无界等嵌套
|
||||
// 下容器不占满整屏时,高度才能正确收缩而非锁死整屏。
|
||||
return (
|
||||
<div className="h-screen flex flex-col bg-muted/20">
|
||||
<div className="h-full flex flex-col bg-muted/20">
|
||||
{sidebarEnabled && <Header collapsed={collapsed} />}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{sidebarEnabled && (
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2Icon, ShieldIcon } from "lucide-react";
|
||||
import { AlertCircle,CheckCircle2, Loader2Icon, ShieldIcon } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -12,13 +13,36 @@ import {
|
||||
useUpdatePrivateSquareSettings,
|
||||
} from "@/core/system-settings/hooks";
|
||||
|
||||
/** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||
const successToastDarkOptions = {
|
||||
classNames: {
|
||||
toast:
|
||||
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
|
||||
title: "!text-[#FFFFFF]",
|
||||
description: "!text-[#FFFFFF]",
|
||||
},
|
||||
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
|
||||
} as const;
|
||||
|
||||
/** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||
const errorToastDarkOptions = {
|
||||
classNames: {
|
||||
toast:
|
||||
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
|
||||
title: "!text-[#FFFFFF]",
|
||||
description: "!text-[#FFFFFF]",
|
||||
},
|
||||
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
|
||||
} as const;
|
||||
|
||||
export default function PrivateSquareSettingsPage() {
|
||||
const { data, isLoading, error } = usePrivateSquareStatus();
|
||||
const update = useUpdatePrivateSquareSettings();
|
||||
|
||||
const { theme } = useTheme();
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirm, setConfirm] = useState("");
|
||||
|
||||
// The status response never contains the password itself, so re-syncing
|
||||
// ``enabled`` from a refetch wouldn't clobber any in-progress edit — but we
|
||||
// still gate the seed on a one-shot ref to match the prompt-prefix page's
|
||||
@@ -31,10 +55,14 @@ export default function PrivateSquareSettingsPage() {
|
||||
setEnabled(data.enabled);
|
||||
initializedRef.current = true;
|
||||
}, [data]);
|
||||
const darkToastOpts =
|
||||
theme === "dark"
|
||||
? { success: successToastDarkOptions, error: errorToastDarkOptions }
|
||||
: { success: undefined, error: undefined };
|
||||
|
||||
const handleSave = async () => {
|
||||
if (password && password !== confirm) {
|
||||
toast.error("两次输入的密码不一致");
|
||||
toast.error("两次输入的密码不一致", darkToastOpts.error);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -48,9 +76,9 @@ export default function PrivateSquareSettingsPage() {
|
||||
});
|
||||
setPassword("");
|
||||
setConfirm("");
|
||||
toast.success("已保存");
|
||||
toast.success("已保存",darkToastOpts.success);
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "保存失败");
|
||||
toast.error(e instanceof Error ? e.message : "保存失败",darkToastOpts.error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -60,9 +88,9 @@ export default function PrivateSquareSettingsPage() {
|
||||
}
|
||||
try {
|
||||
await update.mutateAsync({ password: "" });
|
||||
toast.success("已清除密码");
|
||||
toast.success("已清除密码",darkToastOpts.success);
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "清除失败");
|
||||
toast.error(e instanceof Error ? e.message : "清除失败", darkToastOpts.error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -85,7 +113,7 @@ export default function PrivateSquareSettingsPage() {
|
||||
加载失败:{error.message}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-5 rounded-xl border bg-white px-5 py-5">
|
||||
<div className="flex flex-col gap-5 rounded-xl border bg-white px-5 py-5 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-sm font-medium">启用私密广场</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2Icon } from "lucide-react";
|
||||
import { Loader2Icon,AlertCircle,CheckCircle2, } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -12,11 +12,33 @@ import {
|
||||
usePromptPrefixSettings,
|
||||
useUpdatePromptPrefixSettings,
|
||||
} from "@/core/system-settings/hooks";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
/** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||
const successToastDarkOptions = {
|
||||
classNames: {
|
||||
toast:
|
||||
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
|
||||
title: "!text-[#FFFFFF]",
|
||||
description: "!text-[#FFFFFF]",
|
||||
},
|
||||
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
|
||||
} as const;
|
||||
|
||||
/** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||
const errorToastDarkOptions = {
|
||||
classNames: {
|
||||
toast:
|
||||
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
|
||||
title: "!text-[#FFFFFF]",
|
||||
description: "!text-[#FFFFFF]",
|
||||
},
|
||||
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
|
||||
} as const;
|
||||
export default function PromptPrefixSettingsPage() {
|
||||
const { data, isLoading, error } = usePromptPrefixSettings();
|
||||
const update = useUpdatePromptPrefixSettings();
|
||||
|
||||
const { theme } = useTheme();
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [switchLabel, setSwitchLabel] = useState("");
|
||||
const [promptPrefix, setPromptPrefix] = useState("");
|
||||
@@ -34,13 +56,18 @@ export default function PromptPrefixSettingsPage() {
|
||||
initializedRef.current = true;
|
||||
}, [data]);
|
||||
|
||||
const handleSave = async () => {
|
||||
const handleSave = async () => {
|
||||
const darkToastOpts =
|
||||
theme === "dark"
|
||||
? { success: successToastDarkOptions, error: errorToastDarkOptions }
|
||||
: { success: undefined, error: undefined };
|
||||
|
||||
if (enabled && !switchLabel.trim()) {
|
||||
toast.error("开启时必须填写开关名称");
|
||||
toast.error("开启时必须填写开关名称",darkToastOpts.error);
|
||||
return;
|
||||
}
|
||||
if (enabled && !promptPrefix.trim()) {
|
||||
toast.error("开启时必须填写拼接的前缀内容");
|
||||
toast.error("开启时必须填写拼接的前缀内容",darkToastOpts.error);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -49,9 +76,9 @@ export default function PromptPrefixSettingsPage() {
|
||||
switch_label: switchLabel,
|
||||
prompt_prefix: promptPrefix,
|
||||
});
|
||||
toast.success("已保存");
|
||||
toast.success("已保存",darkToastOpts.success);
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "保存失败");
|
||||
toast.error(e instanceof Error ? e.message : "保存失败",darkToastOpts.error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -74,7 +101,7 @@ export default function PromptPrefixSettingsPage() {
|
||||
加载失败:{error.message}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-5 rounded-xl border bg-white px-5 py-5">
|
||||
<div className="flex flex-col gap-5 rounded-xl border bg-white px-5 py-5 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-sm font-medium">向用户暴露开关</div>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
GripVerticalIcon,
|
||||
Loader2Icon,
|
||||
PencilIcon,
|
||||
@@ -11,6 +13,7 @@ import {
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -34,7 +37,30 @@ type DialogState =
|
||||
| { mode: "edit"; question: RecommendedQuestion }
|
||||
| null;
|
||||
|
||||
/** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||
const successToastDarkOptions = {
|
||||
classNames: {
|
||||
toast:
|
||||
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
|
||||
title: "!text-[#FFFFFF]",
|
||||
description: "!text-[#FFFFFF]",
|
||||
},
|
||||
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
|
||||
} as const;
|
||||
|
||||
/** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||
const errorToastDarkOptions = {
|
||||
classNames: {
|
||||
toast:
|
||||
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
|
||||
title: "!text-[#FFFFFF]",
|
||||
description: "!text-[#FFFFFF]",
|
||||
},
|
||||
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
|
||||
} as const;
|
||||
|
||||
export default function RecommendedQuestionsPage() {
|
||||
const { theme } = useTheme();
|
||||
const { questions, isLoading, error } = useRecommendedQuestions();
|
||||
const createMutation = useCreateRecommendedQuestion();
|
||||
const updateMutation = useUpdateRecommendedQuestion();
|
||||
@@ -70,7 +96,7 @@ export default function RecommendedQuestionsPage() {
|
||||
const title = formTitle.trim();
|
||||
const content = formContent.trim();
|
||||
if (!title || !content) {
|
||||
toast.error("标题和内容不能为空");
|
||||
toast.error("标题和内容不能为空", darkToastOpts.error);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -81,17 +107,20 @@ export default function RecommendedQuestionsPage() {
|
||||
content,
|
||||
sort_order: questions.length,
|
||||
});
|
||||
toast.success("问题已添加");
|
||||
toast.success("问题已添加", darkToastOpts.success);
|
||||
} else if (dialog?.mode === "edit") {
|
||||
await updateMutation.mutateAsync({
|
||||
id: dialog.question.id,
|
||||
body: { title, content },
|
||||
});
|
||||
toast.success("问题已更新");
|
||||
toast.success("问题已更新", darkToastOpts.success);
|
||||
}
|
||||
closeDialog();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "操作失败");
|
||||
toast.error(
|
||||
e instanceof Error ? e.message : "操作失败",
|
||||
darkToastOpts.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,9 +128,12 @@ export default function RecommendedQuestionsPage() {
|
||||
try {
|
||||
await deleteMutation.mutateAsync(q.id);
|
||||
setDeleteTarget(null);
|
||||
toast.success("问题已删除");
|
||||
toast.success("问题已删除", darkToastOpts.success);
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "删除失败");
|
||||
toast.error(
|
||||
e instanceof Error ? e.message : "删除失败",
|
||||
darkToastOpts.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +160,7 @@ export default function RecommendedQuestionsPage() {
|
||||
try {
|
||||
await reorderMutation.mutateAsync(next.map((q) => q.id));
|
||||
} catch {
|
||||
toast.error("排序保存失败");
|
||||
toast.error("排序保存失败", darkToastOpts.error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +170,11 @@ export default function RecommendedQuestionsPage() {
|
||||
deleteMutation.isPending ||
|
||||
reorderMutation.isPending;
|
||||
|
||||
const darkToastOpts =
|
||||
theme === "dark"
|
||||
? { success: successToastDarkOptions, error: errorToastDarkOptions }
|
||||
: { success: undefined, error: undefined };
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-2xl flex-col gap-6 px-6 py-10">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -162,15 +199,15 @@ export default function RecommendedQuestionsPage() {
|
||||
<p>加载失败:{error.message}</p>
|
||||
</div>
|
||||
) : questions.length === 0 ? (
|
||||
<div className="text-muted-foreground flex flex-col items-center gap-3 rounded-xl border border-dashed py-16 text-sm">
|
||||
<div className="text-muted-foreground flex flex-col items-center gap-3 rounded-xl border border-dashed py-16 text-sm dark:text-[#7FACF2] dark:!border-[#213F62]">
|
||||
<p>暂无推荐问题</p>
|
||||
<Button variant="outline" size="sm" onClick={openAdd}>
|
||||
<Button size="sm" onClick={openAdd}>
|
||||
<PlusIcon className="mr-1.5 size-4" />
|
||||
添加第一个问题
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-2 ">
|
||||
{questions.map((q, index) => (
|
||||
<div
|
||||
key={q.id}
|
||||
@@ -182,18 +219,18 @@ export default function RecommendedQuestionsPage() {
|
||||
setDragIndex(null);
|
||||
setDragOverIndex(null);
|
||||
}}
|
||||
className={`flex items-start gap-3 rounded-xl border bg-white px-4 py-3.5 transition-colors ${
|
||||
className={`flex items-start gap-3 rounded-xl border bg-white px-4 py-3.5 transition-colors dark:bg-[#022248] dark:!border-[#213D5E] ${
|
||||
dragOverIndex === index && dragIndex !== index
|
||||
? "border-blue-400 bg-blue-50"
|
||||
? "border-blue-400 bg-blue-50 dark:border-[#2580FF] dark:bg-[#0D2D54]"
|
||||
: "border-[#e4e4e7]"
|
||||
}`}
|
||||
>
|
||||
<GripVerticalIcon className="text-muted-foreground/40 mt-0.5 size-4 shrink-0 cursor-grab active:cursor-grabbing" />
|
||||
<GripVerticalIcon className="text-muted-foreground/40 mt-0.5 size-4 shrink-0 cursor-grab active:cursor-grabbing dark:text-[#566B7F]" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium text-[#18181b]">
|
||||
<div className="truncate text-sm font-medium text-[#18181b] dark:text-white">
|
||||
{q.title}
|
||||
</div>
|
||||
<div className="text-muted-foreground mt-0.5 line-clamp-2 text-xs">
|
||||
<div className="text-muted-foreground mt-0.5 line-clamp-2 text-xs dark:text-[#7FACF2]">
|
||||
{q.content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
AlertCircle,
|
||||
BellOff,
|
||||
BellRing,
|
||||
CheckCircle2,
|
||||
Download,
|
||||
Edit3,
|
||||
Eye,
|
||||
@@ -62,9 +64,32 @@ import {
|
||||
statusLabel,
|
||||
statusVariant,
|
||||
} from "@/components/workspace/scheduled-tasks/schedule-builder";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||
const successToastDarkOptions = {
|
||||
classNames: {
|
||||
toast:
|
||||
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
|
||||
title: "!text-[#FFFFFF]",
|
||||
description: "!text-[#FFFFFF]",
|
||||
},
|
||||
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
|
||||
} as const;
|
||||
|
||||
/** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||
const errorToastDarkOptions = {
|
||||
classNames: {
|
||||
toast:
|
||||
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
|
||||
title: "!text-[#FFFFFF]",
|
||||
description: "!text-[#FFFFFF]",
|
||||
},
|
||||
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
|
||||
} as const;
|
||||
|
||||
function formatDate(value?: string | null) {
|
||||
return value ? new Date(value).toLocaleString() : "暂无";
|
||||
}
|
||||
@@ -88,6 +113,11 @@ const emptyForm = {
|
||||
};
|
||||
|
||||
export default function ScheduledTasksPage() {
|
||||
const { theme } = useTheme();
|
||||
const darkToastOpts =
|
||||
theme === "dark"
|
||||
? { success: successToastDarkOptions, error: errorToastDarkOptions }
|
||||
: { success: undefined, error: undefined };
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
// When returning from a run-detail page the task id is carried in ?task=,
|
||||
@@ -207,7 +237,7 @@ export default function ScheduledTasksPage() {
|
||||
require_markdown: form.require_markdown,
|
||||
});
|
||||
setSelectedTaskId(task.task_id);
|
||||
toast.success("任务已创建");
|
||||
toast.success("任务已创建", darkToastOpts.success);
|
||||
} else if (editing) {
|
||||
await actions.update.mutateAsync({
|
||||
taskId: editing.task_id,
|
||||
@@ -221,11 +251,14 @@ export default function ScheduledTasksPage() {
|
||||
require_markdown: form.require_markdown,
|
||||
},
|
||||
});
|
||||
toast.success("任务已更新");
|
||||
toast.success("任务已更新", darkToastOpts.success);
|
||||
}
|
||||
closeDialog();
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "保存失败");
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "保存失败",
|
||||
darkToastOpts.error,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -233,16 +266,22 @@ export default function ScheduledTasksPage() {
|
||||
try {
|
||||
await downloadScheduledTaskRun(runId);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "下载失败");
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "下载失败",
|
||||
darkToastOpts.error,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRun = async (runId: string) => {
|
||||
try {
|
||||
await actions.removeRun.mutateAsync(runId);
|
||||
toast.success("执行记录已删除");
|
||||
toast.success("执行记录已删除", darkToastOpts.success);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "删除失败");
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "删除失败",
|
||||
darkToastOpts.error,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -273,14 +312,18 @@ export default function ScheduledTasksPage() {
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="dark:bg-[#0D2D54] dark:hover:!bg-[#2580FF] dark:hover:text-white"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await actions.updateDeliveryProfile.mutateAsync({
|
||||
element_user_id: elementUserId || null,
|
||||
});
|
||||
toast.success("Element 配置已保存");
|
||||
toast.success("Element 配置已保存", darkToastOpts.success);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "保存失败");
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "保存失败",
|
||||
darkToastOpts.error,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -437,7 +480,7 @@ export default function ScheduledTasksPage() {
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
actions.trigger.mutate(task.task_id);
|
||||
toast.success("已触发执行");
|
||||
toast.success("已触发执行", darkToastOpts.success);
|
||||
}}
|
||||
>
|
||||
<RefreshCw className="size-4" />
|
||||
@@ -451,13 +494,19 @@ export default function ScheduledTasksPage() {
|
||||
try {
|
||||
if (task.published) {
|
||||
await actions.unpublish.mutateAsync(task.task_id);
|
||||
toast.success("已取消发布(已订阅者保留)");
|
||||
toast.success(
|
||||
"已取消发布(已订阅者保留)",
|
||||
darkToastOpts.success,
|
||||
);
|
||||
} else {
|
||||
await actions.publish.mutateAsync(task.task_id);
|
||||
toast.success("已发布到广场");
|
||||
toast.success("已发布到广场", darkToastOpts.success);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "操作失败");
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "操作失败",
|
||||
darkToastOpts.error,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -488,9 +537,15 @@ export default function ScheduledTasksPage() {
|
||||
taskId: task.task_id,
|
||||
notifyElement: !notifyOn,
|
||||
});
|
||||
toast.success(notifyOn ? "已关闭 Element 通知" : "已开启 Element 通知");
|
||||
toast.success(
|
||||
notifyOn ? "已关闭 Element 通知" : "已开启 Element 通知",
|
||||
darkToastOpts.success,
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "操作失败");
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "操作失败",
|
||||
darkToastOpts.error,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -505,9 +560,12 @@ export default function ScheduledTasksPage() {
|
||||
event.stopPropagation();
|
||||
try {
|
||||
await actions.unsubscribe.mutateAsync(task.task_id);
|
||||
toast.success("已取消收藏");
|
||||
toast.success("已取消收藏", darkToastOpts.success);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "操作失败");
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "操作失败",
|
||||
darkToastOpts.error,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -581,7 +639,7 @@ export default function ScheduledTasksPage() {
|
||||
<SelectTrigger className="h-7 w-28 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent position="popper" side="bottom" sideOffset={4}>
|
||||
<SelectItem value="all">全部时间</SelectItem>
|
||||
<SelectItem value="today">今天</SelectItem>
|
||||
<SelectItem value="7d">近 7 天</SelectItem>
|
||||
@@ -747,12 +805,15 @@ export default function ScheduledTasksPage() {
|
||||
taskId: subscribeTarget.task_id,
|
||||
notifyElement: subscribeNotify,
|
||||
});
|
||||
toast.success("已收藏到我的任务");
|
||||
toast.success("已收藏到我的任务", darkToastOpts.success);
|
||||
setSubscribeTarget(null);
|
||||
setSubscribeNotify(false);
|
||||
setActiveTab("mine");
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "收藏失败");
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "收藏失败",
|
||||
darkToastOpts.error,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -87,9 +87,20 @@ export function WorkspaceLayout({
|
||||
|
||||
return (
|
||||
<SidebarProvider
|
||||
// 关键:把 workspace 侧栏从「锚定视口(fixed)」改成「锚定本布局容器(absolute)」。
|
||||
// 原 shadcn 原语用 fixed + left:var(--page-sidebar-width) + top:var(--header-height)
|
||||
// 把侧栏钉在视口坐标上 —— 在无界等「DOM 挂主文档、容器不占满整屏」的嵌套场景下,
|
||||
// fixed 会逃逸到整屏左侧、高度锁死为视口高度(即超出父元素 / 高度写死的根因)。
|
||||
//
|
||||
// 改 absolute 后侧栏改以本 wrapper(relative)为定位基准;又因外层 main 已经
|
||||
// pl-[--page-sidebar-width] 让开了页面侧栏、且 main 的 pl 恒等于 --page-sidebar-width,
|
||||
// 故这里 left/top 归零即与原 fixed 视觉位置逐像素一致;高度则跟随容器而非视口。
|
||||
// 作用域仅限本 SidebarProvider 实例(不动全局 ui/sidebar 原语),不影响 artifacts 等其它侧栏。
|
||||
className={[
|
||||
'h-full w-full [&_[data-slot=sidebar-container]]:left-[var(--page-sidebar-width)]',
|
||||
wrapperOverflowHidden ? 'overflow-hidden' : 'overflow-x-hidden',
|
||||
'relative h-full w-full overflow-x-hidden',
|
||||
'[&_[data-slot=sidebar-container]]:absolute',
|
||||
'[&_[data-slot=sidebar-container]]:left-0',
|
||||
'[&_[data-slot=sidebar-container]]:top-0',
|
||||
].join(' ')}
|
||||
style={
|
||||
{
|
||||
@@ -97,8 +108,6 @@ export function WorkspaceLayout({
|
||||
'--header-height': headerHidden ? '0rem' : '4rem',
|
||||
} as CSSProperties
|
||||
}
|
||||
// Keep the fixed workspace sidebar aligned to the outer page sidebar width.
|
||||
// This lets it slide together when the outer sidebar collapses or expands.
|
||||
data-page-sidebar-collapsed={pageSidebarCollapsed}
|
||||
defaultOpen
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user