Skip to content

Commit

Permalink
Merge pull request #161 from ahmad2b/ahmad2b/add-chat-feedback
Browse files Browse the repository at this point in the history
Fixes #122: Implement thumbs up/down feedback in chat
  • Loading branch information
bracesproul authored Oct 29, 2024
2 parents 5d443da + e8796d8 commit 68c6f4b
Show file tree
Hide file tree
Showing 7 changed files with 367 additions and 6 deletions.
88 changes: 88 additions & 0 deletions src/app/api/runs/feedback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Client, Feedback } from "langsmith";
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { runId, feedbackKey, score, comment } = body;

if (!runId || !feedbackKey) {
return NextResponse.json(
{ error: "`runId` and `feedbackKey` are required." },
{ status: 400 }
);
}

const lsClient = new Client({
apiKey: process.env.LANGCHAIN_API_KEY,
});

const feedback = await lsClient.createFeedback(runId, feedbackKey, {
score,
comment,
});

return NextResponse.json(
{ success: true, feedback: feedback },
{ status: 200 }
);
} catch (error) {
console.error("Failed to process feedback request:", error);

return NextResponse.json(
{ error: "Failed to submit feedback." },
{ status: 500 }
);
}
}

export async function GET(req: NextRequest) {
try {
const searchParams = req.nextUrl.searchParams;
const runId = searchParams.get("runId");
const feedbackKey = searchParams.get("feedbackKey");

if (!runId || !feedbackKey) {
return new NextResponse(
JSON.stringify({
error: "`runId` and `feedbackKey` are required.",
}),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
);
}

const lsClient = new Client({
apiKey: process.env.LANGCHAIN_API_KEY,
});

const runFeedback: Feedback[] = [];

const run_feedback = await lsClient.listFeedback({
runIds: [runId],
feedbackKeys: [feedbackKey],
});

for await (const feedback of run_feedback) {
runFeedback.push(feedback);
}

return new NextResponse(
JSON.stringify({
feedback: runFeedback,
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
}
);
} catch (error) {
console.error("Failed to fetch feedback:", error);
return NextResponse.json(
{ error: "Failed to fetch feedback." },
{ status: 500 }
);
}
}
73 changes: 73 additions & 0 deletions src/components/chat-interface/feedback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useToast } from "@/hooks/use-toast";
import { FeedbackResponse } from "@/hooks/useFeedback";
import { ThumbsUpIcon, ThumbsDownIcon } from "lucide-react";
import { Dispatch, FC, SetStateAction } from "react";
import { cn } from "@/lib/utils";
import { TooltipIconButton } from "../ui/assistant-ui/tooltip-icon-button";

interface FeedbackButtonProps {
runId: string;
setFeedbackSubmitted: Dispatch<SetStateAction<boolean>>;
sendFeedback: (
runId: string,
feedbackKey: string,
score: number,
comment?: string
) => Promise<FeedbackResponse | undefined>;
feedbackValue: number;
icon: "thumbs-up" | "thumbs-down";
isLoading: boolean;
}

export const FeedbackButton: FC<FeedbackButtonProps> = ({
runId,
setFeedbackSubmitted,
sendFeedback,
isLoading,
feedbackValue,
icon,
}) => {
const { toast } = useToast();

const handleClick = async () => {
try {
const res = await sendFeedback(runId, "feedback", feedbackValue);
if (res?.success) {
setFeedbackSubmitted(true);
} else {
toast({
title: "Failed to submit feedback",
description: "Please try again later.",
variant: "destructive",
});
}
} catch (_) {
toast({
title: "Failed to submit feedback",
description: "Please try again later.",
variant: "destructive",
});
}
};

const tooltip = `Give ${icon === "thumbs-up" ? "positive" : "negative"} feedback on this run`;

return (
<TooltipIconButton
variant="ghost"
size="icon"
onClick={handleClick}
aria-label={tooltip}
tooltip={tooltip}
disabled={isLoading}
>
{icon === "thumbs-up" ? (
<ThumbsUpIcon className={cn("size-4", isLoading && "text-gray-300")} />
) : (
<ThumbsDownIcon
className={cn("size-4", isLoading && "text-gray-300")}
/>
)}
</TooltipIconButton>
);
};
85 changes: 82 additions & 3 deletions src/components/chat-interface/messages.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
"use client";

