-
Notifications
You must be signed in to change notification settings - Fork 969
Nice Chat UI #1265
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
Nice Chat UI #1265
Changes from all commits
6942abd
eefb37a
d63b577
273c38d
1c73b87
74b563c
d5d1a09
a4b6369
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| import { | ||
| Checkpoint, | ||
| CheckpointIcon, | ||
| CheckpointTrigger, | ||
| } from "@superset/ui/ai-elements/checkpoint"; | ||
| import { | ||
| Conversation, | ||
| ConversationContent, | ||
| ConversationEmptyState, | ||
| ConversationScrollButton, | ||
| } from "@superset/ui/ai-elements/conversation"; | ||
| import { Message, MessageContent } from "@superset/ui/ai-elements/message"; | ||
| import { | ||
| PromptInput, | ||
| PromptInputButton, | ||
| PromptInputFooter, | ||
| PromptInputSubmit, | ||
| PromptInputTextarea, | ||
| PromptInputTools, | ||
| } from "@superset/ui/ai-elements/prompt-input"; | ||
| import { Shimmer } from "@superset/ui/ai-elements/shimmer"; | ||
| import { Suggestion, Suggestions } from "@superset/ui/ai-elements/suggestion"; | ||
| import { Fragment, useCallback, useState } from "react"; | ||
| import { | ||
| HiMiniAtSymbol, | ||
| HiMiniChatBubbleLeftRight, | ||
| HiMiniPaperClip, | ||
| } from "react-icons/hi2"; | ||
| import { ChatMessageItem } from "./components/ChatMessageItem"; | ||
| import { ContextIndicator } from "./components/ContextIndicator"; | ||
| import { ModelPicker } from "./components/ModelPicker"; | ||
| import { MOCK_MESSAGES, MODELS, SUGGESTIONS } from "./constants"; | ||
| import type { ChatMessage, ModelOption } from "./types"; | ||
|
|
||
| export function ChatInterface() { | ||
| const [messages, setMessages] = useState<ChatMessage[]>(MOCK_MESSAGES); | ||
| const [isLoading, setIsLoading] = useState(false); | ||
| const [selectedModel, setSelectedModel] = useState<ModelOption>(MODELS[1]); | ||
| const [modelSelectorOpen, setModelSelectorOpen] = useState(false); | ||
|
|
||
| const handleSend = useCallback((message: { text: string }) => { | ||
| if (!message.text.trim()) return; | ||
|
|
||
| const userMessage: ChatMessage = { | ||
| id: `msg-${Date.now()}`, | ||
| role: "user", | ||
| content: message.text, | ||
| }; | ||
|
|
||
| setMessages((prev) => [...prev, userMessage]); | ||
| setIsLoading(true); | ||
|
|
||
| setTimeout(() => { | ||
| const assistantMessage: ChatMessage = { | ||
| id: `msg-${Date.now()}-reply`, | ||
| role: "assistant", | ||
| content: | ||
| "This is a mock response. The chat backend is not connected yet.", | ||
| }; | ||
| setMessages((prev) => [...prev, assistantMessage]); | ||
| setIsLoading(false); | ||
| }, 1000); | ||
| }, []); | ||
|
|
||
| const handleSuggestion = useCallback( | ||
| (suggestion: string) => { | ||
| handleSend({ text: suggestion }); | ||
| }, | ||
| [handleSend], | ||
| ); | ||
|
|
||
| return ( | ||
| <div className="flex h-full flex-col bg-background"> | ||
| <Conversation className="flex-1"> | ||
| <ConversationContent className="mx-auto w-full max-w-3xl gap-6 px-4 py-6"> | ||
| {messages.length === 0 ? ( | ||
| <> | ||
| <ConversationEmptyState | ||
| title="Start a conversation" | ||
| description="Ask anything to get started" | ||
| icon={<HiMiniChatBubbleLeftRight className="size-8" />} | ||
| /> | ||
| <Suggestions className="justify-center"> | ||
| {SUGGESTIONS.map((s) => ( | ||
| <Suggestion | ||
| key={s} | ||
| suggestion={s} | ||
| onClick={handleSuggestion} | ||
| /> | ||
| ))} | ||
| </Suggestions> | ||
| </> | ||
| ) : ( | ||
| messages.map((msg) => ( | ||
| <Fragment key={msg.id}> | ||
| {msg.checkpoint && ( | ||
| <Checkpoint> | ||
| <CheckpointIcon /> | ||
| <CheckpointTrigger tooltip="Restore to this point"> | ||
| {msg.checkpoint} | ||
| </CheckpointTrigger> | ||
| </Checkpoint> | ||
| )} | ||
| <ChatMessageItem message={msg} /> | ||
| </Fragment> | ||
| )) | ||
| )} | ||
| {isLoading && ( | ||
| <Message from="assistant"> | ||
| <MessageContent> | ||
| <Shimmer className="text-sm" duration={1.5}> | ||
| Thinking... | ||
| </Shimmer> | ||
| </MessageContent> | ||
| </Message> | ||
| )} | ||
| </ConversationContent> | ||
| <ConversationScrollButton /> | ||
| </Conversation> | ||
|
|
||
| <div className="border-t bg-background px-4 py-3"> | ||
| <div className="mx-auto w-full max-w-3xl"> | ||
| {messages.length > 0 && ( | ||
| <Suggestions className="mb-3"> | ||
| {SUGGESTIONS.map((s) => ( | ||
| <Suggestion key={s} suggestion={s} onClick={handleSuggestion} /> | ||
| ))} | ||
| </Suggestions> | ||
| )} | ||
| <PromptInput onSubmit={handleSend}> | ||
| <PromptInputTextarea placeholder="Ask anything..." /> | ||
| <PromptInputFooter> | ||
| <PromptInputTools> | ||
| <PromptInputButton> | ||
| <HiMiniPaperClip className="size-4" /> | ||
| </PromptInputButton> | ||
| <PromptInputButton> | ||
| <HiMiniAtSymbol className="size-4" /> | ||
| </PromptInputButton> | ||
| <ModelPicker | ||
| selectedModel={selectedModel} | ||
| onSelectModel={setSelectedModel} | ||
| open={modelSelectorOpen} | ||
| onOpenChange={setModelSelectorOpen} | ||
| /> | ||
| </PromptInputTools> | ||
| <div className="flex items-center gap-1"> | ||
| <ContextIndicator /> | ||
| <PromptInputSubmit | ||
| status={isLoading ? "streaming" : undefined} | ||
| /> | ||
| </div> | ||
| </PromptInputFooter> | ||
| </PromptInput> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import { | ||
| CodeBlock, | ||
| CodeBlockCopyButton, | ||
| } from "@superset/ui/ai-elements/code-block"; | ||
| import { | ||
| Message, | ||
| MessageAction, | ||
| MessageActions, | ||
| MessageContent, | ||
| MessageResponse, | ||
| } from "@superset/ui/ai-elements/message"; | ||
| import { | ||
| Reasoning, | ||
| ReasoningContent, | ||
| ReasoningTrigger, | ||
| } from "@superset/ui/ai-elements/reasoning"; | ||
| import { | ||
| Task, | ||
| TaskContent, | ||
| TaskItem, | ||
| TaskItemFile, | ||
| TaskTrigger, | ||
| } from "@superset/ui/ai-elements/task"; | ||
| import { HiMiniArrowPath, HiMiniClipboard } from "react-icons/hi2"; | ||
| import type { ChatMessage } from "../../types"; | ||
| import { PlanBlock } from "../PlanBlock"; | ||
| import { ToolCallBlock } from "../ToolCallBlock"; | ||
|
|
||
| export function ChatMessageItem({ message }: { message: ChatMessage }) { | ||
| return ( | ||
| <Message from={message.role}> | ||
| <MessageContent> | ||
| {message.reasoning && ( | ||
| <Reasoning> | ||
| <ReasoningTrigger /> | ||
| <ReasoningContent>{message.reasoning}</ReasoningContent> | ||
| </Reasoning> | ||
| )} | ||
|
|
||
| {message.plan && <PlanBlock plan={message.plan} />} | ||
|
|
||
| {message.content && ( | ||
| <MessageResponse>{message.content}</MessageResponse> | ||
| )} | ||
|
|
||
| {message.tasks?.map((task) => ( | ||
| <Task key={task.title}> | ||
| <TaskTrigger title={task.title} /> | ||
| <TaskContent> | ||
| {task.files.map((file) => ( | ||
| <TaskItem key={file}> | ||
| <TaskItemFile>{file}</TaskItemFile> | ||
| </TaskItem> | ||
| ))} | ||
| </TaskContent> | ||
| </Task> | ||
| ))} | ||
|
|
||
| {message.codeBlocks?.map((block) => ( | ||
| <CodeBlock | ||
| key={block.code} | ||
| code={block.code} | ||
| language={block.language as "typescript"} | ||
| > | ||
| <CodeBlockCopyButton /> | ||
| </CodeBlock> | ||
| ))} | ||
|
|
||
| {message.toolCalls?.map((tc) => ( | ||
| <ToolCallBlock key={tc.id} toolCall={tc} /> | ||
| ))} | ||
| </MessageContent> | ||
|
|
||
| {message.role === "assistant" && message.content && ( | ||
| <MessageActions> | ||
| <MessageAction tooltip="Copy"> | ||
| <HiMiniClipboard className="size-3.5" /> | ||
| </MessageAction> | ||
| <MessageAction tooltip="Retry"> | ||
| <HiMiniArrowPath className="size-3.5" /> | ||
| </MessageAction> | ||
| </MessageActions> | ||
| )} | ||
|
Comment on lines
+74
to
+83
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Copy and Retry actions have no Both 🤖 Prompt for AI Agents |
||
| </Message> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { ChatMessageItem } from "./ChatMessageItem"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { | ||
| Context, | ||
| ContextCacheUsage, | ||
| ContextContent, | ||
| ContextContentBody, | ||
| ContextContentFooter, | ||
| ContextContentHeader, | ||
| ContextInputUsage, | ||
| ContextOutputUsage, | ||
| ContextReasoningUsage, | ||
| ContextTrigger, | ||
| } from "@superset/ui/ai-elements/context"; | ||
|
|
||
| const MOCK_CONTEXT = { | ||
| usedTokens: 84_200, | ||
| maxTokens: 200_000, | ||
| modelId: "claude-sonnet-4-5-20250929", | ||
| usage: { | ||
| inputTokens: 42_100, | ||
| outputTokens: 18_300, | ||
| totalTokens: 84_200, | ||
| reasoningTokens: 12_800, | ||
| cachedInputTokens: 11_000, | ||
| }, | ||
| } as const; | ||
|
|
||
| export function ContextIndicator() { | ||
| return ( | ||
| <Context | ||
| maxTokens={MOCK_CONTEXT.maxTokens} | ||
| modelId={MOCK_CONTEXT.modelId} | ||
| usage={MOCK_CONTEXT.usage} | ||
| usedTokens={MOCK_CONTEXT.usedTokens} | ||
| > | ||
| <ContextTrigger /> | ||
| <ContextContent> | ||
| <ContextContentHeader /> | ||
| <ContextContentBody> | ||
| <div className="space-y-1"> | ||
| <ContextInputUsage /> | ||
| <ContextOutputUsage /> | ||
| <ContextReasoningUsage /> | ||
| <ContextCacheUsage /> | ||
| </div> | ||
| </ContextContentBody> | ||
| <ContextContentFooter /> | ||
| </ContextContent> | ||
| </Context> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { ContextIndicator } from "./ContextIndicator"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
key={task.title}andkey={file}may collide if titles or filenames repeat.task.title(Line 47) andfile(Line 51) are user-supplied strings that aren't guaranteed unique. Duplicate keys cause React reconciliation bugs (silent dropped renders). Consider using the array index or a composite key if no stable id is available.🤖 Prompt for AI Agents