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() {...@@ -548,6 +548,7 @@ export default function ChatView() {
548548
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 }
750751
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 }
11071109
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}
2020
21function imageSummary(message: ChatMessage): string | null {21function 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 }
2627
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 {
3334
34function buildMessageBlock(message: ChatMessage): string {35function buildMessageBlock(message: ChatMessage): string {
35 const text = message.text;36 const text = message.text;
36 const images = imageSummary(message);37 const attachments = attachmentSummary(message);
3738
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: z26 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 > 0216 ...(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";
13import { hydratePersistedState, toPersistedState } from "./persistenceSchema";13import { hydratePersistedState, toPersistedState } from "./persistenceSchema";
14import { applyEventToMessages, asObject, asString, evolveSession } from "./session-logic";14import { applyEventToMessages, asObject, asString, evolveSession } from "./session-logic";
15import {15import {
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";
6export const DEFAULT_THREAD_TERMINAL_HEIGHT = 280;6export const DEFAULT_THREAD_TERMINAL_HEIGHT = 280;
77
8export interface ChatImageAttachment {8export 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}
1516
17export type ChatAttachment = ChatImageAttachment;
18
16export interface ChatMessage {19export 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}