This commit is contained in:
phact 2025-08-22 11:25:42 -04:00
parent 674fed412d
commit 60fa42f0a2
5 changed files with 10 additions and 291 deletions

View file

@ -125,7 +125,6 @@ export function KnowledgeDropdown({ active, variant = 'navigation' }: KnowledgeD
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")
@ -166,7 +165,6 @@ export function KnowledgeDropdown({ active, variant = 'navigation' }: KnowledgeD
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")

View file

@ -6,7 +6,6 @@ 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"

View file

@ -1,12 +1,10 @@
"use client"
import { useState, useEffect, useCallback, useRef } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { Search, Loader2, FileText, HardDrive, Building2, Cloud, Plus } from "lucide-react"
import { Search, Loader2, FileText, HardDrive, Building2, Cloud } from "lucide-react"
import { TbBrandOnedrive } from "react-icons/tb"
import { SiGoogledrive } from "react-icons/si"
import { ProtectedRoute } from "@/components/protected-route"
@ -14,7 +12,6 @@ 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 }
interface ChunkResult {
filename: string
@ -75,24 +72,16 @@ function getSourceIcon(connectorType?: string) {
}
function SearchPage() {
const router = useRouter()
const { isMenuOpen } = useTask()
const { parsedFilterData, isPanelOpen } = useKnowledgeFilter()
const [query, setQuery] = useState("*")
const [loading, setLoading] = useState(false)
const [chunkResults, setChunkResults] = useState<ChunkResult[]>([])
const [fileResults, setFileResults] = useState<FileResult[]>([])
const [viewMode, setViewMode] = useState<'files' | 'chunks'>('files')
const [selectedFile, setSelectedFile] = useState<string | null>(null)
const [searchPerformed, setSearchPerformed] = useState(false)
const prevFilterDataRef = useRef<string>("")
// Stats state for knowledge overview
const [statsLoading, setStatsLoading] = useState<boolean>(false)
const [totalDocs, setTotalDocs] = useState<number>(0)
const [totalChunks, setTotalChunks] = useState<number>(0)
const [facetStats, setFacetStats] = useState<{ data_sources: FacetBucket[]; document_types: FacetBucket[]; owners: FacetBucket[]; connector_types: FacetBucket[] } | null>(null)
const handleSearch = useCallback(async (e?: React.FormEvent) => {
if (e) e.preventDefault()
if (!query.trim()) return
@ -116,7 +105,7 @@ function SearchPage() {
const searchPayload: SearchPayload = {
query,
limit: parsedFilterData?.limit || (query.trim() === "*" ? 50 : 10), // Higher limit for wildcard searches
limit: parsedFilterData?.limit || (query.trim() === "*" ? 10000 : 10), // Maximum allowed limit for wildcard searches
scoreThreshold: parsedFilterData?.scoreThreshold || 0
}
@ -284,106 +273,12 @@ function SearchPage() {
prevFilterDataRef.current = currentFilterString
}, [parsedFilterData, searchPerformed, query, handleSearch])
// Fetch stats with current knowledge filter applied
const fetchStats = useCallback(async () => {
try {
setStatsLoading(true)
// Build search payload with current filter data
interface SearchPayload {
query: string;
limit: number;
scoreThreshold: number;
filters?: {
data_sources?: string[];
document_types?: string[];
owners?: string[];
connector_types?: string[];
};
}
const searchPayload: SearchPayload = {
query: '*',
limit: 50, // Get more results to ensure we have owner mapping data
scoreThreshold: parsedFilterData?.scoreThreshold || 0
}
// Add filters from global context if available and not wildcards
if (parsedFilterData?.filters) {
const filters = parsedFilterData.filters
// Only include filters if they're not wildcards (not "*")
const hasSpecificFilters =
!filters.data_sources.includes("*") ||
!filters.document_types.includes("*") ||
!filters.owners.includes("*") ||
(filters.connector_types && !filters.connector_types.includes("*"))
if (hasSpecificFilters) {
const processedFilters: SearchPayload['filters'] = {}
// Only add filter arrays that don't contain wildcards
if (!filters.data_sources.includes("*")) {
processedFilters.data_sources = filters.data_sources
}
if (!filters.document_types.includes("*")) {
processedFilters.document_types = filters.document_types
}
if (!filters.owners.includes("*")) {
processedFilters.owners = filters.owners
}
if (filters.connector_types && !filters.connector_types.includes("*")) {
processedFilters.connector_types = filters.connector_types
}
// Only add filters object if it has any actual filters
if (Object.keys(processedFilters).length > 0) {
searchPayload.filters = processedFilters
}
}
}
const response = await fetch('/api/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(searchPayload)
})
const result = await response.json()
if (response.ok) {
const aggs = result.aggregations || {}
const toBuckets = (agg: { buckets?: Array<{ key: string | number; doc_count: number }> }): FacetBucket[] =>
(agg?.buckets || []).map(b => ({ key: String(b.key), count: b.doc_count }))
// Now we can aggregate directly on owner names since they're keyword fields
const dataSourceBuckets = toBuckets(aggs.data_sources)
setFacetStats({
data_sources: dataSourceBuckets.slice(0, 10),
document_types: toBuckets(aggs.document_types).slice(0, 10),
owners: toBuckets(aggs.owners).slice(0, 10),
connector_types: toBuckets(aggs.connector_types || {}).slice(0, 10)
})
setTotalDocs(dataSourceBuckets.length)
setTotalChunks(Number(result.total || 0))
}
} catch {
// non-fatal keep page functional without stats
} finally {
setStatsLoading(false)
}
}, [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()
}, [fetchStats])
// Only trigger initial search on mount when query is "*"
handleSearch()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) // Only run once on mount - ignore handleSearch dependency
@ -447,8 +342,7 @@ function SearchPage() {
{/* Results Display */}
<div className="space-y-4">
{viewMode === 'files' ? (
selectedFile ? (
{selectedFile ? (
// Show chunks for selected file
<>
<div className="flex items-center gap-2 mb-4">
@ -536,29 +430,7 @@ function SearchPage() {
</tbody>
</table>
</div>
)
) : (
// Show chunks view
chunkResults.map((result, index) => (
<div key={index} className="bg-muted/20 rounded-lg p-4 border border-border/50">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<FileText className="h-4 w-4 text-blue-400" />
<span className="font-medium truncate">{result.filename}</span>
</div>
<span className="text-xs text-green-400 bg-green-400/20 px-2 py-1 rounded">
{result.score.toFixed(2)}
</span>
</div>
<div className="text-sm text-muted-foreground mb-2">
{result.mimetype} Page {result.page}
</div>
<p className="text-sm text-foreground/90 leading-relaxed">
{result.text}
</p>
</div>
))
)}
)}
</div>
</>
)}

View file

@ -7,11 +7,10 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Upload, FolderOpen, Loader2, PlugZap, RefreshCw, Download, Cloud } from "lucide-react"
import { Loader2, PlugZap, RefreshCw } from "lucide-react"
import { ProtectedRoute } from "@/components/protected-route"
import { useTask } from "@/contexts/task-context"
import { useAuth } from "@/contexts/auth-context"
import { FileUploadArea } from "@/components/file-upload-area"
interface Connector {
@ -45,14 +44,6 @@ function KnowledgeSourcesPage() {
const { addTask, tasks } = useTask()
const searchParams = useSearchParams()
// File upload state
const [fileUploadLoading, setFileUploadLoading] = useState(false)
const [pathUploadLoading, setPathUploadLoading] = useState(false)
const [folderPath, setFolderPath] = useState("/app/documents/")
const [bucketUploadLoading, setBucketUploadLoading] = useState(false)
const [bucketUrl, setBucketUrl] = useState("s3://")
const [uploadStatus, setUploadStatus] = useState<string>("")
const [awsEnabled, setAwsEnabled] = useState(false)
// Connectors state
const [connectors, setConnectors] = useState<Connector[]>([])
@ -62,123 +53,6 @@ function KnowledgeSourcesPage() {
const [maxFiles, setMaxFiles] = useState<number>(10)
// File upload handlers
const handleDirectFileUpload = async (file: File) => {
setFileUploadLoading(true)
setUploadStatus("")
try {
const formData = new FormData()
formData.append("file", file)
const response = await fetch("/api/upload", {
method: "POST",
body: formData,
})
const result = await response.json()
if (response.ok) {
setUploadStatus(`File processed successfully! ID: ${result.id}`)
} else {
setUploadStatus(`Error: ${result.error || "Processing failed"}`)
}
} catch (error) {
setUploadStatus(`Error: ${error instanceof Error ? error.message : "Processing failed"}`)
} finally {
setFileUploadLoading(false)
}
}
const handlePathUpload = async (e: React.FormEvent) => {
e.preventDefault()
if (!folderPath.trim()) return
setPathUploadLoading(true)
setUploadStatus("")
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)
setUploadStatus(`🔄 Processing started for ${totalFiles} files. Check the task notification panel for real-time progress. (Task ID: ${taskId})`)
setFolderPath("")
setPathUploadLoading(false)
} else if (response.ok) {
const successful = result.results?.filter((r: {status: string}) => r.status === "indexed").length || 0
const total = result.results?.length || 0
setUploadStatus(`Path processed successfully! ${successful}/${total} files indexed.`)
setFolderPath("")
setPathUploadLoading(false)
} else {
setUploadStatus(`Error: ${result.error || "Path upload failed"}`)
setPathUploadLoading(false)
}
} catch (error) {
setUploadStatus(`Error: ${error instanceof Error ? error.message : "Path upload failed"}`)
setPathUploadLoading(false)
}
}
const handleBucketUpload = async (e: React.FormEvent) => {
e.preventDefault()
if (!bucketUrl.trim()) return
setBucketUploadLoading(true)
setUploadStatus("")
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)
setUploadStatus(`🔄 Processing started for ${totalFiles} files. Check the task notification panel for real-time progress. (Task ID: ${taskId})`)
setBucketUrl("s3://")
} else {
setUploadStatus(`Error: ${result.error || "Bucket processing failed"}`)
}
} catch (error) {
setUploadStatus(
`Error: ${error instanceof Error ? error.message : "Bucket processing failed"}`,
)
} finally {
setBucketUploadLoading(false)
}
}
// Helper function to get connector icon
const getConnectorIcon = (iconName: string) => {
@ -379,21 +253,6 @@ function KnowledgeSourcesPage() {
}, [searchParams, isAuthenticated, checkConnectorStatuses])
// Check AWS availability
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()
}, [])
// Track previous tasks to detect new completions

