From 70507067cbfa67f0baeb223d3d2c6ac47270f9db Mon Sep 17 00:00:00 2001 From: Mike Fortman Date: Wed, 22 Oct 2025 12:09:46 -0500 Subject: [PATCH] init --- frontend/components/knowledge-dropdown.tsx | 138 +++--------------- frontend/lib/upload-utils.ts | 138 ++++++++++++++++++ .../components/onboarding-content.tsx | 119 +++++++++++++-- 3 files changed, 271 insertions(+), 124 deletions(-) create mode 100644 frontend/lib/upload-utils.ts diff --git a/frontend/components/knowledge-dropdown.tsx b/frontend/components/knowledge-dropdown.tsx index 19ddc387..fc522f9b 100644 --- a/frontend/components/knowledge-dropdown.tsx +++ b/frontend/components/knowledge-dropdown.tsx @@ -26,6 +26,10 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useTask } from "@/contexts/task-context"; import { cn } from "@/lib/utils"; +import { + duplicateCheck, + uploadFile as uploadFileUtil, +} from "@/lib/upload-utils"; import type { File as SearchFile } from "@/src/app/api/queries/useGetSearchQuery"; export function KnowledgeDropdown() { @@ -163,8 +167,17 @@ export function KnowledgeDropdown() { fileInputRef.current?.click(); }; - const handleFileChange = async (e: React.ChangeEvent) => { - const files = e.target.files; + const resetFileInput = () => { + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + const handleFileChange = async ( + event: React.ChangeEvent + ) => { + const files = event.target.files; + if (files && files.length > 0) { const file = files[0]; @@ -172,37 +185,16 @@ export function KnowledgeDropdown() { setIsOpen(false); try { - // Check if filename already exists (using ORIGINAL filename) console.log("[Duplicate Check] Checking file:", file.name); - const checkResponse = await fetch( - `/api/documents/check-filename?filename=${encodeURIComponent( - file.name - )}` - ); - - console.log("[Duplicate Check] Response status:", checkResponse.status); - - if (!checkResponse.ok) { - const errorText = await checkResponse.text(); - console.error("[Duplicate Check] Error response:", errorText); - throw new Error( - `Failed to check duplicates: ${checkResponse.statusText}` - ); - } - - const checkData = await checkResponse.json(); + const checkData = await duplicateCheck(file); console.log("[Duplicate Check] Result:", checkData); if (checkData.exists) { - // Show duplicate handling dialog console.log("[Duplicate Check] Duplicate detected, showing dialog"); setPendingFile(file); setDuplicateFilename(file.name); setShowDuplicateDialog(true); - // Reset file input - if (fileInputRef.current) { - fileInputRef.current.value = ""; - } + resetFileInput(); return; } @@ -217,105 +209,20 @@ export function KnowledgeDropdown() { } } - // Reset file input - if (fileInputRef.current) { - fileInputRef.current.value = ""; - } + resetFileInput(); }; const uploadFile = async (file: File, replace: boolean) => { setFileUploading(true); - // Trigger the same file upload event as the chat page - window.dispatchEvent( - new CustomEvent("fileUploadStart", { - detail: { filename: file.name }, - }) - ); - try { - const formData = new FormData(); - formData.append("file", file); - formData.append("replace_duplicates", replace.toString()); - - // Use router upload and ingest endpoint (automatically routes based on configuration) - const uploadIngestRes = await fetch("/api/router/upload_ingest", { - method: "POST", - body: formData, - }); - - const uploadIngestJson = await uploadIngestRes.json(); - - if (!uploadIngestRes.ok) { - throw new Error(uploadIngestJson?.error || "Upload and ingest failed"); - } - - // Extract results from the response - handle both unified and simple formats - const fileId = - uploadIngestJson?.upload?.id || - uploadIngestJson?.id || - uploadIngestJson?.task_id; - const filePath = - uploadIngestJson?.upload?.path || uploadIngestJson?.path || "uploaded"; - const runJson = uploadIngestJson?.ingestion; - const deleteResult = uploadIngestJson?.deletion; - console.log("c", uploadIngestJson); - if (!fileId) { - throw new Error("Upload successful but no file id returned"); - } - // Check if ingestion actually succeeded - if ( - runJson && - runJson.status !== "COMPLETED" && - runJson.status !== "SUCCESS" - ) { - const errorMsg = runJson.error || "Ingestion pipeline failed"; - throw new Error( - `Ingestion failed: ${errorMsg}. Try setting DISABLE_INGEST_WITH_LANGFLOW=true if you're experiencing Langflow component issues.` - ); - } - // Log deletion status if provided - if (deleteResult) { - if (deleteResult.status === "deleted") { - console.log( - "File successfully cleaned up from Langflow:", - deleteResult.file_id - ); - } else if (deleteResult.status === "delete_failed") { - console.warn( - "Failed to cleanup file from Langflow:", - deleteResult.error - ); - } - } - // Notify UI - window.dispatchEvent( - new CustomEvent("fileUploaded", { - detail: { - file: file, - result: { - file_id: fileId, - file_path: filePath, - run: runJson, - deletion: deleteResult, - unified: true, - }, - }, - }) - ); - + await uploadFileUtil(file, replace); refetchTasks(); } catch (error) { - window.dispatchEvent( - new CustomEvent("fileUploadError", { - detail: { - filename: file.name, - error: error instanceof Error ? error.message : "Upload failed", - }, - }) - ); + toast.error("Upload failed", { + description: error instanceof Error ? error.message : "Unknown error", + }); } finally { - window.dispatchEvent(new CustomEvent("fileUploadComplete")); setFileUploading(false); } }; @@ -332,6 +239,7 @@ export function KnowledgeDropdown() { }); await uploadFile(pendingFile, true); + setPendingFile(null); setDuplicateFilename(""); } diff --git a/frontend/lib/upload-utils.ts b/frontend/lib/upload-utils.ts new file mode 100644 index 00000000..6a7e7301 --- /dev/null +++ b/frontend/lib/upload-utils.ts @@ -0,0 +1,138 @@ +export interface DuplicateCheckResponse { + exists: boolean; + [key: string]: unknown; +} + +export interface UploadFileResult { + fileId: string; + filePath: string; + run: unknown; + deletion: unknown; + unified: boolean; + raw: unknown; +} + +export async function duplicateCheck( + file: File +): Promise { + const response = await fetch( + `/api/documents/check-filename?filename=${encodeURIComponent(file.name)}` + ); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + errorText || `Failed to check duplicates: ${response.statusText}` + ); + } + + return response.json(); +} + +export async function uploadFile( + file: File, + replace = false +): Promise { + window.dispatchEvent( + new CustomEvent("fileUploadStart", { + detail: { filename: file.name }, + }) + ); + + try { + const formData = new FormData(); + formData.append("file", file); + formData.append("replace_duplicates", replace.toString()); + + const uploadResponse = await fetch("/api/router/upload_ingest", { + method: "POST", + body: formData, + }); + + let payload: unknown; + try { + payload = await uploadResponse.json(); + } catch (error) { + throw new Error("Upload failed: unable to parse server response"); + } + + const uploadIngestJson = + typeof payload === "object" && payload !== null ? payload : {}; + + if (!uploadResponse.ok) { + const errorMessage = + (uploadIngestJson as { error?: string }).error || + "Upload and ingest failed"; + throw new Error(errorMessage); + } + + const fileId = + (uploadIngestJson as { upload?: { id?: string } }).upload?.id || + (uploadIngestJson as { id?: string }).id || + (uploadIngestJson as { task_id?: string }).task_id; + const filePath = + (uploadIngestJson as { upload?: { path?: string } }).upload?.path || + (uploadIngestJson as { path?: string }).path || + "uploaded"; + const runJson = (uploadIngestJson as { ingestion?: unknown }).ingestion; + const deletionJson = (uploadIngestJson as { deletion?: unknown }).deletion; + + if (!fileId) { + throw new Error("Upload successful but no file id returned"); + } + + if ( + runJson && + typeof runJson === "object" && + "status" in (runJson as Record) && + (runJson as { status?: string }).status !== "COMPLETED" && + (runJson as { status?: string }).status !== "SUCCESS" + ) { + const errorMsg = + (runJson as { error?: string }).error || + "Ingestion pipeline failed"; + throw new Error( + `Ingestion failed: ${errorMsg}. Try setting DISABLE_INGEST_WITH_LANGFLOW=true if you're experiencing Langflow component issues.` + ); + } + + const result: UploadFileResult = { + fileId, + filePath, + run: runJson, + deletion: deletionJson, + unified: true, + raw: uploadIngestJson, + }; + + window.dispatchEvent( + new CustomEvent("fileUploaded", { + detail: { + file, + result: { + file_id: fileId, + file_path: filePath, + run: runJson, + deletion: deletionJson, + unified: true, + }, + }, + }) + ); + + return result; + } catch (error) { + window.dispatchEvent( + new CustomEvent("fileUploadError", { + detail: { + filename: file.name, + error: + error instanceof Error ? error.message : "Upload failed", + }, + }) + ); + throw error; + } finally { + window.dispatchEvent(new CustomEvent("fileUploadComplete")); + } +} diff --git a/frontend/src/app/new-onboarding/components/onboarding-content.tsx b/frontend/src/app/new-onboarding/components/onboarding-content.tsx index 46886f47..a6c28069 100644 --- a/frontend/src/app/new-onboarding/components/onboarding-content.tsx +++ b/frontend/src/app/new-onboarding/components/onboarding-content.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { type ChangeEvent, useRef, useState } from "react"; import { StickToBottom } from "use-stick-to-bottom"; import { AssistantMessage } from "@/app/chat/components/assistant-message"; import { UserMessage } from "@/app/chat/components/user-message"; @@ -8,6 +8,9 @@ import Nudges from "@/app/chat/nudges"; import type { Message } from "@/app/chat/types"; import OnboardingCard from "@/app/onboarding/components/onboarding-card"; import { useChatStreaming } from "@/hooks/useChatStreaming"; +import { DuplicateHandlingDialog } from "@/components/duplicate-handling-dialog"; +import { duplicateCheck, uploadFile as uploadFileUtil } from "@/lib/upload-utils"; +import { toast } from "sonner"; import { OnboardingStep } from "./onboarding-step"; export function OnboardingContent({ @@ -22,6 +25,10 @@ export function OnboardingContent({ const [assistantMessage, setAssistantMessage] = useState( null, ); + const fileInputRef = useRef(null); + const [pendingFile, setPendingFile] = useState(null); + const [showDuplicateDialog, setShowDuplicateDialog] = useState(false); + const [isUploading, setIsUploading] = useState(false); const { streamingMessage, isLoading, sendMessage } = useChatStreaming({ onComplete: (message, newResponseId) => { @@ -54,6 +61,76 @@ export function OnboardingContent({ }, 1500); }; + const resetFileInput = () => { + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + const handleUploadClick = () => { + fileInputRef.current?.click(); + }; + + const handleDuplicateDialogChange = (open: boolean) => { + if (!open) { + setPendingFile(null); + } + setShowDuplicateDialog(open); + }; + + const performUpload = async (file: File, replace = false) => { + setIsUploading(true); + try { + await uploadFileUtil(file, replace); + toast.success("Document uploaded successfully"); + } catch (error) { + toast.error("Upload failed", { + description: error instanceof Error ? error.message : "Unknown error", + }); + } finally { + setIsUploading(false); + } + }; + + const handleFileChange = async (event: ChangeEvent) => { + const selectedFile = event.target.files?.[0]; + if (!selectedFile) { + resetFileInput(); + return; + } + + try { + const duplicateInfo = await duplicateCheck(selectedFile); + if (duplicateInfo.exists) { + setPendingFile(selectedFile); + setShowDuplicateDialog(true); + return; + } + + await performUpload(selectedFile, false); + } catch (error) { + toast.error("Unable to prepare file for upload", { + description: error instanceof Error ? error.message : "Unknown error", + }); + } finally { + resetFileInput(); + } + }; + + const handleOverwriteFile = async () => { + if (!pendingFile) { + return; + } + + const fileToUpload = pendingFile; + setPendingFile(null); + try { + await performUpload(fileToUpload, true); + } finally { + resetFileInput(); + } + }; + // Determine which message to show (streaming takes precedence) const displayMessage = streamingMessage || assistantMessage; @@ -150,18 +227,42 @@ export function OnboardingContent({ >

- Your account is ready to use. Let's start chatting! + Upload a starter document to begin building your knowledge base + or jump straight into a conversation.

- +
+ + +
+
+ );