lint
This commit is contained in:
parent
674fed412d
commit
60fa42f0a2
5 changed files with 10 additions and 291 deletions
|
|
@ -125,7 +125,6 @@ export function KnowledgeDropdown({ active, variant = 'navigation' }: KnowledgeD
|
||||||
|
|
||||||
if (response.status === 201) {
|
if (response.status === 201) {
|
||||||
const taskId = result.task_id || result.id
|
const taskId = result.task_id || result.id
|
||||||
const totalFiles = result.total_files || 0
|
|
||||||
|
|
||||||
if (!taskId) {
|
if (!taskId) {
|
||||||
throw new Error("No task ID received from server")
|
throw new Error("No task ID received from server")
|
||||||
|
|
@ -166,7 +165,6 @@ export function KnowledgeDropdown({ active, variant = 'navigation' }: KnowledgeD
|
||||||
|
|
||||||
if (response.status === 201) {
|
if (response.status === 201) {
|
||||||
const taskId = result.task_id || result.id
|
const taskId = result.task_id || result.id
|
||||||
const totalFiles = result.total_files || 0
|
|
||||||
|
|
||||||
if (!taskId) {
|
if (!taskId) {
|
||||||
throw new Error("No task ID received from server")
|
throw new Error("No task ID received from server")
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { Library, MessageSquare, Settings2, Plus, FileText } from "lucide-react"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useState, useEffect, useRef, useCallback } from "react"
|
import { useState, useEffect, useRef, useCallback } from "react"
|
||||||
import { useChat } from "@/contexts/chat-context"
|
import { useChat } from "@/contexts/chat-context"
|
||||||
import { KnowledgeDropdown } from "@/components/knowledge-dropdown"
|
|
||||||
|
|
||||||
import { EndpointType } from "@/contexts/chat-context"
|
import { EndpointType } from "@/contexts/chat-context"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef } from "react"
|
import { useState, useEffect, useCallback, useRef } from "react"
|
||||||
import { useRouter } from "next/navigation"
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Search, Loader2, FileText, HardDrive, Building2, Cloud } from "lucide-react"
|
||||||
import { Search, Loader2, FileText, HardDrive, Building2, Cloud, Plus } from "lucide-react"
|
|
||||||
import { TbBrandOnedrive } from "react-icons/tb"
|
import { TbBrandOnedrive } from "react-icons/tb"
|
||||||
import { SiGoogledrive } from "react-icons/si"
|
import { SiGoogledrive } from "react-icons/si"
|
||||||
import { ProtectedRoute } from "@/components/protected-route"
|
import { ProtectedRoute } from "@/components/protected-route"
|
||||||
|
|
@ -14,7 +12,6 @@ import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"
|
||||||
import { useTask } from "@/contexts/task-context"
|
import { useTask } from "@/contexts/task-context"
|
||||||
import { KnowledgeDropdown } from "@/components/knowledge-dropdown"
|
import { KnowledgeDropdown } from "@/components/knowledge-dropdown"
|
||||||
|
|
||||||
type FacetBucket = { key: string; count: number }
|
|
||||||
|
|
||||||
interface ChunkResult {
|
interface ChunkResult {
|
||||||
filename: string
|
filename: string
|
||||||
|
|
@ -75,24 +72,16 @@ function getSourceIcon(connectorType?: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchPage() {
|
function SearchPage() {
|
||||||
const router = useRouter()
|
|
||||||
const { isMenuOpen } = useTask()
|
const { isMenuOpen } = useTask()
|
||||||
const { parsedFilterData, isPanelOpen } = useKnowledgeFilter()
|
const { parsedFilterData, isPanelOpen } = useKnowledgeFilter()
|
||||||
const [query, setQuery] = useState("*")
|
const [query, setQuery] = useState("*")
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [chunkResults, setChunkResults] = useState<ChunkResult[]>([])
|
const [chunkResults, setChunkResults] = useState<ChunkResult[]>([])
|
||||||
const [fileResults, setFileResults] = useState<FileResult[]>([])
|
const [fileResults, setFileResults] = useState<FileResult[]>([])
|
||||||
const [viewMode, setViewMode] = useState<'files' | 'chunks'>('files')
|
|
||||||
const [selectedFile, setSelectedFile] = useState<string | null>(null)
|
const [selectedFile, setSelectedFile] = useState<string | null>(null)
|
||||||
const [searchPerformed, setSearchPerformed] = useState(false)
|
const [searchPerformed, setSearchPerformed] = useState(false)
|
||||||
const prevFilterDataRef = useRef<string>("")
|
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) => {
|
const handleSearch = useCallback(async (e?: React.FormEvent) => {
|
||||||
if (e) e.preventDefault()
|
if (e) e.preventDefault()
|
||||||
if (!query.trim()) return
|
if (!query.trim()) return
|
||||||
|
|
@ -116,7 +105,7 @@ function SearchPage() {
|
||||||
|
|
||||||
const searchPayload: SearchPayload = {
|
const searchPayload: SearchPayload = {
|
||||||
query,
|
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
|
scoreThreshold: parsedFilterData?.scoreThreshold || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -284,106 +273,12 @@ function SearchPage() {
|
||||||
prevFilterDataRef.current = currentFilterString
|
prevFilterDataRef.current = currentFilterString
|
||||||
}, [parsedFilterData, searchPerformed, query, handleSearch])
|
}, [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 "*"
|
// Auto-search on mount with "*"
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (query === "*") {
|
// Only trigger initial search on mount when query is "*"
|
||||||
handleSearch()
|
handleSearch()
|
||||||
}
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []) // Only run once on mount
|
}, []) // Only run once on mount - ignore handleSearch dependency
|
||||||
|
|
||||||
// Initial stats fetch and refresh when filter changes
|
|
||||||
useEffect(() => {
|
|
||||||
fetchStats()
|
|
||||||
}, [fetchStats])
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -447,8 +342,7 @@ function SearchPage() {
|
||||||
|
|
||||||
{/* Results Display */}
|
{/* Results Display */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{viewMode === 'files' ? (
|
{selectedFile ? (
|
||||||
selectedFile ? (
|
|
||||||
// Show chunks for selected file
|
// Show chunks for selected file
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
|
@ -536,29 +430,7 @@ function SearchPage() {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,10 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
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 { ProtectedRoute } from "@/components/protected-route"
|
||||||
import { useTask } from "@/contexts/task-context"
|
import { useTask } from "@/contexts/task-context"
|
||||||
import { useAuth } from "@/contexts/auth-context"
|
import { useAuth } from "@/contexts/auth-context"
|
||||||
import { FileUploadArea } from "@/components/file-upload-area"
|
|
||||||
|
|
||||||
|
|
||||||
interface Connector {
|
interface Connector {
|
||||||
|
|
@ -45,14 +44,6 @@ function KnowledgeSourcesPage() {
|
||||||
const { addTask, tasks } = useTask()
|
const { addTask, tasks } = useTask()
|
||||||
const searchParams = useSearchParams()
|
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
|
// Connectors state
|
||||||
const [connectors, setConnectors] = useState<Connector[]>([])
|
const [connectors, setConnectors] = useState<Connector[]>([])
|
||||||
|
|
@ -62,123 +53,6 @@ function KnowledgeSourcesPage() {
|
||||||
const [maxFiles, setMaxFiles] = useState<number>(10)
|
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
|
// Helper function to get connector icon
|
||||||
const getConnectorIcon = (iconName: string) => {
|
const getConnectorIcon = (iconName: string) => {
|
||||||
|
|
@ -379,21 +253,6 @@ function KnowledgeSourcesPage() {
|
||||||
}, [searchParams, isAuthenticated, checkConnectorStatuses])
|
}, [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
|
// Track previous tasks to detect new completions
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,6 @@ export function TaskNotificationMenu() {
|
||||||
const processed = task.processed_files || 0
|
const processed = task.processed_files || 0
|
||||||
const successful = task.successful_files || 0
|
const successful = task.successful_files || 0
|
||||||
const failed = task.failed_files || 0
|
const failed = task.failed_files || 0
|
||||||
const skipped = Math.max(0, processed - successful - failed) // Calculate skipped
|
|
||||||
|
|
||||||
if (total > 0) {
|
if (total > 0) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -70,7 +69,6 @@ export function TaskNotificationMenu() {
|
||||||
processed,
|
processed,
|
||||||
successful,
|
successful,
|
||||||
failed,
|
failed,
|
||||||
skipped,
|
|
||||||
remaining: total - processed
|
remaining: total - processed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -180,12 +178,6 @@ export function TaskNotificationMenu() {
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<div className="w-2 h-2 bg-yellow-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-yellow-500 rounded-full"></div>
|
||||||
<span className="text-yellow-600">
|
<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
|
{formatTaskProgress(task)?.detailed.remaining} pending
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -269,8 +261,7 @@ export function TaskNotificationMenu() {
|
||||||
{task.status === 'completed' && formatTaskProgress(task)?.detailed && (
|
{task.status === 'completed' && formatTaskProgress(task)?.detailed && (
|
||||||
<div className="text-xs text-muted-foreground mt-1">
|
<div className="text-xs text-muted-foreground mt-1">
|
||||||
{formatTaskProgress(task)?.detailed.successful} success, {' '}
|
{formatTaskProgress(task)?.detailed.successful} success, {' '}
|
||||||
{formatTaskProgress(task)?.detailed.failed} failed, {' '}
|
{formatTaskProgress(task)?.detailed.failed} failed
|
||||||
{formatTaskProgress(task)?.detailed.skipped} skipped
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{task.status === 'failed' && task.error && (
|
{task.status === 'failed' && task.error && (
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue