Browse Source

fix: copy button works in all browsers with per-message success feedback

Made-with: Cursor
pull/6453/head
Priyanka Punukollu 1 month ago
parent
commit
ac0302b9e5
  1. 17
      apps/client/src/app/components/ai-chat/ai-chat.component.html
  2. 27
      apps/client/src/app/components/ai-chat/ai-chat.component.scss
  3. 44
      apps/client/src/app/components/ai-chat/ai-chat.component.ts

17
apps/client/src/app/components/ai-chat/ai-chat.component.html

@ -172,7 +172,7 @@
@if (activeTab === 'chat') {
<!-- Messages area -->
<div #messagesContainer class="ai-messages">
@for (msg of messages; track $index) {
@for (msg of messages; track $index; let i = $index) {
<div
class="ai-message"
[class.ai-message--assistant]="msg.role === 'assistant'"
@ -181,6 +181,21 @@
<!-- Bubble -->
<div class="ai-bubble" [innerHTML]="msg.content | aiMarkdown"></div>
<!-- Copy button for assistant messages -->
@if (msg.role === 'assistant') {
<button
class="ai-copy-btn"
(click)="copyToClipboard(msg.content, i)"
[title]="copySuccessMap[i] ? 'Copied!' : 'Copy response'"
>
@if (!copySuccessMap[i]) {
<span>⎘ Copy</span>
} @else {
<span>✓ Copied!</span>
}
</button>
}
<!-- Real estate comparison card -->
@if (msg.comparisonCard) {
<gf-real-estate-card [card]="msg.comparisonCard" />

27
apps/client/src/app/components/ai-chat/ai-chat.component.scss

@ -446,6 +446,33 @@
}
}
.ai-copy-btn {
margin-top: 0.25rem;
padding: 0.2rem 0.5rem;
font-size: 0.7rem;
font-weight: 500;
background: rgba(99, 102, 241, 0.1);
color: #6366f1;
border: 1px solid rgba(99, 102, 241, 0.25);
border-radius: 0.4rem;
cursor: pointer;
transition: background 0.15s ease;
&:hover {
background: rgba(99, 102, 241, 0.2);
}
:host-context(.theme-dark) & {
background: rgba(99, 102, 241, 0.15);
color: #a5b4fc;
border-color: rgba(99, 102, 241, 0.35);
&:hover {
background: rgba(99, 102, 241, 0.25);
}
}
}
.ai-chip {
display: inline-flex;
align-items: center;

44
apps/client/src/app/components/ai-chat/ai-chat.component.ts

@ -95,6 +95,7 @@ export class GfAiChatComponent implements OnInit, OnDestroy {
public isSeeding = false;
public enableRealEstate: boolean;
public agentReachable: boolean | null = null;
public copySuccessMap: { [key: number]: boolean } = {};
// Activity log tab
public activeTab: 'chat' | 'log' = 'chat';
@ -503,6 +504,49 @@ export class GfAiChatComponent implements OnInit, OnDestroy {
return tool.replace(/_/g, ' ');
}
// ---------------------------------------------------------------------------
// Copy to clipboard
// ---------------------------------------------------------------------------
public copyToClipboard(text: string, index: number): void {
const writeToClipboard = (t: string): Promise<void> => {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(t);
}
return new Promise<void>((resolve, reject) => {
const textarea = document.createElement('textarea');
textarea.value = t;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
textarea.style.top = '-9999px';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
document.execCommand('copy');
document.body.removeChild(textarea);
resolve();
} catch (err) {
document.body.removeChild(textarea);
reject(err);
}
});
};
writeToClipboard(text)
.then(() => {
this.copySuccessMap[index] = true;
setTimeout(() => {
this.copySuccessMap[index] = false;
this.changeDetectorRef.markForCheck();
}, 1500);
this.changeDetectorRef.markForCheck();
})
.catch(() => {
console.warn('Copy failed');
});
}
// ---------------------------------------------------------------------------
// Connection health check
// ---------------------------------------------------------------------------

Loading…
Cancel
Save