authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-12 15:37:30-08:00
committergravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-12 15:37:30-08:00
log1cad53dae43aa9d719e190f88f2a3e1ce549942a
tree24656b4d7853835f388c3f37baa229921a03ee6a
parent760f15f2f94965e2fc653f11225741cd2b6fd870

Allow attachments field in ChatView


7 files changed, 42 insertions(+), 29 deletions(-)

apps/web/src/components/ChatView.tsx+5-3
......@@ -548,6 +548,7 @@ export default function ChatView() {
548548
549549 const previewUrl = URL.createObjectURL(file);
550550 nextImages.push({
551 type: "image",
551552 id: crypto.randomUUID(),
552553 name: file.name || "image",
553554 mimeType: file.type,
......@@ -749,7 +750,8 @@ export default function ChatView() {
749750 }
750751
751752 setThreadError(activeThread.id, null);
752 const messageImages: ChatImageAttachment[] = composerImagesSnapshot.map((image) => ({
753 const messageAttachments: ChatImageAttachment[] = composerImagesSnapshot.map((image) => ({
754 type: "image",
753755 id: image.id,
754756 name: image.name,
755757 mimeType: image.mimeType,
......@@ -761,7 +763,7 @@ export default function ChatView() {
761763 threadId: activeThread.id,
762764 id: crypto.randomUUID(),
763765 text: trimmed,
764 ...(messageImages.length > 0 ? { images: messageImages } : {}),
766 ...(messageAttachments.length > 0 ? { attachments: messageAttachments } : {}),
765767 });
766768 const previousMessages = activeThread.messages;
767769 setPrompt("");
......@@ -1106,7 +1108,7 @@ export default function ChatView() {
11061108 }
11071109
11081110 if (timelineEntry.message.role === "user") {
1109 const userImages = timelineEntry.message.images ?? [];
1111 const userImages = timelineEntry.message.attachments ?? [];
11101112 return (
11111113 <Fragment key={timelineEntry.id}>
11121114 <div className="flex justify-end">
apps/web/src/historyBootstrap.test.ts+2-1
......@@ -99,8 +99,9 @@ describe("buildBootstrapInput", () => {
9999 id: "u-image",
100100 role: "user",
101101 text: "",
102 images: [
102 attachments: [
103103 {
104 type: "image",
104105 id: "img-1",
105106 name: "screenshot.png",
106107 mimeType: "image/png",
apps/web/src/historyBootstrap.ts+9-8
......@@ -18,13 +18,14 @@ function messageRoleLabel(message: ChatMessage): "USER" | "ASSISTANT" {
1818 return message.role === "assistant" ? "ASSISTANT" : "USER";
1919}
2020
21function imageSummary(message: ChatMessage): string | null {
22 const count = message.images?.length ?? 0;
21function attachmentSummary(message: ChatMessage): string | null {
22 const imageAttachments = message.attachments?.filter((attachment) => attachment.type === "image");
23 const count = imageAttachments?.length ?? 0;
2324 if (count === 0) {
2425 return null;
2526 }
2627
27 const names = message.images?.slice(0, 3).map((image) => image.name) ?? [];
28 const names = imageAttachments?.slice(0, 3).map((image) => image.name) ?? [];
2829 const namesSummary = names.join(", ");
2930 const extraCount = count - names.length;
3031 const extraSummary = extraCount > 0 ? ` (+${extraCount} more)` : "";
......@@ -33,16 +34,16 @@ function imageSummary(message: ChatMessage): string | null {
3334
3435function buildMessageBlock(message: ChatMessage): string {
3536 const text = message.text;
36 const images = imageSummary(message);
37 const attachments = attachmentSummary(message);
3738
38 if (text && images) {
39 return `${messageRoleLabel(message)}:\n${text}\n${images}`;
39 if (text && attachments) {
40 return `${messageRoleLabel(message)}:\n${text}\n${attachments}`;
4041 }
4142 if (text) {
4243 return `${messageRoleLabel(message)}:\n${text}`;
4344 }
44 if (images) {
45 return `${messageRoleLabel(message)}:\n${images}`;
45 if (attachments) {
46 return `${messageRoleLabel(message)}:\n${attachments}`;
4647 }
4748 return `${messageRoleLabel(message)}:\n(empty message)`;
4849}
apps/web/src/persistenceSchema.test.ts+4-2
......@@ -200,8 +200,9 @@ describe("toPersistedState", () => {
200200 id: "m-1",
201201 role: "user",
202202 text: "Hi",
203 images: [
203 attachments: [
204204 {
205 type: "image",
205206 id: "img-1",
206207 name: "diagram.png",
207208 mimeType: "image/png",
......@@ -250,8 +251,9 @@ describe("toPersistedState", () => {
250251 id: "m-1",
251252 role: "user",
252253 text: "Hi",
253 images: [
254 attachments: [
254255 {
256 type: "image",
255257 id: "img-1",
256258 name: "diagram.png",
257259 mimeType: "image/png",
apps/web/src/persistenceSchema.ts+13-11
......@@ -23,9 +23,10 @@ const persistedMessageSchema = z.object({
2323 id: z.string().min(1),
2424 role: z.enum(["user", "assistant"]),
2525 text: z.string(),
26 images: z
26 attachments: z
2727 .array(
2828 z.object({
29 type: z.literal("image"),
2930 id: z.string().min(1),
3031 name: z.string().min(1),
3132 mimeType: z.string().min(1),
......@@ -137,14 +138,14 @@ function hydrateThread(
137138 terminalHeight: thread.terminalHeight ?? DEFAULT_THREAD_TERMINAL_HEIGHT,
138139 session: null,
139140 messages: thread.messages.map((message) => {
140 const hydratedImages = message.images?.map((image) => ({
141 ...image,
142 }));
141 const hydratedAttachments = message.attachments?.map((attachment) => ({ ...attachment }));
143142 return {
144143 id: message.id,
145144 role: message.role,
146145 text: message.text,
147 ...(hydratedImages && hydratedImages.length > 0 ? { images: hydratedImages } : {}),
146 ...(hydratedAttachments && hydratedAttachments.length > 0
147 ? { attachments: hydratedAttachments }
148 : {}),
148149 createdAt: message.createdAt,
149150 streaming: false,
150151 };
......@@ -212,13 +213,14 @@ export function toPersistedState(
212213 id: message.id,
213214 role: message.role,
214215 text: message.text,
215 ...(message.images && message.images.length > 0
216 ...(message.attachments && message.attachments.length > 0
216217 ? {
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,
222224 })),
223225 }
224226 : {}),
apps/web/src/store.ts+5-3
......@@ -13,7 +13,7 @@ import { resolveModelSlug } from "./model-logic";
1313import { hydratePersistedState, toPersistedState } from "./persistenceSchema";
1414import { applyEventToMessages, asObject, asString, evolveSession } from "./session-logic";
1515import {
16 type ChatImageAttachment,
16 type ChatAttachment,
1717 DEFAULT_RUNTIME_MODE,
1818 type Project,
1919 type RuntimeMode,
......@@ -43,7 +43,7 @@ type Action =
4343 threadId: string;
4444 id: string;
4545 text: string;
46 images?: ChatImageAttachment[];
46 attachments?: ChatAttachment[];
4747 }
4848 | { type: "SET_ERROR"; threadId: string; error: string | null }
4949 | { type: "SET_THREAD_TITLE"; threadId: string; title: string }
......@@ -387,7 +387,9 @@ export function reducer(state: AppState, action: Action): AppState {
387387 id: action.id,
388388 role: "user" as const,
389389 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 : {}),
391393 createdAt: new Date().toISOString(),
392394 streaming: false,
393395 },
apps/web/src/types.ts+4-1
......@@ -6,6 +6,7 @@ export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access";
66export const DEFAULT_THREAD_TERMINAL_HEIGHT = 280;
77
88export interface ChatImageAttachment {
9 type: "image";
910 id: string;
1011 name: string;
1112 mimeType: string;
......@@ -13,11 +14,13 @@ export interface ChatImageAttachment {
1314 previewUrl?: string;
1415}
1516
17export type ChatAttachment = ChatImageAttachment;
18
1619export interface ChatMessage {
1720 id: string;
1821 role: "user" | "assistant";
1922 text: string;
20 images?: ChatImageAttachment[];
23 attachments?: ChatAttachment[];
2124 createdAt: string;
2225 streaming: boolean;
2326}