mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
[aidan] ui/ux: when building app window appears immediately
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import Box from '@mui/material/Box';
|
||||
import Fade from '@mui/material/Fade';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
@@ -10,7 +11,7 @@ import CloseIcon from '@mui/icons-material/Close';
|
||||
import GridViewRoundedIcon from '@mui/icons-material/GridViewRounded';
|
||||
import { Output, SERVE_BASE } from '@/shared/state/outputsSlice';
|
||||
import { setViewCardPosition, setViewCardSize, removeViewCard } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { useAppDispatch } from '@/shared/hooks';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { API_BASE, getAuthToken } from '@/shared/config';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import ViewPreview, { ViewPreviewHandle } from '@/app/pages/Views/ViewPreview';
|
||||
@@ -407,6 +408,7 @@ const DashboardViewCard: React.FC<Props> = ({
|
||||
inputData={inputData}
|
||||
backendResult={backendResult}
|
||||
/>
|
||||
<BuildingOverlay sessionId={output.session_id ?? null} />
|
||||
</Box>
|
||||
|
||||
{/* Resize handles */}
|
||||
@@ -481,6 +483,51 @@ const DashboardViewCard: React.FC<Props> = ({
|
||||
|
||||
export default React.memo(DashboardViewCard);
|
||||
|
||||
// Calm overlay shown while the App Builder chat that owns this output is
|
||||
// actively editing it. Hides whatever transient half-broken state the agent
|
||||
// might be writing through (a missing import, a syntax error mid-keystroke)
|
||||
// so the user sees "Building..." instead of an error iframe. Fades in/out.
|
||||
const BuildingOverlay: React.FC<{ sessionId: string | null }> = ({ sessionId }) => {
|
||||
const c = useClaudeTokens();
|
||||
const status = useAppSelector((s) => (sessionId ? s.agents.sessions[sessionId]?.status : undefined));
|
||||
const isBuilding = status === 'running' || status === 'waiting_approval';
|
||||
return (
|
||||
<Fade in={isBuilding} timeout={{ enter: 200, exit: 220 }} unmountOnExit>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: 11,
|
||||
bgcolor: c.bg.surface,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 1.25,
|
||||
// Block pointer events to the iframe behind so user can't click into the half-built app.
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
bgcolor: c.accent.primary,
|
||||
animation: 'view-card-building-pulse 1.2s ease-in-out infinite',
|
||||
'@keyframes view-card-building-pulse': {
|
||||
'0%, 100%': { opacity: 0.35, transform: 'scale(0.85)' },
|
||||
'50%': { opacity: 1, transform: 'scale(1)' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Typography sx={{ color: c.text.secondary, fontSize: '0.85rem', fontWeight: 500 }}>
|
||||
Building…
|
||||
</Typography>
|
||||
</Box>
|
||||
</Fade>
|
||||
);
|
||||
};
|
||||
|
||||
// Old-mode outputs render the legacy serve URL; new-mode webapp_template outputs attach to a runtime and point the webview at Vite once frontend_url arrives.
|
||||
const DashboardOutputPreview: React.FC<{
|
||||
previewRef: React.Ref<ViewPreviewHandle>;
|
||||
|
||||
@@ -38,7 +38,6 @@ interface UseAgentSpawnArgs {
|
||||
setToolbarOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setAutoFocusSessionId: Dispatch<SetStateAction<string | null>>;
|
||||
setPendingSelectSessionId: Dispatch<SetStateAction<string | null>>;
|
||||
pendingViewBuilderSessionsRef: RefObject<Set<string>>;
|
||||
}
|
||||
|
||||
export function useAgentSpawn({
|
||||
@@ -55,7 +54,6 @@ export function useAgentSpawn({
|
||||
setToolbarOpen,
|
||||
setAutoFocusSessionId,
|
||||
setPendingSelectSessionId,
|
||||
pendingViewBuilderSessionsRef,
|
||||
}: UseAgentSpawnArgs) {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -157,12 +155,6 @@ export function useAgentSpawn({
|
||||
).then((action) => {
|
||||
if (launchAndSendFirstMessage.fulfilled.match(action)) {
|
||||
const realId = action.payload.session.id;
|
||||
// App Builder chats expect a view card to pop in next to the chat
|
||||
// once the backend seeds its Output row. Register the session id
|
||||
// so useDashboardLifecycle's output watcher can do the auto-open.
|
||||
if (mode === 'view-builder') {
|
||||
pendingViewBuilderSessionsRef.current?.add(realId);
|
||||
}
|
||||
dispatch(generateTitle({ sessionId: realId, prompt }));
|
||||
if (selectedBrowserIds?.length) {
|
||||
dispatch(setGlowingBrowserCards({ browserIds: selectedBrowserIds, sessionId: realId, label: 'Use Browser' }));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, type MutableRefObject, type RefObject } from 'react';
|
||||
import { useEffect, useRef, type MutableRefObject } from 'react';
|
||||
import { report } from '@/shared/serviceClient';
|
||||
import { store } from '@/shared/state/store';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
@@ -40,7 +40,6 @@ interface UseDashboardLifecycleArgs {
|
||||
handleHighlightCard: (cardId: string) => void;
|
||||
hasFittedRef: MutableRefObject<boolean>;
|
||||
restoredExpandedRef: MutableRefObject<boolean>;
|
||||
pendingViewBuilderSessionsRef: RefObject<Set<string>>;
|
||||
}
|
||||
|
||||
export function useDashboardLifecycle({
|
||||
@@ -57,7 +56,6 @@ export function useDashboardLifecycle({
|
||||
handleHighlightCard,
|
||||
hasFittedRef,
|
||||
restoredExpandedRef,
|
||||
pendingViewBuilderSessionsRef,
|
||||
}: UseDashboardLifecycleArgs) {
|
||||
const dispatch = useAppDispatch();
|
||||
const pendingBrowserUrl = useAppSelector((state) => state.tempState.pendingBrowserUrl);
|
||||
@@ -256,27 +254,35 @@ export function useDashboardLifecycle({
|
||||
}
|
||||
}, [layoutInitialized, outputsLoaded, viewCards, outputs, dispatch]);
|
||||
|
||||
// App Builder auto-open: when a view-builder session launched on this
|
||||
// dashboard, useAgentSpawn parks its id in pendingViewBuilderSessionsRef.
|
||||
// The backend then seeds an Output row and broadcasts agent:output_upserted;
|
||||
// when that lands in outputsSlice we drop a ViewCard next to the chat so the
|
||||
// user sees the app appear without a manual "open" step. Per-mount tracked
|
||||
// (not redux) so a manual close after auto-open stays closed.
|
||||
// On first load after outputs settle, snapshot every existing Output id as
|
||||
// "already accounted for." Any output that ARRIVES later (typically the
|
||||
// agent:output_upserted WS broadcast the backend fires the instant a
|
||||
// view-builder session is seeded, at session start) whose session_id points
|
||||
// at a view-builder chat on this dashboard gets a view card dropped on the
|
||||
// canvas right away. Per-mount tracked so a manual close after auto-open
|
||||
// stays closed. Prior approach keyed off a pending-set populated inside
|
||||
// launchAndSendFirstMessage.then(): the WS upsert won the race and the
|
||||
// effect saw an empty set, so the card didn't pop until the session-end
|
||||
// meta-sync re-broadcast.
|
||||
const autoOpenedOutputsRef = useRef<Set<string>>(new Set());
|
||||
const outputsSnapshottedRef = useRef<boolean>(false);
|
||||
useEffect(() => {
|
||||
if (!layoutInitialized) return;
|
||||
const pending = pendingViewBuilderSessionsRef.current;
|
||||
if (!pending || pending.size === 0) return;
|
||||
if (!layoutInitialized || !outputsLoaded) return;
|
||||
if (!outputsSnapshottedRef.current) {
|
||||
for (const oid of Object.keys(outputs)) autoOpenedOutputsRef.current.add(oid);
|
||||
outputsSnapshottedRef.current = true;
|
||||
return;
|
||||
}
|
||||
for (const output of Object.values(outputs)) {
|
||||
if (autoOpenedOutputsRef.current.has(output.id)) continue;
|
||||
const sid = output.session_id;
|
||||
if (!sid || !pending.has(sid)) continue;
|
||||
if (viewCards[output.id]) {
|
||||
pending.delete(sid);
|
||||
continue;
|
||||
}
|
||||
// Let addViewCard pick a collision-free grid cell; the auto-fit below
|
||||
// pans/zooms to show the chat + new view card together regardless.
|
||||
if (!sid) continue;
|
||||
const sess = sessions[sid];
|
||||
if (!sess || sess.mode !== 'view-builder') continue;
|
||||
if (sess.dashboard_id !== dashboardId) continue;
|
||||
autoOpenedOutputsRef.current.add(output.id);
|
||||
if (viewCards[output.id]) continue;
|
||||
dispatch(addViewCard({ outputId: output.id, expandedSessionIds }));
|
||||
pending.delete(sid);
|
||||
const outputId = output.id;
|
||||
setTimeout(() => {
|
||||
const vc = store.getState().dashboardLayout.viewCards[outputId];
|
||||
@@ -288,7 +294,7 @@ export function useDashboardLifecycle({
|
||||
handleHighlightCard(outputId);
|
||||
}, 200);
|
||||
}
|
||||
}, [layoutInitialized, outputs, viewCards, expandedSessionIds, dispatch, canvasActions, handleHighlightCard, pendingViewBuilderSessionsRef]);
|
||||
}, [layoutInitialized, outputsLoaded, outputs, sessions, viewCards, dashboardId, expandedSessionIds, dispatch, canvasActions, handleHighlightCard]);
|
||||
|
||||
const namedOnFirstMessageRef = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
|
||||
@@ -113,13 +113,6 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
|
||||
contentRef: canvas.contentRef,
|
||||
});
|
||||
|
||||
// Session ids that just launched in App Builder ('view-builder') mode and
|
||||
// are still awaiting their backend-seeded Output row. useAgentSpawn writes
|
||||
// here; useDashboardLifecycle consumes it to auto-open the view card the
|
||||
// moment the output upsert arrives. Per-mount ref (not redux) so a manual
|
||||
// close after auto-open stays closed.
|
||||
const pendingViewBuilderSessionsRef = useRef<Set<string>>(new Set());
|
||||
|
||||
useDashboardLifecycle({
|
||||
isActive,
|
||||
dashboardId,
|
||||
@@ -134,7 +127,6 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
|
||||
handleHighlightCard,
|
||||
hasFittedRef,
|
||||
restoredExpandedRef,
|
||||
pendingViewBuilderSessionsRef,
|
||||
});
|
||||
|
||||
// ---- Auto-reveal / collapse / unreveal sub-agent cards ----
|
||||
@@ -229,7 +221,6 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
|
||||
setToolbarOpen,
|
||||
setAutoFocusSessionId,
|
||||
setPendingSelectSessionId,
|
||||
pendingViewBuilderSessionsRef,
|
||||
});
|
||||
|
||||
const {
|
||||
|
||||
Reference in New Issue
Block a user