feat: add adaptive agent workspace windows
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
export const MAX_ANALYSIS_WINDOWS = 6;
|
||||
|
||||
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 AnalysisMetric = {
|
||||
label: string;
|
||||
value: string;
|
||||
detail: string;
|
||||
tone: "neutral" | "info" | "success" | "warning" | "danger";
|
||||
};
|
||||
|
||||
export type AnalysisChartSeries = {
|
||||
name: string;
|
||||
values: number[];
|
||||
color: string;
|
||||
type?: "line" | "bar";
|
||||
};
|
||||
|
||||
export type AnalysisBlock =
|
||||
| {
|
||||
id: string;
|
||||
kind: "metric-grid";
|
||||
title: string;
|
||||
span: 12;
|
||||
metrics: AnalysisMetric[];
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
kind: "chart";
|
||||
title: string;
|
||||
description: string;
|
||||
span: 5 | 7 | 12;
|
||||
chartType: "line" | "bar";
|
||||
categories: string[];
|
||||
unit: string;
|
||||
series: AnalysisChartSeries[];
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
kind: "process-flow";
|
||||
title: string;
|
||||
description: string;
|
||||
span: 5 | 7 | 12;
|
||||
nodes: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
detail: string;
|
||||
status: "complete" | "active" | "pending";
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
kind: "data-table";
|
||||
title: string;
|
||||
description: string;
|
||||
span: 5 | 7 | 12;
|
||||
columns: Array<{ key: string; label: string; numeric?: boolean }>;
|
||||
rows: Array<Record<string, string | number>>;
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
kind: "narrative";
|
||||
title: string;
|
||||
span: 5 | 7 | 12;
|
||||
paragraphs: string[];
|
||||
};
|
||||
|
||||
export type AnalysisDocument = {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
generatedAt: string;
|
||||
blocks: AnalysisBlock[];
|
||||
};
|
||||
|
||||
export type AnalysisWorkspaceWindow = {
|
||||
id: string;
|
||||
kind: "agent-analysis";
|
||||
title: string;
|
||||
mode: Exclude<WorkspaceWindowMode, "docked">;
|
||||
rect: WorkspaceRect;
|
||||
restoreRect: WorkspaceRect;
|
||||
zIndex: number;
|
||||
document: AnalysisDocument;
|
||||
};
|
||||
|
||||
export type MapWorkspaceWindow = {
|
||||
id: "workspace-map";
|
||||
kind: "map";
|
||||
title: string;
|
||||
mode: WorkspaceWindowMode;
|
||||
rect: WorkspaceRect;
|
||||
restoreRect: WorkspaceRect;
|
||||
zIndex: number;
|
||||
};
|
||||
|
||||
export type WorkspaceState = {
|
||||
map: MapWorkspaceWindow;
|
||||
analyses: AnalysisWorkspaceWindow[];
|
||||
activeWindowId: string;
|
||||
nextZIndex: number;
|
||||
};
|
||||
|
||||
const FALLBACK_BOUNDS: WorkspaceBounds = { width: 960, height: 680 };
|
||||
|
||||
export function createInitialWorkspaceState(bounds: WorkspaceBounds = FALLBACK_BOUNDS): WorkspaceState {
|
||||
const mapRect = getDefaultMapRect(bounds);
|
||||
return {
|
||||
map: {
|
||||
id: "workspace-map",
|
||||
kind: "map",
|
||||
title: "供水管网地图",
|
||||
mode: "docked",
|
||||
rect: mapRect,
|
||||
restoreRect: mapRect,
|
||||
zIndex: 1
|
||||
},
|
||||
analyses: [],
|
||||
activeWindowId: "workspace-map",
|
||||
nextZIndex: 2
|
||||
};
|
||||
}
|
||||
|
||||
export function openAnalysisDocument(
|
||||
state: WorkspaceState,
|
||||
document: AnalysisDocument,
|
||||
bounds: WorkspaceBounds
|
||||
): WorkspaceState {
|
||||
if (state.analyses.length >= MAX_ANALYSIS_WINDOWS) {
|
||||
return state;
|
||||
}
|
||||
|
||||
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,
|
||||
document
|
||||
};
|
||||
|
||||
return {
|
||||
map: { ...state.map, mode: "minimized" },
|
||||
analyses: [...state.analyses, window],
|
||||
activeWindowId: window.id,
|
||||
nextZIndex: state.nextZIndex + 1
|
||||
};
|
||||
}
|
||||
|
||||
export function closeAnalysisWindow(state: WorkspaceState, windowId: string): 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", zIndex: state.nextZIndex },
|
||||
analyses,
|
||||
activeWindowId: state.map.id,
|
||||
nextZIndex: state.nextZIndex + 1
|
||||
};
|
||||
}
|
||||
|
||||
const nextActive = [...analyses].sort((a, b) => b.zIndex - a.zIndex)[0];
|
||||
return {
|
||||
...state,
|
||||
analyses,
|
||||
activeWindowId: nextActive?.id ?? state.map.id
|
||||
};
|
||||
}
|
||||
|
||||
export function focusWorkspaceWindow(state: WorkspaceState, windowId: string): WorkspaceState {
|
||||
if (windowId === state.map.id) {
|
||||
return {
|
||||
...state,
|
||||
map: { ...state.map, zIndex: state.nextZIndex },
|
||||
activeWindowId: windowId,
|
||||
nextZIndex: state.nextZIndex + 1
|
||||
};
|
||||
}
|
||||
|
||||
if (!state.analyses.some((window) => window.id === windowId)) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
analyses: state.analyses.map((window) =>
|
||||
window.id === windowId ? { ...window, zIndex: state.nextZIndex } : window
|
||||
),
|
||||
activeWindowId: windowId,
|
||||
nextZIndex: state.nextZIndex + 1
|
||||
};
|
||||
}
|
||||
|
||||
export function minimizeWorkspaceWindow(state: WorkspaceState, windowId: string): WorkspaceState {
|
||||
if (windowId === state.map.id) {
|
||||
return { ...state, map: { ...state.map, mode: "minimized" } };
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
analyses: state.analyses.map((window) =>
|
||||
window.id === windowId ? { ...window, mode: "minimized" } : window
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
export function maximizeWorkspaceWindow(state: WorkspaceState, windowId: string): WorkspaceState {
|
||||
if (windowId === state.map.id) {
|
||||
return focusWorkspaceWindow(
|
||||
{ ...state, map: { ...state.map, mode: "maximized" } },
|
||||
windowId
|
||||
);
|
||||
}
|
||||
|
||||
return focusWorkspaceWindow(
|
||||
{
|
||||
...state,
|
||||
analyses: state.analyses.map((window) =>
|
||||
window.id === windowId ? { ...window, mode: "maximized" } : window
|
||||
)
|
||||
},
|
||||
windowId
|
||||
);
|
||||
}
|
||||
|
||||
export function restoreWorkspaceWindow(
|
||||
state: WorkspaceState,
|
||||
windowId: string,
|
||||
bounds: WorkspaceBounds
|
||||
): WorkspaceState {
|
||||
if (windowId === state.map.id) {
|
||||
const rect = clampWorkspaceRect(state.map.restoreRect, bounds, "map");
|
||||
return focusWorkspaceWindow(
|
||||
{ ...state, map: { ...state.map, mode: "floating", rect } },
|
||||
windowId
|
||||
);
|
||||
}
|
||||
|
||||
return focusWorkspaceWindow(
|
||||
{
|
||||
...state,
|
||||
analyses: state.analyses.map((window) =>
|
||||
window.id === windowId
|
||||
? {
|
||||
...window,
|
||||
mode: "floating",
|
||||
rect: clampWorkspaceRect(window.restoreRect, bounds, "analysis")
|
||||
}
|
||||
: window
|
||||
)
|
||||
},
|
||||
windowId
|
||||
);
|
||||
}
|
||||
|
||||
export function updateWorkspaceWindowRect(
|
||||
state: WorkspaceState,
|
||||
windowId: string,
|
||||
rect: WorkspaceRect,
|
||||
bounds: WorkspaceBounds
|
||||
): WorkspaceState {
|
||||
if (windowId === state.map.id) {
|
||||
const nextRect = clampWorkspaceRect(rect, bounds, "map");
|
||||
return {
|
||||
...state,
|
||||
map: { ...state.map, mode: "floating", rect: nextRect, restoreRect: nextRect }
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
analyses: state.analyses.map((window) => {
|
||||
if (window.id !== windowId) return window;
|
||||
const nextRect = clampWorkspaceRect(rect, bounds, "analysis");
|
||||
return { ...window, mode: "floating", rect: nextRect, restoreRect: nextRect };
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
export function resizeWorkspace(state: WorkspaceState, bounds: WorkspaceBounds): WorkspaceState {
|
||||
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")
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
export function clampWorkspaceRect(
|
||||
rect: WorkspaceRect,
|
||||
bounds: WorkspaceBounds,
|
||||
kind: "map" | "analysis"
|
||||
): WorkspaceRect {
|
||||
const minimumWidth = kind === "map" ? 360 : 640;
|
||||
const minimumHeight = kind === "map" ? 280 : 420;
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user