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

1529 lines
83 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// == 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;
}
})();