diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index f67fb51..a95698d 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -234,6 +234,13 @@ } @layer utilities { + /* Prompt/workflow editors opt into the entire area below the shared topbar. */ + .app-content:has([data-app-content="full-bleed"]) { + min-height: 0; + overflow: hidden; + padding: 0; + } + .scrollbar-subtle { scrollbar-width: thin; scrollbar-color: color-mix( diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index d1db827..0760514 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -26,7 +26,7 @@ export const metadata: Metadata = { }; // Apply the persisted theme before paint to avoid a flash. Defaults to dark navy. -const themeScript = `(function(){try{var t=localStorage.getItem('theme');if(t==='light'){document.documentElement.classList.remove('dark')}else{document.documentElement.classList.add('dark')}}catch(e){document.documentElement.classList.add('dark')}})();`; +const themeScript = `(function(){try{var t=localStorage.getItem('theme');var d=t==='light'?false:t==='system'?window.matchMedia('(prefers-color-scheme: dark)').matches:true;document.documentElement.classList.toggle('dark',d)}catch(e){document.documentElement.classList.add('dark')}})();`; export default function RootLayout({ children, diff --git a/frontend/src/components/assistant-editor/prompt-editor.tsx b/frontend/src/components/assistant-editor/prompt-editor.tsx index 1d388b1..a851d4e 100644 --- a/frontend/src/components/assistant-editor/prompt-editor.tsx +++ b/frontend/src/components/assistant-editor/prompt-editor.tsx @@ -5,6 +5,7 @@ import { Braces, Bot, Brain, + Bug, Database, Loader2, MessageSquareText, @@ -33,6 +34,7 @@ import { SectionCard } from "@/components/editor/section-card"; import { VisionConfigSection } from "@/components/editor/vision-config-section"; import { TurnConfigEditor } from "@/components/turn-config-editor"; import { Button } from "@/components/ui/button"; +import { TopbarPortal } from "@/components/layout/topbar-portal"; import { Input } from "@/components/ui/input"; import { Select, @@ -151,6 +153,7 @@ export function PromptEditor({ const selectedAnchorRef = useRef(null); const [activeSection, setActiveSection] = useState("conversation"); + const [debugOpen, setDebugOpen] = useState(false); useEffect(() => { const container = scrollContainerRef.current; @@ -263,43 +266,61 @@ export function PromptEditor({ } return ( -
-
-
- - updateForm("name", value)} - /> - -
+ <> + +
+
+ + updateForm("name", value)} + /> + +
-
- {saveError && ( - - {saveError} - - )} - + + +
-
+ -
-
+
+
-
+ ); } diff --git a/frontend/src/components/assistant-editor/workflow-page.tsx b/frontend/src/components/assistant-editor/workflow-page.tsx index 12df9fb..e3ffdf7 100644 --- a/frontend/src/components/assistant-editor/workflow-page.tsx +++ b/frontend/src/components/assistant-editor/workflow-page.tsx @@ -10,6 +10,7 @@ import { EditorBackButton, } from "@/components/assistant-editor/editor-controls"; import { Button } from "@/components/ui/button"; +import { TopbarPortal } from "@/components/layout/topbar-portal"; import { WorkflowEditor } from "@/components/workflow/WorkflowEditor"; import type { WorkflowSettings } from "@/components/workflow/types"; import { @@ -92,55 +93,57 @@ export function WorkflowPage({ setDynamicVariablesOpen, }: WorkflowPageProps) { return ( -
-
-
- - - -
+ <> + +
+
+ + + +
-
- {saveError && ( - - {saveError} - - )} - - + + +
-
+ -
+
-
+ ); } diff --git a/frontend/src/components/layout/AppShell.tsx b/frontend/src/components/layout/AppShell.tsx index 3a7c403..6851890 100644 --- a/frontend/src/components/layout/AppShell.tsx +++ b/frontend/src/components/layout/AppShell.tsx @@ -3,19 +3,27 @@ import { useState } from "react"; import { Sidebar } from "./Sidebar"; import { Topbar } from "./Topbar"; +import { TopbarTargetProvider } from "./topbar-portal"; export function AppShell({ children }: { children: React.ReactNode }) { const [collapsed, setCollapsed] = useState(false); + const [topbarTarget, setTopbarTarget] = useState(null); return (
setCollapsed((v) => !v)} /> -
- + +
+ -
{children}
-
+
+ {children} +
+
+
); } diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 9a8b099..232fbfb 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -3,12 +3,15 @@ import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { + Bell, Bot, Boxes, Brain, ChevronLeft, + ChevronUp, Clock3, Database, + HelpCircle, Wrench, Home, LogOut, @@ -16,6 +19,14 @@ import { Video, } from "lucide-react"; import { useAuth } from "@/components/auth/AuthProvider"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { ThemeModeSelector } from "./ThemeToggle"; type SidebarProps = { collapsed: boolean; @@ -202,64 +213,83 @@ export function Sidebar({ collapsed, onToggle }: SidebarProps) {
- - - + + + + - - {user?.displayName ?? "超级管理员"} - - - - - + + + + + 帮助 + + + + 消息提示 + + + void handleLogout()} + > + + 退出登录 + + + {/* 收起 / 展开侧栏 */} + {themeOptions.map((option) => { + const Icon = option.icon; + const selected = mode === option.value; + + return ( + + ); + })} +
); } diff --git a/frontend/src/components/layout/Topbar.tsx b/frontend/src/components/layout/Topbar.tsx index e6b6e38..c62c91e 100644 --- a/frontend/src/components/layout/Topbar.tsx +++ b/frontend/src/components/layout/Topbar.tsx @@ -1,41 +1,13 @@ -import { Bell, HelpCircle } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { ThemeToggle } from "./ThemeToggle"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; +import type { Ref } from "react"; -export function Topbar() { +export function Topbar({ + contentRef, +}: { + contentRef: Ref; +}) { return ( -
- - - - - - - - - - 通知 - - +
+
); } diff --git a/frontend/src/components/layout/topbar-portal.tsx b/frontend/src/components/layout/topbar-portal.tsx new file mode 100644 index 0000000..c56ba94 --- /dev/null +++ b/frontend/src/components/layout/topbar-portal.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { createContext, useContext, type ReactNode } from "react"; +import { createPortal } from "react-dom"; + +const TopbarTargetContext = createContext(null); + +export function TopbarTargetProvider({ + target, + children, +}: { + target: HTMLDivElement | null; + children: ReactNode; +}) { + return ( + + {children} + + ); +} + +/** + * Render page-owned controls into the shared application topbar. + * + * A portal keeps editor state and event handlers in the page that owns them, + * while AppShell only remains responsible for positioning the shared bar. + */ +export function TopbarPortal({ children }: { children: ReactNode }) { + const target = useContext(TopbarTargetContext); + + return target ? createPortal(children, target) : null; +} + +export function PageTopbar({ + title, + action, +}: { + title: string; + action?: ReactNode; +}) { + return ( +
+

+ {title} +

+ {action &&
{action}
} +
+ ); +} diff --git a/frontend/src/components/pages/AssistantPage.tsx b/frontend/src/components/pages/AssistantPage.tsx index 94d609e..6fa38cf 100644 --- a/frontend/src/components/pages/AssistantPage.tsx +++ b/frontend/src/components/pages/AssistantPage.tsx @@ -32,6 +32,10 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { DataList } from "@/components/ui/data-list"; +import { + PageTopbar, + TopbarPortal, +} from "@/components/layout/topbar-portal"; import { PageHeader } from "@/components/ui/page-header"; import { FilterPills } from "@/components/ui/filter-pills"; import { SearchInput } from "@/components/ui/search-input"; @@ -1005,268 +1009,270 @@ export function AssistantPage(props: AssistantPageProps) { } /> -
- - } - search={ - - } - /> +
+ + } + search={ + + } + /> - - rows={paginatedAssistants} - rowKey={(assistant) => assistant.id} - loading={listLoading} - loadingText="正在加载助手列表…" - error={listError} - onRetry={() => void loadAssistants()} - empty={{ - title: listItems.length === 0 ? "暂无助手" : "未找到匹配的助手", - description: - listItems.length === 0 - ? "点击右上角「创建助手」开始。" - : "请调整关键词或筛选条件后再试。", - }} - pagination={{ - page: safeCurrentPage, - totalPages, - onPageChange: setCurrentPage, - summary: - filteredAssistants.length === 0 - ? "没有数据" - : `显示 ${pageStart + 1}-${Math.min(pageEnd, filteredAssistants.length)} / 共 ${filteredAssistants.length} 个助手`, - }} - columns={[ - { - key: "name", - header: "助手名称", - width: "md:w-[360px]", - cell: (assistant) => ( - <> -
- {assistant.name} -
-
- {assistant.id} -
- - ), - }, - { - key: "type", - header: "助手类型", - width: "md:w-[128px]", - cell: (assistant) => ( - - {assistant.type} - - ), - }, - { - key: "updatedAt", - width: "md:w-[176px]", - header: ( - - ), - cellClassName: - "whitespace-nowrap tabular-nums text-muted-foreground", - cell: (assistant) => assistant.updatedAt, - }, - { - key: "actions", - header: "操作", - align: "right", - cellClassName: "flex justify-end gap-2", - cell: (assistant) => ( - <> - - - - - - - + ), + }, + { + key: "updatedAt", + width: "md:w-[176px]", + header: ( + + ), + cellClassName: + "whitespace-nowrap tabular-nums text-muted-foreground", + cell: (assistant) => assistant.updatedAt, + }, + { + key: "actions", + header: "操作", + align: "right", + cellClassName: "flex justify-end gap-2", + cell: (assistant) => ( + <> + + + + + + + - - 复制 - - { - event.preventDefault(); - void handleDelete(assistant.id); - }} - > - - 删除 - - - - - ), - }, - ]} - /> -
+ handleDuplicate(assistant)} + > + + 复制 + + { + event.preventDefault(); + void handleDelete(assistant.id); + }} + > + + 删除 + + + + + ), + }, + ]} + /> +
); } if (view === "choose") { return ( -
-
-
-

创建助手

-

- 先为助手取个名字,再选择构建方式。确认后将立即创建助手并进入编辑页。 -

-
+ <> + + router.push("/assistants")} + > + + 返回列表 + + } + /> + - -
+
+

