Compare commits

..

10 commits

Author SHA1 Message Date
mic
c270586071 logs improved 2024-10-03 19:06:47 +02:00
mic
6dc2638de0 version set to 2.2.0_i145_v4 2024-10-03 19:06:13 +02:00
mic
954e6c55d8 version set to 2.2.0_i145_v3 2024-10-02 23:26:59 +02:00
mic
e702ab9c1a init with api key 2024-10-02 23:26:36 +02:00
mic
e85ecc9e50 using openaicomp apikey 2024-10-02 23:24:46 +02:00
mic
589c8fef5b managing broken chunck 2024-10-02 23:23:57 +02:00
mic
a924f946a5 version set to 2.2.0_i145_v2 2024-10-01 22:48:00 +02:00
mic
2d02bc1bb8 trying to get the missing chunk part 2024-10-01 22:47:39 +02:00
mic
59b3bd5019 printing lines to the log 2024-10-01 21:51:55 +02:00
mic
b5efe247cf version set to 2.2.0_i145_v1 2024-10-01 21:49:24 +02:00
190 changed files with 4074 additions and 51552 deletions

View file

@ -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.yml&title=
[fr]: https://github.com/micz/ThunderAI/issues/new?assignees=&labels=&projects=&template=feature_request.md&title=
- type: textarea
validations:
required: true
@ -36,7 +36,7 @@ body:
attributes:
label: Which version of Thunderbird are you using?
description: >
Thunderbird version like 140.0 or 147.0.1.
Thunderbird version like 115.14.0 or 128.1.
- type: input
id: version
validations:
@ -52,13 +52,9 @@ body:
attributes:
label: Which integration are you using?
options:
- -- Select an option --
- ChatGPT Web Interface
- OpenAI ChatGPT API
- Google Gemini API
- Claude API
- Ollama API
- OpenAI Compatible API
- type: markdown
attributes:
value: |
@ -67,7 +63,7 @@ body:
id: logs
attributes:
label: Anything in the Thunderbird console logs that might be useful?
description: For example, error message, or stack traces. Be sure to have activated the debug in the add-on preferences. Open the console using the keyboard shortcut CTRL+SHIFT+J.
description: For example, error message, or stack traces.
render: txt
- type: textarea
id: additional

View file

@ -0,0 +1,17 @@
---
name: Feature Request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -1,40 +0,0 @@
name: Feature Request
description: Suggest a new idea for ThunderAI
body:
- type: markdown
attributes:
value: |
This issue form is for suggesting new ideas only!
If you need to report a bug, please use the [bug][bg] form.
[bg]: https://github.com/micz/ThunderAI/issues/new?template=bug_report.yml
- type: textarea
validations:
required: true
id: idea
attributes:
label: Your idea
description: >-
Describe the idea you'd like to see implemented in ThunderAI.
Provide a clear and concise description.
- type: markdown
attributes:
value: |
# Details
- type: textarea
id: value
attributes:
label: Value
description: >-
What value does this idea bring to ThunderAI? How does it improve the
user experience or functionality?
- type: textarea
id: additional
attributes:
label: Additional information
description: >
If you have any additional information, use the field below.
Please note, you can attach screenshots or screen recordings here, by
dragging and dropping files in the field below.

View file

@ -1,80 +0,0 @@
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.');
}
})();

View file

@ -1,117 +0,0 @@
name: Mark released issues on close
on:
issues:
types: [closed]
permissions:
contents: read
issues: write
jobs:
mark-released:
# Run only if closed as completed AND has at least one "status:" label
if: ${{ github.event.issue.state_reason == 'completed' && contains(toJSON(github.event.issue.labels), 'status:') }}
runs-on: ubuntu-latest
steps:
- name: Set "released" label when closed completed and was ready for release
uses: actions/github-script@v7
with:
script: |
// Helpers
const owner = context.repo.owner;
const repo = context.repo.repo;
const issue = context.payload.issue;
if (!issue) {
core.info('No issue payload found, skipping.');
return;
}
// Only proceed if closed as "completed" (not "not_planned")
const isCompleted = issue.state_reason === 'completed';
if (!isCompleted) {
core.info('Issue state_reason is not "completed"; skipping.');
return;
}
const getName = (l) => typeof l === 'string' ? l : l.name;
const labels = (issue.labels || []).map(getName);
// Status detection (robust to case/spacing)
const isStatusLabel = (name) => /^status:\s*/i.test(String(name || ''));
const hasAnyStatus = labels.some(isStatusLabel);
if (!hasAnyStatus) {
core.info('Issue has no "status:*" label; skipping.');
return;
}
const READY_REGEX = /^status:\s*ready\s*for\s*release$/i;
const hasReady = labels.some((n) => READY_REGEX.test(String(n || '')));
if (!hasReady) {
core.info('Issue is not "status: ready for release"; nothing to do.');
return;
}
const releasedLabel = 'status: released';
// Ensure "released" label exists
try {
await github.rest.issues.getLabel({ owner, repo, name: releasedLabel });
} catch (e) {
if (e.status === 404) {
core.info(`Label "${releasedLabel}" not found. Creating it.`);
await github.rest.issues.createLabel({
owner,
repo,
name: releasedLabel,
color: '184738', // deep green
description: 'Feature released.'
});
} else {
throw e;
}
}
// Remove all status:* labels, add "status: released"
const filtered = labels.filter((name) => !isStatusLabel(name));
const newLabels = [...new Set([...filtered, releasedLabel])];
await github.rest.issues.update({
owner,
repo,
issue_number: issue.number,
labels: newLabels
});
core.info(`Issue #${issue.number} relabeled as "${releasedLabel}".`);
// === NEW: add release comment with milestone if present ===
const milestone = issue.milestone;
if (!milestone || !milestone.title) {
core.info('Issue has no milestone; no release comment added.');
return;
}
const commentBody = `Released in version ${milestone.title}.`;
// Avoid duplicate comment if the action reruns
const existingComments = await github.paginate(
github.rest.issues.listComments,
{ owner, repo, issue_number: issue.number, per_page: 100 }
);
const alreadyCommented = existingComments.some(c =>
typeof c.body === 'string' && c.body.trim() === commentBody
);
if (alreadyCommented) {
core.info('Release comment already present; skipping comment creation.');
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issue.number,
body: commentBody
});
core.info('Release comment added.');
}

View file

@ -1,75 +0,0 @@
name: Issues Validate Integration Field
on:
issues:
types: [opened, edited]
permissions:
issues: write
jobs:
check-integration:
runs-on: ubuntu-latest
steps:
- name: Validate dropdown selection (only for bug report form)
uses: actions/github-script@v7
with:
script: |
const issue = context.payload.issue;
const bodyText = issue.body || "";
// ---- Guard: only act if this looks like a Bug Report form ----
if (!bodyText.includes("### The problem")) {
core.info("Skipped: This issue does not contain '### The problem', so it is not a Bug Report.");
return;
}
core.info("Detected a Bug Report issue. Proceeding with validation...");
// Extract selection under "Which integration are you using?"
const header = "### Which integration are you using?";
const nextHeaderRegex = /\n### /;
let selection = "";
const startIdx = bodyText.indexOf(header);
if (startIdx !== -1) {
const afterHeader = bodyText.slice(startIdx + header.length).replace(/^\r?\n+/, "");
const nextHeaderIdx = afterHeader.search(nextHeaderRegex);
selection = (nextHeaderIdx === -1 ? afterHeader : afterHeader.slice(0, nextHeaderIdx)).trim();
}
core.info(`Integration field extracted: "${selection}"`);
const isInvalid = !selection || selection.startsWith("-- Select an option");
if (isInvalid) {
core.info("Integration field is invalid or not selected.");
} else {
core.info("Integration field is valid. Nothing to do.");
}
const owner = context.repo.owner;
const repo = context.repo.repo;
const issue_number = issue.number;
// Avoid duplicate comments
const marker = "please specify **Which integration are you using?**";
const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number });
const alreadyCommented = comments.some(c => c.body && c.body.includes(marker));
if (isInvalid) {
if (!alreadyCommented) {
core.info("No existing reminder found. Posting a new comment...");
await github.rest.issues.createComment({
owner, repo, issue_number,
body: [
`👋 Hello @${issue.user.login}!`,
`To complete your bug report, please specify which integration you are using.`,
"",
"Right now the field `**Which integration are you using?**` is set to `-- Select an option --`.",
"",
"Click **Edit** at the top right of the issue, choose a valid option from the dropdown, and save.",
].join("\n")
});
} else {
core.info("Reminder comment already exists. Skipping new comment.");
}
}

View file

@ -1,103 +0,0 @@
name: pre-release comment issues
on:
release:
types: [published]
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
permissions:
contents: read
issues: write
jobs:
comment-milestone-issues:
# allow manual run for testing; in production, only prerelease
if: ${{ github.event.release.prerelease == true }}
runs-on: ubuntu-latest
steps:
- name: Debug context (no checkout)
run: |
echo "Event: ${{ github.event_name }}"
echo "Default branch: ${{ github.event.repository.default_branch }}"
echo "Release tag: ${{ github.event.release.tag_name || 'N/A' }}"
echo "This workflow file lives on MAIN and is executed as-is (no checkout)."
- name: Add comment to issues of matching milestone (filtered by labels)
uses: actions/github-script@v7
with:
script: |
// core, github, context are already available in github-script
await (async () => {
const parsePrereleaseTag = (tag) => {
const m = /^v?(\d+\.\d+\.\d+)pre(\d+)$/.exec(tag ?? "");
return m ? { milestoneTitle: m[1], preNumber: Number(m[2]) } : null;
};
const { owner, repo } = context.repo;
// If this is a manual run, exit with a clear message
if (context.eventName !== 'release') {
core.notice("Manual run: no release payload, nothing to do.");
return;
}
const tagName = context.payload.release?.tag_name;
if (!tagName) { core.notice("No tag_name found on release event."); return; }
const parsed = parsePrereleaseTag(tagName);
if (!parsed) { core.notice(`Tag '${tagName}' does not match vX.Y.ZpreN. Skipping.`); return; }
const milestoneTitle = parsed.milestoneTitle; // es. "3.7.0"
const prereleaseTagDisplay = tagName.startsWith("v") ? tagName : `v${tagName}`;
const commentBody = `This feature is available for testing in the pre-release version [${prereleaseTagDisplay}](https://github.com/micz/ThunderAI/releases).`;
core.info(`(MAIN) Looking for milestone '${milestoneTitle}' in ${owner}/${repo}`);
// Find milestone (open or closed)
const milestones = await github.paginate(
github.rest.issues.listMilestones,
{ owner, repo, state: "all", per_page: 100 }
);
const milestone = milestones.find(m => m.title === milestoneTitle);
if (!milestone) { core.warning(`Milestone '${milestoneTitle}' not found.`); return; }
// All issues in the milestone
const issues = await github.paginate(
github.rest.issues.listForRepo,
{ owner, repo, milestone: milestone.number, state: "all", per_page: 100 }
);
// Only issues (no PRs) with the requested labels
const allowed = new Set(["status: in review", "status: ready for release"]);
const targets = issues.filter(i =>
!i.pull_request &&
Array.isArray(i.labels) &&
i.labels.some(l => allowed.has(l.name))
);
core.info(`Candidate issues: ${targets.length}`);
let commented = 0;
for (const issue of targets) {
const comments = await github.paginate(
github.rest.issues.listComments,
{ owner, repo, issue_number: issue.number, per_page: 100 }
);
const exists = comments.some(c => (c.body || "").trim() === commentBody);
if (exists) {
core.info(`#${issue.number}: comment already present, skip.`);
continue;
}
await github.rest.issues.createComment({
owner, repo, issue_number: issue.number, body: commentBody
});
commented++;
core.info(`#${issue.number}: comment added.`);
}
core.info(`Done. Added comment to ${commented} issue(s).`);
})();

View file

@ -25,4 +25,4 @@ jobs:
days-before-pr-close: -1
remove-stale-when-updated: true
stale-issue-label: "stale"
exempt-issue-labels: "no-stale,new feature,enhancement,bug,docs"
exempt-issue-labels: "no-stale,new feature,enhancement,bug"

View file

