Skip to content

Commit 4ff5073

Browse files
committed
Merge remote-tracking branch 'origin/feature/v4' into feature/improve-llm-v4
2 parents 2996387 + 25e6327 commit 4ff5073

File tree

11 files changed

+404
-95
lines changed

11 files changed

+404
-95
lines changed

src/agent/src/traits/_index.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ entries:
4141
meta:
4242
name: Time Aware
4343
type: agent.trait
44+
title: Time Aware
4445
comment: Adds current time context to agent for time-aware responses with cache stability
4546
tags:
4647
- time

src/facade/README.md

Lines changed: 97 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,20 @@ Portable iframe facade for the Wippy frontend. Serves a thin HTML shell that loa
77
1. `index.html` is served as a static file via `http.static`
88
2. On load, it fetches `GET /api/public/facade/config` to get runtime configuration
99
3. Checks `localStorage` for an auth token, redirects to `login_path` if missing
10-
4. Creates an iframe pointing to the CDN-hosted frontend bundle
11-
5. Bridges auth and routing between the host page and the iframe via `postMessage`
10+
4. Loads the Web Host bundle from CDN (`facade_url + '/module.js'`)
11+
5. Calls `initWippyApp()` with auth, feature flags, and customization config
12+
6. Shows inline loader/error UI during initialization (no external CSS dependencies)
1213

13-
API URL and WebSocket URL are derived from the `domain` requirement. If `domain` is empty, the browser falls back to `window.location`.
14+
## Derived values
15+
16+
These fields are NOT configurable via requirements — they are computed at runtime:
17+
18+
| Field | Source | Description |
19+
|---|---|---|
20+
| `api_url` | `PUBLIC_API_URL` env var | Base URL for API calls. Falls back to `window.location.origin` in the browser if empty. |
21+
| `ws_url` | Derived from `api_url` | WebSocket URL — `http://``ws://`, `https://``wss://` |
22+
| `iframe_origin` | Extracted from `fe_facade_url` | Origin portion of facade URL (e.g. `https://web-host.wippy.ai`), used for `postMessage` security |
23+
| `iframe_url` | `fe_facade_url` + `fe_entry_path` + `?waitForCustomConfig` | Full iframe URL passed to the Web Host |
1424

1525
## Requirements
1626

@@ -25,36 +35,50 @@ API URL and WebSocket URL are derived from the `domain` requirement. If `domain`
2535

2636
| Requirement | Default | Description |
2737
|---|---|---|
28-
| `domain` | _(empty)_ | Canonical app domain (e.g. `localhost:8085`, `app.wippy.ai`). Derives API and WS URLs. |
29-
| `fe_facade_url` | `https://web-host.wippy.ai/webcomponents-1.0.5` | CDN base URL for frontend bundle |
30-
| `fe_entry_path` | `/iframe.html` | Entry point path within the bundle |
38+
| `fe_facade_url` | `https://web-host.wippy.ai/webcomponents-1.0.12` | CDN base URL for the Web Host frontend bundle |
39+
| `fe_entry_path` | `/iframe.html` | Iframe HTML entry point path (appended to `fe_facade_url`) |
3140

3241
### App Identity
3342

34-
| Requirement | Default | Description |
35-
|---|---|---|
36-
| `app_title` | `Wippy` | Sidebar title |
37-
| `app_name` | `Wippy AI` | Full app name |
38-
| `app_icon` | `wippy:logo` | Iconify icon reference |
43+
Passed to the Web Host as `customization.i18n.app` — controls branding in sidebar and navigation.
44+
45+
| Requirement | Default | Config path | Description |
46+
|---|---|---|---|
47+
| `app_title` | `Wippy` | `customization.i18n.app.title` | Short title shown in sidebar header |
48+
| `app_name` | `Wippy AI` | `customization.i18n.app.appName` | Full application name |
49+
| `app_icon` | `wippy:logo` | `customization.i18n.app.icon` | Iconify icon reference (e.g. `custom:logo`, `tabler:home`) |
3950

4051
### Feature Flags
4152

