Merge remote-tracking branch 'origin/master' into ji

# Conflicts:
#	frontend-web/src/pages/AgentChatPage.tsx
This commit is contained in:
吉鑫升 committed 2026-05-21 17:44:09 +08:00
commit 643f9b2c76
51 files changed
+2654 -2164

No files matched your search

+29 -54
View File
@@ -3,10 +3,11 @@
import { BotIcon, PlusSquare } from "lucide-react";
import { useParams, useRouter } from "next/navigation";
import { useRouteBase } from "@/core/route-base";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
import { Button } from "@/components/ui/button";
import { AgentWelcome } from "@/components/workspace/agent-welcome";
import { ArtifactTrigger } from "@/components/workspace/artifacts";
import { ChatBox, useThreadChat } from "@/components/workspace/chats";
import { ExportTrigger } from "@/components/workspace/export-trigger";
@@ -55,11 +56,6 @@ export default function AgentChatPage() {
}
}, [isNewThread, threadId]);
// Auto-submit a message that the user typed on ChatPage right before
// selecting this agent. ChatPage stashed it in sessionStorage so we don't
// depend on router state (the next/navigation shim doesn't carry it).
const autoSubmitDoneRef = useRef(false);
const { showNotification } = useNotification();
const [thread, sendMessage] = useThreadStream({
threadId: isNewThread ? undefined : threadId,
@@ -104,29 +100,6 @@ export default function AgentChatPage() {
[sendMessage, threadId, agent_id],
);
useEffect(() => {
if (autoSubmitDoneRef.current) return;
if (!isNewThread || hasStartedConversation) return;
let raw: string | null = null;
try {
raw = sessionStorage.getItem("pendingAgentMessage");
} catch {
return;
}
if (!raw) return;
try {
const parsed = JSON.parse(raw) as { agentId?: string; text?: string };
if (parsed.agentId !== agent_id || !parsed.text) {
return;
}
autoSubmitDoneRef.current = true;
sessionStorage.removeItem("pendingAgentMessage");
handleSubmit({ text: parsed.text, files: [] });
} catch {
sessionStorage.removeItem("pendingAgentMessage");
}
}, [agent_id, hasStartedConversation, handleSubmit, isNewThread]);
const handleClarificationSubmit = useCallback(
(answer: string) => {
setHasStartedConversation(true);
@@ -143,12 +116,7 @@ export default function AgentChatPage() {
? MESSAGE_LIST_DEFAULT_PADDING_BOTTOM +
MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM
: undefined;
// AgentChatPage skips the centered welcome/landing state entirely — when the
// user reaches this page they've already committed to an agent (either by
// selecting one on the homepage or by visiting the agent's URL), so we drop
// them straight into the conversation layout: messages area on top, input at
// the bottom. The agent badge in the header still identifies the agent.
const isLandingState = false;
const isLandingState = isNewThread && !hasStartedConversation;
return (
<ThreadContext.Provider value={{ thread }}>
@@ -228,25 +196,32 @@ export default function AgentChatPage() {
</div>
</div>
<InputBox
className={cn("bg-background/5 w-full -translate-y-4")}
isNewThread={isNewThread}
threadId={threadId}
autoFocus={isNewThread}
status={
thread.error
? "error"
: thread.isLoading
? "streaming"
: "ready"
}
context={settings.context}
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"}
onContextChange={(context) => setSettings("context", context)}
onFollowupsVisibilityChange={setShowFollowups}
onSubmit={handleSubmit}
onStop={handleStop}
/>
<div className="w-full -translate-y-4">
<InputBox
className="w-full"
isNewThread={isLandingState}
threadId={threadId}
autoFocus={isLandingState}
status={
thread.error
? "error"
: thread.isLoading
? "streaming"
: "ready"
}
context={settings.context}
extraHeader={
isLandingState && (
<AgentWelcome agent={agent} agentName={agent?.name ?? agent_id} />
)
}
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"}
onContextChange={(context) => setSettings("context", context)}
onFollowupsVisibilityChange={setShowFollowups}
onSubmit={handleSubmit}
onStop={handleStop}
/>
</div>
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" && (
<div className="text-muted-foreground/67 w-full translate-y-12 text-center text-xs">
{t.common.notAvailableInDemoMode}
@@ -0,0 +1,96 @@
"use client";
import { useMemo, useState } from "react";
import { ArrowRight, NotebookPen, Sparkles } from "lucide-react";
import { useTheme } from "@/contexts/ThemeContext";
import { cn } from "@/lib/utils";
export default function LightAppNewNotePage() {
const { theme } = useTheme();
const isDark = theme === "dark";
const [titleInput, setTitleInput] = useState("");
const hasTitle = useMemo(() => titleInput.trim().length > 0, [titleInput]);
return (
<div
className={cn(
"flex h-full w-full items-center justify-center px-8",
isDark ? "bg-slate-900/20" : "bg-[#fdfcfc]",
)}
>
<div className="w-full max-w-[720px] -translate-y-12 space-y-8">
{/* 固定标题:新建笔记 */}
<div className="flex items-center gap-3">
<NotebookPen
className={cn("h-5 w-5 shrink-0", isDark ? "text-slate-400" : "text-black/70")}
/>
<h1
className={cn(
"text-base font-medium tracking-tight",
isDark ? "text-slate-200" : "text-black/85",
)}
>
新建笔记
</h1>
</div>
{/* 标题输入框 */}
<div className="flex items-center gap-3">
<input
value={titleInput}
onChange={(e) => setTitleInput(e.target.value)}
placeholder="新建笔记"
className={cn(
"min-w-0 flex-1 bg-transparent text-[42px] leading-tight font-normal tracking-tight outline-none",
hasTitle
? isDark
? "text-white"
: "text-black/85"
: isDark
? "text-slate-500 placeholder:text-slate-600"
: "text-black/25 placeholder:text-black/25",
)}
/>
{hasTitle && (
<button
type="button"
className={cn(
"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full transition-colors",
isDark
? "bg-blue-500 text-white hover:bg-blue-400"
: "bg-sky-200 text-slate-700 hover:bg-sky-300",
)}
aria-label="进入笔记"
>
<ArrowRight className="h-4 w-4" />
</button>
)}
</div>
{/* 说明文字 */}
<div
className={cn(
"flex h-11 items-center gap-2.5 rounded-xl px-4",
isDark
? "border border-slate-700 bg-slate-800/70"
: "border border-black/5 bg-[#ececef]",
)}
>
<Sparkles
className={cn("h-3.5 w-3.5 shrink-0", isDark ? "text-slate-400" : "text-black/55")}
/>
<p
className={cn(
"text-sm",
isDark ? "text-slate-400" : "text-black/55",
)}
>
新建笔记开始使用
</p>
</div>
</div>
</div>
);
}
+12 -2
View File
@@ -1,10 +1,14 @@
"use client";
import { useParams, useRouter } from "next/navigation";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import { loginByUsername } from "@/core/auth/api";
import { setStoredAuth } from "@/core/auth";
import {
isSidebarEnabledFromSearch,
persistSidebarEnabled,
} from "@/core/page-layout/sidebar-enabled";
const DEFAULT_USERNAME = "guest";
@@ -13,6 +17,11 @@ type LoginStatus = "loading" | "success" | "error";
export default function LoginPage() {
const { username } = useParams<{ username?: string }>();
const router = useRouter();
const searchParams = useSearchParams();
const sidebarEnabled = useMemo(
() => isSidebarEnabledFromSearch(searchParams.toString()),
[searchParams],
);
const resolvedUsername = useMemo(() => {
const raw = username?.trim();
return raw && raw.length > 0 ? raw : DEFAULT_USERNAME;
@@ -32,6 +41,7 @@ export default function LoginPage() {
return;
}
setStoredAuth(auth);
persistSidebarEnabled(sidebarEnabled);
setStatus("success");
router.replace("/page/workspace/chats/new");
} catch (error) {
@@ -49,7 +59,7 @@ export default function LoginPage() {
return () => {
cancelled = true;
};
}, [resolvedUsername, router]);
}, [resolvedUsername, router, sidebarEnabled]);
return (
<div className="flex min-h-screen items-center justify-center bg-background px-6">
+9 -9
View File
@@ -1,22 +1,21 @@
import { useState } from "react";
import { useMemo, useState } from "react";
import { Navigate, Route, Routes } from "react-router-dom";
import { PageSidebar } from "@/components/page-sidebar";
import { readSidebarEnabled } from "@/core/page-layout/sidebar-enabled";
import WorkspaceRoutes from "./WorkspaceRoutes";
import StrategyRoutes from "./StrategyRoutes";
import Header from "@/strategy-components/Header";
import OpenCanvasRoutes from "@/open-canvas/routes/OpenCanvasRoutes";
// 设为 true 时隐藏侧边栏(开发中,暂未完成)
const SIDEBAR_ENABLED = false;
function PageLayout() {
const [collapsed, setCollapsed] = useState(false);
const sidebarEnabled = useMemo(() => readSidebarEnabled(), []);
return (
<div className="h-screen flex bg-muted/20">
{SIDEBAR_ENABLED && (
{sidebarEnabled && (
<PageSidebar
collapsed={collapsed}
onToggleCollapsed={() => setCollapsed((prev) => !prev)}
@@ -24,10 +23,10 @@ function PageLayout() {
)}
<div
className={`flex flex-col flex-1 overflow-hidden transition-[padding] duration-300 ${
!SIDEBAR_ENABLED ? "" : collapsed ? "pl-16" : "pl-64"
!sidebarEnabled ? "" : collapsed ? "pl-16" : "pl-64"
}`}
>
{SIDEBAR_ENABLED && <Header />}
{sidebarEnabled && <Header />}
<main className="flex-1 overflow-hidden">
<Routes>
<Route index element={<Navigate to="workspace" replace />} />
@@ -36,8 +35,9 @@ function PageLayout() {
element={
<WorkspaceRoutes
pageSidebarCollapsed={collapsed}
pageSidebarHidden={!SIDEBAR_ENABLED}
headerHidden={!SIDEBAR_ENABLED}
pageSidebarHidden={!sidebarEnabled}
headerHidden={!sidebarEnabled}
compactWorkspaceSidebar={sidebarEnabled}
/>
}
/>
+80 -2
View File
@@ -12,9 +12,19 @@ import ChatsPage from "./ChatsPage";
import ChatPage from "./ChatPage";
import NewAgentPage from "./NewAgentPage";
import AgentChatPage from "./AgentChatPage";
import SkillsPage from "./SkillsPage";
import SkillEvolutionPage from "./SkillEvolutionPage";
import MemoryPage from "./MemoryPage";
import ScheduledTasksPage from "./ScheduledTasksPage";
import ScheduledTaskRunDetailPage from "./ScheduledTaskRunDetailPage";
import RecommendedQuestionsPage from "./RecommendedQuestionsPage";
import PromptPrefixSettingsPage from "./PromptPrefixSettingsPage";
import LlmMetricsPage from "./LlmMetricsPage";
import UsersPage from "./UsersPage";
import AdminScheduledTasksPage from "./AdminScheduledTasksPage";
import AdminUserActivityPage from "./AdminUserActivityPage";
import TagsPage from "./TagsPage";
import LightAppNewNotePage from "./LightAppNewNotePage";
export default function StrategyRoutes() {
return (
@@ -27,6 +37,9 @@ export default function StrategyRoutes() {
{/* 问答中心 */}
<Route path="qa/general" element={<GeneralQA />} />
{/* 轻应用 */}
<Route path="light-app/new-note" element={<LightAppNewNotePage />} />
{/* 对话 */}
<Route path="chats" element={<ChatsPage />} />
<Route
@@ -54,6 +67,11 @@ export default function StrategyRoutes() {
}
/>
{/* 技能 / 记忆 */}
<Route path="skills" element={<SkillsPage />} />
<Route path="skills/evolution" element={<SkillEvolutionPage />} />
<Route path="memory" element={<MemoryPage />} />
{/* 定时任务 */}
<Route
path="scheduled"
@@ -65,14 +83,74 @@ export default function StrategyRoutes() {
element={<ScheduledTaskRunDetailPage />}
/>
{/* 设置 */}
{/* 设置 / 管理 — 权限与 workspace 侧一致,非 admin 重定向到 chats/new */}
<Route
path="settings/recommended-questions"
element={
getStoredAuth()?.system_role === "admin" ? (
<RecommendedQuestionsPage />
) : (
<Navigate to="/page/strategy/chats" replace />
<Navigate to="/page/workspace/chats/new" replace />
)
}
/>
<Route
path="settings/prompt-prefix"
element={
getStoredAuth()?.system_role === "admin" ? (
<PromptPrefixSettingsPage />
) : (
<Navigate to="/page/workspace/chats/new" replace />
)
}
/>
<Route
path="settings/llm-metrics"
element={
getStoredAuth()?.system_role === "admin" ? (
<LlmMetricsPage />
) : (
<Navigate to="/page/workspace/chats/new" replace />
)
}
/>
<Route
path="admin/users"
element={
getStoredAuth()?.system_role === "admin" ? (
<UsersPage />
) : (
<Navigate to="/page/workspace/chats/new" replace />
)
}
/>
<Route
path="admin/scheduled-tasks"
element={
getStoredAuth()?.system_role === "admin" ? (
<AdminScheduledTasksPage />
) : (
<Navigate to="/page/workspace/chats/new" replace />
)
}
/>
<Route
path="admin/user-activity"
element={
getStoredAuth()?.system_role === "admin" ? (
<AdminUserActivityPage />
) : (
<Navigate to="/page/workspace/chats/new" replace />
)
}
/>
<Route
path="admin/tags"
element={
getStoredAuth()?.system_role === "admin" ? (
<TagsPage />
) : (
<Navigate to="/page/workspace/chats/new" replace />
)
}
/>
+11 -2
View File
@@ -37,11 +37,13 @@ export function WorkspaceLayout({
pageSidebarCollapsed,
pageSidebarHidden = false,
headerHidden = false,
compactWorkspaceSidebar = false,
}: {
children: ReactNode;
pageSidebarCollapsed: boolean;
pageSidebarHidden?: boolean;
headerHidden?: boolean;
compactWorkspaceSidebar?: boolean;
}) {
const pageSidebarWidth = pageSidebarHidden
? "0rem"
@@ -63,7 +65,7 @@ export function WorkspaceLayout({
data-page-sidebar-collapsed={pageSidebarCollapsed}
defaultOpen
>
<WorkspaceSidebar />
<WorkspaceSidebar compactChrome={compactWorkspaceSidebar} />
<SidebarInset className="min-w-0">{children}</SidebarInset>
<CommandPalette />
<Toaster position="top-center" />
@@ -85,12 +87,19 @@ export default function WorkspaceRoutes({
pageSidebarCollapsed = false,
pageSidebarHidden = false,
headerHidden = false,
compactWorkspaceSidebar = false,
}: {
pageSidebarCollapsed?: boolean;
pageSidebarHidden?: boolean;
headerHidden?: boolean;
compactWorkspaceSidebar?: boolean;
}) {
const layoutProps = { pageSidebarCollapsed, pageSidebarHidden, headerHidden };
const layoutProps = {
pageSidebarCollapsed,
pageSidebarHidden,
headerHidden,
compactWorkspaceSidebar,
};
return (
<RouteBaseProvider value="/page/workspace">