Skip to content

Commit ead83eb

Browse files
authored
feat: Feedback buttons (#423)
* feat: Feedback buttons * cr
1 parent b99628c commit ead83eb

File tree

4 files changed

+574
-365
lines changed

4 files changed

+574
-365
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { Client, Feedback } from "langsmith";
2+
import { NextRequest, NextResponse } from "next/server";
3+
4+
export async function POST(req: NextRequest) {
5+
try {
6+
const body = await req.json();
7+
const { runId, feedbackKey, score, comment } = body;
8+
9+
if (!runId || !feedbackKey) {
10+
return NextResponse.json(
11+
{ error: "`runId` and `feedbackKey` are required." },
12+
{ status: 400 },
13+
);
14+
}
15+
16+
const lsClient = new Client({
17+
apiKey: process.env.LANGCHAIN_API_KEY,
18+
});
19+
20+
const feedback = await lsClient.createFeedback(runId, feedbackKey, {
21+
score,
22+
comment,
23+
});
24+
25+
return NextResponse.json(
26+
{ success: true, feedback: feedback },
27+
{ status: 200 },
28+
);
29+
} catch (error) {
30+
console.error("Failed to process feedback request:", error);
31+
32+
return NextResponse.json(
33+
{ error: "Failed to submit feedback." },
34+
{ status: 500 },
35+
);
36+
}
37+
}
38+
39+
export async function GET(req: NextRequest) {
40+
try {
41+
const searchParams = req.nextUrl.searchParams;
42+
const runId = searchParams.get("runId");
43+
const feedbackKey = searchParams.get("feedbackKey");
44+
45+
if (!runId || !feedbackKey) {
46+
return new NextResponse(
47+
JSON.stringify({
48+
error: "`runId` and `feedbackKey` are required.",
49+
}),
50+
{
51+
status: 400,
52+
headers: { "Content-Type": "application/json" },
53+
},
54+
);
55+
}
56+
57+
const lsClient = new Client({
58+
apiKey: process.env.LANGCHAIN_API_KEY,
59+
});
60+
61+
const runFeedback: Feedback[] = [];
62+
63+
const run_feedback = lsClient.listFeedback({
64+
runIds: [runId],
65+
feedbackKeys: [feedbackKey],
66+
});
67+
68+
for await (const feedback of run_feedback) {
69+
runFeedback.push(feedback);
70+
}
71+
72+
return new NextResponse(
73+
JSON.stringify({
74+
feedback: runFeedback,
75+
}),
76+
{
77+
status: 200,
78+
headers: { "Content-Type": "application/json" },
79+
},
80+
);
81+
} catch (error) {
82+
console.error("Failed to fetch feedback:", error);
83+
return NextResponse.json(
84+
{ error: "Failed to fetch feedback." },
85+
{ status: 500 },
86+
);
87+
}
88+
}

frontend/app/components/chat-interface/messages.tsx

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@ import {
55
useMessage,
66
useThreadRuntime,
77
} from "@assistant-ui/react";
8-
import { type FC } from "react";
8+
import { useState, type FC } from "react";
99

1010
import { MarkdownText } from "../ui/assistant-ui/markdown-text";
11+
import { useGraphContext } from "@/app/contexts/GraphContext";
12+
import { useRuns } from "@/app/hooks/useRuns";
13+
import { TooltipIconButton } from "../ui/assistant-ui/tooltip-icon-button";
14+
import { ThumbsDownIcon, ThumbsUpIcon } from "lucide-react";
1115

1216
export const UserMessage: FC = () => {
1317
return (
@@ -19,6 +23,59 @@ export const UserMessage: FC = () => {
1923
);
2024
};
2125

26+
function FeedbackButtons() {
27+
const {
28+
graphData: { runId, isStreaming },
29+
} = useGraphContext();
30+
const { sendFeedback } = useRuns();
31+
const [feedback, setFeedback] = useState<"good" | "bad">();
32+
33+
const feedbackKey = "user_feedback";
34+
const goodScore = 1;
35+
const badScore = 0;
36+
37+
if (!runId || isStreaming) return null;
38+
39+
if (feedback) {
40+
return (
41+
<div className="flex gap-2 items-center mt-4">
42+
{feedback === "good" ? (
43+
<ThumbsUpIcon className="w-4 h-4 text-green-500" />
44+
) : (
45+
<ThumbsDownIcon className="w-4 h-4 text-red-500" />
46+
)}
47+
</div>
48+
);
49+
}
50+
51+
return (
52+
<div className="flex gap-2 items-center mt-4">
53+
<TooltipIconButton
54+
delayDuration={200}
55+
variant="ghost"
56+
tooltip="Good response"
57+
onClick={() => {
58+
sendFeedback(runId, feedbackKey, goodScore);
59+
setFeedback("good");
60+
}}
61+
>
62+
<ThumbsUpIcon className="w-4 h-4" />
63+
</TooltipIconButton>
64+
<TooltipIconButton
65+
delayDuration={200}
66+
variant="ghost"
67+
tooltip="Bad response"
68+
onClick={() => {
69+
sendFeedback(runId, feedbackKey, badScore);
70+
setFeedback("bad");
71+
}}
72+
>
73+
<ThumbsDownIcon className="w-4 h-4" />
74+
</TooltipIconButton>
75+
</div>
76+
);
77+
}
78+
2279
export const AssistantMessage: FC = () => {
2380
const threadRuntime = useThreadRuntime();
2481
const threadState = threadRuntime.getState();
@@ -34,6 +91,7 @@ export const AssistantMessage: FC = () => {
3491
{shouldRenderMessageBreak ? (
3592
<hr className="relative left-1/2 -translate-x-1/2 w-[90vw] sm:w-[45vw] mt-4 sm:mt-6 border-gray-600" />
3693
) : null}
94+
{isLast && <FeedbackButtons />}
3795
</div>
3896
</MessagePrimitive.Root>
3997
);

0 commit comments

Comments
 (0)