diff --git a/apps/web/src/domains/library/library-detail-page.tsx b/apps/web/src/domains/library/library-detail-page.tsx index 0708d860c2e..8d8a862f4ce 100644 --- a/apps/web/src/domains/library/library-detail-page.tsx +++ b/apps/web/src/domains/library/library-detail-page.tsx @@ -1,13 +1,112 @@ -import { useParams } from "react-router"; +import { Loader2 } from "lucide-react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useNavigate, useParams } from "react-router"; + +import { toast } from "@vellum/design-library"; + +import { useAssistantContext } from "@/domains/chat/assistant-context.js"; +import { openApp, shareApp } from "@/domains/chat/lib/apps.js"; +import { AppViewerContainer } from "@/domains/intelligence/components/apps/app-viewer-container.js"; +import { routes } from "@/utils/routes.js"; + +interface LoadedApp { + appId: string; + dirName?: string; + name: string; + html: string; +} export function LibraryDetailPage() { const { appId } = useParams<{ appId: string }>(); + const { assistantId } = useAssistantContext(); + const navigate = useNavigate(); + + const [app, setApp] = useState(null); + const [error, setError] = useState(null); + const [isSharing, setIsSharing] = useState(false); + const requestRef = useRef(null); + + useEffect(() => { + if (!assistantId || !appId) return; + requestRef.current = appId; + setApp(null); + setError(null); + + openApp(assistantId, appId) + .then((result) => { + if (requestRef.current !== appId) return; + setApp({ + appId: result.appId, + dirName: result.dirName, + name: result.name, + html: result.html, + }); + }) + .catch((err) => { + if (requestRef.current !== appId) return; + setError(err instanceof Error ? err.message : "Failed to open app"); + }); + + return () => { + requestRef.current = null; + }; + }, [assistantId, appId]); + + const handleClose = useCallback(() => { + void navigate(routes.library.root); + }, [navigate]); + + const handleShare = useCallback(async () => { + if (!assistantId || !app || isSharing) return; + setIsSharing(true); + try { + await shareApp(assistantId, app.appId, app.name); + toast.success("App exported", { description: `${app.name}.vellum` }); + } catch (err) { + toast.error("Failed to share app", { + description: err instanceof Error ? err.message : undefined, + }); + } finally { + setIsSharing(false); + } + }, [assistantId, app, isSharing]); + + if (!assistantId || !appId) return null; + + if (error) { + return ( +
+

+ {error} +

+ +
+ ); + } + + if (!app) { + return ( +
+ +
+ ); + } + return ( -
-

Library item

-

- Placeholder for library item {appId}. -

-
+ ); } diff --git a/apps/web/src/domains/library/library-page.tsx b/apps/web/src/domains/library/library-page.tsx index 0c432852dc6..f8fe58d7d70 100644 --- a/apps/web/src/domains/library/library-page.tsx +++ b/apps/web/src/domains/library/library-page.tsx @@ -1,8 +1,35 @@ +import { useCallback } from "react"; +import { useNavigate } from "react-router"; + +import { useAssistantContext } from "@/domains/chat/assistant-context.js"; +import { LibraryView } from "@/domains/intelligence/components/apps/library-view.js"; +import { routes } from "@/utils/routes.js"; + export function LibraryPage() { + const { assistantId } = useAssistantContext(); + const navigate = useNavigate(); + + const handleNewConversation = useCallback( + (_initialMessage?: string) => { + // TODO: initialMessage seeding requires cross-route state coordination + // (e.g. a Zustand store or sessionStorage handoff). The platform passes + // initialMessage directly via startNewConversation() in the same React + // tree, but here the library is a separate route. For now we just + // navigate to chat; the deploy-flow prompt handoff will come with the + // broader cross-route state work. + void navigate(routes.assistant); + }, + [navigate], + ); + + if (!assistantId) return null; + return ( -
-

Library

-

Placeholder route. Library listing lands with the platform/web code port.

-
+
+ +
); }