import { useEffect, useState } from "react"; import { Button } from "../ui/button"; import { Dialog, DialogContent, DialogDescription, DialogTitle } from "../ui/dialog"; import SpacesSelector from "./SpacesSelector"; import { motion } from "framer-motion"; import { AlertCircle, CheckCircle, Upload } from "lucide-react"; import { toast } from "sonner"; interface MarkdownUploadModalProps { isOpen: boolean; onClose: () => void; } export function MarkdownUploadModal({ isOpen, onClose }: MarkdownUploadModalProps) { const [files, setFiles] = useState([]); const [isUploading, setIsUploading] = useState(false); const [progress, setProgress] = useState<{ progress: number; processed: number; total: number; succeeded: number; failed: number; status: string; } | null>(null); const [selectedSpaces, setSelectedSpaces] = useState([]); const handleFileChange = async (e: React.ChangeEvent) => { const selectedFiles = e.target.files; if (!selectedFiles) return; const mdFiles: File[] = []; const processFile = async (file: File) => { if (file.name.endsWith(".md")) { mdFiles.push(file); } }; // Handle both individual files and directory const files = Array.from(selectedFiles); await Promise.all(files.map(processFile)); if (mdFiles.length === 0) { toast.error("No markdown files found"); return; } setFiles(mdFiles); toast.success(`Found ${mdFiles.length} markdown files`); }; const handleUpload = async () => { if (files.length === 0) return; setIsUploading(true); const progressToastId = toast.loading("Starting markdown import..."); let lastToastTime = Date.now(); try { // Convert markdown files to content const contents = await Promise.all( files.map(async (file) => { const content = await file.text(); return { content, title: file.name.replace(".md", ""), type: "note", }; }), ); // Send to batch endpoint const response = await fetch("/backend/v1/batch-add", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ contents, spaces: selectedSpaces, }), }); if (!response.ok) { throw new Error("Failed to start batch upload"); } const reader = response.body?.getReader(); if (!reader) throw new Error("No reader available"); while (true) { const { done, value } = await reader.read(); if (done) break; const text = new TextDecoder().decode(value); const lines = text.split("\n"); for (const line of lines) { if (line.startsWith("data: ")) { try { const data = JSON.parse(line.slice(5)); setProgress(data); // Update the main progress toast every 500ms const now = Date.now(); if (now - lastToastTime > 500) { toast.loading( `Processing files: ${data.processed}/${data.total} (${data.progress}%)`, { id: progressToastId }, ); lastToastTime = now; } // Show individual file status toasts with a limit if (data.status === "success" && data.processed % 5 === 0) { toast.success(`Successfully imported ${data.processed} files so far`); } else if (data.status === "error") { toast.error(`Failed to import: ${data.title} - ${data.error}`); } if (data.status === "complete") { toast.success( `Import complete! ${data.succeeded} succeeded, ${data.failed} failed`, { id: progressToastId, duration: 5000 }, ); // Wait for 2 seconds before closing await new Promise((resolve) => setTimeout(resolve, 2000)); onClose(); setFiles([]); setProgress(null); break; } } catch (e) { console.error("Error parsing SSE data:", e); } } } } } catch (error) { toast.error("Upload failed: " + (error instanceof Error ? error.message : "Unknown error")); } finally { setIsUploading(false); } }; return ( !isUploading && onClose()}> {isUploading ? (
{progress ? ( <>
{progress.progress}%
Processing Files ({progress.processed}/{progress.total}) {progress.succeeded} succeeded, {progress.failed} failed ) : ( )}
) : (
Import from Obsidian Upload markdown files from your Obsidian vault. You can select multiple files or drop a folder.
{/* Individual Files Selection */}
OR
{/* Folder Selection */}
{files.length > 0 && ( <>
{files.length} markdown files selected
)}
)}
); }