Files
gott/wwwroot/js/modals.js
T
Meleki 3b7bdb7a20 feat: OpenClaw session import with sorted history
Backend:
- New OpenClawEndpoint with GET /api/openclaw/sessions and POST /api/openclaw/import
- Lists all OpenClaw sessions with preview messages, model, summary
- Imports user/assistant text messages (skips tool-use/tool-result)
- Cleans user messages (removes webchat prefixes, cron tags, conversation info blocks)
- Re-import support: deletes previous openclaw-import chains before importing
- New DatabaseService methods: CreateChainForImport, CreateMessageForImport, DeleteOpenClawImport
- Added SortKey to MessageRecord (computed: openclaw-import gets id-10B, else id)

History sorting:
- OpenClaw-imported messages always appear before native messages
- Sort by CASE WHEN tag='openclaw-import' THEN id-10000000000 ELSE id END
- Cursor-based paging updated from beforeId/afterId to beforeSortKey/afterSortKey
- All history queries (GetLatestActive, Before, After, CountBefore) updated

Frontend:
- Dashboard: new 'OpenClaw Import' menu item in session card dropdown
- Modal: shows OpenClaw sessions with metadata, summary, preview messages
- Imported messages display blue 'OC' badge in chat
- API functions: apiGetOpenclawSessions, apiImportOpenclawSession
- History paging updated to use sortKey cursors
2026-03-08 17:32:32 +01:00

290 lines
11 KiB
JavaScript

