feat: complete page ai hermes control checklist

This commit is contained in:
lix-2026
2026-05-14 18:24:30 +08:00
parent f257b2a014
commit 0fc9fa7551
7 changed files with 687 additions and 64 deletions
@@ -9,6 +9,7 @@ use futures_util::TryStreamExt;
use serde::Deserialize;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::fs;
use std::process::Command;
use std::time::Duration;
@@ -387,8 +388,50 @@ pub async fn list_tools(
{
"name": "mnote.page.get",
"scope": "page.read",
"kind": "read",
"schemaVersion": "mnote.hermes_tool.v1",
"status": "planned_by_task_e"
"status": "available",
"description": "读取当前页面正文、标题、设置与结构快照"
},
{
"name": "mnote.page.save",
"scope": "page.write",
"kind": "write",
"schemaVersion": "mnote.hermes_tool.v1",
"status": "available",
"description": "保存当前页面正文"
},
{
"name": "mnote.page.update_title",
"scope": "page.write",
"kind": "write",
"schemaVersion": "mnote.hermes_tool.v1",
"status": "available",
"description": "更新当前页面标题"
},
{
"name": "mnote.page.update_options",
"scope": "page.write",
"kind": "write",
"schemaVersion": "mnote.hermes_tool.v1",
"status": "available",
"description": "更新当前页面设置"
},
{
"name": "mnote.artifact.create_summary",
"scope": "artifact.write",
"kind": "write",
"schemaVersion": "mnote.hermes_tool.v1",
"status": "available",
"description": "为当前页面创建或更新 AI Summary"
},
{
"name": "mnote.artifact.create_ai_note",
"scope": "artifact.write",
"kind": "write",
"schemaVersion": "mnote.hermes_tool.v1",
"status": "available",
"description": "基于当前页面创建新的 AI Note"
}
]
})),
@@ -411,20 +454,61 @@ fn ensure_authenticated(context: &RequestContext) -> Result<(), WebError> {
}
fn configured_upstream() -> Option<String> {
std::env::var("MNOTE_WEB_HERMES_UPSTREAM_URL")
.ok()
.or_else(|| std::env::var("MNOTE_HERMES_UPSTREAM_URL").ok())
.map(|value| value.trim().trim_end_matches('/').to_string())
.filter(|value| !value.is_empty())
[
"MNOTE_WEB_HERMES_UPSTREAM_URL",
"MNOTE_HERMES_UPSTREAM_URL",
"MNOTE_HERMES_API_BASE_URL",
]
.into_iter()
.find_map(env_or_dotenv)
.map(|value| value.trim().trim_end_matches('/').to_string())
.filter(|value| !value.is_empty())
}
fn configured_api_key() -> Option<String> {
std::env::var("MNOTE_WEB_HERMES_API_KEY")
.ok()
.or_else(|| std::env::var("HERMES_API_SERVER_KEY").ok())
.or_else(|| std::env::var("API_SERVER_KEY").ok())
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
[
"MNOTE_WEB_HERMES_API_KEY",
"MNOTE_HERMES_API_KEY",
"HERMES_API_SERVER_KEY",
"API_SERVER_KEY",
]
.into_iter()
.find_map(env_or_dotenv)
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
}
fn env_or_dotenv(key: &str) -> Option<String> {
if let Ok(value) = std::env::var(key) {
let trimmed = value.trim().trim_matches('"').to_string();
if !trimmed.is_empty() {
return Some(trimmed);
}
}
if cfg!(test) {
return None;
}
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../..")
.join(".env.all");
let content = fs::read_to_string(root).ok()?;
for line in content.lines() {
let line = line.trim_end_matches('\r');
if line.starts_with('#') || line.trim().is_empty() {
continue;
}
let Some((candidate_key, value)) = line.split_once('=') else {
continue;
};
if candidate_key.trim() != key {
continue;
}
let trimmed = value.trim().trim_matches('"').to_string();
if !trimmed.is_empty() {
return Some(trimmed);
}
}
None
}
fn build_run_upstream_body(context: &RequestContext, payload: Value) -> Result<Value, WebError> {
@@ -33,6 +33,7 @@ use bridge_runtime::RuntimeQueryEnvelopeWire;
use core_protocol::KernelProjectionKind;
use serde::Deserialize;
use serde_json::json;
use std::fs;
use std::path::{Component, Path as FsPath, PathBuf};
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
@@ -195,6 +196,7 @@ pub async fn document_page_shell(
secondary_page_options_json={secondary_page_options_json.unwrap_or_default()}
/>
});
let hermes_settings_config_script = render_hermes_settings_config_script();
let html = format!(
r#"<!doctype html>
<html lang="zh-CN">
@@ -212,6 +214,7 @@ pub async fn document_page_shell(
{}
{}
{}
{}
</body>
</html>"#,
escape_html(title),
@@ -223,6 +226,7 @@ pub async fn document_page_shell(
escape_script_json(&snapshot_json),
escape_script_json(&bootstrap_json),
escape_script_json(&panes_bootstrap_json),
hermes_settings_config_script,
secondary_snapshot_json
.as_ref()
.map(|value| format!(r#"<script id="__MNOTE_SECONDARY_PAGE_AGGREGATE__" type="application/json">{}</script>"#, escape_script_json(value)))
@@ -256,6 +260,60 @@ pub(crate) fn build_editor_bootstrap_json(
)
}
fn render_hermes_settings_config_script() -> String {
let Some(base_url) = [
"MNOTE_WEB_HERMES_UPSTREAM_URL",
"MNOTE_HERMES_UPSTREAM_URL",
"MNOTE_HERMES_API_BASE_URL",
]
.into_iter()
.find_map(env_or_dotenv)
.map(|value| value.trim().trim_end_matches('/').to_string())
.filter(|value| !value.is_empty())
else {
return String::new();
};
let settings_url = format!("{base_url}/hermes/settings");
let encoded = serde_json::to_string(&settings_url).unwrap_or_else(|_| "\"\"".to_string());
format!(
r#"<script>window.__mnoteHermesSettingsUrl = {};</script>"#,
escape_script_json(&encoded)
)
}
fn env_or_dotenv(key: &str) -> Option<String> {
if let Ok(value) = std::env::var(key) {
let trimmed = value.trim().trim_matches('"').to_string();
if !trimmed.is_empty() {
return Some(trimmed);
}
}
if cfg!(test) {
return None;
}
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../..")
.join(".env.all");
let content = fs::read_to_string(root).ok()?;
for line in content.lines() {
let line = line.trim_end_matches('\r');
if line.starts_with('#') || line.trim().is_empty() {
continue;
}
let Some((candidate_key, value)) = line.split_once('=') else {
continue;
};
if candidate_key.trim() != key {
continue;
}
let trimmed = value.trim().trim_matches('"').to_string();
if !trimmed.is_empty() {
return Some(trimmed);
}
}
None
}
pub(crate) fn build_editor_bootstrap_json_with_ids(
aggregate: &PageAggregate,
context: &RequestContext,
+226 -22
View File
@@ -40,6 +40,9 @@ const SIDEBAR_TREE_JS: &str = r##"
pageAiCurrentRunId: '',
pageAiAbortController: null,
pageAiContextScope: 'page',
pageAiTools: [],
pageAiToolsError: '',
pageAiLastToolCall: null,
pageAiProfiles: [],
pageAiActiveProfileName: 'default',
pageAiProfileError: '',
@@ -230,6 +233,42 @@ const SIDEBAR_TREE_JS: &str = r##"
};
}
function currentPageAiSelectedText() {
try {
var selection = window.getSelection ? window.getSelection() : null;
return selection ? searchText(selection.toString() || '') : '';
} catch (_) {
return '';
}
}
function pageAiScopedPageContext(contextSnapshot) {
var aggregate = contextSnapshot.aggregate || {};
var body = contextSnapshot.body || {};
var subtree = contextSnapshot.subtree || null;
var outline = subtree && subtree.outline ? subtree.outline : null;
var scope = pageUiState.pageAiContextScope || 'page';
var title = aggregate.head && aggregate.head.title ? aggregate.head.title : '';
var selectedText = scope === 'selection' ? currentPageAiSelectedText() : '';
return {
pageContext: {
contextScope: scope,
documentBlocks: scope === 'page' ? (body.content || null) : null,
node: {
documentId: currentDocumentId(),
title: title
},
subtree: scope === 'page' ? subtree : null,
outline: scope === 'page' ? outline : null,
pageSubtreeSource: scope === 'page' ? (contextSnapshot.pageSubtreeSource || 'none') : 'scope:' + scope,
evidence: selectedText ? [{ kind: 'selection', text: selectedText }] : null,
pageOptions: scope === 'options' || scope === 'page' ? currentPageOptions() : null
},
selectedText: selectedText,
selectedBlockId: null
};
}
function defaultPageOptions() {
return {
wideLayout: false,
@@ -3212,6 +3251,21 @@ const SIDEBAR_TREE_JS: &str = r##"
return pageAiProfileValue(selected) || 'default';
}
function pageAiCurrentProfileRecord() {
var active = pageAiCurrentProfile();
return pageUiState.pageAiProfiles.find(function(profile) {
return pageAiProfileValue(profile) === active;
}) || null;
}
function pageAiCurrentModelLabel() {
var profile = pageAiCurrentProfileRecord();
if (!profile) return '由 Hermes 决定';
var model = String(profile.model || '').trim();
var gateway = String(profile.gateway || '').trim();
return [model, gateway].filter(Boolean).join(' · ') || '由 Hermes 决定';
}
function pageAiNormalizeProfiles(payload) {
var upstream = pageAiUnwrapUpstream(payload);
var profiles = pageAiNormalizeArray(upstream && upstream.profiles ? upstream.profiles : upstream);
@@ -3319,6 +3373,38 @@ const SIDEBAR_TREE_JS: &str = r##"
return '当前页';
}
function pageAiHermesSettingsUrl() {
var configured = String(window.__mnoteHermesSettingsUrl || '').trim();
return configured || '';
}
function pageAiOpenHermesSettings() {
var url = pageAiHermesSettingsUrl();
if (url) {
window.open(url, '_blank', 'noopener,noreferrer');
return;
}
pageUiState.pageAiProfileError = '未配置 Hermes 设置入口:请设置 MNOTE_WEB_HERMES_UPSTREAM_URL。';
renderPageAiControls();
}
function pageAiNormalizeTools(payload) {
var upstream = pageAiUnwrapUpstream(payload) || {};
var tools = pageAiNormalizeArray(upstream.tools || upstream);
return tools.map(function(tool) {
var name = String(tool && (tool.name || tool.toolName || tool.tool) || '').trim();
if (!name) return null;
return {
name: name,
description: String(tool && tool.description || '').trim(),
scope: String(tool && (tool.scope || tool.requiredScope || '') || '').trim(),
kind: String(tool && (tool.kind || tool.mode || '') || '').trim(),
status: String(tool && (tool.status || tool.permission || 'available') || '').trim(),
unavailableReason: String(tool && (tool.unavailableReason || tool.reason || '') || '').trim()
};
}).filter(Boolean);
}
function pageAiSetDraftForSection(section, value) {
pageUiState.pageAiProfileMemoryDrafts[section] = String(value == null ? '' : value);
}
@@ -3337,6 +3423,49 @@ const SIDEBAR_TREE_JS: &str = r##"
};
}
async function pageAiLoadTools() {
try {
var response = await fetch('/api/hermes/client/tools?scope=mnote', {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(payload && payload.code ? payload.code : 'tools_failed_' + response.status);
pageUiState.pageAiTools = pageAiNormalizeTools(payload);
pageUiState.pageAiToolsError = '';
} catch (error) {
pageUiState.pageAiToolsError = error instanceof Error ? error.message : String(error);
}
renderPageAiControls();
}
async function pageAiStopRun() {
var runId = String(pageUiState.pageAiCurrentRunId || '').trim();
if (!runId || pageUiState.pageAiRunStatus === 'idle' || pageUiState.pageAiRunStatus === 'completed') return;
try {
var response = await fetch('/api/hermes/client/runs/' + encodeURIComponent(runId) + '/abort', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
sessionId: pageUiState.pageAiActiveSessionId,
profile: pageAiCurrentProfile(),
reason: 'page_ai_user_stop'
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(payload && payload.code ? payload.code : 'run_abort_failed_' + response.status);
pageAiSetRunStatus('aborted', runId);
pageUiState.pageAiMessages.push({ role: 'assistant', content: '已请求 Hermes 停止当前 run。' });
} catch (error) {
pageAiSetRunStatus('failed', runId);
pageUiState.pageAiMessages.push({
role: 'assistant',
content: '停止 Hermes run 失败:' + (error instanceof Error ? error.message : String(error))
});
}
renderPageAiControls();
renderPageAiConversation();
}
function pageAiFilteredSkillEntries() {
var query = String(pageUiState.pageAiSkillQuery || '').trim().toLowerCase();
return pageAiSkillListEntries().filter(function(skill) {
@@ -3519,18 +3648,32 @@ const SIDEBAR_TREE_JS: &str = r##"
var profiles = pageUiState.pageAiProfiles.length ? pageUiState.pageAiProfiles : [{ name: activeProfile, active: true }];
profileSelect.innerHTML = profiles.map(function(profile) {
var name = pageAiProfileValue(profile) || 'default';
var label = profile.alias ? name + ' · ' + profile.alias : name;
var model = [profile.model, profile.gateway].filter(Boolean).join(' / ');
var label = [name, profile.alias, model].filter(Boolean).join(' · ');
return '<option value="' + escapeHtml(name) + '"' + (name === activeProfile ? ' selected' : '') + '>' + escapeHtml(label) + '</option>';
}).join('');
profileSelect.value = activeProfile;
}
var runStatus = drawer.querySelector('[data-page-ai-run-status]');
if (runStatus instanceof HTMLElement) runStatus.textContent = pageAiRunStatusLabel(pageUiState.pageAiRunStatus);
var stopButton = drawer.querySelector('[data-page-ai-action="stop-run"]');
if (stopButton instanceof HTMLButtonElement) {
var canStop = ['queued', 'running', 'tool_calling'].indexOf(pageUiState.pageAiRunStatus) >= 0 && pageUiState.pageAiCurrentRunId;
stopButton.disabled = !canStop;
stopButton.setAttribute('aria-disabled', canStop ? 'false' : 'true');
}
var settingsLink = drawer.querySelector('[data-page-ai-action="open-hermes-settings"]');
if (settingsLink instanceof HTMLButtonElement) {
settingsLink.disabled = !pageAiHermesSettingsUrl();
settingsLink.title = pageAiHermesSettingsUrl() ? '打开 Hermes 设置' : '未配置 MNOTE_WEB_HERMES_UPSTREAM_URL';
}
var sessionNode = drawer.querySelector('[data-page-ai-session-status]');
if (sessionNode instanceof HTMLElement) {
var session = pageAiCurrentSession();
sessionNode.textContent = session && session.id ? String(session.title || '当前页问答') : '等待 Hermes session';
}
var modelNode = drawer.querySelector('[data-page-ai-model-status]');
if (modelNode instanceof HTMLElement) modelNode.textContent = pageAiCurrentModelLabel();
var scopeSelect = drawer.querySelector('[data-page-ai-context-scope]');
if (scopeSelect instanceof HTMLSelectElement) scopeSelect.value = pageUiState.pageAiContextScope;
var scopeLabel = drawer.querySelector('[data-page-ai-context-scope-label]');
@@ -3606,6 +3749,34 @@ const SIDEBAR_TREE_JS: &str = r##"
}).join('');
}
}
var toolsList = drawer.querySelector('[data-page-ai-tool-list]');
if (toolsList instanceof HTMLElement) {
var tools = pageAiNormalizeArray(pageUiState.pageAiTools);
if (!tools.length) {
toolsList.innerHTML = '<div class="wolai-page-ai-empty">尚未读取到 mnote tool manifest。</div>';
} else {
toolsList.innerHTML = tools.map(function(tool) {
return '' +
'<div class="wolai-page-ai-tool-row">' +
'<div class="wolai-page-ai-tool-name">' + escapeHtml(tool.name) + '</div>' +
'<div class="wolai-page-ai-tool-meta">' + escapeHtml(tool.scope || tool.kind || 'mnote') + ' · ' + escapeHtml(tool.status || 'available') + '</div>' +
(tool.unavailableReason ? '<div class="wolai-page-ai-tool-meta">' + escapeHtml(tool.unavailableReason) + '</div>' : '') +
'</div>';
}).join('');
}
}
var toolError = drawer.querySelector('[data-page-ai-tool-error]');
if (toolError instanceof HTMLElement) {
toolError.textContent = pageUiState.pageAiToolsError || '';
toolError.hidden = !pageUiState.pageAiToolsError;
}
var lastTool = drawer.querySelector('[data-page-ai-last-tool]');
if (lastTool instanceof HTMLElement) {
var call = pageUiState.pageAiLastToolCall;
lastTool.textContent = call
? [call.event, call.name, call.runId, call.traceId || call.auditId].filter(Boolean).join(' · ')
: '暂无 tool call';
}
}
function humanizePageAiResponse(rawText, promptText) {
@@ -3656,11 +3827,15 @@ const SIDEBAR_TREE_JS: &str = r##"
'<span class="wolai-page-ai-statuslabel">状态</span>' +
'<span class="wolai-page-ai-statusvalue" data-page-ai-run-status>空闲</span>' +
'</div>' +
'<div class="wolai-page-ai-statusitem">' +
'<span class="wolai-page-ai-statuslabel">模型</span>' +
'<span class="wolai-page-ai-statusvalue" data-page-ai-model-status>由 Hermes 决定</span>' +
'</div>' +
'<div class="wolai-page-ai-statusitem">' +
'<span class="wolai-page-ai-statuslabel">上下文</span>' +
'<span class="wolai-page-ai-statusvalue" data-page-ai-context-scope-label>当前页</span>' +
'</div>' +
'<a class="wolai-page-ai-settings-link" href="/auth" target="_blank" rel="noreferrer">Hermes 设置</a>' +
'<button type="button" class="wolai-page-ai-settings-link" data-page-ai-action="open-hermes-settings">Hermes 设置</button>' +
'</div>' +
'<div class="wolai-page-ai-topbar">' +
'<label class="wolai-page-ai-profile-select">' +
@@ -3697,6 +3872,14 @@ const SIDEBAR_TREE_JS: &str = r##"
'</div>' +
'<div class="wolai-page-ai-suggestion-list" data-page-ai-suggestion-list></div>' +
'</div>' +
'<div class="wolai-page-ai-tools-panel">' +
'<div class="wolai-page-ai-tools-head">' +
'<span>mnote tools</span>' +
'<span data-page-ai-last-tool>暂无 tool call</span>' +
'</div>' +
'<div class="wolai-page-ai-inline-error" data-page-ai-tool-error hidden></div>' +
'<div class="wolai-page-ai-tool-list" data-page-ai-tool-list></div>' +
'</div>' +
'<div class="wolai-page-ai-conversation" data-page-ai-conversation></div>' +
'</section>' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="agent" hidden>' +
@@ -3721,6 +3904,7 @@ const SIDEBAR_TREE_JS: &str = r##"
'<div class="wolai-page-ai-toolbar">' +
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-action="new-session" aria-label="当前已是新会话">新会话</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-action="history">历史会话</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-action="stop-run" disabled>停止</button>' +
'</div>' +
'<div class="wolai-page-ai-input-row">' +
'<textarea class="wolai-page-ai-input" data-page-ai-input rows="3" placeholder="问我你想知道的"></textarea>' +
@@ -3794,7 +3978,8 @@ const SIDEBAR_TREE_JS: &str = r##"
Promise.all([
pageAiLoadProfiles(),
pageAiLoadProfileMemory(),
pageAiLoadSkills()
pageAiLoadSkills(),
pageAiLoadTools()
]).then(function() {
renderPageAiControls();
}).catch(function() {}).then(function() {
@@ -3864,11 +4049,9 @@ const SIDEBAR_TREE_JS: &str = r##"
try {
await pageAiEnsureHermesSession();
pageAiSetRunStatus('queued');
renderPageAiControls();
var contextSnapshot = currentPageAiContextSnapshot();
var aggregate = contextSnapshot.aggregate || {};
var body = contextSnapshot.body || {};
var subtree = contextSnapshot.subtree || null;
var outline = subtree && subtree.outline ? subtree.outline : null;
var scopedContext = pageAiScopedPageContext(contextSnapshot);
pageUiState.pageAiMessages.push({ role: 'user', content: prompt });
currentSession = pageAiCurrentSession();
if (currentSession) {
@@ -3887,22 +4070,12 @@ const SIDEBAR_TREE_JS: &str = r##"
documentId: currentDocumentId(),
sessionId: pageUiState.pageAiActiveSessionId,
profile: pageAiCurrentProfile(),
contextScope: pageUiState.pageAiContextScope,
message: prompt,
model: 'hermes-agent',
pageContext: {
documentBlocks: body.content || null,
node: {
documentId: currentDocumentId(),
title: aggregate.head && aggregate.head.title ? aggregate.head.title : ''
},
subtree: subtree,
outline: outline,
pageSubtreeSource: contextSnapshot.pageSubtreeSource || 'none',
evidence: null,
pageOptions: currentPageOptions()
},
selectedBlockId: null,
selectedText: null,
pageContext: scopedContext.pageContext,
selectedBlockId: scopedContext.selectedBlockId,
selectedText: scopedContext.selectedText,
traceId: 'page-ai-run-' + Date.now().toString(36)
})
});
@@ -3914,7 +4087,9 @@ const SIDEBAR_TREE_JS: &str = r##"
var upstream = runPayload && runPayload.upstream ? runPayload.upstream : runPayload;
var runId = upstream && (upstream.run_id || upstream.runId);
if (!runId) throw new Error('hermes_run_missing_run_id');
var runTraceId = String((upstream && (upstream.trace_id || upstream.traceId)) || (runPayload && (runPayload.trace_id || runPayload.traceId)) || '');
pageAiSetRunStatus('running', runId);
renderPageAiControls();
var eventResponse = await fetch('/api/hermes/client/events/' + encodeURIComponent(runId), {
headers: { 'accept': 'text/event-stream' }
});
@@ -3942,15 +4117,30 @@ const SIDEBAR_TREE_JS: &str = r##"
if (eventName === 'tool.started' || eventName === 'tool.completed' || eventName === 'tool.failed') {
try {
var toolEvent = JSON.parse(payloadText || 'null');
pageUiState.pageAiLastToolCall = {
event: eventName,
name: String(toolEvent && (toolEvent.name || toolEvent.tool || eventName) || eventName),
runId: String(toolEvent && (toolEvent.run_id || toolEvent.runId) || runId),
traceId: String(toolEvent && (toolEvent.trace_id || toolEvent.traceId) || runTraceId),
auditId: String(toolEvent && (toolEvent.audit_id || toolEvent.auditId) || '')
};
pageUiState.pageAiMessages.push({
role: 'tool',
content: String(toolEvent && (toolEvent.name || toolEvent.tool || eventName) || eventName)
content: pageUiState.pageAiLastToolCall.name
});
} catch (_) {
pageUiState.pageAiLastToolCall = {
event: eventName,
name: eventName,
runId: runId,
traceId: runTraceId,
auditId: ''
};
pageUiState.pageAiMessages.push({ role: 'tool', content: eventName });
}
renderPageAiConversation();
pageAiSetRunStatus('tool_calling', runId);
renderPageAiControls();
}
});
pageUiState.pageAiMessages.push({
@@ -4521,6 +4711,20 @@ const SIDEBAR_TREE_JS: &str = r##"
return;
}
var pageAiSettings = closestAction(e.target, '[data-page-ai-action="open-hermes-settings"]');
if (pageAiSettings) {
e.preventDefault();
pageAiOpenHermesSettings();
return;
}
var pageAiStop = closestAction(e.target, '[data-page-ai-action="stop-run"]');
if (pageAiStop) {
e.preventDefault();
void pageAiStopRun();
return;
}
var pageAiRotate = closestAction(e.target, '[data-page-ai-action="rotate"]');
if (pageAiRotate) {
e.preventDefault();
+69 -1
View File
@@ -2670,7 +2670,7 @@ body {
.wolai-page-ai-statusbar {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(68px, auto) minmax(72px, auto) auto;
grid-template-columns: minmax(0, 1fr) minmax(68px, auto) minmax(82px, auto) minmax(72px, auto) auto;
gap: 8px;
align-items: stretch;
}
@@ -2714,9 +2714,16 @@ body {
padding: 0 10px;
border: 1px solid rgba(27, 28, 28, 0.08);
border-radius: 8px;
background: #FFF;
color: #1B1C1C;
font-size: 12px;
text-decoration: none;
cursor: pointer;
}
.wolai-page-ai-settings-link:disabled {
color: #AAA6A0;
cursor: not-allowed;
}
.wolai-page-ai-topbar {
@@ -2811,6 +2818,67 @@ body {
gap: 8px;
}
.wolai-page-ai-tools-panel {
display: grid;
gap: 8px;
padding: 10px;
border: 1px solid rgba(27, 28, 28, 0.08);
border-radius: 8px;
background: #FAFAFA;
}
.wolai-page-ai-tools-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
color: #8B8782;
font-size: 11px;
line-height: 16px;
}
.wolai-page-ai-tools-head span:last-child {
min-width: 0;
overflow: hidden;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
.wolai-page-ai-tool-list {
display: grid;
max-height: 96px;
gap: 6px;
overflow: auto;
}
.wolai-page-ai-tool-row {
display: grid;
gap: 2px;
padding: 6px 8px;
border-radius: 8px;
background: #FFF;
}
.wolai-page-ai-tool-name {
overflow: hidden;
color: #1B1C1C;
font-size: 12px;
font-weight: 600;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.wolai-page-ai-tool-meta {
overflow: hidden;
color: #8B8782;
font-size: 11px;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.wolai-page-ai-provider-group {
display: flex;
flex-wrap: wrap;