From 0f79b1d753074ee16785ce309737b2047323fa48 Mon Sep 17 00:00:00 2001 From: Mike Fortman Date: Wed, 1 Oct 2025 14:34:47 -0500 Subject: [PATCH 1/9] Fix the double callback call resulting in the Auth code already used screen --- frontend/src/app/auth/callback/page.tsx | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/frontend/src/app/auth/callback/page.tsx b/frontend/src/app/auth/callback/page.tsx index 9c6bdbb0..23c1972a 100644 --- a/frontend/src/app/auth/callback/page.tsx +++ b/frontend/src/app/auth/callback/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useEffect, useState, useRef, Suspense } from "react" +import { useEffect, useState, Suspense } from "react" import { useRouter, useSearchParams } from "next/navigation" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" @@ -14,17 +14,21 @@ function AuthCallbackContent() { const [status, setStatus] = useState<"processing" | "success" | "error">("processing") const [error, setError] = useState(null) const [purpose, setPurpose] = useState("app_auth") - const hasProcessed = useRef(false) useEffect(() => { - // Prevent double execution in React Strict Mode - if (hasProcessed.current) return - hasProcessed.current = true + const code = searchParams.get('code') + const callbackKey = `callback_processed_${code}` + + // Prevent double execution across component remounts + if (sessionStorage.getItem(callbackKey)) { + console.log('Callback already processed, skipping') + return + } + sessionStorage.setItem(callbackKey, 'true') const handleCallback = async () => { try { // Get parameters from URL - const code = searchParams.get('code') const state = searchParams.get('state') const errorParam = searchParams.get('error') @@ -37,14 +41,6 @@ function AuthCallbackContent() { const detectedPurpose = authPurpose || (storedConnectorType?.includes('drive') ? 'data_source' : 'app_auth') setPurpose(detectedPurpose) - // Debug logging - console.log('OAuth Callback Debug:', { - urlParams: { code: !!code, state: !!state, error: errorParam }, - localStorage: { connectorId, storedConnectorType, authPurpose }, - detectedPurpose, - fullUrl: window.location.href - }) - // Use state parameter as connection_id if localStorage is missing const finalConnectorId = connectorId || state From 2924a5d52df2d0c4d7f5c2b1f57fe14f5ce9feca Mon Sep 17 00:00:00 2001 From: Mike Fortman Date: Wed, 1 Oct 2025 14:38:30 -0500 Subject: [PATCH 2/9] Revert "Fix the double callback call resulting in the Auth code already used screen" This reverts commit 0f79b1d753074ee16785ce309737b2047323fa48. --- frontend/src/app/auth/callback/page.tsx | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/auth/callback/page.tsx b/frontend/src/app/auth/callback/page.tsx index 23c1972a..9c6bdbb0 100644 --- a/frontend/src/app/auth/callback/page.tsx +++ b/frontend/src/app/auth/callback/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useEffect, useState, Suspense } from "react" +import { useEffect, useState, useRef, Suspense } from "react" import { useRouter, useSearchParams } from "next/navigation" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" @@ -14,21 +14,17 @@ function AuthCallbackContent() { const [status, setStatus] = useState<"processing" | "success" | "error">("processing") const [error, setError] = useState(null) const [purpose, setPurpose] = useState("app_auth") + const hasProcessed = useRef(false) useEffect(() => { - const code = searchParams.get('code') - const callbackKey = `callback_processed_${code}` - - // Prevent double execution across component remounts - if (sessionStorage.getItem(callbackKey)) { - console.log('Callback already processed, skipping') - return - } - sessionStorage.setItem(callbackKey, 'true') + // Prevent double execution in React Strict Mode + if (hasProcessed.current) return + hasProcessed.current = true const handleCallback = async () => { try { // Get parameters from URL + const code = searchParams.get('code') const state = searchParams.get('state') const errorParam = searchParams.get('error') @@ -41,6 +37,14 @@ function AuthCallbackContent() { const detectedPurpose = authPurpose || (storedConnectorType?.includes('drive') ? 'data_source' : 'app_auth') setPurpose(detectedPurpose) + // Debug logging + console.log('OAuth Callback Debug:', { + urlParams: { code: !!code, state: !!state, error: errorParam }, + localStorage: { connectorId, storedConnectorType, authPurpose }, + detectedPurpose, + fullUrl: window.location.href + }) + // Use state parameter as connection_id if localStorage is missing const finalConnectorId = connectorId || state From d62945110cda5adb27960f7ec0d033fb526c95e5 Mon Sep 17 00:00:00 2001 From: Mike Fortman Date: Wed, 1 Oct 2025 14:42:59 -0500 Subject: [PATCH 3/9] Fix double OAuth callback execution using sessionStorage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced useRef with sessionStorage to prevent duplicate callback executions when component remounts (React Strict Mode). The ref was resetting on remount, causing the auth code to be used twice and triggering "code already used" errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- frontend/src/app/auth/callback/page.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/auth/callback/page.tsx b/frontend/src/app/auth/callback/page.tsx index 9c6bdbb0..780b45b6 100644 --- a/frontend/src/app/auth/callback/page.tsx +++ b/frontend/src/app/auth/callback/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useEffect, useState, useRef, Suspense } from "react" +import { useEffect, useState, Suspense } from "react" import { useRouter, useSearchParams } from "next/navigation" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" @@ -14,17 +14,20 @@ function AuthCallbackContent() { const [status, setStatus] = useState<"processing" | "success" | "error">("processing") const [error, setError] = useState(null) const [purpose, setPurpose] = useState("app_auth") - const hasProcessed = useRef(false) useEffect(() => { - // Prevent double execution in React Strict Mode - if (hasProcessed.current) return - hasProcessed.current = true + const code = searchParams.get('code') + const callbackKey = `callback_processed_${code}` + + // Prevent double execution across component remounts + if (sessionStorage.getItem(callbackKey)) { + return + } + sessionStorage.setItem(callbackKey, 'true') const handleCallback = async () => { try { // Get parameters from URL - const code = searchParams.get('code') const state = searchParams.get('state') const errorParam = searchParams.get('error') From 26023ab244256f5fac5100756ac949db834ab0b2 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:48:57 -0300 Subject: [PATCH 4/9] fix: truncate text on @ filter selector on chat (#133) * Added truncation on name and description for knowledge filters * fixed selected filter not truncating right --- frontend/src/app/chat/page.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/chat/page.tsx b/frontend/src/app/chat/page.tsx index 5ae746bf..7c2a94bc 100644 --- a/frontend/src/app/chat/page.tsx +++ b/frontend/src/app/chat/page.tsx @@ -2344,12 +2344,14 @@ function ChatPage() { ))} From 09d502c94bacec516036e1a3029be7d0ceacec1a Mon Sep 17 00:00:00 2001 From: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:49:08 -0300 Subject: [PATCH 5/9] fix: remove duplicate toast and ! character (#165) * removed duplicated toast * removed ! character from copy --- frontend/src/app/upload/[provider]/page.tsx | 674 ++++++++++---------- frontend/src/contexts/task-context.tsx | 2 +- 2 files changed, 334 insertions(+), 342 deletions(-) diff --git a/frontend/src/app/upload/[provider]/page.tsx b/frontend/src/app/upload/[provider]/page.tsx index 522a45b7..7c72ec3d 100644 --- a/frontend/src/app/upload/[provider]/page.tsx +++ b/frontend/src/app/upload/[provider]/page.tsx @@ -1,386 +1,378 @@ "use client"; -import { useState, useEffect } from "react"; +import { AlertCircle, ArrowLeft } from "lucide-react"; import { useParams, useRouter } from "next/navigation"; -import { Button } from "@/components/ui/button"; -import { ArrowLeft, AlertCircle } from "lucide-react"; -import { UnifiedCloudPicker, CloudFile } from "@/components/cloud-picker"; +import { useEffect, useState } from "react"; +import { type CloudFile, UnifiedCloudPicker } from "@/components/cloud-picker"; import type { IngestSettings } from "@/components/cloud-picker/types"; -import { useTask } from "@/contexts/task-context"; +import { Button } from "@/components/ui/button"; import { Toast } from "@/components/ui/toast"; +import { useTask } from "@/contexts/task-context"; // CloudFile interface is now imported from the unified cloud picker interface CloudConnector { - id: string; - name: string; - description: string; - status: "not_connected" | "connecting" | "connected" | "error"; - type: string; - connectionId?: string; - clientId: string; - hasAccessToken: boolean; - accessTokenError?: string; + id: string; + name: string; + description: string; + status: "not_connected" | "connecting" | "connected" | "error"; + type: string; + connectionId?: string; + clientId: string; + hasAccessToken: boolean; + accessTokenError?: string; } export default function UploadProviderPage() { - const params = useParams(); - const router = useRouter(); - const provider = params.provider as string; - const { addTask, tasks } = useTask(); + const params = useParams(); + const router = useRouter(); + const provider = params.provider as string; + const { addTask, tasks } = useTask(); - const [connector, setConnector] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [accessToken, setAccessToken] = useState(null); - const [selectedFiles, setSelectedFiles] = useState([]); - const [isIngesting, setIsIngesting] = useState(false); - const [currentSyncTaskId, setCurrentSyncTaskId] = useState( - null - ); - const [showSuccessToast, setShowSuccessToast] = useState(false); - const [ingestSettings, setIngestSettings] = useState({ - chunkSize: 1000, - chunkOverlap: 200, - ocr: false, - pictureDescriptions: false, - embeddingModel: "text-embedding-3-small", - }); + const [connector, setConnector] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [accessToken, setAccessToken] = useState(null); + const [selectedFiles, setSelectedFiles] = useState([]); + const [isIngesting, setIsIngesting] = useState(false); + const [currentSyncTaskId, setCurrentSyncTaskId] = useState( + null, + ); + const [ingestSettings, setIngestSettings] = useState({ + chunkSize: 1000, + chunkOverlap: 200, + ocr: false, + pictureDescriptions: false, + embeddingModel: "text-embedding-3-small", + }); - useEffect(() => { - const fetchConnectorInfo = async () => { - setIsLoading(true); - setError(null); + useEffect(() => { + const fetchConnectorInfo = async () => { + setIsLoading(true); + setError(null); - try { - // Fetch available connectors to validate the provider - const connectorsResponse = await fetch("/api/connectors"); - if (!connectorsResponse.ok) { - throw new Error("Failed to load connectors"); - } + try { + // Fetch available connectors to validate the provider + const connectorsResponse = await fetch("/api/connectors"); + if (!connectorsResponse.ok) { + throw new Error("Failed to load connectors"); + } - const connectorsResult = await connectorsResponse.json(); - const providerInfo = connectorsResult.connectors[provider]; + const connectorsResult = await connectorsResponse.json(); + const providerInfo = connectorsResult.connectors[provider]; - if (!providerInfo || !providerInfo.available) { - setError( - `Cloud provider "${provider}" is not available or configured.` - ); - return; - } + if (!providerInfo || !providerInfo.available) { + setError( + `Cloud provider "${provider}" is not available or configured.`, + ); + return; + } - // Check connector status - const statusResponse = await fetch( - `/api/connectors/${provider}/status` - ); - if (!statusResponse.ok) { - throw new Error(`Failed to check ${provider} status`); - } + // Check connector status + const statusResponse = await fetch( + `/api/connectors/${provider}/status`, + ); + if (!statusResponse.ok) { + throw new Error(`Failed to check ${provider} status`); + } - const statusData = await statusResponse.json(); - const connections = statusData.connections || []; - const activeConnection = connections.find( - (conn: { is_active: boolean; connection_id: string }) => - conn.is_active - ); - const isConnected = activeConnection !== undefined; + const statusData = await statusResponse.json(); + const connections = statusData.connections || []; + const activeConnection = connections.find( + (conn: { is_active: boolean; connection_id: string }) => + conn.is_active, + ); + const isConnected = activeConnection !== undefined; - let hasAccessToken = false; - let accessTokenError: string | undefined = undefined; + let hasAccessToken = false; + let accessTokenError: string | undefined; - // Try to get access token for connected connectors - if (isConnected && activeConnection) { - try { - const tokenResponse = await fetch( - `/api/connectors/${provider}/token?connection_id=${activeConnection.connection_id}` - ); - if (tokenResponse.ok) { - const tokenData = await tokenResponse.json(); - if (tokenData.access_token) { - hasAccessToken = true; - setAccessToken(tokenData.access_token); - } - } else { - const errorData = await tokenResponse - .json() - .catch(() => ({ error: "Token unavailable" })); - accessTokenError = errorData.error || "Access token unavailable"; - } - } catch { - accessTokenError = "Failed to fetch access token"; - } - } + // Try to get access token for connected connectors + if (isConnected && activeConnection) { + try { + const tokenResponse = await fetch( + `/api/connectors/${provider}/token?connection_id=${activeConnection.connection_id}`, + ); + if (tokenResponse.ok) { + const tokenData = await tokenResponse.json(); + if (tokenData.access_token) { + hasAccessToken = true; + setAccessToken(tokenData.access_token); + } + } else { + const errorData = await tokenResponse + .json() + .catch(() => ({ error: "Token unavailable" })); + accessTokenError = errorData.error || "Access token unavailable"; + } + } catch { + accessTokenError = "Failed to fetch access token"; + } + } - setConnector({ - id: provider, - name: providerInfo.name, - description: providerInfo.description, - status: isConnected ? "connected" : "not_connected", - type: provider, - connectionId: activeConnection?.connection_id, - clientId: activeConnection?.client_id, - hasAccessToken, - accessTokenError, - }); - } catch (error) { - console.error("Failed to load connector info:", error); - setError( - error instanceof Error - ? error.message - : "Failed to load connector information" - ); - } finally { - setIsLoading(false); - } - }; + setConnector({ + id: provider, + name: providerInfo.name, + description: providerInfo.description, + status: isConnected ? "connected" : "not_connected", + type: provider, + connectionId: activeConnection?.connection_id, + clientId: activeConnection?.client_id, + hasAccessToken, + accessTokenError, + }); + } catch (error) { + console.error("Failed to load connector info:", error); + setError( + error instanceof Error + ? error.message + : "Failed to load connector information", + ); + } finally { + setIsLoading(false); + } + }; - if (provider) { - fetchConnectorInfo(); - } - }, [provider]); + if (provider) { + fetchConnectorInfo(); + } + }, [provider]); - // Watch for sync task completion and redirect - useEffect(() => { - if (!currentSyncTaskId) return; + // Watch for sync task completion and redirect + useEffect(() => { + if (!currentSyncTaskId) return; - const currentTask = tasks.find(task => task.task_id === currentSyncTaskId); + const currentTask = tasks.find( + (task) => task.task_id === currentSyncTaskId, + ); - if (currentTask && currentTask.status === "completed") { - // Task completed successfully, show toast and redirect - setIsIngesting(false); - setShowSuccessToast(true); - setTimeout(() => { - router.push("/knowledge"); - }, 2000); // 2 second delay to let user see toast - } else if (currentTask && currentTask.status === "failed") { - // Task failed, clear the tracking but don't redirect - setIsIngesting(false); - setCurrentSyncTaskId(null); - } - }, [tasks, currentSyncTaskId, router]); + if (currentTask && currentTask.status === "completed") { + // Task completed successfully, show toast and redirect + setIsIngesting(false); + setTimeout(() => { + router.push("/knowledge"); + }, 2000); // 2 second delay to let user see toast + } else if (currentTask && currentTask.status === "failed") { + // Task failed, clear the tracking but don't redirect + setIsIngesting(false); + setCurrentSyncTaskId(null); + } + }, [tasks, currentSyncTaskId, router]); - const handleFileSelected = (files: CloudFile[]) => { - setSelectedFiles(files); - console.log(`Selected ${files.length} files from ${provider}:`, files); - // You can add additional handling here like triggering sync, etc. - }; + const handleFileSelected = (files: CloudFile[]) => { + setSelectedFiles(files); + console.log(`Selected ${files.length} files from ${provider}:`, files); + // You can add additional handling here like triggering sync, etc. + }; - const handleSync = async (connector: CloudConnector) => { - if (!connector.connectionId || selectedFiles.length === 0) return; + const handleSync = async (connector: CloudConnector) => { + if (!connector.connectionId || selectedFiles.length === 0) return; - setIsIngesting(true); + setIsIngesting(true); - try { - const syncBody: { - connection_id: string; - max_files?: number; - selected_files?: string[]; - settings?: IngestSettings; - } = { - connection_id: connector.connectionId, - selected_files: selectedFiles.map(file => file.id), - settings: ingestSettings, - }; + try { + const syncBody: { + connection_id: string; + max_files?: number; + selected_files?: string[]; + settings?: IngestSettings; + } = { + connection_id: connector.connectionId, + selected_files: selectedFiles.map((file) => file.id), + settings: ingestSettings, + }; - const response = await fetch(`/api/connectors/${connector.type}/sync`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(syncBody), - }); + const response = await fetch(`/api/connectors/${connector.type}/sync`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(syncBody), + }); - const result = await response.json(); + const result = await response.json(); - if (response.status === 201) { - const taskIds = result.task_ids; - if (taskIds && taskIds.length > 0) { - const taskId = taskIds[0]; // Use the first task ID - addTask(taskId); - setCurrentSyncTaskId(taskId); - } - } else { - console.error("Sync failed:", result.error); - } - } catch (error) { - console.error("Sync error:", error); - setIsIngesting(false); - } - }; + if (response.status === 201) { + const taskIds = result.task_ids; + if (taskIds && taskIds.length > 0) { + const taskId = taskIds[0]; // Use the first task ID + addTask(taskId); + setCurrentSyncTaskId(taskId); + } + } else { + console.error("Sync failed:", result.error); + } + } catch (error) { + console.error("Sync error:", error); + setIsIngesting(false); + } + }; - const getProviderDisplayName = () => { - const nameMap: { [key: string]: string } = { - google_drive: "Google Drive", - onedrive: "OneDrive", - sharepoint: "SharePoint", - }; - return nameMap[provider] || provider; - }; + const getProviderDisplayName = () => { + const nameMap: { [key: string]: string } = { + google_drive: "Google Drive", + onedrive: "OneDrive", + sharepoint: "SharePoint", + }; + return nameMap[provider] || provider; + }; - if (isLoading) { - return ( -
-
-
-
-

Loading {getProviderDisplayName()} connector...

-
-
-
- ); - } + if (isLoading) { + return ( +
+
+
+
+

Loading {getProviderDisplayName()} connector...

+
+
+
+ ); + } - if (error || !connector) { - return ( -
-
- -
+ if (error || !connector) { + return ( +
+
+ +
-
-
- -

- Provider Not Available -

-

{error}

- -
-
-
- ); - } +
+
+ +

+ Provider Not Available +

+

{error}

+ +
+
+
+ ); + } - if (connector.status !== "connected") { - return ( -
-
- -
+ if (connector.status !== "connected") { + return ( +
+
+ +
-
-
- -

- {connector.name} Not Connected -

-

- You need to connect your {connector.name} account before you can - select files. -

- -
-
-
- ); - } +
+
+ +

+ {connector.name} Not Connected +

+

+ You need to connect your {connector.name} account before you can + select files. +

+ +
+
+
+ ); + } - if (!connector.hasAccessToken) { - return ( -
-
- -
+ if (!connector.hasAccessToken) { + return ( +
+
+ +
-
-
- -

- Access Token Required -

-

- {connector.accessTokenError || - `Unable to get access token for ${connector.name}. Try reconnecting your account.`} -

- -
-
-
- ); - } +
+
+ +

+ Access Token Required +

+

+ {connector.accessTokenError || + `Unable to get access token for ${connector.name}. Try reconnecting your account.`} +

+ +
+
+
+ ); + } - return ( -
-
- -

- Add from {getProviderDisplayName()} -

-
+ return ( +
+
+ +

+ Add from {getProviderDisplayName()} +

+
-
- -
+
+ +
-
-
- - -
-
- - {/* Success toast notification */} - setShowSuccessToast(false)} - duration={20000} - /> -
- ); +
+
+ + +
+
+
+ ); } diff --git a/frontend/src/contexts/task-context.tsx b/frontend/src/contexts/task-context.tsx index 4b6c18c2..5eb10ea9 100644 --- a/frontend/src/contexts/task-context.tsx +++ b/frontend/src/contexts/task-context.tsx @@ -197,7 +197,7 @@ export function TaskProvider({ children }: { children: React.ReactNode }) { newTask.status === "completed" ) { // Task just completed - show success toast - toast.success("Task completed successfully!", { + toast.success("Task completed successfully", { description: `Task ${newTask.task_id} has finished processing.`, action: { label: "View", From 522b8b81590ea74a48f088ee315014a196c25c63 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:49:19 -0300 Subject: [PATCH 6/9] Removed privacy policy and subheading, adjusted gap on sign in page (#164) --- frontend/src/app/login/page.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index 1639a4be..837706d7 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -69,18 +69,12 @@ function LoginPageContent() { />
+

Welcome to OpenRAG

-

- All your knowledge at your fingertips. -

-
-
-

Systems Operational

• -

Privacy Policy

+
); From 28dbca4ba03fdfac7a74712a7743e7bcf12460d1 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:49:33 -0300 Subject: [PATCH 7/9] fix: make onboarding match designs (#163) * implement custom value when custom is active on model selector * changed ibm logo to watsonx * set endpoint selector as custom * changed IBM to IBM watsonx.ai * upload copies on ibm onboarding * disable select on disabled button * changed use env open ai api key style * Changed width to match designs * disable default tag * made selector full width and made default disappear * removed ! * removed onboarding toast * fix card border radius --- frontend/components/label-wrapper.tsx | 2 +- frontend/components/logo/ibm-logo.tsx | 23 +- frontend/components/ui/button.tsx | 2 +- frontend/components/ui/card.tsx | 2 +- .../onboarding/components/ibm-onboarding.tsx | 23 +- .../onboarding/components/model-selector.tsx | 247 ++++++++++-------- .../components/openai-onboarding.tsx | 219 ++++++++-------- frontend/src/app/onboarding/page.tsx | 7 +- 8 files changed, 287 insertions(+), 238 deletions(-) diff --git a/frontend/components/label-wrapper.tsx b/frontend/components/label-wrapper.tsx index 691b7726..0ebde8a6 100644 --- a/frontend/components/label-wrapper.tsx +++ b/frontend/components/label-wrapper.tsx @@ -37,7 +37,7 @@ export function LabelWrapper({ >