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
43 changes: 42 additions & 1 deletion src/components/chat-composer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,51 @@ describe("ChatComposer", () => {
await user.type(input, " Summarize this document ");
await user.click(screen.getByRole("button", { name: "Send message" }));

expect(onSend).toHaveBeenCalledWith("Summarize this document");
expect(onSend).toHaveBeenCalledWith("Summarize this document", {
useAgentic: true,
});
expect(input.value).toBe("");
});

it("defaults to deep search enabled and explains the toggle", async () => {
const user = userEvent.setup();

render(React.createElement(ChatComposer));

const toggle = screen.getByRole("checkbox", {
name: "Deep search",
});
expect(toggle.getAttribute("aria-checked")).toBe("true");

await user.hover(toggle);

const tooltip = await screen.findByRole("tooltip");
expect(tooltip.textContent).toContain(
"Deep search plans document selection and navigation",
);
});

it("sends useAgentic false after toggling deep search off", async () => {
const user = userEvent.setup();
const onSend = vi.fn();

render(React.createElement(ChatComposer, { onSend }));

const toggle = screen.getByRole("checkbox", {
name: "Deep search",
});
await user.click(toggle);
expect(toggle.getAttribute("aria-checked")).toBe("false");

const input = getComposerTextArea();
await user.type(input, "Quick summary");
await user.click(screen.getByRole("button", { name: "Send message" }));

expect(onSend).toHaveBeenCalledWith("Quick summary", {
useAgentic: false,
});
});

