diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 86d30208c46522c94ccae8d41f0791e5e49b2732..b37397e9ff4f293d984b3f8818426bb16c37e644 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -548,6 +548,7 @@ export default function ChatView() { const previewUrl = URL.createObjectURL(file); nextImages.push({ + type: "image", id: crypto.randomUUID(), name: file.name || "image", mimeType: file.type, @@ -749,7 +750,8 @@ export default function ChatView() { } setThreadError(activeThread.id, null); - const messageImages: ChatImageAttachment[] = composerImagesSnapshot.map((image) => ({ + const messageAttachments: ChatImageAttachment[] = composerImagesSnapshot.map((image) => ({ + type: "image", id: image.id, name: image.name, mimeType: image.mimeType, @@ -761,7 +763,7 @@ export default function ChatView() { threadId: activeThread.id, id: crypto.randomUUID(), text: trimmed, - ...(messageImages.length > 0 ? { images: messageImages } : {}), + ...(messageAttachments.length > 0 ? { attachments: messageAttachments } : {}), }); const previousMessages = activeThread.messages; setPrompt(""); @@ -1106,7 +1108,7 @@ export default function ChatView() { } if (timelineEntry.message.role === "user") { - const userImages = timelineEntry.message.images ?? []; + const userImages = timelineEntry.message.attachments ?? []; return (
diff --git a/apps/web/src/historyBootstrap.test.ts b/apps/web/src/historyBootstrap.test.ts index 97770234d3fa9b073dfe35b9b1661eb1b066a224..c330dcc3ade3c1718944c7a417d05a7a42984f73 100644 --- a/apps/web/src/historyBootstrap.test.ts +++ b/apps/web/src/historyBootstrap.test.ts @@ -99,8 +99,9 @@ describe("buildBootstrapInput", () => { id: "u-image", role: "user", text: "", - images: [ + attachments: [ { + type: "image", id: "img-1", name: "screenshot.png", mimeType: "image/png", diff --git a/apps/web/src/historyBootstrap.ts b/apps/web/src/historyBootstrap.ts index b31ed5c14675719c7aa0896f56d925d070298b08..b6a2570f0d9c6bbd5b96aeb34eb0916f14d4fc6d 100644 --- a/apps/web/src/historyBootstrap.ts +++ b/apps/web/src/historyBootstrap.ts @@ -18,13 +18,14 @@ function messageRoleLabel(message: ChatMessage): "USER" | "ASSISTANT" { return message.role === "assistant" ? "ASSISTANT" : "USER"; } -function imageSummary(message: ChatMessage): string | null { - const count = message.images?.length ?? 0; +function attachmentSummary(message: ChatMessage): string | null { + const imageAttachments = message.attachments?.filter((attachment) => attachment.type === "image"); + const count = imageAttachments?.length ?? 0; if (count === 0) { return null; } - const names = message.images?.slice(0, 3).map((image) => image.name) ?? []; + const names = imageAttachments?.slice(0, 3).map((image) => image.name) ?? []; const namesSummary = names.join(", "); const extraCount = count - names.length; const extraSummary = extraCount > 0 ? ` (+${extraCount} more)` : ""; @@ -33,16 +34,16 @@ function imageSummary(message: ChatMessage): string | null { function buildMessageBlock(message: ChatMessage): string { const text = message.text; - const images = imageSummary(message); + const attachments = attachmentSummary(message); - if (text && images) { - return `${messageRoleLabel(message)}:\n${text}\n${images}`; + if (text && attachments) { + return `${messageRoleLabel(message)}:\n${text}\n${attachments}`; } if (text) { return `${messageRoleLabel(message)}:\n${text}`; } - if (images) { - return `${messageRoleLabel(message)}:\n${images}`; + if (attachments) { + return `${messageRoleLabel(message)}:\n${attachments}`; } return `${messageRoleLabel(message)}:\n(empty message)`; } diff --git a/apps/web/src/persistenceSchema.test.ts b/apps/web/src/persistenceSchema.test.ts index 7ffe72a17d4c1347803df752d66986f7acfec6ca..a72f7eaea1ca2ab7614ffb3cb291a2f227588d6f 100644 --- a/apps/web/src/persistenceSchema.test.ts +++ b/apps/web/src/persistenceSchema.test.ts @@ -200,8 +200,9 @@ describe("toPersistedState", () => { id: "m-1", role: "user", text: "Hi", - images: [ + attachments: [ { + type: "image", id: "img-1", name: "diagram.png", mimeType: "image/png", @@ -250,8 +251,9 @@ describe("toPersistedState", () => { id: "m-1", role: "user", text: "Hi", - images: [ + attachments: [ { + type: "image", id: "img-1", name: "diagram.png", mimeType: "image/png", diff --git a/apps/web/src/persistenceSchema.ts b/apps/web/src/persistenceSchema.ts index b120aa1b29808f52170479c7344da3d6acb46e41..31d108ba907216c6138f33533d9522349c9cd5e2 100644 --- a/apps/web/src/persistenceSchema.ts +++ b/apps/web/src/persistenceSchema.ts @@ -23,9 +23,10 @@ const persistedMessageSchema = z.object({ id: z.string().min(1), role: z.enum(["user", "assistant"]), text: z.string(), - images: z + attachments: z .array( z.object({ + type: z.literal("image"), id: z.string().min(1), name: z.string().min(1), mimeType: z.string().min(1), @@ -137,14 +138,14 @@ function hydrateThread( terminalHeight: thread.terminalHeight ?? DEFAULT_THREAD_TERMINAL_HEIGHT, session: null, messages: thread.messages.map((message) => { - const hydratedImages = message.images?.map((image) => ({ - ...image, - })); + const hydratedAttachments = message.attachments?.map((attachment) => ({ ...attachment })); return { id: message.id, role: message.role, text: message.text, - ...(hydratedImages && hydratedImages.length > 0 ? { images: hydratedImages } : {}), + ...(hydratedAttachments && hydratedAttachments.length > 0 + ? { attachments: hydratedAttachments } + : {}), createdAt: message.createdAt, streaming: false, }; @@ -212,13 +213,14 @@ export function toPersistedState( id: message.id, role: message.role, text: message.text, - ...(message.images && message.images.length > 0 + ...(message.attachments && message.attachments.length > 0 ? { - images: message.images.map((image) => ({ - id: image.id, - name: image.name, - mimeType: image.mimeType, - sizeBytes: image.sizeBytes, + attachments: message.attachments.map((attachment) => ({ + type: attachment.type, + id: attachment.id, + name: attachment.name, + mimeType: attachment.mimeType, + sizeBytes: attachment.sizeBytes, })), } : {}), diff --git a/apps/web/src/store.ts b/apps/web/src/store.ts index b46da2b2ff9a8743d7832c389bf819c74b623b1d..6d2946b6eb452c7d758608b4f3bde97fdae23dd4 100644 --- a/apps/web/src/store.ts +++ b/apps/web/src/store.ts @@ -13,7 +13,7 @@ import { resolveModelSlug } from "./model-logic"; import { hydratePersistedState, toPersistedState } from "./persistenceSchema"; import { applyEventToMessages, asObject, asString, evolveSession } from "./session-logic"; import { - type ChatImageAttachment, + type ChatAttachment, DEFAULT_RUNTIME_MODE, type Project, type RuntimeMode, @@ -43,7 +43,7 @@ type Action = threadId: string; id: string; text: string; - images?: ChatImageAttachment[]; + attachments?: ChatAttachment[]; } | { type: "SET_ERROR"; threadId: string; error: string | null } | { type: "SET_THREAD_TITLE"; threadId: string; title: string } @@ -387,7 +387,9 @@ export function reducer(state: AppState, action: Action): AppState { id: action.id, role: "user" as const, text: action.text, - ...(action.images && action.images.length > 0 ? { images: action.images } : {}), + ...(action.attachments && action.attachments.length > 0 + ? { attachments: action.attachments } + : {}), createdAt: new Date().toISOString(), streaming: false, }, diff --git a/apps/web/src/types.ts b/apps/web/src/types.ts index 40a6bad4dfb8af4a810446f53539e35cd56de59a..d3f0539ba27a4b148f06bb1fabb01267e1df4413 100644 --- a/apps/web/src/types.ts +++ b/apps/web/src/types.ts @@ -6,6 +6,7 @@ export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; export const DEFAULT_THREAD_TERMINAL_HEIGHT = 280; export interface ChatImageAttachment { + type: "image"; id: string; name: string; mimeType: string; @@ -13,11 +14,13 @@ export interface ChatImageAttachment { previewUrl?: string; } +export type ChatAttachment = ChatImageAttachment; + export interface ChatMessage { id: string; role: "user" | "assistant"; text: string; - images?: ChatImageAttachment[]; + attachments?: ChatAttachment[]; createdAt: string; streaming: boolean; }