Skip to content
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
4 changes: 2 additions & 2 deletions src/components/chat-composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export function ChatComposer({
<pre
aria-hidden="true"
data-testid="chat-composer-highlight-layer"
className="pointer-events-none absolute inset-0 h-[128px] min-w-0 whitespace-pre-wrap break-words border border-transparent px-4 py-3 font-sans text-sm leading-5 text-foreground sm:px-5 sm:py-4"
className="pointer-events-none absolute inset-0 h-[128px] min-w-0 whitespace-pre-wrap break-words border border-transparent px-4 py-3 font-sans text-sm font-normal leading-5 text-foreground sm:px-5 sm:py-4"
>
<span
style={{ transform: `translateY(-${textareaScrollTop}px)` }}
Expand All @@ -164,7 +164,7 @@ export function ChatComposer({
ref={textareaRef}
id={chatComposerId}
name={chatComposerId}
className="relative h-[128px] w-full min-w-0 resize-none border-0 bg-transparent px-4 py-3 text-sm leading-5 text-foreground shadow-none placeholder:text-muted-foreground focus-visible:ring-0 sm:px-5 sm:py-4"
className="relative h-[128px] w-full min-w-0 resize-none border-0 bg-transparent px-4 py-3 text-sm font-normal leading-5 text-foreground shadow-none placeholder:text-muted-foreground focus-visible:ring-0 sm:px-5 sm:py-4"
placeholder={
isDisabled
? "Add a ready source to start asking questions."
Expand Down
70 changes: 2 additions & 68 deletions src/components/chat-message-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { type CSSProperties, type ReactElement } from "react";
import { type VirtualItem } from "@tanstack/react-virtual";
import { BarChart3, ImageIcon, MessageCircle } from "lucide-react";
import { ImageIcon, MessageCircle } from "lucide-react";
import ReactMarkdown, {
defaultUrlTransform,
type Components,
Expand All @@ -12,7 +12,6 @@ import remarkGfm from "remark-gfm";
import { ChatDiagramCard } from "@/components/chat-diagram-card";
import { useChatMessageListWorkflow } from "@/components/chat-message-list-workflow";
import { chatPanelModel } from "@/components/chat-panel-model";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Spinner } from "@/components/ui/spinner";
import {
Expand Down Expand Up @@ -83,7 +82,6 @@ const assistantMarkdownComponents: Components = {
export type ChatMessageListProps = {
readonly diagramStatesByMessageId?: Readonly<Record<string, ChatDiagramState>>;
readonly isDisabled?: boolean;
readonly isDiagramActionDisabled?: boolean;
readonly isSending?: boolean;
readonly messages?: readonly ChatMessageView[];
readonly needsLogin?: boolean;
Expand All @@ -93,21 +91,18 @@ export type ChatMessageListProps = {
) => void;
readonly pendingCitationId?: string | null;
readonly pendingStatusText?: string | null;
readonly onCreateDiagram?: (message: ChatMessageView) => void;
readonly sourceTitlesByDocumentId?: Readonly<Record<string, string>>;
};

export function ChatMessageList({
diagramStatesByMessageId = {},
isDisabled = false,
isDiagramActionDisabled = false,
isSending = false,
messages = [],
needsLogin = false,
onCitationClick,
pendingCitationId = null,
pendingStatusText = null,
onCreateDiagram,
sourceTitlesByDocumentId = {},
}: ChatMessageListProps): ReactElement {
const {
Expand Down Expand Up @@ -148,8 +143,6 @@ export function ChatMessageList({
diagramStatesByMessageId[getVirtualMessage(virtualItem)?.id ?? ""]
}
onCitationClick={onCitationClick}
isDiagramActionDisabled={isDiagramActionDisabled}
onCreateDiagram={onCreateDiagram}
pendingCitationId={pendingCitationId}
sourceTitlesByDocumentId={sourceTitlesByDocumentId}
/>
Expand Down Expand Up @@ -219,8 +212,6 @@ function VirtualMessageRow({
message,
measureElement,
onCitationClick,
isDiagramActionDisabled,
onCreateDiagram,
pendingCitationId,
sourceTitlesByDocumentId,
}: {
Expand All @@ -232,8 +223,6 @@ function VirtualMessageRow({
citation: ChatCitationView,
citationId: string,
) => void;
readonly isDiagramActionDisabled: boolean;
readonly onCreateDiagram?: (message: ChatMessageView) => void;
readonly pendingCitationId?: string | null;
readonly sourceTitlesByDocumentId: Readonly<Record<string, string>>;
}): ReactElement | null {
Expand All @@ -256,10 +245,8 @@ function VirtualMessageRow({
>
<MessageBubble
diagramState={diagramState ?? idleDiagramState}
isDiagramActionDisabled={isDiagramActionDisabled}
message={message}
onCitationClick={onCitationClick}
onCreateDiagram={onCreateDiagram}
pendingCitationId={pendingCitationId}
sourceTitlesByDocumentId={sourceTitlesByDocumentId}
/>
Expand Down Expand Up @@ -295,21 +282,17 @@ function EmptyChat({

function MessageBubble({
diagramState,
isDiagramActionDisabled,
message,
onCitationClick,
onCreateDiagram,
pendingCitationId,
sourceTitlesByDocumentId,
}: {
readonly diagramState: ChatDiagramState;
readonly isDiagramActionDisabled: boolean;
readonly message: ChatMessageView;
readonly onCitationClick?: (
citation: ChatCitationView,
citationId: string,
) => void;
readonly onCreateDiagram?: (message: ChatMessageView) => void;
readonly pendingCitationId?: string | null;
readonly sourceTitlesByDocumentId: Readonly<Record<string, string>>;
}): ReactElement {
Expand Down Expand Up @@ -359,12 +342,6 @@ function MessageBubble({
</div>
)}
<AssistantDiagram state={diagramState} />
<AssistantDiagramAction
isDisabled={isDiagramActionDisabled}
message={message}
onCreateDiagram={onCreateDiagram}
state={diagramState}
/>
{displayImageCitations.length > 0 && (
<div className="mt-3 border-t border-border/70 pt-2.5">
<p className="mb-2 flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-muted-foreground">
Expand Down Expand Up @@ -403,49 +380,6 @@ function MessageBubble({
);
}

function AssistantDiagramAction({
isDisabled,
message,
onCreateDiagram,
state,
}: {
readonly isDisabled: boolean;
readonly message: ChatMessageView;
readonly onCreateDiagram?: (message: ChatMessageView) => void;
readonly state: ChatDiagramState;
}): ReactElement | null {
if (
!onCreateDiagram ||
state.status === "loading" ||
state.status === "ready" ||
message.content.trim().length === 0
) {
return null;
}

const label =
state.status === "empty" || state.status === "error"
? "Try diagram again"
: "Create diagram";

return (
<div className="mt-3 border-t border-border/70 pt-2.5">
<Button
type="button"
variant="outline"
size="sm"
aria-label={`${label} for this answer`}
disabled={isDisabled}
className="h-8 gap-1.5 rounded-md px-2.5 text-xs font-semibold"
onClick={() => onCreateDiagram(message)}
>
<BarChart3 className="size-3.5" />
{label}
</Button>
</div>
);
}

function AssistantDiagram({
state,
}: {
Expand Down Expand Up @@ -707,7 +641,7 @@ function CitationChip({
disabled={!onCitationClick || isPending}
onClick={() => onCitationClick?.(citation, citationId)}
aria-busy={isPending}
className="inline-flex h-8 max-w-[250px] cursor-pointer items-center rounded-md border border-transparent bg-[#5c606b] px-3 text-left font-mono text-xs font-semibold leading-none text-[#cfd3dc] shadow-none transition-[background-color,border-color,color,box-shadow,transform] hover:border-[#8f96a8] hover:bg-[#4f535e] hover:text-white hover:shadow-[0_0_0_2px_rgba(143,150,168,0.22)] active:translate-y-px active:bg-[#454955] focus:outline-none focus:ring-4 focus:ring-ring/15 focus:ring-offset-2 focus:ring-offset-background disabled:cursor-wait disabled:opacity-75 disabled:hover:border-transparent disabled:hover:bg-[#5c606b] disabled:hover:text-[#cfd3dc] disabled:hover:shadow-none"
className="inline-flex h-8 max-w-[250px] cursor-pointer items-center rounded-md border border-primary/20 bg-primary/10 px-3 text-left font-mono text-xs font-semibold leading-none text-primary shadow-[0_1px_0_rgba(15,23,42,0.06)] transition-[background-color,border-color,color,box-shadow,transform] hover:border-primary/35 hover:bg-primary/15 hover:text-primary hover:shadow-[0_0_0_2px_rgba(37,99,235,0.12)] active:translate-y-px active:bg-primary/20 focus:outline-none focus:ring-4 focus:ring-ring/15 focus:ring-offset-2 focus:ring-offset-background disabled:cursor-wait disabled:opacity-75 disabled:hover:border-primary/20 disabled:hover:bg-primary/10 disabled:hover:text-primary disabled:hover:shadow-[0_1px_0_rgba(15,23,42,0.06)] dark:border-transparent dark:bg-[#5c606b] dark:text-[#cfd3dc] dark:shadow-none dark:hover:border-[#8f96a8] dark:hover:bg-[#4f535e] dark:hover:text-white dark:hover:shadow-[0_0_0_2px_rgba(143,150,168,0.22)] dark:active:bg-[#454955] dark:disabled:hover:border-transparent dark:disabled:hover:bg-[#5c606b] dark:disabled:hover:text-[#cfd3dc] dark:disabled:hover:shadow-none"
aria-label={`Open source ${label}`}
>
<span className="min-w-0 truncate">{label}</span>
Expand Down
76 changes: 23 additions & 53 deletions src/components/chat-panel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ describe("ChatPanel", () => {
expect(
screen.queryByRole("button", { name: "Create diagram" }),
).toBeNull();
expect(
screen.queryByRole("button", {
name: "Create diagram for this answer",
}),
).toBeNull();

await user.click(screen.getByRole("button", { name: "Create" }));
await user.click(
Expand All @@ -133,17 +138,13 @@ describe("ChatPanel", () => {
).toBeTruthy();
});

it("creates a diagram directly from an assistant answer", async () => {
it("shows a friendly no-diagram state for non-chartable answers", async () => {
const user = userEvent.setup();
vi.mocked(workspaceClient.createChatDiagram).mockResolvedValue({
diagram: {
type: "column",
source: "chart-visualization-skills",
title: "Revenue by Segment",
data: [
{ category: "Cloud", value: 42 },
{ category: "Ads", value: 28 },
],
type: "none",
reason:
"No clear chartable data was found. Ask for a table or numeric comparison first.",
},
});

Expand All @@ -153,54 +154,22 @@ describe("ChatPanel", () => {
{
id: "assistant_1",
role: "assistant",
content: "Cloud revenue was 42 and Ads revenue was 28.",
content: "This is a qualitative summary without comparable numbers.",
},
],
}),
);

await user.click(
screen.getByRole("button", {
name: "Create diagram for this answer",
}),
);

expect(workspaceClient.createChatDiagram).toHaveBeenCalledWith({
answer: "Cloud revenue was 42 and Ads revenue was 28.",
});
expect(await screen.findByText("Revenue by Segment")).toBeTruthy();
expect(
screen.queryByRole("button", {
name: "Create diagram for this answer",
}),
).toBeNull();
});

it("shows a friendly no-diagram state for non-chartable answers", async () => {
const user = userEvent.setup();
vi.mocked(workspaceClient.createChatDiagram).mockResolvedValue({
diagram: {
type: "none",
reason:
"No clear chartable data was found. Ask for a table or numeric comparison first.",
},
});

render(
React.createElement(C, {
messages: [
{
id: "assistant_1",
role: "assistant",
content: "This is a qualitative summary without comparable numbers.",
},
],
}),
);

await user.click(screen.getByRole("button", { name: "Create" }));
await user.click(
screen.getByRole("button", {
name: "Create diagram for this answer",
screen.getByRole("menuitem", {
name: "Create diagram from latest answer",
}),
);

Expand All @@ -211,10 +180,10 @@ describe("ChatPanel", () => {
),
).toBeTruthy();
expect(
screen.getByRole("button", {
screen.queryByRole("button", {
name: "Try diagram again for this answer",
}),
).toBeTruthy();
).toBeNull();
});

it("treats slash diagram text as a local command instead of a chat message", async () => {
Expand Down Expand Up @@ -502,14 +471,15 @@ describe("ChatPanel", () => {
expect(citationButton.className).toContain("h-8");
expect(citationButton.className).toContain("max-w-[250px]");
expect(citationButton.className).toContain("font-mono");
expect(citationButton.className).toContain("text-[#cfd3dc]");
expect(citationButton.className).toContain("bg-[#5c606b]");
expect(citationButton.className).toContain("border-transparent");
expect(citationButton.className).toContain("hover:border-[#8f96a8]");
expect(citationButton.className).toContain("hover:bg-[#4f535e]");
expect(citationButton.className).toContain("hover:text-white");
expect(citationButton.className).toContain("text-primary");
expect(citationButton.className).toContain("bg-primary/10");
expect(citationButton.className).toContain("border-primary/20");
expect(citationButton.className).toContain("hover:border-primary/35");
expect(citationButton.className).toContain("hover:bg-primary/15");
expect(citationButton.className).toContain("dark:bg-[#5c606b]");
expect(citationButton.className).toContain("dark:text-[#cfd3dc]");
expect(citationButton.className).toContain(
"hover:shadow-[0_0_0_2px_rgba(143,150,168,0.22)]",
"hover:shadow-[0_0_0_2px_rgba(37,99,235,0.12)]",
);
expect(citationButton.className).not.toContain("border-border");
expect(citationButton.className).not.toContain("bg-background/80");
Expand Down
2 changes: 0 additions & 2 deletions src/components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,12 +273,10 @@ export function ChatPanel({
<ChatMessageList
diagramStatesByMessageId={diagramStatesByMessageId}
isDisabled={isDisabled}
isDiagramActionDisabled={isDisabled || isSending}
isSending={isSending}
messages={messages}
needsLogin={Boolean(onLoginClick)}
onCitationClick={onCitationClick}
onCreateDiagram={handleCreateDiagramCommand}
pendingCitationId={pendingCitationId}
pendingStatusText={pendingStatusText}
sourceTitlesByDocumentId={sourceTitlesByDocumentId}
Expand Down
Loading