mirror of
https://github.com/MervinPraison/PraisonAIUI.git
synced 2026-11-07 05:56:17 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78493e0e17 | ||
|
|
bf5415555b | ||
|
|
e69a06c2d5 | ||
|
|
085bdd3793 | ||
|
|
93abaac212 | ||
|
|
f2ecc8e39e | ||
|
|
e86b187246 | ||
|
|
1255f44105 | ||
|
|
7002e956fb |
No files matched your search
@@ -27,7 +27,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].19
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].20
|
||||
secrets: inherit
|
||||
with:
|
||||
hub_ref: v1.1.19
|
||||
hub_ref: v1.1.20
|
||||
@@ -16,8 +16,8 @@ permissions:
|
||||
|
||||
jobs:
|
||||
recover:
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].19
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].20
|
||||
secrets: inherit
|
||||
with:
|
||||
hub_ref: v1.1.19
|
||||
hub_ref: v1.1.20
|
||||
pr_number: ${{ github.event.inputs.pr_number || '' }}
|
||||
@@ -25,8 +25,8 @@ permissions:
|
||||
|
||||
jobs:
|
||||
detect-and-trigger:
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].19
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].20
|
||||
secrets: inherit
|
||||
with:
|
||||
hub_ref: v1.1.19
|
||||
hub_ref: v1.1.20
|
||||
pr_number: ${{ github.event.inputs.pr_number || '' }}
|
||||
@@ -43,9 +43,9 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].19
|
||||
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].20
|
||||
with:
|
||||
hub_ref: v1.1.19
|
||||
hub_ref: v1.1.20
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
@@ -88,9 +88,9 @@ jobs:
|
||||
actions: read
|
||||
id-token: write
|
||||
statuses: write # the assess job records security/outsider-review
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].19
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].20
|
||||
secrets: inherit
|
||||
with:
|
||||
hub_ref: v1.1.19
|
||||
hub_ref: v1.1.20
|
||||
pr_number: ${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || '' }}
|
||||
merge_method: ${{ github.event.inputs.merge_method || 'merge' }}
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
issues: write
|
||||
actions: read
|
||||
id-token: write
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].19
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].20
|
||||
secrets: inherit
|
||||
with:
|
||||
hub_ref: v1.1.19
|
||||
hub_ref: v1.1.20
|
||||
@@ -25,9 +25,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].19
|
||||
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].20
|
||||
with:
|
||||
hub_ref: v1.1.19
|
||||
hub_ref: v1.1.20
|
||||
- name: Detect conflicting PRs and trigger Claude
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
|
||||
@@ -13,7 +13,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].19
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].20
|
||||
secrets: inherit
|
||||
with:
|
||||
hub_ref: v1.1.19
|
||||
hub_ref: v1.1.20
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "aiui"
|
||||
version = "0.3.149"
|
||||
version = "0.3.151"
|
||||
description = "YAML-driven website generator - CLI and compiler"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
|
||||
Generated
+87
-23
@@ -82,20 +82,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@a2ui/markdown-it": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@a2ui/markdown-it/-/markdown-it-0.0.4.tgz",
|
||||
"integrity": "sha512-uaYNywdiHDH4i//pHpyckFnApvOshiUNT2Xl8awceNjL1XAX/gqtki9GRUJQ26RE7id2+aPNgOy1W6mXNBxriQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"dompurify": "^3.3.1",
|
||||
"markdown-it": "^14.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@a2ui/web_core": "^0.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@a2ui/react": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@a2ui/react/-/react-0.10.0.tgz",
|
||||
@@ -129,16 +115,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@a2ui/web_core": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@a2ui/web_core/-/web_core-0.10.0.tgz",
|
||||
"integrity": "sha512-pSieKxaB6aZWBL2LjZfKo4/1v4xMWylczpzte1ehuGH0kfNGf/iTd73PKsIKYx3b0FNWTbD8gAloOM4JCVqUig==",
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@a2ui/web_core/-/web_core-0.10.2.tgz",
|
||||
"integrity": "sha512-IfS4K7baTL9P9AzUOzKRWzL9A9/+Z7TCgMY2GPwsZ0H7LgY/9/uJnlbReNhpHsAfN12KZtW9iERWkw5HOf71GA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@preact/signals-core": "^1.13.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"@preact/signals-core": "^1.14.2",
|
||||
"date-fns": "^4.4.0",
|
||||
"zod": "^3.25.76",
|
||||
"zod-to-json-schema": "^3.25.1"
|
||||
"zod-to-json-schema": "^3.25.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@a2ui/web_core/node_modules/zod": {
|
||||
@@ -5502,9 +5488,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz",
|
||||
"integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -9747,6 +9733,84 @@
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/@a2ui/react/node_modules/@a2ui/markdown-it": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@a2ui/markdown-it/-/markdown-it-0.0.4.tgz",
|
||||
"integrity": "sha512-uaYNywdiHDH4i//pHpyckFnApvOshiUNT2Xl8awceNjL1XAX/gqtki9GRUJQ26RE7id2+aPNgOy1W6mXNBxriQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"dompurify": "^3.3.1",
|
||||
"markdown-it": "^14.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@a2ui/web_core": "^0.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.8.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.1.0",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.8.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.1.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1",
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
"""Version information."""
|
||||
|
||||
__version__ = "0.3.149"
|
||||
__version__ = "0.3.151"
|
||||
@@ -1303,9 +1303,11 @@ async def patch_session(request: Request) -> JSONResponse:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
return JSONResponse({"error": "Invalid JSON"}, status_code=400)
|
||||
title = body.get("title")
|
||||
title = body.get("title") if isinstance(body, dict) else None
|
||||
if title is not None:
|
||||
await _datastore.update_session(session_id, title=title)
|
||||
if not isinstance(title, str) or not title.strip():
|
||||
return JSONResponse({"error": "title must be a non-empty string"}, status_code=400)
|
||||
await _datastore.update_session(session_id, title=title.strip()[:200])
|
||||
return JSONResponse({"status": "updated", "session_id": session_id})
|
||||
|
||||
|
||||
@@ -3298,6 +3300,7 @@ def create_app(
|
||||
"group": "Agent",
|
||||
"description": "AI agent chat",
|
||||
"order": 1,
|
||||
"layout": "full-bleed",
|
||||
},
|
||||
{
|
||||
"id": "chat-canvas",
|
||||
@@ -3306,6 +3309,7 @@ def create_app(
|
||||
"group": "Agent",
|
||||
"description": "Chat with live A2UI preview panel",
|
||||
"order": 2,
|
||||
"layout": "full-bleed",
|
||||
},
|
||||
{
|
||||
"id": "canvas",
|
||||
|
||||
@@ -378,6 +378,9 @@ const DASHBOARD_STYLE = `
|
||||
.db-main { flex: 1; padding: 32px 40px; overflow-y: auto; min-width: 0; }
|
||||
.db-main.db-clean { padding: 0; display: flex; flex-direction: column; overflow: hidden; height: 100vh; }
|
||||
.db-main.db-clean > #db-page-content { flex: 1; display: flex; flex-direction: column; min-height: 0; }
|
||||
/* Per-page full bleed (page.layout === 'full-bleed'): view fills the main column. */
|
||||
.db-main.db-fill { display: flex; flex-direction: column; overflow: hidden; height: 100vh; box-sizing: border-box; }
|
||||
.db-main.db-fill > #db-page-content { flex: 1; display: flex; flex-direction: column; min-height: 0; }
|
||||
.db-page-header { margin-bottom: 28px; }
|
||||
.db-page-title { font-size: 26px; font-weight: 700; letter-spacing: -0.02em; margin: 0 0 4px; }
|
||||
.db-page-icon svg, .db-nav-icon svg { display: inline-block; vertical-align: -0.125em; }
|
||||
@@ -1992,8 +1995,12 @@ async function selectPage(pageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Full-bleed pages (e.g. chat) fill the main column and carry their own chrome
|
||||
const fullBleed = page.layout === 'full-bleed';
|
||||
main.classList.toggle('db-fill', fullBleed);
|
||||
|
||||
// Set page header (controlled by dashboard config protocol)
|
||||
const showPageHeader = main.dataset.showPageHeader !== 'false';
|
||||
const showPageHeader = main.dataset.showPageHeader !== 'false' && !fullBleed;
|
||||
const toolbarSlotId = `page:${pageId}:toolbar`;
|
||||
const headerHtml = showPageHeader ? `
|
||||
<div class="db-page-header">
|
||||
|
||||
@@ -164,6 +164,20 @@ function renderProgressBar(percentage) {
|
||||
`;
|
||||
}
|
||||
|
||||
function escapeAttr(value) {
|
||||
return String(value).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
// Navigate to Chat and load the job's linked session (same event the
|
||||
// session-search palette and run recorder use).
|
||||
function openJobSession(sessionId) {
|
||||
if (!sessionId) return;
|
||||
const nav = (typeof window.aiui?.selectPage === 'function') ? window.aiui.selectPage('chat') : null;
|
||||
Promise.resolve(nav).finally(() => {
|
||||
window.dispatchEvent(new CustomEvent('aiui:session-select', { detail: { sessionId } }));
|
||||
});
|
||||
}
|
||||
|
||||
function renderJobRow(job) {
|
||||
const status = job.status || 'queued';
|
||||
const isTerminal = ['succeeded', 'failed', 'cancelled'].includes(status);
|
||||
@@ -199,6 +213,7 @@ function renderJobRow(job) {
|
||||
` : `
|
||||
${_jobsCfg.backend !== 'praisonai' ? `<button class="aiui-btn aiui-btn-sm aiui-btn-delete" onclick="window.aiuiDeleteJob('${job.id}')" title="Delete">🗑️</button>` : ''}
|
||||
`}
|
||||
${job.session_id ? `<button class="aiui-btn aiui-btn-sm aiui-btn-open-session" data-session-id="${escapeAttr(job.session_id)}" onclick="window.aiuiOpenJobSession(this.dataset.sessionId)" title="Open session in Chat">💬</button>` : ''}
|
||||
<button class="aiui-btn aiui-btn-sm aiui-btn-view" onclick="window.aiuiViewJob('${job.id}')" title="View Details">
|
||||
👁️
|
||||
</button>
|
||||
@@ -599,6 +614,7 @@ function checkForJobsPage(root) {
|
||||
window.aiuiCancelJob = cancelJob;
|
||||
window.aiuiDeleteJob = deleteJob;
|
||||
window.aiuiViewJob = viewJob;
|
||||
window.aiuiOpenJobSession = openJobSession;
|
||||
|
||||
export default {
|
||||
name: 'jobs',
|
||||
|
||||
@@ -21,6 +21,7 @@ function injectStyles() {
|
||||
overflow: hidden;
|
||||
}
|
||||
.db-clean .chat-canvas-root { height: 100vh; }
|
||||
.db-fill .chat-canvas-root { flex: 1; min-height: 0; height: auto; }
|
||||
.chat-canvas-chat {
|
||||
flex: 1 1 60%;
|
||||
min-width: 0;
|
||||
|
||||
@@ -19,6 +19,7 @@ let isStreaming = false;
|
||||
let streamingSessionId = null; // tracks which session is actively streaming
|
||||
let currentRunId = null;
|
||||
let agents = [];
|
||||
let allSessions = []; // last fetched session list, filtered client-side by the rail search
|
||||
let messageQueue = [];
|
||||
let focusMode = false;
|
||||
let sessionSelectHandler = null;
|
||||
@@ -221,6 +222,11 @@ function injectStyles() {
|
||||
style.textContent = `
|
||||
.chat-root { display:flex; height:calc(100vh - 140px); gap:0; border-radius:12px; overflow:hidden; border:1px solid var(--db-border); background:var(--db-card-bg); }
|
||||
.db-clean .chat-root { height:100%; border-radius:0; border:none; }
|
||||
.db-fill .chat-root { flex:1; min-height:0; height:auto; }
|
||||
@media (max-width:900px) {
|
||||
.db-fill .chat-root { flex-direction:column; }
|
||||
.db-fill .chat-sidebar-panel { width:100%; min-width:0; max-height:40vh; border-right:none; border-bottom:1px solid var(--db-border); }
|
||||
}
|
||||
.chat-root.focus-mode .chat-sidebar-panel { display:none; }
|
||||
.chat-root.focus-mode .chat-main-panel { border-left:none; }
|
||||
|
||||
@@ -230,12 +236,17 @@ function injectStyles() {
|
||||
.chat-sidebar-agent { padding:10px 16px; border-bottom:1px solid var(--db-border); }
|
||||
.chat-sidebar-agent label { font-size:11px; color:var(--db-text-dim); display:block; margin-bottom:4px; text-transform:uppercase; font-weight:600; letter-spacing:.5px; }
|
||||
.chat-sidebar-agent select { width:100%; padding:6px 10px; background:var(--db-card-bg); border:1px solid var(--db-border); border-radius:6px; color:var(--db-text); font-size:13px; }
|
||||
.chat-sess-search { padding:8px 16px 0; }
|
||||
.chat-sess-search input { width:100%; box-sizing:border-box; padding:6px 10px; background:var(--db-card-bg); border:1px solid var(--db-border); border-radius:6px; color:var(--db-text); font-size:13px; }
|
||||
.chat-sess-list { flex:1; overflow-y:auto; padding:8px; }
|
||||
.chat-sess-item { padding:10px 12px; border-radius:8px; cursor:pointer; margin-bottom:4px; font-size:13px; transition:background .15s; display:flex; flex-direction:column; gap:2px; }
|
||||
.chat-sess-item:hover { background:rgba(var(--db-accent-rgb,100,100,255),.08); }
|
||||
.chat-sess-item.active { background:rgba(var(--db-accent-rgb,100,100,255),.15); }
|
||||
.chat-sess-item .sess-title { font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
.chat-sess-item .sess-meta { font-size:11px; color:var(--db-text-dim); }
|
||||
.chat-sess-item { position:relative; }
|
||||
.chat-sess-item .sess-rename { position:absolute; top:6px; right:6px; display:none; background:none; border:none; color:var(--db-text-dim); cursor:pointer; font-size:12px; padding:2px 4px; }
|
||||
.chat-sess-item:hover .sess-rename, .chat-sess-item .sess-rename:focus { display:block; }
|
||||
|
||||
.chat-main-panel { flex:1; display:flex; flex-direction:column; min-width:0; }
|
||||
.chat-top-bar { padding:10px 16px; border-bottom:1px solid var(--db-border); display:flex; align-items:center; justify-content:space-between; gap:10px; }
|
||||
@@ -812,6 +823,9 @@ export async function render(container) {
|
||||
<label>Agent</label>
|
||||
<select id="chat-agent-selector"><option value="">Auto (default)</option></select>
|
||||
</div>
|
||||
<div class="chat-sess-search">
|
||||
<input type="search" id="chat-session-search" placeholder="Search sessions…" aria-label="Search sessions" autocomplete="off">
|
||||
</div>
|
||||
<div class="chat-sess-list" id="chat-sessions-list"></div>
|
||||
</div>
|
||||
<div class="chat-main-panel">
|
||||
@@ -892,6 +906,8 @@ export async function render(container) {
|
||||
const agentSelector = document.getElementById('chat-agent-selector');
|
||||
const attachBtn = document.getElementById('chat-attach-btn');
|
||||
const fileInput = document.getElementById('chat-file-input');
|
||||
const sessionSearch = document.getElementById('chat-session-search');
|
||||
if (sessionSearch) sessionSearch.addEventListener('input', renderSessionList);
|
||||
// Guard: if DOM elements aren't present yet (race condition), bail out
|
||||
if (!input || !sendBtn) {
|
||||
console.warn('[chat] DOM elements not ready — skipping event binding');
|
||||
@@ -1039,47 +1055,92 @@ async function loadSessions() {
|
||||
resp = await fetch('/sessions');
|
||||
}
|
||||
const data = await resp.json();
|
||||
const sessions = data.sessions || data || [];
|
||||
const list = document.getElementById('chat-sessions-list');
|
||||
if (!list) return;
|
||||
list.innerHTML = '';
|
||||
if (sessions.length === 0) {
|
||||
list.innerHTML = '<div style="padding:16px;text-align:center;color:var(--db-text-dim);font-size:12px">No sessions yet</div>';
|
||||
return;
|
||||
}
|
||||
sessions.slice(0, 30).forEach((s) => {
|
||||
const el = document.createElement('div');
|
||||
const id = s.id || s.session_id || '';
|
||||
el.className = 'chat-sess-item' + (id === currentSessionId ? ' active' : '');
|
||||
// Show platform icon for channel sessions (e.g. Slack with SVG)
|
||||
const platformName = s.platform || '';
|
||||
const platSvg = _getPlatformSVG(platformName);
|
||||
let title;
|
||||
let titlePrefix = '';
|
||||
if (platformName) {
|
||||
const displayName = platformName.charAt(0).toUpperCase() + platformName.slice(1);
|
||||
if (platSvg) {
|
||||
titlePrefix = '<span class="sess-platform-icon ' + _getPlatformClass(platformName) + '">' + platSvg + '</span>';
|
||||
}
|
||||
title = displayName;
|
||||
} else {
|
||||
title = s.name || s.title || s.label || id.substring(0, 12) + '…';
|
||||
}
|
||||
const agent = s.agent_id || s.agent || '';
|
||||
const time = timeAgo(s.created_at || s.updated_at);
|
||||
el.innerHTML = `
|
||||
<div class="sess-title">${titlePrefix}${escapeHtml(title)}</div>
|
||||
<div class="sess-meta">${agent ? '🤖 ' + escapeHtml(agent) + ' · ' : ''}${time}</div>
|
||||
`;
|
||||
el.title = id;
|
||||
el.addEventListener('click', () => loadSession(id));
|
||||
list.appendChild(el);
|
||||
});
|
||||
const fetched = data.sessions || data || [];
|
||||
allSessions = Array.isArray(fetched) ? fetched : [];
|
||||
renderSessionList();
|
||||
} catch (e) {
|
||||
console.warn('[Chat] Failed to load sessions:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function _sessionMatches(s, q) {
|
||||
const fields = [s.id, s.session_id, s.name, s.title, s.label, s.agent_id, s.agent, s.platform];
|
||||
return fields.some((v) => typeof v === 'string' && v.toLowerCase().includes(q));
|
||||
}
|
||||
|
||||
function renderSessionList() {
|
||||
const list = document.getElementById('chat-sessions-list');
|
||||
if (!list) return;
|
||||
const input = document.getElementById('chat-session-search');
|
||||
const q = (input && input.value || '').trim().toLowerCase();
|
||||
const sessions = q ? allSessions.filter((s) => _sessionMatches(s, q)) : allSessions;
|
||||
list.innerHTML = '';
|
||||
if (sessions.length === 0) {
|
||||
const msg = q ? 'No matching sessions' : 'No sessions yet';
|
||||
list.innerHTML = '<div style="padding:16px;text-align:center;color:var(--db-text-dim);font-size:12px">' + msg + '</div>';
|
||||
return;
|
||||
}
|
||||
sessions.slice(0, 30).forEach((s) => {
|
||||
const el = document.createElement('div');
|
||||
const id = s.id || s.session_id || '';
|
||||
el.className = 'chat-sess-item' + (id === currentSessionId ? ' active' : '');
|
||||
// Show platform icon for channel sessions (e.g. Slack with SVG)
|
||||
const platformName = s.platform || '';
|
||||
const platSvg = _getPlatformSVG(platformName);
|
||||
let title;
|
||||
let titlePrefix = '';
|
||||
if (platformName) {
|
||||
const displayName = platformName.charAt(0).toUpperCase() + platformName.slice(1);
|
||||
if (platSvg) {
|
||||
titlePrefix = '<span class="sess-platform-icon ' + _getPlatformClass(platformName) + '">' + platSvg + '</span>';
|
||||
}
|
||||
title = displayName;
|
||||
} else {
|
||||
title = s.name || s.title || s.label || id.substring(0, 12) + '…';
|
||||
}
|
||||
const agent = s.agent_id || s.agent || '';
|
||||
const time = timeAgo(s.created_at || s.updated_at);
|
||||
el.innerHTML = `
|
||||
<div class="sess-title">${titlePrefix}${escapeHtml(title)}</div>
|
||||
<div class="sess-meta">${agent ? '🤖 ' + escapeHtml(agent) + ' · ' : ''}${time}</div>
|
||||
`;
|
||||
el.title = id;
|
||||
el.addEventListener('click', () => loadSession(id));
|
||||
if (!platformName && id) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'sess-rename';
|
||||
btn.title = 'Rename session';
|
||||
btn.setAttribute('aria-label', 'Rename session');
|
||||
btn.textContent = '✎';
|
||||
btn.addEventListener('click', (e) => { e.stopPropagation(); renameSession(id, title); });
|
||||
el.appendChild(btn);
|
||||
}
|
||||
list.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
async function renameSession(sessionId, currentTitle) {
|
||||
const next = (window.prompt('Rename session', currentTitle || '') || '').trim();
|
||||
if (!next || next === currentTitle) return;
|
||||
try {
|
||||
const resp = await fetch('/sessions/' + encodeURIComponent(sessionId), {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title: next }),
|
||||
});
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
const s = allSessions.find((x) => (x.id || x.session_id) === sessionId);
|
||||
if (s) { s.title = next; delete s.name; }
|
||||
renderSessionList();
|
||||
if (sessionId === currentSessionId) {
|
||||
const header = document.getElementById('chat-header-title');
|
||||
if (header) header.textContent = next;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Chat] Failed to rename session:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSession(sessionId) {
|
||||
currentSessionId = sessionId;
|
||||
refreshStreamingUI();
|
||||
@@ -1131,7 +1192,8 @@ async function loadSession(sessionId) {
|
||||
title.textContent = (sessionIcon || '📨') + ' ' + displayName;
|
||||
}
|
||||
} else if (title) {
|
||||
title.textContent = 'Session ' + sessionId.substring(0, 8);
|
||||
const cached = allSessions.find((x) => (x.id || x.session_id) === sessionId);
|
||||
title.textContent = (cached && (cached.name || cached.title)) || ('Session ' + sessionId.substring(0, 8));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Chat] Failed to load session:', e);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Regression tests for #322 — Chat must fill the dashboard main column.
|
||||
|
||||
Chat pages declare ``layout: "full-bleed"``; ``selectPage()`` then adds
|
||||
``db-fill`` to ``#db-main-content`` and skips the duplicate page header, and
|
||||
``.chat-root`` sizes from flex allocation instead of ``calc(100vh - 140px)``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
PLUGINS = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "src" / "praisonaiui" / "templates" / "frontend" / "plugins"
|
||||
)
|
||||
|
||||
|
||||
def _select_page_body() -> str:
|
||||
src = (PLUGINS / "dashboard.js").read_text(encoding="utf-8")
|
||||
m = re.search(r"async function selectPage\(pageId\)\s*\{(.*?)^}", src, re.DOTALL | re.MULTILINE)
|
||||
assert m
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def test_chat_pages_declare_full_bleed():
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from praisonaiui.server import create_app, reset_state
|
||||
|
||||
reset_state() # isolate from page whitelists set by other tests
|
||||
pages = {p["id"]: p for p in TestClient(create_app()).get("/api/pages").json()["pages"]}
|
||||
for pid in ("chat", "chat-canvas"):
|
||||
assert pages[pid].get("layout") == "full-bleed"
|
||||
assert pages["overview"].get("layout") is None
|
||||
|
||||
|
||||
def test_select_page_toggles_fill_and_skips_header():
|
||||
body = _select_page_body()
|
||||
assert "page.layout === 'full-bleed'" in body
|
||||
assert "classList.toggle('db-fill'" in body
|
||||
assert re.search(r"showPageHeader = .*&& !fullBleed", body)
|
||||
|
||||
|
||||
def test_fill_css_defined():
|
||||
src = (PLUGINS / "dashboard.js").read_text(encoding="utf-8")
|
||||
assert ".db-main.db-fill > #db-page-content" in src
|
||||
|
||||
|
||||
def test_chat_root_fills_column_in_fill_mode():
|
||||
chat = (PLUGINS / "views" / "chat.js").read_text(encoding="utf-8")
|
||||
assert re.search(r"\.db-fill \.chat-root \{[^}]*flex:1;[^}]*min-height:0", chat)
|
||||
canvas = (PLUGINS / "views" / "chat-canvas.js").read_text(encoding="utf-8")
|
||||
assert ".db-fill .chat-canvas-root" in canvas
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Tests for the in-rail session search in the chat view (issue #324).
|
||||
|
||||
The chat sidebar must expose a search input that filters the already
|
||||
fetched session list client-side, without refetching from the API.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
CHAT = Path("src/praisonaiui/templates/frontend/plugins/views/chat.js")
|
||||
|
||||
|
||||
class TestChatSessionRailSearch:
|
||||
def test_search_input_rendered_in_rail(self):
|
||||
src = CHAT.read_text()
|
||||
assert 'id="chat-session-search"' in src
|
||||
assert src.index('id="chat-session-search"') < src.index('id="chat-sessions-list"')
|
||||
|
||||
def test_input_wired_to_client_side_render(self):
|
||||
src = CHAT.read_text()
|
||||
assert "sessionSearch.addEventListener('input', renderSessionList)" in src
|
||||
|
||||
def test_load_sessions_caches_and_delegates_render(self):
|
||||
src = CHAT.read_text()
|
||||
assert "allSessions = Array.isArray(fetched) ? fetched : [];" in src
|
||||
body = src.split("async function loadSessions()", 1)[1].split("\n}\n", 1)[0]
|
||||
assert "renderSessionList();" in body
|
||||
|
||||
def test_filter_matches_title_and_id_fields(self):
|
||||
src = CHAT.read_text()
|
||||
assert "function _sessionMatches(s, q)" in src
|
||||
assert "No matching sessions" in src
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Issue #331 — rename a session from the chat rail via the existing PATCH route."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import Route
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
import praisonaiui.server as server
|
||||
from praisonaiui.datastore import MemoryDataStore
|
||||
|
||||
CHAT = Path("src/praisonaiui/templates/frontend/plugins/views/chat.js")
|
||||
|
||||
|
||||
def _client(monkeypatch):
|
||||
ds = MemoryDataStore()
|
||||
monkeypatch.setattr(server, "_datastore", ds)
|
||||
app = Starlette(routes=[Route("/sessions/{session_id}", server.patch_session, methods=["PATCH"])])
|
||||
sid = asyncio.run(ds.create_session())["id"]
|
||||
return TestClient(app), ds, sid
|
||||
|
||||
|
||||
def test_patch_renames_and_trims(monkeypatch):
|
||||
client, ds, sid = _client(monkeypatch)
|
||||
r = client.patch(f"/sessions/{sid}", json={"title": " My chat "})
|
||||
assert r.status_code == 200
|
||||
assert asyncio.run(ds.get_session(sid))["title"] == "My chat"
|
||||
|
||||
|
||||
def test_patch_rejects_blank_or_non_string_title(monkeypatch):
|
||||
client, _, sid = _client(monkeypatch)
|
||||
assert client.patch(f"/sessions/{sid}", json={"title": " "}).status_code == 400
|
||||
assert client.patch(f"/sessions/{sid}", json={"title": 123}).status_code == 400
|
||||
|
||||
|
||||
def test_patch_unknown_session_404(monkeypatch):
|
||||
client, _, _ = _client(monkeypatch)
|
||||
assert client.patch("/sessions/nope", json={"title": "x"}).status_code == 404
|
||||
|
||||
|
||||
def test_rail_has_rename_button_wired_to_patch():
|
||||
src = CHAT.read_text()
|
||||
assert "async function renameSession(sessionId, currentTitle)" in src
|
||||
assert "method: 'PATCH'" in src
|
||||
assert "renameSession(id, title)" in src
|
||||
assert "e.stopPropagation()" in src
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Issue #323 — Jobs rows link to their chat session when ``session_id`` is set.
|
||||
|
||||
Source-parsing test, mirroring ``test_session_search_dashboard.py``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
JOBS_JS = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "src" / "praisonaiui" / "templates" / "frontend" / "plugins" / "jobs.js"
|
||||
)
|
||||
|
||||
|
||||
def _src() -> str:
|
||||
return JOBS_JS.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_open_session_button_only_when_session_id():
|
||||
src = _src()
|
||||
assert "job.session_id ? `<button" in src
|
||||
assert 'data-session-id="${escapeAttr(job.session_id)}"' in src
|
||||
|
||||
|
||||
def test_open_session_reuses_session_select_event():
|
||||
src = _src()
|
||||
assert "function openJobSession(sessionId)" in src
|
||||
assert "selectPage('chat')" in src
|
||||
assert "'aiui:session-select'" in src
|
||||
assert "window.aiuiOpenJobSession = openJobSession;" in src
|
||||
Reference in new issue
Block a user