v1.0.1
This commit is contained in:
+101
-19
@@ -4154,6 +4154,76 @@ function toggleSteps(id) {
|
||||
el.previousElementSibling.textContent = `${visible ? '?' : '?'} ${el.querySelectorAll('.react-step').length} tool step${el.querySelectorAll('.react-step').length !== 1 ? 's' : ''} used`;
|
||||
}
|
||||
|
||||
function renderFileDownloads(steps, processEntries) {
|
||||
const seenUrls = new Set();
|
||||
const buttons = [];
|
||||
const URL_RE = /\/api\/files\/[^\s)"'\]]+/g;
|
||||
|
||||
function extractUrls(text) {
|
||||
const urls = [];
|
||||
let m;
|
||||
while ((m = URL_RE.exec(text)) !== null) urls.push(m[0]);
|
||||
return urls;
|
||||
}
|
||||
|
||||
// Collect /api/files/ URLs from all available sources
|
||||
const allUrls = [];
|
||||
|
||||
// 1. msg.steps (legacy step events)
|
||||
if (steps && steps.length) {
|
||||
for (const s of steps) {
|
||||
const resultText = typeof s.toolResult === 'string' ? s.toolResult : JSON.stringify(s.toolResult || '');
|
||||
allUrls.push(...extractUrls(resultText));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. msg.processEntries (stored per-message in chatHistory)
|
||||
if (processEntries && processEntries.length) {
|
||||
for (const e of processEntries) {
|
||||
const txt = String(e.content || '');
|
||||
if (txt.indexOf('/api/files/') !== -1) allUrls.push(...extractUrls(txt));
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Global processLogEntries (current session's live log)
|
||||
try {
|
||||
const entries = (typeof processLogEntries !== 'undefined' && Array.isArray(processLogEntries)) ? processLogEntries : [];
|
||||
for (const e of entries) {
|
||||
const txt = String(e.content || '');
|
||||
if (txt.indexOf('/api/files/') !== -1) allUrls.push(...extractUrls(txt));
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// 4. msg.content itself (LLM may include the link in its reply)
|
||||
if (arguments.length > 2 && arguments[2]) {
|
||||
const contentText = String(arguments[2]);
|
||||
if (contentText.indexOf('/api/files/') !== -1) allUrls.push(...extractUrls(contentText));
|
||||
}
|
||||
|
||||
for (const rawUrl of allUrls) {
|
||||
if (seenUrls.has(rawUrl)) continue;
|
||||
seenUrls.add(rawUrl);
|
||||
const decoded = decodeURIComponent(rawUrl);
|
||||
const filename = decoded.split('/').pop() || 'download';
|
||||
const isPptx = decoded.toLowerCase().indexOf('.pptx') !== -1;
|
||||
if (isPptx) {
|
||||
const previewId = 'pptx-chat-' + Math.random().toString(36).substr(2, 9);
|
||||
buttons.push(`
|
||||
<div class="pptx-file-row" style="margin:6px 0">
|
||||
<a href="${escHtml(rawUrl)}" onclick="downloadFile(event, this)" class="download-btn" title="Download ${escHtml(filename)}">
|
||||
<span class="download-icon">⬇</span> ${escHtml(filename)}</a>
|
||||
<button class="pptx-preview-btn" onclick="togglePptxPreview('${escHtml(rawUrl)}', '${previewId}', this)" title="Preview slides">◀ Preview</button>
|
||||
<div id="${previewId}" class="pptx-preview-strip" style="display:none"></div>
|
||||
</div>`);
|
||||
} else {
|
||||
buttons.push(`
|
||||
<a href="${escHtml(rawUrl)}" onclick="downloadFile(event, this)" class="download-btn" title="Download ${escHtml(filename)}">
|
||||
<span class="download-icon">⬇</span> ${escHtml(filename)}</a>`);
|
||||
}
|
||||
}
|
||||
return buttons.length ? `<div style="margin-top:8px">${buttons.join(' ')}</div>` : '';
|
||||
}
|
||||
|
||||
function renderAssistantContent(content) {
|
||||
const text = String(content || '');
|
||||
const hasStaged = /(^|\n)(Initial chat|Execution result|Final chat):\s*/i.test(text);
|
||||
@@ -4264,6 +4334,7 @@ function renderChatMessages() {
|
||||
<div class="msg-role">${msg.role === 'user' ? 'You' : (msg.mode === 'agentic' ? 'CherryClaw (Agent)' : 'CherryClaw')}</div>
|
||||
${msg.role === 'ai' ? renderAssistantContent(msg.content) : `<div class="msg-content">${renderMarkdown(msg.content)}</div>`}
|
||||
${msg.role === 'ai' ? renderArtifacts(msg.artifacts) : ''}
|
||||
${msg.role === 'ai' ? renderFileDownloads(msg.steps, msg.processEntries, msg.content) : ''}
|
||||
${msg.role === 'ai' && msg.processEntries && msg.processEntries.length ? renderProcessPill(msg.processEntries) : ''}
|
||||
${msg.role === 'ai' ? renderThinkBlock(msg.thinking) : ''}
|
||||
${msg.role === 'ai' ? renderReactSteps(msg.steps) : ''}
|
||||
@@ -4335,7 +4406,9 @@ function formatProcessLines(entries) {
|
||||
const rawType = String(e.type || 'info');
|
||||
const typeLabel = escHtml(rawType).toUpperCase();
|
||||
const typeColor = TYPE_COLORS[rawType] || 'var(--muted)';
|
||||
const content = escHtml(String(e.content || ''));
|
||||
const rawContent = String(e.content || '');
|
||||
// Render markdown for result entries so download links show as clickable buttons
|
||||
const content = rawType === 'result' ? renderMarkdown(rawContent) : escHtml(rawContent);
|
||||
return `<div style="margin-bottom:4px"><span style="color:var(--muted)">[${escHtml(String(e.ts || ''))}]</span> <span style="color:${typeColor};font-weight:600">${typeLabel}</span> ${content}</div>`;
|
||||
}).join('');
|
||||
}
|
||||
@@ -7419,6 +7492,27 @@ function escHtml(str) {
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
function _renderLinkButton(text, url) {
|
||||
var fileExts = ['.pptx','.pdf','.xlsx','.xls','.docx','.doc','.zip','.csv','.mp4','.mp3'];
|
||||
var isDownload = fileExts.some(function(ext) { return text.toLowerCase().endsWith(ext); })
|
||||
|| fileExts.some(function(ext) { return decodeURIComponent(url).toLowerCase().indexOf(ext) !== -1; });
|
||||
var isPptx = decodeURIComponent(url).toLowerCase().indexOf('.pptx') !== -1 || text.toLowerCase().endsWith('.pptx');
|
||||
if (isPptx) {
|
||||
var previewId = 'pptx-preview-' + Math.random().toString(36).substr(2, 9);
|
||||
return '<div class="pptx-file-row">'
|
||||
+ '<a href="' + url + '" onclick="downloadFile(event, this)" class="download-btn" title="Download ' + text + '">'
|
||||
+ '<span class="download-icon">⬇</span> ' + text + '</a>'
|
||||
+ ' <button class="pptx-preview-btn" onclick="togglePptxPreview(\'' + escHtml(url) + '\', \'' + previewId + '\', this)" title="Preview slides">◀ Preview</button>'
|
||||
+ '<div id="' + previewId + '" class="pptx-preview-strip" style="display:none"></div>'
|
||||
+ '</div>';
|
||||
}
|
||||
if (isDownload) {
|
||||
return '<a href="' + url + '" onclick="downloadFile(event, this)" class="download-btn" title="Download ' + text + '">'
|
||||
+ '<span class="download-icon">⬇</span> ' + text + '</a>';
|
||||
}
|
||||
return '<a href="' + url + '" target="_blank" rel="noopener">' + text + '</a>';
|
||||
}
|
||||
|
||||
function renderMarkdown(text) {
|
||||
if (!text) return '';
|
||||
|
||||
@@ -7453,24 +7547,12 @@ function renderMarkdown(text) {
|
||||
|
||||
// Links: [text](url) — render download links for file types as buttons, PPTX gets preview
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(_, text, url) {
|
||||
var fileExts = ['.pptx','.pdf','.xlsx','.xls','.docx','.doc','.zip','.csv','.mp4','.mp3'];
|
||||
var isDownload = fileExts.some(function(ext) { return text.toLowerCase().endsWith(ext); })
|
||||
|| fileExts.some(function(ext) { return decodeURIComponent(url).toLowerCase().indexOf(ext) !== -1; });
|
||||
var isPptx = decodeURIComponent(url).toLowerCase().indexOf('.pptx') !== -1 || text.toLowerCase().endsWith('.pptx');
|
||||
if (isPptx) {
|
||||
var previewId = 'pptx-preview-' + Math.random().toString(36).substr(2, 9);
|
||||
return '<div class="pptx-file-row">'
|
||||
+ '<a href="' + url + '" onclick="downloadFile(event, this)" class="download-btn" title="Download ' + text + '">'
|
||||
+ '<span class="download-icon">⬇</span> ' + text + '</a>'
|
||||
+ ' <button class="pptx-preview-btn" onclick="togglePptxPreview(\'' + escHtml(url) + '\', \'' + previewId + '\', this)" title="Preview slides">◀ Preview</button>'
|
||||
+ '<div id="' + previewId + '" class="pptx-preview-strip" style="display:none"></div>'
|
||||
+ '</div>';
|
||||
}
|
||||
if (isDownload) {
|
||||
return '<a href="' + url + '" onclick="downloadFile(event, this)" class="download-btn" title="Download ' + text + '">'
|
||||
+ '<span class="download-icon">⬇</span> ' + text + '</a>';
|
||||
}
|
||||
return '<a href="' + url + '" target="_blank" rel="noopener">' + text + '</a>';
|
||||
return _renderLinkButton(text, url);
|
||||
});
|
||||
|
||||
// Bare /api/files/... URLs — auto-convert to download buttons even without markdown syntax
|
||||
html = html.replace(/(\s|^)(\/api\/files\/[^\s<"]+)/g, function(_, space, url) {
|
||||
return space + _renderLinkButton(decodeURIComponent(url.split('/').pop() || 'download'), url);
|
||||
});
|
||||
|
||||
// Bold
|
||||
|
||||
Reference in New Issue
Block a user