Skip to content

Commit d4aed43

Browse files
committed
feat: add SIWA feedback functionality to dashboard chat (#7272)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on refactoring the `CustomChatContent` and `CustomChats` components to improve type safety and functionality in handling user messages and feedback. It introduces new types and methods for managing chat messages and feedback submission. ### Detailed summary - Replaced `NebulaUserMessage` with `UserMessage` and `CustomChatMessage`. - Added `handleFeedback` function for submitting feedback on messages. - Updated `CustomChats` component to handle new message types and feedback. - Modified `sendMessage` logic in `ChatBar` to use the new message structure. - Introduced `RenderMessage` and `StyledMarkdownRenderer` for rendering chat messages. - Added feedback buttons for assistant messages in `RenderMessage`. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a new chat interface supporting multiple message types with distinct visual styles. - Enabled users to submit feedback (thumbs up/down) on assistant messages. - Enhanced chat experience with auto-scrolling that respects user interactions. - **Bug Fixes** - Prevented duplicate feedback submissions on assistant messages. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent f89c15c commit d4aed43

File tree

2 files changed

+431
-12
lines changed

2 files changed

+431
-12
lines changed

apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@ import { useCallback, useState } from "react";
88
import type { ThirdwebClient } from "thirdweb";
99
import { useActiveWalletConnectionStatus } from "thirdweb/react";
1010
import type { NebulaContext } from "../../api/chat";
11-
import type { NebulaUserMessage } from "../../api/types";
1211
import type { ExamplePrompt } from "../../data/examplePrompts";
1312
import { NebulaIcon } from "../../icons/NebulaIcon";
1413
import { ChatBar } from "../ChatBar";
15-
import { Chats } from "../Chats";
16-
import type { ChatMessage } from "../Chats";
14+
import { type CustomChatMessage, CustomChats } from "./CustomChats";
15+
import type { UserMessage, UserMessageContent } from "./CustomChats";
1716

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

6362
const handleSendMessage = useCallback(
64-
async (userMessage: NebulaUserMessage) => {
63+
async (userMessage: UserMessage) => {
6564
const abortController = new AbortController();
6665
setUserHasSubmittedMessage(true);
6766
setIsChatStreaming(true);
6867
setEnableAutoScroll(true);
6968

70-
const textMessage = userMessage.content.find((x) => x.type === "text");
69+
const textMessage = userMessage.content.find(
70+
(x: UserMessageContent) => x.type === "text",
71+
);
7172

7273
trackEvent({
7374
category: "siwa",
@@ -80,7 +81,7 @@ function CustomChatContentLoggedIn(props: {
8081
...prev,
8182
{
8283
type: "user",
83-
content: userMessage.content,
84+
content: userMessage.content as UserMessageContent[],
8485
},
8586
// instant loading indicator feedback to user
8687
{
@@ -93,7 +94,7 @@ function CustomChatContentLoggedIn(props: {
9394
// deep clone `userMessage` to avoid mutating the original message, its a pretty small object so JSON.parse is fine
9495
const messageToSend = JSON.parse(
9596
JSON.stringify(userMessage),
96-
) as NebulaUserMessage;
97+
) as UserMessage;
9798

9899
try {
99100
setChatAbortController(abortController);
@@ -149,6 +150,70 @@ function CustomChatContentLoggedIn(props: {
149150
[props.authToken, props.clientId, props.teamId, sessionId, trackEvent],
150151
);
151152

153+
const handleFeedback = useCallback(
154+
async (messageIndex: number, feedback: 1 | -1) => {
155+
if (!sessionId) {
156+
console.error("Cannot submit feedback: missing session ID");
157+
return;
158+
}
159+
160+
// Validate message exists and is of correct type
161+
const message = messages[messageIndex];
162+
if (!message || message.type !== "assistant") {
163+
console.error("Invalid message for feedback:", messageIndex);
164+
return;
165+
}
166+
167+
// Prevent duplicate feedback
168+
if (message.feedback) {
169+
console.warn("Feedback already submitted for this message");
170+
return;
171+
}
172+
173+
try {
174+
trackEvent({
175+
category: "siwa",
176+
action: "submit-feedback",
177+
rating: feedback === 1 ? "good" : "bad",
178+
sessionId,
179+
teamId: props.teamId,
180+
});
181+
182+
const apiUrl = process.env.NEXT_PUBLIC_SIWA_URL;
183+
const response = await fetch(`${apiUrl}/v1/chat/feedback`, {
184+
method: "POST",
185+
headers: {
186+
"Content-Type": "application/json",
187+
Authorization: `Bearer ${props.authToken}`,
188+
...(props.teamId ? { "x-team-id": props.teamId } : {}),
189+
},
190+
body: JSON.stringify({
191+
conversationId: sessionId,
192+
feedbackRating: feedback,
193+
}),
194+
});
195+
196+
if (!response.ok) {
197+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
198+
}
199+
200+
// Update the message with feedback
201+
setMessages((prev) =>
202+
prev.map((msg, index) =>
203+
index === messageIndex && msg.type === "assistant"
204+
? { ...msg, feedback }
205+
: msg,
206+
),
207+
);
208+
} catch (error) {
209+
console.error("Failed to send feedback:", error);
210+
// Optionally show user-facing error notification
211+
// Consider implementing retry logic here
212+
}
213+
},
214+
[sessionId, props.authToken, props.teamId, trackEvent, messages],
215+
);
216+
152217
const showEmptyState = !userHasSubmittedMessage && messages.length === 0;
153218
return (
154219
<div className="flex grow flex-col overflow-hidden">
@@ -158,7 +223,7 @@ function CustomChatContentLoggedIn(props: {
158223
examplePrompts={props.examplePrompts}
159224
/>
160225
) : (
161-
<Chats
226+
<CustomChats
162227
messages={messages}
163228
isChatStreaming={isChatStreaming}
164229
authToken={props.authToken}
@@ -169,6 +234,7 @@ function CustomChatContentLoggedIn(props: {
169234
setEnableAutoScroll={setEnableAutoScroll}
170235
useSmallText
171236
sendMessage={handleSendMessage}
237+
onFeedback={handleFeedback}
172238
/>
173239
)}
174240
<ChatBar
@@ -192,7 +258,15 @@ function CustomChatContentLoggedIn(props: {
192258
}}
193259
isChatStreaming={isChatStreaming}
194260
prefillMessage={undefined}
195-
sendMessage={handleSendMessage}
261+
sendMessage={(siwaUserMessage) => {
262+
const userMessage: UserMessage = {
263+
type: "user",
264+
content: siwaUserMessage.content
265+
.filter((c) => c.type === "text")
266+
.map((c) => ({ type: "text", text: c.text })),
267+
};
268+
handleSendMessage(userMessage);
269+
}}
196270
className="rounded-none border-x-0 border-b-0"
197271
allowImageUpload={false}
198272
/>
@@ -237,7 +311,7 @@ function LoggedOutStateChatContent() {
237311
}
238312

239313
function EmptyStateChatPageContent(props: {
240-
sendMessage: (message: NebulaUserMessage) => void;
314+
sendMessage: (message: UserMessage) => void;
241315
examplePrompts: { title: string; message: string }[];
242316
}) {
243317
return (
@@ -264,7 +338,7 @@ function EmptyStateChatPageContent(props: {
264338
size="sm"
265339
onClick={() =>
266340
props.sendMessage({
267-
role: "user",
341+
type: "user",
268342
content: [
269343
{
270344
type: "text",

0 commit comments

Comments
 (0)