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 */}
{/* 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 */}
>
) : (
/* Empty State */
No Project Selected
Create a new project or import a document
)}
);
}