diff --git a/apps/dashboard/src/@/components/ui/text-shimmer.tsx b/apps/dashboard/src/@/components/ui/text-shimmer.tsx
new file mode 100644
index 00000000000..7355cb8c18a
--- /dev/null
+++ b/apps/dashboard/src/@/components/ui/text-shimmer.tsx
@@ -0,0 +1,23 @@
+import { cn } from "../../lib/utils";
+
+export function TextShimmer(props: {
+ text: string;
+ className?: string;
+}) {
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx
index da8a79eaaac..e1ab264463e 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx
@@ -213,7 +213,7 @@ export function ChatPageContent(props: {
// instant loading indicator feedback to user
{
type: "presence",
- text: "Thinking...",
+ texts: [],
},
]);
@@ -521,19 +521,8 @@ export async function handleNebulaPrompt(params: {
hasReceivedResponse = true;
setMessages((prev) => {
const lastMessage = prev[prev.length - 1];
- // if last message is presence, overwrite it
- if (lastMessage?.type === "presence") {
- return [
- ...prev.slice(0, -1),
- {
- text: res.data.v,
- type: "assistant",
- request_id: requestIdForMessage,
- },
- ];
- }
- // if last message is from chat, append to it
+ // append to previous assistant message
if (lastMessage?.type === "assistant") {
return [
...prev.slice(0, -1),
@@ -545,7 +534,7 @@ export async function handleNebulaPrompt(params: {
];
}
- // otherwise, add a new message
+ // start a new assistant message
return [
...prev,
{
@@ -560,28 +549,27 @@ export async function handleNebulaPrompt(params: {
if (res.event === "presence") {
setMessages((prev) => {
const lastMessage = prev[prev.length - 1];
- // if last message is presence, overwrite it
+
+ // append to previous presence message
if (lastMessage?.type === "presence") {
return [
...prev.slice(0, -1),
- { text: res.data.data, type: "presence" },
+ {
+ type: "presence",
+ texts: [...lastMessage.texts, res.data.data],
+ },
];
}
- // otherwise, add a new message
- return [...prev, { text: res.data.data, type: "presence" }];
+
+ // start a new presence message
+ return [...prev, { texts: [res.data.data], type: "presence" }];
});
}
if (res.event === "action") {
if (res.type === "sign_transaction") {
hasReceivedResponse = true;
- setMessages((prev) => {
- let prevMessages = prev;
- // if last message is presence, remove it
- if (prevMessages[prevMessages.length - 1]?.type === "presence") {
- prevMessages = prevMessages.slice(0, -1);
- }
-
+ setMessages((prevMessages) => {
return [
...prevMessages,
{
@@ -608,10 +596,7 @@ export async function handleNebulaPrompt(params: {
// show an error message in that case
if (!hasReceivedResponse) {
setMessages((prev) => {
- const newMessages = prev.slice(
- 0,
- prev[prev.length - 1]?.type === "presence" ? -1 : undefined,
- );
+ const newMessages = [...prev];
newMessages.push({
text: "No response received, please try again",
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx
index ad34b3f0e24..2aa478113f2 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx
@@ -1,24 +1,105 @@
import type { Meta, StoryObj } from "@storybook/react";
import { randomLorem } from "stories/stubs";
-import { BadgeContainer, storybookThirdwebClient } from "stories/utils";
+import { storybookThirdwebClient } from "stories/utils";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import { type ChatMessage, Chats } from "./Chats";
const meta = {
title: "Nebula/Chats",
- component: Story,
+ component: Variant,
parameters: {
nextjs: {
appDirectory: true,
},
},
-} satisfies Meta;
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+} satisfies Meta;
export default meta;
type Story = StoryObj;
-export const Variants: Story = {
- args: {},
+export const UserPresenceError: Story = {
+ args: {
+ messages: [
+ {
+ text: randomLorem(10),
+ type: "user",
+ },
+ {
+ texts: [randomLorem(20)],
+ type: "presence",
+ },
+ {
+ text: randomLorem(20),
+ type: "error",
+ },
+ ],
+ },
+};
+
+export const SendTransaction: Story = {
+ args: {
+ messages: [
+ {
+ text: randomLorem(40),
+ type: "assistant",
+ request_id: undefined,
+ },
+ {
+ type: "send_transaction",
+ data: {
+ chainId: 1,
+ to: "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37",
+ data: "0x",
+ value: "0x16345785d8a0000",
+ },
+ },
+ ],
+ },
+};
+
+export const InvalidTxData: Story = {
+ args: {
+ messages: [
+ {
+ text: randomLorem(40),
+ type: "assistant",
+ request_id: undefined,
+ },
+ {
+ type: "send_transaction",
+ data: null,
+ },
+ ],
+ },
+};
+
+export const WithAndWithoutRequestId: Story = {
+ args: {
+ messages: [
+ {
+ text: randomLorem(40),
+ type: "assistant",
+ request_id: "xxxxx",
+ },
+ {
+ text: randomLorem(50),
+ type: "assistant",
+ request_id: undefined,
+ },
+ ],
+ },
};
const markdownExample = `\
@@ -129,134 +210,35 @@ ${randomLorem(20)}
${markdownExample}
`;
-function Story() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- {}}
- enableAutoScroll={false}
- setEnableAutoScroll={() => {}}
- client={storybookThirdwebClient}
- authToken="xxxxx"
- isChatStreaming={false}
- sessionId="xxxxx"
- messages={[
- {
- text: randomLorem(40),
- type: "assistant",
- request_id: "xxxxx",
- },
- {
- text: randomLorem(50),
- type: "assistant",
- request_id: undefined,
- },
- ]}
- />
-
-
-
- {}}
- enableAutoScroll={false}
- setEnableAutoScroll={() => {}}
- client={storybookThirdwebClient}
- authToken="xxxxx"
- isChatStreaming={false}
- sessionId="xxxxx"
- messages={[
- {
- text: responseWithCodeMarkdown,
- type: "assistant",
- request_id: undefined,
- },
- {
- text: responseWithCodeMarkdown,
- type: "user",
- },
- ]}
- />
-
-
-
- );
-}
+export const Markdown: Story = {
+ args: {
+ messages: [
+ {
+ text: responseWithCodeMarkdown,
+ type: "assistant",
+ request_id: undefined,
+ },
+ {
+ text: responseWithCodeMarkdown,
+ type: "user",
+ },
+ ],
+ },
+};
function Variant(props: {
- label: string;
messages: ChatMessage[];
}) {
return (
-
- {}}
- client={storybookThirdwebClient}
- sendMessage={() => {}}
- authToken="xxxxx"
- isChatStreaming={false}
- sessionId="xxxxx"
- messages={props.messages}
- />
-
+ {}}
+ client={storybookThirdwebClient}
+ sendMessage={() => {}}
+ authToken="xxxxx"
+ isChatStreaming={false}
+ sessionId="xxxxx"
+ messages={props.messages}
+ />
);
}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx
index ad88ec3fd35..b31aaa68d7a 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx
@@ -18,6 +18,7 @@ import type { ThirdwebClient } from "thirdweb";
import { submitFeedback } from "../api/feedback";
import { NebulaIcon } from "../icons/NebulaIcon";
import { ExecuteTransactionCard } from "./ExecuteTransactionCard";
+import { Reasoning } from "./Reasoning/Reasoning";
export type NebulaTxData = {
chainId: number;
@@ -29,7 +30,11 @@ export type NebulaTxData = {
export type ChatMessage =
| {
text: string;
- type: "user" | "error" | "presence";
+ type: "user" | "error";
+ }
+ | {
+ texts: string[];
+ type: "presence";
}
| {
// assistant type message loaded from history doesn't have request_id
@@ -140,7 +145,7 @@ export function Chats(props: {
)}
>
{message.type === "presence" && (
-
+
)}
{message.type === "assistant" && (
@@ -176,11 +181,12 @@ export function Chats(props: {
);
}}
/>
- ) : (
-
- {message.text}
-
- )}
+ ) : message.type === "presence" ? (
+
+ ) : null}
{message.type === "assistant" &&
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChatContent.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChatContent.tsx
index 23789d1c2eb..b3ee4cb7418 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChatContent.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChatContent.tsx
@@ -126,7 +126,7 @@ function FloatingChatContentLoggedIn(props: {
// instant loading indicator feedback to user
{
type: "presence",
- text: "Thinking...",
+ texts: [],
},
]);
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/Reasoning/Reasoning.stories.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/Reasoning/Reasoning.stories.tsx
new file mode 100644
index 00000000000..e1014413aa9
--- /dev/null
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/Reasoning/Reasoning.stories.tsx
@@ -0,0 +1,34 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { randomLorem } from "../../../../../stories/stubs";
+import { Reasoning } from "./Reasoning";
+
+const meta = {
+ title: "Nebula/Reasoning",
+ component: Story,
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Pending: Story = {
+ args: {},
+};
+
+function Story() {
+ return (
+
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/Reasoning/Reasoning.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/Reasoning/Reasoning.tsx
new file mode 100644
index 00000000000..504df554f6b
--- /dev/null
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/Reasoning/Reasoning.tsx
@@ -0,0 +1,53 @@
+import { DynamicHeight } from "@/components/ui/DynamicHeight";
+import { Button } from "@/components/ui/button";
+import { TextShimmer } from "@/components/ui/text-shimmer";
+import { cn } from "@/lib/utils";
+import { ChevronDownIcon } from "lucide-react";
+import { useState } from "react";
+
+export function Reasoning(props: {
+ isPending: boolean;
+ texts: string[];
+}) {
+ const [_isOpen, setIsOpen] = useState(false);
+ const isOpen = props.isPending ? true : _isOpen;
+
+ return (
+
+
+
+ {isOpen && props.texts.length > 0 && (
+
+ {props.texts.map((text) => (
+ -
+ {text.trim()}
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/apps/dashboard/src/app/nebula-app/nebula-global.css b/apps/dashboard/src/app/nebula-app/nebula-global.css
index d0a1d7bbba6..345dfcb1578 100644
--- a/apps/dashboard/src/app/nebula-app/nebula-global.css
+++ b/apps/dashboard/src/app/nebula-app/nebula-global.css
@@ -1,3 +1,4 @@
::selection {
- background-color: hsl(var(--nebula-pink-foreground) / 0.25);
+ background-color: hsl(var(--inverted));
+ color: hsl(var(--inverted-foreground));
}
diff --git a/apps/dashboard/tailwind.config.js b/apps/dashboard/tailwind.config.js
index 1356d7ea55b..fdc57832312 100644
--- a/apps/dashboard/tailwind.config.js
+++ b/apps/dashboard/tailwind.config.js
@@ -103,12 +103,17 @@ module.exports = {
"0%,70%,100%": { opacity: "1" },
"20%,50%": { opacity: "0" },
},
+ "text-shimmer": {
+ "0%": { backgroundPosition: "100% 50%" },
+ "100%": { backgroundPosition: "-100% 50%" },
+ },
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
skeleton: "skeleton 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
"caret-blink": "caret-blink 1.25s ease-out infinite",
+ "text-shimmer": "text-shimmer 1.25s linear infinite",
},
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans],