Merge remote-tracking branch 'origin/main' into add-globe-icon-text-html-url

This commit is contained in:
Lucas Oliveira 2025-10-06 14:40:46 -03:00
commit c718e21eea
14 changed files with 571 additions and 135 deletions

View file

@ -2559,12 +2559,12 @@
"zoom": 0.602433700773958
}
},
"description": "OpenRAG Open Search Agent",
"description": "OpenRAG OpenSearch Agent",
"endpoint_name": null,
"id": "1098eea1-6649-4e1d-aed1-b77249fb8dd0",
"is_component": false,
"last_tested_version": "1.6.3.dev0",
"name": "OpenRAG Open Search Agent",
"name": "OpenRAG OpenSearch Agent",
"tags": [
"assistants",
"agents"

View file

@ -2337,12 +2337,12 @@
"zoom": 0.5380793988167256
}
},
"description": "OpenRAG Open Search Nudges generator, based on the Open Search documents and the chat history.",
"description": "OpenRAG OpenSearch Nudges generator, based on the OpenSearch documents and the chat history.",
"endpoint_name": null,
"id": "ebc01d31-1976-46ce-a385-b0240327226c",
"is_component": false,
"last_tested_version": "1.6.0",
"name": "OpenRAG Open Search Nudges",
"name": "OpenRAG OpenSearch Nudges",
"tags": [
"assistants",
"agents"

View file

@ -0,0 +1,134 @@
"use client";
import { AlertTriangle, ExternalLink, Copy } from "lucide-react";
import { useDoclingHealthQuery } from "@/src/app/api/queries/useDoclingHealthQuery";
import { Banner, BannerIcon, BannerTitle, BannerAction } from "@/components/ui/banner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter
} from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
import { useState } from "react";
interface DoclingHealthBannerProps {
className?: string;
}
// DoclingSetupDialog component
interface DoclingSetupDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
className?: string;
}
function DoclingSetupDialog({
open,
onOpenChange,
className
}: DoclingSetupDialogProps) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
await navigator.clipboard.writeText("uv run openrag");
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className={cn("max-w-lg", className)}>
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-base">
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400" />
docling-serve is stopped. Knowledge ingest is unavailable.
</DialogTitle>
<DialogDescription>
Start docling-serve by running:
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="flex items-center gap-2">
<code className="flex-1 bg-muted px-3 py-2.5 rounded-md text-sm font-mono">
uv run openrag
</code>
<Button
variant="ghost"
size="icon"
onClick={handleCopy}
className="shrink-0"
title={copied ? "Copied!" : "Copy to clipboard"}
>
<Copy className="h-4 w-4" />
</Button>
</div>
<DialogDescription>
Then, select <span className="font-semibold text-foreground">Start Native Services</span> in the TUI. Once docling-serve is running, refresh OpenRAG.
</DialogDescription>
</div>
<DialogFooter>
<Button
variant="default"
onClick={() => onOpenChange(false)}
>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export function DoclingHealthBanner({ className }: DoclingHealthBannerProps) {
const { data: health, isLoading, isError } = useDoclingHealthQuery();
const [showDialog, setShowDialog] = useState(false);
const isHealthy = health?.status === "healthy" && !isError;
const isUnhealthy = health?.status === "unhealthy" || isError;
// Only show banner when service is unhealthy
if (isLoading || isHealthy) {
return null;
}
if (isUnhealthy) {
return (
<>
<Banner
className={cn(
"bg-amber-50 text-amber-900 dark:bg-amber-950 dark:text-amber-200 border-amber-200 dark:border-amber-800",
className
)}
>
<BannerIcon
icon={AlertTriangle}
/>
<BannerTitle className="font-medium">
docling-serve native service is stopped. Knowledge ingest is unavailable.
</BannerTitle>
<BannerAction
onClick={() => setShowDialog(true)}
className="bg-foreground text-background hover:bg-primary/90"
>
Setup Docling Serve
<ExternalLink className="h-3 w-3 ml-1" />
</BannerAction>
</Banner>
<DoclingSetupDialog
open={showDialog}
onOpenChange={setShowDialog}
/>
</>
);
}
return null;
}

