mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 11:25:50 -06:00
fix(webchat): keep media loads pinned to bottom (#97575)
* fix(webchat): keep media loads pinned to bottom * test(webchat): type message group mock options
This commit is contained in:
@@ -3902,6 +3902,7 @@ export function renderApp(state: AppViewState) {
|
||||
},
|
||||
showNewMessages: state.chatNewMessagesBelow && !state.chatManualRefreshInFlight,
|
||||
onScrollToBottom: () => state.scrollToBottom(),
|
||||
onAssistantAttachmentLoaded: () => state.scheduleChatScroll(),
|
||||
// Sidebar props for tool output viewing
|
||||
sidebarOpen: state.sidebarOpen,
|
||||
sidebarContent: state.sidebarContent,
|
||||
|
||||
@@ -182,6 +182,7 @@ export type AppViewState = {
|
||||
sidebarError: string | null;
|
||||
splitRatio: number;
|
||||
scrollToBottom: (opts?: { smooth?: boolean }) => void;
|
||||
scheduleChatScroll: () => void;
|
||||
devicesLoading: boolean;
|
||||
devicesError: string | null;
|
||||
devicesList: DevicePairingList | null;
|
||||
|
||||
@@ -985,6 +985,10 @@ export class OpenClawApp extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
scheduleChatScroll() {
|
||||
scheduleChatScrollInternal(this as unknown as Parameters<typeof scheduleChatScrollInternal>[0]);
|
||||
}
|
||||
|
||||
async loadAssistantIdentity(opts?: { sessionKey?: string; expectedSessionKey?: string }) {
|
||||
await loadAssistantIdentityInternal(this, opts);
|
||||
}
|
||||
|
||||
@@ -1461,6 +1461,32 @@ describe("grouped chat rendering", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("notifies when assistant audio and video attachment metadata loads", () => {
|
||||
const container = document.createElement("div");
|
||||
const onAssistantAttachmentLoaded = vi.fn();
|
||||
|
||||
renderAssistantMessage(
|
||||
container,
|
||||
{
|
||||
id: "assistant-media-layout",
|
||||
role: "assistant",
|
||||
content:
|
||||
"Audio and video\nMEDIA:https://example.com/voice.ogg\nMEDIA:https://example.com/clip.mp4",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{ showToolCalls: false, onAssistantAttachmentLoaded },
|
||||
);
|
||||
|
||||
expectElement(container, "audio", HTMLAudioElement).dispatchEvent(
|
||||
new Event("loadedmetadata", { bubbles: true }),
|
||||
);
|
||||
expectElement(container, "video", HTMLVideoElement).dispatchEvent(
|
||||
new Event("loadedmetadata", { bubbles: true }),
|
||||
);
|
||||
|
||||
expect(onAssistantAttachmentLoaded).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("renders allowed transcript and content image variants", async () => {
|
||||
resetAssistantAttachmentAvailabilityCacheForTest();
|
||||
const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
|
||||
|
||||
@@ -421,6 +421,7 @@ type RenderMessageGroupOptions = {
|
||||
isToolExpanded?: (toolCardId: string) => boolean;
|
||||
onToggleToolExpanded?: (toolCardId: string) => void;
|
||||
onRequestUpdate?: () => void;
|
||||
onAssistantAttachmentLoaded?: () => void;
|
||||
assistantName?: string;
|
||||
assistantAvatar?: string | null;
|
||||
userName?: string | null;
|
||||
@@ -456,6 +457,7 @@ function buildGroupedMessageRenderOptions(
|
||||
isToolExpanded: opts.isToolExpanded,
|
||||
onToggleToolExpanded: opts.onToggleToolExpanded,
|
||||
onRequestUpdate: opts.onRequestUpdate,
|
||||
onAssistantAttachmentLoaded: opts.onAssistantAttachmentLoaded,
|
||||
canvasPluginSurfaceUrl: opts.canvasPluginSurfaceUrl,
|
||||
basePath: opts.basePath,
|
||||
localMediaPreviewRoots: opts.localMediaPreviewRoots,
|
||||
@@ -1373,6 +1375,7 @@ function renderAssistantAttachments(
|
||||
basePath?: string,
|
||||
authToken?: string | null,
|
||||
onRequestUpdate?: () => void,
|
||||
onAssistantAttachmentLoaded?: () => void,
|
||||
) {
|
||||
if (attachments.length === 0) {
|
||||
return nothing;
|
||||
@@ -1424,7 +1427,12 @@ function renderAssistantAttachments(
|
||||
: nothing}
|
||||
</div>
|
||||
${attachmentUrl
|
||||
? html`<audio controls preload="metadata" src=${attachmentUrl}></audio>`
|
||||
? html`<audio
|
||||
controls
|
||||
preload="metadata"
|
||||
src=${attachmentUrl}
|
||||
@loadedmetadata=${() => onAssistantAttachmentLoaded?.()}
|
||||
></audio>`
|
||||
: availability.status === "unavailable"
|
||||
? html`<div class="chat-assistant-attachment-card__reason">
|
||||
${availability.reason}
|
||||
@@ -1444,7 +1452,12 @@ function renderAssistantAttachments(
|
||||
}
|
||||
return html`
|
||||
<div class="chat-assistant-attachment-card chat-assistant-attachment-card--video">
|
||||
<video controls preload="metadata" src=${attachmentUrl}></video>
|
||||
<video
|
||||
controls
|
||||
preload="metadata"
|
||||
src=${attachmentUrl}
|
||||
@loadedmetadata=${() => onAssistantAttachmentLoaded?.()}
|
||||
></video>
|
||||
<a
|
||||
class="chat-assistant-attachment-card__link"
|
||||
href=${attachmentUrl}
|
||||
@@ -1615,6 +1628,7 @@ function renderGroupedMessage(
|
||||
basePath?: string;
|
||||
localMediaPreviewRoots?: readonly string[];
|
||||
assistantAttachmentAuthToken?: string | null;
|
||||
onAssistantAttachmentLoaded?: () => void;
|
||||
embedSandboxMode?: EmbedSandboxMode;
|
||||
allowExternalEmbedUrls?: boolean;
|
||||
},
|
||||
@@ -1812,6 +1826,7 @@ function renderGroupedMessage(
|
||||
opts.basePath,
|
||||
opts.assistantAttachmentAuthToken,
|
||||
opts.onRequestUpdate,
|
||||
opts.onAssistantAttachmentLoaded,
|
||||
)}
|
||||
${reasoningMarkdown
|
||||
? html`<div class="chat-thinking">
|
||||
@@ -1869,6 +1884,7 @@ function renderGroupedMessage(
|
||||
opts.basePath,
|
||||
opts.assistantAttachmentAuthToken,
|
||||
opts.onRequestUpdate,
|
||||
opts.onAssistantAttachmentLoaded,
|
||||
)}
|
||||
${reasoningMarkdown
|
||||
? html`<div class="chat-thinking">
|
||||
|
||||
@@ -102,23 +102,28 @@ const buildChatItemsMock = vi.hoisted(() =>
|
||||
}),
|
||||
);
|
||||
const renderMessageGroupMock = vi.hoisted(() =>
|
||||
vi.fn((group: { messages: Array<{ message: unknown }> }) => {
|
||||
const element = document.createElement("div");
|
||||
element.className = "chat-group";
|
||||
element.textContent = group.messages
|
||||
.map(({ message }) => {
|
||||
if (typeof message === "object" && message !== null && "content" in message) {
|
||||
const content = (message as { content?: unknown }).content;
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
vi.fn(
|
||||
(
|
||||
group: { messages: Array<{ message: unknown }> },
|
||||
_opts?: { onAssistantAttachmentLoaded?: () => void },
|
||||
) => {
|
||||
const element = document.createElement("div");
|
||||
element.className = "chat-group";
|
||||
element.textContent = group.messages
|
||||
.map(({ message }) => {
|
||||
if (typeof message === "object" && message !== null && "content" in message) {
|
||||
const content = (message as { content?: unknown }).content;
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
}
|
||||
return content == null ? "" : JSON.stringify(content);
|
||||
}
|
||||
return content == null ? "" : JSON.stringify(content);
|
||||
}
|
||||
return String(message);
|
||||
})
|
||||
.join("\n");
|
||||
return element;
|
||||
}),
|
||||
return String(message);
|
||||
})
|
||||
.join("\n");
|
||||
return element;
|
||||
},
|
||||
),
|
||||
);
|
||||
const assistantAttachmentRenderVersionMock = vi.hoisted(() => ({ value: 0 }));
|
||||
|
||||
@@ -1145,6 +1150,20 @@ describe("chat transcript rendering cache", () => {
|
||||
expect(renderMessageGroupMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("passes assistant attachment load callbacks to transcript groups", () => {
|
||||
const onAssistantAttachmentLoaded = vi.fn();
|
||||
|
||||
renderChatView({
|
||||
messages: [{ role: "assistant", content: "MEDIA:https://example.com/voice.ogg" }],
|
||||
onAssistantAttachmentLoaded,
|
||||
});
|
||||
|
||||
expect(renderMessageGroupMock).toHaveBeenCalledTimes(1);
|
||||
expect(renderMessageGroupMock.mock.calls[0]?.[1]).toMatchObject({
|
||||
onAssistantAttachmentLoaded,
|
||||
});
|
||||
});
|
||||
|
||||
it("rebuilds transcript items when the transcript reference changes", () => {
|
||||
const toolMessages: unknown[] = [];
|
||||
const streamSegments: Array<{ text: string; ts: number }> = [];
|
||||
|
||||
@@ -163,6 +163,7 @@ export type ChatProps = {
|
||||
onAttachmentsChange?: (attachments: ChatAttachment[]) => void;
|
||||
showNewMessages?: boolean;
|
||||
onScrollToBottom?: () => void;
|
||||
onAssistantAttachmentLoaded?: () => void;
|
||||
onRefresh: () => void;
|
||||
onToggleFocusMode?: () => void;
|
||||
getDraft?: () => string;
|
||||
@@ -2282,6 +2283,7 @@ export function renderChat(props: ChatProps) {
|
||||
expandedToolCards.get(toolCardId) ?? false,
|
||||
onToggleToolExpanded: toggleToolCardExpanded,
|
||||
onRequestUpdate: requestUpdate,
|
||||
onAssistantAttachmentLoaded: props.onAssistantAttachmentLoaded,
|
||||
assistantName: props.assistantName,
|
||||
assistantAvatar: assistantIdentity.avatar,
|
||||
userName: props.userName ?? null,
|
||||
|
||||
Reference in New Issue
Block a user