mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 13:17:40 +02:00
[aidan] ux/app-builder-dashboard: improve app builder dashboard (#89)
* [aidan] ux: when agent creates app always opens in dashboard * [aidan] enhancement: app editing ui mimicing browseragents * [aidan] ui/ux: when building app window appears immediately * [aidan]: app view spacing dashboard mimicing browser * [aidan] ux: app error agent loop * [aidan] fix: remove duplicate logic
This commit is contained in:
@@ -38,6 +38,7 @@ export interface ViewCardPosition {
|
||||
width: number;
|
||||
height: number;
|
||||
zOrder: number;
|
||||
parent_session_id?: string | null;
|
||||
}
|
||||
|
||||
export interface BrowserTab {
|
||||
@@ -199,6 +200,11 @@ interface Rect {
|
||||
h: number;
|
||||
}
|
||||
|
||||
interface CardPlacementExclusion {
|
||||
type: CardType;
|
||||
id: string;
|
||||
}
|
||||
|
||||
function rectsOverlap(a: Rect, b: Rect): boolean {
|
||||
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
|
||||
}
|
||||
@@ -206,20 +212,25 @@ function rectsOverlap(a: Rect, b: Rect): boolean {
|
||||
function collectOccupiedRects(
|
||||
state: DashboardLayoutState,
|
||||
expandedSessionIds?: string[],
|
||||
exclude?: CardPlacementExclusion,
|
||||
): Rect[] {
|
||||
const expanded = new Set(expandedSessionIds);
|
||||
const rects: Rect[] = [];
|
||||
for (const c of Object.values(state.cards)) {
|
||||
if (exclude?.type === 'agent' && exclude.id === c.session_id) continue;
|
||||
const h = expanded.has(c.session_id) ? Math.max(EXPANDED_CARD_MIN_H, c.height) : c.height;
|
||||
rects.push({ x: c.x, y: c.y, w: c.width, h });
|
||||
}
|
||||
for (const c of Object.values(state.viewCards)) {
|
||||
if (exclude?.type === 'view' && exclude.id === c.output_id) continue;
|
||||
rects.push({ x: c.x, y: c.y, w: c.width, h: c.height });
|
||||
}
|
||||
for (const c of Object.values(state.browserCards)) {
|
||||
if (exclude?.type === 'browser' && exclude.id === c.browser_id) continue;
|
||||
rects.push({ x: c.x, y: c.y, w: c.width, h: c.height });
|
||||
}
|
||||
for (const n of Object.values(state.notes)) {
|
||||
if (exclude?.type === 'note' && exclude.id === n.note_id) continue;
|
||||
rects.push({ x: n.x, y: n.y, w: n.width, h: n.height });
|
||||
}
|
||||
return rects;
|
||||
@@ -312,6 +323,36 @@ export function findOpenSpotNear(
|
||||
return findOpenGridCell(occupiedRects, newW, newH);
|
||||
}
|
||||
|
||||
export function placeInParentColumn(
|
||||
state: DashboardLayoutState,
|
||||
parentSessionId: string | null | undefined,
|
||||
newW: number,
|
||||
newH: number,
|
||||
expandedSessionIds?: string[],
|
||||
exclude?: CardPlacementExclusion,
|
||||
): { x: number; y: number } {
|
||||
const rects = collectOccupiedRects(state, expandedSessionIds, exclude);
|
||||
const parentCard = parentSessionId ? state.cards[parentSessionId] : null;
|
||||
if (!parentCard) {
|
||||
return findOpenGridCell(rects, newW, newH);
|
||||
}
|
||||
|
||||
const targetX = parentCard.x + parentCard.width + GRID_GAP * 12;
|
||||
const columnCards = [
|
||||
...Object.values(state.browserCards).filter(
|
||||
(c) => !(exclude?.type === 'browser' && exclude.id === c.browser_id),
|
||||
),
|
||||
...Object.values(state.viewCards).filter(
|
||||
(c) => !(exclude?.type === 'view' && exclude.id === c.output_id),
|
||||
),
|
||||
].filter((c) => Math.abs(c.x - targetX) < 50);
|
||||
const targetY = columnCards.length > 0
|
||||
? Math.max(...columnCards.map((c) => c.y + c.height)) + GRID_GAP
|
||||
: parentCard.y;
|
||||
|
||||
return findOpenSpotNear(targetX, targetY, rects, newW, newH);
|
||||
}
|
||||
|
||||
// Reconnect-refetch merge: ADD only the cards the snapshot carries that the
|
||||
// client is missing (e.g. a spawned browser whose broadcast was lost in a
|
||||
// socket gap), collision-resolving each against the live layout so a recovered
|
||||
@@ -522,27 +563,38 @@ const dashboardLayoutSlice = createSlice({
|
||||
|
||||
addViewCard(state, action: PayloadAction<{
|
||||
outputId: string; expandedSessionIds?: string[];
|
||||
parentSessionId?: string | null;
|
||||
x?: number; y?: number; width?: number; height?: number;
|
||||
}>) {
|
||||
const { outputId, expandedSessionIds, x, y, width, height } = action.payload;
|
||||
const { outputId, expandedSessionIds, parentSessionId, x, y, width, height } = action.payload;
|
||||
if (state.viewCards[outputId]) return;
|
||||
const w = width || DEFAULT_VIEW_CARD_W;
|
||||
const h = height || DEFAULT_VIEW_CARD_H;
|
||||
let posX: number, posY: number;
|
||||
if (x != null && y != null) {
|
||||
posX = x;
|
||||
posY = y;
|
||||
} else {
|
||||
const rects = collectOccupiedRects(state, expandedSessionIds);
|
||||
const pos = findOpenGridCell(rects, DEFAULT_VIEW_CARD_W, DEFAULT_VIEW_CARD_H);
|
||||
posX = pos.x;
|
||||
posY = pos.y;
|
||||
const parentCard = parentSessionId ? state.cards[parentSessionId] : null;
|
||||
if (parentCard) {
|
||||
const pos = placeInParentColumn(state, parentSessionId, w, h, expandedSessionIds);
|
||||
posX = pos.x;
|
||||
posY = pos.y;
|
||||
} else {
|
||||
const rects = collectOccupiedRects(state, expandedSessionIds);
|
||||
const pos = findOpenGridCell(rects, w, h);
|
||||
posX = pos.x;
|
||||
posY = pos.y;
|
||||
}
|
||||
}
|
||||
state.viewCards[outputId] = {
|
||||
output_id: outputId,
|
||||
x: posX,
|
||||
y: posY,
|
||||
width: width || DEFAULT_VIEW_CARD_W,
|
||||
height: height || DEFAULT_VIEW_CARD_H,
|
||||
width: w,
|
||||
height: h,
|
||||
zOrder: state.nextZOrder++,
|
||||
parent_session_id: parentSessionId || null,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
clearTurnLabel,
|
||||
} from '../state/agentsSlice';
|
||||
import { streamStart, streamDelta, streamEnd, clearStreamingForSession } from '../state/streamingSlice';
|
||||
import { addBrowserCardFromBackend, markBrowserCardEnding, keepBrowserCardOpen, setBrowserCardPosition, setGlowingBrowserCards, GRID_GAP } from '../state/dashboardLayoutSlice';
|
||||
import { addBrowserCardFromBackend, markBrowserCardEnding, keepBrowserCardOpen, placeInParentColumn, setBrowserCardPosition, setGlowingBrowserCards } from '../state/dashboardLayoutSlice';
|
||||
import { upsertOutput } from '../state/outputsSlice';
|
||||
import { displaySessionName } from '../state/sessionDisplay';
|
||||
import { getAuthToken } from '../config';
|
||||
@@ -762,21 +762,20 @@ class WebSocketManager {
|
||||
const parentId = data.parent_session_id;
|
||||
if (parentId) {
|
||||
const layoutState = store.getState().dashboardLayout;
|
||||
const parentCard = layoutState.cards[parentId];
|
||||
if (parentCard) {
|
||||
const targetX = parentCard.x + parentCard.width + GRID_GAP * 12;
|
||||
let targetY = parentCard.y;
|
||||
const columnCards = Object.values(layoutState.browserCards).filter(
|
||||
(c) => Math.abs(c.x - targetX) < 50 && c.browser_id !== data.browser_card.browser_id,
|
||||
const browserCard = layoutState.browserCards[data.browser_card.browser_id];
|
||||
if (layoutState.cards[parentId] && browserCard) {
|
||||
const pos = placeInParentColumn(
|
||||
layoutState,
|
||||
parentId,
|
||||
browserCard.width,
|
||||
browserCard.height,
|
||||
undefined,
|
||||
{ type: 'browser', id: browserCard.browser_id },
|
||||
);
|
||||
if (columnCards.length > 0) {
|
||||
const lowestBottom = Math.max(...columnCards.map((c) => c.y + c.height));
|
||||
targetY = lowestBottom + GRID_GAP;
|
||||
}
|
||||
store.dispatch(setBrowserCardPosition({
|
||||
browserId: data.browser_card.browser_id,
|
||||
x: targetX,
|
||||
y: targetY,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
}));
|
||||
store.dispatch(setGlowingBrowserCards({
|
||||
browserIds: [data.browser_card.browser_id],
|
||||
|
||||
Reference in New Issue
Block a user