add chat flow for search results
This commit is contained in:
@@ -8,6 +8,7 @@ import { ChatMessagesPanel } from "@/components/chat/chat-messages-panel";
|
||||
import { SearchResultsPanel } from "@/components/search/search-results-panel";
|
||||
import {
|
||||
createChat,
|
||||
createChatFromSearch,
|
||||
createSearch,
|
||||
deleteChat,
|
||||
deleteSearch,
|
||||
@@ -164,6 +165,10 @@ function isToolCallLogMessage(message: Message) {
|
||||
return asToolLogMetadata(message.metadata) !== null;
|
||||
}
|
||||
|
||||
function isDisplayableMessage(message: Message) {
|
||||
return message.role !== "system";
|
||||
}
|
||||
|
||||
function buildOptimisticToolMessage(event: ToolCallEvent): Message {
|
||||
return {
|
||||
id: `temp-tool-${event.toolCallId}`,
|
||||
@@ -427,6 +432,7 @@ export default function App() {
|
||||
const [isLoadingCollections, setIsLoadingCollections] = useState(false);
|
||||
const [isLoadingSelection, setIsLoadingSelection] = useState(false);
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const [isStartingSearchChat, setIsStartingSearchChat] = useState(false);
|
||||
const [pendingChatState, setPendingChatState] = useState<{ chatId: string | null; messages: Message[] } | null>(null);
|
||||
const [composer, setComposer] = useState("");
|
||||
const [provider, setProvider] = useState<Provider>("openai");
|
||||
@@ -699,14 +705,14 @@ export default function App() {
|
||||
selectedItem?.kind === "chat" &&
|
||||
selectedItem.id === pendingChatState.chatId;
|
||||
const displayMessages = useMemo(() => {
|
||||
if (!pendingChatState) return messages;
|
||||
if (!pendingChatState) return messages.filter(isDisplayableMessage);
|
||||
if (pendingChatState.chatId) {
|
||||
if (selectedItem?.kind === "chat" && selectedItem.id === pendingChatState.chatId) {
|
||||
return pendingChatState.messages;
|
||||
return pendingChatState.messages.filter(isDisplayableMessage);
|
||||
}
|
||||
return messages;
|
||||
return messages.filter(isDisplayableMessage);
|
||||
}
|
||||
return isSearchMode ? messages : pendingChatState.messages;
|
||||
return (isSearchMode ? messages : pendingChatState.messages).filter(isDisplayableMessage);
|
||||
}, [isSearchMode, messages, pendingChatState, selectedItem]);
|
||||
|
||||
const selectedChatSummary = useMemo(() => {
|
||||
@@ -1149,6 +1155,47 @@ export default function App() {
|
||||
await refreshCollections({ kind: "search", id: searchId });
|
||||
};
|
||||
|
||||
const handleStartChatFromSearch = async () => {
|
||||
if (!selectedSearch || isStartingSearchChat || isSending) return;
|
||||
|
||||
setError(null);
|
||||
setIsStartingSearchChat(true);
|
||||
try {
|
||||
const chat = await createChatFromSearch(selectedSearch.id);
|
||||
setDraftKind(null);
|
||||
setPendingChatState(null);
|
||||
setComposer("");
|
||||
setChats((current) => {
|
||||
const withoutExisting = current.filter((existing) => existing.id !== chat.id);
|
||||
return [chat, ...withoutExisting];
|
||||
});
|
||||
setSelectedItem({ kind: "chat", id: chat.id });
|
||||
setSelectedChat({
|
||||
id: chat.id,
|
||||
title: chat.title,
|
||||
createdAt: chat.createdAt,
|
||||
updatedAt: chat.updatedAt,
|
||||
initiatedProvider: chat.initiatedProvider,
|
||||
initiatedModel: chat.initiatedModel,
|
||||
lastUsedProvider: chat.lastUsedProvider,
|
||||
lastUsedModel: chat.lastUsedModel,
|
||||
messages: [],
|
||||
});
|
||||
setSelectedSearch(null);
|
||||
await refreshCollections({ kind: "chat", id: chat.id });
|
||||
await refreshChat(chat.id);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
if (message.includes("bearer token")) {
|
||||
handleAuthFailure(message);
|
||||
} else {
|
||||
setError(message);
|
||||
}
|
||||
} finally {
|
||||
setIsStartingSearchChat(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSend = async () => {
|
||||
const content = composer.trim();
|
||||
if (!content || isSending) return;
|
||||
@@ -1388,7 +1435,13 @@ export default function App() {
|
||||
{!isSearchMode ? (
|
||||
<ChatMessagesPanel messages={displayMessages} isLoading={isLoadingSelection} isSending={isSendingActiveChat} />
|
||||
) : (
|
||||
<SearchResultsPanel search={selectedSearch} isLoading={isLoadingSelection} isRunning={isSearchRunning} />
|
||||
<SearchResultsPanel
|
||||
search={selectedSearch}
|
||||
isLoading={isLoadingSelection}
|
||||
isRunning={isSearchRunning}
|
||||
isStartingChat={isStartingSearchChat}
|
||||
onStartChat={selectedSearch ? handleStartChatFromSearch : undefined}
|
||||
/>
|
||||
)}
|
||||
<div ref={transcriptEndRef} />
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import type { SearchDetail } from "@/lib/api";
|
||||
import { MarkdownContent } from "@/components/markdown/markdown-content";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { MessageSquare } from "lucide-preact";
|
||||
|
||||
function formatHost(url: string) {
|
||||
try {
|
||||
@@ -29,6 +30,8 @@ type Props = {
|
||||
className?: string;
|
||||
enableKeyboardNavigation?: boolean;
|
||||
openLinksInNewTab?: boolean;
|
||||
isStartingChat?: boolean;
|
||||
onStartChat?: () => void;
|
||||
};
|
||||
|
||||
export function SearchResultsPanel({
|
||||
@@ -38,6 +41,8 @@ export function SearchResultsPanel({
|
||||
className,
|
||||
enableKeyboardNavigation = false,
|
||||
openLinksInNewTab = true,
|
||||
isStartingChat = false,
|
||||
onStartChat,
|
||||
}: Props) {
|
||||
const ANSWER_COLLAPSED_HEIGHT_CLASS = "h-[3rem]";
|
||||
const [isAnswerExpanded, setIsAnswerExpanded] = useState(false);
|
||||
@@ -133,17 +138,31 @@ export function SearchResultsPanel({
|
||||
const isAnswerLoading = isRunning && !hasAnswerText;
|
||||
const hasCitations = citationEntries.length > 0;
|
||||
const isExpandable = hasAnswerText && (canExpandAnswer || hasCitations);
|
||||
const canStartChat = !!search && !isLoading && !isRunning && !isStartingChat && (!!search.answerText || search.results.length > 0);
|
||||
|
||||
return (
|
||||
<div className={className ?? "mx-auto w-full max-w-4xl"}>
|
||||
{search?.query ? (
|
||||
<div className="mb-5">
|
||||
<p className="text-sm text-muted-foreground">Results for</p>
|
||||
<h2 className="mt-1 break-words text-xl font-semibold text-violet-50">{search.query}</h2>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{search.results.length} result{search.results.length === 1 ? "" : "s"}
|
||||
{search.latencyMs ? ` • ${search.latencyMs} ms` : ""}
|
||||
</p>
|
||||
<div className="mb-5 flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-muted-foreground">Results for</p>
|
||||
<h2 className="mt-1 break-words text-xl font-semibold text-violet-50">{search.query}</h2>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{search.results.length} result{search.results.length === 1 ? "" : "s"}
|
||||
{search.latencyMs ? ` • ${search.latencyMs} ms` : ""}
|
||||
</p>
|
||||
</div>
|
||||
{onStartChat ? (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-10 shrink-0 items-center justify-center gap-2 rounded-lg border border-violet-300/24 bg-violet-300/10 px-3 text-sm font-medium text-violet-50 transition hover:bg-violet-300/16 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
onClick={onStartChat}
|
||||
disabled={!canStartChat}
|
||||
>
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
{isStartingChat ? "Starting chat..." : "Chat with results"}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -239,6 +239,14 @@ export async function getSearch(searchId: string) {
|
||||
return data.search;
|
||||
}
|
||||
|
||||
export async function createChatFromSearch(searchId: string, body?: { title?: string }) {
|
||||
const data = await api<{ chat: ChatSummary }>(`/v1/searches/${searchId}/chat`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body ?? {}),
|
||||
});
|
||||
return data.chat;
|
||||
}
|
||||
|
||||
export async function deleteSearch(searchId: string) {
|
||||
await api<{ deleted: true }>(`/v1/searches/${searchId}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user