import { MessagePrimitive } from "@assistant-ui/react";
import { type FC } from "react";
import {
ActionBarPrimitive,
MessagePrimitive,
useMessage,
} from "@assistant-ui/react";
import React, { Dispatch, SetStateAction, type FC } from "react";

import { MarkdownText } from "@/components/ui/assistant-ui/markdown-text";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { FeedbackButton } from "./feedback";
import { TighterText } from "../ui/header";
import { useFeedback } from "@/hooks/useFeedback";

export const AssistantMessage: FC = () => {
interface AssistantMessageProps {
runId: string | undefined;
feedbackSubmitted: boolean;
setFeedbackSubmitted: Dispatch<SetStateAction<boolean>>;
}

export const AssistantMessage: FC<AssistantMessageProps> = ({
runId,
feedbackSubmitted,
setFeedbackSubmitted,
}) => {
const isLast = useMessage().isLast;
return (
<MessagePrimitive.Root className="relative grid w-full max-w-2xl grid-cols-[auto_auto_1fr] grid-rows-[auto_1fr] py-4">
<Avatar className="col-start-1 row-span-full row-start-1 mr-4">
Expand All @@ -15,6 +33,15 @@ export const AssistantMessage: FC = () => {

<div className="text-foreground col-span-2 col-start-2 row-start-1 my-1.5 max-w-xl break-words leading-7">
<MessagePrimitive.Content components={{ Text: MarkdownText }} />
{isLast && runId && (
<MessagePrimitive.If lastOrHover assistant>
<AssistantMessageBar
feedbackSubmitted={feedbackSubmitted}
setFeedbackSubmitted={setFeedbackSubmitted}
runId={runId}
/>
</MessagePrimitive.If>
)}
</div>
</MessagePrimitive.Root>
);
Expand All @@ -29,3 +56,55 @@ export const UserMessage: FC = () => {
</MessagePrimitive.Root>
);
};

interface AssistantMessageBarProps {
runId: string;
feedbackSubmitted: boolean;
setFeedbackSubmitted: Dispatch<SetStateAction<boolean>>;
}

const AssistantMessageBarComponent = ({
runId,
feedbackSubmitted,
setFeedbackSubmitted,
}: AssistantMessageBarProps) => {
const { isLoading, sendFeedback } = useFeedback();
return (
<ActionBarPrimitive.Root
hideWhenRunning
autohide="not-last"
className="flex items-center mt-2"
>
{feedbackSubmitted ? (
<TighterText className="text-gray-500 text-sm">
Feedback received! Thank you!
</TighterText>
) : (
<>
<ActionBarPrimitive.FeedbackPositive asChild>
<FeedbackButton
isLoading={isLoading}
sendFeedback={sendFeedback}
setFeedbackSubmitted={setFeedbackSubmitted}
runId={runId}
feedbackValue={1.0}
icon="thumbs-up"
/>
</ActionBarPrimitive.FeedbackPositive>
<ActionBarPrimitive.FeedbackNegative asChild>
<FeedbackButton
isLoading={isLoading}
sendFeedback={sendFeedback}
setFeedbackSubmitted={setFeedbackSubmitted}
runId={runId}
feedbackValue={0.0}
icon="thumbs-down"
/>
</ActionBarPrimitive.FeedbackNegative>
</>
)}
</ActionBarPrimitive.Root>
);
};

const AssistantMessageBar = React.memo(AssistantMessageBarComponent);
11 changes: 9 additions & 2 deletions src/components/chat-interface/thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const Thread: FC<ThreadProps> = (props: ThreadProps) => {
const {
userData: { user },
threadData: { createThread, modelName, setModelName, assistantId },
graphData: { clearState },
graphData: { clearState, runId, feedbackSubmitted, setFeedbackSubmitted },
} = useGraphContext();

useLangSmithLinkToolUI();
Expand Down Expand Up @@ -116,7 +116,14 @@ export const Thread: FC<ThreadProps> = (props: ThreadProps) => {
<ThreadPrimitive.Messages
components={{
UserMessage: UserMessage,
AssistantMessage: AssistantMessage,
AssistantMessage: (prop) => (
<AssistantMessage
{...prop}
feedbackSubmitted={feedbackSubmitted}
setFeedbackSubmitted={setFeedbackSubmitted}
runId={runId}
/>
),
}}
/>
</ThreadPrimitive.Viewport>
Expand Down
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const LANGGRAPH_API_URL =
process.env.LANGGRAPH_API_URL ?? "http://localhost:56339";
process.env.LANGGRAPH_API_URL ?? "http://localhost:50944";
// v2 is tied to the 'open-canvas-prod' deployment.
export const ASSISTANT_ID_COOKIE = "oc_assistant_id_v2";
// export const ASSISTANT_ID_COOKIE = "oc_assistant_id";
Expand Down
13 changes: 13 additions & 0 deletions src/contexts/GraphContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,16 @@ import { reverseCleanContent } from "@/lib/normalize_string";
import { setCookie } from "@/lib/cookies";

interface GraphData {
runId: string | undefined;
isStreaming: boolean;
selectedBlocks: TextHighlight | undefined;
messages: BaseMessage[];
artifact: ArtifactV3 | undefined;
updateRenderedArtifactRequired: boolean;
isArtifactSaved: boolean;
firstTokenReceived: boolean;
feedbackSubmitted: boolean;
setFeedbackSubmitted: Dispatch<SetStateAction<boolean>>;
setArtifact: Dispatch<SetStateAction<ArtifactV3 | undefined>>;
setSelectedBlocks: Dispatch<SetStateAction<TextHighlight | undefined>>;
setSelectedArtifact: (index: number) => void;
Expand Down Expand Up @@ -126,6 +129,8 @@ export function GraphProvider({ children }: { children: ReactNode }) {
const [isArtifactSaved, setIsArtifactSaved] = useState(true);
const [threadSwitched, setThreadSwitched] = useState(false);
const [firstTokenReceived, setFirstTokenReceived] = useState(false);
const [runId, setRunId] = useState<string>();
const [feedbackSubmitted, setFeedbackSubmitted] = useState(false);

useEffect(() => {
if (userData.user) return;
Expand Down Expand Up @@ -289,10 +294,13 @@ export function GraphProvider({ children }: { children: ReactNode }) {
}

setIsStreaming(true);
setRunId(undefined);
setFeedbackSubmitted(false);
// The root level run ID of this stream
let runId = "";
let followupMessageId = "";
// let lastMessage: AIMessage | undefined = undefined;

try {
const stream = client.runs.stream(
threadData.threadId,
Expand Down Expand Up @@ -344,6 +352,7 @@ export function GraphProvider({ children }: { children: ReactNode }) {
try {
if (!runId && chunk.data?.metadata?.run_id) {
runId = chunk.data.metadata.run_id;
setRunId(runId);
}
if (chunk.data.event === "on_chain_start") {
if (
Expand Down Expand Up @@ -783,6 +792,7 @@ export function GraphProvider({ children }: { children: ReactNode }) {
});
return newMessageWithToolCall;
}

return msg;
});
return newMsgs;
Expand Down Expand Up @@ -920,13 +930,16 @@ export function GraphProvider({ children }: { children: ReactNode }) {
userData,
threadData,
graphData: {
runId,
isStreaming,
selectedBlocks,
messages,
artifact,
updateRenderedArtifactRequired,
isArtifactSaved,
firstTokenReceived,
feedbackSubmitted,
setFeedbackSubmitted,
setArtifact,
setSelectedBlocks,
setSelectedArtifact,
Expand Down
Loading

0 comments on commit 68c6f4b

Please sign in to comment.