Skip to content

Add "Edit Draft" AI rewrite button with dedicated prompt#40

Open
ku9nov wants to merge 4 commits into
presswizards:mainfrom
ku9nov:main
Open

Add "Edit Draft" AI rewrite button with dedicated prompt#40
ku9nov wants to merge 4 commits into
presswizards:mainfrom
ku9nov:main

Conversation

@ku9nov
Copy link
Copy Markdown

@ku9nov ku9nov commented Jan 28, 2026

This PR adds a new Edit Draft action to the reply editor, allowing agents to send their current reply text to AI for grammar, clarity, and tone improvements without generating a new response from the customer message.

  • Adds a dedicated settings field for the draft-editing prompt (message_edit_prompt)
  • Adds a new toolbar button (pen icon) that edits the current reply text in-place
  • Introduces a new backend endpoint POST /freescoutgpt/edit-draft to process draft edits without saving to threads

Fixes request behavior:

  • Hides GPT toolbar buttons when the module is disabled for the mailbox
  • Adds CSRF support to prevent 419 errors on reply edits

Summary by CodeRabbit

  • New Features

    • AI-powered "Edit Draft" button in the reply toolbar to edit existing reply drafts.
    • New "Message Editing Instructions" setting to customize how the AI revises drafts.
    • Server-side edit endpoint and frontend integration to send drafts and return edited text.
  • Improvements

    • Settings now persist and are exposed to the frontend.
    • Better client-side handling and user-facing error feedback during edit requests.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 28, 2026

Warning

Rate limit exceeded

@ku9nov has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 47 minutes and 42 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 47 minutes and 42 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 61e3c2d1-ff8e-4ea2-bd4a-8b605e9140c1

📥 Commits

Reviewing files that changed from the base of the PR and between 1e5d8ce and 534d2ed.

📒 Files selected for processing (1)
  • Http/Controllers/FreeScoutGPTController.php
📝 Walkthrough

Walkthrough

Adds a nullable message_edit_prompt column, surfaces that field in GPTSettings and frontend data, introduces an edit-draft endpoint and controller helpers that call OpenAI, adds a route and frontend Edit Draft UI/JS to post drafts and replace editor content, and adds a settings textarea for edit instructions.

Changes

Cohort / File(s) Summary
Database Migration
Database/Migrations/2026_01_28_000001_add_message_edit_prompt_to_freescoutgpt_table.php
Adds nullable message_edit_prompt text column in up() and drops it in down().
Model Layer
Entities/GPTSettings.php
Adds 'message_edit_prompt' to the model $fillable array for mass-assignment.
Backend Controller & Routing
Http/Controllers/FreeScoutGPTController.php, Http/routes.php
Adds editDraft(Request $request), stripHtmlToText() helper, threads skip_client_data into context building, updates fetchArticlesContext(..., $skipClientData), validates input, fetches settings (including message_edit_prompt), calls OpenAI chat completions, and registers POST /freescoutgpt/edit-draft.
Service Provider / Frontend Data
Providers/FreeScoutGPTServiceProvider.php
Adds message_edit_prompt and UI labels (editDraft, nothingToEdit, moduleDisabled) to the freescoutGPTData JS payload exposed to the frontend.
Frontend UI & Logic
Public/js/module.js
Injects an "Edit Draft" button, registers .gpteditbutton handler, adds getReplyDraftText(), setReplyDraftText(text), and editDraftMessage(e) to POST draft + skip_client_data: 1, handle responses/errors, and update Summernote content.
Settings View
Resources/views/settings.blade.php
Adds "Message Editing Instructions" textarea (message_edit_prompt, rows=10) with placeholder and help text to mailbox settings.

Sequence Diagram