View file

@ -0,0 +1,141 @@
'use client';
import { useControllableState } from '@radix-ui/react-use-controllable-state';
import { type LucideIcon, XIcon } from 'lucide-react';
import {
type ComponentProps,
createContext,
type HTMLAttributes,
type MouseEventHandler,
useContext,
} from 'react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
type BannerContextProps = {
show: boolean;
setShow: (show: boolean) => void;
};
export const BannerContext = createContext<BannerContextProps>({
show: true,
setShow: () => {},
});
export type BannerProps = HTMLAttributes<HTMLDivElement> & {
visible?: boolean;
defaultVisible?: boolean;
onClose?: () => void;
inset?: boolean;
};
export const Banner = ({
children,
visible,
defaultVisible = true,
onClose,
className,
inset = false,
...props
}: BannerProps) => {
const [show, setShow] = useControllableState({
defaultProp: defaultVisible,
prop: visible,
onChange: onClose,
});
if (!show) {
return null;
}
return (
<BannerContext.Provider value={{ show, setShow }}>
<div
className={cn(
'flex w-full items-center justify-between gap-2 bg-primary px-4 py-2 text-primary-foreground',
inset && 'rounded-lg',
className
)}
{...props}
>
{children}
</div>
</BannerContext.Provider>
);
};
export type BannerIconProps = HTMLAttributes<HTMLDivElement> & {
icon: LucideIcon;
};
export const BannerIcon = ({
icon: Icon,
className,
...props
}: BannerIconProps) => (
<div
className={cn(
'p-1',
className
)}
{...props}
>
<Icon size={16} />
</div>
);
export type BannerTitleProps = HTMLAttributes<HTMLParagraphElement>;
export const BannerTitle = ({ className, ...props }: BannerTitleProps) => (
<p className={cn('flex-1 text-sm', className)} {...props} />
);
export type BannerActionProps = ComponentProps<typeof Button>;
export const BannerAction = ({
variant = 'outline',
size = 'sm',
className,
...props
}: BannerActionProps) => (
<Button
className={cn(
'shrink-0 bg-transparent hover:bg-background/10 hover:text-background',
className
)}
size={size}
variant={variant}
{...props}
/>
);
export type BannerCloseProps = ComponentProps<typeof Button>;
export const BannerClose = ({
variant = 'ghost',
size = 'icon',
onClick,
className,
...props
}: BannerCloseProps) => {
const { setShow } = useContext(BannerContext);
const handleClick: MouseEventHandler<HTMLButtonElement> = (e) => {
setShow(false);
onClick?.(e);
};
return (
<Button
className={cn(
'shrink-0 bg-transparent hover:bg-background/10 hover:text-background',
className
)}
onClick={handleClick}
size={size}
variant={variant}
{...props}
>
<XIcon size={18} />
</Button>
);
};

View file