@ -1,61 +0,0 @@
name: Check Tom-Select Latest Release
on:
schedule:
- cron: '0 19 * * *' # Check every day at 19:00
workflow_dispatch: # Allows manual triggering for testing purposes
permissions:
contents: read
issues: write
jobs:
check-and-notify:
runs-on: ubuntu-latest
steps:
- name: Get latest release from external repo
id: get_release
run: |
REPO_TARGET="orchidjs/tom-select"
LATEST_TAG=$(curl -s https://api.github.com/repos/$REPO_TARGET/releases/latest | jq -r .tag_name)
if [ "$LATEST_TAG" == "null" ] || [ -z "$LATEST_TAG" ]; then
echo "Error: unable to fetch tag or no release found."
exit 1
fi
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
echo "target=$REPO_TARGET" >> $GITHUB_OUTPUT
- name: Check if Issue already exists and create it
uses: actions/github-script@v7
with:
script: |
const latestTag = "${{ steps.get_release.outputs.tag }}";
const targetRepo = "${{ steps.get_release.outputs.target }}";
const { owner, repo } = context.repo;
const issueTitle = `Update Tom Select to version ${latestTag}`;
// Search through issues (both open and closed for safety)
const response = await github.rest.search.issuesAndPullRequests({
q: `repo:${owner}/${repo} is:issue "${issueTitle}"`,
});
if (response.data.total_count > 0) {
core.info(`The issue for version ${latestTag} already exists. No action taken.`);
} else {
await github.rest.issues.create({
owner,
repo,
title: issueTitle,
labels: ['enhancement'],
body: `Update Tom Select to version **${latestTag}**.
https://cdn.jsdelivr.net/npm/tom-select@${latestTag}/dist/js/tom-select.base.js
https://cdn.jsdelivr.net/npm/tom-select@${latestTag}/dist/css/tom-select.default.min.css
Change the VENDORS.md file to trigger the library update.`
});
core.info(`New issue created for version ${latestTag}.`);
}

View file

@ -1,81 +0,0 @@
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
View file

@ -1,30 +0,0 @@
# 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

View file

@ -3,418 +3,9 @@
<h2>Version 4.1.0 - 13/05/2026</h2>
<ul>
<li>Antispam information are now permanently saved for each message [<a href="https://github.com/micz/ThunderAI/issues/675">#675</a>].</li>
<li><i>[All APIs]</i> A summary has been added above the mail content [<a href="https://github.com/micz/ThunderAI/issues/580">#580</a>].</li>
<li><i>[All APIs]</i> Added inline auto translation for emails [<a href="https://github.com/micz/ThunderAI/issues/247">#247</a>].</li>
<li>Custom menus configuration added. Now it's possibile to define which prompts show in the ThunderAI menu, which ones in the context menu and in which order [<a href="https://github.com/micz/ThunderAI/issues/49">#49</a>, <a href="https://github.com/micz/ThunderAI/issues/184">#184</a>, <a href="https://github.com/micz/ThunderAI/issues/680">#680</a>].</li>
<li>Now the popup menu closes immediatly and the working indicator is in the button icon [<a href="https://github.com/micz/ThunderAI/issues/247">#677</a>].</li>
<li><i>[All APIs]</i> Error messages added also for background operations when the API has not been configured correctly [<a href="https://github.com/micz/ThunderAI/issues/766">#766</a>].</li>
<li><i>[Ollama API]</i> Added <i>format: json</i> option [<a href="https://github.com/micz/ThunderAI/issues/703">#703</a>].</li>
<li>Fix: The "Important Information" section in the options page now updates correctly when choosing an integration [<a href="https://github.com/micz/ThunderAI/issues/730">#730</a>].</li>
<li>In the options page now is visible if a special prompt is using a specific API integration [<a href="https://github.com/micz/ThunderAI/issues/676">#676</a>].</li>
<li>Added an antispam skip list to ensure messages from designated addresses are not forwarded to the AI [<a href="https://github.com/micz/ThunderAI/issues/743">#743</a>].</li>
<li>Fix: Correctly setting the end date for a new calendar event [<a href="https://github.com/micz/ThunderAI/issues/750">#750</a>].</li>
<li>Now it's possibile to use different date and time formats in the AI output when creating a calendar event [<a href="https://github.com/micz/ThunderAI/issues/737">#737</a>].</li>
<li>Added the <i>{%mail_full_headers%}</i> placeholder to retrieve all the email headers at once [<a href="https://github.com/micz/ThunderAI/issues/713">#713</a>].</li>
<li><i>[All APIs]</i> In the API webchat the status messages have different colors [<a href="https://github.com/micz/ThunderAI/issues/3">#3</a>].</li>
<li>Account exclusion lists for add tags and antispam are enforced only for automatic analysis of incoming emails and not for the context menu action that is always executed [<a href="https://github.com/micz/ThunderAI/issues/749">#749</a>].</li>
</ul>
<h2>Version 4.0.7 - 17/04/2026</h2>
<ul>
<li>Fix: Correctly parsing the body of HTML base64 encoded mails [<a href="https://github.com/micz/ThunderAI/issues/757">#757</a>].</li>
</ul>
<h2>Version 4.0.6 - 01/04/2026</h2>
<ul>
<li>Fix: Now it's possibile to create a tag also with accented characters in the label [<a href="https://github.com/micz/ThunderAI/issues/738">#738</a>].</li>
</ul>
<h2>Version 4.0.5 - 27/03/2026</h2>
<ul>
<li>Fix: HTML part of the mail body used in prompt is displayed as HTML code and it is not rendered. This a display fix, there is no change on how the prompt is sent to the AI [<a href="https://github.com/micz/ThunderAI/issues/711">#711</a>].</li>
<li>Fix: HTML elements added by ThunderAI (like the antispam banner) are now not present in HTML or text data placeholders [<a href="https://github.com/micz/ThunderAI/issues/710">#710</a>].</li>
<li><i>[All APIs]</i> The API webchat window now has a dynamic title [<a href="https://github.com/micz/ThunderAI/issues/696">#696</a>]</li>
</ul>
<h2>Version 4.0.4 - 26/03/2026</h2>
<ul>
<li>Fix: Correctly showing the selected model in the special prompt pages.</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>
<li>Added a new model selector with a search functionality to dynamically filter the list [<a href="https://github.com/micz/ThunderAI/issues/603">#603</a>].</li>
<li><i>[All APIs]</i> Added a special prompt to summarize one or more emails, using a context menu command [<a href="https://github.com/micz/ThunderAI/issues/615">#615</a>]. Thanks to <a href="https://github.com/gdkrmr">Guido Kraemer</a> for his great work on this feature.</li>
<li><i>[All APIs]</i> "Analyze for spam" and "Add tags" context menu items are always shown when the corresponding feature is enabled [<a href="https://github.com/micz/ThunderAI/issues/609">#609</a>].</li>
<li><i>[OpenAI Comp API][Ollama API]</i> Asking for the single host for permission to avoid CORS errors, instead of <i>all_urls</i>, as requested by the Thunderbird Review Team [<a href="https://github.com/micz/ThunderAI/issues/524">#524</a>].</li>
<li>Fix: Using also the mail folder owner to search for the right identity to use when composing a reply [<a href="https://github.com/micz/ThunderAI/issues/627">#627</a>].</li>
<li>Context menu items are always ordered alfabetically [<a href="https://github.com/micz/ThunderAI/issues/630">#630</a>].</li>
<li>The prompt export now includes an option to incorporate specific API settings, when present [<a href="https://github.com/micz/ThunderAI/issues/624">#624</a>].</li>
<li>Added the <i>{%mail_text_body_or_selected%}</i> placeholder to retrieve the selected text or the full text body of the email if no selection is present [<a href="https://github.com/micz/ThunderAI/issues/641">#641</a>].</li>
<li>Added the <i>{%mail_html_body_or_selected%}</i> placeholder to retrieve the selected HTML or the full HTML body of the email if no selection is present [<a href="https://github.com/micz/ThunderAI/issues/641">#641</a>].</li>
<li><i>[All APIs]</i> Added an option to get a calendar event without selecting some text, but using the full text body of the email [<a href="https://github.com/micz/ThunderAI/issues/518">#518</a>].</li>
<li><i>[All APIs]</i> Added a new menu item to create a calendar event from the text saved in the clipboard [<a href="https://github.com/micz/ThunderAI/issues/362">#362</a>].</li>
<li>Added a button to copy a prompt in the Custom Prompts page [<a href="https://github.com/micz/ThunderAI/issues/598">#598</a>].</li>
<li><i>[All APIs]</i> Showing the spam filter info at the top of the message. The data is saved only for the session in which the message has been checked for spam [<a href="https://github.com/micz/ThunderAI/issues/506">#506</a>, <a href="https://github.com/micz/ThunderAI/issues/658">#658</a>].</li>
<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>
</ul>
<h2>Version 3.8.5 - 22/02/2026</h2>
<ul>
<li>Fix: Correctly showing email addresses when using mail headers in data placeholders [<a href="https://github.com/micz/ThunderAI/issues/672">#672</a>].</li>
</ul>
<h2>Version 3.8.4 - 10/02/2026</h2>
<ul>
<li><i>[ChatGPT Web]</i> Fix: Correctly importing the selected text into the compose windows also when ChatGPT shows the advanced mail editor in the response [<a href="https://github.com/micz/ThunderAI/issues/646">#646</a>].</li>
</ul>
<h2>Version 3.8.3 - 22/01/2026</h2>
<ul>
<li>Fix: Correctly saving the API settings in new custom prompts [<a href="https://github.com/micz/ThunderAI/issues/623">#623</a>].</li>
<li>Japanese (ja) translation added, thanks to <a href="https://hosted.weblate.org/user/watya1/">Taichi Ito</a>.</li>
</ul>
<h2>Version 3.8.2 - 20/01/2026</h2>
<ul>
<li>Fix: Correctly saving the enabled status in custom prompts [<a href="https://github.com/micz/ThunderAI/issues/621">#621</a>].</li>
</ul>
<h2>Version 3.8.1 - 20/01/2026</h2>
<ul>
<li><i>[OpenAI API]</i> Fix: Correctly sending the prompt after opening the chat window [<a href="https://github.com/micz/ThunderAI/issues/620">#620</a>].</li>
</ul>
<h2>Version 3.8.0 - 16/01/2026</h2>
<ul>
<li><i>[All APIs]</i> Now it is possible to define an API and its settings for any custom prompt. This allows anyone to use different AI providers for different prompts [<a href="https://github.com/micz/ThunderAI/pull/102">#102</a>].</li>
<li><i>[All APIs]</i> When using special prompts (like automatically adding tags or the spam filter) with a specific API integration, all the settings for that integration can be specific. In this way you can use different api keys for the same integration, or different system prompt or temperature [<a href="https://github.com/micz/ThunderAI/pull/590">#590</a>].</li>
<li><i>[All APIs]</i> Added the temperature parameter [<a href="https://github.com/micz/ThunderAI/issues/561">#561</a>].</li>
<li><i>[OpenAI API]</i> Model filtering improved when choosing a model in the options page.</li>
<li><i>[OpenAI API]</i> Now using the new Responses API [<a href="https://github.com/micz/ThunderAI/issues/407">#407</a>].</li>
<li>It is now possible to define a custom placeholder with dynamic data to retrieve any header present in the current email [<a href="https://github.com/micz/ThunderAI/issues/527">#527</a>].</li>
<li><i>[All APIs]</i> The configuration information reported in the webchat API has been improved for all integrations.</li>
<li>Spanish (es) translation added, thanks to <a href="https://hosted.weblate.org/user/gerardo.sobarzo/">Gerardo Sobarzo</a>, <a href="https://hosted.weblate.org/user/arendon/">Andrés Rendón Hernández</a>, <a href="https://hosted.weblate.org/user/ErickLimonG/">Erick Limon</a>.</li>
<li>Swedish (sv) translation added, thanks to <a href="https://hosted.weblate.org/user/Andy_tb/">Andreas Pettersson</a>.</li>
<li>Various fixes.</li>
</ul>
<h2>Version 3.7.9 - 06/01/2026</h2>
<ul>
<li><i>[ChatGPT Web]</i> Fix: Correctly getting the job completion [<a href="https://github.com/micz/ThunderAI/issues/607">#607</a>].</li>
</ul>
<h2>Version 3.7.8 - 18/12/2025</h2>
<ul>
<li>Greek (el) translation added, thanks to <a href="https://github.com/christoskaterini">ChristosK.</a>.</li>
</ul>
<h2>Version 3.7.7 - 14/12/2025</h2>
<ul>
<li><i>[Claude API][OpenAI API]</i> Fix: asking required permissions before fetching models [<a href="https://github.com/micz/ThunderAI/issues/558">#558</a>].</li>
<li><i>[Google Gemini API][Ollama API][OpenAI API][OpenAI Comp API]</i> Fix: improved error handling when parsing responses [<a href="https://github.com/micz/ThunderAI/issues/550">#550</a>].</li>
</ul>
<h2>Version 3.7.6 - 08/12/2025</h2>
<ul>
<li><i>[Claude API]</i> Added the System Prompt configuration option [<a href="https://github.com/micz/ThunderAI/issues/549">#549</a>].</li>
<li><i>[ChatGPT Web]</i> Fix: correctly showing the input field after an update in the HTML page from OpenAI [<a href="https://github.com/micz/ThunderAI/issues/556">#556</a>].</li></li>
</ul>
<h2>Version 3.7.5 - 22/10/2025</h2>
<ul>
<li><i>[OpenAI API]</i> Fixed a bug when handling responses without choices [<a href="https://github.com/micz/ThunderAI/issues/535">#535</a>].</li>
</ul>
<h2>Version 3.7.4 - 20/10/2025</h2>
<ul>
<li><i>[ChatGPT Web]</i> Fixed a bug preventing the ChatGPT web interface from working in new installs [<a href="https://github.com/micz/ThunderAI/issues/534">#534</a>].</li>
</ul>
<h2>Version 3.7.3 - 16/10/2025</h2>
<ul>
<li><i>[ChatGPT Web]</i> Fix: Correctly managing custom projects in any condition [<a href="https://github.com/micz/ThunderAI/issues/520">#520</a>].</li>
<li><i>[OpenAI API]</i> Added an optional permission for the OpenAI API endpoint to avoid a CORS errors [<a href="https://github.com/micz/ThunderAI/issues/529">#529</a>].</li>
</ul>
<h2>Version 3.7.2 - 03/10/2025</h2>
<ul>
<li><i>[ChatGPT Web]</i> Fix: Not showing the force complete hint if the prompt has not been sent.</li>
<li><i>[ChatGPT Web]</i> Fix: Correctly getting when ChatGPT has finished sending the response even when using custom projects.</li>
<li><i>[ChatGPT Web]</i> Fix: Under certain conditions, asking for additional text prevents ThunderAI from sending the prompt to ChatGPT [<a href="https://github.com/micz/ThunderAI/issues/522">#522</a>].</li>
</ul>
<h2>Version 3.7.1 - 26/09/2025</h2>
<ul>
<li><i>[Google Gemini API]</i> Fix: Correctly handling empty responses [<a href="https://github.com/micz/ThunderAI/issues/514">#514</a>].</li>
</ul>
<h2>Version 3.7.0 - 18/09/2025</h2>
<ul>
<li><i>[All APIs]</i> It's now possibile to define a list of tags to be used when autotagging received emails [<a href="https://github.com/micz/ThunderAI/issues/436">#436</a>]. The tags are are now shown in the information header in the AI API chat [<a href="https://github.com/micz/ThunderAI/issues/289">#289</a>].</li>
<li><i>[All APIs]</i> The prompt id and name are now shown in the information header in the AI API chat [<a href="https://github.com/micz/ThunderAI/issues/436">#436</a>].</li>
<li><i>[All APIs]</i> It's now possibile to define a specific API integration for spamfilter and auto tagging [<a href="https://github.com/micz/ThunderAI/issues/438">#438</a>].</li>
<li>Added the <i>{%mail_attachments_info%}</i> placeholder to retrieve the name, type and file size of the mail attachments [<a href="https://github.com/micz/ThunderAI/issues/446">#446</a>].</li>
<li><i>[Google Gemini API]</i> Support for the thinkingBudget parameter has been added [<a href="https://github.com/micz/ThunderAI/issues/494">#494</a>].</li>
<li><i>[OpenAI Comp API]</i> Added DeepSeek configuration [<a href="https://github.com/micz/ThunderAI/issues/486">#486</a>].</li>
<li><i>[ChatGPT Web]</i> Added a message to explain to click on "Force completion" if the ChatGPT job is not done after 7 seconds [<a href="https://github.com/micz/ThunderAI/issues/419">#419</a>].</li>
<li>Anthropic API renamed to Claude API [<a href="https://github.com/micz/ThunderAI/issues/510">#510</a>].</li>
<li>Various code improvements and minor bugs fixed.</li>
</ul>
<h2>Version 3.6.1 - 23/08/2025</h2>
<ul>
<li><i>[OpenAI Comp API]</i> Perplexity configuration fixed [<a href="https://github.com/micz/ThunderAI/issues/461">#461</a>].</li>
<li><i>[OpenAI Comp API]</i> Added a button to clear the models list [<a href="https://github.com/micz/ThunderAI/issues/472">#472</a>].</li>
<li><i>[ChatGPT Web]</i> Fix: Any &lt;br&gt; tag in the response is now replaced with a line break [<a href="https://github.com/micz/ThunderAI/issues/462">#462</a>].</li>
<li><i>[ChatGPT Web]</i> Models list updated. [<a href="https://github.com/micz/ThunderAI/issues/464">#464</a>].</li>
<li><i>[ChatGPT Web]</i> Improved how the line breaks are rendered in the ChatGPT web page [<a href="https://github.com/micz/ThunderAI/issues/482">#482</a>].</li>
<li><i>[All APIs]</i> Fix: If the incoming email has no text only part, the html part is used [<a href="https://github.com/micz/ThunderAI/issues/470">#470</a>].</li>
<li><i>[All APIs]</i> Fix: Improved the extraction of the plain text from html emails [<a href="https://github.com/micz/ThunderAI/issues/469">#469</a>].</li>
<li><i>[All APIs]</i> Fix: Improved how the line breaks are rendered in the AI chat [<a href="https://github.com/micz/ThunderAI/issues/469">#469</a>].</li>
<li><i>[All APIs]</i> If the option to use only existing tags is enabled, this now works also when adding tags manually using the ThunderAI menu [<a href="https://github.com/micz/ThunderAI/issues/475">#475</a>].</li>
<li><i>[All APIs][ChatGPT Web]</i> Fix: Correctly showing and hiding the context menu when changing connection type.</li>
</ul>
<h2>Version 3.6.0 - 30/07/2025</h2>
<ul>
<li>Now it's possibile to define custom data placeholders to be used in custom prompts [<a href="https://github.com/micz/ThunderAI/issues/156">#156</a>].</li>
<li>Improved the handling of HTML and line breaks between the email text and the AI Chat.</li>
<li>When replying, it's now possibile to choose a different reply type (between "all" or "sender only") directly in the AI chat window [<a href="https://github.com/micz/ThunderAI/issues/372">#372</a>].</li>
<li>Added a new default prompt for replying to emails, which asks for a custom command each time it's used [<a href="https://github.com/micz/ThunderAI/issues/444">#444</a>].</li>
<li><i>[All APIs]</i> Added an option to choose to exclude a tag only with an exact match in the excluded words list [<a href="https://github.com/micz/ThunderAI/issues/395">#395</a>].</li>
<li><i>[OpenAI API]</i> Added an option to enable the OpenAI storage for API requests [<a href="https://github.com/micz/ThunderAI/issues/406">#406</a>].</li>
<li><i>[OpenAI API]</i> In the AI chat page, the initial configuration now also displays the storage setting and the "Developer Messages" [<a href="https://github.com/micz/ThunderAI/issues/430">#430</a>].</li>
<li><i>[Ollama API]</i> Added an option to enable the thinking feature [<a href="https://github.com/micz/ThunderAI/issues/398">#398</a>].</li>
<li><i>[Google Gemini API]</i> In the AI chat page, the initial configuration now also displays the "System Instructions". [<a href="https://github.com/micz/ThunderAI/issues/429">#429</a>].</li>
<li><i>[OpenAI Comp API]</i> Handling responses without choices when using RAG [<a href="https://github.com/micz/ThunderAI/issues/416">#416</a>].</li>
<li><i>[OpenAI Comp API]</i> Added Perplexity configuration [<a href="https://github.com/micz/ThunderAI/issues/405">#405</a>].</li>
<li><i>[OpenAI Comp API]</i> Added OpenRouter configuration [<a href="https://github.com/micz/ThunderAI/issues/401">#401</a>].</li>
<li>Traditional Chinese (zh_Hant) translation added, thanks to <a href="https://github.com/evez">evez</a>.</li>
<li>Russian (ru) translation added, thanks to <a href="https://hosted.weblate.org/user/law820314/">Maksim</a>.</li>
<li>Some English typing errors have been fixed [<a href="https://github.com/micz/ThunderAI/issues/422">#422</a>].</li>
</ul>
<h2>Version 3.5.5 - 21/07/2025</h2>
<ul>
<li><i>[All APIs]</i> Fix: Autotag and Antispam filter working again [<a href="https://github.com/micz/ThunderAI/issues/449">#449</a>].</li>
</ul>
<h2>Version 3.5.4 - 13/06/2025</h2>
<ul>
<li><i>[ChatGPT Web]</i> Fix: Changed again how to detect when the response is completed.</li>
<li><i>[ChatGPT Web]</i> Fix: Auto scroll to bottom works again.</li>
</ul>
<h2>Version 3.5.3 - 10/06/2025</h2>
<ul>
<li><i>[ChatGPT Web]</i> Fix: Correctly detecting when the response is completed.</li>
</ul>
<h2>Version 3.5.2 - 05/06/2025</h2>
<ul>
<li><i>[ChatGPT Web]</i> Fix: Correctly hiding the model warning message when forcing to send the prompt [<a href="https://github.com/micz/ThunderAI/issues/410">#410</a>].</li>
<li><i>[ChatGPT Web]</i> Fix: ThunderAI is now working also with a free account [<a href="https://github.com/micz/ThunderAI/issues/408">#408</a>].</li>
<li><i>[ChatGPT Web]</i> Fix: Correctly showing a warning message to a not logged in user [<a href="https://github.com/micz/ThunderAI/issues/411">#411</a>].</li>
<li><i>[ChatGPT Web]</i> Improved the model not found message [<a href="https://github.com/micz/ThunderAI/issues/413">#413</a>].</li>
<li><i>[All APIs]</i> Fix: The selection info message in the API WebChat is shown only when needed [<a href="https://github.com/micz/ThunderAI/issues/412">#412</a>].</li>
<li>Czech (cs) translation updated, thanks to <a href="https://hosted.weblate.org/user/jaroush/">Jaroslav Staněk</a>.</li>
</ul>
<h2>Version 3.5.1 - 31/05/2025</h2>
<ul>
<li>Fix: correctly saving text options [<a href="https://github.com/micz/ThunderAI/issues/400">#400</a>].</li>
</ul>
<h2>Version 3.5.0 - 30/05/2025</h2>
<ul>
<li>Added Anthropic API support [<a href="https://github.com/micz/ThunderAI/issues/349">#349</a>].</li>
<li>Added the <i>{%selected_html%}</i> placeholder to retrieve the HTML portion of the selected text [<a href="https://github.com/micz/ThunderAI/issues/368">#368</a>].</li>
<li><i>[OpenAI Comp API]</i> Added a shortcut to select configurations for known AI services. Currently, <i>Grok AI</i> and <i>Mistral AI</i> are available [<a href="https://github.com/micz/ThunderAI/issues/378">#378</a>]. <a href="https://github.com/micz/ThunderAI/issues/new?template=feature_request.md">Open an issue</a> to request additional services.</li>
<li><i>[All APIs]</i> In the API WebChat is now possibile to select a part of the answer and use only that [<a href="https://github.com/micz/ThunderAI/issues/356">#356</a>].</li>
<li><i>[All APIs]</i> Setting the "Max prompt length" to zero on the options page will disable the length check when sending a prompt to the AI. [<a href="https://github.com/micz/ThunderAI/issues/380">#380</a>].</li>
<li><i>[All APIs]</i> Added a button to the options page to reset the 'Max prompt length' value to its default.</li>
<li><i>[All APIs]</i> Adding tags automatically or with the context menu will now use also tags not created by ThunderAI [<a href="https://github.com/micz/ThunderAI/issues/390">#390</a>].</li>
<li>Fix: in the Spamfilter page the unsaved changes warning is now correctly shown.</li>
<li>Fix: The default keyboard shortcut is no longer enforced at every Thunderbird startup [<a href="https://github.com/micz/ThunderAI/issues/384">#384</a>].</li>
<li>Fix: Incoming email processing now works correctly when auto-tagging is enabled and the full tagging feature is subsequently disabled.</li>
<li>Fix: The reply type is correctly saved in the options. [<a href="https://github.com/micz/ThunderAI/issues/387">#387</a>].</li>
</ul>
<h2>Version 3.4.1 - 12/05/2025</h2>
<ul>
<li><i>[All APIs]</i> Fix: correctly assigning tags when receiving mails. Thanks to <a href="https://github.com/jdkio">jdkio</a> [<a href="https://github.com/micz/ThunderAI/issues/374">#374</a>].</li>
</ul>
<h2>Version 3.4.0 - 07/04/2025</h2>
<ul>
<li><i>[All APIs]</i> Added a special prompt to get tasks data from emails [<a href="https://github.com/micz/ThunderAI/issues/333">#333</a>]. To use this feature, you must install also the <a href="https://addons.thunderbird.net/it/thunderbird/addon/thunderai-sparks/">Sparks</a> add-on.</li>
<li><i>[ChatGPT Web]</i> It is now possible to define a Custom GPT or a Project in the options page to be used by default, or directly in a custom prompt to be used only for that prompt [<a href="https://github.com/micz/ThunderAI/issues/168">#168</a>, <a href="https://github.com/micz/ThunderAI/issues/277">#277</a>].</li>
<li>The chatgpt.com access permission is no more requested when installing the addon. It's mandatory to give this permission to use the ChatGPT Web Interface integration [<a href="https://github.com/micz/ThunderAI/issues/293">#293</a>].</li>
<li>Added the <i>{%mail_quoted_text%}</i> placeholder to get the quoted text when composing a new message [<a href="https://github.com/micz/ThunderAI/issues/324">#324</a>].</li>
<li>Fix: correctly choosing the right account when replying [<a href="https://github.com/micz/ThunderAI/issues/369">#369</a>].</li>
<li>Fix: In the <i>{%mail_typed_text%}</i> placeholder text on different lines is now separated by a space.</li>
<li>Fix: correctly showing text differences when using {%mail_typed_text%} placeholder.</li>
<li>Added a default proofread prompt [<a href="https://github.com/micz/ThunderAI/issues/21">#21</a>].</li>
<li>Added the <i>{%empty%}</i> placeholder to prevent the email body from being automatically appended at the end of the prompt [<a href="https://github.com/micz/ThunderAI/issues/345">#345</a>].</li>
<li>Added a check for the presence of the correct version of ThunderAI Sparks [<a href="https://github.com/micz/ThunderAI/issues/315">#315</a>].</li>
<li><i>[All APIs]</i> Improved the form asking for additional text [<a href="https://github.com/micz/ThunderAI/issues/97">#97</a>].</li>
<li><i>[All APIs]</i> Fix: Ensure HTML body is generated from plain text if no parts are available when processing incoming messages for tags or spam.</li>
<li>Fix a condition when using a prompt from the compose window with a "Do reply" action that is changed in "Substitute Text" [<a href="https://github.com/micz/ThunderAI/issues/353">#353</a>].</li>
<li>Custom Prompts form improved.</li>
<li>Various improvements.</li>
</ul>
<h2>Version 3.3.5 - 22/04/2025</h2>
<ul>
<li>Using a prompt from the compose window with a "Do reply" action is changed in "Substitute Text", asking also to insert text if none is selected [<a href="https://github.com/micz/ThunderAI/issues/353">#353</a>].</li>
<li>Added an option when composing in plain text to remove the extra empty lines [<a href="https://github.com/micz/ThunderAI/issues/350">#350</a>].</li>
<li><i>[Ollama API]</i> It's now possibile to define the default context length (the <i>num_ctx</i> parameter) in the options page [<a href="https://github.com/micz/ThunderAI/issues/351">#351</a>].</li>
<li><i>[ChatGPT Web]</i> Updated the list of available models in the option page.</li>
<li><i>[ChatGPT Web]</i> Opening the ChatGPT webpage from the options now enforce the selected model, if any.</li>
<li><i>[All APIs]</i> Fix: Really correctly getting the message body even in multilevel subpart messages when processing incoming messages for tags or spam [<a href="https://github.com/micz/ThunderAI/issues/335">#335</a>].</li>
<li>Minor improvements.</li>
</ul>
<h2>Version 3.3.4 - 14/04/2025</h2>
<ul>
<li><i>[ChatGPT Web]</i> Fixed a blocking error with the " character in the mail text [<a href="https://github.com/micz/ThunderAI/issues/344">#344</a>].</li>
</ul>
<h2>Version 3.3.3 - 12/04/2025</h2>
<ul>
<li><i>[All APIs]</i> Fix: Correctly getting the message body even in multilevel subpart messages when processing incoming messages for tags or spam [<a href="https://github.com/micz/ThunderAI/issues/335">#335</a>].</li>
<li>Minor improvements.</li>
</ul>
<h2>Version 3.3.2 - 08/04/2025</h2>
<ul>
<li><i>[Ollama API][OpenAI Comp API]</i> Added an info panel to the options page about CORS, along with a button to request the "All URLs" optional permission to avoid potential CORS issues. [<a href="https://github.com/micz/ThunderAI/issues/330">#330</a>, <a href="https://github.com/micz/ThunderAI/issues/331">#331</a>]. Thanks to <a href="https://github.com/jobisoft">John Bieling</a> for the hint.</li>
</ul>
<h2>Version 3.3.1 - 06/04/2025</h2>
<ul>
<li><i>[ChatGPT Web]</i> The "Show diff" button is now shown only when correctly set up [<a href="https://github.com/micz/ThunderAI/issues/321">#321</a>].</li>
<li><i>[ChatGPT Web]</i> The additional text field is now focused [<a href="https://github.com/micz/ThunderAI/issues/323">#323</a>].</li>
<li>Added a warning in the options page for linux users [<a href="https://github.com/micz/ThunderAI/issues/318">#318</a>].</li>
<li><i>[All APIs]</i> Fix: Correctly not showing the "Add tags menu" soon after installation if the option is not checked [<a href="https://github.com/micz/ThunderAI/issues/329">#329</a>].</li>
<li>Czech (cs) translation updated, thanks to <a href="https://hosted.weblate.org/user/jaroush/">Jaroslav Staněk</a>.</li>
</ul>
<h2>Version 3.3.0 - 31/03/2025</h2>
<ul>
<li>Added the <i>{%mail_folder_name%}</i> placeholder to get the mail folder name [<a href="https://github.com/micz/ThunderAI/issues/253">#253</a>].</li>
<li>Added the <i>{%mail_folder_path%}</i> placeholder to get the mail folder path [<a href="https://github.com/micz/ThunderAI/issues/253">#253</a>].</li>
<li>Added the <i>{%account_email_address%}</i> placeholder to get the current account mail address [<a href="https://github.com/micz/ThunderAI/issues/272">#272</a>].</li>
<li><i>[ChatGPT Web][All APIs]</i> Added a diff viewer to compare the old and new text. This feature could be activated at prompt level, and it's useful for "rewrite" prompts [<a href="https://github.com/micz/ThunderAI/issues/109">#109</a>].</li>
<li><i>[All APIs]</i> Added a context menu to automatically add tags and run the spam filter on selected messages. [<a href="https://github.com/micz/ThunderAI/issues/262">#262</a>].</li>
<li><i>[All APIs]</i> It's now possibile to define a timezone in the calendar event settings page [<a href="https://github.com/micz/ThunderAI/issues/250">#250</a>].</li>
<li><i>[All APIs]</i> It's now possibile to add the attendees in the calendar event, be sure to update the prompt in the settings [<a href="https://github.com/micz/ThunderAI/issues/258">#258</a>].</li>
<li>The button icon now shows a loading indicator when ThunderAI is performing an operation [<a href="https://github.com/micz/ThunderAI/issues/295">#295</a>].</li>
<li>Improved the handling of null or undefined placeholders [<a href="https://github.com/micz/ThunderAI/issues/288">#288</a>].</li>
<li>Czech (cs) translation added, thanks to <a href="https://hosted.weblate.org/user/jaroush/">Jaroslav Staněk</a> and <a href="https://hosted.weblate.org/user/Fjuro/">Fjuro</a>.</li>
<li>Simplified Chinese (zh_Hans) translation added, thanks to <a href="https://github.com/jeklau">jeklau</a>.</li>
<li>Some old strings are now translated in the API webchat [<a href="https://github.com/micz/ThunderAI/issues/298">#298</a>].</li>
</ul>
<h2>Version 3.2.3 - 06/03/2025</h2>
<ul>
<li><i>[ChatGPT Web]</i> Fixed hiding the Spam Filter options [<a href="https://github.com/micz/ThunderAI/issues/284">#284</a>].</li>
<li>Unchecking the "Add tags" and "Spam Filter" options if the user revokes the related optional permissions [<a href="https://github.com/micz/ThunderAI/issues/286">#286</a>].</li>
<li>The permission to 'List message tags' is now mandatory; otherwise, the tags-related placeholder won't be usable. The permission to modify tags is optional and required to tag emails.</li>
<li>Various improvements.</li>
</ul>
<h2>Version 3.2.2 - 05/03/2025</h2>
<ul>
<li>Fixed compatibility with Thunderbird 115 [<a href="https://github.com/micz/ThunderAI/issues/281">#281</a>].</li>
<li>Fixed a bug with the optional permissions not retained at startup [<a href="https://github.com/micz/ThunderAI/issues/279">#279</a>]</li>
<li>Fixed a race condition in saving the report data of the Spam Filter.</li>
</ul>
<h2>Version 3.2.1 - 03/03/2025</h2>
<ul>
<li>Fix calling a prompt without giving tags related permissions [<a href="https://github.com/micz/ThunderAI/issues/275">#275</a>].</li>
<li>Deactivating the add tags feature if the related permissions are revoked [<a href="https://github.com/micz/ThunderAI/issues/276">#276</a>].</li>
<li>Minor bugs fixed.</li>
</ul>
<h2>Version 3.2.0 - 27/02/2025</h2>
<ul>
<li><i>[All APIs]</i> Added an option to automatically tag incoming emails [<a href="https://github.com/micz/ThunderAI/issues/237">#237</a>].</li>
<li><i>[All APIs]</i> Added an configurable antispam filter for incoming emails [<a href="https://github.com/micz/ThunderAI/issues/231">#231</a>].</li>
<li>Tags related permissions are now optional and asked for only when the user activates the tags feature [<a href="https://github.com/micz/ThunderAI/issues/259">#259</a>].</li>
<li>Added the <i>{%thunderai_def_sign%}</i> placeholder to get the default signature as defined in the options [<a href="https://github.com/micz/ThunderAI/issues/248">#248</a>].</li>
<li>Added the <i>{%thunderai_def_lang%}</i> placeholder to get the default language as defined in the options [<a href="https://github.com/micz/ThunderAI/issues/248">#248</a>].</li>
<li>Croatian (hr) translation added, thanks to Petar Jedvaj.</li>
<li>German (de) translation errors fixed.</li>
<li>Translations improved thanks to Hosted Weblate. <a href="https://micz.it/thunderbird-addon-thunderai/translate/">Help translating ThunderAI!</a>.</li>
<li>Various minor improvements.</li>
</ul>
<h2>Version 3.1.3 - 02/02/2025</h2>
<ul>
<li><i>[ChatGPT Web]</i> Correctly hiding the "Download Sparks" message when using [<a href="https://github.com/micz/ThunderAI/issues/245">#245</a>].</li>
<li><i>[ChatGPT Web]</i> Added support to autochoose the new <i>o1</i>, <i>o3-mini</i> and <i>o3-mini-high</i> models [<a href="https://github.com/micz/ThunderAI/issues/244">#244</a>].</li>
</ul>
<h2>Version 3.1.2 - 29/01/2025</h2>
<ul>
<li>Forcing ThunderAI menu reload when installing Sparks [<a href="https://github.com/micz/ThunderAI/issues/240">#240</a>].</li>
<li>Fixed a bug in translating special prompts.</li>
</ul>
<h2>Version 3.1.1 - 29/01/2025</h2>
<ul>
<li>Polish (pl) translation improved, thanks to <a href="https://github.com/neexpl">neexpl</a>.</li>
<li>Fixed the text in the add calendar event settings page.</li>
</ul>
<h2>Version 3.1.0 - 27/01/2025</h2>
<ul>
<li><i>[All APIs]</i> Added a special prompt to get calendar events data from emails [<a href="https://github.com/micz/ThunderAI/issues/182">#182</a>]. To use this feature, you must install also the <a href="https://addons.thunderbird.net/it/thunderbird/addon/thunderai-sparks/">Sparks</a> add-on.</li>
<li>Added Google Gemini API support [<a href="https://github.com/micz/ThunderAI/issues/204">#204</a>, <a href="https://github.com/micz/ThunderAI/issues/217">#217</a>].</li>
<li>Added the <i>{%mail_typed_text%}</i> placeholder to get the text inserted before the quoted mail body when replying [<a href="https://github.com/micz/ThunderAI/issues/196">#196</a>].</li>
<li>Using the <i>{%mail_typed_text%}</i> placeholder the typed text inserted before the quoted mail body will be selected automatically to be replaced afterwards with the AI response [<a href="https://github.com/micz/ThunderAI/issues/229">#229</a>].</li>
<li>Added the <i>{%mail_datetime%}</i> placeholder to get the date and time of the email [<a href="https://github.com/micz/ThunderAI/issues/223">#223</a>].</li>
<li>Added the <i>{%current_datetime%}</i> data placeholder to get the current date and time [<a href="https://github.com/micz/ThunderAI/issues/224">#224</a>].</li>
<li>Added an info text about using the new <i>{%tags_full_list%}</i> placeholder in the "Add Tags Prompt" page [<a href="https://github.com/micz/ThunderAI/issues/215">#215</a>].</li>
</ul>
<h2>Version 3.0.0 - 05/01/2025</h2>
<ul>
<li><i>[All APIs]</i> Added a special prompt to apply tags to emails [<a href="https://github.com/micz/ThunderAI/issues/183">#183</a>].</li>
<li>Default prompts text has been translated [<a href="https://github.com/micz/ThunderAI/issues/185">#185</a>].</li>
<li>Added the <i>{%tags_full_list%}</i> data placeholder for the full available tags list added [<a href="https://github.com/micz/ThunderAI/issues/197">#197</a>].</li>
<li>Added the <i>{%tags_current_email%}</i> data placeholder for the single mail tags list added [<a href="https://github.com/micz/ThunderAI/issues/198">#198</a>].</li>
<li>User <a href="https://forms.gle/1qK2wcbuhaRzhwyt9">survey link</a> added [<a href="https://github.com/micz/ThunderAI/issues/202">#202</a>].</li>
<li><i>[OpenAI Comp API]</i> Added a button in the options page to manually insert the model [<a href="https://github.com/micz/ThunderAI/issues/205">#205</a>].</li>
<li>Polish (pl) translation added, thanks to <a href="https://github.com/neexpl">neexpl</a>.</li>
<li>The Custom Prompts tab has now an icon.</li>
<li>The red border in the Custom Prompts configuration page that highlights a needed prompt configuration that is not selected is now removed when the corresponding placeholder is removed [<a href="https://github.com/micz/ThunderAI/issues/201">#201</a>].</li>
<li>Minor bugs fixed.</li>
</ul>
<h2>Version 2.3.4 - 06/12/2024</h2>
<ul>
<li><i>[ChatGPT API]</i> Correctly showwing an error message received from the ChatGPT API [<a href="https://github.com/micz/ThunderAI/issues/191">#191</a>].</li>
</ul>
<h2>Version 2.3.3 - 29/11/2024</h2>
<ul>
<li><i>[ChatGPT Web]</i> Correctly sending the prompt even if the audio button is present on the web interface [<a href="https://github.com/micz/ThunderAI/issues/188">#188</a>].</li>
<li><i>[ChatGPT Web]</i> The input field is always visibile [<a href="https://github.com/micz/ThunderAI/issues/189">#189</a>].</li>
</ul>
<h2>Version 2.3.2 - 31/10/2024</h2>
<ul>
<li><i>[ChatGPT Web]</i> Clicking on one of the allowed model values in the options page, sets the corresponding field value.</li>
<li><i>[ChatGPT Web]</i> Debug log improved.</li>
<li><i>[ChatGPT Web]</i> Improved the default value for model enforcing [<a href="https://github.com/micz/ThunderAI/issues/176">#176</a>].</li>
</ul>
<h2>Version 2.3.1 - 24/10/2024</h2>
<ul>
<li>Reverted the position of the action button in the compose window [<a href="https://github.com/micz/ThunderAI/issues/175">#175</a>].</li>
</ul>
<h2>Version 2.3.0 - 23/10/2024</h2>
<ul>
<li>The action button in the compose window has been moved to the formatting toolbar [<a href="https://github.com/micz/ThunderAI/issues/173">#173</a>].</li>
<li>On the custom prompts page, when editing a prompt, pressing the cancel button will revert any modified values in the form to their saved state.</li>
<li>Implemented placeholders to add additional data to prompts [<a href="https://micz.it/thunderbird-addon-thunderai/data-placeholders/">More info</a>] [<a href="https://github.com/micz/ThunderAI/issues/146">#146</a>, <a href="https://github.com/micz/ThunderAI/issues/153">#153</a>].</li>
<li>Text improved for the "Reply to this" prompt.</li>
<li>Added an option to set the maximum number of characters in the prompt [<a href="https://github.com/micz/ThunderAI/issues/165">#165</a>].</li>
<li>Added a workaround to show an alert message when needed [<a href="https://github.com/micz/ThunderAI/issues/166">#166</a>].</li>
<li><i>[ChatGPT Web]</i> Added an option to set the model to use [<a href="https://github.com/micz/ThunderAI/issues/171">#171</a>].</li>
<li><i>[ChatGPT Web]</i> Added an option to use the temporary chat [<a href="https://github.com/micz/ThunderAI/issues/169">#169</a>].</li>
<li>A few typos fixed [<a href="https://github.com/micz/ThunderAI/issues/170">#170</a>].</li>
<li>Added a loading indicator in the menu when sending a prompt.</li>
<li>Added a new default prompt that uses the placeholders and allows replying to an email thread. It requires selecting the text of the first email to identify which is the one to reply to and the others in the thread [<a href="https://github.com/micz/ThunderAI/issues/150">#150</a>].</li>
</ul>
<h2>Version 2.2.2 - 15/10/2024</h2>
<ul>
<li>APIs error handling improved.</li>
<li><i>[OpenAI Comp API]</i> Added an option to remove the <i>v1</i> segment in the API calls path [<a href="https://github.com/micz/ThunderAI/issues/161">#161</a>].</li>
</ul>
<h2>Version 2.2.1 - 11/10/2024</h2>
<ul>
<li><i>[ChatGPT Web]</i> Added a workaround to login in ChatGPT. See the new button in the options page.</li>
<li>Brazilian Portuguese (pt-br) translation updated, thanks to Bruno Pereira de Souza.</li>
</ul>
<h2>Version 2.2.0 - 07/10/2024</h2>
<h2>Version 2.2.0 - ??/??/2024</h2>
<ul>
<li><i>[ChatGPT Web]</i> Removed the option to force the ChatGPT4 model over ChatGPT3.5, since it was useless now.</li>
<li><i>[ChatGPT Web]</i> Minor internal improvements.</li>
@ -428,13 +19,7 @@
<li>Fixed a bug that occurred when importing prompts after an export without closing the Custom Prompts page.</li>
<li>Brazilian Portuguese (pt-br) translation added, thanks to Bruno Pereira de Souza.</li>
<li><i>[Ollama API]</i> Added a link to the <a href="https://micz.it/thunderbird-addon-thunderai/ollama-cors-information/">CORS information page</a>.</li>
<li><i>[All APIs]</i> Improved handling of streaming responses even in case of broken chunks [<a href="https://github.com/micz/ThunderAI/issues/147">#147</a>].</li>
<li>Fixed a race condition that occurred when opening the chat window under certain circumstances. Thanks to <a href="https://github.com/jobisoft" target="_blank">@jobisoft</a> for helping refine the code, and to <a href="https://github.com/Mikilio" target="_blank">@Mikilio</a> and <a href="https://github.com/mattcaron" target="_blank">@mattcaron</a> for the extensive testing [<a href="https://github.com/micz/ThunderAI/issues/143">#143</a>].</li>
</ul>
<h2>Version 2.1.5 - 23/09/2024</h2>
<ul>
<li><i>[ChatGPT Web]</i> Working again in Thunderbird 115. Implemented a workaround for <i>Intl.Segmenter</i> [<a href="https://github.com/micz/ThunderAI/issues/139">#139</a>].</li>
</ul>
<h2>Version 2.1.4 - 11/09/2024</h2>
<ul>
<li><i>[ChatGPT API][Ollama API]</i> Fixed the colors of the light theme in the chat window status logger.</li>
@ -491,7 +76,7 @@
<li>Added a better error message when there is an error fetching models.</li>
<li>When selecting a correct model in the options page, the field is no more highlighted in red [<a href="https://github.com/micz/ThunderAI/issues/100">#100</a>].</li>
<li>When using the ChatGPT API, the double quotes at the beginning and end of the response are removed [<a href="https://github.com/micz/ThunderAI/issues/99">#99</a>].</li>
<li><i>[ChatGPT Web]</i> "Keep formatting" option removed.</li>
<li><i>[ChatGPT Web]</i>"Keep formatting" option removed.</li>
</ul>
<h2>Version 2.0.1 - 09/08/2024</h2>
<ul>

View file

@ -1,65 +0,0 @@
# ThunderAI - Claude Code Guide
## Project Overview
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.
- **Extension ID:** `thunderai@micz.it`
- **Min Thunderbird:** 140.0+
- **Language:** Plain ES6+ JavaScript modules — no build tools, no transpilation, no npm
- **License:** GPLv3
## Key Rules
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.
## Directory Map
```
/
├── 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)

15
LANG.md
View file

@ -1,15 +0,0 @@
cs
de
el
en
es
fr
hr
it
ja
pl
pt-br
ru
sv
zh_Hans
zh_Hant

123
README.md
View file

@ -1,74 +1,38 @@
# ![ThunderAI icon](images/icon-32px.png "ThunderAI") ThunderAI
ThunderAI is a Thunderbird Addon that uses the capabilities of ChatGPT, Google Gemini, Claude or Ollama to enhance email management.
ThunderAI is a Thunderbird Addon that uses the capabilities of ChatGPT or Ollama to enhance email management.
It enables users to analyse, write, correct, assign tags, create calendar events or tasks and optimize their emails, facilitating more effective and professional communication.
It enables users to analyse, write, correct, and optimize their emails, facilitating more effective and professional communication.
ThunderAI is a tool for anyone looking to improve their email quality, both in content and grammar, making the writing process quicker and more intuitive.
You can also define, export and import your own **[custom prompts](https://micz.it/thunderbird-addon-thunderai/custom-prompts/)**!
You can also define, export and import your own **custom prompts**!
Find out how [here](https://micz.it/thunderbird-addon-thunderai/custom-prompts/)!
In any custom prompt you can use additional **[data placeholders](https://micz.it/thunderbird-addon-thunderai/data-placeholders/)**!
Using an API integration, you can activate some automatic features:
- Tagging incoming emails
- Moving spam emails to the junk folder
> [!TIP]
> **Using ChatGPT**
>
> There is no need for an API key and is possibile to use this extension even with a free account, using the web interface!
>
> Starting from version 2.0.0, if you want to connect with the OpenAI API integration, now you can use an API Key!
<br>
> [!NOTE]
> **Available Integrations**
> - **ChatGPT Web**
> - There is no need for an API key!
> - You can use a free account!
> [!TIP]
> **Using Ollama**
>
> <br>
> From version 2.1.0 is possible to use a local Ollama server!
>
> - **OpenAI API**
> - Connect directly to ChatGPT using your API key.
>
> <br>
>
> - **Google Gemini**
> - You can use also the _System Instructions_ and _thinkingBudget_ options if needed.
>
>
> <br>
>
> - **Claude API**
> - You need to grant the permission "_Access your data for sites in the https://anthropic.com domain_" to use the Claude API.
>
>
> <br>
>
> - **Using Ollama**
> - Just remember to add `OLLAMA_ORIGINS = moz-extension://*` to the Ollama server environment variables.
> - [More info about CORS](https://micz.it/thunderbird-addon-thunderai/ollama-cors-information/)
>
> <br>
>
> - **OpenAI Compatible API**
> - You can also use a local OpenAI Compatible API server, like LM Studio or Mistral AI!
> - There is also an option to remove the "v1" segment from the API url, if needed, and to manually set the model name if the server doesn't have a models list endpoint.
> - You can also use one of these predefined configurations:
> - DeepSeek API
> - Grok API
> - Mistral API
> - OpenRouter API
> - Perplexity API
> Just remember to add `OLLAMA_ORIGINS = moz-extension://*` to the Ollama server environment variables.
<br>
## Documentation
> [!TIP]
> **Using an OpenAI Compatible API**
>
> From version 2.2.0 is possible to use a local OpenAI Compatible API server, like LM Studio!
[Setup Guides](https://micz.it/thunderbird-addon-thunderai/guides/) - Step-by-step guides to connect ThunderAI to the AI backend of your choice, from ChatGPT to local models with Ollama.
[Custom Prompt Tutorial](https://micz.it/thunderbird-addon-thunderai/tutorial/) - Learn how to build your first custom prompt from scratch, combining placeholders and user input to automate your email replies.
[ThunderAI Prompt Architect](https://chatgpt.com/g/g-69b6b11c89b88191a6798be6e97025f1-thunder-ai-prompt-architect) - Let ChatGPT help you crafting your custom prompts. Thanks to [Paweł](https://github.com/PawelKinczyk) for this tool!
<br>
@ -77,8 +41,6 @@ Do you want to help translate this addon?
[Find out how!](https://micz.it/thunderbird-addon-thunderai/translate/)
<br>
## Changelog
@ -100,46 +62,25 @@ Are you using this addon in your Thunderbird?
## Attributions
### Translations
- 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">
- English (en-US): [Mic](https://github.com/micz/)
- French (fr): Generated automatically
- German (de): Generated automatically
- Italian (it): [Mic](https://github.com/micz/)
- Português Brasileiro (pt-br): Bruno Pereira de Souza
<br>
Do you want to help translate this addon? [Find out how!](https://micz.it/thunderbird-addon-thunderai/translate/) <br>
_The language status represents the percentage of translated strings in the latest stable release._
### Miscellaneous
- <a href="https://github.com/KudoAI/chatgpt.js">chatgpt.js</a> for providing methods to interact with the ChatGTP frontend
- <a href="https://github.com/ali-raheem/Aify">Aify</a> for inspiration
- <a href="https://github.com/boxabirds">Julian Harris</a> for his project <a href="https://github.com/boxabirds/chatgpt-frontend-nobuild">chatgpt-frontend-nobuild</a>, that has been used as a starting point for the API Web Interface
<br>
### Graphics
- ChatGPT-4 for the help with the addon icon ;-)
- <a href="https://loading.io">loading.io</a> for the loading SVGs
- Addon icon thanks for the help to ChatGPT-4 ;-)
- <a href="https://loading.io">loading.io</a> for the dynamic menu loading SVG
- [Fluent Design System](https://www.iconfinder.com/fluent-designsystem) for the Custom Prompts table sorting icons
- [JessiGue](https://www.flaticon.com/authors/jessigue) for the show/hide icon for api key fields
- [Iconka.com](https://www.iconarchive.com/artist/iconka.html) for the autotag context menu icon
- [Icojam](https://www.iconarchive.com/artist/icojam.html) for the spam filter context menu icon
- [Roundicons](https://www.flaticon.com/authors/roundicons) for the summarize context menu icon
- [HideMau](https://www.flaticon.com/authors/hidemaru) for the ai summarize icon
- [Hilmy Abiyyu A.](https://www.flaticon.com/authors/hilmy-abiyyu-a) for the ai translate and context menu icons
- [bearicons](https://www.flaticon.com/authors/bearicons) for the empty context menu icon
- [meaicon](https://www.flaticon.com/authors/meaicon) for the add task context menu icon
<br>
### Miscellaneous
- <a href="https://github.com/KudoAI/chatgpt.js">chatgpt.js</a> for providing methods to interact with the ChatGPT web frontend
- <a href="https://github.com/boxabirds">Julian Harris</a> for his project <a href="https://github.com/boxabirds/chatgpt-frontend-nobuild">chatgpt-frontend-nobuild</a>, that has been used as a starting point for the API Web Interface
- <a href="https://hosted.weblate.org/widgets/thunderai/">Hosted Weblate</a> for managing the localization
- [JessiGue](https://www.flaticon.com/authors/jessigue) for the show/hide api key field

View file

@ -1,11 +0,0 @@
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@v2.6.1/dist/js/tom-select.base.js
file: pages\_lib\tom-select.default.min.css
source: https://cdn.jsdelivr.net/npm/tom-select@v2.6.1/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

View file

@ -1,200 +0,0 @@
{
"extensionDescription": {
"message": "Използвайте ChatGPT, Google Gemini, Claude или Ollama, за да подобрите вашите имейли!"
},
"menu_title": {
"message": "ИИ"
},
"prompt_lang": {
"message": "Отговорено за"
},
"prompt_reply": {
"message": "Отговор на този имейл"
},
"prompt_reply_advanced": {
"message": "Отговор към тази нишка"
},
"prompt_reply_custom_command": {
"message": "Отговаряне с команда"
},
"prompt_rewrite_polite": {
"message": "Пренаписване по-учтиво"
},
"prompt_rewrite_formal": {
"message": "Пренаписване по-формално"
},
"prompt_classify": {
"message": "Класифициране"
},
"prompt_translate_this": {
"message": "Превеждане"
},
"prompt_this": {
"message": "Поискайте това"
},
"prompt_selection_needed": {
"message": "Селектирайте текст, за да продължите!"
},
"customPrompts_managePrompts": {
"message": "Управление на подкани"
},
"more_info_string": {
"message": "Повече информация"
},
"customPrompts_managePrompts_info_default": {
"message": "Подканите по подразбиране не са редактируеми. Можете да ги изключите, да копирате текста им и да го поставите в нова, модифицирана версия."
},
"customPrompts_managePrompts_info_default_2": {
"message": "Можете да внесете и изнесете подканите. Съществуващите подкани със същото ID ще бъдат презаписани. Подканите с нови ID-та ще бъдат добавени."
},
"customPrompts_managePrompts_info_default_3": {
"message": "Ако всичко е правилно, след внасянето натиснете бутона \"Запази всичко\"."
},
"customPrompts_start_saving": {
"message": "Запазване на подканите..."
},
"customPrompts_reindexing_list": {
"message": "Преиндексиране на списъка..."
},
"customPrompts_filtering_prompts": {
"message": "Филтриране на подканите..."
},
"customPrompts_saving_default_prompts": {
"message": "Запазване на подкани по подразбиране..."
},
"customPrompts_saving_custom_prompts": {
"message": "Запазване на персонализирани подкани..."
},
"customPrompts_reloading_menus": {
"message": "Презареждане на менюта..."
},
"customPrompts_saved": {
"message": "Подканите са запазени!"
},
"customPrompts_form_label_ID": {
"message": "ID"
},
"customPrompts_form_label_ID_rules": {
"message": "Трябва да е уникално, с малки букви и без разстояния"
},
"customPrompts_form_label_Name": {
"message": "Име"
},
"customPrompts_form_label_Text": {
"message": "Текст на подкана"
},
"customPrompts_form_label_Action": {
"message": "Действие"
},
"customPrompts_form_label_need_selected": {
"message": "Необходима е селекция на текст"
},
"customPrompts_form_label_need_signature": {
"message": "Винаги добавяй подпис"
},
"customPrompts_form_label_need_custom_text": {
"message": "Поискай допълнителен текст"
},
"customPrompts_form_label_enabled": {
"message": "Разрешено"
},
"customPrompts_form_label_use_diff_viewer": {
"message": "Активиране на инструмента за сравнение на текст"
},
"customPrompts_form_label_use_diff_viewer_title": {
"message": "Инструментът за сравнение на текст може да бъде избран, когато действието е зададено на „Замести текст“."
},
"customPrompts_form_required_fields": {
"message": "Задължително поле"
},
"customPrompts_btnEdit": {
"message": "Редакция"
},
"customPrompts_btnCancel": {
"message": "Откажи"
},
"customPrompts_btnOK": {
"message": "ОК"
},
"customPrompts_btnDelete": {
"message": "Изтрий"
},
"customPrompts_btnDelete_confirmText": {
"message": "Сигурни ли сте, че искате да изтриете този елемент?"
},
"customPrompts_unsaved_changes": {
"message": "Има незапазени промени!"
},
"btnSaveAll_string": {
"message": "Запази всичко"
},
"btnNew_string": {
"message": "Добави нов"
},
"customPrompts_btnAddNewCommit": {
"message": "Добави подканата"
},
"customPrompts_add_to_menu": {
"message": "Добави към меню"
},
"customPrompts_add_to_menu_always": {
"message": "Винаги"
},
"customPrompts_add_to_menu_reading": {
"message": "Четене на писмо"
},
"customPrompts_add_to_menu_composing": {
"message": "Писане на писмо"
},
"customPrompts_close_button": {
"message": "Бутон за изключване"
},
"customPrompts_do_reply": {
"message": "Отговори"
},
"customPrompts_substitute_text": {
"message": "Замени тескт"
},
"chatgpt_win_working": {
"message": "В процес на работа..."
},
"chatgpt_win_job_completed": {
"message": "Завършено!"
},
"chatgpt_win_job_completed_select": {
"message": "Изберете текста, който искате да използвате, и щракнете върху бутона."
},
"chatgpt_win_get_answer": {
"message": "Използвайте избрания отговор"
},
"chatgpt_win_close": {
"message": "Затвори"
},
"chatgpt_textarea_not_found_error": {
"message": "Изглежда, че страницата на ChatGPT се зарежда твърде бавно. Ако зареждането приключи, щракнете върху бутона вдясно. Ако проблемът продължава, моля, проверете състоянието на услугата."
},
"chatgpt_btn_retry": {
"message": овторeн опит"
},
"chatgpt_sendbutton_not_found_error": {
"message": "Кликнете върху бутона за изпращане, за да изпратите подканата."
},
"chatgpt_user_not_logged_in": {
"message": "Не сте влезли в ChatGPT. Моля, влезте с вашите идентификационни данни, затворете прозореца на ChatGPT и след това повторете действието, което сте опитали. След това ще останете влезли."
},
"chatgpt_win_model_warning": {
"message": "По някаква причина не е възможно да се провери дали е зареден правилният модел. Засега можете да натиснете синия бутон, за да продължите."
},
"chatgpt_win_custom_text": {
"message": "Въведете тук допълнителния текст за подканата."
},
"chatgpt_win_send": {
"message": "Изпрати"
},
"warn_API_needed": {
"message": "За да използвате тази функция, ви е необходима API интеграция, а не ChatGPT Web интеграция. Можете да дефинирате конкретен API на страницата с настройки на функцията, като първо поставите отметка в квадратчето по-горе и след това щракнете върху бутона отляво."
},
"prefs_google_gemini_thinking_budget": {
"message": "Бюджет за мислене"
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,173 +0,0 @@
{
"prompt_lang": {
"message": "Respondi per"
},
"extensionDescription": {
"message": "Uzu ChatGPT, Google Gemini, Claude aŭ Ollama por poluri viajn retpoŝtajn mesaĝojn!"
},
"prompt_rewrite_formal": {
"message": "Reverki formale"
},
"more_info_string": {
"message": "Pliaj informoj"
},
"customPrompts_managePrompts": {
"message": "Administri instrukciojn"
},
"customPrompts_btnCancel": {
"message": "Nuligi"
},
"customPrompts_form_required_fields": {
"message": "Postulataj kampoj"
},
"btnSaveAll_string": {
"message": "Konservi ĉion"
},
"btnNew_string": {
"message": "Aldoni novan"
},
"chatgpt_btn_retry": {
"message": "Reprovi"
},
"customPrompts_close_button": {
"message": "Fermobutono"
},
"customPrompts_add_to_menu": {
"message": "Aldoni en menuon"
},
"From": {
"message": "De"
},
"no_string": {
"message": "Ne"
},
"apiwebchat_stopping": {
"message": "Ĉesante"
},
"prompt_classify": {
"message": "Klasifiku"
},
"customPrompts_add_to_menu_always": {
"message": "Ĉiam"
},
"menu_title": {
"message": "AI"
},
"customPrompts_form_label_Text": {
"message": "Teksto de instrukcio"
},
"prompt_reply": {
"message": "Respondu al ĉi tiu retpoŝta mesaĝo"
},
"customPrompts_btnDelete": {
"message": "Forviŝi"
},
"prompt_reply_advanced": {
"message": "Respondu al ĉi tiu fadeno"
},
"prompt_translate_this": {
"message": "Traduku ĉi tion"
},
"save": {
"message": "Konservi"
},
"customPrompts_form_label_ID": {
"message": "Identigilo"
},
"customPrompts_form_label_enabled": {
"message": "Ŝaltita"
},
"customPrompts_form_label_Name": {
"message": "Nomo"
},
"customPrompts_form_label_Action": {
"message": "Ago"
},
"chatgpt_win_send": {
"message": "Sendi"
},
"Date": {
"message": "Dato"
},
"customPrompts_btnEdit": {
"message": "Modifi"
},
"Loading": {
"message": "Ŝargante…"
},
"customPrompts_btnOK": {
"message": "Bone"
},
"chatgpt_win_job_completed": {
"message": "Finite!"
},
"chatgpt_win_close": {
"message": "Fermi"
},
"prefs_OptionText_chatgpt_win_height": {
"message": "Alto"
},
"prefs_OptionText_chatgpt_win_width": {
"message": "Larĝo"
},
"backToOptionsText": {
"message": "Opcioj"
},
"Subject": {
"message": "Temo"
},
"Explanation": {
"message": "Klarigo"
},
"yes_string": {
"message": "Jes"
},
"apiwebchat_you": {
"message": "Vi"
},
"apiwebchat_info": {
"message": "Informoj"
},
"apiwebchat_error": {
"message": "Eraro"
},
"prompt_rewrite_polite": {
"message": "Reverki ĝentile"
},
"customPrompts_start_saving": {
"message": "Konservante instrukciojn…"
},
"placeholder_recipients": {
"message": "Ricevantoj"
},
"placeholder_author": {
"message": "Aŭtoro"
},
"Ollama_Models": {
"message": "Modeloj de Ollama"
},
"Ollama_Models_Fetch": {
"message": "Ĝisdatigi liston de modeloj Ollama"
},
"prefs_OptionText_reply_all": {
"message": "Respondi al ĉiuj"
},
"prefs_OptionText_reply_sender": {
"message": "Respondi al sendinto"
},
"prefs_OptionText_reply_type": {
"message": "Speco de respondo"
},
"SelectAll": {
"message": "Elekti ĉion"
},
"DeselectAll": {
"message": "Malelekti ĉion"
},
"prompt_reply_custom_command": {
"message": "Respondi per komando..."
},
"customPrompts_substitute_text": {
"message": "Anstataŭigi tekston"
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,822 +0,0 @@
{
"extensionDescription": {
"message": "Koristite ChatGPT, Google Gemini, Claude ili Ollama kako bi poboljšali vaše e-poruke!",
"description": "Description of the extension."
},
"menu_title": {
"message": "AI"
},
"prompt_lang": {
"message": "Odgovori u"
},
"prompt_reply": {
"message": "Odgovori na ovu e-poruku"
},
"prompt_reply_advanced": {
"message": "Odgovori na ovu nit"
},
"prompt_rewrite_polite": {
"message": "Prepravi pristojno"
},
"prompt_rewrite_formal": {
"message": "Prepravi formalno"
},
"prompt_classify": {
"message": "Klasificiraj"
},
"prompt_translate_this": {
"message": "Prevedi ovo"
},
"prompt_this": {
"message": "Upitaj ovo"
},
"prompt_selection_needed": {
"message": "Za nastavak morate odabrati neki tekst!"
},
"customPrompts_managePrompts": {
"message": "Upravljaj upitima"
},
"more_info_string": {
"message": "Više informacija"
},
"customPrompts_managePrompts_info_default": {
"message": "Zadane upite nije moguće uređivati. Možete ih onemogućiti, a zatim kopirati tekst upita i zalijepiti ga u novi kako biste stvorili modificiranu verziju."
},
"customPrompts_managePrompts_info_default_2": {
"message": "Možete uvoziti i izvoziti upite. Postojeći upiti s istim ID-om bit će prebrisani. Dodat će se upiti s novim ID-ovima."
},
"customPrompts_managePrompts_info_default_3": {
"message": "Ako je sve ispravno, nakon uvoza pritisnite gumb 'Spremi sve'."
},
"customPrompts_start_saving": {
"message": "Spremanje upita..."
},
"customPrompts_reindexing_list": {
"message": "Ponovno indeksiranje popisa..."
},
"customPrompts_filtering_prompts": {
"message": "Filtriranje upita..."
},
"customPrompts_saving_default_prompts": {
"message": "Spremanje zadanih upita..."
},
"customPrompts_saving_custom_prompts": {
"message": "Spremanje prilagođenih upita..."
},
"customPrompts_reloading_menus": {
"message": "Ponovno učitavanje izbornika..."
},
"customPrompts_saved": {
"message": "Upiti su spremljeni!"
},
"customPrompts_form_label_ID": {
"message": "ID"
},
"customPrompts_form_label_ID_rules": {
"message": "Mora biti jedinstveno, malim slovima i bez razmaka"
},
"customPrompts_form_label_Name": {
"message": "Ime"
},
"customPrompts_form_label_Text": {
"message": "Tekst upita"
},
"customPrompts_form_label_Action": {
"message": "Radnja"
},
"customPrompts_form_label_need_selected": {
"message": "Potreban je odabir teksta"
},
"customPrompts_form_label_need_signature": {
"message": "Uvijek dodaj potpis"
},
"customPrompts_form_label_need_custom_text": {
"message": "Zatraži dodatni tekst"
},
"customPrompts_form_label_enabled": {
"message": "Omogućeno"
},
"customPrompts_form_required_fields": {
"message": "Obavezna polja"
},
"customPrompts_btnEdit": {
"message": "Uredi"
},
"customPrompts_btnCancel": {
"message": "Otkaži"
},
"customPrompts_btnOK": {
"message": "U redu"
},
"customPrompts_btnDelete": {
"message": "Obriši"
},
"customPrompts_btnDelete_confirmText": {
"message": "Jeste li sigurni da želite izbrisati ovu stavku?"
},
"customPrompts_unsaved_changes": {
"message": "Ima nespremljenih promjena!"
},
"btnSaveAll_string": {
"message": "Spremi sve"
},
"btnNew_string": {
"message": "Dodaj novo"
},
"customPrompts_btnAddNewCommit": {
"message": "Dodaj upit"
},
"customPrompts_add_to_menu": {
"message": "Dodaj u izbornik"
},
"customPrompts_add_to_menu_always": {
"message": "Uvijek"
},
"customPrompts_add_to_menu_reading": {
"message": "Čitanje e-poruke"
},
"customPrompts_add_to_menu_composing": {
"message": "Sastavljanje e-poruke"
},
"customPrompts_close_button": {
"message": "gumb Zatvori"
},
"customPrompts_do_reply": {
"message": "Odgovori"
},
"customPrompts_substitute_text": {
"message": "Zamijeni tekst"
},
"chatgpt_win_working": {
"message": "Radovi u tijeku..."
},
"chatgpt_win_job_completed": {
"message": "Završeno!"
},
"chatgpt_win_job_completed_select": {
"message": "Odaberite tekst koji želite koristiti i pritisnite gumb."
},
"chatgpt_win_get_answer": {
"message": "Koristite odabrani odgovor"
},
"chatgpt_win_close": {
"message": "Zatvori"
},
"chatgpt_textarea_not_found_error": {
"message": "Čini se da se stranica ChatGPT predugo učitava. Ako završi s učitavanjem, pritisnite gumb s desne strane. Ako se problem nastavi, provjerite status usluge."
},
"chatgpt_btn_retry": {
"message": "Pokušaj ponovo"
},
"chatgpt_sendbutton_not_found_error": {
"message": "Pritisnite gumb Pošalji kako biste poslali upit."
},
"chatgpt_user_not_logged_in": {
"message": "Niste prijavljeni na ChatGPT. Prijavite se sa svojim vjerodajnicama, zatvorite prozor ChatGPT, a zatim ponovite radnju koju ste pokušali. Nakon toga ćete ostati prijavljeni."
},
"chatgpt_win_model_warning": {
"message": "Iz nekog razloga nije moguće provjeriti je li učitan ispravan model. Za sada možeš pritisnuti plavi gumb za nastavak."
},
"chatgpt_win_custom_text": {
"message": "Ovdje umetnite dodatni tekst za upit."
},
"chatgpt_win_send": {
"message": "Pošalji"
},
"chatgpt_force_completion": {
"message": "prisilno dovrši"
},
"chatgpt_force_completion_title": {
"message": "Pritisnite ovdje za prikaz gumba 'Koristi zadnji odgovor' ako je ChatGPT završio svoj posao, ali se gumb nije pojavio."
},
"msg_prompt_too_long": {
"message": "Tekst koji ste unijeli je predug. Morate ga skratiti."
},
"prefs_OptionText_release_notes": {
"message": "Bilješke o izdanju"
},
"prefs_status_page": {
"message": "status usluge"
},
"prefs_OptionText_chatgpt_win_text": {
"message": "Dimenzije prozora za AI razgovor"
},
"prefs_OptionText_chatgpt_win_height": {
"message": "Visina"
},
"prefs_OptionText_chatgpt_win_width": {
"message": "Širina"
},
"prefs_OptionText_default_sign_name": {
"message": "Zadani naziv potpisa"
},
"prefs_OptionText_default_chatgpt_lang": {
"message": "Zadani jezik za odgovore"
},
"prefs_OptionText_reply_all": {
"message": "Odgovori svima"
},
"prefs_OptionText_reply_sender": {
"message": "Odgovori pošiljatelju"
},
"prefs_OptionText_reply_type": {
"message": "Vrsta odgovora"
},
"prefs_OptionText_btnManagePrompts": {
"message": "Upravljaj svojim upitima"
},
"prefsInfoTitle": {
"message": "Važne informacije"
},
"prefsInfoDesc_1": {
"message": "Može se dogoditi da se ChatGPT web sučelje promijeni na način koji pokvari rad dodatka. Provjerite stranicu \"Status usluge\" povezanu na dnu ove stranice. Također, zapamtite da kada prvi put koristite ThunderAI, morate se prijaviti na ChatGPT."
},
"prefsInfoDesc_2": {
"message": "Za korištenje ChatGPT API-ja potreban vam je OpenAI ChatGPT API ključ i morate odabrati model."
},
"prefsInfoDesc_3": {
"message": "Za korištenje integracije s Ollama, morate postaviti lokalni Ollama poslužitelj. Nakon što je poslužitelj pokrenut, unesite njegovu adresu u predviđeno polje unutar aplikacije. Kako biste osigurali ispravnu komunikaciju između ThunderAI i Ollama poslužitelja, ne zaboravite postaviti OLLAMA_ORIGINS=moz-extension://*."
},
"prefsInfoDesc_4": {
"message": "Za korištenje ove integracije morate postaviti lokalni poslužitelj kompatibilan s OpenAI API-jem, poput LM Studio. Nakon što je poslužitelj pokrenut, unesite njegovu adresu u predviđeno polje unutar aplikacije. Kako biste osigurali ispravnu komunikaciju između ThunderAI i lokalnog poslužitelja, ne zaboravite pravilno postaviti CORS postavke."
},
"prefsInfoDesc_5": {
"message": "Ne zaboravite da možete otvoriti izbornik ThunderAI pomoću tipkovničke prečice CTRL+ALT+A."
},
"prefsInfoDesc_6": {
"message": "Prečac možete promijeniti pritiskom ikone zupčanika u gornjem desnom kutu ove stranice i odabirom \"Upravljaj tipkovničkim prečacima dodatka\"."
},
"prefsDonation_1": {
"message": "Sviđa li vam se ovaj dodatak?"
},
"prefsDonation_2": {
"message": "Razmislite o davanju donacije!"
},
"backToOptionsText": {
"message": "Mogućnosti"
},
"TranslateText": {
"message": "Želite li pomoći u prijevodu ovog dodatka?"
},
"TranslateLink": {
"message": "Saznajte kako!"
},
"customPrompts_ExportAll": {
"message": "Izvezi sve upite"
},
"customPrompts_Import": {
"message": "Uvezi nove upite"
},
"importPrompts_confirmText": {
"message": "Upravo ćete uvesti nove upite."
},
"customPrompts_start_import": {
"message": "Pokretanje uvoza prilagođenih upita..."
},
"customPrompts_import_completed": {
"message": "Uvoz prilagođenih upita dovršen! Morate pritisnuti gumb 'Spremi sve' da biste spremili svoje promjene."
},
"importPrompts_invalidFile": {
"message": "Datoteka koju pokušavate uvesti nije važeća datoteka prilagođenih upita."
},
"importPrompts_invalidPrompts": {
"message": "Datoteka koju pokušavate uvesti ne sadrži valjane upite."
},
"currently_used_prompt": {
"message": "Trenutno korišteno ime upita"
},
"customprompts_form_label_define_response_lang": {
"message": "Odredite jezik odgovora u upitu"
},
"prefs_Connection_type": {
"message": "Vrsta veze"
},
"prefs_Connection_type_ChatGPT_Web": {
"message": "ChatGPT web sučelje"
},
"prefs_Connection_type_ChatGPT_API": {
"message": "ChatGPT OpenAI API"
},
"prefs_ChatGPT_API_Key": {
"message": "ChatGPT API ključ"
},
"ChatGPT_Models": {
"message": "ChatGPT modeli"
},
"ChatGPT_Models_Fetch": {
"message": "Ažuriraj popis ChatGPT modela"
},
"ChatGPT_Models_Error_fetching": {
"message": "Pogreška pri pokušaju dohvaćanja ChatGPT modela"
},
"Loading": {
"message": "Učitavanje..."
},
"error": {
"message": "ThunderAI greška!"
},
"chatgpt_empty_apikey": {
"message": "Niste dodali API ključ za ChatGPT API. Unesite jedan na stranicu s opcijama."
},
"chatgpt_empty_model": {
"message": "Niste odabrali model za ChatGPT API. Odaberite jedan na stranici s opcijama."
},
"chagpt_api_send_button": {
"message": "Korištenje modela"
},
"Debug": {
"message": "Otklanjanje pogrešaka"
},
"prefs_OptionText_do_debug_info": {
"message": "Aktiviraj sustav za otklanjanje pogrešaka"
},
"prefs_Connection_type_Ollama_API": {
"message": "Ollama API (lokalni LLM)"
},
"prefs_Connection_type_OpenAI_Comp_API": {
"message": "OpenAI kompatibilan API"
},
"prefs_API_Host": {
"message": "Adresa glavnog računala"
},
"Ollama_Models": {
"message": "Ollama modeli"
},
"Ollama_Models_Fetch": {
"message": "Ažurirajte popis Ollama modela"
},
"Ollama_Models_Error_fetching": {
"message": "Pogreška pri pokušaju dohvaćanja Ollama modela"
},
"API_Models_Error_NoModels": {
"message": "Nema pronađenih modela"
},
"ollama_empty_host": {
"message": "Niste dodali adresu glavnog računala za Ollama API. Unesite jednu na stranici s mogućnostima."
},
"ollama_empty_model": {
"message": "Niste odabrali model za Ollama API. Odaberite jedan na stranici s mogućnostima."
},
"error_connection_interrupted": {
"message": "Veza s poslužiteljem je neočekivano prekinuta"
},
"ollama_api_request_failed": {
"message": "Ollama API zahtjev nije uspio"
},
"chatgpt_api_request_failed": {
"message": "OpenAI ChatGPT API zahtjev nije uspio"
},
"WaitingServerResponse": {
"message": "Čeka se odgovor poslužitelja"
},
"prefs_API_Host_Info": {
"message": "Nešto poput"
},
"OpenAIComp_Models": {
"message": "OpenAI kompatibilni API modeli"
},
"OpenAIComp_Models_Fetch": {
"message": "Ažuriraj popis OpenAI kompatibilnih API modela"
},
"OpenAIComp_Models_Error_fetching": {
"message": "Pogreška pri pokušaju dohvaćanja OpenAI kompatibilnih API modela"
},
"OpenAIComp_empty_host": {
"message": "Niste dodali adresu glavnog računala za OpenAI kompatibilan API. Unesite jedan na stranicu s mogućnostima."
},
"OpenAIComp_empty_model": {
"message": "Niste odabrali model za OpenAI kompatibilan API. Odaberite jedan na stranici s opcijama."
},
"OpenAIComp_api_request_failed": {
"message": "OpenAI Comp API zahtjev nije uspio"
},
"prefs_OpenAIComp_ChatName": {
"message": "Naziv čavrljanja"
},
"prefs_OpenAIComp_ChatName_Info": {
"message": "Ovo je ime koje će se koristiti u čavrljanju s AI."
},
"StorageSpace": {
"message": "Ukupan zauzeto prostora za pohranu"
},
"SearchPrompt": {
"message": "Pretraži upite"
},
"prefs_OptionText_dynamic_menu_force_enter": {
"message": "Izbornik: neposredno slanje upita"
},
"prefs_OptionText_dynamic_menu_force_enter_info": {
"message": "Ako je označeno, korištenje tipkovničkog prečaca CTRL+ALT+A automatski će poslati istaknuti upit iz izbornika. U protivnom će korisniku biti prikazan naziv upita, koji će zahtijevati još jedan pritisak tipke Enter za slanje."
},
"prefs_OptionText_chatgpt_win_dims_info": {
"message": "Postavite na 0 ako ne želite odrediti veličinu prozora."
},
"prefs_OpenAIComp_API_Key": {
"message": "OpenAI Comp API ključ"
},
"Optional": {
"message": "Neobavezno"
},
"OpenChatGPTTab": {
"message": "Otvorite karticu ChatGPT"
},
"OpenChatGPTTab_Info": {
"message": "U slučaju problema s prijavom u prozor ThunderAI, otvorite ChatGPT u novoj kartici pomoću gumba s desne strane, prijavite se, zatim zatvorite karticu i nastavite koristiti ThunderAI."
},
"placeholder_mail_text_body": {
"message": "Tijelo e-poruke"
},
"placeholder_mail_html_body": {
"message": "HTML tijelo e-poruke"
},
"placeholder_mail_subject": {
"message": "Naslov e-poruke"
},
"placeholder_selected_text": {
"message": "Odabrani tekst"
},
"placeholder_additional_text": {
"message": "Dodatni tekst"
},
"placeholder_junk_score": {
"message": "Rezultat smeća"
},
"placeholder_recipients": {
"message": "Primatelji"
},
"placeholder_cc_list": {
"message": "CC popis"
},
"placeholder_author": {
"message": "Autor"
},
"prefs_OptionText_placeholders_use_default_value": {
"message": "Rezervirana mjesta: koristite zadanu vrijednost"
},
"prefs_OptionText_placeholders_use_default_value_info": {
"message": "Ako je označeno, rezervirana mjesta bit će ispunjena zadanim vrijednostima kada nije navedena vrijednost. U suprotnom će rezervirana mjesta ostati umjesto."
},
"prefs_OptionText_max_prompt_length": {
"message": "Maksimalna duljina upita"
},
"prefs_OptionText_max_prompt_length_Info": {
"message": "Ovo je najveći broj znakova koji se mogu koristiti u upitu. U protivnom će se prikazati poruka o pogrešci. Vrijednost nije moguće uređivati za ChatGPT web sučelje. Postavite na nulu kako biste onemogućili provjeru."
},
"prefs_OptionText_chatgpt_web_model": {
"message": "ChatGPT Web Model"
},
"prefs_OptionText_chatgpt_web_model_info": {
"message": "Ovo je Model koji će se provoditi za ChatGPT web sučelje. Ako nijedan nije naveden ili je naveden netočan, zadani model postavit će ChatGPT na web stranici. Ova postavka neće raditi s besplatnim ChatGPT računom."
},
"prefs_OptionText_chatgpt_web_tempchat": {
"message": "ChatGPT Web privremeno čavrljanje"
},
"prefs_OptionText_chatgpt_web_tempchat_info": {
"message": "Ako je označeno, privremeno čavrljanje koristit će se u web sučelju ChatGPT."
},
"chatgpt_btn_model": {
"message": "Koristi trenutni model"
},
"AllowedValues": {
"message": "Dopuštene vrijednosti"
},
"prefs_OptionText_btnManagePrompts_infoline": {
"message": "Možete koristiti dodatna rezervirana mjesta za podatke."
},
"prefs_OptionText_openai_comp_use_v1": {
"message": "Zadrži \"v1\" kompatibilnost"
},
"prefs_OptionText_openai_comp_use_v1_info": {
"message": "Ako je označeno, segment \"v1\" na putanji API poziva će se zadržati, poput \"http://localhost:1234/v1/chat/completions\"."
},
"prefs_OptionText_owl_warning": {
"message": "Čini se da barem jedan od vaših računa koristi dodatak Owl for Exchange. Postoji poznati problem između Thunderbirda i Owl, koji se trenutno rješava. Trenutačno možete koristiti ThunderAI dok sastavljate e-poruke, ali ne i dok ih čitate."
},
"prompt_reply_full_text": {
"message": "Odgovori na sljedeću e-poruku. Odgovorit samo s potrebnim tekstom i bez dodatnih komentara ili drugog teksta."
},
"prompt_reply_additional_text": {
"message": "Nemoj dodavati naslov u odgovor."
},
"reply_same_lang": {
"message": "Odgovori na istom jeziku."
},
"sign_msg_as": {
"message": "Potpiši e-poruku kao"
},
"prompt_reply_advanced_full_text": {
"message": "Odgovori na sljedeću e-poruku \"{%selected_text%}\", s obzirom da je ovo cijela nit e-poruka \"{%mail_html_body%}\". Odgovori samo s potrebnim tekstom i bez dodatnih komentara ili drugog teksta."
},
"prompt_rewrite_full_text": {
"message": "Prepravi sljedeći tekst kako biste bili pristojniji. Odgovori samo s prepravljenim tekstom i bez dodatnih komentara ili drugog teksta."
},
"prompt_rewrite_formal_full_text": {
"message": "Prepravi sljedeći tekst da bude formalniji. Odgovori samo s prepravljenim tekstom i bez dodatnih komentara ili drugog teksta."
},
"prompt_classify_full_text": {
"message": "Klasificiraj sljedeći tekst u smislu ljubaznosti, topline, formalnosti, asertivnosti, uvredljivosti dajući postotak za svaku kategoriju. Odgovori samo kategorijom i ocijeni bez dodatnih komentara ili drugog teksta."
},
"prompt_translate_this_full_text": {
"message": "Prevedite donju e-poštu na {%thunderai_translate_lang%}.\n\nPravila:\n- Prevedite i predmet i tijelo e-pošte.\n- Vratite rezultat kao JSON objekt s tri polja: \"subject\", \"body\" i \"status\".\n- Ako je prijevod izvršen, status je jedan 1.\n- Ako je e-pošta napisana na jednom od ovih jezika \"{%thunderai_translate_exclude_lang%}\" ili na jeziku {%thunderai_translate_lang%}, vratite prazan niz za tijelo i predmet i postavite status na -1.\n- Nemojte dodavati objašnjenja, bilješke ili bilo kakav tekst izvan JSON-a.\n\nPredmet e-pošte: {%mail_subject%}\n\nTijelo e-pošte: {%mail_html_body%}\n\nGenerirajte odgovor isključivo u JSON formatu. Izlaz treba biti samo JSON objekt. Evo primjera JSON formata koji treba koristiti:\n{\n\"subject\": \"subject translation\",\n\"body\": \"body translation\",\n\"status\": \"status result\"\n}"
},
"prompt_this_full_text": {
"message": "Odgovori samo s potrebnim tekstom i bez dodatnih komentara ili drugog teksta."
},
"prefs_OptionText_add_tags": {
"message": "Dodaj oznake e-porukama"
},
"prefs_OptionText_add_tags_Info": {
"message": "Ako je označeno, stavka će biti uključena u izbornik za primjenu oznaka na e-poruke."
},
"prompt_add_tags": {
"message": "Dodaj oznake ovoj e-poruci"
},
"prompt_add_tags_full_text": {
"message": "Analiziraj sljedeći tekst e-pošte i generiraj JSON niz oznaka koje sažimaju njezin sadržaj. Koristi teme, ključne pojmove i relevantne opise kao oznake. Pobrini se da oznake budu sažete i relevantne za sadržaj poruke.\nTekst e-poruke: {%mail_text_body%}\nUzmi u obzir sljedeće pojedinosti za kontekst:\n- Šalje: {%author%}\n- Primatelji: {%recipients%}\n- CC popis: {%cc_list%}\n- Naslov: {%mail_subject%}\nTemelji oznake na tekstu poruke i kontekstu, zanemarujući nebitne informacije ili trivijalne detalje.\nGeneriraj odgovor isključivo u JSON formatu. Izlaz mora biti samo JSON niz oznaka, bez ikakvih dodatnih komentara ili teksta. Evo primjera formata koji treba koristiti:\n{\n\"tags\": [\"tag1\", \"tag2\", \"tag3\", \"tag4\", \"tag5\"]\n}"
},
"placeholder_tags_current_email": {
"message": "Oznake e-pošte"
},
"placeholder_tags_full_list": {
"message": "Postojeće oznake"
},
"prefs_OptionText_add_tags_maxnum": {
"message": "Maksimalan broj oznaka"
},
"prefs_OptionText_add_tags_maxnum_Info": {
"message": "Maksimalni broj oznaka koje je predložio AI. Postavite na 0 ako ne želite ograničiti broj oznaka."
},
"prompt_add_tags_maxnum": {
"message": "Ograniči broj oznaka na"
},
"prefs_OptionText_add_tags_hide_exclusions": {
"message": "Sakrij izuzete oznake"
},
"prefs_OptionText_add_tags_hide_exclusions_Info": {
"message": "Ako je označeno, oznake koje se nalaze na popisu izuzetaka bit će skrivene u dijaloškom okviru za potvrdu."
},
"prefs_OptionText_btnManageTagsInfo": {
"message": "Upravljaj postavkama oznaka"
},
"AddTags_PageTitle": {
"message": "Upravljaj postavkama oznaka"
},
"AddTags_info_default": {
"message": "Na ovoj stranici možete promijeniti zadani upit koji se koristi za dodavanje oznaka u e-poruke i upravljanje popisom izuzetaka."
},
"AddTags_prompt_text_title": {
"message": "Trenutačni tekst upita"
},
"AddTags_excl_list_title": {
"message": "Popis izuzetaka"
},
"AddTags_excl_list_infoline": {
"message": "Ovo je popis oznaka koje nije dopušteno dodavati e-porukama."
},
"save": {
"message": "Spremi"
},
"addtags_info_additional_statements": {
"message": "Ova će izjava biti dodana na kraju upita:"
},
"reset_default": {
"message": "Vrati na zadano"
},
"addtags_excl_list_infoline2": {
"message": "Dodaj riječ po retku ili odvojenu zarezom."
},
"addtags_dialog_title": {
"message": "Dodaj oznake e-poruci"
},
"addtags_exclude_tag": {
"message": "Isključi oznaku"
},
"addtags_no_tags_received": {
"message": "Nisu primljene oznake od AI."
},
"addtags_no_valid_tags": {
"message": "Nisu pronađene važeće oznake nakon filtriranja s popisom izuzetaka."
},
"thunderai_error_title": {
"message": "ThunderAI greška"
},
"thunderai_warning_title": {
"message": "ThunderAI upozorenje"
},
"prefs_OptionText_add_tags_first_uppercase": {
"message": "Prvo slovo veliko"
},
"prefs_OptionText_add_tags_first_uppercase_Info": {
"message": "Ako je označeno, etiketa oznaka bit će postavljena na mala slova sa samo prvim velikim slovom."
},
"AddTags_prompt_prefs_title": {
"message": "Mogućnosti dodavanja oznaka"
},
"prefs_SurveyLinkText": {
"message": "Podijelite svoje povratne informacije i pomozite nam poboljšati ThunderAI!"
},
"prefs_SurveyLinkText2": {
"message": "Pritisnite ovdje, traje samo minutu!"
},
"prefs_OpenAIComp_ForceModel": {
"message": "Ručno umetni model"
},
"OpenAIComp_force_model_ask": {
"message": "Ovdje umetni naziv modela koji želiš koristiti."
},
"prefs_OptionText_add_tags_force_lang": {
"message": "Nametni jezik"
},
"prefs_OptionText_add_tags_force_lang_Info": {
"message": "Ako je označeno, jezik oznaka će biti prisiljen odgovarati jeziku definiranom na stranici mogućnosti ThunderAI, ako je navedeno."
},
"prompt_add_tags_force_lang": {
"message": "Oznake moraju biti upisane"
},
"prefs_Connection_type_Google_Gemini_API": {
"message": "Google Gemini API"
},
"prefs_GoogleGemini_API_Key": {
"message": "API ključ"
},
"GoogleGemini_Models": {
"message": "Google Gemini API modeli"
},
"GoogleGemini_Models_Fetch": {
"message": "Ažuriraj popis Google Gemini modela"
},
"GoogleGemini_Models_Error_fetching": {
"message": "Pogreška pri pokušaju dohvaćanja modela Google Gemini"
},
"google_gemini_api_request_failed": {
"message": "Google Gemini API zahtjev nije uspio"
},
"google_gemini_empty_apikey": {
"message": "Niste dodali API ključ za Google Gemini API. Unesite jedan na stranicu s mogućnostima."
},
"google_gemini_empty_model": {
"message": "Niste odabrali model za Google Gemini API. Odaberite jedan na stranici s mogućnostima."
},
"GoogleGemini_SystemInstruction": {
"message": "Uputa za sustav"
},
"GoogleGemini_SystemInstruction_Info": {
"message": "Kada postavite uputu za sustav, dajete modelu dodatni kontekst za razumijevanje zadatka, dajete prilagođenije odgovore i pridržavate se specifičnih smjernica u vezi s upitom koji će biti poslan."
},
"ChatGPT_Developer_Messages": {
"message": "Poruke razvojnog programera"
},
"ChatGPT_Developer_Messages_Info": {
"message": "Kada postavite poruke razvojnog programera, modelu dajete dodatni kontekst za razumijevanje zadatka, dajete prilagođenije odgovore i pridržavate se specifičnih smjernica za upit koji će biti poslan."
},
"prefs_OptionText_btnManagePrompts_infoline3": {
"message": "Možete upotrijebiti {%tags_full_list%} rezervirano mjesto podataka u upitu za popis dostupnih oznaka. Uz odgovarajući upit, tada možete prisiliti da se oznake izaberu samo s popisa onih koji već postoje."
},
"placeholder_mail_typed_text": {
"message": "Upisani tekst prije citiranog tijela e-poruke"
},
"prompt_get_calendar_event": {
"message": "Dodaj novi kalendarski događaj"
},
"prompt_get_calendar_event_full_text": {
"message": "Izdvoji sve relevantne detalje potrebne za generiranje kalendarskog događaja iz sljedećeg teksta. Izdvojene informacije trebaju uključivati:\n- Naslov događaja\n- Datum i vrijeme početka (uključujući vremensku zonu, ako je navedeno)\n- Datum i vrijeme završetka (uključujući vremensku zonu, ako je navedeno)\n- Cijeli dan (ako je navedeno)\n- Sudionici\nOsiguraj da su podaci oblikovani jasno i dosljedno kako bi se mogli izravno koristiti za stvaranje kalendarskog događaja.\nAko postoje relativne vremenske napomene, smatraj da su datum i vrijeme e-poruke \"{%mail_datetime%}\". Izračunajte datum i vrijeme početka na temelju ove napomene. Ako su izračunati početni datum i vrijeme raniji od \"{%current_datetime%}\", ponovno izračunaj početni datum i vrijeme koristeći \"{%current_datetime%}\" kao osnovu.\nAko trajanje nije navedeno, postavi ga na jedan sat.\nOvo su sudionici: {%author%}, {%recipients%}, {%cc_list%}. Ako je prisutna, isključi moju adresu: {%account_email_address%}.\nAko je događaj cjelodnevni, **endDate** mora biti jedan dan nakon **startDate** s vremenom postavljenim na **\"T000000\"**.\nAko ne možeš dobiti jednu ili više potrebnih informacija, odgovori praznim nizom.\nGeneriraj odgovor samo u JSON formatu. Nemoj uključivati nikakav dodatni tekst ili objašnjenja; pruži samo JSON. Ovo je format koji će se koristiti:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Sažetak kalendarskih događaja ovdje\",\n\"forceAllDay\": false\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nOvo je tekst:\"{%mail_text_body_or_selected%}\""
},
"prefs_OptionText_get_calendar_event": {
"message": "Dodaj novi kalendarski događaj iz odabranog teksta"
},
"prefs_OptionText_get_calendar_event_Info": {
"message": "Ako je označeno, stavka će biti uključena u izbornik za dobivanje informacija o kalendarskom događaju iz odabranog teksta."
},
"prefs_OptionText_btnManageCalendarEventInfo": {
"message": "Upravljaj postavkama kalendarskih događaja"
},
"GetCalendarEvent_PageTitle": {
"message": "Upravljaj postavkama kalendarskih događaja"
},
"GetCalendarEvent_info_default": {
"message": "Na ovoj stranici možete promijeniti zadani upit koji se koristi za dobivanje kalendarskih događaja iz odabranog teksta."
},
"GetCalendarEvent_prompt_text_title": {
"message": "Trenutačni tekst upita"
},
"prefs_OptionText_AdvancedPromptResponse_infoline2": {
"message": "Možete promijeniti upit kako želite, ali odgovor primljen od AI mora biti u JSON formatu kako je navedeno u zadanom upitu!"
},
"prefs_OptionText_get_calendar_event_Sparks_not_present": {
"message": "Za korištenje značajki kalendarskih događaja i zadataka, molimo instalirajte dodatak ThunderAI Sparks."
},
"prefs_OptionText_download_now": {
"message": "Preuzmite ThunderAI Sparks sada!"
},
"placeholder_mail_datetime": {
"message": "Datum i vrijeme e-poruke"
},
"placeholder_current_datetime": {
"message": "Trenutačni datum i vrijeme"
},
"calendar_getting_data_error": {
"message": "Pogreška pri dohvaćanju podataka o kalendarskom događaju"
},
"calendar_opening_dialog_error": {
"message": "Pogreška pri otvaranju dijaloškog okvira kalendarskog događaja"
},
"prefs_OptionText_add_tags_auto": {
"message": "Dodajte oznake automatski"
},
"prefs_OptionText_add_tags_auto_Info": {
"message": "Ako je označeno, AI će automatski dodati oznake novoprimljenim e-porukama."
},
"prefs_OptionText_add_tags_auto_force_existing": {
"message": "Prisilno koristi postojeće oznake pri automatskom označavanju ili korištenju kontekstnog izbornika"
},
"prefs_OptionText_add_tags_auto_force_existing_Info": {
"message": "Ako je označeno, AI će novoprimljenim e-porukama dodati samo postojeće oznake i neće stvarati nove oznake."
},
"prefs_OptionText_add_tags_auto_only_inbox": {
"message": "Dodajte oznake samo e-porukama u Primljenoj pošti"
},
"prefs_OptionText_add_tags_auto_only_inbox_Info": {
"message": "Ako je označeno, AI će dodati oznake samo e-porukama u mapu Primljena pošta."
},
"placeholder_thunderai_def_sign": {
"message": "Zadani potpis kako je određeno u mogućnostima ThunderAI."
},
"placeholder_thunderai_def_lang": {
"message": "Zadani jezik kako je određeno u mogućnostima ThunderAI."
},
"prefs_OptionText_spamfilter": {
"message": "Automatski filter neželjene pošte"
},
"prefs_OptionText_spamfilter_Info": {
"message": "Ako je označeno, ThunderAI će automatski premjestiti neželjenu poštu u mapu Neželjena pošta."
},
"prefs_OptionText_btnManageSpamFilterInfo": {
"message": "Upravljaj postavkama filtera neželjene pošte"
},
"SpamFilter_PageTitle": {
"message": "Upravljaj postavkama filtera neželjene pošte"
},
"SpamFilter_info_default": {
"message": "Na ovoj stranici možete promijeniti zadani upit koji se koristi za otkrivanje neželjene pošte."
},
"SpamFilter_prompt_text_title": {
"message": "Trenutačni tekst upita"
},
"prompt_spamfilter": {
"message": "Prepoznaj neželjenu poštu"
},
"prompt_spamfilter_full_text": {
"message": "Analiziraj sljedeću e-poruku i utvrdi je li neželjena ili ne. Razmotri čimbenike kao što su sumnjive ključne riječi, pretjerani promotivni jezik, zavaravajuće linije predmeta, zahtjevi za osobnim podacima i neobične adrese pošiljatelja.\nNavedi vrijednost od 0 (nije spam) do 100 (neželjena pošta) i objašnjenje od najviše 10 riječi.\nU slučaju nedostatka podataka poruke, postavite vrijednost na 0 (nije spam) i navedite razlog.\nGeneriraj odgovor samo u JSON formatu. Nemoj uključivati nikakav dodatni tekst ili objašnjenje; pruži samo JSON. Ovdje je format koji treba koristiti:\n{\n\"explanation\": \"Kratko objašnjenje vašeg obrazloženja\",\n\"spamValue\": <cijeli broj od 0 do 100>\n}\nOvdje su informacije o e-poruci:\nŠalje: \"{%author%}\"\nNaslov: \"{%mail_subject%}\"\nHtml tijelo: \"{%mail_html_body%}\""
},
"SpamFilter_prompt_prefs_title": {
"message": "Mogućnosti filtera neželjene pošte"
},
"prefs_OptionText_spamfilter_threshold": {
"message": "Prag neželjene pošte"
},
"prefs_OptionText_spamfilter_threshold_Info": {
"message": "Ako je vrijednost koju vraća AI iznad ovog praga, e-poruka će biti premještena u mapu Neželjena pošta."
},
"spamfilter_threshold_too_low": {
"message": "Prag neželjene pošte je prenizak! Vjerojatno ćete označiti previše e-poruka kao neželjenu poštu!"
},
"spamfilter_threshold_zero": {
"message": "Prag neželjene pošte je nula! Označit ćete sve e-poruke kao neželjenu poštu!"
},
"spamfilter_no_reports": {
"message": "Nijedna poruka još nije pregledana na neželjenu poštu. Ovdje ćete pronaći popis zadnjih 100 izvješća o neželjenoj pošti samo za trenutnu sjednicu."
},
"SpamReport_Title": {
"message": "Izvješća filtera neželjene pošte"
},
"Date": {
"message": "Datum"
},
"From": {
"message": "Šalje"
},
"Subject": {
"message": "Naslov"
},
"Spam_Value": {
"message": "Vrijednost neželjene pošte"
},
"Moved_to_Spam": {
"message": "Premješteno u Neželjenu poštu"
},
"Explanation": {
"message": "Obrazloženje"
},
"Report_Date": {
"message": "Datum izvješća"
},
"yes_string": {
"message": "Da"
},
"no_string": {
"message": "Ne"
},
"prefs_OptionText_openai_comp_info_remote": {
"message": "Ovdje možete unijeti i adresu udaljenog poslužitelja."
}
}

View file

@ -1,17 +0,0 @@
{
"extensionDescription": {
"message": "Használja a ChatGPT, Google Gemini, Claude vagy Ollama modelleket, hogy még jobbá tegye emailjeit!"
},
"menu_title": {
"message": "MI"
},
"prompt_reply": {
"message": "Válasz erre az emailre"
},
"prompt_rewrite_polite": {
"message": "Újraírás udvariasabban"
},
"prompt_rewrite_formal": {
"message": "Újraírás formálisan"
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
{}

View file

@ -1,5 +0,0 @@
{
"extensionDescription": {
"message": "Gebruik ChatGPT, Google Gemini, Claude of Ollama om uw emails te verbeteren."
}
}

View file

@ -1,870 +0,0 @@
{
"extensionDescription": {
"message": "Używaj ChatGPT, Goolge Gemini, Claude lub Ollama do ulepszania swoich e-maili!",
"description": "Description of the extension."
},
"menu_title": {
"message": "AI"
},
"prompt_lang": {
"message": "Odpowiedz w"
},
"prompt_reply": {
"message": "Odpowiedz na ten e-mail"
},
"prompt_reply_advanced": {
"message": "Odpowiedz na ten wątek"
},
"prompt_rewrite_polite": {
"message": "Przepisz uprzejmie"
},
"prompt_rewrite_formal": {
"message": "Przepisz formalnie"
},
"prompt_classify": {
"message": "Klasyfikuj"
},
"prompt_translate_this": {
"message": "Przetłumacz to"
},
"prompt_this": {
"message": "Zapytaj o to"
},
"prompt_selection_needed": {
"message": "Aby kontynuować, musisz zaznaczyć tekst!"
},
"customPrompts_managePrompts": {
"message": "Zarządzaj poleceniami"
},
"more_info_string": {
"message": "Więcej informacji"
},
"customPrompts_managePrompts_info_default": {
"message": "Domyślne polecenia nie są edytowalne. Możesz je wyłączyć, skopiować tekst polecenia i wkleić go do nowego, aby stworzyć zmodyfikowaną wersję."
},
"customPrompts_managePrompts_info_default_2": {
"message": "Możesz importować i eksportować polecenia. Istniejące polecenia o tym samym ID zostaną nadpisane. Polecenia z nowymi ID zostaną dodane."
},
"customPrompts_managePrompts_info_default_3": {
"message": "Jeśli wszystko jest poprawne, po imporcie kliknij przycisk 'Zapisz wszystko'."
},
"customPrompts_start_saving": {
"message": "Zapisywanie poleceń..."
},
"customPrompts_reindexing_list": {
"message": "Ponowne indeksowanie listy..."
},
"customPrompts_filtering_prompts": {
"message": "Filtrowanie polece<63><65>..."
},
"customPrompts_saving_default_prompts": {
"message": "Zapisywanie domyślnych poleceń..."
},
"customPrompts_saving_custom_prompts": {
"message": "Zapisywanie własnych poleceń..."
},
"customPrompts_reloading_menus": {
"message": "Odświeżanie menu..."
},
"customPrompts_saved": {
"message": "Polecenia zapisane!"
},
"customPrompts_form_label_ID": {
"message": "ID"
},
"customPrompts_form_label_ID_rules": {
"message": "Musi być unikalne, pisane małymi literami i bez spacji"
},
"customPrompts_form_label_Name": {
"message": "Nazwa"
},
"customPrompts_form_label_Text": {
"message": "Tekst polecenia"
},
"customPrompts_form_label_Action": {
"message": "Akcja"
},
"customPrompts_form_label_need_selected": {
"message": "Wymagane zaznaczenie tekstu"
},
"customPrompts_form_label_need_signature": {
"message": "Zawsze dodawaj podpis"
},
"customPrompts_form_label_need_custom_text": {
"message": "Poproś o dodatkowy tekst"
},
"customPrompts_form_label_enabled": {
"message": "Włączone"
},
"customPrompts_form_required_fields": {
"message": "Pola wymagane"
},
"customPrompts_btnEdit": {
"message": "Edytuj"
},
"customPrompts_btnCancel": {
"message": "Anuluj"
},
"customPrompts_btnOK": {
"message": "OK"
},
"customPrompts_btnDelete": {
"message": "Usuń"
},
"customPrompts_btnDelete_confirmText": {
"message": "Czy na pewno chcesz usunąć ten element?"
},
"customPrompts_unsaved_changes": {
"message": "Istnieją niezapisane zmiany!"
},
"btnSaveAll_string": {
"message": "Zapisz wszystko"
},
"btnNew_string": {
"message": "Dodaj nowe"
},
"customPrompts_btnAddNewCommit": {
"message": "Dodaj polecenie"
},
"customPrompts_add_to_menu": {
"message": "Dodaj do menu"
},
"customPrompts_add_to_menu_always": {
"message": "Zawsze"
},
"customPrompts_add_to_menu_reading": {
"message": "Podczas czytania e-maila"
},
"customPrompts_add_to_menu_composing": {
"message": "Podczas pisania e-maila"
},
"customPrompts_close_button": {
"message": "Przycisk zamknięcia"
},
"customPrompts_do_reply": {
"message": "Odpowiedz"
},
"customPrompts_substitute_text": {
"message": "Zastąp tekst"
},
"chatgpt_win_working": {
"message": "Praca w toku..."
},
"chatgpt_win_job_completed": {
"message": "Zakończono!"
},
"chatgpt_win_job_completed_select": {
"message": "Wybierz tekst, którego chcesz użyć i kliknij przycisk."
},
"chatgpt_win_get_answer": {
"message": "Użyj wybranej odpowiedzi"
},
"chatgpt_win_close": {
"message": "Zamknij"
},
"chatgpt_textarea_not_found_error": {
"message": "Wygląda na to, że strona ChatGPT ładuje się zbyt długo. Jeśli ładowanie zakończy się, kliknij przycisk po prawej stronie. Jeśli problem będzie się powtarzał, sprawdź status usługi."
},
"chatgpt_btn_retry": {
"message": "Spróbuj ponownie"
},
"chatgpt_sendbutton_not_found_error": {
"message": "Kliknij przycisk wysyłania, aby przesłać zapytanie."
},
"chatgpt_user_not_logged_in": {
"message": "Nie jesteś zalogowany do ChatGPT. Zaloguj się swoimi danymi, zamknij okno ChatGPT, a następnie powtórz próbę. Po autoryzacji pozostaniesz zalogowany."
},
"chatgpt_win_model_warning": {
"message": "Z jakiegoś powodu nie można zweryfikować, czy załadowano właściwy model. Na razie możesz nacisnąć niebieski przycisk, aby kontynuować."
},
"chatgpt_win_custom_text": {
"message": "Wstaw tutaj dodatkowy tekst dla zapytania."
},
"chatgpt_win_send": {
"message": "Wyślij"
},
"chatgpt_force_completion": {
"message": "wymuś zakończenie"
},
"chatgpt_force_completion_title": {
"message": "Kliknij tutaj, aby wyświetlić przycisk 'Użyj ostatniej odpowiedzi', jeśli ChatGPT zakończył pracę, ale przycisk się nie pojawił."
},
"msg_prompt_too_long": {
"message": "Podany tekst jest zbyt długi. Musisz go skrócić."
},
"prefs_OptionText_release_notes": {
"message": "Informacje o wydaniu"
},
"prefs_status_page": {
"message": "status usługi"
},
"prefs_OptionText_chatgpt_win_text": {
"message": "Wymiary okna czatu AI"
},
"prefs_OptionText_chatgpt_win_height": {
"message": "Wysokość"
},
"prefs_OptionText_chatgpt_win_width": {
"message": "Szerokość"
},
"prefs_OptionText_default_sign_name": {
"message": "Domyślna nazwa podpisu"
},
"prefs_OptionText_default_chatgpt_lang": {
"message": "Domyślny język odpowiedzi"
},
"prefs_OptionText_reply_all": {
"message": "Odpowiedz wszystkim"
},
"prefs_OptionText_reply_sender": {
"message": "Odpowiedz nadawcy"
},
"prefs_OptionText_reply_type": {
"message": "Typ odpowiedzi"
},
"prefs_OptionText_btnManagePrompts": {
"message": "Zarządzaj poleceniami"
},
"prefsInfoTitle": {
"message": "Ważne informacje"
},
"prefsInfoDesc_1": {
"message": "Może się zdarzyć, że interfejs ChatGPT zmieni się w sposób, który zakłóci działanie dodatku. Sprawdź stronę \"Status usługi\" połączoną na dole tej strony. Pamiętaj też, że przy pierwszym użyciu ThunderAI musisz zalogować się do ChatGPT."
},
"prefsInfoDesc_2": {
"message": "Aby korzystać z API ChatGPT, potrzebujesz klucza API OpenAI ChatGPT i musisz wybrać model."
},
"prefsInfoDesc_3": {
"message": "Aby korzystać z integracji z Ollama, musisz skonfigurować lokalny serwer Ollama. Po uruchomieniu serwera wprowadź jego adres w odpowiednim polu w aplikacji. Aby zapewnić prawidłową komunikację między ThunderAI a serwerem Ollama pamiętaj o ustawieniu OLLAMA_ORIGINS=moz-extension://*."
},
"prefsInfoDesc_4": {
"message": "Aby korzystać z tej integracji, musisz skonfigurować lokalny serwer kompatybilny z API OpenAI, jak LM Studio. Po uruchomieniu serwera wprowadź jego adres w odpowiednim polu w aplikacji. Aby zapewnić prawidłową komunikację między ThunderAI a serwerem lokalnym pamiętaj o prawidłowym ustawieniu CORS."
},
"prefsInfoDesc_5": {
"message": "Pamiętaj, że możesz otworzyć menu ThunderAI używając skrótu klawiszowego CTRL+ALT+A."
},
"prefsInfoDesc_6": {
"message": "Możesz zmienić skrót klikając ikonę koła zębatego w prawym górnym rogu tej strony i wybierając \"Zarządzaj skrótami rozszerzenia\"."
},
"prefsDonation_1": {
"message": "Czy podoba Ci się ten dodatek?"
},
"prefsDonation_2": {
"message": "Rozważ wsparcie poprzez darowiznę!"
},
"backToOptionsText": {
"message": "Opcje"
},
"TranslateText": {
"message": "Czy chcesz pomóc w tłumaczeniu tego dodatku?"
},
"TranslateLink": {
"message": "Dowiedz się jak!"
},
"customPrompts_ExportAll": {
"message": "Eksportuj wszystkie polecenia"
},
"customPrompts_Import": {
"message": "Importuj nowe polecenia"
},
"importPrompts_confirmText": {
"message": "Zamierzasz zaimportować nowe polecenia."
},
"customPrompts_start_import": {
"message": "Rozpoczynanie importu własnych poleceń..."
},
"customPrompts_import_completed": {
"message": "Import własnych poleceń zakończony! Musisz kliknąć przycisk 'Zapisz wszystko', aby zapisać zmiany."
},
"importPrompts_invalidFile": {
"message": "Plik, który próbujesz zaimportować, nie jest prawidłowym plikiem poleceń."
},
"importPrompts_invalidPrompts": {
"message": "Plik, który próbujesz zaimportować, nie zawiera żadnych prawidłowych poleceń."
},
"currently_used_prompt": {
"message": "Aktualnie używana nazwa polecenia"
},
"customprompts_form_label_define_response_lang": {
"message": "Określ język odpowiedzi w poleceniu"
},
"prefs_Connection_type": {
"message": "Typ połączenia"
},
"prefs_Connection_type_ChatGPT_Web": {
"message": "Interfejs webowy ChatGPT"
},
"prefs_Connection_type_ChatGPT_API": {
"message": "API ChatGPT OpenAI"
},
"prefs_ChatGPT_API_Key": {
"message": "Klucz API ChatGPT"
},
"ChatGPT_Models": {
"message": "Modele ChatGPT"
},
"ChatGPT_Models_Fetch": {
"message": "Zaktualizuj listę modeli ChatGPT"
},
"ChatGPT_Models_Error_fetching": {
"message": "Błąd podczas pobierania modeli ChatGPT"
},
"Loading": {
"message": "Ładowanie..."
},
"error": {
"message": "Błąd ThunderAI!"
},
"chatgpt_empty_apikey": {
"message": "Nie dodałeś klucza API dla ChatGPT API. Proszę wprowadź go na stronie opcji."
},
"chatgpt_empty_model": {
"message": "Nie wybrałeś modelu dla ChatGPT API. Proszę wybierz jeden na stronie opcji."
},
"chagpt_api_send_button": {
"message": "Używając modelu"
},
"Debug": {
"message": "Debugowanie"
},
"prefs_OptionText_do_debug_info": {
"message": "Aktywuj system debugowania"
},
"prefs_Connection_type_Ollama_API": {
"message": "API Ollama (Lokalny LLM)"
},
"prefs_Connection_type_OpenAI_Comp_API": {
"message": "API kompatybilne z OpenAI"
},
"prefs_API_Host": {
"message": "Adres hosta"
},
"Ollama_Models": {
"message": "Modele Ollama"
},
"Ollama_Models_Fetch": {
"message": "Zaktualizuj listę modeli Ollama"
},
"Ollama_Models_Error_fetching": {
"message": "Błąd podczas pobierania modeli Ollama"
},
"API_Models_Error_NoModels": {
"message": "Nie znaleziono modeli"
},
"ollama_empty_host": {
"message": "Nie dodałeś adresu hosta dla API Ollama. Proszę wprowadź go na stronie opcji."
},
"ollama_empty_model": {
"message": "Nie wybrałeś modelu dla API Ollama. Proszę wybierz jeden na stronie opcji."
},
"error_connection_interrupted": {
"message": "Połączenie z serwerem zostało nieoczekiwanie przerwane"
},
"ollama_api_request_failed": {
"message": "Zapytanie do API Ollama nie powiodło się"
},
"chatgpt_api_request_failed": {
"message": "Zapytanie do API OpenAI ChatGPT nie powiodło się"
},
"WaitingServerResponse": {
"message": "Oczekiwanie na odpowiedź serwera"
},
"prefs_API_Host_Info": {
"message": "Taki jak"
},
"OpenAIComp_Models": {
"message": "Modele API kompatybilne z OpenAI"
},
"OpenAIComp_Models_Fetch": {
"message": "Zaktualizuj listę modeli API kompatybilnych z OpenAI"
},
"OpenAIComp_Models_Error_fetching": {
"message": "Błąd podczas pobierania modeli API kompatybilnych z OpenAI"
},
"OpenAIComp_empty_host": {
"message": "Nie dodałeś adresu hosta dla API kompatybilnego z OpenAI. Proszę wprowadź go na stronie opcji."
},
"OpenAIComp_empty_model": {
"message": "Nie wybrałeś modelu dla API kompatybilnego z OpenAI. Proszę wybierz jeden na stronie opcji."
},
"OpenAIComp_api_request_failed": {
"message": "Zapytanie do API kompatybilnego z OpenAI nie powiodło się"
},
"prefs_OpenAIComp_ChatName": {
"message": "Nazwa czatu"
},
"prefs_OpenAIComp_ChatName_Info": {
"message": "To jest nazwa, która będzie używana w czacie AI."
},
"StorageSpace": {
"message": "Całkowita zajęta przestrzeń dyskowa"
},
"SearchPrompt": {
"message": "Szukaj poleceń"
},
"prefs_OptionText_dynamic_menu_force_enter": {
"message": "Menu: natychmiastowe wysyłanie polecenia"
},
"prefs_OptionText_dynamic_menu_force_enter_info": {
"message": "Jeśli zaznaczone, użycie skrótu klawiszowego CTRL+ALT+A automatycznie wyśle zaznaczone polecenie z menu. W przeciwnym razie nazwa polecenia zostanie wyświetlona użytkownikowi, wymagając kolejnego naciśnięcia klawisza Enter, aby je wysłać."
},
"prefs_OptionText_chatgpt_win_dims_info": {
"message": "Ustaw na 0, jeśli nie chcesz określać rozmiaru okna."
},
"prefs_OpenAIComp_API_Key": {
"message": "Klucz API kompatybilny z OpenAI"
},
"Optional": {
"message": "Opcjonalne"
},
"OpenChatGPTTab": {
"message": "Otwórz kartę ChatGPT"
},
"OpenChatGPTTab_Info": {
"message": "W przypadku problemów z logowaniem w oknie ThunderAI, otwórz ChatGPT w nowej karcie używając przycisku po prawej stronie, zaloguj się, następnie zamknij kartę i kontynuuj korzystanie z ThunderAI."
},
"placeholder_mail_text_body": {
"message": "Treść wiadomości"
},
"placeholder_mail_html_body": {
"message": "Treść wiadomości HTML"
},
"placeholder_mail_subject": {
"message": "Temat wiadomości"
},
"placeholder_selected_text": {
"message": "Zaznaczony tekst"
},
"placeholder_additional_text": {
"message": "Dodatkowy tekst"
},
"placeholder_junk_score": {
"message": "Wynik spam"
},
"placeholder_recipients": {
"message": "Odbiorcy"
},
"placeholder_cc_list": {
"message": "Lista DW"
},
"placeholder_author": {
"message": "Autor"
},
"prefs_OptionText_placeholders_use_default_value": {
"message": "Symbole zastępcze: użyj wartości domyślnej"
},
"prefs_OptionText_placeholders_use_default_value_info": {
"message": "Jeśli zaznaczone, symbole zastępcze zostaną wypełnione wartościami domyślnymi, gdy nie podano wartości. W przeciwnym razie symbole zastępcze pozostaną na miejscu."
},
"prefs_OptionText_max_prompt_length": {
"message": "Maksymalna długość polecenia"
},
"prefs_OptionText_max_prompt_length_Info": {
"message": "To jest maksymalna liczba znaków, która może być użyta w poleceniu. W przeciwnym razie zostanie wyświetlony komunikat o błędzie. Wartość nie jest edytowalna dla interfejsu webowego ChatGPT. Ustaw na zero, aby wyłączyć sprawdzanie."
},
"prefs_OptionText_chatgpt_web_model": {
"message": "Model interfejsu webowego ChatGPT"
},
"prefs_OptionText_chatgpt_web_model_info": {
"message": "To jest model, który będzie wymuszony dla interfejsu webowego ChatGPT. Jeśli nie zostanie określony lub podano nieprawidłowy, domyślny model zostanie ustawiony przez ChatGPT na stronie. To ustawienie nie będzie działać z darmowym kontem ChatGPT."
},
"prefs_OptionText_chatgpt_web_tempchat": {
"message": "Tymczasowy czat w interfejsie webowym ChatGPT"
},
"prefs_OptionText_chatgpt_web_tempchat_info": {
"message": "Jeśli zaznaczone, w interfejsie webowym ChatGPT będzie używany tymczasowy czat."
},
"chatgpt_btn_model": {
"message": "Użyj bieżącego modelu"
},
"AllowedValues": {
"message": "Dozwolone wartości"
},
"prefs_OptionText_btnManagePrompts_infoline": {
"message": "Możesz użyć dodatkowych symboli zastępczych danych."
},
"prefs_OptionText_openai_comp_use_v1": {
"message": "Zachowaj kompatybilność z \"v1\""
},
"prefs_OptionText_openai_comp_use_v1_info": {
"message": "Jeśli zaznaczone, segment \"v1\" w ścieżce wywołań API zostanie zachowany, np. \"http://localhost:1234/v1/chat/completions\"."
},
"prefs_OptionText_owl_warning": {
"message": "Wygląda na to, że przynajmniej jedno z Twoich kont używa dodatku Owl for Exchange. Istnieje znany problem między Thunderbirdem a Owl, który jest obecnie rozwiązywany. Na ten moment możesz używać ThunderAI podczas pisania e-maili, ale nie podczas ich czytania."
},
"prompt_reply_full_text": {
"message": "Odpowiedz na poniższy e-mail. Odpowiedz wyłącznie wymaganym tekstem, bez dodatkowych komentarzy ani innego tekstu."
},
"prompt_reply_additional_text": {
"message": "Nie dodawaj tematu do odpowiedzi."
},
"reply_same_lang": {
"message": "Odpowiedz w tym samym języku."
},
"sign_msg_as": {
"message": "Podpisz wiadomość jako"
},
"prompt_reply_advanced_full_text": {
"message": "Odpowiedz na poniższy e-mail \"{%selected_text%}\", biorąc pod uwagę, że jest to pełny wątek e-maili \"{%mail_html_body%}\". Odpowiedz wyłącznie wymaganym tekstem, bez dodatkowych komentarzy ani innego tekstu."
},
"prompt_rewrite_full_text": {
"message": "Przepisz poniższy tekst, aby był bardziej uprzejmy. Odpowiedz wyłącznie przepisanym tekstem, bez dodatkowych komentarzy ani innego tekstu."
},
"prompt_rewrite_formal_full_text": {
"message": "Przepisz poniższy tekst, aby był bardziej formalny. Odpowiedz wyłącznie przepisanym tekstem, bez dodatkowych komentarzy ani innego tekstu."
},
"prompt_classify_full_text": {
"message": "Sklasyfikuj poniższy tekst pod względem uprzejmości, serdeczności, formalności, stanowczości, obraźliwości, podając procent dla każdej kategorii. Odpowiedz wyłącznie kategorią i wynikiem, bez dodatkowych komentarzy ani innego tekstu."
},
"prompt_translate_this_full_text": {
"message": "Przetłumacz poniższą wiadomość e-mail na język {%thunderai_translate_lang%}.\n\nZasady:\n- Przetłumacz zarówno temat, jak i treść wiadomości.\n- Zwróć wynik jako obiekt JSON z trzema polami: \"subject\", \"body\" i \"status\".\n- Jeśli tłumaczenie zostało wykonane, status wynosi 1.\n- Jeśli wiadomość e-mail jest napisana w jednym z tych języków \"{%thunderai_translate_exclude_lang%}\" lub w języku {%thunderai_translate_lang%}, zwróć pusty ciąg znaków dla treści i tematu oraz ustaw status na -1.\n- Nie dodawaj wyjaśnień, notatek ani żadnego tekstu poza formatem JSON.\n\nTemat wiadomości: {%mail_subject%}\n\nTreść wiadomości: {%mail_html_body%}\n\nWygeneruj odpowiedź wyłącznie w formacie JSON. Wynikiem powinien być tylko obiekt JSON. Oto przykład formatu JSON, którego należy użyć:\n{\n\"subject\": \"subject translation\",\n\"body\": \"body translation\",\n\"status\": \"status result\"\n}"
},
"prompt_this_full_text": {
"message": "Odpowiedz wyłącznie wymaganym tekstem, bez dodatkowych komentarzy ani innego tekstu."
},
"prefs_OptionText_add_tags": {
"message": "Dodaj tagi do e-maili"
},
"prefs_OptionText_add_tags_Info": {
"message": "Jeśli zaznaczone, element zostanie uwzględniony w menu w celu zastosowania tagów do e-maili."
},
"prompt_add_tags": {
"message": "Dodaj tagi do tego e-maila"
},
"prompt_add_tags_full_text": {
"message": "Przeanalizuj poniższy tekst e-maila i wygeneruj tablicę JSON z tagami podsumowującymi jego treść. Użyj tematów, kluczowych zagadnień i odpowiednich określeń jako tagów. Upewnij się, że tagi są zwięzłe i adekwatne do treści wiadomości.\nTekst e-maila: {%mail_text_body%}\nWeź pod uwagę następujące szczegóły w kontekście:\n- Nadawca: {%author%}\n- Odbiorcy: {%recipients%}\n- Lista DW: {%cc_list%}\n- Temat e-maila: {%mail_subject%}\nNa podstawie treści wiadomości i kontekstu wygeneruj tagi, pomijając zbędne informacje lub nieistotne szczegóły.\nWygeneruj odpowiedź wyłącznie w formacie JSON. Wynik powinien zawierać jedynie tablicę tagów w formacie JSON, bez dodatkowych komentarzy czy tekstu. Oto przykład wymaganego formatu:\n{\n\"tags\": [\"tag1\", \"tag2\", \"tag3\", \"tag4\", \"tag5\"]\n}"
},
"placeholder_tags_current_email": {
"message": "Tagi e-maila"
},
"placeholder_tags_full_list": {
"message": "Istniejące tagi"
},
"prefs_OptionText_add_tags_maxnum": {
"message": "Maksymalna liczba tagów"
},
"prefs_OptionText_add_tags_maxnum_Info": {
"message": "Maksymalna liczba tagów proponowanych przez AI. Ustaw na 0, jeśli nie chcesz ograniczać liczby tagów."
},
"prompt_add_tags_maxnum": {
"message": "Ogranicz liczbę tagów do"
},
"prefs_OptionText_add_tags_hide_exclusions": {
"message": "Ukryj wykluczone tagi"
},
"prefs_OptionText_add_tags_hide_exclusions_Info": {
"message": "Jeśli zaznaczone, tagi znajdujące się na liście wykluczeń zostaną ukryte w oknie potwierdzenia."
},
"prefs_OptionText_btnManageTagsInfo": {
"message": "Zarządzaj ustawieniami tagów"
},
"AddTags_PageTitle": {
"message": "Zarządzaj Ustawieniami Tagów"
},
"AddTags_info_default": {
"message": "Na tej stronie możesz zmodyfikować domyślne polecenie używane do dodawania tagów do e-maili i zarządzać listą wykluczeń."
},
"AddTags_prompt_text_title": {
"message": "Aktualny tekst polecenia"
},
"AddTags_excl_list_title": {
"message": "Lista wykluczeń"
},
"AddTags_excl_list_infoline": {
"message": "Jest to lista tagów, które nie mogą być dodane do e-maili."
},
"save": {
"message": "Zapisz"
},
"addtags_info_additional_statements": {
"message": "To stwierdzenie zostanie dodane na końcu polecenia:"
},
"reset_default": {
"message": "Przywróć domyślne"
},
"addtags_excl_list_infoline2": {
"message": "Dodaj jedno słowo na linię lub oddzielone przecinkiem."
},
"addtags_dialog_title": {
"message": "Dodaj tagi do e-maila"
},
"addtags_exclude_tag": {
"message": "Wyklucz tag"
},
"addtags_no_tags_received": {
"message": "Nie otrzymano tagów od AI."
},
"addtags_no_valid_tags": {
"message": "Nie znaleziono poprawnych tagów po przefiltrowaniu z listą wykluczeń."
},
"thunderai_error_title": {
"message": "Błąd ThunderAI"
},
"thunderai_warning_title": {
"message": "Ostrzeżenie ThunderAI"
},
"prefs_OptionText_add_tags_first_uppercase": {
"message": "Pierwsza litera wielka"
},
"prefs_OptionText_add_tags_first_uppercase_Info": {
"message": "Jeśli zaznaczone, etykieta tagów będzie ustawiona na małe litery z wyjątkiem pierwszej wielkiej litery."
},
"AddTags_prompt_prefs_title": {
"message": "Opcje dodawania tagów"
},
"prefs_SurveyLinkText": {
"message": "Podziel się swoją opinią i pomóż nam ulepszyć ThunderAI!"
},
"prefs_SurveyLinkText2": {
"message": "Kliknij tutaj, to zajmuje tylko minutę!"
},
"prefs_OpenAIComp_ForceModel": {
"message": "Ręcznie wprowadź model"
},
"OpenAIComp_force_model_ask": {
"message": "Wprowadź tutaj nazwę modelu, którego chcesz użyć."
},
"prefs_OptionText_add_tags_force_lang": {
"message": "Wymuś język"
},
"prefs_OptionText_add_tags_force_lang_Info": {
"message": "Jeśli zaznaczone, język tagów zostanie wymuszony na zgodny z językiem określonym na stronie opcji ThunderAI, jeśli został zdefiniowany."
},
"prompt_add_tags_force_lang": {
"message": "Tagi muszą być napisane w"
},
"GoogleGemini_Models_Fetch": {
"message": "Zaktualizuj listę modeli Google Gemini"
},
"GoogleGemini_Models_Error_fetching": {
"message": "Błąd podczas próby pobrania modeli Google Gemini"
},
"google_gemini_api_request_failed": {
"message": "Połączenie do interfejsu API Google Gemini nie powiodło się"
},
"google_gemini_empty_apikey": {
"message": "Nie dodałeś klucza API dla API Google Gemini. Wstaw go na stronie opcji."
},
"google_gemini_empty_model": {
"message": "Nie wybrałeś modelu interfejsu API Google Gemini. Wybierz jedną na stronie opcji."
},
"GoogleGemini_SystemInstruction": {
"message": "Instrukcja systemowa"
},
"GoogleGemini_SystemInstruction_Info": {
"message": "Ustawiając instrukcję systemową, dajesz modelowi dodatkowy kontekst umożliwiający zrozumienie zadania, zapewnianie bardziej dostosowanych odpowiedzi i przestrzeganie określonych wytycznych dotyczących zapytania, które zostanie wysłane."
},
"ChatGPT_Developer_Messages": {
"message": "Wiadomości programistów"
},
"ChatGPT_Developer_Messages_Info": {
"message": "Konfigurując komunikaty dla programistów, dajesz modelowi dodatkowy kontekst umożliwiający zrozumienie zadania, zapewnianie bardziej dostosowanych odpowiedzi i przestrzeganie określonych wytycznych dotyczących zapytania, które zostanie wysłane."
},
"prefs_OptionText_btnManagePrompts_infoline3": {
"message": "Aby wyświetlić listę dostępnych tagów, możesz użyć obiektu danych {%tags_full_list%} w zapytaniu. Za pomocą odpowiedniego zapytania można następnie wymusić wybranie tagów tylko z listy już istniejących."
},
"placeholder_mail_typed_text": {
"message": "Wpisano tekst przed cytowaną treścią wiadomości"
},
"prompt_get_calendar_event": {
"message": "Dodaj nowe wydarzenie w kalendarzu"
},
"prompt_get_calendar_event_full_text": {
"message": "Wyodrębnij wszystkie istotne szczegóły wymagane do wygenerowania wydarzenia w kalendarzu z poniższego tekstu. Wyodrębnione informacje powinny obejmować:\n- Tytuł wydarzenia\n- Datę i godzinę rozpoczęcia (w tym strefę czasową, jeśli została określona)\n- Datę i godzinę zakończenia (w tym strefę czasową, jeśli została określona)\n- Cały dzień (jeśli jest podany)\n- Uczestnicy\nUpewnij się, że dane są sformatowane w sposób jasny i spójny, tak aby można go bezpośrednio wykorzystać do utworzenia wydarzenia w kalendarzu.\nJeśli istnieją odniesienia do czasu względnego, pamiętaj, że data i godzina wysłania wiadomości e-mail to „{%mail_datetime%}”. Oblicz datę i godzinę rozpoczęcia na podstawie tego odniesienia. Jeśli obliczona data i godzina rozpoczęcia są wcześniejsze niż „{%current_datetime%}”, oblicz ponownie datę i godzinę rozpoczęcia, stosując jako podstawę „{%current_datetime%}”.\nJeśli wydarzenie jest całodniowe, data zakończenia (endDate) musi przypadać na dzień po dacie rozpoczęcia (startDate), a godzina musi być ustawiona na \"T000000\".\nJeśli czas trwania nie jest określony, ustaw go na jedną godzinę.\nOto uczestnicy: {%author%}, {%recipients%}, {%cc_list%}. Jeśli jest obecny, wyklucz mój adres: {%account_email_address%}.\nJeśli nie możesz uzyskać co najmniej jednej z wymaganych informacji, w odpowiedzi wpisz pusty ciąg znaków.\nWygeneruj odpowiedź tylko w formacie JSON. Nie dołączaj żadnego dodatkowego tekstu ani wyjaśnień; podaj tylko JSON. Oto format, którego należy użyć:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Tutaj podsumowanie wydarzenia w kalendarzu\",\n\"forceAllDay\": false\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nOto tekst:\"{%mail_text_body_or_selected%}\""
},
"prefs_OptionText_get_calendar_event": {
"message": "Dodaj nowe wydarzenie w kalendarzu z zaznaczonego tekstu"
},
"prefs_OptionText_get_calendar_event_Info": {
"message": "Jeśli zaznaczone, pozycja zostanie uwzględniona w menu, aby uzyskać informacje o wydarzeniu w kalendarzu z zaznaczonego tekstu."
},
"prefs_OptionText_btnManageCalendarEventInfo": {
"message": "Zarządzaj ustawieniami wydarzeń w kalendarzu"
},
"GetCalendarEvent_PageTitle": {
"message": "Zarządzaj ustawieniami wydarzeń w kalendarzu"
},
"GetCalendarEvent_info_default": {
"message": "Na tej stronie możesz zmodyfikować domyślny monit używany do pobierania wydarzenia w kalendarzu z zaznaczonego tekstu."
},
"GetCalendarEvent_prompt_text_title": {
"message": "Bieżący tekst zapytania"
},
"prefs_OptionText_AdvancedPromptResponse_infoline2": {
"message": "Możesz zmienić monit według własnego uznania, ale odpowiedź otrzymana od AI musi być w formacie JSON, jak określono w domyślnym zapytaniu!"
},
"prefs_OptionText_get_calendar_event_Sparks_not_present": {
"message": "Aby korzystać z funkcji wydarzeń w kalendarzu, zainstaluj dodatek ThunderAI Sparks."
},
"prefs_OptionText_download_now": {
"message": "Pobierz teraz ThunderAI Sparks!"
},
"placeholder_mail_datetime": {
"message": "Data i godzina wysłania wiadomości e-mail"
},
"placeholder_current_datetime": {
"message": "Bieżąca data i godzina"
},
"calendar_getting_data_error": {
"message": "Błąd podczas pobierania wydarzeń z kalendarza"
},
"calendar_opening_dialog_error": {
"message": "Błąd podczas otwierania okna wydarzenia w kalendarzu"
},
"GoogleGemini_Models": {
"message": "Modele Google Gemini",
"description": "Lista modeli interfejsu API Google Gemini"
},
"prefs_Connection_type_Google_Gemini_API": {
"message": "Interfejs API Google Gemini",
"description": "Typ połączenia: Google Gemini API"
},
"prefs_GoogleGemini_API_Key": {
"message": "Klucz API Google Gemini",
"description": "Klucz API dla interfejsu API Google Gemini"
},
"Subject": {
"message": "Temat"
},
"Report_Date": {
"message": "Data raportu"
},
"SpamFilter_prompt_prefs_title": {
"message": "Opcje filtra spamu"
},
"Explanation": {
"message": "Wyjaśnienie"
},
"Spam_Value": {
"message": "Wartość spamu"
},
"placeholder_thunderai_def_lang": {
"message": "Domyślny język zgodnie z opcjami ThunderAI."
},
"prompt_spamfilter": {
"message": "Wykrywanie e-maili spamowych"
},
"prefs_OptionText_add_tags_auto_force_existing": {
"message": "Wymuś użycie istniejących tagów podczas automatycznego tagowania lub korzystania z menu kontekstowego"
},
"prefs_OptionText_add_tags_auto": {
"message": "Automatycznie dodawaj tagi"
},
"prefs_OptionText_spamfilter_threshold": {
"message": "Próg spamu"
},
"placeholder_thunderai_def_sign": {
"message": "Domyślny podpis zgodnie z opcjami ThunderAI."
},
"Moved_to_Spam": {
"message": "Przeniesiono do spamu"
},
"no_string": {
"message": "Nie"
},
"prefs_OptionText_add_tags_auto_force_existing_Info": {
"message": "Jeśli zaznaczone, AI doda tylko istniejące tagi do nowo odebranych e-maili i nie utworzy nowych."
},
"SpamFilter_info_default": {
"message": "Na tej stronie możesz edytować domyślny prompt używany do wykrywania e-maili spamowych."
},
"From": {
"message": "Od"
},
"spamfilter_threshold_too_low": {
"message": "Próg spamu jest zbyt niski! Prawdopodobnie oznaczysz zbyt wiele e-maili jako spam!"
},
"prefs_OptionText_btnManageSpamFilterInfo": {
"message": "Zarządzaj ustawieniami filtra spamu"
},
"yes_string": {
"message": "Tak"
},
"prefs_OptionText_spamfilter": {
"message": "Automatyczny filtr spamu"
},
"SpamReport_Title": {
"message": "Raporty filtra spamu"
},
"prefs_OptionText_openai_comp_info_remote": {
"message": "Tutaj możesz również wprowadzić adres zdalnego serwera."
},
"prefs_OptionText_spamfilter_Info": {
"message": "Jeśli zaznaczone, ThunderAI automatycznie przeniesie e-maile spamowe do folderu spamu."
},
"prefs_OptionText_add_tags_auto_Info": {
"message": "Jeśli zaznaczone, AI automatycznie doda tagi do nowo odebranych e-maili."
},
"Date": {
"message": "Data"
},
"prefs_OptionText_add_tags_auto_only_inbox": {
"message": "Dodawaj tagi tylko do e-maili w skrzynce odbiorczej"
},
"SpamFilter_prompt_text_title": {
"message": "Bieżący tekst prompta"
},
"prefs_OptionText_add_tags_auto_only_inbox_Info": {
"message": "Jeśli zaznaczone, AI doda tagi tylko do e-maili odebranych w folderze skrzynki odbiorczej."
},
"SpamFilter_PageTitle": {
"message": "Zarządzaj ustawieniami filtra spamu"
},
"prompt_spamfilter_full_text": {
"message": "Przeanalizuj poniższy e-mail i określ, czy jest to spam, czy nie. Weź pod uwagę takie czynniki jak podejrzane słowa kluczowe, nadmierny język promocyjny, wprowadzające w błąd tematy, prośby o podanie danych osobowych i nietypowe adresy nadawców.\nPodaj wartość od 0 (nie spam) do 100 (spam) oraz wyjaśnienie nie dłuższe niż 10 słów.\nW przypadku braku danych wiadomości ustaw wartość na 0 (nie spam) i podaj powód.\nWygeneruj odpowiedź tylko w formacie JSON. Nie dodawaj żadnego dodatkowego tekstu ani wyjaśnień; podaj tylko JSON. Oto format, który należy użyć:\n{\n\"explanation\": \"Krótkie wyjaśnienie twojego rozumowania\",\n\"spamValue\": <liczba całkowita od 0 do 100>\n}\nOto informacje o e-mailu:\nNadawca: \"{%author%}\"\nTemat: \"{%mail_subject%}\"\nTreść HTML: \"{%mail_html_body%}\""
},
"prefs_OptionText_spamfilter_threshold_Info": {
"message": "Jeśli wartość zwrócona przez AI przekroczy ten próg, e-mail zostanie przeniesiony do folderu spamu."
},
"spamfilter_threshold_zero": {
"message": "Próg spamu wynosi zero! Oznaczysz wszystkie e-maile jako spam!"
},
"spamfilter_no_reports": {
"message": "Nie przeskanowano jeszcze żadnych wiadomości pod kątem spamu. Tutaj znajdziesz listę ostatnich 100 raportów o spamie tylko dla bieżącej sesji."
},
"customPrompts_form_label_use_diff_viewer_title": {
"message": "Widok zmian może zostać wybrany, kiedy wybrana akcja to \"Tekst zastępczy\"."
},
"empty": {
"message": "Ten tekst zastępczy nie dodaje żadnego tekstu, tylko zapobiega przed automatycznym wstawianiem treści emaila na końcu prompta."
},
"prefs_OptionText_calendar_enforce_timezone_Info": {
"message": "Jeżeli zaznaczone, określona strefa czasowa będzie wymuszana dla wydarzeń w kalendarzu."
},
"prefs_OptionText_get_calendar_event_Sparks_wrong_version": {
"message": "By używać wydarzeń w kalendarzu prosimy zainstalować zaktualizowaną wersję dodatku ThunderAI Sparks."
},
"customPrompts_form_label_use_diff_viewer": {
"message": "Włącz podgląd zmian"
},
"placeholder_folder_name": {
"message": "Nazwa folderu"
},
"placeholder_folder_path": {
"message": "Ścieżka folderu"
},
"Select_your_timezone": {
"message": "Wybierz swoją strefę czasową"
},
"get_calendar_event_prompt_prefs_title": {
"message": "Opcje Wydarzeń Kalendarza"
},
"prefs_OptionText_calendar_enforce_timezone": {
"message": "Wymuś konkretną strefę czasową"
},
"placeholder_account_email_address": {
"message": "Adres email konta"
},
"prompt_proofread_this": {
"message": "Skoryguj ten email"
},
"prompt_proofread_this_full_text": {
"message": "Skoryguj poniższy email, oraz popraw wszelkie błędy ortograficzne lub gramatyczne. Niech twoja odpowiedź zawiera wyłącznie skorygowany tekst, bez żadnych komentarzy lub dodatkowego tekstu.\n\n\"{%mail_typed_text%}\""
},
"placeholder_mail_quoted_text": {
"message": "Zacytowany tekst w treści maila"
},
"placeholder_selected_html": {
"message": "Zaznaczony HTML"
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,173 +0,0 @@
{
"prompt_lang": {
"message": "Responder em"
},
"prompt_selection_needed": {
"message": "Para avançar tens que selecionar texto!"
},
"customPrompts_form_label_need_custom_text": {
"message": "Pedir texto adicional"
},
"customPrompts_form_label_use_diff_viewer_title": {
"message": "O visualizador de comparação de texto pode ser selecionado quando a ação escolhida é \"Substituir texto\"."
},
"chatgpt_win_job_completed": {
"message": "Concluído!"
},
"chatgpt_win_job_completed_select": {
"message": "Seleciona o texto que queres usar e carrega no botão."
},
"prompt_reply_advanced": {
"message": "Responder a esta sequência"
},
"more_info_string": {
"message": "Mais informação"
},
"menu_title": {
"message": "IA"
},
"prompt_classify": {
"message": "Classificar"
},
"customPrompts_add_to_menu_always": {
"message": "Sempre"
},
"prompt_reply": {
"message": "Responder a este email"
},
"prompt_rewrite_polite": {
"message": "Rescrever educadamente"
},
"prompt_rewrite_formal": {
"message": "Rescrever formalmente"
},
"prompt_translate_this": {
"message": "Traduzir isto"
},
"customPrompts_reindexing_list": {
"message": "A redefinir os índices da lista..."
},
"customPrompts_reloading_menus": {
"message": "A recarregar menus..."
},
"customPrompts_form_label_ID": {
"message": "ID"
},
"customPrompts_form_label_ID_rules": {
"message": "Deve ser único, letra minúscula e sem espaços"
},
"customPrompts_form_label_Action": {
"message": "Ação"
},
"customPrompts_form_label_need_selected": {
"message": "Necessário selecionar texto"
},
"customPrompts_form_label_need_signature": {
"message": "Adicionar sempre assinatura"
},
"customPrompts_form_label_enabled": {
"message": "Ativo"
},
"customPrompts_form_required_fields": {
"message": "Campos obrigatórios"
},
"customPrompts_btnEdit": {
"message": "Editar"
},
"customPrompts_btnCancel": {
"message": "Cancelar"
},
"customPrompts_btnOK": {
"message": "OK"
},
"customPrompts_btnDelete": {
"message": "Apagar"
},
"customPrompts_btnDelete_confirmText": {
"message": "Tens a certeza que queres apagar este item?"
},
"customPrompts_unsaved_changes": {
"message": "Existem alterações por guardar!"
},
"btnSaveAll_string": {
"message": "Guardar Tudo"
},
"btnNew_string": {
"message": "Adicionar Novo"
},
"customPrompts_add_to_menu": {
"message": "Adicionar ao menu"
},
"customPrompts_add_to_menu_reading": {
"message": "A ler um email"
},
"customPrompts_substitute_text": {
"message": "Substituir texto"
},
"customPrompts_managePrompts_info_default_3": {
"message": "Se estiver tudo certo, depois de importar carrega no botão \"Guardar Tudo\"."
},
"extensionDescription": {
"message": "Usa o ChatGPT, Google Gemini, Claude ou Ollama para melhorar os teus emails!"
},
"customPrompts_form_label_Name": {
"message": "Nome"
},
"customPrompts_add_to_menu_composing": {
"message": "A compor um email"
},
"customPrompts_form_label_use_diff_viewer": {
"message": "Ativar o visualizador de comparação de texto"
},
"chatgpt_win_working": {
"message": "Trabalho em curso..."
},
"customPrompts_managePrompts": {
"message": "Gerir prompts"
},
"customPrompts_managePrompts_info_default": {
"message": "Os prompts padrão não são editáveis. Podes desativá-los, copiar o texto do prompt e colá-lo num novo, para criar uma versão modificada."
},
"customPrompts_managePrompts_info_default_2": {
"message": "Podes importar e exportar prompts. Prompts existentes com o mesmo ID serão escritos por cima. Prompts com um ID novo serão adicionados."
},
"customPrompts_start_saving": {
"message": "A guardar prompts..."
},
"customPrompts_filtering_prompts": {
"message": "A filtrar prompts..."
},
"customPrompts_saving_default_prompts": {
"message": "A guardar prompts padrão..."
},
"customPrompts_saving_custom_prompts": {
"message": "A guardar prompts personalizados..."
},
"customPrompts_saved": {
"message": "Prompts guardados!"
},
"customPrompts_btnAddNewCommit": {
"message": "Adicionar o prompt"
},
"customPrompts_do_reply": {
"message": "Responder"
},
"prompt_reply_custom_command": {
"message": "Responder com o comando..."
},
"chatgpt_win_close": {
"message": "Fechar"
},
"chatgpt_win_get_answer": {
"message": "Utilize a resposta escolhida"
},
"customPrompts_close_button": {
"message": "Botão de Fechar"
},
"chatgpt_btn_retry": {
"message": "Repetir"
},
"chatgpt_win_send": {
"message": "Enviar"
}
}

View file

@ -1,230 +0,0 @@
{
"menu_title": {
"message": "IA"
},
"more_info_string": {
"message": "Informații"
},
"customPrompts_form_label_Action": {
"message": "Acțiune"
},
"customPrompts_form_label_need_signature": {
"message": "Adaugă întotdeauna semnatura"
},
"extensionDescription": {
"message": "Folosește ChatGPT, Google Gemini, Claude sau Ollama pentru îmbunătățirea e-mailurilor tale!"
},
"prompt_lang": {
"message": "Răspunde în"
},
"prompt_reply": {
"message": "Răspunde la acest e-mail"
},
"prompt_reply_advanced": {
"message": "Răspunde la această conversație"
},
"prompt_reply_custom_command": {
"message": "Răspunde cu instrucțiuni suplimentare..."
},
"prompt_rewrite_polite": {
"message": "Rescrie politicos"
},
"prompt_rewrite_formal": {
"message": "Rescrie formal"
},
"prompt_classify": {
"message": "Clasifica"
},
"prompt_translate_this": {
"message": "Tradu"
},
"prompt_selection_needed": {
"message": "Pentru a putea continua trebuie selectat un text!"
},
"customPrompts_managePrompts": {
"message": "Administrare prompturi"
},
"customPrompts_managePrompts_info_default": {
"message": "Prompturile predefinite nu pot fi modificate. Însă poți să le dezactivezi, să le copiezi și, în final, să creezi o nouă versiune a lor."
},
"customPrompts_form_label_ID": {
"message": "ID"
},
"msg_prompt_too_long": {
"message": "Textul pe care l-ai furnizat este pre lung. Este necesară reducerea lui."
},
"prefsDonation_2": {
"message": "Consideră o donație!"
},
"customPrompts_managePrompts_info_default_2": {
"message": "Poți importa și exporta prompturile. Prompturile existente cu același ID vor fi suprascrise. Prompturile cu ID-uri noi vor fi adăugate."
},
"customPrompts_managePrompts_info_default_3": {
"message": "Dacă totul este corect, dupa import apasă pe butonul 'Salvează Tot'."
},
"customPrompts_saved": {
"message": "Prompturile au fost salvate!"
},
"customPrompts_form_label_Name": {
"message": "Nume"
},
"customPrompts_form_label_Text": {
"message": "Traducere"
},
"customPrompts_form_label_need_selected": {
"message": "Este necesară selecția unui text"
},
"customPrompts_form_label_need_custom_text": {
"message": "Solicită text suplimentar"
},
"customPrompts_form_required_fields": {
"message": "Câmpuri necesare"
},
"customPrompts_btnCancel": {
"message": "Renunță"
},
"customPrompts_btnOK": {
"message": "OK"
},
"customPrompts_btnDelete": {
"message": "Șterge"
},
"customPrompts_btnDelete_confirmText": {
"message": "Ești sigur că dorești ștergerea acestui element?"
},
"customPrompts_unsaved_changes": {
"message": "Există schimbări nesalvate!"
},
"btnSaveAll_string": {
"message": "Salvează Tot"
},
"btnNew_string": {
"message": "Adaugă"
},
"customPrompts_btnAddNewCommit": {
"message": "Adaugă Promptul"
},
"customPrompts_add_to_menu": {
"message": "Adaugă la meniu"
},
"customPrompts_add_to_menu_always": {
"message": "Întotdeauna"
},
"customPrompts_close_button": {
"message": "Buton de închidere"
},
"customPrompts_substitute_text": {
"message": "Înlocuiește textul"
},
"chatgpt_win_job_completed": {
"message": "Finalizat!"
},
"chatgpt_win_job_completed_select": {
"message": "Selectează textul pe care vrei să-l folosești și apasă butonul."
},
"chatgpt_win_get_answer": {
"message": "Folosește răspunsul selectat"
},
"chatgpt_win_close": {
"message": "Închide"
},
"chatgpt_btn_retry": {
"message": "Reîncearcă"
},
"chatgpt_sendbutton_not_found_error": {
"message": "Apasă butonul de trimitere pentru a transmite promptul."
},
"chatgpt_win_custom_text": {
"message": "Introdu aici textul suplimentar pentru prompt."
},
"chatgpt_win_send": {
"message": "Trimite"
},
"prefs_status_page": {
"message": "starea serviciului"
},
"prefs_OptionText_chatgpt_win_text": {
"message": "Dimensiunile ferestrei de dialog IA"
},
"prefs_OptionText_chatgpt_win_height": {
"message": "Înălțime"
},
"prefs_OptionText_chatgpt_win_width": {
"message": "Lățime"
},
"prefs_OptionText_default_sign_name": {
"message": "Numele semnăturii implicite"
},
"prefs_OptionText_default_chatgpt_lang": {
"message": "Limba implicită pentru răspunsuri"
},
"prefs_OptionText_reply_all": {
"message": "Răspunde tuturor"
},
"prefs_OptionText_reply_sender": {
"message": "Răspunde expeditorului"
},
"prefs_OptionText_reply_type": {
"message": "Tipul răspunsului"
},
"prefs_OptionText_btnManagePrompts": {
"message": "Administrează prompturile"
},
"prompt_this": {
"message": "Întreabă"
},
"customPrompts_form_label_ID_rules": {
"message": "Trebuie să fie unic, cu litere mici și fără spații"
},
"customPrompts_form_label_enabled": {
"message": "Activat"
},
"customPrompts_form_label_use_diff_viewer": {
"message": "Activează compararea de text"
},
"customPrompts_form_label_use_diff_viewer_title": {
"message": "Comparatorul de text poate fi selectat când acțiunea este setată pe \"Înlocuire text\"."
},
"customPrompts_btnEdit": {
"message": "Editează"
},
"customPrompts_add_to_menu_reading": {
"message": "Citirea unui e-mail"
},
"customPrompts_add_to_menu_composing": {
"message": "Compunerea unui e-mail"
},
"customPrompts_do_reply": {
"message": "Răspunde"
},
"chatgpt_win_working": {
"message": "În lucru..."
},
"chatgpt_textarea_not_found_error": {
"message": "Se pare că pagina ChatGPT durează prea mult să se încarce. Dacă încărcarea se termină, faceți clic pe butonul din dreapta. Dacă problema persistă, vă rugăm să verificați starea serviciului."
},
"chatgpt_user_not_logged_in": {
"message": "Nu ești conectat la ChatGPT. Te rugăm să te conectezi cu datele tale de autentificare, să închizi fereastra ChatGPT și apoi să repeți acțiunea încercată. Vei rămâne conectat ulterior."
},
"chatgpt_win_model_warning": {
"message": "Din anumite motive, nu este posibil să verificăm dacă este încărcat modelul corect. Deocamdată, puteți apăsa butonul albastru pentru a continua."
},
"chatgpt_force_completion": {
"message": "închidere forțată"
},
"chatgpt_force_completion_title": {
"message": "Apăsați aici pentru a afișa butonul 'Utilizați ultimul răspuns' dacă ChatGPT a terminat treaba, dar butonul nu a apărut."
},
"prefs_OptionText_release_notes": {
"message": "Note de Lansare"
},
"prefs_OptionText_reply_type_Info": {
"message": "Acesta este tipul de răspuns implicit care va fi utilizat atunci când răspundeți la un e-mail. Puteți alege celălalt tip ulterior în fereastra de răspuns."
},
"prefsInfoTitle": {
"message": "Informații Importante"
},
"prefsInfoDesc_1": {
"message": "S-ar putea ca interfața web ChatGPT să se modifice într-un mod care să perturbe funcționarea addon-ului. Verificați pagina \"Starea serviciului\" accesibilă prin legătura din partea de jos a acestei pagini. De asemenea, rețineți că prima dată când utilizați ThunderAI, trebuie să vă conectați la ChatGPT."
}
}

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
{}

File diff suppressed because it is too large Load diff

View file

@ -1,20 +0,0 @@
{
"extensionDescription": {
"message": "E-postalarınızı geliştirmek için ChatGPT, Google Gemini, Claude veya Ollamayı kullanın!"
},
"menu_title": {
"message": "YZ"
},
"customPrompts_managePrompts_info_default": {
"message": "Varsayılan promptlar düzenlenemez. Bunları devre dışı bırakabilir, ardından prompt metnini kopyalayıp yeni bir prompta yapıştırarak düzenlenmiş bir sürüm oluşturabilirsiniz."
},
"customPrompts_managePrompts_info_default_2": {
"message": "Promptları içe ve dışa aktarabilirsiniz. Aynı kimliğe (ID) sahip mevcut istemlerin üzerine yazılır. Yeni kimliklere (ID) sahip promptlar eklenir."
},
"customPrompts_managePrompts_info_default_3": {
"message": "Her şey doğruysa, içe aktarma işleminden sonra Tümünü Kaydet düğmesine tıklayın."
},
"customPrompts_start_saving": {
"message": "Promptlar kaydediliyor…"
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
/*
* ThunderAI [https://micz.it/thunderbird-addon-thunderai/]
* Copyright (C) 2024 - 2026 Mic (m@micz.it)
* Copyright (C) 2024 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
@ -20,26 +20,10 @@
* The original code has been released under the Apache License, Version 2.0.
*/
import { prefs_default, integration_options_config } from '../options/mzta-options-default.js';
import { placeholdersUtils } from '../js/mzta-placeholders.js';
import { getAPIsInitMessageString, convertNewlinesToBr } from '../js/mzta-utils.js';
import { loadPrompt } from '../js/mzta-prompts.js';
// Get the LLM to be used
const urlParams = new URLSearchParams(window.location.search);
const llm = urlParams.get('llm');
const call_id = urlParams.get('call_id');
const ph_def_val = urlParams.get('ph_def_val');
const prompt_id = urlParams.get('prompt_id');
const prompt_name = urlParams.get('prompt_name');
// Data received from the user
let promptData = null;
const messageInput = document.querySelector('message-input');
const messagesArea = document.querySelector('messages-area');
//console.log(">>>>>>>>>> controller.js DOMContentLoaded");
// console.log(">>>>>>>>>>> llm: " + llm);
// console.log(">>>>>>>>>>> call_id: " + call_id);
@ -47,192 +31,87 @@ const messagesArea = document.querySelector('messages-area');
// The controller wires up all the components and workers together,
// managing the dependencies. A kind of "DI" class.
let worker = null;
const integration = llm.replace('_api', '');
const worker_path_map = {
chatgpt: '../js/workers/model-worker-openai_responses.js',
google_gemini: '../js/workers/model-worker-google_gemini.js',
ollama: '../js/workers/model-worker-ollama.js',
openai_comp: '../js/workers/model-worker-openai_comp.js',
anthropic: '../js/workers/model-worker-anthropic.js',
};
const worker_path = worker_path_map[integration];
if (worker_path) {
worker = new Worker(worker_path, { type: 'module' });
} else {
console.error('[ThunderAI] API WebChat Unknown LLM type:', llm);
switch (llm) {
case "chatgpt_api":
browser.runtime.sendMessage({command: "openai_api_ready_" + call_id, window_id: (await browser.windows.getCurrent()).id});
worker = new Worker('model-worker-openai.js', { type: 'module' });
break;
case "ollama_api":
browser.runtime.sendMessage({command: "ollama_api_ready_" + call_id, window_id: (await browser.windows.getCurrent()).id});
worker = new Worker('model-worker-ollama.js', { type: 'module' });
break;
case "openai_comp_api":
browser.runtime.sendMessage({command: "openai_comp_api_ready_" + call_id, window_id: (await browser.windows.getCurrent()).id});
worker = new Worker('model-worker-openai_comp.js', { type: 'module' });
break;
}
if (worker) {
messagesArea.init(worker);
messageInput.init(worker);
messageInput.setMessagesArea(messagesArea);
const messagesArea = document.querySelector('messages-area');
messagesArea.init(worker);
if (integration_options_config[integration]) {
const integration_prefix = integration;
const options_config = integration_options_config[integration];
let prefsToGet = { do_debug: prefs_default.do_debug, hide_thinking: prefs_default.hide_thinking };
for (const key in options_config) {
prefsToGet[`${integration_prefix}_${key}`] = prefs_default[`${integration_prefix}_${key}`];
}
if (integration === 'openai_comp') {
prefsToGet.openai_comp_chat_name = prefs_default.openai_comp_chat_name;
}
// Initialize the messageInput component and pass the worker to it
const messageInput = document.querySelector('message-input');
messageInput.init(worker);
messageInput.setMessagesArea(messagesArea);
let prefs_api = await browser.storage.sync.get(prefsToGet);
// Data received from the user
let promptData = null;
if (prompt_id) {
try {
const prompt = await loadPrompt(prompt_id);
if (prompt && prompt.api_type === llm) {
for (const key in options_config) {
const prefKey = `${integration_prefix}_${key}`;
if (prompt[prefKey] !== undefined) {
prefs_api[prefKey] = prompt[prefKey];
}
}
}
} catch (e) {
console.error("[ThunderAI] Error loading prompt settings:", e);
}
}
// ============================== TESTING
// let browser = {
// i18n: {
// getMessage: async function(key) {
// return key;
// }
// },
// storage: {
// sync: {
// get: async function(key) {
// return 'apitest';
// }
// }
// }
// }
// ============================== TESTING - END
switch (llm) {
case "chatgpt_api":
let prefs_api = await browser.storage.sync.get({chatgpt_api_key: '', chatgpt_model: ''});
//console.log(">>>>>>>>>>> chatgpt_api_key: " + prefs_api_key.chatgpt_api_key);
messageInput.setModel(prefs_api.chatgpt_model);
messagesArea.setLLMName("ChatGPT");
worker.postMessage({ type: 'init', chatgpt_api_key: prefs_api.chatgpt_api_key, chatgpt_model: prefs_api.chatgpt_model});
messagesArea.appendUserMessage(browser.i18n.getMessage("chagpt_api_connecting") + " " +browser.i18n.getMessage("AndModel") + " \"" + prefs_api.chatgpt_model + "\"...", "info");
break;
case "ollama_api": {
let prefs_api = await browser.storage.sync.get({ollama_host: '', ollama_model: ''});
let i18nStrings = {};
const i18n_msg_key = integration === 'openai_comp' ? 'OpenAIComp_api_request_failed' : `${integration}_api_request_failed`;
i18nStrings[i18n_msg_key] = browser.i18n.getMessage(i18n_msg_key);
i18nStrings["ollama_api_request_failed"] = browser.i18n.getMessage('ollama_api_request_failed');
i18nStrings["error_connection_interrupted"] = browser.i18n.getMessage('error_connection_interrupted');
messageInput.setModel(prefs_api[`${integration_prefix}_model`]);
let llmName = "API";
switch(integration) {
case 'chatgpt': llmName = "ChatGPT"; break;
case 'google_gemini': llmName = "Google Gemini"; break;
case 'ollama': llmName = "Ollama Local"; break;
case 'openai_comp': llmName = prefs_api.openai_comp_chat_name || "OpenAI Comp"; break;
case 'anthropic': llmName = "Claude"; break;
}
messagesArea.setLLMName(llmName);
messagesArea.setHideThinking(!!prefs_api.hide_thinking);
document.title += " [" + llmName + " | " + decodeURIComponent(prompt_name) + "]";
document.title += " [" + llmName + " | " + decodeURIComponent(prompt_name) + "]";
let workerInitMessage = {
type: 'init',
do_debug: prefs_api.do_debug,
i18nStrings: i18nStrings,
};
for (const key in options_config) {
const prefKey = `${integration_prefix}_${key}`;
workerInitMessage[prefKey] = prefs_api[prefKey];
}
worker.postMessage(workerInitMessage);
const additional_messages_config = {
chatgpt: [
{ key: 'store', labelKey: 'ChatGPT_chatgpt_api_store', type: 'boolean' },
{ key: 'developer_messages', labelKey: 'ChatGPT_Developer_Messages', type: 'string' },
{ key: 'temperature', labelKey: 'prefs_api_temperature', type: 'string' }
],
google_gemini: [
{ key: 'system_instruction', labelKey: 'GoogleGemini_SystemInstruction', type: 'string' },
{ key: 'temperature', labelKey: 'prefs_api_temperature', type: 'string' },
{ key: 'thinking_budget', labelKey: 'prefs_google_gemini_thinking_budget', type: 'string' }
],
ollama: [
{ key: 'think', labelKey: 'prefs_ollama_think', type: 'boolean' },
{ key: 'temperature', labelKey: 'prefs_api_temperature', type: 'string' },
{ key: 'num_ctx', labelKey: 'prefs_ollama_num_ctx', type: 'number_gt_zero' }
],
openai_comp: [
{ key: 'temperature', labelKey: 'prefs_api_temperature', type: 'string' }
],
anthropic: [
{ key: 'system_prompt', labelKey: 'Anthropic_System_Prompt', type: 'string' },
{ key: 'max_tokens', labelKey: 'prefs_OptionText_anthropic_max_tokens', type: 'number_gt_zero' },
{ key: 'temperature', labelKey: 'prefs_api_temperature', type: 'string' },
{ key: 'extended_thinking_budget', labelKey: 'prefs_OptionText_anthropic_extended_thinking_budget', type: 'number_gt_zero' }
]
};
const getAdditionalMessages = (integration, prefs) => {
const messages = [];
const config = additional_messages_config[integration];
if (!config) return messages;
for (const item of config) {
const prefKey = `${integration}_${item.key}`;
const value = prefs[prefKey];
if (value !== undefined && value !== null && value !== '') {
let displayValue;
let shouldAdd = false;
switch (item.type) {
case 'boolean':
displayValue = value ? 'Yes' : 'No';
shouldAdd = true;
break;
case 'string':
if (value.length > 0) {
displayValue = value;
shouldAdd = true;
}
break;
case 'number_gt_zero':
if (value > 0) {
displayValue = value;
shouldAdd = true;
}
break;
}
if (shouldAdd) {
messages.push({ label: browser.i18n.getMessage(item.labelKey), value: displayValue });
}
}
}
return messages;
};
let additional_text_elements = [];
additional_text_elements.push({label: browser.i18n.getMessage("prompt_string"), value: '[' + prompt_id + '] ' + decodeURIComponent(prompt_name)});
additional_text_elements.push(...getAdditionalMessages(integration, prefs_api));
const api_strings = {
chatgpt: "ChatGPT API",
google_gemini: "Google Gemini API",
ollama: "Ollama API",
openai_comp: "OpenAI Compatible API",
anthropic: "Claude API"
};
messagesArea.appendUserMessage(getAPIsInitMessageString({
api_string: api_strings[integration],
model_string: prefs_api[`${integration_prefix}_model`],
host_string: prefs_api[`${integration_prefix}_host`],
version_string: prefs_api[`${integration_prefix}_version`],
additional_messages: additional_text_elements
}), "info");
//console.log(`>>>>>>>>>>>>> command: ${llm}_ready_${call_id}`,)
browser.runtime.sendMessage({
command: `${llm}_ready_${call_id}`,
window_id: (await browser.windows.getCurrent()).id
});
//console.log(">>>>>>>>>>> ollama_host: " + prefs_api_key.ollama_host);
messageInput.setModel(prefs_api.ollama_model);
messagesArea.setLLMName("Ollama Local");
worker.postMessage({ type: 'init', ollama_host: prefs_api.ollama_host, ollama_model: prefs_api.ollama_model, i18nStrings: i18nStrings});
messagesArea.appendUserMessage(browser.i18n.getMessage("ollama_api_connecting") + " \"" + prefs_api.ollama_host + "\" " +browser.i18n.getMessage("AndModel") + " \"" + prefs_api.ollama_model + "\"...", "info");
break;
}
case "openai_comp_api": {
let prefs_api = await browser.storage.sync.get({openai_comp_host: '', openai_comp_model: '', openai_comp_api_key: '', openai_comp_chat_name: ''});
let i18nStrings = {};
i18nStrings["OpenAIComp_api_request_failed"] = browser.i18n.getMessage('OpenAIComp_api_request_failed');
i18nStrings["error_connection_interrupted"] = browser.i18n.getMessage('error_connection_interrupted');
messageInput.setModel(prefs_api.openai_comp_model);
messagesArea.setLLMName(prefs_api.openai_comp_chat_name);
worker.postMessage({ type: 'init', openai_comp_host: prefs_api.openai_comp_host, openai_comp_model: prefs_api.openai_comp_model, openai_comp_api_key: prefs_api.openai_comp_api_key, i18nStrings: i18nStrings});
messagesArea.appendUserMessage(browser.i18n.getMessage("OpenAIComp_api_connecting") + " \"" + prefs_api.openai_comp_host + "\" " +browser.i18n.getMessage("AndModel") + " \"" + prefs_api.openai_comp_model + "\"...", "info");
break;
}
}
//let prefs_ph = await browser.storage.sync.get({placeholders_use_default_value: false});
// Event listeners for worker messages
worker.onmessage = async function(event) {
worker.onmessage = function(event) {
const { type, payload } = event.data;
switch (type) {
case 'messageSent':
@ -240,19 +119,15 @@ worker.onmessage = async function(event) {
break;
case 'newToken':
messagesArea.handleNewToken(payload.token);
messageInput.setStatusMessage(browser.i18n.getMessage("apiwebchat_receiving_data") + '...');
break;
case 'newThinkingToken':
messagesArea.handleNewThinkingToken(payload.token);
messageInput.setStatusMessage(browser.i18n.getMessage("apiwebchat_receiving_data") + '...');
messageInput.setStatusMessage('Receiving data...');
break;
case 'tokensDone':
await messagesArea.handleTokensDone(promptData);
messagesArea.handleTokensDone(promptData);
messageInput.enableInput();
break;
case 'error':
messagesArea.appendBotMessage(payload,'error');
messageInput.enableInput(false);
messageInput.enableInput();
break;
default:
console.error('[ThunderAI] Unknown event type from API worker:', type);
@ -261,51 +136,23 @@ worker.onmessage = async function(event) {
// handling commands from the backgound page
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
//console.log(">>>>>>>>>>>>> controller.js onMessage: " + JSON.stringify(message));
switch (message.command) {
case "api_send":
promptData = message;
//send the received prompt to the llm api
if(message.do_custom_text=="1") {
messageInput._showCustomTextField(message.prompt_info?.custom_text_array);
}else{
sendPrompt(message);
}
break;
case 'api_send_custom_text':
let userInput = message.custom_text; // From version 4.0.0 this is an array
let userInput = prompt(browser.i18n.getMessage("chatgpt_win_custom_text"));
if(userInput !== null) {
if(!placeholdersUtils.hasPlaceholder(promptData.prompt, 'additional_text')){
// no additional_text placeholder, do as usual
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 = {};
if (Array.isArray(userInput)) {
userInput.forEach(obj => {
finalSubs[obj.placeholder.replace(/^{%|%}$/g, '').trim()] = obj.custom_text;
});
} else {
finalSubs["additional_text"] = userInput;
}
promptData.prompt = placeholdersUtils.replacePlaceholders({
text: promptData.prompt,
replacements: finalSubs,
use_default_value: ph_def_val==='1'
})
}
sendPrompt(promptData);
message.prompt += " " + userInput;
}
}
promptData = message;
messageInput._setMessageInputValue(message.prompt);
messageInput._handleNewChatMessage();
break;
case "api_error":
messagesArea.appendBotMessage(message.error,'error');
messageInput.enableInput(false);
messageInput.enableInput();
break;
}
});
function sendPrompt(message){
messageInput._setMessageInputValue(convertNewlinesToBr(message.prompt));
messageInput._handleNewChatMessage();
}
});

View file

@ -3,6 +3,7 @@
<head>
<!-- Other meta tags and stylesheets -->
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<!-- Use the custom tags directly -->
@ -16,7 +17,6 @@
<!-- Include the JavaScript files at bottom to avoid blocking UI -->
<script src="messageInput.js" type="module" defer></script>
<script src="markdown-it.min.js"></script>
<script src="../js/lib/diff.js"></script>
<script src="messagesArea.js" type="module" defer></script>
<script src="controller.js" type="module" defer></script>
</body>

View file

@ -1,6 +1,6 @@
/*
* ThunderAI [https://micz.it/thunderbird-addon-thunderai/]
* Copyright (C) 2024 - 2026 Mic (m@micz.it)
* Copyright (C) 2024 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
@ -23,7 +23,7 @@
const messageInputTemplate = document.createElement('template');
const messagesInputStyle = document.createElement('style');
messagesInputStyle.textContent = `
messagesInputStyle .textContent = `
:host {
display: flex;
justify-content: space-between;
@ -49,7 +49,6 @@ messagesInputStyle.textContent = `
height: 36px;
cursor: pointer;
border-radius: 10px;
border: 1px outset buttonface;
}
#stopButton {
width: 44px;
@ -66,105 +65,17 @@ messagesInputStyle.textContent = `
border-radius: 5px;
padding: 5px;
background: #F2F2F2;
display: flex;
align-items: center;
gap: 3px;
}
#statusLoggerImg{
display: none;
vertical-align: middle;
}
#statusLoggerText{
font-weight: 600;
}
#statusLogger.status-working{
border-color: #2196F3;
background: #E3F2FD;
color: #1565C0;
}
#statusLogger.status-done{
border-color: #4CAF50;
background: #E8F5E9;
color: #2E7D32;
}
@keyframes statusFadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
#statusLogger.status-fadeout{
animation: statusFadeOut 0.5s ease-out forwards;
}
#mzta-custom_text{
padding:10px;
width:50%;
min-width:300px;
max-width:80%;
height:auto;
max-height:80%;
border-radius:5px;
overflow-y:auto;
overflow-x:hidden;
position:fixed;
top:50%;
left:50%;
display:none;
transform:translate(-50%,-50%);
text-align:center;
background:#333;
color:white;
border:3px solid white;
box-sizing: border-box;
}
#mzta-custom_loading{
height:50px;display:none;
}
#mzta-custom_textarea{
color:black;
padding:5px;
font-size:15px;
width:100%;
box-sizing: border-box;
resize: vertical;
}
#mzta-custom_info{
text-align:center;
width:100%;
padding-bottom:10px;
font-size:15px;
}
#mzta-custom_info span{
font-size:0.8em;
}
#mzta-custom_step{
position: absolute;
bottom: 5px;
right: 10px;
font-size: 12px;
color: #ccc;
}
#mzta-custom_btn{
margin-top:7px;
}
@media (prefers-color-scheme: dark) {
#messageInputField {
background-color: #303030;
color: #ffffff;
}
#statusLogger{
background: #212121;
color: #ffffff;
}
#statusLogger.status-working{
border-color: #64B5F6;
background: #1A3A5C;
color: #90CAF9;
}
#statusLogger.status-done{
border-color: #81C784;
background: #1B3D1E;
color: #A5D6A7;
}
#messageInputField {
background-color: #303030;
color: #ffffff;
}
#statusLogger{
background: #212121;
color: #ffffff;
}
}
`;
messageInputTemplate.content.appendChild(messagesInputStyle);
@ -220,47 +131,13 @@ messageInputTemplate.content.appendChild(stopButton);
const statusLogger = document.createElement('div');
statusLogger.id = 'statusLogger';
statusLogger.textContent = '';
statusLogger.style.display = 'none';
const statusLoggerImg = document.createElement('img');
statusLoggerImg.id = 'statusLoggerImg';
statusLoggerImg.src = browser.runtime.getURL('/images/mzta-loading.svg');
statusLogger.appendChild(statusLoggerImg);
const statusLoggerText = document.createElement('span');
statusLoggerText.id = 'statusLoggerText';
statusLogger.appendChild(statusLoggerText);
messageInputTemplate.content.appendChild(statusLogger);
//div per custom text
const customDiv = document.createElement('div');
customDiv.id = 'mzta-custom_text';
const customInfo = document.createElement('div');
customInfo.id = 'mzta-custom_info';
customInfo.textContent = browser.i18n.getMessage("chatgpt_win_custom_text");
customDiv.appendChild(customInfo);
const customTextArea = document.createElement('textarea');
customTextArea.id = 'mzta-custom_textarea';
customTextArea.rows = 5;
customDiv.appendChild(customTextArea);
const customLoading = document.createElement('img');
customLoading.src = browser.runtime.getURL("/images/loading.gif");
customLoading.id = "mzta-custom_loading";
customDiv.appendChild(customLoading);
const customBtn = document.createElement('button');
customBtn.id = 'mzta-custom_btn';
customBtn.textContent = browser.i18n.getMessage("chatgpt_win_send");
customBtn.classList.add('mzta-btn');
customDiv.appendChild(customBtn);
const customStep = document.createElement('div');
customStep.id = 'mzta-custom_step';
customDiv.appendChild(customStep);
messageInputTemplate.content.appendChild(customDiv);
class MessageInput extends HTMLElement {
model = '';
_doneTimeout = null;
_customTextArray = [];
_currentCustomTextIndex = 0;
constructor() {
super();
@ -271,25 +148,10 @@ class MessageInput extends HTMLElement {
this._sendButton = shadowRoot.querySelector('#sendButton');
this._stopButton = shadowRoot.querySelector('#stopButton');
this._statusLogger = shadowRoot.querySelector('#statusLogger');
this._statusLoggerImg = shadowRoot.querySelector('#statusLoggerImg');
this._statusLoggerText = shadowRoot.querySelector('#statusLoggerText');
this._messageInputField.addEventListener('keydown', this._handleKeyDown.bind(this));
this._sendButton.addEventListener('click', this._handleClick.bind(this));
this._stopButton.addEventListener('click', this._handleStopClick.bind(this));
this._customText = shadowRoot.querySelector('#mzta-custom_text');
this._customTextArea = shadowRoot.querySelector('#mzta-custom_textarea');
this._customLoading = shadowRoot.querySelector('#mzta-custom_loading');
this._customBtn = shadowRoot.querySelector('#mzta-custom_btn');
this._customStep = shadowRoot.querySelector('#mzta-custom_step');
this._customBtn.addEventListener("click", () => { this._customTextBtnClick({customBtn:this._customBtn,customLoading:this._customLoading,customDiv:this._customText}) });
this._customTextArea.addEventListener("keydown", (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
this._customTextBtnClick({customBtn:this._customBtn,customLoading:this._customLoading,customDiv:this._customText});
}
});
}
connectedCallback() {
@ -307,8 +169,8 @@ class MessageInput extends HTMLElement {
setModel(model){
this.model = model;
this._sendButton.title = browser.i18n.getMessage("chagpt_api_send_button") + ": " + this.model;
this._stopButton.title = browser.i18n.getMessage("chagpt_api_send_button") + ": " + this.model;
this._sendButton.title = browser.i18n.getMessage("chagtp_api_send_button") + ": " + this.model;
this._stopButton.title = browser.i18n.getMessage("chagtp_api_send_button") + ": " + this.model;
}
handleMessageSent() {
@ -316,7 +178,7 @@ class MessageInput extends HTMLElement {
this._messageInputField.value = '';
}
enableInput(showDone = true) {
enableInput() {
// console.log("[ThunderAI] enableInput");
this._messageInputField.value = '';
this._messageInputField.removeAttribute('disabled');
@ -324,57 +186,21 @@ class MessageInput extends HTMLElement {
this._sendButton.style.display = 'block';
this._stopButton.setAttribute('disabled', 'disabled');
this._stopButton.style.display = 'none';
this._stopButton.title = browser.i18n.getMessage("chagpt_api_send_button") + ": " + this.model;
if (showDone) {
this.showDoneStatus();
} else {
this.hideStatusMessage();
this.setStatusMessage('');
}
this._stopButton.title = browser.i18n.getMessage("chagtp_api_send_button") + ": " + this.model;
this.hideStatusMessage();
this.setStatusMessage('');
}
setStatusMessage(message) {
this._statusLoggerText.textContent = message;
this._statusLogger.textContent = message;
}
showStatusMessage(state = 'working') {
if (this._doneTimeout) {
clearTimeout(this._doneTimeout);
this._doneTimeout = null;
}
this._setStatusClass('status-' + state);
this._statusLogger.style.display = 'flex';
showStatusMessage() {
this._statusLogger.style.display = 'block';
}
hideStatusMessage() {
this._statusLogger.style.display = 'none';
this._statusLoggerImg.style.display = 'none';
this._setStatusClass(null);
}
_setStatusClass(className) {
this._statusLogger.classList.remove('status-working', 'status-done', 'status-fadeout');
if (className) {
this._statusLogger.classList.add(className);
}
}
showDoneStatus() {
if (this._doneTimeout) {
clearTimeout(this._doneTimeout);
}
this._statusLoggerImg.style.display = 'none';
this.setStatusMessage(browser.i18n.getMessage('apiwebchat_done'));
this._setStatusClass('status-done');
this._statusLogger.style.display = 'flex';
this._doneTimeout = setTimeout(() => {
this._statusLogger.classList.add('status-fadeout');
this._doneTimeout = setTimeout(() => {
this.hideStatusMessage();
this.setStatusMessage('');
}, 500);
}, 1500);
}
_handleKeyDown(event) {
@ -390,7 +216,7 @@ class MessageInput extends HTMLElement {
_handleStopClick() {
this.worker.postMessage({ type: 'stop' });
this._stopButton.setAttribute('disabled', 'disabled');
this._stopButton.title = browser.i18n.getMessage("apiwebchat_stopping") + '...';
this._stopButton.title = 'Stopping...';
}
_handleNewChatMessage() {
@ -409,8 +235,7 @@ class MessageInput extends HTMLElement {
if (this.messagesAreaComponent) {
this.messagesAreaComponent.appendUserMessage(messageContent);
}
this.setStatusMessage(browser.i18n.getMessage('WaitingServerResponse') + '...');
this._statusLoggerImg.style.display = 'inline';
this.setStatusMessage(browser.i18n.getMessage('WaitingServerReponse') + '...');
this.showStatusMessage();
this.worker.postMessage({ type: 'chatMessage', message: messageContent });
}
@ -418,66 +243,6 @@ class MessageInput extends HTMLElement {
_setMessageInputValue(msg) {
this._messageInputField.value = msg;
}
_showCustomTextField(custom_text_array){
this._customTextArray = custom_text_array || [];
if (this._customTextArray.length === 0) {
this._customTextArray.push({ placeholder: "{%additional_text%}", info: "" });
}
this._currentCustomTextIndex = 0;
this._customText.style.display = 'block';
this._renderCustomTextStep();
}
_renderCustomTextStep() {
const currentItem = this._customTextArray[this._currentCustomTextIndex];
const infoDiv = this.shadowRoot.querySelector('#mzta-custom_info');
this._customTextArea.value = "";
infoDiv.textContent = browser.i18n.getMessage("chatgpt_win_custom_text");
if (currentItem.info && currentItem.info.trim() !== "") {
infoDiv.appendChild(document.createElement("br"));
const infoSpan = document.createElement("span");
infoSpan.textContent = "[" + browser.i18n.getMessage("customPrompts_form_label_ID") + ": " + currentItem.info + "]";
infoDiv.appendChild(infoSpan);
}
if(this._customTextArray.length > 1) {
this._customStep.textContent = (this._currentCustomTextIndex + 1) + "/" + this._customTextArray.length;
this._customStep.style.display = 'block';
} else {
this._customStep.style.display = 'none';
}
this._customTextArea.focus();
}
async _customTextBtnClick(args) {
const customText = this._customTextArea.value;
if (this._customTextArray[this._currentCustomTextIndex]) {
this._customTextArray[this._currentCustomTextIndex].custom_text = customText;
}
this._currentCustomTextIndex++;
if (this._currentCustomTextIndex < this._customTextArray.length) {
this._renderCustomTextStep();
} else {
args.customBtn.disabled = true;
args.customBtn.classList.add('disabled');
args.customLoading.style.display = 'inline-block';
let tab = await browser.tabs.query({ active: true, currentWindow: true });
browser.runtime.sendMessage({ command: "api_send_custom_text", custom_text: this._customTextArray, tabId: tab[0].id });
args.customDiv.style.display = 'none';
args.customBtn.disabled = false;
args.customBtn.classList.remove('disabled');
args.customLoading.style.display = 'none';
}
}
}
customElements.define('message-input', MessageInput);

View file

@ -1,6 +1,6 @@
/*
* ThunderAI [https://micz.it/thunderbird-addon-thunderai/]
* Copyright (C) 2024 - 2026 Mic (m@micz.it)
* Copyright (C) 2024 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
@ -20,7 +20,6 @@
* The original code has been released under the Apache License, Version 2.0.
*/
import { prefs_default } from '../options/mzta-options-default.js';
const messagesAreaTemplate = document.createElement('template');
const messagesAreaStyle = document.createElement('style');
@ -46,6 +45,7 @@ messagesAreaStyle.textContent = `
line-height: 1.3;
padding: 5px;
border-radius: 10px;
}
.message p{
margin: 0;
@ -58,29 +58,14 @@ messagesAreaStyle.textContent = `
}
.action-buttons {
line-height: 1.3;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.action-buttons button {
display: inline;
margin: 0 10px;
padding: 5px 10px;
cursor: pointer;
border: 1px outset buttonface;
}
.action-buttons button.close_btn, .action-buttons button.diffv_btn {
border-radius: 5px;
}
.action-buttons button.action_btn {
margin-right: 0;
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
}
.action_btn_info {
font-size: 0.6rem;
color: gray;
display: inline-block;
cursor: pointer;
}
@keyframes fadeIn {
to {
@ -104,128 +89,6 @@ messagesAreaStyle.textContent = `
background: lightblue;
color: navy;
margin-bottom: var(--margin);
font-size: 0.8em;
}
.info_obj{
color:rgb(0, 71, 36);
}
.sel_info{
font-size: 0.7rem;
color: gray;
margin-top: 5px;
display: none;
width: 100%;
text-align: center;
}
/* diff viewer */
.added {
background-color: #d4fcdc;
display: inline;
}
.removed {
background-color: #fddddd;
display: inline;
text-decoration: line-through;
}
/* Split button styles */
.split-button {
display: inline-flex;
position: relative;
font-family: sans-serif;
}
.split-button button {
padding: 5px 0px 5px 10px;
cursor: pointer;
font-size: 14px;
}
.split-button .dropdown-toggle {
border-left: none;
display: flex;
align-items: center;
justify-content: center;
width: 38px;
margin-left:-1px;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
padding:0;
}
.dropdown-toggle svg {
fill: #555;
margin-left: -4px;
}
.dropdown-menu {
position: absolute;
top: 2.55rem;
right:0;
display: none;
flex-direction: column;
background-color: white;
border: 1px solid #ccc;
min-width: 160px;
z-index: 1000;
margin-top: 2px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
border-radius: 5px;
text-align: right;
width: -moz-available;
}
.dropdown-menu button {
padding: 10px 14px;
border: none;
background-color: white;
text-align: right;
cursor: pointer;
font-size: 0.6rem;
color: gray;
}
.dropdown-menu button:hover {
background-color: #f0f0f0;
}
.dropdown-menu.show {
display: flex;
}
/* Thinking block styles */
details.thinking-block {
border-left: 3px solid #bbb;
background: #f7f7f7;
padding: 0.3em 0.6em;
margin: 0 0 0.6em 0;
font-size: 0.9em;
color: #555;
border-radius: 4px;
}
details.thinking-block > summary {
cursor: pointer;
font-weight: 600;
}
details.thinking-block .thinking-content {
white-space: pre-wrap;
margin-top: 0.3em;
}
/* Dark mode styles */
@media (prefers-color-scheme: dark) {
.added {
background-color:rgb(0, 94, 0);
}
.removed {
background-color:rgb(90, 0, 0);
}
details.thinking-block {
background: #2a2a2a;
color: #bbb;
border-left-color: #555;
}
}
`;
messagesAreaTemplate.content.appendChild(messagesAreaStyle);
@ -242,8 +105,6 @@ class MessagesArea extends HTMLElement {
constructor() {
super();
this.accumulatingMessageEl = null;
this.thinkingAccumulator = '';
this.hideThinking = false;
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.appendChild(messagesAreaTemplate.content.cloneNode(true));
@ -274,17 +135,9 @@ class MessagesArea extends HTMLElement {
this.llmName = llmName;
}
setHideThinking(val) {
this.hideThinking = !!val;
}
handleNewThinkingToken(token) {
this.thinkingAccumulator += token;
}
async handleTokensDone(promptData = null) {
handleTokensDone(promptData = null) {
this.flushAccumulatingMessage();
await this.addActionButtons(promptData);
this.addActionButtons(promptData);
this.addDivider();
}
@ -292,13 +145,13 @@ class MessagesArea extends HTMLElement {
this.fullTextHTML = "";
// console.log("[ThunderAI] appendUserMessage: " + messageText);
const header = document.createElement('h2');
let source = browser.i18n.getMessage("apiwebchat_you");
let source = "You";
switch (type) {
case "user":
source = browser.i18n.getMessage("apiwebchat_you");
source = "You";
break;
case "info":
source = browser.i18n.getMessage("apiwebchat_info");
source = "Information";
break;
}
header.textContent = source;
@ -306,21 +159,7 @@ class MessagesArea extends HTMLElement {
const messageElement = document.createElement('div');
messageElement.classList.add('message', type);
// Replace \n with <br> for correct HTML display
if (type === "info") {
messageElement.appendChild(htmlStringToFragment(messageText));
} else {
messageElement.appendChild(textWithBrToFragment(messageText));
}
// messageElement.textContent = messageText;
// // Replace \n with <br> elements for correct HTML display
// messageElement.innerHTML = '';
// messageText.split('\n').forEach((line, idx, arr) => {
// messageElement.appendChild(document.createTextNode(line));
// if (idx < arr.length - 1) {
// messageElement.appendChild(document.createElement('br'));
// }
// });
messageElement.textContent = messageText;
this.messages.appendChild(messageElement);
this.scrollToBottom();
}
@ -335,7 +174,7 @@ class MessagesArea extends HTMLElement {
if (isLastMessageFromUser) {
const header = document.createElement('h2');
header.textContent = this.llmName + (type=='error' ? " - " + browser.i18n.getMessage("apiwebchat_error") : "");
header.textContent = "Chat GPT" + (type=='error' ? " - Error" : "");
this.messages.appendChild(header);
}
@ -367,174 +206,59 @@ class MessagesArea extends HTMLElement {
this.messages.scrollTop = this.messages.scrollHeight;
}
// Helper to create dropdown options
createOption(label, callback) {
const btn = document.createElement('button');
btn.textContent = label;
btn.onclick = callback;
return btn;
}
// click callcback for the "use this answer" button
handleUseThisAnswerButtonClick(promptData, replyType, fullTextHTMLAtAssignment){
return async () => {
if(promptData.mailMessageId == -1) { // we are using the reply from the compose window!
promptData.action = "2"; // replace text
}
let finalText = removeAloneBRs(fullTextHTMLAtAssignment);
const selectedHTML = this.getCurrentSelectionHTML();
if(selectedHTML != "") {
finalText = removeAloneBRs(selectedHTML);
}
switch(promptData.action) {
case "1": // do reply
// console.log("[ThunderAI] (do reply) fullTextHTMLAtAssignment: " + fullTextHTMLAtAssignment);
await browser.runtime.sendMessage({command: "chatgpt_replyMessage", text: finalText, tabId: promptData.tabId, mailMessageId: promptData.mailMessageId, replyType: replyType});
browser.runtime.sendMessage({command: "chatgpt_close", window_id: (await browser.windows.getCurrent()).id});
break;
case "2": // replace text
// console.log("[ThunderAI] (replace text) fullTextHTMLAtAssignment: " + fullTextHTMLAtAssignment);
await browser.runtime.sendMessage({command: "chatgpt_replaceSelectedText", text: finalText, tabId: promptData.tabId, mailMessageId: promptData.mailMessageId});
browser.runtime.sendMessage({command: "chatgpt_close", window_id: (await browser.windows.getCurrent()).id});
break;
}
}
}
async addActionButtons(promptData = null) {
addActionButtons(promptData = null) {
// ============================== TESTING
// promptData = {
// action: "1",
// tabId: 1,
// mailMessageId: 1
// }
// let browser = {
// i18n: {
// getMessage: async function(key) {
// return key;
// }
// },
// storage: {
// sync: {
// get: async function(key) {
// return 'apitest';
// }
// }
// }
// }
// ============================== TESTING - END
if(promptData == null) { return; }
const actionButtons = document.createElement('div');
actionButtons.classList.add('action-buttons');
// Create the main container for the "use this answer" button when replying
const splitButton = document.createElement('div');
splitButton.className = 'split-button';
// selection info
const selectionInfo = document.createElement('p');
selectionInfo.textContent = browser.i18n.getMessage("apiwebchat_selection_info");
selectionInfo.classList.add('sel_info');
// main button
const actionButton = document.createElement('button');
actionButton.className = 'action_btn';
const actionButton_line1 = document.createElement('span');
actionButton_line1.textContent = browser.i18n.getMessage("apiwebchat_use_this_answer");
actionButton.appendChild(actionButton_line1);
splitButton.appendChild(actionButton);
actionButton.textContent = 'Use this answer';
//actionButton.textContent = browser.i18n.getMessage("chatgpt_win_get_answer");
const fullTextHTMLAtAssignment = this.fullTextHTML.trim().replace(/^"|"$/g, '').replace(/^<p>&quot;/, '<p>').replace(/&quot;<\/p>$/, '</p>'); // strip quotation marks
//console.log(">>>>>>>>>>>> fullTextHTMLAtAssignment: " + fullTextHTMLAtAssignment);
let reply_type_pref = await browser.storage.sync.get({ reply_type: prefs_default.reply_type });
if((promptData.action == "1") && (promptData.mailMessageId != -1)) {
const actionButton_line2 = document.createElement('span');
actionButton_line2.classList.add('action_btn_info');
actionButton_line2.textContent = reply_type_pref.reply_type == 'reply_all' ? browser.i18n.getMessage("prefs_OptionText_reply_all") : browser.i18n.getMessage("prefs_OptionText_reply_sender");
actionButton.appendChild(document.createElement('br'));
actionButton.appendChild(actionButton_line2);
// Dropdown toggle button
const toggleBtn = document.createElement('button');
toggleBtn.className = 'dropdown-toggle';
toggleBtn.setAttribute('aria-label', 'Show options');
// SVG icon
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 20 20');
svg.setAttribute('width', '16');
svg.setAttribute('height', '16');
svg.setAttribute('fill', 'currentColor');
svg.setAttribute('stroke-width', '2');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M19 9l-7 7-7-7');
svg.appendChild(path);
toggleBtn.appendChild(svg);
splitButton.appendChild(toggleBtn);
// Dropdown menu
const dropdown = document.createElement('div');
dropdown.className = 'dropdown-menu';
dropdown.id = 'dropdown';
// Add options
dropdown.appendChild(this.createOption(
reply_type_pref.reply_type == 'reply_all' ? browser.i18n.getMessage("prefs_OptionText_reply_sender") : browser.i18n.getMessage("prefs_OptionText_reply_all"),
this.handleUseThisAnswerButtonClick(promptData, reply_type_pref.reply_type == 'reply_all' ? 'reply_sender' : 'reply_all', fullTextHTMLAtAssignment))
);
splitButton.appendChild(dropdown);
let dropdownJustOpened = false;
// Toggle function
toggleBtn.onclick = () => {
dropdownJustOpened = true;
dropdown.classList.toggle('show');
};
// Close on outside click
window.addEventListener('click', (e) => {
// Delay the execution to allow other handlers (like toggle) to run first
if (dropdownJustOpened) {
dropdownJustOpened = false;
return; // Skip this click because it's the one that opened the menu
}
setTimeout(() => {
if (!splitButton.contains(e.target)) {
dropdown.classList.remove('show');
}
}, 0);
});
}else{
actionButton.style.paddingRight = "10px";
actionButton.style.borderTopRightRadius = "5px";
actionButton.style.borderBottomRightRadius = "5px";
actionButton.style.marginRight = "10px";
}
actionButton.addEventListener('click', this.handleUseThisAnswerButtonClick(promptData,reply_type_pref.reply_type, fullTextHTMLAtAssignment));
actionButton.addEventListener('click', async () => {
switch(promptData.action) {
case "1": // do reply
// console.log("[ThunderAI] (do reply) fullTextHTMLAtAssignment: " + fullTextHTMLAtAssignment);
await browser.runtime.sendMessage({command: "chatgpt_replyMessage", text: fullTextHTMLAtAssignment, tabId: promptData.tabId, mailMessageId: promptData.mailMessageId});
browser.runtime.sendMessage({command: "chatgpt_close", window_id: (await browser.windows.getCurrent()).id});
break;
case "2": // replace text
// console.log("[ThunderAI] (replace text) fullTextHTMLAtAssignment: " + fullTextHTMLAtAssignment);
await browser.runtime.sendMessage({command: "chatgpt_replaceSelectedText", text: fullTextHTMLAtAssignment, tabId: promptData.tabId, mailMessageId: promptData.mailMessageId});
browser.runtime.sendMessage({command: "chatgpt_close", window_id: (await browser.windows.getCurrent()).id});
break;
}
});
const closeButton = document.createElement('button');
closeButton.textContent = browser.i18n.getMessage("chatgpt_win_close");
closeButton.classList.add('close_btn');
closeButton.addEventListener('click', async () => {
// console.log("[ThunderAI] (close) fullTextHTMLAtAssignment: " + fullTextHTMLAtAssignment);
browser.runtime.sendMessage({command: "chatgpt_close", window_id: (await browser.windows.getCurrent()).id}); // close window
});
if(promptData.action != 0) {
actionButtons.appendChild(splitButton);
selectionInfo.style.display = "block"; // show selection info
}
// Save as Summary button (only shown for summary webchat sessions)
if(promptData.prompt_info?.headerMessageId && promptData.prompt_info?.summaryTabId) {
const saveSummaryButton = document.createElement('button');
saveSummaryButton.textContent = browser.i18n.getMessage("webchat_save_as_summary");
saveSummaryButton.classList.add('action_btn');
saveSummaryButton.addEventListener('click', async () => {
let finalText = removeAloneBRs(fullTextHTMLAtAssignment);
const selectedHTML = this.getCurrentSelectionHTML();
if(selectedHTML != "") {
finalText = removeAloneBRs(selectedHTML);
}
await browser.runtime.sendMessage({
command: "chatgpt_saveSummary",
text: finalText,
headerMessageId: promptData.prompt_info.headerMessageId,
tabId: promptData.prompt_info.summaryTabId || promptData.tabId,
});
browser.runtime.sendMessage({command: "chatgpt_close", window_id: (await browser.windows.getCurrent()).id});
});
actionButtons.appendChild(saveSummaryButton);
selectionInfo.style.display = "block";
}
// diff viewer button
if(promptData.prompt_info?.use_diff_viewer == "1") {
const diffvButton = document.createElement('button');
diffvButton.textContent = browser.i18n.getMessage("btn_show_differences");
diffvButton.classList.add('diffv_btn');
diffvButton.addEventListener('click', async () => {
let strippedText = fullTextHTMLAtAssignment.replace(/<\/?[^>]+(>|$)/g, "");
let originalText = promptData.prompt_info?.selection_text;
if((originalText == null) || (originalText == "")) {
originalText = promptData.prompt_info?.body_text;
}
this.appendDiffViewer(originalText, strippedText);
diffvButton.disabled = true;
});
actionButtons.appendChild(diffvButton);
}
if(promptData.action != 0) { actionButtons.appendChild(actionButton); }
actionButtons.appendChild(closeButton);
this.messages.appendChild(actionButtons);
this.messages.appendChild(selectionInfo);
this.scrollToBottom();
}
@ -544,47 +268,6 @@ class MessagesArea extends HTMLElement {
this.scrollToBottom();
}
appendDiffViewer(originalText, newText) {
const wordDiff = Diff.diffWords(originalText, newText);
const messageElement = document.createElement('div');
messageElement.classList.add('message', 'bot');
// Iterate over each part of the diff to create the HTML output
wordDiff.forEach(part => {
// Split part.value by <br> (handling <br>, <br/>, <br />)
const brRegex = /(<br\s*\/?>)/gi;
const segments = part.value.split(brRegex);
segments.forEach(segment => {
if (segment.match(brRegex)) {
// It's a <br>, add a real <br> element
messageElement.appendChild(document.createElement("br"));
} else if (segment.length > 0) {
const diffElement = document.createElement("span");
if (part.added) {
diffElement.className = "added";
diffElement.textContent = segment;
} else if (part.removed) {
diffElement.className = "removed";
diffElement.textContent = segment;
} else {
diffElement.textContent = segment;
}
messageElement.appendChild(diffElement);
}
});
});
const header = document.createElement('h2');
header.textContent = browser.i18n.getMessage("chatgpt_win_diff_title");
this.messages.appendChild(header);
this.messages.appendChild(messageElement);
this.addDivider();
this.scrollToBottom();
}
flushAccumulatingMessage() {
if (this.accumulatingMessageEl) {
// Collect all tokens in a full text
@ -592,67 +275,21 @@ class MessagesArea extends HTMLElement {
this.accumulatingMessageEl.querySelectorAll('.token').forEach(tokenEl => {
fullText += tokenEl.textContent;
});
// If an unterminated <think> block is present (mid-stream), defer the
// markdown render until the closing tag arrives — tokens stay in the DOM
// as raw fading spans, but the partial <think> content is never sent
// through markdown-it or promoted to the final thinking block.
const openThink = fullText.match(/<think>/i);
const closeThink = fullText.match(/<\/think>/i);
if (openThink && !closeThink) {
return;
}
// Extract inline <think>...</think> blocks (Ollama / OpenAI Comp) and strip them from fullText.
let inlineThinking = '';
const thinkRegex = /<think>([\s\S]*?)<\/think>/gi;
let match;
while ((match = thinkRegex.exec(fullText)) !== null) {
inlineThinking += (inlineThinking ? '\n' : '') + match[1];
}
fullText = fullText.replace(thinkRegex, '').replace(/^\s+/, '');
// Combined thinking content: worker-side (Anthropic) + inline (<think> tags)
let combinedThinking = this.thinkingAccumulator;
if (inlineThinking) {
combinedThinking += (combinedThinking ? '\n' : '') + inlineThinking;
}
this.thinkingAccumulator = '';
// Convert Markdown to DOM nodes using the markdown-it library
const md = window.markdownit();
const html = md.render(fullText);
this.fullTextHTML += html;
// console.log(">>>>>>>>>>>>>>>> flushAccumulatingMessage this.fullTextHTML: " + this.fullTextHTML);
// 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) {
this.accumulatingMessageEl.removeChild(this.accumulatingMessageEl.firstChild);
}
// Prepend thinking block (if any). hide_thinking controls the initial
// open/collapsed state: true -> collapsed, false -> open. Users can always
// toggle with a click.
if (combinedThinking) {
const details = document.createElement('details');
details.classList.add('thinking-block');
if (!this.hideThinking) details.open = true;
const summary = document.createElement('summary');
summary.textContent = browser.i18n.getMessage('prefs_OptionText_thinking_summary') || 'Thinking';
const content = document.createElement('div');
content.classList.add('thinking-content');
content.textContent = combinedThinking;
details.appendChild(summary);
details.appendChild(content);
this.accumulatingMessageEl.appendChild(details);
}
// Append new nodes
Array.from(doc.body.childNodes).forEach(node => {
@ -663,89 +300,6 @@ class MessagesArea extends HTMLElement {
}
}
getCurrentSelectionHTML() {
const selection = window.getSelection();
// console.log(">>>>>>>>>>>>>>>> getCurrentSelectionHTML: " + JSON.stringify(selection.toString()));
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const container = document.createElement('div');
container.appendChild(range.cloneContents());
return container.innerHTML;
}
return '';
}
}
customElements.define('messages-area', MessagesArea);
function textWithBrToFragment(text) {
const fragment = document.createDocumentFragment();
const segments = text.split(/<br\s*\/?>/gi);
segments.forEach((segment, idx) => {
if (segment.length > 0) {
fragment.appendChild(document.createTextNode(segment));
}
if (idx < segments.length - 1) {
fragment.appendChild(document.createElement('br'));
}
});
return fragment;
}
function htmlStringToFragment(htmlString) {
// console.log(">>>>>>>>>>>>>>>> htmlStringToFragment htmlString: " + htmlString);
const normalizedHtml = htmlString.replace(/\n/g, '<br>');
// console.log(">>>>>>>>>>>>>>>> htmlStringToFragment normalizedHtml: " + normalizedHtml);
const parser = new DOMParser();
const doc = parser.parseFromString(normalizedHtml, 'text/html');
const fragment = document.createDocumentFragment();
Array.from(doc.body.childNodes).forEach(node => fragment.appendChild(node));
return fragment;
}
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) {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlString, 'text/html');
const brElements = Array.from(doc.querySelectorAll('br'));
brElements.forEach(br => {
let current = br;
let isInsideP = false;
while (current.parentElement) {
if (current.parentElement.tagName.toLowerCase() === 'p') {
isInsideP = true;
break;
}
current = current.parentElement;
}
if (!isInsideP) {
br.remove();
}
});
return doc.body.innerHTML;
}
customElements.define('messages-area', MessagesArea);

View file

@ -1,6 +1,6 @@
/*
* ThunderAI [https://micz.it/thunderbird-addon-thunderai/]
* Copyright (C) 2024 - 2026 Mic (m@micz.it)
* Copyright (C) 2024 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
@ -20,14 +20,13 @@
* The original code has been released under the Apache License, Version 2.0.
*/
import { Ollama } from '../api/ollama.js';
import { taLogger } from '../mzta-logger.js';
import { Ollama } from '../js/api/ollama.js';
let ollama_host = null;
let ollama_model = '';
let ollama = null;
let stopStreaming = false;
let i18nStrings = null;
let do_debug = false;
let taLog = null;
let conversationHistory = [];
let assistantResponseAccumulator = '';
@ -35,17 +34,11 @@ let assistantResponseAccumulator = '';
self.onmessage = async function(event) {
switch (event.data.type) {
case 'init':
let config = { stream: true };
for (const key in event.data) {
if (key.startsWith('ollama_')) {
let newKey = key.replace('ollama_', '');
config[newKey] = event.data[key];
}
}
ollama = new Ollama(config);
do_debug = event.data.do_debug;
ollama_host = event.data.ollama_host;
ollama_model = event.data.ollama_model;
//console.log(">>>>>>>>>>> ollama_host: " + ollama_host);
ollama = new Ollama(ollama_host, ollama_model, true);
i18nStrings = event.data.i18nStrings;
taLog = new taLogger('model-worker-ollama', do_debug);
break; // init
case 'chatMessage':
conversationHistory.push({ role: 'user', content: event.data.message });
@ -59,29 +52,23 @@ self.onmessage = async function(event) {
if(response.is_exception === true){
error_message = response.error;
}else{
try{
const errorJSON = await response.json();
errorDetail = JSON.stringify(errorJSON);
error_message = errorJSON.error.message;
}catch(e){
error_message = response.statusText;
}
taLog.log("error_message: " + JSON.stringify(error_message));
const errorJSON = await response.json();
errorDetail = JSON.stringify(errorJSON);
error_message = errorJSON.error;
//console.log(">>>>>>>>>>>>> errorJSON.error.message: " + JSON.stringify(errorJSON.error.message));
}
postMessage({ type: 'error', payload: i18nStrings["ollama_api_request_failed"] + ": " + response.status + " " + response.statusText + ", Detail: " + error_message + " " + errorDetail });
throw new Error("[ThunderAI] Ollama API request failed: " + response.status + " " + response.statusText + ", Detail: " + error_message + " " + errorDetail);
postMessage({ type: 'error', payload: i18nStrings["ollama_api_request_failed"] + ": " + error_message });
throw new Error("[ThunderAI] Ollama API request failed: " + response.status + " " + response.statusText + ", Detail: " + errorDetail);
}
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer= '';
try {
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' });
@ -90,7 +77,6 @@ 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' });
@ -98,29 +84,12 @@ self.onmessage = async function(event) {
}
// lots of low-level Ollama response parsing stuff
const chunk = decoder.decode(value);
buffer += chunk;
taLog.log("buffer: " + buffer);
const lines = buffer.split("\n");
buffer = lines.pop();
let parsedLines = [];
try{
parsedLines = lines
.map((line) => line.replace(/^chunk: /, "").trim()) // Remove the "chunk: " prefix
.filter((line) => line !== "" && line !== "[DONE]") // Remove empty lines and "[DONE]"
// .map((line) => JSON.parse(line)); // Parse the JSON string
.map((line) => {
try {
taLog.log("line: " + JSON.stringify(line));
return JSON.parse(line);
} catch (e) {
taLog.warn("JSON parse warning, skipped line: " + line + " - " + e.message);
return null;
}
})
.filter((parsed) => parsed !== null);
}catch(e){
taLog.error("Error parsing lines: " + e);
}
//console.log(">>>>>>>>>>>>> chunk: " + chunk);
const lines = chunk.split("\n");
const parsedLines = lines
.map((line) => line.replace(/^chunk: /, "").trim()) // Remove the "chunk: " prefix
.filter((line) => line !== "" && line !== "[DONE]") // Remove empty lines and "[DONE]"
.map((line) => JSON.parse(line)); // Parse the JSON string
for (const parsedLine of parsedLines) {
const { message } = parsedLine;

View file

@ -0,0 +1,162 @@
/*
* ThunderAI [https://micz.it/thunderbird-addon-thunderai/]
* Copyright (C) 2024 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/>.
*
*
* This file contains a modified version of the code from the project at https://github.com/boxabirds/chatgpt-frontend-nobuild
* The original code has been released under the Apache License, Version 2.0.
*/
import { OpenAI } from '../js/api/openai.js';
//========================== for testing
// const MOCK_TOKENS = ['Good', ' morning', ' Mr', ' Plop', 'py', ',', 'and', ' I', ' said', '\n', '"', 'Good', ' morn', 'ing', ' Mrs',' Plop', 'py', ,'"', '\n', 'Oh', ' how', ' the', ' win', 'ter', ' even', 'ings', ' must', ' just', ' fly'];
//
// function mockDelay(ms) {
// return new Promise(resolve => setTimeout(resolve, ms));
// }
// async function processMockTokens() {
// for (const token of MOCK_TOKENS) {
// await mockDelay(Math.random() * 50 + 50); // Random delay between 100ms and 150ms
// postMessage({ type: 'newToken', payload: { token } });
// }
// postMessage({ type: 'tokensDone' });
// }
//========================== for testing - END
let chatgpt_api_key = null;
let chatgpt_model = '';
let openai = null;
let stopStreaming = false;
let conversationHistory = [];
let assistantResponseAccumulator = '';
self.onmessage = async function(event) {
if (event.data.type === 'init') {
chatgpt_api_key = event.data.chatgpt_api_key;
chatgpt_model = event.data.chatgpt_model;
//console.log(">>>>>>>>>>> chatgpt_api_key: " + chatgpt_api_key);
openai = new OpenAI(chatgpt_api_key, chatgpt_model, true);
} else if (event.data.type === 'chatMessage') {
conversationHistory.push({ role: 'user', content: event.data.message });
// ============================== TESTING
// // Simulate sending the message to an HTTP endpoint
// await mockDelay(1000); // Wait for 1 second
// // Notify that the chat message was sent
// postMessage({ type: 'messageSent' });
// // Start processing tokens
// await processMockTokens();
// return;
// ============================== TESTING - END
// https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo
// 4096 output tokens
// 128,000 input tokens
// const response = await fetch(API_URL, {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// "Authorization": `Bearer ${openaiApiKey}`,
// },
// body: JSON.stringify({
// model: "gpt-4-1106-preview",
// messages: conversationHistory,
// stream: true,
// }),
// });
const response = await openai.fetchResponse(conversationHistory); //4096);
postMessage({ type: 'messageSent' });
if (!response.ok) {
let error_message = '';
let errorDetail = '';
if(response.is_exception === true){
error_message = response.error;
}else{
const errorJSON = await response.json();
errorDetail = JSON.stringify(errorJSON);
error_message = errorJSON.error.message;
//console.log(">>>>>>>>>>>>> errorJSON.error.message: " + JSON.stringify(errorJSON.error.message));
}
postMessage({ type: 'error', payload: error_message });
throw new Error("[ThunderAI] OpenAI API request failed: " + response.status + " " + response.statusText + ", Detail: " + errorDetail);
}
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let chunk = '';
while (true) {
if (stopStreaming) {
stopStreaming = false;
reader.cancel();
conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator });
assistantResponseAccumulator = '';
postMessage({ type: 'tokensDone' });
break;
}
const { done, value } = await reader.read();
if (done) {
conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator });
assistantResponseAccumulator = '';
postMessage({ type: 'tokensDone' });
break;
}
// lots of low-level OpenAI response parsing stuff
chunk += decoder.decode(value);
console.log(">>>>>>>>>>>>>> [ThunderAI] chunk: " + JSON.stringify(chunk));
const lines = chunk.split("\n");
let parsedLines = [];
try{
parsedLines = lines
.map((line) => line.replace(/^data: /, "").trim()) // Remove the "data: " prefix
.filter((line) => line !== "" && line !== "[DONE]") // Remove empty lines and "[DONE]"
// .map((line) => JSON.parse(line)); // Parse the JSON string
.map((line) => {
console.log(">>>>>>>>>>>>> [ThunderAI] line: " + JSON.stringify(line));
return JSON.parse(line);
});
chunk = chunk.substring(chunk.lastIndexOf('\n') + 1);
console.log(">>>>>>>>>>>>>> [ThunderAI] last chunk: " + JSON.stringify(chunk));
}catch(e){
console.log(">>>>>>>>>>>>>> [ThunderAI] last chunk: " + JSON.stringify(chunk));
console.error(">>>>>>>>>>>>> [ThunderAI] error: " + e);
}
for (const parsedLine of parsedLines) {
const { choices } = parsedLine;
const { delta } = choices[0];
const { content } = delta;
// Update the UI with the new content
if (content) {
assistantResponseAccumulator += content;
postMessage({ type: 'newToken', payload: { token: content } });
}
}
}
} else if (event.data.type === 'stop') {
stopStreaming = true;
}
};

View file

@ -1,6 +1,6 @@
/*
* ThunderAI [https://micz.it/thunderbird-addon-thunderai/]
* Copyright (C) 2024 - 2026 Mic (m@micz.it)
* Copyright (C) 2024 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
@ -20,32 +20,23 @@
* The original code has been released under the Apache License, Version 2.0.
*/
import { OpenAIComp } from '../api/openai_comp.js';
import { taLogger } from '../mzta-logger.js';
import { OpenAIComp } from '../js/api/openai_comp.js';
let openai_comp_host = null;
let openai_comp_model = '';
let openai_comp_api_key = '';
let openai_comp = null;
let stopStreaming = false;
let i18nStrings = null;
let do_debug = false;
let taLog = null;
let conversationHistory = [];
let assistantResponseAccumulator = '';
self.onmessage = async function(event) {
if (event.data.type === 'init') {
let config = { stream: true };
for (const key in event.data) {
if (key.startsWith('openai_comp_')) {
let newKey = key.replace('openai_comp_', '');
if (newKey === 'api_key') newKey = 'apiKey';
config[newKey] = event.data[key];
}
}
openai_comp = new OpenAIComp(config);
do_debug = event.data.do_debug;
i18nStrings = event.data.i18nStrings;
taLog = new taLogger('model-worker-openai_comp', do_debug);
openai_comp_host = event.data.openai_comp_host;
openai_comp_model = event.data.openai_comp_model;
openai_comp_api_key = event.data.openai_comp_api_key;
openai_comp = new OpenAIComp(openai_comp_host, openai_comp_model, openai_comp_api_key, true);
} else if (event.data.type === 'chatMessage') {
conversationHistory.push({ role: 'user', content: event.data.message });
@ -58,28 +49,23 @@ self.onmessage = async function(event) {
if(response.is_exception === true){
error_message = response.error;
}else{
try{
const errorJSON = await response.json();
errorDetail = JSON.stringify(errorJSON);
error_message = errorJSON.error.message;
}catch(e){
error_message = response.statusText;
}
taLog.log("error_message: " + JSON.stringify(error_message));
const errorJSON = await response.json();
errorDetail = JSON.stringify(errorJSON);
error_message = errorJSON.error.message;
//console.log(">>>>>>>>>>>>> errorJSON.error.message: " + JSON.stringify(errorJSON.error.message));
}
postMessage({ type: 'error', payload: i18nStrings["OpenAIComp_api_request_failed"] + ": " + response.status + " " + response.statusText + ", Detail: " + error_message + " " + errorDetail });
throw new Error("[ThunderAI] OpenAI Comp API request failed: " + response.status + " " + response.statusText + ", Detail: " + error_message + " " + errorDetail);
postMessage({ type: 'error', payload: error_message });
throw new Error("[ThunderAI] OpenAI Comp API request failed: " + response.status + " " + response.statusText + ", Detail: " + errorDetail);
}
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = '';
let chunk = '';
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' });
@ -87,45 +73,33 @@ 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' });
break;
}
// lots of low-level OpenAI response parsing stuff
const chunk = decoder.decode(value);
buffer += chunk;
taLog.log("buffer: " + buffer);
const lines = buffer.split("\n");
buffer = lines.pop();
chunk += decoder.decode(value);
console.log(">>>>>>>>>>>>>> [ThunderAI] chunk: " + JSON.stringify(chunk));
const lines = chunk.split("\n");
let parsedLines = [];
try{
parsedLines = lines
.map((line) => line.replace(/^data: /, "").trim()) // Remove the "data: " prefix
.map((line) => line.replace(/^: OPENROUTER PROCESSING/, "").trim()) // Remove the ": OPENROUTER PROCESSING " prefix
.filter((line) => line !== "" && line !== "[DONE]") // Remove empty lines and "[DONE]"
// .map((line) => JSON.parse(line)); // Parse the JSON string
.map((line) => {
try {
taLog.log("line: " + JSON.stringify(line));
return JSON.parse(line);
} catch (e) {
taLog.warn("JSON parse warning, skipped line: " + line + " - " + e.message);
return null;
}
})
.filter((parsed) => parsed !== null);
console.log(">>>>>>>>>>>>> [ThunderAI] line: " + JSON.stringify(line));
return JSON.parse(line);
});
chunk = chunk.substring(chunk.lastIndexOf('\n') + 1);
console.log(">>>>>>>>>>>>>> [ThunderAI] last chunk: " + JSON.stringify(chunk));
}catch(e){
taLog.error("Error parsing lines: " + e);
console.error(">>>>>>>>>>>>> [ThunderAI] error: " + JSON.stringify(e));
}
for (const parsedLine of parsedLines) {
const { choices } = parsedLine;
if (!choices || choices.length === 0) {
taLog.warn("No choices found in parsed line: " + JSON.stringify(parsedLine));
continue;
}
const { delta } = choices[0];
const { content } = delta;
// Update the UI with the new content

View file

@ -1,230 +0,0 @@
# 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 and display window)
```
### Data Flow: Inline Summary on Message Display
The `summarize_display_mode` preference (`'inline'` or `'webchat'`) controls where
the summary is displayed. The `summarize_auto` preference controls when it is triggered.
- `summarize_auto = 2` (automatic) always generates inline, regardless of `summarize_display_mode`.
- `summarize_auto = 3` (on receive) pre-caches the summary silently when the email arrives via `onNewMailReceived`. When the user later opens the message, the cache hit triggers an instant display.
- `summarize_auto = 1` (manual button) respects `summarize_display_mode`:
- `'inline'` → button click triggers inline generation
- `'webchat'` → button click opens the AI chat window via `_openSummaryWebchat()`
- Context menu summarize also respects `summarize_display_mode`:
- `'inline'` with a single message → generates inline via `_generateSummaryForMessage()`
- `'webchat'` or multiple messages → opens the AI chat window via `openChatGPT()`
```
User opens/selects a message in Thunderbird
mzta-compose-script.js (sends "initSummary" to background)
mzta-background.js (checks summarize_auto + summarize_display_mode prefs)
┌──────────────────────────────────────────────────────────┐
│ summarize_auto = 0 → do nothing │
│ summarize_auto = 1 → show "click to generate" button │
│ display_mode = inline → click triggers inline gen │
│ display_mode = webchat → click opens chat window │
│ summarize_auto = 2 → generate immediately (always inline)│
│ summarize_auto = 3 → cache hit (pre-cached on receive) │
└──────────────────────────────────────────────────────────┘
↓ (if generating inline)
taSummaryStore (check cache / set processing)
↓ (cache miss)
mzta-special-commands (via Web Worker, NOT chatgpt_web)
taSummaryStore (save result via taStorage)
mzta-compose-script.js (render summary banner in message body)
```
### Data Flow: Inline Translation on Message Display
The `translate_auto` preference controls when translation is triggered.
Translation always renders inline (webchat mode has been removed).
The target language is determined by `translate_lang` (fallback on `default_chatgpt_lang`).
```
User opens/selects a message in Thunderbird
mzta-compose-script.js (sends "initTranslation" to background)
mzta-background.js (checks translate + translate_auto prefs)
┌──────────────────────────────────────────────────────────┐
│ translate_auto = 0 → do nothing │
│ translate_auto = 1 → show "click to translate" button │
│ translate_auto = 2 → generate immediately │
└──────────────────────────────────────────────────────────┘
taTranslationStore (check cache / set processing)
↓ (cache miss)
mzta-special-commands (via Web Worker, NOT chatgpt_web)
taTranslationStore (save result via taStorage)
mzta-compose-script.js (render translation banner in message body)
```
### Data Flow: Background Summary on Email Receive (summarize_auto = 3)
When `summarize_auto = 3`, a summary is generated silently when a new email arrives. The flow mirrors `add_tags_auto`:
```
New email arrives
browser.messages.onNewMailReceived
newEmailListener (checks _process_incoming, which includes summarize_auto === 3)
processEmails({ summarizeOnReceive: true })
↓ (single loop — shared with addTagsAuto / spamFilter / translateOnReceive)
_generateSummaryForMessage(headerMessageId, null, { messageData })
← tabId is null → no UI messages sent, silent pre-cache
taSummaryStore.saveSummary()
[later] user opens the message → initSummary → cache hit → showSummary instantly
```
### Data Flow: Background Translation on Email Receive (translate_auto = 3)
When `translate_auto = 3`, a translation is generated silently when a new email arrives. Mirrors the summarize on-receive flow:
```
New email arrives
browser.messages.onNewMailReceived
newEmailListener (checks _process_incoming, which includes translate_auto === 3)
processEmails({ translateOnReceive: true })
↓ (single loop — shared with addTagsAuto / spamFilter / summarizeOnReceive)
_generateTranslationForMessage(headerMessageId, null, { messageData })
← tabId is null → no UI messages sent, silent pre-cache
taTranslationStore.saveTranslation()
[later] user opens the message → initTranslation → cache hit → showTranslation instantly
```
## 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, `buildSummaryPrompt()` for unified summary prompt assembly, `buildTranslationPrompt()` for translation prompt assembly) |
| `js/mzta-compose-script.js` | Content script for compose and message display: injects AI response into compose window, renders unified toolbar (spam badge, summary/translation trigger buttons) and content panels (generic error, spam explanation, summary, translation) in message display via `#mzta-container` |
| `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-storage.js` | Unified per-message storage layer (`taStorage` class) for summary, spam, and translation data |
| `js/mzta-summarystore.js` | Summary-specific storage wrapper (`taSummaryStore` class) with caching, truncation, and processing-state tracking |
| `js/mzta-translationstore.js` | Translation-specific storage wrapper (`taTranslationStore` class) with caching, truncation, and processing-state tracking |
| `js/mzta-working-status.js` | Visual status indicator during AI processing |
| `js/mzta-addtags-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 |
| `menu_order/` | Drag-and-drop reordering and visibility control for popup and context menus |
| `spamfilter/` | Spam filter settings |
| `summarize/` | Email summarization settings |
| `translate/` | Email translation 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.
### Per-Message Data Storage
Per-message data (summaries, spam reports, translations) is stored via `js/mzta-storage.js` (`taStorage` class). Each record is keyed by `msg:<headerMessageId>` in `messenger.storage.local` and follows schema version 1. Records contain optional fields: `summary`, `spam`, `translation`, plus metadata (`v`, `ts`). The `taStorage` class provides typed read/write/delete methods per field, automatic record cleanup when all fields are removed, and age-based cleanup.
`js/mzta-summarystore.js` (`taSummaryStore` class) wraps `taStorage` for summary-specific operations: load/save/remove summaries, track in-flight generation state via `browser.storage.session`, enforce a 100-entry cache limit with oldest-first truncation, and store error states.
`js/mzta-translationstore.js` (`taTranslationStore` class) wraps `taStorage` for translation-specific operations: load/save/remove translations, track in-flight generation state via `browser.storage.session`, enforce a 100-entry cache limit with oldest-first truncation, and store error states. Each translation record stores `translated_text`, `lang`, and optional error information.

View file

@ -1,203 +0,0 @@
# 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 menus |
| `position_display` | number | Sort order for the popup menu in reading view |
| `position_compose` | number | Sort order for the popup menu in compose view |
| `position_context` | number | Sort order for the context menu |
| `show_in` | string | `"popup"` = popup only, `"context"` = context menu only, `"both"` = both, `"none"` = hidden from all menus. Default: `"popup"` for default/custom prompts, `"both"` for special prompts |
| `custom_icon` | string | Filename (with extension) of an icon in `images/context_menu/custom/` used as the context-menu icon. Empty string = no icon. Only used for non-special prompts (special prompts use their hard-coded icons in `specialPromptToContextMenuID`). Selectable from a dropdown on the Menu Order page, context-menu tab. |
### 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 |
| `translate` | Translate email content into a target language |
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`.
## Menu System
### Popup Menu
- Displays prompts filtered by `show_in` (`"popup"` or `"both"`) and by tab context (`type` property: reading view shows types `0`+`1`, compose view shows types `0`+`2`)
- Ordering: always position-based using `position_display` (reading view) or `position_compose` (compose view). Alphabetical ordering has been removed
- Special prompts retain their colored background (CSS class `special_prompt`) in the popup based on `is_special == "1"`
### Context Menu
- Dynamically built from all prompts with `show_in` set to `"context"` or `"both"`, filtered to reading types only (`type` 0 or 1)
- Appears as a "ThunderAI" submenu in the `message_list` context
- Ordering: position-based using `position_context` (fallback to alphabetical only when positions are equal)
- Special prompts (add_tags, spamfilter, summarize, translate) route through `processEmails()` for batch processing; regular prompts execute via `menus.executeMenuAction()`
- Icons: special prompts use dedicated icons (defined in `contextMenuIconsPath`); all other prompts use the addon icon (`images/icon-32.png`)
- Add Tags in context menu assigns tags automatically (`addTagsAuto: true`), while in the popup it shows the interactive tag selection form
### Menu Order Page (`pages/menu_order/`)
Dedicated page for reordering, enabling, and disabling menu items across both the popup and the context menu. Opened from the options page via the "Menu Order" button.
**UI layout** — two side-by-side panels:
- **Popup Menu panel**: sub-tabs for "Reading" / "Composing" switch the list between `position_display` / `position_compose` ordering and between the allowed types (`0`+`1` vs `0`+`2`)
- **Context Menu panel**: single list ordered by `position_context`. Items with `type: "2"` (composing-only) are never shown here
Each list has two sections:
- **Visible items**: active for the menu (`show_in` includes the menu), draggable to reorder
- **Hidden items**: inactive for the menu (`show_in` excludes the menu), sorted alphabetically, not draggable
**Toggle coordination** — flipping the checkbox updates the prompt's `show_in` with four-state logic:
- Popup ON: `"none"``"popup"`, `"context"``"both"`
- Popup OFF: `"popup"``"none"`, `"both"``"context"`
- Context ON: `"none"``"context"`, `"popup"``"both"`
- Context OFF: `"context"``"none"`, `"both"``"popup"`
**Drag and drop** — native HTML5 DnD assigns sequential position numbers (1, 2, 3, ...) to `position_display`, `position_compose`, or `position_context` depending on which list is being sorted.
**Exclusions from the UI** (preserved on save so data is not lost):
- Prompts with `enabled === 0` (disabled)
- Special prompts whose base definition has `show_in: "none"` (internal prompts like `prompt_summarize_email_template` and `prompt_summarize_email_separator`) — retrieved via `getHiddenSpecialPromptIds()`
- Special prompts whose feature is not active — retrieved from background via `get_active_special_ids` message, which calls `getActiveSpecialPromptsIDs()` with current prefs and `_sparks_presence`
**Cross-tab reload** — the page listens on `browser.storage.onChanged` for changes to `_default_prompts_properties`, `_custom_prompt`, or `_special_prompts`. When one of those keys changes (e.g. user saves from the Custom Prompts page in another tab), the page reloads its data with a 200ms debounce. Any unsaved local changes are discarded to avoid overwriting the other page's work.
**Save flow**:
1. Re-concat preserved prompts (disabled + hidden-specials + inactive-feature specials) with the UI-visible prompts
2. Split by `is_default` / `is_special` and call `setDefaultPromptsProperties()`, `setCustomPrompts()`, `setSpecialPrompts()`
3. Send `reload_menus` to the background to rebuild both menus
### Alphabetic-to-Position Migration
The `dynamic_menu_order_alphabet` preference (previously a user-facing option) has been retired and removed from the UI, but the key still exists in storage as a one-shot migration flag. At every background startup, `migrateMenuOrderAlphabetic()` in `js/mzta-prompts.js` runs:
1. Reads `dynamic_menu_order_alphabet` (defaults to `true` if unset)
2. If `true`: sorts all visible prompts with special prompts first (alphabetically), then the rest (alphabetically), and assigns sequential `position_display` = `position_compose` = `position_context` numbers. Hidden special prompts are preserved untouched.
3. Persists the new positions via `setDefaultPromptsProperties` / `setCustomPrompts` / `setSpecialPrompts`
4. Sets `dynamic_menu_order_alphabet = false` in sync storage so the migration does not run again
This ensures existing users upgrading from the previous alphabetical-default behaviour get the same visible ordering on first run, while subsequent launches keep whatever custom ordering the user has set.
### Special Prompt Visibility Dependencies
`getActiveSpecialPromptsIDs()` in `js/mzta-utils.js` maps feature prefs to active special prompt IDs. Notable dependency:
- `prompt_get_calendar_event_from_clipboard` is emitted only if **both** `get_calendar_event` and `get_calendar_event_from_clipboard` are active. If `get_calendar_event` is off, neither calendar prompt is shown regardless of the clipboard pref.
### Summarize: Dual-Mode Prompt System
The summarize feature uses two distinct prompt pathways:
**Context Menu Summarize** (right-click on messages in message list):
- Activated via the `summarize` context menu item, controlled by the `summarize` feature flag
- Uses 3 special prompts stored in `specialPrompts`:
- `prompt_summarize` — the main instruction prompt for the LLM
- `prompt_summarize_email_template` — template for formatting each email's content
- `prompt_summarize_email_separator` — separator text between multiple emails
- Supports multi-email summarization: each selected message is formatted with the email template, joined by the separator, then prepended with the instruction prompt
- All 3 prompts support placeholder autocomplete (`{%placeholder%}` syntax)
- Result is displayed via `openChatGPT()` in the standard chat output window (not inline)
- Default prompt texts are stored as i18n keys: `prompt_summarize_full_text`, `prompt_summarize_email_template_full_text`, `prompt_summarize_email_separator_full_text`
**Inline Summary on Message Display** (automatic or manual per `summarize_auto` pref):
- Uses the same 3 special prompts as webchat mode, via `taPromptUtils.buildSummaryPrompt()` in `js/mzta-utils-prompt.js`
- Does **not** support `chatgpt_web` connection type (shows error if configured)
- Result is rendered as a styled banner at the top of the message body via `mzta-compose-script.js`
- Banner includes a refresh button (↻) to regenerate the summary
- Cached per-message via `taSummaryStore` / `taStorage` (max 100 entries)
**Unified Prompt Building** — `taPromptUtils.buildSummaryPrompt(messageDataArray)`:
- All summary paths (inline, webchat single, webchat multi) use this single method
- Accepts an array of `{ message, fullMessage }` entries
- Returns `{ promptText, promptInfo }` where `promptInfo` is the `prompt_summarize` prompt object
### Translate: Inline-Only Prompt System
The translate feature uses a single special prompt (`prompt_translate_this`) for translating emails. Translation always renders inline (no webchat mode).
**Inline Translation on Message Display** (controlled by `translate_auto` pref):
- Uses a single special prompt: `prompt_translate_this`
- The prompt uses placeholders (`{%mail_subject%}`, `{%mail_html_body%}`, `{%thunderai_translate_lang%}`, `{%thunderai_translate_exclude_lang%}`) resolved via the standard placeholder system
- The AI response is a JSON object: `{ "subject": "...", "body": "...", "status": "1"|"-1" }`
- `status = "1"`: translation completed, subject and body are displayed
- `status = "-1"`: translation skipped (excluded/target language), a "skipped" message is shown
- Target language is determined by `translate_lang` pref, falling back to `default_chatgpt_lang`
- Does **not** support `chatgpt_web` connection type (shows error if configured)
- Result is rendered as a styled banner (green/teal theme) in the message body via `mzta-compose-script.js`
- Banner includes refresh (↻) and delete (×) buttons
- Cached per-message via `taTranslationStore` / `taStorage` (max 100 entries)
- The prompt was originally a regular prompt (`defaultPrompts`) and was moved to `specialPrompts` with `is_special: "1"` and `type: "1"` (reading email only)
**Prompt Building** — `taPromptUtils.buildTranslationPrompt(fullMessage)`:
- Retrieves the `prompt_translate_this` special prompt text
- Resolves placeholders via `placeholdersUtils.getPlaceholdersValues()` + `replacePlaceholders()`
- Returns `{ promptText, promptInfo }`
## 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.

View file

@ -1,79 +0,0 @@
# 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 |
| `mail_full_headers` | All mail headers (key: value format, newline-separated) | 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`

View file

@ -1,119 +0,0 @@
# 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`, `ollama_format_json`
- 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`, `anthropic_extended_thinking_budget`
- **Extended thinking**: when `anthropic_extended_thinking_budget > 0`, the request body adds `thinking: { type: 'enabled', budget_tokens: N }` and **omits** `temperature` (the Claude API forbids setting temperature with extended thinking). Thinking output arrives in the SSE stream as `content_block_delta` events with `delta.type === 'thinking_delta'` and is forwarded to the webchat UI as `newThinkingToken` messages, captured into a `thinkingAccumulator` in the worker and passed on `tokensDone`.
## Thinking output in the webchat UI
Two provider categories emit reasoning/thinking content:
- **Ollama / OpenAI Compatible**: thinking arrives inline in the normal token stream wrapped in `<think>…</think>` tags. `MessagesArea.flushAccumulatingMessage()` strips these blocks from the rendered text and renders them as a `<details class="thinking-block">` prepended to the answer. If an unterminated `<think>` is detected mid-stream, the flush is deferred until the closing tag arrives.
- **Anthropic**: thinking is captured in the worker and posted to the controller as `newThinkingToken`. `MessagesArea` accumulates it and renders the same `<details>` block on final flush.
The global `hide_thinking` pref (default `true`) controls the **initial open/collapsed state** of the thinking block: `true` → collapsed, `false` → open. The user can always toggle by clicking. Thinking content is never discarded. Other providers (Google Gemini, OpenAI Responses, ChatGPT Web) are not affected by this UI logic.
## Configuration Validation
For special prompts (`mzta_specialCommand`), required fields are validated in `initWorker()` (`js/mzta-special-commands.js`) **before** the worker is created. If a required field is empty, an `Error` with `isConfigError = true` is thrown. Validation covers:
| Provider | Required fields |
|----------|----------------|
| `chatgpt_api` | `chatgpt_api_key`, `chatgpt_model` |
| `google_gemini_api` | `google_gemini_api_key`, `google_gemini_model` |
| `ollama_api` | `ollama_host`, `ollama_model` |
| `openai_comp_api` | `openai_comp_host`, `openai_comp_model` |
| `anthropic_api` | `anthropic_api_key`, `anthropic_model`, `anthropic_version` |
Validation is skipped when `use_specific_api = true` (i.e., the prompt's own `api_type` overrides the global setting — credentials come from the prompt config, not global prefs).
The `isConfigError` flag on the thrown error tells callers in `mzta-background.js` to display the error in the panel **without saving it to storage** — so the user can fix settings and retry cleanly.
Feature-specific routing of `isConfigError`:
- `summarize` / `translate` / `spamfilter`: the error is shown in their dedicated panel (summary / translation / spam panel) and **not** persisted to storage.
- `add_tags`: it has **no dedicated panel**, so the error is routed to the **generic error panel** via `showGenericError(errMsg, source)` in `mzta-background.js`, which broadcasts a `showGenericError` message to all tabs. The content script `js/mzta-compose-script.js` renders it as `#mzta-generic-error` inside `#mzta-container`. The panel is dismissible and reusable by any future feature without its own UI.
For regular prompts (`openChatGPT()`), validation still happens inside the listener callback after the API webchat window is created (unchanged behavior).
## 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`

View file

@ -1,161 +0,0 @@
# 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, anthropic_extended_thinking_budget
```
Plus the global connection selector:
```
connection_type (default: 'chatgpt_web')
use_specific_integration (default: false)
```
### Special Prompt Integration Overrides
The 6 special prompts (`add_tags`, `spamfilter`, `summarize`, `get_calendar_event`, `get_task`, `translate`) 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` | Internal migration flag only; no UI. Set to `false` by `migrateMenuOrderAlphabetic()` on first boot after upgrade to bootstrap position-based ordering. See `claude-spec/02-prompts.md` for details. |
| `placeholders_use_default_value` | `false` | Use placeholder defaults when empty |
| `hide_thinking` | `true` | Controls the initial state of the thinking `<details>` block prepended above the answer: `true` = collapsed by default, `false` = open by default. The user can always toggle with a click; thinking content is never discarded. |
| `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 |
| `summarize_auto` | `1` | Auto-summarize mode: `0` = disabled, `1` = manual (show "click to generate" button), `2` = automatic (generate on message open), `3` = generate on email receive (background pre-cache via `onNewMailReceived`, no UI during generation) |
| `summarize_display_mode` | `'inline'` | Where to display summaries: `'inline'` = message pane banner, `'webchat'` = AI chat window. Note: `summarize_auto = 2` and `summarize_auto = 3` always use inline regardless of this setting. |
| `summarize_max_display_length` | `0` | Maximum characters shown in inline summary before truncation. `0` = no limit (show full text). When set, text is truncated at a word boundary and a "See more"/"See less" toggle link is shown. |
| `summarize_strip_formatting` | `false` | Strip HTML and Markdown formatting from AI-generated summaries, showing plain text only. |
| `translate` | `true` | Enable email translation |
| `translate_auto` | `0` | Auto-translate mode: `0` = disabled, `1` = manual (show button), `2` = automatic (translate on message open), `3` = generate on email receive (background pre-cache via `onNewMailReceived`, no UI during generation) |
| `translate_max_display_length` | `0` | Maximum characters shown in inline translation before truncation. `0` = no limit (show full text). When set, text is truncated at a word boundary and a "See more"/"See less" toggle link is shown. |
| `translate_lang` | `''` | Target language for translation. Falls back to `default_chatgpt_lang` if empty. |
### Summarize Settings Page (`pages/summarize/`)
The summarize settings page provides:
1. **Specific integration checkbox** — enables per-feature API override (like other special prompts)
2. **Auto-summarize dropdown** (`summarize_auto`) — three modes:
- `0` (Disabled) — no inline summaries
- `1` (Manual) — shows a "Click to generate summary" button in message display
- `2` (Automatic) — generates summary immediately when message is opened
3. **Display mode dropdown** (`summarize_display_mode`) — controls where summaries are shown:
- `'inline'` — summary banner in the message pane (default)
- `'webchat'` — opens the AI chat window
- Note: `summarize_auto = 2` always generates inline regardless of this setting. Context menu summarize with multiple messages always falls back to webchat.
4. **Max display length** (`summarize_max_display_length`) — number input, limits inline summary text to N characters. `0` = no limit. When truncated, a "See more"/"See less" toggle link is appended.
5. **Strip formatting** (`summarize_strip_formatting`) — checkbox, removes HTML/Markdown formatting from AI summary responses, displaying plain text only. Default: off.
6. **Three editable prompts** (used by context menu summarize and webchat mode):
- Summarize instruction prompt (`prompt_summarize`)
- Email template prompt (`prompt_summarize_email_template`)
- Email separator prompt (`prompt_summarize_email_separator`)
- Each has Save/Reset buttons and placeholder autocomplete
- Default text comes from i18n strings (`prompt_summarize_full_text`, etc.)
### Menu Order Page (`pages/menu_order/`)
Entry point from the options page via the "Menu Order" button (next to "Manage your prompts"). Provides drag-and-drop reordering and toggle-based visibility control for both the popup and the context menu. See `claude-spec/02-prompts.md` ("Menu Order Page") for the full behaviour, data flow, and exclusion rules.
### Translate Settings Page (`pages/translate/`)
The translate settings page provides:
1. **Specific integration checkbox** — enables per-feature API override (like other special prompts)
2. **Auto-translate dropdown** (`translate_auto`) — three modes:
- `0` (Disabled) — no inline translations
- `1` (Manual) — shows a "Get AI Translation" button in message display
- `2` (Automatic) — generates translation immediately when message is opened
3. **Max display length** (`translate_max_display_length`) — number input, limits inline translation text to N characters. `0` = no limit. When truncated, a "See more"/"See less" toggle link is appended.
4. **Target language** (`translate_lang`) — text input for the destination language. If empty, falls back to `default_chatgpt_lang`.
5. **One editable prompt** — the translation instruction prompt (`prompt_translate_this`) with Save/Reset buttons and placeholder autocomplete. Default text comes from i18n string `prompt_translate_this_full_text`.
## 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;
```

View file

@ -1,91 +0,0 @@
# 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 |

View file

@ -1,398 +0,0 @@
# 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

View file

@ -25,7 +25,6 @@ table {
thead {
position: sticky;
top: 26px;
z-index: 90;
}
#command_palette{
@ -34,7 +33,6 @@ thead {
padding-top: 1px;
height: 26px;
width: 100%;
z-index: 90;
}
#btnNew{
@ -104,10 +102,6 @@ table .sort.desc {
background-image: url(../images/listjs-arrow-sort-active-up.png);
}
tbody.list tr{
vertical-align: top;
}
.hiddendata{
display: none;
}
@ -141,24 +135,6 @@ tbody.list tr{
font-style: italic;
}
.field_title{
font-weight: bold;
}
.field_title_s{
font-weight: bold;
font-size: small;
}
.field_title_us{
font-weight: bold;
font-size: smaller;
}
label, .id_show, .text_show, .name_show, .type_show, .action_show{
font-size: small;
}
#txtIdNew, .id_output{
text-transform: lowercase;
}
@ -187,111 +163,6 @@ label, .id_show, .text_show, .name_show, .type_show, .action_show{
margin-bottom: 0px;
}
.autocomplete-container {
position: relative;
}
.autocomplete-list {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: white;
border: 1px solid #ccc;
z-index: 1000;
max-height: 200px;
overflow-y: auto;
padding: 0;
margin: 0;
list-style: none;
font-size: small;
}
.autocomplete-list li {
padding: 8px;
cursor: pointer;
}
.autocomplete-list li:hover {
background-color: #f0f0f0;
}
.autocomplete-list li.active {
background-color: #ddd;
}
.hidden {
display: none;
}
.need_custom_text_span, .need_selected_span{
padding-right: 2px;
}
#chatgpt_web_additional_info_toggle td{
padding:0px 5px;
font-style: italic;
}
#chatgpt_web_additional_info, #chatgpt_web_additional_info_toggle, .chatgpt_web_additional_info, .chatgpt_web_additional_info_toggle, .chatgpt_web_additional_info_show{
background-color: rgb(255, 209, 183);
display:none;
}
#chatgpt_web_additional_info_toggle, .chatgpt_web_additional_info_toggle{
cursor: pointer;
}
#chatgpt_web_additional_info_toggle:hover, .chatgpt_web_additional_info_toggle:hover{
background-color: rgb(255, 189, 153);
text-decoration: underline;
}
#chatgpt_web_additional_info_toggle td{
text-align: center;
}
#chatgpt_web_additional_info td{
vertical-align: top;
}
.chatgpt_web_additional_info_toggle{
width: -moz-available;
text-align: center;
margin-top: 10px;
}
.chatgpt_web_additional_info_show{
flex-direction: column;
justify-content: space-between;
height: 100%;
padding: 3px;
margin-top: 5px;
}
.chatgpt_web_additional_info_row{
display: flex;
}
.chatgpt_web_additional_info_row span{
margin-right: 0.4em;
}
input.input_additional[type="text"]{
width: -moz-available;
}
.small_info{
font-size:smaller;
}
.conntype_chatgpt_web_option{
cursor: pointer;
}
label:has(input[type="checkbox"]) {
cursor: pointer;
}
@media (prefers-color-scheme: dark) {
body {
@ -325,27 +196,6 @@ label:has(input[type="checkbox"]) {
.storage_space{
color: rgb(182, 182, 182);
}
}
.autocomplete-list {
background-color: #2E2F36;
border: 1px solid #2E2F36;
}
.autocomplete-list li:hover {
background-color: #4c4e58;
}
.autocomplete-list li.active {
background-color: #4c4e58;
}
#chatgpt_web_additional_info, #chatgpt_web_additional_info_toggle, .chatgpt_web_additional_info, .chatgpt_web_additional_info_toggle, .chatgpt_web_additional_info_show{
background-color: rgb(39, 11, 0);
color: rgb(182, 182, 182);
}
#chatgpt_web_additional_info_toggle:hover, .chatgpt_web_additional_info_toggle:hover{
background-color: rgb(88, 25, 0);
}
}

View file

@ -0,0 +1,100 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ThunderAI - __MSG_customPrompts_managePrompts__</title>
<link rel="stylesheet" href="mzta-custom-prompts.css">
</head>
<body>
<div>
<h1 class="page_title">__MSG_customPrompts_managePrompts__</h1>
<p>__MSG_customPrompts_managePrompts_info_default__<br>
__MSG_customPrompts_managePrompts_info_default_2__<br>
__MSG_customPrompts_managePrompts_info_default_3__
<br><a href="https://micz.it/thunderbird-addon-thunderai/custom-prompts/">__MSG_customPrompts_managePrompts_help__</a></p>
</div>
<div id="import_export">
<button id="btnExportAll">__MSG_customPrompts_ExportAll__</button><br>
<button id="btnImport">__MSG_customPrompts_Import__</button>
</div>
<div id="command_palette">
<button id="btnSaveAll" disabled>__MSG_customPrompts_btnSaveAll__</button>
<span id="msgDisplay"></span>
<button id="btnNew">__MSG_customPrompts_btnNew__</button>
</div>
<div id="formNew">
<table class="width_all">
<tr>
<td class="w19">
<label for="txtIdNew">*__MSG_customPrompts_form_label_ID__:</label>
<br>
<input type="text" id="txtIdNew" name="id" class="input_new" tabindex="1">
<br><span class="small_label">[__MSG_customPrompts_form_label_ID_rules__]</span>
</td>
<td rowspan="2" class="w25">
<label for="txtTextNew">*__MSG_customPrompts_form_label_Text__:</label>
<br>
<textarea id="txtTextNew" name="text" class="input_new text_output" tabindex="3"></textarea>
</td>
<td class="w19">
<label for="selectTypeNew">__MSG_customPrompts_add_to_menu__:</label>
<br>
<select id="selectTypeNew" name="type" tabindex="4">
<option value="0">__MSG_customPrompts_add_to_menu_always__</option>
<option value="1">__MSG_customPrompts_add_to_menu_reading__</option>
<option value="2">__MSG_customPrompts_add_to_menu_composing__</option>
</select>
</td>
<td rowspan="2" class="w19">
<input type="checkbox" id="checkboxNeedSelectedNew" name="need_selected" value="1" tabindex="6"> <label for="checkboxNeedSelectedNew">__MSG_customPrompts_form_label_need_selected__</label>
<br>
<input type="checkbox" id="checkboxNeedSignatureNew" name="need_signature" value="1" tabindex="7"> <label for="checkboxNeedSignatureNew">__MSG_customPrompts_form_label_need_signature__</label>
<br>
<input type="checkbox" id="checkboxNeedCustomTextNew" name="need_custom_text" value="1" tabindex="8"> <label for="checkboxNeedCustomTextNew">__MSG_customPrompts_form_label_need_custom_text__</label>
<br>
<input type="checkbox" id="checkboxDefineResponseLangNew" name="define_response_lang" value="1" tabindex="8"> <label for="checkboxDefineResponseLangNew">__MSG_customPrompts_form_label_define_response_lang__</label>
</td>
</tr>
<tr>
<td class="w19">
<label for="txtNameNew">*__MSG_customPrompts_form_label_Name__:</label>
<br>
<input type="text" id="txtNameNew" name="name" class="input_new" tabindex="2">
</td>
<td class="w19">
<label for="selectActionNew">__MSG_customPrompts_form_label_Action__:</label>
<br>
<select id="selectActionNew" name="action" tabindex="5">
<option value="0">__MSG_customPrompts_close_button__</option>
<option value="1">__MSG_customPrompts_do_reply__</option>
<option value="2">__MSG_customPrompts_substitute_text__</option>
</select>
</td>
</tr>
</table>
<div class="go_right_float">* __MSG_customPrompts_form_required_fields__</div>
<br>
<div class="go_right"><button id="btnAddNew" disabled>__MSG_customPrompts_btnAddNewCommit__</button></div>
</div>
<div id="all_prompts">
<table class="prompts_list">
<thead>
<tr>
<th class="sort" data-sort="id">__MSG_customPrompts_form_label_ID__</th>
<th class="sort" data-sort="name">__MSG_customPrompts_form_label_Name__</th>
<th class="sort" data-sort="text">__MSG_customPrompts_form_label_Text__</th>
<th class="sort" data-sort="type">__MSG_customPrompts_add_to_menu__</th>
<th>Properties</th>
<th>__MSG_customPrompts_form_label_Action__</th>
</tr>
</thead>
<tbody class="list">
</tbody>
</table>
</div>
<div class="storage_space">__MSG_StorageSpace__: <span id="storage_space"></span></div>
<script src="list.js"></script>
<script src="mzta-custom-prompts.js" type="module"></script>
<script src="../js/mzta-i18n.js"></script>
</body>
</html>

View file

@ -1,6 +1,6 @@
/*
* ThunderAI [https://micz.it/thunderbird-addon-thunderai/]
* Copyright (C) 2024 - 2026 Mic (m@micz.it)
* Copyright (C) 2024 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,43 +16,30 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { prefs_default } from "../../options/mzta-options-default.js";
import {
getLocalStorageUsedSpace,
sanitizeHtml,
openTab
} from "../../js/mzta-utils.js";
import { taLogger } from "../../js/mzta-logger.js";
import {
getPlaceholders,
setCustomPlaceholders,
getCustomPlaceholders,
prepareCustomDataPHsForExport,
prepareCustomDataPHsForImport,
placeholdersUtils
} from "../../js/mzta-placeholders.js";
import { textareaAutocomplete } from "../../js/mzta-placeholders-autocomplete.js";
import { getPrompts, setDefaultPromptsProperties, setCustomPrompts, preparePromptsForExport, preparePromptsForImport } from "../js/mzta-prompts.js";
import { isThunderbird128OrGreater, getCustomPromptsUsedSpace } from "../js/mzta-utils.js";
import { taLogger } from "../js/mzta-logger.js";
let prefs = null;
var customDataPHsList = null;
var promptsList = null;
var somethingChanged = false;
var positionMax_compose = 0;
var positionMax_display = 0;
var idnumMax = 0;
var msgTimeout = null;
let taLog = null;
let autocompleteSuggestions = [];
document.addEventListener('DOMContentLoaded', async () => {
prefs = await browser.storage.sync.get({ do_debug: prefs_default.do_debug });
taLog = new taLogger("mzta-custom-dataplaceholders", prefs.do_debug);
let prefs_debug = await browser.storage.sync.get({do_debug: false});
taLog = new taLogger("mzta-custom-prompts", prefs_debug.do_debug);
setStorageSpace();
let values = await getCustomPlaceholders();
let values = await getPrompts();
//console.log('>>>>>>>>>>>>>>>> values: ' + JSON.stringify(values));
loadCustomDataPHsList(values);
loadPromptsList(values);
const btnSaveAll = document.getElementById('btnSaveAll');
btnSaveAll.disabled = true;
@ -79,38 +66,63 @@ document.addEventListener('DOMContentLoaded', async () => {
}
btnNew.addEventListener('click', handleNewClick);
// Display selected value next to select input
// function handleSelectChange(e) {
// e.preventDefault();
// const spanElement = e.target.nextElementSibling;
// spanElement.textContent = e.target.value;
// }
// document.querySelectorAll('select.input_mod').forEach(element => {
// element.addEventListener('change', handleSelectChange);
// });
// Log data ID number from item row and prepare for edit action
// Handle "type" select changes and log new state
// function handleTypeSelectChange(e) {
// e.preventDefault();
// const spanElement = e.target.nextElementSibling;
// spanElement.textContent = e.target.value;
// }
// let type_select_elements = document.querySelectorAll("select.type_output");
// type_select_elements.forEach(element => {
// element.addEventListener('change', handleTypeSelectChange);
// });
// Handle "action" select changes and log new state
// function handleActionSelectChange(e) {
// e.preventDefault();
// const spanElement = e.target.nextElementSibling;
// spanElement.textContent = e.target.value;
// }
// let action_select_elements = document.querySelectorAll("select.action_output");
// action_select_elements.forEach(element => {
// element.addEventListener('change', handleActionSelectChange);
// });
// for the new prompt form
let btnNew_elements = document.querySelectorAll(".input_new");
if(btnNew_elements) {
btnNew_elements.forEach(element => {
element.addEventListener('input', (e) => {
element.addEventListener('change', (e) => {
e.preventDefault();
checkFields();
});
});
}
const textareas = document.querySelectorAll('.editor');
autocompleteSuggestions = (await getPlaceholders())
.filter(p => p.is_default == "1")
.map(p => ({ command: '{%' + p.id + '%}', type: p.type }));
// console.log('>>>>>>>>>>> suggestions: ' + JSON.stringify(suggestions));
textareas.forEach(textarea => {
textareaAutocomplete(textarea, autocompleteSuggestions);
textareas.forEach(textarea => {
textareaAutocomplete(textarea, autocompleteSuggestions);
});
});
i18n.updateDocument();
//To add a new item
var txtIdNew = document.getElementById('txtIdNew');
var txtNameNew = document.getElementById('txtNameNew');
var txtTextNew = document.getElementById('txtTextNew');
var selectTypeNew = document.getElementById('selectTypeNew');
var selectTypeNew = document.getElementById('selectTypeNew');
var selectActionNew = document.getElementById('selectActionNew');
var checkboxNeedSelectedNew = document.getElementById('checkboxNeedSelectedNew');
var checkboxNeedSignatureNew = document.getElementById('checkboxNeedSignatureNew');
var checkboxNeedCustomTextNew = document.getElementById('checkboxNeedCustomTextNew');
var checkboxDefineResponseLangNew = document.getElementById('checkboxDefineResponseLangNew');
const btnAddNew = document.getElementById('btnAddNew');
btnAddNew.addEventListener('click', (e) => {
@ -118,17 +130,22 @@ document.addEventListener('DOMContentLoaded', async () => {
if(!checkFields()) {
return;
}
let newItemData = {
let newItem = promptsList.add({
id: String(txtIdNew.value.trim()).toLocaleLowerCase(),
name: txtNameNew.value.trim(),
text: txtTextNew.value.trim(),
type: selectTypeNew.value,
type: selectTypeNew.value,
action: selectActionNew.value,
need_selected: (checkboxNeedSelectedNew.checked) ? 1 : 0,
need_signature: (checkboxNeedSignatureNew.checked) ? 1 : 0,
need_custom_text: (checkboxNeedCustomTextNew.checked) ? 1 : 0,
define_response_lang: (checkboxDefineResponseLangNew.checked) ? 1 : 0,
enabled: 1,
position_compose: positionMax_compose + 1,
position_display: positionMax_display + 1,
is_default: 0,
idnum: idnumMax + 1,
};
let newItem = customDataPHsList.add(newItemData);
});
idnumMax++;
let curr_idnum = newItem[0].values().idnum;
let checkboxes = document.querySelectorAll(`tr[data-idnum="${curr_idnum}"] input[type="checkbox"]`);
@ -160,14 +177,14 @@ document.addEventListener('DOMContentLoaded', async () => {
const btnExportAll = document.getElementById('btnExportAll');
btnExportAll.addEventListener('click', (e) => {
e.preventDefault();
exportCustomDataPHs();
exportPrompts();
});
async function exportCustomDataPHs() {
async function exportPrompts() {
const manifest = browser.runtime.getManifest();
const addonVersion = manifest.version;
const outputCustomDataPHs = prepareCustomDataPHsForExport(await getCustomPlaceholders());
let outputObj = {id: 'thunderai-custom-data-placeholders', addon_version: addonVersion, customdataplaceholders: outputCustomDataPHs};
const outputPrompts = preparePromptsForExport(await getPrompts());
let outputObj = {id: 'thunderai-prompts', addon_version: addonVersion, prompts: outputPrompts};
const blob = new Blob([JSON.stringify(outputObj, null, 2)], {
type: "application/json",
});
@ -175,7 +192,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const time_stamp = `${currentDate.getFullYear()}${String(currentDate.getMonth() + 1).padStart(2, '0')}${String(currentDate.getDate()).padStart(2, '0')}${String(currentDate.getHours()).padStart(2, '0')}${String(currentDate.getMinutes()).padStart(2, '0')}${String(currentDate.getSeconds()).padStart(2, '0')}`;
messenger.downloads.download({
url: URL.createObjectURL(blob),
filename: `thunderai-custom-data-placeholders-${time_stamp}.json`,
filename: `thunderai-prompts-${time_stamp}.json`,
saveAs: true,
});
}
@ -183,58 +200,60 @@ document.addEventListener('DOMContentLoaded', async () => {
const btnImport = document.getElementById('btnImport');
btnImport.addEventListener('click', (e) => {
e.preventDefault();
importCustomDataPHs();
importPrompts();
});
function importCustomDataPHs() {
if(confirm(browser.i18n.getMessage("importCustomDataPH_confirmText") + '\n' + browser.i18n.getMessage("customDataPH_manageDataPH_info_default_2") + '\n' + browser.i18n.getMessage("customPrompts_managePrompts_info_default_3"))) {
//ask the user to choose a JSON file, and then read it, check if the serialized JSON is valid as generated from importCustomDataPHs(), and if so, add it to the list
function importPrompts() {
if(confirm(browser.i18n.getMessage("importPrompts_confirmText") + '\n' + browser.i18n.getMessage("customPrompts_managePrompts_info_default_2") + '\n' + browser.i18n.getMessage("customPrompts_managePrompts_info_default_3"))) {
//ask the user to choose a JSON file, and then read it, check if the serialized JSON is valid as generated from exportPrompts(), and if so, add it to the list
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.click();
input.onchange = async () => {
setMessage(browser.i18n.getMessage('importCustomDataPH_start_import'));
setMessage(browser.i18n.getMessage('customPrompts_start_import'));
const file = input.files[0];
const reader = new FileReader();
reader.onload = async () => {
const json = reader.result;
try {
const obj = JSON.parse(json);
if(obj.id !== 'thunderai-custom-data-placeholders') {
alert(browser.i18n.getMessage("importCustomDataPH_invalidFile"));
setMessage(browser.i18n.getMessage('importCustomDataPH_invalidFile'),'red');
if(obj.id !== 'thunderai-prompts') {
alert(browser.i18n.getMessage("importPrompts_invalidFile"));
setMessage(browser.i18n.getMessage('importPrompts_invalidFile'),'red');
return;
}
// if(obj.addon_version !== manifest.version) {
// alert(browser.i18n.getMessage("importPrompts_invalidVersion"));
// return;
// }
if(!Array.isArray(obj.customdataplaceholders)) {
alert(browser.i18n.getMessage("importCustomDataPH_invalidDataPHs"));
setMessage(browser.i18n.getMessage('importCustomDataPH_invalidDataPHs'),'red');
if(!Array.isArray(obj.prompts)) {
alert(browser.i18n.getMessage("importPrompts_invalidPrompts"));
setMessage(browser.i18n.getMessage('customPrompts_invalidPrompts'),'red');
return;
}
customDataPHsList.clear();
loadCustomDataPHsList(await prepareCustomDataPHsForImport(obj.customdataplaceholders));
//setCustomPrompts(obj.prompts);
promptsList.clear();
loadPromptsList(await preparePromptsForImport(obj.prompts));
setSomethingChanged();
i18n.updateDocument();
setMessage(browser.i18n.getMessage('importCustomDataPH_import_completed'), 'orange');
// browser.runtime.sendMessage({command: "reload_menus"});
setMessage(browser.i18n.getMessage('customPrompts_import_completed'), 'orange');
// msgTimeout = setTimeout(() => {
// clearMessage();
// }, 10000);
} catch(err) {
alert(browser.i18n.getMessage("importCustomDataPH_invalidFile") + ' ' + err);
setMessage(browser.i18n.getMessage('importCustomDataPH_invalidFile'),'red');
alert(browser.i18n.getMessage("importPrompts_invalidFile") + ' ' + err);
setMessage(browser.i18n.getMessage('importPrompts_invalidFile'),'red');
return;
}
};
reader.readAsText(file);
};
};
}
// document.getElementById('btnManagePrompts').addEventListener('click', () => {
// openTab('/pages/customprompts/mzta-custom-prompts.html');
// });
}, { once: true });
//========= handling an item in a row
@ -255,12 +274,16 @@ function showItemRowEditor(tr) {
tr.querySelector('.id_show').style.display = 'none';
tr.querySelector('.name_output').style.display = 'inline';
tr.querySelector('.name_show').style.display = 'none';
const text_output = tr.querySelector('.text_output');
text_output.style.display = 'inline';
textareaAutocomplete(text_output, autocompleteSuggestions)
tr.querySelector('.text_output').style.display = 'inline';
tr.querySelector('.text_show').style.display = 'none';
tr.querySelector('.type_output').style.display = 'inline';
tr.querySelector('.type_output').style.display = 'inline';
tr.querySelector('.type_show').style.display = 'none';
tr.querySelector('.action_output').style.display = 'inline';
tr.querySelector('.action_show').style.display = 'none';
tr.querySelector('input.need_selected').disabled = false;
tr.querySelector('input.need_signature').disabled = false;
tr.querySelector('input.need_custom_text').disabled = false;
tr.querySelector('input.define_response_lang').disabled = false;
}
function hideItemRowEditor(tr) {
@ -270,8 +293,14 @@ function hideItemRowEditor(tr) {
tr.querySelector('.name_show').style.display = 'inline';
tr.querySelector('.text_output').style.display = 'none';
tr.querySelector('.text_show').style.display = 'inline';
tr.querySelector('.type_output').style.display = 'none';
tr.querySelector('.type_output').style.display = 'none';
tr.querySelector('.type_show').style.display = 'inline';
tr.querySelector('.action_output').style.display = 'none';
tr.querySelector('.action_show').style.display = 'inline';
tr.querySelector('input.need_selected').disabled = true;
tr.querySelector('input.need_signature').disabled = true;
tr.querySelector('input.need_custom_text').disabled = true;
tr.querySelector('input.define_response_lang').disabled = true;
}
// Confirm and log deletion action
@ -283,7 +312,7 @@ function handleDeleteClick(e) {
}
const tr = e.target.parentNode.parentNode;
//console.log('>>>>>>>> tr: ' + tr.getAttribute('data-idnum'));
customDataPHsList.remove("id", tr.querySelector('span.id').innerText);
promptsList.remove("id", tr.querySelector('span.id').innerText);
setSomethingChanged();
}
@ -295,10 +324,6 @@ function handleCancelClick(e) {
// tr.querySelector('.btnCancelItem').style.display = 'none'; // Cancel btn
tr.querySelector('.btnEditItem').style.display = 'inline'; // Edit btn
tr.querySelector('.btnDeleteItem').style.display = 'inline'; // Delete btn
tr.querySelector('.id_output').value = tr.querySelector('.id_show').innerText.toLocaleUpperCase();
tr.querySelector('.name_output').value = tr.querySelector('.name_show').innerText;
tr.querySelector('.text_output').value = sanitizeHtml(tr.querySelector('.text_show').innerHTML).replace(/<br\s*\/?>/gi, "\n");
tr.querySelector('.type_output').value = tr.querySelector('.type').innerText;
hideItemRowEditor(tr);
}
@ -314,13 +339,22 @@ function handleConfirmClick(e) {
tr.querySelector('.id_show').innerText = String(tr.querySelector('.id_output').value).toLocaleLowerCase();
tr.querySelector('.name_show').innerText = tr.querySelector('.name_output').value;
tr.querySelector('.text_show').innerText = tr.querySelector('.text_output').value;
tr.querySelector('.type').innerText = tr.querySelector('.type_output').value;
tr.querySelector('.type').innerText = tr.querySelector('.type_output').value;
tr.querySelector('.type_show').innerText = tr.querySelector('.type_output').selectedOptions[0].text;
tr.querySelector('.action').innerText = tr.querySelector('.action_output').value;
tr.querySelector('.action_show').innerText = tr.querySelector('.action_output').selectedOptions[0].text;
// the checkboxes update is handled directly by themselves
hideItemRowEditor(tr);
setSomethingChanged();
}
// Handle checkbox changes and log new state
function handleCheckboxChange(e) {
e.preventDefault();
e.target.setAttribute('checked_val', e.target.checked ? '1' : '0');
//console.log('>>>>>>>> checked_val: ' + e.target.getAttribute('checked_val'));
}
// Enable save button on input change
function handleInputChange(e) {
e.preventDefault();
@ -330,20 +364,11 @@ function handleInputChange(e) {
//========= handling an item in a row - END
function loadCustomDataPHsList(values){
// console.log('>>>>>>>> loadCustomDataPHsList values: ' + JSON.stringify(values));
function loadPromptsList(values){
// console.log('>>>>>>>> loadPromptsList values: ' + JSON.stringify(values));
let options = {
valueNames: [
{ data: ['idnum'] },
'is_default',
'id',
'name',
'text',
'type',
{ name: 'enabled', attr: 'checked_val'}
],
valueNames: [ { data: ['idnum'] }, 'is_default', 'id', 'name', 'text', 'type', 'action', 'position_compose', 'position_display', { name: 'need_selected', attr: 'checked_val'}, { name: 'need_signature', attr: 'checked_val'}, { name: 'need_custom_text', attr: 'checked_val'}, { name: 'define_response_lang', attr: 'checked_val'}, { name: 'enabled', attr: 'checked_val'} ],
item: function(values) {
values.id = placeholdersUtils.stripCustomDataPH_ID_Prefix(values.id);
let type_output = '';
switch(String(values.type)){
case "0":
@ -356,28 +381,51 @@ function loadCustomDataPHsList(values){
type_output = `__MSG_customPrompts_add_to_menu_composing__`;
break;
}
let action_output = '';
switch(String(values.action)){
case "0":
action_output = `__MSG_customPrompts_close_button__`;
break;
case "1":
action_output = `__MSG_customPrompts_do_reply__`;
break;
case "2":
action_output = `__MSG_customPrompts_substitute_text__`;
break;
}
//console.log('>>>>>>>>>>>>> action_output: ' + JSON.stringify(action_output));
let output = `<tr ` + ((values.is_default == 1) ? 'class="is_default"':'') + `>
<td class="w08"><i>thunderai_custom_</i><span class="id id_show"></span><input type="text" class="hiddendata id_output" value="` + values.id + `" /></td>
<td class="w08"><span class="id id_show"></span><input type="text" class="hiddendata id_output" value="` + values.id + `" /></td>
<td class="w08"><span class="name name_show"></span><input type="text" class="hiddendata name_output" value="` + values.name + `" /></td>
<td class="w40">
<span class="text text_show"></span>
<div class="autocomplete-container">
<textarea class="hiddendata text_output editor">` + values.text.replace(/<br\s*\/?>/gi, "\n") + `</textarea>
<ul class="autocomplete-list hidden"></ul>
</div>
</td>
<td class="w08"><span class="field_title_s">__MSG_customDataPH_add_to_menu__:</span>
<br>
<span class="type_show">` + type_output + `</span>
<select class="type_output hiddendata">
<option value="0"` + ((values.type == "0") ? ' selected':'') + `>__MSG_customPrompts_add_to_menu_always__</option>
<option value="1"` + ((values.type == "1") ? ' selected':'') + `>__MSG_customPrompts_add_to_menu_reading__</option>
<option value="2"` + ((values.type == "2") ? ' selected':'') + `>__MSG_customPrompts_add_to_menu_composing__</option>
</select>` +
`<span class="type hiddendata"></span>
<td class="w40"><span class="text text_show"></span><textarea class="hiddendata text_output">` + values.text + `</textarea></td>
<td class="w08"><span class="type_show">` + type_output + `</span>
<select class="type_output hiddendata">
<option value="0"` + ((values.type == "0") ? ' selected':'') + `>__MSG_customPrompts_add_to_menu_always__</option>
<option value="1"` + ((values.type == "1") ? ' selected':'') + `>__MSG_customPrompts_add_to_menu_reading__</option>
<option value="2"` + ((values.type == "2") ? ' selected':'') + `>__MSG_customPrompts_add_to_menu_composing__</option>
</select>` +
`<span class="type hiddendata"></span>
</td>
<td class="w17">
<label><input type="checkbox" class="enabled input_mod"> __MSG_customPrompts_form_label_enabled__</label>
Action: <span class="action_show">` + action_output + `</span>
<select class="action_output hiddendata">
<option value="0"` + ((values.action == "0") ? ' selected':'') + `>__MSG_customPrompts_close_button__</option>
<option value="1"` + ((values.action == "1") ? ' selected':'') + `>__MSG_customPrompts_do_reply__</option>
<option value="2"` + ((values.action == "2") ? ' selected':'') + `>__MSG_customPrompts_substitute_text__</option>
</select>` +
`<span class="action hiddendata"></span>
<br>
<input type="checkbox" class="need_selected" disabled> __MSG_customPrompts_form_label_need_selected__
<br>
<input type="checkbox" class="need_signature" disabled> __MSG_customPrompts_form_label_need_signature__
<br>
<input type="checkbox" class="need_custom_text` + ((values.is_default == 1) ? ' input_mod':'') + `"` + ((values.is_default == 0) ? ' disabled':'') + ` > __MSG_customPrompts_form_label_need_custom_text__
<br>
<input type="checkbox" class="define_response_lang" disabled> __MSG_customPrompts_form_label_define_response_lang__
<br>
<input type="checkbox" class="enabled input_mod"> __MSG_customPrompts_form_label_enabled__
<span class="is_default hiddendata"></span>
<span class="position_compose hiddendata"></span>
<span class="position_display hiddendata"></span>
@ -391,15 +439,14 @@ function loadCustomDataPHsList(values){
</td>
</tr>`;
//console.log('>>>>>>>> values.name: ' + JSON.stringify(values.name));
positionMax_compose = Math.max(positionMax_compose, values.position_compose);
positionMax_display = Math.max(positionMax_display, values.position_display);
idnumMax = Math.max(idnumMax, values.idnum);
return output;
}
};
// console.log('>>>>>>>>>>>>> options: ' + JSON.stringify(options));
// console.log('>>>>>>>>>>>>> values: ' + JSON.stringify(values));
customDataPHsList = new List('all_custom_dataplaceholders', options, values);
promptsList = new List('all_prompts', options, values);
checkSelectedBoxes();
let btnEditItem_elements = document.querySelectorAll(".btnEditItem");
@ -422,14 +469,19 @@ function loadCustomDataPHsList(values){
element.addEventListener('click', handleConfirmClick);
});
let checkbox_elements = document.querySelectorAll("input[type='checkbox']");
checkbox_elements.forEach(element => {
element.addEventListener('change', handleCheckboxChange);
});
document.querySelectorAll('.input_mod').forEach(element => {
element.addEventListener('change', handleInputChange);
});
}
function checkFields() {
//console.log('>>>>>>>>>>>>> typeof customDataPHsList: ' + typeof customDataPHsList);
//console.log('>>>>>>>>>>>>> Array.isArray(customDataPHsList): ' + Array.isArray(customDataPHsList));
//console.log('>>>>>>>>>>>>> typeof promptsList: ' + typeof promptsList);
//console.log('>>>>>>>>>>>>> Array.isArray(promptsList): ' + Array.isArray(promptsList));
// the id must be unique and without spaces
let is_error = false;
let id_value = document.getElementById('txtIdNew').value.trim();
@ -438,7 +490,7 @@ function checkFields() {
inputSetError('txtIdNew');
is_error = true;
} else {
let exists = customDataPHsList.get("id", id_value);
let exists = promptsList.get("id", id_value);
//console.log('>>>>>>>>>>>>> exists: ' + JSON.stringify(exists));
if(exists && exists.length > 0) {
inputSetError('txtIdNew');
@ -468,7 +520,11 @@ function clearFields() {
document.getElementById('txtIdNew').value = '';
document.getElementById('txtNameNew').value = '';
document.getElementById('txtTextNew').value = '';
document.getElementById('selectTypeNew').value = '0';
document.getElementById('selectTypeNew').value = '0';
document.getElementById('selectActionNew').value = '0';
document.getElementById('checkboxNeedSelectedNew').value = '0';
document.getElementById('checkboxNeedSignatureNew').value = '0';
document.getElementById('checkboxNeedCustomTextNew').value = '0';
document.getElementById('formNew').style.display = 'none';
}
@ -503,6 +559,10 @@ function setNothingChanged(){
function checkSelectedBoxes(checkboxes = null) {
if(checkboxes == null){
checkboxes = [
...document.querySelectorAll('.need_selected[type="checkbox"]'),
...document.querySelectorAll('.need_signature[type="checkbox"]'),
...document.querySelectorAll('.need_custom_text[type="checkbox"]'),
...document.querySelectorAll('.define_response_lang[type="checkbox"]'),
...document.querySelectorAll('.enabled[type="checkbox"]'),
];
}
@ -519,21 +579,33 @@ function checkSelectedBoxes(checkboxes = null) {
});
}
//Save all custom data placeholders
//Save all prompts
async function saveAll() {
setMessage(browser.i18n.getMessage('customDataPH_saving_custom'));
setMessage(browser.i18n.getMessage('customPrompts_start_saving'));
setNothingChanged();
if(customDataPHsList != null) {
if(promptsList != null) {
setMessage(browser.i18n.getMessage('customPrompts_reindexing_list'));
customDataPHsList.reIndex();
let newCustomDataPHs = customDataPHsList.items.filter(item => item.values().is_default == 0).map(item => item.values());
taLog.log('newCustomDataPlaceholders: ' + JSON.stringify(newCustomDataPHs));
// newCustomDataPHs.forEach(prompt => {
promptsList.reIndex();
let newPrompts = promptsList.items.map(item => {
// For each item in the array, return only the '_values' part
return item.values();
});
taLog.log('newPrompts: ' + JSON.stringify(newPrompts));
// newPrompts.forEach(prompt => {
// console.log('>>>>>>>>>>>>> id: ' + JSON.stringify(prompt));
// });
//console.log('>>>>>>>>>>>>> saveAll: ' + JSON.stringify(newCustomDataPHs));
await setCustomPlaceholders(newCustomDataPHs);
setMessage(browser.i18n.getMessage('customDataPH_saved'),'green');
//console.log('>>>>>>>>>>>>> saveAll: ' + JSON.stringify(newPrompts));
setMessage(browser.i18n.getMessage('customPrompts_filtering_prompts'));
let newDefaultPrompts = newPrompts.filter(item => item.is_default == 1);
//console.log('>>>>>>>>>>>>> newDefaultPrompts: ' + JSON.stringify(newDefaultPrompts));
let newCustomPrompts = newPrompts.filter(item => item.is_default == 0);
setMessage(browser.i18n.getMessage('customPrompts_saving_default_prompts'));
await setDefaultPromptsProperties(newDefaultPrompts);
setMessage(browser.i18n.getMessage('customPrompts_saving_custom_prompts'));
await setCustomPrompts(newCustomPrompts);
setMessage(browser.i18n.getMessage('customPrompts_reloading_menus'));
browser.runtime.sendMessage({command: "reload_menus"});
setMessage(browser.i18n.getMessage('customPrompts_saved'),'green');
msgTimeout = setTimeout(() => {
clearMessage();
}, 10000)
@ -557,12 +629,16 @@ function clearMessage() {
}
async function setStorageSpace() {
let storage_space = await getLocalStorageUsedSpace();
let storage_space = await getCustomPromptsUsedSpace();
document.getElementById('storage_space').textContent = storage_space;
}
window.addEventListener('beforeunload', function (event) {
if (somethingChanged) {
event.preventDefault();
}
});
if(await isThunderbird128OrGreater()){
window.addEventListener('beforeunload', function (event) {
// Check if any changes have been made (Only for Thunderbird 128+ see https://github.com/micz/ThunderAI/issues/88)
if (somethingChanged) {
event.preventDefault();
}
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 740 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 994 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 971 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 954 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 705 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 664 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 838 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 795 B

Some files were not shown because too many files have changed in this diff Show more