42-
| Requirement | Default | Description |
43-
|---|---|---|
44-
| `show_admin` | `true` | Show admin panel and keeper controls in the sidebar |
45-
| `start_nav_open` | `false` | Navigation drawer open by default (collapsed shows icons only) |
46-
| `hide_nav_bar` | `false` | Completely hide the left navigation sidebar |
47-
| `disable_right_panel` | `false` | Disable the right sidebar panel |
48-
| `allow_select_model` | `false` | Allow LLM model selection |
49-
| `session_type` | `non-persistent` | Chat session persistence (`non-persistent` or `persistent`) |
50-
| `history_mode` | `hash` | Browser history mode (`hash` or `history`) |
53+
Passed to the Web Host as `feature.*` — control UI behavior and visibility.
54+
55+
| Requirement | Default | Type | Description |
56+
|---|---|---|---|
57+
| `show_admin` | `true` | bool (`~= "false"`) | Show admin panel and keeper controls in the sidebar |
58+
| `start_nav_open` | `false` | bool (`== "true"`) | Navigation drawer open by default (collapsed shows icons only) |
59+
| `hide_nav_bar` | `false` | bool (`== "true"`) | Completely hide the left navigation sidebar |
60+
| `disable_right_panel` | `false` | bool (`== "true"`) | Disable the right sidebar panel |
61+
| `allow_select_model` | `false` | bool (`== "true"`) | Allow LLM model selection dropdown in chat |
62+
| `session_type` | `non-persistent` | string | Chat session persistence (`non-persistent` or `cookie`) |
63+
| `history_mode` | `hash` | string | Browser history mode (`hash` or `history`/`browser`) |
64+
65+
> **Boolean parsing:** `show_admin` defaults to `true` (any value except `"false"` is truthy). All other boolean flags default to `false` (only `"true"` is truthy). This is implemented in `config_handler.lua`.
5166
52-
### Auth & Theming
67+
### Auth
5368

5469
| Requirement | Default | Description |
5570
|---|---|---|
56-
| `login_path` | `/login.html` | Unauthenticated redirect path |
57-
| `custom_css` | Poppins font import | Custom CSS injected into iframe config |
71+
| `login_path` | `/login.html` | Path to redirect unauthenticated users (no token in localStorage) |
72+
73+
### Theming & Customization
74+
75+
Passed to the Web Host as `customization.*` — control visual appearance across all host-rendered pages and web components.
76+
77+
| Requirement | Default | Config path | Description |
78+
|---|---|---|---|
79+
| `custom_css` | Poppins font `@import` | `customization.custom_css` | Raw CSS string injected as `<style>` into host and child iframes. Use for font imports, body font-family, and custom rules. |
80+
| `css_variables` | `{}` | `customization.css_variables` | JSON object of CSS custom properties (e.g. `{"--p-primary":"#6366f1"}`). Injected as `:root { --key: value; }` overrides. Supports `@dark` and `@light` variants. |
81+
| `icons` | `{}` | `customization.icons` | JSON object of custom Iconify icons (e.g. `{"logo":{"body":"<svg>...</svg>","width":24,"height":24}}`). Registered under `custom:` prefix — use as `custom:logo`. |
5882

5983
## Usage
6084