// ── Modal Helper ──
function _createModal(titleText, extraClasses) {
const overlay = document.createElement('div');
overlay.className = 'sp-overlay' + (extraClasses && extraClasses.overlay ? ' ' + extraClasses.overlay : '');
const modal = document.createElement('div');
modal.className = 'sp-modal' + (extraClasses && extraClasses.modal ? ' ' + extraClasses.modal : '');
const header = document.createElement('div');
header.className = 'sp-header';
const title = document.createElement('span');
title.textContent = titleText;
const closeBtn = document.createElement('button');
closeBtn.innerHTML = '<i class="fa fa-times"></i>';
closeBtn.className = 'sp-close icon-btn';
header.appendChild(title);
header.appendChild(closeBtn);
const body = document.createElement('div');
body.className = 'sp-body' + (extraClasses && extraClasses.body ? ' ' + extraClasses.body : '');
modal.appendChild(header);
modal.appendChild(body);
overlay.appendChild(modal);
document.body.appendChild(overlay);
const close = () => overlay.remove();
closeBtn.addEventListener('click', close);
overlay.addEventListener('click', (e) => {
if (e.target === overlay) close();
});
const onKey = (e) => {
if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); }
};
document.addEventListener('keydown', onKey);
return { overlay, modal, body, close };
}
// ── System-Prompt Modal ──
function _renderSystemPromptBlocks(container, blocks) {
for (var i = 0; i < blocks.length; i++) {
var block = blocks[i];
if (!block.content) continue;
var wrapper = document.createElement('div');
wrapper.className = 'sp-block';
var label = document.createElement('div');
label.className = 'sp-block-label';
label.innerHTML = '<i class="fa ' + esc(block.icon || 'fa-cog') + '"></i> ' + esc(block.label || 'Block');
wrapper.appendChild(label);
var content = document.createElement('div');
content.className = 'sp-block-content markdown';
content.innerHTML = renderMarkdown(block.content);
parseFluentEmojis(content);
wrapper.appendChild(content);
container.appendChild(wrapper);
}
}
async function showSystemPromptModal(sessionId) {
const { body } = _createModal('System-Prompt', { body: 'markdown' });
body.innerHTML = '<div class="sp-loading">Laden...</div>';
try {
const blocks = await apiGetSystemPromptBlocks(sessionId, typeof _planModeActive !== 'undefined' && _planModeActive);
body.innerHTML = '';
_renderSystemPromptBlocks(body, blocks);
} catch (e) {
body.innerHTML = '<div class="sp-loading" style="color:var(--dangerous)">Fehler beim Laden des System-Prompts</div>';
}
}
// ── Command Output Modal ──
function showCommandOutputModal(name, command, result) {
const { body } = _createModal(name, { modal: 'cmd-output-modal' });
const exitCode = result.exitCode != null ? result.exitCode : '?';
const isError = exitCode !== 0;
const statusText = result.status === 'timeout' ? 'Timeout' : `Exit ${exitCode}`;
const meta = document.createElement('div');
meta.className = 'cmd-output-meta';
meta.innerHTML = `<code>${esc(command)}</code> <span class="cmd-exit-code${isError ? ' cmd-exit-error' : ''}">${esc(statusText)}</span>`;
body.appendChild(meta);
const output = document.createElement('pre');
output.className = 'cmd-output-content';
output.textContent = result.output || '(kein Output)';
body.appendChild(output);
}
// ── Krea Image Preview Modal ──
function showKreaImageModal(titleText, fullUrl) {
const { body } = _createModal(titleText, { overlay: 'krea-overlay', modal: 'krea-modal', body: 'krea-modal-body' });
var img = document.createElement('img');
img.src = fullUrl;
img.className = 'krea-modal-image';
img.alt = titleText;
img.addEventListener('error', function() {
body.innerHTML = '<div class="sp-loading" style="color:var(--dangerous)">Bild konnte nicht geladen werden</div>';
});
body.appendChild(img);
}
// ── Picker Modal Helper ──
function _openPickerModal(title, items, currentId, onSelect, options) {
options = options || {};
const noneLabel = options.noneLabel || 'Keine Auswahl';
const noneIcon = options.noneIcon || 'fa-ban';
const overlay = document.createElement('div');
overlay.className = 'agent-picker-overlay';
const modal = document.createElement('div');
modal.className = 'agent-picker-modal';
const header = document.createElement('div');
header.className = 'agent-picker-header';
header.innerHTML = `<span>${esc(title)}</span><button class="icon-btn agent-picker-close"><i class="fa fa-times"></i></button>`;
modal.appendChild(header);
const body = document.createElement('div');
body.className = 'agent-picker-body';
// "Keine" Option
const noneCard = document.createElement('div');
noneCard.className = 'card agent-picker-item' + (!currentId ? ' selected' : '');
noneCard.innerHTML = `
<div class="card-image" style="display:flex;align-items:center;justify-content:center"><i class="fa ${esc(noneIcon)}" style="font-size:2em;opacity:var(--semi-transparent)"></i></div>
<div class="card-body">
<div class="card-title-row">
<span class="card-title">${esc(noneLabel)}</span>
</div>
</div>
`;
noneCard.addEventListener('click', () => {
onSelect(null);
overlay.remove();
});
body.appendChild(noneCard);
// Items
for (const item of items) {
const cardEl = options.buildCard(item);
cardEl.classList.add('agent-picker-item');
if (item._pickerId === currentId) cardEl.classList.add('selected');
cardEl.addEventListener('click', () => {
onSelect(item._pickerId);
overlay.remove();
});
body.appendChild(cardEl);
}
modal.appendChild(body);
overlay.appendChild(modal);
document.body.appendChild(overlay);
header.querySelector('.agent-picker-close').addEventListener('click', () => overlay.remove());
overlay.addEventListener('click', (e) => {
if (e.target === overlay) overlay.remove();
});
}
// ── OpenClaw Import Modal ──
async function showOpenClawImportModal(sessionId) {
const { body, close } = _createModal('OpenClaw Import', { modal: 'openclaw-import-modal' });
body.innerHTML = '<div class="sp-loading">Sessions werden geladen...</div>';
try {
const sessions = await apiGetOpenclawSessions();
if (!sessions || sessions.length === 0) {
body.innerHTML = '<div class="sp-loading">Keine OpenClaw-Sessions gefunden</div>';
return;
}
body.innerHTML = '';
var hint = document.createElement('div');
hint.className = 'openclaw-hint';
hint.textContent = 'Wähle eine OpenClaw-Session zum Importieren. Vorherige Imports werden aktualisiert.';
body.appendChild(hint);
var list = document.createElement('div');
list.className = 'openclaw-session-list';
for (var i = 0; i < sessions.length; i++) {
var s = sessions[i];
var card = document.createElement('div');
card.className = 'openclaw-session-card';
// Header
var header = document.createElement('div');
header.className = 'openclaw-card-header';
var idSpan = document.createElement('span');
idSpan.className = 'openclaw-session-id';
idSpan.textContent = s.sessionId.substring(0, 8) + '…';
idSpan.title = s.sessionId;
header.appendChild(idSpan);
var meta = document.createElement('span');
meta.className = 'openclaw-card-meta';
meta.innerHTML = '<span class="openclaw-meta-item"><i class="fa fa-calendar"></i> ' + esc(formatDate(s.createdAt)) + '</span>'
+ '<span class="openclaw-meta-item"><i class="fa fa-comments"></i> ' + s.totalMessages + ' Nachrichten</span>'
+ (s.model && s.model !== 'unknown' ? '<span class="openclaw-meta-item"><i class="fa fa-microchip"></i> ' + esc(s.model) + '</span>' : '');
header.appendChild(meta);
card.appendChild(header);
// Summary
if (s.summary) {
var summary = document.createElement('div');
summary.className = 'openclaw-summary';
// Take first line of summary that's meaningful (skip empty lines and headers)
var summaryLines = s.summary.split('\n').filter(function(l) { return l.trim() && !l.startsWith('#'); });
var summaryText = summaryLines.slice(0, 2).join(' ').trim();
if (summaryText.length > 150) summaryText = summaryText.substring(0, 150) + '…';
summary.textContent = summaryText || s.summary.substring(0, 150);
card.appendChild(summary);
}
// Preview messages
if (s.previewMessages && s.previewMessages.length > 0) {
var preview = document.createElement('div');
preview.className = 'openclaw-preview';
for (var j = 0; j < s.previewMessages.length; j++) {
var pm = s.previewMessages[j];
var line = document.createElement('div');
line.className = 'openclaw-preview-msg openclaw-preview-' + pm.role;
var icon = pm.role === 'user' ? 'fa-user' : 'fa-cog';
line.innerHTML = '<i class="fa ' + icon + '"></i> <span>' + esc(pm.text) + '</span>';
preview.appendChild(line);
}
card.appendChild(preview);
}
// Click handler - capture session in closure
(function(openclawSession) {
card.addEventListener('click', async function() {
// Confirm
var confirmed = confirm('OpenClaw-Session importieren?\n\nSession: ' + openclawSession.sessionId.substring(0, 8) + '…\n' + openclawSession.totalMessages + ' Nachrichten\n\nVorhandene Imports werden ersetzt.');
if (!confirmed) return;
card.style.opacity = '0.5';
card.style.pointerEvents = 'none';
try {
var result = await apiImportOpenclawSession(openclawSession.sessionId, sessionId);
close();
showToast(result.messageCount + ' Nachrichten in ' + result.chainCount + ' Chains importiert', 'success');
} catch (e) {
card.style.opacity = '1';
card.style.pointerEvents = '';
showToast('Import fehlgeschlagen: ' + e.message, 'error');
}
});
})(s);
list.appendChild(card);
}
body.appendChild(list);
} catch (e) {
body.innerHTML = '<div class="sp-loading" style="color:var(--dangerous)">Fehler: ' + esc(e.message) + '</div>';
}
}
// Event-Delegation für Krea-Thumbnails im Chat
document.addEventListener('click', function(e) {
var ref = e.target.closest('.krea-ref');
if (ref) {
var fullUrl = ref.dataset.fullurl;
var filename = ref.dataset.filename;
if (fullUrl) showKreaImageModal(filename || 'Bild', fullUrl);
}
});