From 7b34608ed7b64c04de047bd6744678ec83aed447 Mon Sep 17 00:00:00 2001 From: MervinPraison Date: Wed, 15 Apr 2026 04:33:01 +0100 Subject: [PATCH] feat(ui): add Multica-style UI example with floating chat and sidebar Added a new example demonstrating the Multica-inspired UI features in PraisonAIUI. This includes a floating, resizable chat window, a collapsible sidebar, custom branding options, and a multi-page dashboard layout. Updated relevant files and the package version in `pyproject.toml`. --- examples/python/27-multica-style/app.py | 180 ++++++++++ pyproject.toml | 2 +- server.pid | 1 + src/praisonaiui/__init__.py | 6 +- src/praisonaiui/features/theme.py | 57 +++- src/praisonaiui/server.py | 110 ++++++ .../templates/frontend/plugins/dashboard.js | 215 +++++++++--- .../frontend/plugins/plugin-loader.js | 2 + .../templates/frontend/plugins/views/chat.js | 313 ++++++++++++++++++ ui-config.test.json | 1 + uv.lock | 2 +- 11 files changed, 839 insertions(+), 50 deletions(-) create mode 100644 examples/python/27-multica-style/app.py create mode 100644 server.pid create mode 100644 ui-config.test.json diff --git a/examples/python/27-multica-style/app.py b/examples/python/27-multica-style/app.py new file mode 100644 index 0000000..5ec2a76 --- /dev/null +++ b/examples/python/27-multica-style/app.py @@ -0,0 +1,180 @@ +""" +Multica-Style UI Example +======================== + +Demonstrates the new Multica-inspired UI features in PraisonAIUI: +- Floating chat window (resizable, draggable, minimizable) +- Collapsible sidebar +- Custom brand color +- Multi-page dashboard with exact Multica look + +Run: python app.py +""" + +import praisonaiui as aiui + +# ── Configure Multica-style UI ──────────────────────────────────── +aiui.set_style("dashboard") + +# Brand color (Multica uses indigo-based palette) +aiui.set_brand_color("#818cf8") # Indigo-400 + +# Branding +aiui.set_branding(title="Multica", logo="🤖") + +# Theme: dark mode with rounded corners +aiui.set_theme(preset="indigo", dark_mode=True, radius="lg") + +# Floating chat window (Multica's signature feature) +aiui.set_chat_mode( + "floating", + position=(20, 20), # bottom-right offset + size=(420, 550), # width x height + resizable=True, + minimized=False, +) + +# Collapsible sidebar +aiui.set_sidebar_config( + collapsible=True, + default_collapsed=False, + width=260, +) + +# ── Register a simple agent ─────────────────────────────────────── +@aiui.profiles +async def profiles(): + return [ + { + "id": "assistant", + "name": "Assistant", + "description": "A helpful AI assistant", + "avatar": "🤖", + }, + { + "id": "coder", + "name": "Coder", + "description": "Expert programmer", + "avatar": "👨‍💻", + }, + ] + + +@aiui.reply +async def reply(message: str, agent_name: str = None): + """Simple echo reply for demo purposes.""" + agent = agent_name or "Assistant" + yield f"**{agent}**: I received your message: *{message}*\n\n" + yield "This is a demo of the Multica-style floating chat interface. " + yield "Try:\n" + yield "- **Drag** the chat window by its header\n" + yield "- **Resize** by dragging the edges\n" + yield "- **Minimize** with the − button\n" + yield "- **Collapse** the sidebar with the toggle button\n" + + +# ── Dashboard Pages ─────────────────────────────────────────────── +@aiui.page("overview", title="Overview", icon="📊", group="Main") +async def overview_page(): + return { + "_components": [ + aiui.columns([ + aiui.metric(label="Active Agents", value="2", delta="+1"), + aiui.metric(label="Sessions Today", value="12", delta="+3"), + aiui.metric(label="Messages", value="156", delta="+24"), + aiui.metric(label="Uptime", value="99.9%"), + ]), + aiui.card( + title="Welcome to Multica-Style UI", + children=[ + aiui.text( + "This example demonstrates the new Multica-inspired features " + "in PraisonAIUI. The floating chat window, collapsible sidebar, " + "and custom brand color are all configurable via Python." + ), + ], + ), + aiui.card( + title="Features Demonstrated", + children=[ + aiui.text("• **Floating Chat**: Resizable, draggable chat window"), + aiui.text("• **Collapsible Sidebar**: Click the toggle to collapse"), + aiui.text("• **Brand Color**: Custom accent color (#818cf8)"), + aiui.text("• **Multi-page**: Multiple dashboard pages in one file"), + ], + ), + ] + } + + +@aiui.page("agents", title="Agents", icon="🤖", group="Main") +async def agents_page(): + return { + "_components": [ + aiui.card( + title="Available Agents", + children=[ + aiui.table( + headers=["Agent", "Status", "Description"], + rows=[ + ["🤖 Assistant", "Active", "General-purpose AI assistant"], + ["👨‍💻 Coder", "Active", "Expert programmer for coding tasks"], + ], + ), + ], + ), + ] + } + + +@aiui.page("settings", title="Settings", icon="⚙️", group="System") +async def settings_page(): + return { + "_components": [ + aiui.card( + title="UI Configuration", + children=[ + aiui.text("**Chat Mode**: Floating"), + aiui.text("**Position**: Bottom-right (20px, 20px)"), + aiui.text("**Size**: 420 × 550 px"), + aiui.text("**Resizable**: Yes"), + aiui.separator(), + aiui.text("**Sidebar**: Collapsible"), + aiui.text("**Width**: 260px"), + aiui.separator(), + aiui.text("**Brand Color**: #818cf8 (Indigo-400)"), + aiui.text("**Theme**: Indigo, Dark Mode, Large Radius"), + ], + ), + aiui.card( + title="Code Example", + children=[ + aiui.code_block( + code='''import praisonaiui as aiui + +# Floating chat +aiui.set_chat_mode( + "floating", + position=(20, 20), + size=(420, 550), + resizable=True, +) + +# Collapsible sidebar +aiui.set_sidebar_config( + collapsible=True, + width=260, +) + +# Brand color +aiui.set_brand_color("#818cf8")''', + language="python", + ), + ], + ), + ] + } + + +# ── Run the app ─────────────────────────────────────────────────── +# Run with: aiui run app.py --style dashboard diff --git a/pyproject.toml b/pyproject.toml index 94b3687..1981deb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "aiui" -version = "0.3.101" +version = "0.3.102" description = "YAML-driven website generator - CLI and compiler" readme = "README.md" license = {text = "MIT"} diff --git a/server.pid b/server.pid new file mode 100644 index 0000000..25bb10b --- /dev/null +++ b/server.pid @@ -0,0 +1 @@ +93373 diff --git a/src/praisonaiui/__init__.py b/src/praisonaiui/__init__.py index 0404149..8439a5d 100644 --- a/src/praisonaiui/__init__.py +++ b/src/praisonaiui/__init__.py @@ -28,7 +28,8 @@ def __getattr__(name: str): _server_attrs = {"register_agent", "register_page", "set_datastore", "get_datastore", "set_provider", "get_provider", "set_style", "set_pages", "remove_page", "set_branding", "set_theme", "set_custom_css", "register_theme", - "set_chat_features", "set_dashboard"} + "set_chat_features", "set_dashboard", "set_chat_mode", "set_brand_color", + "set_sidebar_config"} _datastore_attrs = {"BaseDataStore", "MemoryDataStore", "JSONFileDataStore"} _provider_attrs = {"BaseProvider", "RunEvent", "RunEventType"} _providers_attrs = {"PraisonAIProvider"} @@ -136,6 +137,9 @@ __all__ = [ "set_custom_css", "set_chat_features", "set_dashboard", + "set_chat_mode", + "set_brand_color", + "set_sidebar_config", "register_theme", "set_pages", "remove_page", diff --git a/src/praisonaiui/features/theme.py b/src/praisonaiui/features/theme.py index 2a258dd..72dcaa7 100644 --- a/src/praisonaiui/features/theme.py +++ b/src/praisonaiui/features/theme.py @@ -83,17 +83,41 @@ RADIUS_MAP: Dict[str, str] = { } # Dark / light mode CSS variable sets +# Extended with Multica-style tokens: brand, sidebar-specific, semantic colors MODE_VARS: Dict[str, Dict[str, str]] = { "dark": { - "--db-bg": "#0a0a0f", - "--db-sidebar-bg": "#111118", - "--db-text": "#e4e4e7", - "--db-text-dim": "#71717a", - "--db-border": "rgba(255,255,255,0.06)", + # Core background/text + "--db-bg": "#18181b", + "--db-sidebar-bg": "#18181b", + "--db-text": "#ffffff", + "--db-text-dim": "#a1a1aa", + "--db-border": "#3f3f46", "--db-card-bg": "rgba(255,255,255,0.03)", - "--db-hover": "rgba(255,255,255,0.04)", + "--db-hover": "#27272a", + # Multica-style extended tokens + "--db-canvas": "#000000", + "--db-popover": "#18181b", + "--db-muted": "#27272a", + "--db-muted-fg": "#a1a1aa", + # Semantic colors + "--db-success": "#22c55e", + "--db-success-bg": "rgba(34,197,94,0.12)", + "--db-warning": "#eab308", + "--db-warning-bg": "rgba(234,179,8,0.12)", + "--db-error": "#ef4444", + "--db-error-bg": "rgba(239,68,68,0.12)", + "--db-info": "#3b82f6", + "--db-info-bg": "rgba(59,130,246,0.12)", + # Sidebar-specific + "--db-sidebar-accent": "rgba(255,255,255,0.06)", + "--db-sidebar-accent-fg": "#e4e4e7", + "--db-sidebar-border": "rgba(255,255,255,0.06)", + # Scrollbar + "--db-scrollbar-thumb": "rgba(255,255,255,0.08)", + "--db-scrollbar-track": "transparent", }, "light": { + # Core background/text "--db-bg": "#fafafa", "--db-sidebar-bg": "#f4f4f5", "--db-text": "#18181b", @@ -101,6 +125,27 @@ MODE_VARS: Dict[str, Dict[str, str]] = { "--db-border": "rgba(0,0,0,0.08)", "--db-card-bg": "rgba(0,0,0,0.02)", "--db-hover": "rgba(0,0,0,0.04)", + # Multica-style extended tokens + "--db-canvas": "#f5f5f5", + "--db-popover": "#ffffff", + "--db-muted": "#e4e4e7", + "--db-muted-fg": "#71717a", + # Semantic colors + "--db-success": "#16a34a", + "--db-success-bg": "rgba(22,163,74,0.12)", + "--db-warning": "#ca8a04", + "--db-warning-bg": "rgba(202,138,4,0.12)", + "--db-error": "#dc2626", + "--db-error-bg": "rgba(220,38,38,0.12)", + "--db-info": "#2563eb", + "--db-info-bg": "rgba(37,99,235,0.12)", + # Sidebar-specific + "--db-sidebar-accent": "rgba(0,0,0,0.04)", + "--db-sidebar-accent-fg": "#18181b", + "--db-sidebar-border": "rgba(0,0,0,0.08)", + # Scrollbar + "--db-scrollbar-thumb": "rgba(0,0,0,0.10)", + "--db-scrollbar-track": "transparent", }, } diff --git a/src/praisonaiui/server.py b/src/praisonaiui/server.py index 5ea3e26..8a20f09 100644 --- a/src/praisonaiui/server.py +++ b/src/praisonaiui/server.py @@ -252,6 +252,106 @@ def set_dashboard( } +# Chat mode config set via aiui.set_chat_mode() +_chat_mode: Optional[dict[str, Any]] = None + + +def set_chat_mode( + mode: str = "fullpage", + *, + position: tuple[int, int] | None = None, + size: tuple[int, int] | None = None, + resizable: bool = True, + minimized: bool = False, +) -> None: + """Configure chat window display mode. + + Controls how the chat interface is rendered — as a full page, + floating window, or sidebar panel. + + Args: + mode: Display mode. Options: + - "fullpage" (default): Chat fills the main content area + - "floating": Resizable floating window (bottom-right) + - "sidebar": Fixed panel on the right side + position: (bottom, right) pixel offset for floating mode. + Default: (20, 20) + size: (width, height) initial size for floating mode. + Default: (400, 500) + resizable: Allow resizing the floating window. Default True. + minimized: Start minimized (floating mode only). Default False. + + Example:: + + import praisonaiui as aiui + aiui.set_chat_mode("floating", position=(20, 20), size=(420, 550)) + """ + global _chat_mode + _chat_mode = { + "mode": mode, + "position": list(position) if position else [20, 20], + "size": list(size) if size else [400, 500], + "resizable": resizable, + "minimized": minimized, + } + + +# Brand color set via aiui.set_brand_color() +_brand_color: Optional[str] = None + + +def set_brand_color(color: str) -> None: + """Set the brand/primary accent color. + + This color is used for primary buttons, links, and highlights + throughout the UI. Overrides the theme's default accent color. + + Args: + color: Hex color (e.g. "#6366f1") or CSS color value. + + Example:: + + import praisonaiui as aiui + aiui.set_brand_color("#818cf8") # Indigo-400 + """ + global _brand_color + _brand_color = color + + +def set_sidebar_config( + *, + collapsible: bool = True, + default_collapsed: bool = False, + width: int = 260, + min_width: int = 200, + max_width: int = 360, +) -> None: + """Configure sidebar behavior. + + Args: + collapsible: Allow sidebar to be collapsed. Default True. + default_collapsed: Start with sidebar collapsed. Default False. + width: Default sidebar width in pixels. Default 260. + min_width: Minimum width when resizing. Default 200. + max_width: Maximum width when resizing. Default 360. + + Example:: + + import praisonaiui as aiui + aiui.set_sidebar_config(collapsible=True, width=280) + """ + global _dashboard_config + if _dashboard_config is None: + _dashboard_config = {} + _dashboard_config.update({ + "sidebarCollapsible": collapsible, + "sidebarCollapsed": default_collapsed, + "sidebarWidth": width, + "sidebarMinWidth": min_width, + "sidebarMaxWidth": max_width, + }) + + def register_theme(name: str, variables: dict[str, str]) -> None: """Register a custom theme preset via the protocol-driven ThemeManager. @@ -1536,6 +1636,12 @@ def create_app( except Exception: pass + # Chat mode config + _chat_mode_cfg = _chat_mode or {"mode": "fullpage"} + + # Brand color + _brand = _brand_color + return JSONResponse({ "style": effective_style, "site": { @@ -1543,9 +1649,13 @@ def create_app( "logo": _logo, "theme": _theme_cfg, "customCss": _css, + **({ + "brandColor": _brand, + } if _brand else {}), }, "chat": { "enabled": effective_style in ("chat", "agents", "playground", "dashboard"), + "mode": _chat_mode_cfg, **({ "features": _chat_features, } if _chat_features else {}), diff --git a/src/praisonaiui/templates/frontend/plugins/dashboard.js b/src/praisonaiui/templates/frontend/plugins/dashboard.js index e723456..9cf00e3 100644 --- a/src/praisonaiui/templates/frontend/plugins/dashboard.js +++ b/src/praisonaiui/templates/frontend/plugins/dashboard.js @@ -53,20 +53,32 @@ window.aiui.registerView = function(pageId, renderFn, cleanupFn) { }; window.aiui.views = VIEW_REGISTRY; +// ── Sidebar State ──────────────────────────────────────────────── +let sidebarCollapsed = false; +let sidebarConfig = { + collapsible: true, + defaultCollapsed: false, + width: 260, + minWidth: 200, + maxWidth: 360, +}; + const DASHBOARD_STYLE = ` /* ── Dashboard layout ───────────────────────────────────── */ :root { --db-sidebar-w: 260px; - --db-bg: #0a0a0f; - --db-sidebar-bg: #111118; - --db-border: rgba(255,255,255,0.06); - --db-text: #e4e4e7; - --db-text-dim: #71717a; - --db-accent: #6366f1; - --db-accent-glow: rgba(99,102,241,0.15); + --db-sidebar-collapsed-w: 60px; + --db-bg: #18181b; + --db-sidebar-bg: #18181b; + --db-border: #3f3f46; + --db-text: #ffffff; + --db-text-dim: #a1a1aa; --db-card-bg: rgba(255,255,255,0.03); - --db-hover: rgba(255,255,255,0.05); - --db-radius: 10px; + --db-hover: #27272a; + --db-radius: 8px; + --db-accent: #71717a; + --db-accent-rgb: 113, 113, 122; + --db-accent-glow: rgba(113, 113, 122, 0.15); --db-transition: 0.2s cubic-bezier(0.4,0,0.2,1); } @@ -81,6 +93,27 @@ const DASHBOARD_STYLE = ` background: var(--db-sidebar-bg); border-right: 1px solid var(--db-border); display: flex; flex-direction: column; overflow-y: auto; position: sticky; top: 0; height: 100vh; + transition: width 0.2s ease, min-width 0.2s ease; + } + .db-sidebar.collapsed { + width: var(--db-sidebar-collapsed-w); + min-width: var(--db-sidebar-collapsed-w); + } + .db-sidebar.collapsed .db-sidebar-header span:not(.logo), + .db-sidebar.collapsed .db-group-label, + .db-sidebar.collapsed .db-nav-item span:not(.db-nav-icon) { + display: none; + } + .db-sidebar.collapsed .db-sidebar-header { + justify-content: center; + padding: 20px 10px; + } + .db-sidebar.collapsed .db-nav-item { + justify-content: center; + padding: 12px 10px; + } + .db-sidebar.collapsed .db-nav-icon { + margin: 0; } .db-sidebar-header { padding: 20px; font-size: 15px; font-weight: 600; @@ -88,6 +121,32 @@ const DASHBOARD_STYLE = ` display: flex; align-items: center; gap: 10px; } .db-sidebar-header .logo { font-size: 20px; } + .db-sidebar-toggle { + position: absolute; + right: -12px; + top: 50%; + transform: translateY(-50%); + width: 24px; + height: 24px; + background: var(--db-sidebar-bg); + border: 1px solid var(--db-border); + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + color: var(--db-text-dim); + z-index: 10; + transition: all 0.15s; + } + .db-sidebar-toggle:hover { + background: var(--db-hover); + color: var(--db-text); + } + .db-sidebar.collapsed .db-sidebar-toggle { + transform: translateY(-50%) rotate(180deg); + } .db-group-label { padding: 18px 20px 6px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--db-text-dim); @@ -477,65 +536,85 @@ const PRESET_COLORS = { rose: { accent: '#f43f5e', accentRgb: '244,63,94' }, }; -function applyThemeFromConfig(cfg) { +async function applyThemeFromConfig(cfg) { const theme = cfg?.site?.theme; if (!theme) return; const root = document.documentElement; const preset = theme.preset || 'zinc'; let colors = PRESET_COLORS[preset]; + let customVars = {}; // If preset not in hardcoded list, fetch from /api/theme (supports custom themes) if (!colors) { - fetch('/api/theme') - .then(r => r.json()) - .then(data => { - // Apply all CSS variables from the theme API - if (data.variables) { - for (const [key, val] of Object.entries(data.variables)) { - root.style.setProperty(key, val); - } - } - }) - .catch(() => {}); + console.debug('[AIUI Theme] Custom preset detected:', preset, '- fetching from /api/theme'); + try { + const r = await fetch('/api/theme'); + const data = await r.json(); + console.debug('[AIUI Theme] Applying variables:', data.variables); + if (data.variables) { + customVars = data.variables; + } + } catch (err) { + console.error('[AIUI Theme] Fetch error:', err); + } } else { - root.style.setProperty('--db-accent', colors.accent); - root.style.setProperty('--db-accent-glow', `rgba(${colors.accentRgb},0.15)`); - root.style.setProperty('--db-accent-rgb', colors.accentRgb); + customVars['--db-accent'] = colors.accent; + customVars['--db-accent-glow'] = `rgba(${colors.accentRgb},0.15)`; + customVars['--db-accent-rgb'] = colors.accentRgb; } // Dark/light mode if (theme.darkMode === false) { - root.style.setProperty('--db-bg', '#fafafa'); - root.style.setProperty('--db-sidebar-bg', '#f4f4f5'); - root.style.setProperty('--db-text', '#18181b'); - root.style.setProperty('--db-text-dim', '#71717a'); - root.style.setProperty('--db-border', 'rgba(0,0,0,0.08)'); - root.style.setProperty('--db-card-bg', 'rgba(0,0,0,0.02)'); - root.style.setProperty('--db-hover', 'rgba(0,0,0,0.04)'); + customVars['--db-bg'] = '#fafafa'; + customVars['--db-sidebar-bg'] = '#f4f4f5'; + customVars['--db-text'] = '#18181b'; + customVars['--db-text-dim'] = '#71717a'; + customVars['--db-border'] = 'rgba(0,0,0,0.08)'; + customVars['--db-card-bg'] = 'rgba(0,0,0,0.02)'; + customVars['--db-hover'] = 'rgba(0,0,0,0.04)'; document.body.style.background = 'var(--db-bg)'; } // Radius const radiusMap = { none: '0', sm: '6px', md: '10px', lg: '14px', xl: '20px' }; if (theme.radius && radiusMap[theme.radius]) { - root.style.setProperty('--db-radius', radiusMap[theme.radius]); + customVars['--db-radius'] = radiusMap[theme.radius]; } + + // Inject via