Render anchor badge and similarity scores in chat results

This commit is contained in:
Josh Hawkins 2026-04-08 15:29:18 -05:00
parent 8f5128b905
commit 30997c20d7
2 changed files with 67 additions and 24 deletions

View File

@ -1,31 +1,39 @@
import { useApiHost } from "@/api"; import { useApiHost } from "@/api";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
type ChatEvent = { id: string; score?: number };
type ChatEventThumbnailsRowProps = { type ChatEventThumbnailsRowProps = {
events: { id: string }[]; events: ChatEvent[];
anchor?: { id: string } | null;
}; };
/** /**
* Horizontal scroll row of event thumbnail images for chat (e.g. after search_objects). * Horizontal scroll row of event thumbnail images for chat.
* Renders nothing when events is empty. * Optionally renders an anchor thumbnail with a "reference" badge above the
* results, and per-event similarity scores when provided.
* Renders nothing when there is nothing to show.
*/ */
export function ChatEventThumbnailsRow({ export function ChatEventThumbnailsRow({
events, events,
anchor = null,
}: ChatEventThumbnailsRowProps) { }: ChatEventThumbnailsRowProps) {
const apiHost = useApiHost(); const apiHost = useApiHost();
const { t } = useTranslation(["views/chat"]);
if (events.length === 0) return null; if (events.length === 0 && !anchor) return null;
return ( const renderThumb = (event: ChatEvent, isAnchor = false) => (
<div className="flex min-w-0 max-w-full flex-col gap-1 self-start">
<div className="scrollbar-container min-w-0 overflow-x-auto">
<div className="flex w-max gap-2">
{events.map((event) => (
<a <a
key={event.id} key={event.id}
href={`/explore?event_id=${event.id}`} href={`/explore?event_id=${event.id}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="relative aspect-square size-32 shrink-0 overflow-hidden rounded-lg" className={cn(
"relative aspect-square size-32 shrink-0 overflow-hidden rounded-lg",
isAnchor && "ring-2 ring-primary",
)}
> >
<img <img
className="size-full object-cover" className="size-full object-cover"
@ -33,10 +41,33 @@ export function ChatEventThumbnailsRow({
alt="" alt=""
loading="lazy" loading="lazy"
/> />
{typeof event.score === "number" && !isAnchor && (
<span className="absolute bottom-1 right-1 rounded bg-black/60 px-1 text-[10px] text-white">
{Math.round(event.score * 100)}%
</span>
)}
{isAnchor && (
<span className="absolute left-1 top-1 rounded bg-primary px-1 text-[10px] text-primary-foreground">
{t("anchor")}
</span>
)}
</a> </a>
))} );
return (
<div className="flex min-w-0 max-w-full flex-col gap-2 self-start">
{anchor && (
<div className="scrollbar-container min-w-0 overflow-x-auto">
<div className="flex w-max gap-2">{renderThumb(anchor, true)}</div>
</div>
)}
{events.length > 0 && (
<div className="scrollbar-container min-w-0 overflow-x-auto">
<div className="flex w-max gap-2">
{events.map((event) => renderThumb(event))}
</div> </div>
</div> </div>
)}
</div> </div>
); );
} }

View File

@ -12,6 +12,7 @@ import { ChatStartingState } from "@/components/chat/ChatStartingState";
import type { ChatMessage } from "@/types/chat"; import type { ChatMessage } from "@/types/chat";
import { import {
getEventIdsFromSearchObjectsToolCalls, getEventIdsFromSearchObjectsToolCalls,
getFindSimilarObjectsFromToolCalls,
streamChatCompletion, streamChatCompletion,
} from "@/utils/chatUtil"; } from "@/utils/chatUtil";
@ -161,6 +162,17 @@ export default function ChatPage() {
{msg.role === "assistant" && {msg.role === "assistant" &&
isComplete && isComplete &&
(() => { (() => {
const similar = getFindSimilarObjectsFromToolCalls(
msg.toolCalls,
);
if (similar) {
return (
<ChatEventThumbnailsRow
events={similar.results}
anchor={similar.anchor}
/>
);
}
const events = getEventIdsFromSearchObjectsToolCalls( const events = getEventIdsFromSearchObjectsToolCalls(
msg.toolCalls, msg.toolCalls,
); );