@@ -68,13 +92,11 @@ API URL and WebSocket URL are derived from the `domain` requirement. If `domain`
6892
value: app:gateway
6993
- name: router
7094
value: app:api.public
71-
- name: domain
72-
value: localhost:8085
7395
```
7496
7597
Only override what differs from defaults.
7698
77-
### Page-only app (no chat sidebar)
99+
### Themed page-only app (no chat sidebar)
78100
79101
```yaml
80102
- name: hide_nav_bar
@@ -83,6 +105,19 @@ Only override what differs from defaults.
83105
value: "true"
84106
- name: show_admin
85107
value: "false"
108+
- name: custom_css
109+
value: "@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); body { font-family: 'Inter', sans-serif; }"
110+
- name: css_variables
111+
value: '{"--p-primary":"#6366f1"}'
112+
```
113+
114+
### Custom icons
115+
116+
```yaml
117+
- name: icons
118+
value: '{"logo":{"body":"<svg viewBox=\"0 0 24 24\"><path d=\"M12 2L2 22h20L12 2z\"/></svg>","width":24,"height":24}}'
119+
- name: app_icon
120+
value: "custom:logo"
86121
```
87122
88123
## Config Response
@@ -91,30 +126,56 @@ Only override what differs from defaults.
91126

92127
```json
93128
{
94-
"facade_url": "https://web-host.wippy.ai/webcomponents-1.0.5",
129+
"facade_url": "https://web-host.wippy.ai/webcomponents-1.0.12",
95130
"iframe_origin": "https://web-host.wippy.ai",
96-
"iframe_url": "https://web-host.wippy.ai/webcomponents-1.0.5/iframe.html?waitForCustomConfig",
97-
"api_url": "http://localhost:8086",
98-
"ws_url": "ws://localhost:8086",
131+
"iframe_url": "https://web-host.wippy.ai/webcomponents-1.0.12/iframe.html?waitForCustomConfig",
132+
"api_url": "http://localhost:8085",
133+
"ws_url": "ws://localhost:8085",
99134
"feature": {
100135
"session_type": "non-persistent",
101136
"history_mode": "hash",
102-
"show_admin": false,
137+
"show_admin": true,
103138
"allow_select_model": false,
104139
"start_nav_open": false,
105140
"hide_nav_bar": false,
106141
"disable_right_panel": false
107142
},
108143
"customization": {
109-
"custom_css": "...",
110-
"css_variables": [],
111-
"i18n": { "app": { "title": "...", "icon": "...", "appName": "..." } },
112-
"icons": []
144+
"custom_css": "@import url('https://fonts.googleapis.com/css2?family=Poppins...');",
145+
"css_variables": {},
146+
"i18n": { "app": { "title": "Wippy", "icon": "wippy:logo", "appName": "Wippy AI" } },
147+
"icons": {}
113148
},
114-
"login_path": "/api/public/auth/login"
149+
"login_path": "/login.html"
115150
}
116151
```
117152

153+
### How `index.html` uses the config
154+
155+
```
156+
fetch /api/public/facade/config
157+
→ check localStorage for auth token → redirect to login_path if missing
158+
→ import(facade_url + '/module.js') → load Web Host bundle from CDN
159+
→ initWippyApp({ auth, feature, customization }, '#app')
160+
→ listen for 'authExpired' and 'error' events → redirect to login_path
161+
```
162+
163+
If any step fails (config fetch, CDN import, missing `initWippyApp`), the page shows a styled error message with a Retry button. No external CSS is required for the loader/error UI.
164+
165+
## Web Host options not exposed by facade
166+
167+
The Web Host (`initWippyApp`) supports additional options that are intentionally not wired as facade requirements because they are advanced or context-specific:
168+
169+
- `hideSessionSelector` — hide session picker dropdown
170+
- `apiRoutes` — override API endpoint paths
171+
- `axiosDefaults` — custom HTTP client defaults
172+
- `routePrefix` — prefix for internal route links (facade passes `api_url` as `routePrefix`)
173+
- `chat.convertPasteToFile` — auto-convert pasted content to file uploads
174+
- `allowAdditionalTags` — HTML sanitizer tag whitelist
175+
- `externalEvents` — cross-origin event bridging
176+
177+
These can be set by directly modifying `index.html` or by creating a custom facade entry.
178+
118179
## Publishing
119180
120181
```bash

src/facade/_index.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ entries:
3333
targets:
3434
- entry: wippy.facade:fe_facade_url
3535
path: .default
36-
default: https://web-host.wippy.ai/webcomponents-1.0.5
36+
default: https://web-host.wippy.ai/webcomponents-1.0.12
3737

3838
- name: fe_entry_path
3939
kind: ns.requirement

src/facade/config_handler_test.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ local function define_tests()
9393
end)
9494

