Skip to content

feat: add SIWA feedback functionality to dashboard chat #7272

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 6, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ import { useCallback, useState } from "react";
import type { ThirdwebClient } from "thirdweb";
import { useActiveWalletConnectionStatus } from "thirdweb/react";
import type { NebulaContext } from "../../api/chat";
import type { NebulaUserMessage } from "../../api/types";
import type { ExamplePrompt } from "../../data/examplePrompts";
import { NebulaIcon } from "../../icons/NebulaIcon";
import { ChatBar } from "../ChatBar";
import { Chats } from "../Chats";
import type { ChatMessage } from "../Chats";
import { type CustomChatMessage, CustomChats } from "./CustomChats";
import type { UserMessage, UserMessageContent } from "./CustomChats";

export default function CustomChatContent(props: {
authToken: string | undefined;
Expand Down Expand Up @@ -49,7 +48,7 @@ function CustomChatContentLoggedIn(props: {
networks: NebulaContext["networks"];
}) {
const [userHasSubmittedMessage, setUserHasSubmittedMessage] = useState(false);
const [messages, setMessages] = useState<Array<ChatMessage>>([]);
const [messages, setMessages] = useState<Array<CustomChatMessage>>([]);
// sessionId is initially undefined, will be set to conversationId from API after first response
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
const [chatAbortController, setChatAbortController] = useState<
Expand All @@ -61,13 +60,15 @@ function CustomChatContentLoggedIn(props: {
const connectionStatus = useActiveWalletConnectionStatus();

const handleSendMessage = useCallback(
async (userMessage: NebulaUserMessage) => {
async (userMessage: UserMessage) => {
const abortController = new AbortController();
setUserHasSubmittedMessage(true);
setIsChatStreaming(true);
setEnableAutoScroll(true);

const textMessage = userMessage.content.find((x) => x.type === "text");
const textMessage = userMessage.content.find(
(x: UserMessageContent) => x.type === "text",
);

trackEvent({
category: "siwa",
Expand All @@ -80,7 +81,7 @@ function CustomChatContentLoggedIn(props: {
...prev,
{
type: "user",
content: userMessage.content,
content: userMessage.content as UserMessageContent[],
},
// instant loading indicator feedback to user
{
Expand All @@ -93,7 +94,7 @@ function CustomChatContentLoggedIn(props: {
// deep clone `userMessage` to avoid mutating the original message, its a pretty small object so JSON.parse is fine
const messageToSend = JSON.parse(
JSON.stringify(userMessage),
) as NebulaUserMessage;
) as UserMessage;

try {
setChatAbortController(abortController);
Expand Down Expand Up @@ -149,6 +150,70 @@ function CustomChatContentLoggedIn(props: {
[props.authToken, props.clientId, props.teamId, sessionId, trackEvent],
);

const handleFeedback = useCallback(
async (messageIndex: number, feedback: 1 | -1) => {
if (!sessionId) {
console.error("Cannot submit feedback: missing session ID");
return;
}

// Validate message exists and is of correct type
const message = messages[messageIndex];
if (!message || message.type !== "assistant") {
console.error("Invalid message for feedback:", messageIndex);
return;
}

// Prevent duplicate feedback
if (message.feedback) {
console.warn("Feedback already submitted for this message");
return;
}

try {
trackEvent({
category: "siwa",
action: "submit-feedback",
rating: feedback === 1 ? "good" : "bad",
sessionId,
teamId: props.teamId,
});

const apiUrl = process.env.NEXT_PUBLIC_SIWA_URL;
const response = await fetch(`${apiUrl}/v1/chat/feedback`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${props.authToken}`,
...(props.teamId ? { "x-team-id": props.teamId } : {}),
},
body: JSON.stringify({
conversationId: sessionId,
feedbackRating: feedback,
}),
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

// Update the message with feedback
setMessages((prev) =>
prev.map((msg, index) =>
index === messageIndex && msg.type === "assistant"
? { ...msg, feedback }
: msg,
),
);
} catch (error) {
console.error("Failed to send feedback:", error);
// Optionally show user-facing error notification
// Consider implementing retry logic here
}
Comment on lines +209 to +212
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Provide user-facing feedback for errors.

Errors are only logged to console. Users should be notified when feedback submission fails.

Consider implementing a toast notification system or showing an inline error message to inform users when feedback submission fails. This improves the user experience by providing transparency about the operation status.

🤖 Prompt for AI Agents
In
apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx
around lines 207 to 210, the code only logs errors to the console when feedback
submission fails, without notifying the user. To fix this, add a user-facing
error notification such as a toast message or an inline error display to inform
users about the failure. Integrate this notification within the existing error
handling block to improve user experience by providing clear feedback on the
operation status.

},
[sessionId, props.authToken, props.teamId, trackEvent, messages],
);

const showEmptyState = !userHasSubmittedMessage && messages.length === 0;
return (
<div className="flex grow flex-col overflow-hidden">
Expand All @@ -158,7 +223,7 @@ function CustomChatContentLoggedIn(props: {
examplePrompts={props.examplePrompts}
/>
) : (
<Chats
<CustomChats
messages={messages}
isChatStreaming={isChatStreaming}
authToken={props.authToken}
Expand All @@ -169,6 +234,7 @@ function CustomChatContentLoggedIn(props: {
setEnableAutoScroll={setEnableAutoScroll}
useSmallText
sendMessage={handleSendMessage}
onFeedback={handleFeedback}
/>
)}
<ChatBar
Expand All @@ -192,7 +258,15 @@ function CustomChatContentLoggedIn(props: {
}}
isChatStreaming={isChatStreaming}
prefillMessage={undefined}
sendMessage={handleSendMessage}
sendMessage={(siwaUserMessage) => {
const userMessage: UserMessage = {
type: "user",
content: siwaUserMessage.content
.filter((c) => c.type === "text")
.map((c) => ({ type: "text", text: c.text })),
};
handleSendMessage(userMessage);
}}
className="rounded-none border-x-0 border-b-0"
allowImageUpload={false}
/>
Expand Down Expand Up @@ -237,7 +311,7 @@ function LoggedOutStateChatContent() {
}

function EmptyStateChatPageContent(props: {
sendMessage: (message: NebulaUserMessage) => void;
sendMessage: (message: UserMessage) => void;
examplePrompts: { title: string; message: string }[];
}) {
return (
Expand All @@ -264,7 +338,7 @@ function EmptyStateChatPageContent(props: {
size="sm"
onClick={() =>
props.sendMessage({
role: "user",
type: "user",
content: [
{
type: "text",
Expand Down
Loading
Loading