Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontend/app/workspace/widgets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ const Widgets = memo(() => {
<>
<div
ref={containerRef}
className="flex flex-col w-12 overflow-hidden py-1 -ml-1 select-none"
className="flex flex-col w-full overflow-hidden py-1 select-none"
onContextMenu={handleWidgetsBarContextMenu}
>
{mode === "supercompact" ? (
Expand Down Expand Up @@ -504,7 +504,7 @@ const Widgets = memo(() => {

<div
ref={measurementRef}
className="flex flex-col w-12 py-1 -ml-1 select-none absolute -z-10 opacity-0 pointer-events-none"
className="flex flex-col w-full py-1 select-none absolute -z-10 opacity-0 pointer-events-none"
>
{widgets?.map((data, idx) => (
<Widget key={`measurement-widget-${idx}`} widget={data} mode="normal" />
Expand Down
120 changes: 120 additions & 0 deletions frontend/app/workspace/workspace-layout-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,25 @@ const AIPANEL_DEFAULTWIDTHRATIO = 0.33;
const AIPANEL_MINWIDTH = 300;
const AIPANEL_MAXWIDTHRATIO = 0.66;

const WIDGETS_DEFAULTWIDTH = 48;
const WIDGETS_MINWIDTH = 48;
const WIDGETS_MAXWIDTH = 300;

class WorkspaceLayoutModel {
private static instance: WorkspaceLayoutModel | null = null;

aiPanelRef: ImperativePanelHandle | null;
panelGroupRef: ImperativePanelGroupHandle | null;
panelContainerRef: HTMLDivElement | null;
aiPanelWrapperRef: HTMLDivElement | null;
innerPanelGroupRef: ImperativePanelGroupHandle | null;
inResize: boolean; // prevents recursive setLayout calls (setLayout triggers onLayout which calls setLayout)
inInnerResize: boolean;
private aiPanelVisible: boolean;
private aiPanelWidth: number | null;
private widgetsWidth: number | null;
private debouncedPersistWidth: (width: number) => void;
private debouncedPersistWidgetsWidth: (width: number) => void;
private initialized: boolean = false;
private transitionTimeoutRef: NodeJS.Timeout | null = null;
private focusTimeoutRef: NodeJS.Timeout | null = null;
Expand All @@ -41,13 +49,17 @@ class WorkspaceLayoutModel {
this.panelGroupRef = null;
this.panelContainerRef = null;
this.aiPanelWrapperRef = null;
this.innerPanelGroupRef = null;
this.inResize = false;
this.inInnerResize = false;
this.aiPanelVisible = false;
this.aiPanelWidth = null;
this.widgetsWidth = null;
this.panelVisibleAtom = jotai.atom(this.aiPanelVisible);

this.handleWindowResize = this.handleWindowResize.bind(this);
this.handlePanelLayout = this.handlePanelLayout.bind(this);
this.handleInnerPanelLayout = this.handleInnerPanelLayout.bind(this);

this.debouncedPersistWidth = debounce((width: number) => {
try {
Expand All @@ -59,6 +71,17 @@ class WorkspaceLayoutModel {
console.warn("Failed to persist panel width:", e);
}
}, 300);

this.debouncedPersistWidgetsWidth = debounce((width: number) => {
try {
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("tab", this.getTabId()),
meta: { "widgets:width": width },
});
} catch (e) {
console.warn("Failed to persist widgets width:", e);
}
}, 300);
}

static getInstance(): WorkspaceLayoutModel {
Expand All @@ -75,6 +98,7 @@ class WorkspaceLayoutModel {
try {
const savedVisible = globalStore.get(this.getPanelOpenAtom());
const savedWidth = globalStore.get(this.getPanelWidthAtom());
const savedWidgetsWidth = globalStore.get(this.getWidgetsWidthAtom());

if (savedVisible != null) {
this.aiPanelVisible = savedVisible;
Expand All @@ -83,6 +107,9 @@ class WorkspaceLayoutModel {
if (savedWidth != null) {
this.aiPanelWidth = savedWidth;
}
if (savedWidgetsWidth != null) {
this.widgetsWidth = savedWidgetsWidth;
}
} catch (e) {
console.warn("Failed to initialize from tab meta:", e);
}
Expand All @@ -102,6 +129,11 @@ class WorkspaceLayoutModel {
return getOrefMetaKeyAtom(tabORef, "waveai:panelwidth");
}

private getWidgetsWidthAtom(): jotai.Atom<number> {
const tabORef = WOS.makeORef("tab", this.getTabId());
return getOrefMetaKeyAtom(tabORef, "widgets:width");
}

registerRefs(
aiPanelRef: ImperativePanelHandle,
panelGroupRef: ImperativePanelGroupHandle,
Expand All @@ -116,6 +148,11 @@ class WorkspaceLayoutModel {
this.updateWrapperWidth();
}

registerInnerPanelGroupRef(ref: ImperativePanelGroupHandle): void {
this.innerPanelGroupRef = ref;
this.syncInnerPanelLayout();
}

updateWrapperWidth(): void {
if (!this.aiPanelWrapperRef) {
return;
Expand Down Expand Up @@ -160,6 +197,7 @@ class WorkspaceLayoutModel {
this.panelGroupRef.setLayout(layout);
this.inResize = false;
this.updateWrapperWidth();
this.syncInnerPanelLayout();
}

handlePanelLayout(sizes: number[]): void {
Expand Down Expand Up @@ -297,6 +335,88 @@ class WorkspaceLayoutModel {
const clampedWidth = this.getClampedAIPanelWidth(width, windowWidth);
this.setAIPanelWidth(clampedWidth);
}

getWidgetsWidth(): number {
this.initializeFromTabMeta();
if (this.widgetsWidth == null) {
this.widgetsWidth = WIDGETS_DEFAULTWIDTH;
}
return this.widgetsWidth;
}

setWidgetsWidth(width: number): void {
this.widgetsWidth = width;
this.debouncedPersistWidgetsWidth(width);
}

getClampedWidgetsWidth(width: number): number {
return Math.max(WIDGETS_MINWIDTH, Math.min(width, WIDGETS_MAXWIDTH));
}

getWidgetsPercentage(containerWidth: number): number {
if (containerWidth <= 0) {
return 5;
}
const widgetsWidth = this.getWidgetsWidth();
const clamped = this.getClampedWidgetsWidth(widgetsWidth);
return (clamped / containerWidth) * 100;
}

getWidgetsMinPercentage(containerWidth: number): number {
if (containerWidth <= 0) {
return 3;
}
return (WIDGETS_MINWIDTH / containerWidth) * 100;
}

getWidgetsMaxPercentage(containerWidth: number): number {
if (containerWidth <= 0) {
return 30;
}
return (WIDGETS_MAXWIDTH / containerWidth) * 100;
}

getInnerContainerWidth(): number {
// The inner panel group's container width is the main content panel width
// which is the window width minus the AI panel width (if visible)
const windowWidth = window.innerWidth;
if (this.getAIPanelVisible()) {
const aiWidth = this.getClampedAIPanelWidth(this.getAIPanelWidth(), windowWidth);
return windowWidth - aiWidth;
}
return windowWidth;
}

syncInnerPanelLayout(): void {
if (!this.innerPanelGroupRef) {
return;
}
const containerWidth = this.getInnerContainerWidth();
const widgetsPercentage = this.getWidgetsPercentage(containerWidth);
const mainPercentage = 100 - widgetsPercentage;
this.inInnerResize = true;
this.innerPanelGroupRef.setLayout([mainPercentage, widgetsPercentage]);
this.inInnerResize = false;
}

handleInnerPanelLayout(sizes: number[]): void {
if (this.inInnerResize) {
return;
}
if (!this.innerPanelGroupRef) {
return;
}
const containerWidth = this.getInnerContainerWidth();
const widgetsPixelWidth = (sizes[1] / 100) * containerWidth;
const clampedWidth = this.getClampedWidgetsWidth(widgetsPixelWidth);
this.setWidgetsWidth(clampedWidth);

const newWidgetsPercentage = (clampedWidth / containerWidth) * 100;
const mainPercentage = 100 - newWidgetsPercentage;
this.inInnerResize = true;
this.innerPanelGroupRef.setLayout([mainPercentage, newWidgetsPercentage]);
this.inInnerResize = false;
}
}

export { WorkspaceLayoutModel };
44 changes: 40 additions & 4 deletions frontend/app/workspace/workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,45 @@ import {
PanelResizeHandle,
} from "react-resizable-panels";

const InnerContent = memo(({ tabId }: { tabId: string }) => {
const workspaceLayoutModel = WorkspaceLayoutModel.getInstance();
const innerContainerWidth = workspaceLayoutModel.getInnerContainerWidth();
const initialWidgetsPercentage = workspaceLayoutModel.getWidgetsPercentage(innerContainerWidth);
const innerPanelGroupRef = useRef<ImperativePanelGroupHandle>(null);

useEffect(() => {
if (innerPanelGroupRef.current) {
workspaceLayoutModel.registerInnerPanelGroupRef(innerPanelGroupRef.current);
}
}, []);

const widgetsMinSize = workspaceLayoutModel.getWidgetsMinPercentage(innerContainerWidth);
const widgetsMaxSize = workspaceLayoutModel.getWidgetsMaxPercentage(innerContainerWidth);

return (
<PanelGroup
direction="horizontal"
onLayout={workspaceLayoutModel.handleInnerPanelLayout}
ref={innerPanelGroupRef}
>
<Panel order={1} defaultSize={100 - initialWidgetsPercentage}>
<TabContent key={tabId} tabId={tabId} />
</Panel>
<PanelResizeHandle className="w-0.5 bg-transparent hover:bg-zinc-500/20 transition-colors" />
<Panel
order={2}
defaultSize={initialWidgetsPercentage}
minSize={widgetsMinSize}
maxSize={widgetsMaxSize}
>
<Widgets />
</Panel>
</PanelGroup>
);
});

InnerContent.displayName = "InnerContent";

const WorkspaceElem = memo(() => {
const workspaceLayoutModel = WorkspaceLayoutModel.getInstance();
const tabId = useAtomValue(atoms.staticTabId);
Expand Down Expand Up @@ -77,10 +116,7 @@ const WorkspaceElem = memo(() => {
{tabId === "" ? (
<CenteredDiv>No Active Tab</CenteredDiv>
) : (
<div className="flex flex-row h-full">
<TabContent key={tabId} tabId={tabId} />
<Widgets />
</div>
<InnerContent tabId={tabId} />
)}
</Panel>
</PanelGroup>
Expand Down
1 change: 1 addition & 0 deletions frontend/types/gotypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,7 @@ declare global {
"waveai:model"?: string;
"waveai:chatid"?: string;
"waveai:widgetcontext"?: boolean;
"widgets:width"?: number;
"term:*"?: boolean;
"term:fontsize"?: number;
"term:fontfamily"?: string;
Expand Down
2 changes: 2 additions & 0 deletions pkg/waveobj/metaconsts.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ const (
MetaKey_WaveAiChatId = "waveai:chatid"
MetaKey_WaveAiWidgetContext = "waveai:widgetcontext"

MetaKey_WidgetsWidth = "widgets:width"

MetaKey_TermClear = "term:*"
MetaKey_TermFontSize = "term:fontsize"
MetaKey_TermFontFamily = "term:fontfamily"
Expand Down
2 changes: 2 additions & 0 deletions pkg/waveobj/wtypemeta.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ type MetaTSType struct {
WaveAiChatId string `json:"waveai:chatid,omitempty"`
WaveAiWidgetContext *bool `json:"waveai:widgetcontext,omitempty"` // default is true

WidgetsWidth int `json:"widgets:width,omitempty"`

TermClear bool `json:"term:*,omitempty"`
TermFontSize int `json:"term:fontsize,omitempty"`
TermFontFamily string `json:"term:fontfamily,omitempty"`
Expand Down