diff --git a/frontend/src/app/canvas/CanvasPage.tsx b/frontend/src/app/canvas/CanvasPage.tsx index 3f08f51..204ab02 100644 --- a/frontend/src/app/canvas/CanvasPage.tsx +++ b/frontend/src/app/canvas/CanvasPage.tsx @@ -76,12 +76,60 @@ const snapToGrid = (x: number, y: number): { x: number; y: number } => ({ y: Math.round(y / SNAP_GRID[1]) * SNAP_GRID[1], }) -function FlowFitViewOnLoad() { +function FlowFitViewOnLoad({ disabled }: { disabled?: boolean }) { const nodesInitialized = useNodesInitialized() const { fitView } = useReactFlow() React.useEffect(() => { - if (nodesInitialized) fitView?.({ duration: 200 }) - }, [nodesInitialized, fitView]) + if (disabled || !nodesInitialized) return + fitView?.({ duration: 200 }) + }, [disabled, nodesInitialized, fitView]) + return null +} + +function FocusNodeOnLoad({ focusNodeId }: { focusNodeId?: string }) { + const nodesInitialized = useNodesInitialized() + const { getNodes, setCenter, screenToFlowPosition, project } = useReactFlow() + + React.useEffect(() => { + if (!focusNodeId || !nodesInitialized) return + // Small delay so node DOM and layout are fully ready before centering/zooming. + const timer = setTimeout(() => { + const nodes = getNodes() + const node = nodes.find((n) => n.id === focusNodeId) + if (!node) return + + const el = document.querySelector( + `.react-flow__node[data-id="${focusNodeId}"]` + ) as HTMLElement | null + + if (el) { + const rect = el.getBoundingClientRect() + const screenCenter = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 } + const toFlow = screenToFlowPosition ?? project + if (toFlow) { + const flowCenter = toFlow(screenCenter) + setCenter(flowCenter.x, flowCenter.y, { duration: 400, zoom: 1.8 }) + return + } + } + + // Fallback: center using node position + dimensions from React Flow state. + const anyNode = node as Node & { + positionAbsolute?: { x: number; y: number } + width?: number + height?: number + } + const basePos = anyNode.positionAbsolute ?? anyNode.position ?? { x: 0, y: 0 } + const width = anyNode.width ?? 0 + const height = anyNode.height ?? 0 + const centerX = basePos.x + width / 2 + const centerY = basePos.y + height / 2 + setCenter(centerX, centerY, { duration: 400, zoom: 1.8 }) + }, 150) + + return () => clearTimeout(timer) + }, [focusNodeId, nodesInitialized, getNodes, setCenter, screenToFlowPosition, project]) + return null } @@ -152,9 +200,11 @@ function FullscreenNodeContent({ node }: { node: AppNode }) { export type CanvasPageProps = { /** Optional recollection id for per-recollection graph loading */ recollectionId?: string + /** Optional node id to focus when the canvas loads (e.g. from Katalogos artifacts). */ + focusNodeId?: string } -export function CanvasPage({ recollectionId }: CanvasPageProps) { +export function CanvasPage({ recollectionId, focusNodeId }: CanvasPageProps) { const { theme } = useTheme() const { showMinimap } = usePlatform() const { @@ -688,7 +738,8 @@ export function CanvasPage({ recollectionId }: CanvasPageProps) { )} - + + () + const [searchParams] = useSearchParams() const { updateLastEdited } = usePlatform() const updateLastEditedRef = useRef(updateLastEdited) updateLastEditedRef.current = updateLastEdited @@ -17,11 +19,13 @@ export function FluxRoute() { if (recollectionId) updateLastEditedRef.current(recollectionId) }, [recollectionId]) + const focusNodeId = searchParams.get('focusNode') ?? undefined + if (!recollectionId) return null return (
- +
) } diff --git a/frontend/src/app/recollections/katalogos/KatalogosPage.tsx b/frontend/src/app/recollections/katalogos/KatalogosPage.tsx index 8dd58fe..e12de46 100644 --- a/frontend/src/app/recollections/katalogos/KatalogosPage.tsx +++ b/frontend/src/app/recollections/katalogos/KatalogosPage.tsx @@ -1,29 +1,125 @@ /** * Katalogos page: third view for a recollection. - * Simple placeholder layout, matching Logos/Flux flex and typography. + * Shows live \"Artifacts\" from Flux rendering nodes in a card grid. */ -import React from 'react' -import { useParams } from 'react-router-dom' +import React, { useMemo } from 'react' +import { useNavigate, useParams } from 'react-router-dom' import { usePlatform } from '@/app/kosmos/KosmosContext' +import { getRenderOutputCache, formatTimeSinceLastUpdate } from '../state/recollectionStore' +import { Button } from '@/components/ui/button' export function KatalogosPage() { const { recollectionId } = useParams<{ recollectionId: string }>() const { recollections } = usePlatform() + const navigate = useNavigate() const recollection = recollectionId ? recollections.find((p) => p.id === recollectionId) : null const title = recollection?.name ?? 'Untitled' + const artifacts = useMemo( + () => (recollectionId ? getRenderOutputCache(recollectionId) : []), + [recollectionId] + ) + + const handleViewInFlux = (nodeId: string) => { + if (!recollectionId) return + navigate(`/recollections/${recollectionId}/flux?focusNode=${encodeURIComponent(nodeId)}`) + } + return (
-
+

{title}

-

Katalogos

-
- Katalogos view coming soon. -
+

+ Katalogos · Live artifacts produced by Flux rendering nodes for this recollection. +

+ {artifacts.length === 0 ? ( +
+ No artifacts yet. In Flux, run a graph with a rendering node; its output will be cached as an artifact and + appear here, as well as in Logos blocks that insert artifacts. +
+ ) : ( +
+ {artifacts.map((artifact) => { + const label = artifact.label || artifact.nodeId + const isImage = + artifact.type === 'image' || + Boolean(artifact.content?.trim() && /]/i.test(artifact.content.trim())) + const imageSrc = + isImage && artifact.content + ? artifact.content.startsWith('data:') + ? artifact.content + : `data:image/svg+xml;utf8,${encodeURIComponent(artifact.content)}` + : null + + return ( +
+
+
+

{label}

+

Node: {artifact.nodeId}

+
+ + + Live + +
+
+ {isImage && imageSrc ? ( +
+ {label +
+ ) : artifact.content ? ( +
+