added thinking hide option. see #636
This commit is contained in:
parent
85afdb0a77
commit
565134ecbd
10 changed files with 169 additions and 16 deletions
|
|
@ -724,6 +724,18 @@
|
|||
"message": "If checked, the placeholders will be filled with the default values when no value is provided. Otherwise, the placeholders will be kept in place.",
|
||||
"description": ""
|
||||
},
|
||||
"prefs_OptionText_hide_thinking": {
|
||||
"message": "Hide thinking output",
|
||||
"description": ""
|
||||
},
|
||||
"prefs_OptionText_hide_thinking_info": {
|
||||
"message": "If checked, reasoning/thinking output produced by the model is completely removed. If unchecked, it is shown in a collapsed block above the answer.",
|
||||
"description": ""
|
||||
},
|
||||
"prefs_OptionText_thinking_summary": {
|
||||
"message": "Thinking",
|
||||
"description": ""
|
||||
},
|
||||
"prefs_OptionText_max_prompt_length" : {
|
||||
"message": "Max prompt length",
|
||||
"description": ""
|
||||
|
|
@ -1728,6 +1740,14 @@
|
|||
"message": "The maximum number of tokens to generate in the completion. The token count of your prompt plus max_tokens cannot exceed the model's context length.",
|
||||
"description": ""
|
||||
},
|
||||
"prefs_OptionText_anthropic_extended_thinking_budget": {
|
||||
"message": "Extended thinking budget (tokens)",
|
||||
"description": ""
|
||||
},
|
||||
"prefs_OptionText_anthropic_extended_thinking_budget_Info": {
|
||||
"message": "Maximum tokens the model may spend on extended thinking. Set to 0 to disable extended thinking. When enabled, the temperature value is ignored by the Claude API.",
|
||||
"description": ""
|
||||
},
|
||||
"anthropic_empty_apikey": {
|
||||
"message": "You've not added an API Key for the Claude API. Please insert one in the options page.",
|
||||
"description": ""
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ if (worker) {
|
|||
const integration_prefix = integration;
|
||||
const options_config = integration_options_config[integration];
|
||||
|
||||
let prefsToGet = { do_debug: prefs_default.do_debug };
|
||||
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}`];
|
||||
}
|
||||
|
|
@ -115,6 +115,7 @@ if (worker) {
|
|||
case 'anthropic': llmName = "Claude"; break;
|
||||
}
|
||||
messagesArea.setLLMName(llmName);
|
||||
messagesArea.setHideThinking(!!prefs_api.hide_thinking);
|
||||
|
||||
document.title += " [" + llmName + " | " + decodeURIComponent(prompt_name) + "]";
|
||||
|
||||
|
|
@ -153,7 +154,8 @@ if (worker) {
|
|||
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: 'temperature', labelKey: 'prefs_api_temperature', type: 'string' },
|
||||
{ key: 'extended_thinking_budget', labelKey: 'prefs_OptionText_anthropic_extended_thinking_budget', type: 'number_gt_zero' }
|
||||
]
|
||||
};
|
||||
|
||||
|
|
@ -238,6 +240,10 @@ worker.onmessage = async function(event) {
|
|||
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") + '...');
|
||||
break;
|
||||
case 'tokensDone':
|
||||
await messagesArea.handleTokensDone(promptData);
|
||||
messageInput.enableInput();
|
||||
|
|
|
|||
|
|
@ -194,6 +194,25 @@ messagesAreaStyle.textContent = `
|
|||
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 {
|
||||
|
|
@ -202,6 +221,11 @@ messagesAreaStyle.textContent = `
|
|||
.removed {
|
||||
background-color:rgb(90, 0, 0);
|
||||
}
|
||||
details.thinking-block {
|
||||
background: #2a2a2a;
|
||||
color: #bbb;
|
||||
border-left-color: #555;
|
||||
}
|
||||
}
|
||||
`;
|
||||
messagesAreaTemplate.content.appendChild(messagesAreaStyle);
|
||||
|
|
@ -218,6 +242,8 @@ 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));
|
||||
|
|
@ -248,6 +274,14 @@ class MessagesArea extends HTMLElement {
|
|||
this.llmName = llmName;
|
||||
}
|
||||
|
||||
setHideThinking(val) {
|
||||
this.hideThinking = !!val;
|
||||
}
|
||||
|
||||
handleNewThinkingToken(token) {
|
||||
this.thinkingAccumulator += token;
|
||||
}
|
||||
|
||||
async handleTokensDone(promptData = null) {
|
||||
this.flushAccumulatingMessage();
|
||||
await this.addActionButtons(promptData);
|
||||
|
|
@ -559,6 +593,32 @@ class MessagesArea extends HTMLElement {
|
|||
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);
|
||||
|
|
@ -577,6 +637,23 @@ class MessagesArea extends HTMLElement {
|
|||
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 => {
|
||||
this.accumulatingMessageEl.appendChild(node);
|
||||
|
|
|
|||
|
|
@ -49,7 +49,17 @@ Content script `js/lib/diff.js` is injected into ChatGPT pages for diff-view sup
|
|||
### 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`
|
||||
- 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
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ chatgpt_api_key, chatgpt_model, chatgpt_developer_messages, chatgpt_temperature,
|
|||
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_api_key, anthropic_model, anthropic_version, anthropic_max_tokens, anthropic_system_prompt, anthropic_temperature, anthropic_extended_thinking_budget
|
||||
```
|
||||
|
||||
Plus the global connection selector:
|
||||
|
|
@ -66,6 +66,7 @@ These are generated programmatically at the bottom of `mzta-options-default.js`
|
|||
| `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
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export class Anthropic {
|
|||
system_prompt = '';
|
||||
temperature = '';
|
||||
max_tokens = 4096;
|
||||
extended_thinking_budget = 0;
|
||||
stream = false;
|
||||
|
||||
constructor({
|
||||
|
|
@ -36,6 +37,7 @@ export class Anthropic {
|
|||
system_prompt = '',
|
||||
temperature = '',
|
||||
max_tokens = 4096,
|
||||
extended_thinking_budget = 0,
|
||||
stream = false,
|
||||
} = {}) {
|
||||
this.apiKey = apiKey;
|
||||
|
|
@ -44,6 +46,7 @@ export class Anthropic {
|
|||
this.system_prompt = system_prompt;
|
||||
this.temperature = temperature;
|
||||
this.max_tokens = max_tokens > 0 ? max_tokens : 4096;
|
||||
this.extended_thinking_budget = extended_thinking_budget;
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
|
|
@ -97,9 +100,15 @@ export class Anthropic {
|
|||
stream: this.stream,
|
||||
};
|
||||
|
||||
const tempFloat = parseFloat(this.temperature);
|
||||
const thinkingBudget = parseInt(this.extended_thinking_budget);
|
||||
const thinkingEnabled = !Number.isNaN(thinkingBudget) && thinkingBudget > 0;
|
||||
|
||||
if(this.temperature != '' && !Number.isNaN(tempFloat)) claude_body.temperature = tempFloat;
|
||||
if (thinkingEnabled) {
|
||||
claude_body.thinking = { type: 'enabled', budget_tokens: thinkingBudget };
|
||||
} else {
|
||||
const tempFloat = parseFloat(this.temperature);
|
||||
if(this.temperature != '' && !Number.isNaN(tempFloat)) claude_body.temperature = tempFloat;
|
||||
}
|
||||
|
||||
// console.log(">>>>>>>>>>>>>>>>> [ThunderAI] Anthropic API request: " + JSON.stringify(claude_body));
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ let taLog = null;
|
|||
|
||||
let conversationHistory = [];
|
||||
let assistantResponseAccumulator = '';
|
||||
let thinkingAccumulator = '';
|
||||
|
||||
self.onmessage = async function(event) {
|
||||
if (event.data.type === 'init') {
|
||||
|
|
@ -83,7 +84,8 @@ self.onmessage = async function(event) {
|
|||
taLog.log("AI full response [STOPPED]: " + assistantResponseAccumulator);
|
||||
conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator });
|
||||
assistantResponseAccumulator = '';
|
||||
postMessage({ type: 'tokensDone' });
|
||||
postMessage({ type: 'tokensDone', payload: { thinking: thinkingAccumulator } });
|
||||
thinkingAccumulator = '';
|
||||
break;
|
||||
}
|
||||
const { done, value } = await reader.read();
|
||||
|
|
@ -91,7 +93,8 @@ self.onmessage = async function(event) {
|
|||
taLog.log("AI full response: " + assistantResponseAccumulator);
|
||||
conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator });
|
||||
assistantResponseAccumulator = '';
|
||||
postMessage({ type: 'tokensDone' });
|
||||
postMessage({ type: 'tokensDone', payload: { thinking: thinkingAccumulator } });
|
||||
thinkingAccumulator = '';
|
||||
break;
|
||||
}
|
||||
// lots of low-level Claude response parsing stuff
|
||||
|
|
@ -124,7 +127,11 @@ self.onmessage = async function(event) {
|
|||
// Events handling
|
||||
switch (parsedData.type) {
|
||||
case 'content_block_delta':
|
||||
if (parsedData.delta && parsedData.delta.text) {
|
||||
if (parsedData.delta && parsedData.delta.type === 'thinking_delta' && typeof parsedData.delta.thinking === 'string') {
|
||||
const token = parsedData.delta.thinking;
|
||||
thinkingAccumulator += token;
|
||||
postMessage({ type: 'newThinkingToken', payload: { token } });
|
||||
} else if (parsedData.delta && typeof parsedData.delta.text === 'string') {
|
||||
const token = parsedData.delta.text;
|
||||
assistantResponseAccumulator += token;
|
||||
postMessage({ type: 'newToken', payload: { token } });
|
||||
|
|
@ -143,7 +150,8 @@ self.onmessage = async function(event) {
|
|||
taLog.log("AI full response: " + assistantResponseAccumulator);
|
||||
conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator });
|
||||
assistantResponseAccumulator = '';
|
||||
postMessage({ type: 'tokensDone' });
|
||||
postMessage({ type: 'tokensDone', payload: { thinking: thinkingAccumulator } });
|
||||
thinkingAccumulator = '';
|
||||
return; // end the loop
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@ export const integration_options_config = {
|
|||
version: '2023-06-01',
|
||||
max_tokens: 4096,
|
||||
system_prompt: '',
|
||||
temperature: ''
|
||||
temperature: '',
|
||||
extended_thinking_budget: 0
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -115,6 +116,7 @@ export const prefs_default = {
|
|||
chatgpt_web_load_wait_time: 1000,
|
||||
dynamic_menu_force_enter: false,
|
||||
placeholders_use_default_value: false,
|
||||
hide_thinking: true,
|
||||
max_prompt_length: 30000, // max string length for prompt
|
||||
add_tags: false,
|
||||
add_tags_maxnum: 3,
|
||||
|
|
|
|||
|
|
@ -147,6 +147,17 @@
|
|||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label>
|
||||
<span class="opt_title">__MSG_prefs_OptionText_hide_thinking__</span>
|
||||
</label></td>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" id="hide_thinking" name="hide_thinking" class="option-input" />
|
||||
<span>__MSG_prefs_OptionText_hide_thinking_info__</span>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label>
|
||||
<span class="opt_title">__MSG_prefs_OptionText_composing_plain_text__</span>
|
||||
|
|
|
|||
|
|
@ -549,6 +549,15 @@ export async function injectConnectionUI({
|
|||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="conntype_anthropic_api${tr_class ? ` ${tr_class}` : ''}">
|
||||
<td><span class="opt_title">__MSG_prefs_OptionText_anthropic_extended_thinking_budget__</span></td>
|
||||
<td>
|
||||
<label>
|
||||
<input type="number" id="${modelId_prefix ? `${modelId_prefix}` : ''}anthropic_extended_thinking_budget" name="${modelId_prefix ? `${modelId_prefix}` : ''}anthropic_extended_thinking_budget" class="option-input" />
|
||||
<br>__MSG_prefs_OptionText_anthropic_extended_thinking_budget_Info__
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
const template = document.createElement('template');
|
||||
|
|
|
|||
Loading…
Reference in a new issue