From f2fb2228073f58fe3d5117357d1fdb6e18a49935 Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Tue, 19 May 2026 00:21:39 +0700 Subject: [PATCH] refactor(frontend): collapse PageTitle into DocumentTitle via route handles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single mechanism for the document . Every route now declares its title in the react-router handle: • handle: { title: 'Dashboard' } — static listing pages • handle: { title: (params) => formatPromptId(params.promptId) } — params-derived (e.g. /settings/prompts/:promptId) • handle: { titleComponent: TemplateTitle } — Apollo cache-driven, reactive to cache updates after the page mounts A single <DocumentTitle/> in RootLayout walks useMatches() and picks the deepest match. PageTitle is gone — 13 call sites across 13 page components migrated to handle, the component deleted. Other improvements: - Split document-title.tsx so the shell-only <DocumentTitle/> has zero graphql imports — the resource title components (FlowTitle, KnowledgeTitle, ProviderTitle, TemplateTitle) live in resource-titles.tsx. This lets the unit tests render DocumentTitle without dragging the codegenerated graphql/types.ts through Vite. - String(p.id) === providerId replaces sloppy == in ProviderTitle. - matches.findLast(...) replaces the imperative for-loop. - Extracted formatPromptId into lib/utils/format-prompt-id.ts so the prompt detail page and the route handle resolver share one formatter. 7 unit tests added for DocumentTitle covering: static title, derived title from params, deepest-match-wins, titleComponent rendering, titleComponent precedence over static title, empty-string fallback, and no-handle fallback. 19 test files / 482 tests pass; lint + tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- frontend/src/app.tsx | 26 ++- .../components/shared/document-title.test.tsx | 172 ++++++++++++++++++ .../src/components/shared/document-title.tsx | 138 +++++--------- frontend/src/components/shared/page-title.tsx | 23 --- .../src/components/shared/resource-titles.tsx | 80 ++++++++ frontend/src/lib/utils/format-prompt-id.ts | 7 + frontend/src/pages/dashboard/dashboard.tsx | 2 - frontend/src/pages/flows/flow-report.tsx | 4 - frontend/src/pages/flows/flow.tsx | 2 +- frontend/src/pages/flows/flows.tsx | 4 - frontend/src/pages/flows/new-flow.tsx | 2 - frontend/src/pages/knowledges/knowledges.tsx | 4 - frontend/src/pages/login.tsx | 2 - frontend/src/pages/oauth-result.tsx | 2 - frontend/src/pages/resources/resources.tsx | 2 - .../pages/settings/settings-api-tokens.tsx | 5 - .../src/pages/settings/settings-prompt.tsx | 15 +- .../src/pages/settings/settings-prompts.tsx | 5 - .../src/pages/settings/settings-providers.tsx | 5 - frontend/src/pages/templates/templates.tsx | 3 - 20 files changed, 330 insertions(+), 173 deletions(-) create mode 100644 frontend/src/components/shared/document-title.test.tsx delete mode 100644 frontend/src/components/shared/page-title.tsx create mode 100644 frontend/src/components/shared/resource-titles.tsx create mode 100644 frontend/src/lib/utils/format-prompt-id.ts diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx index 6ba78225..4d2da73b 100644 --- a/frontend/src/app.tsx +++ b/frontend/src/app.tsx @@ -15,16 +15,12 @@ import MainLayout from '@/components/layouts/main-layout'; import SettingsLayout from '@/components/layouts/settings-layout'; import ProtectedRoute from '@/components/routes/protected-route'; import PublicRoute from '@/components/routes/public-route'; -import { - DocumentTitle, - FlowTitle, - KnowledgeTitle, - ProviderTitle, - TemplateTitle, -} from '@/components/shared/document-title'; +import { DocumentTitle } from '@/components/shared/document-title'; import PageLoader from '@/components/shared/page-loader'; +import { FlowTitle, KnowledgeTitle, ProviderTitle, TemplateTitle } from '@/components/shared/resource-titles'; import { Toaster } from '@/components/ui/sonner'; import client from '@/lib/apollo'; +import { formatPromptId } from '@/lib/utils/format-prompt-id'; import { FavoritesProvider } from '@/providers/favorites-provider'; import { FlowProvider } from '@/providers/flow-provider'; import { KnowledgesProvider } from '@/providers/knowledges-provider'; @@ -138,6 +134,7 @@ const router = createBrowserRouter( <Route element={<MainLayout />}> <Route element={<Dashboard />} + handle={{ title: 'Dashboard' }} path="dashboard" /> @@ -145,10 +142,12 @@ const router = createBrowserRouter( <Route element={<FlowsLayout />}> <Route element={<Flows />} + handle={{ title: 'Flows' }} path="flows" /> <Route element={<NewFlow />} + handle={{ title: 'New flow' }} path="flows/new" /> <Route @@ -160,6 +159,7 @@ const router = createBrowserRouter( <Route element={<Templates />} + handle={{ title: 'Templates' }} path="templates" /> <Route @@ -171,6 +171,7 @@ const router = createBrowserRouter( <Route element={<KnowledgesLayout />}> <Route element={<Knowledges />} + handle={{ title: 'Knowledges' }} path="knowledges" /> <Route @@ -182,6 +183,7 @@ const router = createBrowserRouter( <Route element={<Resources />} + handle={{ title: 'Resources' }} path="resources" /> </Route> @@ -202,6 +204,7 @@ const router = createBrowserRouter( /> <Route element={<SettingsProviders />} + handle={{ title: 'Providers' }} path="providers" /> <Route @@ -211,14 +214,20 @@ const router = createBrowserRouter( /> <Route element={<SettingsPrompts />} + handle={{ title: 'Prompts' }} path="prompts" /> <Route element={<SettingsPrompt />} + handle={{ + title: (params: Record<string, string | undefined>) => + params.promptId ? formatPromptId(params.promptId) : 'Prompt', + }} path="prompts/:promptId" /> <Route element={<SettingsAPITokens />} + handle={{ title: 'API tokens' }} path="api-tokens" /> {/* Catch-all route for unknown settings paths */} @@ -237,17 +246,20 @@ const router = createBrowserRouter( {/* report routes */} <Route element={<ProtectedReportLayout />} + handle={{ title: 'Flow report' }} path="flows/:flowId/report" /> {/* public routes */} <Route element={<PublicLoginLayout />} + handle={{ title: 'Login' }} path="login" /> <Route element={<OAuthResult />} + handle={{ title: 'OAuth' }} path="oauth/result" /> diff --git a/frontend/src/components/shared/document-title.test.tsx b/frontend/src/components/shared/document-title.test.tsx new file mode 100644 index 00000000..cffe906c --- /dev/null +++ b/frontend/src/components/shared/document-title.test.tsx @@ -0,0 +1,172 @@ +import { render, waitFor } from '@testing-library/react'; +import { createMemoryRouter, Outlet, RouterProvider } from 'react-router-dom'; +import { describe, expect, it } from 'vitest'; + +import { DocumentTitle } from './document-title'; + +const renderAt = (initialPath: string, routes: Parameters<typeof createMemoryRouter>[0]) => { + const router = createMemoryRouter(routes, { initialEntries: [initialPath] }); + + return render(<RouterProvider router={router} />); +}; + +describe('DocumentTitle', () => { + it('renders APP_NAME when no matched route exposes a title handle', async () => { + renderAt('/anywhere', [ + { + // No child route handle — DocumentTitle should fall back to APP_NAME only. + children: [{ element: <span>page</span>, path: 'anywhere' }], + element: ( + <> + <DocumentTitle /> + <Outlet /> + </> + ), + path: '/', + }, + ]); + + await waitFor(() => expect(document.title).toBe('PentAGI')); + }); + + it('renders a static title from handle', async () => { + renderAt('/dashboard', [ + { + children: [{ element: <span>page</span>, handle: { title: 'Dashboard' }, path: 'dashboard' }], + element: ( + <> + <DocumentTitle /> + <Outlet /> + </> + ), + path: '/', + }, + ]); + + await waitFor(() => expect(document.title).toBe('Dashboard — PentAGI')); + }); + + it('renders a derived title from a handle.title function reading params', async () => { + renderAt('/prompts/agentSelector', [ + { + children: [ + { + element: <span>page</span>, + handle: { + title: ({ promptId }: Record<string, string | undefined>) => + promptId + ? promptId.replaceAll(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase()) + : 'Prompt', + }, + path: 'prompts/:promptId', + }, + ], + element: ( + <> + <DocumentTitle /> + <Outlet /> + </> + ), + path: '/', + }, + ]); + + await waitFor(() => expect(document.title).toBe('Agent Selector — PentAGI')); + }); + + it('renders the deepest matching handle (child wins over parent)', async () => { + renderAt('/settings/api-tokens', [ + { + children: [ + { + children: [ + { element: <span>tokens</span>, handle: { title: 'API tokens' }, path: 'api-tokens' }, + ], + element: <Outlet />, + handle: { title: 'Settings' }, + path: 'settings', + }, + ], + element: ( + <> + <DocumentTitle /> + <Outlet /> + </> + ), + path: '/', + }, + ]); + + await waitFor(() => expect(document.title).toBe('API tokens — PentAGI')); + }); + + it('renders a title from handle.titleComponent', async () => { + function CustomTitle({ params }: { params: Record<string, string | undefined> }) { + // React 19 only hoists <title> when the child is a single string + // (not text + interpolation + text), so compute the string ahead. + return <title>{`Custom #${params.id} — PentAGI`}; + } + + renderAt('/items/42', [ + { + children: [{ element: page, handle: { titleComponent: CustomTitle }, path: 'items/:id' }], + element: ( + <> + + + + ), + path: '/', + }, + ]); + + await waitFor(() => expect(document.title).toBe('Custom #42 — PentAGI')); + }); + + it('prefers titleComponent over static title when both are present on the same handle', async () => { + function CustomTitle() { + return From component — PentAGI; + } + + renderAt('/x', [ + { + children: [ + { + element: page, + handle: { title: 'From static', titleComponent: CustomTitle }, + path: 'x', + }, + ], + element: ( + <> + + + + ), + path: '/', + }, + ]); + + await waitFor(() => expect(document.title).toBe('From component — PentAGI')); + }); + + it('falls back to APP_NAME when handle.title returns an empty string', async () => { + renderAt('/x', [ + { + children: [{ element: page, handle: { title: '' }, path: 'x' }], + element: ( + <> + + + + ), + path: '/', + }, + ]); + + // An empty string from the resolver is treated as "no title" — fall back + // to APP_NAME alone. This guards the route-level convention: pages that + // do not want a prefix can return '' instead of omitting the handle. + await waitFor(() => expect(document.title).toBe('PentAGI')); + }); +}); diff --git a/frontend/src/components/shared/document-title.tsx b/frontend/src/components/shared/document-title.tsx index e9996726..891a61b7 100644 --- a/frontend/src/components/shared/document-title.tsx +++ b/frontend/src/components/shared/document-title.tsx @@ -2,124 +2,82 @@ import type { ComponentType } from 'react'; import { useMatches } from 'react-router-dom'; -import { - useFlowQuery, - useFlowTemplateQuery, - useKnowledgeDocumentQuery, - useSettingsProvidersQuery, -} from '@/graphql/types'; - const APP_NAME = 'PentAGI'; export interface RouteHandleWithTitle { - titleComponent: TitleComponent; + /** + * Static string or a pure function of route params. Use for routes + * where the title is known synchronously — listing pages, /new routes, + * params-derived titles like `/settings/prompts/:promptId`. + */ + title?: TitleResolver; + /** + * Component that renders `` and may subscribe to Apollo cache or + * other reactive sources. Use when the title depends on resource data + * that needs to react to cache updates after the page mounts. + */ + titleComponent?: TitleComponent; } type RouteParams = Record<string, string | undefined>; type TitleComponent = ComponentType<{ params: RouteParams }>; -const hasTitleComponent = (handle: unknown): handle is RouteHandleWithTitle => +type TitleResolver = ((params: RouteParams) => string) | string; + +const hasTitle = (handle: unknown): handle is { title: TitleResolver } => { + if (typeof handle !== 'object' || handle === null || !('title' in handle)) { + return false; + } + + const value = (handle as { title: unknown }).title; + + return typeof value === 'string' || typeof value === 'function'; +}; + +const hasTitleComponent = (handle: unknown): handle is { titleComponent: TitleComponent } => typeof handle === 'object' && handle !== null && 'titleComponent' in handle && - typeof (handle as RouteHandleWithTitle).titleComponent === 'function'; + typeof (handle as { titleComponent: unknown }).titleComponent === 'function'; const renderTitle = (label: null | string) => <title>{label ? `${label} — ${APP_NAME}` : APP_NAME}; /** - * Renders the document `` driven by react-router route handles. Walks - * matches deepest-first; the first match exposing `handle.titleComponent` - * wins. Lives in the app shell so it survives navigation between sibling - * detail routes — that's what fixes the previous "Provider — PentAGI" - * flash when DetailNavigation switches between siblings and the destination - * page unmounts/remounts during data fetch. + * Renders the document `<title>` driven by react-router route handles. The + * deepest match exposing `handle.title` or `handle.titleComponent` wins. * - * Routes without a `titleComponent` keep using `<PageTitle>` inside the page - * component — the migration is route-by-route. + * - `handle.title: string` — static (e.g. "Dashboard"). + * - `handle.title: (params) => string` — derived from route params + * (e.g. `/settings/prompts/:promptId` formatting the id). + * - `handle.titleComponent` — reactive component that subscribes to Apollo + * cache for resource-driven titles that need to react to cache updates. + * + * Living in the app shell, this component survives navigation between + * sibling detail routes — fixing the previous "Provider — PentAGI" flash + * during DetailNavigation prev/next. */ export function DocumentTitle() { const matches = useMatches(); + const match = matches.findLast((m) => hasTitleComponent(m.handle) || hasTitle(m.handle)); - for (let i = matches.length - 1; i >= 0; i--) { - const handle = matches[i].handle; - - if (hasTitleComponent(handle)) { - const TitleComponent = handle.titleComponent; - - return <TitleComponent params={matches[i].params} />; - } + if (!match) { + return renderTitle(null); } - return null; -} + const handle = match.handle; -// Per-resource title components read the same Apollo cache the destination -// page is about to populate. `fetchPolicy: 'cache-only'` avoids a duplicate -// HTTP request — the page's own query fills the cache, this subscription -// reacts to it. The variables shape mirrors the page so cache lookups hit. + if (hasTitleComponent(handle)) { + const TitleComponent = handle.titleComponent; -export function FlowTitle({ params }: { params: RouteParams }) { - const flowId = params.flowId; - const { data } = useFlowQuery({ - fetchPolicy: 'cache-only', - skip: !flowId, - variables: flowId ? { id: flowId } : undefined, - }); - - const flowTitle = data?.flow?.title; - - return renderTitle(flowTitle && flowId ? `Flow #${flowId} — ${flowTitle}` : 'Flow'); -} - -export function KnowledgeTitle({ params }: { params: RouteParams }) { - const knowledgeId = params.knowledgeId; - const isNew = knowledgeId === 'new'; - const { data } = useKnowledgeDocumentQuery({ - fetchPolicy: 'cache-only', - skip: isNew || !knowledgeId, - variables: !isNew && knowledgeId ? { id: knowledgeId } : undefined, - }); - - if (isNew) { - return renderTitle('New knowledge'); + return <TitleComponent params={match.params} />; } - return renderTitle(data?.knowledgeDocument?.question || 'Knowledge'); -} + if (hasTitle(handle)) { + const resolved = typeof handle.title === 'function' ? handle.title(match.params) : handle.title; -export function ProviderTitle({ params }: { params: RouteParams }) { - const providerId = params.providerId; - const isNew = providerId === 'new'; - const { data } = useSettingsProvidersQuery({ - fetchPolicy: 'cache-only', - skip: isNew, - }); - - if (isNew) { - return renderTitle('New provider'); + return renderTitle(resolved); } - // Provider detail isn't a separate query — `settingsProviders` returns the - // full list and the page filters by id. We do the same here so a single - // cache entry serves both. - const provider = data?.settingsProviders.userDefined?.find((candidate) => candidate.id == providerId); - - return renderTitle(provider?.name || 'Provider'); -} - -export function TemplateTitle({ params }: { params: RouteParams }) { - const templateId = params.templateId; - const isNew = templateId === 'new'; - const { data } = useFlowTemplateQuery({ - fetchPolicy: 'cache-only', - skip: isNew || !templateId, - variables: templateId && !isNew ? { templateId } : undefined, - }); - - if (isNew) { - return renderTitle('New template'); - } - - return renderTitle(data?.flowTemplate?.title || 'Template'); + return renderTitle(null); } diff --git a/frontend/src/components/shared/page-title.tsx b/frontend/src/components/shared/page-title.tsx deleted file mode 100644 index 4c41ca82..00000000 --- a/frontend/src/components/shared/page-title.tsx +++ /dev/null @@ -1,23 +0,0 @@ -const APP_NAME = 'PentAGI'; - -interface PageTitleProps { - /** - * Page-specific prefix. Skipped when nullish/empty so loading states can - * render `<PageTitle>{flow?.title}</PageTitle>` without temporarily - * setting the tab title to "— PentAGI". - */ - children?: null | string; -} - -/** - * Renders a `<title>` element that React 19 hoists into <head> automatically. - * - * Detail routes (templates/:id, knowledges/:id, providers/:id, flows/:id) - * use route handles via `<DocumentTitle/>` instead — that lets the shell - * own the title and survives navigation between sibling documents. This - * helper is for listing/static pages where in-route flicker isn't a - * concern. - */ -export function PageTitle({ children }: PageTitleProps) { - return <title>{children ? `${children} — ${APP_NAME}` : APP_NAME}; -} diff --git a/frontend/src/components/shared/resource-titles.tsx b/frontend/src/components/shared/resource-titles.tsx new file mode 100644 index 00000000..5f3efee6 --- /dev/null +++ b/frontend/src/components/shared/resource-titles.tsx @@ -0,0 +1,80 @@ +import { + useFlowQuery, + useFlowTemplateQuery, + useKnowledgeDocumentQuery, + useSettingsProvidersQuery, +} from '@/graphql/types'; + +const APP_NAME = 'PentAGI'; + +type RouteParams = Record; + +const renderTitle = (label: null | string) => {label ? `${label} — ${APP_NAME}` : APP_NAME}; + +// Reactive title components used by detail routes via `handle.titleComponent`. +// Each uses `fetchPolicy: 'cache-only'` so it subscribes to the Apollo cache +// without issuing a duplicate HTTP request — the destination page's own +// query fills the cache. The variables shape mirrors the page so cache +// lookups hit. + +export function FlowTitle({ params }: { params: RouteParams }) { + const flowId = params.flowId; + const { data } = useFlowQuery({ + fetchPolicy: 'cache-only', + skip: !flowId, + variables: flowId ? { id: flowId } : undefined, + }); + + const flowTitle = data?.flow?.title; + + return renderTitle(flowTitle && flowId ? `Flow #${flowId} — ${flowTitle}` : 'Flow'); +} + +export function KnowledgeTitle({ params }: { params: RouteParams }) { + const knowledgeId = params.knowledgeId; + const isNew = knowledgeId === 'new'; + const { data } = useKnowledgeDocumentQuery({ + fetchPolicy: 'cache-only', + skip: isNew || !knowledgeId, + variables: !isNew && knowledgeId ? { id: knowledgeId } : undefined, + }); + + if (isNew) { + return renderTitle('New knowledge'); + } + + return renderTitle(data?.knowledgeDocument?.question || 'Knowledge'); +} + +export function ProviderTitle({ params }: { params: RouteParams }) { + const providerId = params.providerId; + const isNew = providerId === 'new'; + const { data } = useSettingsProvidersQuery({ + fetchPolicy: 'cache-only', + skip: isNew, + }); + + if (isNew) { + return renderTitle('New provider'); + } + + const provider = data?.settingsProviders.userDefined?.find((candidate) => String(candidate.id) === providerId); + + return renderTitle(provider?.name || 'Provider'); +} + +export function TemplateTitle({ params }: { params: RouteParams }) { + const templateId = params.templateId; + const isNew = templateId === 'new'; + const { data } = useFlowTemplateQuery({ + fetchPolicy: 'cache-only', + skip: isNew || !templateId, + variables: templateId && !isNew ? { templateId } : undefined, + }); + + if (isNew) { + return renderTitle('New template'); + } + + return renderTitle(data?.flowTemplate?.title || 'Template'); +} diff --git a/frontend/src/lib/utils/format-prompt-id.ts b/frontend/src/lib/utils/format-prompt-id.ts new file mode 100644 index 00000000..3c0d3f36 --- /dev/null +++ b/frontend/src/lib/utils/format-prompt-id.ts @@ -0,0 +1,7 @@ +/** + * Converts a camelCase prompt key (e.g. "agentSelector") into a display + * label ("Agent Selector"). Used in the prompt detail page and route + * handle title to keep the two in sync. + */ +export const formatPromptId = (key: string): string => + key.replaceAll(/([A-Z])/g, ' $1').replace(/^./, (str) => str.toUpperCase()); diff --git a/frontend/src/pages/dashboard/dashboard.tsx b/frontend/src/pages/dashboard/dashboard.tsx index ee590ab9..92d1cf41 100644 --- a/frontend/src/pages/dashboard/dashboard.tsx +++ b/frontend/src/pages/dashboard/dashboard.tsx @@ -1,7 +1,6 @@ import { LayoutDashboard } from 'lucide-react'; import { useState, useTransition } from 'react'; -import { PageTitle } from '@/components/shared/page-title'; import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage } from '@/components/ui/breadcrumb'; import { Separator } from '@/components/ui/separator'; import { SidebarTrigger } from '@/components/ui/sidebar'; @@ -75,7 +74,6 @@ function Dashboard() { return ( <> - Dashboard
diff --git a/frontend/src/pages/flows/flow-report.tsx b/frontend/src/pages/flows/flow-report.tsx index 217c4108..af053fa9 100644 --- a/frontend/src/pages/flows/flow-report.tsx +++ b/frontend/src/pages/flows/flow-report.tsx @@ -3,7 +3,6 @@ import { useParams, useSearchParams } from 'react-router-dom'; import Logo from '@/components/icons/logo'; import Markdown from '@/components/shared/markdown'; -import { PageTitle } from '@/components/shared/page-title'; import { useFlowReportQuery } from '@/graphql/types'; import { Log } from '@/lib/log'; import { generateFileName, generatePDFFromMarkdown, generateReport } from '@/lib/report'; @@ -94,7 +93,6 @@ function FlowReport() { if (state === 'loading' || state === 'generating') { return (
- Flow report
@@ -116,7 +114,6 @@ function FlowReport() { if (state === 'error') { return (
- Flow report
@@ -138,7 +135,6 @@ function FlowReport() { return (
- Flow report
diff --git a/frontend/src/pages/flows/flow.tsx b/frontend/src/pages/flows/flow.tsx index 1d6159d9..e635904f 100644 --- a/frontend/src/pages/flows/flow.tsx +++ b/frontend/src/pages/flows/flow.tsx @@ -122,7 +122,7 @@ function Flow() { } // Drop the new title into the optimistic state immediately so the - // breadcrumb/tab/PageTitle flip before the network round-trip. The + // breadcrumb and document title flip before the network round-trip. The // optimistic value lives only inside this transition — once the // mutation settles, useOptimistic falls back to the Apollo cache // (which the mutation response has already updated on success, or diff --git a/frontend/src/pages/flows/flows.tsx b/frontend/src/pages/flows/flows.tsx index 600865fc..32d71678 100644 --- a/frontend/src/pages/flows/flows.tsx +++ b/frontend/src/pages/flows/flows.tsx @@ -13,7 +13,6 @@ import { ProviderIcon } from '@/components/icons/provider-icon'; import ConfirmationDialog from '@/components/shared/confirmation-dialog'; import { HeaderButton } from '@/components/shared/header-button'; import { InlineEditInput } from '@/components/shared/inline-edit'; -import { PageTitle } from '@/components/shared/page-title'; import { Badge } from '@/components/ui/badge'; import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage } from '@/components/ui/breadcrumb'; import { Button } from '@/components/ui/button'; @@ -629,7 +628,6 @@ function Flows() { if (isLoading) { return ( <> - Flows {pageHeader}
- Flows {pageHeader}
- Flows {pageHeader}
diff --git a/frontend/src/pages/flows/new-flow.tsx b/frontend/src/pages/flows/new-flow.tsx index 5777c51a..bc5f10ee 100644 --- a/frontend/src/pages/flows/new-flow.tsx +++ b/frontend/src/pages/flows/new-flow.tsx @@ -1,7 +1,6 @@ import { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { PageTitle } from '@/components/shared/page-title'; import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage } from '@/components/ui/breadcrumb'; import { Card, CardContent } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; @@ -48,7 +47,6 @@ function NewFlow() { return ( <> - New flow
- Knowledges {pageHeader}
- Knowledges {pageHeader}
- Knowledges {pageHeader}
- Login
{!isLoading ? ( diff --git a/frontend/src/pages/oauth-result.tsx b/frontend/src/pages/oauth-result.tsx index 7248c56d..89ff5537 100644 --- a/frontend/src/pages/oauth-result.tsx +++ b/frontend/src/pages/oauth-result.tsx @@ -1,7 +1,6 @@ import { useEffect, useLayoutEffect, useRef, useState } from 'react'; import Logo from '@/components/icons/logo'; -import { PageTitle } from '@/components/shared/page-title'; function OAuthResult() { const [statusMessage, setStatusMessage] = useState('Authentication in progress...'); @@ -111,7 +110,6 @@ function OAuthResult() { return (
- OAuth
{statusMessage}
diff --git a/frontend/src/pages/resources/resources.tsx b/frontend/src/pages/resources/resources.tsx index 00a049bb..28d74225 100644 --- a/frontend/src/pages/resources/resources.tsx +++ b/frontend/src/pages/resources/resources.tsx @@ -25,7 +25,6 @@ import { } from '@/components/shared/file-manager'; import { HeaderButton } from '@/components/shared/header-button'; import { OverwriteDialog, useOverwrite } from '@/components/shared/overwrite'; -import { PageTitle } from '@/components/shared/page-title'; import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage } from '@/components/ui/breadcrumb'; import { Button } from '@/components/ui/button'; import { @@ -449,7 +448,6 @@ function Resources() { return ( <> - Resources {pageHeader}
- API tokens - API tokens @@ -894,7 +891,6 @@ function SettingsAPITokens() { if (tokens.length === 0 && !creatingToken) { return (
- API tokens - API tokens {(createError || updateError || deleteError || deleteErrorMessage) && ( diff --git a/frontend/src/pages/settings/settings-prompt.tsx b/frontend/src/pages/settings/settings-prompt.tsx index 06b3e729..67a99520 100644 --- a/frontend/src/pages/settings/settings-prompt.tsx +++ b/frontend/src/pages/settings/settings-prompt.tsx @@ -21,7 +21,6 @@ import { z } from 'zod'; import type { AgentPrompt, AgentPrompts, DefaultPrompt, PromptType } from '@/graphql/types'; import ConfirmationDialog from '@/components/shared/confirmation-dialog'; -import { PageTitle } from '@/components/shared/page-title'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; @@ -38,6 +37,7 @@ import { useValidatePromptMutation, } from '@/graphql/types'; import { cn } from '@/lib/utils'; +import { formatPromptId } from '@/lib/utils/format-prompt-id'; // Form schemas for each tab const systemFormSchema = z.object({ @@ -95,11 +95,6 @@ function FormTextareaItem({ className, control, disabled, label, name, placehold ); } -// Helper function to format display name -const formatName = (key: string): string => { - return key.replaceAll(/([A-Z])/g, ' $1').replace(/^./, (str) => str.toUpperCase()); -}; - // Helper function to extract used variables from template const getUsedVariables = (template: string | undefined): Set => { const usedVariables = new Set(); @@ -328,7 +323,7 @@ function SettingsPrompt() { data: agentData, defaultHumanTemplate: (agentData as AgentPrompts)?.human?.template || '', defaultSystemTemplate: agentData?.system?.template || '', - displayName: formatName(promptId), + displayName: formatPromptId(promptId), hasHuman: !!(agentData as AgentPrompts)?.human, humanTemplate: userHumanPrompt?.template || (agentData as AgentPrompts)?.human?.template || '', systemTemplate: userSystemPrompt?.template || agentData?.system?.template || '', @@ -348,7 +343,7 @@ function SettingsPrompt() { data: toolData, defaultHumanTemplate: '', defaultSystemTemplate: toolData?.template || '', - displayName: formatName(promptId), + displayName: formatPromptId(promptId), hasHuman: false, humanTemplate: '', systemTemplate: userToolPrompt?.template || toolData?.template || '', @@ -591,7 +586,6 @@ function SettingsPrompt() { if (loading) { return ( <> - {promptInfo?.displayName ?? 'Prompt'} } @@ -605,7 +599,6 @@ function SettingsPrompt() { if (error) { return ( <> - {promptInfo?.displayName ?? 'Prompt'} Error loading prompt data @@ -619,7 +612,6 @@ function SettingsPrompt() { if (!promptInfo) { return ( <> - {promptInfo?.displayName ?? 'Prompt'} Prompt not found @@ -719,7 +711,6 @@ function SettingsPrompt() { return (
- {promptInfo.displayName}

{promptInfo.type === 'agent' ? ( diff --git a/frontend/src/pages/settings/settings-prompts.tsx b/frontend/src/pages/settings/settings-prompts.tsx index 3ac12788..29fb140a 100644 --- a/frontend/src/pages/settings/settings-prompts.tsx +++ b/frontend/src/pages/settings/settings-prompts.tsx @@ -21,7 +21,6 @@ import { useNavigate } from 'react-router-dom'; import type { AgentPrompt, AgentPrompts, DefaultPrompt, PromptType } from '@/graphql/types'; import ConfirmationDialog from '@/components/shared/confirmation-dialog'; -import { PageTitle } from '@/components/shared/page-title'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; @@ -798,7 +797,6 @@ function SettingsPrompts() { if (isLoading) { return (
- Prompts - Prompts @@ -829,7 +826,6 @@ function SettingsPrompts() { if (agentPrompts.length === 0 && toolPrompts.length === 0) { return (
- Prompts - Prompts
diff --git a/frontend/src/pages/settings/settings-providers.tsx b/frontend/src/pages/settings/settings-providers.tsx index f47f92f2..b60cc3c3 100644 --- a/frontend/src/pages/settings/settings-providers.tsx +++ b/frontend/src/pages/settings/settings-providers.tsx @@ -19,7 +19,6 @@ import Ollama from '@/components/icons/ollama'; import OpenAi from '@/components/icons/open-ai'; import Qwen from '@/components/icons/qwen'; import ConfirmationDialog from '@/components/shared/confirmation-dialog'; -import { PageTitle } from '@/components/shared/page-title'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; @@ -390,7 +389,6 @@ function SettingsProviders() { if (isLoading) { return (
- Providers - Providers @@ -421,7 +418,6 @@ function SettingsProviders() { if (providers.length === 0) { return (
- Providers - Providers {/* Delete Error Alert */} diff --git a/frontend/src/pages/templates/templates.tsx b/frontend/src/pages/templates/templates.tsx index c67863f3..fe440669 100644 --- a/frontend/src/pages/templates/templates.tsx +++ b/frontend/src/pages/templates/templates.tsx @@ -8,7 +8,6 @@ import { toast } from 'sonner'; import ConfirmationDialog from '@/components/shared/confirmation-dialog'; import { HeaderButton } from '@/components/shared/header-button'; import { InlineEditInput } from '@/components/shared/inline-edit'; -import { PageTitle } from '@/components/shared/page-title'; import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage } from '@/components/ui/breadcrumb'; import { Button } from '@/components/ui/button'; import { ContextMenuItem, ContextMenuSeparator } from '@/components/ui/context-menu'; @@ -274,7 +273,6 @@ function Templates() { if (!templates.length) { return ( <> - Templates {pageHeader}
- Templates {pageHeader}