Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions frontend/app/aipanel/aifeedbackbuttons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { cn, makeIconClass } from "@/util/util";
import { memo, useState } from "react";

interface AIFeedbackButtonsProps {
messageText: string;
}

export const AIFeedbackButtons = memo(({ messageText }: AIFeedbackButtonsProps) => {
const [thumbsUpClicked, setThumbsUpClicked] = useState(false);
const [thumbsDownClicked, setThumbsDownClicked] = useState(false);
const [copied, setCopied] = useState(false);

const handleThumbsUp = () => {
setThumbsUpClicked(!thumbsUpClicked);
if (thumbsDownClicked) {
setThumbsDownClicked(false);
}
if (!thumbsUpClicked) {
RpcApi.RecordTEventCommand(TabRpcClient, {
event: "waveai:feedback",
props: {
"waveai:feedback": "good",
},
});
}
};

const handleThumbsDown = () => {
setThumbsDownClicked(!thumbsDownClicked);
if (thumbsUpClicked) {
setThumbsUpClicked(false);
}
if (!thumbsDownClicked) {
RpcApi.RecordTEventCommand(TabRpcClient, {
event: "waveai:feedback",
props: {
"waveai:feedback": "bad",
},
});
}
};

const handleCopy = () => {
navigator.clipboard.writeText(messageText);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
Comment on lines +48 to +52
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling and timeout cleanup.

The clipboard API can fail (e.g., permission denied, insecure context), and the timeout is not cleaned up if the component unmounts during the 2-second window.

Apply this diff to add error handling and cleanup:

+    const copyTimeoutRef = useRef<NodeJS.Timeout | null>(null);
+
+    useEffect(() => {
+        return () => {
+            if (copyTimeoutRef.current) {
+                clearTimeout(copyTimeoutRef.current);
+            }
+        };
+    }, []);
+
     const handleCopy = () => {
-        navigator.clipboard.writeText(messageText);
-        setCopied(true);
-        setTimeout(() => setCopied(false), 2000);
+        navigator.clipboard.writeText(messageText).then(
+            () => {
+                setCopied(true);
+                if (copyTimeoutRef.current) {
+                    clearTimeout(copyTimeoutRef.current);
+                }
+                copyTimeoutRef.current = setTimeout(() => setCopied(false), 2000);
+            },
+            (err) => {
+                console.error("Failed to copy text:", err);
+            }
+        );
     };

Committable suggestion skipped: line range outside the PR's diff.


return (
<div className="flex items-center gap-0.5 mt-2">
<button
onClick={handleThumbsUp}
className={cn(
"p-1.5 rounded cursor-pointer transition-colors",
thumbsUpClicked
? "text-accent"
: "text-secondary hover:bg-gray-700 hover:text-primary"
)}
title="Good Response"
>
<i className={makeIconClass(thumbsUpClicked ? "solid@thumbs-up" : "regular@thumbs-up", false)} />
</button>
<button
onClick={handleThumbsDown}
className={cn(
"p-1.5 rounded cursor-pointer transition-colors",
thumbsDownClicked
? "text-accent"
: "text-secondary hover:bg-gray-700 hover:text-primary"
)}
title="Bad Response"
>
<i className={makeIconClass(thumbsDownClicked ? "solid@thumbs-down" : "regular@thumbs-down", false)} />
</button>
{messageText?.trim() && (
<button
onClick={handleCopy}
className={cn(
"p-1.5 rounded cursor-pointer transition-colors",
copied
? "text-success"
: "text-secondary hover:bg-gray-700 hover:text-primary"
)}
title="Copy Message"
>
<i className={makeIconClass(copied ? "solid@check" : "regular@copy", false)} />
</button>
)}
</div>
);
});

AIFeedbackButtons.displayName = "AIFeedbackButtons";
Loading
Loading