| import { useEffect, useMemo, useState, useCallback, useRef } from "react"; |
| import { stripMetadata } from "../utils/contentUtils.js"; |
|
|
| |
| const STORAGE_KEY = "labs_projects_global"; |
|
|
| |
| |
| |
| function loadProjects() { |
| try { |
| const raw = localStorage.getItem(STORAGE_KEY); |
| if (!raw) { |
| console.log("[Labs] No saved projects"); |
| return []; |
| } |
| const parsed = JSON.parse(raw); |
| if (!Array.isArray(parsed)) { |
| console.warn("[Labs] Invalid data format"); |
| return []; |
| } |
| console.log(`[Labs] Loaded ${parsed.length} projects`); |
| return parsed; |
| } catch (e) { |
| console.error("[Labs] Load error:", e); |
| return []; |
| } |
| } |
|
|
| |
| |
| |
| function saveProjects(projects) { |
| try { |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(projects)); |
| console.log(`[Labs] Saved ${projects.length} projects`); |
| } catch (e) { |
| console.error("[Labs] Save error:", e); |
| } |
| } |
|
|
| |
| |
| |
| export function useLabsProjects() { |
| |
| const [projects, setProjects] = useState(() => { |
| const loaded = loadProjects(); |
| return loaded; |
| }); |
|
|
| const [activeProjectId, setActiveProjectIdState] = useState(() => { |
| const loaded = loadProjects(); |
| return loaded.length > 0 ? loaded[0].id : ""; |
| }); |
|
|
| const [isProcessing, setIsProcessing] = useState(false); |
|
|
| |
| const activeProject = useMemo(() => { |
| return projects.find(p => p.id === activeProjectId) || null; |
| }, [projects, activeProjectId]); |
|
|
| |
| const isProjectLocked = useMemo(() => { |
| if (!activeProject) return false; |
| return (activeProject.document?.trim().length ?? 0) > 0; |
| }, [activeProject]); |
|
|
| |
| const [labsModelName, setLabsModelName] = useState(""); |
| useEffect(() => { |
| fetch("/labs-model") |
| .then(r => r.json()) |
| .then(data => { |
| if (data.model?.name) setLabsModelName(data.model.name); |
| }) |
| .catch(() => { }); |
| }, []); |
|
|
| |
| useEffect(() => { |
| if (projects.length > 0) { |
| saveProjects(projects); |
| } |
| }, [projects]); |
|
|
| |
| useEffect(() => { |
| if (projects.length === 0) { |
| console.log("[Labs] Creating initial project"); |
| const newProject = { |
| id: crypto.randomUUID(), |
| sessionId: crypto.randomUUID(), |
| name: "Untitled Project", |
| createdAt: Date.now(), |
| updatedAt: Date.now(), |
| document: "" |
| }; |
| setProjects([newProject]); |
| setActiveProjectIdState(newProject.id); |
| } |
| }, [projects.length]); |
|
|
| |
| const projectList = useMemo(() => { |
| return [...projects].sort((a, b) => b.updatedAt - a.updatedAt); |
| }, [projects]); |
|
|
| |
| const getModelName = useCallback(() => { |
| return labsModelName || "Labs Model"; |
| }, [labsModelName]); |
|
|
| |
| |
| |
| const setActiveProjectId = useCallback((projectId) => { |
| const project = projects.find(p => p.id === projectId); |
| if (project) { |
| setActiveProjectIdState(projectId); |
| console.log(`[Labs] Activated: ${project.name}`); |
| } |
| }, [projects]); |
|
|
| |
| |
| |
| const handleNewProject = useCallback((name = "Untitled Project", document = "") => { |
| const newProject = { |
| id: crypto.randomUUID(), |
| sessionId: crypto.randomUUID(), |
| name, |
| createdAt: Date.now(), |
| updatedAt: Date.now(), |
| document |
| }; |
| setProjects(prev => [newProject, ...prev]); |
| setActiveProjectIdState(newProject.id); |
| console.log(`[Labs] Created: ${name}`); |
| return newProject; |
| }, []); |
|
|
| |
| |
| |
| const handleImportDocument = useCallback(async (file) => { |
| if (!file) return null; |
| const name = file.name.replace(/\.(txt|md|docx)$/i, "") || "Imported"; |
| let content = ""; |
| try { |
| if (file.name.endsWith(".docx")) { |
| const mammoth = await import("mammoth"); |
| const buffer = await file.arrayBuffer(); |
| const result = await mammoth.extractRawText({ arrayBuffer: buffer }); |
| content = result.value || ""; |
| } else { |
| content = await file.text(); |
| } |
| return handleNewProject(name, content); |
| } catch (e) { |
| console.error("[Labs] Import error:", e); |
| throw e; |
| } |
| }, [handleNewProject]); |
|
|
| |
| |
| |
| const handleDeleteProject = useCallback((projectId) => { |
| setProjects(prev => { |
| const filtered = prev.filter(p => p.id !== projectId); |
| |
| if (projectId === activeProjectId && filtered.length > 0) { |
| setActiveProjectIdState(filtered[0].id); |
| } |
| |
| if (filtered.length === 0) { |
| localStorage.removeItem(STORAGE_KEY); |
| } |
| return filtered; |
| }); |
| }, [activeProjectId]); |
|
|
| |
| |
| |
| const handleRenameProject = useCallback((projectId, newName) => { |
| setProjects(prev => prev.map(p => |
| p.id === projectId ? { ...p, name: newName, updatedAt: Date.now() } : p |
| )); |
| }, []); |
|
|
| |
| |
| |
| const handleUpdateDocument = useCallback((newDocument) => { |
| if (!activeProjectId) return; |
| setProjects(prev => prev.map(p => |
| p.id === activeProjectId |
| ? { ...p, document: newDocument, updatedAt: Date.now() } |
| : p |
| )); |
| }, [activeProjectId]); |
|
|
| |
| |
| |
| const handleAIEdit = useCallback(async (instruction) => { |
| if (!activeProject) return null; |
| setIsProcessing(true); |
| console.log(`[Labs] AI Edit with session: ${activeProject.sessionId}`); |
|
|
| try { |
| const response = await fetch("/labs-edit", { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ |
| document: activeProject.document, |
| instruction, |
| sessionId: activeProject.sessionId |
| }) |
| }); |
|
|
| if (!response.ok) { |
| const err = await response.text().catch(() => ""); |
| throw new Error(err || `Request failed (${response.status})`); |
| } |
|
|
| 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); |
| } |
| } |
| } |
| } |
|
|
| if (fullContent) { |
| const cleaned = stripMetadata(fullContent); |
| handleUpdateDocument(cleaned); |
| } |
| return fullContent; |
| } catch (e) { |
| console.error("[Labs] AI edit error:", e); |
| throw e; |
| } finally { |
| setIsProcessing(false); |
| } |
| }, [activeProject, handleUpdateDocument]); |
|
|
| |
| |
| |
| const forceSync = useCallback(() => { |
| saveProjects(projects); |
| }, [projects]); |
|
|
| return { |
| projects: projectList, |
| activeProject, |
| activeProjectId, |
| setActiveProjectId, |
| isProjectLocked, |
| isProcessing, |
| handleNewProject, |
| handleImportDocument, |
| handleDeleteProject, |
| handleRenameProject, |
| handleUpdateDocument, |
| handleAIEdit, |
| forceSync, |
| getModelName, |
| labsModelName |
| }; |
| } |
|
|