This commit is contained in:
董宏凯 committed 2026-05-22 21:41:52 +08:00
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 />;
}
+53 -10
View File
@@ -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">
+12 -7
View File
@@ -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>
+18 -27
View File
@@ -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={