View file

@ -60,7 +60,6 @@ export function TaskNotificationMenu() {
const processed = task.processed_files || 0
const successful = task.successful_files || 0
const failed = task.failed_files || 0
const skipped = Math.max(0, processed - successful - failed) // Calculate skipped
if (total > 0) {
return {
@ -70,7 +69,6 @@ export function TaskNotificationMenu() {
processed,
successful,
failed,
skipped,
remaining: total - processed
}
}
@ -180,12 +178,6 @@ export function TaskNotificationMenu() {
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-yellow-500 rounded-full"></div>
<span className="text-yellow-600">
{formatTaskProgress(task)?.detailed.skipped} skipped
</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-gray-400 rounded-full"></div>
<span className="text-muted-foreground">
{formatTaskProgress(task)?.detailed.remaining} pending
</span>
</div>
@ -269,8 +261,7 @@ export function TaskNotificationMenu() {
{task.status === 'completed' && formatTaskProgress(task)?.detailed && (
<div className="text-xs text-muted-foreground mt-1">
{formatTaskProgress(task)?.detailed.successful} success, {' '}
{formatTaskProgress(task)?.detailed.failed} failed, {' '}
{formatTaskProgress(task)?.detailed.skipped} skipped
{formatTaskProgress(task)?.detailed.failed} failed
</div>
)}
{task.status === 'failed' && task.error && (