@ -0,0 +1,55 @@
import {
type UseQueryOptions,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
export interface DoclingHealthResponse {
status: "healthy" | "unhealthy";
message?: string;
}
export const useDoclingHealthQuery = (
options?: Omit<UseQueryOptions<DoclingHealthResponse>, "queryKey" | "queryFn">,
) => {
const queryClient = useQueryClient();
async function checkDoclingHealth(): Promise<DoclingHealthResponse> {
try {
const response = await fetch("http://127.0.0.1:5001/health", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (response.ok) {
return { status: "healthy" };
} else {
return {
status: "unhealthy",
message: `Health check failed with status: ${response.status}`,
};
}
} catch (error) {
return {
status: "unhealthy",
message: error instanceof Error ? error.message : "Connection failed",
};
}
}
const queryResult = useQuery(
{
queryKey: ["docling-health"],
queryFn: checkDoclingHealth,
retry: 1,
refetchInterval: 30000, // Check every 30 seconds
staleTime: 25000, // Consider data stale after 25 seconds
...options,
},
queryClient,
);
return queryResult;
};

View file

@ -29,6 +29,7 @@ export interface ChunkResult {
owner_email?: string;
file_size?: number;
connector_type?: string;
index?: number;
}
export interface File {
@ -55,7 +56,7 @@ export interface File {
export const useGetSearchQuery = (
query: string,
queryData?: ParsedQueryData | null,
options?: Omit<UseQueryOptions, "queryKey" | "queryFn">,
options?: Omit<UseQueryOptions, "queryKey" | "queryFn">
) => {
const queryClient = useQueryClient();
@ -184,7 +185,7 @@ export const useGetSearchQuery = (
queryFn: getFiles,
...options,
},
queryClient,
queryClient
);
return queryResult;

View file

@ -31,6 +31,7 @@ import {
import { useAuth } from "@/contexts/auth-context";
import { type EndpointType, useChat } from "@/contexts/chat-context";
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
import { useLayout } from "@/contexts/layout-context";
import { useTask } from "@/contexts/task-context";
import { useLoadingStore } from "@/stores/loadingStore";
import { useGetNudgesQuery } from "../api/queries/useGetNudgesQuery";
@ -151,6 +152,7 @@ function ChatPage() {
const streamIdRef = useRef(0);
const lastLoadedConversationRef = useRef<string | null>(null);
const { addTask, isMenuOpen } = useTask();
const { totalTopOffset } = useLayout();
const { selectedFilter, parsedFilterData, isPanelOpen, setSelectedFilter } =
useKnowledgeFilter();
@ -2046,7 +2048,7 @@ function ChatPage() {
return (
<div
className={`fixed inset-0 md:left-72 top-[53px] flex flex-col transition-all duration-300 ${
className={`fixed inset-0 md:left-72 flex flex-col transition-all duration-300 ${
isMenuOpen && isPanelOpen
? "md:right-[704px]" // Both open: 384px (menu) + 320px (KF panel)
: isMenuOpen
@ -2055,6 +2057,7 @@ function ChatPage() {
? "md:right-80" // Only KF panel open: 320px
: "md:right-6" // Neither open: 24px
}`}
style={{ top: `${totalTopOffset}px` }}
>
{/* Debug header - only show in debug mode */}
{isDebugMode && (

View file

@ -1,20 +1,21 @@
"use client";
import { ArrowLeft, Check, Copy, Loader2, Search } from "lucide-react";
import { ArrowLeft, Check, Copy, Loader2, Search, X } from "lucide-react";
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { ProtectedRoute } from "@/components/protected-route";
import { Button } from "@/components/ui/button";
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
import { useLayout } from "@/contexts/layout-context";
import { useTask } from "@/contexts/task-context";
import {
type ChunkResult,
type File,
useGetSearchQuery,
} from "../../api/queries/useGetSearchQuery";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
// import { Label } from "@/components/ui/label";
// import { Checkbox } from "@/components/ui/checkbox";
import { filterAccentClasses } from "@/components/knowledge-filter-panel";
const getFileTypeLabel = (mimetype: string) => {
if (mimetype === "application/pdf") return "PDF";
@ -26,8 +27,10 @@ const getFileTypeLabel = (mimetype: string) => {
function ChunksPageContent() {
const router = useRouter();
const searchParams = useSearchParams();
const { selectedFilter, setSelectedFilter, parsedFilterData, isPanelOpen } =
useKnowledgeFilter();
const { isMenuOpen } = useTask();
const { parsedFilterData, isPanelOpen } = useKnowledgeFilter();
const { totalTopOffset } = useLayout();
const filename = searchParams.get("filename");
const [chunks, setChunks] = useState<ChunkResult[]>([]);
@ -85,7 +88,9 @@ function ChunksPageContent() {
return;
}
setChunks(fileData?.chunks || []);
setChunks(
fileData?.chunks?.map((chunk, i) => ({ ...chunk, index: i + 1 })) || []
);
}, [data, filename]);
// Set selected state for all checkboxes when selectAll changes
@ -101,20 +106,20 @@ function ChunksPageContent() {
router.push("/knowledge");
}, [router]);
const handleChunkCardCheckboxChange = useCallback(
(index: number) => {
setSelectedChunks((prevSelected) => {
const newSelected = new Set(prevSelected);
if (newSelected.has(index)) {
newSelected.delete(index);
} else {
newSelected.add(index);
}
return newSelected;
});
},
[setSelectedChunks]
);
// const handleChunkCardCheckboxChange = useCallback(
// (index: number) => {
// setSelectedChunks((prevSelected) => {
// const newSelected = new Set(prevSelected);
// if (newSelected.has(index)) {
// newSelected.delete(index);
// } else {
// newSelected.add(index);
// }
// return newSelected;
// });
// },
// [setSelectedChunks]
// );
if (!filename) {
return (
@ -132,7 +137,7 @@ function ChunksPageContent() {
return (
<div
className={`fixed inset-0 md:left-72 top-[53px] flex flex-row transition-all duration-300 ${
className={`fixed inset-0 md:left-72 flex flex-row transition-all duration-300 ${
isMenuOpen && isPanelOpen
? "md:right-[704px]"
: // Both open: 384px (menu) + 320px (KF panel)
@ -144,6 +149,7 @@ function ChunksPageContent() {
: // Only KF panel open: 320px
"md:right-6" // Neither open: 24px
}`}
style={{ top: `${totalTopOffset}px` }}
>
<div className="flex-1 flex flex-col min-h-0 px-6 py-6">
{/* Header */}
@ -158,19 +164,38 @@ function ChunksPageContent() {
</h1>
</div>
<div className="flex flex-col items-start mt-2">
<div className="flex-1 flex items-center gap-2 w-full max-w-[616px] mb-8">
<Input
name="search-query"
icon={!queryInputText.length ? <Search size={18} /> : null}
id="search-query"
type="text"
defaultValue={parsedFilterData?.query}
value={queryInputText}
onChange={(e) => setQueryInputText(e.target.value)}
placeholder="Search chunks..."
/>
<div className="flex-1 flex items-center gap-2 w-full max-w-[640px]">
<div className="primary-input min-h-10 !flex items-center flex-nowrap focus-within:border-foreground transition-colors !p-[0.3rem]">
{selectedFilter?.name && (
<div
className={`flex items-center gap-1 h-full px-1.5 py-0.5 mr-1 rounded max-w-[25%] ${
filterAccentClasses[parsedFilterData?.color || "zinc"]
}`}
>
<span className="truncate">{selectedFilter?.name}</span>
<X
aria-label="Remove filter"
className="h-4 w-4 flex-shrink-0 cursor-pointer"
onClick={() => setSelectedFilter(null)}
/>
</div>
)}
<Search
className="h-4 w-4 ml-1 flex-shrink-0 text-placeholder-foreground"
strokeWidth={1.5}
/>
<input
className="bg-transparent w-full h-full ml-2 focus:outline-none focus-visible:outline-none font-mono placeholder:font-mono"
name="search-query"
id="search-query"
type="text"
placeholder="Enter your search query..."
onChange={(e) => setQueryInputText(e.target.value)}
value={queryInputText}
/>
</div>
</div>
<div className="flex items-center pl-4 gap-2">
{/* <div className="flex items-center pl-4 gap-2">
<Checkbox
id="selectAllChunks"
checked={selectAll}
@ -184,7 +209,7 @@ function ChunksPageContent() {
>
Select all
</Label>
</div>
</div> */}
</div>
</div>
@ -202,10 +227,9 @@ function ChunksPageContent() {
) : chunks.length === 0 ? (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<Search className="h-12 w-12 mx-auto mb-4 text-muted-foreground/50" />
<p className="text-lg text-muted-foreground">No chunks found</p>
<p className="text-sm text-muted-foreground/70 mt-2">
This file may not have been indexed yet
<p className="text-xl font-semibold mb-2">No knowledge</p>
<p className="text-sm text-secondary-foreground">
Clear the knowledge filter or return to the knowledge page
</p>
</div>
</div>
@ -218,16 +242,16 @@ function ChunksPageContent() {
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<div>
{/* <div>
<Checkbox
checked={selectedChunks.has(index)}
onCheckedChange={() =>
handleChunkCardCheckboxChange(index)
}
/>
</div>
</div> */}
<span className="text-sm font-bold">
Chunk {chunk.page}
Chunk {chunk.index}
</span>
<span className="bg-background p-1 rounded text-xs text-muted-foreground/70">
{chunk.text.length} chars
@ -247,6 +271,10 @@ function ChunksPageContent() {
</div>
</div>
<span className="bg-background p-1 rounded text-xs text-muted-foreground/70">
{chunk.score.toFixed(2)} score
</span>
{/* TODO: Update to use active toggle */}
{/* <span className="px-2 py-1 text-green-500">
<Switch
@ -256,7 +284,7 @@ function ChunksPageContent() {
Active
</span> */}
</div>
<blockquote className="text-sm text-muted-foreground leading-relaxed border-l-2 border-input ml-1.5 pl-4">
<blockquote className="text-sm text-muted-foreground leading-relaxed ml-1.5">
{chunk.text}
</blockquote>
</div>
@ -266,24 +294,29 @@ function ChunksPageContent() {
</div>
</div>
{/* Right panel - Summary (TODO), Technical details, */}
<div className="w-[320px] py-20 px-2">
<div className="mb-8">
<h2 className="text-xl font-semibold mt-3 mb-4">Technical details</h2>
<dl>
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0 mb-2.5">
<dt className="text-sm/6 text-muted-foreground">Total chunks</dt>
<dd className="mt-1 text-sm/6 text-gray-100 sm:col-span-2 sm:mt-0">
{chunks.length}
</dd>
</div>
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0 mb-2.5">
<dt className="text-sm/6 text-muted-foreground">Avg length</dt>
<dd className="mt-1 text-sm/6 text-gray-100 sm:col-span-2 sm:mt-0">
{averageChunkLength.toFixed(0)} chars
</dd>
</div>
{/* TODO: Uncomment after data is available */}
{/* <div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0 mb-2.5">
{chunks.length > 0 && (
<div className="w-[320px] py-20 px-2">
<div className="mb-8">
<h2 className="text-xl font-semibold mt-3 mb-4">
Technical details
</h2>
<dl>
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0 mb-2.5">
<dt className="text-sm/6 text-muted-foreground">
Total chunks
</dt>
<dd className="mt-1 text-sm/6 text-gray-100 sm:col-span-2 sm:mt-0">
{chunks.length}
</dd>
</div>
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0 mb-2.5">
<dt className="text-sm/6 text-muted-foreground">Avg length</dt>
<dd className="mt-1 text-sm/6 text-gray-100 sm:col-span-2 sm:mt-0">
{averageChunkLength.toFixed(0)} chars
</dd>
</div>
{/* TODO: Uncomment after data is available */}
{/* <div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0 mb-2.5">
<dt className="text-sm/6 text-muted-foreground">Process time</dt>
<dd className="mt-1 text-sm/6 text-gray-100 sm:col-span-2 sm:mt-0">
</dd>
@ -293,51 +326,54 @@ function ChunksPageContent() {
<dd className="mt-1 text-sm/6 text-gray-100 sm:col-span-2 sm:mt-0">
</dd>
</div> */}
</dl>
</div>
<div className="mb-8">
<h2 className="text-xl font-semibold mt-2 mb-3">Original document</h2>
<dl>
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0 mb-2.5">
</dl>
</div>
<div className="mb-8">
<h2 className="text-xl font-semibold mt-2 mb-3">
Original document
</h2>
<dl>
{/* <div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0 mb-2.5">
<dt className="text-sm/6 text-muted-foreground">Name</dt>
<dd className="mt-1 text-sm/6 text-gray-100 sm:col-span-2 sm:mt-0">
{fileData?.filename}
</dd>
</div>
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0 mb-2.5">
<dt className="text-sm/6 text-muted-foreground">Type</dt>
<dd className="mt-1 text-sm/6 text-gray-100 sm:col-span-2 sm:mt-0">
{fileData ? getFileTypeLabel(fileData.mimetype) : "Unknown"}
</dd>
</div>
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0 mb-2.5">
<dt className="text-sm/6 text-muted-foreground">Size</dt>
<dd className="mt-1 text-sm/6 text-gray-100 sm:col-span-2 sm:mt-0">
{fileData?.size
? `${Math.round(fileData.size / 1024)} KB`
: "Unknown"}
</dd>
</div>
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0 mb-2.5">
</div> */}
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0 mb-2.5">
<dt className="text-sm/6 text-muted-foreground">Type</dt>
<dd className="mt-1 text-sm/6 text-gray-100 sm:col-span-2 sm:mt-0">
{fileData ? getFileTypeLabel(fileData.mimetype) : "Unknown"}
</dd>
</div>
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0 mb-2.5">
<dt className="text-sm/6 text-muted-foreground">Size</dt>
<dd className="mt-1 text-sm/6 text-gray-100 sm:col-span-2 sm:mt-0">
{fileData?.size
? `${Math.round(fileData.size / 1024)} KB`
: "Unknown"}
</dd>
</div>
{/* <div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0 mb-2.5">
<dt className="text-sm/6 text-muted-foreground">Uploaded</dt>
<dd className="mt-1 text-sm/6 text-gray-100 sm:col-span-2 sm:mt-0">
N/A
</dd>
</div>
{/* TODO: Uncomment after data is available */}
{/* <div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0 mb-2.5">
</div> */}
{/* TODO: Uncomment after data is available */}
{/* <div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0 mb-2.5">
<dt className="text-sm/6 text-muted-foreground">Source</dt>
<dd className="mt-1 text-sm/6 text-gray-100 sm:col-span-2 sm:mt-0"></dd>
</div> */}
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0 mb-2.5">
{/* <div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0 mb-2.5">
<dt className="text-sm/6 text-muted-foreground">Updated</dt>
<dd className="mt-1 text-sm/6 text-gray-100 sm:col-span-2 sm:mt-0">
N/A
</dd>
</div>
</dl>
</div> */}
</dl>
</div>
</div>
</div>
)}
</div>
);
}

View file

@ -19,6 +19,7 @@ import { KnowledgeDropdown } from "@/components/knowledge-dropdown";
import { ProtectedRoute } from "@/components/protected-route";
import { Button } from "@/components/ui/button";
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
import { useLayout } from "@/contexts/layout-context";
import { useTask } from "@/contexts/task-context";
import { type File, useGetSearchQuery } from "../api/queries/useGetSearchQuery";
import "@/components/AgGrid/registerAgGridModules";
@ -60,12 +61,13 @@ function getSourceIcon(connectorType?: string) {
}
function SearchPage() {
const router = useRouter();
const { isMenuOpen, files: taskFiles } = useTask();
const { selectedFilter, setSelectedFilter, parsedFilterData, isPanelOpen } =
useKnowledgeFilter();
const [selectedRows, setSelectedRows] = useState<File[]>([]);
const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false);
const router = useRouter();
const { isMenuOpen, files: taskFiles } = useTask();
const { totalTopOffset } = useLayout();
const { selectedFilter, setSelectedFilter, parsedFilterData, isPanelOpen } =
useKnowledgeFilter();
const [selectedRows, setSelectedRows] = useState<File[]>([]);
const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false);
const deleteDocumentMutation = useDeleteDocument();
@ -243,26 +245,27 @@ function SearchPage() {
}
};
return (
<div
className={`fixed inset-0 md:left-72 top-[53px] flex flex-col transition-all duration-300 ${
isMenuOpen && isPanelOpen
? "md:right-[704px]"
: // Both open: 384px (menu) + 320px (KF panel)
isMenuOpen
? "md:right-96"
: // Only menu open: 384px
isPanelOpen
? "md:right-80"
: // Only KF panel open: 320px
"md:right-6" // Neither open: 24px
}`}
>
<div className="flex-1 flex flex-col min-h-0 px-6 py-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold">Project Knowledge</h2>
<KnowledgeDropdown variant="button" />
</div>
return (
<div
className={`fixed inset-0 md:left-72 flex flex-col transition-all duration-300 ${
isMenuOpen && isPanelOpen
? "md:right-[704px]"
: // Both open: 384px (menu) + 320px (KF panel)
isMenuOpen
? "md:right-96"
: // Only menu open: 384px
isPanelOpen
? "md:right-80"
: // Only KF panel open: 320px
"md:right-6" // Neither open: 24px
}`}
style={{ top: `${totalTopOffset}px` }}
>
<div className="flex-1 flex flex-col min-h-0 px-6 py-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold">Project Knowledge</h2>
<KnowledgeDropdown variant="button" />
</div>
{/* Search Input Area */}
<div className="flex-shrink-0 mb-6 xl:max-w-[75%]">

View file

@ -149,7 +149,7 @@ function KnowledgeSourcesPage() {
const [systemPrompt, setSystemPrompt] = useState<string>("");
const [chunkSize, setChunkSize] = useState<number>(1024);
const [chunkOverlap, setChunkOverlap] = useState<number>(50);
const [tableStructure, setTableStructure] = useState<boolean>(false);
const [tableStructure, setTableStructure] = useState<boolean>(true);
const [ocr, setOcr] = useState<boolean>(false);
const [pictureDescriptions, setPictureDescriptions] =
useState<boolean>(false);

View file

@ -7,6 +7,7 @@ import {
type ChatConversation,
} from "@/app/api/queries/useGetConversationsQuery";
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
import { DoclingHealthBanner } from "@/components/docling-health-banner";
import { KnowledgeFilterPanel } from "@/components/knowledge-filter-panel";
import Logo from "@/components/logo/logo";
import { Navigation } from "@/components/navigation";
@ -16,9 +17,11 @@ import { UserNav } from "@/components/user-nav";
import { useAuth } from "@/contexts/auth-context";
import { useChat } from "@/contexts/chat-context";
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
import { LayoutProvider } from "@/contexts/layout-context";
// import { GitHubStarButton } from "@/components/github-star-button"
// import { DiscordLink } from "@/components/discord-link"
import { useTask } from "@/contexts/task-context";
import { useDoclingHealthQuery } from "@/src/app/api/queries/useDoclingHealthQuery";
import { cn } from "@/lib/utils";
export function LayoutWrapper({ children }: { children: React.ReactNode }) {
@ -35,6 +38,11 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
const { isLoading: isSettingsLoading, data: settings } = useGetSettingsQuery({
enabled: isAuthenticated || isNoAuthMode,
});
const {
data: health,
isLoading: isHealthLoading,
isError,
} = useDoclingHealthQuery();
// Only fetch conversations on chat page
const isOnChatPage = pathname === "/" || pathname === "/chat";
@ -64,6 +72,17 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
task.status === "processing"
);
const isUnhealthy = health?.status === "unhealthy" || isError;
const isBannerVisible = !isHealthLoading && isUnhealthy;
// Dynamic height calculations based on banner visibility
const headerHeight = 53;
const bannerHeight = 52; // Approximate banner height
const totalTopOffset = isBannerVisible
? headerHeight + bannerHeight
: headerHeight;
const mainContentHeight = `calc(100vh - ${totalTopOffset}px)`;
// Show loading state when backend isn't ready
if (isLoading || isSettingsLoading) {
return (
@ -76,7 +95,7 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
);
}
if (isAuthPage || (settings && !settings.edited)) {
if (isAuthPage) {
// For auth pages, render without navigation
return <div className="h-full">{children}</div>;
}
@ -84,6 +103,7 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
// For all other pages, render with Langflow-styled navigation and task menu
return (
<div className="h-full relative">
<DoclingHealthBanner className="w-full pt-2" />
<header className="header-arrangement bg-background sticky top-0 z-50 h-10">
<div className="header-start-display px-[16px]">
{/* Logo/Title */}
@ -124,7 +144,10 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
</div>
</div>
</header>
<div className="side-bar-arrangement bg-background fixed left-0 top-[40px] bottom-0 md:flex hidden pt-1">
<div
className="side-bar-arrangement bg-background fixed left-0 top-[40px] bottom-0 md:flex hidden pt-1"
style={{ top: `${totalTopOffset}px` }}
>
<Navigation
conversations={conversations}
isConversationsLoading={isConversationsLoading}
@ -132,7 +155,7 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
/>
</div>
<main
className={`md:pl-72 transition-all duration-300 overflow-y-auto h-[calc(100vh-53px)] ${
className={`md:pl-72 transition-all duration-300 overflow-y-auto ${
isMenuOpen && isPanelOpen
? "md:pr-[728px]"
: // Both open: 384px (menu) + 320px (KF panel) + 24px (original padding)
@ -144,15 +167,21 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
: // Only KF panel open: 320px
"md:pr-0" // Neither open: 24px
}`}
style={{ height: mainContentHeight }}
>
<div
className={cn(
"py-6 lg:py-8 px-4 lg:px-6",
isSmallWidthPath ? "max-w-[850px]" : "container"
)}
<LayoutProvider
headerHeight={headerHeight}
totalTopOffset={totalTopOffset}
>
{children}
</div>
<div
className={cn(
"py-6 lg:py-8 px-4 lg:px-6",
isSmallWidthPath ? "max-w-[850px]" : "container"
)}
>
{children}
</div>
</LayoutProvider>
</main>
<TaskNotificationMenu />
<KnowledgeFilterPanel />

View file

@ -0,0 +1,34 @@
"use client";
import { createContext, useContext } from "react";
interface LayoutContextType {
headerHeight: number;
totalTopOffset: number;
}
const LayoutContext = createContext<LayoutContextType | undefined>(undefined);
export function useLayout() {
const context = useContext(LayoutContext);
if (context === undefined) {
throw new Error("useLayout must be used within a LayoutProvider");
}
return context;
}
export function LayoutProvider({
children,
headerHeight,
totalTopOffset
}: {
children: React.ReactNode;
headerHeight: number;
totalTopOffset: number;
}) {
return (
<LayoutContext.Provider value={{ headerHeight, totalTopOffset }}>
{children}
</LayoutContext.Provider>
);
}

View file

@ -12,7 +12,7 @@ export const DEFAULT_AGENT_SETTINGS = {
export const DEFAULT_KNOWLEDGE_SETTINGS = {
chunk_size: 1000,
chunk_overlap: 200,
table_structure: false,
table_structure: true,
ocr: false,
picture_descriptions: false
} as const;

View file

@ -27,7 +27,7 @@ class KnowledgeConfig:
embedding_model: str = "text-embedding-3-small"
chunk_size: int = 1000
chunk_overlap: int = 200
table_structure: bool = False
table_structure: bool = True
ocr: bool = False
picture_descriptions: bool = False