+ 先为助手取个名字,再选择构建方式。确认后将立即创建助手并进入编辑页。 +

-
-
-
-
构建方式
- -
- {assistantTypeOptions.map((option) => { - const selected = draftType === option.type; - - return ( - - ); - })} -
-
- -
- {createError && ( - {createError} - )} - +
+ {createError && ( + {createError} + )} +
-
+ ); } diff --git a/frontend/src/components/pages/HistoryPage.tsx b/frontend/src/components/pages/HistoryPage.tsx index 85ddf34..3a45310 100644 --- a/frontend/src/components/pages/HistoryPage.tsx +++ b/frontend/src/components/pages/HistoryPage.tsx @@ -183,7 +183,7 @@ export function HistoryPage() { clearSkipRowClick(); } }, - [clearSkipRowClick, currentPage, detail?.id, load, rows.length], + [clearSkipRowClick, currentPage, detail, load, rows.length], ); const handleRowClick = useCallback( diff --git a/frontend/src/components/workflow/WorkflowCanvas.tsx b/frontend/src/components/workflow/WorkflowCanvas.tsx index ebe6f5f..834b7ee 100644 --- a/frontend/src/components/workflow/WorkflowCanvas.tsx +++ b/frontend/src/components/workflow/WorkflowCanvas.tsx @@ -8,6 +8,7 @@ import { Controls, type Edge, MarkerType, + MiniMap, type Node, type NodeChange, type OnConnectEnd, @@ -561,7 +562,7 @@ export function WorkflowCanvas({
-
+
+
diff --git a/frontend/src/components/workflow/WorkflowEditor.tsx b/frontend/src/components/workflow/WorkflowEditor.tsx index 662d8a6..436d599 100644 --- a/frontend/src/components/workflow/WorkflowEditor.tsx +++ b/frontend/src/components/workflow/WorkflowEditor.tsx @@ -23,7 +23,7 @@ export function WorkflowEditor(props: WorkflowEditorProps) { if (loading) { return ( -
+
正在加载节点目录…
@@ -31,7 +31,7 @@ export function WorkflowEditor(props: WorkflowEditorProps) { } if (error) { return ( -
+
加载节点目录失败:{error}
);