| author | |
| committer | |
| log | 1cad53dae43aa9d719e190f88f2a3e1ce549942a |
| tree | 24656b4d7853835f388c3f37baa229921a03ee6a |
| parent | 760f15f2f94965e2fc653f11225741cd2b6fd870 |
7 files changed, 42 insertions(+), 29 deletions(-)
apps/web/src/components/ChatView.tsx+5-3| ... | ... | @@ -548,6 +548,7 @@ export default function ChatView() { |
| 548 | 548 | |
| 549 | 549 | const previewUrl = URL.createObjectURL(file); |
| 550 | 550 | nextImages.push({ |
| 551 | type: "image", | |
| 551 | 552 | id: crypto.randomUUID(), |
| 552 | 553 | name: file.name || "image", |
| 553 | 554 | mimeType: file.type, |
| ... | ... | @@ -749,7 +750,8 @@ export default function ChatView() { |
| 749 | 750 | } |
| 750 | 751 | |
| 751 | 752 | setThreadError(activeThread.id, null); |
| 752 | const messageImages: ChatImageAttachment[] = composerImagesSnapshot.map((image) => ({ | |
| 753 | const messageAttachments: ChatImageAttachment[] = composerImagesSnapshot.map((image) => ({ | |
| 754 | type: "image", | |
| 753 | 755 | id: image.id, |
| 754 | 756 | name: image.name, |
| 755 | 757 | mimeType: image.mimeType, |
| ... | ... | @@ -761,7 +763,7 @@ export default function ChatView() { |
| 761 | 763 | threadId: activeThread.id, |
| 762 | 764 | id: crypto.randomUUID(), |
| 763 | 765 | text: trimmed, |
| 764 | ...(messageImages.length > 0 ? { images: messageImages } : {}), | |
| 766 | ...(messageAttachments.length > 0 ? { attachments: messageAttachments } : {}), | |
| 765 | 767 | }); |
| 766 | 768 | const previousMessages = activeThread.messages; |
| 767 | 769 | setPrompt(""); |
| ... | ... | @@ -1106,7 +1108,7 @@ export default function ChatView() { |
| 1106 | 1108 | } |
| 1107 | 1109 | |
| 1108 | 1110 | if (timelineEntry.message.role === "user") { |
| 1109 | const userImages = timelineEntry.message.images ?? []; | |
| 1111 | const userImages = timelineEntry.message.attachments ?? []; | |
| 1110 | 1112 | return ( |
| 1111 | 1113 | <Fragment key={timelineEntry.id}> |
| 1112 | 1114 | <div className="flex justify-end"> |
apps/web/src/historyBootstrap.test.ts+2-1| ... | ... | @@ -99,8 +99,9 @@ describe("buildBootstrapInput", () => { |
| 99 | 99 | id: "u-image", |
| 100 | 100 | role: "user", |
| 101 | 101 | text: "", |
| 102 | images: [ | |
| 102 | attachments: [ | |
| 103 | 103 | { |
| 104 | type: "image", | |
| 104 | 105 | id: "img-1", |
| 105 | 106 | name: "screenshot.png", |
| 106 | 107 | mimeType: "image/png", |
apps/web/src/historyBootstrap.ts+9-8| ... | ... | @@ -18,13 +18,14 @@ function messageRoleLabel(message: ChatMessage): "USER" | "ASSISTANT" { |
| 18 | 18 | return message.role === "assistant" ? "ASSISTANT" : "USER"; |
| 19 | 19 | } |
| 20 | 20 | |
| 21 | function imageSummary(message: ChatMessage): string | null { | |
| 22 | const count = message.images?.length ?? 0; | |
| 21 | function attachmentSummary(message: ChatMessage): string | null { | |
| 22 | const imageAttachments = message.attachments?.filter((attachment) => attachment.type === "image"); | |
| 23 | const count = imageAttachments?.length ?? 0; | |
| 23 | 24 | if (count === 0) { |
| 24 | 25 | return null; |
| 25 | 26 | } |
| 26 | 27 | |
| 27 | const names = message.images?.slice(0, 3).map((image) => image.name) ?? []; | |
| 28 | const names = imageAttachments?.slice(0, 3).map((image) => image.name) ?? []; | |
| 28 | 29 | const namesSummary = names.join(", "); |
| 29 | 30 | const extraCount = count - names.length; |
| 30 | 31 | const extraSummary = extraCount > 0 ? ` (+${extraCount} more)` : ""; |
| ... | ... | @@ -33,16 +34,16 @@ function imageSummary(message: ChatMessage): string | null { |
| 33 | 34 | |
| 34 | 35 | function buildMessageBlock(message: ChatMessage): string { |
| 35 | 36 | const text = message.text; |
| 36 | const images = imageSummary(message); | |
| 37 | const attachments = attachmentSummary(message); | |
| 37 | 38 | |
| 38 | if (text && images) { | |
| 39 | return `${messageRoleLabel(message)}:\n${text}\n${images}`; | |
| 39 | if (text && attachments) { | |
| 40 | return `${messageRoleLabel(message)}:\n${text}\n${attachments}`; | |
| 40 | 41 | } |
| 41 | 42 | if (text) { |
| 42 | 43 | return `${messageRoleLabel(message)}:\n${text}`; |
| 43 | 44 | } |
| 44 | if (images) { | |
| 45 | return `${messageRoleLabel(message)}:\n${images}`; | |
| 45 | if (attachments) { | |
| 46 | return `${messageRoleLabel(message)}:\n${attachments}`; | |
| 46 | 47 | } |
| 47 | 48 | return `${messageRoleLabel(message)}:\n(empty message)`; |
| 48 | 49 | } |
apps/web/src/persistenceSchema.test.ts+4-2| ... | ... | @@ -200,8 +200,9 @@ describe("toPersistedState", () => { |
| 200 | 200 | id: "m-1", |
| 201 | 201 | role: "user", |
| 202 | 202 | text: "Hi", |
| 203 | images: [ | |
| 203 | attachments: [ | |
| 204 | 204 | { |
| 205 | type: "image", | |
| 205 | 206 | id: "img-1", |
| 206 | 207 | name: "diagram.png", |
| 207 | 208 | mimeType: "image/png", |
| ... | ... | @@ -250,8 +251,9 @@ describe("toPersistedState", () => { |
| 250 | 251 | id: "m-1", |
| 251 | 252 | role: "user", |
| 252 | 253 | text: "Hi", |
| 253 | images: [ | |
| 254 | attachments: [ | |
| 254 | 255 | { |
| 256 | type: "image", | |
| 255 | 257 | id: "img-1", |
| 256 | 258 | name: "diagram.png", |
| 257 | 259 | mimeType: "image/png", |
apps/web/src/persistenceSchema.ts+13-11| ... | ... | @@ -23,9 +23,10 @@ const persistedMessageSchema = z.object({ |
| 23 | 23 | id: z.string().min(1), |
| 24 | 24 | role: z.enum(["user", "assistant"]), |
| 25 | 25 | text: z.string(), |
| 26 | images: z | |
| 26 | attachments: z | |
| 27 | 27 | .array( |
| 28 | 28 | z.object({ |
| 29 | type: z.literal("image"), | |
| 29 | 30 | id: z.string().min(1), |
| 30 | 31 | name: z.string().min(1), |
| 31 | 32 | mimeType: z.string().min(1), |
| ... | ... | @@ -137,14 +138,14 @@ function hydrateThread( |
| 137 | 138 | terminalHeight: thread.terminalHeight ?? DEFAULT_THREAD_TERMINAL_HEIGHT, |
| 138 | 139 | session: null, |
| 139 | 140 | messages: thread.messages.map((message) => { |
| 140 | const hydratedImages = message.images?.map((image) => ({ | |
| 141 | ...image, | |
| 142 | })); | |
| 141 | const hydratedAttachments = message.attachments?.map((attachment) => ({ ...attachment })); | |
| 143 | 142 | return { |
| 144 | 143 | id: message.id, |
| 145 | 144 | role: message.role, |
| 146 | 145 | text: message.text, |
| 147 | ...(hydratedImages && hydratedImages.length > 0 ? { images: hydratedImages } : {}), | |
| 146 | ...(hydratedAttachments && hydratedAttachments.length > 0 | |
| 147 | ? { attachments: hydratedAttachments } | |
| 148 | : {}), | |
| 148 | 149 | createdAt: message.createdAt, |
| 149 | 150 | streaming: false, |
| 150 | 151 | }; |
| ... | ... | @@ -212,13 +213,14 @@ export function toPersistedState( |
| 212 | 213 | id: message.id, |
| 213 | 214 | role: message.role, |
| 214 | 215 | text: message.text, |
| 215 | ...(message.images && message.images.length > 0 | |
| 216 | ...(message.attachments && message.attachments.length > 0 | |
| 216 | 217 | ? { |
| 217 | images: message.images.map((image) => ({ | |
| 218 | id: image.id, | |
| 219 | name: image.name, | |
| 220 | mimeType: image.mimeType, | |
| 221 | sizeBytes: image.sizeBytes, | |
| 218 | attachments: message.attachments.map((attachment) => ({ | |
| 219 | type: attachment.type, | |
| 220 | id: attachment.id, | |
| 221 | name: attachment.name, | |
| 222 | mimeType: attachment.mimeType, | |
| 223 | sizeBytes: attachment.sizeBytes, | |
| 222 | 224 | })), |
| 223 | 225 | } |
| 224 | 226 | : {}), |
apps/web/src/store.ts+5-3| ... | ... | @@ -13,7 +13,7 @@ import { resolveModelSlug } from "./model-logic"; |
| 13 | 13 | import { hydratePersistedState, toPersistedState } from "./persistenceSchema"; |
| 14 | 14 | import { applyEventToMessages, asObject, asString, evolveSession } from "./session-logic"; |
| 15 | 15 | import { |
| 16 | type ChatImageAttachment, | |
| 16 | type ChatAttachment, | |
| 17 | 17 | DEFAULT_RUNTIME_MODE, |
| 18 | 18 | type Project, |
| 19 | 19 | type RuntimeMode, |
| ... | ... | @@ -43,7 +43,7 @@ type Action = |
| 43 | 43 | threadId: string; |
| 44 | 44 | id: string; |
| 45 | 45 | text: string; |
| 46 | images?: ChatImageAttachment[]; | |
| 46 | attachments?: ChatAttachment[]; | |
| 47 | 47 | } |
| 48 | 48 | | { type: "SET_ERROR"; threadId: string; error: string | null } |
| 49 | 49 | | { type: "SET_THREAD_TITLE"; threadId: string; title: string } |
| ... | ... | @@ -387,7 +387,9 @@ export function reducer(state: AppState, action: Action): AppState { |
| 387 | 387 | id: action.id, |
| 388 | 388 | role: "user" as const, |
| 389 | 389 | text: action.text, |
| 390 | ...(action.images && action.images.length > 0 ? { images: action.images } : {}), | |
| 390 | ...(action.attachments && action.attachments.length > 0 | |
| 391 | ? { attachments: action.attachments } | |
| 392 | : {}), | |
| 391 | 393 | createdAt: new Date().toISOString(), |
| 392 | 394 | streaming: false, |
| 393 | 395 | }, |
apps/web/src/types.ts+4-1| ... | ... | @@ -6,6 +6,7 @@ export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; |
| 6 | 6 | export const DEFAULT_THREAD_TERMINAL_HEIGHT = 280; |
| 7 | 7 | |
| 8 | 8 | export interface ChatImageAttachment { |
| 9 | type: "image"; | |
| 9 | 10 | id: string; |
| 10 | 11 | name: string; |
| 11 | 12 | mimeType: string; |
| ... | ... | @@ -13,11 +14,13 @@ export interface ChatImageAttachment { |
| 13 | 14 | previewUrl?: string; |
| 14 | 15 | } |
| 15 | 16 | |
| 17 | export type ChatAttachment = ChatImageAttachment; | |
| 18 | ||
| 16 | 19 | export interface ChatMessage { |
| 17 | 20 | id: string; |
| 18 | 21 | role: "user" | "assistant"; |
| 19 | 22 | text: string; |
| 20 | images?: ChatImageAttachment[]; | |
| 23 | attachments?: ChatAttachment[]; | |
| 21 | 24 | createdAt: string; |
| 22 | 25 | streaming: boolean; |
| 23 | 26 | } |