Merge branch 'summary_on_message_issue580' into main
This commit is contained in:
commit
65bf9c440c
53 changed files with 2130 additions and 236 deletions
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -8,7 +8,7 @@ body:
|
|||
|
||||
If you have a feature or enhancement request, please use the [feature request][fr] form.
|
||||
|
||||
[fr]: https://github.com/micz/ThunderAI/issues/new?assignees=&labels=&projects=&template=feature_request.md&title=
|
||||
[fr]: https://github.com/micz/ThunderAI/issues/new?assignees=&labels=&projects=&template=feature_request.yml&title=
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
|
|
|
|||
80
.github/scripts/tom-select-update.js
vendored
Normal file
80
.github/scripts/tom-select-update.js
vendored
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
const path = require('path');
|
||||
|
||||
// --- Utility: fetch URL following redirects ---
|
||||
function fetchUrl(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
https.get(url, { headers: { 'User-Agent': 'ThunderAI-vendor-updater' } }, (res) => {
|
||||
if (res.statusCode === 301 || res.statusCode === 302) {
|
||||
return fetchUrl(res.headers.location).then(resolve).catch(reject);
|
||||
}
|
||||
const chunks = [];
|
||||
res.on('data', chunk => chunks.push(chunk));
|
||||
res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks) }));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Read VENDORS.md ---
|
||||
const vendorsPath = 'VENDORS.md';
|
||||
const content = fs.readFileSync(vendorsPath, 'utf8');
|
||||
|
||||
// --- Find all entries: file + source ---
|
||||
const entryRegex = /file:\s*(.+?)\r?\nsource:\s*(https?:\/\/.+)/g;
|
||||
const entries = [];
|
||||
let match;
|
||||
while ((match = entryRegex.exec(content)) !== null) {
|
||||
entries.push({
|
||||
file: match[1].trim(),
|
||||
source: match[2].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
// --- Filter tom-select entries only ---
|
||||
const filteredEntries = entries.filter(e => e.file.toLowerCase().includes('tom-select'));
|
||||
|
||||
if (filteredEntries.length === 0) {
|
||||
console.log('No entries found in VENDORS.md');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// --- Main ---
|
||||
(async () => {
|
||||
let hasErrors = false;
|
||||
|
||||
for (const entry of filteredEntries) {
|
||||
const filePath = entry.file.replace(/\\/g, '/');
|
||||
console.log(`\nProcessing: ${filePath}`);
|
||||
console.log(` Source: ${entry.source}`);
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
console.log(` → Directory created: ${dir}`);
|
||||
}
|
||||
|
||||
// Download file
|
||||
try {
|
||||
const res = await fetchUrl(entry.source);
|
||||
if (res.status === 200) {
|
||||
fs.writeFileSync(filePath, res.body);
|
||||
console.log(` ✓ File updated (${res.body.length} bytes)`);
|
||||
} else {
|
||||
console.error(` ✗ Download failed: HTTP ${res.status}`);
|
||||
hasErrors = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(` ✗ Error: ${err.message}`);
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasErrors) {
|
||||
console.error('\nSome files were not updated.');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('\nAll files updated successfully.');
|
||||
}
|
||||
})();
|
||||
|
|
@ -55,7 +55,7 @@ jobs:
|
|||
|
||||
https://cdn.jsdelivr.net/npm/tom-select@${latestTag}/dist/css/tom-select.default.min.css
|
||||
|
||||
Remember also to update the VENDORS.md file.`
|
||||
Change the VENDORS.md file to trigger the library update.`
|
||||
});
|
||||
core.info(`New issue created for version ${latestTag}.`);
|
||||
}
|
||||
|
|
|
|||
81
.github/workflows/tom-select-update.yml
vendored
Normal file
81
.github/workflows/tom-select-update.yml
vendored
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
name: Update Tom Select Files
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'VENDORS.md'
|
||||
|
||||
jobs:
|
||||
tom-select-update:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Check and update vendor files
|
||||
run: node .github/scripts/tom-select-update.js
|
||||
|
||||
- name: Commit and push to PR branch
|
||||
id: commit
|
||||
run: |
|
||||
BASE_BRANCH="${{ github.ref_name }}"
|
||||
PR_BRANCH="tom-select-update-${BASE_BRANCH}"
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# If there are no changes, exit without doing anything
|
||||
if git diff --quiet; then
|
||||
echo "No changes to vendor files."
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Crea o aggiorna la branch PR
|
||||
git checkout -B "$PR_BRANCH"
|
||||
git add -A
|
||||
git commit -m "Update Tom Select files from VENDORS.md"
|
||||
git push origin "$PR_BRANCH" --force
|
||||
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
echo "pr_branch=$PR_BRANCH" >> $GITHUB_OUTPUT
|
||||
echo "base_branch=$BASE_BRANCH" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Pull Request via API
|
||||
if: steps.commit.outputs.has_changes == 'true'
|
||||
run: |
|
||||
# Try to create the PR; if it already exists (422) ignore the error
|
||||
HTTP_CODE=$(curl -s -o /tmp/pr_response.json -w "%{http_code}" \
|
||||
-X POST \
|
||||
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/${{ github.repository }}/pulls \
|
||||
-d "{
|
||||
\"title\": \"Update Tom Select files from VENDORS.md\",
|
||||
\"body\": \"Tom Select files updated automatically after a change to \`VENDORS.md\`.\n\nPlease review the downloaded files before merging.\",
|
||||
\"head\": \"${{ steps.commit.outputs.pr_branch }}\",
|
||||
\"base\": \"${{ steps.commit.outputs.base_branch }}\"
|
||||
}")
|
||||
|
||||
cat /tmp/pr_response.json
|
||||
|
||||
if [ "$HTTP_CODE" = "201" ]; then
|
||||
echo "PR created successfully."
|
||||
elif [ "$HTTP_CODE" = "422" ]; then
|
||||
echo "PR already exists, updated with the new commit."
|
||||
else
|
||||
echo "Error creating PR: HTTP $HTTP_CODE"
|
||||
exit 1
|
||||
fi
|
||||
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode
|
||||
.vscode/*
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
dist
|
||||
|
||||
.claude
|
||||
24
CHANGELOG.md
24
CHANGELOG.md
|
|
@ -3,8 +3,27 @@
|
|||
|
||||
|
||||
|
||||
|
||||
<h2>Version 4.0.0 - ??/??/2026</h2>
|
||||
<h2>Version 4.1.0 - ??/??/2026</h2>
|
||||
<ul>
|
||||
<li>...</li>
|
||||
</ul>
|
||||
<h2>Version 4.0.3 - 20/03/2026</h2>
|
||||
<ul>
|
||||
<li>Fixed a bug in creating new tags [<a href="https://github.com/micz/ThunderAI/issues/698">#698</a>].</li>
|
||||
</ul>
|
||||
<h2>Version 4.0.2 - 11/03/2026</h2>
|
||||
<ul>
|
||||
<li>Now it's possible to automatically save the AI window position [<a href="https://github.com/micz/ThunderAI/issues/685">#685</a>].</li>
|
||||
<li><i>[OpenAI API]</i> Fix: Correctly showing failed response errors during streaming [<a href="https://github.com/micz/ThunderAI/issues/690">#690</a>].</li>
|
||||
<li>Fix: Correctly adding tags with non-ASCII characters [<a href="https://github.com/micz/ThunderAI/issues/689">#689</a>].</li>
|
||||
<li>Improved the spacing between lines when displaying the AI response in the API webchat [<a href="https://github.com/micz/ThunderAI/issues/686">#686</a>].</li>
|
||||
<li>Some minor improvments.</li>
|
||||
</ul>
|
||||
<h2>Version 4.0.1 - 27/02/2026</h2>
|
||||
<ul>
|
||||
<li>Fix: Correctly handling additional text without a placeholder [<a href="https://github.com/micz/ThunderAI/issues/681">#681</a>].</li>
|
||||
</ul>
|
||||
<h2>Version 4.0.0 - 24/02/2026</h2>
|
||||
<ul>
|
||||
<li>ThunderAI is now compatible only with Thunderbird 140 and later [<a href="https://github.com/micz/ThunderAI/issues/616">#616</a>].</li>
|
||||
<li><i>[All APIs]</i> It's now possibile to define a specific API integration for calendar and task recognition [<a href="https://github.com/micz/ThunderAI/issues/498">#498</a>].</li>
|
||||
|
|
@ -24,7 +43,6 @@
|
|||
<li>Fix: Now it's possibile to use multiple <i>additional_text</i> placeholders in a single prompt, also using custom placeholders [<a href="https://github.com/micz/ThunderAI/issues/554">#554</a>].</li>
|
||||
<li>When using the <i>additional_text</i> placeholder is now possibile to specify an ID that will be shown in the form asking for the text [<a href="https://github.com/micz/ThunderAI/issues/525">#525</a>].</li>
|
||||
<li><i>[ChatGPT Web]</i> Added an option to define a custom time to wait for the page load. Sometimes, on slow PCs, the ChatGPT page loads slowly and ThunderAI inject its content too early. With this option you can adjust the waiting time [<a href="https://github.com/micz/ThunderAI/issues/634">#634</a>].</li>
|
||||
<li>...</li>
|
||||
</ul>
|
||||
<h2>Version 3.8.5 - 22/02/2026</h2>
|
||||
<ul>
|
||||
|
|
|
|||
90
CLAUDE.md
90
CLAUDE.md
|
|
@ -1,39 +1,65 @@
|
|||
# ThunderAI Project Agent Context
|
||||
# ThunderAI - Claude Code Guide
|
||||
|
||||
## Project Overview
|
||||
ThunderAI is a Mozilla Thunderbird add-on that integrates Large Language Models (LLMs) such as ChatGPT, Gemini, Claude, and Ollama for advanced email management. It allows users to analyze, draft, correct, tag, and create calendar events directly from the email client.
|
||||
ThunderAI is a **Thunderbird WebExtension (Manifest V2)** that integrates multiple AI providers (ChatGPT Web, OpenAI API, Google Gemini, Claude/Anthropic, Ollama, and OpenAI-compatible APIs) directly into the Thunderbird email client.
|
||||
|
||||
## Technical Stack
|
||||
- **Languages:** JavaScript (ES6+), HTML5, CSS3.
|
||||
- **Environment:** Thunderbird MailExtension API (based on WebExtensions).
|
||||
- **Core Logic:** Primarily located in `background.js` and UI-specific scripts.
|
||||
- **API Integrations:** Specific logic is contained within dedicated API provider files (e.g., `api_openai.js`, `api_google_gemini.js`).
|
||||
- **Extension ID:** `thunderai@micz.it`
|
||||
- **Min Thunderbird:** 140.0+
|
||||
- **Language:** Plain ES6+ JavaScript modules — no build tools, no transpilation, no npm
|
||||
- **License:** GPLv3
|
||||
|
||||
## Development Rules & Style Guide
|
||||
- **Code Style:** Use modern JavaScript. Prefer `async/await` over chained Promises.
|
||||
- **Naming Conventions:** Use `camelCase` for variables and functions.
|
||||
- **Security:** - Never hardcode API keys in the source code.
|
||||
- Use `messenger.storage.local` for data persistence.
|
||||
- Strictly adhere to CORS policies for external API calls.
|
||||
- **i18n:**
|
||||
- The project supports multiple languages via `_locales/`. When adding UI strings, always use `messenger.i18n.getMessage()`.
|
||||
- When you need to modify language file, modify only the english version.
|
||||
- **Logging:**
|
||||
- When possibile use the taLog object to log errors, otherwise use the console.error(<msg>) method.
|
||||
- If it's useful to log add debug logs using taLog.log(<msg>) method.
|
||||
## Key Rules
|
||||
|
||||
## Files & Directories Structure
|
||||
- `manifest.json`: Extension entry point and permission definitions.
|
||||
- `api_*.js`: Modules for integration with different AI providers.
|
||||
- `options/`: Configuration and settings pages.
|
||||
- `_locales/`: Translation files (JSON format).
|
||||
- `graphics/`: Visual assets and icons.
|
||||
1. **Localization:** Modify ONLY `_locales/en/messages.json`. All other locale files are managed via Weblate — never touch them.
|
||||
2. **No build system:** There is no bundler, compiler, or package manager. All JS files are plain ES6 modules loaded directly by the browser engine.
|
||||
3. **Module imports:** Use relative paths with `.js` extension (e.g., `import { foo } from '../js/mzta-utils.js'`).
|
||||
4. **Placeholder format:** Placeholders in prompt text use the `{%placeholder_id%}` syntax (e.g., `{%mail_text_body_or_selected%}`).
|
||||
5. **No test suite:** There is no automated test framework. Testing is done manually in Thunderbird.
|
||||
6. **Settings defaults:** All new preferences must be added to `options/mzta-options-default.js` in `prefs_default`.
|
||||
7. **Keep spec files up to date:** When making code changes that affect a subsystem described in claude-spec/, update the relevant spec file to reflect the new behavior. Read the spec before modifying, update it after.
|
||||
|
||||
## Important Commands & Workflow
|
||||
- **Testing:** Since this is a Thunderbird add-on, testing is performed by loading the extension as a "Temporary Add-on" via Thunderbird's `Debug Add-ons` menu.
|
||||
- **Build:** The project does not use complex build tools (no Webpack/Vite by default); it is a "pure" MailExtension.
|
||||
## Directory Map
|
||||
|
||||
## Agent Instructions
|
||||
1. **Context Awareness:** When modifying API-related files, ensure compatibility with the existing placeholder system (e.g., `{%mail_body%}`, `{%additional_text%}`).
|
||||
2. **Permission Review:** Before modifying `manifest.json`, verify that any new permissions requested are strictly necessary for the feature.
|
||||
3. **Compatibility:** Ensure code is compatible with the latest Thunderbird ESR (Extended Support Release) versions.
|
||||
```
|
||||
/
|
||||
├── mzta-background.js # Background script (main entry point)
|
||||
├── mzta-background.html # Loads the background script
|
||||
├── manifest.json # Extension manifest
|
||||
├── js/ # Core modules
|
||||
│ ├── api/ # AI API integration modules
|
||||
│ ├── workers/ # Web Workers (one per API provider)
|
||||
│ ├── lib/ # Third-party libraries (diff.js)
|
||||
│ └── mzta-*.js # Core utilities, menus, prompts, placeholders
|
||||
├── options/ # Settings UI
|
||||
│ ├── mzta-options.html/.js/.css
|
||||
│ ├── mzta-options-default.js # ALL default preference values
|
||||
│ └── mzta-release-notes.html
|
||||
├── pages/ # Feature-specific settings pages
|
||||
│ ├── addtags/
|
||||
│ ├── customprompts/
|
||||
│ ├── customdataplaceholders/
|
||||
│ ├── get-calendar-event/
|
||||
│ ├── get-task/
|
||||
│ ├── spamfilter/
|
||||
│ ├── summarize/
|
||||
│ └── onboarding/
|
||||
├── popup/ # Popup menu (shown on toolbar click)
|
||||
│ └── mzta-popup.html/.js/.css
|
||||
├── _locales/ # Localization
|
||||
│ ├── en/messages.json # ← ONLY THIS FILE is edited directly
|
||||
│ └── [15 other languages managed by Weblate]
|
||||
├── images/ # Icons and graphical assets
|
||||
└── api_webchat/ # Web chat API interface
|
||||
```
|
||||
|
||||
## Spec Files
|
||||
|
||||
For detailed documentation see [`claude-spec/`](claude-spec/):
|
||||
|
||||
- [01-architecture.md](claude-spec/01-architecture.md) — Module structure and data flow
|
||||
- [02-prompts.md](claude-spec/02-prompts.md) — Prompt system (types, actions, properties)
|
||||
- [03-placeholders.md](claude-spec/03-placeholders.md) — Placeholder system
|
||||
- [04-api-integrations.md](claude-spec/04-api-integrations.md) — AI provider integrations
|
||||
- [05-options.md](claude-spec/05-options.md) — Settings and preferences system
|
||||
- [06-localization.md](claude-spec/06-localization.md) — i18n rules and workflow
|
||||
- [99-thunderbird-team-spec.md](claude-spec/99-thunderbird-team-spec.md) — Thunderbird WebExtensions development guidelines (API usage, experiments, review requirements)
|
||||
|
|
|
|||
28
README.md
28
README.md
|
|
@ -88,20 +88,20 @@ Are you using this addon in your Thunderbird?
|
|||
## Attributions
|
||||
|
||||
### Translations
|
||||
- Chinese (Simplified) (zh_Hans): [jeklau](https://github.com/jeklau) <img src="https://micz.it/weblate/thunderai/zh_Hans.svg">
|
||||
- Chinese (Traditional) (zh_Hant): [evez](https://github.com/evez) <img src="https://micz.it/weblate/thunderai/zh_Hant.svg">
|
||||
- Croatian (hr): Petar Jedvaj <img src="https://micz.it/weblate/thunderai/hr.svg">
|
||||
- Czech (cs): [Fjuro](https://hosted.weblate.org/user/Fjuro/), [Jaroslav Staněk](https://hosted.weblate.org/user/jaroush/) <img src="https://micz.it/weblate/thunderai/cs.svg">
|
||||
- French (fr): Generated automatically, [Noam](https://github.com/noam-sc) <img src="https://micz.it/weblate/thunderai/fr.svg">
|
||||
- German (de): Generated automatically <img src="https://micz.it/weblate/thunderai/de.svg">
|
||||
- Greek (el): [ChristosK.](https://github.com/christoskaterini) <img src="https://micz.it/weblate/thunderai/el.svg">
|
||||
- Italian (it): [Mic](https://github.com/micz) <img src="https://micz.it/weblate/thunderai/it.svg">
|
||||
- Japanese (ja): [Taichi Ito](https://github.com/watya1) <img src="https://micz.it/weblate/thunderai/ja.svg">
|
||||
- Polski (pl): [neexpl](https://github.com/neexpl), [makkacprzak](https://github.com/makkacprzak) <img src="https://micz.it/weblate/thunderai/pl.svg">
|
||||
- Português Brasileiro (pt-br): Bruno Pereira de Souza <img src="https://micz.it/weblate/thunderai/pt-br.svg">
|
||||
- Russian (ru): [Maksim](https://hosted.weblate.org/user/law820314/) <img src="https://micz.it/weblate/thunderai/ru.svg">
|
||||
- Spanish (es): [Gerardo Sobarzo](https://hosted.weblate.org/user/gerardo.sobarzo/), [Andrés Rendón Hernández](https://hosted.weblate.org/user/arendon/), [Erick Limon](https://hosted.weblate.org/user/ErickLimonG/) <img src="https://micz.it/weblate/thunderai/es.svg">
|
||||
- Swedish (sv): [Andreas Pettersson](https://hosted.weblate.org/user/Andy_tb/) , [Luna Jernberg](https://hosted.weblate.org/user/bittin1ddc447d824349b2/) <img src="https://micz.it/weblate/thunderai/sv.svg">
|
||||
- Brazilian Portuguese - Português Brasileiro (pt-br): Bruno Pereira de Souza <img src="https://micz.it/weblate/thunderai/pt-br.svg">
|
||||
- Chinese (Simplified) - Jiǎntǐ Zhōngwén (简体中文) (zh_Hans): [jeklau](https://github.com/jeklau), [Min9X1n](https://github.com/Min9X1n) <img src="https://micz.it/weblate/thunderai/zh_Hans.svg">
|
||||
- Chinese (Traditional) - Fántǐ Zhōngwén (繁體中文) (zh_Hant): [evez](https://github.com/evez) <img src="https://micz.it/weblate/thunderai/zh_Hant.svg">
|
||||
- Croatian - Hrvatski (hr): Petar Jedvaj <img src="https://micz.it/weblate/thunderai/hr.svg">
|
||||
- Czech - Čeština (cs): [Fjuro](https://hosted.weblate.org/user/Fjuro/), [Jaroslav Staněk](https://hosted.weblate.org/user/jaroush/) <img src="https://micz.it/weblate/thunderai/cs.svg">
|
||||
- French - Français (fr): Generated automatically, [Noam](https://github.com/noam-sc) <img src="https://micz.it/weblate/thunderai/fr.svg">
|
||||
- German - Deutsch (de): Generated automatically <img src="https://micz.it/weblate/thunderai/de.svg">
|
||||
- Greek - Elliniká (Ελληνικά) (el): [ChristosK.](https://github.com/christoskaterini) <img src="https://micz.it/weblate/thunderai/el.svg">
|
||||
- Italian - Italiano (it): [Mic](https://github.com/micz) <img src="https://micz.it/weblate/thunderai/it.svg">
|
||||
- Japanese - Nihongo (日本語) (ja): [Taichi Ito](https://github.com/watya1) <img src="https://micz.it/weblate/thunderai/ja.svg">
|
||||
- Polish - Polski (pl): [neexpl](https://github.com/neexpl), [makkacprzak](https://github.com/makkacprzak) <img src="https://micz.it/weblate/thunderai/pl.svg">
|
||||
- Russian - Russkiy (русский) (ru): [Maksim](https://hosted.weblate.org/user/law820314/) <img src="https://micz.it/weblate/thunderai/ru.svg">
|
||||
- Spanish - Español (es): [Gerardo Sobarzo](https://hosted.weblate.org/user/gerardo.sobarzo/), [Andrés Rendón Hernández](https://hosted.weblate.org/user/arendon/), [Erick Limon](https://hosted.weblate.org/user/ErickLimonG/) <img src="https://micz.it/weblate/thunderai/es.svg">
|
||||
- Swedish - Svenska (sv): [Andreas Pettersson](https://hosted.weblate.org/user/Andy_tb/) , [Luna Jernberg](https://hosted.weblate.org/user/bittin1ddc447d824349b2/) <img src="https://micz.it/weblate/thunderai/sv.svg">
|
||||
<br>
|
||||
|
||||
Do you want to help translate this addon? [Find out how!](https://micz.it/thunderbird-addon-thunderai/translate/) <br>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ file: pages\_lib\list.js
|
|||
source: https://raw.githubusercontent.com/javve/list.js/v2.3.1/dist/list.js
|
||||
|
||||
file: pages\_lib\tom-select.base.js
|
||||
source: https://cdn.jsdelivr.net/npm/tom-select@2.5.1/dist/js/tom-select.base.js
|
||||
source: https://cdn.jsdelivr.net/npm/tom-select@2.5.2/dist/js/tom-select.base.js
|
||||
|
||||
file: pages\_lib\tom-select.default.min.css
|
||||
source: https://cdn.jsdelivr.net/npm/tom-select@v2.5.2/dist/css/tom-select.default.min.css
|
||||
|
||||
file: js\lib\diff.js
|
||||
source: https://cdnjs.cloudflare.com/ajax/libs/jsdiff/7.0.0/diff.js
|
||||
|
|
@ -1214,7 +1214,7 @@
|
|||
"message": "Antworten Sie auf die folgende E-Mail „{%mail_text_body%}“. {%additional_text%}. Antworten Sie nur mit dem erforderlichen Text und ohne zusätzliche Kommentare oder anderen Text."
|
||||
},
|
||||
"prompt_reply_custom_command": {
|
||||
"message": "Mit Befehl antworten"
|
||||
"message": "Mit Befehl antworten..."
|
||||
},
|
||||
"prefs_OptionText_chatgpt_web_br_replace_info": {
|
||||
"message": "Bitte beachten Sie, dass alle <br>-Tags in der Antwort der KI durch Zeilenumbrüche ersetzt werden."
|
||||
|
|
@ -1449,5 +1449,32 @@
|
|||
},
|
||||
"antispam_by": {
|
||||
"message": "Antispam von"
|
||||
},
|
||||
"prefs_doc_title": {
|
||||
"message": "Dokumentation"
|
||||
},
|
||||
"prefs_doc_setup_guide": {
|
||||
"message": "Einrichtungsleitfäden"
|
||||
},
|
||||
"prefs_doc_custom_prompt_tutorial": {
|
||||
"message": "Tutorial für benutzerdefinierte Prompts"
|
||||
},
|
||||
"prefs_doc_open_welcome": {
|
||||
"message": "Die Willkommensseite öffnen"
|
||||
},
|
||||
"prefs_OptionText_chatgpt_win_pos_text": {
|
||||
"message": "Position des KI-Chatfensters"
|
||||
},
|
||||
"prefs_OptionText_chatgpt_win_top": {
|
||||
"message": "Oben"
|
||||
},
|
||||
"prefs_OptionText_chatgpt_win_left": {
|
||||
"message": "Links"
|
||||
},
|
||||
"prefs_chatgpt_win_save_position": {
|
||||
"message": "Fensterposition beim Schließen automatisch speichern."
|
||||
},
|
||||
"prefs_chatgpt_win_position_info": {
|
||||
"message": "Leer lassen, um die Standardposition zu verwenden."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"message": "Απάντηση σε αυτό το νήμα"
|
||||
},
|
||||
"prompt_reply_custom_command": {
|
||||
"message": "Απάντηση με εντολή"
|
||||
"message": "Απάντηση με εντολή..."
|
||||
},
|
||||
"prompt_rewrite_polite": {
|
||||
"message": "Ξαναγράψε ευγενικά"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
"description": ""
|
||||
},
|
||||
"prompt_reply_custom_command": {
|
||||
"message": "Reply with command",
|
||||
"message": "Reply with command...",
|
||||
"description": ""
|
||||
},
|
||||
"prompt_rewrite_polite": {
|
||||
|
|
@ -931,6 +931,22 @@
|
|||
"message": "Click here, it only takes a minute!",
|
||||
"description": ""
|
||||
},
|
||||
"prefs_doc_title": {
|
||||
"message": "Documentation",
|
||||
"description": ""
|
||||
},
|
||||
"prefs_doc_setup_guide": {
|
||||
"message": "Setup Guides",
|
||||
"description": ""
|
||||
},
|
||||
"prefs_doc_custom_prompt_tutorial": {
|
||||
"message": "Custom Prompt Tutorial",
|
||||
"description": ""
|
||||
},
|
||||
"prefs_doc_open_welcome": {
|
||||
"message": "Open the Welcome Page",
|
||||
"description": ""
|
||||
},
|
||||
"prefs_OpenAIComp_ForceModel": {
|
||||
"message": "Manually insert model",
|
||||
"description": ""
|
||||
|
|
@ -1970,5 +1986,25 @@
|
|||
"prefs_THStats_2": {
|
||||
"message": "Click here! Try ThunderStats!",
|
||||
"description": ""
|
||||
},
|
||||
"prefs_OptionText_chatgpt_win_pos_text":{
|
||||
"message": "AI chat window position",
|
||||
"description": ""
|
||||
},
|
||||
"prefs_OptionText_chatgpt_win_top":{
|
||||
"message": "Top",
|
||||
"description": ""
|
||||
},
|
||||
"prefs_OptionText_chatgpt_win_left":{
|
||||
"message": "Left",
|
||||
"description": ""
|
||||
},
|
||||
"prefs_chatgpt_win_save_position": {
|
||||
"message": "Automatically save the window position when using the close button.",
|
||||
"description": ""
|
||||
},
|
||||
"prefs_chatgpt_win_position_info":{
|
||||
"message": "Leave empty to use the default position.",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
|
|
@ -168,7 +168,7 @@
|
|||
"message": "Malelekti ĉion"
|
||||
},
|
||||
"prompt_reply_custom_command": {
|
||||
"message": "Respondi per komando"
|
||||
"message": "Respondi per komando..."
|
||||
},
|
||||
"customPrompts_substitute_text": {
|
||||
"message": "Anstataŭigi tekston"
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
"message": "Clasificar"
|
||||
},
|
||||
"prompt_reply_custom_command": {
|
||||
"message": "Responder con comando"
|
||||
"message": "Responder con comando..."
|
||||
},
|
||||
"prompt_this": {
|
||||
"message": "Crea un prompt de esto"
|
||||
|
|
|
|||
|
|
@ -1211,7 +1211,7 @@
|
|||
"message": "Gérer les espaces réservés de données"
|
||||
},
|
||||
"prompt_reply_custom_command": {
|
||||
"message": "Répondre avec une commande"
|
||||
"message": "Répondre avec une commande..."
|
||||
},
|
||||
"prompt_reply_custom_command_full_text": {
|
||||
"message": "Répondez à l'e-mail suivant « {%mail_text_body%} ». {%additional_text%}. Répondez uniquement avec le texte nécessaire, sans commentaires ou texte supplémentaire."
|
||||
|
|
@ -1449,5 +1449,32 @@
|
|||
},
|
||||
"antispam_by": {
|
||||
"message": "Antispam par"
|
||||
},
|
||||
"prefs_doc_title": {
|
||||
"message": "Documentation"
|
||||
},
|
||||
"prefs_doc_setup_guide": {
|
||||
"message": "Guides de configuration"
|
||||
},
|
||||
"prefs_doc_custom_prompt_tutorial": {
|
||||
"message": "Tutoriel sur les prompts personnalisés"
|
||||
},
|
||||
"prefs_doc_open_welcome": {
|
||||
"message": "Ouvrir la page d'accueil"
|
||||
},
|
||||
"prefs_OptionText_chatgpt_win_pos_text": {
|
||||
"message": "Position de la fenêtre de chat IA"
|
||||
},
|
||||
"prefs_OptionText_chatgpt_win_top": {
|
||||
"message": "Haut"
|
||||
},
|
||||
"prefs_OptionText_chatgpt_win_left": {
|
||||
"message": "Gauche"
|
||||
},
|
||||
"prefs_chatgpt_win_save_position": {
|
||||
"message": "Enregistrer automatiquement la position de la fenêtre lors de la fermeture."
|
||||
},
|
||||
"prefs_chatgpt_win_position_info": {
|
||||
"message": "Laisser vide pour utiliser la position par défaut."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1211,7 +1211,7 @@
|
|||
"message": "I segnaposto di dati esistenti con lo stesso ID verranno sovrascritti. I segnaposto con ID nuovi verranno aggiunti."
|
||||
},
|
||||
"prompt_reply_custom_command": {
|
||||
"message": "Rispondi con istruzioni aggiuntive"
|
||||
"message": "Rispondi con istruzioni aggiuntive..."
|
||||
},
|
||||
"prompt_reply_custom_command_full_text": {
|
||||
"message": "Rispondi alla seguente email \"{%mail_text_body%}\". {%additional_text%}. Rispondi solo con il testo necessario, senza commenti aggiuntivi o altro testo."
|
||||
|
|
@ -1449,5 +1449,32 @@
|
|||
},
|
||||
"antispam_by": {
|
||||
"message": "Antispam di"
|
||||
},
|
||||
"prefs_doc_title": {
|
||||
"message": "Documentazione"
|
||||
},
|
||||
"prefs_doc_setup_guide": {
|
||||
"message": "Guide di configurazione"
|
||||
},
|
||||
"prefs_doc_custom_prompt_tutorial": {
|
||||
"message": "Tutorial Prompt Personalizzato"
|
||||
},
|
||||
"prefs_doc_open_welcome": {
|
||||
"message": "Apri la Pagina di Benvenuto"
|
||||
},
|
||||
"prefs_OptionText_chatgpt_win_pos_text": {
|
||||
"message": "Posizione finestra della chat AI"
|
||||
},
|
||||
"prefs_OptionText_chatgpt_win_top": {
|
||||
"message": "Alto"
|
||||
},
|
||||
"prefs_OptionText_chatgpt_win_left": {
|
||||
"message": "Sinistra"
|
||||
},
|
||||
"prefs_chatgpt_win_save_position": {
|
||||
"message": "Salva automaticamente la posizione della finestra quando si usa il pulsante chiudi."
|
||||
},
|
||||
"prefs_chatgpt_win_position_info": {
|
||||
"message": "Lascia vuoto per usare la posizione di default."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
_locales/nl/messages.json
Normal file
5
_locales/nl/messages.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extensionDescription": {
|
||||
"message": "Gebruik ChatGPT, Google Gemini, Claude of Ollama om uw emails te verbeteren."
|
||||
}
|
||||
}
|
||||
|
|
@ -156,7 +156,7 @@
|
|||
"message": "Responder"
|
||||
},
|
||||
"prompt_reply_custom_command": {
|
||||
"message": "Responder com o comando"
|
||||
"message": "Responder com o comando..."
|
||||
},
|
||||
"chatgpt_win_close": {
|
||||
"message": "Fechar"
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
"message": "Răspunde la această conversație"
|
||||
},
|
||||
"prompt_reply_custom_command": {
|
||||
"message": "Răspunde cu instrucțiuni suplimentare"
|
||||
"message": "Răspunde cu instrucțiuni suplimentare..."
|
||||
},
|
||||
"prompt_rewrite_polite": {
|
||||
"message": "Rescrie politicos"
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@
|
|||
"message": "Svara på denna tråd"
|
||||
},
|
||||
"prompt_reply_custom_command": {
|
||||
"message": "Svara med kommando"
|
||||
"message": "Svara med kommando..."
|
||||
},
|
||||
"prompt_rewrite_polite": {
|
||||
"message": "Skriv om artigt"
|
||||
|
|
@ -1433,5 +1433,32 @@
|
|||
},
|
||||
"prefs_OptionText_calendar_no_selection_missing_placeholder": {
|
||||
"message": "Prompten måste innehålla platshållaren {%mail_text_body_or_selected%} eller {%mail_html_body_or_selected%} för att aktivera det här alternativet. Lägg till en av dessa platshållare i prompten eller återställ den till standard."
|
||||
},
|
||||
"prefs_doc_title": {
|
||||
"message": "Dokumentation"
|
||||
},
|
||||
"prefs_doc_setup_guide": {
|
||||
"message": "Installationsguider"
|
||||
},
|
||||
"prefs_doc_custom_prompt_tutorial": {
|
||||
"message": "Handledning för anpassad prompt"
|
||||
},
|
||||
"prefs_doc_open_welcome": {
|
||||
"message": "Öppna välkomstsidan"
|
||||
},
|
||||
"prefs_OptionText_chatgpt_win_pos_text": {
|
||||
"message": "AI-chattfönstrets position"
|
||||
},
|
||||
"prefs_OptionText_chatgpt_win_top": {
|
||||
"message": "Överst"
|
||||
},
|
||||
"prefs_OptionText_chatgpt_win_left": {
|
||||
"message": "Vänster"
|
||||
},
|
||||
"prefs_chatgpt_win_save_position": {
|
||||
"message": "Spara automatiskt fönstrets position när stängningsknappen används."
|
||||
},
|
||||
"prefs_chatgpt_win_position_info": {
|
||||
"message": "Lämna tomt för att använda standardpositionen."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -843,7 +843,7 @@
|
|||
"message": "未找到可编辑的日历!"
|
||||
},
|
||||
"customPrompts_form_label_use_diff_viewer": {
|
||||
"message": "启用差异查看器"
|
||||
"message": "启用文本差异查看器"
|
||||
},
|
||||
"get_calendar_event_prompt_prefs_title": {
|
||||
"message": "日历事件选项"
|
||||
|
|
@ -888,6 +888,27 @@
|
|||
"message": "说明"
|
||||
},
|
||||
"customPrompts_form_label_use_diff_viewer_title": {
|
||||
"message": "当操作设置为“替换文本”时,可以选择差异查看器。"
|
||||
"message": "当操作设置为“替换文本”时,可以选择文本差异查看器。"
|
||||
},
|
||||
"prompt_reply_custom_command": {
|
||||
"message": "使用附加的提示词指令..."
|
||||
},
|
||||
"prompt_string": {
|
||||
"message": "提示词"
|
||||
},
|
||||
"prefs_OptionText_reply_type_Info": {
|
||||
"message": "此即回复电子邮件时默认采用的回复类型。您可在后续的回信对话框中另行选择其他选项。"
|
||||
},
|
||||
"prefs_OptionText_btnManageCustomDataPH": {
|
||||
"message": "管理您的数据占位符"
|
||||
},
|
||||
"OpenChatGPTTab_Info2": {
|
||||
"message": "从此处打开 ChatGPT Web 版将应用已配置好的模型、项目及自定义 GPT 相关设置。"
|
||||
},
|
||||
"placeholder_mail_headers": {
|
||||
"message": "邮件头"
|
||||
},
|
||||
"placeholder_selected_html": {
|
||||
"message": "已选中的 HTML"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,6 +115,8 @@ if (worker) {
|
|||
case 'anthropic': llmName = "Claude"; break;
|
||||
}
|
||||
messagesArea.setLLMName(llmName);
|
||||
|
||||
document.title += " [" + llmName + " | " + decodeURIComponent(prompt_name) + "]";
|
||||
|
||||
let workerInitMessage = {
|
||||
type: 'init',
|
||||
|
|
@ -266,7 +268,8 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||
if(userInput !== null) {
|
||||
if(!placeholdersUtils.hasPlaceholder(promptData.prompt, 'additional_text')){
|
||||
// no additional_text placeholder, do as usual
|
||||
promptData.prompt += " " + userInput;
|
||||
const inputText = Array.isArray(userInput) ? userInput.map(obj => obj.custom_text).join(' ') : userInput;
|
||||
promptData.prompt += " " + inputText;
|
||||
}else{
|
||||
// we have the additional_text placeholder, do the magic!
|
||||
let finalSubs = {};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<!-- Other meta tags and stylesheets -->
|
||||
<link rel="stylesheet" type="text/css" href="styles.css">
|
||||
|
||||
<title>ThunderAI Assistant</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Use the custom tags directly -->
|
||||
|
|
|
|||
|
|
@ -534,7 +534,7 @@ class MessagesArea extends HTMLElement {
|
|||
|
||||
// Convert Markdown to DOM nodes using the markdown-it library
|
||||
const md = window.markdownit();
|
||||
const html = convertNewlinesToBr(md.render(fullText));
|
||||
const html = md.render(fullText);
|
||||
|
||||
this.fullTextHTML += html;
|
||||
|
||||
|
|
@ -543,6 +543,7 @@ class MessagesArea extends HTMLElement {
|
|||
// Create a new DOM parser
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
convertTextNodeNewlinesToBr(doc.body);
|
||||
|
||||
// Remove existing tokens
|
||||
while (this.accumulatingMessageEl.firstChild) {
|
||||
|
|
@ -586,8 +587,23 @@ function htmlStringToFragment(htmlString) {
|
|||
return fragment;
|
||||
}
|
||||
|
||||
function convertNewlinesToBr(text) {
|
||||
return text.replace(/\n/g, '<br>');
|
||||
function convertTextNodeNewlinesToBr(element) {
|
||||
element.childNodes.forEach(node => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
if (node.textContent.includes('\n') && node.textContent.trim() !== '') {
|
||||
const fragment = document.createDocumentFragment();
|
||||
node.textContent.split('\n').forEach((part, idx, arr) => {
|
||||
fragment.appendChild(document.createTextNode(part));
|
||||
if (idx < arr.length - 1) {
|
||||
fragment.appendChild(document.createElement('br'));
|
||||
}
|
||||
});
|
||||
node.parentNode.replaceChild(fragment, node);
|
||||
}
|
||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
convertTextNodeNewlinesToBr(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeAloneBRs(htmlString) {
|
||||
|
|
|
|||
108
claude-spec/01-architecture.md
Normal file
108
claude-spec/01-architecture.md
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# Architecture
|
||||
|
||||
## Extension Structure (Manifest V2)
|
||||
|
||||
ThunderAI runs as a standard Thunderbird WebExtension with three main execution contexts:
|
||||
|
||||
```
|
||||
Background Page → mzta-background.html / mzta-background.js
|
||||
Popup → popup/mzta-popup.html / popup/mzta-popup.js
|
||||
Options Page → options/mzta-options.html / options/mzta-options.js
|
||||
Feature Pages → pages/*/
|
||||
Content Script → js/lib/diff.js (injected into chatgpt.com)
|
||||
Web Workers → js/workers/model-worker-*.js (one per API provider)
|
||||
```
|
||||
|
||||
## Data Flow: User Action → AI Response
|
||||
|
||||
```
|
||||
User clicks popup or presses Ctrl+Alt+A
|
||||
↓
|
||||
popup/mzta-popup.js (renders prompt list, handles selection)
|
||||
↓ (sendMessage to background)
|
||||
mzta-background.js (orchestrates everything)
|
||||
↓
|
||||
js/mzta-placeholders.js (resolves {%placeholder%} values from email data)
|
||||
↓
|
||||
js/mzta-prompts.js (builds final prompt string)
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Based on connection_type: │
|
||||
│ chatgpt_web → js/mzta-chatgpt.js │ (opens ChatGPT window)
|
||||
│ chatgpt_api → Web Worker (openai) │
|
||||
│ ollama_api → Web Worker (ollama) │
|
||||
│ google_gemini → Web Worker (gemini) │
|
||||
│ anthropic → Web Worker (anthropic)│
|
||||
│ openai_comp → Web Worker (comp) │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
Result returned to background
|
||||
↓
|
||||
js/mzta-compose-script.js (inserts text into Thunderbird compose window)
|
||||
```
|
||||
|
||||
## Key Modules
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `mzta-background.js` | Main orchestrator: listens for messages, coordinates all features |
|
||||
| `js/mzta-menus.js` | Context menu creation and management |
|
||||
| `js/mzta-prompts.js` | Prompt definitions (built-in) and custom prompt loading |
|
||||
| `js/mzta-placeholders.js` | Placeholder definitions and resolution logic |
|
||||
| `js/mzta-utils.js` | General utilities (email parsing, storage helpers, etc.) |
|
||||
| `js/mzta-utils-prompt.js` | Prompt-specific utilities (text truncation, lang injection) |
|
||||
| `js/mzta-compose-script.js` | Injects AI response into Thunderbird compose window |
|
||||
| `js/mzta-chatgpt.js` | ChatGPT Web integration (opens browser window, reads DOM) |
|
||||
| `js/mzta-special-commands.js` | Handles special prompt actions (add_tags, calendar, task) |
|
||||
| `js/mzta-spamreport.js` | Spam filter logic |
|
||||
| `js/mzta-i18n.js` | i18n helper (wraps `browser.i18n.getMessage`) |
|
||||
| `js/mzta-logger.js` | Debug logging (gated by `do_debug` pref) |
|
||||
| `js/mzta-store.js` | Storage abstraction helpers |
|
||||
| `js/mzta-working-status.js` | Visual status indicator during AI processing |
|
||||
| `js/mzta-addatags-exclusion-list.js` | Tag exclusion list management |
|
||||
| `js/mzta-placeholders-autocomplete.js` | Autocomplete for placeholders in prompt editor |
|
||||
|
||||
## API Modules (`js/api/`)
|
||||
|
||||
Each file handles HTTP communication for one provider:
|
||||
|
||||
| File | Provider |
|
||||
|------|----------|
|
||||
| `anthropic.js` | Claude (Anthropic) API |
|
||||
| `google_gemini.js` | Google Gemini API |
|
||||
| `ollama.js` | Ollama (self-hosted) |
|
||||
| `openai_comp.js` | OpenAI-compatible APIs |
|
||||
| `openai_comp_configs.js` | Pre-configured providers (DeepSeek, Grok, Mistral, OpenRouter, Perplexity) |
|
||||
| `openai_responses.js` | OpenAI Responses API |
|
||||
|
||||
## Web Workers (`js/workers/`)
|
||||
|
||||
Each API provider has a dedicated Web Worker so API calls don't block the UI:
|
||||
|
||||
- `model-worker-anthropic.js`
|
||||
- `model-worker-google_gemini.js`
|
||||
- `model-worker-ollama.js`
|
||||
- `model-worker-openai_comp.js`
|
||||
- `model-worker-openai_responses.js`
|
||||
|
||||
Workers receive a message with the prompt and settings, make the API call, and post back the result.
|
||||
|
||||
## Feature Pages (`pages/`)
|
||||
|
||||
Each subdirectory is a self-contained settings/UI page for a specific feature:
|
||||
|
||||
| Directory | Feature |
|
||||
|-----------|---------|
|
||||
| `addtags/` | Auto-tagging configuration |
|
||||
| `customprompts/` | Custom prompt editor |
|
||||
| `customdataplaceholders/` | Custom placeholder editor |
|
||||
| `get-calendar-event/` | Calendar event extraction settings |
|
||||
| `get-task/` | Task creation settings |
|
||||
| `spamfilter/` | Spam filter settings |
|
||||
| `summarize/` | Email summarization settings |
|
||||
| `onboarding/` | First-run welcome page |
|
||||
| `_lib/` | Shared libraries used by pages |
|
||||
|
||||
## Storage
|
||||
|
||||
All preferences are stored via `browser.storage.local`. The keys and default values are defined in `options/mzta-options-default.js` (`prefs_default` export). Custom prompts and custom placeholders are stored separately in storage under their own keys.
|
||||
86
claude-spec/02-prompts.md
Normal file
86
claude-spec/02-prompts.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# Prompts System
|
||||
|
||||
## Overview
|
||||
|
||||
Prompts are the core user-facing feature of ThunderAI. Each prompt defines an AI instruction and how it behaves. There are two kinds:
|
||||
|
||||
- **Built-in prompts** — defined in `js/mzta-prompts.js`
|
||||
- **Custom prompts** — created by the user and stored in `browser.storage.local`
|
||||
|
||||
## Prompt Properties
|
||||
|
||||
### Base Properties (built-in only)
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `id` | string | Unique identifier |
|
||||
| `name` | string | `__MSG_key__` i18n reference or plain text |
|
||||
| `prompt` | string | The prompt template text (may contain `{%placeholder%}` tokens) |
|
||||
| `type` | number | `0` = always visible, `1` = reading email only, `2` = composing only |
|
||||
| `action` | number | `0` = close, `1` = reply (open compose), `2` = substitute text in-place |
|
||||
| `need_selected` | number | `0` = use full message body, `1` = requires text selection |
|
||||
| `need_signature` | number | `0` = no signature, `1` = include signature |
|
||||
| `need_custom_text` | number | `0` = no custom input, `1` = show custom text input field |
|
||||
| `define_response_lang` | number | `0` = no language hint, `1` = append response language instruction |
|
||||
| `use_diff_viewer` | number | `0` = normal output, `1` = show diff viewer (ChatGPT Web only) |
|
||||
|
||||
### User Properties (stored per-prompt in storage)
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `enabled` | number | `0` = hidden, `1` = shown in popup |
|
||||
| `position_display` | number | Sort order in reading view |
|
||||
| `position_compose` | number | Sort order in compose view |
|
||||
|
||||
### Per-Prompt API Override Properties
|
||||
|
||||
Each prompt can override the global API connection. These mirror the keys in `integration_options_config` and `prefs_default`:
|
||||
|
||||
| Property | Description |
|
||||
|----------|-------------|
|
||||
| `connection_type` | Override API type for this prompt |
|
||||
| `chatgpt_web_model` | Override ChatGPT Web model |
|
||||
| `chatgpt_web_project` | Override ChatGPT Web project |
|
||||
| `chatgpt_web_custom_gpt` | Override custom GPT |
|
||||
| All `chatgpt_*`, `ollama_*`, `openai_comp_*`, `google_gemini_*`, `anthropic_*` keys | Override specific API settings |
|
||||
|
||||
## Special Prompts
|
||||
|
||||
Some prompts trigger additional Thunderbird actions beyond just sending text to the AI. They are identified by their `id`:
|
||||
|
||||
| ID | Feature |
|
||||
|----|---------|
|
||||
| `add_tags` | Auto-tag the email after AI response |
|
||||
| `spamfilter` | Classify as spam and optionally move email |
|
||||
| `summarize` | Summarize email content |
|
||||
| `get_calendar_event` | Extract and create a calendar event |
|
||||
| `get_task` | Extract and create a task |
|
||||
|
||||
These special prompts can have their own dedicated API integration settings (configured in the Options page). The list of these special prompts is in `options/mzta-options-default.js` as `special_prompts_with_integration`.
|
||||
|
||||
## Prompt Types Reference
|
||||
|
||||
```
|
||||
type 0 → shown when reading AND composing
|
||||
type 1 → shown only when reading an email (message display)
|
||||
type 2 → shown only when composing an email
|
||||
```
|
||||
|
||||
## Action Types Reference
|
||||
|
||||
```
|
||||
action 0 → no output, just close (e.g. for tag/spam actions handled in background)
|
||||
action 1 → open a reply compose window with AI response
|
||||
action 2 → replace selected text (or insert) in compose window
|
||||
```
|
||||
|
||||
## Adding a New Built-in Prompt
|
||||
|
||||
1. Add the prompt object to the `defaultPrompts` array in `js/mzta-prompts.js`
|
||||
2. Add the `name` string key to `_locales/en/messages.json`
|
||||
3. If the prompt text needs a localized string, add it to `_locales/en/messages.json` as well
|
||||
4. Reference any needed placeholders using `{%placeholder_id%}` syntax in the `prompt` field
|
||||
|
||||
## Custom Prompts
|
||||
|
||||
Custom prompts are stored in `browser.storage.local` and managed via `pages/customprompts/`. They follow the same property structure as built-in prompts but are created/edited/deleted by the user through the UI. Custom placeholders can also be referenced in custom prompt text.
|
||||
78
claude-spec/03-placeholders.md
Normal file
78
claude-spec/03-placeholders.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Placeholders System
|
||||
|
||||
## Overview
|
||||
|
||||
Placeholders are dynamic tokens embedded in prompt text that get replaced with real data at runtime (email content, headers, user input, etc.).
|
||||
|
||||
**Format:** `{%placeholder_id%}`
|
||||
|
||||
Example in a prompt: `"Summarize this email: {%mail_text_body_or_selected%}"`
|
||||
|
||||
## Placeholder Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `id` | string | Unique identifier used in `{%id%}` tokens |
|
||||
| `name` | string | Display name (i18n `__MSG_key__` or plain text) |
|
||||
| `default_value` | string | Value used if placeholder cannot be resolved |
|
||||
| `type` | number | `0` = always, `1` = reading only, `2` = composing only |
|
||||
| `is_default` | string | `"1"` = built-in (not editable/deletable), `"0"` = custom |
|
||||
| `is_dynamic` | string | `"0"` = fixed value, `"1"` = dynamic (takes a parameter after `:`) |
|
||||
| `enabled` | number | `0` = disabled, `1` = enabled |
|
||||
| `text` | string | Content for custom placeholders only |
|
||||
|
||||
## Built-in Placeholders (defined in `js/mzta-placeholders.js`)
|
||||
|
||||
| ID | Description | Type |
|
||||
|----|-------------|------|
|
||||
| `mail_text_body` | Full plain text of the email | 0 |
|
||||
| `mail_html_body` | Full HTML of the email | 0 |
|
||||
| `mail_typed_text` | Text typed so far in compose window | 2 |
|
||||
| `mail_text_body_or_selected` | Plain text body, or selected text if any | 1 |
|
||||
| `mail_html_body_or_selected` | HTML body, or selected HTML if any | 1 |
|
||||
| `mail_selected_text` | Only the selected text | 1 |
|
||||
| `mail_selected_html` | Only the selected HTML | 1 |
|
||||
| `mail_subject` | Email subject line | 0 |
|
||||
| `mail_date` | Email date | 1 |
|
||||
| `mail_author` | Email sender | 0 |
|
||||
| `mail_recipients` | Email recipients | 0 |
|
||||
| `mail_tags` | Current tags on the email | 1 |
|
||||
| `mail_available_tags` | All available tags in Thunderbird | 1 |
|
||||
| `identity_name` | Current identity display name | 0 |
|
||||
| `identity_email` | Current identity email address | 0 |
|
||||
| `identity_signature` | Current identity signature | 0 |
|
||||
| `additional_text[id]` | User input field (dynamic, shows input in popup) | 0 |
|
||||
| `mail_header:name` | Any email header by name (dynamic) | 1 |
|
||||
|
||||
## Dynamic Placeholders
|
||||
|
||||
Dynamic placeholders use a colon separator to pass a parameter:
|
||||
|
||||
```
|
||||
{%additional_text:my_field_id%} → shows an input field labelled "my_field_id" in the popup
|
||||
{%mail_header:x-spam-score%} → fetches the X-Spam-Score header value
|
||||
```
|
||||
|
||||
The `is_dynamic: "1"` property signals this behavior in the placeholder definition.
|
||||
|
||||
## Custom Placeholders
|
||||
|
||||
Users can define their own placeholders via `pages/customdataplaceholders/`. Custom placeholders:
|
||||
- Have `is_default: "0"`
|
||||
- Have a `text` property containing the replacement value
|
||||
- Are stored in `browser.storage.local`
|
||||
- Are merged with default placeholders at runtime before prompt processing
|
||||
|
||||
## Placeholder Resolution Order
|
||||
|
||||
1. Built-in placeholders are defined in `js/mzta-placeholders.js`
|
||||
2. Custom placeholders are loaded from storage
|
||||
3. At runtime, `mzta-background.js` gathers email data (via Thunderbird APIs)
|
||||
4. Each `{%id%}` token in the prompt string is replaced with the resolved value
|
||||
5. If a value cannot be resolved, `default_value` is used as fallback
|
||||
|
||||
## Adding a New Built-in Placeholder
|
||||
|
||||
1. Add the object to the `defaultPlaceholders` array in `js/mzta-placeholders.js`
|
||||
2. Add the `name` i18n key to `_locales/en/messages.json` as `placeholder_<id>` (or choose a descriptive key)
|
||||
3. Implement the resolution logic in the relevant section of `mzta-background.js`
|
||||
86
claude-spec/04-api-integrations.md
Normal file
86
claude-spec/04-api-integrations.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# API Integrations
|
||||
|
||||
## Connection Types
|
||||
|
||||
The active AI provider is controlled by the `connection_type` preference. Possible values:
|
||||
|
||||
| `connection_type` value | Provider |
|
||||
|------------------------|----------|
|
||||
| `chatgpt_web` | ChatGPT Web (no API key, opens browser window) |
|
||||
| `chatgpt_api` | OpenAI API (ChatGPT via API key) |
|
||||
| `ollama_api` | Ollama (self-hosted LLM) |
|
||||
| `openai_comp_api` | OpenAI-compatible API |
|
||||
| `google_gemini_api` | Google Gemini API |
|
||||
| `anthropic_api` | Claude (Anthropic) API |
|
||||
|
||||
The global default is `chatgpt_web`. Each special prompt (`add_tags`, `spamfilter`, etc.) can independently override this via its own `{prefix}_connection_type` pref.
|
||||
|
||||
## Provider Configuration
|
||||
|
||||
Each provider has its own settings block in `integration_options_config` (`options/mzta-options-default.js`):
|
||||
|
||||
### ChatGPT Web
|
||||
Controlled via `js/mzta-chatgpt.js`. Opens a browser window to `chatgpt.com`, injects the prompt via DOM automation, and reads back the response. Settings: `chatgpt_web_model`, `chatgpt_web_tempchat`, `chatgpt_web_project`, `chatgpt_web_custom_gpt`, `chatgpt_web_load_wait_time`.
|
||||
|
||||
Content script `js/lib/diff.js` is injected into ChatGPT pages for diff-view support.
|
||||
|
||||
### OpenAI API (`chatgpt_api`)
|
||||
- Module: `js/api/openai_responses.js`
|
||||
- Worker: `js/workers/model-worker-openai_responses.js`
|
||||
- Settings keys: `chatgpt_api_key`, `chatgpt_model`, `chatgpt_developer_messages`, `chatgpt_temperature`, `chatgpt_store`
|
||||
|
||||
### Ollama (`ollama_api`)
|
||||
- Module: `js/api/ollama.js`
|
||||
- Worker: `js/workers/model-worker-ollama.js`
|
||||
- Settings keys: `ollama_host`, `ollama_model`, `ollama_num_ctx`, `ollama_temperature`, `ollama_think`
|
||||
- Requires CORS to be configured on the Ollama server
|
||||
|
||||
### OpenAI-Compatible (`openai_comp_api`)
|
||||
- Module: `js/api/openai_comp.js`
|
||||
- Worker: `js/workers/model-worker-openai_comp.js`
|
||||
- Settings keys: `openai_comp_host`, `openai_comp_model`, `openai_comp_api_key`, `openai_comp_use_v1`, `openai_comp_chat_name`, `openai_comp_temperature`
|
||||
- Pre-configured providers: `js/api/openai_comp_configs.js` (DeepSeek, Grok, Mistral, OpenRouter, Perplexity)
|
||||
|
||||
### Google Gemini (`google_gemini_api`)
|
||||
- Module: `js/api/google_gemini.js`
|
||||
- Worker: `js/workers/model-worker-google_gemini.js`
|
||||
- Settings keys: `google_gemini_api_key`, `google_gemini_model`, `google_gemini_system_instruction`, `google_gemini_thinking_budget`, `google_gemini_temperature`
|
||||
|
||||
### Anthropic / Claude (`anthropic_api`)
|
||||
- Module: `js/api/anthropic.js`
|
||||
- Worker: `js/workers/model-worker-anthropic.js`
|
||||
- Settings keys: `anthropic_api_key`, `anthropic_model`, `anthropic_version`, `anthropic_max_tokens`, `anthropic_system_prompt`, `anthropic_temperature`
|
||||
|
||||
## Web Worker Pattern
|
||||
|
||||
For all API-based providers (everything except ChatGPT Web), the call goes through a Web Worker:
|
||||
|
||||
```
|
||||
mzta-background.js
|
||||
→ creates new Worker('js/workers/model-worker-<provider>.js')
|
||||
→ postMessage({ prompt, settings })
|
||||
→ worker makes HTTP fetch to provider API
|
||||
→ worker postMessage({ result }) back
|
||||
→ background handles result
|
||||
```
|
||||
|
||||
This keeps API calls off the main thread and avoids blocking the Thunderbird UI.
|
||||
|
||||
## Optional Permissions
|
||||
|
||||
API calls require host permissions. These are declared as `optional_permissions` in `manifest.json` and requested at runtime:
|
||||
|
||||
- `https://*.chatgpt.com/*` and `https://*.openai.com/*` for ChatGPT
|
||||
- `https://*.anthropic.com/*` for Claude
|
||||
- `https://*/*` and `http://*/*` for Ollama and OpenAI-compatible endpoints
|
||||
|
||||
## Adding a New Provider
|
||||
|
||||
1. Create `js/api/<provider>.js` with the API call logic
|
||||
2. Create `js/workers/model-worker-<provider>.js` that imports and calls the API module
|
||||
3. Add a new `connection_type` value constant
|
||||
4. Add settings keys to `integration_options_config` in `options/mzta-options-default.js`
|
||||
5. Add UI controls to `options/mzta-options.html` and `options/mzta-options.js`
|
||||
6. Add the new `connection_type` case to the dispatch logic in `mzta-background.js`
|
||||
7. Add required host permissions to `manifest.json` optional_permissions
|
||||
8. Add i18n strings to `_locales/en/messages.json`
|
||||
113
claude-spec/05-options.md
Normal file
113
claude-spec/05-options.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# Options & Settings System
|
||||
|
||||
## Overview
|
||||
|
||||
All extension preferences are stored in `browser.storage.local`. Defaults and the full list of valid keys are defined in `options/mzta-options-default.js`.
|
||||
|
||||
## Key Exports from `mzta-options-default.js`
|
||||
|
||||
| Export | Description |
|
||||
|--------|-------------|
|
||||
| `prefs_default` | All preference keys with their default values |
|
||||
| `integration_options_config` | Per-provider API settings structure |
|
||||
| `getDynamicSettingsDefaults(keysFilter)` | Returns per-special-prompt integration defaults |
|
||||
| `getDynamicSettingValue(prefs, prefix, settingName)` | Reads a prefixed setting for a special prompt |
|
||||
|
||||
## Settings Structure
|
||||
|
||||
### Global Integration Settings
|
||||
|
||||
Stored flat in `prefs_default` with `{provider}_{key}` naming:
|
||||
|
||||
```
|
||||
chatgpt_api_key, chatgpt_model, chatgpt_developer_messages, chatgpt_temperature, chatgpt_store
|
||||
ollama_host, ollama_model, ollama_num_ctx, ollama_temperature, ollama_think
|
||||
openai_comp_host, openai_comp_model, openai_comp_api_key, openai_comp_use_v1, openai_comp_chat_name, openai_comp_temperature
|
||||
google_gemini_api_key, google_gemini_model, google_gemini_system_instruction, google_gemini_thinking_budget, google_gemini_temperature
|
||||
anthropic_api_key, anthropic_model, anthropic_version, anthropic_max_tokens, anthropic_system_prompt, anthropic_temperature
|
||||
```
|
||||
|
||||
Plus the global connection selector:
|
||||
```
|
||||
connection_type (default: 'chatgpt_web')
|
||||
use_specific_integration (default: false)
|
||||
```
|
||||
|
||||
### Special Prompt Integration Overrides
|
||||
|
||||
The 5 special prompts (`add_tags`, `spamfilter`, `summarize`, `get_calendar_event`, `get_task`) each get their own `use_specific_integration` and `connection_type` keys:
|
||||
|
||||
```
|
||||
{prefix}_use_specific_integration (default: false)
|
||||
{prefix}_connection_type (default: 'chatgpt_api')
|
||||
```
|
||||
|
||||
These are generated programmatically at the bottom of `mzta-options-default.js` using `special_prompts_with_integration` array.
|
||||
|
||||
### UI & Feature Preferences
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `do_debug` | `false` | Enable debug logging |
|
||||
| `chatgpt_win_height` | `800` | ChatGPT window height |
|
||||
| `chatgpt_win_width` | `700` | ChatGPT window width |
|
||||
| `chatgpt_win_top` | `''` | Window top position |
|
||||
| `chatgpt_win_left` | `''` | Window left position |
|
||||
| `chatgpt_win_save_position` | `false` | Remember window position |
|
||||
| `default_chatgpt_lang` | `''` | Force response language |
|
||||
| `default_sign_name` | `''` | Default signature name |
|
||||
| `reply_type` | `'reply_all'` | Default reply type |
|
||||
| `composing_plain_text` | `false` | Use plain text in compose |
|
||||
| `chatgpt_web_model` | `''` | ChatGPT Web model override |
|
||||
| `chatgpt_web_tempchat` | `false` | Use temporary chat |
|
||||
| `chatgpt_web_project` | `''` | ChatGPT Web project |
|
||||
| `chatgpt_web_custom_gpt` | `''` | Custom GPT URL |
|
||||
| `chatgpt_web_load_wait_time` | `1000` | Wait time (ms) for ChatGPT page |
|
||||
| `dynamic_menu_force_enter` | `false` | Force Enter to submit in popup |
|
||||
| `dynamic_menu_order_alphabet` | `true` | Sort prompts alphabetically |
|
||||
| `placeholders_use_default_value` | `false` | Use placeholder defaults when empty |
|
||||
| `max_prompt_length` | `30000` | Max prompt string length |
|
||||
|
||||
### Feature Flags
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `add_tags` | `false` | Enable auto-tagging feature |
|
||||
| `add_tags_maxnum` | `3` | Max tags to apply |
|
||||
| `add_tags_hide_exclusions` | `false` | Hide excluded tags from menu |
|
||||
| `add_tags_exclusions_exact_match` | `false` | Exact match for exclusions |
|
||||
| `add_tags_first_uppercase` | `true` | Capitalize first letter of tags |
|
||||
| `add_tags_force_lang` | `true` | Force language for tags |
|
||||
| `add_tags_auto` | `false` | Auto-tag on message open |
|
||||
| `add_tags_auto_force_existing` | `false` | Only use existing tags |
|
||||
| `add_tags_auto_only_inbox` | `true` | Auto-tag only inbox messages |
|
||||
| `add_tags_auto_uselist` | `false` | Use tag allow-list |
|
||||
| `add_tags_auto_uselist_list` | `''` | Tag allow-list content |
|
||||
| `add_tags_enabled_accounts` | `[]` | Accounts where auto-tag is active |
|
||||
| `get_calendar_event` | `true` | Enable calendar event extraction |
|
||||
| `get_calendar_event_from_clipboard` | `false` | Enable calendar from clipboard |
|
||||
| `get_task` | `true` | Enable task creation |
|
||||
| `calendar_enforce_timezone` | `false` | Force specific timezone |
|
||||
| `calendar_timezone` | `''` | Timezone to enforce |
|
||||
| `calendar_no_selection` | `false` | Skip selection prompt |
|
||||
| `spamfilter` | `false` | Enable spam filter |
|
||||
| `spamfilter_threshold` | `70` | Spam confidence threshold (%) |
|
||||
| `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 |
|
||||
|
||||
## Adding a New Preference
|
||||
|
||||
1. Add the key and default value to `prefs_default` in `options/mzta-options-default.js`
|
||||
2. Add UI control to `options/mzta-options.html`
|
||||
3. Add load/save logic to `options/mzta-options.js`
|
||||
4. Add i18n label to `_locales/en/messages.json`
|
||||
5. Read the pref in the relevant module via `browser.storage.local.get()`
|
||||
|
||||
## Reading Preferences at Runtime
|
||||
|
||||
```javascript
|
||||
const prefs = await browser.storage.local.get(prefs_default);
|
||||
// prefs now contains all keys with defaults for any unset values
|
||||
const myPref = prefs.my_new_pref;
|
||||
```
|
||||
91
claude-spec/06-localization.md
Normal file
91
claude-spec/06-localization.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# Localization
|
||||
|
||||
## Golden Rule
|
||||
|
||||
**Only ever modify `_locales/en/messages.json`.**
|
||||
|
||||
All other locale files (`de`, `fr`, `it`, `es`, `zh_Hans`, `zh_Hant`, `pl`, `ru`, `pt-br`, `sv`, `el`, `cs`, `hr`, `ja`, `nb_NO`) are managed by translators through [Weblate](https://hosted.weblate.org/). Never edit them manually.
|
||||
|
||||
## Message File Format
|
||||
|
||||
Each entry in `_locales/en/messages.json` follows the standard WebExtension i18n format:
|
||||
|
||||
```json
|
||||
"key_name": {
|
||||
"message": "The English text",
|
||||
"description": "Context for translators explaining where/how this string is used"
|
||||
}
|
||||
```
|
||||
|
||||
The `description` field is important — it helps Weblate translators understand the context.
|
||||
|
||||
## Using Strings in Code
|
||||
|
||||
### In JavaScript
|
||||
```javascript
|
||||
import { i18n } from './mzta-i18n.js';
|
||||
const text = i18n('key_name');
|
||||
// or directly:
|
||||
const text = browser.i18n.getMessage('key_name');
|
||||
```
|
||||
|
||||
### In HTML
|
||||
```html
|
||||
<span data-i18n="key_name"></span>
|
||||
<!-- or via manifest/attribute references: -->
|
||||
__MSG_key_name__
|
||||
```
|
||||
|
||||
### In manifest.json
|
||||
```json
|
||||
"description": "__MSG_extensionDescription__"
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
| Prefix | Usage |
|
||||
|--------|-------|
|
||||
| `menu_*` | Context menu and popup menu labels |
|
||||
| `prompt_*` | Built-in prompt names |
|
||||
| `placeholder_*` | Placeholder display names |
|
||||
| `options_*` | Settings page labels |
|
||||
| `pages_*` | Feature page labels |
|
||||
| `error_*` | Error messages |
|
||||
| `info_*` | Informational messages |
|
||||
| `btn_*` | Button labels |
|
||||
|
||||
## Adding a New String
|
||||
|
||||
1. Open `_locales/en/messages.json`
|
||||
2. Add the new key in alphabetical order within the file (or near related keys)
|
||||
3. Include both `message` and `description` fields
|
||||
4. Use the string in code via `browser.i18n.getMessage('key_name')` or `__MSG_key_name__`
|
||||
|
||||
Example:
|
||||
```json
|
||||
"my_new_feature_label": {
|
||||
"message": "My New Feature",
|
||||
"description": "Label for the new feature button in the options page"
|
||||
}
|
||||
```
|
||||
|
||||
## Supported Languages (16)
|
||||
|
||||
| Code | Language |
|
||||
|------|----------|
|
||||
| `en` | English (source) |
|
||||
| `de` | German |
|
||||
| `es` | Spanish |
|
||||
| `fr` | French |
|
||||
| `it` | Italian |
|
||||
| `pl` | Polish |
|
||||
| `ru` | Russian |
|
||||
| `pt-br` | Brazilian Portuguese |
|
||||
| `sv` | Swedish |
|
||||
| `el` | Greek |
|
||||
| `cs` | Czech |
|
||||
| `hr` | Croatian |
|
||||
| `ja` | Japanese |
|
||||
| `nb_NO` | Norwegian Bokmål |
|
||||
| `zh_Hans` | Chinese Simplified |
|
||||
| `zh_Hant` | Chinese Traditional |
|
||||
398
claude-spec/99-thunderbird-team-spec.md
Normal file
398
claude-spec/99-thunderbird-team-spec.md
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
# Thunderbird WebExtensions Development Guidelines
|
||||
|
||||
> **Purpose:** This file provides operative guidelines for Claude when helping develop or modify ThunderAI. These rules override general AI assistant behavior and must be followed strictly.
|
||||
|
||||
## ThunderAI-Specific Context
|
||||
|
||||
- ThunderAI uses **Manifest Version 2** — do not suggest or apply any MV3 migration.
|
||||
- No build tools, no transpilation, no npm — plain ES6 modules loaded directly.
|
||||
- The manifest already uses `browser_specific_settings` (not `applications`).
|
||||
- All module imports use relative paths with `.js` extension.
|
||||
- The `mzta-` prefix is used for all core module filenames.
|
||||
|
||||
---
|
||||
|
||||
## Important Guidelines for AI Assistants
|
||||
|
||||
### 1. Always use `browser_specific_settings` in manifest.json
|
||||
|
||||
The `applications` manifest entry is deprecated. Always use `browser_specific_settings`:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "ThunderAI",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "thunderai@micz.it",
|
||||
"strict_min_version": "140.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Do not guess APIs by using Try-Catch
|
||||
|
||||
A widespread antipattern in AI-generated Thunderbird extensions:
|
||||
|
||||
```javascript
|
||||
// WRONG - Never do this!
|
||||
try {
|
||||
await browser.someApi.method({ guessedParam: value });
|
||||
} catch (e) {
|
||||
try {
|
||||
await browser.someApi.method({ differentGuess: value });
|
||||
} catch (e2) {
|
||||
// Giving up silently — this makes debugging impossible
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why this is harmful:**
|
||||
- Makes code unmaintainable
|
||||
- Hides real errors from developers
|
||||
- Makes debugging extremely difficult
|
||||
|
||||
**The correct approach:**
|
||||
1. Read the API documentation FIRST
|
||||
2. Use the exact parameter names and types specified
|
||||
3. Only use try-catch for expected error conditions with proper handling
|
||||
4. Never suppress errors without logging or handling them
|
||||
|
||||
### 3. Do not use Experiments unnecessarily
|
||||
|
||||
```javascript
|
||||
// WRONG - Using Experiment when standard API exists
|
||||
// Don't use Experiment just because you found example code using it
|
||||
|
||||
// RIGHT - Check if standard API can do it first
|
||||
const folders = await browser.folders.query({ name: "Inbox" });
|
||||
```
|
||||
|
||||
### 4. Handle file storage correctly
|
||||
|
||||
```javascript
|
||||
// WRONG - Trying to use raw filesystem APIs
|
||||
const fs = require('fs'); // Not available!
|
||||
|
||||
// RIGHT - Use storage.local with File objects
|
||||
const file = new File([content], "data.txt", { type: "text/plain" });
|
||||
await browser.storage.local.set({ file });
|
||||
```
|
||||
|
||||
### 5. Do not use async listeners for the runtime.onMessage listener
|
||||
|
||||
See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage
|
||||
|
||||
### 6. Parse vCard, vTodo, vEvent and iCal strings using a 3rd party library
|
||||
|
||||
Follow https://webextension-api.thunderbird.net/en/mv2/guides/vcard.html to parse vCard, vEvent and vTodo strings.
|
||||
|
||||
### 7. Parse Mailbox Strings using messengerUtilities
|
||||
|
||||
Extract email addresses from mailbox strings like "John Doe <john@example.com>":
|
||||
|
||||
```javascript
|
||||
const parsed = await browser.messengerUtilities.parseMailboxString(
|
||||
"John Doe <john@example.com>, Jane <jane@example.com>"
|
||||
);
|
||||
|
||||
// Result:
|
||||
// [
|
||||
// { name: "John Doe", email: "john@example.com" },
|
||||
// { name: "Jane", email: "jane@example.com" }
|
||||
// ]
|
||||
|
||||
// Extract just emails:
|
||||
const emails = parsed.map(p => p.email);
|
||||
```
|
||||
|
||||
**Documentation:** https://webextension-api.thunderbird.net/en/mv2/messengerUtilities.html
|
||||
|
||||
**Options:**
|
||||
- `preserveGroups`: Keep grouped hierarchies
|
||||
- `expandMailingLists`: Expand Thunderbird mailing lists (requires `addressBook` permission)
|
||||
|
||||
### 8. Set correct `strict_min_version` entry
|
||||
|
||||
Make sure `manifest.json` has a `strict_min_version` entry matching the used functions. If a function added in Thunderbird 137 is used, it must be set to `137.0` or higher.
|
||||
|
||||
### 9. Always use background type "module"
|
||||
|
||||
Always use `type: "module"` for background scripts. This allows use of the `import` directive for ES6 modules, and non-ES6 libraries can still be loaded via the `scripts` array:
|
||||
|
||||
```json
|
||||
// RIGHT - Always use type: "module"
|
||||
"background": {
|
||||
"scripts": ["lib/some-non-ES6-lib.js", "background.js"],
|
||||
"type": "module"
|
||||
}
|
||||
```
|
||||
|
||||
Then in `background.js`, import libraries explicitly:
|
||||
```javascript
|
||||
// Import ES6 module with default export
|
||||
import ICAL from "./lib/ical.js";
|
||||
|
||||
// Import ES6 module with named exports
|
||||
import { someFunction, someConstant } from "./lib/somemodule.js";
|
||||
```
|
||||
|
||||
### 10. Verify API return types — do not assume array access
|
||||
|
||||
Many Thunderbird APIs return wrapped objects, not direct arrays. Always verify the return type in the documentation before accessing the data.
|
||||
|
||||
**Common pitfall — MessageList:**
|
||||
```javascript
|
||||
// WRONG - getDisplayedMessages() returns MessageList, not an array
|
||||
const [message] = await browser.messageDisplay.getDisplayedMessages(tabId);
|
||||
|
||||
// RIGHT - MessageList has a .messages array property
|
||||
const { messages: [message] } = await browser.messageDisplay.getDisplayedMessages(tabId);
|
||||
```
|
||||
|
||||
**Common pitfall — HeadersDictionary:**
|
||||
```javascript
|
||||
// WRONG - headers might not exist or might not be an array
|
||||
let returnPath = headers["Return-Path"];
|
||||
|
||||
// RIGHT - keys are lowercase, values are always arrays
|
||||
const returnPathArray = headers["return-path"];
|
||||
const returnPath = returnPathArray?.[0] ?? null;
|
||||
```
|
||||
|
||||
**APIs that return wrapped objects (NOT direct arrays):**
|
||||
|
||||
| API | Returns | Access Pattern |
|
||||
|-----|---------|----------------|
|
||||
| `messageDisplay.getDisplayedMessages()` | `MessageList` | `result.messages[0]` |
|
||||
| `messages.list()` | `MessageList` | `result.messages[0]` |
|
||||
| `messages.query()` | `MessageList` | `result.messages[0]` |
|
||||
| `messages.getHeaders()` | `HeadersDictionary` | `result["header-name"][0]` |
|
||||
| `messages.getFull()` | `MessagePart` | `result.headers["header-name"][0]` |
|
||||
|
||||
**APIs that return direct arrays:**
|
||||
|
||||
| API | Returns | Access Pattern |
|
||||
|-----|---------|----------------|
|
||||
| `tabs.query()` | array of Tab | `result[0]` |
|
||||
| `mailTabs.query()` | array of MailTab | `result[0]` |
|
||||
| `addressBooks.list()` | array of AddressBookNode | `result[0]` |
|
||||
| `contacts.list()` | array of ContactNode | `result[0]` |
|
||||
| `folders.query()` | array of MailFolder | `result[0]` |
|
||||
|
||||
---
|
||||
|
||||
## Official API Documentation
|
||||
|
||||
**Primary resource:** https://webextension-api.thunderbird.net/en/mv2/
|
||||
|
||||
Documentation exists for different channels:
|
||||
- **Release (mv2):** https://webextension-api.thunderbird.net/en/mv2/
|
||||
- **ESR (esr-mv2):** https://webextension-api.thunderbird.net/en/esr-mv2/
|
||||
|
||||
**Key feature:** Search functionality and cross-references between types and functions.
|
||||
|
||||
---
|
||||
|
||||
## Understanding Thunderbird Release Channels
|
||||
|
||||
### Standard Release Channel (Monthly)
|
||||
- Update cadence: ~4 weeks
|
||||
- A new major version with each release (`147.0`, `148.0`, ...)
|
||||
- Gets new features and APIs (and bug fixes & security fixes)
|
||||
|
||||
### ESR Channel (Extended Support Release)
|
||||
- Update cadence: 1 major update per year, with lots of versions "missing" in between (`115.*`, `128.*`, `140.*`, `153.*`, ...)
|
||||
- Receives bug fixes & security fixes on regular basis alongside the major monthly releases, but as minor releases (`140.1`, `140.2`, ...)
|
||||
- No new features or APIs
|
||||
|
||||
### For ThunderAI
|
||||
- Target the Release channel for standard API usage.
|
||||
- Target the ESR channel for add-ons relying on Experiment APIs. Targeting the Release channel with Experiments is acceptable *only* when the developer can guarantee required monthly updates.
|
||||
|
||||
---
|
||||
|
||||
## Experiment APIs
|
||||
|
||||
### What Are Experiment APIs?
|
||||
Experiment APIs allow add-ons to access Thunderbird's core internals directly. They have full access to modify everything in Thunderbird.
|
||||
|
||||
### Critical Rules for Experiments
|
||||
|
||||
**1. Avoid Experiments Unless Absolutely Necessary**
|
||||
- Standard WebExtension APIs should always be your first choice
|
||||
- Only use Experiments when standard APIs genuinely cannot accomplish your goal
|
||||
|
||||
**2. Experiments Require Updates for Each Major Version**
|
||||
- This was manageable with ESR (1x/year)
|
||||
- Became unsustainable with monthly Release channel (12x/year)
|
||||
|
||||
**3. When Suggesting Experiments**
|
||||
- Only if standard APIs cannot accomplish the core goal
|
||||
- Developer must explicitly understand the maintenance burden
|
||||
- **Target ESR channel specifically**
|
||||
- Reference `esr-mv2` documentation
|
||||
|
||||
### Available semi-official Experiment APIs
|
||||
|
||||
#### Calendar Experiment API
|
||||
|
||||
Use this instead of creating a custom Experiment for calendar interactions.
|
||||
|
||||
**Use cases:**
|
||||
- Reading existing event/task items from Thunderbird's calendar
|
||||
- Listening for item updates
|
||||
- Creating/updating/deleting items
|
||||
|
||||
**Setup requirements:**
|
||||
1. Temporarily clone the [webext-experiments](https://github.com/thunderbird/webext-experiments/) repository.
|
||||
2. Add all `experiment_apis` entries found in `calendar/manifest.json` to the project's `manifest.json`.
|
||||
3. Copy `calendar/experiments/calendar/` into the project as `experiments/calendar/`. Do not modify these files.
|
||||
|
||||
**Note:** Always request iCal format:
|
||||
|
||||
```javascript
|
||||
// Always consult schema first, if this example is still correct
|
||||
browser.calendar.items.onCreated.addListener(
|
||||
async (calendarItem) => {
|
||||
if (calendarItem.type === "task") {
|
||||
console.log("Task in iCal format:", calendarItem.item);
|
||||
}
|
||||
},
|
||||
{ returnFormat: "ical" }
|
||||
);
|
||||
```
|
||||
|
||||
### Other Experiment Repositories
|
||||
|
||||
- https://github.com/thunderbird/webext-support — Helper APIs and modules
|
||||
- https://github.com/thunderbird/webext-examples — Example extensions (includes some Experiments)
|
||||
|
||||
---
|
||||
|
||||
## Native File System Access
|
||||
|
||||
### Current Limitations
|
||||
Native filesystem access is NOT available in Thunderbird WebExtensions.
|
||||
|
||||
### Recommended Approach
|
||||
|
||||
**For data persistence:**
|
||||
```javascript
|
||||
await browser.storage.local.set({ myData: someValue });
|
||||
const data = await browser.storage.local.get("myData");
|
||||
```
|
||||
|
||||
**For user file input:**
|
||||
```javascript
|
||||
const file = new File([content], "filename.txt", { type: "text/plain" });
|
||||
await browser.storage.local.set({ file });
|
||||
|
||||
// Retrieve later
|
||||
const data = await browser.storage.local.get("file");
|
||||
console.log(data.file.name);
|
||||
```
|
||||
|
||||
**Important:** File objects can be stored directly in `browser.storage.local` without serialization.
|
||||
|
||||
---
|
||||
|
||||
## Add-on Review Requirements
|
||||
|
||||
**Review policy:** https://thunderbird.github.io/atn-review-policy/
|
||||
|
||||
### Key Requirements
|
||||
|
||||
**1. No Build Tools**
|
||||
- Include 3rd party libraries directly (don't use webpack, rollup, etc.)
|
||||
- Include a `VENDOR.md` file documenting all 3rd party libraries with links to exact versions (not "latest"). Example: https://webextension-api.thunderbird.net/en/mv2/guides/vcard.html
|
||||
|
||||
**2. Permissions**
|
||||
- Only request permissions you actually need
|
||||
- The `tabs` and `activeTab` permissions are almost never needed in Thunderbird
|
||||
- Unnecessary permissions may cause rejection during ATN review
|
||||
|
||||
---
|
||||
|
||||
## Example Repositories
|
||||
|
||||
- https://github.com/thunderbird/webext-examples — Official example extensions
|
||||
- https://github.com/thunderbird/webext-support — Support libraries and helpers
|
||||
|
||||
Use these to see proper code structure, learn common patterns, and understand best practices.
|
||||
|
||||
---
|
||||
|
||||
## Mandatory Checklist Before Providing Code
|
||||
|
||||
Before providing any code, verify ALL of these:
|
||||
|
||||
- [ ] Consulted official API documentation — do NOT guess methods or parameters
|
||||
- [ ] NO try-catch blocks for guessing API parameters
|
||||
- [ ] Used 3rd party libraries or API methods for parsing — minimize manual string parsing or regex
|
||||
- [ ] Used 3rd party libraries are the most recent stable version
|
||||
- [ ] Event listeners registered at file scope (NOT inside init function)
|
||||
- [ ] VENDOR.md includes ALL dependencies with exact version URLs
|
||||
- [ ] Used `browser_specific_settings` (NOT deprecated `applications`)
|
||||
- [ ] Included proper error handling
|
||||
- [ ] Code has comments explaining the approach
|
||||
- [ ] No hardcoded user-facing strings — use the i18n API (`_locales/en/messages.json` only)
|
||||
- [ ] Add-on fulfills all requirements in the "Add-on Review Requirements" section
|
||||
- [ ] All guidelines in "Important Guidelines for AI Assistants" are followed
|
||||
- [ ] Manifest uses correct `strict_min_version`
|
||||
- [ ] If using Experiments: manifest has `strict_max_version` targeting current ESR (fetch https://webextension-api.thunderbird.net/en/esr-mv2/ to get the major version, then use format `"<major>.*"`)
|
||||
|
||||
If ANY checkbox is unchecked, DO NOT provide the code. Fix it first.
|
||||
|
||||
---
|
||||
|
||||
## Mandatory 3rd Party Library Audit
|
||||
|
||||
For EACH 3rd party library included in the project:
|
||||
|
||||
- [ ] Inspect the actual file to determine the export type:
|
||||
- **ES6 default export:** Look for `export default` → use `import LibName from "./lib/file.js"`
|
||||
- **ES6 named exports:** Look for `export { name1, name2 }` → use `import { name1, name2 } from "./lib/file.js"`
|
||||
- **UMD/IIFE (no ES6 exports):** Look for `(function(root, factory)` or assignments to `window`/`globalThis` → load via `scripts` array in manifest
|
||||
- [ ] Always prefer the minified module version
|
||||
- [ ] Output a library audit table:
|
||||
|
||||
| Library | File | Module Type | Import Statement |
|
||||
|---------|------|-------------|------------------|
|
||||
| ical.js | lib/ical.js | ES6 default | `import ICAL from "./lib/ical.js"` |
|
||||
|
||||
- [ ] Update VENDOR.md with the correct file path and version URL
|
||||
|
||||
---
|
||||
|
||||
## Mandatory API Audit
|
||||
|
||||
Before finalizing any code:
|
||||
|
||||
- [ ] List all used API methods
|
||||
- [ ] For EACH API method, fetch its documentation page: `https://webextension-api.thunderbird.net/en/mv2/<api-name>.html`
|
||||
- [ ] For EACH API method, verify:
|
||||
- **Parameters:** Correct names and types
|
||||
- **Return type:** The actual type returned by the Promise
|
||||
- **Access pattern:** How to extract data from the return value
|
||||
- **Required permission:** What permission is needed in manifest.json
|
||||
- [ ] Output an API audit table:
|
||||
|
||||
| API Method | Returns | Access Pattern | Required Permission |
|
||||
|------------|---------|----------------|---------------------|
|
||||
| `browser.messageDisplay.getDisplayedMessages()` | MessageList | `result.messages[0]` | messagesRead |
|
||||
| `browser.messages.getHeaders()` | HeadersDictionary | `result["header-name"][0]` | messagesRead |
|
||||
| `browser.mailTabs.query()` | array of MailTab | `result[0]` | (none) |
|
||||
| `browser.storage.local.get` | object | `result.keyName` | storage |
|
||||
| `browser.i18n.getMessage` | string | direct | (none) |
|
||||
|
||||
- [ ] Update the permissions entry in manifest.json to include ALL required permissions
|
||||
|
||||
---
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Developer documentation:** https://developer.thunderbird.net/
|
||||
- **Support forum:** https://thunderbird.topicbox.com/groups/addons
|
||||
- **Matrix chat:** #tb-addon-developers:mozilla.org
|
||||
|
|
@ -614,11 +614,21 @@ async function doProceed(message, customText = ''){
|
|||
let final_prompt = message.prompt;
|
||||
|
||||
if (Array.isArray(customText)) {
|
||||
let anyReplaced = false;
|
||||
customText.forEach(obj => {
|
||||
let escapedPH = obj.placeholder.replace(/[.*+?^{$}()|[\\]\\\\]/g, '\\\\$&');
|
||||
let regex = new RegExp(escapedPH, 'g');
|
||||
final_prompt = final_prompt.replace(regex, obj.custom_text);
|
||||
if (final_prompt.includes(obj.placeholder)) {
|
||||
anyReplaced = true;
|
||||
let escapedPH = obj.placeholder.replace(/[.*+?^{$}()|[\\]\\\\]/g, '\\\\$&');
|
||||
let regex = new RegExp(escapedPH, 'g');
|
||||
final_prompt = final_prompt.replace(regex, obj.custom_text);
|
||||
}
|
||||
});
|
||||
if (!anyReplaced) {
|
||||
const inputText = customText.map(obj => obj.custom_text).join(' ');
|
||||
if (inputText !== '') {
|
||||
final_prompt += ' ' + inputText;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//check if there is the additional_text placeholder
|
||||
if(final_prompt.includes('{%additional_text%}')){
|
||||
|
|
|
|||
|
|
@ -682,13 +682,13 @@ switch (message.command) {
|
|||
borderColor = '#006600';
|
||||
}
|
||||
|
||||
container.style.cssText = `background-color: ${bgColor}; color: ${textColor}; border-bottom: 1px solid ${borderColor}; padding: 8px 12px; font-family: system-ui, -apple-system, sans-serif; font-size: 13px; display: flex; align-items: start; gap: 15px; width: 100%; box-sizing: border-box;`;
|
||||
container.style.cssText = `background-color: ${bgColor}; color: ${textColor}; border-bottom: 1px solid ${borderColor}; padding: 8px 12px; font-family: system-ui, -apple-system, sans-serif; font-size: 13px; display: flex; align-items: center; gap: 15px; width: 100%; box-sizing: border-box;`;
|
||||
|
||||
const scoreText = document.createElement('strong');
|
||||
if (data.spamValue == -999) {
|
||||
scoreText.textContent = browser.i18n.getMessage("apiwebchat_error");
|
||||
} else {
|
||||
scoreText.textContent = ((data.spamValue >= (data.SpamThreshold || 50)) ? browser.i18n.getMessage("Spam") : browser.i18n.getMessage("Valid")) + " [" + data.spamValue + "/100]";
|
||||
scoreText.textContent = ((data.spamValue >= (data.SpamThreshold || 50)) ? "⚠️ " + browser.i18n.getMessage("Spam") : "🛡️ " + browser.i18n.getMessage("Valid")) + " [" + data.spamValue + "/100]";
|
||||
}
|
||||
|
||||
const reasonText = document.createElement('span');
|
||||
|
|
@ -700,7 +700,7 @@ switch (message.command) {
|
|||
|
||||
const branding = document.createElement('span');
|
||||
branding.textContent = browser.i18n.getMessage("antispam_by") + " ThunderAI";
|
||||
branding.style.cssText = 'margin-left: auto; font-style: italic; font-size: 11px; opacity: 0.7;';
|
||||
branding.style.cssText = 'margin-left: auto; font-style: italic; font-size: 10px; opacity: 0.5;';
|
||||
|
||||
const closeBtn = document.createElement('span');
|
||||
closeBtn.textContent = '×';
|
||||
|
|
|
|||
|
|
@ -16,30 +16,49 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const taSpamReport = {
|
||||
logger: console,
|
||||
_data_prefix: 'mzta-spam-report-',
|
||||
_processing_prefix: 'mzta-spam-processing-',
|
||||
_max_reports: 100,
|
||||
import { taStorage } from './mzta-storage.js';
|
||||
import { taLogger } from './mzta-logger.js';
|
||||
|
||||
export class taSpamReport {
|
||||
|
||||
_processing_prefix = 'mzta-spam-processing-';
|
||||
_max_reports = 100;
|
||||
_storage = null;
|
||||
taLog = null;
|
||||
|
||||
constructor(do_debug = false) {
|
||||
this._storage = new taStorage(do_debug);
|
||||
this.taLog = new taLogger('mzta-spamreport', do_debug);
|
||||
}
|
||||
|
||||
async setProcessing(data_id) {
|
||||
this.taLog.log("[setProcessing] data_id: " + data_id);
|
||||
const key = this._processing_prefix + data_id;
|
||||
await browser.storage.session.set({ [key]: true });
|
||||
},
|
||||
}
|
||||
|
||||
async isProcessing(data_id) {
|
||||
this.taLog.log("[isProcessing] data_id: " + data_id);
|
||||
const key = this._processing_prefix + data_id;
|
||||
let output = await browser.storage.session.get(key);
|
||||
return output[key] || false;
|
||||
},
|
||||
let result = output[key] || false;
|
||||
this.taLog.log("[isProcessing] result: " + result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async saveReportData(data, data_id) {
|
||||
const key = this._data_prefix + data_id;
|
||||
await browser.storage.session.set({ [key]: data });
|
||||
await browser.storage.session.remove(this._processing_prefix + data_id);
|
||||
},
|
||||
this.taLog.log("[saveReportData] data_id: " + data_id);
|
||||
try {
|
||||
await this._storage.writeSpam(data_id, data, true);
|
||||
await browser.storage.session.remove(this._processing_prefix + data_id);
|
||||
} catch (e) {
|
||||
this.taLog.error("[saveReportData] error: " + e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async saveError(data_id, error_message) {
|
||||
this.taLog.log("[saveError] data_id: " + data_id + ", error_message: " + error_message);
|
||||
let data = {
|
||||
spamValue: -999,
|
||||
explanation: error_message,
|
||||
|
|
@ -48,53 +67,71 @@ export const taSpamReport = {
|
|||
};
|
||||
await this.saveReportData(data, data_id);
|
||||
return data;
|
||||
},
|
||||
}
|
||||
|
||||
async loadReportData(data_id) {
|
||||
const key = this._data_prefix + data_id;
|
||||
let output = await browser.storage.session.get(key);
|
||||
return output[key] || null;
|
||||
},
|
||||
this.taLog.log("[loadReportData] data_id: " + data_id);
|
||||
let record = await this._storage.getRecord(data_id);
|
||||
if (!record || !this._storage.hasField(record, taStorage.FIELD_SPAM)) {
|
||||
this.taLog.log("[loadReportData] no record found for data_id: " + data_id);
|
||||
return null;
|
||||
}
|
||||
let spam = record.spam;
|
||||
return {
|
||||
headerMessageId: data_id,
|
||||
spamValue: spam.spamValue,
|
||||
explanation: spam.explanation,
|
||||
report_date: new Date(spam.ts),
|
||||
subject: spam.subject,
|
||||
from: spam.from,
|
||||
message_date: spam.message_date,
|
||||
moved: spam.moved,
|
||||
SpamThreshold: spam.SpamThreshold,
|
||||
};
|
||||
}
|
||||
|
||||
async removeReportData(data_id) {
|
||||
const key = this._data_prefix + data_id;
|
||||
await browser.storage.session.remove(key);
|
||||
this.taLog.log("[removeReportData] data_id: " + data_id);
|
||||
await this._storage.deleteSpamField(data_id);
|
||||
await browser.storage.session.remove(this._processing_prefix + data_id);
|
||||
},
|
||||
}
|
||||
|
||||
async getAllReportData() {
|
||||
let allData = await browser.storage.session.get(null);
|
||||
let reportData = {};
|
||||
|
||||
for (const [key, value] of Object.entries(allData)) {
|
||||
if (key.startsWith(this._data_prefix)) {
|
||||
reportData[key.replace(this._data_prefix, '')] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return reportData;
|
||||
},
|
||||
this.taLog.log("[getAllReportData] loading all reports");
|
||||
return await this._storage.getAllSpamRecords();
|
||||
}
|
||||
|
||||
async clearReportData() {
|
||||
let allData = await browser.storage.session.get(null);
|
||||
let keysToDelete = Object.keys(allData).filter(key => key.startsWith(this._data_prefix) || key.startsWith(this._processing_prefix));
|
||||
|
||||
this.taLog.log("[clearReportData] clearing all report data");
|
||||
let allSpam = await this._storage.getAllSpamRecords();
|
||||
let spamKeys = Object.keys(allSpam);
|
||||
this.taLog.log("[clearReportData] deleting " + spamKeys.length + " spam records");
|
||||
for (let messageId of spamKeys) {
|
||||
await this._storage.deleteSpamField(messageId);
|
||||
}
|
||||
let allSession = await browser.storage.session.get(null);
|
||||
let keysToDelete = Object.keys(allSession).filter(k => k.startsWith(this._processing_prefix));
|
||||
this.taLog.log("[clearReportData] deleting " + keysToDelete.length + " session keys");
|
||||
for (let key of keysToDelete) {
|
||||
await browser.storage.session.remove(key);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
async truncReportData() {
|
||||
let data = await this.getAllReportData();
|
||||
this.taLog.log("[truncReportData] checking report count");
|
||||
let data = await this._storage.getAllSpamRecords();
|
||||
let sortedData = this.sortReportsByDate(data);
|
||||
let keys = Object.keys(sortedData);
|
||||
this.taLog.log("[truncReportData] total reports: " + keys.length + ", max: " + this._max_reports);
|
||||
|
||||
if (keys.length > this._max_reports) {
|
||||
let toDelete = keys.length - this._max_reports;
|
||||
this.taLog.log("[truncReportData] truncating " + toDelete + " oldest reports");
|
||||
for (let i = this._max_reports; i < keys.length; i++) {
|
||||
await browser.storage.session.remove(this._data_prefix + keys[i]);
|
||||
await this._storage.deleteSpamField(keys[i]);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
sortReportsByDate(data) {
|
||||
if (!data) return {};
|
||||
|
|
@ -112,4 +149,4 @@ export const taSpamReport = {
|
|||
|
||||
return sortedReports;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
276
js/mzta-storage.js
Normal file
276
js/mzta-storage.js
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
/*
|
||||
* ThunderAI [https://micz.it/thunderbird-addon-thunderai/]
|
||||
* Copyright (C) 2024 - 2026 Mic (m@micz.it)
|
||||
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { taLogger } from './mzta-logger.js';
|
||||
|
||||
export class taStorage {
|
||||
|
||||
static STORAGE_KEY_PREFIX = 'msg:';
|
||||
static SCHEMA_VERSION = 1;
|
||||
static FIELD_SPAM = 'spam';
|
||||
static FIELD_SUMMARY = 'summary';
|
||||
static FIELD_TRANSLATION = 'translation';
|
||||
|
||||
taLog = null;
|
||||
|
||||
constructor(do_debug = false) {
|
||||
this.taLog = new taLogger("mzta-storage", do_debug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the storage key for a given Message-ID.
|
||||
* @param {string} messageId - The Message-ID header string.
|
||||
* @returns {string} The prefixed storage key.
|
||||
*/
|
||||
_buildKey(messageId) {
|
||||
return taStorage.STORAGE_KEY_PREFIX + messageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the full record for a given Message-ID.
|
||||
* @param {string} messageId - The Message-ID header string.
|
||||
* @returns {Promise<object|null>} The record object or null if not found.
|
||||
*/
|
||||
async getRecord(messageId) {
|
||||
this.taLog.log('[getRecord] messageId: ' + messageId);
|
||||
try {
|
||||
let key = this._buildKey(messageId);
|
||||
let result = await messenger.storage.local.get(key);
|
||||
let record = result[key] || null;
|
||||
this.taLog.log('[getRecord] record found: ' + (record !== null));
|
||||
return record;
|
||||
} catch (e) {
|
||||
this.taLog.error('getRecord error: ' + e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a record contains the specified field.
|
||||
* @param {object|null} record - The record object (from getRecord).
|
||||
* @param {string} field - The field name to check ("spam", "summary", or "translation").
|
||||
* @returns {boolean} True if the record exists and the field is present.
|
||||
*/
|
||||
hasField(record, field) {
|
||||
this.taLog.log('[hasField] field: ' + field);
|
||||
try {
|
||||
let result = record !== null && record !== undefined && field in record;
|
||||
this.taLog.log('[hasField] result: ' + result);
|
||||
return result;
|
||||
} catch (e) {
|
||||
this.taLog.error('hasField error: ' + e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the spam field for a given Message-ID.
|
||||
* @param {string} messageId - The Message-ID header string.
|
||||
* @param {object} report_data - The full spam report object with fields:
|
||||
* spamValue, explanation, subject, from, message_date, moved, SpamThreshold.
|
||||
* @param {boolean} [force=true] - If true, overwrite existing spam data.
|
||||
*/
|
||||
async writeSpam(messageId, report_data, force = true) {
|
||||
this.taLog.log('[writeSpam] messageId: ' + messageId + ', force: ' + force);
|
||||
try {
|
||||
let key = this._buildKey(messageId);
|
||||
let record = await this.getRecord(messageId) || { v: taStorage.SCHEMA_VERSION };
|
||||
if (taStorage.FIELD_SPAM in record && !force) {
|
||||
this.taLog.log('[writeSpam] spam field already exists, skipping (force=false)');
|
||||
return;
|
||||
}
|
||||
let now = Date.now();
|
||||
record[taStorage.FIELD_SPAM] = {
|
||||
spamValue: report_data.spamValue,
|
||||
explanation: report_data.explanation,
|
||||
subject: report_data.subject,
|
||||
from: report_data.from,
|
||||
message_date: report_data.message_date instanceof Date
|
||||
? report_data.message_date.toISOString()
|
||||
: report_data.message_date,
|
||||
moved: report_data.moved,
|
||||
SpamThreshold: report_data.SpamThreshold,
|
||||
ts: now,
|
||||
};
|
||||
record.ts = now;
|
||||
await messenger.storage.local.set({ [key]: record });
|
||||
} catch (e) {
|
||||
this.taLog.error('writeSpam error: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all records that contain a spam field.
|
||||
* @returns {Promise<object>} Map of messageId -> spam data object (legacy shape).
|
||||
*/
|
||||
async getAllSpamRecords() {
|
||||
this.taLog.log('[getAllSpamRecords] loading all spam records');
|
||||
try {
|
||||
let all = await messenger.storage.local.get(null);
|
||||
let result = {};
|
||||
for (let [key, record] of Object.entries(all)) {
|
||||
if (!key.startsWith(taStorage.STORAGE_KEY_PREFIX)) continue;
|
||||
if (!this.hasField(record, taStorage.FIELD_SPAM)) continue;
|
||||
let messageId = key.slice(taStorage.STORAGE_KEY_PREFIX.length);
|
||||
let spam = record[taStorage.FIELD_SPAM];
|
||||
result[messageId] = {
|
||||
headerMessageId: messageId,
|
||||
spamValue: spam.spamValue,
|
||||
explanation: spam.explanation,
|
||||
report_date: new Date(spam.ts),
|
||||
subject: spam.subject,
|
||||
from: spam.from,
|
||||
message_date: spam.message_date,
|
||||
moved: spam.moved,
|
||||
SpamThreshold: spam.SpamThreshold,
|
||||
};
|
||||
}
|
||||
this.taLog.log('[getAllSpamRecords] found ' + Object.keys(result).length + ' spam records');
|
||||
return result;
|
||||
} catch (e) {
|
||||
this.taLog.error('getAllSpamRecords error: ' + e);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete only the spam field from a record.
|
||||
* Deletes the entire record if no other data fields remain.
|
||||
* @param {string} messageId - The Message-ID header string.
|
||||
*/
|
||||
async deleteSpamField(messageId) {
|
||||
this.taLog.log('[deleteSpamField] messageId: ' + messageId);
|
||||
try {
|
||||
let key = this._buildKey(messageId);
|
||||
let record = await this.getRecord(messageId);
|
||||
if (!record || !(taStorage.FIELD_SPAM in record)) {
|
||||
this.taLog.log('[deleteSpamField] no spam field found for messageId: ' + messageId);
|
||||
return;
|
||||
}
|
||||
delete record[taStorage.FIELD_SPAM];
|
||||
const remainingFields = Object.keys(record).filter(k => k !== 'v' && k !== 'ts');
|
||||
if (remainingFields.length === 0) {
|
||||
this.taLog.log('[deleteSpamField] no remaining fields, deleting entire record');
|
||||
await messenger.storage.local.remove(key);
|
||||
} else {
|
||||
this.taLog.log('[deleteSpamField] remaining fields: ' + remainingFields.join(', '));
|
||||
await messenger.storage.local.set({ [key]: record });
|
||||
}
|
||||
} catch (e) {
|
||||
this.taLog.error('deleteSpamField error: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the summary field for a given Message-ID.
|
||||
* @param {string} messageId - The Message-ID header string.
|
||||
* @param {string} text - The summary text.
|
||||
* @param {boolean} [force=true] - If true, overwrite existing summary data.
|
||||
*/
|
||||
async writeSummary(messageId, text, force = true) {
|
||||
this.taLog.log('[writeSummary] messageId: ' + messageId + ', force: ' + force);
|
||||
try {
|
||||
let key = this._buildKey(messageId);
|
||||
let record = await this.getRecord(messageId) || { v: taStorage.SCHEMA_VERSION };
|
||||
if (taStorage.FIELD_SUMMARY in record && !force) {
|
||||
this.taLog.log('[writeSummary] summary field already exists, skipping (force=false)');
|
||||
return;
|
||||
}
|
||||
let now = Date.now();
|
||||
record[taStorage.FIELD_SUMMARY] = { text: text, ts: now };
|
||||
record.ts = now;
|
||||
await messenger.storage.local.set({ [key]: record });
|
||||
} catch (e) {
|
||||
this.taLog.error('writeSummary error: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the translation field for a given Message-ID.
|
||||
* @param {string} messageId - The Message-ID header string.
|
||||
* @param {string} translated_text - The translated text.
|
||||
* @param {string} lang - Target language code.
|
||||
* @param {boolean} [force=true] - If true, overwrite existing translation data.
|
||||
*/
|
||||
async writeTranslation(messageId, translated_text, lang, force = true) {
|
||||
this.taLog.log('[writeTranslation] messageId: ' + messageId + ', lang: ' + lang + ', force: ' + force);
|
||||
try {
|
||||
let key = this._buildKey(messageId);
|
||||
let record = await this.getRecord(messageId) || { v: taStorage.SCHEMA_VERSION };
|
||||
if (taStorage.FIELD_TRANSLATION in record && !force) {
|
||||
this.taLog.log('[writeTranslation] translation field already exists, skipping (force=false)');
|
||||
return;
|
||||
}
|
||||
let now = Date.now();
|
||||
record[taStorage.FIELD_TRANSLATION] = { translated_text: translated_text, lang: lang, ts: now };
|
||||
record.ts = now;
|
||||
await messenger.storage.local.set({ [key]: record });
|
||||
} catch (e) {
|
||||
this.taLog.error('writeTranslation error: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the entire record for a given Message-ID.
|
||||
* @param {string} messageId - The Message-ID header string.
|
||||
*/
|
||||
async deleteRecord(messageId) {
|
||||
this.taLog.log('[deleteRecord] messageId: ' + messageId);
|
||||
try {
|
||||
let key = this._buildKey(messageId);
|
||||
await messenger.storage.local.remove(key);
|
||||
} catch (e) {
|
||||
this.taLog.error('deleteRecord error: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all records older than maxAgeDays.
|
||||
* @param {number} maxAgeDays - Maximum age in days. If 0, does nothing.
|
||||
* @returns {Promise<number>} The number of deleted records.
|
||||
*/
|
||||
async cleanup(maxAgeDays) {
|
||||
this.taLog.log('[cleanup] maxAgeDays: ' + maxAgeDays);
|
||||
if (maxAgeDays === 0) {
|
||||
this.taLog.log('[cleanup] maxAgeDays is 0, skipping cleanup');
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
let all = await messenger.storage.local.get(null);
|
||||
let cutoff = Date.now() - (maxAgeDays * 24 * 60 * 60 * 1000);
|
||||
let keysToDelete = [];
|
||||
for (let key of Object.keys(all)) {
|
||||
if (!key.startsWith(taStorage.STORAGE_KEY_PREFIX)) {
|
||||
continue;
|
||||
}
|
||||
let record = all[key];
|
||||
if (record.ts && record.ts < cutoff) {
|
||||
keysToDelete.push(key);
|
||||
}
|
||||
}
|
||||
this.taLog.log('[cleanup] found ' + keysToDelete.length + ' records older than ' + maxAgeDays + ' days');
|
||||
if (keysToDelete.length > 0) {
|
||||
await messenger.storage.local.remove(keysToDelete);
|
||||
}
|
||||
return keysToDelete.length;
|
||||
} catch (e) {
|
||||
this.taLog.error('cleanup error: ' + e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
import { prefs_default, getDynamicSettingValue } from '../options/mzta-options-default.js';
|
||||
const sparks_min = '1.2.0'; // Minimum version of ThunderAI-Sparks required for the add-on to work
|
||||
export const ChatGPTWeb_models = ['gpt-5','gpt-5-instant','gpt-5-t-mini','gpt-5-thinking']; // List of models available in ChatGPT Web
|
||||
const MICZ_IT_LOCALIZED_LANGS = ['es', 'de', 'fr', 'it'];
|
||||
|
||||
export const getMenuContextCompose = () => 'compose_action_menu';
|
||||
export const getMenuContextDisplay = () => 'message_display_action_menu';
|
||||
|
|
@ -38,6 +39,12 @@ export function getLanguageDisplayName(languageCode) {
|
|||
return lang_string.charAt(0).toUpperCase() + lang_string.slice(1);
|
||||
}
|
||||
|
||||
export function getMiczItUrl(path) {
|
||||
const lang = browser.i18n.getUILanguage().split('-')[0];
|
||||
const prefix = MICZ_IT_LOCALIZED_LANGS.includes(lang) ? `${lang}/` : '';
|
||||
return `https://micz.it/${prefix}${path}`;
|
||||
}
|
||||
|
||||
function fixMsgHeader(msgHeader) {
|
||||
if (!msgHeader.bccList) {
|
||||
msgHeader.bccList = [];
|
||||
|
|
@ -220,7 +227,7 @@ export function sanitizeHtml(input) {
|
|||
}
|
||||
|
||||
export function sanitizeMailHeaders(input){
|
||||
console.log(">>>>>>>>>>>> sanitizeMailHeaders input: " + JSON.stringify(input));
|
||||
// console.log(">>>>>>>>>>>> sanitizeMailHeaders input: " + JSON.stringify(input));
|
||||
if(!input) return '';
|
||||
return input.replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
|
@ -482,19 +489,25 @@ function getTagsKeyFromLabel(tag_names, all_tags_list) {
|
|||
}
|
||||
|
||||
function sanitizeString(input) {
|
||||
input = input.toLowerCase();
|
||||
// Define the regex to match valid characters
|
||||
const regex = /^[^ ()/{%*<>"]+$/;
|
||||
const validChar = /^[^ ()/{%*<>"]+$/;
|
||||
// Filter out invalid characters from the string
|
||||
let sanitized = '';
|
||||
for (const char of input) {
|
||||
// Check if the character is valid according to the regex
|
||||
if (regex.test(char)) {
|
||||
sanitized += char;
|
||||
const cp = char.codePointAt(0);
|
||||
if (cp > 0x7F) {
|
||||
// Encode non-ASCII characters (e.g. Chinese, emoji) as uXXXX
|
||||
sanitized += 'u' + cp.toString(16);
|
||||
} else {
|
||||
if (validChar.test(char)) {
|
||||
sanitized += char;
|
||||
}
|
||||
// else: discard blacklisted ASCII chars
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
// Truncate to fit the 50-char total key limit:
|
||||
// $ta- (4) + callID (16) + - (1) + sanitized (max 29) = 50
|
||||
return sanitized.toLowerCase().slice(0, 29);
|
||||
}
|
||||
|
||||
/* returnType:
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ self.onmessage = async function(event) {
|
|||
if (stopStreaming) {
|
||||
stopStreaming = false;
|
||||
reader.cancel();
|
||||
taLog.log("AI full response [STOPPED]: " + assistantResponseAccumulator);
|
||||
conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator });
|
||||
assistantResponseAccumulator = '';
|
||||
postMessage({ type: 'tokensDone' });
|
||||
|
|
@ -87,6 +88,7 @@ self.onmessage = async function(event) {
|
|||
}
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
taLog.log("AI full response: " + assistantResponseAccumulator);
|
||||
conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator });
|
||||
assistantResponseAccumulator = '';
|
||||
postMessage({ type: 'tokensDone' });
|
||||
|
|
@ -138,6 +140,7 @@ self.onmessage = async function(event) {
|
|||
break;
|
||||
|
||||
case 'message_stop':
|
||||
taLog.log("AI full response: " + assistantResponseAccumulator);
|
||||
conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator });
|
||||
assistantResponseAccumulator = '';
|
||||
postMessage({ type: 'tokensDone' });
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ self.onmessage = async function(event) {
|
|||
if (stopStreaming) {
|
||||
stopStreaming = false;
|
||||
reader.cancel();
|
||||
taLog.log("AI full response [STOPPED]: " + assistantResponseAccumulator);
|
||||
conversationHistory.push({ role: 'model', parts: [{"text": assistantResponseAccumulator}] });
|
||||
assistantResponseAccumulator = '';
|
||||
postMessage({ type: 'tokensDone' });
|
||||
|
|
@ -86,6 +87,7 @@ self.onmessage = async function(event) {
|
|||
}
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
taLog.log("AI full response: " + assistantResponseAccumulator);
|
||||
conversationHistory.push({ role: 'model', parts: [{"text": assistantResponseAccumulator}] });
|
||||
assistantResponseAccumulator = '';
|
||||
postMessage({ type: 'tokensDone' });
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ self.onmessage = async function(event) {
|
|||
if (stopStreaming) {
|
||||
stopStreaming = false;
|
||||
reader.cancel();
|
||||
taLog.log("AI full response [STOPPED]: " + assistantResponseAccumulator);
|
||||
conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator });
|
||||
assistantResponseAccumulator = '';
|
||||
postMessage({ type: 'tokensDone' });
|
||||
|
|
@ -89,6 +90,7 @@ self.onmessage = async function(event) {
|
|||
}
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
taLog.log("AI full response: " + assistantResponseAccumulator);
|
||||
conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator });
|
||||
assistantResponseAccumulator = '';
|
||||
postMessage({ type: 'tokensDone' });
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ self.onmessage = async function(event) {
|
|||
if (stopStreaming) {
|
||||
stopStreaming = false;
|
||||
reader.cancel();
|
||||
taLog.log("AI full response [STOPPED]: " + assistantResponseAccumulator);
|
||||
conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator });
|
||||
assistantResponseAccumulator = '';
|
||||
postMessage({ type: 'tokensDone' });
|
||||
|
|
@ -86,6 +87,7 @@ self.onmessage = async function(event) {
|
|||
}
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
taLog.log("AI full response: " + assistantResponseAccumulator);
|
||||
conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator });
|
||||
assistantResponseAccumulator = '';
|
||||
postMessage({ type: 'tokensDone' });
|
||||
|
|
|
|||
|
|
@ -85,11 +85,13 @@ self.onmessage = async function(event) {
|
|||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
let buffer = '';
|
||||
|
||||
let streamError = false;
|
||||
|
||||
while (true) {
|
||||
if (stopStreaming) {
|
||||
stopStreaming = false;
|
||||
reader.cancel();
|
||||
taLog.log("AI full response [STOPPED]: " + assistantResponseAccumulator);
|
||||
conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator });
|
||||
assistantResponseAccumulator = '';
|
||||
postMessage({ type: 'tokensDone' });
|
||||
|
|
@ -97,6 +99,7 @@ self.onmessage = async function(event) {
|
|||
}
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
taLog.log("AI full response: " + assistantResponseAccumulator);
|
||||
conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator });
|
||||
assistantResponseAccumulator = '';
|
||||
postMessage({ type: 'tokensDone' });
|
||||
|
|
@ -137,8 +140,18 @@ self.onmessage = async function(event) {
|
|||
postMessage({ type: 'newToken', payload: { token: parsedLine.delta } });
|
||||
// } else if (parsedLine.type === 'response.completed' && parsedLine.response && parsedLine.response.id) {
|
||||
// previous_response_id = parsedLine.response.id;
|
||||
} else if (parsedLine.type === 'response.failed' && parsedLine.response && parsedLine.response.error) {
|
||||
const error = parsedLine.response.error;
|
||||
const errorMessage = error.message || JSON.stringify(error);
|
||||
taLog.error("response.failed: " + JSON.stringify(error));
|
||||
postMessage({ type: 'error', payload: i18nStrings["chatgpt_api_request_failed"] + ": " + errorMessage });
|
||||
reader.cancel();
|
||||
assistantResponseAccumulator = '';
|
||||
streamError = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (streamError) break;
|
||||
}
|
||||
} else if (event.data.type === 'stop') {
|
||||
stopStreaming = true;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"manifest_version": 2,
|
||||
"name": "ThunderAI",
|
||||
"description": "__MSG_extensionDescription__",
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.0",
|
||||
"author": "Mic (m@micz.it)",
|
||||
"homepage_url": "https://micz.it/thunderbird-addon-thunderai/",
|
||||
"browser_specific_settings": {
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ await reload_pref_init();
|
|||
|
||||
let taLog = new taLogger("mzta-background",prefs_init.do_debug);
|
||||
taWorkingStatus.taLog = taLog;
|
||||
let spamReport = new taSpamReport(prefs_init.do_debug);
|
||||
|
||||
let special_prompts_ids = getActiveSpecialPromptsIDs({
|
||||
addtags: prefs_init.add_tags,
|
||||
|
|
@ -114,25 +115,6 @@ messenger.messageDisplayScripts.register({
|
|||
js: [{ file: "js/mzta-compose-script.js" }]
|
||||
});
|
||||
|
||||
// Inject script in all already open message tabs.
|
||||
let openTabs = await messenger.tabs.query();
|
||||
let messageTabs = openTabs.filter(
|
||||
tab => ["mail", "messageDisplay"].includes(tab.type)
|
||||
);
|
||||
for (let messageTab of messageTabs) {
|
||||
if((messageTab.url == undefined) || (["start.thunderbird.net","about:blank"].some(blockedUrl => messageTab.url.includes(blockedUrl)))) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await browser.tabs.executeScript(messageTab.id, {
|
||||
file: "js/mzta-compose-script.js"
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[ThunderAI] Error injecting message display script:", error);
|
||||
console.error("[ThunderAI] Message tab:", messageTab.url);
|
||||
}
|
||||
}
|
||||
|
||||
browser.contentScripts.register({
|
||||
matches: ["https://*.chatgpt.com/*"],
|
||||
js: [{file: "js/mzta-chatgpt-loader.js"}],
|
||||
|
|
@ -292,14 +274,24 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||
// openChatGPT(message.prompt,message.action,message.tabId);
|
||||
// return true;
|
||||
case 'chatgpt_close':
|
||||
browser.windows.remove(message.window_id).then(() => {
|
||||
taLog.log("ChatGPT window closed successfully.");
|
||||
return true;
|
||||
}).catch((error) => {
|
||||
taLog.error("Error closing ChatGPT window:", error);
|
||||
return false;
|
||||
});
|
||||
break;
|
||||
async function _closeChatGptWindow(window_id) {
|
||||
let prefs_close = await browser.storage.sync.get({chatgpt_win_save_position: prefs_default.chatgpt_win_save_position});
|
||||
if(prefs_close.chatgpt_win_save_position){
|
||||
try {
|
||||
let winInfo = await browser.windows.get(window_id);
|
||||
await browser.storage.sync.set({chatgpt_win_top: winInfo.top, chatgpt_win_left: winInfo.left});
|
||||
taLog.log("Window position saved: top=" + winInfo.top + ", left=" + winInfo.left);
|
||||
} catch(e) {
|
||||
taLog.error("Error saving window position: " + e);
|
||||
}
|
||||
}
|
||||
return browser.windows.remove(window_id).then(() => {
|
||||
taLog.log("ChatGPT window closed successfully.");
|
||||
}).catch((error) => {
|
||||
taLog.error("Error closing ChatGPT window:", error);
|
||||
});
|
||||
}
|
||||
return _closeChatGptWindow(message.window_id);
|
||||
case 'chatgpt_replaceSelectedText':
|
||||
async function _replaceSelectedText(tabId, text) {
|
||||
//console.log('chatgpt_replaceSelectedText: [' + tabId +'] ' + text)
|
||||
|
|
@ -422,10 +414,10 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||
if (sender.tab.type !== 'messageDisplay' && sender.tab.type !== 'mail') return;
|
||||
let message = await browser.messageDisplay.getDisplayedMessage(tabId);
|
||||
if (!message) return;
|
||||
let report = await taSpamReport.loadReportData(message.headerMessageId);
|
||||
let report = await spamReport.loadReportData(message.headerMessageId);
|
||||
if (report) {
|
||||
browser.tabs.sendMessage(tabId, { command: "showSpamReport", data: report });
|
||||
} else if (await taSpamReport.isProcessing(message.headerMessageId)) {
|
||||
} else if (await spamReport.isProcessing(message.headerMessageId)) {
|
||||
browser.tabs.sendMessage(tabId, { command: "showSpamCheckInProgress" });
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -435,7 +427,7 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||
_checkSpamReport(sender.tab.id);
|
||||
break;
|
||||
case 'removeSpamReport':
|
||||
taSpamReport.removeReportData(message.headerMessageId);
|
||||
spamReport.removeReportData(message.headerMessageId);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
@ -601,12 +593,7 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_
|
|||
type: "popup",
|
||||
}
|
||||
|
||||
taLog.log("[chatgpt_web] prefs.chatgpt_win_width: " + prefs.chatgpt_win_width + ", prefs.chatgpt_win_height: " + prefs.chatgpt_win_height);
|
||||
|
||||
if((prefs.chatgpt_win_width != '') && (prefs.chatgpt_win_height != '') && (prefs.chatgpt_win_width != 0) && (prefs.chatgpt_win_height != 0)){
|
||||
win_options.width = prefs.chatgpt_win_width,
|
||||
win_options.height = prefs.chatgpt_win_height
|
||||
}
|
||||
applyWindowPositionAndSize(win_options, prefs);
|
||||
|
||||
const listener = (message, sender, sendResponse) => {
|
||||
async function handleChatGptWeb(createdTab) {
|
||||
|
|
@ -704,12 +691,7 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_
|
|||
type: "popup",
|
||||
}
|
||||
|
||||
taLog.log("[chatgpt_api] prefs.chatgpt_win_width: " + prefs.chatgpt_win_width + ", prefs.chatgpt_win_height: " + prefs.chatgpt_win_height);
|
||||
|
||||
if((prefs.chatgpt_win_width != '') && (prefs.chatgpt_win_height != '') && (prefs.chatgpt_win_width != 0) && (prefs.chatgpt_win_height != 0)){
|
||||
win_options2.width = prefs.chatgpt_win_width,
|
||||
win_options2.height = prefs.chatgpt_win_height
|
||||
}
|
||||
applyWindowPositionAndSize(win_options2, prefs);
|
||||
|
||||
await browser.windows.create(win_options2);
|
||||
}
|
||||
|
|
@ -755,12 +737,7 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_
|
|||
type: "popup",
|
||||
}
|
||||
|
||||
taLog.log("[google_gemini_api] prefs.chatgpt_win_width: " + prefs.chatgpt_win_width + ", prefs.chatgpt_win_height: " + prefs.chatgpt_win_height);
|
||||
|
||||
if((prefs.chatgpt_win_width != '') && (prefs.chatgpt_win_height != '') && (prefs.chatgpt_win_width != 0) && (prefs.chatgpt_win_height != 0)){
|
||||
win_options5.width = prefs.chatgpt_win_width,
|
||||
win_options5.height = prefs.chatgpt_win_height
|
||||
}
|
||||
applyWindowPositionAndSize(win_options5, prefs);
|
||||
|
||||
await browser.windows.create(win_options5);
|
||||
}
|
||||
|
|
@ -813,12 +790,7 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_
|
|||
type: "popup",
|
||||
}
|
||||
|
||||
taLog.log("[ollama_api] prefs.chatgpt_win_width: " + prefs.chatgpt_win_width + ", prefs.chatgpt_win_height: " + prefs.chatgpt_win_height);
|
||||
|
||||
if((prefs.chatgpt_win_width != '') && (prefs.chatgpt_win_height != '') && (prefs.chatgpt_win_width != 0) && (prefs.chatgpt_win_height != 0)){
|
||||
win_options3.width = prefs.chatgpt_win_width,
|
||||
win_options3.height = prefs.chatgpt_win_height
|
||||
}
|
||||
applyWindowPositionAndSize(win_options3, prefs);
|
||||
|
||||
await browser.windows.create(win_options3);
|
||||
|
||||
|
|
@ -866,13 +838,8 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_
|
|||
type: "popup",
|
||||
}
|
||||
|
||||
taLog.log("[openai_comp_api] prefs.chatgpt_win_width: " + prefs.chatgpt_win_width + ", prefs.chatgpt_win_height: " + prefs.chatgpt_win_height);
|
||||
applyWindowPositionAndSize(win_options4, prefs);
|
||||
|
||||
if((prefs.chatgpt_win_width != '') && (prefs.chatgpt_win_height != '') && (prefs.chatgpt_win_width != 0) && (prefs.chatgpt_win_height != 0)){
|
||||
win_options4.width = prefs.chatgpt_win_width,
|
||||
win_options4.height = prefs.chatgpt_win_height
|
||||
}
|
||||
|
||||
await browser.windows.create(win_options4);
|
||||
}
|
||||
break; // openai_comp_api - END
|
||||
|
|
@ -881,13 +848,13 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_
|
|||
{
|
||||
// We are using the Anthropic API
|
||||
|
||||
let rand_call_id5 = '_anthropic_' + generateCallID();
|
||||
let rand_call_id6 = '_anthropic_' + generateCallID();
|
||||
|
||||
const listener5 = (message, sender, sendResponse) => {
|
||||
const listener6 = (message, sender, sendResponse) => {
|
||||
|
||||
function handleAnthropicApi(createdTab) {
|
||||
let mailMessageId5 = -1;
|
||||
if(mailMessage) mailMessageId5 = mailMessage.id;
|
||||
let mailMessageId6 = -1;
|
||||
if(mailMessage) mailMessageId6 = mailMessage.id;
|
||||
|
||||
// check if the config is present, or give a message error
|
||||
if (prefs.anthropic_api_key == '') {
|
||||
|
|
@ -903,32 +870,27 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_
|
|||
return;
|
||||
}
|
||||
//console.log(">>>>>>>>>> sender: " + JSON.stringify(sender));
|
||||
browser.tabs.sendMessage(createdTab.id, { command: "api_send", prompt: promptText, action: action, tabId: curr_tabId, mailMessageId: mailMessageId5, do_custom_text: do_custom_text, prompt_info: prompt_info});
|
||||
browser.tabs.sendMessage(createdTab.id, { command: "api_send", prompt: promptText, action: action, tabId: curr_tabId, mailMessageId: mailMessageId6, do_custom_text: do_custom_text, prompt_info: prompt_info});
|
||||
taLog.log('[OpenAI ChatGPT] Connection succeded!');
|
||||
browser.runtime.onMessage.removeListener(listener5);
|
||||
browser.runtime.onMessage.removeListener(listener6);
|
||||
}
|
||||
|
||||
if (message.command === "anthropic_api_ready_"+rand_call_id5) {
|
||||
if (message.command === "anthropic_api_ready_"+rand_call_id6) {
|
||||
return handleAnthropicApi(sender.tab);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.addListener(listener5);
|
||||
browser.runtime.onMessage.addListener(listener6);
|
||||
|
||||
let win_options5 = {
|
||||
url: browser.runtime.getURL('api_webchat/index.html?llm='+prefs.connection_type+'&call_id='+rand_call_id5+'&ph_def_val='+(prefs.placeholders_use_default_value?'1':'0')+'&prompt_id='+encodeURIComponent(prompt_info.id) + '&prompt_name=' + encodeURIComponent(i18nConditionalGet(prompt_info.name))),
|
||||
let win_options6 = {
|
||||
url: browser.runtime.getURL('api_webchat/index.html?llm='+prefs.connection_type+'&call_id='+rand_call_id6+'&ph_def_val='+(prefs.placeholders_use_default_value?'1':'0')+'&prompt_id='+encodeURIComponent(prompt_info.id) + '&prompt_name=' + encodeURIComponent(i18nConditionalGet(prompt_info.name))),
|
||||
type: "popup",
|
||||
}
|
||||
|
||||
taLog.log("[chatgpt_api] prefs.chatgpt_win_width: " + prefs.chatgpt_win_width + ", prefs.chatgpt_win_height: " + prefs.chatgpt_win_height);
|
||||
applyWindowPositionAndSize(win_options6, prefs);
|
||||
|
||||
if((prefs.chatgpt_win_width != '') && (prefs.chatgpt_win_height != '') && (prefs.chatgpt_win_width != 0) && (prefs.chatgpt_win_height != 0)){
|
||||
win_options5.width = prefs.chatgpt_win_width,
|
||||
win_options5.height = prefs.chatgpt_win_height
|
||||
}
|
||||
|
||||
await browser.windows.create(win_options5);
|
||||
await browser.windows.create(win_options6);
|
||||
}
|
||||
break; // anthropic_api - END
|
||||
|
||||
|
|
@ -944,10 +906,24 @@ function checkScreenDimensions(prefs){
|
|||
|
||||
if(prefs.chatgpt_win_height > height) prefs.chatgpt_win_height = height - 50;
|
||||
if(prefs.chatgpt_win_width > width) prefs.chatgpt_win_width = width - 50;
|
||||
|
||||
|
||||
return prefs;
|
||||
}
|
||||
|
||||
function applyWindowPositionAndSize(win_options, prefs){
|
||||
if((prefs.chatgpt_win_width != '') && (prefs.chatgpt_win_height != '') && (prefs.chatgpt_win_width != 0) && (prefs.chatgpt_win_height != 0)){
|
||||
win_options.width = prefs.chatgpt_win_width;
|
||||
win_options.height = prefs.chatgpt_win_height;
|
||||
taLog.log("Applying saved window dimensions: width=" + prefs.chatgpt_win_width + ", height=" + prefs.chatgpt_win_height);
|
||||
}
|
||||
if((prefs.chatgpt_win_top != '') && (prefs.chatgpt_win_left != '')){
|
||||
win_options.top = prefs.chatgpt_win_top;
|
||||
win_options.left = prefs.chatgpt_win_left;
|
||||
taLog.log("Applying saved window position: top=" + prefs.chatgpt_win_top + ", left=" + prefs.chatgpt_win_left);
|
||||
}
|
||||
return win_options;
|
||||
}
|
||||
|
||||
function doGetSparkFeature(spark_feature_active) {
|
||||
if(spark_feature_active) {
|
||||
return (_sparks_presence == 1);
|
||||
|
|
@ -972,6 +948,7 @@ async function reload_pref_init(){
|
|||
spamfilter_threshold: prefs_default.spamfilter_threshold,
|
||||
spamfilter_show_msg_panel: prefs_default.spamfilter_show_msg_panel,
|
||||
dynamic_menu_force_enter: prefs_default.dynamic_menu_force_enter,
|
||||
chatgpt_win_save_position: prefs_default.chatgpt_win_save_position,
|
||||
...getDynamicSettingsDefaults(['use_specific_integration', 'connection_type'])
|
||||
});
|
||||
_process_incoming = prefs_init.add_tags_auto || prefs_init.spamfilter;
|
||||
|
|
@ -1196,8 +1173,6 @@ const newEmailListener = (folder, messagesList) => {
|
|||
async function _newEmailListener(){
|
||||
let messages = getMessages(messagesList);
|
||||
|
||||
taSpamReport.logger = taLog;
|
||||
|
||||
let add_tags_auto_enabled = prefs_init.add_tags && prefs_init.add_tags_auto;
|
||||
|
||||
await processEmails({
|
||||
|
|
@ -1207,7 +1182,7 @@ const newEmailListener = (folder, messagesList) => {
|
|||
});
|
||||
|
||||
if(prefs_init.spamfilter){
|
||||
taSpamReport.truncReportData();
|
||||
spamReport.truncReportData();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1330,8 +1305,8 @@ async function processEmails(args) {
|
|||
}
|
||||
}
|
||||
|
||||
await taSpamReport.removeReportData(message.headerMessageId);
|
||||
await taSpamReport.setProcessing(message.headerMessageId);
|
||||
await spamReport.removeReportData(message.headerMessageId);
|
||||
await spamReport.setProcessing(message.headerMessageId);
|
||||
|
||||
await updateSpamPanel(message.headerMessageId, "showSpamCheckInProgress");
|
||||
|
||||
|
|
@ -1362,7 +1337,7 @@ async function processEmails(args) {
|
|||
spamfilter_result = (await cmd_spamfilter.sendPrompt()).trim();
|
||||
} catch (err) {
|
||||
console.error("[ThunderAI | SpamFilter] Error getting spamfilter: ", err);
|
||||
let err_data = await taSpamReport.saveError(message.headerMessageId, err.message || String(err));
|
||||
let err_data = await spamReport.saveError(message.headerMessageId, err.message || String(err));
|
||||
await updateSpamPanel(message.headerMessageId, "showSpamReport", err_data);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -1373,7 +1348,7 @@ async function processEmails(args) {
|
|||
jsonObj = extractJsonObject(spamfilter_result);
|
||||
} catch (e) {
|
||||
console.error("[ThunderAI | SpamFilter] Error extracting JSON from AI response: ", e);
|
||||
let err_data = await taSpamReport.saveError(message.headerMessageId, e.message || String(e));
|
||||
let err_data = await spamReport.saveError(message.headerMessageId, e.message || String(e));
|
||||
await updateSpamPanel(message.headerMessageId, "showSpamReport", err_data);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -1399,7 +1374,7 @@ async function processEmails(args) {
|
|||
taLog.log("Marked as spam [" + message.headerMessageId + "]");
|
||||
}
|
||||
|
||||
taSpamReport.saveReportData(report_data, message.headerMessageId);
|
||||
spamReport.saveReportData(report_data, message.headerMessageId);
|
||||
|
||||
// Check if the message is currently displayed and update the banner
|
||||
await updateSpamPanel(message.headerMessageId, "showSpamReport", report_data);
|
||||
|
|
@ -1473,6 +1448,23 @@ async function processEmails(args) {
|
|||
taWorkingStatus.stopWorking();
|
||||
}
|
||||
|
||||
|
||||
|
||||
browser.messages.onNewMailReceived.addListener(newEmailListener, !prefs_init.add_tags_auto_only_inbox);
|
||||
|
||||
// Inject script and CSS in all already open message tabs.
|
||||
let openTabs = await messenger.tabs.query();
|
||||
let messageTabs = openTabs.filter(
|
||||
tab => ["mail", "messageDisplay"].includes(tab.type)
|
||||
);
|
||||
for (let messageTab of messageTabs) {
|
||||
if((messageTab.url == undefined) || (["start.thunderbird.net","about:blank"].some(blockedUrl => messageTab.url.includes(blockedUrl)))) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await browser.tabs.executeScript(messageTab.id, {
|
||||
file: "js/mzta-compose-script.js"
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[ThunderAI] Error injecting message display script:", error);
|
||||
console.error("[ThunderAI] Message tab:", messageTab.url);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,9 @@ export const prefs_default = {
|
|||
do_debug: false,
|
||||
chatgpt_win_height: 800,
|
||||
chatgpt_win_width: 700,
|
||||
chatgpt_win_top: '',
|
||||
chatgpt_win_left: '',
|
||||
chatgpt_win_save_position: false,
|
||||
default_chatgpt_lang: '',
|
||||
default_sign_name: '',
|
||||
reply_type: 'reply_all',
|
||||
|
|
|
|||
|
|
@ -69,11 +69,7 @@ div#miczDescription p{
|
|||
margin-top: 0px;
|
||||
}
|
||||
|
||||
input#chatgpt_win_width{
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
input#chatgpt_win_height{
|
||||
input.win_prop{
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +131,7 @@ div#miczTranslate{
|
|||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#mzta_top_msg div{
|
||||
#mzta_top_msg > td > div{
|
||||
color: #212529;
|
||||
font-style: italic;
|
||||
background-color: #e0e0e0;
|
||||
|
|
@ -158,6 +154,40 @@ div#miczTranslate{
|
|||
border:0 !important;
|
||||
}
|
||||
|
||||
#mzta_top_msg > td:first-child{
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#mzta_top_msg > td:first-child > div{
|
||||
position: absolute;
|
||||
inset: 2px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#mzta_doc_block{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.doc_links{
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.doc_welcome{
|
||||
border-top: 1px solid rgba(0,0,0,0.15);
|
||||
padding-top: 6px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn_small{
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
|
@ -223,7 +253,7 @@ label:has(input[type="checkbox"]) {
|
|||
filter: invert(1);
|
||||
}
|
||||
|
||||
#mzta_top_msg div{
|
||||
#mzta_top_msg > td > div{
|
||||
color:#EEEEEE;
|
||||
background-color: #313131;
|
||||
}
|
||||
|
|
@ -231,11 +261,15 @@ label:has(input[type="checkbox"]) {
|
|||
#mzta_top_msg div a{
|
||||
color: #00ADB5;
|
||||
}
|
||||
|
||||
|
||||
#mzta_top_msg div a:hover{
|
||||
color: #FF5722;
|
||||
}
|
||||
|
||||
.doc_welcome{
|
||||
border-top-color: rgba(255,255,255,0.15);
|
||||
}
|
||||
|
||||
#no_sparks td{
|
||||
background: #3b514f;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,24 @@
|
|||
<div id="miczStatusPage" class="conntype_chatgpt_web"><a href="https://micz.it/thunderbird-addon-thunderai/status/">__MSG_prefs_status_page__</a></div>
|
||||
<table id="miczPrefs">
|
||||
<tr id="mzta_top_msg">
|
||||
<td colspan="2"><div>__MSG_prefs_SurveyLinkText__<br><a href="https://forms.gle/1qK2wcbuhaRzhwyt9">__MSG_prefs_SurveyLinkText2__</a></div></td>
|
||||
<td>
|
||||
<div>
|
||||
__MSG_prefs_SurveyLinkText__<br>
|
||||
<a href="https://forms.gle/1qK2wcbuhaRzhwyt9">__MSG_prefs_SurveyLinkText2__</a>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="mzta_doc_block">
|
||||
<span class="doc_title">__MSG_prefs_doc_title__</span>
|
||||
<div class="doc_links">
|
||||
<a id="link_doc_guides" href="#">__MSG_prefs_doc_setup_guide__</a>
|
||||
<a id="link_doc_tutorial" href="#">__MSG_prefs_doc_custom_prompt_tutorial__</a>
|
||||
</div>
|
||||
<div class="doc_welcome">
|
||||
<a href="#" id="btn_welcome">__MSG_prefs_doc_open_welcome__</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="thstats">
|
||||
<td colspan="2">
|
||||
|
|
@ -28,7 +45,7 @@
|
|||
<td class="nb">
|
||||
<label>
|
||||
<span class="dims_label">__MSG_prefs_OptionText_chatgpt_win_height__</span>
|
||||
<input type="number" id="chatgpt_win_height" name="chatgpt_win_height" class="option-input" />
|
||||
<input type="number" id="chatgpt_win_height" name="chatgpt_win_height" class="option-input win_prop" />
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -36,7 +53,30 @@
|
|||
<td class="nt"><i>__MSG_prefs_OptionText_chatgpt_win_dims_info__</i></td>
|
||||
<td class="nt">
|
||||
<span class="dims_label">__MSG_prefs_OptionText_chatgpt_win_width__</span>
|
||||
<input type="number" id="chatgpt_win_width" name="chatgpt_win_width" class="option-input" />
|
||||
<input type="number" id="chatgpt_win_width" name="chatgpt_win_width" class="option-input win_prop" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nb"><span class="opt_title">__MSG_prefs_OptionText_chatgpt_win_pos_text__</span>
|
||||
<td class="nb">
|
||||
<label>
|
||||
<span class="dims_label">__MSG_prefs_OptionText_chatgpt_win_top__</span>
|
||||
<input type="number" id="chatgpt_win_top" name="chatgpt_win_top" class="option-input win_prop" />
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nt nb"><i>__MSG_prefs_chatgpt_win_position_info__</td>
|
||||
<td class="nt nb">
|
||||
<span class="dims_label">__MSG_prefs_OptionText_chatgpt_win_left__</span>
|
||||
<input type="number" id="chatgpt_win_left" name="chatgpt_win_left" class="option-input win_prop" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nt"> </td>
|
||||
<td class="nt">
|
||||
<input type="checkbox" id="chatgpt_win_save_position" name="chatgpt_win_save_position" class="option-input" />
|
||||
__MSG_prefs_chatgpt_win_save_position__
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ import {
|
|||
getChatGPTWebModelsList_HTML,
|
||||
isAPIKeyValue,
|
||||
getConnectionType,
|
||||
setTomSelectBorder
|
||||
setTomSelectBorder,
|
||||
getMiczItUrl
|
||||
} from '../js/mzta-utils.js';
|
||||
import {
|
||||
injectConnectionUI,
|
||||
|
|
@ -240,6 +241,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
}
|
||||
|
||||
i18n.updateDocument();
|
||||
|
||||
document.getElementById('link_doc_guides').href = getMiczItUrl('thunderbird-addon-thunderai/guides/');
|
||||
document.getElementById('link_doc_tutorial').href = getMiczItUrl('thunderbird-addon-thunderai/tutorial/');
|
||||
|
||||
document.querySelectorAll(".option-input").forEach(element => {
|
||||
element.addEventListener("change", saveOptions);
|
||||
});
|
||||
|
|
@ -359,6 +364,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
|
||||
document.getElementById('reset_max_prompt_length').addEventListener('click', resetMaxPromptLength);
|
||||
|
||||
document.getElementById('btn_welcome').addEventListener('click', async () => {
|
||||
await browser.tabs.create({ url: "../pages/onboarding/onboarding.html" });
|
||||
});
|
||||
|
||||
browser.runtime.getPlatformInfo().then(info => {
|
||||
taLog.log("OS: " + info.os);
|
||||
if ((info.os === "linux")&&(prefs_opt.chatgpt_win_height!=0)&&(prefs_opt.chatgpt_win_width!=0)){
|
||||
|
|
|
|||
|
|
@ -7,7 +7,27 @@
|
|||
<body>
|
||||
<div id="miczBackPrefs"><a href="mzta-options.html">__MSG_backToOptionsText__</a></div>
|
||||
<div id="miczRelNotes"><h1>ThunderAI Release Notes</h1>
|
||||
<h2>Version 4.0.0 - ??/??/2026</h2>
|
||||
<h2>Version 4.1.0 - ??/??/2026</h2>
|
||||
<ul>
|
||||
<li>...</li>
|
||||
</ul>
|
||||
<h2>Version 4.0.3 - 20/03/2026</h2>
|
||||
<ul>
|
||||
<li>Fixed a bug in creating new tags [<a href="https://github.com/micz/ThunderAI/issues/698">#698</a>].</li>
|
||||
</ul>
|
||||
<h2>Version 4.0.2 - 11/03/2026</h2>
|
||||
<ul>
|
||||
<li>Now it's possible to automatically save the AI window position [<a href="https://github.com/micz/ThunderAI/issues/685">#685</a>].</li>
|
||||
<li><i>[OpenAI API]</i> Fix: Correctly showing failed response errors during streaming [<a href="https://github.com/micz/ThunderAI/issues/690">#690</a>].</li>
|
||||
<li>Fix: Correctly adding tags with non-ASCII characters [<a href="https://github.com/micz/ThunderAI/issues/689">#689</a>].</li>
|
||||
<li>Improved the spacing between lines when displaying the AI response in the API webchat [<a href="https://github.com/micz/ThunderAI/issues/686">#686</a>].</li>
|
||||
<li>Some minor improvments.</li>
|
||||
</ul>
|
||||
<h2>Version 4.0.1 - 27/02/2026</h2>
|
||||
<ul>
|
||||
<li>Fix: Correctly handling additional text without a placeholder [<a href="https://github.com/micz/ThunderAI/issues/681">#681</a>].</li>
|
||||
</ul>
|
||||
<h2>Version 4.0.0 - 24/02/2026</h2>
|
||||
<ul>
|
||||
<li>ThunderAI is now compatible only with Thunderbird 140 and later [<a href="https://github.com/micz/ThunderAI/issues/616">#616</a>].</li>
|
||||
<li><i>[All APIs]</i> It's now possibile to define a specific API integration for calendar and task recognition [<a href="https://github.com/micz/ThunderAI/issues/498">#498</a>].</li>
|
||||
|
|
@ -27,7 +47,6 @@
|
|||
<li>Fix: Now it's possibile to use multiple <i>additional_text</i> placeholders in a single prompt, also using custom placeholders [<a href="https://github.com/micz/ThunderAI/issues/554">#554</a>].</li>
|
||||
<li>When using the <i>additional_text</i> placeholder is now possibile to specify an ID that will be shown in the form asking for the text [<a href="https://github.com/micz/ThunderAI/issues/525">#525</a>].</li>
|
||||
<li><i>[ChatGPT Web]</i> Added an option to define a custom time to wait for the page load. Sometimes, on slow PCs, the ChatGPT page loads slowly and ThunderAI inject its content too early. With this option you can adjust the waiting time [<a href="https://github.com/micz/ThunderAI/issues/634">#634</a>].</li>
|
||||
<li>...</li>
|
||||
</ul>
|
||||
<h2>Version 3.8.5 - 22/02/2026</h2>
|
||||
<ul>
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ h2 {
|
|||
background: light-dark(#fff, #27272a);
|
||||
box-shadow: 0px 0px 20px 0px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.section {
|
||||
|
|
@ -216,4 +217,40 @@ h2 {
|
|||
#integration_permission_ok:hover {
|
||||
background-color: darkgreen;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
#onboarding_doc_panel {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 14px 18px;
|
||||
background: light-dark(#fff, #303036);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 2px 16px 0px rgba(0, 0, 0, 0.18);
|
||||
font-size: 15px;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
#onboarding_doc_panel .doc_panel_title {
|
||||
font-weight: bold;
|
||||
color: light-dark(#333, #fff);
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 2px;
|
||||
border-bottom: 1px solid light-dark(rgba(0,0,0,0.1), rgba(255,255,255,0.1));
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
#onboarding_doc_panel a {
|
||||
color: #0a84ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#onboarding_doc_panel a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
@ -13,8 +13,7 @@
|
|||
<link rel="icon" href="../../images/icon-16px.png">
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<div id="chatgpt_web_permission">__MSG_ask_chatgptweb_permission_1__
|
||||
<div id="chatgpt_web_permission">__MSG_ask_chatgptweb_permission_1__
|
||||
<br>__MSG_ask_integration_permission_2__
|
||||
</div>
|
||||
<div id="openai_api_permission">__MSG_ask_openai_api_permission_1__
|
||||
|
|
@ -24,6 +23,12 @@
|
|||
<br>__MSG_ask_integration_permission_2__
|
||||
</div>
|
||||
<div id="integration_permission_ok">__MSG_ask_integration_permission_ok__</div>
|
||||
<div class="content">
|
||||
<div id="onboarding_doc_panel">
|
||||
<span class="doc_panel_title">__MSG_prefs_doc_title__</span>
|
||||
<a id="link_doc_guides" href="#" target="_blank">__MSG_prefs_doc_setup_guide__</a>
|
||||
<a id="link_doc_tutorial" href="#" target="_blank">__MSG_prefs_doc_custom_prompt_tutorial__</a>
|
||||
</div>
|
||||
<div class="section header">
|
||||
<h1><img src="../../images/icon.png" class="title-icon"/>ThunderAI</h1>
|
||||
<p>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
import { taLogger } from '../../js/mzta-logger.js';
|
||||
import { prefs_default } from '../../options/mzta-options-default.js';
|
||||
import { getMiczItUrl } from '../../js/mzta-utils.js';
|
||||
|
||||
let taLog = null;
|
||||
|
||||
|
|
@ -28,6 +29,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
});
|
||||
taLog = new taLogger("mzta-popup",prefs.do_debug);
|
||||
i18n.updateDocument();
|
||||
|
||||
document.getElementById('link_doc_guides').href = getMiczItUrl('thunderbird-addon-thunderai/guides/');
|
||||
document.getElementById('link_doc_tutorial').href = getMiczItUrl('thunderbird-addon-thunderai/tutorial/');
|
||||
if(prefs.connection_type === 'chatgpt_web'){
|
||||
let permission_chatgpt = await messenger.permissions.contains({ origins: ["https://*.chatgpt.com/*"] });
|
||||
if(permission_chatgpt === false){
|
||||
|
|
|
|||
|
|
@ -41,11 +41,15 @@ import {
|
|||
} from "../_lib/connection-ui.js";
|
||||
|
||||
let autocompleteSuggestions = [];
|
||||
let taLog = new taLogger("mzta-spamfilter-page",true);
|
||||
taSpamReport.logger = taLog;
|
||||
let taLog = null;
|
||||
let spamReport = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
let prefs = await browser.storage.sync.get({ do_debug: prefs_default.do_debug });
|
||||
taLog = new taLogger("mzta-spamfilter-page", prefs.do_debug);
|
||||
spamReport = new taSpamReport(prefs.do_debug);
|
||||
|
||||
let specialPrompts = await getSpecialPrompts();
|
||||
let spamfilter_prompt = specialPrompts.find(prompt => prompt.id === 'prompt_spamfilter');
|
||||
|
||||
|
|
@ -162,10 +166,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
}
|
||||
if (selectedAccounts.length === document.querySelectorAll('.accountCheckbox').length) {
|
||||
browser.storage.sync.set({ spamfilter_enabled_accounts: [] });
|
||||
taSpamReport.logger.log("All accounts selected, saving spamfilter_enabled_accounts = [].");
|
||||
taLog.log("All accounts selected, saving spamfilter_enabled_accounts = [].");
|
||||
} else {
|
||||
browser.storage.sync.set({ spamfilter_enabled_accounts: selectedAccounts });
|
||||
taSpamReport.logger.log("Saving spamfilter_enabled_accounts = " + JSON.stringify(selectedAccounts) + ".");
|
||||
taLog.log("Saving spamfilter_enabled_accounts = " + JSON.stringify(selectedAccounts) + ".");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -200,7 +204,7 @@ function check_spamfilter_threshold(event) {
|
|||
}
|
||||
|
||||
async function loadSpamReport(){
|
||||
let report_data = await taSpamReport.getAllReportData();
|
||||
let report_data = await spamReport.getAllReportData();
|
||||
//console.log(">>>>>>>>>>>> loadSpamReport: " + JSON.stringify(report_data));
|
||||
//document.getElementById("report_data").textContent = JSON.stringify(report_data, null, 2);
|
||||
if(report_data == undefined){
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* ThunderAI [https://micz.it/thunderbird-addon-thunderai/]
|
||||
* Copyright (C) 2024 - 2025 Mic (m@micz.it)
|
||||
* Copyright (C) 2024 - 2026 Mic (m@micz.it)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -16,7 +16,10 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { prefs_default, integration_options_config } from '../../options/mzta-options-default.js';
|
||||
import {
|
||||
prefs_default,
|
||||
integration_options_config
|
||||
} from '../../options/mzta-options-default.js';
|
||||
import { taLogger } from "../../js/mzta-logger.js";
|
||||
import {
|
||||
getSpecialPrompts,
|
||||
|
|
@ -28,7 +31,6 @@ import {
|
|||
} from "../../js/mzta-placeholders.js";
|
||||
import { textareaAutocomplete } from "../../js/mzta-placeholders-autocomplete.js";
|
||||
import {
|
||||
getAccountsList,
|
||||
normalizeStringList,
|
||||
isAPIKeyValue
|
||||
} from "../../js/mzta-utils.js";
|
||||
|
|
|
|||
Loading…
Reference in a new issue