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 ( +
+

+ {props.text} +

+
+ ); +} 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],