Compare commits
15 commits
main
...
knowledge-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2657f0f29d | ||
|
|
8017707fa9 | ||
|
|
c3c7b9a836 | ||
|
|
fad175efe3 | ||
|
|
9a14192fd3 | ||
|
|
5f68fa1a9a | ||
|
|
526e4a9f48 | ||
|
|
40d2112d1f | ||
|
|
fd26c91187 | ||
|
|
60a1c09d0b | ||
|
|
ae2e95e023 | ||
|
|
3e10f97659 | ||
|
|
abb0c9868c | ||
|
|
a711573c29 | ||
|
|
61625a30a8 |
10 changed files with 529 additions and 110 deletions
|
|
@ -14,7 +14,7 @@ interface DeleteDocumentResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteDocument = async (
|
const deleteDocument = async (
|
||||||
data: DeleteDocumentRequest
|
data: DeleteDocumentRequest,
|
||||||
): Promise<DeleteDocumentResponse> => {
|
): Promise<DeleteDocumentResponse> => {
|
||||||
const response = await fetch("/api/documents/delete-by-filename", {
|
const response = await fetch("/api/documents/delete-by-filename", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -37,9 +37,11 @@ export const useDeleteDocument = () => {
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: deleteDocument,
|
mutationFn: deleteDocument,
|
||||||
onSuccess: () => {
|
onSettled: () => {
|
||||||
// Invalidate and refetch search queries to update the UI
|
// Invalidate and refetch search queries to update the UI
|
||||||
queryClient.invalidateQueries({ queryKey: ["search"] });
|
setTimeout(() => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["search"] });
|
||||||
|
}, 1000);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ export const useGetOpenAIModelsQuery = (
|
||||||
queryKey: ["models", "openai", params],
|
queryKey: ["models", "openai", params],
|
||||||
queryFn: getOpenAIModels,
|
queryFn: getOpenAIModels,
|
||||||
retry: 2,
|
retry: 2,
|
||||||
enabled: options?.enabled !== false, // Allow enabling/disabling from options
|
enabled: !!params?.apiKey,
|
||||||
staleTime: 0, // Always fetch fresh data
|
staleTime: 0, // Always fetch fresh data
|
||||||
gcTime: 0, // Don't cache results
|
gcTime: 0, // Don't cache results
|
||||||
...options,
|
...options,
|
||||||
|
|
|
||||||
|
|
@ -34,21 +34,28 @@ export interface ChunkResult {
|
||||||
export interface File {
|
export interface File {
|
||||||
filename: string;
|
filename: string;
|
||||||
mimetype: string;
|
mimetype: string;
|
||||||
chunkCount: number;
|
chunkCount?: number;
|
||||||
avgScore: number;
|
avgScore?: number;
|
||||||
source_url: string;
|
source_url: string;
|
||||||
owner: string;
|
owner?: string;
|
||||||
owner_name: string;
|
owner_name?: string;
|
||||||
owner_email: string;
|
owner_email?: string;
|
||||||
size: number;
|
size: number;
|
||||||
connector_type: string;
|
connector_type: string;
|
||||||
chunks: ChunkResult[];
|
status?:
|
||||||
|
| "processing"
|
||||||
|
| "active"
|
||||||
|
| "unavailable"
|
||||||
|
| "failed"
|
||||||
|
| "hidden"
|
||||||
|
| "sync";
|
||||||
|
chunks?: ChunkResult[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useGetSearchQuery = (
|
export const useGetSearchQuery = (
|
||||||
query: string,
|
query: string,
|
||||||
queryData?: ParsedQueryData | null,
|
queryData?: ParsedQueryData | null,
|
||||||
options?: Omit<UseQueryOptions, "queryKey" | "queryFn">
|
options?: Omit<UseQueryOptions, "queryKey" | "queryFn">,
|
||||||
) => {
|
) => {
|
||||||
const queryClient = useQueryClient();
|
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,
|
filename: file.filename,
|
||||||
mimetype: file.mimetype,
|
mimetype: file.mimetype,
|
||||||
chunkCount: file.chunks.length,
|
chunkCount: file.chunks.length,
|
||||||
|
|
@ -173,11 +180,11 @@ export const useGetSearchQuery = (
|
||||||
const queryResult = useQuery(
|
const queryResult = useQuery(
|
||||||
{
|
{
|
||||||
queryKey: ["search", effectiveQuery],
|
queryKey: ["search", effectiveQuery],
|
||||||
placeholderData: prev => prev,
|
placeholderData: (prev) => prev,
|
||||||
queryFn: getFiles,
|
queryFn: getFiles,
|
||||||
...options,
|
...options,
|
||||||
},
|
},
|
||||||
queryClient
|
queryClient,
|
||||||
);
|
);
|
||||||
|
|
||||||
return queryResult;
|
return queryResult;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,10 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import type { ColDef } from "ag-grid-community";
|
||||||
Building2,
|
import { AgGridReact, type CustomCellRendererProps } from "ag-grid-react";
|
||||||
Cloud,
|
import { Building2, Cloud, HardDrive, Search, Trash2, X } from "lucide-react";
|
||||||
HardDrive,
|
|
||||||
Search,
|
|
||||||
Trash2,
|
|
||||||
X,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { AgGridReact, CustomCellRendererProps } from "ag-grid-react";
|
|
||||||
import { useCallback, useState, useRef, ChangeEvent } from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { type ChangeEvent, useCallback, useRef, useState } from "react";
|
||||||
import { SiGoogledrive } from "react-icons/si";
|
import { SiGoogledrive } from "react-icons/si";
|
||||||
import { TbBrandOnedrive } from "react-icons/tb";
|
import { TbBrandOnedrive } from "react-icons/tb";
|
||||||
import { KnowledgeDropdown } from "@/components/knowledge-dropdown";
|
import { KnowledgeDropdown } from "@/components/knowledge-dropdown";
|
||||||
|
|
@ -19,13 +13,13 @@ import { Button } from "@/components/ui/button";
|
||||||
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
|
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
|
||||||
import { useTask } from "@/contexts/task-context";
|
import { useTask } from "@/contexts/task-context";
|
||||||
import { type File, useGetSearchQuery } from "../api/queries/useGetSearchQuery";
|
import { type File, useGetSearchQuery } from "../api/queries/useGetSearchQuery";
|
||||||
import { ColDef } from "ag-grid-community";
|
|
||||||
import "@/components/AgGrid/registerAgGridModules";
|
import "@/components/AgGrid/registerAgGridModules";
|
||||||
import "@/components/AgGrid/agGridStyles.css";
|
import "@/components/AgGrid/agGridStyles.css";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { KnowledgeActionsDropdown } from "@/components/knowledge-actions-dropdown";
|
import { KnowledgeActionsDropdown } from "@/components/knowledge-actions-dropdown";
|
||||||
|
import { StatusBadge } from "@/components/ui/status-badge";
|
||||||
import { DeleteConfirmationDialog } from "../../../components/confirmation-dialog";
|
import { DeleteConfirmationDialog } from "../../../components/confirmation-dialog";
|
||||||
import { useDeleteDocument } from "../api/mutations/useDeleteDocument";
|
import { useDeleteDocument } from "../api/mutations/useDeleteDocument";
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
// Function to get the appropriate icon for a connector type
|
// Function to get the appropriate icon for a connector type
|
||||||
function getSourceIcon(connectorType?: string) {
|
function getSourceIcon(connectorType?: string) {
|
||||||
|
|
@ -51,7 +45,7 @@ function getSourceIcon(connectorType?: string) {
|
||||||
|
|
||||||
function SearchPage() {
|
function SearchPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { isMenuOpen } = useTask();
|
const { isMenuOpen, files: taskFiles } = useTask();
|
||||||
const { selectedFilter, setSelectedFilter, parsedFilterData, isPanelOpen } =
|
const { selectedFilter, setSelectedFilter, parsedFilterData, isPanelOpen } =
|
||||||
useKnowledgeFilter();
|
useKnowledgeFilter();
|
||||||
const [selectedRows, setSelectedRows] = useState<File[]>([]);
|
const [selectedRows, setSelectedRows] = useState<File[]>([]);
|
||||||
|
|
@ -61,14 +55,38 @@ function SearchPage() {
|
||||||
|
|
||||||
const { data = [], isFetching } = useGetSearchQuery(
|
const { data = [], isFetching } = useGetSearchQuery(
|
||||||
parsedFilterData?.query || "*",
|
parsedFilterData?.query || "*",
|
||||||
parsedFilterData
|
parsedFilterData,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTableSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleTableSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
gridRef.current?.api.setGridOption("quickFilterText", e.target.value);
|
gridRef.current?.api.setGridOption("quickFilterText", e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileResults = data as File[];
|
// 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<AgGridReact>(null);
|
const gridRef = useRef<AgGridReact>(null);
|
||||||
|
|
||||||
|
|
@ -82,13 +100,14 @@ function SearchPage() {
|
||||||
minWidth: 220,
|
minWidth: 220,
|
||||||
cellRenderer: ({ data, value }: CustomCellRendererProps<File>) => {
|
cellRenderer: ({ data, value }: CustomCellRendererProps<File>) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<button
|
||||||
className="flex items-center gap-2 cursor-pointer hover:text-blue-600 transition-colors"
|
type="button"
|
||||||
|
className="flex items-center gap-2 cursor-pointer hover:text-blue-600 transition-colors text-left w-full"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
router.push(
|
router.push(
|
||||||
`/knowledge/chunks?filename=${encodeURIComponent(
|
`/knowledge/chunks?filename=${encodeURIComponent(
|
||||||
data?.filename ?? ""
|
data?.filename ?? "",
|
||||||
)}`
|
)}`,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -96,7 +115,7 @@ function SearchPage() {
|
||||||
<span className="font-medium text-foreground truncate">
|
<span className="font-medium text-foreground truncate">
|
||||||
{value}
|
{value}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</button>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -119,6 +138,7 @@ function SearchPage() {
|
||||||
{
|
{
|
||||||
field: "chunkCount",
|
field: "chunkCount",
|
||||||
headerName: "Chunks",
|
headerName: "Chunks",
|
||||||
|
valueFormatter: (params) => params.data?.chunkCount?.toString() || "-",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "avgScore",
|
field: "avgScore",
|
||||||
|
|
@ -127,11 +147,20 @@ function SearchPage() {
|
||||||
cellRenderer: ({ value }: CustomCellRendererProps<File>) => {
|
cellRenderer: ({ value }: CustomCellRendererProps<File>) => {
|
||||||
return (
|
return (
|
||||||
<span className="text-xs text-green-400 bg-green-400/20 px-2 py-1 rounded">
|
<span className="text-xs text-green-400 bg-green-400/20 px-2 py-1 rounded">
|
||||||
{value.toFixed(2)}
|
{value?.toFixed(2) ?? "-"}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: "status",
|
||||||
|
headerName: "Status",
|
||||||
|
cellRenderer: ({ data }: CustomCellRendererProps<File>) => {
|
||||||
|
// Default to 'active' status if no status is provided
|
||||||
|
const status = data?.status || "active";
|
||||||
|
return <StatusBadge status={status} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
cellRenderer: ({ data }: CustomCellRendererProps<File>) => {
|
cellRenderer: ({ data }: CustomCellRendererProps<File>) => {
|
||||||
return <KnowledgeActionsDropdown filename={data?.filename || ""} />;
|
return <KnowledgeActionsDropdown filename={data?.filename || ""} />;
|
||||||
|
|
@ -172,7 +201,7 @@ function SearchPage() {
|
||||||
try {
|
try {
|
||||||
// Delete each file individually since the API expects one filename at a time
|
// Delete each file individually since the API expects one filename at a time
|
||||||
const deletePromises = selectedRows.map((row) =>
|
const deletePromises = selectedRows.map((row) =>
|
||||||
deleteDocumentMutation.mutateAsync({ filename: row.filename })
|
deleteDocumentMutation.mutateAsync({ filename: row.filename }),
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all(deletePromises);
|
await Promise.all(deletePromises);
|
||||||
|
|
@ -180,7 +209,7 @@ function SearchPage() {
|
||||||
toast.success(
|
toast.success(
|
||||||
`Successfully deleted ${selectedRows.length} document${
|
`Successfully deleted ${selectedRows.length} document${
|
||||||
selectedRows.length > 1 ? "s" : ""
|
selectedRows.length > 1 ? "s" : ""
|
||||||
}`
|
}`,
|
||||||
);
|
);
|
||||||
setSelectedRows([]);
|
setSelectedRows([]);
|
||||||
setShowBulkDeleteDialog(false);
|
setShowBulkDeleteDialog(false);
|
||||||
|
|
@ -193,7 +222,7 @@ function SearchPage() {
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
: "Failed to delete some documents"
|
: "Failed to delete some documents",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,13 @@ import { Loader2, PlugZap, RefreshCw } from "lucide-react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { Suspense, useCallback, useEffect, useState } from "react";
|
import { Suspense, useCallback, useEffect, useState } from "react";
|
||||||
import { useUpdateFlowSettingMutation } from "@/app/api/mutations/useUpdateFlowSettingMutation";
|
import { useUpdateFlowSettingMutation } from "@/app/api/mutations/useUpdateFlowSettingMutation";
|
||||||
|
import {
|
||||||
|
useGetIBMModelsQuery,
|
||||||
|
useGetOllamaModelsQuery,
|
||||||
|
useGetOpenAIModelsQuery,
|
||||||
|
} from "@/app/api/queries/useGetModelsQuery";
|
||||||
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
|
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
|
||||||
import { useGetOpenAIModelsQuery, useGetOllamaModelsQuery, useGetIBMModelsQuery } from "@/app/api/queries/useGetModelsQuery";
|
|
||||||
import { ConfirmationDialog } from "@/components/confirmation-dialog";
|
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 { ProtectedRoute } from "@/components/protected-route";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -33,6 +35,8 @@ import { Textarea } from "@/components/ui/textarea";
|
||||||
import { useAuth } from "@/contexts/auth-context";
|
import { useAuth } from "@/contexts/auth-context";
|
||||||
import { useTask } from "@/contexts/task-context";
|
import { useTask } from "@/contexts/task-context";
|
||||||
import { useDebounce } from "@/lib/debounce";
|
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;
|
const MAX_SYSTEM_PROMPT_CHARS = 2000;
|
||||||
|
|
||||||
|
|
@ -105,42 +109,46 @@ function KnowledgeSourcesPage() {
|
||||||
|
|
||||||
// Fetch settings using React Query
|
// Fetch settings using React Query
|
||||||
const { data: settings = {} } = useGetSettingsQuery({
|
const { data: settings = {} } = useGetSettingsQuery({
|
||||||
enabled: isAuthenticated,
|
enabled: isAuthenticated || isNoAuthMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get the current provider from settings
|
// 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
|
// Fetch available models based on provider
|
||||||
const { data: openaiModelsData } = useGetOpenAIModelsQuery(
|
const { data: openaiModelsData } = useGetOpenAIModelsQuery(
|
||||||
undefined, // Let backend use stored API key from configuration
|
undefined, // Let backend use stored API key from configuration
|
||||||
{
|
{
|
||||||
enabled: isAuthenticated && currentProvider === 'openai',
|
enabled:
|
||||||
}
|
(isAuthenticated || isNoAuthMode) && currentProvider === "openai",
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: ollamaModelsData } = useGetOllamaModelsQuery(
|
const { data: ollamaModelsData } = useGetOllamaModelsQuery(
|
||||||
undefined, // No params for now, could be extended later
|
undefined, // No params for now, could be extended later
|
||||||
{
|
{
|
||||||
enabled: isAuthenticated && currentProvider === 'ollama',
|
enabled:
|
||||||
}
|
(isAuthenticated || isNoAuthMode) && currentProvider === "ollama",
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: ibmModelsData } = useGetIBMModelsQuery(
|
const { data: ibmModelsData } = useGetIBMModelsQuery(
|
||||||
undefined, // No params for now, could be extended later
|
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
|
// Select the appropriate models data based on provider
|
||||||
const modelsData = currentProvider === 'openai'
|
const modelsData =
|
||||||
? openaiModelsData
|
currentProvider === "openai"
|
||||||
: currentProvider === 'ollama'
|
? openaiModelsData
|
||||||
? ollamaModelsData
|
: currentProvider === "ollama"
|
||||||
: currentProvider === 'ibm'
|
? ollamaModelsData
|
||||||
? ibmModelsData
|
: currentProvider === "ibm"
|
||||||
: openaiModelsData; // fallback to openai
|
? ibmModelsData
|
||||||
|
: openaiModelsData; // fallback to openai
|
||||||
|
|
||||||
// Mutations
|
// Mutations
|
||||||
const updateFlowSettingMutation = useUpdateFlowSettingMutation({
|
const updateFlowSettingMutation = useUpdateFlowSettingMutation({
|
||||||
|
|
@ -152,7 +160,6 @@ function KnowledgeSourcesPage() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Debounced update function
|
// Debounced update function
|
||||||
const debouncedUpdate = useDebounce(
|
const debouncedUpdate = useDebounce(
|
||||||
(variables: Parameters<typeof updateFlowSettingMutation.mutate>[0]) => {
|
(variables: Parameters<typeof updateFlowSettingMutation.mutate>[0]) => {
|
||||||
|
|
@ -224,7 +231,6 @@ function KnowledgeSourcesPage() {
|
||||||
debouncedUpdate({ doclingPresets: mode });
|
debouncedUpdate({ doclingPresets: mode });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Helper function to get connector icon
|
// Helper function to get connector icon
|
||||||
const getConnectorIcon = useCallback((iconName: string) => {
|
const getConnectorIcon = useCallback((iconName: string) => {
|
||||||
const iconMap: { [key: string]: React.ReactElement } = {
|
const iconMap: { [key: string]: React.ReactElement } = {
|
||||||
|
|
@ -613,7 +619,11 @@ function KnowledgeSourcesPage() {
|
||||||
Language Model
|
Language Model
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={settings.agent?.llm_model || modelsData?.language_models?.find(m => m.default)?.value || "gpt-4"}
|
value={
|
||||||
|
settings.agent?.llm_model ||
|
||||||
|
modelsData?.language_models?.find((m) => m.default)?.value ||
|
||||||
|
"gpt-4"
|
||||||
|
}
|
||||||
onValueChange={handleModelChange}
|
onValueChange={handleModelChange}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="model-select">
|
<SelectTrigger id="model-select">
|
||||||
|
|
@ -638,10 +648,20 @@ function KnowledgeSourcesPage() {
|
||||||
value={systemPrompt}
|
value={systemPrompt}
|
||||||
onChange={(e) => setSystemPrompt(e.target.value)}
|
onChange={(e) => setSystemPrompt(e.target.value)}
|
||||||
rows={6}
|
rows={6}
|
||||||
className={`resize-none ${systemPrompt.length > MAX_SYSTEM_PROMPT_CHARS ? 'border-red-500 focus:border-red-500' : ''}`}
|
className={`resize-none ${
|
||||||
|
systemPrompt.length > MAX_SYSTEM_PROMPT_CHARS
|
||||||
|
? "border-red-500 focus:border-red-500"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-start">
|
||||||
<span className={`text-xs ${systemPrompt.length > MAX_SYSTEM_PROMPT_CHARS ? 'text-red-500' : 'text-muted-foreground'}`}>
|
<span
|
||||||
|
className={`text-xs ${
|
||||||
|
systemPrompt.length > MAX_SYSTEM_PROMPT_CHARS
|
||||||
|
? "text-red-500"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{systemPrompt.length}/{MAX_SYSTEM_PROMPT_CHARS} characters
|
{systemPrompt.length}/{MAX_SYSTEM_PROMPT_CHARS} characters
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -649,7 +669,10 @@ function KnowledgeSourcesPage() {
|
||||||
<div className="flex justify-end pt-2">
|
<div className="flex justify-end pt-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSystemPromptSave}
|
onClick={handleSystemPromptSave}
|
||||||
disabled={updateFlowSettingMutation.isPending || systemPrompt.length > MAX_SYSTEM_PROMPT_CHARS}
|
disabled={
|
||||||
|
updateFlowSettingMutation.isPending ||
|
||||||
|
systemPrompt.length > MAX_SYSTEM_PROMPT_CHARS
|
||||||
|
}
|
||||||
className="min-w-[120px]"
|
className="min-w-[120px]"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -736,7 +759,9 @@ function KnowledgeSourcesPage() {
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={
|
value={
|
||||||
settings.knowledge?.embedding_model || modelsData?.embedding_models?.find(m => m.default)?.value || "text-embedding-ada-002"
|
settings.knowledge?.embedding_model ||
|
||||||
|
modelsData?.embedding_models?.find((m) => m.default)?.value ||
|
||||||
|
"text-embedding-ada-002"
|
||||||
}
|
}
|
||||||
onValueChange={handleEmbeddingModelChange}
|
onValueChange={handleEmbeddingModelChange}
|
||||||
>
|
>
|
||||||
|
|
@ -746,7 +771,9 @@ function KnowledgeSourcesPage() {
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<ModelSelectItems
|
<ModelSelectItems
|
||||||
models={modelsData?.embedding_models}
|
models={modelsData?.embedding_models}
|
||||||
fallbackModels={getFallbackModels(currentProvider).embedding}
|
fallbackModels={
|
||||||
|
getFallbackModels(currentProvider).embedding
|
||||||
|
}
|
||||||
provider={currentProvider}
|
provider={currentProvider}
|
||||||
/>
|
/>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -807,7 +834,10 @@ function KnowledgeSourcesPage() {
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<RadioGroupItem value="standard" id="standard" />
|
<RadioGroupItem value="standard" id="standard" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Label htmlFor="standard" className="text-base font-medium cursor-pointer">
|
<Label
|
||||||
|
htmlFor="standard"
|
||||||
|
className="text-base font-medium cursor-pointer"
|
||||||
|
>
|
||||||
Standard
|
Standard
|
||||||
</Label>
|
</Label>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
|
|
@ -818,18 +848,28 @@ function KnowledgeSourcesPage() {
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<RadioGroupItem value="ocr" id="ocr" />
|
<RadioGroupItem value="ocr" id="ocr" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Label htmlFor="ocr" className="text-base font-medium cursor-pointer">
|
<Label
|
||||||
|
htmlFor="ocr"
|
||||||
|
className="text-base font-medium cursor-pointer"
|
||||||
|
>
|
||||||
Extract text from images
|
Extract text from images
|
||||||
</Label>
|
</Label>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
Uses OCR to extract text from images/PDFs. Ingest is slower when enabled
|
Uses OCR to extract text from images/PDFs. Ingest is
|
||||||
|
slower when enabled
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<RadioGroupItem value="picture_description" id="picture_description" />
|
<RadioGroupItem
|
||||||
|
value="picture_description"
|
||||||
|
id="picture_description"
|
||||||
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Label htmlFor="picture_description" className="text-base font-medium cursor-pointer">
|
<Label
|
||||||
|
htmlFor="picture_description"
|
||||||
|
className="text-base font-medium cursor-pointer"
|
||||||
|
>
|
||||||
Generate Description
|
Generate Description
|
||||||
</Label>
|
</Label>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
|
|
@ -840,11 +880,15 @@ function KnowledgeSourcesPage() {
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<RadioGroupItem value="VLM" id="VLM" />
|
<RadioGroupItem value="VLM" id="VLM" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Label htmlFor="VLM" className="text-base font-medium cursor-pointer">
|
<Label
|
||||||
|
htmlFor="VLM"
|
||||||
|
className="text-base font-medium cursor-pointer"
|
||||||
|
>
|
||||||
AI Vision
|
AI Vision
|
||||||
</Label>
|
</Label>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
Advanced processing with vision language models. Highest quality but most expensive
|
Advanced processing with vision language models. Highest
|
||||||
|
quality but most expensive
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
49
frontend/src/components/ui/animated-processing-icon.tsx
Normal file
49
frontend/src/components/ui/animated-processing-icon.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
interface AnimatedProcessingIconProps {
|
||||||
|
className?: string;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AnimatedProcessingIcon = ({
|
||||||
|
className = "",
|
||||||
|
size = 10,
|
||||||
|
}: AnimatedProcessingIconProps) => {
|
||||||
|
const width = Math.round((size * 6) / 10);
|
||||||
|
const height = size;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
viewBox="0 0 6 10"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
.dot-1 { animation: pulse-wave 1.5s infinite; animation-delay: 0s; }
|
||||||
|
.dot-2 { animation: pulse-wave 1.5s infinite; animation-delay: 0.1s; }
|
||||||
|
.dot-3 { animation: pulse-wave 1.5s infinite; animation-delay: 0.2s; }
|
||||||
|
.dot-4 { animation: pulse-wave 1.5s infinite; animation-delay: 0.3s; }
|
||||||
|
.dot-5 { animation: pulse-wave 1.5s infinite; animation-delay: 0.4s; }
|
||||||
|
|
||||||
|
@keyframes pulse-wave {
|
||||||
|
0%, 60%, 100% {
|
||||||
|
opacity: 0.25;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
<circle className="dot-1" cx="1" cy="5" r="1" fill="currentColor" />
|
||||||
|
<circle className="dot-2" cx="1" cy="9" r="1" fill="currentColor" />
|
||||||
|
<circle className="dot-3" cx="5" cy="1" r="1" fill="currentColor" />
|
||||||
|
<circle className="dot-4" cx="5" cy="5" r="1" fill="currentColor" />
|
||||||
|
<circle className="dot-5" cx="5" cy="9" r="1" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
58
frontend/src/components/ui/status-badge.tsx
Normal file
58
frontend/src/components/ui/status-badge.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { AnimatedProcessingIcon } from "./animated-processing-icon";
|
||||||
|
|
||||||
|
export type Status =
|
||||||
|
| "processing"
|
||||||
|
| "active"
|
||||||
|
| "unavailable"
|
||||||
|
| "hidden"
|
||||||
|
| "sync"
|
||||||
|
| "failed";
|
||||||
|
|
||||||
|
interface StatusBadgeProps {
|
||||||
|
status: Status;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusConfig = {
|
||||||
|
processing: {
|
||||||
|
label: "Processing",
|
||||||
|
className: "text-muted-foreground dark:text-muted-foreground ",
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
label: "Active",
|
||||||
|
className: "text-emerald-600 dark:text-emerald-400 ",
|
||||||
|
},
|
||||||
|
unavailable: {
|
||||||
|
label: "Unavailable",
|
||||||
|
className: "text-red-600 dark:text-red-400 ",
|
||||||
|
},
|
||||||
|
failed: {
|
||||||
|
label: "Failed",
|
||||||
|
className: "text-red-600 dark:text-red-400 ",
|
||||||
|
},
|
||||||
|
hidden: {
|
||||||
|
label: "Hidden",
|
||||||
|
className: "text-zinc-400 dark:text-zinc-500 ",
|
||||||
|
},
|
||||||
|
sync: {
|
||||||
|
label: "Sync",
|
||||||
|
className: "text-amber-700 dark:text-amber-300 underline",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StatusBadge = ({ status, className }: StatusBadgeProps) => {
|
||||||
|
const config = statusConfig[status];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`inline-flex items-center gap-1 ${config.className} ${
|
||||||
|
className || ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{status === "processing" && (
|
||||||
|
<AnimatedProcessingIcon className="text-current mr-2" size={10} />
|
||||||
|
)}
|
||||||
|
{config.label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -35,9 +35,22 @@ export interface Task {
|
||||||
files?: Record<string, Record<string, unknown>>;
|
files?: Record<string, Record<string, unknown>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TaskFile {
|
||||||
|
filename: string;
|
||||||
|
mimetype: string;
|
||||||
|
source_url: string;
|
||||||
|
size: number;
|
||||||
|
connector_type: string;
|
||||||
|
status: "active" | "failed" | "processing";
|
||||||
|
task_id: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
interface TaskContextType {
|
interface TaskContextType {
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
|
files: TaskFile[];
|
||||||
addTask: (taskId: string) => void;
|
addTask: (taskId: string) => void;
|
||||||
|
addFiles: (files: Partial<TaskFile>[], taskId: string) => void;
|
||||||
removeTask: (taskId: string) => void;
|
removeTask: (taskId: string) => void;
|
||||||
refreshTasks: () => Promise<void>;
|
refreshTasks: () => Promise<void>;
|
||||||
cancelTask: (taskId: string) => Promise<void>;
|
cancelTask: (taskId: string) => Promise<void>;
|
||||||
|
|
@ -51,6 +64,7 @@ const TaskContext = createContext<TaskContextType | undefined>(undefined);
|
||||||
|
|
||||||
export function TaskProvider({ children }: { children: React.ReactNode }) {
|
export function TaskProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [tasks, setTasks] = useState<Task[]>([]);
|
const [tasks, setTasks] = useState<Task[]>([]);
|
||||||
|
const [files, setFiles] = useState<TaskFile[]>([]);
|
||||||
const [isPolling, setIsPolling] = useState(false);
|
const [isPolling, setIsPolling] = useState(false);
|
||||||
const [isFetching, setIsFetching] = useState(false);
|
const [isFetching, setIsFetching] = useState(false);
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
|
@ -58,12 +72,32 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const refetchSearch = () => {
|
const refetchSearch = useCallback(() => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["search"],
|
queryKey: ["search"],
|
||||||
exact: false,
|
exact: false,
|
||||||
});
|
});
|
||||||
};
|
}, [queryClient]);
|
||||||
|
|
||||||
|
const addFiles = useCallback(
|
||||||
|
(newFiles: Partial<TaskFile>[], taskId: string) => {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const filesToAdd: TaskFile[] = newFiles.map((file) => ({
|
||||||
|
filename: file.filename || "",
|
||||||
|
mimetype: file.mimetype || "",
|
||||||
|
source_url: file.source_url || "",
|
||||||
|
size: file.size || 0,
|
||||||
|
connector_type: file.connector_type || "local",
|
||||||
|
status: "processing",
|
||||||
|
task_id: taskId,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setFiles((prevFiles) => [...prevFiles, ...filesToAdd]);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const fetchTasks = useCallback(async () => {
|
const fetchTasks = useCallback(async () => {
|
||||||
if (!isAuthenticated && !isNoAuthMode) return;
|
if (!isAuthenticated && !isNoAuthMode) return;
|
||||||
|
|
@ -76,13 +110,87 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
|
||||||
const newTasks = data.tasks || [];
|
const newTasks = data.tasks || [];
|
||||||
|
|
||||||
// Update tasks and check for status changes in the same state update
|
// Update tasks and check for status changes in the same state update
|
||||||
setTasks(prevTasks => {
|
setTasks((prevTasks) => {
|
||||||
// Check for newly completed tasks to show toasts
|
// Check for newly completed tasks to show toasts
|
||||||
if (prevTasks.length > 0) {
|
if (prevTasks.length > 0) {
|
||||||
newTasks.forEach((newTask: Task) => {
|
newTasks.forEach((newTask: Task) => {
|
||||||
const oldTask = prevTasks.find(
|
const oldTask = prevTasks.find(
|
||||||
t => t.task_id === newTask.task_id
|
(t) => t.task_id === newTask.task_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update or add files from task.files if available
|
||||||
|
if (newTask.files && typeof newTask.files === "object") {
|
||||||
|
const taskFileEntries = Object.entries(newTask.files);
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
taskFileEntries.forEach(([filePath, fileInfo]) => {
|
||||||
|
if (typeof fileInfo === "object" && fileInfo) {
|
||||||
|
const fileName = filePath.split("/").pop() || filePath;
|
||||||
|
const fileStatus = fileInfo.status as string;
|
||||||
|
|
||||||
|
// Map backend file status to our TaskFile status
|
||||||
|
let mappedStatus: TaskFile["status"];
|
||||||
|
switch (fileStatus) {
|
||||||
|
case "pending":
|
||||||
|
case "running":
|
||||||
|
mappedStatus = "processing";
|
||||||
|
break;
|
||||||
|
case "completed":
|
||||||
|
mappedStatus = "active";
|
||||||
|
break;
|
||||||
|
case "failed":
|
||||||
|
mappedStatus = "failed";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
mappedStatus = "processing";
|
||||||
|
}
|
||||||
|
|
||||||
|
setFiles((prevFiles) => {
|
||||||
|
const existingFileIndex = prevFiles.findIndex(
|
||||||
|
(f) =>
|
||||||
|
f.source_url === filePath &&
|
||||||
|
f.task_id === newTask.task_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Detect connector type based on file path or other indicators
|
||||||
|
let connectorType = "local";
|
||||||
|
if (filePath.includes("/") && !filePath.startsWith("/")) {
|
||||||
|
// Likely S3 key format (bucket/path/file.ext)
|
||||||
|
connectorType = "s3";
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileEntry: TaskFile = {
|
||||||
|
filename: fileName,
|
||||||
|
mimetype: "", // We don't have this info from the task
|
||||||
|
source_url: filePath,
|
||||||
|
size: 0, // We don't have this info from the task
|
||||||
|
connector_type: connectorType,
|
||||||
|
status: mappedStatus,
|
||||||
|
task_id: newTask.task_id,
|
||||||
|
created_at:
|
||||||
|
typeof fileInfo.created_at === "string"
|
||||||
|
? fileInfo.created_at
|
||||||
|
: now,
|
||||||
|
updated_at:
|
||||||
|
typeof fileInfo.updated_at === "string"
|
||||||
|
? fileInfo.updated_at
|
||||||
|
: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingFileIndex >= 0) {
|
||||||
|
// Update existing file
|
||||||
|
const updatedFiles = [...prevFiles];
|
||||||
|
updatedFiles[existingFileIndex] = fileEntry;
|
||||||
|
return updatedFiles;
|
||||||
|
} else {
|
||||||
|
// Add new file
|
||||||
|
return [...prevFiles, fileEntry];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
oldTask &&
|
oldTask &&
|
||||||
oldTask.status !== "completed" &&
|
oldTask.status !== "completed" &&
|
||||||
|
|
@ -99,9 +207,14 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
|
||||||
refetchSearch();
|
refetchSearch();
|
||||||
// Dispatch knowledge updated event for all knowledge-related pages
|
// Dispatch knowledge updated event for all knowledge-related pages
|
||||||
console.log(
|
console.log(
|
||||||
"Task completed successfully, dispatching knowledgeUpdated event"
|
"Task completed successfully, dispatching knowledgeUpdated event",
|
||||||
);
|
);
|
||||||
window.dispatchEvent(new CustomEvent("knowledgeUpdated"));
|
window.dispatchEvent(new CustomEvent("knowledgeUpdated"));
|
||||||
|
|
||||||
|
// Remove files for this completed task from the files list
|
||||||
|
setFiles((prevFiles) =>
|
||||||
|
prevFiles.filter((file) => file.task_id !== newTask.task_id),
|
||||||
|
);
|
||||||
} else if (
|
} else if (
|
||||||
oldTask &&
|
oldTask &&
|
||||||
oldTask.status !== "failed" &&
|
oldTask.status !== "failed" &&
|
||||||
|
|
@ -114,6 +227,8 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
|
||||||
newTask.error || "Unknown error"
|
newTask.error || "Unknown error"
|
||||||
}`,
|
}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Files will be updated to failed status by the file parsing logic above
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -126,7 +241,7 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
|
||||||
} finally {
|
} finally {
|
||||||
setIsFetching(false);
|
setIsFetching(false);
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, isNoAuthMode]); // Removed 'tasks' from dependencies to prevent infinite loop!
|
}, [isAuthenticated, isNoAuthMode, refetchSearch]); // Removed 'tasks' from dependencies to prevent infinite loop!
|
||||||
|
|
||||||
const addTask = useCallback((taskId: string) => {
|
const addTask = useCallback((taskId: string) => {
|
||||||
// Immediately start aggressive polling for the new task
|
// Immediately start aggressive polling for the new task
|
||||||
|
|
@ -140,19 +255,21 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const newTasks = data.tasks || [];
|
const newTasks = data.tasks || [];
|
||||||
const foundTask = newTasks.find(
|
const foundTask = newTasks.find(
|
||||||
(task: Task) => task.task_id === taskId
|
(task: Task) => task.task_id === taskId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (foundTask) {
|
if (foundTask) {
|
||||||
// Task found! Update the tasks state
|
// Task found! Update the tasks state
|
||||||
setTasks(prevTasks => {
|
setTasks((prevTasks) => {
|
||||||
// Check if task is already in the list
|
// Check if task is already in the list
|
||||||
const exists = prevTasks.some(t => t.task_id === taskId);
|
const exists = prevTasks.some((t) => t.task_id === taskId);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
return [...prevTasks, foundTask];
|
return [...prevTasks, foundTask];
|
||||||
}
|
}
|
||||||
// Update existing task
|
// Update existing task
|
||||||
return prevTasks.map(t => (t.task_id === taskId ? foundTask : t));
|
return prevTasks.map((t) =>
|
||||||
|
t.task_id === taskId ? foundTask : t,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
return; // Stop polling, we found it
|
return; // Stop polling, we found it
|
||||||
}
|
}
|
||||||
|
|
@ -177,7 +294,7 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
|
||||||
}, [fetchTasks]);
|
}, [fetchTasks]);
|
||||||
|
|
||||||
const removeTask = useCallback((taskId: string) => {
|
const removeTask = useCallback((taskId: string) => {
|
||||||
setTasks(prev => prev.filter(task => task.task_id !== taskId));
|
setTasks((prev) => prev.filter((task) => task.task_id !== taskId));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const cancelTask = useCallback(
|
const cancelTask = useCallback(
|
||||||
|
|
@ -204,11 +321,11 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[fetchTasks]
|
[fetchTasks],
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleMenu = useCallback(() => {
|
const toggleMenu = useCallback(() => {
|
||||||
setIsMenuOpen(prev => !prev);
|
setIsMenuOpen((prev) => !prev);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Periodic polling for task updates
|
// Periodic polling for task updates
|
||||||
|
|
@ -231,7 +348,9 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
|
||||||
const value: TaskContextType = {
|
const value: TaskContextType = {
|
||||||
tasks,
|
tasks,
|
||||||
|
files,
|
||||||
addTask,
|
addTask,
|
||||||
|
addFiles,
|
||||||
removeTask,
|
removeTask,
|
||||||
refreshTasks,
|
refreshTasks,
|
||||||
cancelTask,
|
cancelTask,
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,18 @@ async def get_openai_models(request, models_service, session_manager):
|
||||||
try:
|
try:
|
||||||
config = get_openrag_config()
|
config = get_openrag_config()
|
||||||
api_key = config.provider.api_key
|
api_key = config.provider.api_key
|
||||||
logger.info(f"Retrieved API key from config: {'yes' if api_key else 'no'}")
|
logger.info(
|
||||||
|
f"Retrieved API key from config: {'yes' if api_key else 'no'}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to get config: {e}")
|
logger.error(f"Failed to get config: {e}")
|
||||||
|
|
||||||
if not api_key:
|
if not api_key:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
{"error": "OpenAI API key is required either as query parameter or in configuration"},
|
{
|
||||||
status_code=400
|
"error": "OpenAI API key is required either as query parameter or in configuration"
|
||||||
|
},
|
||||||
|
status_code=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
models = await models_service.get_openai_models(api_key=api_key)
|
models = await models_service.get_openai_models(api_key=api_key)
|
||||||
|
|
@ -32,8 +36,7 @@ async def get_openai_models(request, models_service, session_manager):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to get OpenAI models: {str(e)}")
|
logger.error(f"Failed to get OpenAI models: {str(e)}")
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
{"error": f"Failed to retrieve OpenAI models: {str(e)}"},
|
{"error": f"Failed to retrieve OpenAI models: {str(e)}"}, status_code=500
|
||||||
status_code=500
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -44,13 +47,31 @@ async def get_ollama_models(request, models_service, session_manager):
|
||||||
query_params = dict(request.query_params)
|
query_params = dict(request.query_params)
|
||||||
endpoint = query_params.get("endpoint")
|
endpoint = query_params.get("endpoint")
|
||||||
|
|
||||||
|
# If no API key provided, try to get it from stored configuration
|
||||||
|
if not endpoint:
|
||||||
|
try:
|
||||||
|
config = get_openrag_config()
|
||||||
|
endpoint = config.provider.endpoint
|
||||||
|
logger.info(
|
||||||
|
f"Retrieved endpoint from config: {'yes' if endpoint else 'no'}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get config: {e}")
|
||||||
|
|
||||||
|
if not endpoint:
|
||||||
|
return JSONResponse(
|
||||||
|
{
|
||||||
|
"error": "Endpoint is required either as query parameter or in configuration"
|
||||||
|
},
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
models = await models_service.get_ollama_models(endpoint=endpoint)
|
models = await models_service.get_ollama_models(endpoint=endpoint)
|
||||||
return JSONResponse(models)
|
return JSONResponse(models)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to get Ollama models: {str(e)}")
|
logger.error(f"Failed to get Ollama models: {str(e)}")
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
{"error": f"Failed to retrieve Ollama models: {str(e)}"},
|
{"error": f"Failed to retrieve Ollama models: {str(e)}"}, status_code=500
|
||||||
status_code=500
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -63,15 +84,65 @@ async def get_ibm_models(request, models_service, session_manager):
|
||||||
api_key = query_params.get("api_key")
|
api_key = query_params.get("api_key")
|
||||||
project_id = query_params.get("project_id")
|
project_id = query_params.get("project_id")
|
||||||
|
|
||||||
|
config = get_openrag_config()
|
||||||
|
# If no API key provided, try to get it from stored configuration
|
||||||
|
if not api_key:
|
||||||
|
try:
|
||||||
|
api_key = config.provider.api_key
|
||||||
|
logger.info(
|
||||||
|
f"Retrieved API key from config: {'yes' if api_key else 'no'}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get config: {e}")
|
||||||
|
|
||||||
|
if not api_key:
|
||||||
|
return JSONResponse(
|
||||||
|
{
|
||||||
|
"error": "OpenAI API key is required either as query parameter or in configuration"
|
||||||
|
},
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not endpoint:
|
||||||
|
try:
|
||||||
|
endpoint = config.provider.endpoint
|
||||||
|
logger.info(
|
||||||
|
f"Retrieved endpoint from config: {'yes' if endpoint else 'no'}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get config: {e}")
|
||||||
|
|
||||||
|
if not endpoint:
|
||||||
|
return JSONResponse(
|
||||||
|
{
|
||||||
|
"error": "Endpoint is required either as query parameter or in configuration"
|
||||||
|
},
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not project_id:
|
||||||
|
try:
|
||||||
|
project_id = config.provider.project_id
|
||||||
|
logger.info(
|
||||||
|
f"Retrieved project ID from config: {'yes' if project_id else 'no'}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get config: {e}")
|
||||||
|
|
||||||
|
if not project_id:
|
||||||
|
return JSONResponse(
|
||||||
|
{
|
||||||
|
"error": "Project ID is required either as query parameter or in configuration"
|
||||||
|
},
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
models = await models_service.get_ibm_models(
|
models = await models_service.get_ibm_models(
|
||||||
endpoint=endpoint,
|
endpoint=endpoint, api_key=api_key, project_id=project_id
|
||||||
api_key=api_key,
|
|
||||||
project_id=project_id
|
|
||||||
)
|
)
|
||||||
return JSONResponse(models)
|
return JSONResponse(models)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to get IBM models: {str(e)}")
|
logger.error(f"Failed to get IBM models: {str(e)}")
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
{"error": f"Failed to retrieve IBM models: {str(e)}"},
|
{"error": f"Failed to retrieve IBM models: {str(e)}"}, status_code=500
|
||||||
status_code=500
|
)
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,9 @@ class TaskService:
|
||||||
def __init__(self, document_service=None, process_pool=None):
|
def __init__(self, document_service=None, process_pool=None):
|
||||||
self.document_service = document_service
|
self.document_service = document_service
|
||||||
self.process_pool = process_pool
|
self.process_pool = process_pool
|
||||||
self.task_store: dict[str, dict[str, UploadTask]] = {} # user_id -> {task_id -> UploadTask}
|
self.task_store: dict[
|
||||||
|
str, dict[str, UploadTask]
|
||||||
|
] = {} # user_id -> {task_id -> UploadTask}
|
||||||
self.background_tasks = set()
|
self.background_tasks = set()
|
||||||
|
|
||||||
if self.process_pool is None:
|
if self.process_pool is None:
|
||||||
|
|
@ -122,18 +124,27 @@ class TaskService:
|
||||||
|
|
||||||
# Process files with limited concurrency to avoid overwhelming the system
|
# Process files with limited concurrency to avoid overwhelming the system
|
||||||
max_workers = get_worker_count()
|
max_workers = get_worker_count()
|
||||||
semaphore = asyncio.Semaphore(max_workers * 2) # Allow 2x process pool size for async I/O
|
semaphore = asyncio.Semaphore(
|
||||||
|
max_workers * 2
|
||||||
|
) # Allow 2x process pool size for async I/O
|
||||||
|
|
||||||
async def process_with_semaphore(file_path: str):
|
async def process_with_semaphore(file_path: str):
|
||||||
async with semaphore:
|
async with semaphore:
|
||||||
await self.document_service.process_single_file_task(upload_task, file_path)
|
await self.document_service.process_single_file_task(
|
||||||
|
upload_task, file_path
|
||||||
|
)
|
||||||
|
|
||||||
tasks = [process_with_semaphore(file_path) for file_path in upload_task.file_tasks.keys()]
|
tasks = [
|
||||||
|
process_with_semaphore(file_path)
|
||||||
|
for file_path in upload_task.file_tasks.keys()
|
||||||
|
]
|
||||||
|
|
||||||
await asyncio.gather(*tasks, return_exceptions=True)
|
await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Background upload processor failed", task_id=task_id, error=str(e))
|
logger.error(
|
||||||
|
"Background upload processor failed", task_id=task_id, error=str(e)
|
||||||
|
)
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
@ -141,7 +152,9 @@ class TaskService:
|
||||||
self.task_store[user_id][task_id].status = TaskStatus.FAILED
|
self.task_store[user_id][task_id].status = TaskStatus.FAILED
|
||||||
self.task_store[user_id][task_id].updated_at = time.time()
|
self.task_store[user_id][task_id].updated_at = time.time()
|
||||||
|
|
||||||
async def background_custom_processor(self, user_id: str, task_id: str, items: list) -> None:
|
async def background_custom_processor(
|
||||||
|
self, user_id: str, task_id: str, items: list
|
||||||
|
) -> None:
|
||||||
"""Background task to process items using custom processor"""
|
"""Background task to process items using custom processor"""
|
||||||
try:
|
try:
|
||||||
upload_task = self.task_store[user_id][task_id]
|
upload_task = self.task_store[user_id][task_id]
|
||||||
|
|
@ -163,7 +176,9 @@ class TaskService:
|
||||||
try:
|
try:
|
||||||
await processor.process_item(upload_task, item, file_task)
|
await processor.process_item(upload_task, item, file_task)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to process item", item=str(item), error=str(e))
|
logger.error(
|
||||||
|
"Failed to process item", item=str(item), error=str(e)
|
||||||
|
)
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
@ -190,7 +205,9 @@ class TaskService:
|
||||||
pass
|
pass
|
||||||
raise # Re-raise to properly handle cancellation
|
raise # Re-raise to properly handle cancellation
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Background custom processor failed", task_id=task_id, error=str(e))
|
logger.error(
|
||||||
|
"Background custom processor failed", task_id=task_id, error=str(e)
|
||||||
|
)
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
@ -212,7 +229,10 @@ class TaskService:
|
||||||
|
|
||||||
upload_task = None
|
upload_task = None
|
||||||
for candidate_user_id in candidate_user_ids:
|
for candidate_user_id in candidate_user_ids:
|
||||||
if candidate_user_id in self.task_store and task_id in self.task_store[candidate_user_id]:
|
if (
|
||||||
|
candidate_user_id in self.task_store
|
||||||
|
and task_id in self.task_store[candidate_user_id]
|
||||||
|
):
|
||||||
upload_task = self.task_store[candidate_user_id][task_id]
|
upload_task = self.task_store[candidate_user_id][task_id]
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -271,10 +291,23 @@ class TaskService:
|
||||||
if task_id in tasks_by_id:
|
if task_id in tasks_by_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Calculate running and pending counts
|
# Calculate running and pending counts and build file statuses
|
||||||
running_files_count = 0
|
running_files_count = 0
|
||||||
pending_files_count = 0
|
pending_files_count = 0
|
||||||
for file_task in upload_task.file_tasks.values():
|
file_statuses = {}
|
||||||
|
|
||||||
|
for file_path, file_task in upload_task.file_tasks.items():
|
||||||
|
if file_task.status.value != "completed":
|
||||||
|
file_statuses[file_path] = {
|
||||||
|
"status": file_task.status.value,
|
||||||
|
"result": file_task.result,
|
||||||
|
"error": file_task.error,
|
||||||
|
"retry_count": file_task.retry_count,
|
||||||
|
"created_at": file_task.created_at,
|
||||||
|
"updated_at": file_task.updated_at,
|
||||||
|
"duration_seconds": file_task.duration_seconds,
|
||||||
|
}
|
||||||
|
|
||||||
if file_task.status.value == "running":
|
if file_task.status.value == "running":
|
||||||
running_files_count += 1
|
running_files_count += 1
|
||||||
elif file_task.status.value == "pending":
|
elif file_task.status.value == "pending":
|
||||||
|
|
@ -292,6 +325,7 @@ class TaskService:
|
||||||
"created_at": upload_task.created_at,
|
"created_at": upload_task.created_at,
|
||||||
"updated_at": upload_task.updated_at,
|
"updated_at": upload_task.updated_at,
|
||||||
"duration_seconds": upload_task.duration_seconds,
|
"duration_seconds": upload_task.duration_seconds,
|
||||||
|
"files": file_statuses,
|
||||||
}
|
}
|
||||||
|
|
||||||
# First, add user-owned tasks; then shared anonymous;
|
# First, add user-owned tasks; then shared anonymous;
|
||||||
|
|
@ -312,7 +346,10 @@ class TaskService:
|
||||||
|
|
||||||
store_user_id = None
|
store_user_id = None
|
||||||
for candidate_user_id in candidate_user_ids:
|
for candidate_user_id in candidate_user_ids:
|
||||||
if candidate_user_id in self.task_store and task_id in self.task_store[candidate_user_id]:
|
if (
|
||||||
|
candidate_user_id in self.task_store
|
||||||
|
and task_id in self.task_store[candidate_user_id]
|
||||||
|
):
|
||||||
store_user_id = candidate_user_id
|
store_user_id = candidate_user_id
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -326,7 +363,10 @@ class TaskService:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Cancel the background task to stop scheduling new work
|
# Cancel the background task to stop scheduling new work
|
||||||
if hasattr(upload_task, "background_task") and not upload_task.background_task.done():
|
if (
|
||||||
|
hasattr(upload_task, "background_task")
|
||||||
|
and not upload_task.background_task.done()
|
||||||
|
):
|
||||||
upload_task.background_task.cancel()
|
upload_task.background_task.cancel()
|
||||||
|
|
||||||
# Mark task as failed (cancelled)
|
# Mark task as failed (cancelled)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue