diff --git a/__tests__/panel-action-service.test.ts b/__tests__/panel-action-service.test.ts
new file mode 100644
index 0000000..92729d6
--- /dev/null
+++ b/__tests__/panel-action-service.test.ts
@@ -0,0 +1,135 @@
+import { parsePanelActions } from '@/services/panel-action-service';
+
+describe('Panel Action Service', () => {
+ test('should parse direct panel actions', () => {
+ const action = {
+ action: 'addTab',
+ tab: {
+ id: 'tab1',
+ title: 'Test Tab',
+ zones: []
+ }
+ };
+
+ const result = parsePanelActions(action);
+ expect(result).toHaveLength(1);
+ expect(result?.[0]).toEqual(action);
+ });
+
+ test('should parse panel actions in tool/payload structure', () => {
+ const message = {
+ tool: 'panelAction',
+ payload: {
+ action: 'switchTab',
+ tabId: 'tab1'
+ }
+ };
+
+ const result = parsePanelActions(message);
+ expect(result).toHaveLength(1);
+ expect(result?.[0]).toEqual(message.payload);
+ });
+
+ test('should parse multiple panel actions in payload array', () => {
+ const message = {
+ tool: 'panelAction',
+ payload: [
+ {
+ action: 'addTab',
+ tab: {
+ id: 'tab1',
+ title: 'Test Tab',
+ zones: []
+ }
+ },
+ {
+ action: 'addZone',
+ tabId: 'tab1',
+ zone: {
+ id: 'zone1',
+ components: []
+ }
+ }
+ ]
+ };
+
+ const result = parsePanelActions(message);
+ expect(result).toHaveLength(2);
+ expect(result?.[0]).toEqual(message.payload[0]);
+ expect(result?.[1]).toEqual(message.payload[1]);
+ });
+
+ test('should parse panel actions in content structure', () => {
+ const message = {
+ content: {
+ tool: 'panelAction',
+ payload: {
+ action: 'removeComponent',
+ tabId: 'tab1',
+ zoneId: 'zone1',
+ componentId: 'comp1'
+ }
+ }
+ };
+
+ const result = parsePanelActions(message);
+ expect(result).toHaveLength(1);
+ expect(result?.[0]).toEqual(message.content.payload);
+ });
+
+ test('should parse panel actions from JSON string', () => {
+ const jsonString = JSON.stringify({
+ action: 'updateComponent',
+ tabId: 'tab1',
+ zoneId: 'zone1',
+ componentId: 'comp1',
+ props: { title: 'Updated Title' }
+ });
+
+ const result = parsePanelActions(jsonString);
+ expect(result).toHaveLength(1);
+ expect(result?.[0].action).toBe('updateComponent');
+ });
+
+ test('should return null for non-panel actions', () => {
+ const message = {
+ type: 'text',
+ content: 'This is a regular message'
+ };
+
+ const result = parsePanelActions(message);
+ expect(result).toBeNull();
+ });
+
+ test('should validate required fields for different action types', () => {
+ // Valid action
+ const validAction = {
+ action: 'addComponent',
+ tabId: 'tab1',
+ zoneId: 'zone1',
+ component: {
+ id: 'comp1',
+ type: 'chart',
+ props: { data: [1, 2, 3] }
+ }
+ };
+
+ // Missing required field
+ const invalidAction = {
+ action: 'addComponent',
+ tabId: 'tab1',
+ // Missing zoneId
+ component: {
+ id: 'comp1',
+ type: 'chart',
+ props: { data: [1, 2, 3] }
+ }
+ };
+
+ const validResult = parsePanelActions(validAction);
+ expect(validResult).toHaveLength(1);
+
+ const invalidResult = parsePanelActions(invalidAction);
+ expect(invalidResult).toBeNull();
+ });
+});
\ No newline at end of file
diff --git a/__tests__/panel-state.test.tsx b/__tests__/panel-state.test.tsx
new file mode 100644
index 0000000..72c734e
--- /dev/null
+++ b/__tests__/panel-state.test.tsx
@@ -0,0 +1,226 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { PanelStateProvider, usePanelState, usePanelActions } from '@/hooks/use-panel-state';
+import { PanelTab, PanelZone, PanelComponent } from '@/types/panel';
+
+// Test component to interact with panel state
+function TestPanelComponent() {
+ const { state } = usePanelState();
+ const {
+ addTab,
+ removeTab,
+ renameTab,
+ switchTab,
+ addZone,
+ removeZone,
+ addComponent,
+ removeComponent,
+ updateComponent,
+ undo,
+ redo
+ } = usePanelActions();
+
+ // Helper function to create a test tab
+ const createTestTab = () => {
+ const tab: PanelTab = {
+ id: 'test-tab',
+ title: 'Test Tab',
+ zones: []
+ };
+ addTab(tab);
+ };
+
+ // Helper function to create a test zone
+ const createTestZone = () => {
+ if (state.tabs.length === 0) {
+ createTestTab();
+ }
+
+ const zone: PanelZone = {
+ id: 'test-zone',
+ components: []
+ };
+ addZone('test-tab', zone);
+ };
+
+ // Helper function to create a test component
+ const createTestComponent = () => {
+ if (state.tabs.length === 0 || state.tabs[0].zones.length === 0) {
+ createTestZone();
+ }
+
+ const component: PanelComponent = {
+ id: 'test-component',
+ type: 'test',
+ props: { text: 'Test Component' }
+ };
+ addComponent('test-tab', 'test-zone', component);
+ };
+
+ return (
+
+
+ {JSON.stringify(state)}
+
+
+
+ Add Tab
+
+
+
removeTab('test-tab')} data-testid="remove-tab">
+ Remove Tab
+
+
+
renameTab('test-tab', 'Renamed Tab')} data-testid="rename-tab">
+ Rename Tab
+
+
+
switchTab('test-tab')} data-testid="switch-tab">
+ Switch Tab
+
+
+
+ Add Zone
+
+
+
removeZone('test-tab', 'test-zone')} data-testid="remove-zone">
+ Remove Zone
+
+
+
+ Add Component
+
+
+
removeComponent('test-tab', 'test-zone', 'test-component')} data-testid="remove-component">
+ Remove Component
+
+
+
updateComponent('test-tab', 'test-zone', 'test-component', undefined, { text: 'Updated Component' })} data-testid="update-component">
+ Update Component
+
+
+
+ Undo
+
+
+
+ Redo
+
+
+ );
+}
+
+describe('Panel State Management', () => {
+ test('should add and remove tabs', () => {
+ render(
+
+
+
+ );
+
+ // Initial state should have no tabs
+ expect(JSON.parse(screen.getByTestId('panel-state').textContent || '{}')).toHaveProperty('tabs', []);
+
+ // Add a tab
+ fireEvent.click(screen.getByTestId('add-tab'));
+ const stateWithTab = JSON.parse(screen.getByTestId('panel-state').textContent || '{}');
+ expect(stateWithTab.tabs).toHaveLength(1);
+ expect(stateWithTab.tabs[0].id).toBe('test-tab');
+ expect(stateWithTab.tabs[0].title).toBe('Test Tab');
+
+ // Remove the tab
+ fireEvent.click(screen.getByTestId('remove-tab'));
+ expect(JSON.parse(screen.getByTestId('panel-state').textContent || '{}')).toHaveProperty('tabs', []);
+ });
+
+ test('should rename tabs', () => {
+ render(
+
+
+
+ );
+
+ // Add a tab
+ fireEvent.click(screen.getByTestId('add-tab'));
+
+ // Rename the tab
+ fireEvent.click(screen.getByTestId('rename-tab'));
+ const stateWithRenamedTab = JSON.parse(screen.getByTestId('panel-state').textContent || '{}');
+ expect(stateWithRenamedTab.tabs[0].title).toBe('Renamed Tab');
+ });
+
+ test('should add and remove zones', () => {
+ render(
+
+
+
+ );
+
+ // Add a tab and zone
+ fireEvent.click(screen.getByTestId('add-zone'));
+
+ // Check that the zone was added
+ const stateWithZone = JSON.parse(screen.getByTestId('panel-state').textContent || '{}');
+ expect(stateWithZone.tabs[0].zones).toHaveLength(1);
+ expect(stateWithZone.tabs[0].zones[0].id).toBe('test-zone');
+
+ // Remove the zone
+ fireEvent.click(screen.getByTestId('remove-zone'));
+ const stateWithoutZone = JSON.parse(screen.getByTestId('panel-state').textContent || '{}');
+ expect(stateWithoutZone.tabs[0].zones).toHaveLength(0);
+ });
+
+ test('should add, update, and remove components', () => {
+ render(
+
+
+
+ );
+
+ // Add a tab, zone, and component
+ fireEvent.click(screen.getByTestId('add-component'));
+
+ // Check that the component was added
+ const stateWithComponent = JSON.parse(screen.getByTestId('panel-state').textContent || '{}');
+ expect(stateWithComponent.tabs[0].zones[0].components).toHaveLength(1);
+ expect(stateWithComponent.tabs[0].zones[0].components[0].id).toBe('test-component');
+ expect(stateWithComponent.tabs[0].zones[0].components[0].props.text).toBe('Test Component');
+
+ // Update the component
+ fireEvent.click(screen.getByTestId('update-component'));
+ const stateWithUpdatedComponent = JSON.parse(screen.getByTestId('panel-state').textContent || '{}');
+ expect(stateWithUpdatedComponent.tabs[0].zones[0].components[0].props.text).toBe('Updated Component');
+
+ // Remove the component
+ fireEvent.click(screen.getByTestId('remove-component'));
+ const stateWithoutComponent = JSON.parse(screen.getByTestId('panel-state').textContent || '{}');
+ expect(stateWithoutComponent.tabs[0].zones[0].components).toHaveLength(0);
+ });
+
+ test('should support undo and redo', () => {
+ render(
+
+
+
+ );
+
+ // Add a tab
+ fireEvent.click(screen.getByTestId('add-tab'));
+ const stateWithTab = JSON.parse(screen.getByTestId('panel-state').textContent || '{}');
+
+ // Remove the tab
+ fireEvent.click(screen.getByTestId('remove-tab'));
+ const stateWithoutTab = JSON.parse(screen.getByTestId('panel-state').textContent || '{}');
+ expect(stateWithoutTab.tabs).toHaveLength(0);
+
+ // Undo the removal
+ fireEvent.click(screen.getByTestId('undo'));
+ const stateAfterUndo = JSON.parse(screen.getByTestId('panel-state').textContent || '{}');
+ expect(stateAfterUndo.tabs).toHaveLength(1);
+
+ // Redo the removal
+ fireEvent.click(screen.getByTestId('redo'));
+ const stateAfterRedo = JSON.parse(screen.getByTestId('panel-state').textContent || '{}');
+ expect(stateAfterRedo.tabs).toHaveLength(0);
+ });
+});
\ No newline at end of file
diff --git a/app/panel-demo/page.tsx b/app/panel-demo/page.tsx
new file mode 100644
index 0000000..ab96907
--- /dev/null
+++ b/app/panel-demo/page.tsx
@@ -0,0 +1,37 @@
+"use client"
+
+import React, { useEffect } from 'react';
+import { ChatWithPanel } from '@/components/panel/chat-with-panel';
+import { registerAllAgentComponents } from '@/agentic-ui/registry';
+
+/**
+ * PanelDemoPage - Demo page for the AI-controlled panel with tabs and zones
+ */
+export default function PanelDemoPage() {
+ // Register all components when the page loads
+ useEffect(() => {
+ registerAllAgentComponents();
+ console.log("Registered all agent components for panel demo");
+ }, []);
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/components/panel/chat-with-panel.tsx b/components/panel/chat-with-panel.tsx
new file mode 100644
index 0000000..febdaca
--- /dev/null
+++ b/components/panel/chat-with-panel.tsx
@@ -0,0 +1,674 @@
+"use client"
+
+import React, { useRef, useState, useEffect } from 'react';
+import { ChatTab } from '@/components/demo/chat-tab';
+import { PanelWithTabsZones } from './panel-with-tabs-zones';
+import { PanelStateProvider, usePanelState, usePanelActions } from '@/hooks/use-panel-state';
+import { parseAIResponseWithPanelSupport } from '@/services/ai-service-panel-extension';
+import { PanelActionType } from '@/types/panel';
+import { Button } from '@/components/ui/button';
+import { PanelLeft, PanelRight } from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+interface Message {
+ type: 'user' | 'assistant' | 'component' | 'event';
+ content: any;
+}
+
+interface ChatWithPanelProps {
+ initialMessages?: Message[];
+ className?: string;
+}
+
+/**
+ * ChatWithPanelContent - Inner component with access to panel state
+ */
+function ChatWithPanelContent({ initialMessages = [], className }: ChatWithPanelProps) {
+ const [messages, setMessages] = useState(initialMessages);
+ const [input, setInput] = useState('');
+ const [isPanelVisible, setIsPanelVisible] = useState(true);
+ const messagesEndRef = useRef(null);
+ const { state, executeAction } = usePanelState();
+
+ // Scroll to bottom when messages change
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }, [messages]);
+
+ // Handle sending a message
+ const handleSendMessage = async (e: React.FormEvent, modelKey: string) => {
+ e.preventDefault();
+ if (!input.trim()) return;
+
+ // Add user message
+ const userMessage: Message = { type: 'user', content: input };
+ setMessages((prev) => [...prev, userMessage]);
+ setInput('');
+
+ // Simulate AI response (in a real app, this would call your API)
+ setTimeout(() => {
+ // Debug logs
+ console.log('Current panel state:', state);
+ console.log('Processing message:', input);
+
+ // For demo purposes, check if the message contains panel-related keywords
+ const lowerInput = input.toLowerCase();
+
+ // Check for panel-related keywords or specific commands
+ if (lowerInput.includes('panel') ||
+ lowerInput.includes('tab') ||
+ lowerInput.includes('zone') ||
+ lowerInput.includes('component') ||
+ lowerInput.includes('create') ||
+ lowerInput.includes('add') ||
+ lowerInput.includes('dashboard')) {
+ // Simulate a panel action response
+ handlePanelActionDemo(input);
+ } else {
+ // Regular text response
+ setMessages((prev) => [
+ ...prev,
+ {
+ type: 'assistant',
+ content: "I'm a simulated assistant. In a real implementation, I would connect to your backend API and could control the panel with tabs and zones."
+ }
+ ]);
+ }
+ }, 1000);
+ };
+
+ // Demo function to simulate panel actions based on user input
+ const handlePanelActionDemo = (input: string) => {
+ const lowerInput = input.toLowerCase();
+
+ // Extract tab name if present
+ let tabName = '';
+ if (lowerInput.includes('named')) {
+ const parts = input.split(/named\s+/i);
+ if (parts.length > 1) {
+ tabName = parts[1].trim().split(/\s+/)[0];
+ }
+ } else if (lowerInput.includes('dashboard')) {
+ tabName = 'Dashboard';
+ } else if (lowerInput.includes('analytics')) {
+ tabName = 'Analytics';
+ } else if (lowerInput.includes('reports')) {
+ tabName = 'Reports';
+ } else if (lowerInput.includes('create tab') || lowerInput.includes('add tab')) {
+ tabName = `Tab ${Math.floor(Math.random() * 100)}`;
+ }
+
+ // CREATE TAB: Create a new tab
+ if ((lowerInput.includes('create') || lowerInput.includes('add')) &&
+ (lowerInput.includes('tab') || lowerInput.includes('dashboard') ||
+ lowerInput.includes('analytics') || lowerInput.includes('reports')) &&
+ !lowerInput.includes('component') && !lowerInput.includes('zone')) {
+
+ const tabId = `tab-${Date.now()}`;
+
+ // If no tab name was extracted, use a default
+ if (!tabName) {
+ tabName = `Tab ${Math.floor(Math.random() * 100)}`;
+ }
+
+ const action: PanelActionType = {
+ action: 'addTab',
+ tab: {
+ id: tabId,
+ title: tabName,
+ zones: []
+ }
+ };
+
+ // Add a demo zone to the tab
+ setTimeout(() => {
+ const zoneAction: PanelActionType = {
+ action: 'addZone',
+ tabId,
+ zone: {
+ id: `zone-${Date.now()}`,
+ components: []
+ }
+ };
+
+ executeAction(zoneAction);
+
+ // Add a demo component to the zone
+ setTimeout(() => {
+ const componentAction: PanelActionType = {
+ action: 'addComponent',
+ tabId,
+ zoneId: zoneAction.zone.id,
+ component: {
+ id: `component-${Date.now()}`,
+ type: 'markdownrenderer',
+ props: {
+ content: `# ${tabName} Tab
+
+## This is a demo component in the ${tabName} tab
+
+This component was added to demonstrate the panel functionality.
+
+- The panel supports multiple tabs
+- Each tab can have multiple zones
+- Each zone can have multiple components
+- Components can be added, removed, and updated by AI actions`
+ }
+ }
+ };
+
+ executeAction(componentAction);
+ }, 500);
+ }, 500);
+
+ // Execute the action and add a message
+ executeAction(action);
+ setMessages((prev) => [
+ ...prev,
+ {
+ type: 'assistant',
+ content: `I've created a new tab named "${tabName}" in the panel with a sample component.`
+ }
+ ]);
+ }
+
+ // SWITCH TAB: Switch to a different tab
+ else if (lowerInput.includes('switch') || lowerInput.includes('change')) {
+ // Find the tab to switch to
+ let targetTabId = null;
+ let targetTabName = '';
+
+ // Check if a specific tab name is mentioned
+ for (const tab of state.tabs) {
+ if (lowerInput.includes(tab.title.toLowerCase())) {
+ targetTabId = tab.id;
+ targetTabName = tab.title;
+ break;
+ }
+ }
+
+ // If no specific tab is found but there are tabs, switch to the first one
+ if (!targetTabId && state.tabs.length > 0) {
+ targetTabId = state.tabs[0].id;
+ targetTabName = state.tabs[0].title;
+ }
+
+ if (targetTabId) {
+ const action: PanelActionType = {
+ action: 'switchTab',
+ tabId: targetTabId
+ };
+
+ executeAction(action);
+ setMessages((prev) => [
+ ...prev,
+ {
+ type: 'assistant',
+ content: `I've switched to the "${targetTabName}" tab.`
+ }
+ ]);
+ } else {
+ setMessages((prev) => [
+ ...prev,
+ {
+ type: 'assistant',
+ content: "I couldn't find any tabs to switch to. Please create a tab first."
+ }
+ ]);
+ }
+ }
+
+ // REMOVE TAB: Delete a tab
+ else if ((lowerInput.includes('remove') || lowerInput.includes('delete')) &&
+ lowerInput.includes('tab')) {
+ // Find the tab to remove
+ let targetTabId = null;
+ let targetTabName = '';
+
+ // Check if a specific tab name is mentioned
+ for (const tab of state.tabs) {
+ if (lowerInput.includes(tab.title.toLowerCase())) {
+ targetTabId = tab.id;
+ targetTabName = tab.title;
+ break;
+ }
+ }
+
+ // If no specific tab is found but there are tabs, remove the active tab
+ if (!targetTabId && state.tabs.length > 0) {
+ if (state.activeTabId) {
+ const activeTab = state.tabs.find(tab => tab.id === state.activeTabId);
+ if (activeTab) {
+ targetTabId = activeTab.id;
+ targetTabName = activeTab.title;
+ }
+ }
+
+ // If still no target, use the first tab
+ if (!targetTabId) {
+ targetTabId = state.tabs[0].id;
+ targetTabName = state.tabs[0].title;
+ }
+ }
+
+ if (targetTabId) {
+ const action: PanelActionType = {
+ action: 'removeTab',
+ tabId: targetTabId
+ };
+
+ executeAction(action);
+ setMessages((prev) => [
+ ...prev,
+ {
+ type: 'assistant',
+ content: `I've removed the "${targetTabName}" tab.`
+ }
+ ]);
+ } else {
+ setMessages((prev) => [
+ ...prev,
+ {
+ type: 'assistant',
+ content: "I couldn't find any tabs to remove. Please create a tab first."
+ }
+ ]);
+ }
+ }
+
+ // ADD COMPONENT: Add a component to a tab
+ else if ((lowerInput.includes('add') || lowerInput.includes('create')) &&
+ lowerInput.includes('component') && !lowerInput.includes('zone')) {
+ // Find the tab to add the component to
+ let targetTabId = null;
+ let targetTabName = '';
+ let targetZoneId = null;
+
+ // Check if a specific tab name is mentioned
+ for (const tab of state.tabs) {
+ if (lowerInput.includes(tab.title.toLowerCase())) {
+ targetTabId = tab.id;
+ targetTabName = tab.title;
+
+ // Get the first zone in the tab
+ if (tab.zones.length > 0) {
+ targetZoneId = tab.zones[0].id;
+ }
+ break;
+ }
+ }
+
+ // If no specific tab is found but there are tabs, use the active tab
+ if (!targetTabId && state.tabs.length > 0) {
+ if (state.activeTabId) {
+ const activeTab = state.tabs.find(tab => tab.id === state.activeTabId);
+ if (activeTab) {
+ targetTabId = activeTab.id;
+ targetTabName = activeTab.title;
+
+ // Get the first zone in the tab
+ if (activeTab.zones.length > 0) {
+ targetZoneId = activeTab.zones[0].id;
+ }
+ }
+ }
+
+ // If still no target, use the first tab
+ if (!targetTabId) {
+ const firstTab = state.tabs[0];
+ targetTabId = firstTab.id;
+ targetTabName = firstTab.title;
+
+ // Get the first zone in the tab
+ if (firstTab.zones.length > 0) {
+ targetZoneId = firstTab.zones[0].id;
+ }
+ }
+ }
+
+ // If we have a tab but no zone, create a zone
+ if (targetTabId && !targetZoneId) {
+ const zoneId = `zone-${Date.now()}`;
+ const zoneAction: PanelActionType = {
+ action: 'addZone',
+ tabId: targetTabId,
+ zone: {
+ id: zoneId,
+ components: []
+ }
+ };
+
+ executeAction(zoneAction);
+ targetZoneId = zoneId;
+ }
+
+ if (targetTabId && targetZoneId) {
+ // Determine component type based on input
+ let componentType = 'markdownrenderer';
+ let componentProps: any = {
+ content: `# New Component
+
+This is a new component added to the ${targetTabName} tab.`
+ };
+
+ // Check for specific component types in the input
+ if (lowerInput.includes('chart')) {
+ componentType = 'chart';
+ componentProps = {
+ title: 'Sample Chart',
+ type: 'bar',
+ data: {
+ labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
+ datasets: [
+ {
+ label: 'Sales',
+ data: [65, 59, 80, 81, 56],
+ backgroundColor: 'rgba(54, 162, 235, 0.5)'
+ }
+ ]
+ }
+ };
+ } else if (lowerInput.includes('table')) {
+ componentType = 'datatable';
+ componentProps = {
+ title: 'Sample Table',
+ data: [
+ { id: 1, name: 'John Doe', email: 'john@example.com' },
+ { id: 2, name: 'Jane Smith', email: 'jane@example.com' },
+ { id: 3, name: 'Bob Johnson', email: 'bob@example.com' }
+ ],
+ columns: [
+ { header: 'ID', accessorKey: 'id' },
+ { header: 'Name', accessorKey: 'name' },
+ { header: 'Email', accessorKey: 'email' }
+ ]
+ };
+ }
+
+ const componentAction: PanelActionType = {
+ action: 'addComponent',
+ tabId: targetTabId,
+ zoneId: targetZoneId,
+ component: {
+ id: `component-${Date.now()}`,
+ type: componentType,
+ props: componentProps
+ }
+ };
+
+ console.log('Adding component with action:', componentAction);
+ executeAction(componentAction);
+ setMessages((prev) => [
+ ...prev,
+ {
+ type: 'assistant',
+ content: `I've added a new ${componentType} component to the "${targetTabName}" tab.`
+ }
+ ]);
+ } else {
+ setMessages((prev) => [
+ ...prev,
+ {
+ type: 'assistant',
+ content: "I couldn't find any tabs to add a component to. Please create a tab first."
+ }
+ ]);
+ }
+ }
+
+ // ADD ZONE: Add a new zone to a tab
+ else if ((lowerInput.includes('add') || lowerInput.includes('create')) &&
+ lowerInput.includes('zone')) {
+ // Find the tab to add the zone to
+ let targetTabId = null;
+ let targetTabName = '';
+
+ // Check if a specific tab name is mentioned
+ for (const tab of state.tabs) {
+ if (lowerInput.includes(tab.title.toLowerCase())) {
+ targetTabId = tab.id;
+ targetTabName = tab.title;
+ break;
+ }
+ }
+
+ // If no specific tab is found but there are tabs, use the active tab
+ if (!targetTabId && state.tabs.length > 0) {
+ if (state.activeTabId) {
+ const activeTab = state.tabs.find(tab => tab.id === state.activeTabId);
+ if (activeTab) {
+ targetTabId = activeTab.id;
+ targetTabName = activeTab.title;
+ }
+ }
+
+ // If still no target, use the first tab
+ if (!targetTabId) {
+ targetTabId = state.tabs[0].id;
+ targetTabName = state.tabs[0].title;
+ }
+ }
+
+ if (targetTabId) {
+ const zoneAction: PanelActionType = {
+ action: 'addZone',
+ tabId: targetTabId,
+ zone: {
+ id: `zone-${Date.now()}`,
+ components: []
+ }
+ };
+
+ console.log('Adding zone with action:', zoneAction);
+ executeAction(zoneAction);
+
+ // Add a demo component to the zone
+ setTimeout(() => {
+ const zoneId = zoneAction.zone.id;
+ const componentAction: PanelActionType = {
+ action: 'addComponent',
+ tabId: targetTabId!,
+ zoneId: zoneId,
+ component: {
+ id: `component-${Date.now()}`,
+ type: 'markdownrenderer',
+ props: {
+ content: `# New Zone
+
+This is a new zone added to the ${targetTabName} tab.`
+ }
+ }
+ };
+
+ executeAction(componentAction);
+ }, 500);
+
+ setMessages((prev) => [
+ ...prev,
+ {
+ type: 'assistant',
+ content: `I've added a new zone to the "${targetTabName}" tab with a sample component.`
+ }
+ ]);
+ } else {
+ setMessages((prev) => [
+ ...prev,
+ {
+ type: 'assistant',
+ content: "I couldn't find any tabs to add a zone to. Please create a tab first."
+ }
+ ]);
+ }
+ }
+
+ // RENAME TAB: Rename a tab
+ else if (lowerInput.includes('rename') && lowerInput.includes('tab')) {
+ // Find the tab to rename
+ let targetTabId = null;
+ let oldTabName = '';
+ let newTabName = '';
+
+ // Extract new name if present
+ if (lowerInput.includes('to')) {
+ const parts = input.split(/\s+to\s+/i);
+ if (parts.length > 1) {
+ newTabName = parts[1].trim().split(/\s+/)[0];
+ }
+ }
+
+ if (!newTabName) {
+ newTabName = `Tab ${Math.floor(Math.random() * 100)}`;
+ }
+
+ // Check if a specific tab name is mentioned
+ for (const tab of state.tabs) {
+ if (lowerInput.includes(tab.title.toLowerCase())) {
+ targetTabId = tab.id;
+ oldTabName = tab.title;
+ break;
+ }
+ }
+
+ // If no specific tab is found but there are tabs, use the active tab
+ if (!targetTabId && state.tabs.length > 0) {
+ if (state.activeTabId) {
+ const activeTab = state.tabs.find(tab => tab.id === state.activeTabId);
+ if (activeTab) {
+ targetTabId = activeTab.id;
+ oldTabName = activeTab.title;
+ }
+ }
+
+ // If still no target, use the first tab
+ if (!targetTabId) {
+ targetTabId = state.tabs[0].id;
+ oldTabName = state.tabs[0].title;
+ }
+ }
+
+ if (targetTabId) {
+ const action: PanelActionType = {
+ action: 'renameTab',
+ tabId: targetTabId,
+ title: newTabName
+ };
+
+ executeAction(action);
+ setMessages((prev) => [
+ ...prev,
+ {
+ type: 'assistant',
+ content: `I've renamed the "${oldTabName}" tab to "${newTabName}".`
+ }
+ ]);
+ } else {
+ setMessages((prev) => [
+ ...prev,
+ {
+ type: 'assistant',
+ content: "I couldn't find any tabs to rename. Please create a tab first."
+ }
+ ]);
+ }
+ }
+
+ // UNDO: Undo the last action
+ else if (lowerInput.includes('undo')) {
+ const action: PanelActionType = {
+ action: 'undo'
+ };
+
+ executeAction(action);
+ setMessages((prev) => [
+ ...prev,
+ {
+ type: 'assistant',
+ content: "I've undone the last panel action."
+ }
+ ]);
+ }
+
+ // REDO: Redo the last undone action
+ else if (lowerInput.includes('redo')) {
+ const action: PanelActionType = {
+ action: 'redo'
+ };
+
+ executeAction(action);
+ setMessages((prev) => [
+ ...prev,
+ {
+ type: 'assistant',
+ content: "I've redone the last undone panel action."
+ }
+ ]);
+ }
+
+ // General panel info
+ else {
+ setMessages((prev) => [
+ ...prev,
+ {
+ type: 'assistant',
+ content: `The panel with tabs and zones can be controlled by AI actions. Try these commands:
+
+- "Create a tab named Dashboard"
+- "Add a component to the Dashboard tab"
+- "Add a zone to the Dashboard tab"
+- "Switch to the Dashboard tab"
+- "Rename the Dashboard tab to Analytics"
+- "Remove the Analytics tab"
+- "Undo the last action"
+- "Redo the last undone action"`
+ }
+ ]);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
setIsPanelVisible(!isPanelVisible)}
+ className="h-8 w-8"
+ title={isPanelVisible ? "Hide panel" : "Show panel"}
+ >
+ {isPanelVisible ? : }
+
+
+
+
+
+ );
+}
+
+/**
+ * ChatWithPanel - Main component that wraps the content with PanelStateProvider
+ */
+export function ChatWithPanel(props: ChatWithPanelProps) {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/panel/panel-component-renderer.tsx b/components/panel/panel-component-renderer.tsx
new file mode 100644
index 0000000..7e9a33b
--- /dev/null
+++ b/components/panel/panel-component-renderer.tsx
@@ -0,0 +1,51 @@
+"use client"
+
+import React from 'react';
+import { AgentRenderer } from '@/components/agent-renderer';
+import { PanelComponent } from '@/types/panel';
+import { usePanelActions } from '@/hooks/use-panel-state';
+import { Button } from '@/components/ui/button';
+import { X } from 'lucide-react';
+
+interface PanelComponentRendererProps {
+ tabId: string;
+ zoneId: string;
+ component: PanelComponent;
+}
+
+/**
+ * PanelComponentRenderer - Renders a component within a zone
+ * Uses the AgentRenderer to render the actual component
+ */
+export function PanelComponentRenderer({ tabId, zoneId, component }: PanelComponentRendererProps) {
+ const { removeComponent } = usePanelActions();
+
+ const handleRemove = () => {
+ removeComponent(tabId, zoneId, component.id);
+ };
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/components/panel/panel-with-tabs-zones.tsx b/components/panel/panel-with-tabs-zones.tsx
new file mode 100644
index 0000000..415bd92
--- /dev/null
+++ b/components/panel/panel-with-tabs-zones.tsx
@@ -0,0 +1,114 @@
+"use client"
+
+import React from 'react';
+import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { ChevronLeft, ChevronRight, Undo, Redo } from 'lucide-react';
+import { usePanelState, usePanelActions } from '@/hooks/use-panel-state';
+import { PanelZoneComponent } from './panel-zone';
+import { cn } from '@/lib/utils';
+
+interface PanelWithTabsZonesProps {
+ className?: string;
+}
+
+/**
+ * PanelWithTabsZones - Main component for the AI-controlled panel with tabs and zones
+ */
+export function PanelWithTabsZones({ className }: PanelWithTabsZonesProps) {
+ const { state } = usePanelState();
+ const { switchTab, undo, redo } = usePanelActions();
+
+ // If there are no tabs, show an empty state
+ if (state.tabs.length === 0) {
+ return (
+
+
+ Panel
+
+
+
+
No panel content available.
+
The AI can add content to this panel.
+
+
+
+ );
+ }
+
+ // Handle tab change
+ const handleTabChange = (tabId: string) => {
+ switchTab(tabId);
+ };
+
+ return (
+
+
+ Panel
+
+ undo()}
+ disabled={state.undoStack.length === 0}
+ title="Undo"
+ >
+
+
+ redo()}
+ disabled={state.redoStack.length === 0}
+ title="Redo"
+ >
+
+
+
+
+
+
+
+
+
+ {state.tabs.map((tab) => (
+
+ {tab.title}
+
+ ))}
+
+
+
+ {state.tabs.map((tab) => (
+
+ {tab.zones.length === 0 ? (
+
+
No zones in this tab.
+
+ ) : (
+
+ {tab.zones.map((zone) => (
+
+ ))}
+
+ )}
+
+ ))}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/panel/panel-zone.tsx b/components/panel/panel-zone.tsx
new file mode 100644
index 0000000..0430343
--- /dev/null
+++ b/components/panel/panel-zone.tsx
@@ -0,0 +1,37 @@
+"use client"
+
+import React from 'react';
+import { Card, CardContent } from '@/components/ui/card';
+import { PanelZone } from '@/types/panel';
+import { PanelComponentRenderer } from './panel-component-renderer';
+
+interface PanelZoneComponentProps {
+ tabId: string;
+ zone: PanelZone;
+}
+
+/**
+ * PanelZoneComponent - Renders a zone (row) within a tab
+ */
+export function PanelZoneComponent({ tabId, zone }: PanelZoneComponentProps) {
+ return (
+
+
+ {zone.components.length === 0 ? (
+
+ ) : (
+ zone.components.map((component) => (
+
+ ))
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/docs/guidelines/panel-tabs-zones-guidelines.md b/docs/guidelines/panel-tabs-zones-guidelines.md
new file mode 100644
index 0000000..164095c
--- /dev/null
+++ b/docs/guidelines/panel-tabs-zones-guidelines.md
@@ -0,0 +1,148 @@
+# Guidelines: Panel with Tabs & Zones Feature
+
+This document provides best practices and guidelines to follow during the development of the AI-controllable panel with tabs and zones in AgenticGenUI.
+
+---
+
+## 1. Code Quality & Maintainability
+
+- **Type Safety:**
+ Use TypeScript for all new files and ensure strong typing across the data model (tabs, zones, components).
+
+- **Component Decomposition:**
+ Break UI into small, reusable components:
+ - Panel container
+ - Tabs bar
+ - Tab content
+ - Zone row
+ - Component renderer
+
+- **State Management:**
+ - Use React Context or a state management solution (Zustand, Redux, etc.) for global panel state.
+ - Keep state updates as pure functions (reducers) for easy testing and undo/redo functionality.
+
+- **Naming:**
+ - Use descriptive and consistent naming (e.g., `PanelTab`, `PanelZone`, `PanelComponent`).
+ - Avoid ambiguous abbreviations.
+
+- **Documentation:**
+ - Comment code where logic is non-obvious.
+ - Document public APIs and props for all components.
+
+---
+
+## 2. Frontend Design & Responsiveness
+
+- **Layout:**
+ - Use Flexbox or CSS Grid for layout of tabs, zones, and components.
+ - Panel should be resizable/collapsible, especially on desktop.
+ - On mobile, panel should slide in/out or appear as a modal or drawer.
+
+- **Responsiveness:**
+ - Use media queries or utility CSS (e.g., Tailwind, if available) to adjust panel width, tab bar, and content for various screen sizes.
+ - Ensure the main chat area and panel can stack vertically on smaller devices.
+ - Tabs should become a dropdown or scrollable bar on mobile if there are too many to fit.
+
+- **Accessibility:**
+ - Use semantic HTML (e.g., `` for tab bar, `` for tab selectors).
+ - Ensure keyboard navigation for tabs and panel controls (Tab, Arrow keys, Enter, Escape).
+ - Use ARIA attributes for tablists, tabpanels, and roles.
+
+- **Visual Feedback:**
+ - Highlight the active tab.
+ - Indicate loading states or errors in panel components.
+ - Animate transitions where possible (panel open/close, tab switch).
+
+---
+
+## 3. Extensibility
+
+- **Component Registry:**
+ Make it easy to register new component types for AI to render.
+ Document the process for adding new components.
+
+- **Action Protocol:**
+ Design action messages for future extensibility (e.g., new actions, metadata, localization).
+
+- **Theming:**
+ Use CSS variables or a theming system to allow easy customization of panel and tab appearance.
+
+---
+
+## 4. Error Handling & Robustness
+
+- **Graceful Degradation:**
+ If a component type is unknown, render a fallback with an error message but keep the UI functional.
+
+- **Invalid Actions:**
+ Log and ignore invalid/unsupported action messages from the AI, never crash the UI.
+
+- **Undo/Redo:**
+ Test undo/redo for all supported actions; ensure state integrity.
+
+---
+
+## 5. Testing
+
+- **Unit Tests:**
+ Write tests for reducers, utility functions, and critical UI logic.
+
+- **Component Tests:**
+ Use a testing library (e.g., React Testing Library) for rendering and interaction tests.
+
+- **Manual QA:**
+ Check behavior on all target devices and browsers.
+ Verify accessibility (screen reader, keyboard-only navigation).
+
+---
+
+## 6. Performance
+
+- **Efficient Rendering:**
+ Only re-render changed tabs/zones/components where possible.
+ Consider virtualization for long lists of components or zones.
+
+- **Async Loads:**
+ Lazy-load heavy components if appropriate.
+
+---
+
+## 7. Collaboration & PRs
+
+- **Branch Naming:**
+ Use descriptive branch names (e.g., `feature/panel-tabs-zones`).
+
+- **PR Size:**
+ Keep pull requests focused and small; split into logical steps if needed.
+
+- **Reviews:**
+ Request review from at least one other team member.
+ Address review comments promptly.
+
+---
+
+## 8. Documentation & Demos
+
+- **Update Docs:**
+ Document new APIs, components, and usage patterns.
+
+- **Demo Scenarios:**
+ Add/extend demo scenarios to showcase new panel features.
+
+---
+
+## 9. Security
+
+- **Input Validation:**
+ Sanitize and validate all inputs, especially those coming from the AI protocol.
+
+---
+
+## 10. Future Considerations
+
+- Plan for collaborative/multi-user support if needed later.
+- Consider internationalization/localization hooks in UI text.
+
+---
+
+*These guidelines should be reviewed by all contributors before starting and during development of the Panel Tabs & Zones feature.*
\ No newline at end of file
diff --git a/docs/panel-with-tabs-zones.md b/docs/panel-with-tabs-zones.md
new file mode 100644
index 0000000..3ae77ee
--- /dev/null
+++ b/docs/panel-with-tabs-zones.md
@@ -0,0 +1,243 @@
+# AI-Controlled Panel with Tabs and Zones
+
+This document provides an overview of the AI-controlled panel with tabs and zones feature in AgenticGenUI.
+
+## Overview
+
+The panel with tabs and zones is a flexible UI component that allows AI agents to create and manage dynamic, interactive content in a side panel adjacent to the main chat interface. The panel supports multiple tabs, each containing vertically stacked "zones" (rows) that can hold various interactive components.
+
+## Key Features
+
+- **Multi-tab Interface**: Support for multiple tabs with dynamic creation, removal, and reordering.
+- **Zoned Layout**: Each tab contains vertically stacked zones (rows) for organizing content.
+- **Dynamic Components**: Zones can contain any component from the AgenticGenUI component registry.
+- **AI Control**: The panel's layout and content can be fully controlled by AI actions.
+- **Undo/Redo**: Support for undoing and redoing panel changes.
+
+## Data Model
+
+The panel state is structured as follows:
+
+```typescript
+interface PanelState {
+ tabs: PanelTab[];
+ activeTabId: string | null;
+ undoStack: PanelState[];
+ redoStack: PanelState[];
+}
+
+interface PanelTab {
+ id: string;
+ title: string;
+ zones: PanelZone[];
+}
+
+interface PanelZone {
+ id: string;
+ components: PanelComponent[];
+}
+
+interface PanelComponent {
+ id: string;
+ type: string;
+ props: Record;
+}
+```
+
+## Action Protocol
+
+The panel can be controlled through a set of actions:
+
+### Tab Actions
+
+- `addTab`: Add a new tab
+- `removeTab`: Remove an existing tab
+- `renameTab`: Change the title of a tab
+- `reorderTabs`: Change the order of tabs
+- `switchTab`: Set the active (focused) tab
+
+### Zone Actions
+
+- `addZone`: Add a new zone to a tab
+- `removeZone`: Remove a zone from a tab
+- `reorderZones`: Change the order of zones within a tab
+
+### Component Actions
+
+- `addComponent`: Add a new component to a zone
+- `removeComponent`: Remove a component from a zone
+- `updateComponent`: Change the type or properties of a component
+- `reorderComponents`: Change the order of components within a zone
+
+### Bulk/Advanced Actions
+
+- `setPanelState`: Replace the entire panel state
+- `undo`: Undo the last action
+- `redo`: Redo the last undone action
+
+## Example Actions
+
+Here are some example actions:
+
+```json
+// Add a new tab
+{
+ "action": "addTab",
+ "tab": {
+ "id": "dashboard",
+ "title": "Dashboard",
+ "zones": []
+ }
+}
+
+// Add a zone to a tab
+{
+ "action": "addZone",
+ "tabId": "dashboard",
+ "zone": {
+ "id": "metrics",
+ "components": []
+ }
+}
+
+// Add a component to a zone
+{
+ "action": "addComponent",
+ "tabId": "dashboard",
+ "zoneId": "metrics",
+ "component": {
+ "id": "revenue-metric",
+ "type": "metricCard",
+ "props": {
+ "title": "Revenue",
+ "value": "$12,345",
+ "description": "Total revenue this month",
+ "trend": {
+ "value": 12.5,
+ "isPositive": true
+ },
+ "icon": "dollar-sign"
+ }
+ }
+}
+
+// Switch to a different tab
+{
+ "action": "switchTab",
+ "tabId": "dashboard"
+}
+```
+
+## Integration with AI
+
+AI agents can control the panel by sending action messages in their responses. The system will parse these actions and update the panel accordingly.
+
+### AI Response Format
+
+AI responses can include panel actions in several formats:
+
+1. Direct action object:
+```json
+{
+ "action": "addTab",
+ "tab": { "id": "tab1", "title": "New Tab", "zones": [] }
+}
+```
+
+2. Tool/payload structure:
+```json
+{
+ "tool": "panelAction",
+ "payload": {
+ "action": "addTab",
+ "tab": { "id": "tab1", "title": "New Tab", "zones": [] }
+ }
+}
+```
+
+3. Multiple actions in payload array:
+```json
+{
+ "tool": "panelAction",
+ "payload": [
+ { "action": "addTab", "tab": { "id": "tab1", "title": "New Tab", "zones": [] } },
+ { "action": "addZone", "tabId": "tab1", "zone": { "id": "zone1", "components": [] } }
+ ]
+}
+```
+
+## Using the Panel in Your Application
+
+To add the panel to your application:
+
+1. Wrap your application with the `PanelStateProvider`:
+```tsx
+import { PanelStateProvider } from '@/hooks/use-panel-state';
+
+function MyApp() {
+ return (
+
+
+
+ );
+}
+```
+
+2. Use the `PanelWithTabsZones` component:
+```tsx
+import { PanelWithTabsZones } from '@/components/panel/panel-with-tabs-zones';
+
+function MyComponent() {
+ return (
+
+
+ {/* Your main content */}
+
+
+
+ );
+}
+```
+
+3. Use the `usePanelState` and `usePanelActions` hooks to interact with the panel:
+```tsx
+import { usePanelState, usePanelActions } from '@/hooks/use-panel-state';
+
+function MyComponent() {
+ const { state } = usePanelState();
+ const { addTab, switchTab } = usePanelActions();
+
+ // Use the state and actions...
+}
+```
+
+## Demo
+
+A demo of the panel is available at `/panel-demo`. This demo showcases the panel's capabilities and allows you to interact with it through simulated AI responses.
+
+## Extending the Panel
+
+### Adding New Component Types
+
+To add new component types that can be used in the panel:
+
+1. Create your component in the `agentic-ui/components` directory.
+2. Register it in the `agentic-ui/registry.ts` file.
+3. The component will automatically be available for use in the panel.
+
+### Customizing the Panel
+
+The panel's appearance and behavior can be customized by:
+
+- Modifying the `PanelWithTabsZones` component
+- Extending the panel state with additional fields
+- Adding new action types to the panel reducer
+
+## Best Practices
+
+- Generate unique IDs for tabs, zones, and components to avoid conflicts.
+- Keep component props simple and serializable.
+- Use the undo/redo functionality to allow users to recover from unwanted changes.
+- Consider mobile responsiveness when designing panel layouts.
\ No newline at end of file
diff --git a/docs/requirements/panel-zones-tabs_Version2.md b/docs/requirements/panel-zones-tabs_Version2.md
new file mode 100644
index 0000000..298f5b0
--- /dev/null
+++ b/docs/requirements/panel-zones-tabs_Version2.md
@@ -0,0 +1,141 @@
+# Requirements Document: AI-Controlled Panel with Tabs and Zones
+
+## Overview
+
+The objective of this change is to enhance the interactive chat UI by introducing a side panel that supports multiple tabs, each containing vertically stacked "zones" (rows). Each zone can display interactive components, and the AI agent can dynamically control the contents, focus, and configuration of the panel, tabs, zones, and components.
+
+---
+
+## Functional Requirements
+
+### 1. Panel Structure
+
+- The UI shall include a side panel adjacent to the main chat interface.
+- The panel shall support multiple tabs.
+- Each tab shall contain one or more "zones", which are vertically stacked rows.
+- Each zone shall render a list of interactive components (e.g., tables, charts, forms).
+
+### 2. AI Control Capabilities
+
+The AI agent must be able to:
+
+- Add, remove, rename, and reorder tabs.
+- Set which tab is currently in focus (active).
+- Add, remove, and reorder zones within a tab.
+- Add, remove, update, and reorder components within a zone.
+- Move components between zones and between tabs.
+- Replace the entire panel state atomically (for resets or large updates).
+
+### 3. Data Model
+
+- Each panel shall be represented by a state object containing:
+ - A list of tabs.
+ - The ID of the currently active tab.
+- Each tab shall have:
+ - A unique identifier (ID).
+ - A title (string).
+ - An ordered list of zones.
+- Each zone shall have:
+ - A unique identifier (ID).
+ - An ordered list of components.
+- Each component shall have:
+ - A unique identifier (ID).
+ - A type (string, e.g. `"chart"`, `"table"`, `"form"`).
+ - Props (object) to configure its behavior and display.
+
+### 4. Action Protocol
+
+The system shall support a set of discrete actions for UI state changes, including but not limited to:
+
+#### Tab Actions
+
+- `addTab`: Add a new tab.
+- `removeTab`: Remove an existing tab.
+- `renameTab`: Change the title of a tab.
+- `reorderTabs`: Change the order of tabs.
+- `switchTab`: Set the active (focused) tab.
+
+#### Zone Actions
+
+- `addZone`: Add a new zone to a tab.
+- `removeZone`: Remove a zone from a tab.
+- `reorderZones`: Change the order of zones within a tab.
+
+#### Component Actions
+
+- `addComponent`: Add a new component to a zone.
+- `removeComponent`: Remove a component from a zone.
+- `updateComponent`: Change the type or properties of a component.
+- `reorderComponents`: Change the order of components within a zone.
+
+#### Bulk/Advanced Actions
+
+- `setPanelState`: Replace the entire panel state.
+
+All actions must be defined as messages (such as JSON objects) with explicit fields for target IDs and payloads.
+
+---
+
+## Non-Functional Requirements
+
+- All updates to the panel must be reflected in real-time in the UI.
+- The system should ensure state consistency and atomicity for bulk updates.
+- Tab, zone, and component IDs must be unique within their respective scopes.
+- The system must be extensible to support additional component types in the future.
+- The data model and action protocol should be designed to support both local and remote (AI-driven) control.
+- The system shall support undo and redo functionality for AI-driven changes.
+
+---
+
+## Example Action Messages
+
+```json
+// Switch active tab
+{ "action": "switchTab", "tabId": "tab2" }
+
+// Add a new component to a zone
+{
+ "action": "addComponent",
+ "tabId": "tab2",
+ "zoneId": "zoneA",
+ "component": {
+ "id": "comp5",
+ "type": "chart",
+ "props": { "data": [1,2,3] }
+ }
+}
+```
+
+---
+
+## Out of Scope
+
+- Deeply nested zones (zones within zones).
+- Component types not already supported by the base system.
+- UI styling and theming (focus is on structure and state/protocol).
+- Hiding/showing tabs or zones without removal.
+- Collaborative (multi-user) control of the panel.
+
+---
+
+## Open Questions & Answers
+
+- Should the AI be able to hide/show entire tabs or zones without removal?
+ **A:** No, removal only for now.
+- Do we need undo/redo mechanisms for AI-driven changes?
+ **A:** Yes, include undo/redo functionality.
+- Is there a need for collaborative (multi-user) control of the panel?
+ **A:** No.
+
+---
+
+## Acceptance Criteria
+
+- The side panel with tabbed zones must be present and function as specified.
+- All AI-driven actions described above must be supported and reflected in the UI.
+- Undo/redo for AI-driven changes must be implemented.
+- The data model and action protocol must be well-documented and extensible.
+
+---
+
+*End of Requirements Document*
\ No newline at end of file
diff --git a/docs/requirements/technical-panel-zones-tabs.md b/docs/requirements/technical-panel-zones-tabs.md
new file mode 100644
index 0000000..0da5e51
--- /dev/null
+++ b/docs/requirements/technical-panel-zones-tabs.md
@@ -0,0 +1,133 @@
+# Technical Requirements: AI-Controlled Panel with Tabs and Zones
+
+This document outlines the technical requirements and integration details for implementing an AI-controllable side panel with tabbed and zoned UI in the AgenticGenUI codebase.
+
+---
+
+## 1. **Existing Architecture Overview**
+
+- **Chat Interface:** Implemented in `components/demo/chat-tab.tsx` and `components/custom-copilot-ui.tsx`.
+ - Chat messages are managed as an array of objects with `type` (user, assistant, event, component).
+ - Components in chat are rendered using ` `.
+
+- **Component Rendering:**
+ - `components/agent-renderer.tsx` dynamically renders UI components based on instructions received from the AI, using a registry in `agentic-ui/registry.ts`.
+ - Components are referenced by `componentType` and `props`.
+
+- **Sidebar/Panel:**
+ - Core sidebar logic is in `components/ui/sidebar.tsx` (collapsible, responsive, context-driven).
+ - Some demo logic in `components/demo/sidebar.tsx`.
+ - No existing support for AI-controlled multi-tab, multi-zone dynamic layouts.
+
+- **Tabs:**
+ - UI primitives for tabs exist in `components/ui/tabs.tsx` and as higher-level layout in `agentic-ui/components/tab-layout.tsx`.
+
+---
+
+## 2. **Required Data Model Extensions**
+
+**Panel State:**
+```typescript
+type PanelComponent = {
+ id: string;
+ type: string;
+ props: Record;
+};
+
+type PanelZone = {
+ id: string;
+ components: PanelComponent[];
+};
+
+type PanelTab = {
+ id: string;
+ title: string;
+ zones: PanelZone[];
+};
+
+type PanelState = {
+ tabs: PanelTab[];
+ activeTabId: string;
+ undoStack?: PanelState[];
+ redoStack?: PanelState[];
+};
+```
+
+---
+
+## 3. **Required Actions and Protocols**
+
+Implement action message types and handler logic to support:
+
+- **Tab Actions:** addTab, removeTab, renameTab, reorderTabs, switchTab
+- **Zone Actions:** addZone, removeZone, reorderZones within tab
+- **Component Actions:** addComponent, removeComponent, updateComponent, reorderComponents within zone
+- **Bulk/Advanced:** setPanelState (atomic replace), undo, redo
+
+**Example Action:**
+```json
+{
+ "action": "addComponent",
+ "tabId": "tab1",
+ "zoneId": "zoneA",
+ "component": { "id": "c1", "type": "chart", "props": { "data": [1,2,3] } }
+}
+```
+
+---
+
+## 4. **Integration Points and UI Structure**
+
+- **Panel Rendering:**
+ - Implement a new Panel UI component (e.g., `PanelWithTabsZones`) in the side panel area.
+ - Use the `Tabs`, `TabsList`, `TabsTrigger`, and `TabsContent` primitives for tab structure.
+ - Each `TabsContent` renders its list of `PanelZone` (rows), each rendering its list of `PanelComponent` via `AgentRenderer`.
+
+- **State Management:**
+ - Use React context or Redux to store and update `PanelState`.
+ - Support undo/redo by maintaining history stacks in state.
+
+- **AI Protocol Integration:**
+ - Extend the AI message parsing (see `services/ai-service.ts`, e.g. `parseAIResponse`) to recognize and dispatch panel actions.
+ - Support both chat-injected and direct AI panel control messages.
+
+---
+
+## 5. **Component Registration**
+
+- Extend `agentic-ui/registry.ts` to ensure all possible AI target components are registered and can be rendered dynamically via their `type`.
+
+---
+
+## 6. **Undo/Redo Support**
+
+- Implement undo/redo as mutations of the `PanelState`, triggered by explicit `undo`/`redo` action messages.
+
+---
+
+## 7. **Error Handling**
+
+- If a referenced component type is not registered, `AgentRenderer` should render an error and log available types.
+- Invalid action messages should not break the panel; invalid actions should be logged and ignored.
+
+---
+
+## 8. **Testing and Acceptance**
+
+- All action types must be covered by unit tests.
+- Panel must reflect all state changes in real time.
+- Undo/redo must reliably restore previous states.
+- AI must be able to update both chat and panel simultaneously via protocol messages.
+
+---
+
+## 9. **Non-Functional**
+
+- Must remain compatible with existing chat and sidebar logic.
+- Panel and chat must render independently.
+- All new types must be strongly typed (TypeScript).
+- Code must be documented inline and in a new `docs/` technical guide.
+
+---
+
+*This document serves as the technical blueprint for implementing the AI-controllable panel with tabs and zones in AgenticGenUI.*
\ No newline at end of file
diff --git a/hooks/use-panel-state.tsx b/hooks/use-panel-state.tsx
new file mode 100644
index 0000000..82d50b8
--- /dev/null
+++ b/hooks/use-panel-state.tsx
@@ -0,0 +1,450 @@
+"use client"
+
+import React, { createContext, useContext, useReducer, ReactNode, useCallback } from 'react';
+import {
+ PanelState,
+ PanelActionType,
+ PanelTab,
+ PanelZone,
+ PanelComponent
+} from '@/types/panel';
+
+// Initial state for the panel
+const initialPanelState: PanelState = {
+ tabs: [],
+ activeTabId: null,
+ undoStack: [],
+ redoStack: []
+};
+
+// Create context for panel state
+const PanelStateContext = createContext<{
+ state: PanelState;
+ dispatch: React.Dispatch;
+ executeAction: (action: PanelActionType) => void;
+} | undefined>(undefined);
+
+/**
+ * Deep clone an object to ensure immutability
+ */
+function deepClone(obj: T): T {
+ return JSON.parse(JSON.stringify(obj));
+}
+
+/**
+ * Panel state reducer function
+ */
+function panelReducer(state: PanelState, action: PanelActionType): PanelState {
+ // Create a copy of the current state for the undo stack (excluding undo/redo actions)
+ const shouldAddToUndoStack = action.action !== 'undo' && action.action !== 'redo';
+
+ // Helper function to create a new state with updated undo stack
+ const createNewState = (updatedState: Partial): PanelState => {
+ const newState = {
+ ...state,
+ ...updatedState
+ };
+
+ if (shouldAddToUndoStack) {
+ // Add current state to undo stack
+ const currentState = deepClone({
+ tabs: state.tabs,
+ activeTabId: state.activeTabId
+ });
+
+ return {
+ ...newState,
+ undoStack: [...state.undoStack, currentState as PanelState],
+ redoStack: [] // Clear redo stack on new action
+ };
+ }
+
+ return newState;
+ };
+
+ // Handle different action types
+ switch (action.action) {
+ // Tab Actions
+ case 'addTab': {
+ const newTabs = [...state.tabs, action.tab];
+ const activeTabId = state.activeTabId || action.tab.id;
+ return createNewState({ tabs: newTabs, activeTabId });
+ }
+
+ case 'removeTab': {
+ const newTabs = state.tabs.filter(tab => tab.id !== action.tabId);
+ let activeTabId = state.activeTabId;
+
+ // If the active tab is being removed, select another tab
+ if (activeTabId === action.tabId) {
+ activeTabId = newTabs.length > 0 ? newTabs[0].id : null;
+ }
+
+ return createNewState({ tabs: newTabs, activeTabId });
+ }
+
+ case 'renameTab': {
+ const newTabs = state.tabs.map(tab =>
+ tab.id === action.tabId ? { ...tab, title: action.title } : tab
+ );
+ return createNewState({ tabs: newTabs });
+ }
+
+ case 'reorderTabs': {
+ // Create a map for quick lookup
+ const tabMap = new Map(state.tabs.map(tab => [tab.id, tab]));
+
+ // Create new tabs array in the specified order
+ const newTabs = action.tabIds
+ .filter(id => tabMap.has(id))
+ .map(id => tabMap.get(id)!)
+ .concat(state.tabs.filter(tab => !action.tabIds.includes(tab.id)));
+
+ return createNewState({ tabs: newTabs });
+ }
+
+ case 'switchTab': {
+ // Only update if the tab exists
+ const tabExists = state.tabs.some(tab => tab.id === action.tabId);
+ if (!tabExists) return state;
+
+ return createNewState({ activeTabId: action.tabId });
+ }
+
+ // Zone Actions
+ case 'addZone': {
+ const newTabs = state.tabs.map(tab => {
+ if (tab.id === action.tabId) {
+ return {
+ ...tab,
+ zones: [...tab.zones, action.zone]
+ };
+ }
+ return tab;
+ });
+
+ return createNewState({ tabs: newTabs });
+ }
+
+ case 'removeZone': {
+ const newTabs = state.tabs.map(tab => {
+ if (tab.id === action.tabId) {
+ return {
+ ...tab,
+ zones: tab.zones.filter(zone => zone.id !== action.zoneId)
+ };
+ }
+ return tab;
+ });
+
+ return createNewState({ tabs: newTabs });
+ }
+
+ case 'reorderZones': {
+ const newTabs = state.tabs.map(tab => {
+ if (tab.id === action.tabId) {
+ // Create a map for quick lookup
+ const zoneMap = new Map(tab.zones.map(zone => [zone.id, zone]));
+
+ // Create new zones array in the specified order
+ const newZones = action.zoneIds
+ .filter(id => zoneMap.has(id))
+ .map(id => zoneMap.get(id)!)
+ .concat(tab.zones.filter(zone => !action.zoneIds.includes(zone.id)));
+
+ return { ...tab, zones: newZones };
+ }
+ return tab;
+ });
+
+ return createNewState({ tabs: newTabs });
+ }
+
+ // Component Actions
+ case 'addComponent': {
+ const newTabs = state.tabs.map(tab => {
+ if (tab.id === action.tabId) {
+ return {
+ ...tab,
+ zones: tab.zones.map(zone => {
+ if (zone.id === action.zoneId) {
+ return {
+ ...zone,
+ components: [...zone.components, action.component]
+ };
+ }
+ return zone;
+ })
+ };
+ }
+ return tab;
+ });
+
+ return createNewState({ tabs: newTabs });
+ }
+
+ case 'removeComponent': {
+ const newTabs = state.tabs.map(tab => {
+ if (tab.id === action.tabId) {
+ return {
+ ...tab,
+ zones: tab.zones.map(zone => {
+ if (zone.id === action.zoneId) {
+ return {
+ ...zone,
+ components: zone.components.filter(comp => comp.id !== action.componentId)
+ };
+ }
+ return zone;
+ })
+ };
+ }
+ return tab;
+ });
+
+ return createNewState({ tabs: newTabs });
+ }
+
+ case 'updateComponent': {
+ const newTabs = state.tabs.map(tab => {
+ if (tab.id === action.tabId) {
+ return {
+ ...tab,
+ zones: tab.zones.map(zone => {
+ if (zone.id === action.zoneId) {
+ return {
+ ...zone,
+ components: zone.components.map(comp => {
+ if (comp.id === action.componentId) {
+ return {
+ ...comp,
+ ...(action.type && { type: action.type }),
+ ...(action.props && { props: { ...comp.props, ...action.props } })
+ };
+ }
+ return comp;
+ })
+ };
+ }
+ return zone;
+ })
+ };
+ }
+ return tab;
+ });
+
+ return createNewState({ tabs: newTabs });
+ }
+
+ case 'reorderComponents': {
+ const newTabs = state.tabs.map(tab => {
+ if (tab.id === action.tabId) {
+ return {
+ ...tab,
+ zones: tab.zones.map(zone => {
+ if (zone.id === action.zoneId) {
+ // Create a map for quick lookup
+ const componentMap = new Map(zone.components.map(comp => [comp.id, comp]));
+
+ // Create new components array in the specified order
+ const newComponents = action.componentIds
+ .filter(id => componentMap.has(id))
+ .map(id => componentMap.get(id)!)
+ .concat(zone.components.filter(comp => !action.componentIds.includes(comp.id)));
+
+ return { ...zone, components: newComponents };
+ }
+ return zone;
+ })
+ };
+ }
+ return tab;
+ });
+
+ return createNewState({ tabs: newTabs });
+ }
+
+ // Bulk/Advanced Actions
+ case 'setPanelState': {
+ return createNewState({
+ tabs: action.state.tabs,
+ activeTabId: action.state.activeTabId
+ });
+ }
+
+ case 'undo': {
+ if (state.undoStack.length === 0) return state;
+
+ // Get the last state from the undo stack
+ const previousState = state.undoStack[state.undoStack.length - 1];
+ const newUndoStack = state.undoStack.slice(0, -1);
+
+ // Add current state to redo stack
+ const currentState = {
+ tabs: state.tabs,
+ activeTabId: state.activeTabId
+ };
+
+ return {
+ tabs: previousState.tabs,
+ activeTabId: previousState.activeTabId,
+ undoStack: newUndoStack,
+ redoStack: [...state.redoStack, currentState as PanelState]
+ };
+ }
+
+ case 'redo': {
+ if (state.redoStack.length === 0) return state;
+
+ // Get the last state from the redo stack
+ const nextState = state.redoStack[state.redoStack.length - 1];
+ const newRedoStack = state.redoStack.slice(0, -1);
+
+ // Add current state to undo stack
+ const currentState = {
+ tabs: state.tabs,
+ activeTabId: state.activeTabId
+ };
+
+ return {
+ tabs: nextState.tabs,
+ activeTabId: nextState.activeTabId,
+ undoStack: [...state.undoStack, currentState as PanelState],
+ redoStack: newRedoStack
+ };
+ }
+
+ default:
+ console.warn('Unknown panel action:', action);
+ return state;
+ }
+}
+
+/**
+ * Provider component for panel state
+ */
+export function PanelStateProvider({ children }: { children: ReactNode }) {
+ const [state, dispatch] = useReducer(panelReducer, initialPanelState);
+
+ // Helper function to execute an action and handle errors
+ const executeAction = useCallback((action: PanelActionType) => {
+ try {
+ console.log('Executing panel action:', action);
+ dispatch(action);
+ } catch (error) {
+ console.error('Error executing panel action:', error);
+ }
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook to use panel state
+ */
+export function usePanelState() {
+ const context = useContext(PanelStateContext);
+ if (context === undefined) {
+ throw new Error('usePanelState must be used within a PanelStateProvider');
+ }
+ return context;
+}
+
+/**
+ * Helper functions for common panel operations
+ */
+export function usePanelActions() {
+ const { executeAction } = usePanelState();
+
+ // Tab actions
+ const addTab = useCallback((tab: PanelTab) => {
+ executeAction({ action: 'addTab', tab });
+ }, [executeAction]);
+
+ const removeTab = useCallback((tabId: string) => {
+ executeAction({ action: 'removeTab', tabId });
+ }, [executeAction]);
+
+ const renameTab = useCallback((tabId: string, title: string) => {
+ executeAction({ action: 'renameTab', tabId, title });
+ }, [executeAction]);
+
+ const reorderTabs = useCallback((tabIds: string[]) => {
+ executeAction({ action: 'reorderTabs', tabIds });
+ }, [executeAction]);
+
+ const switchTab = useCallback((tabId: string) => {
+ executeAction({ action: 'switchTab', tabId });
+ }, [executeAction]);
+
+ // Zone actions
+ const addZone = useCallback((tabId: string, zone: PanelZone) => {
+ executeAction({ action: 'addZone', tabId, zone });
+ }, [executeAction]);
+
+ const removeZone = useCallback((tabId: string, zoneId: string) => {
+ executeAction({ action: 'removeZone', tabId, zoneId });
+ }, [executeAction]);
+
+ const reorderZones = useCallback((tabId: string, zoneIds: string[]) => {
+ executeAction({ action: 'reorderZones', tabId, zoneIds });
+ }, [executeAction]);
+
+ // Component actions
+ const addComponent = useCallback((tabId: string, zoneId: string, component: PanelComponent) => {
+ executeAction({ action: 'addComponent', tabId, zoneId, component });
+ }, [executeAction]);
+
+ const removeComponent = useCallback((tabId: string, zoneId: string, componentId: string) => {
+ executeAction({ action: 'removeComponent', tabId, zoneId, componentId });
+ }, [executeAction]);
+
+ const updateComponent = useCallback((
+ tabId: string,
+ zoneId: string,
+ componentId: string,
+ type?: string,
+ props?: Record
+ ) => {
+ executeAction({ action: 'updateComponent', tabId, zoneId, componentId, type, props });
+ }, [executeAction]);
+
+ const reorderComponents = useCallback((tabId: string, zoneId: string, componentIds: string[]) => {
+ executeAction({ action: 'reorderComponents', tabId, zoneId, componentIds });
+ }, [executeAction]);
+
+ // Bulk/Advanced actions
+ const setPanelState = useCallback((state: Omit) => {
+ executeAction({ action: 'setPanelState', state });
+ }, [executeAction]);
+
+ const undo = useCallback(() => {
+ executeAction({ action: 'undo' });
+ }, [executeAction]);
+
+ const redo = useCallback(() => {
+ executeAction({ action: 'redo' });
+ }, [executeAction]);
+
+ return {
+ addTab,
+ removeTab,
+ renameTab,
+ reorderTabs,
+ switchTab,
+ addZone,
+ removeZone,
+ reorderZones,
+ addComponent,
+ removeComponent,
+ updateComponent,
+ reorderComponents,
+ setPanelState,
+ undo,
+ redo
+ };
+}
\ No newline at end of file
diff --git a/services/ai-service-panel-extension.ts b/services/ai-service-panel-extension.ts
new file mode 100644
index 0000000..ffdac51
--- /dev/null
+++ b/services/ai-service-panel-extension.ts
@@ -0,0 +1,90 @@
+/**
+ * Extension to the AI service to handle panel actions
+ * This file contains the modifications needed to extend the parseAIResponse function
+ * to support panel actions.
+ */
+
+import { parsePanelActions } from './panel-action-service';
+
+/**
+ * Extends the parseAIResponse function to handle panel actions
+ * This function should be integrated into the existing parseAIResponse function
+ * in services/ai-service.ts
+ */
+export function parseAIResponseWithPanelSupport(response: string): {
+ isComponent: boolean;
+ isPanelAction: boolean;
+ content: any;
+} {
+ try {
+ console.log("Raw AI response:", response);
+
+ // First, try to parse as JSON
+ let parsedJson: any = null;
+ try {
+ // Try to find JSON in code blocks
+ const jsonCodeBlockRegex = /```(?:json)?\s*([\s\S]*?)\s*```/;
+ const jsonCodeBlockMatch = response.match(jsonCodeBlockRegex);
+
+ if (jsonCodeBlockMatch && jsonCodeBlockMatch[1]) {
+ const jsonStr = jsonCodeBlockMatch[1].trim();
+ parsedJson = JSON.parse(jsonStr);
+ } else {
+ // Try to parse the entire response as JSON
+ parsedJson = JSON.parse(response);
+ }
+ } catch (err) {
+ // Not valid JSON, continue with other parsing methods
+ }
+
+ // Check if it's a panel action
+ if (parsedJson) {
+ const panelActions = parsePanelActions(parsedJson);
+ if (panelActions) {
+ console.log("Found panel actions:", panelActions);
+ return {
+ isComponent: false,
+ isPanelAction: true,
+ content: panelActions
+ };
+ }
+
+ // Check if it's a component
+ if (parsedJson.componentType && parsedJson.props) {
+ console.log("Found component:", parsedJson);
+ return {
+ isComponent: true,
+ isPanelAction: false,
+ content: parsedJson
+ };
+ }
+ }
+
+ // Check the raw response for panel actions
+ const panelActions = parsePanelActions(response);
+ if (panelActions) {
+ console.log("Found panel actions in raw response:", panelActions);
+ return {
+ isComponent: false,
+ isPanelAction: true,
+ content: panelActions
+ };
+ }
+
+ // If we get here, it's not a panel action or component
+ // Continue with the original parseAIResponse logic
+ // This is a placeholder for integration with the existing function
+ return {
+ isComponent: false,
+ isPanelAction: false,
+ content: response
+ };
+ } catch (error) {
+ console.error("Error in parseAIResponseWithPanelSupport:", error);
+ return {
+ isComponent: false,
+ isPanelAction: false,
+ content: "I encountered an error processing your request. Please try again."
+ };
+ }
+}
\ No newline at end of file
diff --git a/services/panel-action-service.ts b/services/panel-action-service.ts
new file mode 100644
index 0000000..97fd549
--- /dev/null
+++ b/services/panel-action-service.ts
@@ -0,0 +1,174 @@
+import { PanelActionType } from '@/types/panel';
+
+/**
+ * Parses a message to check if it contains panel actions
+ * @param message The message to parse
+ * @returns An array of panel actions if found, or null if no panel actions are found
+ */
+export function parsePanelActions(message: any): PanelActionType[] | null {
+ try {
+ // Check if the message is a direct panel action
+ if (message && typeof message === 'object' && message.action) {
+ if (isPanelAction(message)) {
+ return [message as PanelActionType];
+ }
+ }
+
+ // Check if the message contains panel actions in a tool/payload structure
+ if (message?.tool === 'panelAction' && message?.payload) {
+ if (Array.isArray(message.payload)) {
+ // Multiple actions
+ const validActions = message.payload.filter(isPanelAction);
+ if (validActions.length > 0) {
+ return validActions as PanelActionType[];
+ }
+ } else if (isPanelAction(message.payload)) {
+ // Single action
+ return [message.payload as PanelActionType];
+ }
+ }
+
+ // Check if the message contains panel actions in content
+ if (message?.content?.tool === 'panelAction' && message?.content?.payload) {
+ if (Array.isArray(message.content.payload)) {
+ // Multiple actions
+ const validActions = message.content.payload.filter(isPanelAction);
+ if (validActions.length > 0) {
+ return validActions as PanelActionType[];
+ }
+ } else if (isPanelAction(message.content.payload)) {
+ // Single action
+ return [message.content.payload as PanelActionType];
+ }
+ }
+
+ // Try to find panel actions in JSON strings
+ if (typeof message === 'string') {
+ try {
+ const parsed = JSON.parse(message);
+ if (isPanelAction(parsed)) {
+ return [parsed as PanelActionType];
+ }
+
+ if (parsed.tool === 'panelAction' && parsed.payload) {
+ if (Array.isArray(parsed.payload)) {
+ const validActions = parsed.payload.filter(isPanelAction);
+ if (validActions.length > 0) {
+ return validActions as PanelActionType[];
+ }
+ } else if (isPanelAction(parsed.payload)) {
+ return [parsed.payload as PanelActionType];
+ }
+ }
+ } catch (e) {
+ // Not valid JSON, continue
+ }
+ }
+
+ return null;
+ } catch (error) {
+ console.error('Error parsing panel actions:', error);
+ return null;
+ }
+}
+
+/**
+ * Checks if an object is a valid panel action
+ * @param obj The object to check
+ * @returns True if the object is a valid panel action, false otherwise
+ */
+function isPanelAction(obj: any): boolean {
+ if (!obj || typeof obj !== 'object' || !obj.action) {
+ return false;
+ }
+
+ // List of valid panel actions
+ const validActions = [
+ 'addTab',
+ 'removeTab',
+ 'renameTab',
+ 'reorderTabs',
+ 'switchTab',
+ 'addZone',
+ 'removeZone',
+ 'reorderZones',
+ 'addComponent',
+ 'removeComponent',
+ 'updateComponent',
+ 'reorderComponents',
+ 'setPanelState',
+ 'undo',
+ 'redo'
+ ];
+
+ // Check if the action is valid
+ if (!validActions.includes(obj.action)) {
+ return false;
+ }
+
+ // Validate required fields based on action type
+ switch (obj.action) {
+ case 'addTab':
+ return !!obj.tab && typeof obj.tab === 'object' &&
+ !!obj.tab.id && !!obj.tab.title && Array.isArray(obj.tab.zones);
+
+ case 'removeTab':
+ case 'switchTab':
+ return !!obj.tabId && typeof obj.tabId === 'string';
+
+ case 'renameTab':
+ return !!obj.tabId && typeof obj.tabId === 'string' &&
+ !!obj.title && typeof obj.title === 'string';
+
+ case 'reorderTabs':
+ return Array.isArray(obj.tabIds);
+
+ case 'addZone':
+ return !!obj.tabId && typeof obj.tabId === 'string' &&
+ !!obj.zone && typeof obj.zone === 'object' &&
+ !!obj.zone.id && Array.isArray(obj.zone.components);
+
+ case 'removeZone':
+ return !!obj.tabId && typeof obj.tabId === 'string' &&
+ !!obj.zoneId && typeof obj.zoneId === 'string';
+
+ case 'reorderZones':
+ return !!obj.tabId && typeof obj.tabId === 'string' &&
+ Array.isArray(obj.zoneIds);
+
+ case 'addComponent':
+ return !!obj.tabId && typeof obj.tabId === 'string' &&
+ !!obj.zoneId && typeof obj.zoneId === 'string' &&
+ !!obj.component && typeof obj.component === 'object' &&
+ !!obj.component.id && !!obj.component.type &&
+ !!obj.component.props && typeof obj.component.props === 'object';
+
+ case 'removeComponent':
+ return !!obj.tabId && typeof obj.tabId === 'string' &&
+ !!obj.zoneId && typeof obj.zoneId === 'string' &&
+ !!obj.componentId && typeof obj.componentId === 'string';
+
+ case 'updateComponent':
+ return !!obj.tabId && typeof obj.tabId === 'string' &&
+ !!obj.zoneId && typeof obj.zoneId === 'string' &&
+ !!obj.componentId && typeof obj.componentId === 'string' &&
+ (!!obj.type || !!obj.props);
+
+ case 'reorderComponents':
+ return !!obj.tabId && typeof obj.tabId === 'string' &&
+ !!obj.zoneId && typeof obj.zoneId === 'string' &&
+ Array.isArray(obj.componentIds);
+
+ case 'setPanelState':
+ return !!obj.state && typeof obj.state === 'object' &&
+ Array.isArray(obj.state.tabs) &&
+ (obj.state.activeTabId === null || typeof obj.state.activeTabId === 'string');
+
+ case 'undo':
+ case 'redo':
+ return true;
+
+ default:
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/types/panel.ts b/types/panel.ts
new file mode 100644
index 0000000..2f553ff
--- /dev/null
+++ b/types/panel.ts
@@ -0,0 +1,165 @@
+/**
+ * Types for the AI-controlled panel with tabs and zones
+ */
+
+/**
+ * Represents a component within a zone
+ */
+export interface PanelComponent {
+ id: string;
+ type: string;
+ props: Record;
+}
+
+/**
+ * Represents a zone (row) within a tab
+ */
+export interface PanelZone {
+ id: string;
+ components: PanelComponent[];
+}
+
+/**
+ * Represents a tab within the panel
+ */
+export interface PanelTab {
+ id: string;
+ title: string;
+ zones: PanelZone[];
+}
+
+/**
+ * Represents the entire panel state
+ */
+export interface PanelState {
+ tabs: PanelTab[];
+ activeTabId: string | null;
+ undoStack: PanelState[];
+ redoStack: PanelState[];
+}
+
+/**
+ * Base interface for all panel actions
+ */
+export interface PanelAction {
+ action: string;
+}
+
+/**
+ * Tab Actions
+ */
+export interface AddTabAction extends PanelAction {
+ action: 'addTab';
+ tab: PanelTab;
+}
+
+export interface RemoveTabAction extends PanelAction {
+ action: 'removeTab';
+ tabId: string;
+}
+
+export interface RenameTabAction extends PanelAction {
+ action: 'renameTab';
+ tabId: string;
+ title: string;
+}
+
+export interface ReorderTabsAction extends PanelAction {
+ action: 'reorderTabs';
+ tabIds: string[];
+}
+
+export interface SwitchTabAction extends PanelAction {
+ action: 'switchTab';
+ tabId: string;
+}
+
+/**
+ * Zone Actions
+ */
+export interface AddZoneAction extends PanelAction {
+ action: 'addZone';
+ tabId: string;
+ zone: PanelZone;
+}
+
+export interface RemoveZoneAction extends PanelAction {
+ action: 'removeZone';
+ tabId: string;
+ zoneId: string;
+}
+
+export interface ReorderZonesAction extends PanelAction {
+ action: 'reorderZones';
+ tabId: string;
+ zoneIds: string[];
+}
+
+/**
+ * Component Actions
+ */
+export interface AddComponentAction extends PanelAction {
+ action: 'addComponent';
+ tabId: string;
+ zoneId: string;
+ component: PanelComponent;
+}
+
+export interface RemoveComponentAction extends PanelAction {
+ action: 'removeComponent';
+ tabId: string;
+ zoneId: string;
+ componentId: string;
+}
+
+export interface UpdateComponentAction extends PanelAction {
+ action: 'updateComponent';
+ tabId: string;
+ zoneId: string;
+ componentId: string;
+ type?: string;
+ props?: Record;
+}
+
+export interface ReorderComponentsAction extends PanelAction {
+ action: 'reorderComponents';
+ tabId: string;
+ zoneId: string;
+ componentIds: string[];
+}
+
+/**
+ * Bulk/Advanced Actions
+ */
+export interface SetPanelStateAction extends PanelAction {
+ action: 'setPanelState';
+ state: Omit;
+}
+
+export interface UndoAction extends PanelAction {
+ action: 'undo';
+}
+
+export interface RedoAction extends PanelAction {
+ action: 'redo';
+}
+
+/**
+ * Union type of all possible panel actions
+ */
+export type PanelActionType =
+ | AddTabAction
+ | RemoveTabAction
+ | RenameTabAction
+ | ReorderTabsAction
+ | SwitchTabAction
+ | AddZoneAction
+ | RemoveZoneAction
+ | ReorderZonesAction
+ | AddComponentAction
+ | RemoveComponentAction
+ | UpdateComponentAction
+ | ReorderComponentsAction
+ | SetPanelStateAction
+ | UndoAction
+ | RedoAction;
\ No newline at end of file