feat: expand page ai hermes control surface
This commit is contained in:
@@ -36,6 +36,19 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
pageAiSuggestionIndex: 0,
|
||||
pageAiProvider: 'hermes',
|
||||
pageAiPage: 'chat',
|
||||
pageAiRunStatus: 'idle',
|
||||
pageAiCurrentRunId: '',
|
||||
pageAiAbortController: null,
|
||||
pageAiContextScope: 'page',
|
||||
pageAiProfiles: [],
|
||||
pageAiActiveProfileName: 'default',
|
||||
pageAiProfileError: '',
|
||||
pageAiProfileMemory: { memory: '', user: '', soul: '' },
|
||||
pageAiProfileMemoryDrafts: { memory: '', user: '', soul: '' },
|
||||
pageAiProfileMemoryError: '',
|
||||
pageAiSkills: { categories: [], archived: [] },
|
||||
pageAiSkillQuery: '',
|
||||
pageAiSkillError: '',
|
||||
pageAiSessions: [],
|
||||
pageAiActiveSessionId: ''
|
||||
};
|
||||
@@ -3022,6 +3035,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
return {
|
||||
id: 'sess_' + now + '_' + Math.random().toString(16).slice(2, 8),
|
||||
title: title || '新会话',
|
||||
profile: pageAiCurrentProfile(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
messages: []
|
||||
@@ -3035,6 +3049,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
return {
|
||||
id: String(session && session.id || '').trim() || pageAiNewSession().id,
|
||||
title: String(session && session.title || '').trim() || '新会话',
|
||||
profile: String(session && session.profile || pageAiCurrentProfile()).trim() || 'default',
|
||||
createdAt: Number(session && session.createdAt || Date.now()),
|
||||
updatedAt: Number(session && session.updatedAt || Date.now()),
|
||||
messages: Array.isArray(session && session.messages) ? session.messages.slice(-40) : []
|
||||
@@ -3051,6 +3066,8 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
var raw = window.localStorage.getItem(pageAiStorageKey());
|
||||
var parsed = raw ? JSON.parse(raw) : null;
|
||||
var activeId = String(parsed && parsed.activeSessionId || '').trim();
|
||||
var activeProfile = String(parsed && parsed.activeProfileName || '').trim();
|
||||
if (activeProfile) pageAiSetActiveProfile(activeProfile);
|
||||
if (activeId) {
|
||||
pageUiState.pageAiSessions = [pageAiNewSession('当前页问答')];
|
||||
pageUiState.pageAiSessions[0].id = activeId;
|
||||
@@ -3070,15 +3087,16 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-session-key', pageAiStorageKey());
|
||||
try {
|
||||
window.localStorage.setItem(pageAiStorageKey(), JSON.stringify({
|
||||
activeSessionId: pageUiState.pageAiActiveSessionId
|
||||
activeSessionId: pageUiState.pageAiActiveSessionId,
|
||||
activeProfileName: pageAiCurrentProfile()
|
||||
}));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async function pageAiEnsureHermesSession() {
|
||||
async function pageAiEnsureHermesSession(forceCreate) {
|
||||
pageAiLoadSessions();
|
||||
var current = pageAiCurrentSession();
|
||||
if (current && String(current.id || '').startsWith('mnote_')) return current;
|
||||
if (!forceCreate && current && String(current.id || '').startsWith('mnote_') && current.profile === pageAiCurrentProfile()) return current;
|
||||
var response = await fetch('/api/hermes/client/sessions', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
@@ -3086,6 +3104,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
workspaceId: resolveWorkspaceId(document.body),
|
||||
documentId: currentDocumentId(),
|
||||
traceId: 'page-ai-' + Date.now().toString(36),
|
||||
profile: pageAiCurrentProfile(),
|
||||
title: current && current.title ? current.title : '当前页问答'
|
||||
})
|
||||
});
|
||||
@@ -3097,6 +3116,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
var session = {
|
||||
id: String(payload.sessionId || '').trim(),
|
||||
title: String(payload.title || '当前页问答'),
|
||||
profile: String(payload.profile || pageAiCurrentProfile()).trim() || 'default',
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
messages: pageUiState.pageAiMessages.slice()
|
||||
@@ -3118,6 +3138,9 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
var session = payload && (payload.session || (payload.upstream && payload.upstream.session) || payload);
|
||||
var messages = session && Array.isArray(session.messages) ? session.messages : [];
|
||||
if (session && (session.profile || session.profileName)) {
|
||||
current.profile = String(session.profile || session.profileName || current.profile || pageAiCurrentProfile());
|
||||
}
|
||||
if (!messages.length) return;
|
||||
pageUiState.pageAiMessages = messages.slice(-40).map(function(message) {
|
||||
return {
|
||||
@@ -3143,6 +3166,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
pageUiState.pageAiMessages = Array.isArray(session.messages) ? session.messages.slice() : [];
|
||||
pageUiState.pageAiPage = 'chat';
|
||||
pageAiPersistSessions();
|
||||
renderPageAiControls();
|
||||
renderPageAiConversation();
|
||||
}
|
||||
|
||||
@@ -3153,6 +3177,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
pageUiState.pageAiMessages = [];
|
||||
pageUiState.pageAiPage = 'chat';
|
||||
pageAiPersistSessions();
|
||||
renderPageAiControls();
|
||||
renderPageAiConversation();
|
||||
}
|
||||
|
||||
@@ -3162,6 +3187,316 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
return 'Hermes';
|
||||
}
|
||||
|
||||
function pageAiNormalizeArray(value) {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
function pageAiUnwrapUpstream(payload) {
|
||||
if (payload && typeof payload === 'object' && payload.upstream) return payload.upstream;
|
||||
return payload || null;
|
||||
}
|
||||
|
||||
function pageAiProfileValue(profile) {
|
||||
if (profile && typeof profile === 'object') {
|
||||
return String(profile.name || profile.profile || profile.id || '').trim();
|
||||
}
|
||||
return String(profile || '').trim();
|
||||
}
|
||||
|
||||
function pageAiCurrentProfile() {
|
||||
var active = String(pageUiState.pageAiActiveProfileName || '').trim();
|
||||
if (active) return active;
|
||||
var selected = pageUiState.pageAiProfiles.find(function(profile) {
|
||||
return profile && profile.active;
|
||||
});
|
||||
return pageAiProfileValue(selected) || 'default';
|
||||
}
|
||||
|
||||
function pageAiNormalizeProfiles(payload) {
|
||||
var upstream = pageAiUnwrapUpstream(payload);
|
||||
var profiles = pageAiNormalizeArray(upstream && upstream.profiles ? upstream.profiles : upstream);
|
||||
return profiles.map(function(profile) {
|
||||
return {
|
||||
name: pageAiProfileValue(profile) || 'default',
|
||||
active: Boolean(profile && profile.active),
|
||||
model: String(profile && profile.model || '').trim(),
|
||||
gateway: String(profile && profile.gateway || '').trim(),
|
||||
alias: String(profile && profile.alias || '').trim()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function pageAiNormalizeSkills(payload) {
|
||||
var upstream = pageAiUnwrapUpstream(payload);
|
||||
var categories = pageAiNormalizeArray(upstream && upstream.categories ? upstream.categories : []);
|
||||
var archived = pageAiNormalizeArray(upstream && upstream.archived ? upstream.archived : []);
|
||||
return {
|
||||
categories: categories.map(function(category) {
|
||||
return {
|
||||
name: String(category && category.name || '').trim() || 'misc',
|
||||
description: String(category && category.description || '').trim(),
|
||||
skills: pageAiNormalizeArray(category && category.skills).map(function(skill) {
|
||||
return {
|
||||
name: String(skill && skill.name || '').trim(),
|
||||
description: String(skill && skill.description || '').trim(),
|
||||
enabled: skill && skill.enabled !== false,
|
||||
source: String(skill && skill.source || 'local').trim() || 'local',
|
||||
modified: Boolean(skill && skill.modified)
|
||||
};
|
||||
})
|
||||
};
|
||||
}),
|
||||
archived: archived.map(function(skill) {
|
||||
return {
|
||||
name: String(skill && skill.name || '').trim(),
|
||||
description: String(skill && skill.description || '').trim(),
|
||||
enabled: skill && skill.enabled !== false,
|
||||
source: String(skill && skill.source || 'local').trim() || 'local',
|
||||
modified: Boolean(skill && skill.modified)
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
function pageAiSkillListEntries() {
|
||||
var result = [];
|
||||
pageAiNormalizeArray(pageUiState.pageAiSkills.categories).forEach(function(category) {
|
||||
pageAiNormalizeArray(category.skills).forEach(function(skill) {
|
||||
result.push({
|
||||
category: category.name,
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
enabled: skill.enabled !== false,
|
||||
source: skill.source || 'local',
|
||||
modified: Boolean(skill.modified)
|
||||
});
|
||||
});
|
||||
});
|
||||
return result.concat(pageAiNormalizeArray(pageUiState.pageAiSkills.archived));
|
||||
}
|
||||
|
||||
function pageAiSetActiveProfile(profileName) {
|
||||
var next = String(profileName || '').trim() || 'default';
|
||||
pageUiState.pageAiActiveProfileName = next;
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-profile', next);
|
||||
}
|
||||
|
||||
function pageAiSetRunStatus(status, runId) {
|
||||
pageUiState.pageAiRunStatus = status || 'idle';
|
||||
pageUiState.pageAiCurrentRunId = runId || pageUiState.pageAiCurrentRunId || '';
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-run-status', pageUiState.pageAiRunStatus);
|
||||
if (pageUiState.pageAiCurrentRunId) {
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-run-id', pageUiState.pageAiCurrentRunId);
|
||||
}
|
||||
}
|
||||
|
||||
function pageAiSetContextScope(scope) {
|
||||
var next = String(scope || '').trim() || 'page';
|
||||
pageUiState.pageAiContextScope = next;
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-context-scope', next);
|
||||
}
|
||||
|
||||
function pageAiRunStatusLabel(status) {
|
||||
if (status === 'queued') return '排队中';
|
||||
if (status === 'running') return '运行中';
|
||||
if (status === 'tool_calling') return '调用工具';
|
||||
if (status === 'completed') return '已完成';
|
||||
if (status === 'failed') return '失败';
|
||||
if (status === 'aborted') return '已停止';
|
||||
return '空闲';
|
||||
}
|
||||
|
||||
function pageAiMemoryFileLabel(section) {
|
||||
if (section === 'soul') return 'SOUL.md';
|
||||
if (section === 'user') return 'USER.md';
|
||||
return 'MEMORY.md';
|
||||
}
|
||||
|
||||
function pageAiContextScopeLabel(scope) {
|
||||
if (scope === 'selection') return '当前选区';
|
||||
if (scope === 'block') return '当前块';
|
||||
if (scope === 'options') return '页面设置';
|
||||
return '当前页';
|
||||
}
|
||||
|
||||
function pageAiSetDraftForSection(section, value) {
|
||||
pageUiState.pageAiProfileMemoryDrafts[section] = String(value == null ? '' : value);
|
||||
}
|
||||
|
||||
function pageAiApplyProfileMemory(payload) {
|
||||
var upstream = pageAiUnwrapUpstream(payload) || {};
|
||||
pageUiState.pageAiProfileMemory = {
|
||||
memory: String(upstream.memory || ''),
|
||||
user: String(upstream.user || ''),
|
||||
soul: String(upstream.soul || '')
|
||||
};
|
||||
pageUiState.pageAiProfileMemoryDrafts = {
|
||||
memory: pageUiState.pageAiProfileMemory.memory,
|
||||
user: pageUiState.pageAiProfileMemory.user,
|
||||
soul: pageUiState.pageAiProfileMemory.soul
|
||||
};
|
||||
}
|
||||
|
||||
function pageAiFilteredSkillEntries() {
|
||||
var query = String(pageUiState.pageAiSkillQuery || '').trim().toLowerCase();
|
||||
return pageAiSkillListEntries().filter(function(skill) {
|
||||
if (!query) return true;
|
||||
return String(skill.name || '').toLowerCase().indexOf(query) >= 0
|
||||
|| String(skill.description || '').toLowerCase().indexOf(query) >= 0
|
||||
|| String(skill.category || '').toLowerCase().indexOf(query) >= 0;
|
||||
});
|
||||
}
|
||||
|
||||
async function pageAiLoadProfiles() {
|
||||
try {
|
||||
var response = await fetch('/api/hermes/client/profiles', {
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok) throw new Error(payload && payload.code ? payload.code : 'profiles_failed_' + response.status);
|
||||
var profiles = pageAiNormalizeProfiles(payload);
|
||||
pageUiState.pageAiProfiles = profiles.length ? profiles : [{ name: 'default', active: true }];
|
||||
var active = pageAiProfileValue(pageUiState.pageAiProfiles.find(function(profile) { return profile.active; }));
|
||||
pageAiSetActiveProfile(active || pageAiCurrentProfile());
|
||||
pageUiState.pageAiProfileError = '';
|
||||
} catch (error) {
|
||||
pageUiState.pageAiProfileError = error instanceof Error ? error.message : String(error);
|
||||
if (!pageUiState.pageAiProfiles.length) pageUiState.pageAiProfiles = [{ name: 'default', active: true }];
|
||||
pageAiSetActiveProfile(pageAiCurrentProfile());
|
||||
}
|
||||
renderPageAiProviderButtons();
|
||||
renderPageAiControls();
|
||||
}
|
||||
|
||||
async function pageAiSwitchProfile(profileName) {
|
||||
var next = String(profileName || '').trim();
|
||||
if (!next) return;
|
||||
pageAiSetActiveProfile(next);
|
||||
renderPageAiProviderButtons();
|
||||
renderPageAiControls();
|
||||
try {
|
||||
var response = await fetch('/api/hermes/client/profiles/active', {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ name: next })
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok) throw new Error(payload && payload.code ? payload.code : 'profile_switch_failed_' + response.status);
|
||||
pageUiState.pageAiSessions = pageUiState.pageAiSessions.filter(function(session) {
|
||||
return session && session.profile === next;
|
||||
});
|
||||
pageUiState.pageAiActiveSessionId = '';
|
||||
pageUiState.pageAiMessages = [];
|
||||
pageAiPersistSessions();
|
||||
await pageAiEnsureHermesSession(true);
|
||||
await pageAiLoadProfileMemory();
|
||||
await pageAiLoadSkills();
|
||||
renderPageAiControls();
|
||||
} catch (error) {
|
||||
pageUiState.pageAiProfileError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
renderPageAiConversation();
|
||||
}
|
||||
|
||||
async function pageAiLoadProfileMemory() {
|
||||
try {
|
||||
var response = await fetch('/api/hermes/client/profile-memory?profile=' + encodeURIComponent(pageAiCurrentProfile()), {
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok) throw new Error(payload && payload.code ? payload.code : 'profile_memory_failed_' + response.status);
|
||||
pageAiApplyProfileMemory(payload);
|
||||
pageUiState.pageAiProfileMemoryError = '';
|
||||
} catch (error) {
|
||||
pageUiState.pageAiProfileMemoryError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
renderPageAiControls();
|
||||
renderPageAiConversation();
|
||||
}
|
||||
|
||||
async function pageAiSaveProfileMemory(section) {
|
||||
var normalized = String(section || '').trim();
|
||||
if (['memory', 'user', 'soul'].indexOf(normalized) < 0) return;
|
||||
var content = String(pageUiState.pageAiProfileMemoryDrafts[normalized] || '');
|
||||
try {
|
||||
var response = await fetch('/api/hermes/client/profile-memory', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
profile: pageAiCurrentProfile(),
|
||||
section: normalized,
|
||||
content: content
|
||||
})
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok) throw new Error(payload && payload.code ? payload.code : 'profile_memory_save_failed_' + response.status);
|
||||
pageUiState.pageAiProfileMemory[normalized] = content;
|
||||
pageUiState.pageAiProfileMemoryError = '';
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-memory-saved', normalized);
|
||||
} catch (error) {
|
||||
pageUiState.pageAiProfileMemoryError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
renderPageAiControls();
|
||||
renderPageAiConversation();
|
||||
}
|
||||
|
||||
async function pageAiLoadSkills() {
|
||||
try {
|
||||
var response = await fetch('/api/hermes/client/skills?profile=' + encodeURIComponent(pageAiCurrentProfile()), {
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok) throw new Error(payload && payload.code ? payload.code : 'skills_failed_' + response.status);
|
||||
pageUiState.pageAiSkills = pageAiNormalizeSkills(payload);
|
||||
pageUiState.pageAiSkillError = '';
|
||||
} catch (error) {
|
||||
pageUiState.pageAiSkillError = error instanceof Error ? error.message : String(error);
|
||||
pageUiState.pageAiSkills = { categories: [], archived: [] };
|
||||
}
|
||||
renderPageAiControls();
|
||||
renderPageAiConversation();
|
||||
}
|
||||
|
||||
async function pageAiToggleSkill(skillName, enabled) {
|
||||
var name = String(skillName || '').trim();
|
||||
if (!name) return;
|
||||
var previous = null;
|
||||
pageAiSkillListEntries().forEach(function(skill) {
|
||||
if (skill.name === name && previous == null) previous = skill.enabled !== false;
|
||||
});
|
||||
try {
|
||||
var response = await fetch('/api/hermes/client/skills/toggle', {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
profile: pageAiCurrentProfile(),
|
||||
name: name,
|
||||
enabled: Boolean(enabled)
|
||||
})
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok) throw new Error(payload && payload.code ? payload.code : 'skill_toggle_failed_' + response.status);
|
||||
pageAiNormalizeArray(pageUiState.pageAiSkills.categories).forEach(function(category) {
|
||||
pageAiNormalizeArray(category.skills).forEach(function(skill) {
|
||||
if (skill.name === name) skill.enabled = Boolean(enabled);
|
||||
});
|
||||
});
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-skill-toggled', name);
|
||||
pageUiState.pageAiSkillError = '';
|
||||
} catch (error) {
|
||||
pageUiState.pageAiSkillError = error instanceof Error ? error.message : String(error);
|
||||
if (previous != null) {
|
||||
pageAiNormalizeArray(pageUiState.pageAiSkills.categories).forEach(function(category) {
|
||||
pageAiNormalizeArray(category.skills).forEach(function(skill) {
|
||||
if (skill.name === name) skill.enabled = previous;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
renderPageAiControls();
|
||||
renderPageAiConversation();
|
||||
}
|
||||
|
||||
function renderPageAiProviderButtons() {
|
||||
var drawer = ensurePageAiDrawer();
|
||||
drawer.querySelectorAll('[data-page-ai-provider]').forEach(function(button) {
|
||||
@@ -3176,6 +3511,103 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
}
|
||||
|
||||
function renderPageAiControls() {
|
||||
var drawer = ensurePageAiDrawer();
|
||||
var activeProfile = pageAiCurrentProfile();
|
||||
var profileSelect = drawer.querySelector('[data-page-ai-profile-select]');
|
||||
if (profileSelect instanceof HTMLSelectElement) {
|
||||
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;
|
||||
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 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 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]');
|
||||
if (scopeLabel instanceof HTMLElement) scopeLabel.textContent = pageAiContextScopeLabel(pageUiState.pageAiContextScope);
|
||||
drawer.querySelectorAll('[data-page-ai-tab]').forEach(function(button) {
|
||||
var target = button.getAttribute('data-page-ai-tab') || 'chat';
|
||||
var active = target === pageUiState.pageAiPage;
|
||||
button.classList.toggle('is-active', active);
|
||||
button.setAttribute('aria-selected', active ? 'true' : 'false');
|
||||
});
|
||||
drawer.querySelectorAll('[data-page-ai-panel]').forEach(function(panel) {
|
||||
if (panel instanceof HTMLElement) {
|
||||
panel.hidden = panel.getAttribute('data-page-ai-panel') !== pageUiState.pageAiPage;
|
||||
}
|
||||
});
|
||||
var profileError = drawer.querySelector('[data-page-ai-profile-error]');
|
||||
if (profileError instanceof HTMLElement) {
|
||||
profileError.textContent = pageUiState.pageAiProfileError || '';
|
||||
profileError.hidden = !pageUiState.pageAiProfileError;
|
||||
}
|
||||
var memoryError = drawer.querySelector('[data-page-ai-memory-error]');
|
||||
if (memoryError instanceof HTMLElement) {
|
||||
memoryError.textContent = pageUiState.pageAiProfileMemoryError || '';
|
||||
memoryError.hidden = !pageUiState.pageAiProfileMemoryError;
|
||||
}
|
||||
var agentPanel = drawer.querySelector('[data-page-ai-agent-panel]');
|
||||
if (agentPanel instanceof HTMLElement) {
|
||||
agentPanel.innerHTML = ['soul', 'user', 'memory'].map(function(section) {
|
||||
var label = pageAiMemoryFileLabel(section);
|
||||
var value = pageUiState.pageAiProfileMemoryDrafts[section] || '';
|
||||
return '' +
|
||||
'<section class="wolai-page-ai-memory-card">' +
|
||||
'<div class="wolai-page-ai-memory-head">' +
|
||||
'<div>' +
|
||||
'<div class="wolai-page-ai-memory-title">' + escapeHtml(label) + '</div>' +
|
||||
'<div class="wolai-page-ai-memory-scope">保存到 Hermes profile: ' + escapeHtml(activeProfile) + '</div>' +
|
||||
'</div>' +
|
||||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-memory-save="' + escapeHtml(section) + '">保存</button>' +
|
||||
'</div>' +
|
||||
'<textarea class="wolai-page-ai-memory-editor" data-page-ai-memory-editor="' + escapeHtml(section) + '" spellcheck="false">' + escapeHtml(value) + '</textarea>' +
|
||||
'</section>';
|
||||
}).join('');
|
||||
}
|
||||
var skillError = drawer.querySelector('[data-page-ai-skill-error]');
|
||||
if (skillError instanceof HTMLElement) {
|
||||
skillError.textContent = pageUiState.pageAiSkillError || '';
|
||||
skillError.hidden = !pageUiState.pageAiSkillError;
|
||||
}
|
||||
var skillSearch = drawer.querySelector('[data-page-ai-skill-search]');
|
||||
if (skillSearch instanceof HTMLInputElement && document.activeElement !== skillSearch) {
|
||||
skillSearch.value = pageUiState.pageAiSkillQuery;
|
||||
}
|
||||
var skillList = drawer.querySelector('[data-page-ai-skill-list]');
|
||||
if (skillList instanceof HTMLElement) {
|
||||
var skills = pageAiFilteredSkillEntries();
|
||||
if (!skills.length) {
|
||||
skillList.innerHTML = '<div class="wolai-page-ai-empty">没有匹配的 Hermes skill。</div>';
|
||||
} else {
|
||||
skillList.innerHTML = skills.map(function(skill) {
|
||||
var source = skill.source || 'local';
|
||||
var sourceText = source + (skill.modified ? ' · modified' : '');
|
||||
return '' +
|
||||
'<div class="wolai-page-ai-skill-row">' +
|
||||
'<div class="wolai-page-ai-skill-copy">' +
|
||||
'<div class="wolai-page-ai-skill-name">' + escapeHtml(skill.name) + '</div>' +
|
||||
'<div class="wolai-page-ai-skill-desc">' + escapeHtml(skill.description || '无描述') + '</div>' +
|
||||
'<div class="wolai-page-ai-skill-source">' + escapeHtml(sourceText) + '</div>' +
|
||||
'</div>' +
|
||||
'<button type="button" class="wolai-page-ai-skill-switch' + (skill.enabled !== false ? ' is-on' : '') + '" data-page-ai-skill-toggle="' + escapeHtml(skill.name) + '" aria-pressed="' + (skill.enabled !== false ? 'true' : 'false') + '">' +
|
||||
'<span></span>' +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function humanizePageAiResponse(rawText, promptText) {
|
||||
var providerLabel = pageAiProviderLabel(pageUiState.pageAiProvider);
|
||||
var text = String(rawText || '').trim();
|
||||
@@ -3210,32 +3642,85 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
'<div class="wolai-page-ai-panel" role="dialog" aria-modal="false">' +
|
||||
'<div class="wolai-page-ai-header">' +
|
||||
'<div class="wolai-page-ai-header-copy">' +
|
||||
'<h2 class="wolai-page-ai-title" data-title="page-ai-title">智能问答</h2>' +
|
||||
'<h2 class="wolai-page-ai-title" data-title="page-ai-title">页面 AI</h2>' +
|
||||
'<div class="wolai-page-ai-subtitle">当前页面上下文优先 · Hermes</div>' +
|
||||
'</div>' +
|
||||
'<button type="button" class="wolai-surface-close" data-page-ai-action="close" aria-label="关闭页面 AI">×</button>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-page-ai-body">' +
|
||||
'<div class="wolai-page-ai-suggestions">' +
|
||||
'<div class="wolai-page-ai-suggestions-header">' +
|
||||
'<span>推荐问题</span>' +
|
||||
'<div class="wolai-page-ai-intents">' +
|
||||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-intent="create-summary">创建 Summary</button>' +
|
||||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-intent="create-ai-note">创建 AI Note</button>' +
|
||||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-action="rotate">换一换</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-page-ai-suggestion-list" data-page-ai-suggestion-list></div>' +
|
||||
'<div class="wolai-page-ai-statusbar">' +
|
||||
'<div class="wolai-page-ai-statusitem">' +
|
||||
'<span class="wolai-page-ai-statuslabel">会话</span>' +
|
||||
'<span class="wolai-page-ai-statusvalue" data-page-ai-session-status>等待 Hermes session</span>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-page-ai-conversation" data-page-ai-conversation></div>' +
|
||||
'<div class="wolai-page-ai-statusitem">' +
|
||||
'<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-context-scope-label>当前页</span>' +
|
||||
'</div>' +
|
||||
'<a class="wolai-page-ai-settings-link" href="/auth" target="_blank" rel="noreferrer">Hermes 设置</a>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-page-ai-topbar">' +
|
||||
'<label class="wolai-page-ai-profile-select">' +
|
||||
'<span>agent / profile</span>' +
|
||||
'<select data-page-ai-profile-select></select>' +
|
||||
'</label>' +
|
||||
'<label class="wolai-page-ai-context-select">' +
|
||||
'<span>上下文</span>' +
|
||||
'<select data-page-ai-context-scope>' +
|
||||
'<option value="page">当前页</option>' +
|
||||
'<option value="selection">当前选区</option>' +
|
||||
'<option value="block">当前块</option>' +
|
||||
'<option value="options">页面设置</option>' +
|
||||
'</select>' +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-page-ai-inline-error" data-page-ai-profile-error hidden></div>' +
|
||||
'<div class="wolai-page-ai-body">' +
|
||||
'<div class="wolai-page-ai-tabs" role="tablist" aria-label="页面 AI 面板">' +
|
||||
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-tab="chat" role="tab" aria-selected="true">chat</button>' +
|
||||
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="agent" role="tab" aria-selected="false">agent</button>' +
|
||||
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="skills" role="tab" aria-selected="false">skills</button>' +
|
||||
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="history" role="tab" aria-selected="false">history</button>' +
|
||||
'</div>' +
|
||||
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="chat">' +
|
||||
'<div class="wolai-page-ai-suggestions">' +
|
||||
'<div class="wolai-page-ai-suggestions-header">' +
|
||||
'<span>推荐问题</span>' +
|
||||
'<div class="wolai-page-ai-intents">' +
|
||||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-intent="create-summary">创建 Summary</button>' +
|
||||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-intent="create-ai-note">创建 AI Note</button>' +
|
||||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-action="rotate">换一换</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-page-ai-suggestion-list" data-page-ai-suggestion-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>' +
|
||||
'<div class="wolai-page-ai-inline-error" data-page-ai-memory-error hidden></div>' +
|
||||
'<div class="wolai-page-ai-memory-grid" data-page-ai-agent-panel></div>' +
|
||||
'</section>' +
|
||||
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="skills" hidden>' +
|
||||
'<div class="wolai-page-ai-skills-toolbar">' +
|
||||
'<label class="wolai-page-ai-skill-search">' +
|
||||
'<span>搜索技能</span>' +
|
||||
'<input type="search" data-page-ai-skill-search placeholder="搜索技能…" />' +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-page-ai-inline-error" data-page-ai-skill-error hidden></div>' +
|
||||
'<div class="wolai-page-ai-skill-list" data-page-ai-skill-list></div>' +
|
||||
'</section>' +
|
||||
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="history" hidden>' +
|
||||
'<div class="wolai-page-ai-conversation" data-page-ai-conversation></div>' +
|
||||
'</section>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-page-ai-footer">' +
|
||||
'<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>' +
|
||||
'<div class="wolai-page-ai-provider-group">' +
|
||||
'<button type="button" class="wolai-page-ai-model-chip is-active" data-page-ai-provider="hermes" aria-pressed="true">Hermes</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-page-ai-input-row">' +
|
||||
'<textarea class="wolai-page-ai-input" data-page-ai-input rows="3" placeholder="问我你想知道的"></textarea>' +
|
||||
@@ -3249,7 +3734,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
|
||||
function renderPageAiSuggestions() {
|
||||
var drawer = ensurePageAiDrawer();
|
||||
var list = drawer.querySelector('[data-page-ai-suggestion-list]');
|
||||
var list = drawer.querySelector('[data-page-ai-panel="chat"] [data-page-ai-suggestion-list]');
|
||||
if (!(list instanceof HTMLElement)) return;
|
||||
var suggestions = pageAiSuggestions();
|
||||
var offset = pageUiState.pageAiSuggestionIndex % suggestions.length;
|
||||
@@ -3261,7 +3746,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
|
||||
function renderPageAiConversation() {
|
||||
var drawer = ensurePageAiDrawer();
|
||||
var conversation = drawer.querySelector('[data-page-ai-conversation]');
|
||||
var conversation = drawer.querySelector('[data-page-ai-panel="' + cssEscape(pageUiState.pageAiPage || 'chat') + '"] [data-page-ai-conversation]');
|
||||
if (!(conversation instanceof HTMLElement)) return;
|
||||
if (pageUiState.pageAiPage === 'history') {
|
||||
if (!pageUiState.pageAiSessions.length) {
|
||||
@@ -3301,18 +3786,30 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
renderPageAiSuggestions();
|
||||
renderPageAiConversation();
|
||||
renderPageAiProviderButtons();
|
||||
renderPageAiControls();
|
||||
var drawer = ensurePageAiDrawer();
|
||||
drawer.hidden = false;
|
||||
pageUiState.pageAiOpen = true;
|
||||
updatePageAiTriggerState();
|
||||
pageAiEnsureHermesSession().then(function() {
|
||||
Promise.all([
|
||||
pageAiLoadProfiles(),
|
||||
pageAiLoadProfileMemory(),
|
||||
pageAiLoadSkills()
|
||||
]).then(function() {
|
||||
renderPageAiControls();
|
||||
}).catch(function() {}).then(function() {
|
||||
return pageAiEnsureHermesSession();
|
||||
}).then(function() {
|
||||
return pageAiRestoreHermesSession();
|
||||
}).then(function() {
|
||||
renderPageAiControls();
|
||||
}).catch(function(error) {
|
||||
pageUiState.pageAiMessages.push({
|
||||
role: 'assistant',
|
||||
content: 'Hermes 当前不可用:' + (error instanceof Error ? error.message : String(error))
|
||||
});
|
||||
renderPageAiConversation();
|
||||
renderPageAiControls();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3366,6 +3863,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
var currentSession = null;
|
||||
try {
|
||||
await pageAiEnsureHermesSession();
|
||||
pageAiSetRunStatus('queued');
|
||||
var contextSnapshot = currentPageAiContextSnapshot();
|
||||
var aggregate = contextSnapshot.aggregate || {};
|
||||
var body = contextSnapshot.body || {};
|
||||
@@ -3388,6 +3886,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
workspaceId: resolveWorkspaceId(document.body),
|
||||
documentId: currentDocumentId(),
|
||||
sessionId: pageUiState.pageAiActiveSessionId,
|
||||
profile: pageAiCurrentProfile(),
|
||||
message: prompt,
|
||||
model: 'hermes-agent',
|
||||
pageContext: {
|
||||
@@ -3415,6 +3914,7 @@ 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');
|
||||
pageAiSetRunStatus('running', runId);
|
||||
var eventResponse = await fetch('/api/hermes/client/events/' + encodeURIComponent(runId), {
|
||||
headers: { 'accept': 'text/event-stream' }
|
||||
});
|
||||
@@ -3437,6 +3937,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
var completed = JSON.parse(payloadText || 'null');
|
||||
if (completed && completed.output) assistantText = searchText(completed.output);
|
||||
} catch (_) {}
|
||||
pageAiSetRunStatus('completed', runId);
|
||||
}
|
||||
if (eventName === 'tool.started' || eventName === 'tool.completed' || eventName === 'tool.failed') {
|
||||
try {
|
||||
@@ -3449,6 +3950,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
pageUiState.pageAiMessages.push({ role: 'tool', content: eventName });
|
||||
}
|
||||
renderPageAiConversation();
|
||||
pageAiSetRunStatus('tool_calling', runId);
|
||||
}
|
||||
});
|
||||
pageUiState.pageAiMessages.push({
|
||||
@@ -3461,6 +3963,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
currentSession.updatedAt = Date.now();
|
||||
}
|
||||
} catch (error) {
|
||||
pageAiSetRunStatus('failed');
|
||||
pageUiState.pageAiMessages.push({
|
||||
role: 'assistant',
|
||||
content: 'Hermes 当前请求失败:' + (error instanceof Error ? error.message : String(error))
|
||||
@@ -3473,6 +3976,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
} finally {
|
||||
pageUiState.pageAiBusy = false;
|
||||
renderPageAiConversation();
|
||||
renderPageAiControls();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4039,6 +4543,15 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
}
|
||||
|
||||
var pageAiTab = closestAction(e.target, '[data-page-ai-tab]');
|
||||
if (pageAiTab) {
|
||||
e.preventDefault();
|
||||
pageUiState.pageAiPage = pageAiTab.getAttribute('data-page-ai-tab') || 'chat';
|
||||
renderPageAiControls();
|
||||
renderPageAiConversation();
|
||||
return;
|
||||
}
|
||||
|
||||
var pageAiProvider = closestAction(e.target, '[data-page-ai-provider]');
|
||||
if (pageAiProvider) {
|
||||
e.preventDefault();
|
||||
@@ -4047,6 +4560,22 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
return;
|
||||
}
|
||||
|
||||
var pageAiMemorySave = closestAction(e.target, '[data-page-ai-memory-save]');
|
||||
if (pageAiMemorySave) {
|
||||
e.preventDefault();
|
||||
void pageAiSaveProfileMemory(pageAiMemorySave.getAttribute('data-page-ai-memory-save') || '');
|
||||
return;
|
||||
}
|
||||
|
||||
var pageAiSkillToggle = closestAction(e.target, '[data-page-ai-skill-toggle]');
|
||||
if (pageAiSkillToggle) {
|
||||
e.preventDefault();
|
||||
var skillName = pageAiSkillToggle.getAttribute('data-page-ai-skill-toggle') || '';
|
||||
var nextEnabled = pageAiSkillToggle.getAttribute('aria-pressed') !== 'true';
|
||||
void pageAiToggleSkill(skillName, nextEnabled);
|
||||
return;
|
||||
}
|
||||
|
||||
var pageAiSession = closestAction(e.target, '[data-page-ai-session]');
|
||||
if (pageAiSession) {
|
||||
e.preventDefault();
|
||||
@@ -4079,6 +4608,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
e.preventDefault();
|
||||
pageAiLoadSessions();
|
||||
pageUiState.pageAiPage = pageUiState.pageAiPage === 'history' ? 'chat' : 'history';
|
||||
renderPageAiControls();
|
||||
renderPageAiConversation();
|
||||
return;
|
||||
}
|
||||
@@ -4281,7 +4811,34 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('input', function(event) {
|
||||
var skillSearch = closestAction(event.target, '[data-page-ai-skill-search]');
|
||||
if (skillSearch instanceof HTMLInputElement) {
|
||||
pageUiState.pageAiSkillQuery = skillSearch.value;
|
||||
renderPageAiControls();
|
||||
return;
|
||||
}
|
||||
var memoryEditor = closestAction(event.target, '[data-page-ai-memory-editor]');
|
||||
if (memoryEditor instanceof HTMLTextAreaElement) {
|
||||
var section = memoryEditor.getAttribute('data-page-ai-memory-editor') || '';
|
||||
if (['memory', 'user', 'soul'].indexOf(section) >= 0) {
|
||||
pageUiState.pageAiProfileMemoryDrafts[section] = memoryEditor.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('change', function(event) {
|
||||
var pageAiProfileSelect = closestAction(event.target, '[data-page-ai-profile-select]');
|
||||
if (pageAiProfileSelect instanceof HTMLSelectElement) {
|
||||
void pageAiSwitchProfile(pageAiProfileSelect.value);
|
||||
return;
|
||||
}
|
||||
var pageAiContextSelect = closestAction(event.target, '[data-page-ai-context-scope]');
|
||||
if (pageAiContextSelect instanceof HTMLSelectElement) {
|
||||
pageAiSetContextScope(pageAiContextSelect.value);
|
||||
renderPageAiControls();
|
||||
return;
|
||||
}
|
||||
var globalCheckbox = closestAction(event.target, '[data-global-option-checkbox="showHeadingNumbers"]');
|
||||
if (globalCheckbox instanceof HTMLInputElement) {
|
||||
writeGlobalShowHeadingNumbers(globalCheckbox.checked);
|
||||
|
||||
Reference in New Issue
Block a user