feat: refactor artifact analysis workspace
This commit is contained in:
@@ -1,19 +1,11 @@
|
||||
export const MAX_ANALYSIS_WINDOWS = 6;
|
||||
export const ARTIFACT_MAP_SPLIT_MIN_WIDTH = 1360;
|
||||
|
||||
export type WorkspaceRect = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type WorkspaceBounds = {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type WorkspaceWindowMode = "docked" | "floating" | "maximized" | "minimized";
|
||||
export type WorkspaceLayoutMode = "free" | "primary" | "split" | "grid" | "cascade";
|
||||
export type ArtifactType = "diagnosis" | "simulation" | "metric_analysis";
|
||||
export type ArtifactLifecycle = "draft" | "reviewed" | "published" | "archived";
|
||||
export type ArtifactViewMode = "map_split" | "focus";
|
||||
export type ArtifactRequestedView = ArtifactViewMode | "map_only";
|
||||
export type ArtifactMapRelation = "required" | "optional" | "none";
|
||||
export type WorkbenchSurfaceMode = "default" | ArtifactRequestedView;
|
||||
|
||||
export type AnalysisMetric = {
|
||||
label: string;
|
||||
@@ -78,548 +70,100 @@ export type AnalysisBlock =
|
||||
paragraphs: string[];
|
||||
};
|
||||
|
||||
export type AnalysisDocument = {
|
||||
export type AnalysisArtifactAction = {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
tone: "primary" | "neutral" | "warning";
|
||||
};
|
||||
|
||||
export type AnalysisArtifact = {
|
||||
id: string;
|
||||
type: ArtifactType;
|
||||
lifecycle: ArtifactLifecycle;
|
||||
revision: number;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
summary: string;
|
||||
scope: string;
|
||||
confidence: string;
|
||||
generatedAt: string;
|
||||
preferredView: ArtifactViewMode;
|
||||
mapRelation: ArtifactMapRelation;
|
||||
blocks: AnalysisBlock[];
|
||||
sources: string[];
|
||||
limitations: string[];
|
||||
actions: AnalysisArtifactAction[];
|
||||
};
|
||||
|
||||
type WorkspaceWindowState = {
|
||||
mode: WorkspaceWindowMode;
|
||||
rect: WorkspaceRect;
|
||||
restoreRect: WorkspaceRect;
|
||||
zIndex: number;
|
||||
minimizedByLayout: boolean;
|
||||
export type ArtifactWorkspaceState = {
|
||||
pendingArtifact: AnalysisArtifact | null;
|
||||
activeArtifact: AnalysisArtifact | null;
|
||||
requestedView: ArtifactRequestedView;
|
||||
};
|
||||
|
||||
export type AnalysisWorkspaceWindow = WorkspaceWindowState & {
|
||||
id: string;
|
||||
kind: "agent-analysis";
|
||||
title: string;
|
||||
document: AnalysisDocument;
|
||||
};
|
||||
|
||||
export type MapWorkspaceWindow = WorkspaceWindowState & {
|
||||
id: "workspace-map";
|
||||
kind: "map";
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type WorkspaceState = {
|
||||
map: MapWorkspaceWindow;
|
||||
analyses: AnalysisWorkspaceWindow[];
|
||||
activeWindowId: string;
|
||||
nextZIndex: number;
|
||||
layout: {
|
||||
mode: WorkspaceLayoutMode;
|
||||
orderedWindowIds: string[];
|
||||
};
|
||||
};
|
||||
|
||||
const FALLBACK_BOUNDS: WorkspaceBounds = { width: 960, height: 680 };
|
||||
const MAP_WINDOW_ID = "workspace-map";
|
||||
const LAYOUT_INSET = 8;
|
||||
const LAYOUT_GAP = 8;
|
||||
|
||||
export function createInitialWorkspaceState(bounds: WorkspaceBounds = FALLBACK_BOUNDS): WorkspaceState {
|
||||
const mapRect = getDefaultMapRect(bounds);
|
||||
export function createInitialArtifactWorkspaceState(): ArtifactWorkspaceState {
|
||||
return {
|
||||
map: {
|
||||
id: MAP_WINDOW_ID,
|
||||
kind: "map",
|
||||
title: "供水管网地图",
|
||||
mode: "docked",
|
||||
rect: mapRect,
|
||||
restoreRect: mapRect,
|
||||
zIndex: 1,
|
||||
minimizedByLayout: false
|
||||
},
|
||||
analyses: [],
|
||||
activeWindowId: MAP_WINDOW_ID,
|
||||
nextZIndex: 2,
|
||||
layout: { mode: "free", orderedWindowIds: [MAP_WINDOW_ID] }
|
||||
pendingArtifact: null,
|
||||
activeArtifact: null,
|
||||
requestedView: "map_split"
|
||||
};
|
||||
}
|
||||
|
||||
export function openAnalysisDocument(
|
||||
state: WorkspaceState,
|
||||
document: AnalysisDocument,
|
||||
bounds: WorkspaceBounds
|
||||
): WorkspaceState {
|
||||
if (state.analyses.length >= MAX_ANALYSIS_WINDOWS) return state;
|
||||
export function previewAnalysisArtifact(
|
||||
state: ArtifactWorkspaceState,
|
||||
artifact: AnalysisArtifact
|
||||
): ArtifactWorkspaceState {
|
||||
return { ...state, pendingArtifact: artifact };
|
||||
}
|
||||
|
||||
const rect = getDefaultAnalysisRect(bounds, state.analyses.length);
|
||||
const window: AnalysisWorkspaceWindow = {
|
||||
id: document.id,
|
||||
kind: "agent-analysis",
|
||||
title: document.title,
|
||||
mode: "floating",
|
||||
rect,
|
||||
restoreRect: rect,
|
||||
zIndex: state.nextZIndex,
|
||||
minimizedByLayout: false,
|
||||
document
|
||||
export function openPendingArtifact(state: ArtifactWorkspaceState): ArtifactWorkspaceState {
|
||||
if (!state.pendingArtifact) return state;
|
||||
return {
|
||||
pendingArtifact: null,
|
||||
activeArtifact: state.pendingArtifact,
|
||||
requestedView: getPreferredArtifactView(state.pendingArtifact)
|
||||
};
|
||||
const opened: WorkspaceState = {
|
||||
...state,
|
||||
analyses: [...state.analyses, window],
|
||||
activeWindowId: window.id,
|
||||
nextZIndex: state.nextZIndex + 1,
|
||||
layout: {
|
||||
...state.layout,
|
||||
orderedWindowIds: uniqueIds([window.id, MAP_WINDOW_ID, ...state.layout.orderedWindowIds])
|
||||
}
|
||||
};
|
||||
|
||||
if (state.layout.mode !== "free") return arrangeWorkspaceLayout(opened, bounds);
|
||||
return opened;
|
||||
}
|
||||
|
||||
export function closeAnalysisWindow(
|
||||
state: WorkspaceState,
|
||||
windowId: string,
|
||||
bounds: WorkspaceBounds = FALLBACK_BOUNDS
|
||||
): WorkspaceState {
|
||||
const analyses = state.analyses.filter((window) => window.id !== windowId);
|
||||
if (analyses.length === state.analyses.length) return state;
|
||||
|
||||
if (analyses.length === 0) {
|
||||
return {
|
||||
...state,
|
||||
map: {
|
||||
...state.map,
|
||||
mode: "docked",
|
||||
minimizedByLayout: false,
|
||||
zIndex: state.nextZIndex
|
||||
},
|
||||
analyses,
|
||||
activeWindowId: MAP_WINDOW_ID,
|
||||
nextZIndex: state.nextZIndex + 1,
|
||||
layout: { mode: "free", orderedWindowIds: [MAP_WINDOW_ID] }
|
||||
};
|
||||
}
|
||||
|
||||
const orderedWindowIds = state.layout.orderedWindowIds.filter((id) => id !== windowId);
|
||||
const nextActive = state.activeWindowId === windowId
|
||||
? orderedWindowIds.find((id) => getWindowById({ ...state, analyses }, id)?.mode !== "minimized")
|
||||
: state.activeWindowId;
|
||||
const closed: WorkspaceState = {
|
||||
...state,
|
||||
analyses,
|
||||
activeWindowId: nextActive ?? MAP_WINDOW_ID,
|
||||
layout: { ...state.layout, orderedWindowIds }
|
||||
};
|
||||
return state.layout.mode === "free" ? closed : arrangeWorkspaceLayout(closed, bounds);
|
||||
}
|
||||
|
||||
export function focusWorkspaceWindow(state: WorkspaceState, windowId: string): WorkspaceState {
|
||||
if (!getWindowById(state, windowId)) return state;
|
||||
return updateWindowById(
|
||||
{ ...state, activeWindowId: windowId, nextZIndex: state.nextZIndex + 1 },
|
||||
windowId,
|
||||
(window) => ({ ...window, zIndex: state.nextZIndex })
|
||||
);
|
||||
}
|
||||
|
||||
export function minimizeWorkspaceWindow(
|
||||
state: WorkspaceState,
|
||||
windowId: string,
|
||||
bounds: WorkspaceBounds = FALLBACK_BOUNDS
|
||||
): WorkspaceState {
|
||||
if (!getWindowById(state, windowId)) return state;
|
||||
const minimized = updateWindowById(state, windowId, (window) => ({
|
||||
...window,
|
||||
mode: "minimized",
|
||||
minimizedByLayout: false
|
||||
}));
|
||||
return state.layout.mode === "free" ? minimized : arrangeWorkspaceLayout(minimized, bounds);
|
||||
}
|
||||
|
||||
export function maximizeWorkspaceWindow(state: WorkspaceState, windowId: string): WorkspaceState {
|
||||
if (!getWindowById(state, windowId)) return state;
|
||||
return focusWorkspaceWindow(
|
||||
updateWindowById(state, windowId, (window) => ({
|
||||
...window,
|
||||
mode: "maximized",
|
||||
minimizedByLayout: false
|
||||
})),
|
||||
windowId
|
||||
);
|
||||
}
|
||||
|
||||
export function restoreWorkspaceWindow(
|
||||
state: WorkspaceState,
|
||||
windowId: string,
|
||||
bounds: WorkspaceBounds
|
||||
): WorkspaceState {
|
||||
const window = getWindowById(state, windowId);
|
||||
if (!window) return state;
|
||||
|
||||
if (state.layout.mode !== "free") {
|
||||
const restored = updateWindowById(
|
||||
{
|
||||
...state,
|
||||
layout: {
|
||||
...state.layout,
|
||||
orderedWindowIds: uniqueIds([windowId, ...state.layout.orderedWindowIds])
|
||||
}
|
||||
},
|
||||
windowId,
|
||||
(current) => ({ ...current, mode: "floating", minimizedByLayout: false })
|
||||
);
|
||||
return focusWorkspaceWindow(arrangeWorkspaceLayout(restored, bounds), windowId);
|
||||
}
|
||||
|
||||
const kind = windowId === MAP_WINDOW_ID ? "map" : "analysis";
|
||||
const rect = clampWorkspaceRect(window.restoreRect, bounds, kind);
|
||||
return focusWorkspaceWindow(
|
||||
updateWindowById(state, windowId, (current) => ({
|
||||
...current,
|
||||
mode: "floating",
|
||||
rect,
|
||||
minimizedByLayout: false
|
||||
})),
|
||||
windowId
|
||||
);
|
||||
}
|
||||
|
||||
export function updateWorkspaceWindowRect(
|
||||
state: WorkspaceState,
|
||||
windowId: string,
|
||||
rect: WorkspaceRect,
|
||||
bounds: WorkspaceBounds
|
||||
): WorkspaceState {
|
||||
if (!getWindowById(state, windowId)) return state;
|
||||
const kind = windowId === MAP_WINDOW_ID ? "map" : "analysis";
|
||||
const nextRect = clampWorkspaceRect(rect, bounds, kind);
|
||||
return updateWindowById(
|
||||
{
|
||||
...state,
|
||||
layout: { ...state.layout, mode: "free" }
|
||||
},
|
||||
windowId,
|
||||
(window) => ({
|
||||
...window,
|
||||
mode: "floating",
|
||||
rect: nextRect,
|
||||
restoreRect: nextRect,
|
||||
minimizedByLayout: false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function applyWorkspaceLayout(
|
||||
state: WorkspaceState,
|
||||
mode: WorkspaceLayoutMode,
|
||||
bounds: WorkspaceBounds
|
||||
): WorkspaceState {
|
||||
if (mode === "free") {
|
||||
return { ...state, layout: { ...state.layout, mode } };
|
||||
}
|
||||
|
||||
const orderedWindowIds = uniqueIds([
|
||||
state.activeWindowId,
|
||||
MAP_WINDOW_ID,
|
||||
...allWorkspaceWindows(state).sort((a, b) => b.zIndex - a.zIndex).map((window) => window.id),
|
||||
...state.layout.orderedWindowIds
|
||||
]);
|
||||
const restored = mapAllWorkspaceWindows(
|
||||
{
|
||||
...state,
|
||||
layout: { mode, orderedWindowIds }
|
||||
},
|
||||
(window) => ({ ...window, mode: "floating", minimizedByLayout: false })
|
||||
);
|
||||
return arrangeWorkspaceLayout(restored, bounds, true);
|
||||
}
|
||||
|
||||
export function setWorkspacePrimaryWindow(
|
||||
state: WorkspaceState,
|
||||
windowId: string,
|
||||
bounds: WorkspaceBounds
|
||||
): WorkspaceState {
|
||||
if (!getWindowById(state, windowId) || state.layout.mode === "free") return state;
|
||||
const next = {
|
||||
...state,
|
||||
layout: {
|
||||
...state.layout,
|
||||
orderedWindowIds: uniqueIds([windowId, ...state.layout.orderedWindowIds])
|
||||
}
|
||||
};
|
||||
return focusWorkspaceWindow(arrangeWorkspaceLayout(next, bounds), windowId);
|
||||
}
|
||||
|
||||
export function swapWorkspaceWindows(
|
||||
state: WorkspaceState,
|
||||
sourceId: string,
|
||||
targetId: string,
|
||||
bounds: WorkspaceBounds
|
||||
): WorkspaceState {
|
||||
const source = getWindowById(state, sourceId);
|
||||
const target = getWindowById(state, targetId);
|
||||
if (!source || !target || sourceId === targetId || source.mode === "minimized" || target.mode === "minimized") {
|
||||
return state;
|
||||
}
|
||||
|
||||
if (state.layout.mode !== "free") {
|
||||
const order = normalizeWindowOrder(state);
|
||||
const sourceIndex = order.indexOf(sourceId);
|
||||
const targetIndex = order.indexOf(targetId);
|
||||
if (sourceIndex < 0 || targetIndex < 0) return state;
|
||||
[order[sourceIndex], order[targetIndex]] = [order[targetIndex], order[sourceIndex]];
|
||||
const swapped = { ...state, layout: { ...state.layout, orderedWindowIds: order } };
|
||||
return focusWorkspaceWindow(arrangeWorkspaceLayout(swapped, bounds), sourceId);
|
||||
}
|
||||
|
||||
const swapped = updateWindowById(
|
||||
updateWindowById(state, sourceId, (window) => ({
|
||||
...window,
|
||||
mode: target.mode,
|
||||
rect: target.rect,
|
||||
restoreRect: target.restoreRect,
|
||||
minimizedByLayout: false
|
||||
})),
|
||||
targetId,
|
||||
(window) => ({
|
||||
...window,
|
||||
mode: source.mode,
|
||||
rect: source.rect,
|
||||
restoreRect: source.restoreRect,
|
||||
minimizedByLayout: false
|
||||
})
|
||||
);
|
||||
return focusWorkspaceWindow(swapped, sourceId);
|
||||
}
|
||||
|
||||
export function resizeWorkspace(state: WorkspaceState, bounds: WorkspaceBounds): WorkspaceState {
|
||||
if (state.layout.mode !== "free") return arrangeWorkspaceLayout(state, bounds);
|
||||
export function collapseActiveArtifact(state: ArtifactWorkspaceState): ArtifactWorkspaceState {
|
||||
if (!state.activeArtifact) return state;
|
||||
return {
|
||||
...state,
|
||||
map: {
|
||||
...state.map,
|
||||
rect: clampWorkspaceRect(state.map.rect, bounds, "map"),
|
||||
restoreRect: clampWorkspaceRect(state.map.restoreRect, bounds, "map")
|
||||
},
|
||||
analyses: state.analyses.map((window) => ({
|
||||
...window,
|
||||
rect: clampWorkspaceRect(window.rect, bounds, "analysis"),
|
||||
restoreRect: clampWorkspaceRect(window.restoreRect, bounds, "analysis")
|
||||
}))
|
||||
pendingArtifact: state.activeArtifact,
|
||||
activeArtifact: null
|
||||
};
|
||||
}
|
||||
|
||||
export function clampWorkspaceRect(
|
||||
rect: WorkspaceRect,
|
||||
bounds: WorkspaceBounds,
|
||||
kind: "map" | "analysis"
|
||||
): WorkspaceRect {
|
||||
const minimumWidth = kind === "map" ? 320 : 360;
|
||||
const minimumHeight = kind === "map" ? 240 : 280;
|
||||
const availableWidth = Math.max(280, bounds.width - 24);
|
||||
const availableHeight = Math.max(240, bounds.height - 24);
|
||||
const width = clamp(rect.width, Math.min(minimumWidth, availableWidth), availableWidth);
|
||||
const height = clamp(rect.height, Math.min(minimumHeight, availableHeight), availableHeight);
|
||||
return {
|
||||
x: clamp(rect.x, 12, Math.max(12, bounds.width - width - 12)),
|
||||
y: clamp(rect.y, 12, Math.max(12, bounds.height - height - 12)),
|
||||
width,
|
||||
height
|
||||
};
|
||||
export function destroyPendingArtifact(state: ArtifactWorkspaceState): ArtifactWorkspaceState {
|
||||
return state.pendingArtifact ? { ...state, pendingArtifact: null } : state;
|
||||
}
|
||||
|
||||
function arrangeWorkspaceLayout(
|
||||
state: WorkspaceState,
|
||||
bounds: WorkspaceBounds,
|
||||
restoreAll = false
|
||||
): WorkspaceState {
|
||||
if (state.layout.mode === "free") return state;
|
||||
|
||||
const orderedWindowIds = normalizeWindowOrder(state);
|
||||
const eligibleIds = orderedWindowIds.filter((id) => {
|
||||
const window = getWindowById(state, id);
|
||||
return window && (restoreAll || window.mode !== "minimized" || window.minimizedByLayout);
|
||||
});
|
||||
const visibleIds = selectVisibleWindowIds(eligibleIds, state.layout.mode);
|
||||
const visibleSet = new Set(visibleIds);
|
||||
const eligibleSet = new Set(eligibleIds);
|
||||
const rects = getLayoutRects(state.layout.mode, visibleIds.length, bounds);
|
||||
let next = mapAllWorkspaceWindows(state, (window) => {
|
||||
const visibleIndex = visibleIds.indexOf(window.id);
|
||||
if (visibleIndex >= 0) {
|
||||
const rect = rects[visibleIndex] ?? window.rect;
|
||||
return {
|
||||
...window,
|
||||
mode: !restoreAll && window.mode === "maximized" ? "maximized" : "floating",
|
||||
rect,
|
||||
restoreRect: rect,
|
||||
minimizedByLayout: false
|
||||
};
|
||||
}
|
||||
if (eligibleSet.has(window.id) && !visibleSet.has(window.id)) {
|
||||
return { ...window, mode: "minimized", minimizedByLayout: true };
|
||||
}
|
||||
return window;
|
||||
});
|
||||
const activeWindowId = visibleSet.has(next.activeWindowId)
|
||||
? next.activeWindowId
|
||||
: visibleIds[0] ?? next.activeWindowId;
|
||||
next = {
|
||||
...next,
|
||||
activeWindowId,
|
||||
layout: { ...next.layout, orderedWindowIds }
|
||||
};
|
||||
return next;
|
||||
export function destroyActiveArtifact(state: ArtifactWorkspaceState): ArtifactWorkspaceState {
|
||||
return state.activeArtifact
|
||||
? { ...state, activeArtifact: null, requestedView: "map_split" }
|
||||
: state;
|
||||
}
|
||||
|
||||
function selectVisibleWindowIds(ids: string[], mode: WorkspaceLayoutMode) {
|
||||
const capacity = mode === "split" ? 2 : mode === "primary" ? 3 : ids.length;
|
||||
const selected = ids.slice(0, capacity);
|
||||
if (
|
||||
(mode === "split" || mode === "primary") &&
|
||||
ids.includes(MAP_WINDOW_ID) &&
|
||||
!selected.includes(MAP_WINDOW_ID)
|
||||
) {
|
||||
selected[selected.length - 1] = MAP_WINDOW_ID;
|
||||
}
|
||||
return selected;
|
||||
export function requestArtifactView(
|
||||
state: ArtifactWorkspaceState,
|
||||
view: ArtifactRequestedView
|
||||
): ArtifactWorkspaceState {
|
||||
if (!state.activeArtifact) return state;
|
||||
return { ...state, requestedView: view };
|
||||
}
|
||||
|
||||
function getLayoutRects(mode: WorkspaceLayoutMode, count: number, bounds: WorkspaceBounds): WorkspaceRect[] {
|
||||
if (count === 0) return [];
|
||||
const width = Math.max(1, bounds.width - LAYOUT_INSET * 2);
|
||||
const height = Math.max(1, bounds.height - LAYOUT_INSET * 2);
|
||||
|
||||
if (mode === "primary") {
|
||||
if (count === 1) return [{ x: LAYOUT_INSET, y: LAYOUT_INSET, width, height }];
|
||||
const primaryWidth = Math.floor((width - LAYOUT_GAP) * 0.68);
|
||||
const secondaryWidth = width - LAYOUT_GAP - primaryWidth;
|
||||
const secondaryHeight = count === 2 ? height : Math.floor((height - LAYOUT_GAP) / 2);
|
||||
return [
|
||||
{ x: LAYOUT_INSET, y: LAYOUT_INSET, width: primaryWidth, height },
|
||||
{ x: LAYOUT_INSET + primaryWidth + LAYOUT_GAP, y: LAYOUT_INSET, width: secondaryWidth, height: secondaryHeight },
|
||||
...(count === 3
|
||||
? [{
|
||||
x: LAYOUT_INSET + primaryWidth + LAYOUT_GAP,
|
||||
y: LAYOUT_INSET + secondaryHeight + LAYOUT_GAP,
|
||||
width: secondaryWidth,
|
||||
height: height - secondaryHeight - LAYOUT_GAP
|
||||
}]
|
||||
: [])
|
||||
];
|
||||
}
|
||||
|
||||
if (mode === "split") {
|
||||
if (count === 1) return [{ x: LAYOUT_INSET, y: LAYOUT_INSET, width, height }];
|
||||
const firstWidth = Math.floor((width - LAYOUT_GAP) / 2);
|
||||
return [
|
||||
{ x: LAYOUT_INSET, y: LAYOUT_INSET, width: firstWidth, height },
|
||||
{
|
||||
x: LAYOUT_INSET + firstWidth + LAYOUT_GAP,
|
||||
y: LAYOUT_INSET,
|
||||
width: width - firstWidth - LAYOUT_GAP,
|
||||
height
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (mode === "cascade") {
|
||||
const windowWidth = Math.max(280, Math.floor(width * 0.78));
|
||||
const windowHeight = Math.max(240, Math.floor(height * 0.78));
|
||||
return Array.from({ length: count }, (_, index) => ({
|
||||
x: Math.min(LAYOUT_INSET + index * 28, Math.max(LAYOUT_INSET, bounds.width - windowWidth - LAYOUT_INSET)),
|
||||
y: Math.min(LAYOUT_INSET + index * 28, Math.max(LAYOUT_INSET, bounds.height - windowHeight - LAYOUT_INSET)),
|
||||
width: Math.min(windowWidth, width),
|
||||
height: Math.min(windowHeight, height)
|
||||
}));
|
||||
}
|
||||
|
||||
const columns = count === 1 ? 1 : count <= 4 ? 2 : 3;
|
||||
const rows = Math.ceil(count / columns);
|
||||
const cellWidth = Math.floor((width - LAYOUT_GAP * (columns - 1)) / columns);
|
||||
const cellHeight = Math.floor((height - LAYOUT_GAP * (rows - 1)) / rows);
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const column = index % columns;
|
||||
const row = Math.floor(index / columns);
|
||||
return {
|
||||
x: LAYOUT_INSET + column * (cellWidth + LAYOUT_GAP),
|
||||
y: LAYOUT_INSET + row * (cellHeight + LAYOUT_GAP),
|
||||
width: column === columns - 1 ? width - column * (cellWidth + LAYOUT_GAP) : cellWidth,
|
||||
height: row === rows - 1 ? height - row * (cellHeight + LAYOUT_GAP) : cellHeight
|
||||
};
|
||||
});
|
||||
export function resolveWorkbenchSurfaceMode(
|
||||
state: ArtifactWorkspaceState,
|
||||
viewportWidth: number
|
||||
): WorkbenchSurfaceMode {
|
||||
const artifact = state.activeArtifact;
|
||||
if (!artifact) return "default";
|
||||
if (state.requestedView === "map_only") return "map_only";
|
||||
if (artifact.mapRelation === "none" || state.requestedView === "focus") return "focus";
|
||||
return viewportWidth >= ARTIFACT_MAP_SPLIT_MIN_WIDTH ? "map_split" : "focus";
|
||||
}
|
||||
|
||||
function normalizeWindowOrder(state: WorkspaceState) {
|
||||
const ids = new Set(allWorkspaceWindows(state).map((window) => window.id));
|
||||
return uniqueIds([
|
||||
...state.layout.orderedWindowIds,
|
||||
...allWorkspaceWindows(state).sort((a, b) => b.zIndex - a.zIndex).map((window) => window.id)
|
||||
]).filter((id) => ids.has(id));
|
||||
}
|
||||
|
||||
function getWindowById(state: WorkspaceState, windowId: string): MapWorkspaceWindow | AnalysisWorkspaceWindow | undefined {
|
||||
return windowId === MAP_WINDOW_ID ? state.map : state.analyses.find((window) => window.id === windowId);
|
||||
}
|
||||
|
||||
function updateWindowById(
|
||||
state: WorkspaceState,
|
||||
windowId: string,
|
||||
update: <T extends MapWorkspaceWindow | AnalysisWorkspaceWindow>(window: T) => T
|
||||
): WorkspaceState {
|
||||
if (windowId === MAP_WINDOW_ID) {
|
||||
return { ...state, map: update(state.map) };
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
analyses: state.analyses.map((window) => window.id === windowId ? update(window) : window)
|
||||
};
|
||||
}
|
||||
|
||||
function mapAllWorkspaceWindows(
|
||||
state: WorkspaceState,
|
||||
update: <T extends MapWorkspaceWindow | AnalysisWorkspaceWindow>(window: T) => T
|
||||
): WorkspaceState {
|
||||
return {
|
||||
...state,
|
||||
map: update(state.map),
|
||||
analyses: state.analyses.map((window) => update(window))
|
||||
};
|
||||
}
|
||||
|
||||
function allWorkspaceWindows(state: WorkspaceState) {
|
||||
return [state.map, ...state.analyses] as Array<MapWorkspaceWindow | AnalysisWorkspaceWindow>;
|
||||
}
|
||||
|
||||
function uniqueIds(ids: string[]) {
|
||||
return [...new Set(ids)];
|
||||
}
|
||||
|
||||
function getDefaultAnalysisRect(bounds: WorkspaceBounds, index: number): WorkspaceRect {
|
||||
const width = Math.min(Math.max(bounds.width * 0.8, 640), Math.max(640, bounds.width - 48));
|
||||
const height = Math.min(Math.max(bounds.height * 0.84, 420), Math.max(420, bounds.height - 48));
|
||||
const cascade = (index % 5) * 28;
|
||||
return clampWorkspaceRect(
|
||||
{ x: 24 + cascade, y: 20 + cascade, width, height },
|
||||
bounds,
|
||||
"analysis"
|
||||
);
|
||||
}
|
||||
|
||||
function getDefaultMapRect(bounds: WorkspaceBounds): WorkspaceRect {
|
||||
return clampWorkspaceRect(
|
||||
{
|
||||
x: Math.max(12, bounds.width * 0.36),
|
||||
y: Math.max(12, bounds.height * 0.18),
|
||||
width: bounds.width * 0.6,
|
||||
height: bounds.height * 0.72
|
||||
},
|
||||
bounds,
|
||||
"map"
|
||||
);
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
function getPreferredArtifactView(artifact: AnalysisArtifact): ArtifactViewMode {
|
||||
return artifact.mapRelation === "none" ? "focus" : artifact.preferredView;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user