sequenceDiagram
    participant User
    participant Client as Browser (JS)
    participant Server as FreeScoutGPTController
    participant DB as Database (GPTSettings)
    participant OpenAI as OpenAI API

    User->>Client: Click "Edit Draft"
    Client->>Client: getReplyDraftText() (strip HTML)
    Client->>Server: POST /freescoutgpt/edit-draft {mailbox_id, text, skip_client_data}
    Server->>DB: Load GPTSettings (includes message_edit_prompt)
    DB-->>Server: Return settings
    Server->>OpenAI: Chat completion request (model, prompt = message_edit_prompt + user draft)
    OpenAI-->>Server: Edited text
    Server-->>Client: Respond { answer: edited text }
    Client->>Client: setReplyDraftText(edited text)
    Client->>User: Editor updated with edited draft
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I nibble on drafts with a curious hop,
A prompt, a tweak, and I don’t stop.
Grammar straightened, tone made bright,
The draft hops out—just right by night.
Bravo, buttons—what a delight! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add "Edit Draft" AI rewrite button with dedicated prompt' accurately and clearly summarizes the main feature addition of this PR, which introduces a new toolbar button for editing reply drafts with a dedicated AI prompt.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@Http/Controllers/FreeScoutGPTController.php`:
- Around line 247-249: The $skipClientData flag is only declared inside the
Responses API branch so the standard chat flow still sends client data; move the
declaration of $skipClientData = $request->boolean('skip_client_data') to the
top of the method (or at least before the Responses API conditional) so it's
available to both flows, then use that variable in the existing checks (e.g.,
the if that currently reads $settings->client_data_enabled && !$skipClientData
and any later logic that reads $skipClientData when building
$customerName/$client data) in FreeScoutGPTController to ensure the privacy flag
is honored in both paths.

In `@Providers/FreeScoutGPTServiceProvider.php`:
- Around line 75-95: The injected prompt strings (e.g., $message_edit_prompt,
$responses_api_prompt, $start_message, $modifyPrompt, etc.) are embedded raw
into the JS object freescoutGPTData and can break the script or allow XSS;
replace the manual string concatenation with safe JSON serialization: build a
PHP array/object containing all keys (copiedToClipboard, updateAvailable,
version, start_message, message_edit_prompt, responses_api_prompt, modifyPrompt,
send, editDraft, nothingToEdit, moduleDisabled) and output it via json_encode so
values are properly escaped for JavaScript and cannot inject backticks, ${}, or
</script>.

In `@Public/js/module.js`:
- Around line 200-207: The setReplyDraftText function is injecting
server-provided text into Summernote as HTML without escaping special
characters, allowing XSS; update setReplyDraftText to HTML-escape &, <, >, " and
' (or use a DOM/text-node-based sanitizer) before converting newlines to <br>
and calling $('#body').summernote('code', ...), or use Summernote's safe text
API if available; target the setReplyDraftText function and the
$('#body').summernote('code', ...) call to ensure the value is escaped/sanitized
prior to insertion.
- Around line 234-237: The success handler currently calls
setReplyDraftText(response.answer) unconditionally; update the success function
that includes setReplyDraftText and $(".gpteditbutton i").removeClass(...) to
first validate that response and response.answer exist and are the expected type
(e.g., typeof response.answer === "string" and response.answer.length > 0)
before calling setReplyDraftText; if validation fails, avoid updating the editor
(or pass a safe default like an empty string) and optionally log or handle the
unexpected response so the UI state change (removing the fa-beat-fade class)
still occurs safely.
🧹 Nitpick comments (2)
Http/Controllers/FreeScoutGPTController.php (1)

16-22: Remove or use the unused stripHtmlToText helper.
Line 16–22 is currently unused (static analysis flagged it). Either delete it or wire it into editDraft if HTML stripping is intended.

🧹 Remove if not needed
-    private function stripHtmlToText($html)
-    {
-        if (!is_string($html)) {
-            return '';
-        }
-        return trim(strip_tags($html));
-    }
Public/js/module.js (1)

224-254: Consider adding a timeout for the AI request.

AI API calls can take a significant amount of time (30+ seconds). Without a timeout, the UI could remain in a loading state indefinitely if the server or AI service becomes unresponsive. Adding a timeout with appropriate user feedback would improve reliability.

⏱️ Suggested timeout configuration
     $.ajax({
         url: '/freescoutgpt/edit-draft',
         method: 'POST',
         dataType: 'json',
+        timeout: 60000, // 60 second timeout
         headers: csrfToken ? { 'X-CSRF-TOKEN': csrfToken } : {},
         data: {
             mailbox_id: mailbox_id,
             text: text,
             skip_client_data: 1
         },
         // ...
         error: function(xhr) {
             $(".gpteditbutton i").removeClass("fa-beat-fade");
+            if (xhr && xhr.statusText === 'timeout') {
+                showFloatingAlert('error', 'Request timed out. Please try again.');
+                return;
+            }
             // ... rest of error handling
         }
     });

Comment thread Http/Controllers/FreeScoutGPTController.php Outdated
Comment thread Providers/FreeScoutGPTServiceProvider.php Outdated
Comment thread Public/js/module.js
Comment thread Public/js/module.js
@presswizards
Copy link
Copy Markdown
Owner

