Update CLI and ModelDialog to enhance functionality and user experience. Modify interrogation command in CLI for improved output generation, adjust model settings in ModelDialog for better reasoning effort, and introduce a new plugin structure in plan.md for LLM integration in Roundcube. Add spinner functionality in InkApp for loading states and improve error handling in read_file.js to ensure proper line breaks in file content output.

This commit is contained in:
sebseb7
2025-08-21 08:20:38 +00:00
parent 3c6bf7184c
commit 7ad5d10378
6 changed files with 424 additions and 22 deletions

View File

@@ -24,7 +24,7 @@ modelDialog.on('reasoningUpdate', (output) => {
//const output = await modelDialog.interrogate('Can you remember "seven" ?');
//console.log(output.output,JSON.stringify(output.reasoning,null,2));
//const output2 = await modelDialog.interrogate('read a file that is what you remebered plus 1 as a word with txt ending, check that file.');
const output2 = await modelDialog.interrogate('Ersttelle eine beispiel business webseite für acme mit react und webpack. Lege die Dateien in /demo an');
const output2 = await modelDialog.interrogate('lege 5 Dateinen an, mache in jede eine webseite mit einem farbverlauf.');
console.log('final output:',output2.output);
console.log('reasoning:',output2.reasoning);
console.log('Tokens:',output2.inputTokens,output2.cachedTokens,output2.outputTokens);

View File

@@ -88,10 +88,10 @@ class ModelDialog {
this.messagesSent.push(...messagesToSend);
const call = {
model: 'gpt-5-nano',
model: 'gpt-5',
input: messagesToSend,
text: { format: { type: 'text' }, verbosity: 'low' },
reasoning: { effort: 'medium', summary: 'detailed' },
reasoning: { effort: 'low', summary: 'detailed' },
tools: Object.values(toolsByFile).map(t => t.def),
store: true,
previous_response_id: this.previousResponseId
@@ -174,4 +174,4 @@ class ModelDialog {
}
}
export default ModelDialog;
export default ModelDialog;

359
plan.md Normal file
View File

@@ -0,0 +1,359 @@
You're on the right track! You want to create a **Roundcube plugin** that allows users to **enhance or rewrite the email body using an LLM**, by sending the current message content to a backend API (your LLM service), then replacing the textarea content with the response.
Heres a complete working example of such a plugin, called `llm_compose_helper`. It adds a button to the compose screen, sends the current message text to a configured URL via AJAX, and replaces the message body with the LLM-generated result.
---
## ✅ Goal
- Add a "Rewrite with AI" button in the compose window.
- On click: open a popup asking the user for a rewrite prompt/instructions.
- Submit both the current message body and the user prompt to the configured LLM API endpoint.
- Replace the `<textarea>` content with the returned text.
---
## 📁 Plugin Structure
```
plugins/
llm_compose_helper/
llm_compose_helper.php <-- Main plugin class
config.inc.php <-- Configuration file
localization/en_US.inc <-- Language labels
js/llm_compose_helper.js <-- Client-side JavaScript
```
---
## 1. `llm_compose_helper.php` Plugin Class
```php
<?php
/**
* LLM Compose Helper Plugin
*
* Allows rewriting the message body using an external LLM service
*
* @version 1.0
* @author You
* @license GPL
*/
class llm_compose_helper extends rcube_plugin
{
public $task = 'mail';
function init()
{
// Load configuration
$this->load_config();
// Load localization texts (available in JS if second param = true)
$this->add_texts('localization/', true);
// Only run in compose view
$rcmail = rcmail::get_instance();
if ($rcmail->action == 'compose') {
// Include JS and CSS
$this->include_script('js/llm_compose_helper.js');
// Register UI placeholder handler
$this->register_handler('plugin.llm_button', array($this, 'llm_button'));
}
}
/**
* Generate the button HTML to be inserted into the toolbar
*/
function llm_button()
{
$rcmail = rcmail::get_instance();
return html::tag('a', array(
'href' => '#',
'id' => 'llm_rewrite_button',
'class' => 'button',
'title' => $this->gettext('rewrite_with_llm'),
'onclick' => 'return false;'
), $this->gettext('rewrite_label'));
}
}
```
---
## 2. `config.inc.php` Plugin Configuration
```php
<?php
// URL of your LLM API endpoint
$config['llm_api_url'] = 'https://your-llm-api.example.com/rewrite';
// Optional: headers (e.g., auth tokens)
$config['llm_api_headers'] = array(
'Authorization: Bearer your-secret-token',
'Content-Type: application/json'
);
```
> 💡 You can later override this in `config/config.inc.php` globally.
---
## 3. `localization/en_US.inc`
```php
<?php
$labels = array(
'rewrite_with_llm' => 'Rewrite message with AI',
'rewrite_label' => 'AI Rewrite',
'enter_prompt' => 'Enter rewrite prompt',
'rewrite_submit' => 'Rewrite',
'rewrite_cancel' => 'Cancel',
);
$messages = array(
'prompt_placeholder' => 'e.g., Make it shorter and more formal',
);
```
> Add other language files like `fr_FR.inc`, `de_DE.inc`, etc., if needed.
---
## 4. `js/llm_compose_helper.js` Client-Side Logic
```javascript
// Wait for Roundcube to initialize
rcmail.addEventListener('init', function() {
const button = document.getElementById('llm_rewrite_button');
if (!button) return;
// Insert button into compose toolbar
const toolbar = rcmail.gui_objects.toolbar;
if (toolbar) {
toolbar.appendChild(button);
}
// Attach click event
$(button).click(function(e) {
e.preventDefault();
const textarea = rcmail.env.html_editor ? rcmail.editor : document.getElementById('composebody');
let messageText;
// Get current message body
if (rcmail.env.html_editor && rcmail.editor && rcmail.editor.getData) {
messageText = rcmail.editor.getData(); // CKEditor
} else {
messageText = $('#composebody').val();
}
if (!messageText || messageText.trim() === '') {
alert(rcmail.gettext('non_empty', 'llm_compose_helper'));
return;
}
// Build prompt dialog content
var promptId = 'llm_prompt_input_' + Date.now();
var dialogHtml = '<div style="padding:8px 0">' +
'<label for="' + promptId + '">' + rcmail.gettext('enter_prompt', 'llm_compose_helper') + '</label>' +
'<textarea id="' + promptId + '" style="width:100%;height:120px;box-sizing:border-box;margin-top:6px" placeholder="' + (rcmail.gettext('prompt_placeholder', 'llm_compose_helper') || '') + '"></textarea>' +
'</div>';
var buttons = [
{
text: rcmail.gettext('rewrite_submit', 'llm_compose_helper'),
classes: 'mainaction',
click: function(e, ref) {
var promptValue = document.getElementById(promptId).value || '';
// Show loading
rcmail.set_busy(true, 'loading');
// Send to LLM API with message and prompt
rcmail.http_post('plugin.llm_rewrite', {
message: messageText,
prompt: promptValue
}, function() {
rcmail.set_busy(false);
});
if (ref && ref.hide) ref.hide();
}
},
{
text: rcmail.gettext('rewrite_cancel', 'llm_compose_helper'),
click: function(e, ref) { if (ref && ref.hide) ref.hide(); }
}
];
// Open Roundcube dialog
rcmail.show_popup_dialog(dialogHtml, rcmail.gettext('rewrite_with_llm', 'llm_compose_helper'), buttons, {modal: true, width: 520});
});
});
// Handle response from server
rcmail.addEventListener('plugin.llm_rewrite_response', function(response) {
if (response.status === 'success' && response.text) {
const newText = response.text;
if (rcmail.env.html_editor && rcmail.editor && rcmail.editor.setData) {
rcmail.editor.setData(newText); // For CKEditor
} else {
$('#composebody').val(newText);
}
rcmail.showMessage(rcmail.gettext('rewrite_success', 'llm_compose_helper'), 'confirmation');
} else {
var errorMsg = response && response.message ? String(response.message) : rcmail.gettext('rewrite_error', 'llm_compose_helper');
rcmail.showMessage(errorMsg, 'error');
}
});
```
---
## 5. Extend `llm_compose_helper.php` Add Server-Side Action
Update the `llm_compose_helper.php` file to register the AJAX action and handle the request:
```php
function init()
{
$this->load_config();
$this->add_texts('localization/', true);
$rcmail = rcmail::get_instance();
if ($rcmail->action == 'compose') {
$this->include_script('js/llm_compose_helper.js');
$this->register_handler('plugin.llm_button', array($this, 'llm_button'));
// Register custom action for AJAX
$this->register_action('plugin.llm_rewrite', array($this, 'action_handler'));
}
}
function action_handler()
{
$rcmail = rcmail::get_instance();
// Get input
$message = rcube_utils::get_input_value('message', rcube_utils::INPUT_POST);
$prompt = rcube_utils::get_input_value('prompt', rcube_utils::INPUT_POST);
if (empty($message)) {
$rcmail->output->command('plugin.llm_rewrite_response', [
'status' => 'error',
'message' => 'No message provided'
]);
return;
}
// Get config
$api_url = $rcmail->config->get('llm_api_url');
if (!$api_url) {
$rcmail->output->command('plugin.llm_rewrite_response', [
'status' => 'error',
'message' => 'LLM API URL not configured'
]);
return;
}
$headers = $rcmail->config->get('llm_api_headers', ['Content-Type: application/json']);
// Prepare request
$data = json_encode(['text' => $message, 'prompt' => $prompt]);
// Use file_get_contents or cURL
$options = [
'http' => [
'header' => $headers,
'method' => 'POST',
'content' => $data,
'timeout' => 30
],
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
'cafile' => '/etc/ssl/certs/ca-certificates.crt', // Adjust as needed
]
];
$context = stream_context_create($options);
$result = file_get_contents($api_url, false, $context);
if ($result === false) {
$rcmail->output->command('plugin.llm_rewrite_response', [
'status' => 'error',
'message' => 'Request failed'
]);
return;
}
$response = json_decode($result, true);
$rewritten = isset($response['text']) && $response['text'] !== null ? $response['text'] : ($response['message'] ?? $result);
// Return success
$rcmail->output->command('plugin.llm_rewrite_response', [
'status' => 'success',
'text' => $rewritten
]);
}
```
---
## 6. Example LLM API Response Format (Expected)
Your external LLM endpoint should accept POST JSON:
```json
{ "text": "Hello, I am writing to ask about...", "prompt": "Make it shorter and more formal" }
```
And return:
```json
{ "text": "Hi, I hope you're doing well. I'm reaching out to inquire about..." }
```
Ensure CORS is allowed if hosted separately.
---
## 7. Optional: Add More Labels
In `localization/en_US.inc`, add:
```php
$messages = array(
'rewrite_success' => 'Message rewritten successfully!',
'rewrite_error' => 'Failed to rewrite message.',
'non_empty' => 'Please write something before using AI rewrite.'
);
```
---
## 8. Enable the Plugin
In `config/config.inc.php`:
```php
$config['plugins'] = array(
// ... other plugins
'llm_compose_helper'
);
```
---
## ✅ Final Result
When composing or replying:
- A new button labeled "AI Rewrite" appears in the toolbar.
- Clicking it opens a dialog asking for a rewrite prompt.
- Submitting sends the current message and the prompt to your LLM API.
- The response replaces the message body.
- Works with plain text and HTML (via CKEditor if enabled).

View File

@@ -3,6 +3,10 @@ import { Box, Text } from 'ink';
import uiService from './uiService.js';
import TextInput from 'ink-text-input';
import terminalService from '../terminalService.js';
import ModelDialog from '../../modelDialog.js';
const sharedModelDialog = new ModelDialog();
const npmSpinnerFrames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
class Pane extends React.Component {
constructor(props) {
@@ -113,7 +117,9 @@ export default class InkApp extends React.Component {
menuIndex: 0,
model: 'gpt-5',
reasoningEffort: 'minimal',
outputVerbosity: 'low'
outputVerbosity: 'low',
isLoading: false,
spinnerIndex: 0
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
@@ -156,6 +162,12 @@ export default class InkApp extends React.Component {
try { process.stdin.setRawMode(true); } catch {}
process.stdin.on('data', this.onKeypress);
}
// spinner timer
this._spinnerTimer = setInterval(() => {
if (this.state.isLoading) {
this.setState((s) => ({ spinnerIndex: (s.spinnerIndex + 1) % npmSpinnerFrames.length }));
}
}, 80);
}
componentWillUnmount() {
if (this.terminalUnsub) {
@@ -169,6 +181,10 @@ export default class InkApp extends React.Component {
if (process.stdin && process.stdin.off) {
process.stdin.off('data', this.onKeypress);
}
if (this._spinnerTimer) {
clearInterval(this._spinnerTimer);
this._spinnerTimer = null;
}
}
setPaneLines(stateKey, lines) {
@@ -205,21 +221,42 @@ export default class InkApp extends React.Component {
this.setState({ input: value });
}
handleSubmit() {
async handleSubmit() {
const { input } = this.state;
if (!input) return;
try {
terminalService.write(`${input}\r`);
} catch (e) {
// do not hide errors; show in logs
this.setState((state) => ({
logs: [...state.logs, `! write error: ${String(e && e.message ? e.message : e)}`],
}));
}
this.setState((state) => ({
logs: [...state.logs, `> ${input}`],
input: ''
input: '',
isLoading: true
}));
try {
const result = await sharedModelDialog.interrogate(input);
const finalOutput = Array.isArray(result && result.output) ? result.output : [String(result && result.output ? result.output : '')];
const finalReasoning = Array.isArray(result && result.reasoning) ? result.reasoning : (result && result.reasoning ? [String(result.reasoning)] : []);
// Append to LLM output with a separator, overwrite chain of thought
this.setState((state) => ({
llmOutput: [
...state.llmOutput,
...(state.llmOutput.length ? ['----------'] : []),
...finalOutput
]
}));
this.setChainOfThought(finalReasoning);
this.setState((state) => ({
logs: [
...state.logs,
`tokens input: ${JSON.stringify(result && result.inputTokens)}`,
`tokens cached: ${JSON.stringify(result && result.cachedTokens)}`,
`tokens output: ${JSON.stringify(result && result.outputTokens)}`
]
}));
} catch (e) {
this.setState((state) => ({
logs: [...state.logs, `! interrogate error: ${String(e && e.message ? e.message : e)}`]
}));
} finally {
this.setState({ isLoading: false });
}
}
toggleMenu(open) {
@@ -412,12 +449,16 @@ export default class InkApp extends React.Component {
)}
<Box marginTop={1}>
<Text>Input: </Text>
<TextInput
value={input}
onChange={this.handleChange}
onSubmit={this.handleSubmit}
placeholder="Type and press Enter..."
/>
{this.state.isLoading ? (
<Text color="yellow">{npmSpinnerFrames[this.state.spinnerIndex]} Processing...</Text>
) : (
<TextInput
value={input}
onChange={this.handleChange}
onSubmit={this.handleSubmit}
placeholder="Type and press Enter..."
/>
)}
</Box>
</Box>
);

2
todo.md Normal file
View File

@@ -0,0 +1,2 @@
return the function call result via event.
display function call evenst in logging

View File

@@ -56,7 +56,7 @@ export async function run(args) {
}
}
return 'Filecontent: ´´´'+lines.join('')+'´´´';
return 'Filecontent: ´´´'+lines.join('\n')+'´´´';
} catch (error) {
return `read_file error: ${error.message}`;
}