Chat improvements (#22823)

* Add score fusion helpers for find_similar_objects chat tool

* Add candidate query builder for find_similar_objects chat tool

* register find_similar_objects chat tool definition

* implement _execute_find_similar_objects chat tool dispatcher

* Dispatch find_similar_objects in chat tool executor

* Teach chat system prompt when to use find_similar_objects

* Add i18n strings for find_similar_objects chat tool

* Add frontend extractor for find_similar_objects tool response

* Render anchor badge and similarity scores in chat results

* formatting

* filter similarity results in python, not sqlite-vec

* extract pure chat helpers to chat_util module

* Teach chat system prompt about attached_event marker

* Add parseAttachedEvent and prependAttachment helpers

* Add i18n strings for chat event attachments

* Add ChatAttachmentChip component

* Make chat thumbnails attach to composer on click

* Render attachment chip in user chat bubbles

* Add ChatQuickReplies pill row component

* Add ChatPaperclipButton with event picker popover

* Wire event attachments into chat composer and messages

* add ability to stop streaming

* tweak cursor to appear at the end of the same line of the streaming response

* use abort signal

* add tooltip

* display label and camera on attachment chip
This commit is contained in:
Josh Hawkins
2026-04-09 14:31:37 -06:00
committed by GitHub
parent 556d5d8c9d
commit 98c2fe00c1
12 changed files with 1318 additions and 109 deletions
+80 -5
View File
@@ -25,6 +25,7 @@ export async function streamChatCompletion(
headers: Record<string, string>,
apiMessages: { role: string; content: string }[],
callbacks: StreamChatCallbacks,
signal?: AbortSignal,
): Promise<void> {
const {
updateMessages,
@@ -38,6 +39,7 @@ export async function streamChatCompletion(
method: "POST",
headers,
body: JSON.stringify({ messages: apiMessages, stream: true }),
signal,
});
if (!res.ok) {
@@ -152,11 +154,15 @@ export async function streamChatCompletion(
return next;
});
}
} catch {
onError(defaultErrorMessage);
updateMessages((prev) =>
prev.filter((m) => !(m.role === "assistant" && m.content === "")),
);
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") {
// User stopped generation — not an error
} else {
onError(defaultErrorMessage);
updateMessages((prev) =>
prev.filter((m) => !(m.role === "assistant" && m.content === "")),
);
}
} finally {
onDone();
}
@@ -191,3 +197,72 @@ export function getEventIdsFromSearchObjectsToolCalls(
}
return results;
}
const ATTACHED_EVENT_MARKER = /^\[attached_event:([A-Za-z0-9._-]+)\]\s*\n?/;
export function parseAttachedEvent(content: string): {
eventId: string | null;
body: string;
} {
if (!content) return { eventId: null, body: content };
const match = content.match(ATTACHED_EVENT_MARKER);
if (!match) return { eventId: null, body: content };
const body = content.slice(match[0].length).replace(/^\n+/, "");
return { eventId: match[1], body };
}
export function prependAttachment(body: string, eventId: string): string {
return `[attached_event:${eventId}]\n\n${body}`;
}
export type FindSimilarObjectsResult = {
anchor: { id: string } | null;
results: { id: string; score?: number }[];
};
/**
* Parse find_similar_objects tool call response(s) into anchor + ranked results.
* Returns null if no find_similar_objects call is present so the caller can
* decide whether to render.
*/
export function getFindSimilarObjectsFromToolCalls(
toolCalls: ToolCall[] | undefined,
): FindSimilarObjectsResult | null {
if (!toolCalls?.length) return null;
for (const tc of toolCalls) {
if (tc.name !== "find_similar_objects" || !tc.response?.trim()) continue;
try {
const parsed = JSON.parse(tc.response) as {
anchor?: { id?: unknown };
results?: unknown;
};
const anchorId =
parsed.anchor && typeof parsed.anchor.id === "string"
? parsed.anchor.id
: null;
const anchor = anchorId ? { id: anchorId } : null;
const results: { id: string; score?: number }[] = [];
if (Array.isArray(parsed.results)) {
for (const item of parsed.results) {
if (
item &&
typeof item === "object" &&
"id" in item &&
typeof (item as { id: unknown }).id === "string"
) {
const entry: { id: string; score?: number } = {
id: (item as { id: string }).id,
};
const rawScore = (item as { score?: unknown }).score;
if (typeof rawScore === "number") entry.score = rawScore;
results.push(entry);
}
}
}
return { anchor, results };
} catch {
// ignore parse errors
}
}
return null;
}