@ku9nov There are a few issues with the code that CodeRabbit has identified, are you able to address these? I will review in more detail as well.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@Public/js/module.js`:
- Around line 189-199: getReplyDraftText currently converts the editor HTML to
plain text using tmp.textContent which collapses <br> and paragraph boundaries;
update getReplyDraftText to preserve line breaks by first transforming the DOM:
replace/insert newline markers for <br> elements and for block-level elements
(e.g., p, div, li) before reading text, then read the node text (tmp.textContent
or tmp.innerText) and trim; locate the getReplyDraftText function and apply this
DOM-to-text conversion after tmp.innerHTML = html and before returning the
trimmed string so drafts keep their original line breaks.
🧹 Nitpick comments (1)
Http/Controllers/FreeScoutGPTController.php (1)

16-22: Use stripHtmlToText when normalizing the draft input.
The helper is currently unused; applying it in editDraft both strips HTML and clears the unused-method warning.

♻️ Suggested wiring
-        $draftText = (string)$request->get('text', '');
-        $draftText = trim($draftText);
+        $draftText = $this->stripHtmlToText($request->get('text', ''));

Also applies to: 472-488

Comment thread Public/js/module.js
Comment on lines +189 to +199
function getReplyDraftText() {
if (!$('#body').length || typeof $('#body').summernote !== 'function') {
return '';
}
const html = $('#body').summernote('code') || '';
// Convert HTML to plain text for the editing request.
const tmp = document.createElement('div');
tmp.innerHTML = html;
return (tmp.textContent || tmp.innerText || '').trim();
}

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Preserve line breaks when extracting draft text.
Using textContent first can collapse <br>/paragraphs, flattening the draft before editing.

📝 Suggested tweak
-    return (tmp.textContent || tmp.innerText || '').trim();
+    return (tmp.innerText || tmp.textContent || '').trim();
🤖 Prompt for AI Agents
In `@Public/js/module.js` around lines 189 - 199, getReplyDraftText currently
converts the editor HTML to plain text using tmp.textContent which collapses
<br> and paragraph boundaries; update getReplyDraftText to preserve line breaks
by first transforming the DOM: replace/insert newline markers for <br> elements
and for block-level elements (e.g., p, div, li) before reading text, then read
the node text (tmp.textContent or tmp.innerText) and trim; locate the
getReplyDraftText function and apply this DOM-to-text conversion after
tmp.innerHTML = html and before returning the trimmed string so drafts keep
their original line breaks.

Copy link
Copy Markdown
Author

@ku9nov ku9nov Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switching the order to innerText does not reliably preserve line breaks and is browser-dependent.
For deterministic behavior, explicit handling of <br> / block elements would be required.
Keeping the current implementation for now.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ku9nov, you're absolutely right. innerText strips <br> tags without replacement, and its behavior varies significantly across browsers, while textContent adheres to the W3C standard and is more performant since it doesn't trigger reflows.

For reliable line break preservation, you'd indeed need to explicitly process <br> and block-level elements in the DOM before extracting text. Keeping the current implementation is a sensible choice for consistency and cross-browser compatibility.


✏️ Learnings added
Learnt from: ku9nov
Repo: presswizards/FreeScoutGPT PR: 40
File: Public/js/module.js:189-199
Timestamp: 2026-01-29T10:29:43.858Z
Learning: In the FreeScoutGPT module file `Public/js/module.js`, the `getReplyDraftText()` function uses `textContent` (not `innerText`) to extract text from the Summernote editor for consistency and cross-browser compatibility, accepting that line breaks may be collapsed. Switching to `innerText` is not preferred due to its browser-dependent behavior.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@ku9nov
Copy link
Copy Markdown
Author

ku9nov commented Feb 2, 2026

Hey @presswizards
It looks like all the issues are resolved

@presswizards
Copy link
Copy Markdown
Owner

@ku9nov Ok we'll dig into it more, do some testing, and ensure it's working well across the our test installations, etc.

@presswizards
Copy link
Copy Markdown
Owner

@ku9nov This branch has conflicts that must be resolved. Can you look into this more, it may be needed to rework the pull request against the latest commit.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Http/Controllers/FreeScoutGPTController.php (1)

362-362: ⚠️ Potential issue | 🟡 Minor

Inconsistent $skipClientData handling in Infomaniak API path.

The Infomaniak API path doesn't pass $skipClientData to fetchArticlesContext(), causing it to default to false. This means the skip_client_data request flag is ignored for Infomaniak, and client data will always be included when client_data_enabled is true—inconsistent with the Responses API path (line 278) that correctly passes the flag.

🐛 Proposed fix to pass $skipClientData
-            $fetchResult = $this->fetchArticlesContext($articleUrls, $settings, $request);
+            $fetchResult = $this->fetchArticlesContext($articleUrls, $settings, $request, $skipClientData);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Http/Controllers/FreeScoutGPTController.php` at line 362, The Infomaniak API
branch in FreeScoutGPTController calls fetchArticlesContext($articleUrls,
$settings, $request) without forwarding the $skipClientData flag, causing
skip_client_data to be ignored for Infomaniak; update that call to pass the
existing $skipClientData variable (matching the Responses API path which calls
fetchArticlesContext(..., $skipClientData) around the other branch) so
fetchArticlesContext receives and respects the skip-client-data behavior.
🧹 Nitpick comments (1)
Http/Controllers/FreeScoutGPTController.php (1)

