From 1eb0fb3593458f2a064aea14af237d8a99f4a5f9 Mon Sep 17 00:00:00 2001 From: Brent O'Neill Date: Fri, 3 Oct 2025 15:00:53 -0600 Subject: [PATCH 01/12] design sweep for ingest for connector page --- frontend/src/app/upload/[provider]/page.tsx | 659 +++++++++--------- .../components/cloud-picker/picker-header.tsx | 8 +- 2 files changed, 332 insertions(+), 335 deletions(-) diff --git a/frontend/src/app/upload/[provider]/page.tsx b/frontend/src/app/upload/[provider]/page.tsx index 7c72ec3d..8e0a306c 100644 --- a/frontend/src/app/upload/[provider]/page.tsx +++ b/frontend/src/app/upload/[provider]/page.tsx @@ -12,367 +12,370 @@ 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 [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; + 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); - 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()} +

+
-
- -
+
+ +
-
-
- - -
-
-
- ); +
+
+ + +
+
+
+ ); } diff --git a/frontend/src/components/cloud-picker/picker-header.tsx b/frontend/src/components/cloud-picker/picker-header.tsx index 54407aa7..e0d9cfa4 100644 --- a/frontend/src/components/cloud-picker/picker-header.tsx +++ b/frontend/src/components/cloud-picker/picker-header.tsx @@ -51,19 +51,13 @@ export const PickerHeader = ({ Select files from {getProviderName(provider)} to ingest.

-
- csv, json, pdf,{" "} - +16 more{" "} - 150 MB max -
); From 01084d2a8957e18c8bdededba51e6777550bb81b Mon Sep 17 00:00:00 2001 From: Brent O'Neill Date: Fri, 3 Oct 2025 15:30:45 -0600 Subject: [PATCH 02/12] wip commit --- frontend/src/app/upload/[provider]/page.tsx | 9 ++--- .../src/components/cloud-picker/file-item.tsx | 37 +++++++++++++++---- .../src/components/cloud-picker/file-list.tsx | 16 ++++++-- .../cloud-picker/ingest-settings.tsx | 12 +++--- .../cloud-picker/unified-cloud-picker.tsx | 9 +++-- 5 files changed, 55 insertions(+), 28 deletions(-) diff --git a/frontend/src/app/upload/[provider]/page.tsx b/frontend/src/app/upload/[provider]/page.tsx index 8e0a306c..e3f3f57e 100644 --- a/frontend/src/app/upload/[provider]/page.tsx +++ b/frontend/src/app/upload/[provider]/page.tsx @@ -328,7 +328,7 @@ export default function UploadProviderPage() { return (
-
+
@@ -366,12 +366,9 @@ export default function UploadProviderPage() { disabled={selectedFiles.length === 0 || isIngesting} > {isIngesting ? ( - <> - Ingesting {selectedFiles.length} file - {selectedFiles.length > 1 ? "s" : ""}... - + <>Ingesting {selectedFiles.length} Files... ) : ( - <>Ingest files + <>Start ingest )}
diff --git a/frontend/src/components/cloud-picker/file-item.tsx b/frontend/src/components/cloud-picker/file-item.tsx index 3f6b5ab5..e9bdec38 100644 --- a/frontend/src/components/cloud-picker/file-item.tsx +++ b/frontend/src/components/cloud-picker/file-item.tsx @@ -1,10 +1,15 @@ "use client"; import { Badge } from "@/components/ui/badge"; -import { FileText, Folder, Trash } from "lucide-react"; +import { FileText, Folder, Trash, Trash2 } from "lucide-react"; import { CloudFile } from "./types"; +import GoogleDriveIcon from "@/app/settings/icons/google-drive-icon"; +import SharePointIcon from "@/app/settings/icons/share-point-icon"; +import OneDriveIcon from "@/app/settings/icons/one-drive-icon"; +import { Button } from "@/components/ui/button"; interface FileItemProps { + provider: string; file: CloudFile; onRemove: (fileId: string) => void; } @@ -41,27 +46,43 @@ const formatFileSize = (bytes?: number) => { return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`; }; -export const FileItem = ({ file, onRemove }: FileItemProps) => ( +const getProviderIcon = (provider: string) => { + switch (provider) { + case "google_drive": + return ; + case "onedrive": + return ; + case "sharepoint": + return ; + default: + return ; + } +}; + +export const FileItem = ({ file, onRemove, provider }: FileItemProps) => (
- {getFileIcon(file.mimeType)} + {provider ? getProviderIcon(provider) : getFileIcon(file.mimeType)} {file.name} {getMimeTypeLabel(file.mimeType)}
-
+
{formatFileSize(file.size) || "—"} - - onRemove(file.id)} - /> + > + +
); diff --git a/frontend/src/components/cloud-picker/file-list.tsx b/frontend/src/components/cloud-picker/file-list.tsx index 775d78c4..b584249a 100644 --- a/frontend/src/components/cloud-picker/file-list.tsx +++ b/frontend/src/components/cloud-picker/file-list.tsx @@ -5,12 +5,14 @@ import { CloudFile } from "./types"; import { FileItem } from "./file-item"; interface FileListProps { + provider: string; files: CloudFile[]; onClearAll: () => void; onRemoveFile: (fileId: string) => void; } export const FileList = ({ + provider, files, onClearAll, onRemoveFile, @@ -22,19 +24,25 @@ export const FileList = ({ return (
-

Added files

+

Added files ({files.length})

- {files.map(file => ( - + {files.map((file) => ( + ))}
diff --git a/frontend/src/components/cloud-picker/ingest-settings.tsx b/frontend/src/components/cloud-picker/ingest-settings.tsx index d5843a2a..8c72ce6b 100644 --- a/frontend/src/components/cloud-picker/ingest-settings.tsx +++ b/frontend/src/components/cloud-picker/ingest-settings.tsx @@ -44,7 +44,7 @@ export const IngestSettings = ({
@@ -65,7 +65,7 @@ export const IngestSettings = ({ + onChange={(e) => handleSettingsChange({ chunkSize: parseInt(e.target.value) || 0, }) @@ -77,7 +77,7 @@ export const IngestSettings = ({ + onChange={(e) => handleSettingsChange({ chunkOverlap: parseInt(e.target.value) || 0, }) @@ -95,7 +95,7 @@ export const IngestSettings = ({
+ onCheckedChange={(checked) => handleSettingsChange({ ocr: checked }) } /> @@ -112,7 +112,7 @@ export const IngestSettings = ({
+ onCheckedChange={(checked) => handleSettingsChange({ pictureDescriptions: checked }) } /> @@ -126,7 +126,7 @@ export const IngestSettings = ({ + onChange={(e) => handleSettingsChange({ embeddingModel: e.target.value }) } placeholder="text-embedding-3-small" diff --git a/frontend/src/components/cloud-picker/unified-cloud-picker.tsx b/frontend/src/components/cloud-picker/unified-cloud-picker.tsx index fd77698f..0a4699af 100644 --- a/frontend/src/components/cloud-picker/unified-cloud-picker.tsx +++ b/frontend/src/components/cloud-picker/unified-cloud-picker.tsx @@ -116,7 +116,7 @@ export const UnifiedCloudPicker = ({ const handler = createProviderHandler( provider, accessToken, - isOpen => { + (isOpen) => { setIsPickerOpen(isOpen); onPickerStateChange?.(isOpen); }, @@ -126,8 +126,8 @@ export const UnifiedCloudPicker = ({ handler.openPicker((files: CloudFile[]) => { // Merge new files with existing ones, avoiding duplicates - const existingIds = new Set(selectedFiles.map(f => f.id)); - const newFiles = files.filter(f => !existingIds.has(f.id)); + const existingIds = new Set(selectedFiles.map((f) => f.id)); + const newFiles = files.filter((f) => !existingIds.has(f.id)); onFileSelected([...selectedFiles, ...newFiles]); }); } catch (error) { @@ -138,7 +138,7 @@ export const UnifiedCloudPicker = ({ }; const handleRemoveFile = (fileId: string) => { - const updatedFiles = selectedFiles.filter(file => file.id !== fileId); + const updatedFiles = selectedFiles.filter((file) => file.id !== fileId); onFileSelected(updatedFiles); }; @@ -179,6 +179,7 @@ export const UnifiedCloudPicker = ({ /> Date: Fri, 3 Oct 2025 15:41:45 -0600 Subject: [PATCH 03/12] fix file item spacing and typography --- frontend/src/components/cloud-picker/file-item.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/cloud-picker/file-item.tsx b/frontend/src/components/cloud-picker/file-item.tsx index e9bdec38..5370e4cc 100644 --- a/frontend/src/components/cloud-picker/file-item.tsx +++ b/frontend/src/components/cloud-picker/file-item.tsx @@ -62,14 +62,14 @@ const getProviderIcon = (provider: string) => { export const FileItem = ({ file, onRemove, provider }: FileItemProps) => (
{provider ? getProviderIcon(provider) : getFileIcon(file.mimeType)} {file.name} - + {getMimeTypeLabel(file.mimeType)} - +
From a2e1210a4dcf12543bb13d406240c620e3d17dfb Mon Sep 17 00:00:00 2001 From: Brent O'Neill Date: Mon, 6 Oct 2025 08:50:19 -0600 Subject: [PATCH 04/12] wip commit --- frontend/src/app/upload/[provider]/page.tsx | 11 ++++++----- frontend/src/components/cloud-picker/file-item.tsx | 4 ++-- frontend/src/components/cloud-picker/file-list.tsx | 5 +++++ frontend/src/components/cloud-picker/types.ts | 1 + .../components/cloud-picker/unified-cloud-picker.tsx | 2 ++ 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/upload/[provider]/page.tsx b/frontend/src/app/upload/[provider]/page.tsx index e3f3f57e..3254bc73 100644 --- a/frontend/src/app/upload/[provider]/page.tsx +++ b/frontend/src/app/upload/[provider]/page.tsx @@ -6,7 +6,6 @@ import { useEffect, useState } from "react"; import { type CloudFile, UnifiedCloudPicker } from "@/components/cloud-picker"; import type { IngestSettings } from "@/components/cloud-picker/types"; 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 @@ -345,6 +344,7 @@ export default function UploadProviderPage() { onFileSelected={handleFileSelected} selectedFiles={selectedFiles} isAuthenticated={true} + isIngesting={isIngesting} accessToken={accessToken || undefined} clientId={connector.clientId} onSettingsChange={setIngestSettings} @@ -361,14 +361,15 @@ export default function UploadProviderPage() { Back
diff --git a/frontend/src/components/cloud-picker/file-item.tsx b/frontend/src/components/cloud-picker/file-item.tsx index 5370e4cc..f06aa92e 100644 --- a/frontend/src/components/cloud-picker/file-item.tsx +++ b/frontend/src/components/cloud-picker/file-item.tsx @@ -1,7 +1,6 @@ "use client"; -import { Badge } from "@/components/ui/badge"; -import { FileText, Folder, Trash, Trash2 } from "lucide-react"; +import { FileText, Folder, Trash2 } from "lucide-react"; import { CloudFile } from "./types"; import GoogleDriveIcon from "@/app/settings/icons/google-drive-icon"; import SharePointIcon from "@/app/settings/icons/share-point-icon"; @@ -11,6 +10,7 @@ import { Button } from "@/components/ui/button"; interface FileItemProps { provider: string; file: CloudFile; + shouldDisableActions: boolean; onRemove: (fileId: string) => void; } diff --git a/frontend/src/components/cloud-picker/file-list.tsx b/frontend/src/components/cloud-picker/file-list.tsx index b584249a..db7daf64 100644 --- a/frontend/src/components/cloud-picker/file-list.tsx +++ b/frontend/src/components/cloud-picker/file-list.tsx @@ -9,6 +9,7 @@ interface FileListProps { files: CloudFile[]; onClearAll: () => void; onRemoveFile: (fileId: string) => void; + shouldDisableActions: boolean; } export const FileList = ({ @@ -16,11 +17,14 @@ export const FileList = ({ files, onClearAll, onRemoveFile, + shouldDisableActions, }: FileListProps) => { if (files.length === 0) { return null; } + console.log({ shouldDisableActions }); + return (
@@ -42,6 +46,7 @@ export const FileList = ({ file={file} onRemove={onRemoveFile} provider={provider} + shouldDisableActions={shouldDisableActions} /> ))}
diff --git a/frontend/src/components/cloud-picker/types.ts b/frontend/src/components/cloud-picker/types.ts index ca346bf0..20b1eae0 100644 --- a/frontend/src/components/cloud-picker/types.ts +++ b/frontend/src/components/cloud-picker/types.ts @@ -25,6 +25,7 @@ export interface UnifiedCloudPickerProps { baseUrl?: string; // Ingest settings onSettingsChange?: (settings: IngestSettings) => void; + isIngesting: boolean; } export interface GoogleAPI { diff --git a/frontend/src/components/cloud-picker/unified-cloud-picker.tsx b/frontend/src/components/cloud-picker/unified-cloud-picker.tsx index 0a4699af..e92cef13 100644 --- a/frontend/src/components/cloud-picker/unified-cloud-picker.tsx +++ b/frontend/src/components/cloud-picker/unified-cloud-picker.tsx @@ -16,6 +16,7 @@ export const UnifiedCloudPicker = ({ onFileSelected, selectedFiles = [], isAuthenticated, + isIngesting, accessToken, onPickerStateChange, clientId, @@ -183,6 +184,7 @@ export const UnifiedCloudPicker = ({ files={selectedFiles} onClearAll={handleClearAll} onRemoveFile={handleRemoveFile} + shouldDisableActions={isIngesting} /> Date: Mon, 6 Oct 2025 10:40:57 -0600 Subject: [PATCH 05/12] updated inputs in Ingest Settings for cloud-picker component --- .../components/ui/inputs/embedding-model.tsx | 62 ++++++++++++ .../components/ui/inputs/number-input.tsx | 68 +++++++++++++ .../cloud-picker/ingest-settings.tsx | 98 +++++++++++++------ 3 files changed, 197 insertions(+), 31 deletions(-) create mode 100644 frontend/components/ui/inputs/embedding-model.tsx create mode 100644 frontend/components/ui/inputs/number-input.tsx diff --git a/frontend/components/ui/inputs/embedding-model.tsx b/frontend/components/ui/inputs/embedding-model.tsx new file mode 100644 index 00000000..ded138ad --- /dev/null +++ b/frontend/components/ui/inputs/embedding-model.tsx @@ -0,0 +1,62 @@ +import { ModelOption } from "@/app/api/queries/useGetModelsQuery"; +import { + getFallbackModels, + ModelProvider, +} from "@/app/settings/helpers/model-helpers"; +import { ModelSelectItems } from "@/app/settings/helpers/model-select-item"; +import { LabelWrapper } from "@/components/label-wrapper"; +import { + Select, + SelectContent, + SelectTrigger, + SelectValue, +} from "@radix-ui/react-select"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@radix-ui/react-tooltip"; + +interface EmbeddingModelInputProps { + disabled?: boolean; + value: string; + onChange: (value: string) => void; + modelsData?: { + embedding_models: ModelOption[]; + }; + currentProvider?: ModelProvider; +} + +export const EmbeddingModelInput = ({ + disabled, + value, + onChange, + modelsData, + currentProvider = "openai", +}: EmbeddingModelInputProps) => { + return ( + + + + ); +}; diff --git a/frontend/components/ui/inputs/number-input.tsx b/frontend/components/ui/inputs/number-input.tsx new file mode 100644 index 00000000..44e04fcb --- /dev/null +++ b/frontend/components/ui/inputs/number-input.tsx @@ -0,0 +1,68 @@ +import { LabelWrapper } from "@/components/label-wrapper"; +import { Button } from "../button"; +import { Input } from "../input"; +import { Minus, Plus } from "lucide-react"; + +interface NumberInputProps { + id: string; + label: string; + value: number; + onChange: (value: number) => void; + unit: string; + min?: number; + max?: number; + disabled?: boolean; +} + +export const NumberInput = ({ + id, + label, + value, + onChange, + min = 1, + max, + disabled, + unit, +}: NumberInputProps) => { + return ( + +
+ onChange(parseInt(e.target.value) || 0)} + className="w-full pr-20 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + /> +
+ + {unit} + +
+ + +
+
+
+
+ ); +}; diff --git a/frontend/src/components/cloud-picker/ingest-settings.tsx b/frontend/src/components/cloud-picker/ingest-settings.tsx index 8c72ce6b..422e1a70 100644 --- a/frontend/src/components/cloud-picker/ingest-settings.tsx +++ b/frontend/src/components/cloud-picker/ingest-settings.tsx @@ -7,8 +7,23 @@ import { CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; -import { ChevronRight, Info } from "lucide-react"; +import { ChevronRight } from "lucide-react"; import { IngestSettings as IngestSettingsType } from "./types"; +import { LabelWrapper } from "@/components/label-wrapper"; +import { + Select, + SelectContent, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { ModelSelectItems } from "@/app/settings/helpers/model-select-item"; +import { getFallbackModels } from "@/app/settings/helpers/model-helpers"; +import { NumberInput } from "@/components/ui/inputs/number-input"; interface IngestSettingsProps { isOpen: boolean; @@ -40,6 +55,8 @@ export const IngestSettings = ({ onSettingsChange?.(updatedSettings); }; + console.log({ currentSettings }); + return ( +
+ {/* Embedding model selection - currently disabled */} + + + +
-
Chunk size
- - handleSettingsChange({ - chunkSize: parseInt(e.target.value) || 0, - }) - } + onChange={(value) => handleSettingsChange({ chunkSize: value })} + unit="characters" />
-
Chunk overlap
- - handleSettingsChange({ - chunkOverlap: parseInt(e.target.value) || 0, - }) + onChange={(value) => + handleSettingsChange({ chunkOverlap: value }) } + unit="characters" />
@@ -117,21 +168,6 @@ export const IngestSettings = ({ } />
- -
-
- Embedding model - -
- - handleSettingsChange({ embeddingModel: e.target.value }) - } - placeholder="text-embedding-3-small" - /> -
From 5de3a821c484eb2d519b02d6eee362aea24417cb Mon Sep 17 00:00:00 2001 From: Brent O'Neill Date: Mon, 6 Oct 2025 10:48:39 -0600 Subject: [PATCH 06/12] remove console.log --- frontend/src/components/cloud-picker/ingest-settings.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/components/cloud-picker/ingest-settings.tsx b/frontend/src/components/cloud-picker/ingest-settings.tsx index 422e1a70..883ebf13 100644 --- a/frontend/src/components/cloud-picker/ingest-settings.tsx +++ b/frontend/src/components/cloud-picker/ingest-settings.tsx @@ -1,6 +1,5 @@ "use client"; -import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Collapsible, @@ -55,8 +54,6 @@ export const IngestSettings = ({ onSettingsChange?.(updatedSettings); }; - console.log({ currentSettings }); - return ( Date: Mon, 6 Oct 2025 10:53:42 -0600 Subject: [PATCH 07/12] heading margin --- frontend/src/app/upload/[provider]/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/upload/[provider]/page.tsx b/frontend/src/app/upload/[provider]/page.tsx index 3254bc73..ab169063 100644 --- a/frontend/src/app/upload/[provider]/page.tsx +++ b/frontend/src/app/upload/[provider]/page.tsx @@ -327,7 +327,7 @@ export default function UploadProviderPage() { return (
-
+
From 9f16f9f25a1b72715617c2110dff6ff5d24d8a22 Mon Sep 17 00:00:00 2001 From: Brent O'Neill Date: Mon, 6 Oct 2025 11:38:33 -0600 Subject: [PATCH 08/12] Added inner shadow and capped height --- frontend/components/ui/button.tsx | 2 +- frontend/src/app/globals.css | 13 ++++++++++ .../src/components/cloud-picker/file-list.tsx | 26 +++++++++---------- .../cloud-picker/unified-cloud-picker.tsx | 20 +++++++------- 4 files changed, 38 insertions(+), 23 deletions(-) diff --git a/frontend/components/ui/button.tsx b/frontend/components/ui/button.tsx index c901d8ad..81a3537d 100644 --- a/frontend/components/ui/button.tsx +++ b/frontend/components/ui/button.tsx @@ -3,7 +3,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import * as React from "react"; const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none disabled:select-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none disabled:select-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 150d1c56..f3f04679 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -312,4 +312,17 @@ .discord-error { @apply text-xs opacity-70; } + + .box-shadow-inner::after { + content: " "; + position: absolute; + bottom: 0; + left: 0; + right: 0; + pointer-events: none; + background: linear-gradient(to top, hsl(var(--background)), transparent); + display: block; + width: 100%; + height: 30px; + } } diff --git a/frontend/src/components/cloud-picker/file-list.tsx b/frontend/src/components/cloud-picker/file-list.tsx index db7daf64..7033fcf8 100644 --- a/frontend/src/components/cloud-picker/file-list.tsx +++ b/frontend/src/components/cloud-picker/file-list.tsx @@ -23,10 +23,8 @@ export const FileList = ({ return null; } - console.log({ shouldDisableActions }); - return ( -
+

Added files ({files.length})

-
- {files.map((file) => ( - - ))} +
+
+ {files.map((file) => ( + + ))} +
); diff --git a/frontend/src/components/cloud-picker/unified-cloud-picker.tsx b/frontend/src/components/cloud-picker/unified-cloud-picker.tsx index e92cef13..ff0c3903 100644 --- a/frontend/src/components/cloud-picker/unified-cloud-picker.tsx +++ b/frontend/src/components/cloud-picker/unified-cloud-picker.tsx @@ -169,15 +169,17 @@ export const UnifiedCloudPicker = ({ } return ( -
- +
+
+ +
Date: Mon, 6 Oct 2025 11:46:41 -0600 Subject: [PATCH 09/12] remove max head room --- frontend/src/app/upload/[provider]/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/upload/[provider]/page.tsx b/frontend/src/app/upload/[provider]/page.tsx index ab169063..0a015c82 100644 --- a/frontend/src/app/upload/[provider]/page.tsx +++ b/frontend/src/app/upload/[provider]/page.tsx @@ -326,7 +326,7 @@ export default function UploadProviderPage() { } return ( -
+
-
+
From b3a11fed39aaff0e313f0062c52a97023d9d7bdd Mon Sep 17 00:00:00 2001 From: Brent O'Neill Date: Mon, 6 Oct 2025 14:36:21 -0600 Subject: [PATCH 11/12] fix number input border colors --- .../components/ui/inputs/number-input.tsx | 18 +++++++++----- .../cloud-picker/ingest-settings.tsx | 24 +++++++++++++++---- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/frontend/components/ui/inputs/number-input.tsx b/frontend/components/ui/inputs/number-input.tsx index 44e04fcb..b312292f 100644 --- a/frontend/components/ui/inputs/number-input.tsx +++ b/frontend/components/ui/inputs/number-input.tsx @@ -37,28 +37,34 @@ export const NumberInput = ({ onChange={(e) => onChange(parseInt(e.target.value) || 0)} className="w-full pr-20 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" /> -
+
{unit} -
+
diff --git a/frontend/src/components/cloud-picker/ingest-settings.tsx b/frontend/src/components/cloud-picker/ingest-settings.tsx index 883ebf13..a9ed0c8f 100644 --- a/frontend/src/components/cloud-picker/ingest-settings.tsx +++ b/frontend/src/components/cloud-picker/ingest-settings.tsx @@ -110,8 +110,8 @@ export const IngestSettings = ({
-
-
+
+
-
+ {/*
+
+
Table Structure
+
+ Capture table structure during ingest. +
+
+ + handleSettingsChange({ tableStructure: checked }) + } + /> +
*/} + +
OCR
@@ -149,7 +165,7 @@ export const IngestSettings = ({ />
-
+
Picture descriptions From a54ea69869334157a5e9ce75006ab52b4d109802 Mon Sep 17 00:00:00 2001 From: Brent O'Neill Date: Mon, 6 Oct 2025 15:14:39 -0600 Subject: [PATCH 12/12] hide tooltip when it has files --- frontend/src/app/upload/[provider]/page.tsx | 49 ++++++++++++++------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/upload/[provider]/page.tsx b/frontend/src/app/upload/[provider]/page.tsx index 608dde98..10b9b0e5 100644 --- a/frontend/src/app/upload/[provider]/page.tsx +++ b/frontend/src/app/upload/[provider]/page.tsx @@ -7,6 +7,11 @@ import { type CloudFile, UnifiedCloudPicker } from "@/components/cloud-picker"; import type { IngestSettings } from "@/components/cloud-picker/types"; import { Button } from "@/components/ui/button"; import { useTask } from "@/contexts/task-context"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; // CloudFile interface is now imported from the unified cloud picker @@ -325,6 +330,8 @@ export default function UploadProviderPage() { ); } + const hasSelectedFiles = selectedFiles.length > 0; + return (
@@ -355,26 +362,36 @@ export default function UploadProviderPage() {
- + + + + + {!hasSelectedFiles ? ( + + Select at least one file before ingesting + + ) : null} +