+ )}
+
{/* Chat Page Specific Sections */}
{isOnChatPage && (
diff --git a/frontend/components/ui/textarea.tsx b/frontend/components/ui/textarea.tsx
index 98f7f86a..e381511e 100644
--- a/frontend/components/ui/textarea.tsx
+++ b/frontend/components/ui/textarea.tsx
@@ -1,18 +1,18 @@
-import * as React from "react"
+import * as React from "react";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
- )
+ );
}
-export { Textarea }
+export { Textarea };
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 58d78031..0b79bb00 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -28,6 +28,7 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/forms": "^0.5.10",
+ "@tailwindcss/line-clamp": "^0.4.4",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-query": "^5.86.0",
"ag-grid-community": "^34.2.0",
@@ -2317,6 +2318,14 @@
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1"
}
},
+ "node_modules/@tailwindcss/line-clamp": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.4.tgz",
+ "integrity": "sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g==",
+ "peerDependencies": {
+ "tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1"
+ }
+ },
"node_modules/@tailwindcss/typography": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index bc9eb72c..d4bc0a4c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -29,6 +29,7 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/forms": "^0.5.10",
+ "@tailwindcss/line-clamp": "^0.4.4",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-query": "^5.86.0",
"ag-grid-community": "^34.2.0",
diff --git a/frontend/src/app/api/[...path]/route.ts b/frontend/src/app/api/[...path]/route.ts
index 2e047b46..7e7a8dbb 100644
--- a/frontend/src/app/api/[...path]/route.ts
+++ b/frontend/src/app/api/[...path]/route.ts
@@ -50,35 +50,61 @@ async function proxyRequest(
try {
let body: string | ArrayBuffer | undefined = undefined;
-
+ let willSendBody = false;
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
const contentType = request.headers.get('content-type') || '';
-
+ const contentLength = request.headers.get('content-length');
+
// For file uploads (multipart/form-data), preserve binary data
if (contentType.includes('multipart/form-data')) {
- body = await request.arrayBuffer();
+ const buf = await request.arrayBuffer();
+ if (buf && buf.byteLength > 0) {
+ body = buf;
+ willSendBody = true;
+ }
} else {
// For JSON and other text-based content, use text
- body = await request.text();
+ const text = await request.text();
+ if (text && text.length > 0) {
+ body = text;
+ willSendBody = true;
+ }
+ }
+
+ // Guard against incorrect non-zero content-length when there is no body
+ if (!willSendBody && contentLength) {
+ // We'll drop content-length/header below
}
}
const headers = new Headers();
-
+
// Copy relevant headers from the original request
for (const [key, value] of request.headers.entries()) {
- if (!key.toLowerCase().startsWith('host') &&
- !key.toLowerCase().startsWith('x-forwarded') &&
- !key.toLowerCase().startsWith('x-real-ip')) {
- headers.set(key, value);
+ const lower = key.toLowerCase();
+ if (
+ lower.startsWith('host') ||
+ lower.startsWith('x-forwarded') ||
+ lower.startsWith('x-real-ip') ||
+ lower === 'content-length' ||
+ (!willSendBody && lower === 'content-type')
+ ) {
+ continue;
}
+ headers.set(key, value);
}
- const response = await fetch(backendUrl, {
+ const init: RequestInit = {
method: request.method,
headers,
- body,
- });
+ };
+ if (willSendBody) {
+ // Convert ArrayBuffer to Uint8Array to satisfy BodyInit in all environments
+ const bodyInit: BodyInit = typeof body === 'string' ? body : new Uint8Array(body as ArrayBuffer);
+ init.body = bodyInit;
+ }
+ const response = await fetch(backendUrl, init);
const responseBody = await response.text();
const responseHeaders = new Headers();
diff --git a/frontend/src/app/api/mutations/useCreateFilter.ts b/frontend/src/app/api/mutations/useCreateFilter.ts
new file mode 100644
index 00000000..6cfd11e0
--- /dev/null
+++ b/frontend/src/app/api/mutations/useCreateFilter.ts
@@ -0,0 +1,50 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { KnowledgeFilter } from "../queries/useGetFiltersSearchQuery";
+
+export interface CreateFilterRequest {
+ name: string;
+ description?: string;
+ queryData: string; // stringified ParsedQueryData
+}
+
+export interface CreateFilterResponse {
+ success: boolean;
+ filter: KnowledgeFilter;
+ message?: string;
+}
+
+async function createFilter(
+ data: CreateFilterRequest,
+): Promise
{
+ const response = await fetch("/api/knowledge-filter", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: data.name,
+ description: data.description ?? "",
+ queryData: data.queryData,
+ }),
+ });
+
+ const json = await response.json().catch(() => ({}));
+
+ if (!response.ok) {
+ const errorMessage = (json && (json.error as string)) || "Failed to create knowledge filter";
+ throw new Error(errorMessage);
+ }
+
+ return json as CreateFilterResponse;
+}
+
+export const useCreateFilter = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: createFilter,
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["knowledge-filters"]});
+ },
+ });
+};
diff --git a/frontend/src/app/api/mutations/useDeleteDocument.ts b/frontend/src/app/api/mutations/useDeleteDocument.ts
index 78985498..47b852b1 100644
--- a/frontend/src/app/api/mutations/useDeleteDocument.ts
+++ b/frontend/src/app/api/mutations/useDeleteDocument.ts
@@ -14,7 +14,7 @@ interface DeleteDocumentResponse {
}
const deleteDocument = async (
- data: DeleteDocumentRequest
+ data: DeleteDocumentRequest,
): Promise => {
const response = await fetch("/api/documents/delete-by-filename", {
method: "POST",
@@ -37,9 +37,11 @@ export const useDeleteDocument = () => {
return useMutation({
mutationFn: deleteDocument,
- onSuccess: () => {
+ onSettled: () => {
// Invalidate and refetch search queries to update the UI
- queryClient.invalidateQueries({ queryKey: ["search"] });
+ setTimeout(() => {
+ queryClient.invalidateQueries({ queryKey: ["search"] });
+ }, 1000);
},
});
};
diff --git a/frontend/src/app/api/mutations/useDeleteFilter.ts b/frontend/src/app/api/mutations/useDeleteFilter.ts
new file mode 100644
index 00000000..2e19ba40
--- /dev/null
+++ b/frontend/src/app/api/mutations/useDeleteFilter.ts
@@ -0,0 +1,39 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+export interface DeleteFilterRequest {
+ id: string;
+}
+
+export interface DeleteFilterResponse {
+ success: boolean;
+ message?: string;
+}
+
+async function deleteFilter(
+ data: DeleteFilterRequest,
+): Promise {
+ const response = await fetch(`/api/knowledge-filter/${data.id}`, {
+ method: "DELETE",
+ });
+
+ const json = await response.json().catch(() => ({}));
+
+ if (!response.ok) {
+ const errorMessage = (json && (json.error as string)) || "Failed to delete knowledge filter";
+ throw new Error(errorMessage);
+ }
+
+ return (json as DeleteFilterResponse) || { success: true };
+}
+
+export const useDeleteFilter = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: deleteFilter,
+ onSuccess: () => {
+ // Invalidate filters queries so UI refreshes automatically
+ queryClient.invalidateQueries({ queryKey: ["knowledge-filters"] });
+ },
+ });
+};
diff --git a/frontend/src/app/api/mutations/useUpdateFilter.ts b/frontend/src/app/api/mutations/useUpdateFilter.ts
new file mode 100644
index 00000000..3e6392ca
--- /dev/null
+++ b/frontend/src/app/api/mutations/useUpdateFilter.ts
@@ -0,0 +1,52 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { KnowledgeFilter } from "../queries/useGetFiltersSearchQuery";
+
+export interface UpdateFilterRequest {
+ id: string;
+ name?: string;
+ description?: string;
+ queryData?: string; // stringified ParsedQueryData
+}
+
+export interface UpdateFilterResponse {
+ success: boolean;
+ filter: KnowledgeFilter;
+ message?: string;
+}
+
+async function updateFilter(data: UpdateFilterRequest): Promise {
+ // Build a body with only provided fields
+ const body: Record = {};
+ if (typeof data.name !== "undefined") body.name = data.name;
+ if (typeof data.description !== "undefined") body.description = data.description;
+ if (typeof data.queryData !== "undefined") body.queryData = data.queryData;
+
+ const response = await fetch(`/api/knowledge-filter/${data.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(body),
+ });
+
+ const json = await response.json().catch(() => ({}));
+
+ if (!response.ok) {
+ const errorMessage = (json && (json.error as string)) || "Failed to update knowledge filter";
+ throw new Error(errorMessage);
+ }
+
+ return json as UpdateFilterResponse;
+}
+
+export const useUpdateFilter = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: updateFilter,
+ onSuccess: () => {
+ // Refresh any knowledge filter lists/searches
+ queryClient.invalidateQueries({ queryKey: ["knowledge-filters"] });
+ },
+ });
+};
diff --git a/frontend/src/app/api/queries/useGetFiltersSearchQuery.ts b/frontend/src/app/api/queries/useGetFiltersSearchQuery.ts
new file mode 100644
index 00000000..b2e75d2c
--- /dev/null
+++ b/frontend/src/app/api/queries/useGetFiltersSearchQuery.ts
@@ -0,0 +1,47 @@
+import {
+ useQuery,
+ useQueryClient,
+ type UseQueryOptions,
+} from "@tanstack/react-query";
+
+export interface KnowledgeFilter {
+ id: string;
+ name: string;
+ description: string;
+ query_data: string;
+ owner: string;
+ created_at: string;
+ updated_at: string;
+}
+
+export const useGetFiltersSearchQuery = (
+ search: string,
+ limit = 20,
+ options?: Omit, "queryKey" | "queryFn">
+) => {
+ const queryClient = useQueryClient();
+
+ async function getFilters(): Promise {
+ const response = await fetch("/api/knowledge-filter/search", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ query: search, limit }),
+ });
+
+ const json = await response.json();
+ if (!response.ok || !json.success) {
+ // ensure we always return a KnowledgeFilter[] to satisfy the return type
+ return [];
+ }
+ return (json.filters || []) as KnowledgeFilter[];
+ }
+
+ return useQuery(
+ {
+ queryKey: ["knowledge-filters", search, limit],
+ queryFn: getFilters,
+ ...options,
+ },
+ queryClient
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/app/api/queries/useGetModelsQuery.ts b/frontend/src/app/api/queries/useGetModelsQuery.ts
index cd24131b..4ce55bd3 100644
--- a/frontend/src/app/api/queries/useGetModelsQuery.ts
+++ b/frontend/src/app/api/queries/useGetModelsQuery.ts
@@ -54,7 +54,7 @@ export const useGetOpenAIModelsQuery = (
queryKey: ["models", "openai", params],
queryFn: getOpenAIModels,
retry: 2,
- enabled: options?.enabled !== false, // Allow enabling/disabling from options
+ enabled: !!params?.apiKey,
staleTime: 0, // Always fetch fresh data
gcTime: 0, // Don't cache results
...options,
diff --git a/frontend/src/app/api/queries/useGetSearchAggregations.ts b/frontend/src/app/api/queries/useGetSearchAggregations.ts
new file mode 100644
index 00000000..fcf65f06
--- /dev/null
+++ b/frontend/src/app/api/queries/useGetSearchAggregations.ts
@@ -0,0 +1,47 @@
+import { useQuery, useQueryClient, type UseQueryOptions } from "@tanstack/react-query";
+
+export interface FacetBucket {
+ key: string;
+ count: number;
+}
+
+export interface SearchAggregations {
+ data_sources?: { buckets: FacetBucket[] };
+ document_types?: { buckets: FacetBucket[] };
+ owners?: { buckets: FacetBucket[] };
+ connector_types?: { buckets: FacetBucket[] };
+}
+
+type Options = Omit, "queryKey" | "queryFn">;
+
+export const useGetSearchAggregations = (
+ query: string,
+ limit: number,
+ scoreThreshold: number,
+ options?: Options
+) => {
+ const queryClient = useQueryClient();
+
+ async function fetchAggregations(): Promise {
+ const response = await fetch("/api/search", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ query, limit, scoreThreshold }),
+ });
+
+ const json = await response.json().catch(() => ({}));
+
+ if (!response.ok) {
+ throw new Error((json && json.error) || "Failed to load search aggregations");
+ }
+
+ return (json.aggregations || {}) as SearchAggregations;
+ }
+
+ return useQuery({
+ queryKey: ["search-aggregations", query, limit, scoreThreshold],
+ queryFn: fetchAggregations,
+ placeholderData: prev => prev,
+ ...options,
+ }, queryClient);
+};
diff --git a/frontend/src/app/api/queries/useGetSearchQuery.ts b/frontend/src/app/api/queries/useGetSearchQuery.ts
index 9928af3d..37798ce5 100644
--- a/frontend/src/app/api/queries/useGetSearchQuery.ts
+++ b/frontend/src/app/api/queries/useGetSearchQuery.ts
@@ -34,21 +34,28 @@ export interface ChunkResult {
export interface File {
filename: string;
mimetype: string;
- chunkCount: number;
- avgScore: number;
+ chunkCount?: number;
+ avgScore?: number;
source_url: string;
- owner: string;
- owner_name: string;
- owner_email: string;
+ owner?: string;
+ owner_name?: string;
+ owner_email?: string;
size: number;
connector_type: string;
- chunks: ChunkResult[];
+ status?:
+ | "processing"
+ | "active"
+ | "unavailable"
+ | "failed"
+ | "hidden"
+ | "sync";
+ chunks?: ChunkResult[];
}
export const useGetSearchQuery = (
query: string,
queryData?: ParsedQueryData | null,
- options?: Omit
+ options?: Omit,
) => {
const queryClient = useQueryClient();
@@ -149,7 +156,7 @@ export const useGetSearchQuery = (
}
});
- const files: File[] = Array.from(fileMap.values()).map(file => ({
+ const files: File[] = Array.from(fileMap.values()).map((file) => ({
filename: file.filename,
mimetype: file.mimetype,
chunkCount: file.chunks.length,
@@ -173,11 +180,11 @@ export const useGetSearchQuery = (
const queryResult = useQuery(
{
queryKey: ["search", effectiveQuery],
- placeholderData: prev => prev,
+ placeholderData: (prev) => prev,
queryFn: getFiles,
...options,
},
- queryClient
+ queryClient,
);
return queryResult;
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css
index 4b7072c1..565966a1 100644
--- a/frontend/src/app/globals.css
+++ b/frontend/src/app/globals.css
@@ -162,7 +162,7 @@
}
.side-bar-arrangement {
- @apply flex h-full w-[14.5rem] flex-col overflow-hidden border-r scrollbar-hide;
+ @apply flex h-full w-[18rem] flex-col overflow-hidden border-r scrollbar-hide;
}
.side-bar-search-div-placement {
diff --git a/frontend/src/app/knowledge/chunks/page.tsx b/frontend/src/app/knowledge/chunks/page.tsx
index 9385c474..cdc9fcc3 100644
--- a/frontend/src/app/knowledge/chunks/page.tsx
+++ b/frontend/src/app/knowledge/chunks/page.tsx
@@ -1,17 +1,14 @@
"use client";
import {
- Building2,
- Cloud,
- FileText,
- HardDrive,
+ ArrowLeft,
+ Copy,
+ File as FileIcon,
Loader2,
Search,
} from "lucide-react";
-import { Suspense, useCallback, useEffect, useState } from "react";
+import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
-import { SiGoogledrive } from "react-icons/si";
-import { TbBrandOnedrive } from "react-icons/tb";
import { ProtectedRoute } from "@/components/protected-route";
import { Button } from "@/components/ui/button";
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
@@ -21,22 +18,16 @@ import {
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";
-// Function to get the appropriate icon for a connector type
-function getSourceIcon(connectorType?: string) {
- switch (connectorType) {
- case "google_drive":
- return ;
- case "onedrive":
- return ;
- case "sharepoint":
- return ;
- case "s3":
- return ;
- default:
- return ;
- }
-}
+const getFileTypeLabel = (mimetype: string) => {
+ if (mimetype === "application/pdf") return "PDF";
+ if (mimetype === "text/plain") return "Text";
+ if (mimetype === "application/msword") return "Word Document";
+ return "Unknown";
+};
function ChunksPageContent() {
const router = useRouter();
@@ -46,10 +37,47 @@ function ChunksPageContent() {
const filename = searchParams.get("filename");
const [chunks, setChunks] = useState([]);
+ const [chunksFilteredByQuery, setChunksFilteredByQuery] = useState<
+ ChunkResult[]
+ >([]);
+ const [selectedChunks, setSelectedChunks] = useState>(new Set());
+
+ // Calculate average chunk length
+ const averageChunkLength = useMemo(
+ () =>
+ chunks.reduce((acc, chunk) => acc + chunk.text.length, 0) /
+ chunks.length || 0,
+ [chunks]
+ );
+
+ const [selectAll, setSelectAll] = useState(false);
+ const [queryInputText, setQueryInputText] = useState(
+ parsedFilterData?.query ?? ""
+ );
// Use the same search query as the knowledge page, but we'll filter for the specific file
const { data = [], isFetching } = useGetSearchQuery("*", parsedFilterData);
+ useEffect(() => {
+ if (queryInputText === "") {
+ setChunksFilteredByQuery(chunks);
+ } else {
+ setChunksFilteredByQuery(
+ chunks.filter((chunk) =>
+ chunk.text.toLowerCase().includes(queryInputText.toLowerCase())
+ )
+ );
+ }
+ }, [queryInputText, chunks]);
+
+ const handleCopy = useCallback((text: string) => {
+ navigator.clipboard.writeText(text);
+ }, []);
+
+ const fileData = (data as File[]).find(
+ (file: File) => file.filename === filename
+ );
+
// Extract chunks for the specific file
useEffect(() => {
if (!filename || !(data as File[]).length) {
@@ -57,16 +85,37 @@ function ChunksPageContent() {
return;
}
- const fileData = (data as File[]).find(
- (file: File) => file.filename === filename
- );
setChunks(fileData?.chunks || []);
}, [data, filename]);
+ // Set selected state for all checkboxes when selectAll changes
+ useEffect(() => {
+ if (selectAll) {
+ setSelectedChunks(new Set(chunks.map((_, index) => index)));
+ } else {
+ setSelectedChunks(new Set());
+ }
+ }, [selectAll, setSelectedChunks, chunks]);
+
const handleBack = useCallback(() => {
- router.back();
+ 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]
+ );
+
if (!filename) {
return (
@@ -83,7 +132,7 @@ function ChunksPageContent() {
return (
{/* Header */}
-
-
-
+ {/* Right panel - Summary (TODO), Technical details, */}
+
+
+
Technical details
+
+
+
- Total chunks
+ -
+ {chunks.length}
+
+
+
+
- Avg length
+ -
+ {averageChunkLength.toFixed(0)} chars
+
+
+ {/* TODO: Uncomment after data is available */}
+ {/*
+
- Process time
+ -
+
+
+
+
- Model
+ -
+
+ */}
+
+
+
+
Original document
+
+
+
- Name
+ -
+ {fileData?.filename}
+
+
+
+
- Type
+ -
+ {fileData ? getFileTypeLabel(fileData.mimetype) : "Unknown"}
+
+
+
+
- Size
+ -
+ {fileData?.size
+ ? `${Math.round(fileData.size / 1024)} KB`
+ : "Unknown"}
+
+
+
+
- Uploaded
+ -
+ N/A
+
+
+ {/* TODO: Uncomment after data is available */}
+ {/*
+
- Source
+
+ */}
+
+
- Updated
+ -
+ N/A
+
+
+
+
+
);
}
diff --git a/frontend/src/app/knowledge/page.tsx b/frontend/src/app/knowledge/page.tsx
index b23acf84..5155f4e2 100644
--- a/frontend/src/app/knowledge/page.tsx
+++ b/frontend/src/app/knowledge/page.tsx
@@ -1,38 +1,25 @@
"use client";
-import {
- Building2,
- Cloud,
- HardDrive,
- Loader2,
- Search,
- Trash2,
-} from "lucide-react";
-import { AgGridReact, CustomCellRendererProps } from "ag-grid-react";
-import {
- type FormEvent,
- useCallback,
- useEffect,
- useState,
- useRef,
-} from "react";
+import type { ColDef } from "ag-grid-community";
+import { AgGridReact, type CustomCellRendererProps } from "ag-grid-react";
+import { Building2, Cloud, HardDrive, Search, Trash2, X } from "lucide-react";
import { useRouter } from "next/navigation";
+import { type ChangeEvent, useCallback, useRef, useState } from "react";
import { SiGoogledrive } from "react-icons/si";
import { TbBrandOnedrive } from "react-icons/tb";
import { KnowledgeDropdown } from "@/components/knowledge-dropdown";
import { ProtectedRoute } from "@/components/protected-route";
import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
import { useTask } from "@/contexts/task-context";
import { type File, useGetSearchQuery } from "../api/queries/useGetSearchQuery";
-import { ColDef } from "ag-grid-community";
import "@/components/AgGrid/registerAgGridModules";
import "@/components/AgGrid/agGridStyles.css";
+import { toast } from "sonner";
import { KnowledgeActionsDropdown } from "@/components/knowledge-actions-dropdown";
+import { StatusBadge } from "@/components/ui/status-badge";
import { DeleteConfirmationDialog } from "../../../components/confirmation-dialog";
import { useDeleteDocument } from "../api/mutations/useDeleteDocument";
-import { toast } from "sonner";
// Function to get the appropriate icon for a connector type
function getSourceIcon(connectorType?: string) {
@@ -58,41 +45,48 @@ function getSourceIcon(connectorType?: string) {
function SearchPage() {
const router = useRouter();
- const { isMenuOpen } = useTask();
- const { parsedFilterData, isPanelOpen } = useKnowledgeFilter();
- const [query, setQuery] = useState("");
- const [queryInputText, setQueryInputText] = useState("");
+ const { isMenuOpen, files: taskFiles } = useTask();
+ const { selectedFilter, setSelectedFilter, parsedFilterData, isPanelOpen } =
+ useKnowledgeFilter();
const [selectedRows, setSelectedRows] = useState
([]);
const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false);
const deleteDocumentMutation = useDeleteDocument();
- const {
- data = [],
- isFetching,
- refetch: refetchSearch,
- } = useGetSearchQuery(query, parsedFilterData);
-
- // Update query when global filter changes
- useEffect(() => {
- if (parsedFilterData?.query) {
- setQueryInputText(parsedFilterData.query);
- }
- }, [parsedFilterData]);
-
- const handleSearch = useCallback(
- (e?: FormEvent) => {
- if (e) e.preventDefault();
- if (query.trim() === queryInputText.trim()) {
- refetchSearch();
- return;
- }
- setQuery(queryInputText);
- },
- [queryInputText, refetchSearch, query]
+ const { data = [], isFetching } = useGetSearchQuery(
+ parsedFilterData?.query || "*",
+ parsedFilterData,
);
- const fileResults = data as File[];
+ const handleTableSearch = (e: ChangeEvent) => {
+ gridRef.current?.api.setGridOption("quickFilterText", e.target.value);
+ };
+
+ // Convert TaskFiles to File format and merge with backend results
+ const taskFilesAsFiles: File[] = taskFiles.map((taskFile) => {
+ return {
+ filename: taskFile.filename,
+ mimetype: taskFile.mimetype,
+ source_url: taskFile.source_url,
+ size: taskFile.size,
+ connector_type: taskFile.connector_type,
+ status: taskFile.status,
+ };
+ });
+
+ const backendFiles = data as File[];
+
+ const filteredTaskFiles = taskFilesAsFiles.filter((taskFile) => {
+ return (
+ taskFile.status !== "active" &&
+ !backendFiles.some(
+ (backendFile) => backendFile.filename === taskFile.filename,
+ )
+ );
+ });
+
+ // Combine task files first, then backend files
+ const fileResults = [...backendFiles, ...filteredTaskFiles];
const gridRef = useRef(null);
@@ -106,13 +100,14 @@ function SearchPage() {
minWidth: 220,
cellRenderer: ({ data, value }: CustomCellRendererProps) => {
return (
- {
router.push(
`/knowledge/chunks?filename=${encodeURIComponent(
- data?.filename ?? ""
- )}`
+ data?.filename ?? "",
+ )}`,
);
}}
>
@@ -120,7 +115,7 @@ function SearchPage() {
{value}
-
+
);
},
},
@@ -143,18 +138,29 @@ function SearchPage() {
{
field: "chunkCount",
headerName: "Chunks",
+ valueFormatter: (params) => params.data?.chunkCount?.toString() || "-",
},
{
field: "avgScore",
headerName: "Avg score",
+ initialFlex: 0.5,
cellRenderer: ({ value }: CustomCellRendererProps) => {
return (
- {value.toFixed(2)}
+ {value?.toFixed(2) ?? "-"}
);
},
},
+ {
+ field: "status",
+ headerName: "Status",
+ cellRenderer: ({ data }: CustomCellRendererProps) => {
+ // Default to 'active' status if no status is provided
+ const status = data?.status || "active";
+ return ;
+ },
+ },
{
cellRenderer: ({ data }: CustomCellRendererProps) => {
return ;
@@ -167,9 +173,8 @@ function SearchPage() {
},
colId: "actions",
filter: false,
- width: 60,
- minWidth: 60,
- maxWidth: 60,
+ minWidth: 0,
+ width: 40,
resizable: false,
sortable: false,
initialFlex: 0,
@@ -196,7 +201,7 @@ function SearchPage() {
try {
// Delete each file individually since the API expects one filename at a time
const deletePromises = selectedRows.map((row) =>
- deleteDocumentMutation.mutateAsync({ filename: row.filename })
+ deleteDocumentMutation.mutateAsync({ filename: row.filename }),
);
await Promise.all(deletePromises);
@@ -204,7 +209,7 @@ function SearchPage() {
toast.success(
`Successfully deleted ${selectedRows.length} document${
selectedRows.length > 1 ? "s" : ""
- }`
+ }`,
);
setSelectedRows([]);
setShowBulkDeleteDialog(false);
@@ -217,7 +222,7 @@ function SearchPage() {
toast.error(
error instanceof Error
? error.message
- : "Failed to delete some documents"
+ : "Failed to delete some documents",
);
}
};
@@ -244,19 +249,29 @@ function SearchPage() {
{/* Search Input Area */}
-
params.data.filename}
+ domLayout="autoHeight"
onSelectionChanged={onSelectionChanged}
- suppressHorizontalScroll={false}
noRowsOverlayComponent={() => (
diff --git a/frontend/src/app/onboarding/page.tsx b/frontend/src/app/onboarding/page.tsx
index bed6a389..c58abfea 100644
--- a/frontend/src/app/onboarding/page.tsx
+++ b/frontend/src/app/onboarding/page.tsx
@@ -1,10 +1,11 @@
"use client";
+import { useRouter } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
import { toast } from "sonner";
import {
- useOnboardingMutation,
- type OnboardingVariables,
+ type OnboardingVariables,
+ useOnboardingMutation,
} from "@/app/api/mutations/useOnboardingMutation";
import IBMLogo from "@/components/logo/ibm-logo";
import OllamaLogo from "@/components/logo/ollama-logo";
@@ -12,198 +13,198 @@ import OpenAILogo from "@/components/logo/openai-logo";
import { ProtectedRoute } from "@/components/protected-route";
import { Button } from "@/components/ui/button";
import {
- Card,
- CardContent,
- CardFooter,
- CardHeader,
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
} from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { useGetSettingsQuery } from "../api/queries/useGetSettingsQuery";
import { IBMOnboarding } from "./components/ibm-onboarding";
import { OllamaOnboarding } from "./components/ollama-onboarding";
import { OpenAIOnboarding } from "./components/openai-onboarding";
-import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from "@/components/ui/tooltip";
-import { useGetSettingsQuery } from "../api/queries/useGetSettingsQuery";
-import { useRouter } from "next/navigation";
function OnboardingPage() {
- const { data: settingsDb, isLoading: isSettingsLoading } =
- useGetSettingsQuery();
+ const { data: settingsDb, isLoading: isSettingsLoading } =
+ useGetSettingsQuery();
- const redirect = "/";
+ const redirect = "/";
- const router = useRouter();
+ const router = useRouter();
- // Redirect if already authenticated or in no-auth mode
- useEffect(() => {
- if (!isSettingsLoading && settingsDb && settingsDb.edited) {
- router.push(redirect);
- }
- }, [isSettingsLoading, redirect]);
+ // Redirect if already authenticated or in no-auth mode
+ useEffect(() => {
+ if (!isSettingsLoading && settingsDb && settingsDb.edited) {
+ router.push(redirect);
+ }
+ }, [isSettingsLoading, settingsDb, router]);
- const [modelProvider, setModelProvider] = useState
("openai");
+ const [modelProvider, setModelProvider] = useState("openai");
- const [sampleDataset, setSampleDataset] = useState(true);
+ const [sampleDataset, setSampleDataset] = useState(true);
- const handleSetModelProvider = (provider: string) => {
- setModelProvider(provider);
- setSettings({
- model_provider: provider,
- embedding_model: "",
- llm_model: "",
- });
- };
+ const handleSetModelProvider = (provider: string) => {
+ setModelProvider(provider);
+ setSettings({
+ model_provider: provider,
+ embedding_model: "",
+ llm_model: "",
+ });
+ };
- const [settings, setSettings] = useState({
- model_provider: modelProvider,
- embedding_model: "",
- llm_model: "",
- });
+ const [settings, setSettings] = useState({
+ model_provider: modelProvider,
+ embedding_model: "",
+ llm_model: "",
+ });
- // Mutations
- const onboardingMutation = useOnboardingMutation({
- onSuccess: (data) => {
- toast.success("Onboarding completed successfully!");
- console.log("Onboarding completed successfully", data);
- },
- onError: (error) => {
- toast.error("Failed to complete onboarding", {
- description: error.message,
- });
- },
- });
+ // Mutations
+ const onboardingMutation = useOnboardingMutation({
+ onSuccess: (data) => {
+ toast.success("Onboarding completed successfully!");
+ console.log("Onboarding completed successfully", data);
+ router.push(redirect);
+ },
+ onError: (error) => {
+ toast.error("Failed to complete onboarding", {
+ description: error.message,
+ });
+ },
+ });
- const handleComplete = () => {
- if (
- !settings.model_provider ||
- !settings.llm_model ||
- !settings.embedding_model
- ) {
- toast.error("Please complete all required fields");
- return;
- }
+ const handleComplete = () => {
+ if (
+ !settings.model_provider ||
+ !settings.llm_model ||
+ !settings.embedding_model
+ ) {
+ toast.error("Please complete all required fields");
+ return;
+ }
- // Prepare onboarding data
- const onboardingData: OnboardingVariables = {
- model_provider: settings.model_provider,
- llm_model: settings.llm_model,
- embedding_model: settings.embedding_model,
- sample_data: sampleDataset,
- };
+ // Prepare onboarding data
+ const onboardingData: OnboardingVariables = {
+ model_provider: settings.model_provider,
+ llm_model: settings.llm_model,
+ embedding_model: settings.embedding_model,
+ sample_data: sampleDataset,
+ };
- // Add API key if available
- if (settings.api_key) {
- onboardingData.api_key = settings.api_key;
- }
+ // Add API key if available
+ if (settings.api_key) {
+ onboardingData.api_key = settings.api_key;
+ }
- // Add endpoint if available
- if (settings.endpoint) {
- onboardingData.endpoint = settings.endpoint;
- }
+ // Add endpoint if available
+ if (settings.endpoint) {
+ onboardingData.endpoint = settings.endpoint;
+ }
- // Add project_id if available
- if (settings.project_id) {
- onboardingData.project_id = settings.project_id;
- }
+ // Add project_id if available
+ if (settings.project_id) {
+ onboardingData.project_id = settings.project_id;
+ }
- onboardingMutation.mutate(onboardingData);
- };
+ onboardingMutation.mutate(onboardingData);
+ };
- const isComplete = !!settings.llm_model && !!settings.embedding_model;
+ const isComplete = !!settings.llm_model && !!settings.embedding_model;
- return (
-
-
-
-
- Configure your models
-
-
[description of task]
-
-
-
-
-
-
-
- OpenAI
-
-
-
- IBM
-
-
-
- Ollama
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Complete
-
-
-
- {!isComplete ? "Please fill in all required fields" : ""}
-
-
-
-
-
-
- );
+ return (
+
+
+
+
+ Configure your models
+
+
[description of task]
+
+
+
+
+
+
+
+ OpenAI
+
+
+
+ IBM
+
+
+
+ Ollama
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Complete
+
+
+
+ {!isComplete ? "Please fill in all required fields" : ""}
+
+
+
+
+
+
+ );
}
export default function ProtectedOnboardingPage() {
- return (
-
- Loading onboarding... }>
-
-
-
- );
+ return (
+
+ Loading onboarding... }>
+
+
+
+ );
}
diff --git a/frontend/src/app/settings/page.tsx b/frontend/src/app/settings/page.tsx
index 50dc7867..f49ff393 100644
--- a/frontend/src/app/settings/page.tsx
+++ b/frontend/src/app/settings/page.tsx
@@ -4,11 +4,13 @@ import { Loader2, PlugZap, RefreshCw } from "lucide-react";
import { useSearchParams } from "next/navigation";
import { Suspense, useCallback, useEffect, useState } from "react";
import { useUpdateFlowSettingMutation } from "@/app/api/mutations/useUpdateFlowSettingMutation";
+import {
+ useGetIBMModelsQuery,
+ useGetOllamaModelsQuery,
+ useGetOpenAIModelsQuery,
+} from "@/app/api/queries/useGetModelsQuery";
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
-import { useGetOpenAIModelsQuery, useGetOllamaModelsQuery, useGetIBMModelsQuery } from "@/app/api/queries/useGetModelsQuery";
import { ConfirmationDialog } from "@/components/confirmation-dialog";
-import { ModelSelectItems } from "./helpers/model-select-item";
-import { getFallbackModels, type ModelProvider } from "./helpers/model-helpers";
import { ProtectedRoute } from "@/components/protected-route";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -33,6 +35,8 @@ import { Textarea } from "@/components/ui/textarea";
import { useAuth } from "@/contexts/auth-context";
import { useTask } from "@/contexts/task-context";
import { useDebounce } from "@/lib/debounce";
+import { getFallbackModels, type ModelProvider } from "./helpers/model-helpers";
+import { ModelSelectItems } from "./helpers/model-select-item";
const MAX_SYSTEM_PROMPT_CHARS = 2000;
@@ -105,42 +109,46 @@ function KnowledgeSourcesPage() {
// Fetch settings using React Query
const { data: settings = {} } = useGetSettingsQuery({
- enabled: isAuthenticated,
+ enabled: isAuthenticated || isNoAuthMode,
});
// Get the current provider from settings
- const currentProvider = (settings.provider?.model_provider || 'openai') as ModelProvider;
+ const currentProvider = (settings.provider?.model_provider ||
+ "openai") as ModelProvider;
// Fetch available models based on provider
const { data: openaiModelsData } = useGetOpenAIModelsQuery(
undefined, // Let backend use stored API key from configuration
{
- enabled: isAuthenticated && currentProvider === 'openai',
- }
+ enabled:
+ (isAuthenticated || isNoAuthMode) && currentProvider === "openai",
+ },
);
const { data: ollamaModelsData } = useGetOllamaModelsQuery(
undefined, // No params for now, could be extended later
{
- enabled: isAuthenticated && currentProvider === 'ollama',
- }
+ enabled:
+ (isAuthenticated || isNoAuthMode) && currentProvider === "ollama",
+ },
);
const { data: ibmModelsData } = useGetIBMModelsQuery(
undefined, // No params for now, could be extended later
{
- enabled: isAuthenticated && currentProvider === 'ibm',
- }
+ enabled: (isAuthenticated || isNoAuthMode) && currentProvider === "ibm",
+ },
);
// Select the appropriate models data based on provider
- const modelsData = currentProvider === 'openai'
- ? openaiModelsData
- : currentProvider === 'ollama'
- ? ollamaModelsData
- : currentProvider === 'ibm'
- ? ibmModelsData
- : openaiModelsData; // fallback to openai
+ const modelsData =
+ currentProvider === "openai"
+ ? openaiModelsData
+ : currentProvider === "ollama"
+ ? ollamaModelsData
+ : currentProvider === "ibm"
+ ? ibmModelsData
+ : openaiModelsData; // fallback to openai
// Mutations
const updateFlowSettingMutation = useUpdateFlowSettingMutation({
@@ -219,10 +227,10 @@ function KnowledgeSourcesPage() {
// Update processing mode
const handleProcessingModeChange = (mode: string) => {
setProcessingMode(mode);
+ // Update the configuration setting (backend will also update the flow automatically)
debouncedUpdate({ doclingPresets: mode });
};
-
// Helper function to get connector icon
const getConnectorIcon = useCallback((iconName: string) => {
const iconMap: { [key: string]: React.ReactElement } = {
@@ -611,7 +619,11 @@ function KnowledgeSourcesPage() {
Language Model