Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1
This commit is contained in:
commit
d6ff00226a
34 files changed
+1322
-234
No files matched your search
@@ -0,0 +1,5 @@
|
||||
import { AdminUserHub } from "@/components/workspace/admin/admin-user-hub";
|
||||
|
||||
export default function AdminUserHubPage() {
|
||||
return <AdminUserHub />;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import { BotIcon, PlusSquare } from "lucide-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useRouteBase } from "@/core/route-base";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -36,14 +36,33 @@ export default function AgentChatPage() {
|
||||
const routeBase = useRouteBase();
|
||||
const { t } = useI18n();
|
||||
const [showFollowups, setShowFollowups] = useState(false);
|
||||
const [hasStartedConversation, setHasStartedConversation] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const { agent_id } = useParams<{
|
||||
agent_id: string;
|
||||
}>();
|
||||
|
||||
const { agent } = useAgent(agent_id);
|
||||
// If ChatPage stashed an opening message for this agent, skip the landing
|
||||
// screen on first render so the user doesn't see a flash of the welcome UI
|
||||
// before the auto-sent message takes over.
|
||||
const hasPendingForThisAgent = () => {
|
||||
if (typeof window === "undefined" || !agent_id) return false;
|
||||
try {
|
||||
const raw = sessionStorage.getItem("pendingAgentMessage");
|
||||
if (!raw) return false;
|
||||
const parsed = JSON.parse(raw);
|
||||
return parsed?.agentId === agent_id && !!parsed?.text;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const [hasStartedConversation, setHasStartedConversation] = useState(
|
||||
hasPendingForThisAgent,
|
||||
);
|
||||
|
||||
const { agent, isLoading: isAgentLoading } = useAgent(agent_id);
|
||||
const agentDisplayName =
|
||||
agent?.name ?? (isAgentLoading ? t.common.loading : "");
|
||||
|
||||
const { threadId, setThreadId, isNewThread, setIsNewThread } =
|
||||
useThreadChat();
|
||||
@@ -52,9 +71,10 @@ export default function AgentChatPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (isNewThread) {
|
||||
setHasStartedConversation(false);
|
||||
setHasStartedConversation(hasPendingForThisAgent());
|
||||
}
|
||||
}, [isNewThread, threadId]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isNewThread, threadId, agent_id]);
|
||||
|
||||
const { showNotification } = useNotification();
|
||||
const [thread, sendMessage] = useThreadStream({
|
||||
@@ -100,6 +120,26 @@ export default function AgentChatPage() {
|
||||
[sendMessage, threadId, agent_id],
|
||||
);
|
||||
|
||||
// ChatPage stashes the opening message in sessionStorage when the user picks
|
||||
// an agent on the landing screen, then redirects here. Replay it once so the
|
||||
// user doesn't have to re-type the question on the agent chat page.
|
||||
const pendingMessageHandled = useRef(false);
|
||||
useEffect(() => {
|
||||
if (pendingMessageHandled.current) return;
|
||||
if (!agent_id || !isNewThread || hasStartedConversation) return;
|
||||
let pending: { agentId?: string; text?: string } | null = null;
|
||||
try {
|
||||
const raw = sessionStorage.getItem("pendingAgentMessage");
|
||||
if (raw) pending = JSON.parse(raw);
|
||||
} catch {
|
||||
pending = null;
|
||||
}
|
||||
if (!pending || pending.agentId !== agent_id || !pending.text) return;
|
||||
pendingMessageHandled.current = true;
|
||||
sessionStorage.removeItem("pendingAgentMessage");
|
||||
handleSubmit({ text: pending.text, files: [] });
|
||||
}, [agent_id, isNewThread, hasStartedConversation, handleSubmit]);
|
||||
|
||||
const handleClarificationSubmit = useCallback(
|
||||
(answer: string) => {
|
||||
setHasStartedConversation(true);
|
||||
@@ -133,8 +173,11 @@ export default function AgentChatPage() {
|
||||
{/* Agent badge */}
|
||||
<div className="flex shrink-0 items-center gap-1.5 rounded-md border px-2 py-1">
|
||||
<BotIcon className="text-primary h-3.5 w-3.5" />
|
||||
<span className="text-xs font-medium">
|
||||
{agent?.name ?? agent_id}
|
||||
<span
|
||||
className="max-w-[200px] truncate text-xs font-medium"
|
||||
title={agentDisplayName}
|
||||
>
|
||||
{agentDisplayName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -180,8 +223,8 @@ export default function AgentChatPage() {
|
||||
"relative w-full",
|
||||
isLandingState && "-translate-y-[calc(50vh-96px)]",
|
||||
isLandingState
|
||||
? "max-w-(--container-width-sm)"
|
||||
: "max-w-(--container-width-md)",
|
||||
? "max-w-(--container-width-md)"
|
||||
: "max-w-(--container-width-lg)",
|
||||
)}
|
||||
>
|
||||
<div className="absolute -top-4 right-0 left-0 z-0">
|
||||
@@ -212,7 +255,7 @@ export default function AgentChatPage() {
|
||||
context={settings.context}
|
||||
extraHeader={
|
||||
isLandingState && (
|
||||
<AgentWelcome agent={agent} agentName={agent?.name ?? agent_id} />
|
||||
<AgentWelcome agent={agent} agentName={agentDisplayName} />
|
||||
)
|
||||
}
|
||||
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"}
|
||||
|
||||
@@ -193,6 +193,19 @@ function formatDate(value?: string | null): string {
|
||||
return value ? new Date(value).toLocaleString() : "暂无";
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact "M月D日" badge prefixed to each run title so visually identical
|
||||
* names (the agent often emits the same title every run) become scannable.
|
||||
* Returns empty string for an invalid / missing date so the title falls back
|
||||
* to its bare form instead of rendering "Invalid Date".
|
||||
*/
|
||||
function formatScheduledDatePrefix(value?: string | null): string {
|
||||
if (!value) return "";
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return "";
|
||||
return `${date.getMonth() + 1}月${date.getDate()}日`;
|
||||
}
|
||||
|
||||
function isMarkdownFile(file: { name: string; kind?: string | null; mime_type?: string | null }): boolean {
|
||||
const name = file.name.toLowerCase();
|
||||
return (
|
||||
@@ -373,6 +386,7 @@ export default function PublicScheduledTaskPage() {
|
||||
const runTitle = stripTimestampSuffix(
|
||||
run.result?.title || formatDate(run.scheduled_for),
|
||||
);
|
||||
const datePrefix = formatScheduledDatePrefix(run.scheduled_for);
|
||||
const isDeletingThis =
|
||||
deleteRunMutation.isPending && deleteRunMutation.variables === run.id;
|
||||
return (
|
||||
@@ -398,13 +412,18 @@ export default function PublicScheduledTaskPage() {
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className="text-sm leading-snug font-medium break-all"
|
||||
title={runTitle}
|
||||
title={datePrefix ? `${datePrefix} · ${runTitle}` : runTitle}
|
||||
>
|
||||
{datePrefix && (
|
||||
<span className="text-muted-foreground mr-1.5 text-xs font-normal tabular-nums">
|
||||
{datePrefix}
|
||||
</span>
|
||||
)}
|
||||
{runTitle}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-[360px] break-all">
|
||||
{runTitle}
|
||||
{datePrefix ? `${datePrefix} · ${runTitle}` : runTitle}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="text-muted-foreground mt-1 text-xs">
|
||||
|
||||
@@ -758,11 +758,11 @@ export default function ScheduledTasksPage() {
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={creating || !!editing} onOpenChange={(open) => !open && closeDialog()}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogContent className="max-w-2xl overflow-hidden">
|
||||
<DialogHeader className="min-w-0">
|
||||
<DialogTitle>{creating ? "新建定时任务" : "编辑定时任务"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex min-w-0 flex-col gap-3">
|
||||
<Input
|
||||
value={form.name}
|
||||
onChange={(event) =>
|
||||
@@ -798,15 +798,20 @@ export default function ScheduledTasksPage() {
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="w-full min-w-0 max-w-full overflow-hidden [&>span]:min-w-0 [&>span]:flex-1 [&>span]:truncate">
|
||||
<SelectValue placeholder="选择执行智能体" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent
|
||||
position="popper"
|
||||
className="w-[var(--radix-select-trigger-width)] max-w-[var(--radix-select-trigger-width)]"
|
||||
>
|
||||
<SelectItem value="__default__">默认助手(不指定)</SelectItem>
|
||||
{agents.map((agent) => (
|
||||
<SelectItem key={agent.id} value={agent.id}>
|
||||
{agent.name}
|
||||
{agent.description ? ` — ${agent.description}` : ""}
|
||||
<span className="block truncate">
|
||||
{agent.name}
|
||||
{agent.description ? ` — ${agent.description}` : ""}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -29,6 +29,7 @@ import SkillsPage from "./SkillsPage";
|
||||
import UsersPage from "./UsersPage";
|
||||
import AdminUserActivityPage from "./AdminUserActivityPage";
|
||||
import AdminMemoryPage from "./AdminMemoryPage";
|
||||
import AdminUserHubPage from "./AdminUserHubPage";
|
||||
import TagsPage from "./TagsPage";
|
||||
import RecommendedQuestionsPage from "./RecommendedQuestionsPage";
|
||||
import AIWritingPage from "@/open-canvas/pages/AIWritingPage";
|
||||
@@ -206,18 +207,32 @@ export default function WorkspaceRoutes({
|
||||
</WorkspaceLayout>
|
||||
}
|
||||
/>
|
||||
{/* Unified admin user hub */}
|
||||
<Route
|
||||
path="admin/users"
|
||||
path="admin"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<WorkspaceLayout {...layoutProps}>
|
||||
<UsersPage />
|
||||
<WorkspaceLayout {...layoutProps} pageSidebarHidden>
|
||||
<AdminUserHubPage />
|
||||
</WorkspaceLayout>
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
{/* Legacy redirects */}
|
||||
<Route
|
||||
path="admin/users"
|
||||
element={<Navigate to="/page/workspace/admin" replace />}
|
||||
/>
|
||||
<Route
|
||||
path="admin/user-activity"
|
||||
element={<Navigate to="/page/workspace/admin" replace />}
|
||||
/>
|
||||
<Route
|
||||
path="admin/memory"
|
||||
element={<Navigate to="/page/workspace/admin" replace />}
|
||||
/>
|
||||
<Route
|
||||
path="admin/scheduled-tasks"
|
||||
element={
|
||||
@@ -230,30 +245,6 @@ export default function WorkspaceRoutes({
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/user-activity"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<WorkspaceLayout {...layoutProps}>
|
||||
<AdminUserActivityPage />
|
||||
</WorkspaceLayout>
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/memory"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<WorkspaceLayout {...layoutProps}>
|
||||
<AdminMemoryPage />
|
||||
</WorkspaceLayout>
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/tags"
|
||||
element={
|
||||
|
||||
Reference in new issue
Block a user