';
}
function renderEmptyState(container) {
container.innerHTML = '' +
'
' +
'
?
' +
'开始对话' +
'
试试以下示例,或直接输入您的指令
' +
'
' +
'' +
'' +
'' +
'' +
'' +
'
' +
'
';
}
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 = '
暂无 Pi Lab 历史
';
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 '';
}).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, ' '),
}));
}
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: '' + escapeHtml(tc.name || 'tool') + '' + escapeHtml(tc.status || '') + '' }));
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 = '
' + escapeHtml(error.message || '历史加载失败') + '
';
});
});
}
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;
}
})();