feat: window management system with minimize, fullscreen, and AI chat split

- Window manager store (Zustand) for managing open/minimized/fullscreen windows
- Window component with header controls: KI-Chat toggle, fullscreen, minimize, close
- AI chat panel with streaming chat via existing /ai/sessions API
- WindowContainer renders all non-minimized windows
- TopBar shows minimized windows as clickable pills
- ContactEditForm extracted from ContactEditModal for window rendering
- ContactsList and ContactDetailPage use openWindow() instead of modal state
- Draggable windows with z-index management
This commit is contained in:
Agent Zero
2026-07-24 23:42:53 +02:00
parent c1d49e4dfe
commit 35e87b5d51
9 changed files with 936 additions and 39 deletions
+131
View File
@@ -0,0 +1,131 @@
import { create } from 'zustand';
import type { ComponentType } from 'react';
export interface WindowState {
id: string;
title: string;
type: string; // 'contact-create', 'contact-edit', etc.
component: ComponentType<any>;
componentProps: Record<string, any>;
state: 'open' | 'minimized' | 'fullscreen';
aiChatVisible: boolean;
zIndex: number;
position: { x: number; y: number };
size: { width: number; height: number };
}
export interface WindowManagerStore {
windows: WindowState[];
activeWindowId: string | null;
nextZIndex: number;
openWindow: (config: Omit<WindowState, 'id' | 'state' | 'aiChatVisible' | 'zIndex' | 'position' | 'size'>) => string;
closeWindow: (id: string) => void;
minimizeWindow: (id: string) => void;
restoreWindow: (id: string) => void;
toggleFullscreen: (id: string) => void;
toggleAiChat: (id: string) => void;
setActiveWindow: (id: string) => void;
updateWindowPosition: (id: string, pos: { x: number; y: number }) => void;
updateWindowSize: (id: string, size: { width: number; height: number }) => void;
}
let windowIdCounter = 0;
const DEFAULT_SIZE = { width: 800, height: 600 };
const DEFAULT_POSITION = { x: 120, y: 80 };
export const useWindowStore = create<WindowManagerStore>((set, get) => ({
windows: [],
activeWindowId: null,
nextZIndex: 100,
openWindow: (config) => {
const id = `win-${++windowIdCounter}`;
const z = get().nextZIndex;
const offset = get().windows.length * 24;
const newWindow: WindowState = {
...config,
id,
state: 'open',
aiChatVisible: false,
zIndex: z,
position: { x: DEFAULT_POSITION.x + offset, y: DEFAULT_POSITION.y + offset },
size: { ...DEFAULT_SIZE },
};
set((s) => ({
windows: [...s.windows, newWindow],
activeWindowId: id,
nextZIndex: z + 1,
}));
return id;
},
closeWindow: (id) =>
set((s) => ({
windows: s.windows.filter((w) => w.id !== id),
activeWindowId: s.activeWindowId === id ? null : s.activeWindowId,
})),
minimizeWindow: (id) =>
set((s) => ({
windows: s.windows.map((w) =>
w.id === id ? { ...w, state: 'minimized' as const } : w
),
activeWindowId: s.activeWindowId === id ? null : s.activeWindowId,
})),
restoreWindow: (id) => {
const z = get().nextZIndex;
set((s) => ({
windows: s.windows.map((w) =>
w.id === id
? { ...w, state: 'open' as const, zIndex: z }
: w
),
activeWindowId: id,
nextZIndex: z + 1,
}));
},
toggleFullscreen: (id) =>
set((s) => ({
windows: s.windows.map((w) =>
w.id === id
? { ...w, state: w.state === 'fullscreen' ? 'open' as const : 'fullscreen' as const }
: w
),
})),
toggleAiChat: (id) =>
set((s) => ({
windows: s.windows.map((w) =>
w.id === id ? { ...w, aiChatVisible: !w.aiChatVisible } : w
),
})),
setActiveWindow: (id) => {
const z = get().nextZIndex;
set((s) => ({
windows: s.windows.map((w) =>
w.id === id ? { ...w, zIndex: z } : w
),
activeWindowId: id,
nextZIndex: z + 1,
}));
},
updateWindowPosition: (id, pos) =>
set((s) => ({
windows: s.windows.map((w) =>
w.id === id ? { ...w, position: pos } : w
),
})),
updateWindowSize: (id, size) =>
set((s) => ({
windows: s.windows.map((w) =>
w.id === id ? { ...w, size } : w
),
})),
}));