import React, { useState, useEffect, useRef, useCallback } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Plus, Trash2, FileText, Pencil, Download, Sparkles, Send, Loader2, Check, X, Menu, ChevronLeft, Save, Upload, FileDown } from "lucide-react"; import { clsx } from "clsx"; import { twMerge } from "tailwind-merge"; import { useLabsProjects } from "../hooks/useLabsProjects.js"; import LabsEditor from "./LabsEditor.jsx"; import { exportToWord } from "../utils/exportToWord.js"; import { exportToPdf } from "../utils/exportToPdf.js"; import { stripMetadata } from "../utils/contentUtils.js"; function cn(...inputs) { return twMerge(clsx(inputs)); } /** * Project item in the sidebar */ function ProjectItem({ project, isActive, onSelect, onRename, onDelete, modelName }) { const [isEditing, setIsEditing] = useState(false); const [editName, setEditName] = useState(project.name); const inputRef = useRef(null); useEffect(() => { if (isEditing && inputRef.current) { inputRef.current.focus(); inputRef.current.select(); } }, [isEditing]); const handleSave = () => { if (editName.trim()) { onRename(project.id, editName.trim()); } else { setEditName(project.name); } setIsEditing(false); }; const handleKeyDown = (e) => { if (e.key === "Enter") { handleSave(); } else if (e.key === "Escape") { setEditName(project.name); setIsEditing(false); } }; return ( !isEditing && onSelect(project.id)} >
{isEditing ? ( setEditName(e.target.value)} onBlur={handleSave} onKeyDown={handleKeyDown} onClick={(e) => e.stopPropagation()} className="w-full bg-transparent border-b border-foreground/30 outline-none text-sm py-0.5 text-white" /> ) : ( <> {project.name} {modelName && ( {modelName} )} )}
{!isEditing && ( <> )}
); } /** * Main Labs workspace component */ export default function LabsArea({ toggleSidebar, sidebarOpen, onProjectLockChange }) { const { projects, activeProject, activeProjectId, setActiveProjectId, isProjectLocked, isProcessing, handleNewProject, handleImportDocument, handleDeleteProject, handleRenameProject, handleUpdateDocument, handleAIEdit, forceSync, getModelName } = useLabsProjects(); const [instruction, setInstruction] = useState(""); const [error, setError] = useState(""); const [showProjectList, setShowProjectList] = useState(true); const [saveStatus, setSaveStatus] = useState("saved"); const [isImporting, setIsImporting] = useState(false); const [showExportMenu, setShowExportMenu] = useState(false); const instructionRef = useRef(null); const importInputRef = useRef(null); const exportMenuRef = useRef(null); // Notify parent when project lock state changes useEffect(() => { if (onProjectLockChange) { onProjectLockChange(isProjectLocked); } }, [isProjectLocked, onProjectLockChange]); // Handle document changes with save status const handleDocumentChange = useCallback((newContent) => { setSaveStatus("unsaved"); handleUpdateDocument(newContent); setTimeout(() => { setSaveStatus("saved"); }, 500); }, [handleUpdateDocument]); // Manual save function const handleManualSave = () => { setSaveStatus("saving"); forceSync?.(); setTimeout(() => { setSaveStatus("saved"); }, 300); }; // Handle file import const handleFileImport = async (e) => { const file = e.target.files?.[0]; if (!file) return; setIsImporting(true); setError(""); try { await handleImportDocument(file); } catch (err) { setError("Failed to import document: " + (err.message || "Unknown error")); } finally { setIsImporting(false); if (importInputRef.current) { importInputRef.current.value = ""; } } }; // Handle AI instruction submission const handleSubmitInstruction = async () => { if (!instruction.trim() || isProcessing) return; setError(""); try { await handleAIEdit(instruction.trim()); setInstruction(""); } catch (err) { setError(err.message || "Failed to process instruction"); } }; // Handle selection-based AI editing const handleSelectionEdit = async ({ selectedText, instruction, contextBefore, contextAfter }) => { try { const response = await fetch("/labs-edit-selection", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ selectedText, instruction, contextBefore, contextAfter, sessionId: activeProject?.sessionId }) }); if (!response.ok) { const errorText = await response.text().catch(() => ""); throw new Error(errorText || `Request failed (${response.status})`); } // Parse SSE response if (!response.body) { throw new Error("No response body"); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let fullContent = ""; let buffer = ""; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (line.startsWith("data: ")) { try { const data = JSON.parse(line.slice(6)); if (data.text) { fullContent += data.text; } } catch { fullContent += line.slice(6); } } } } return stripMetadata(fullContent); } catch (error) { console.error("[Labs] Selection edit failed:", error); setError(error.message || "Failed to edit selection"); throw error; } }; const handleKeyDown = (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSubmitInstruction(); } }; // Handle Word export const handleExportWord = async () => { if (!activeProject?.document) return; setShowExportMenu(false); try { await exportToWord(activeProject.document, activeProject.name); } catch (err) { setError("Failed to export document"); console.error("[Labs] Export error:", err); } }; // Handle PDF export const handleExportPdf = async () => { if (!activeProject?.document) return; setShowExportMenu(false); try { await exportToPdf(activeProject.document, activeProject.name); } catch (err) { setError("Failed to export PDF"); console.error("[Labs] PDF Export error:", err); } }; // Close export menu when clicking outside useEffect(() => { const handleClickOutside = (e) => { if (exportMenuRef.current && !exportMenuRef.current.contains(e.target)) { setShowExportMenu(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); // Hide project list on mobile by default useEffect(() => { const handleResize = () => { if (window.innerWidth < 768) { setShowProjectList(false); } }; handleResize(); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); return (
{/* Mobile Header */}
Labs
{/* Project Sidebar */} {showProjectList && ( {/* Sidebar Header */}

Projects

{/* Import Button */}
{/* Project List */}
{projects.map((project) => ( { setActiveProjectId(id); if (window.innerWidth < 768) { setShowProjectList(false); } }} onRename={handleRenameProject} onDelete={handleDeleteProject} modelName={getModelName()} /> ))}
)}
{/* Main Editor Area */}
{activeProject ? ( <> {/* Editor Header */}

{activeProject.name}

{saveStatus === "saved" && "Saved"} {saveStatus === "saving" && "Saving..."} {saveStatus === "unsaved" && "Unsaved"}
{/* Export Dropdown */}
{showExportMenu && ( )}
{/* Document Editor */}
{/* AI Instruction Bar */}
{error && (
{error}
)}
setInstruction(e.target.value)} onKeyDown={handleKeyDown} placeholder={activeProject.document ? "Give an instruction to edit..." : "Describe what to create..." } disabled={isProcessing} className="flex-1 bg-transparent outline-none text-white placeholder:text-muted-foreground text-sm" />
) : ( /* Empty State */

No Project Selected

Create a new project or import a document

)}
); }