16-22: Remove unused private method.

The static analysis correctly identifies that stripHtmlToText() is never called anywhere in this controller. This appears to be dead code that should be removed.

🧹 Proposed fix to remove dead code
-    private function stripHtmlToText($html)
-    {
-        if (!is_string($html)) {
-            return '';
-        }
-        return trim(strip_tags($html));
-    }
-
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Http/Controllers/FreeScoutGPTController.php` around lines 16 - 22, The
private method stripHtmlToText in FreeScoutGPTController is dead code and should
be removed; locate the method definition (private function
stripHtmlToText($html)) inside the FreeScoutGPTController class, confirm there
are no callers (search for stripHtmlToText), and delete the entire method
declaration and its body to clean up the controller.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Http/Controllers/FreeScoutGPTController.php`:
- Around line 623-633: The OpenAI chatCompletions API call in
FreeScoutGPTController is not wrapped in error handling; wrap the call to
$openaiClient->chatCompletions()->create(...) in a try-catch (catch \Throwable
or \Exception and optionally the Tectalic OpenAI exception type), log the
exception (use the controller's logger or Laravel's Log::error) including the
exception message and context, and return a JSON error response (e.g.,
Response::json(['error' => 'OpenAI request failed', 'details' =>
$e->getMessage()], 500)) when an exception occurs; keep the existing successful
return of the answer unchanged.

---

Outside diff comments:
In `@Http/Controllers/FreeScoutGPTController.php`:
- Line 362: The Infomaniak API branch in FreeScoutGPTController calls
fetchArticlesContext($articleUrls, $settings, $request) without forwarding the
$skipClientData flag, causing skip_client_data to be ignored for Infomaniak;
update that call to pass the existing $skipClientData variable (matching the
Responses API path which calls fetchArticlesContext(..., $skipClientData) around
the other branch) so fetchArticlesContext receives and respects the
skip-client-data behavior.

---

Nitpick comments:
In `@Http/Controllers/FreeScoutGPTController.php`:
- Around line 16-22: The private method stripHtmlToText in
FreeScoutGPTController is dead code and should be removed; locate the method
definition (private function stripHtmlToText($html)) inside the
FreeScoutGPTController class, confirm there are no callers (search for
stripHtmlToText), and delete the entire method declaration and its body to clean
up the controller.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b4cc2d49-bcb8-4904-b5b9-f78de2397b1f

📥 Commits

Reviewing files that changed from the base of the PR and between 206a99a and 1e5d8ce.

📒 Files selected for processing (5)
  • Entities/GPTSettings.php
  • Http/Controllers/FreeScoutGPTController.php
  • Http/routes.php
  • Public/js/module.js
  • Resources/views/settings.blade.php
✅ Files skipped from review due to trivial changes (1)
  • Entities/GPTSettings.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • Http/routes.php

Comment thread Http/Controllers/FreeScoutGPTController.php Outdated
@ku9nov
Copy link
Copy Markdown
Author

ku9nov commented Apr 14, 2026

Hey @presswizards
The merge conflicts have been resolved very carefully, keeping your incoming changes as much as possible and then re-integrating feature.

Both key flows were checked and are working:

  • reply generation
  • draft editing (edit feature)

@presswizards
Copy link
Copy Markdown
Owner

@ku9nov I’ll review and do some testing of this more soon and look at merging once complete.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants