This commit is contained in:
Mike Fortman 2025-10-22 12:09:46 -05:00
parent 092ec306d5
commit 70507067cb
3 changed files with 271 additions and 124 deletions

View file

@ -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<HTMLInputElement>) => {
const files = e.target.files;
const resetFileInput = () => {
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const handleFileChange = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
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("");
}

View file

@ -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<DuplicateCheckResponse> {
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<UploadFileResult> {
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<string, unknown>) &&
(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"));
}
}

View file

@ -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<Message | null>(
null,
);
const fileInputRef = useRef<HTMLInputElement>(null);
const [pendingFile, setPendingFile] = useState<File | null>(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<HTMLInputElement>) => {
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({
>
<div className="space-y-4">
<p className="text-muted-foreground">
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.
</p>
<button
type="button"
onClick={handleStepComplete}
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
>
Go to Chat
</button>
<div className="flex flex-col sm:flex-row gap-2">
<button
type="button"
onClick={handleUploadClick}
disabled={isUploading}
className="px-4 py-2 border border-primary text-primary rounded-lg hover:bg-primary/10 disabled:cursor-not-allowed disabled:opacity-70"
>
{isUploading ? "Uploading..." : "Upload a Document"}
</button>
<button
type="button"
onClick={handleStepComplete}
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
>
Go to Chat
</button>
</div>
<input
ref={fileInputRef}
type="file"
onChange={handleFileChange}
className="hidden"
accept=".pdf,.doc,.docx,.txt,.md,.rtf,.odt"
/>
</div>
</OnboardingStep>
</div>
<DuplicateHandlingDialog
open={showDuplicateDialog}
onOpenChange={handleDuplicateDialogChange}
onOverwrite={handleOverwriteFile}
isLoading={isUploading}
/>
</StickToBottom.Content>
</StickToBottom>
);