init
This commit is contained in:
parent
fdcc41ea48
commit
ad39d88a7b
3 changed files with 271 additions and 124 deletions
|
|
@ -26,6 +26,10 @@ import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { useTask } from "@/contexts/task-context";
|
import { useTask } from "@/contexts/task-context";
|
||||||
import { cn } from "@/lib/utils";
|
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";
|
import type { File as SearchFile } from "@/src/app/api/queries/useGetSearchQuery";
|
||||||
|
|
||||||
export function KnowledgeDropdown() {
|
export function KnowledgeDropdown() {
|
||||||
|
|
@ -163,8 +167,17 @@ export function KnowledgeDropdown() {
|
||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
const resetFileInput = () => {
|
||||||
const files = e.target.files;
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = async (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
const files = event.target.files;
|
||||||
|
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
|
|
||||||
|
|
@ -172,37 +185,16 @@ export function KnowledgeDropdown() {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if filename already exists (using ORIGINAL filename)
|
|
||||||
console.log("[Duplicate Check] Checking file:", file.name);
|
console.log("[Duplicate Check] Checking file:", file.name);
|
||||||
const checkResponse = await fetch(
|
const checkData = await duplicateCheck(file);
|
||||||
`/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();
|
|
||||||
console.log("[Duplicate Check] Result:", checkData);
|
console.log("[Duplicate Check] Result:", checkData);
|
||||||
|
|
||||||
if (checkData.exists) {
|
if (checkData.exists) {
|
||||||
// Show duplicate handling dialog
|
|
||||||
console.log("[Duplicate Check] Duplicate detected, showing dialog");
|
console.log("[Duplicate Check] Duplicate detected, showing dialog");
|
||||||
setPendingFile(file);
|
setPendingFile(file);
|
||||||
setDuplicateFilename(file.name);
|
setDuplicateFilename(file.name);
|
||||||
setShowDuplicateDialog(true);
|
setShowDuplicateDialog(true);
|
||||||
// Reset file input
|
resetFileInput();
|
||||||
if (fileInputRef.current) {
|
|
||||||
fileInputRef.current.value = "";
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,105 +209,20 @@ export function KnowledgeDropdown() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset file input
|
resetFileInput();
|
||||||
if (fileInputRef.current) {
|
|
||||||
fileInputRef.current.value = "";
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadFile = async (file: File, replace: boolean) => {
|
const uploadFile = async (file: File, replace: boolean) => {
|
||||||
setFileUploading(true);
|
setFileUploading(true);
|
||||||
|
|
||||||
// Trigger the same file upload event as the chat page
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent("fileUploadStart", {
|
|
||||||
detail: { filename: file.name },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
await uploadFileUtil(file, replace);
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
refetchTasks();
|
refetchTasks();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.dispatchEvent(
|
toast.error("Upload failed", {
|
||||||
new CustomEvent("fileUploadError", {
|
description: error instanceof Error ? error.message : "Unknown error",
|
||||||
detail: {
|
});
|
||||||
filename: file.name,
|
|
||||||
error: error instanceof Error ? error.message : "Upload failed",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
window.dispatchEvent(new CustomEvent("fileUploadComplete"));
|
|
||||||
setFileUploading(false);
|
setFileUploading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -332,6 +239,7 @@ export function KnowledgeDropdown() {
|
||||||
});
|
});
|
||||||
|
|
||||||
await uploadFile(pendingFile, true);
|
await uploadFile(pendingFile, true);
|
||||||
|
|
||||||
setPendingFile(null);
|
setPendingFile(null);
|
||||||
setDuplicateFilename("");
|
setDuplicateFilename("");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
138
frontend/lib/upload-utils.ts
Normal file
138
frontend/lib/upload-utils.ts
Normal 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { type ChangeEvent, useRef, useState } from "react";
|
||||||
import { StickToBottom } from "use-stick-to-bottom";
|
import { StickToBottom } from "use-stick-to-bottom";
|
||||||
import { AssistantMessage } from "@/app/chat/components/assistant-message";
|
import { AssistantMessage } from "@/app/chat/components/assistant-message";
|
||||||
import { UserMessage } from "@/app/chat/components/user-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 type { Message } from "@/app/chat/types";
|
||||||
import OnboardingCard from "@/app/onboarding/components/onboarding-card";
|
import OnboardingCard from "@/app/onboarding/components/onboarding-card";
|
||||||
import { useChatStreaming } from "@/hooks/useChatStreaming";
|
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";
|
import { OnboardingStep } from "./onboarding-step";
|
||||||
|
|
||||||
export function OnboardingContent({
|
export function OnboardingContent({
|
||||||
|
|
@ -22,6 +25,10 @@ export function OnboardingContent({
|
||||||
const [assistantMessage, setAssistantMessage] = useState<Message | null>(
|
const [assistantMessage, setAssistantMessage] = useState<Message | null>(
|
||||||
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({
|
const { streamingMessage, isLoading, sendMessage } = useChatStreaming({
|
||||||
onComplete: (message, newResponseId) => {
|
onComplete: (message, newResponseId) => {
|
||||||
|
|
@ -54,6 +61,76 @@ export function OnboardingContent({
|
||||||
}, 1500);
|
}, 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)
|
// Determine which message to show (streaming takes precedence)
|
||||||
const displayMessage = streamingMessage || assistantMessage;
|
const displayMessage = streamingMessage || assistantMessage;
|
||||||
|
|
||||||
|
|
@ -150,18 +227,42 @@ export function OnboardingContent({
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p className="text-muted-foreground">
|
<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>
|
</p>
|
||||||
<button
|
<div className="flex flex-col sm:flex-row gap-2">
|
||||||
type="button"
|
<button
|
||||||
onClick={handleStepComplete}
|
type="button"
|
||||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
|
onClick={handleUploadClick}
|
||||||
>
|
disabled={isUploading}
|
||||||
Go to Chat
|
className="px-4 py-2 border border-primary text-primary rounded-lg hover:bg-primary/10 disabled:cursor-not-allowed disabled:opacity-70"
|
||||||
</button>
|
>
|
||||||
|
{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>
|
</div>
|
||||||
</OnboardingStep>
|
</OnboardingStep>
|
||||||
</div>
|
</div>
|
||||||
|
<DuplicateHandlingDialog
|
||||||
|
open={showDuplicateDialog}
|
||||||
|
onOpenChange={handleDuplicateDialogChange}
|
||||||
|
onOverwrite={handleOverwriteFile}
|
||||||
|
isLoading={isUploading}
|
||||||
|
/>
|
||||||
</StickToBottom.Content>
|
</StickToBottom.Content>
|
||||||
</StickToBottom>
|
</StickToBottom>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue