Files
mnote/rust/crates/mnote-web/browser/sidebar-page-ai-pi-lab-runtime.js
T

1529 lines
83 KiB
JavaScript
Raw Normal View History

2026-07-04 21:47:42 +08:00
// == Pi Lab Page AI Runtime ==
// MNote-native adapter for Pi-first Page AI Lab.
// Renders an independent floating launcher and drawer. OpenHub remains the
// default Page AI surface; Pi Lab never mounts inside the OpenHub drawer, iframe
// page, provider tab, or page container. NO setInterval polling.
//
// UI note: @earendil-works/pi-web-ui@0.75.3 was checked as the mature upstream UI.
// It expects browser-side pi-agent-core, IndexedDB storage, API-key dialogs, model
// selection and builtin tools/artifacts. MNote's lab backend owns Pi RPC, access
// scope and tools, so this file keeps a host-owned adapter while matching the
// upstream ChatPanel/AgentInterface layout: message list, streaming composer,
// tool timeline, model/runtime controls and artifacts-style right rail.
//
// Default model: Omniroute/freefirst (consumed from backend status/default fields).
(function () {
'use strict';
var STATE_IDLE = 'idle';
var STATE_STARTING = 'starting';
var STATE_STARTED = 'started';
var STATE_STREAMING = 'streaming';
var STATE_ABORTED = 'aborted';
var STATE_ERROR = 'error';
var API = {
STATUS: '/api/page-ai/pi/status',
START: '/api/page-ai/pi/start',
SEND: '/api/page-ai/pi/send',
ABORT: '/api/page-ai/pi/abort',
EVENTS: '/api/page-ai/pi/events',
BOOTSTRAP: '/api/page-ai/pi/bootstrap',
SESSIONS: '/api/page-ai/pi/sessions',
EFFECTIVE: '/api/ai-settings/effective',
};
var DEFAULT_MODEL_PROVIDER = 'omniroute';
var DEFAULT_MODEL_ID = 'freefirst';
var piLabInstalled = false;
var piLabPanelEl = null;
var piLabDrawerEl = null;
var piLabLauncherEl = null;
var piLabEventSource = null;
var piLabState = {
status: STATE_IDLE,
enabled: false,
active: false,
standalone: false,
sessionId: null,
providerSessionId: null,
defaultModelProvider: DEFAULT_MODEL_PROVIDER,
defaultModelId: DEFAULT_MODEL_ID,
modelProvider: null,
modelId: null,
runtimeMode: null,
runtimePid: null,
disabledBuiltinTools: null,
session: null,
allowedRootsSummary: '',
selectionSummary: '',
selectionText: '',
changedFiles: [],
messages: [],
diagnostics: '',
receipts: [],
modelOptions: [],
history: [],
};
var streamingAssistantMsg = null;
function injectStyles() {
var styleId = 'pi-lab-runtime-style';
if (document.getElementById(styleId)) return;
var style = document.createElement('style');
style.id = styleId;
style.textContent =
'.wolai-page-ai-pi-lab-launcher{position:fixed;right:26px;bottom:92px;z-index:2600;width:36px;height:36px;border-radius:18px;border:1px solid rgba(37,37,33,.14);background:#fff;color:#252521;box-shadow:0 8px 24px rgba(15,23,42,.16);display:grid;place-items:center;font:600 15px/1 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;cursor:pointer}' +
'.wolai-page-ai-pi-lab-launcher:hover{background:#f7f7f5;box-shadow:0 10px 28px rgba(15,23,42,.20)}' +
'.wolai-page-ai-pi-lab-launcher[data-state="streaming"]::after{content:"";position:absolute;right:4px;top:4px;width:8px;height:8px;border-radius:50%;background:#10a37f;box-shadow:0 0 0 3px rgba(16,163,127,.14)}' +
'.wolai-page-ai-pi-lab-drawer{position:fixed;right:18px;bottom:18px;top:64px;z-index:2590;width:min(760px,calc(100vw - 36px));border:1px solid rgba(35,35,30,.10);border-radius:20px;background:#f5f6fa;box-shadow:0 24px 70px rgba(15,23,42,.22);overflow:hidden;display:flex;flex-direction:column}' +
'.wolai-page-ai-pi-lab-drawer[hidden]{display:none!important}' +
'.wolai-page-ai-pi-lab-drawer[data-minimized="true"]{top:auto;height:54px;width:min(420px,calc(100vw - 36px))}' +
'.wolai-page-ai-pi-lab-drawer[data-minimized="true"] .wolai-page-ai-pi-lab-config,.wolai-page-ai-pi-lab-drawer[data-minimized="true"] .wolai-page-ai-pi-lab-context-strip,.wolai-page-ai-pi-lab-drawer[data-minimized="true"] .wolai-page-ai-pi-lab-body,.wolai-page-ai-pi-lab-drawer[data-minimized="true"] .wolai-page-ai-pi-lab-diagnostics{display:none!important}' +
'.wolai-page-ai-pi-lab-shell{display:flex;flex-direction:column;height:100%;min-height:0;background:#f5f6fa;color:#242424;font:13px/1.45 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;overflow:hidden}' +
'.wolai-page-ai-pi-lab-shell[hidden]{display:none!important}' +
'.wolai-page-ai-pi-lab-topbar{height:58px;display:flex;align-items:center;gap:10px;padding:0 18px;background:#f5f6fa;flex:0 0 auto}' +
'.wolai-page-ai-pi-lab-brand{display:flex;align-items:center;gap:8px;min-width:0;flex:1}' +
'.wolai-page-ai-pi-lab-logo{width:30px;height:30px;border-radius:9px;display:grid;place-items:center;background:#1f6feb;color:#fff;font-weight:700}' +
'.wolai-page-ai-pi-lab-title{display:flex;flex-direction:column;gap:1px;min-width:0}' +
'.wolai-page-ai-pi-lab-title strong{font-size:22px;font-weight:760;letter-spacing:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
'.wolai-page-ai-pi-lab-title span{font-size:11px;color:#6f6a60;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
'.wolai-page-ai-pi-lab-icon-btn{width:30px;height:30px;border:1px solid #e2ded6;border-radius:8px;background:#fff;color:#514f49;display:grid;place-items:center;cursor:pointer}' +
'.wolai-page-ai-pi-lab-icon-btn:hover{background:#f7f6f2}' +
'.wolai-page-ai-pi-lab-commandbar{height:52px;margin:0 14px 28px;padding:0 14px;border:1px solid #e5e8ef;border-radius:14px;background:#fff;display:flex;align-items:center;gap:14px;box-shadow:0 2px 8px rgba(15,23,42,.04);flex:0 0 auto}' +
'.wolai-page-ai-pi-lab-commandbar .wolai-page-ai-pi-lab-tool-icon{width:28px;height:28px;border:0;background:transparent;color:#30343b;display:grid;place-items:center;font-size:19px;cursor:pointer;border-radius:8px}' +
'.wolai-page-ai-pi-lab-commandbar .wolai-page-ai-pi-lab-tool-icon:hover{background:#f3f5f8}' +
'.wolai-page-ai-pi-lab-commandbar .wolai-page-ai-pi-lab-tool-icon[disabled]{opacity:.35;cursor:default}' +
'.wolai-page-ai-pi-lab-commandbar-spacer{flex:1}' +
'.wolai-page-ai-pi-lab-config{display:none;grid-template-columns:minmax(0,1fr) auto;gap:10px;margin:-18px 14px 12px;padding:10px 12px;border:1px solid #e5e8ef;border-radius:12px;background:#fff;box-shadow:0 4px 14px rgba(15,23,42,.05);flex:0 0 auto}' +
'.wolai-page-ai-pi-lab-drawer[data-config-open="true"] .wolai-page-ai-pi-lab-config{display:grid}' +
'.wolai-page-ai-pi-lab-model-row{display:flex;flex-wrap:wrap;gap:6px;min-width:0}' +
'.wolai-page-ai-pi-lab-settings{border:0;min-width:0}' +
'.wolai-page-ai-pi-lab-settings>summary{display:inline-flex;align-items:center;gap:6px;min-height:28px;cursor:pointer;list-style:none;color:#514f49;font-size:12px}' +
'.wolai-page-ai-pi-lab-settings>summary::-webkit-details-marker{display:none}' +
'.wolai-page-ai-pi-lab-settings>summary::before{content:"▸";font-size:10px;color:#8a8378}' +
'.wolai-page-ai-pi-lab-settings[open]>summary::before{content:"▾"}' +
'.wolai-page-ai-pi-lab-settings-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:8px;max-width:560px}' +
'.wolai-page-ai-pi-lab-history-panel{margin:0 18px 10px;border:1px solid #e2ded6;border-radius:8px;background:#fff;max-height:220px;overflow:auto}' +
'.wolai-page-ai-pi-lab-history-panel[hidden]{display:none!important}' +
'.wolai-page-ai-pi-lab-history-row{width:100%;border:0;border-bottom:1px solid #f0eee9;background:#fff;padding:10px 12px;text-align:left;cursor:pointer;display:flex;justify-content:space-between;gap:10px}' +
'.wolai-page-ai-pi-lab-history-row:hover{background:#f8f7f4}' +
'.wolai-page-ai-pi-lab-history-row strong{display:block;font-size:12px;color:#2e2c28}' +
'.wolai-page-ai-pi-lab-history-row span{font-size:11px;color:#817b72}' +
'.wolai-page-ai-pi-lab-field{display:flex;flex-direction:column;gap:4px;font-size:11px;color:#6f6a60}' +
'.wolai-page-ai-pi-lab-field select,.wolai-page-ai-pi-lab-field input{height:30px;border:1px solid #ddd8cf;border-radius:8px;background:#fff;color:#242424;padding:0 8px;font:12px/1.3 inherit;min-width:0}' +
'.wolai-page-ai-pi-lab-context-strip{display:none;padding:0;margin:-6px 14px 12px;border:1px solid #ebe7de;border-radius:12px;background:#faf9f6;flex:0 0 auto}' +
'.wolai-page-ai-pi-lab-drawer[data-config-open="true"] .wolai-page-ai-pi-lab-context-strip{display:block}' +
'.wolai-page-ai-pi-lab-context-strip>summary{display:flex;align-items:center;gap:8px;min-height:34px;padding:0 12px;cursor:pointer;list-style:none;color:#5d594f;font-size:12px}' +
'.wolai-page-ai-pi-lab-context-strip>summary::-webkit-details-marker{display:none}' +
'.wolai-page-ai-pi-lab-context-strip>summary::before{content:"▸";font-size:10px;color:#8a8378}' +
'.wolai-page-ai-pi-lab-context-strip[open]>summary::before{content:"▾"}' +
'.wolai-page-ai-pi-lab-context-title{font-weight:650;color:#42403a}' +
'.wolai-page-ai-pi-lab-context-current{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#242424;font-weight:650}' +
'.wolai-page-ai-pi-lab-context-count{margin-left:auto;color:#8a8378;font-size:11px}' +
'.wolai-page-ai-pi-lab-context-content{display:flex;align-items:center;gap:6px;padding:0 12px 8px;overflow-x:auto}' +
'.wolai-page-ai-pi-lab-chip{height:24px;display:inline-flex;align-items:center;gap:5px;border:1px solid #e4e0d8;border-radius:999px;background:#f8f7f3;color:#514f49;padding:0 8px;font-size:11px;max-width:100%}' +
'.wolai-page-ai-pi-lab-chip strong{font-weight:650;color:#242424;min-width:0;overflow:hidden;text-overflow:ellipsis}' +
'.wolai-page-ai-pi-lab-context-strip .wolai-page-ai-pi-lab-chip{flex:0 0 auto}' +
'.wolai-page-ai-pi-lab-chip[data-tone="ok"]{background:#eef8f3;border-color:#ccebdc;color:#176b4b}' +
'.wolai-page-ai-pi-lab-chip[data-tone="warn"]{background:#fff6e6;border-color:#f2dfb5;color:#865b13}' +
'.wolai-page-ai-pi-lab-chip[data-tone="danger"]{background:#fff0ee;border-color:#f2c4bd;color:#a73b2f}' +
'.wolai-page-ai-pi-lab-actions{display:flex;align-items:center;gap:6px;justify-content:flex-end}' +
'.wolai-page-ai-pi-lab-btn{height:28px;border:1px solid #d9d4ca;border-radius:7px;background:#fff;color:#383631;font-size:12px;padding:0 10px;cursor:pointer;white-space:nowrap}' +
'.wolai-page-ai-pi-lab-btn:hover{background:#f7f6f2}' +
'.wolai-page-ai-pi-lab-btn:disabled{opacity:.45;cursor:default}' +
'.wolai-page-ai-pi-lab-btn-primary{background:#1f6feb;border-color:#1f6feb;color:#fff}' +
'.wolai-page-ai-pi-lab-btn-primary:hover{background:#1b61cf}' +
'.wolai-page-ai-pi-lab-btn-danger{background:#d93025;border-color:#d93025;color:#fff}' +
'.wolai-page-ai-pi-lab-body{display:block;min-height:0;flex:1;background:#f5f6fa;padding:0 14px 12px}' +
'.wolai-page-ai-pi-lab-main{display:flex;flex-direction:column;min-width:0;min-height:0;height:100%}' +
'.wolai-page-ai-pi-lab-messages{flex:1;min-height:0;overflow-y:auto;padding:22px 18px 20px;display:flex;flex-direction:column;gap:12px;overscroll-behavior:contain;background:#fff;border:1px solid #e4e7ef;border-radius:22px 22px 0 0;box-shadow:0 12px 36px rgba(15,23,42,.08)}' +
'.wolai-page-ai-pi-lab-empty{margin:auto;max-width:460px;text-align:center;color:#8a9099;background:transparent;border:0;border-radius:0;padding:10px 0 4px;box-shadow:none}' +
'.wolai-page-ai-pi-lab-empty-icon{width:76px;height:76px;margin:0 auto 18px;border-radius:50%;background:#f0f2f6;color:#8a93a2;display:grid;place-items:center;font-size:42px;font-weight:400}' +
'.wolai-page-ai-pi-lab-empty strong{display:block;color:#242833;margin-bottom:8px;font-size:25px;font-weight:760;letter-spacing:0}' +
'.wolai-page-ai-pi-lab-empty p{margin:0 0 20px;color:#8a9099;font-size:16px}' +
'.wolai-page-ai-pi-lab-examples{display:flex;flex-direction:column;align-items:center;gap:10px}' +
'.wolai-page-ai-pi-lab-example{height:42px;min-width:min(390px,100%);border:1px solid #e1e4eb;border-radius:12px;background:#fff;color:#4a4f58;font-size:16px;display:flex;align-items:center;justify-content:flex-start;gap:12px;padding:0 18px;box-shadow:0 3px 9px rgba(15,23,42,.05);cursor:pointer}' +
'.wolai-page-ai-pi-lab-example:hover{background:#f8fafc}' +
'.wolai-page-ai-pi-lab-example span{font-size:18px;color:#5c6470}' +
'.wolai-page-ai-pi-lab-message{display:flex;flex-direction:column;gap:6px;max-width:92%}' +
'.wolai-page-ai-pi-lab-message[data-role="user"]{align-self:flex-end;align-items:flex-end}' +
'.wolai-page-ai-pi-lab-message[data-role="assistant"],.wolai-page-ai-pi-lab-message[data-role="system"]{align-self:stretch}' +
'.wolai-page-ai-pi-lab-role{display:flex;align-items:center;gap:6px;color:#777166;font-size:11px;font-weight:650}' +
'.wolai-page-ai-pi-lab-message[data-role="user"] .wolai-page-ai-pi-lab-role{justify-content:flex-end}' +
'.wolai-page-ai-pi-lab-bubble{border:1px solid #e6e2da;border-radius:12px;background:#fff;padding:10px 12px;color:#272521;word-break:break-word;box-shadow:0 4px 14px rgba(15,23,42,.035)}' +
'.wolai-page-ai-pi-lab-message[data-role="user"] .wolai-page-ai-pi-lab-bubble{background:#f3f7ff;border-color:#d9e6ff}' +
'.wolai-page-ai-pi-lab-text{white-space:pre-wrap}' +
'.wolai-page-ai-pi-lab-text.streaming-cursor::after{content:"";display:inline-block;width:7px;height:14px;margin-left:3px;vertical-align:-2px;background:#1f6feb;animation:pi-blink .85s steps(2,start) infinite}' +
'@keyframes pi-blink{50%{opacity:.18}}' +
'.wolai-page-ai-pi-lab-tool-calls,.wolai-page-ai-pi-lab-citations{display:flex;flex-direction:column;gap:6px;margin-top:8px}' +
'.wolai-page-ai-pi-lab-tool-call{border:1px solid #e5e0d8;border-radius:9px;background:#faf9f6;overflow:hidden}' +
'.wolai-page-ai-pi-lab-tool-call-header{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:7px 9px;font-size:12px;font-weight:650;color:#42403a}' +
'.wolai-page-ai-pi-lab-tool-call-header span{font-size:11px;color:#777166;font-weight:500}' +
'.wolai-page-ai-pi-lab-tool-call pre{margin:0;border-top:1px solid #ebe7de;background:#fff;padding:8px;max-height:140px;overflow:auto;font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;color:#42403a;white-space:pre-wrap}' +
'.wolai-page-ai-pi-lab-citation{display:inline-flex;align-items:center;gap:6px;width:max-content;max-width:100%;border:1px solid #d9e6ff;border-radius:999px;background:#f3f7ff;color:#2456a6;text-decoration:none;padding:4px 8px;font-size:11px}' +
'.wolai-page-ai-pi-lab-diff{display:inline-flex;align-items:center;width:max-content;max-width:100%;margin-top:8px;border:1px solid #f0dcb7;border-radius:999px;background:#fff7e8;color:#865b13;padding:4px 8px;font-size:11px}' +
'.wolai-page-ai-pi-lab-composer{flex:0 0 auto;border:1px solid #e4e7ef;border-top:0;border-radius:0 0 22px 22px;background:#fff;box-shadow:0 14px 34px rgba(15,23,42,.09);overflow:hidden}' +
'.wolai-page-ai-pi-lab-input{width:100%;min-height:94px;max-height:190px;box-sizing:border-box;border:0;resize:none;padding:20px 22px 10px;font:18px/1.45 inherit;color:#242424;background:#f7f8fb;outline:none}' +
'.wolai-page-ai-pi-lab-input::placeholder{color:#b6bbc5}' +
'.wolai-page-ai-pi-lab-composer-bar{height:48px;display:flex;align-items:center;gap:8px;padding:0 14px;border-top:1px solid #eef0f5;background:#fff}' +
'.wolai-page-ai-pi-lab-quick{height:24px;border:1px solid #e5e0d8;border-radius:999px;background:#f8f7f3;color:#5d594f;font-size:11px;padding:0 8px;cursor:pointer}' +
'.wolai-page-ai-pi-lab-modebar{height:52px;display:flex;align-items:center;gap:10px;padding:0 14px;border-top:1px solid #eef0f5;background:#fff}' +
'.wolai-page-ai-pi-lab-square{width:36px;height:36px;border:1px solid #e1e4eb;border-radius:9px;background:#fff;color:#242833;display:grid;place-items:center;font-size:19px;cursor:pointer}' +
'.wolai-page-ai-pi-lab-segment{display:inline-flex;border:1px solid #e1e4eb;border-radius:10px;overflow:hidden;background:#fff}' +
'.wolai-page-ai-pi-lab-segment button{height:36px;border:0;background:#fff;color:#4a4f58;padding:0 16px;font:14px/1 inherit;cursor:pointer}' +
'.wolai-page-ai-pi-lab-segment button[data-active=\"true\"]{background:#111827;color:#fff}' +
'.wolai-page-ai-pi-lab-model-control{height:36px;min-width:190px;border:1px solid #e1e4eb;border-radius:10px;background:#fff;color:#4a4f58;padding:0 12px;font:13px/1 inherit}' +
'.wolai-page-ai-pi-lab-budget{height:34px;border:1px solid #bfe6ac;border-radius:10px;background:#f4fff0;color:#329125;padding:0 12px;display:inline-flex;align-items:center;font-size:14px}' +
'.wolai-page-ai-pi-lab-spacer{flex:1}' +
'.wolai-page-ai-pi-lab-send{width:52px;height:52px;border:0;border-radius:15px;background:#1d9bf0;color:#fff;display:grid;place-items:center;cursor:pointer;font-size:24px;box-shadow:0 10px 24px rgba(29,155,240,.25)}' +
'.wolai-page-ai-pi-lab-send:disabled{opacity:.45;cursor:default}' +
'.wolai-page-ai-pi-lab-rail{display:none}' +
'.wolai-page-ai-pi-lab-rail-section{border:1px solid #e5e0d8;border-radius:10px;background:#fff;overflow:hidden}' +
'.wolai-page-ai-pi-lab-rail-section>summary{display:flex;align-items:center;justify-content:space-between;gap:8px;cursor:pointer;list-style:none;padding:8px 9px;font-size:11px;font-weight:650;color:#5d594f;background:#faf9f6}' +
'.wolai-page-ai-pi-lab-rail-section>summary::-webkit-details-marker{display:none}' +
'.wolai-page-ai-pi-lab-rail-section>summary::after{content:"▸";font-size:10px;color:#8a8378}' +
'.wolai-page-ai-pi-lab-rail-section[open]>summary::after{content:"▾"}' +
'.wolai-page-ai-pi-lab-rail-count{font-weight:600;color:#8a8378}' +
'.wolai-page-ai-pi-lab-rail-title{padding:8px 9px;border-bottom:1px solid #eee9e0;font-size:11px;font-weight:650;color:#5d594f;background:#faf9f6}' +
'.wolai-page-ai-pi-lab-tool-list{display:flex;flex-direction:column;gap:5px;padding:8px}' +
'.wolai-page-ai-pi-lab-tool-pill{border:1px solid #e6e2da;border-radius:7px;background:#fff;color:#42403a;padding:5px 7px;font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
'.wolai-page-ai-pi-lab-receipts{display:flex;flex-direction:column;gap:5px;padding:8px;max-height:170px;overflow:auto}' +
'.wolai-page-ai-pi-lab-receipt{border-radius:7px;background:#f8f7f3;border:1px solid #e6e2da;padding:6px;font-size:11px;color:#4d4941}' +
'.wolai-page-ai-pi-lab-receipt[data-allowed="false"]{background:#fff0ee;border-color:#f2c4bd;color:#9f352c}' +
'.wolai-page-ai-pi-lab-diagnostics{display:none;border-top:1px solid #e6e2da;background:#fff;flex:0 0 auto}' +
'.wolai-page-ai-pi-lab-drawer[data-config-open="true"] .wolai-page-ai-pi-lab-diagnostics{display:block}' +
'.wolai-page-ai-pi-lab-diagnostics>summary{cursor:pointer;padding:8px 12px;font-size:11px;color:#6f6a60}' +
'.wolai-page-ai-pi-lab-diagnostics pre{margin:0;padding:0 12px 10px;white-space:pre-wrap;word-break:break-word;font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;color:#5d594f}' +
'@media (max-width: 760px){.wolai-page-ai-pi-lab-drawer{left:10px;right:10px;top:58px;bottom:10px;width:auto}.wolai-page-ai-pi-lab-body{grid-template-columns:1fr}.wolai-page-ai-pi-lab-rail{display:none}.wolai-page-ai-pi-lab-config{grid-template-columns:1fr}}';
document.head.appendChild(style);
}
function escapeHtml(str) {
if (str == null) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function el(tag, attrs, children) {
var elem = document.createElement(tag);
if (attrs) {
Object.keys(attrs).forEach(function (key) {
if (key === 'className') elem.className = attrs[key];
else if (key === 'textContent') elem.textContent = attrs[key];
else if (key === 'innerHTML') elem.innerHTML = attrs[key];
else if (key === 'style' && attrs[key] && typeof attrs[key] === 'object') Object.keys(attrs[key]).forEach(function (styleKey) { elem.style[styleKey] = attrs[key][styleKey]; });
else elem.setAttribute(key, attrs[key]);
});
}
if (children) {
(Array.isArray(children) ? children : [children]).forEach(function (child) {
if (typeof child === 'string') elem.appendChild(document.createTextNode(child));
else if (child instanceof Node) elem.appendChild(child);
});
}
return elem;
}
function normalizeSlashes(value) {
return String(value || '').trim().replace(/\\/g, '/');
}
function currentUrl() {
try {
return new URL(window.location.href);
} catch (_) {
return new URL('/', window.location.origin);
}
}
function currentDocumentId() {
var fromBody = document.body && document.body.dataset ? String(document.body.dataset.documentId || '').trim() : '';
if (fromBody) return fromBody;
var pageTab = document.querySelector('[data-mnote-main-tab="page"]');
if (pageTab instanceof HTMLElement) {
var tabDocumentId = String(pageTab.getAttribute('data-document-id') || '').trim();
if (tabDocumentId) return tabDocumentId;
}
var match = currentUrl().pathname.match(/^\/documents\/([^/]+)/);
return match ? decodeURIComponent(match[1]) : '';
}
function currentWorkspaceId() {
return currentUrl().searchParams.get('workspaceId') || (document.body && document.body.dataset ? String(document.body.dataset.workspaceId || '').trim() : '');
}
function currentRootUri() {
return currentUrl().searchParams.get('rootUri') || '';
}
function localMarkdownRelativePathFromDocumentId(documentId) {
var value = String(documentId || '').trim();
if (!value.startsWith('local-md:')) return '';
return value.slice('local-md:'.length).replace(/~2F/g, '/');
}
function currentSelectionText() {
try {
var selection = window.getSelection ? window.getSelection() : null;
return selection ? String(selection.toString() || '').trim() : '';
} catch (_) {
return '';
}
}
function activeEditorSnapshot() {
try {
var snapshot = window.__mnoteDocumentPaneRuntime
&& typeof window.__mnoteDocumentPaneRuntime.getOpenEditorsSnapshot === 'function'
? window.__mnoteDocumentPaneRuntime.getOpenEditorsSnapshot()
: null;
if (!snapshot || typeof snapshot !== 'object') return null;
if (snapshot.activeEditor) return snapshot.activeEditor;
var entries = []
.concat(Array.isArray(snapshot.editors) ? snapshot.editors : [])
.concat(Array.isArray(snapshot.resourceEditors) ? snapshot.resourceEditors : []);
return entries.find(function (entry) { return entry && entry.active === true; }) || entries[0] || null;
} catch (_) {
return null;
}
}
function currentPageContext() {
var active = activeEditorSnapshot() || {};
var workspacePath = active.workspacePath && typeof active.workspacePath === 'object' ? active.workspacePath : {};
var documentId = String(workspacePath.documentId || active.documentId || currentDocumentId() || '').trim();
var pagePath = normalizeSlashes(workspacePath.relativePath || workspacePath.path || active.relativePath || active.path || localMarkdownRelativePathFromDocumentId(documentId));
var rootUri = String(workspacePath.rootUri || active.rootUri || currentRootUri() || '').trim();
var workspaceId = String(workspacePath.workspaceId || active.workspaceId || currentWorkspaceId() || '').trim();
var titleNode = document.querySelector('[data-page-title-current="true"]') || document.querySelector('.wolai-breadcrumb-current');
var title = String(active.title || workspacePath.title || (titleNode && titleNode.textContent) || document.title || '').trim();
var selection = currentSelectionText();
return {
pageId: documentId,
pagePath: pagePath,
pageTitle: title,
rootUri: rootUri,
workspaceId: workspaceId,
selection: selection,
};
}
function refreshSelectionSummary() {
var selection = currentSelectionText();
if (selection) piLabState.selectionText = selection;
var visibleSelection = selection || piLabState.selectionText;
piLabState.selectionSummary = visibleSelection ? ('已选中 ' + visibleSelection.length + ' 字') : '';
return visibleSelection;
}
function applyCurrentContextToState() {
var context = currentPageContext();
refreshSelectionSummary();
if (!piLabState.session) piLabState.session = {};
if (context.pagePath && !piLabState.session.pagePath) piLabState.session.pagePath = context.pagePath;
if (context.pageTitle && !piLabState.session.pageTitle) piLabState.session.pageTitle = context.pageTitle;
if (context.rootUri && !piLabState.session.rootUri) piLabState.session.rootUri = context.rootUri;
if (context.workspaceId && !piLabState.session.workspaceId) piLabState.session.workspaceId = context.workspaceId;
updateContextStrip();
return context;
}
function currentModelLabel() {
var provider = piLabState.modelProvider || piLabState.defaultModelProvider || DEFAULT_MODEL_PROVIDER;
var modelId = piLabState.modelId || piLabState.defaultModelId || DEFAULT_MODEL_ID;
if (String(modelId).indexOf(provider + '/') === 0) modelId = String(modelId).slice(provider.length + 1);
return [provider, modelId].filter(Boolean).join('/');
}
function applyModelControls() {
if (!piLabPanelEl) return;
var providerSelect = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-provider]');
var modelSelect = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-id]');
var bottomModelSelect = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-id-bottom]');
var customInput = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-custom]');
if (providerSelect && providerSelect.value) piLabState.modelProvider = providerSelect.value;
if (customInput && customInput.value.trim()) piLabState.modelId = customInput.value.trim();
else if (modelSelect && modelSelect.value) piLabState.modelId = modelSelect.value;
else if (bottomModelSelect && bottomModelSelect.value) piLabState.modelId = bottomModelSelect.value;
updateModelLabel();
}
function splitModelRef(value) {
var raw = String(value || '').trim();
var slash = raw.indexOf('/');
if (slash < 0) return { provider: DEFAULT_MODEL_PROVIDER, modelId: raw || DEFAULT_MODEL_ID };
return { provider: raw.slice(0, slash), modelId: raw.slice(slash + 1) };
}
function applyEffectiveConfig(data) {
data = data || {};
var defaultRef = splitModelRef(data.defaultModel || data.default_model || '');
piLabState.defaultModelProvider = defaultRef.provider || DEFAULT_MODEL_PROVIDER;
piLabState.defaultModelId = defaultRef.modelId || DEFAULT_MODEL_ID;
piLabState.modelOptions = Array.isArray(data.models) ? data.models : [];
if (Array.isArray(data.allowedRoots)) {
piLabState.allowedRootsSummary = data.allowedRoots.length + ' roots';
}
if (!piLabPanelEl) return;
var providerSelect = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-provider]');
var modelSelect = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-id]');
var bottomModelSelect = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-id-bottom]');
var providers = [];
piLabState.modelOptions.forEach(function (item) {
var provider = String(item.provider || DEFAULT_MODEL_PROVIDER);
if (providers.indexOf(provider) < 0) providers.push(provider);
});
if (!providers.length) providers.push(piLabState.defaultModelProvider);
if (providerSelect) {
providerSelect.innerHTML = providers.map(function (provider) {
return '<option value="' + escapeHtml(provider) + '">' + escapeHtml(provider) + '</option>';
}).join('');
providerSelect.value = piLabState.modelProvider || piLabState.defaultModelProvider;
}
var modelOptions = piLabState.modelOptions.length ? piLabState.modelOptions : [{
provider: piLabState.defaultModelProvider,
id: piLabState.defaultModelId,
name: piLabState.defaultModelId,
}];
var optionHtml = modelOptions.map(function (item) {
var id = String(item.id || item.modelId || item.name || DEFAULT_MODEL_ID);
var label = String(item.name || id);
return '<option value="' + escapeHtml(id) + '">' + escapeHtml(label) + '</option>';
}).join('');
if (modelSelect) modelSelect.innerHTML = optionHtml;
if (bottomModelSelect) bottomModelSelect.innerHTML = optionHtml;
updateModelLabel();
updateContextStrip();
}
function loadEffectiveConfig() {
return fetch(API.EFFECTIVE, { credentials: 'same-origin', cache: 'no-store' })
.then(function (response) {
if (!response.ok) throw new Error('Effective config failed: ' + response.status);
return response.json();
})
.then(function (data) {
applyEffectiveConfig(data);
return data;
});
}
function currentRuntimeLabel() {
var parts = [];
if (piLabState.runtimeMode) parts.push(piLabState.runtimeMode);
if (piLabState.runtimePid) parts.push('pid ' + piLabState.runtimePid);
return parts.join(' · ') || 'runtime pending';
}
function statusTone() {
if (!piLabState.enabled || piLabState.status === STATE_ERROR) return 'danger';
if (piLabState.status === STATE_STREAMING || piLabState.status === STATE_STARTED) return 'ok';
if (piLabState.status === STATE_STARTING || piLabState.status === STATE_ABORTED) return 'warn';
return '';
}
function statusText() {
if (!piLabState.enabled) return 'disabled';
if (piLabState.status === STATE_IDLE) return 'idle';
if (piLabState.status === STATE_STARTING) return 'starting';
if (piLabState.status === STATE_STARTED) return 'ready';
if (piLabState.status === STATE_STREAMING) return 'streaming';
if (piLabState.status === STATE_ABORTED) return 'aborted';
return 'error';
}
function piLabPanelHTML() {
return '' +
'<div class="wolai-page-ai-pi-lab-shell" data-page-ai-pi-lab="panel">' +
'<div class="wolai-page-ai-pi-lab-topbar">' +
'<div class="wolai-page-ai-pi-lab-brand">' +
'<div class="wolai-page-ai-pi-lab-logo">π</div>' +
'<div class="wolai-page-ai-pi-lab-title" data-page-ai-pi-lab-title><strong>Pi Lab 平台</strong><span data-page-ai-pi-lab-model>默认模型:omniroute/freefirst</span></div>' +
'</div>' +
'<button type="button" class="wolai-page-ai-pi-lab-icon-btn" data-page-ai-pi-lab-minimize aria-label="最小化 Pi Lab"></button>' +
'<button type="button" class="wolai-page-ai-pi-lab-icon-btn" data-page-ai-pi-lab-close aria-label="关闭 Pi Lab">×</button>' +
'</div>' +
'<div class="wolai-page-ai-pi-lab-commandbar" data-page-ai-pi-lab-commandbar>' +
'<button type="button" class="wolai-page-ai-pi-lab-tool-icon" data-page-ai-pi-lab-new title="新对话" aria-label="新对话">⊞</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-tool-icon" data-page-ai-pi-lab-history title="历史" aria-label="历史">◷</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-tool-icon" data-page-ai-pi-lab-open title="打开" aria-label="打开">↗</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-tool-icon" data-page-ai-pi-lab-clock title="运行状态" aria-label="运行状态">◴</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-tool-icon" data-page-ai-pi-lab-notify title="通知" aria-label="通知">♧</button>' +
'<span class="wolai-page-ai-pi-lab-commandbar-spacer"></span>' +
'<button type="button" class="wolai-page-ai-pi-lab-tool-icon" data-page-ai-pi-lab-btn-start title="启动 Pi runtime" aria-label="启动 Pi runtime">▶</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-tool-icon" data-page-ai-pi-lab-btn-abort title="中止" aria-label="中止" hidden>■</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-tool-icon" data-page-ai-pi-lab-btn-clear title="清空" aria-label="清空">⌫</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-tool-icon" data-page-ai-pi-lab-open-settings title="设置" aria-label="设置">⚙</button>' +
'</div>' +
'<div class="wolai-page-ai-pi-lab-config">' +
'<details class="wolai-page-ai-pi-lab-settings" data-page-ai-pi-lab-settings>' +
'<summary><span class="wolai-page-ai-pi-lab-chip" data-page-ai-pi-lab-status-chip><strong data-page-ai-pi-lab-status-text>idle</strong></span><span>设置</span><strong data-page-ai-pi-lab-model-chip>omniroute/freefirst</strong></summary>' +
'<div class="wolai-page-ai-pi-lab-settings-grid">' +
'<label class="wolai-page-ai-pi-lab-field">Provider<select data-page-ai-pi-lab-model-provider><option value="omniroute">Omniroute</option></select></label>' +
'<label class="wolai-page-ai-pi-lab-field">Model<select data-page-ai-pi-lab-model-id><option value="freefirst">freefirst</option></select></label>' +
'<label class="wolai-page-ai-pi-lab-field">自定义模型<input data-page-ai-pi-lab-model-custom placeholder="可选,重启后生效"></label>' +
'<span class="wolai-page-ai-pi-lab-chip"><span>runtime</span><strong data-page-ai-pi-lab-runtime-chip>runtime pending</strong></span>' +
'<span class="wolai-page-ai-pi-lab-chip" data-page-ai-pi-lab-builtin-disabled><span>builtin disabled</span><strong>bash/read/write/edit</strong></span>' +
'</div>' +
'</details>' +
'<div class="wolai-page-ai-pi-lab-actions">' +
'<span class="wolai-page-ai-pi-lab-chip"><span>操作</span><strong>见顶部工具栏</strong></span>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-pi-lab-history-panel" data-page-ai-pi-lab-history-panel hidden>' +
'<div class="wolai-page-ai-pi-lab-empty">加载历史...</div>' +
'</div>' +
'<details class="wolai-page-ai-pi-lab-context-strip" data-page-ai-pi-lab-context-strip>' +
'<summary><span class="wolai-page-ai-pi-lab-context-title">上下文</span><span class="wolai-page-ai-pi-lab-context-current" data-page-ai-pi-lab-current-page>未绑定</span><span class="wolai-page-ai-pi-lab-context-count"><strong data-page-ai-pi-lab-changed-files>0</strong> changed</span></summary>' +
'<div class="wolai-page-ai-pi-lab-context-content">' +
'<span class="wolai-page-ai-pi-lab-chip" data-page-ai-pi-lab-context="selection"><span>选区</span><strong data-page-ai-pi-lab-selection>无选区</strong></span>' +
'<span class="wolai-page-ai-pi-lab-chip" data-page-ai-pi-lab-context="allowed-roots"><span>allowed roots</span><strong data-page-ai-pi-lab-allowed-roots>pending</strong></span>' +
'<span class="wolai-page-ai-pi-lab-chip" data-page-ai-pi-lab-context="lightrag"><span>LightRAG</span><strong data-page-ai-pi-lab-lightrag>default</strong></span>' +
'</div>' +
'</details>' +
'<div class="wolai-page-ai-pi-lab-body">' +
'<div class="wolai-page-ai-pi-lab-main">' +
'<div class="wolai-page-ai-pi-lab-messages" data-page-ai-pi-lab-messages></div>' +
'<div class="wolai-page-ai-pi-lab-composer">' +
'<textarea class="wolai-page-ai-pi-lab-input" data-page-ai-pi-lab-input placeholder="问 Pi..." rows="3"></textarea>' +
'<div class="wolai-page-ai-pi-lab-composer-bar">' +
'<button type="button" class="wolai-page-ai-pi-lab-quick" data-page-ai-pi-lab-quick="read-page">当前页</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-quick" data-page-ai-pi-lab-quick="selection">选区</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-quick" data-page-ai-pi-lab-quick="rag">LightRAG</button>' +
'<span class="wolai-page-ai-pi-lab-spacer"></span>' +
'<button type="button" class="wolai-page-ai-pi-lab-send" data-page-ai-pi-lab-btn-send disabled aria-label="发送">↑</button>' +
'</div>' +
'<div class="wolai-page-ai-pi-lab-modebar" data-page-ai-pi-lab-modebar>' +
'<button type="button" class="wolai-page-ai-pi-lab-square" data-page-ai-pi-lab-new-small title="新对话">+</button>' +
'<span class="wolai-page-ai-pi-lab-segment"><button type="button" data-active="true">Build</button><button type="button">Plan</button></span>' +
'<select class="wolai-page-ai-pi-lab-model-control" data-page-ai-pi-lab-model-id-bottom><option value="freefirst">omniroute/freefirst</option></select>' +
'<button type="button" class="wolai-page-ai-pi-lab-square" data-page-ai-pi-lab-lightning title="快速">ϟ</button>' +
'<span class="wolai-page-ai-pi-lab-budget">不限</span>' +
'<button type="button" class="wolai-page-ai-pi-lab-square" data-page-ai-pi-lab-quick="read-page" title="当前页">▤</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-square" data-page-ai-pi-lab-quick="selection" title="选区">▣</button>' +
'</div>' +
'</div>' +
'</div>' +
'<aside class="wolai-page-ai-pi-lab-rail" data-page-ai-pi-lab-rail>' +
'<details class="wolai-page-ai-pi-lab-rail-section">' +
'<summary>MNote tools <span class="wolai-page-ai-pi-lab-rail-count">5</span></summary>' +
'<div class="wolai-page-ai-pi-lab-tool-list" data-page-ai-pi-lab-tool-list>' +
'<span class="wolai-page-ai-pi-lab-tool-pill">mnote.current_page.read</span>' +
'<span class="wolai-page-ai-pi-lab-tool-pill">mnote.selection.read</span>' +
'<span class="wolai-page-ai-pi-lab-tool-pill">mnote.local_file.patch</span>' +
'<span class="wolai-page-ai-pi-lab-tool-pill">mnote.knowledge_rag.query</span>' +
'<span class="wolai-page-ai-pi-lab-tool-pill">mnote.reference.open</span>' +
'</div>' +
'</details>' +
'<details class="wolai-page-ai-pi-lab-rail-section" data-page-ai-pi-lab-receipts-section>' +
'<summary>Receipts <span class="wolai-page-ai-pi-lab-rail-count" data-page-ai-pi-lab-receipt-count>0</span></summary>' +
'<div class="wolai-page-ai-pi-lab-receipts" data-page-ai-pi-lab-receipts></div>' +
'</details>' +
'</aside>' +
'</div>' +
'<details class="wolai-page-ai-pi-lab-diagnostics" data-page-ai-pi-lab-diagnostics>' +
'<summary>诊断</summary>' +
'<pre data-page-ai-pi-lab-diag-text></pre>' +
'</details>' +
'</div>';
}
function renderEmptyState(container) {
container.innerHTML = '' +
'<div class="wolai-page-ai-pi-lab-empty">' +
'<div class="wolai-page-ai-pi-lab-empty-icon">?</div>' +
'<strong>开始对话</strong>' +
'<p>试试以下示例,或直接输入您的指令</p>' +
'<div class="wolai-page-ai-pi-lab-examples">' +
'<button type="button" class="wolai-page-ai-pi-lab-example" data-page-ai-pi-lab-example="帮我解释当前页面的逻辑"><span>▤</span>帮我解释当前页面的逻辑</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-example" data-page-ai-pi-lab-example="如何优化这段内容的结构?"><span>?</span>如何优化这段内容的结构?</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-example" data-page-ai-pi-lab-example="用 LightRAG 查询当前页面相关资料"><span>▥</span>用 LightRAG 查询当前页面相关资料</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-example" data-page-ai-pi-lab-example="帮我把选区改写成更清晰的版本"><span>↗</span>帮我把选区改写成更清晰的版本</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-example" data-page-ai-pi-lab-example="检查当前 Markdown 有什么问题"><span>⌫</span>检查当前 Markdown 有什么问题</button>' +
'</div>' +
'</div>';
}
function renderHistory() {
if (!piLabPanelEl) return;
var panel = piLabPanelEl.querySelector('[data-page-ai-pi-lab-history-panel]');
if (!panel) return;
if (!piLabState.history.length) {
panel.innerHTML = '<div class="wolai-page-ai-pi-lab-empty">暂无 Pi Lab 历史</div>';
return;
}
panel.innerHTML = piLabState.history.map(function (session) {
var title = escapeHtml(session.title || session.preview || session.pageTitle || '未命名会话');
var meta = escapeHtml([session.status, session.modelId, session.updatedAt].filter(Boolean).join(' · '));
return '<button type="button" class="wolai-page-ai-pi-lab-history-row" data-page-ai-pi-lab-history-session="' +
escapeHtml(session.sessionId || '') + '"><span><strong>' + title + '</strong><span>' + meta +
'</span></span><span>打开</span></button>';
}).join('');
}
function loadHistory() {
return fetch(API.SESSIONS + '?limit=50', { credentials: 'same-origin', cache: 'no-store' })
.then(function (response) {
if (!response.ok) throw new Error('History failed: ' + response.status);
return response.json();
})
.then(function (data) {
piLabState.history = Array.isArray(data.sessions) ? data.sessions : [];
renderHistory();
return data;
});
}
function openHistorySession(sessionId) {
if (!sessionId) return Promise.resolve();
return Promise.all([
fetch(API.SESSIONS + '/' + encodeURIComponent(sessionId), { credentials: 'same-origin', cache: 'no-store' }).then(function (r) { return r.json(); }),
fetch(API.SESSIONS + '/' + encodeURIComponent(sessionId) + '/events?limit=500', { credentials: 'same-origin', cache: 'no-store' }).then(function (r) { return r.json(); }),
]).then(function (results) {
var detail = results[0] && results[0].session || {};
var events = results[1] && Array.isArray(results[1].events) ? results[1].events : [];
piLabState.sessionId = detail.sessionId || sessionId;
piLabState.session = detail.runtime || detail;
piLabState.messages = [];
events.forEach(function (event) {
var payload = event.payload || {};
if (event.eventType === 'user_prompt' && payload.message) {
piLabState.messages.push({ role: 'user', text: payload.message });
}
if (event.eventType === 'pi_rpc_event') {
var delta = payload.assistantMessageEvent && payload.assistantMessageEvent.delta;
if (delta) piLabState.messages.push({ role: 'assistant', text: delta });
}
});
updateMessages();
updateConfig();
});
}
function renderMessage(msg) {
var card = el('article', {
className: 'wolai-page-ai-pi-lab-message',
'data-role': msg.role || 'assistant',
'data-page-ai-pi-lab-message-role': msg.role || 'assistant',
});
if (msg.status === 'streaming') card.setAttribute('data-page-ai-pi-lab-streaming', 'true');
var role = msg.role === 'assistant' ? 'Pi' : (msg.role === 'user' ? 'You' : 'System');
var meta = msg.status === 'streaming' ? 'streaming' : (msg.meta || '');
card.appendChild(el('div', { className: 'wolai-page-ai-pi-lab-role' }, role + (meta ? ' · ' + meta : '')));
var bubble = el('div', { className: 'wolai-page-ai-pi-lab-bubble' });
if (msg.text) {
bubble.appendChild(el('div', {
className: 'wolai-page-ai-pi-lab-text' + (msg.status === 'streaming' ? ' streaming-cursor' : ''),
innerHTML: escapeHtml(msg.text).replace(/\n/g, '<br>'),
}));
}
if (msg.toolCalls && msg.toolCalls.length) {
var tools = el('div', { className: 'wolai-page-ai-pi-lab-tool-calls' });
msg.toolCalls.forEach(function (tc) {
var item = el('div', { className: 'wolai-page-ai-pi-lab-tool-call', 'data-page-ai-pi-lab-tool-call': tc.name || 'tool' });
item.appendChild(el('div', { className: 'wolai-page-ai-pi-lab-tool-call-header', innerHTML: '<strong>' + escapeHtml(tc.name || 'tool') + '</strong><span>' + escapeHtml(tc.status || '') + '</span>' }));
if (tc.args) item.appendChild(el('pre', { textContent: JSON.stringify(tc.args, null, 2) }));
if (tc.result) item.appendChild(el('pre', { textContent: typeof tc.result === 'string' ? tc.result : JSON.stringify(tc.result, null, 2) }));
tools.appendChild(item);
});
bubble.appendChild(tools);
}
if (msg.citations && msg.citations.length) {
var citations = el('div', { className: 'wolai-page-ai-pi-lab-citations' });
msg.citations.forEach(function (cit, i) {
citations.appendChild(el('a', {
className: 'wolai-page-ai-pi-lab-citation',
href: cit.url || '#',
target: '_blank',
rel: 'noopener',
'data-page-ai-pi-lab-citation': cit.source || '',
}, '[' + (i + 1) + '] ' + (cit.title || cit.source || 'source')));
});
bubble.appendChild(citations);
}
if (msg.diffSummary) {
var files = msg.diffSummary.files || [];
bubble.appendChild(el('div', { className: 'wolai-page-ai-pi-lab-diff', 'data-page-ai-pi-lab-diff': 'true' }, 'patch · ' + files.join(', ')));
}
card.appendChild(bubble);
return card;
}
function isVisibleMessage(msg) {
if (!msg) return false;
if (msg.status === 'streaming') return true;
if (String(msg.text || '').trim()) return true;
if (msg.toolCalls && msg.toolCalls.length) return true;
if (msg.citations && msg.citations.length) return true;
if (msg.diffSummary) return true;
return msg.role !== 'assistant';
}
function updateStatusBadge() {
updateConfig();
}
function updateModelLabel() {
if (!piLabPanelEl) return;
var titleLabel = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model]');
var modelChip = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-chip]');
var providerSelect = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-provider]');
var modelSelect = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-id]');
var bottomModelSelect = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-id-bottom]');
var customInput = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-custom]');
var provider = piLabState.modelProvider || piLabState.defaultModelProvider || DEFAULT_MODEL_PROVIDER;
var modelId = piLabState.modelId || piLabState.defaultModelId || DEFAULT_MODEL_ID;
if (String(modelId).indexOf(provider + '/') === 0) {
modelId = String(modelId).slice(provider.length + 1);
piLabState.modelId = modelId;
}
if (titleLabel) titleLabel.textContent = '默认模型:' + currentModelLabel();
if (modelChip) modelChip.textContent = currentModelLabel();
if (providerSelect && providerSelect.value !== provider) providerSelect.value = provider;
if (modelSelect && modelSelect.value !== modelId && Array.prototype.some.call(modelSelect.options, function (option) { return option.value === modelId; })) {
modelSelect.value = modelId;
}
if (bottomModelSelect && bottomModelSelect.value !== modelId && Array.prototype.some.call(bottomModelSelect.options, function (option) { return option.value === modelId; })) {
bottomModelSelect.value = modelId;
}
if (customInput && modelId !== DEFAULT_MODEL_ID && modelId !== 'freefirst') customInput.value = modelId;
}
function updateConfig() {
if (!piLabPanelEl) return;
updateModelLabel();
var statusChip = piLabPanelEl.querySelector('[data-page-ai-pi-lab-status-chip]');
var statusNode = piLabPanelEl.querySelector('[data-page-ai-pi-lab-status-text]');
var runtimeNode = piLabPanelEl.querySelector('[data-page-ai-pi-lab-runtime-chip]');
var builtinNode = piLabPanelEl.querySelector('[data-page-ai-pi-lab-builtin-disabled]');
if (statusChip) {
var tone = statusTone();
if (tone) statusChip.setAttribute('data-tone', tone);
else statusChip.removeAttribute('data-tone');
}
if (statusNode) statusNode.textContent = statusText();
if (runtimeNode) runtimeNode.textContent = currentRuntimeLabel();
if (builtinNode) builtinNode.style.display = piLabState.disabledBuiltinTools ? '' : 'none';
updateContextStrip();
updateButtons();
updateLauncherState();
}
function updateContextStrip() {
if (!piLabPanelEl) return;
var currentPage = piLabPanelEl.querySelector('[data-page-ai-pi-lab-current-page]');
var selection = piLabPanelEl.querySelector('[data-page-ai-pi-lab-selection]');
var allowedRoots = piLabPanelEl.querySelector('[data-page-ai-pi-lab-allowed-roots]');
var lightrag = piLabPanelEl.querySelector('[data-page-ai-pi-lab-lightrag]');
var changedFiles = piLabPanelEl.querySelector('[data-page-ai-pi-lab-changed-files]');
var session = piLabState.session || {};
if (currentPage) currentPage.textContent = session.pageTitle || session.pagePath || document.title || '当前页面';
if (selection) selection.textContent = piLabState.selectionSummary || '无选区';
if (allowedRoots) allowedRoots.textContent = piLabState.allowedRootsSummary || 'pending';
if (lightrag) lightrag.textContent = '唯一默认';
if (changedFiles) {
var files = {};
piLabState.changedFiles.forEach(function (file) { if (file) files[file] = true; });
piLabState.messages.forEach(function (msg) {
if (msg && msg.diffSummary && Array.isArray(msg.diffSummary.files)) {
msg.diffSummary.files.forEach(function (file) { if (file) files[file] = true; });
}
});
changedFiles.textContent = String(Object.keys(files).length);
}
}
function updateButtons() {
if (!piLabPanelEl) return;
var startBtn = piLabPanelEl.querySelector('[data-page-ai-pi-lab-btn-start]');
var sendBtn = piLabPanelEl.querySelector('[data-page-ai-pi-lab-btn-send]');
var abortBtn = piLabPanelEl.querySelector('[data-page-ai-pi-lab-btn-abort]');
var input = piLabPanelEl.querySelector('[data-page-ai-pi-lab-input]');
var providerSelect = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-provider]');
var modelSelect = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-id]');
var bottomModelSelect = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-id-bottom]');
var customInput = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-custom]');
var isIdle = piLabState.status === STATE_IDLE || piLabState.status === STATE_ABORTED || piLabState.status === STATE_ERROR;
var isStarted = piLabState.status === STATE_STARTED;
var isStreaming = piLabState.status === STATE_STREAMING || piLabState.status === STATE_STARTING;
if (startBtn) {
startBtn.hidden = !isIdle;
startBtn.disabled = !piLabState.enabled || piLabState.status === STATE_STARTING;
}
if (sendBtn) {
sendBtn.disabled = !piLabState.enabled || !(isStarted || piLabState.status === STATE_IDLE || piLabState.status === STATE_ABORTED || piLabState.status === STATE_ERROR) || !input || !input.value.trim();
}
if (abortBtn) abortBtn.hidden = !isStreaming;
[providerSelect, modelSelect, bottomModelSelect, customInput].forEach(function (node) {
if (node) node.disabled = !isIdle;
});
}
function updateMessages() {
var container = piLabPanelEl && piLabPanelEl.querySelector('[data-page-ai-pi-lab-messages]');
if (!container) return;
container.innerHTML = '';
var visibleMessages = piLabState.messages.filter(isVisibleMessage);
if (!visibleMessages.length) {
renderEmptyState(container);
return;
}
visibleMessages.forEach(function (msg) { container.appendChild(renderMessage(msg)); });
container.scrollTop = container.scrollHeight;
updateContextStrip();
}
function updateDiagnostics(text) {
piLabState.diagnostics = text || piLabState.diagnostics || '';
if (!piLabPanelEl) return;
var pre = piLabPanelEl.querySelector('[data-page-ai-pi-lab-diag-text]');
if (!pre) return;
var lines = [];
lines.push('session=' + (piLabState.sessionId || 'none'));
lines.push('providerSession=' + (piLabState.providerSessionId || 'none'));
lines.push('model=' + currentModelLabel());
lines.push('runtime=' + currentRuntimeLabel());
lines.push('disabledBuiltinTools=' + (piLabState.disabledBuiltinTools ? 'bash/read/write/edit' : 'pending'));
if (piLabState.allowedRootsSummary) lines.push('allowedRoots=' + piLabState.allowedRootsSummary);
if (piLabState.diagnostics) lines.push('message=' + piLabState.diagnostics);
pre.textContent = lines.join('\n');
}
function updateBuiltinDisabled() {
updateConfig();
}
function updateReceiptDisplay() {
if (!piLabPanelEl) return;
var receipts = piLabPanelEl.querySelector('[data-page-ai-pi-lab-receipts]');
var count = piLabPanelEl.querySelector('[data-page-ai-pi-lab-receipt-count]');
if (!receipts) return;
if (count) count.textContent = String(piLabState.receipts.length);
receipts.innerHTML = '';
if (!piLabState.receipts.length) {
receipts.appendChild(el('div', { className: 'wolai-page-ai-pi-lab-receipt' }, 'No tool receipt yet'));
return;
}
piLabState.receipts.slice(-8).reverse().forEach(function (receipt) {
var label = (receipt.toolName || 'tool') + ' · ' + (receipt.allowed === false ? 'denied' : 'allowed');
if (receipt.denyReason) label += ' · ' + receipt.denyReason;
if (receipt.diffSummary) label += ' · diff ' + receipt.diffSummary;
if (receipt.citationCount) label += ' · citations ' + receipt.citationCount;
if (receipt.afterFileVersion) label += ' · v ' + String(receipt.afterFileVersion).slice(0, 8);
receipts.appendChild(el('div', {
className: 'wolai-page-ai-pi-lab-receipt',
'data-allowed': receipt.allowed === false ? 'false' : 'true',
}, label));
});
}
function setState(newState) {
piLabState.status = newState;
updateConfig();
}
function checkStatus() {
return Promise.all([
fetch(API.STATUS, { credentials: 'same-origin', cache: 'no-store' }).then(function (r) { return r.json(); }),
loadEffectiveConfig().catch(function () { return null; }),
])
.then(function (results) {
var data = results[0] || {};
piLabState.enabled = !!data.enabled;
piLabState.sessionId = data.sessionId || piLabState.sessionId || null;
piLabState.providerSessionId = data.providerSessionId || piLabState.providerSessionId || null;
// Consume backend status/default fields when present.
if (data.defaultModelProvider) piLabState.defaultModelProvider = data.defaultModelProvider;
if (data.defaultModelId) piLabState.defaultModelId = data.defaultModelId;
piLabState.modelProvider = (data.session && data.session.modelProvider) || piLabState.modelProvider || null;
piLabState.modelId = (data.session && data.session.modelId) || piLabState.modelId || null;
piLabState.session = data.session || piLabState.session || null;
piLabState.allowedRootsSummary = summarizeAllowedRoots(data.session && data.session.allowedRootsSnapshot);
piLabState.runtimeMode = data.runtimeMode || (data.session && data.session.runtimeMode) || piLabState.runtimeMode || null;
piLabState.runtimePid = data.pid || (data.session && data.session.runtimePid) || piLabState.runtimePid || null;
piLabState.disabledBuiltinTools = data.disabledPiBuiltinTools || piLabState.disabledBuiltinTools || null;
if (data.session && data.session.status === 'runtime_running') {
piLabState.sessionId = data.session.sessionId;
setState(STATE_STARTED);
if (!piLabEventSource || piLabEventSource.readyState === EventSource.CLOSED) connectEventSource(data.session.sessionId);
}
updateBuiltinDisabled();
updateReceiptDisplay();
updateDiagnostics('');
updateMessages();
return data;
})
.catch(function (error) {
piLabState.enabled = false;
setState(STATE_ERROR);
updateDiagnostics(error && error.message ? error.message : 'status failed');
});
}
function startRuntime() {
if (!piLabState.enabled || piLabState.status === STATE_STARTING || piLabState.status === STATE_STREAMING) return Promise.resolve({ skipped: true });
var context = applyCurrentContextToState();
applyModelControls();
setState(STATE_STARTING);
updateDiagnostics('Starting Pi runtime');
return fetch(API.START, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rootUri: context.rootUri || undefined,
workspaceId: context.workspaceId || undefined,
pagePath: context.pagePath || undefined,
pageTitle: context.pageTitle || undefined,
modelProvider: piLabState.modelProvider || piLabState.defaultModelProvider,
modelId: piLabState.modelId || piLabState.defaultModelId,
}),
})
.then(function (r) {
if (!r.ok) throw new Error('Start failed: ' + r.status);
return r.json();
})
.then(function (data) {
var session = data.session || {};
piLabState.sessionId = session.sessionId || data.sessionId || piLabState.sessionId;
piLabState.providerSessionId = session.providerSessionId || data.providerSessionId || piLabState.providerSessionId;
piLabState.modelProvider = session.modelProvider || piLabState.modelProvider || null;
piLabState.modelId = session.modelId || piLabState.modelId || null;
piLabState.runtimeMode = session.runtimeMode || data.runtimeMode || piLabState.runtimeMode || null;
piLabState.runtimePid = session.runtimePid || data.pid || piLabState.runtimePid || null;
piLabState.disabledBuiltinTools = data.disabledPiBuiltinTools || piLabState.disabledBuiltinTools || null;
connectEventSource();
setState(STATE_STARTED);
updateDiagnostics('Pi runtime ready');
piLabState.messages.push({ role: 'system', text: 'Pi session started: ' + (piLabState.sessionId || 'session') });
updateMessages();
return data;
})
.catch(function (err) {
setState(STATE_ERROR);
updateDiagnostics('Start error: ' + err.message);
throw err;
});
}
function connectEventSource(sessionId) {
if (piLabEventSource) {
piLabEventSource.close();
piLabEventSource = null;
}
var sid = sessionId || piLabState.sessionId;
if (!sid) return;
var es = new EventSource(API.EVENTS + '?sessionId=' + encodeURIComponent(sid), { withCredentials: true });
es.addEventListener('connected', function () { updateDiagnostics('SSE connected'); });
es.addEventListener('runtime_started', function (e) {
try {
var data = JSON.parse(e.data);
var payload = data.payload || data;
piLabState.runtimePid = payload.pid || piLabState.runtimePid;
piLabState.runtimeMode = payload.runtimeMode || piLabState.runtimeMode;
piLabState.disabledBuiltinTools = payload.disabledBuiltinTools || piLabState.disabledBuiltinTools;
setState(STATE_STARTED);
updateDiagnostics('runtime_started');
} catch (_) {}
});
es.addEventListener('pi_rpc_event', function (e) {
try {
var data = JSON.parse(e.data);
handlePiRpcEvent(data.payload || data);
} catch (_) {}
});
es.addEventListener('tool_call', function (e) {
try {
var data = JSON.parse(e.data);
var payload = data.payload || data;
applyToolCallSideEffects({
ok: payload.allowed !== false,
toolName: payload.toolName,
result: {
rootUri: payload.rootUri,
relativePath: payload.relativePath,
diffSummary: payload.diffSummary,
},
});
piLabState.receipts.push({
toolName: payload.toolName,
allowed: payload.allowed !== false,
denyReason: payload.denyReason || null,
diffSummary: payload.diffSummary || null,
citationCount: payload.citationCount || 0,
beforeFileVersion: payload.beforeFileVersion || null,
afterFileVersion: payload.afterFileVersion || null,
});
if (payload.normalizedFilePath && payload.diffSummary) piLabState.changedFiles.push(payload.normalizedFilePath);
updateReceiptDisplay();
updateContextStrip();
} catch (_) {}
});
es.addEventListener('runtime_aborted', function () {
setState(STATE_ABORTED);
if (streamingAssistantMsg) {
streamingAssistantMsg.status = 'done';
streamingAssistantMsg = null;
}
updateMessages();
});
es.addEventListener('error', function () {
if (piLabEventSource && piLabEventSource.readyState === EventSource.CLOSED) updateDiagnostics('SSE connection closed');
});
piLabEventSource = es;
}
function ensureStreamingAssistant() {
if (!streamingAssistantMsg) {
streamingAssistantMsg = { role: 'assistant', text: '', toolCalls: [], citations: [], diffSummary: null, status: 'streaming' };
piLabState.messages.push(streamingAssistantMsg);
setState(STATE_STREAMING);
}
return streamingAssistantMsg;
}
function extractVisibleAssistantText(message) {
if (!message || !Array.isArray(message.content)) return '';
return message.content
.filter(function (part) { return part && part.type === 'text' && typeof part.text === 'string'; })
.map(function (part) { return part.text; })
.join('');
}
function handlePiRpcEvent(payload) {
if (!payload) return;
var eventType = payload.type;
var msgData = payload.assistantMessageEvent || payload;
var msgDataType = msgData && msgData.type;
if (msgDataType === 'thinking_start' || msgDataType === 'thinking_delta' || msgDataType === 'thinking_end') {
updateDiagnostics('Pi thinking event hidden from visible reply');
return;
}
if (msgDataType === 'text_start') {
ensureStreamingAssistant();
updateMessages();
return;
}
if (msgDataType === 'text_delta') {
var msg = ensureStreamingAssistant();
if (msgData.delta) msg.text += msgData.delta;
if (msgData.text && !msgData.delta) msg.text = msgData.text;
updateMessages();
} else if (msgDataType === 'text_end') {
var textEndMsg = ensureStreamingAssistant();
textEndMsg.text = msgData.content || textEndMsg.text;
updateMessages();
} else if (eventType === 'message_end' && payload.message && payload.message.role === 'assistant') {
var messageEndMsg = ensureStreamingAssistant();
var visibleText = extractVisibleAssistantText(payload.message);
if (visibleText) messageEndMsg.text = visibleText;
messageEndMsg.status = 'done';
streamingAssistantMsg = null;
setState(STATE_STARTED);
updateMessages();
} else if (eventType === 'agent_end') {
var finalMessages = Array.isArray(payload.messages) ? payload.messages : [];
var finalAssistant = finalMessages.slice().reverse().find(function (message) { return message && message.role === 'assistant'; });
var finalText = extractVisibleAssistantText(finalAssistant);
var lastVisibleAssistant = piLabState.messages.slice().reverse().find(function (message) { return message && message.role === 'assistant'; });
if (!streamingAssistantMsg && lastVisibleAssistant && lastVisibleAssistant.status === 'done') {
if (finalText && !lastVisibleAssistant.text) {
lastVisibleAssistant.text = finalText;
updateMessages();
}
setState(STATE_STARTED);
return;
}
var agentEndMsg = ensureStreamingAssistant();
if (finalText) agentEndMsg.text = finalText;
agentEndMsg.status = 'done';
streamingAssistantMsg = null;
setState(STATE_STARTED);
updateMessages();
} else if (eventType === 'tool_call_start' || msgDataType === 'tool_call_start') {
var startMsg = ensureStreamingAssistant();
startMsg.toolCalls.push({ name: payload.toolName || payload.name || 'tool', args: payload.args || payload.params || {}, status: 'running' });
updateMessages();
} else if (eventType === 'tool_call_end' || msgDataType === 'tool_call_end') {
var endMsg = ensureStreamingAssistant();
var last = endMsg.toolCalls[endMsg.toolCalls.length - 1];
if (last) {
last.status = 'done';
last.result = payload.result || msgData.result || {};
}
updateMessages();
} else if (eventType === 'citation' || msgDataType === 'citation') {
var citMsg = ensureStreamingAssistant();
citMsg.citations.push({ source: payload.source || msgData.source || '', title: payload.title || msgData.title || '', url: payload.url || msgData.url || '#' });
updateMessages();
} else if (eventType === 'diff' || msgDataType === 'diff') {
var diffMsg = ensureStreamingAssistant();
diffMsg.diffSummary = diffMsg.diffSummary || { files: [] };
diffMsg.diffSummary.files = payload.files || msgData.files || [];
diffMsg.diffSummary.files.forEach(function (file) { if (file) piLabState.changedFiles.push(file); });
updateMessages();
} else if (eventType === 'done' || msgDataType === 'done') {
var doneMsg = ensureStreamingAssistant();
doneMsg.status = 'done';
if (msgData.text) doneMsg.text = msgData.text;
if (msgData.toolCalls) doneMsg.toolCalls = msgData.toolCalls;
if (msgData.citations) doneMsg.citations = msgData.citations;
if (msgData.diffSummary) doneMsg.diffSummary = msgData.diffSummary;
streamingAssistantMsg = null;
setState(STATE_STARTED);
updateMessages();
} else if (eventType === 'error' || msgDataType === 'error') {
var errMsg = ensureStreamingAssistant();
errMsg.text += (errMsg.text ? '\n' : '') + '错误: ' + (payload.error || msgData.message || 'Pi runtime error');
errMsg.status = 'done';
streamingAssistantMsg = null;
setState(STATE_STARTED);
updateMessages();
}
}
function sendPrompt(text) {
var prompt = String(text || '').trim();
if (!prompt) return;
piLabState.messages.push({ role: 'user', text: prompt });
streamingAssistantMsg = { role: 'assistant', text: '', toolCalls: [], citations: [], diffSummary: null, status: 'streaming' };
piLabState.messages.push(streamingAssistantMsg);
setState(STATE_STREAMING);
updateMessages();
return fetch(API.SEND, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: piLabState.sessionId, message: prompt }),
})
.then(function (r) {
if (!r.ok) throw new Error('Send failed: ' + r.status);
return r.json();
})
.then(function (data) {
if (!data.accepted) throw new Error('Pi Lab rejected prompt');
if (!piLabEventSource || piLabEventSource.readyState === EventSource.CLOSED) connectEventSource(data.sessionId || piLabState.sessionId);
})
.catch(function (err) {
if (streamingAssistantMsg) {
streamingAssistantMsg.text = '错误: ' + err.message;
streamingAssistantMsg.status = 'done';
streamingAssistantMsg = null;
setState(STATE_STARTED);
updateMessages();
}
});
}
function ensureRuntimeReady() {
if (piLabState.status === STATE_IDLE || piLabState.status === STATE_ABORTED || piLabState.status === STATE_ERROR || !piLabState.sessionId) {
return startRuntime();
}
return Promise.resolve({ ok: true });
}
function callMNoteTool(toolName, params) {
return ensureRuntimeReady().then(function () {
return fetch('/api/page-ai/pi/tool-call', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', accept: 'application/json' },
body: JSON.stringify({
sessionId: piLabState.sessionId,
toolName: toolName,
params: params || {},
}),
});
}).then(function (response) {
return response.json().catch(function () { return {}; }).then(function (payload) {
if (!response.ok) throw new Error(payload.message || ('Tool call failed: ' + response.status));
return payload;
});
});
}
function emitLocalFolderRefresh(rootUri, relativePath, source) {
var normalizedPath = normalizeSlashes(relativePath).replace(/^\/+/, '');
if (!rootUri || !normalizedPath) return false;
var documentId = /\.md(?:own)?$/i.test(normalizedPath) ? 'local-md:' + normalizedPath.replace(/\//g, '~2F') : '';
if (window.__mnoteLocalFolderEventBus && typeof window.__mnoteLocalFolderEventBus.emitChangedFiles === 'function') {
window.__mnoteLocalFolderEventBus.emitChangedFiles({
source: source || 'pi_lab_tool_call',
reason: source || 'pi_lab_tool_call',
rootUri: rootUri,
workspaceId: (piLabState.session && piLabState.session.workspaceId) || currentWorkspaceId(),
changedFiles: [{
relativePath: normalizedPath,
documentId: documentId,
changeType: 'modified',
}],
affectedParents: [{
relativePath: normalizedPath.indexOf('/') >= 0 ? normalizedPath.split('/').slice(0, -1).join('/') : '',
reason: source || 'pi_lab_tool_call',
}],
});
return true;
}
window.dispatchEvent(new CustomEvent('tree:local-folder-watch-batch', {
detail: {
payload: {
schema: 'mnote.local_folder.watch_batch.v1',
source: source || 'pi_lab_tool_call',
rootUri: rootUri,
changedPaths: [{ relativePath: normalizedPath, documentId: documentId, changeType: 'modified' }],
},
},
}));
return true;
}
function refreshPatchedCurrentDocument(rootUri, relativePath, source) {
emitLocalFolderRefresh(rootUri, relativePath, source || 'pi_lab_patch');
var documentId = /\.md(?:own)?$/i.test(String(relativePath || '')) ? 'local-md:' + normalizeSlashes(relativePath).replace(/^\/+/, '').replace(/\//g, '~2F') : currentDocumentId();
var api = window.__mnoteDocumentPaneRuntime;
if (api && typeof api.refreshDocument === 'function') {
api.refreshDocument({
documentId: documentId,
workspaceId: (piLabState.session && piLabState.session.workspaceId) || currentWorkspaceId(),
source: source || 'pi_lab_patch',
}).catch(function () {});
}
}
function applyToolCallSideEffects(payload) {
if (!payload || payload.ok !== true) return;
var result = payload.result || {};
if (payload.toolName === 'mnote.local_file.patch' || result.diffSummary) {
var rootUri = String(result.rootUri || (piLabState.session && piLabState.session.rootUri) || currentRootUri() || '').trim();
var relativePath = String(result.relativePath || '').trim();
if (!relativePath && result.path) relativePath = result.path;
if (relativePath) refreshPatchedCurrentDocument(rootUri, relativePath, 'pi_lab_local_file_patch');
}
}
function setComposerText(text) {
if (!piLabPanelEl) return;
var input = piLabPanelEl.querySelector('[data-page-ai-pi-lab-input]');
if (!input) return;
input.value = text || '';
updateButtons();
input.focus();
}
function handleQuickAction(kind) {
var context = applyCurrentContextToState();
if (kind === 'read-page') {
if (!context.rootUri || !context.pagePath) {
setComposerText('读取当前页面,总结页面要点。');
updateDiagnostics('当前页缺少 rootUri/pagePath,无法调用 mnote.current_page.read');
return;
}
callMNoteTool('mnote.current_page.read', {
rootUri: context.rootUri,
pagePath: context.pagePath,
}).then(function (payload) {
var result = payload.result || {};
var content = String(result.content || '').slice(0, 12000);
piLabState.messages.push({
role: 'system',
text: '已读取当前页:' + (context.pageTitle || context.pagePath) + '\nfileVersion=' + (result.fileVersion || 'unknown'),
});
setComposerText('基于当前页面内容回答:\n\n' + content);
updateMessages();
}).catch(function (error) {
updateDiagnostics(error.message || String(error));
setComposerText('读取当前页面,总结页面要点。');
});
return;
}
if (kind === 'selection') {
var selection = refreshSelectionSummary();
callMNoteTool('mnote.selection.read', {
selection: selection,
selectionSource: 'mnote_sidebar_host',
rootUri: context.rootUri || null,
pagePath: context.pagePath || null,
}).then(function () {
setComposerText(selection
? '基于当前选区给出修改建议:\n\n' + selection
: '当前没有选区。请先在编辑器中选中文本,再让 Pi 读取选区。');
updateContextStrip();
}).catch(function () {
setComposerText(selection || '当前没有选区。');
});
return;
}
setComposerText('通过 LightRAG 查询当前页面相关资料,并带 citation 回复。');
}
function sendCurrentInput() {
if (!piLabPanelEl) return;
var input = piLabPanelEl.querySelector('[data-page-ai-pi-lab-input]');
var text = input ? input.value.trim() : '';
if (!text) return;
if (input) input.value = '';
updateButtons();
var run = piLabState.status === STATE_IDLE || piLabState.status === STATE_ABORTED || piLabState.status === STATE_ERROR
? startRuntime()
: Promise.resolve();
run.then(function () { return sendPrompt(text); }).catch(function () {});
}
function abortPrompt() {
if (piLabEventSource) {
piLabEventSource.close();
piLabEventSource = null;
}
if (piLabState.sessionId) {
fetch(API.ABORT, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: piLabState.sessionId }),
}).catch(function () {});
}
if (streamingAssistantMsg) {
streamingAssistantMsg.text += (streamingAssistantMsg.text ? '\n' : '') + '已中止';
streamingAssistantMsg.status = 'done';
streamingAssistantMsg = null;
}
setState(STATE_ABORTED);
updateMessages();
}
function ensurePanel() {
if (piLabPanelEl) return piLabPanelEl;
var container = document.createElement('div');
container.innerHTML = piLabPanelHTML();
piLabPanelEl = container.firstElementChild;
wireEvents();
updateReceiptDisplay();
updateMessages();
return piLabPanelEl;
}
function summarizeAllowedRoots(snapshot) {
if (!snapshot) return piLabState.allowedRootsSummary || 'pending';
var roots = Array.isArray(snapshot.roots) ? snapshot.roots : (Array.isArray(snapshot) ? snapshot : []);
if (!roots.length) return '0 roots';
return roots.length + ' root' + (roots.length === 1 ? '' : 's');
}
function ensureDrawer() {
if (piLabDrawerEl) return piLabDrawerEl;
piLabDrawerEl = el('section', {
className: 'wolai-page-ai-pi-lab-drawer',
'data-page-ai-pi-lab': 'drawer',
'data-page-ai-pi-lab-drawer': 'true',
'aria-label': 'Pi Lab',
hidden: 'true',
});
piLabDrawerEl.appendChild(ensurePanel());
document.body.appendChild(piLabDrawerEl);
return piLabDrawerEl;
}
function setDrawerVisible(visible) {
var drawer = ensureDrawer();
drawer.hidden = !visible;
drawer.setAttribute('data-open', visible ? 'true' : 'false');
if (visible) drawer.setAttribute('data-minimized', 'false');
}
function showPiLab() {
piLabState.active = true;
setDrawerVisible(true);
applyCurrentContextToState();
updateConfig();
checkStatus();
var input = piLabPanelEl && piLabPanelEl.querySelector('[data-page-ai-pi-lab-input]');
if (input) setTimeout(function () { input.focus(); }, 60);
}
function hidePiLab() {
piLabState.active = false;
if (piLabDrawerEl) setDrawerVisible(false);
updateLauncherState();
}
function updateLauncherState() {
if (!piLabLauncherEl) return;
piLabLauncherEl.setAttribute('data-state', piLabState.status);
piLabLauncherEl.hidden = false;
}
function installLauncher() {
if (piLabLauncherEl) return;
piLabLauncherEl = el('button', {
type: 'button',
className: 'wolai-page-ai-pi-lab-launcher',
'data-page-ai-pi-lab-launcher': 'true',
title: 'Pi Lab',
'aria-label': '打开 Pi Lab',
}, 'π');
piLabLauncherEl.addEventListener('click', showPiLab);
document.body.appendChild(piLabLauncherEl);
}
function wireEvents() {
if (!piLabPanelEl || piLabPanelEl.getAttribute('data-wired') === 'true') return;
piLabPanelEl.setAttribute('data-wired', 'true');
var input = piLabPanelEl.querySelector('[data-page-ai-pi-lab-input]');
var startBtn = piLabPanelEl.querySelector('[data-page-ai-pi-lab-btn-start]');
var sendBtn = piLabPanelEl.querySelector('[data-page-ai-pi-lab-btn-send]');
var abortBtn = piLabPanelEl.querySelector('[data-page-ai-pi-lab-btn-abort]');
var clearBtn = piLabPanelEl.querySelector('[data-page-ai-pi-lab-btn-clear]');
var closeBtn = piLabPanelEl.querySelector('[data-page-ai-pi-lab-close]');
var minimizeBtn = piLabPanelEl.querySelector('[data-page-ai-pi-lab-minimize]');
var providerSelect = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-provider]');
var modelSelect = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-id]');
var bottomModelSelect = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-id-bottom]');
var customInput = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-custom]');
var settingsBtn = piLabPanelEl.querySelector('[data-page-ai-pi-lab-open-settings]');
var historyBtn = piLabPanelEl.querySelector('[data-page-ai-pi-lab-history]');
var openBtn = piLabPanelEl.querySelector('[data-page-ai-pi-lab-open]');
var newBtns = piLabPanelEl.querySelectorAll('[data-page-ai-pi-lab-new], [data-page-ai-pi-lab-new-small]');
if (startBtn) startBtn.addEventListener('click', startRuntime);
if (sendBtn) sendBtn.addEventListener('click', sendCurrentInput);
if (abortBtn) abortBtn.addEventListener('click', abortPrompt);
if (clearBtn) clearBtn.addEventListener('click', function () {
piLabState.messages = [];
piLabState.receipts = [];
streamingAssistantMsg = null;
updateMessages();
updateReceiptDisplay();
});
if (closeBtn) closeBtn.addEventListener('click', hidePiLab);
if (minimizeBtn) {
minimizeBtn.addEventListener('click', function () {
var drawer = ensureDrawer();
var minimized = drawer.getAttribute('data-minimized') === 'true';
drawer.setAttribute('data-minimized', minimized ? 'false' : 'true');
});
}
if (input) {
input.addEventListener('input', updateButtons);
input.addEventListener('keydown', function (e) {
if (e.isComposing || e.key === 'Process') return;
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendCurrentInput();
}
if (e.key === 'Escape' && piLabState.status === STATE_STREAMING) {
e.preventDefault();
abortPrompt();
}
});
}
[providerSelect, modelSelect, bottomModelSelect].forEach(function (node) {
if (node) node.addEventListener('change', applyModelControls);
});
if (customInput) customInput.addEventListener('input', applyModelControls);
if (settingsBtn) {
settingsBtn.addEventListener('click', function () {
var settings = piLabPanelEl.querySelector('[data-page-ai-pi-lab-settings]');
var drawer = ensureDrawer();
var isOpen = drawer.getAttribute('data-config-open') === 'true';
drawer.setAttribute('data-config-open', isOpen ? 'false' : 'true');
if (settings) settings.open = !isOpen;
var context = piLabPanelEl.querySelector('[data-page-ai-pi-lab-context-strip]');
if (context) context.open = !isOpen;
});
}
if (historyBtn) {
historyBtn.addEventListener('click', function () {
var panel = piLabPanelEl.querySelector('[data-page-ai-pi-lab-history-panel]');
if (!panel) return;
panel.hidden = !panel.hidden;
if (!panel.hidden) loadHistory().catch(function (error) {
panel.innerHTML = '<div class="wolai-page-ai-pi-lab-empty">' + escapeHtml(error.message || '历史加载失败') + '</div>';
});
});
}
if (openBtn) {
openBtn.addEventListener('click', function () {
window.location.assign('/user/ai');
});
}
Array.prototype.slice.call(newBtns || []).forEach(function (btn) {
btn.addEventListener('click', function () {
piLabState.messages = [];
piLabState.receipts = [];
streamingAssistantMsg = null;
updateMessages();
updateReceiptDisplay();
});
});
piLabPanelEl.addEventListener('click', function (event) {
var historyTarget = event.target && event.target.closest ? event.target.closest('[data-page-ai-pi-lab-history-session]') : null;
if (historyTarget) {
openHistorySession(historyTarget.getAttribute('data-page-ai-pi-lab-history-session')).catch(function (error) {
updateDiagnostics(error.message || '历史会话加载失败');
});
return;
}
var target = event.target && event.target.closest ? event.target.closest('[data-page-ai-pi-lab-example]') : null;
if (!target) return;
setComposerText(target.getAttribute('data-page-ai-pi-lab-example') || target.textContent || '');
});
Array.prototype.slice.call(piLabPanelEl.querySelectorAll('[data-page-ai-pi-lab-quick]')).forEach(function (btn) {
btn.addEventListener('click', function () {
handleQuickAction(btn.getAttribute('data-page-ai-pi-lab-quick'));
});
});
}
function setupPostMessageListener() {
window.addEventListener('message', function (event) {
if (!event.data || event.data.source !== 'mnote-sidebar') return;
if (event.data.type === 'mnote:pi-lab-status') checkStatus();
if (event.data.type === 'mnote:pi-lab-show') showPiLab();
if (event.data.type === 'mnote:pi-lab-hide') hidePiLab();
});
}
function setupSelectionListener() {
document.addEventListener('selectionchange', function () {
if (!piLabState.active) return;
refreshSelectionSummary();
updateContextStrip();
}, true);
window.addEventListener('mnote:leptos-tiptap-spike:selection', function () {
if (!piLabState.active) return;
refreshSelectionSummary();
updateContextStrip();
}, true);
}
function createSidebarPageAiPiLabRuntime(context) {
if (piLabInstalled) return piLabState;
piLabInstalled = true;
injectStyles();
piLabState.standalone = !!(context && context.standalone);
ensureDrawer();
if (piLabState.standalone) {
setDrawerVisible(true);
piLabState.active = true;
} else {
installLauncher();
}
setupPostMessageListener();
setupSelectionListener();
updateConfig();
updateReceiptDisplay();
checkStatus();
return piLabState;
}
if (typeof window !== 'undefined') {
window.createSidebarPageAiPiLabRuntime = createSidebarPageAiPiLabRuntime;
}
})();