diff --git a/frontend/src/features/knowledges/knowledge-header.tsx b/frontend/src/features/knowledges/knowledge-header.tsx index 514597d2..5e2e5f0e 100644 --- a/frontend/src/features/knowledges/knowledge-header.tsx +++ b/frontend/src/features/knowledges/knowledge-header.tsx @@ -1,14 +1,14 @@ import type { ReactNode } from 'react'; -import { Ellipsis, LibraryBig, Loader2, Pencil, Trash } from 'lucide-react'; -import { useCallback, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { ChevronLeft, ChevronRight, Ellipsis, LibraryBig, Loader2, Pencil, Trash } from 'lucide-react'; +import { useCallback, useMemo, useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import { toast } from 'sonner'; import type { KnowledgeDocumentFragmentFragment } from '@/graphql/types'; import ConfirmationDialog from '@/components/shared/confirmation-dialog'; -import { DetailNavigationToolbar } from '@/components/shared/detail-navigation'; +import { DetailNavigationSheet, DetailNavigationToolbar, useNavigation } from '@/components/shared/detail-navigation'; import { InlineEditInput, useInlineEdit } from '@/components/shared/inline-edit'; import { Badge } from '@/components/ui/badge'; import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage } from '@/components/ui/breadcrumb'; @@ -23,6 +23,8 @@ import { import { Separator } from '@/components/ui/separator'; import { SidebarTrigger } from '@/components/ui/sidebar'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { useBreakpoint } from '@/hooks/use-breakpoint'; +import { mergeHrefWithSearchParams } from '@/lib/url-params'; import { type Knowledge, useKnowledges } from '@/providers/knowledges-provider'; import { useKnowledgeDetailNavigation } from './use-knowledge-detail-navigation'; @@ -42,6 +44,8 @@ interface KnowledgeHeaderProps { export const KnowledgeHeader = ({ isNew, knowledge, onBeforeNavigateAway, saveButton }: KnowledgeHeaderProps) => { const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const { isMobile } = useBreakpoint(); const { deleteKnowledge, updateKnowledge } = useKnowledges(); const [isRenaming, setIsRenaming] = useState(false); const [isDeleting, setIsDeleting] = useState(false); @@ -51,6 +55,56 @@ export const KnowledgeHeader = ({ isNew, knowledge, onBeforeNavigateAway, saveBu const { toolbarProps: knowledgeToolbarProps } = useKnowledgeDetailNavigation(knowledgeId); + // Mirror what `` computes internally so the + // mobile menu items share the same filtered subset as the desktop toolbar. + const mobileNav = useNavigation({ + currentId: knowledgeToolbarProps.currentId, + getId: knowledgeToolbarProps.getId, + getSearchableText: knowledgeToolbarProps.getSearchableText ?? knowledgeToolbarProps.getLabel, + items: knowledgeToolbarProps.items, + query: knowledgeToolbarProps.filter, + }); + const [isMobileNavSheetOpen, setIsMobileNavSheetOpen] = useState(false); + + const mobileNavGoTo = useCallback( + (id: null | string) => { + if (!id) { + return; + } + + const target = mobileNav.filteredItems.find( + (item) => String(knowledgeToolbarProps.getId(item)) === id, + ); + + if (!target) { + return; + } + + navigate(mergeHrefWithSearchParams(knowledgeToolbarProps.getHref(target), searchParams), { + replace: true, + }); + }, + [knowledgeToolbarProps, mobileNav.filteredItems, navigate, searchParams], + ); + + const mobileNavSelectItem = useCallback( + (item: Knowledge) => { + setIsMobileNavSheetOpen(false); + navigate(mergeHrefWithSearchParams(knowledgeToolbarProps.getHref(item), searchParams), { + replace: true, + }); + }, + [knowledgeToolbarProps, navigate, searchParams], + ); + + const mobilePositionLabel = useMemo( + () => + mobileNav.total === 0 || mobileNav.currentIndex === -1 + ? `–/${mobileNav.total}` + : `${mobileNav.currentIndex + 1}/${mobileNav.total}`, + [mobileNav.currentIndex, mobileNav.total], + ); + // Title source-of-truth is the server-side `question`. We intentionally do // not read it from the form draft below — the inline rename flow in this // header writes through `updateKnowledge`, which refreshes `knowledge` via @@ -121,47 +175,49 @@ export const KnowledgeHeader = ({ isNew, knowledge, onBeforeNavigateAway, saveBu return ( <>
- - - - - - - {isEditingTitle && canShowActions ? ( - - ) : canShowActions ? ( - - - - {knowledgeName ?? 'Knowledge'} - - - Double-click to rename - - ) : ( - - {isNew ? 'New knowledge' : (knowledgeName ?? 'Knowledge')} - - )} - - - -
- {canShowActions && ( +
+ + + + + + + {isEditingTitle && canShowActions ? ( + + ) : canShowActions ? ( + + + + {knowledgeName ?? 'Knowledge'} + + + Double-click to rename + + ) : ( + + {isNew ? 'New knowledge' : (knowledgeName ?? 'Knowledge')} + + )} + + + +
+
+ {canShowActions && !isMobile && ( {...knowledgeToolbarProps} renderItem={(item, isCurrent) => ( @@ -199,6 +255,49 @@ export const KnowledgeHeader = ({ isNew, knowledge, onBeforeNavigateAway, saveBu className="min-w-24" onCloseAutoFocus={handleDropdownCloseAutoFocus} > + {isMobile && mobileNav.total > 0 && ( + <> + event.preventDefault()} + > + + Knowledges +
+ + + +
+
+ + + )} Rename @@ -225,6 +324,32 @@ export const KnowledgeHeader = ({ isNew, knowledge, onBeforeNavigateAway, saveBu )}
+ {isMobile && canShowActions && ( + + currentId={knowledgeToolbarProps.currentId} + currentIndex={mobileNav.currentIndex} + getId={knowledgeToolbarProps.getId} + getLabel={knowledgeToolbarProps.getLabel} + items={mobileNav.filteredItems} + onItemSelect={mobileNavSelectItem} + onOpenChange={setIsMobileNavSheetOpen} + open={isMobileNavSheetOpen} + renderItem={(item, isCurrent) => ( + <> + + {item.docType} + + {item.question} + + )} + sheetIcon={} + sheetTitle="Knowledges" + total={mobileNav.total} + /> + )} { const pageHeader = (
-
- +
+ - - - - - Flows + + + + + Flows
-
+
} label="New Flow" diff --git a/frontend/src/pages/flows/new-flow.tsx b/frontend/src/pages/flows/new-flow.tsx index 22d9fff6..5edc46a6 100644 --- a/frontend/src/pages/flows/new-flow.tsx +++ b/frontend/src/pages/flows/new-flow.tsx @@ -48,15 +48,15 @@ const NewFlow = () => { return ( <>
- + - - - - New flow + + + + New flow diff --git a/frontend/src/pages/knowledges/knowledges.tsx b/frontend/src/pages/knowledges/knowledges.tsx index 552c2dc3..8ceb9286 100644 --- a/frontend/src/pages/knowledges/knowledges.tsx +++ b/frontend/src/pages/knowledges/knowledges.tsx @@ -365,22 +365,22 @@ const Knowledges = () => { const pageHeader = (
-
- +
+ - - - - - Knowledges + + + + + Knowledges
-
+
} label="New Knowledge" diff --git a/frontend/src/pages/resources/resources.tsx b/frontend/src/pages/resources/resources.tsx index aa14b002..5cd266aa 100644 --- a/frontend/src/pages/resources/resources.tsx +++ b/frontend/src/pages/resources/resources.tsx @@ -383,22 +383,22 @@ const Resources = () => { const pageHeader = (
-
- +
+ - - - - - Resources + + + + + Resources
-
+
} diff --git a/frontend/src/pages/settings/settings-api-tokens.tsx b/frontend/src/pages/settings/settings-api-tokens.tsx index 999aa986..9a5c0523 100644 --- a/frontend/src/pages/settings/settings-api-tokens.tsx +++ b/frontend/src/pages/settings/settings-api-tokens.tsx @@ -142,8 +142,8 @@ const copyToClipboard = async (text: string): Promise => { const SettingsAPITokensHeader = ({ onCreateClick }: { onCreateClick: () => void }) => { return (
-
-

Manage API tokens for programmatic access

+
+

Manage API tokens for programmatic access

diff --git a/frontend/src/pages/templates/template.tsx b/frontend/src/pages/templates/template.tsx index 53f229da..c62a1c2c 100644 --- a/frontend/src/pages/templates/template.tsx +++ b/frontend/src/pages/templates/template.tsx @@ -1,6 +1,8 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { ChevronDown, + ChevronLeft, + ChevronRight, Ellipsis, FileSymlink, FileText, @@ -13,12 +15,12 @@ import { } from 'lucide-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useForm } from 'react-hook-form'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { toast } from 'sonner'; import { z } from 'zod'; import ConfirmationDialog from '@/components/shared/confirmation-dialog'; -import { DetailNavigationToolbar } from '@/components/shared/detail-navigation'; +import { DetailNavigationSheet, DetailNavigationToolbar, useNavigation } from '@/components/shared/detail-navigation'; import { InlineEditInput, useInlineEdit } from '@/components/shared/inline-edit'; import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage } from '@/components/ui/breadcrumb'; import { Button } from '@/components/ui/button'; @@ -42,6 +44,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip import { useTemplateDetailNavigation } from '@/features/templates/use-template-detail-navigation'; import { useFlowTemplateQuery } from '@/graphql/types'; import { useBreakpoint } from '@/hooks/use-breakpoint'; +import { mergeHrefWithSearchParams } from '@/lib/url-params'; import { cn } from '@/lib/utils'; import { type Template, useTemplates } from '@/providers/templates-provider'; @@ -231,6 +234,7 @@ Action plan: const Template = () => { const navigate = useNavigate(); + const [searchParams] = useSearchParams(); const { templateId } = useParams<{ templateId?: string }>(); const { createTemplate, deleteTemplate, updateTemplate } = useTemplates(); @@ -242,6 +246,56 @@ const Template = () => { // below by `canShowActions`). const { toolbarProps: templateToolbarProps } = useTemplateDetailNavigation(isNew ? null : templateId); + // Mirror what `` computes internally so the + // mobile menu items share the same filtered subset as the desktop toolbar. + const mobileNav = useNavigation