From 674fed412d31d1fa7804de5808a12a71d492b810 Mon Sep 17 00:00:00 2001 From: phact Date: Fri, 22 Aug 2025 11:13:01 -0400 Subject: [PATCH] misc ui improvements --- frontend/components/knowledge-dropdown.tsx | 359 ++++++++++++++++++++ frontend/components/navigation.tsx | 1 + frontend/src/app/knowledge/page.tsx | 361 +++++++++------------ frontend/src/app/settings/page.tsx | 193 +++-------- 4 files changed, 544 insertions(+), 370 deletions(-) create mode 100644 frontend/components/knowledge-dropdown.tsx diff --git a/frontend/components/knowledge-dropdown.tsx b/frontend/components/knowledge-dropdown.tsx new file mode 100644 index 00000000..2e8d315c --- /dev/null +++ b/frontend/components/knowledge-dropdown.tsx @@ -0,0 +1,359 @@ +"use client" + +import { useState, useEffect, useRef } from "react" +import { useRouter } from "next/navigation" +import { ChevronDown, Upload, FolderOpen, Cloud, PlugZap, Plus } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { cn } from "@/lib/utils" +import { useTask } from "@/contexts/task-context" + +interface KnowledgeDropdownProps { + active?: boolean + variant?: 'navigation' | 'button' +} + +export function KnowledgeDropdown({ active, variant = 'navigation' }: KnowledgeDropdownProps) { + const router = useRouter() + const { addTask } = useTask() + const [isOpen, setIsOpen] = useState(false) + const [showFolderDialog, setShowFolderDialog] = useState(false) + const [showS3Dialog, setShowS3Dialog] = useState(false) + const [awsEnabled, setAwsEnabled] = useState(false) + const [folderPath, setFolderPath] = useState("/app/documents/") + const [bucketUrl, setBucketUrl] = useState("s3://") + const [folderLoading, setFolderLoading] = useState(false) + const [s3Loading, setS3Loading] = useState(false) + const fileInputRef = useRef(null) + const dropdownRef = useRef(null) + + // Check AWS availability on mount + useEffect(() => { + const checkAws = async () => { + try { + const res = await fetch("/api/upload_options") + if (res.ok) { + const data = await res.json() + setAwsEnabled(Boolean(data.aws)) + } + } catch (err) { + console.error("Failed to check AWS availability", err) + } + } + checkAws() + }, []) + + // Handle click outside to close dropdown + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false) + } + } + + if (isOpen) { + document.addEventListener("mousedown", handleClickOutside) + return () => document.removeEventListener("mousedown", handleClickOutside) + } + }, [isOpen]) + + const handleFileUpload = () => { + fileInputRef.current?.click() + } + + const handleFileChange = async (e: React.ChangeEvent) => { + const files = e.target.files + if (files && files.length > 0) { + // Trigger the same file upload event as the chat page + window.dispatchEvent(new CustomEvent('fileUploadStart', { + detail: { filename: files[0].name } + })) + + try { + const formData = new FormData() + formData.append('file', files[0]) + + const response = await fetch('/api/upload', { + method: 'POST', + body: formData, + }) + + const result = await response.json() + + if (response.ok) { + window.dispatchEvent(new CustomEvent('fileUploaded', { + detail: { file: files[0], result } + })) + } else { + window.dispatchEvent(new CustomEvent('fileUploadError', { + detail: { filename: files[0].name, error: result.error || 'Upload failed' } + })) + } + } catch (error) { + window.dispatchEvent(new CustomEvent('fileUploadError', { + detail: { filename: files[0].name, error: error instanceof Error ? error.message : 'Upload failed' } + })) + } finally { + window.dispatchEvent(new CustomEvent('fileUploadComplete')) + } + } + + // Reset file input + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + setIsOpen(false) + } + + const handleFolderUpload = async () => { + if (!folderPath.trim()) return + + setFolderLoading(true) + + try { + const response = await fetch("/api/upload_path", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ path: folderPath }), + }) + + const result = await response.json() + + if (response.status === 201) { + const taskId = result.task_id || result.id + const totalFiles = result.total_files || 0 + + if (!taskId) { + throw new Error("No task ID received from server") + } + + addTask(taskId) + setFolderPath("") + setShowFolderDialog(false) + + } else if (response.ok) { + setFolderPath("") + setShowFolderDialog(false) + } else { + console.error("Folder upload failed:", result.error) + } + } catch (error) { + console.error("Folder upload error:", error) + } finally { + setFolderLoading(false) + } + } + + const handleS3Upload = async () => { + if (!bucketUrl.trim()) return + + setS3Loading(true) + + try { + const response = await fetch("/api/upload_bucket", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ s3_url: bucketUrl }), + }) + + const result = await response.json() + + if (response.status === 201) { + const taskId = result.task_id || result.id + const totalFiles = result.total_files || 0 + + if (!taskId) { + throw new Error("No task ID received from server") + } + + addTask(taskId) + setBucketUrl("s3://") + setShowS3Dialog(false) + } else { + console.error("S3 upload failed:", result.error) + } + } catch (error) { + console.error("S3 upload error:", error) + } finally { + setS3Loading(false) + } + } + + const menuItems = [ + { + label: "Add File", + icon: Upload, + onClick: handleFileUpload + }, + { + label: "Process Folder", + icon: FolderOpen, + onClick: () => { + setIsOpen(false) + setShowFolderDialog(true) + } + }, + ...(awsEnabled ? [{ + label: "Process S3 Bucket", + icon: Cloud, + onClick: () => { + setIsOpen(false) + setShowS3Dialog(true) + } + }] : []), + { + label: "Cloud Connectors", + icon: PlugZap, + onClick: () => { + setIsOpen(false) + router.push("/settings") + } + } + ] + + return ( + <> +
+ + + {isOpen && ( +
+
+ {menuItems.map((item, index) => ( + + ))} +
+
+ )} + + +
+ + {/* Process Folder Dialog */} + + + + + + Process Folder + + + Process all documents in a folder path + + +
+
+ + setFolderPath(e.target.value)} + /> +
+
+ + +
+
+
+
+ + {/* Process S3 Bucket Dialog */} + + + + + + Process S3 Bucket + + + Process all documents from an S3 bucket. AWS credentials must be configured. + + +
+
+ + setBucketUrl(e.target.value)} + /> +
+
+ + +
+
+
+
+ + ) +} \ No newline at end of file diff --git a/frontend/components/navigation.tsx b/frontend/components/navigation.tsx index a2d6e08b..49fc18d4 100644 --- a/frontend/components/navigation.tsx +++ b/frontend/components/navigation.tsx @@ -6,6 +6,7 @@ import { Library, MessageSquare, Settings2, Plus, FileText } from "lucide-react" import { cn } from "@/lib/utils" import { useState, useEffect, useRef, useCallback } from "react" import { useChat } from "@/contexts/chat-context" +import { KnowledgeDropdown } from "@/components/knowledge-dropdown" import { EndpointType } from "@/contexts/chat-context" diff --git a/frontend/src/app/knowledge/page.tsx b/frontend/src/app/knowledge/page.tsx index 8fe452c5..50baaefc 100644 --- a/frontend/src/app/knowledge/page.tsx +++ b/frontend/src/app/knowledge/page.tsx @@ -12,6 +12,7 @@ import { SiGoogledrive } from "react-icons/si" import { ProtectedRoute } from "@/components/protected-route" import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context" import { useTask } from "@/contexts/task-context" +import { KnowledgeDropdown } from "@/components/knowledge-dropdown" type FacetBucket = { key: string; count: number } @@ -77,7 +78,7 @@ function SearchPage() { const router = useRouter() const { isMenuOpen } = useTask() const { parsedFilterData, isPanelOpen } = useKnowledgeFilter() - const [query, setQuery] = useState("") + const [query, setQuery] = useState("*") const [loading, setLoading] = useState(false) const [chunkResults, setChunkResults] = useState([]) const [fileResults, setFileResults] = useState([]) @@ -372,6 +373,13 @@ function SearchPage() { } }, [parsedFilterData]) + // Auto-search on mount with "*" + useEffect(() => { + if (query === "*") { + handleSearch() + } + }, []) // Only run once on mount + // Initial stats fetch and refresh when filter changes useEffect(() => { fetchStats() @@ -411,225 +419,150 @@ function SearchPage() { )} - +
+ +
{/* Results Area */}
- {searchPerformed ? ( -
- {fileResults.length === 0 && chunkResults.length === 0 ? ( -
- -

No documents found

-

- Try adjusting your search terms -

-
- ) : ( - <> - {/* View Toggle and Results Count */} -
-
- {viewMode === 'files' ? fileResults.length : chunkResults.length} {viewMode === 'files' ? 'file' : 'result'}{(viewMode === 'files' ? fileResults.length : chunkResults.length) !== 1 ? 's' : ''} found -
-
- - -
-
- - {/* Results Display */} -
- {viewMode === 'files' ? ( - selectedFile ? ( - // Show chunks for selected file - <> -
- - - Chunks from {selectedFile} - -
- {chunkResults - .filter(chunk => chunk.filename === selectedFile) - .map((chunk, index) => ( -
-
-
- - {chunk.filename} -
- - {chunk.score.toFixed(2)} - -
-
- {chunk.mimetype} • Page {chunk.page} -
-

- {chunk.text} -

-
- ))} - - ) : ( - // Show files table -
- - - - - - - - - - - - - {fileResults.map((file, index) => ( - setSelectedFile(file.filename)} - > - - - - - - - - ))} - -
SourceTypeSizeChunksScoreOwner
-
- {getSourceIcon(file.connector_type)} - - {file.filename} - -
-
- {file.mimetype} - - {file.size ? `${Math.round(file.size / 1024)} KB` : '—'} - - {file.chunkCount} - - - {file.avgScore.toFixed(2)} - - - {file.owner_name || file.owner || '—'} -
-
- ) - ) : ( - // Show chunks view - chunkResults.map((result, index) => ( -
-
-
- - {result.filename} -
- - {result.score.toFixed(2)} - -
-
- {result.mimetype} • Page {result.page} -
-

- {result.text} -

-
- )) - )} -
- - )} -
- ) : ( - /* Knowledge Overview - Show when no search has been performed */ -
-
-
-

Knowledge Overview

-
- - {/* Documents row */} -
-
Total documents
-
{statsLoading ? '—' : totalDocs}
-
- - {/* Separator */} -
- - {/* Chunks and breakdown */} -
-
-
Total chunks
-
{statsLoading ? '—' : totalChunks}
-
-
-
Top sources
-
- {(facetStats?.connector_types || []).slice(0,5).map((b) => ( - - {b.key} · {b.count} - - ))} -
-
-
-
Top types
-
- {(facetStats?.document_types || []).slice(0,5).map((b) => ( - {b.key} · {b.count} - ))} -
-
-
-
Top owners
-
- {(facetStats?.owners || []).slice(0,5).map((b) => ( - {b.key || 'unknown'} · {b.count} - ))} -
-
-
+
+ {fileResults.length === 0 && chunkResults.length === 0 && !loading ? ( +
+ +

No documents found

+

+ Try adjusting your search terms +

-
- )} + ) : ( + <> + {/* Results Count */} +
+
+ {fileResults.length} file{fileResults.length !== 1 ? 's' : ''} found +
+
+ + {/* Results Display */} +
+ {viewMode === 'files' ? ( + selectedFile ? ( + // Show chunks for selected file + <> +
+ + + Chunks from {selectedFile} + +
+ {chunkResults + .filter(chunk => chunk.filename === selectedFile) + .map((chunk, index) => ( +
+
+
+ + {chunk.filename} +
+ + {chunk.score.toFixed(2)} + +
+
+ {chunk.mimetype} • Page {chunk.page} +
+

+ {chunk.text} +

+
+ ))} + + ) : ( + // Show files table +
+ + + + + + + + + + + + + {fileResults.map((file, index) => ( + setSelectedFile(file.filename)} + > + + + + + + + + ))} + +
SourceTypeSizeMatching chunksAverage scoreOwner
+
+ {getSourceIcon(file.connector_type)} + + {file.filename} + +
+
+ {file.mimetype} + + {file.size ? `${Math.round(file.size / 1024)} KB` : '—'} + + {file.chunkCount} + + + {file.avgScore.toFixed(2)} + + + {file.owner_name || file.owner || '—'} +
+
+ ) + ) : ( + // Show chunks view + chunkResults.map((result, index) => ( +
+
+
+ + {result.filename} +
+ + {result.score.toFixed(2)} + +
+
+ {result.mimetype} • Page {result.page} +
+

+ {result.text} +

+
+ )) + )} +
+ + )} +
diff --git a/frontend/src/app/settings/page.tsx b/frontend/src/app/settings/page.tsx index 9efe8d11..e6fadcca 100644 --- a/frontend/src/app/settings/page.tsx +++ b/frontend/src/app/settings/page.tsx @@ -425,133 +425,25 @@ function KnowledgeSourcesPage() { return (
- {/* Upload Section */} -
+ {/* Agent Behavior Section */} +
-

Import

+

Agent behavior

+

Adjust your retrieval agent flow

- -
- {/* File Upload Card */} - - - - - Add File - - - Import a single document to be processed and indexed - - - - - - - - {/* Folder Upload Card */} - - - - - Process Folder - - - Process all documents in a folder path - - - -
-
- - setFolderPath(e.target.value)} - /> -
- -
-
-
- - {/* S3 Bucket Upload Card - only show if AWS is enabled */} - {awsEnabled && ( - - - - - Process S3 Bucket - - - Process all documents from an S3 bucket. AWS credentials must be configured. - - - -
-
- - setBucketUrl(e.target.value)} - /> -
- -
-
-
- )} -
- - {/* Upload Status */} - {uploadStatus && ( - - -

{uploadStatus}

-
-
- )} +
+ {/* Connectors Section */}
@@ -559,40 +451,29 @@ function KnowledgeSourcesPage() {
{/* Sync Settings */} - - - - - Sync Settings - - - Configure how many files to sync when manually triggering a sync - - - -
-
-
- - setMaxFiles(parseInt(e.target.value) || 10)} - className="w-16 min-w-16 max-w-16 flex-shrink-0" - min="1" - max="100" - /> - - (Leave blank or set to 0 for unlimited) - -
-
+
+
+

Sync Settings

+

Configure how many files to sync when manually triggering a sync

+
+
+ +
+ setMaxFiles(parseInt(e.target.value) || 10)} + className="w-16 min-w-16 max-w-16 flex-shrink-0" + min="1" + max="100" + title="Leave blank or set to 0 for unlimited" + />
- - +
+
{/* Connectors Grid */}