Merge remote-tracking branch 'origin/fix/chat_filter_clearing' into all-merges
This commit is contained in:
commit
2c9e8c520e
12 changed files with 1952 additions and 1652 deletions
|
|
@ -4,6 +4,7 @@ import {
|
|||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import type { EndpointType } from "@/contexts/chat-context";
|
||||
import { useChat } from "@/contexts/chat-context";
|
||||
|
||||
export interface RawConversation {
|
||||
response_id: string;
|
||||
|
|
@ -50,6 +51,7 @@ export const useGetConversationsQuery = (
|
|||
options?: Omit<UseQueryOptions, "queryKey" | "queryFn">,
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { isOnboardingComplete } = useChat();
|
||||
|
||||
async function getConversations(context: { signal?: AbortSignal }): Promise<ChatConversation[]> {
|
||||
try {
|
||||
|
|
@ -95,6 +97,11 @@ export const useGetConversationsQuery = (
|
|||
}
|
||||
}
|
||||
|
||||
// Extract enabled from options and combine with onboarding completion check
|
||||
// Query is only enabled if onboarding is complete AND the caller's enabled condition is met
|
||||
const callerEnabled = options?.enabled ?? true;
|
||||
const enabled = isOnboardingComplete && callerEnabled;
|
||||
|
||||
const queryResult = useQuery(
|
||||
{
|
||||
queryKey: ["conversations", endpoint, refreshTrigger],
|
||||
|
|
@ -106,6 +113,7 @@ export const useGetConversationsQuery = (
|
|||
refetchOnMount: false, // Don't refetch on every mount
|
||||
refetchOnWindowFocus: false, // Don't refetch when window regains focus
|
||||
...options,
|
||||
enabled, // Override enabled after spreading options to ensure onboarding check is applied
|
||||
},
|
||||
queryClient,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import {
|
|||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { useChat } from "@/contexts/chat-context";
|
||||
import { useProviderHealthQuery } from "./useProviderHealthQuery";
|
||||
|
||||
type Nudge = string;
|
||||
|
||||
|
|
@ -27,6 +29,13 @@ export const useGetNudgesQuery = (
|
|||
) => {
|
||||
const { chatId, filters, limit, scoreThreshold } = params ?? {};
|
||||
const queryClient = useQueryClient();
|
||||
const { isOnboardingComplete } = useChat();
|
||||
|
||||
// Check if LLM provider is healthy
|
||||
// If health data is not available yet, assume healthy (optimistic)
|
||||
// Only disable if health data exists and shows LLM error
|
||||
const { data: health } = useProviderHealthQuery();
|
||||
const isLLMHealthy = health === undefined || (health?.status === "healthy" && !health?.llm_error);
|
||||
|
||||
function cancel() {
|
||||
queryClient.removeQueries({
|
||||
|
|
@ -77,6 +86,11 @@ export const useGetNudgesQuery = (
|
|||
}
|
||||
}
|
||||
|
||||
// Extract enabled from options and combine with onboarding completion and LLM health checks
|
||||
// Query is only enabled if onboarding is complete AND LLM provider is healthy AND the caller's enabled condition is met
|
||||
const callerEnabled = options?.enabled ?? true;
|
||||
const enabled = isOnboardingComplete && isLLMHealthy && callerEnabled;
|
||||
|
||||
const queryResult = useQuery(
|
||||
{
|
||||
queryKey: ["nudges", chatId, filters, limit, scoreThreshold],
|
||||
|
|
@ -91,6 +105,7 @@ export const useGetNudgesQuery = (
|
|||
return Array.isArray(data) && data.length === 0 ? 5000 : false;
|
||||
},
|
||||
...options,
|
||||
enabled, // Override enabled after spreading options to ensure onboarding check is applied
|
||||
},
|
||||
queryClient,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
} from "@tanstack/react-query";
|
||||
import { useChat } from "@/contexts/chat-context";
|
||||
import { useGetSettingsQuery } from "./useGetSettingsQuery";
|
||||
import { useGetTasksQuery } from "./useGetTasksQuery";
|
||||
|
||||
export interface ProviderHealthDetails {
|
||||
llm_model: string;
|
||||
|
|
@ -40,11 +41,20 @@ export const useProviderHealthQuery = (
|
|||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Get chat error state from context (ChatProvider wraps the entire app in layout.tsx)
|
||||
const { hasChatError, setChatError } = useChat();
|
||||
// Get chat error state and onboarding completion from context (ChatProvider wraps the entire app in layout.tsx)
|
||||
const { hasChatError, setChatError, isOnboardingComplete } = useChat();
|
||||
|
||||
const { data: settings = {} } = useGetSettingsQuery();
|
||||
|
||||
// Check if there are any active ingestion tasks
|
||||
const { data: tasks = [] } = useGetTasksQuery();
|
||||
const hasActiveIngestion = tasks.some(
|
||||
(task) =>
|
||||
task.status === "pending" ||
|
||||
task.status === "running" ||
|
||||
task.status === "processing",
|
||||
);
|
||||
|
||||
async function checkProviderHealth(): Promise<ProviderHealthResponse> {
|
||||
try {
|
||||
const url = new URL("/api/provider/health", window.location.origin);
|
||||
|
|
@ -55,6 +65,7 @@ export const useProviderHealthQuery = (
|
|||
}
|
||||
|
||||
// Add test_completion query param if specified or if chat error exists
|
||||
// Use the same testCompletion value that's in the queryKey
|
||||
const testCompletion = params?.test_completion ?? hasChatError;
|
||||
if (testCompletion) {
|
||||
url.searchParams.set("test_completion", "true");
|
||||
|
|
@ -101,7 +112,10 @@ export const useProviderHealthQuery = (
|
|||
}
|
||||
}
|
||||
|
||||
const queryKey = ["provider", "health", params?.test_completion];
|
||||
// Include hasChatError in queryKey so React Query refetches when it changes
|
||||
// This ensures the health check runs with test_completion=true when chat errors occur
|
||||
const testCompletion = params?.test_completion ?? hasChatError;
|
||||
const queryKey = ["provider", "health", testCompletion, hasChatError];
|
||||
const failureCountKey = queryKey.join("-");
|
||||
|
||||
const queryResult = useQuery(
|
||||
|
|
@ -143,7 +157,11 @@ export const useProviderHealthQuery = (
|
|||
refetchOnWindowFocus: false, // Disabled to reduce unnecessary calls on tab switches
|
||||
refetchOnMount: true,
|
||||
staleTime: 30000, // Consider data stale after 30 seconds
|
||||
enabled: !!settings?.edited && options?.enabled !== false, // Only run after onboarding is complete
|
||||
enabled:
|
||||
!!settings?.edited &&
|
||||
isOnboardingComplete &&
|
||||
!hasActiveIngestion && // Disable health checks when ingestion is happening
|
||||
options?.enabled !== false, // Only run after onboarding is complete
|
||||
...options,
|
||||
},
|
||||
queryClient,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { useTask } from "@/contexts/task-context";
|
|||
import { useChatStreaming } from "@/hooks/useChatStreaming";
|
||||
import { FILE_CONFIRMATION, FILES_REGEX } from "@/lib/constants";
|
||||
import { useLoadingStore } from "@/stores/loadingStore";
|
||||
import { useGetConversationsQuery } from "../api/queries/useGetConversationsQuery";
|
||||
import { useGetNudgesQuery } from "../api/queries/useGetNudgesQuery";
|
||||
import { AssistantMessage } from "./_components/assistant-message";
|
||||
import { ChatInput, type ChatInputHandle } from "./_components/chat-input";
|
||||
|
|
@ -36,6 +37,7 @@ function ChatPage() {
|
|||
forkFromResponse,
|
||||
refreshConversations,
|
||||
refreshConversationsSilent,
|
||||
refreshTrigger,
|
||||
previousResponseIds,
|
||||
setPreviousResponseIds,
|
||||
placeholderConversation,
|
||||
|
|
@ -71,6 +73,14 @@ function ChatPage() {
|
|||
const lastLoadedConversationRef = useRef<string | null>(null);
|
||||
const { addTask } = useTask();
|
||||
|
||||
console.log(endpoint, refreshTrigger);
|
||||
|
||||
// Check if chat history is loading
|
||||
const { isLoading: isConversationsLoading } = useGetConversationsQuery(
|
||||
endpoint,
|
||||
refreshTrigger,
|
||||
);
|
||||
|
||||
// Use conversation-specific filter instead of global filter
|
||||
const selectedFilter = conversationFilter;
|
||||
|
||||
|
|
@ -116,7 +126,12 @@ function ChatPage() {
|
|||
if (conversationFilter && typeof window !== "undefined") {
|
||||
const newKey = `conversation_filter_${responseId}`;
|
||||
localStorage.setItem(newKey, conversationFilter.id);
|
||||
console.log("[CHAT] Saved filter association:", newKey, "=", conversationFilter.id);
|
||||
console.log(
|
||||
"[CHAT] Saved filter association:",
|
||||
newKey,
|
||||
"=",
|
||||
conversationFilter.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -507,6 +522,9 @@ function ChatPage() {
|
|||
setTimeout(() => {
|
||||
chatInputRef.current?.focusInput();
|
||||
}, 100);
|
||||
} else if (!conversationData) {
|
||||
// No conversation selected (new conversation)
|
||||
lastLoadedConversationRef.current = null;
|
||||
}
|
||||
}, [
|
||||
conversationData,
|
||||
|
|
@ -677,7 +695,7 @@ function ChatPage() {
|
|||
scoreThreshold: parsedFilterData?.scoreThreshold ?? 0,
|
||||
},
|
||||
{
|
||||
enabled: isOnboardingComplete, // Only fetch nudges after onboarding is complete
|
||||
enabled: isOnboardingComplete && !isConversationsLoading, // Only fetch nudges after onboarding is complete AND chat history is not loading
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -841,7 +859,12 @@ function ChatPage() {
|
|||
|
||||
// Store the response ID if present for this endpoint
|
||||
if (result.response_id) {
|
||||
console.log("[DEBUG] Received response_id:", result.response_id, "currentConversationId:", currentConversationId);
|
||||
console.log(
|
||||
"[DEBUG] Received response_id:",
|
||||
result.response_id,
|
||||
"currentConversationId:",
|
||||
currentConversationId,
|
||||
);
|
||||
|
||||
setPreviousResponseIds((prev) => ({
|
||||
...prev,
|
||||
|
|
@ -850,11 +873,16 @@ function ChatPage() {
|
|||
|
||||
// If this is a new conversation (no currentConversationId), set it now
|
||||
if (!currentConversationId) {
|
||||
console.log("[DEBUG] Setting currentConversationId to:", result.response_id);
|
||||
console.log(
|
||||
"[DEBUG] Setting currentConversationId to:",
|
||||
result.response_id,
|
||||
);
|
||||
setCurrentConversationId(result.response_id);
|
||||
refreshConversations(true);
|
||||
} else {
|
||||
console.log("[DEBUG] Existing conversation, doing silent refresh");
|
||||
console.log(
|
||||
"[DEBUG] Existing conversation, doing silent refresh",
|
||||
);
|
||||
// For existing conversations, do a silent refresh to keep backend in sync
|
||||
refreshConversationsSilent();
|
||||
}
|
||||
|
|
@ -863,7 +891,12 @@ function ChatPage() {
|
|||
if (conversationFilter && typeof window !== "undefined") {
|
||||
const newKey = `conversation_filter_${result.response_id}`;
|
||||
localStorage.setItem(newKey, conversationFilter.id);
|
||||
console.log("[DEBUG] Saved filter association:", newKey, "=", conversationFilter.id);
|
||||
console.log(
|
||||
"[DEBUG] Saved filter association:",
|
||||
newKey,
|
||||
"=",
|
||||
conversationFilter.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import IBMLogo from "@/components/icons/ibm-logo";
|
||||
import { LabelInput } from "@/components/label-input";
|
||||
import { LabelWrapper } from "@/components/label-wrapper";
|
||||
import IBMLogo from "@/components/icons/ibm-logo";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Tooltip,
|
||||
|
|
@ -39,14 +39,16 @@ export function IBMOnboarding({
|
|||
hasEnvApiKey?: boolean;
|
||||
}) {
|
||||
const [endpoint, setEndpoint] = useState(
|
||||
alreadyConfigured ? "" : (existingEndpoint || "https://us-south.ml.cloud.ibm.com"),
|
||||
alreadyConfigured
|
||||
? ""
|
||||
: existingEndpoint || "https://us-south.ml.cloud.ibm.com",
|
||||
);
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [getFromEnv, setGetFromEnv] = useState(
|
||||
hasEnvApiKey && !alreadyConfigured,
|
||||
);
|
||||
const [projectId, setProjectId] = useState(
|
||||
alreadyConfigured ? "" : (existingProjectId || ""),
|
||||
alreadyConfigured ? "" : existingProjectId || "",
|
||||
);
|
||||
|
||||
const options = [
|
||||
|
|
@ -93,14 +95,12 @@ export function IBMOnboarding({
|
|||
} = useGetIBMModelsQuery(
|
||||
{
|
||||
endpoint: debouncedEndpoint ? debouncedEndpoint : undefined,
|
||||
apiKey: getFromEnv ? "" : (debouncedApiKey ? debouncedApiKey : undefined),
|
||||
apiKey: getFromEnv ? "" : debouncedApiKey ? debouncedApiKey : undefined,
|
||||
projectId: debouncedProjectId ? debouncedProjectId : undefined,
|
||||
},
|
||||
{
|
||||
enabled:
|
||||
!!debouncedEndpoint ||
|
||||
!!debouncedApiKey ||
|
||||
!!debouncedProjectId ||
|
||||
(!!debouncedEndpoint && !!debouncedApiKey && !!debouncedProjectId) ||
|
||||
getFromEnv ||
|
||||
alreadyConfigured,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -507,7 +507,7 @@ const OnboardingCard = ({
|
|||
hasEnvApiKey={
|
||||
currentSettings?.providers?.openai?.has_api_key === true
|
||||
}
|
||||
alreadyConfigured={providerAlreadyConfigured}
|
||||
alreadyConfigured={providerAlreadyConfigured && modelProvider === "openai"}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="watsonx">
|
||||
|
|
@ -517,7 +517,7 @@ const OnboardingCard = ({
|
|||
setSampleDataset={setSampleDataset}
|
||||
setIsLoadingModels={setIsLoadingModels}
|
||||
isEmbedding={isEmbedding}
|
||||
alreadyConfigured={providerAlreadyConfigured}
|
||||
alreadyConfigured={providerAlreadyConfigured && modelProvider === "watsonx"}
|
||||
existingEndpoint={currentSettings?.providers?.watsonx?.endpoint}
|
||||
existingProjectId={currentSettings?.providers?.watsonx?.project_id}
|
||||
hasEnvApiKey={currentSettings?.providers?.watsonx?.has_api_key === true}
|
||||
|
|
@ -530,7 +530,7 @@ const OnboardingCard = ({
|
|||
setSampleDataset={setSampleDataset}
|
||||
setIsLoadingModels={setIsLoadingModels}
|
||||
isEmbedding={isEmbedding}
|
||||
alreadyConfigured={providerAlreadyConfigured}
|
||||
alreadyConfigured={providerAlreadyConfigured && modelProvider === "ollama"}
|
||||
existingEndpoint={currentSettings?.providers?.ollama?.endpoint}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
|
|||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [currentStep, setCurrentStep] = useState<number | null>(null);
|
||||
const [uploadedFilename, setUploadedFilename] = useState<string | null>(null);
|
||||
const [uploadedTaskId, setUploadedTaskId] = useState<string | null>(null);
|
||||
const [shouldCreateFilter, setShouldCreateFilter] = useState(false);
|
||||
const [isCreatingFilter, setIsCreatingFilter] = useState(false);
|
||||
|
||||
|
|
@ -43,23 +44,26 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
|
|||
|
||||
// Monitor tasks and call onComplete when file processing is done
|
||||
useEffect(() => {
|
||||
if (currentStep === null || !tasks) {
|
||||
if (currentStep === null || !tasks || !uploadedTaskId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there are any active tasks (pending, running, or processing)
|
||||
const activeTasks = tasks.find(
|
||||
(task) =>
|
||||
task.status === "pending" ||
|
||||
task.status === "running" ||
|
||||
task.status === "processing",
|
||||
);
|
||||
// Find the task by task ID from the upload response
|
||||
const matchingTask = tasks.find((task) => task.task_id === uploadedTaskId);
|
||||
|
||||
// If no active tasks and we have more than 1 task (initial + new upload), complete it
|
||||
if (
|
||||
(!activeTasks || (activeTasks.processed_files ?? 0) > 0) &&
|
||||
tasks.length > 1
|
||||
) {
|
||||
// If no matching task found, wait for it to appear
|
||||
if (!matchingTask) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the matching task is still active (pending, running, or processing)
|
||||
const isTaskActive =
|
||||
matchingTask.status === "pending" ||
|
||||
matchingTask.status === "running" ||
|
||||
matchingTask.status === "processing";
|
||||
|
||||
// If task is completed or has processed files, complete the onboarding step
|
||||
if (!isTaskActive || (matchingTask.processed_files ?? 0) > 0) {
|
||||
// Set to final step to show "Done"
|
||||
setCurrentStep(STEP_LIST.length);
|
||||
|
||||
|
|
@ -91,6 +95,7 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
|
|||
icon: "file",
|
||||
});
|
||||
|
||||
// Wait for filter creation to complete before proceeding
|
||||
createFilterMutation
|
||||
.mutateAsync({
|
||||
name: displayName,
|
||||
|
|
@ -114,9 +119,16 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
|
|||
})
|
||||
.finally(() => {
|
||||
setIsCreatingFilter(false);
|
||||
});
|
||||
}
|
||||
// Refetch nudges to get new ones
|
||||
refetchNudges();
|
||||
|
||||
// Wait a bit before completing (after filter is created)
|
||||
setTimeout(() => {
|
||||
onComplete();
|
||||
}, 1000);
|
||||
});
|
||||
} else {
|
||||
// No filter to create, just complete
|
||||
// Refetch nudges to get new ones
|
||||
refetchNudges();
|
||||
|
||||
|
|
@ -125,7 +137,18 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
|
|||
onComplete();
|
||||
}, 1000);
|
||||
}
|
||||
}, [tasks, currentStep, onComplete, refetchNudges, shouldCreateFilter, uploadedFilename]);
|
||||
}
|
||||
}, [
|
||||
tasks,
|
||||
currentStep,
|
||||
onComplete,
|
||||
refetchNudges,
|
||||
shouldCreateFilter,
|
||||
uploadedFilename,
|
||||
uploadedTaskId,
|
||||
createFilterMutation,
|
||||
isCreatingFilter,
|
||||
]);
|
||||
|
||||
const resetFileInput = () => {
|
||||
if (fileInputRef.current) {
|
||||
|
|
@ -144,6 +167,11 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
|
|||
const result = await uploadFile(file, true, true); // Pass createFilter=true
|
||||
console.log("Document upload task started successfully");
|
||||
|
||||
// Store task ID to track the specific upload task
|
||||
if (result.taskId) {
|
||||
setUploadedTaskId(result.taskId);
|
||||
}
|
||||
|
||||
// Store filename and createFilter flag in state to create filter after ingestion succeeds
|
||||
if (result.createFilter && result.filename) {
|
||||
setUploadedFilename(result.filename);
|
||||
|
|
@ -176,6 +204,7 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
|
|||
|
||||
// Reset on error
|
||||
setCurrentStep(null);
|
||||
setUploadedTaskId(null);
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,8 +47,7 @@ export function ChatRenderer({
|
|||
refreshConversations,
|
||||
startNewConversation,
|
||||
setConversationFilter,
|
||||
setCurrentConversationId,
|
||||
setPreviousResponseIds,
|
||||
setOnboardingComplete,
|
||||
} = useChat();
|
||||
|
||||
// Initialize onboarding state based on local storage and settings
|
||||
|
|
@ -170,12 +169,17 @@ export function ChatRenderer({
|
|||
localStorage.removeItem(ONBOARDING_UPLOAD_STEPS_KEY);
|
||||
}
|
||||
|
||||
// Clear ALL conversation state so next message starts fresh
|
||||
await startNewConversation();
|
||||
// Mark onboarding as complete in context
|
||||
setOnboardingComplete(true);
|
||||
|
||||
// Store the user document filter as default for new conversations and load it
|
||||
// Store the user document filter as default for new conversations FIRST
|
||||
// This must happen before startNewConversation() so the filter is available
|
||||
await storeDefaultFilterForNewConversations(true);
|
||||
|
||||
// Clear ALL conversation state so next message starts fresh
|
||||
// This will pick up the default filter we just set
|
||||
await startNewConversation();
|
||||
|
||||
// Clean up onboarding filter IDs now that we've set the default
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.removeItem(ONBOARDING_OPENRAG_DOCS_FILTER_ID_KEY);
|
||||
|
|
@ -202,6 +206,8 @@ export function ChatRenderer({
|
|||
localStorage.removeItem(ONBOARDING_CARD_STEPS_KEY);
|
||||
localStorage.removeItem(ONBOARDING_UPLOAD_STEPS_KEY);
|
||||
}
|
||||
// Mark onboarding as complete in context
|
||||
setOnboardingComplete(true);
|
||||
// Store the OpenRAG docs filter as default for new conversations
|
||||
storeDefaultFilterForNewConversations(false);
|
||||
setShowLayout(true);
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import { useRouter } from "next/navigation";
|
|||
import { useProviderHealthQuery } from "@/app/api/queries/useProviderHealthQuery";
|
||||
import type { ModelProvider } from "@/app/settings/_helpers/model-helpers";
|
||||
import { Banner, BannerIcon, BannerTitle } from "@/components/ui/banner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useChat } from "@/contexts/chat-context";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
interface ProviderHealthBannerProps {
|
||||
|
|
@ -73,8 +73,14 @@ export function ProviderHealthBanner({ className }: ProviderHealthBannerProps) {
|
|||
let errorMessage: string;
|
||||
|
||||
if (llmError && embeddingError) {
|
||||
// Both have errors - show combined message
|
||||
errorMessage = health?.message || "Provider validation failed";
|
||||
// Both have errors - check if they're the same
|
||||
if (llmError === embeddingError) {
|
||||
// Same error for both - show once
|
||||
errorMessage = llmError;
|
||||
} else {
|
||||
// Different errors - show both
|
||||
errorMessage = `${llmError}; ${embeddingError}`;
|
||||
}
|
||||
errorProvider = undefined; // Don't link to a specific provider
|
||||
} else if (llmError) {
|
||||
// Only LLM has error
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { ONBOARDING_STEP_KEY } from "@/lib/constants";
|
||||
|
||||
export type EndpointType = "chat" | "langflow";
|
||||
|
||||
|
|
@ -81,6 +82,8 @@ interface ChatContextType {
|
|||
setConversationFilter: (filter: KnowledgeFilter | null, responseId?: string | null) => void;
|
||||
hasChatError: boolean;
|
||||
setChatError: (hasError: boolean) => void;
|
||||
isOnboardingComplete: boolean;
|
||||
setOnboardingComplete: (complete: boolean) => void;
|
||||
}
|
||||
|
||||
const ChatContext = createContext<ChatContextType | undefined>(undefined);
|
||||
|
|
@ -112,6 +115,37 @@ export function ChatProvider({ children }: ChatProviderProps) {
|
|||
useState<KnowledgeFilter | null>(null);
|
||||
const [hasChatError, setChatError] = useState(false);
|
||||
|
||||
// Check if onboarding is complete (onboarding step key should be null)
|
||||
const [isOnboardingComplete, setIsOnboardingComplete] = useState(() => {
|
||||
if (typeof window === "undefined") return false;
|
||||
return localStorage.getItem(ONBOARDING_STEP_KEY) === null;
|
||||
});
|
||||
|
||||
// Sync onboarding completion state with localStorage
|
||||
useEffect(() => {
|
||||
const checkOnboarding = () => {
|
||||
if (typeof window !== "undefined") {
|
||||
setIsOnboardingComplete(
|
||||
localStorage.getItem(ONBOARDING_STEP_KEY) === null,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Check on mount
|
||||
checkOnboarding();
|
||||
|
||||
// Listen for storage events (for cross-tab sync)
|
||||
window.addEventListener("storage", checkOnboarding);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("storage", checkOnboarding);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const setOnboardingComplete = useCallback((complete: boolean) => {
|
||||
setIsOnboardingComplete(complete);
|
||||
}, []);
|
||||
|
||||
// Listen for ingestion failures and set chat error flag
|
||||
useEffect(() => {
|
||||
const handleIngestionFailed = () => {
|
||||
|
|
@ -228,6 +262,10 @@ export function ChatProvider({ children }: ChatProviderProps) {
|
|||
const startNewConversation = useCallback(async () => {
|
||||
console.log("[CONVERSATION] Starting new conversation");
|
||||
|
||||
// Check if there's existing conversation data - if so, this is a manual "new conversation" action
|
||||
// Check state values before clearing them
|
||||
const hasExistingConversation = conversationData !== null || placeholderConversation !== null;
|
||||
|
||||
// Clear current conversation data and reset state
|
||||
setCurrentConversationId(null);
|
||||
setPreviousResponseIds({ chat: null, langflow: null });
|
||||
|
|
@ -261,15 +299,22 @@ export function ChatProvider({ children }: ChatProviderProps) {
|
|||
setConversationFilterState(null);
|
||||
}
|
||||
} else {
|
||||
console.log("[CONVERSATION] No default filter set");
|
||||
// No default filter in localStorage
|
||||
if (hasExistingConversation) {
|
||||
// User is manually starting a new conversation - clear the filter
|
||||
console.log("[CONVERSATION] Manual new conversation - clearing filter");
|
||||
setConversationFilterState(null);
|
||||
}
|
||||
} else {
|
||||
setConversationFilterState(null);
|
||||
// First time after onboarding - preserve existing filter if set
|
||||
// This prevents clearing the filter when startNewConversation is called multiple times during onboarding
|
||||
console.log("[CONVERSATION] No default filter set, preserving existing filter if any");
|
||||
// Don't clear the filter - it may have been set by storeDefaultFilterForNewConversations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a temporary placeholder conversation to show in sidebar
|
||||
const placeholderConversation: ConversationData = {
|
||||
const newPlaceholderConversation: ConversationData = {
|
||||
response_id: "new-conversation-" + Date.now(),
|
||||
title: "New conversation",
|
||||
endpoint: endpoint,
|
||||
|
|
@ -284,10 +329,10 @@ export function ChatProvider({ children }: ChatProviderProps) {
|
|||
last_activity: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setPlaceholderConversation(placeholderConversation);
|
||||
setPlaceholderConversation(newPlaceholderConversation);
|
||||
// Force immediate refresh to ensure sidebar shows correct state
|
||||
refreshConversations(true);
|
||||
}, [endpoint, refreshConversations]);
|
||||
}, [endpoint, refreshConversations, conversationData, placeholderConversation]);
|
||||
|
||||
const addConversationDoc = useCallback((filename: string) => {
|
||||
setConversationDocs((prev) => [
|
||||
|
|
@ -375,6 +420,8 @@ export function ChatProvider({ children }: ChatProviderProps) {
|
|||
setConversationFilter,
|
||||
hasChatError,
|
||||
setChatError,
|
||||
isOnboardingComplete,
|
||||
setOnboardingComplete,
|
||||
}),
|
||||
[
|
||||
endpoint,
|
||||
|
|
@ -396,6 +443,8 @@ export function ChatProvider({ children }: ChatProviderProps) {
|
|||
conversationFilter,
|
||||
setConversationFilter,
|
||||
hasChatError,
|
||||
isOnboardingComplete,
|
||||
setOnboardingComplete,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export interface UploadFileResult {
|
|||
raw: unknown;
|
||||
createFilter?: boolean;
|
||||
filename?: string;
|
||||
taskId?: string;
|
||||
}
|
||||
|
||||
export async function duplicateCheck(
|
||||
|
|
@ -158,6 +159,7 @@ export async function uploadFile(
|
|||
(uploadIngestJson as { upload?: { id?: string } }).upload?.id ||
|
||||
(uploadIngestJson as { id?: string }).id ||
|
||||
(uploadIngestJson as { task_id?: string }).task_id;
|
||||
const taskId = (uploadIngestJson as { task_id?: string }).task_id;
|
||||
const filePath =
|
||||
(uploadIngestJson as { upload?: { path?: string } }).upload?.path ||
|
||||
(uploadIngestJson as { path?: string }).path ||
|
||||
|
|
@ -197,6 +199,7 @@ export async function uploadFile(
|
|||
raw: uploadIngestJson,
|
||||
createFilter: shouldCreateFilter,
|
||||
filename,
|
||||
taskId,
|
||||
};
|
||||
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""Provider validation utilities for testing API keys and models during onboarding."""
|
||||
|
||||
import json
|
||||
import httpx
|
||||
from utils.container_utils import transform_localhost_url
|
||||
from utils.logging_config import get_logger
|
||||
|
|
@ -7,6 +8,106 @@ from utils.logging_config import get_logger
|
|||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def _parse_json_error_message(error_text: str) -> str:
|
||||
"""Parse JSON error message and extract just the message field."""
|
||||
try:
|
||||
# Try to parse as JSON
|
||||
error_data = json.loads(error_text)
|
||||
|
||||
if isinstance(error_data, dict):
|
||||
# WatsonX format: {"errors": [{"code": "...", "message": "..."}], ...}
|
||||
if "errors" in error_data and isinstance(error_data["errors"], list):
|
||||
errors = error_data["errors"]
|
||||
if len(errors) > 0 and isinstance(errors[0], dict):
|
||||
message = errors[0].get("message", "")
|
||||
if message:
|
||||
return message
|
||||
code = errors[0].get("code", "")
|
||||
if code:
|
||||
return f"Error: {code}"
|
||||
|
||||
# OpenAI format: {"error": {"message": "...", "type": "...", "code": "..."}}
|
||||
if "error" in error_data:
|
||||
error_obj = error_data["error"]
|
||||
if isinstance(error_obj, dict):
|
||||
message = error_obj.get("message", "")
|
||||
if message:
|
||||
return message
|
||||
|
||||
# Direct message field
|
||||
if "message" in error_data:
|
||||
return error_data["message"]
|
||||
|
||||
# Generic format: {"detail": "..."}
|
||||
if "detail" in error_data:
|
||||
return error_data["detail"]
|
||||
except (json.JSONDecodeError, ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Return original text if not JSON or can't parse
|
||||
return error_text
|
||||
|
||||
|
||||
def _extract_error_details(response: httpx.Response) -> str:
|
||||
"""Extract detailed error message from API response."""
|
||||
try:
|
||||
# Try to parse JSON error response
|
||||
error_data = response.json()
|
||||
|
||||
# Common error response formats
|
||||
if isinstance(error_data, dict):
|
||||
# WatsonX format: {"errors": [{"code": "...", "message": "..."}], ...}
|
||||
if "errors" in error_data and isinstance(error_data["errors"], list):
|
||||
errors = error_data["errors"]
|
||||
if len(errors) > 0 and isinstance(errors[0], dict):
|
||||
# Extract just the message from the first error
|
||||
message = errors[0].get("message", "")
|
||||
if message:
|
||||
return message
|
||||
# Fallback to code if no message
|
||||
code = errors[0].get("code", "")
|
||||
if code:
|
||||
return f"Error: {code}"
|
||||
|
||||
# OpenAI format: {"error": {"message": "...", "type": "...", "code": "..."}}
|
||||
if "error" in error_data:
|
||||
error_obj = error_data["error"]
|
||||
if isinstance(error_obj, dict):
|
||||
message = error_obj.get("message", "")
|
||||
error_type = error_obj.get("type", "")
|
||||
code = error_obj.get("code", "")
|
||||
if message:
|
||||
details = message
|
||||
if error_type:
|
||||
details += f" (type: {error_type})"
|
||||
if code:
|
||||
details += f" (code: {code})"
|
||||
return details
|
||||
|
||||
# Anthropic format: {"error": {"message": "...", "type": "..."}}
|
||||
if "message" in error_data:
|
||||
return error_data["message"]
|
||||
|
||||
# Generic format: {"message": "..."}
|
||||
if "detail" in error_data:
|
||||
return error_data["detail"]
|
||||
|
||||
# If JSON parsing worked but no structured error found, try parsing text
|
||||
response_text = response.text[:500]
|
||||
parsed = _parse_json_error_message(response_text)
|
||||
if parsed != response_text:
|
||||
return parsed
|
||||
return response_text
|
||||
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
# If JSON parsing fails, try parsing the text as JSON string
|
||||
response_text = response.text[:500] if response.text else f"HTTP {response.status_code}"
|
||||
parsed = _parse_json_error_message(response_text)
|
||||
if parsed != response_text:
|
||||
return parsed
|
||||
return response_text
|
||||
|
||||
|
||||
async def validate_provider_setup(
|
||||
provider: str,
|
||||
api_key: str = None,
|
||||
|
|
@ -30,7 +131,7 @@ async def validate_provider_setup(
|
|||
If False, performs lightweight validation (no credits consumed). Default: False.
|
||||
|
||||
Raises:
|
||||
Exception: If validation fails with message "Setup failed, please try again or select a different provider."
|
||||
Exception: If validation fails, raises the original exception with the actual error message.
|
||||
"""
|
||||
provider_lower = provider.lower()
|
||||
|
||||
|
|
@ -70,7 +171,8 @@ async def validate_provider_setup(
|
|||
|
||||
except Exception as e:
|
||||
logger.error(f"Validation failed for provider {provider_lower}: {str(e)}")
|
||||
raise Exception("Setup failed, please try again or select a different provider.")
|
||||
# Preserve the original error message instead of replacing it with a generic one
|
||||
raise
|
||||
|
||||
|
||||
async def test_lightweight_health(
|
||||
|
|
@ -155,8 +257,9 @@ async def _test_openai_lightweight_health(api_key: str) -> None:
|
|||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"OpenAI lightweight health check failed: {response.status_code}")
|
||||
raise Exception(f"OpenAI API key validation failed: {response.status_code}")
|
||||
error_details = _extract_error_details(response)
|
||||
logger.error(f"OpenAI lightweight health check failed: {response.status_code} - {error_details}")
|
||||
raise Exception(f"OpenAI API key validation failed: {error_details}")
|
||||
|
||||
logger.info("OpenAI lightweight health check passed")
|
||||
|
||||
|
|
@ -225,8 +328,9 @@ async def _test_openai_completion_with_tools(api_key: str, llm_model: str) -> No
|
|||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"OpenAI completion test failed: {response.status_code} - {response.text}")
|
||||
raise Exception(f"OpenAI API error: {response.status_code}")
|
||||
error_details = _extract_error_details(response)
|
||||
logger.error(f"OpenAI completion test failed: {response.status_code} - {error_details}")
|
||||
raise Exception(f"OpenAI API error: {error_details}")
|
||||
|
||||
logger.info("OpenAI completion with tool calling test passed")
|
||||
|
||||
|
|
@ -260,8 +364,9 @@ async def _test_openai_embedding(api_key: str, embedding_model: str) -> None:
|
|||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"OpenAI embedding test failed: {response.status_code} - {response.text}")
|
||||
raise Exception(f"OpenAI API error: {response.status_code}")
|
||||
error_details = _extract_error_details(response)
|
||||
logger.error(f"OpenAI embedding test failed: {response.status_code} - {error_details}")
|
||||
raise Exception(f"OpenAI API error: {error_details}")
|
||||
|
||||
data = response.json()
|
||||
if not data.get("data") or len(data["data"]) == 0:
|
||||
|
|
@ -300,8 +405,9 @@ async def _test_watsonx_lightweight_health(
|
|||
)
|
||||
|
||||
if token_response.status_code != 200:
|
||||
logger.error(f"IBM IAM token request failed: {token_response.status_code}")
|
||||
raise Exception("Failed to authenticate with IBM Watson - invalid API key")
|
||||
error_details = _extract_error_details(token_response)
|
||||
logger.error(f"IBM IAM token request failed: {token_response.status_code} - {error_details}")
|
||||
raise Exception(f"Failed to authenticate with IBM Watson: {error_details}")
|
||||
|
||||
bearer_token = token_response.json().get("access_token")
|
||||
if not bearer_token:
|
||||
|
|
@ -335,8 +441,9 @@ async def _test_watsonx_completion_with_tools(
|
|||
)
|
||||
|
||||
if token_response.status_code != 200:
|
||||
logger.error(f"IBM IAM token request failed: {token_response.status_code}")
|
||||
raise Exception("Failed to authenticate with IBM Watson")
|
||||
error_details = _extract_error_details(token_response)
|
||||
logger.error(f"IBM IAM token request failed: {token_response.status_code} - {error_details}")
|
||||
raise Exception(f"Failed to authenticate with IBM Watson: {error_details}")
|
||||
|
||||
bearer_token = token_response.json().get("access_token")
|
||||
if not bearer_token:
|
||||
|
|
@ -388,8 +495,11 @@ async def _test_watsonx_completion_with_tools(
|
|||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"IBM Watson completion test failed: {response.status_code} - {response.text}")
|
||||
raise Exception(f"IBM Watson API error: {response.status_code}")
|
||||
error_details = _extract_error_details(response)
|
||||
logger.error(f"IBM Watson completion test failed: {response.status_code} - {error_details}")
|
||||
# If error_details is still JSON, parse it to extract just the message
|
||||
parsed_details = _parse_json_error_message(error_details)
|
||||
raise Exception(f"IBM Watson API error: {parsed_details}")
|
||||
|
||||
logger.info("IBM Watson completion with tool calling test passed")
|
||||
|
||||
|
|
@ -398,6 +508,13 @@ async def _test_watsonx_completion_with_tools(
|
|||
raise Exception("Request timed out")
|
||||
except Exception as e:
|
||||
logger.error(f"IBM Watson completion test failed: {str(e)}")
|
||||
# If the error message contains JSON, parse it to extract just the message
|
||||
error_str = str(e)
|
||||
if "IBM Watson API error: " in error_str:
|
||||
json_part = error_str.split("IBM Watson API error: ", 1)[1]
|
||||
parsed_message = _parse_json_error_message(json_part)
|
||||
if parsed_message != json_part:
|
||||
raise Exception(f"IBM Watson API error: {parsed_message}")
|
||||
raise
|
||||
|
||||
|
||||
|
|
@ -419,8 +536,9 @@ async def _test_watsonx_embedding(
|
|||
)
|
||||
|
||||
if token_response.status_code != 200:
|
||||
logger.error(f"IBM IAM token request failed: {token_response.status_code}")
|
||||
raise Exception("Failed to authenticate with IBM Watson")
|
||||
error_details = _extract_error_details(token_response)
|
||||
logger.error(f"IBM IAM token request failed: {token_response.status_code} - {error_details}")
|
||||
raise Exception(f"Failed to authenticate with IBM Watson: {error_details}")
|
||||
|
||||
bearer_token = token_response.json().get("access_token")
|
||||
if not bearer_token:
|
||||
|
|
@ -450,8 +568,11 @@ async def _test_watsonx_embedding(
|
|||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"IBM Watson embedding test failed: {response.status_code} - {response.text}")
|
||||
raise Exception(f"IBM Watson API error: {response.status_code}")
|
||||
error_details = _extract_error_details(response)
|
||||
logger.error(f"IBM Watson embedding test failed: {response.status_code} - {error_details}")
|
||||
# If error_details is still JSON, parse it to extract just the message
|
||||
parsed_details = _parse_json_error_message(error_details)
|
||||
raise Exception(f"IBM Watson API error: {parsed_details}")
|
||||
|
||||
data = response.json()
|
||||
if not data.get("results") or len(data["results"]) == 0:
|
||||
|
|
@ -464,6 +585,13 @@ async def _test_watsonx_embedding(
|
|||
raise Exception("Request timed out")
|
||||
except Exception as e:
|
||||
logger.error(f"IBM Watson embedding test failed: {str(e)}")
|
||||
# If the error message contains JSON, parse it to extract just the message
|
||||
error_str = str(e)
|
||||
if "IBM Watson API error: " in error_str:
|
||||
json_part = error_str.split("IBM Watson API error: ", 1)[1]
|
||||
parsed_message = _parse_json_error_message(json_part)
|
||||
if parsed_message != json_part:
|
||||
raise Exception(f"IBM Watson API error: {parsed_message}")
|
||||
raise
|
||||
|
||||
|
||||
|
|
@ -483,8 +611,9 @@ async def _test_ollama_lightweight_health(endpoint: str) -> None:
|
|||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Ollama lightweight health check failed: {response.status_code}")
|
||||
raise Exception(f"Ollama endpoint not responding: {response.status_code}")
|
||||
error_details = _extract_error_details(response)
|
||||
logger.error(f"Ollama lightweight health check failed: {response.status_code} - {error_details}")
|
||||
raise Exception(f"Ollama endpoint not responding: {error_details}")
|
||||
|
||||
logger.info("Ollama lightweight health check passed")
|
||||
|
||||
|
|
@ -537,8 +666,9 @@ async def _test_ollama_completion_with_tools(llm_model: str, endpoint: str) -> N
|
|||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Ollama completion test failed: {response.status_code} - {response.text}")
|
||||
raise Exception(f"Ollama API error: {response.status_code}")
|
||||
error_details = _extract_error_details(response)
|
||||
logger.error(f"Ollama completion test failed: {response.status_code} - {error_details}")
|
||||
raise Exception(f"Ollama API error: {error_details}")
|
||||
|
||||
logger.info("Ollama completion with tool calling test passed")
|
||||
|
||||
|
|
@ -569,8 +699,9 @@ async def _test_ollama_embedding(embedding_model: str, endpoint: str) -> None:
|
|||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Ollama embedding test failed: {response.status_code} - {response.text}")
|
||||
raise Exception(f"Ollama API error: {response.status_code}")
|
||||
error_details = _extract_error_details(response)
|
||||
logger.error(f"Ollama embedding test failed: {response.status_code} - {error_details}")
|
||||
raise Exception(f"Ollama API error: {error_details}")
|
||||
|
||||
data = response.json()
|
||||
if not data.get("embedding"):
|
||||
|
|
@ -616,8 +747,9 @@ async def _test_anthropic_lightweight_health(api_key: str) -> None:
|
|||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Anthropic lightweight health check failed: {response.status_code}")
|
||||
raise Exception(f"Anthropic API key validation failed: {response.status_code}")
|
||||
error_details = _extract_error_details(response)
|
||||
logger.error(f"Anthropic lightweight health check failed: {response.status_code} - {error_details}")
|
||||
raise Exception(f"Anthropic API key validation failed: {error_details}")
|
||||
|
||||
logger.info("Anthropic lightweight health check passed")
|
||||
|
||||
|
|
@ -672,8 +804,9 @@ async def _test_anthropic_completion_with_tools(api_key: str, llm_model: str) ->
|
|||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Anthropic completion test failed: {response.status_code} - {response.text}")
|
||||
raise Exception(f"Anthropic API error: {response.status_code}")
|
||||
error_details = _extract_error_details(response)
|
||||
logger.error(f"Anthropic completion test failed: {response.status_code} - {error_details}")
|
||||
raise Exception(f"Anthropic API error: {error_details}")
|
||||
|
||||
logger.info("Anthropic completion with tool calling test passed")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue