Skip to content

Commit 489f138

Browse files
feat: add feedback tracking for assistant messages (#1597)
* add feedback tracking for assistant messages * style: apply biome auto-fixes [skip ci] * don't show during onboarding * coderabbit suggestions --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent c6e3f37 commit 489f138

7 files changed

Lines changed: 94 additions & 22 deletions

File tree

frontend/app/chat/_components/assistant-message.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ const MarkdownRenderer = dynamic(
1111
{ ssr: false },
1212
);
1313

14+
import { trackButton } from "@/lib/analytics";
1415
import { cn } from "@/lib/utils";
1516
import type {
1617
FunctionCall,
1718
TokenUsage as TokenUsageType,
1819
} from "../_types/types";
1920
import { FunctionCalls } from "./function-calls";
2021
import { Message } from "./message";
22+
import MessageActions from "./message-actions";
2123
import { TokenUsage } from "./token-usage";
2224

2325
interface AssistantMessageProps {
@@ -35,6 +37,8 @@ interface AssistantMessageProps {
3537
delay?: number;
3638
isInitialGreeting?: boolean;
3739
usage?: TokenUsageType;
40+
timestamp?: Date;
41+
showFeedback?: boolean;
3842
}
3943

4044
export function AssistantMessage({
@@ -52,7 +56,19 @@ export function AssistantMessage({
5256
delay = 0.2,
5357
isInitialGreeting = false,
5458
usage,
59+
timestamp,
60+
showFeedback = true,
5561
}: AssistantMessageProps) {
62+
const trackFeedback = (feedback: "like" | "dislike") => {
63+
trackButton({
64+
action: feedback,
65+
elementId: "message-feedback",
66+
namespace: "chat",
67+
CTA: feedback === "like" ? "Like Message" : "Dislike Message",
68+
timestamp: timestamp?.getTime(),
69+
});
70+
};
71+
5672
return (
5773
<motion.div
5874
initial={animate ? { opacity: 0, y: -20 } : { opacity: 1, y: 0 }}
@@ -151,6 +167,9 @@ export function AssistantMessage({
151167
}
152168
/>
153169
{usage && !isStreaming && <TokenUsage usage={usage} />}
170+
{!isInitialGreeting && showFeedback && !isStreaming && (
171+
<MessageActions trackFeedback={trackFeedback} />
172+
)}
154173
</motion.div>
155174
</div>
156175
</Message>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { ThumbsDown, ThumbsUp } from "lucide-react";
2+
import { useState } from "react";
3+
import { Button } from "@/components/ui/button";
4+
5+
interface MessageActionsProps {
6+
trackFeedback: (feedback: "like" | "dislike") => void;
7+
}
8+
9+
const MessageActions = ({ trackFeedback }: MessageActionsProps) => {
10+
const [feedbackSelected, setFeedbackSelected] = useState<
11+
"like" | "dislike" | null
12+
>(null);
13+
14+
const handleFeedback = (feedback: "like" | "dislike") => {
15+
if (feedbackSelected === feedback) return; // Prevent multiple tracking events for the same feedback
16+
trackFeedback(feedback);
17+
setFeedbackSelected(feedback);
18+
};
19+
20+
return (
21+
<div className="flex space-x-2 mt-2">
22+
<Button
23+
variant="ghost"
24+
size="icon"
25+
aria-label="Like"
26+
aria-pressed={feedbackSelected === "like"}
27+
className={
28+
feedbackSelected !== "like"
29+
? "text-muted-foreground hover:text-foreground"
30+
: ""
31+
}
32+
onClick={() => handleFeedback("like")}
33+
>
34+
<ThumbsUp
35+
className={`h-4 w-4 ${feedbackSelected === "like" ? "fill-current" : ""}`}
36+
/>
37+
</Button>
38+
<Button
39+
variant="ghost"
40+
size="icon"
41+
aria-label="Dislike"
42+
aria-pressed={feedbackSelected === "dislike"}
43+
className={
44+
feedbackSelected !== "dislike"
45+
? "text-muted-foreground hover:text-foreground"
46+
: ""
47+
}
48+
onClick={() => handleFeedback("dislike")}
49+
>
50+
<ThumbsDown
51+
className={`h-4 w-4 ${feedbackSelected === "dislike" ? "fill-current" : ""}`}
52+
/>
53+
</Button>
54+
</div>
55+
);
56+
};
57+
58+
export default MessageActions;

frontend/app/chat/_types/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ export interface Message {
2121
usage?: TokenUsage;
2222
}
2323

24+
export const INITIAL_ASSISTANT_MESSAGE: Message = {
25+
role: "assistant",
26+
content: "How can I assist?",
27+
timestamp: new Date(),
28+
};
29+
2430
export interface FunctionCall {
2531
name: string;
2632
arguments?: Record<string, unknown>;

frontend/app/chat/page.tsx

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type {
2828
RequestBody,
2929
ToolCallResult,
3030
} from "./_types/types";
31+
import { INITIAL_ASSISTANT_MESSAGE } from "./_types/types";
3132

3233
function ChatPage() {
3334
const isDebugMode = process.env.NEXT_PUBLIC_OPENRAG_DEBUG === "true";
@@ -52,11 +53,7 @@ function ChatPage() {
5253
setLoading,
5354
} = useChat();
5455
const [messages, setMessages] = useState<Message[]>([
55-
{
56-
role: "assistant",
57-
content: "How can I assist?",
58-
timestamp: new Date(),
59-
},
56+
INITIAL_ASSISTANT_MESSAGE,
6057
]);
6158
const [input, setInput] = useState("");
6259
const { setChatError } = useChat();
@@ -327,13 +324,7 @@ function ChatPage() {
327324
// Abort any in-flight streaming so it doesn't bleed into new chat
328325
abortStream();
329326
// Reset chat UI even if context state was already 'new'
330-
setMessages([
331-
{
332-
role: "assistant",
333-
content: "How can I assist?",
334-
timestamp: new Date(),
335-
},
336-
]);
327+
setMessages([INITIAL_ASSISTANT_MESSAGE]);
337328
setInput("");
338329
setExpandedFunctionCalls(new Set());
339330
setIsFilterHighlighted(false);
@@ -571,13 +562,7 @@ function ChatPage() {
571562
useEffect(() => {
572563
if (placeholderConversation && currentConversationId === null) {
573564
console.log("Starting new conversation");
574-
setMessages([
575-
{
576-
role: "assistant",
577-
content: "How can I assist?",
578-
timestamp: new Date(),
579-
},
580-
]);
565+
setMessages([INITIAL_ASSISTANT_MESSAGE]);
581566
lastLoadedConversationRef.current = null;
582567

583568
// Focus input when starting a new conversation
@@ -1054,9 +1039,11 @@ function ChatPage() {
10541039
isInitialGreeting={
10551040
index === 0 &&
10561041
messages.length === 1 &&
1057-
message.content === "How can I assist?"
1042+
message.content ===
1043+
INITIAL_ASSISTANT_MESSAGE.content
10581044
}
10591045
usage={message.usage}
1046+
timestamp={message.timestamp}
10601047
/>
10611048
)}
10621049
</div>

frontend/app/onboarding/_components/onboarding-content.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ export function OnboardingContent({
287287
onToggle={() => {}}
288288
isStreaming={!!streamingMessage}
289289
isCompleted={currentStep > 3}
290+
showFeedback={false}
290291
/>
291292
)}
292293

frontend/contexts/chat-context.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
useState,
1212
} from "react";
1313
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
14+
import { INITIAL_ASSISTANT_MESSAGE } from "@/app/chat/_types/types";
1415

1516
export type EndpointType = "chat" | "langflow";
1617

@@ -346,7 +347,7 @@ export function ChatProvider({ children }: ChatProviderProps) {
346347
messages: [
347348
{
348349
role: "assistant",
349-
content: "How can I assist?",
350+
content: INITIAL_ASSISTANT_MESSAGE.content,
350351
timestamp: new Date().toISOString(),
351352
},
352353
],

frontend/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"moduleResolution": "bundler",
1212
"resolveJsonModule": true,
1313
"isolatedModules": true,
14-
"jsx": "react-jsx",
14+
"jsx": "preserve",
1515
"incremental": true,
1616
"plugins": [
1717
{

0 commit comments

Comments
 (0)