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