Skip to content

Commit e1db542

Browse files
committed
Release v2.1.3: mindmap-renderer, enhanced-export, voice-input, plugin market UI
1 parent 4af1e8a commit e1db542

File tree

24 files changed

+2123
-248
lines changed

24 files changed

+2123
-248
lines changed
15.2 KB
Loading

Plugins/Plugin_market/enhanced-export/lib/html2pdf.bundle.min.js

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
/**
2+
* Enhanced Export - Export assistant messages as TXT, MD, PDF, or HTML
3+
*
4+
* Features:
5+
* - Export button next to Copy on each assistant message
6+
* - 4 format toggles in settings (TXT, MD, PDF, HTML)
7+
* - Batch download when multiple formats enabled
8+
* - TXT strips Markdown symbols for plain text
9+
* - Includes thinking block when present
10+
*
11+
* Architecture: MutationObserver watches messages container, injects Export button
12+
* into .message-actions when at least one format is enabled.
13+
*/
14+
(function(ChatRawPlugin) {
15+
'use strict';
16+
if (!ChatRawPlugin) {
17+
console.error('[EnhancedExport] ChatRawPlugin not found');
18+
return;
19+
}
20+
21+
const PLUGIN_ID = 'enhanced-export';
22+
const LIB_BASE = `/api/plugins/${PLUGIN_ID}/lib`;
23+
const EXPORT_BTN_CLASS = 'btn-export-ee';
24+
25+
let settings = { exportTxt: true, exportMd: true, exportPdf: true, exportHtml: true };
26+
let html2pdfLoaded = false;
27+
let observerInitialized = false;
28+
29+
const STABILITY_DELAY = 50;
30+
31+
const i18n = {
32+
en: {
33+
export: 'Export',
34+
exporting: 'Exporting...',
35+
exportSuccess: 'Exported',
36+
exportFailed: 'Export failed',
37+
noFormatEnabled: 'Enable at least one format in settings'
38+
},
39+
zh: {
40+
export: '导出',
41+
exporting: '导出中...',
42+
exportSuccess: '已导出',
43+
exportFailed: '导出失败',
44+
noFormatEnabled: '请在设置中开启至少一种导出格式'
45+
}
46+
};
47+
48+
function t(key) {
49+
const lang = ChatRawPlugin?.utils?.getLanguage?.() || 'en';
50+
return i18n[lang]?.[key] || i18n.en[key] || key;
51+
}
52+
53+
function stripMarkdown(md) {
54+
if (typeof md !== 'string') return '';
55+
return md
56+
.replace(/```[\s\S]*?```/g, '') // code blocks
57+
.replace(/`[^`]+`/g, (m) => m.slice(1, -1))
58+
.replace(/\*\*([^*]+)\*\*/g, '$1')
59+
.replace(/\*([^*]+)\*/g, '$1')
60+
.replace(/__([^_]+)__/g, '$1')
61+
.replace(/_([^_]+)_/g, '$1')
62+
.replace(/^#{1,6}\s+/gm, '')
63+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
64+
.replace(/^[-*+]\s+/gm, '')
65+
.replace(/^\d+\.\s+/gm, '')
66+
.replace(/^>\s+/gm, '')
67+
.replace(/^---+$/gm, '')
68+
.replace(/^\*\*\*+$/gm, '')
69+
.replace(/^___+$/gm, '')
70+
.replace(/\n{3,}/g, '\n\n')
71+
.trim();
72+
}
73+
74+
function buildContent(msg) {
75+
const parts = [];
76+
if (msg.thinking && msg.thinking.trim()) {
77+
parts.push(msg.thinking.trim());
78+
}
79+
if (msg.content && msg.content.trim()) {
80+
parts.push(msg.content.trim());
81+
}
82+
return parts.join('\n\n');
83+
}
84+
85+
function buildMdContent(msg) {
86+
const parts = [];
87+
if (msg.thinking && msg.thinking.trim()) {
88+
parts.push('## ' + t('thinking') + '\n\n' + msg.thinking.trim());
89+
}
90+
if (msg.content && msg.content.trim()) {
91+
parts.push('## ' + t('reply') + '\n\n' + msg.content.trim());
92+
}
93+
return parts.join('\n\n');
94+
}
95+
96+
const thinkingI18n = { en: { thinking: 'Thinking Process', reply: 'Reply' }, zh: { thinking: '思考过程', reply: '回复' } };
97+
Object.assign(i18n.en, thinkingI18n.en);
98+
Object.assign(i18n.zh, thinkingI18n.zh);
99+
100+
function downloadBlob(blob, filename) {
101+
const a = document.createElement('a');
102+
a.href = URL.createObjectURL(blob);
103+
a.download = filename;
104+
a.click();
105+
URL.revokeObjectURL(a.href);
106+
}
107+
108+
function getMsgForActions(actionsEl) {
109+
var messages = ChatRawPlugin?.utils?.getMessages?.() || [];
110+
if (!messages.length) return null;
111+
var slot = actionsEl.querySelector('.message-actions-plugin-slot') || actionsEl.closest('.message-actions')?.querySelector('.message-actions-plugin-slot');
112+
if (slot && slot.getAttribute('data-msg-index') != null) {
113+
var idx = parseInt(slot.getAttribute('data-msg-index'), 10);
114+
if (!isNaN(idx) && idx >= 0 && idx < messages.length) return messages[idx];
115+
}
116+
var messageEl = actionsEl.closest('.message');
117+
if (!messageEl || !messageEl.classList.contains('assistant')) return null;
118+
var container = messageEl.closest('.messages');
119+
if (!container) return null;
120+
var assistantMsgs = messages.filter(function (m) { return m.role === 'assistant' && m.content; });
121+
if (!assistantMsgs.length) return null;
122+
var assistantElements = Array.from(container.querySelectorAll('.message.assistant')).filter(function (el) {
123+
return el.querySelector('.message-actions');
124+
});
125+
var idx = assistantElements.indexOf(messageEl);
126+
if (idx < 0 || idx >= assistantMsgs.length) return null;
127+
return assistantMsgs[idx];
128+
}
129+
130+
async function loadHtml2pdf() {
131+
if (html2pdfLoaded || typeof window.html2pdf !== 'undefined') {
132+
html2pdfLoaded = true;
133+
return true;
134+
}
135+
try {
136+
await new Promise((resolve, reject) => {
137+
const s = document.createElement('script');
138+
s.src = `${LIB_BASE}/html2pdf.bundle.min.js`;
139+
s.onload = resolve;
140+
s.onerror = () => reject(new Error('Failed to load html2pdf'));
141+
document.head.appendChild(s);
142+
});
143+
html2pdfLoaded = true;
144+
return true;
145+
} catch (e) {
146+
console.error('[EnhancedExport] Failed to load html2pdf:', e);
147+
return false;
148+
}
149+
}
150+
151+
function getHtmlTemplate(bodyContent) {
152+
return `<!DOCTYPE html>
153+
<html lang="zh-CN">
154+
<head>
155+
<meta charset="UTF-8">
156+
<meta name="viewport" content="width=device-width,initial-scale=1">
157+
<title>Export</title>
158+
<style>
159+
body{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;line-height:1.6;color:#333;max-width:720px;margin:0 auto;padding:24px}
160+
h1,h2,h3{font-weight:600;margin-top:1.5em}
161+
pre,code{background:#f5f5f5;border-radius:4px;padding:2px 6px;font-family:ui-monospace,monospace}
162+
pre{overflow-x:auto;padding:12px}
163+
blockquote{border-left:4px solid #ddd;margin:0;padding-left:16px;color:#666}
164+
</style>
165+
</head>
166+
<body>
167+
<article>${bodyContent}</article>
168+
</body>
169+
</html>`;
170+
}
171+
172+
async function doExport(msg, formats, btn) {
173+
if (!msg || !msg.content) return;
174+
const hasAny = formats.exportTxt || formats.exportMd || formats.exportPdf || formats.exportHtml;
175+
if (!hasAny) {
176+
ChatRawPlugin?.utils?.showToast?.(t('noFormatEnabled'), 'info');
177+
return;
178+
}
179+
const fullContent = buildContent(msg);
180+
if (!fullContent.trim()) return;
181+
182+
const originalTitle = btn.getAttribute('title');
183+
btn.setAttribute('title', t('exporting'));
184+
btn.disabled = true;
185+
if (btn.querySelector('i')) btn.querySelector('i').className = 'ri-loader-4-line';
186+
187+
try {
188+
const base = 'export';
189+
const delay = 200;
190+
191+
if (formats.exportTxt) {
192+
const plain = stripMarkdown(fullContent);
193+
downloadBlob(new Blob([plain], { type: 'text/plain;charset=utf-8' }), `${base}.txt`);
194+
await new Promise(r => setTimeout(r, delay));
195+
}
196+
if (formats.exportMd) {
197+
const md = buildMdContent(msg);
198+
downloadBlob(new Blob([md], { type: 'text/markdown;charset=utf-8' }), `${base}.md`);
199+
await new Promise(r => setTimeout(r, delay));
200+
}
201+
if (formats.exportHtml) {
202+
const marked = window.marked;
203+
const htmlContent = marked ? marked.parse(fullContent) : fullContent.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>');
204+
const doc = getHtmlTemplate(htmlContent);
205+
downloadBlob(new Blob([doc], { type: 'text/html;charset=utf-8' }), `${base}.html`);
206+
await new Promise(r => setTimeout(r, delay));
207+
}
208+
if (formats.exportPdf) {
209+
const ok = await loadHtml2pdf();
210+
if (ok && window.html2pdf) {
211+
const marked = window.marked;
212+
const htmlContent = marked ? marked.parse(fullContent) : fullContent.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>');
213+
const div = document.createElement('div');
214+
div.style.cssText = 'font-family:system-ui,sans-serif;line-height:1.6;color:#333;padding:20px;font-size:14px;max-width:600px';
215+
div.innerHTML = htmlContent;
216+
document.body.appendChild(div);
217+
await window.html2pdf().set({
218+
margin: 10,
219+
filename: `${base}.pdf`,
220+
image: { type: 'jpeg', quality: 0.98 },
221+
html2canvas: { scale: 2 },
222+
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
223+
}).from(div).save();
224+
document.body.removeChild(div);
225+
} else {
226+
ChatRawPlugin?.utils?.showToast?.(t('exportFailed') + ' (PDF)', 'error');
227+
}
228+
}
229+
230+
ChatRawPlugin?.utils?.showToast?.(t('exportSuccess'), 'success');
231+
} catch (e) {
232+
console.error('[EnhancedExport] Export error:', e);
233+
ChatRawPlugin?.utils?.showToast?.(t('exportFailed'), 'error');
234+
} finally {
235+
btn.disabled = false;
236+
btn.setAttribute('title', originalTitle || t('export'));
237+
if (btn.querySelector('i')) btn.querySelector('i').className = 'ri-download-2-line';
238+
}
239+
}
240+
241+
function getContentFromDom(messageEl) {
242+
var mc = messageEl.querySelector('.message-content');
243+
if (!mc) return null;
244+
var divs = Array.prototype.filter.call(mc.children, function (c) { return c.tagName === 'DIV'; });
245+
for (var i = 0; i < divs.length; i++) {
246+
var d = divs[i];
247+
if (!d.classList.contains('thinking-block') && !d.classList.contains('message-actions') && !d.classList.contains('rag-references') && !d.classList.contains('typing-indicator') && (d.innerHTML || d.innerText)) {
248+
return { content: d.innerText || d.textContent || '' };
249+
}
250+
}
251+
return null;
252+
}
253+
254+
function injectExportButton(actionsEl) {
255+
var container = actionsEl.querySelector('.message-actions-plugin-slot') || actionsEl;
256+
if (container.querySelector && container.querySelector('.' + EXPORT_BTN_CLASS)) return;
257+
258+
var hasAny = settings.exportTxt || settings.exportMd || settings.exportPdf || settings.exportHtml;
259+
if (!hasAny) return;
260+
261+
var messageEl = actionsEl.closest('.message');
262+
if (!messageEl || !messageEl.classList.contains('assistant')) return;
263+
264+
if (!actionsEl.querySelector('.btn-copy')) return;
265+
266+
var btn = document.createElement('button');
267+
btn.className = 'btn-copy ' + EXPORT_BTN_CLASS;
268+
btn.setAttribute('title', t('export'));
269+
btn.innerHTML = '<i class="ri-download-2-line"></i>';
270+
btn.addEventListener('click', async function (e) {
271+
e.preventDefault();
272+
e.stopPropagation();
273+
var m = getMsgForActions(actionsEl);
274+
if (!m) {
275+
var domContent = getContentFromDom(messageEl);
276+
if (domContent && domContent.content) {
277+
m = { role: 'assistant', content: domContent.content };
278+
}
279+
}
280+
if (m) {
281+
await doExport(m, settings, btn);
282+
} else {
283+
ChatRawPlugin?.utils?.showToast?.(t('exportFailed'), 'error');
284+
}
285+
});
286+
container.appendChild(btn);
287+
}
288+
289+
function processActions(actionsEl) {
290+
if (!actionsEl || !document.body.contains(actionsEl)) return;
291+
var msg = actionsEl.closest('.message');
292+
if (!msg || !msg.classList.contains('assistant')) return;
293+
if (!actionsEl.querySelector('.btn-copy')) return;
294+
if (actionsEl.querySelector('.' + EXPORT_BTN_CLASS)) return;
295+
injectExportButton(actionsEl);
296+
}
297+
298+
function scheduleProcessing(el) {
299+
var actionsEl = el;
300+
if (!el.classList || !el.classList.contains('message-actions')) {
301+
actionsEl = el.querySelector ? el.querySelector('.message-actions') : null;
302+
}
303+
if (!actionsEl || !document.body.contains(actionsEl)) return;
304+
processActions(actionsEl);
305+
}
306+
307+
function processAllMessageActions() {
308+
var els = document.querySelectorAll('.messages .message-actions');
309+
els.forEach(function(el) { scheduleProcessing(el); });
310+
}
311+
312+
function initObserver() {
313+
if (observerInitialized) return;
314+
315+
var container = document.querySelector('.messages') || document.body;
316+
var observer = new MutationObserver(function() {
317+
processAllMessageActions();
318+
});
319+
observer.observe(container, { childList: true, subtree: true });
320+
321+
processAllMessageActions();
322+
[50, 200, 500, 1000, 2000, 4000].forEach(function(ms) {
323+
setTimeout(processAllMessageActions, ms);
324+
});
325+
setInterval(processAllMessageActions, 1500);
326+
observerInitialized = true;
327+
}
328+
329+
async function loadSettings() {
330+
try {
331+
const res = await fetch('/api/plugins');
332+
if (res.ok) {
333+
const plugins = await res.json();
334+
const p = plugins.find(x => x.id === PLUGIN_ID);
335+
if (p?.settings_values) {
336+
settings = { ...settings, ...p.settings_values };
337+
}
338+
}
339+
} catch (e) {
340+
console.error('[EnhancedExport] Failed to load settings:', e);
341+
}
342+
}
343+
344+
function onAfterReceive() {
345+
if (observerInitialized) {
346+
setTimeout(processAllMessageActions, 100);
347+
setTimeout(processAllMessageActions, 500);
348+
}
349+
return { success: false };
350+
}
351+
352+
async function init() {
353+
try {
354+
await loadSettings();
355+
initObserver();
356+
if (ChatRawPlugin?.hooks?.register) {
357+
ChatRawPlugin.hooks.register('after_receive', { handler: onAfterReceive });
358+
}
359+
} catch (e) {
360+
console.error('[EnhancedExport] Init error:', e);
361+
}
362+
}
363+
364+
if (document.readyState === 'loading') {
365+
document.addEventListener('DOMContentLoaded', init);
366+
} else {
367+
init();
368+
}
369+
})(window.ChatRawPlugin);

0 commit comments

Comments
 (0)