Merge branch 'summary_on_message_issue580' of github.com:micz/ThunderAI into summary_on_message_issue580
This commit is contained in:
commit
29ed9e6df6
13 changed files with 207 additions and 62 deletions
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
<h2>Version 4.1.0 - ??/??/2026</h2>
|
||||
<ul>
|
||||
<li>Antispam information are now permanently saved for each message [<a href="https://github.com/micz/ThunderAI/issues/675">#675</a>].</li>
|
||||
<li>...</li>
|
||||
</ul>
|
||||
<h2>Version 4.0.3 - 20/03/2026</h2>
|
||||
|
|
|
|||
|
|
@ -235,6 +235,10 @@
|
|||
"message": "Close",
|
||||
"description": ""
|
||||
},
|
||||
"webchat_save_as_summary": {
|
||||
"message": "Save as Summary",
|
||||
"description": "Button label in the webchat window to save the AI response as a message summary"
|
||||
},
|
||||
"chatgpt_textarea_not_found_error": {
|
||||
"message": "It seems that the ChatGPT page is taking too long to load. If it finishes loading, click the button on the right. If the problem persists, please check the service status.",
|
||||
"description": ""
|
||||
|
|
@ -1388,17 +1392,13 @@
|
|||
"description": ""
|
||||
},
|
||||
"spamfilter_no_reports": {
|
||||
"message": "No messages have screened for spam yet. Here you'll find a list of the last 100 spam reports for the current session only.",
|
||||
"message": "No messages have screened for spam yet. Here you'll find a list of the last 100 spam reports.",
|
||||
"description": ""
|
||||
},
|
||||
"SpamReport_Title": {
|
||||
"message": "Spam Filter Reports",
|
||||
"description": ""
|
||||
},
|
||||
"SpamReport_infoline": {
|
||||
"message": "This information is saved only for the current session.",
|
||||
"description": ""
|
||||
},
|
||||
"Date": {
|
||||
"message": "Date",
|
||||
"description": ""
|
||||
|
|
|
|||
|
|
@ -458,6 +458,29 @@ class MessagesArea extends HTMLElement {
|
|||
selectionInfo.style.display = "block"; // show selection info
|
||||
}
|
||||
|
||||
// Save as Summary button (only shown for summary webchat sessions)
|
||||
if(promptData.prompt_info?.headerMessageId) {
|
||||
const saveSummaryButton = document.createElement('button');
|
||||
saveSummaryButton.textContent = browser.i18n.getMessage("webchat_save_as_summary");
|
||||
saveSummaryButton.classList.add('action_btn');
|
||||
saveSummaryButton.addEventListener('click', async () => {
|
||||
let finalText = removeAloneBRs(fullTextHTMLAtAssignment);
|
||||
const selectedHTML = this.getCurrentSelectionHTML();
|
||||
if(selectedHTML != "") {
|
||||
finalText = removeAloneBRs(selectedHTML);
|
||||
}
|
||||
await browser.runtime.sendMessage({
|
||||
command: "chatgpt_saveSummary",
|
||||
text: finalText,
|
||||
headerMessageId: promptData.prompt_info.headerMessageId,
|
||||
tabId: promptData.prompt_info.summaryTabId || promptData.tabId,
|
||||
});
|
||||
browser.runtime.sendMessage({command: "chatgpt_close", window_id: (await browser.windows.getCurrent()).id});
|
||||
});
|
||||
actionButtons.appendChild(saveSummaryButton);
|
||||
selectionInfo.style.display = "block";
|
||||
}
|
||||
|
||||
// diff viewer button
|
||||
if(promptData.prompt_info?.use_diff_viewer == "1") {
|
||||
const diffvButton = document.createElement('button');
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ These are generated programmatically at the bottom of `mzta-options-default.js`
|
|||
| `spamfilter_enabled_accounts` | `[]` | Accounts where spam filter is active |
|
||||
| `spamfilter_show_msg_panel` | `true` | Show info panel on spam detection |
|
||||
| `summarize` | `false` | Enable email summarization |
|
||||
| `summarize_auto` | `0` | Auto-summarize mode: `0` = disabled, `1` = manual (show "click to generate" button), `2` = automatic (generate on message open) |
|
||||
| `summarize_auto` | `1` | Auto-summarize mode: `0` = disabled, `1` = manual (show "click to generate" button), `2` = automatic (generate on message open) |
|
||||
| `summarize_display_mode` | `'inline'` | Where to display summaries: `'inline'` = message pane banner, `'webchat'` = AI chat window. Note: `summarize_auto = 2` always uses inline regardless of this setting. |
|
||||
| `summarize_max_display_length` | `0` | Maximum characters shown in inline summary before truncation. `0` = no limit (show full text). When set, text is truncated at a word boundary and a "See more"/"See less" toggle link is shown. |
|
||||
|
||||
|
|
|
|||
|
|
@ -959,8 +959,23 @@ switch (message.command) {
|
|||
|
||||
const summaryText = document.createElement('div');
|
||||
summaryText.className = 'thunderai-summary-content';
|
||||
const hasHtml = !!summaryData.summary_html;
|
||||
|
||||
// Helper to set summary content using DOMParser (innerHTML is blocked in Thunderbird content scripts)
|
||||
function setSummaryHtml(element, html) {
|
||||
element.textContent = '';
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
while (doc.body.firstChild) {
|
||||
element.appendChild(doc.body.firstChild);
|
||||
}
|
||||
element.querySelectorAll('p').forEach(p => { p.style.marginBlockStart = '0'; });
|
||||
}
|
||||
|
||||
if (summaryData.error) {
|
||||
summaryText.textContent = summaryData.message || browser.i18n.getMessage("summarize_error");
|
||||
} else if (hasHtml) {
|
||||
setSummaryHtml(summaryText, summaryData.summary_html);
|
||||
} else {
|
||||
summaryText.textContent = summaryData.summary;
|
||||
}
|
||||
|
|
@ -971,58 +986,94 @@ switch (message.command) {
|
|||
const maxLen = summaryData.maxDisplayLength || 0;
|
||||
const fullText = summaryData.summary;
|
||||
if (!summaryData.error && maxLen > 0 && fullText && fullText.length > maxLen) {
|
||||
let cutPos = fullText.lastIndexOf(' ', maxLen);
|
||||
if (cutPos <= 0) cutPos = maxLen;
|
||||
const truncated = fullText.substring(0, cutPos) + '\u2026';
|
||||
summaryText.textContent = truncated;
|
||||
|
||||
// Set up animated expand/collapse via max-height transition
|
||||
summaryText.style.overflow = 'hidden';
|
||||
summaryText.style.transition = 'max-height 0.2s ease';
|
||||
|
||||
// Measure truncated height after layout
|
||||
requestAnimationFrame(() => {
|
||||
const collapsedHeight = summaryText.scrollHeight;
|
||||
summaryText.style.maxHeight = collapsedHeight + 'px';
|
||||
});
|
||||
if (!hasHtml) {
|
||||
// Plain text: truncate by character position
|
||||
let cutPos = fullText.lastIndexOf(' ', maxLen);
|
||||
if (cutPos <= 0) cutPos = maxLen;
|
||||
const truncated = fullText.substring(0, cutPos) + '\u2026';
|
||||
summaryText.textContent = truncated;
|
||||
|
||||
const toggleLink = document.createElement('a');
|
||||
toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more";
|
||||
toggleLink.href = '#';
|
||||
toggleLink.style.cssText = 'display: inline-block; margin-top: 4px; font-size: 13px; color: ' +
|
||||
(isDarkSummary ? '#6db3f2' : '#1a5fa8') + '; cursor: pointer; text-decoration: underline;';
|
||||
|
||||
let expanded = false;
|
||||
toggleLink.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (!expanded) {
|
||||
// Expand: set full text, measure, animate to full height
|
||||
summaryText.textContent = fullText;
|
||||
const fullHeight = summaryText.scrollHeight;
|
||||
summaryText.style.maxHeight = fullHeight + 'px';
|
||||
toggleLink.textContent = browser.i18n.getMessage("summarize_see_less") || "See less";
|
||||
} else {
|
||||
// Collapse: measure current truncated height, then animate down
|
||||
summaryText.textContent = truncated;
|
||||
// Force layout to get the target height before animating
|
||||
// Measure truncated height after layout
|
||||
requestAnimationFrame(() => {
|
||||
const collapsedHeight = summaryText.scrollHeight;
|
||||
summaryText.textContent = fullText;
|
||||
// Set explicit current height so transition has a starting point
|
||||
summaryText.style.maxHeight = summaryText.scrollHeight + 'px';
|
||||
requestAnimationFrame(() => {
|
||||
summaryText.style.maxHeight = collapsedHeight + 'px';
|
||||
});
|
||||
// Swap text after transition ends
|
||||
summaryText.addEventListener('transitionend', function handler() {
|
||||
summaryText.removeEventListener('transitionend', handler);
|
||||
summaryText.textContent = truncated;
|
||||
});
|
||||
toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more";
|
||||
}
|
||||
expanded = !expanded;
|
||||
});
|
||||
summaryText.style.maxHeight = collapsedHeight + 'px';
|
||||
});
|
||||
|
||||
summaryTextWrapper.appendChild(toggleLink);
|
||||
const toggleLink = document.createElement('a');
|
||||
toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more";
|
||||
toggleLink.href = '#';
|
||||
toggleLink.style.cssText = 'display: inline-block; margin-top: 4px; font-size: 13px; color: ' +
|
||||
(isDarkSummary ? '#6db3f2' : '#1a5fa8') + '; cursor: pointer; text-decoration: underline;';
|
||||
|
||||
let expanded = false;
|
||||
toggleLink.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (!expanded) {
|
||||
// Expand: set full text, measure, animate to full height
|
||||
summaryText.textContent = fullText;
|
||||
const fullHeight = summaryText.scrollHeight;
|
||||
summaryText.style.maxHeight = fullHeight + 'px';
|
||||
toggleLink.textContent = browser.i18n.getMessage("summarize_see_less") || "See less";
|
||||
} else {
|
||||
// Collapse: measure current truncated height, then animate down
|
||||
summaryText.textContent = truncated;
|
||||
// Force layout to get the target height before animating
|
||||
const collapsedHeight = summaryText.scrollHeight;
|
||||
summaryText.textContent = fullText;
|
||||
// Set explicit current height so transition has a starting point
|
||||
summaryText.style.maxHeight = summaryText.scrollHeight + 'px';
|
||||
requestAnimationFrame(() => {
|
||||
summaryText.style.maxHeight = collapsedHeight + 'px';
|
||||
});
|
||||
// Swap text after transition ends
|
||||
summaryText.addEventListener('transitionend', function handler() {
|
||||
summaryText.removeEventListener('transitionend', handler);
|
||||
summaryText.textContent = truncated;
|
||||
});
|
||||
toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more";
|
||||
}
|
||||
expanded = !expanded;
|
||||
});
|
||||
|
||||
summaryTextWrapper.appendChild(toggleLink);
|
||||
} else {
|
||||
// HTML content: use max-height to collapse, preserve full HTML
|
||||
const collapsedMaxHeight = '4.2em'; // ~3 lines collapsed
|
||||
summaryText.style.maxHeight = collapsedMaxHeight;
|
||||
|
||||
const toggleLink = document.createElement('a');
|
||||
toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more";
|
||||
toggleLink.href = '#';
|
||||
toggleLink.style.cssText = 'display: inline-block; margin-top: 4px; font-size: 13px; color: ' +
|
||||
(isDarkSummary ? '#6db3f2' : '#1a5fa8') + '; cursor: pointer; text-decoration: underline;';
|
||||
|
||||
let expanded = false;
|
||||
toggleLink.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (!expanded) {
|
||||
summaryText.style.maxHeight = summaryText.scrollHeight + 'px';
|
||||
toggleLink.textContent = browser.i18n.getMessage("summarize_see_less") || "See less";
|
||||
} else {
|
||||
summaryText.style.maxHeight = collapsedMaxHeight;
|
||||
toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more";
|
||||
}
|
||||
expanded = !expanded;
|
||||
});
|
||||
|
||||
// Only show toggle if content is actually taller than collapsed height
|
||||
requestAnimationFrame(() => {
|
||||
if (summaryText.scrollHeight > summaryText.clientHeight) {
|
||||
summaryTextWrapper.appendChild(toggleLink);
|
||||
} else {
|
||||
summaryText.style.maxHeight = '';
|
||||
summaryText.style.overflow = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const summaryBody = document.createElement('div');
|
||||
|
|
|
|||
|
|
@ -52,6 +52,16 @@
|
|||
0: Do not use the diff viewer
|
||||
1: Use the diff viewer
|
||||
|
||||
================ DYNAMIC PROPERTIES (set at runtime via prompt_info)
|
||||
|
||||
headerMessageId (set by _openSummaryWebchat in mzta-background.js):
|
||||
When present, the webchat UI shows a "Save as Summary" button to capture
|
||||
the AI response and save it as an inline summary for the message identified
|
||||
by this headerMessageId.
|
||||
|
||||
summaryTabId (set by _openSummaryWebchat in mzta-background.js):
|
||||
The tab ID of the message display tab to update with the saved summary.
|
||||
|
||||
================ USER PROPERTIES
|
||||
Enabled (enabled attribute):
|
||||
0: Disabled
|
||||
|
|
|
|||
|
|
@ -198,6 +198,7 @@ export class taStorage {
|
|||
let now = Date.now();
|
||||
record[taStorage.FIELD_SUMMARY] = {
|
||||
summary: summary_data.summary,
|
||||
summary_html: summary_data.summary_html || '',
|
||||
error: summary_data.error || false,
|
||||
message: summary_data.message || '',
|
||||
summary_date: summary_data.summary_date instanceof Date
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ export class taSummaryStore {
|
|||
return {
|
||||
headerMessageId: data_id,
|
||||
summary: summary.summary,
|
||||
summary_html: summary.summary_html || '',
|
||||
error: summary.error || false,
|
||||
message: summary.message || '',
|
||||
summary_date: new Date(summary.summary_date || summary.ts),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="api_webchat/markdown-it.min.js"></script>
|
||||
<script src="mzta-background.js" type="module"></script>
|
||||
</head>
|
||||
|
||||
|
|
|
|||
|
|
@ -220,7 +220,9 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||
async function _initSummary() {
|
||||
try {
|
||||
let tabId = sender.tab.id;
|
||||
let prefs = await browser.storage.sync.get({ summarize_auto: prefs_default.summarize_auto, summarize_display_mode: prefs_default.summarize_display_mode, summarize_max_display_length: prefs_default.summarize_max_display_length });
|
||||
let prefs = await browser.storage.sync.get({ summarize: prefs_default.summarize, summarize_auto: prefs_default.summarize_auto, summarize_display_mode: prefs_default.summarize_display_mode, summarize_max_display_length: prefs_default.summarize_max_display_length });
|
||||
|
||||
if (!prefs.summarize) return;
|
||||
|
||||
let message = await browser.messageDisplay.getDisplayedMessage(tabId);
|
||||
if (!message) return;
|
||||
|
|
@ -282,13 +284,47 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||
async function _refreshSummary(message) {
|
||||
let tabId = sender.tab.id;
|
||||
await summaryStore.removeSummary(message.headerMessageId);
|
||||
await _generateSummaryForMessage(message.headerMessageId, tabId);
|
||||
let prefs_refresh = await browser.storage.sync.get({ summarize_display_mode: prefs_default.summarize_display_mode });
|
||||
if (prefs_refresh.summarize_display_mode === 'webchat') {
|
||||
await _openSummaryWebchat(message.headerMessageId, tabId);
|
||||
} else {
|
||||
await _generateSummaryForMessage(message.headerMessageId, tabId);
|
||||
}
|
||||
}
|
||||
_refreshSummary(message);
|
||||
break;
|
||||
case 'removeSummary':
|
||||
summaryStore.removeSummary(message.headerMessageId);
|
||||
break;
|
||||
case 'chatgpt_saveSummary':
|
||||
async function _saveSummaryFromWebchat(msg) {
|
||||
try {
|
||||
let summaryHtml = msg.text.trim();
|
||||
let cleanedSummary = cleanSummaryText(msg.text);
|
||||
const summaryData = {
|
||||
summary: cleanedSummary,
|
||||
summary_html: summaryHtml,
|
||||
summary_date: new Date(),
|
||||
headerMessageId: msg.headerMessageId
|
||||
};
|
||||
await summaryStore.saveSummary(summaryData, msg.headerMessageId);
|
||||
let prefs_summary = await browser.storage.sync.get({
|
||||
summarize_max_display_length: prefs_default.summarize_max_display_length
|
||||
});
|
||||
try {
|
||||
browser.tabs.sendMessage(msg.tabId, {
|
||||
command: "showSummary",
|
||||
data: { ...summaryData, maxDisplayLength: prefs_summary.summarize_max_display_length }
|
||||
});
|
||||
} catch (e) {
|
||||
taLog.error("Error sending showSummary to tab: " + e);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[ThunderAI] Error saving summary from webchat:", error);
|
||||
}
|
||||
}
|
||||
_saveSummaryFromWebchat(message);
|
||||
break;
|
||||
// case 'chatgpt_open':
|
||||
// openChatGPT(message.prompt,message.action,message.tabId);
|
||||
// return true;
|
||||
|
|
@ -458,6 +494,17 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||
return false;
|
||||
});
|
||||
|
||||
// Clean summary text by stripping HTML, markdown, and formatting artifacts.
|
||||
// Used by both inline summary generation and webchat summary save.
|
||||
function cleanSummaryText(text) {
|
||||
let cleaned = text.replace(/<\/?[^>]+(>|$)/g, ''); // strip HTML tags
|
||||
cleaned = cleaned.replace(/```[\s\S]*?```/g, '');
|
||||
cleaned = cleaned.replace(/[\*#_~`]/g, '');
|
||||
cleaned = cleaned.replace(/\s+/g, ' ').trim();
|
||||
cleaned = cleaned.replace(/^Summary:\s*/i, '');
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
async function _generateSummaryForMessage(headerMessageId, tabId) {
|
||||
try {
|
||||
let prefs = await browser.storage.sync.get({
|
||||
|
|
@ -514,13 +561,13 @@ async function _generateSummaryForMessage(headerMessageId, tabId) {
|
|||
|
||||
await cmd.initWorker();
|
||||
const aiResponse = await cmd.sendPrompt();
|
||||
let cleanedSummary = aiResponse.replace(/```[\s\S]*?```/g, '');
|
||||
cleanedSummary = cleanedSummary.replace(/[\*#_~`]/g, '');
|
||||
cleanedSummary = cleanedSummary.replace(/\s+/g, ' ').trim();
|
||||
cleanedSummary = cleanedSummary.replace(/^Summary:\s*/i, '');
|
||||
let cleanedSummary = cleanSummaryText(aiResponse);
|
||||
const md = window.markdownit();
|
||||
let summaryHtml = md.render(aiResponse);
|
||||
|
||||
const summaryData = {
|
||||
summary: cleanedSummary,
|
||||
summary_html: summaryHtml,
|
||||
summary_date: new Date(),
|
||||
headerMessageId: headerMessageId
|
||||
};
|
||||
|
|
@ -665,7 +712,17 @@ async function _openSummaryWebchat(headerMessageId, tabId) {
|
|||
const curr_message = messageResult.messages[0];
|
||||
const curr_message_full = await browser.messages.getFull(curr_message.id);
|
||||
|
||||
const connectionType = getConnectionType(await browser.storage.sync.get(prefs_default), {}, 'summarize');
|
||||
if (connectionType === 'chatgpt_web') {
|
||||
const errorMsg = browser.i18n.getMessage('summarize_chatgpt_web_not_supported');
|
||||
await summaryStore.saveError(headerMessageId, errorMsg);
|
||||
browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: errorMsg } });
|
||||
return;
|
||||
}
|
||||
|
||||
const { promptText, promptInfo } = await taPromptUtils.buildSummaryPrompt([{ message: curr_message, fullMessage: curr_message_full }]);
|
||||
promptInfo.headerMessageId = headerMessageId;
|
||||
promptInfo.summaryTabId = tabId;
|
||||
|
||||
openChatGPT(promptText, promptInfo.action, tabId, promptInfo.name, promptInfo.need_custom_text, promptInfo);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -137,10 +137,10 @@ export const prefs_default = {
|
|||
spamfilter: false,
|
||||
spamfilter_threshold: 70,
|
||||
spamfilter_enabled_accounts: [],
|
||||
summarize_auto: 0, // 0: disabled, 1: manual button, 2: automatic
|
||||
summarize: false,
|
||||
summarize_auto: 1, // 0: disabled, 1: manual button, 2: automatic
|
||||
summarize_display_mode: 'inline', // 'inline' or 'webchat'
|
||||
summarize_max_display_length: 0, // 0 = no limit, otherwise max chars shown inline
|
||||
spamfilter_show_msg_panel: true,
|
||||
summarize: false,
|
||||
...generated_prefs
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
<div id="miczRelNotes"><h1>ThunderAI Release Notes</h1>
|
||||
<h2>Version 4.1.0 - ??/??/2026</h2>
|
||||
<ul>
|
||||
<li>Antispam information are now permanently saved for each message [<a href="https://github.com/micz/ThunderAI/issues/675">#675</a>].</li>
|
||||
<li>...</li>
|
||||
</ul>
|
||||
<h2>Version 4.0.3 - 20/03/2026</h2>
|
||||
|
|
|
|||
|
|
@ -66,8 +66,7 @@
|
|||
<button type="button" id="accounts_deselect_all">__MSG_DeselectAll__</button>
|
||||
</div>
|
||||
<div id="spamfitler_reports_container">
|
||||
<div><span class="section_title">__MSG_SpamReport_Title__</span>
|
||||
<br><span class="infoline">__MSG_SpamReport_infoline__</span></div>
|
||||
<div><span class="section_title">__MSG_SpamReport_Title__</span></div>
|
||||
<table id="report_data">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
|
|||
Loading…
Reference in a new issue