Skip to content

Commit ebea385

Browse files
committed
feat: add copy-to-clipboard button on AI chat code blocks
1 parent 12ac616 commit ebea385

3 files changed

Lines changed: 61 additions & 0 deletions

File tree

src/core-ai/AIChatPanel.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1191,6 +1191,7 @@ define(function (require, exports, module) {
11911191
try {
11921192
$target.html(marked.parse(_segmentText, { breaks: true, gfm: true }));
11931193
_enhanceColorCodes($target);
1194+
_addCopyButtons($target);
11941195
} catch (e) {
11951196
$target.text(_segmentText);
11961197
}
@@ -1257,6 +1258,35 @@ define(function (require, exports, module) {
12571258
});
12581259
}
12591260

1261+
/**
1262+
* Inject a copy-to-clipboard button into each <pre> block inside the given container.
1263+
* Idempotent: skips <pre> elements that already have a .ai-copy-btn.
1264+
*/
1265+
function _addCopyButtons($container) {
1266+
$container.find("pre").each(function () {
1267+
const $pre = $(this);
1268+
if ($pre.find(".ai-copy-btn").length) {
1269+
return;
1270+
}
1271+
const $btn = $('<button class="ai-copy-btn" title="' + Strings.AI_CHAT_COPY_CODE + '">' +
1272+
'<i class="fa-solid fa-copy"></i></button>');
1273+
$btn.on("click", function (e) {
1274+
e.stopPropagation();
1275+
const $code = $pre.find("code");
1276+
const text = ($code.length ? $code[0] : $pre[0]).textContent;
1277+
Phoenix.app.copyToClipboard(text);
1278+
const $icon = $btn.find("i");
1279+
$icon.removeClass("fa-copy").addClass("fa-check");
1280+
$btn.attr("title", Strings.AI_CHAT_COPIED_CODE);
1281+
setTimeout(function () {
1282+
$icon.removeClass("fa-check").addClass("fa-copy");
1283+
$btn.attr("title", Strings.AI_CHAT_COPY_CODE);
1284+
}, 1500);
1285+
});
1286+
$pre.append($btn);
1287+
});
1288+
}
1289+
12601290
function _appendToolIndicator(toolName, toolId) {
12611291
// Remove thinking indicator on first content
12621292
if (!_hasReceivedContent) {
@@ -1628,6 +1658,9 @@ define(function (require, exports, module) {
16281658
// Finalize: remove ai-stream-target class so future messages get their own container
16291659
$messages.find(".ai-stream-target").removeClass("ai-stream-target");
16301660

1661+
// Ensure copy buttons are present on all code blocks
1662+
_addCopyButtons($messages);
1663+
16311664
// Mark all active tool indicators as done
16321665
_finishActiveTools();
16331666
}

src/nls/root/strings.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1862,6 +1862,8 @@ define({
18621862
"AI_CHAT_CONTEXT_SELECTION": "Selection L{0}-L{1}",
18631863
"AI_CHAT_CONTEXT_CURSOR": "Line {0}",
18641864
"AI_CHAT_CONTEXT_LIVE_PREVIEW": "Live Preview",
1865+
"AI_CHAT_COPY_CODE": "Copy",
1866+
"AI_CHAT_COPIED_CODE": "Copied!",
18651867

18661868
// demo start - Phoenix Code Playground - Interactive Onboarding
18671869
"DEMO_SECTION1_TITLE": "Edit in Live Preview",

src/styles/Extn-AIChatPanel.less

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@
163163
border-radius: 4px;
164164
overflow-x: auto;
165165
margin: 6px 0;
166+
position: relative;
166167

167168
code {
168169
background: none;
@@ -172,6 +173,31 @@
172173
line-height: 1.5;
173174
color: @project-panel-text-1;
174175
}
176+
177+
.ai-copy-btn {
178+
position: absolute;
179+
top: 4px;
180+
right: 4px;
181+
background: rgba(255, 255, 255, 0.08);
182+
border: 1px solid rgba(255, 255, 255, 0.1);
183+
border-radius: 4px;
184+
color: rgba(255, 255, 255, 0.5);
185+
cursor: pointer;
186+
padding: 2px 6px;
187+
font-size: 12px;
188+
line-height: 1;
189+
opacity: 0;
190+
transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease;
191+
192+
&:hover {
193+
background: rgba(255, 255, 255, 0.15);
194+
color: rgba(255, 255, 255, 0.85);
195+
}
196+
}
197+
198+
&:hover .ai-copy-btn {
199+
opacity: 1;
200+
}
175201
}
176202

177203
ul, ol {

0 commit comments

Comments
 (0)