mirror of
https://github.com/MervinPraison/PraisonAIUI.git
synced 2026-11-07 05:56:17 -10:00
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:
1 parent
c1ac4bd78a
commit
7b34608ed7
11 files changed
+839
-50
No files matched your search
@@ -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
@@ -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"}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
93373
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
Reference in new issue
Block a user