feat(webui): render video media attachments

Add signed media URLs to live WebSocket replies and teach the WebUI to classify and render video attachments, so bot-sent videos can play inline in both live chats and session history.

Made-with: Cursor
This commit is contained in:
Xubin Ren
2026-04-25 03:20:40 +08:00
committed by Xubin Ren
parent be05189f39
commit e52fe2a8e2
10 changed files with 327 additions and 15 deletions
+59
View File
@@ -0,0 +1,59 @@
import type { UIMediaAttachment, UIMediaKind } from "@/lib/types";
const IMAGE_EXTENSIONS = new Set([
".png",
".jpg",
".jpeg",
".gif",
".webp",
".bmp",
".ico",
".tif",
".tiff",
]);
const VIDEO_EXTENSIONS = new Set([
".mp4",
".webm",
".mov",
".m4v",
".avi",
".mkv",
".3gp",
]);
function cleanPath(value: string): string {
return value.split(/[?#]/, 1)[0]?.toLowerCase() ?? "";
}
function extensionOf(value?: string): string {
if (!value) return "";
const path = cleanPath(value);
const dot = path.lastIndexOf(".");
if (dot < 0) return "";
return path.slice(dot);
}
export function inferMediaKind(media: { url?: string; name?: string }): UIMediaKind {
const url = media.url ?? "";
if (url.startsWith("data:image/")) return "image";
if (url.startsWith("data:video/")) return "video";
const ext = extensionOf(media.name) || extensionOf(url);
if (IMAGE_EXTENSIONS.has(ext)) return "image";
if (VIDEO_EXTENSIONS.has(ext)) return "video";
return "file";
}
export function toMediaAttachment(media: {
url?: string;
name?: string;
kind?: UIMediaKind;
}): UIMediaAttachment {
return {
kind: media.kind ?? inferMediaKind(media),
url: media.url,
name: media.name,
};
}
+11
View File
@@ -22,6 +22,14 @@ export interface UIImage {
name?: string;
}
export type UIMediaKind = "image" | "video" | "file";
export interface UIMediaAttachment {
kind: UIMediaKind;
url?: string;
name?: string;
}
export interface UIMessage {
id: string;
role: Role;
@@ -34,6 +42,8 @@ export interface UIMessage {
traces?: string[];
/** User turn: optimistic blob URLs for preview. Replay: placeholder chips. */
images?: UIImage[];
/** Signed or local UI-renderable media attachments. */
media?: UIMediaAttachment[];
}
export interface ChatSummary {
@@ -71,6 +81,7 @@ export type InboundEvent =
text: string;
reply_to?: string;
media?: string[];
media_urls?: Array<{ url: string; name?: string }>;
/** Present when the frame is an agent breadcrumb (e.g. tool hint,
* generic progress line) rather than a conversational reply. */
kind?: "tool_hint" | "progress";