9595
test.it("extracts iframe origin from facade URL", function()
96-
local facade_url = "https://web-host.wippy.ai/webcomponents-1.0.5"
96+
local facade_url = "https://web-host.wippy.ai/webcomponents-1.0.12"
9797
local origin = facade_url:match("^(https?://[^/]+)")
9898

9999
test.eq(origin, "https://web-host.wippy.ai")

src/facade/public/index.html

Lines changed: 104 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,127 @@
11
<!DOCTYPE html>
2-
<html lang="en" class="dark">
2+
<html lang="en">
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta name="color-scheme" content="light dark">
67
<title>Wippy</title>
78
<style>
8-
html, body { padding: 0; margin: 0; }
9+
.facade-loader {
10+
display: flex; flex-direction: column; align-items: center; justify-content: center;
11+
height: 100vh; gap: 16px; font-family: system-ui, -apple-system, sans-serif;
12+
}
13+
.facade-loader__spinner {
14+
width: 28px; height: 28px;
15+
border: 2.5px solid rgba(128,128,128,0.15);
16+
border-top-color: rgba(128,128,128,0.5);
17+
border-radius: 50%;
18+
animation: facade-spin 0.7s linear infinite;
19+
}
20+
.facade-loader__text { font-size: 13px; color: rgba(128,128,128,0.6); }
21+
.facade-error {
22+
display: flex; flex-direction: column; align-items: center; justify-content: center;
23+
height: 100vh; gap: 12px; padding: 24px; text-align: center;
24+
font-family: system-ui, -apple-system, sans-serif;
25+
}
26+
.facade-error__title { font-size: 15px; font-weight: 600; }
27+
.facade-error__detail { font-size: 13px; opacity: 0.6; max-width: 420px; word-break: break-word; }
28+
.facade-error__retry {
29+
margin-top: 8px; padding: 6px 18px; font-size: 13px;
30+
background: rgba(128,128,128,0.1); border: 1px solid rgba(128,128,128,0.2);
31+
border-radius: 6px; cursor: pointer; color: inherit;
32+
}
33+
.facade-error__retry:hover { background: rgba(128,128,128,0.18); }
34+
@keyframes facade-spin { to { transform: rotate(360deg); } }
935
</style>
1036
</head>
1137
<body>
12-
<div id="app">Loading...</div>
38+
<div id="app">
39+
<div class="facade-loader">
40+
<div class="facade-loader__spinner"></div>
41+
<div class="facade-loader__text">Loading...</div>
42+
</div>
43+
</div>
1344
<script type="module">
1445
const STORAGE_KEY = '@wippy_token_info';
46+
const appEl = document.getElementById('app');
1547

16-
const res = await fetch('/api/public/facade/config');
17-
if (!res.ok) { document.body.textContent = 'Failed to load config'; throw new Error('config fetch failed'); }
18-
const cfg = await res.json();
48+
function showError(title, detail) {
49+
appEl.innerHTML =
50+
'<div class="facade-error">' +
51+
'<div class="facade-error__title">' + title + '</div>' +
52+
(detail ? '<div class="facade-error__detail">' + detail + '</div>' : '') +
53+
'<button class="facade-error__retry" onclick="location.reload()">Retry</button>' +
54+
'</div>';
55+
}
1956

20-
const stored = localStorage.getItem(STORAGE_KEY);
21-
if (!stored) { window.location.href = cfg.login_path || '/login.html'; throw new Error('no token'); }
57+
try {
58+
const res = await fetch('/api/public/facade/config');
59+
if (!res.ok) {
60+
showError('Failed to load configuration', 'Server returned ' + res.status + '. Check that the backend is running.');
61+
throw new Error('config fetch failed: ' + res.status);
62+
}
63+
const cfg = await res.json();
2264

23-
let token;
24-
try { token = JSON.parse(stored).token; }
25-
catch { window.location.href = cfg.login_path || '/login.html'; throw new Error('bad token'); }
65+
const stored = localStorage.getItem(STORAGE_KEY);
66+
if (!stored) { window.location.href = cfg.login_path || '/login.html'; throw new Error('no token'); }
2667

27-
const apiUrl = cfg.api_url || window.location.origin;
28-
const wsProto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
29-
const wsUrl = cfg.ws_url || (wsProto + '//' + window.location.host);
68+
let token;
69+
try { token = JSON.parse(stored).token; }
70+
catch { window.location.href = cfg.login_path || '/login.html'; throw new Error('bad token'); }
3071

31-
await import(cfg.facade_url + '/module.js');
72+
const apiUrl = cfg.api_url || window.location.origin;
73+
const wsProto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
74+
const wsUrl = cfg.ws_url || (wsProto + '//' + window.location.host);
3275

33-
const events = window.initWippyApp({
34-
auth: { token, expiresAt: new Date(Date.now() + 86400000).toISOString() },
35-
feature: {
36-
session: { type: cfg.feature.session_type },
37-
history: cfg.feature.history_mode,
38-
env: { APP_API_URL: apiUrl, APP_WEBSOCKET_URL: wsUrl },
39-
routePrefix: apiUrl,
40-
showAdmin: cfg.feature.show_admin,
41-
allowSelectModel: cfg.feature.allow_select_model,
42-
startNavOpen: cfg.feature.start_nav_open,
43-
hideNavBar: cfg.feature.hide_nav_bar,
44-
disableRightPanel: cfg.feature.disable_right_panel,
45-
},
46-
customization: {
47-
customCSS: cfg.customization.custom_css || '',
48-
cssVariables: cfg.customization.css_variables || {},
49-
i18n: cfg.customization.i18n || {},
50-
icons: cfg.customization.icons || {},
51-
},
52-
}, '#app');
76+
let mod;
77+
try {
78+
mod = await import(cfg.facade_url + '/module.js');
79+
} catch (importErr) {
80+
showError('Failed to load frontend bundle', 'Could not load ' + cfg.facade_url + '/module.js — ' + importErr.message);
81+
throw importErr;
82+
}
5383

54-
const loginPath = cfg.login_path || '/login.html';
84+
if (typeof window.initWippyApp !== 'function') {
85+
showError('Frontend bundle error', 'initWippyApp not found after loading module. The bundle may be incompatible.');
86+
throw new Error('initWippyApp not found');
87+
}
5588

56-
// Handle logout and session expiration — clean up token and redirect to login
57-
events.on('authExpired', () => {
58-
localStorage.removeItem(STORAGE_KEY);
59-
window.location.href = loginPath;
60-
});
89+
const events = window.initWippyApp({
90+
auth: { token, expiresAt: new Date(Date.now() + 86400000).toISOString() },
91+
feature: {
92+
session: { type: cfg.feature.session_type },
93+
history: cfg.feature.history_mode,
94+
env: { APP_API_URL: apiUrl, APP_WEBSOCKET_URL: wsUrl },
95+
routePrefix: apiUrl,
96+
showAdmin: cfg.feature.show_admin,
97+
allowSelectModel: cfg.feature.allow_select_model,
98+
startNavOpen: cfg.feature.start_nav_open,
99+
hideNavBar: cfg.feature.hide_nav_bar,
100+
disableRightPanel: cfg.feature.disable_right_panel,
101+
},
102+
customization: {
103+
customCSS: cfg.customization.custom_css || '',
104+
cssVariables: cfg.customization.css_variables || {},
105+
i18n: cfg.customization.i18n || {},
106+
icons: cfg.customization.icons || {},
107+
},
108+
}, '#app');
61109

62-
// Handle critical errors
63-
events.on('error', (err) => {
64-
console.error('Wippy critical error:', err);
65-
localStorage.removeItem(STORAGE_KEY);
66-
window.location.href = loginPath;
67-
});
110+
const loginPath = cfg.login_path || '/login.html';
111+
112+
events.on('authExpired', () => {
113+
localStorage.removeItem(STORAGE_KEY);
114+
window.location.href = loginPath;
115+
});
116+
117+
events.on('error', (err) => {
118+
console.error('Wippy critical error:', err);
119+
localStorage.removeItem(STORAGE_KEY);
120+
window.location.href = loginPath;
121+
});
122+
} catch (err) {
123+
console.error('Facade initialization failed:', err);
124+
}
68125
</script>
69126
</body>
70127
</html>

0 commit comments

Comments
 (0)