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`.
This commit is contained in:
MervinPraison authored and MervinPraison committed 2026-04-15 04:33:01 +01:00
1 parent c1ac4bd78a
commit 7b34608ed7
11 files changed
+839 -50

No files matched your search

+180
View File
@@ -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
+1 -1
View File
@@ -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"}
+1
View File
@@ -0,0 +1 @@
93373
+5 -1
View File
@@ -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",
+51 -6
View File
@@ -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",
},
}
+110
View File
@@ -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 {}),
@@ -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 <style> tag and setProperty with !important
let cssText = ':root {\n';
for (const [key, val] of Object.entries(customVars)) {
cssText += ` ${key}: ${val} !important;\n`;
root.style.setProperty(key, val, 'important');
}
cssText += '}\n';
let themeStyle = document.getElementById('aiui-theme-variables');
if (!themeStyle) {
themeStyle = document.createElement('style');
themeStyle.id = 'aiui-theme-variables';
document.head.appendChild(themeStyle);
}
themeStyle.textContent = cssText;
}
function init() {
async function init() {
// Inject styles
const style = document.createElement('style');
style.textContent = DASHBOARD_STYLE;
document.head.appendChild(style);
// Apply theme from config (async — overrides defaults once loaded)
fetch('/ui-config.json')
.then(r => r.json())
.then(cfg => applyThemeFromConfig(cfg))
.catch(() => {});
// Apply theme from config BEFORE building dashboard
try {
const cfgRes = await fetch('/ui-config.json');
const cfg = await cfgRes.json();
await applyThemeFromConfig(cfg);
} catch (e) {
console.warn('[AIUI] Failed to load theme config:', e);
}
// Build the dashboard
// Build the dashboard (after theme is applied)
buildDashboard();
}
@@ -606,17 +685,71 @@ async function buildDashboard() {
function buildSidebar(pages) {
const sidebar = document.createElement('nav');
sidebar.className = 'db-sidebar';
sidebar.style.position = 'relative'; // For toggle button positioning
// Apply initial collapsed state from config
if (sidebarConfig.defaultCollapsed) {
sidebar.classList.add('collapsed');
sidebarCollapsed = true;
}
// Header — branding from /ui-config.json (configurable via app.py or YAML)
const header = document.createElement('div');
header.className = 'db-sidebar-header';
header.innerHTML = 'PraisonAI <span class="logo">🦞</span>';
header.innerHTML = '<span>PraisonAI</span> <span class="logo">🦞</span>';
sidebar.appendChild(header);
// Toggle button (if collapsible)
if (sidebarConfig.collapsible) {
const toggle = document.createElement('button');
toggle.className = 'db-sidebar-toggle';
toggle.innerHTML = '◀';
toggle.title = 'Toggle sidebar';
toggle.addEventListener('click', () => {
sidebarCollapsed = !sidebarCollapsed;
sidebar.classList.toggle('collapsed', sidebarCollapsed);
// Save preference
try { localStorage.setItem('aiui-sidebar-collapsed', sidebarCollapsed); } catch(e) {}
});
sidebar.appendChild(toggle);
// Restore from localStorage
try {
const saved = localStorage.getItem('aiui-sidebar-collapsed');
if (saved === 'true') {
sidebar.classList.add('collapsed');
sidebarCollapsed = true;
}
} catch(e) {}
}
// Update branding from config asynchronously
fetch('/ui-config.json').then(r => r.json()).then(cfg => {
const title = cfg.site?.title || 'PraisonAI';
const logo = cfg.site?.logo || '🦞';
header.innerHTML = `${title} <span class="logo">${logo}</span>`;
header.innerHTML = `<span>${title}</span> <span class="logo">${logo}</span>`;
// Apply sidebar config from server
if (cfg.dashboard) {
if (cfg.dashboard.sidebarCollapsible === false) {
const toggleBtn = sidebar.querySelector('.db-sidebar-toggle');
if (toggleBtn) toggleBtn.style.display = 'none';
}
if (cfg.dashboard.sidebarWidth) {
sidebar.style.setProperty('--db-sidebar-w', cfg.dashboard.sidebarWidth + 'px');
}
}
// Apply brand color if set
if (cfg.site?.brandColor) {
document.documentElement.style.setProperty('--db-accent', cfg.site.brandColor, 'important');
// Also compute RGB for rgba() usage
const hex = cfg.site.brandColor.replace('#', '');
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
document.documentElement.style.setProperty('--db-accent-rgb', `${r},${g},${b}`, 'important');
}
}).catch(() => {});
// Group pages
@@ -1,3 +1,5 @@
console.log('[AIUI Loader] Starting...');
/**
* AIUI Plugin Loader
*
@@ -24,6 +24,12 @@ let currentDeltaEl = null;
let currentDeltaText = '';
let pendingAttachments = []; // { id, filename, content_type, preview_url }
// ── Floating Chat State ──────────────────────────────────────────
let chatModeConfig = { mode: 'fullpage' };
let isMinimized = false;
let isResizing = false;
let floatingChatEl = null;
// ── Helpers ──────────────────────────────────────────────────────
function escapeHtml(str) {
const div = document.createElement('div');
@@ -305,6 +311,114 @@ function injectStyles() {
.chat-attach-item .attach-remove { cursor:pointer; color:var(--db-text-dim); font-size:14px; padding:0 2px; border:none; background:none; line-height:1; }
.chat-attach-item .attach-remove:hover { color:#ef4444; }
.chat-attach-item img.attach-thumb { width:24px; height:24px; object-fit:cover; border-radius:4px; }
/* ── Floating Chat Mode ─────────────────────────────────────── */
.chat-floating-container {
position: fixed;
z-index: 1000;
display: flex;
flex-direction: column;
background: var(--db-sidebar-bg, #111118);
border: 1px solid var(--db-border);
border-radius: 16px;
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.05);
overflow: hidden;
transition: width 0.2s ease, height 0.2s ease, opacity 0.15s ease;
}
.chat-floating-container.minimized {
width: 60px !important;
height: 60px !important;
border-radius: 50%;
cursor: pointer;
}
.chat-floating-container.minimized .chat-root,
.chat-floating-container.minimized .chat-floating-header { display: none; }
.chat-floating-container.minimized .chat-floating-fab { display: flex; }
.chat-floating-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
background: var(--db-card-bg);
border-bottom: 1px solid var(--db-border);
cursor: move;
user-select: none;
}
.chat-floating-header-title {
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.chat-floating-header-actions {
display: flex;
gap: 6px;
}
.chat-floating-header-btn {
background: transparent;
border: 1px solid var(--db-border);
border-radius: 6px;
padding: 4px 8px;
cursor: pointer;
font-size: 12px;
color: var(--db-text-dim);
transition: all 0.15s;
}
.chat-floating-header-btn:hover {
background: var(--db-hover);
color: var(--db-text);
}
.chat-floating-fab {
display: none;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
font-size: 28px;
background: var(--db-accent);
color: #fff;
border-radius: 50%;
}
.chat-floating-container .chat-root {
flex: 1;
height: auto;
border: none;
border-radius: 0;
min-height: 0;
}
.chat-floating-container .chat-sidebar-panel { display: none; }
.chat-floating-container .chat-main-panel { border-left: none; }
.chat-floating-container .chat-messages-area { padding: 12px; }
.chat-floating-container .chat-compose { padding: 10px 12px; }
/* Resize handles */
.chat-resize-handle {
position: absolute;
background: transparent;
}
.chat-resize-handle-n { top: 0; left: 10px; right: 10px; height: 6px; cursor: n-resize; }
.chat-resize-handle-s { bottom: 0; left: 10px; right: 10px; height: 6px; cursor: s-resize; }
.chat-resize-handle-e { right: 0; top: 10px; bottom: 10px; width: 6px; cursor: e-resize; }
.chat-resize-handle-w { left: 0; top: 10px; bottom: 10px; width: 6px; cursor: w-resize; }
.chat-resize-handle-ne { top: 0; right: 0; width: 12px; height: 12px; cursor: ne-resize; }
.chat-resize-handle-nw { top: 0; left: 0; width: 12px; height: 12px; cursor: nw-resize; }
.chat-resize-handle-se { bottom: 0; right: 0; width: 12px; height: 12px; cursor: se-resize; }
.chat-resize-handle-sw { bottom: 0; left: 0; width: 12px; height: 12px; cursor: sw-resize; }
/* Agent selector dropdown in floating mode */
.chat-floating-agent-select {
padding: 4px 8px;
background: var(--db-card-bg);
border: 1px solid var(--db-border);
border-radius: 6px;
color: var(--db-text);
font-size: 12px;
max-width: 120px;
}
`;
document.head.appendChild(style);
}
@@ -328,10 +442,209 @@ function _getPlatformClass(platform) {
return ['telegram','slack','discord','whatsapp','signal'].includes(key) ? 'plat-' + key : 'plat-default';
}
// ── Floating Chat Helpers ────────────────────────────────────────
function createFloatingChat(config) {
const pos = config.position || [20, 20];
const size = config.size || [400, 500];
const wrapper = document.createElement('div');
wrapper.className = 'chat-floating-container' + (config.minimized ? ' minimized' : '');
wrapper.id = 'chat-floating-wrapper';
wrapper.style.cssText = `
bottom: ${pos[0]}px;
right: ${pos[1]}px;
width: ${size[0]}px;
height: ${size[1]}px;
`;
// FAB for minimized state
const fab = document.createElement('div');
fab.className = 'chat-floating-fab';
fab.innerHTML = '💬';
fab.addEventListener('click', () => toggleMinimize());
wrapper.appendChild(fab);
// Header with drag + controls
const header = document.createElement('div');
header.className = 'chat-floating-header';
header.innerHTML = `
<div class="chat-floating-header-title">
<span>💬</span>
<span>Chat</span>
<select class="chat-floating-agent-select" id="chat-floating-agent">
<option value="">Auto</option>
</select>
</div>
<div class="chat-floating-header-actions">
<button class="chat-floating-header-btn" id="chat-float-minimize" title="Minimize">−</button>
<button class="chat-floating-header-btn" id="chat-float-close" title="Close">✕</button>
</div>
`;
wrapper.appendChild(header);
// Chat content container
const chatContainer = document.createElement('div');
chatContainer.id = 'chat-floating-content';
chatContainer.style.cssText = 'flex:1;display:flex;flex-direction:column;min-height:0;';
wrapper.appendChild(chatContainer);
// Resize handles (if resizable)
if (config.resizable !== false) {
['n','s','e','w','ne','nw','se','sw'].forEach(dir => {
const handle = document.createElement('div');
handle.className = `chat-resize-handle chat-resize-handle-${dir}`;
handle.dataset.dir = dir;
wrapper.appendChild(handle);
});
setupResizeHandles(wrapper);
}
// Drag functionality
setupDrag(wrapper, header);
// Button events
header.querySelector('#chat-float-minimize').addEventListener('click', toggleMinimize);
header.querySelector('#chat-float-close').addEventListener('click', () => {
wrapper.style.display = 'none';
});
document.body.appendChild(wrapper);
floatingChatEl = wrapper;
isMinimized = config.minimized || false;
return chatContainer;
}
function toggleMinimize() {
if (!floatingChatEl) return;
isMinimized = !isMinimized;
floatingChatEl.classList.toggle('minimized', isMinimized);
}
function setupDrag(wrapper, header) {
let isDragging = false;
let startX, startY, startRight, startBottom;
header.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'SELECT') return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
startRight = parseInt(wrapper.style.right) || 20;
startBottom = parseInt(wrapper.style.bottom) || 20;
document.body.style.userSelect = 'none';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = startX - e.clientX;
const dy = startY - e.clientY;
wrapper.style.right = Math.max(0, startRight + dx) + 'px';
wrapper.style.bottom = Math.max(0, startBottom + dy) + 'px';
});
document.addEventListener('mouseup', () => {
isDragging = false;
document.body.style.userSelect = '';
});
}
function setupResizeHandles(wrapper) {
let resizeDir = null;
let startX, startY, startW, startH, startRight, startBottom;
wrapper.querySelectorAll('.chat-resize-handle').forEach(handle => {
handle.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
isResizing = true;
resizeDir = handle.dataset.dir;
startX = e.clientX;
startY = e.clientY;
startW = wrapper.offsetWidth;
startH = wrapper.offsetHeight;
startRight = parseInt(wrapper.style.right) || 20;
startBottom = parseInt(wrapper.style.bottom) || 20;
document.body.style.userSelect = 'none';
document.body.style.cursor = handle.style.cursor;
});
});
document.addEventListener('mousemove', (e) => {
if (!isResizing || !resizeDir) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
const minW = 300, minH = 350, maxW = 800, maxH = 800;
let newW = startW, newH = startH, newRight = startRight, newBottom = startBottom;
if (resizeDir.includes('e')) {
newW = Math.min(maxW, Math.max(minW, startW - dx));
newRight = startRight + dx;
}
if (resizeDir.includes('w')) {
newW = Math.min(maxW, Math.max(minW, startW + dx));
}
if (resizeDir.includes('s')) {
newH = Math.min(maxH, Math.max(minH, startH - dy));
newBottom = startBottom + dy;
}
if (resizeDir.includes('n')) {
newH = Math.min(maxH, Math.max(minH, startH + dy));
}
wrapper.style.width = newW + 'px';
wrapper.style.height = newH + 'px';
if (resizeDir.includes('e')) wrapper.style.right = Math.max(0, newRight) + 'px';
if (resizeDir.includes('s')) wrapper.style.bottom = Math.max(0, newBottom) + 'px';
});
document.addEventListener('mouseup', () => {
isResizing = false;
resizeDir = null;
document.body.style.userSelect = '';
document.body.style.cursor = '';
});
}
// ── Main render (ES module entry point) ─────────────────────────
export async function render(container) {
injectStyles();
containerRef = container;
// Fetch chat mode config
try {
const configRes = await fetch('/ui-config.json');
const config = await configRes.json();
chatModeConfig = config.chat?.mode || { mode: 'fullpage' };
} catch (e) {
chatModeConfig = { mode: 'fullpage' };
}
// If floating mode, create floating container and render into it
if (chatModeConfig.mode === 'floating') {
// Remove existing floating chat if present
const existing = document.getElementById('chat-floating-wrapper');
if (existing) existing.remove();
const floatingContainer = createFloatingChat(chatModeConfig);
containerRef = floatingContainer;
container = floatingContainer;
// Update floating agent selector when agents load
setTimeout(() => {
const floatAgentSel = document.getElementById('chat-floating-agent');
const mainAgentSel = document.getElementById('chat-agent-selector');
if (floatAgentSel && mainAgentSel) {
floatAgentSel.innerHTML = mainAgentSel.innerHTML;
floatAgentSel.addEventListener('change', (e) => {
currentAgentName = e.target.value || null;
if (mainAgentSel) mainAgentSel.value = e.target.value;
});
}
}, 500);
}
container.innerHTML = `
<div class="chat-root" id="chat-root">
+1
View File
@@ -0,0 +1 @@
{"style":"dashboard","site":{"title":"AI Chat","logo":"🤖","theme":{"preset":"teal-dark","darkMode":true,"radius":"md"},"customCss":null},"chat":{"enabled":true,"mode":{"mode":"fullpage"}},"dashboard":{"sidebar":false,"pageHeader":false},"debug":false}
Generated
+1 -1
View File
@@ -158,7 +158,7 @@ wheels = [
[[package]]
name = "aiui"
version = "0.3.101"
version = "0.3.102"
source = { editable = "." }
dependencies = [
{ name = "pydantic" },