Skip to content

Commit 0f94642

Browse files
smakoshsteebchen
andauthored
feat(chats): enhance message schema to support images (#652)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Chats now support image attachments; message text is optional. * Playground displays and saves images from both streaming and non‑streaming replies. * **Bug Fixes** * Copy-to-clipboard works even when a message has no text. * **Documentation** * API reference pages reformatted for readability (no behavioral changes). * Changelog updated with “Try it now in the Playground” links for the Gemini 2.5 Flash Image Preview. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Luca Steeb <[email protected]>
1 parent eb228a3 commit 0f94642

File tree

13 files changed

+1838
-20
lines changed

13 files changed

+1838
-20
lines changed

apps/api/src/routes/chats.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ const chatSchema = z.object({
2222
const messageSchema = z.object({
2323
id: z.string(),
2424
role: z.enum(["user", "assistant", "system"]),
25-
content: z.string(),
25+
content: z.string().nullable(),
26+
images: z.string().nullable(), // JSON string
2627
sequence: z.number(),
2728
createdAt: z.string().datetime(),
2829
});
@@ -37,10 +38,15 @@ const updateChatSchema = z.object({
3738
status: z.enum(["active", "archived"]).optional(),
3839
});
3940

40-
const createMessageSchema = z.object({
41-
role: z.enum(["user", "assistant", "system"]),
42-
content: z.string().min(1),
43-
});
41+
const createMessageSchema = z
42+
.object({
43+
role: z.enum(["user", "assistant", "system"]),
44+
content: z.string().optional(),
45+
images: z.string().optional(), // JSON string
46+
})
47+
.refine((data) => data.content || data.images, {
48+
message: "Either content or images must be provided",
49+
});
4450

4551
// List user's chats
4652
const listChats = createRoute({
@@ -274,6 +280,7 @@ chats.openapi(getChat, async (c) => {
274280
id: message.id,
275281
role: message.role as "user" | "assistant" | "system",
276282
content: message.content,
283+
images: message.images,
277284
sequence: message.sequence,
278285
createdAt: message.createdAt.toISOString(),
279286
})),
@@ -503,7 +510,8 @@ chats.openapi(addMessage, async (c) => {
503510
.values({
504511
chatId: id,
505512
role: body.role,
506-
content: body.content,
513+
content: body.content || null,
514+
images: body.images || null,
507515
sequence: nextSequence,
508516
})
509517
.returning();
@@ -520,6 +528,7 @@ chats.openapi(addMessage, async (c) => {
520528
id: newMessage.id,
521529
role: newMessage.role as "user" | "assistant" | "system",
522530
content: newMessage.content,
531+
images: newMessage.images,
523532
sequence: newMessage.sequence,
524533
createdAt: newMessage.createdAt.toISOString(),
525534
},
917 KB
Loading

apps/ui/src/app/playground/playground-client.tsx

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { useApi } from "@/lib/fetch-client";
2727
export interface Message {
2828
id: string;
2929
role: "user" | "assistant" | "system";
30-
content: string;
30+
content: string | null;
3131
timestamp: Date;
3232
images?: Array<{
3333
type: "image_url";
@@ -89,6 +89,16 @@ export function PlaygroundClient() {
8989
role: msg.role,
9090
content: msg.content,
9191
timestamp: new Date(msg.createdAt),
92+
images: msg.images
93+
? (() => {
94+
try {
95+
return JSON.parse(msg.images);
96+
} catch (error) {
97+
console.warn("Failed to parse images JSON:", msg.images, error);
98+
return undefined;
99+
}
100+
})()
101+
: undefined,
92102
}));
93103

94104
// Preserve images from existing local messages when reloading from database
@@ -283,6 +293,7 @@ export function PlaygroundClient() {
283293
const reader = response.body?.getReader();
284294
const decoder = new TextDecoder();
285295
let fullContent = "";
296+
let finalImages: any[] = []; // Track final images received during streaming
286297
let hasReceivedImages = false; // Track if we received images during streaming
287298

288299
if (reader) {
@@ -315,9 +326,11 @@ export function PlaygroundClient() {
315326
let imagesToSet: any[] | undefined;
316327
if (deltaImages && deltaImages.length > 0) {
317328
imagesToSet = deltaImages;
329+
finalImages = [...deltaImages]; // Track final images
318330
hasReceivedImages = true; // Mark that we received images
319331
} else if (images && images.length > 0) {
320332
imagesToSet = images;
333+
finalImages = [...images]; // Track final images
321334
hasReceivedImages = true; // Mark that we received images
322335
}
323336

@@ -361,13 +374,20 @@ export function PlaygroundClient() {
361374
}
362375

363376
// Save the complete assistant response to database
364-
if (fullContent && chatId) {
377+
if ((fullContent || finalImages.length > 0) && chatId) {
365378
try {
366379
await addMessage.mutateAsync({
367380
params: {
368381
path: { id: chatId },
369382
},
370-
body: { role: "assistant", content: fullContent },
383+
body: {
384+
role: "assistant",
385+
content: fullContent || undefined,
386+
images:
387+
finalImages.length > 0
388+
? JSON.stringify(finalImages)
389+
: undefined,
390+
},
371391
});
372392

373393
// Only invalidate query if no images were received (to avoid overwriting image data with DB data)
@@ -384,7 +404,7 @@ export function PlaygroundClient() {
384404
} else {
385405
const data = await response.json();
386406

387-
const assistantContent = data.content || "";
407+
const assistantContent = data.content ?? undefined;
388408
const assistantImages = data.images || [];
389409

390410
addLocalMessage({
@@ -394,13 +414,20 @@ export function PlaygroundClient() {
394414
});
395415

396416
// Save the assistant response to database
397-
if (assistantContent && chatId) {
417+
if ((assistantContent || assistantImages.length > 0) && chatId) {
398418
try {
399419
await addMessage.mutateAsync({
400420
params: {
401421
path: { id: chatId },
402422
},
403-
body: { role: "assistant", content: assistantContent },
423+
body: {
424+
role: "assistant",
425+
content: assistantContent,
426+
images:
427+
assistantImages.length > 0
428+
? JSON.stringify(assistantImages)
429+
: undefined,
430+
},
404431
});
405432

406433
// Only invalidate query if no images (to avoid overwriting image data with DB data)

apps/ui/src/components/dashboard/dashboard-sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -698,7 +698,7 @@ export function DashboardSidebar({
698698
internal: false,
699699
},
700700
{
701-
href: "https://llmgateway.io/playground",
701+
href: "/playground",
702702
label: "Playground",
703703
icon: BotMessageSquare,
704704
internal: false,

apps/ui/src/components/playground/chat-ui.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { cn } from "@/lib/utils";
2828
interface MessageType {
2929
id: string;
3030
role: "user" | "assistant" | "system";
31-
content: string;
31+
content: string | null;
3232
timestamp: Date;
3333
images?: Array<{
3434
type: "image_url";
@@ -343,7 +343,7 @@ export function ChatUi({
343343
message.role === "user" ? "right-0" : "left-0"
344344
}`}
345345
onClick={() =>
346-
copyToClipboard(message.content, message.id)
346+
copyToClipboard(message.content || "", message.id)
347347
}
348348
>
349349
{copiedMessageId === message.id ? (

apps/ui/src/content/changelog/2025-08-26-gemini-2-5-flash-image-preview.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ image:
1313

1414
We're thrilled to announce the addition of **Gemini 2.5 Flash Image Preview** - our **first image generation model** on LLM Gateway! This marks a significant milestone as we expand beyond text generation to visual AI capabilities.
1515

16+
**[Try it now in the Playground](/playground?model=gemini-2.5-flash-image-preview)** 🎨
17+
1618
## 🎨 Introducing Image Generation
1719

1820
**First of its Kind**: Gemini 2.5 Flash Image Preview is our inaugural image generation model, opening up exciting new possibilities for visual content creation through AI.

apps/ui/src/hooks/useChats.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ export interface Chat {
1616
export interface ChatMessage {
1717
id: string;
1818
role: "user" | "assistant" | "system";
19-
content: string;
19+
content: string | null;
20+
images: string | null; // JSON string from API
2021
sequence: number;
2122
createdAt: string;
2223
}

apps/ui/src/lib/api/v1.d.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2249,7 +2249,8 @@ export interface paths {
22492249
id: string;
22502250
/** @enum {string} */
22512251
role: "user" | "assistant" | "system";
2252-
content: string;
2252+
content: string | null;
2253+
images: string | null;
22532254
sequence: number;
22542255
/** Format: date-time */
22552256
createdAt: string;
@@ -2366,7 +2367,8 @@ export interface paths {
23662367
"application/json": {
23672368
/** @enum {string} */
23682369
role: "user" | "assistant" | "system";
2369-
content: string;
2370+
content?: string;
2371+
images?: string;
23702372
};
23712373
};
23722374
};
@@ -2382,7 +2384,8 @@ export interface paths {
23822384
id: string;
23832385
/** @enum {string} */
23842386
role: "user" | "assistant" | "system";
2385-
content: string;
2387+
content: string | null;
2388+
images: string | null;
23862389
sequence: number;
23872390
/** Format: date-time */
23882391
createdAt: string;

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export default [
66
{
77
rules: {
88
"@typescript-eslint/consistent-type-assertions": "off",
9+
"@typescript-eslint/triple-slash-reference": "off",
910
"max-nested-callbacks": "off",
1011
complexity: "off",
1112
"max-depth": "off",
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE "message" ALTER COLUMN "content" DROP NOT NULL;--> statement-breakpoint
2+
ALTER TABLE "message" ADD COLUMN "images" text;

0 commit comments

Comments
 (0)