it("caps long prompts and resets the composer after sending", async () => {
const user = userEvent.setup();
const onSend = vi.fn();
Expand Down
79 changes: 61 additions & 18 deletions src/components/chat-composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { BarChart3, FileText, Plus, Send } from "lucide-react";

import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
DropdownMenu,
DropdownMenuContent,
Expand All @@ -22,6 +23,12 @@ import {
} from "@/components/ui/dropdown-menu";
import { Spinner } from "@/components/ui/spinner";
import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { chatPromptTemplates } from "@/domains/chat/prompt-templates";
const chatComposerName = "chat-composer";
const chatComposerTextAreaMinHeight = 128;
Expand All @@ -33,14 +40,18 @@ type TextRange = {
readonly end: number;
};

export type ChatSendOptions = {
readonly useAgentic: boolean;
};

export type ChatComposerProps = {
readonly canCreateDiagram?: boolean;
readonly isDisabled?: boolean;
readonly isCreatingDiagram?: boolean;
readonly isSending?: boolean;
readonly onCreateDiagram?: () => void;
readonly onLoginClick?: () => void;
readonly onSend?: (text: string) => void;
readonly onSend?: (text: string, options: ChatSendOptions) => void;
};

export function ChatComposer({
Expand All @@ -53,6 +64,7 @@ export function ChatComposer({
onSend,
}: ChatComposerProps): ReactElement {
const [input, setInput] = useState("");
const [useAgentic, setUseAgentic] = useState(true);
const composerInputId = useId();
const pendingTemplatePromptRef = useRef<string | null>(null);
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
Expand All @@ -79,7 +91,7 @@ export function ChatComposer({

function handleSend(): void {
if (!canSend) return;
onSend?.(trimmedInput);
onSend?.(trimmedInput, { useAgentic });
setInput("");
}

Expand Down Expand Up @@ -177,22 +189,53 @@ export function ChatComposer({
onCreateDiagram={onCreateDiagram}
onTemplateSelect={handleTemplateSelect}
/>
<Button
type="button"
variant="default"
size="sm"
className="ml-auto h-12 min-w-28 gap-1.5 rounded-lg px-6"
disabled={!canSend}
onClick={handleSend}
aria-label="Send message"
>
{isSending ? (
<Spinner className="size-4" />
) : (
<Send className="size-4" />
)}
<span>{isSending ? "Sending" : "Send"}</span>
</Button>
<div className="ml-auto flex items-center gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<label
className={`flex cursor-pointer items-center gap-1.5 text-xs font-semibold transition-colors ${
useAgentic ? "text-foreground" : "text-muted-foreground"
} ${
isDisabled || isSending
? "cursor-not-allowed opacity-50"
: "hover:text-foreground"
}`}
>
<Checkbox
checked={useAgentic}
disabled={isDisabled || isSending}
aria-label="Deep search"
onCheckedChange={(checked) =>
setUseAgentic(checked === true)
}
/>
Deep search
</label>
</TooltipTrigger>
<TooltipContent className="max-w-64">
Deep search plans document selection and navigation for
more thorough answers. Turn off for faster classic search.
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Button
type="button"
variant="default"
size="sm"
className="h-12 min-w-28 gap-1.5 rounded-lg px-6"
disabled={!canSend}
onClick={handleSend}
aria-label="Send message"
>
{isSending ? (
<Spinner className="size-4" />
) : (
<Send className="size-4" />
)}
<span>{isSending ? "Sending" : "Send"}</span>
</Button>
</div>
</div>
</>
)}
Expand Down
4 changes: 3 additions & 1 deletion src/components/chat-panel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,9 @@ describe("ChatPanel", () => {
);
await user.click(screen.getByRole("button", { name: "Send message" }));

expect(onSend).toHaveBeenCalledWith("Summarize revenue");
expect(onSend).toHaveBeenCalledWith("Summarize revenue", {
useAgentic: true,
});
expect(
analyticsMocks.trackNotebookAssistantQuestionSubmitted,
).toHaveBeenCalledWith({
Expand Down
14 changes: 10 additions & 4 deletions src/components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import {
type ReactElement,
} from "react";
import { History, Plus } from "lucide-react";
import { ChatComposer } from "@/components/chat-composer";
import {
ChatComposer,
type ChatSendOptions,
} from "@/components/chat-composer";
import { ChatHistorySheet } from "@/components/chat-history-sheet";
import {
ChatMessageList,
Expand Down Expand Up @@ -47,7 +50,7 @@ export type ChatPanelProps = {
messages: ChatMessageView[];
threads: ChatThreadView[];
activeThreadId?: string | null;
onSend?: (text: string) => void;
onSend?: (text: string, options: ChatSendOptions) => void;
onNewChat?: () => void;
onThreadSelect?: (threadId: string) => void;
onThreadArchive?: (threadId: string) => void;
Expand Down Expand Up @@ -152,7 +155,10 @@ export function ChatPanel({
}
}

function handleComposerSend(text: string): void {
function handleComposerSend(
text: string,
options: ChatSendOptions,
): void {
if (isCreateDiagramCommand(text)) {
void handleCreateDiagramCommand();
return;
Expand All @@ -165,7 +171,7 @@ export function ChatPanel({
sourceCountSnapshot: sourceCount,
messageLength: text.length,
});
onSend?.(text);
onSend?.(text, options);
}

return (
Expand Down
13 changes: 10 additions & 3 deletions src/components/workspace-chat-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,15 @@ describe("useWorkspaceChatWorkflow", () => {
})

await act(async () => {
await result.current.handleChatSend("Summarize it")
await result.current.handleChatSend("Summarize it", {
useAgentic: true,
})
})

expect(mocks.sendChatMessage).toHaveBeenCalledWith({
message: "Summarize it",
threadId: undefined,
useAgentic: true,
excludedSourceIds: ["source_excluded"],
})
await waitFor(() => {
Expand Down Expand Up @@ -127,7 +130,9 @@ describe("useWorkspaceChatWorkflow", () => {
})

await act(async () => {
await result.current.handleChatSend("What changed in Q4?")
await result.current.handleChatSend("What changed in Q4?", {
useAgentic: true,
})
})

expect(mocks.materializeDemoSources).toHaveBeenCalledWith({
Expand Down Expand Up @@ -172,7 +177,9 @@ describe("useWorkspaceChatWorkflow", () => {
})

await act(async () => {
await result.current.handleChatSend("Summarize it")
await result.current.handleChatSend("Summarize it", {
useAgentic: true,
})
})

expect(mocks.materializeDemoSources).not.toHaveBeenCalled()
Expand Down
12 changes: 10 additions & 2 deletions src/components/workspace-chat-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
type AnalyticsContext,
} from "@/lib/posthog"
import { workspaceClient } from "@/domains/workspace/client"
import type { ChatSendOptions } from "@/components/chat-composer"
import {
workspaceClientCache,
type ChatThreadDetailResponse,
Expand Down Expand Up @@ -43,7 +44,10 @@ type WorkspaceChatWorkflow = {
readonly chat: ReturnType<typeof workspaceChatState.createInitialState>
readonly chatThreads: ChatThreadView[]
readonly handleArchiveChatThread: (threadId: string) => Promise<void>
readonly handleChatSend: (text: string) => Promise<void>
readonly handleChatSend: (
text: string,
options: ChatSendOptions,
) => Promise<void>
readonly handleCreateChatThread: () => Promise<void>
readonly handleRefreshActiveChatThread: () => Promise<void>
readonly handleSelectChatThread: (threadId: string) => void
Expand Down Expand Up @@ -247,7 +251,10 @@ export function useWorkspaceChatWorkflow({
}
}

async function handleChatSend(text: string): Promise<void> {
async function handleChatSend(
text: string,
options: ChatSendOptions,
): Promise<void> {
const sendStart = Date.now()
const selectedSourcesCount = sources.filter(
(source) =>
Expand Down Expand Up @@ -297,6 +304,7 @@ export function useWorkspaceChatWorkflow({
const body = await sendChatMessage({
message: text,
threadId: chat.threadId ?? undefined,
useAgentic: options.useAgentic,
excludedSourceIds: sources
.filter((source) => source.excludedFromQuery)
.map((source) => source.id),
Expand Down
6 changes: 5 additions & 1 deletion src/components/workspace-shell-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "lucide-react"

import { ChatPanel } from "@/components/chat-panel"
import type { ChatSendOptions } from "@/components/chat-composer"
import { ChunksPanel } from "@/components/chunks-panel"
import { MobileTabBar } from "@/components/mobile-tab-bar"
import { OfficialLibraryPanel } from "@/components/official-library-panel"
Expand Down Expand Up @@ -99,7 +100,10 @@ export type WorkspaceShellLayoutProps = {
readonly onArchiveChatThread: (threadId: string) => void | Promise<void>
readonly onArchiveSource: (sourceId: string) => void | Promise<void>
readonly onRetrySource?: (sourceId: string) => void | Promise<void>
readonly onChatSend: (text: string) => void | Promise<void>
readonly onChatSend: (
text: string,
options: ChatSendOptions,
) => void | Promise<void>
readonly onCitationClick: (
citation: ChatCitationView,
citationId: string,
Expand Down
1 change: 1 addition & 0 deletions src/domains/chat/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export type AnswerQuestionInput = {
namespaces?: readonly string[]
sources: readonly Source[]
excludedSourceIds: readonly string[]
useAgentic?: boolean
retrieval: RetrievalClient
knowledge?: Knowledge
remoteDocumentClient?: NotebookKnowhereRemoteDocumentClient
Expand Down
Loading
Loading