feat(webui): support image uploads in composer and message bubbles
This commit is contained in:
@@ -2,7 +2,7 @@ import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import type { ReactNode } from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { useSessions } from "@/hooks/useSessions";
|
||||
import { useSessionHistory, useSessions } from "@/hooks/useSessions";
|
||||
import * as api from "@/lib/api";
|
||||
import { ClientProvider } from "@/providers/ClientProvider";
|
||||
|
||||
@@ -21,6 +21,7 @@ function fakeClient() {
|
||||
status: "open" as const,
|
||||
defaultChatId: null as string | null,
|
||||
onStatus: () => () => {},
|
||||
onError: () => () => {},
|
||||
onChat: () => () => {},
|
||||
sendMessage: vi.fn(),
|
||||
newChat: vi.fn(),
|
||||
@@ -86,6 +87,55 @@ describe("useSessions", () => {
|
||||
expect(result.current.sessions.map((s) => s.key)).toEqual(["websocket:chat-b"]);
|
||||
});
|
||||
|
||||
it("hydrates media_urls from historical user turns into UIMessage.images", async () => {
|
||||
// Round-trip check for the signed-media replay: the backend emits
|
||||
// ``media_urls`` on a historical user row and the hook must surface them
|
||||
// as ``images`` so the bubble can render the preview. Assistant turns
|
||||
// carry no media_urls and should not sprout an ``images`` field.
|
||||
vi.mocked(api.fetchSessionMessages).mockResolvedValue({
|
||||
key: "websocket:chat-media",
|
||||
created_at: "2026-04-20T10:00:00Z",
|
||||
updated_at: "2026-04-20T10:05:00Z",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "what's this?",
|
||||
timestamp: "2026-04-20T10:00:00Z",
|
||||
media_urls: [
|
||||
{ url: "/api/media/sig-1/payload-1", name: "snap.png" },
|
||||
{ url: "/api/media/sig-2/payload-2", name: "diag.jpg" },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: "it's a cat",
|
||||
timestamp: "2026-04-20T10:00:01Z",
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: "follow-up without images",
|
||||
timestamp: "2026-04-20T10:01:00Z",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useSessionHistory("websocket:chat-media"), {
|
||||
wrapper: wrap(fakeClient()),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
const [first, second, third] = result.current.messages;
|
||||
expect(first.role).toBe("user");
|
||||
expect(first.images).toEqual([
|
||||
{ url: "/api/media/sig-1/payload-1", name: "snap.png" },
|
||||
{ url: "/api/media/sig-2/payload-2", name: "diag.jpg" },
|
||||
]);
|
||||
expect(second.role).toBe("assistant");
|
||||
expect(second.images).toBeUndefined();
|
||||
expect(third.role).toBe("user");
|
||||
expect(third.images).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps the session in the list when delete fails", async () => {
|
||||
vi.mocked(api.listSessions).mockResolvedValue([
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user