From ea9cd211fceea0524bede12717fe33cf3a2aca47 Mon Sep 17 00:00:00 2001 From: Andrew Beal Date: Tue, 23 Dec 2025 12:04:29 +0000 Subject: [PATCH] refactor: extract MIME type mappings and improve attachment handling - Move MimeTypeToFileTypes and FileTypeToMimeType mappings to dedicated FileTypeMimeTypeMapping enum file - Add empty content validation for file attachments - Add PDF support for Gemini - Display attachment references in user messages with file info - Fix OpenAI unsupported mime type message formatting - Improve URI list handling in InputService to include text/plain - Update chat area padding and styling for better attachment display - Prevent drag selection on chat padding element - Update test assertions for new unsupported mime type message format --- AIClasses/BaseAIClass.ts | 3 + AIClasses/Claude/Claude.ts | 5 +- AIClasses/Gemini/Gemini.ts | 7 +- AIClasses/OpenAI/OpenAI.ts | 16 +- Components/ChatArea.svelte | 79 ++++++- Components/ChatAttachments.svelte | 6 +- Components/ChatInput.svelte | 2 +- Conversations/Attachment.ts | 1 + Conversations/Conversation.ts | 2 +- Conversations/ConversationContent.ts | 7 + Conversations/Reference.ts | 24 +++ Enums/FileType.ts | 103 +-------- Enums/FileTypeMimeTypeMapping.ts | 249 ++++++++++++++++++++++ Enums/MimeType.ts | 150 ------------- Services/ChatService.ts | 9 +- Services/ConversationFileSystemService.ts | 17 ++ Services/InputService.ts | 6 +- Styles/custom_styles.css | 11 + __tests__/AIClasses/Claude.test.ts | 4 +- __tests__/AIClasses/Gemini.test.ts | 6 +- __tests__/AIClasses/OpenAI.test.ts | 4 +- __tests__/Services/InputService.test.ts | 29 ++- 22 files changed, 451 insertions(+), 289 deletions(-) create mode 100644 Conversations/Reference.ts create mode 100644 Enums/FileTypeMimeTypeMapping.ts diff --git a/AIClasses/BaseAIClass.ts b/AIClasses/BaseAIClass.ts index b5cfb0a..5985741 100644 --- a/AIClasses/BaseAIClass.ts +++ b/AIClasses/BaseAIClass.ts @@ -138,6 +138,9 @@ export abstract class BaseAIClass implements IAIClass { for (const attachment of attachments) { try { + if (attachment.base64.trim() === "") { + Exception.throw("File has no content!"); + } await this.aiFileService.uploadFile(attachment); } catch (error) { Exception.log(`Failed to upload ${attachment.fileName}: ${Exception.messageFrom(error)}`); diff --git a/AIClasses/Claude/Claude.ts b/AIClasses/Claude/Claude.ts index 0a9356b..1758a9e 100644 --- a/AIClasses/Claude/Claude.ts +++ b/AIClasses/Claude/Claude.ts @@ -11,7 +11,8 @@ import { Role } from "Enums/Role"; import type { RawMessageStreamEvent, ContentBlockParam, Tool } from '@anthropic-ai/sdk/resources/messages'; import { Exception } from "Helpers/Exception"; import { MimeType, toMimeType } from "Enums/MimeType"; -import { isTextFile, MimeTypeToFileTypes } from "Enums/FileType"; +import { isTextFile } from "Enums/FileType"; +import { MimeTypeToFileTypes } from "Enums/FileTypeMimeTypeMapping"; export class Claude extends BaseAIClass { @@ -286,7 +287,7 @@ export class Claude extends BaseAIClass { } return [ - {type: "text", text: attachment.fileName}, + {type: "text", text: `Binary data for ${attachment.fileName} follows in next message` }, { type: isPlainText || mimeType === MimeType.APPLICATION_PDF ? "document" : "image", source: { diff --git a/AIClasses/Gemini/Gemini.ts b/AIClasses/Gemini/Gemini.ts index 553c9e0..b8a841c 100644 --- a/AIClasses/Gemini/Gemini.ts +++ b/AIClasses/Gemini/Gemini.ts @@ -11,7 +11,8 @@ import type { ConversationContent } from "Conversations/ConversationContent"; import type { Candidate, Part, FunctionDeclaration } from "@google/genai"; import { FinishReason } from "@google/genai"; import { MimeType, toMimeType } from "Enums/MimeType"; -import { isTextFile, MimeTypeToFileTypes } from "Enums/FileType"; +import { isTextFile } from "Enums/FileType"; +import { MimeTypeToFileTypes } from "Enums/FileTypeMimeTypeMapping"; export class Gemini extends BaseAIClass { @@ -30,6 +31,8 @@ export class Gemini extends BaseAIClass { // Images MimeType.IMAGE_JPEG, MimeType.IMAGE_PNG, + // PDF + MimeType.APPLICATION_PDF, // Data Formats MimeType.APPLICATION_JSON, MimeType.APPLICATION_XML, @@ -328,7 +331,7 @@ export class Gemini extends BaseAIClass { continue; } - parts.push({text: attachment.fileName}); + parts.push({ text: `Binary data for ${attachment.fileName} follows in next message` }); parts.push({ fileData: { mimeType: mimeType, diff --git a/AIClasses/OpenAI/OpenAI.ts b/AIClasses/OpenAI/OpenAI.ts index f5ebf14..dfbe0bf 100644 --- a/AIClasses/OpenAI/OpenAI.ts +++ b/AIClasses/OpenAI/OpenAI.ts @@ -11,7 +11,8 @@ import type { ResponseEvent, ResponseOutputTextDelta, ResponseOutputItemDone, Re import { Exception } from "Helpers/Exception"; import { ApiErrorType } from "Types/ApiError"; import { MimeType, toMimeType } from "Enums/MimeType"; -import { isTextFile, MimeTypeToFileTypes } from "Enums/FileType"; +import { isTextFile } from "Enums/FileType"; +import { MimeTypeToFileTypes } from "Enums/FileTypeMimeTypeMapping"; export class OpenAI extends BaseAIClass { @@ -332,14 +333,17 @@ export class OpenAI extends BaseAIClass { } if (!isPlainText && !this.isSupportedMimeType(mimeType)) { - contentBlocks.push([{ type: "input_text", text: `Unsupported mime type '${mimeType}': ${attachment.fileName}` }]); + contentBlocks.push({ type: "input_text", text: `Unsupported mime type '${mimeType}': ${attachment.fileName}` }); continue; } - contentBlocks.push({ - type: isPlainText || mimeType === MimeType.APPLICATION_PDF ? "input_file" : "input_image", - file_id: fileID - }); + contentBlocks.push( + { type: "input_text", text: `Binary data for ${attachment.fileName} follows in next message` }, + { + type: isPlainText || mimeType === MimeType.APPLICATION_PDF ? "input_file" : "input_image", + file_id: fileID + } + ); } return JSON.stringify([{ diff --git a/Components/ChatArea.svelte b/Components/ChatArea.svelte index 2eac1a2..12ada55 100644 --- a/Components/ChatArea.svelte +++ b/Components/ChatArea.svelte @@ -66,7 +66,7 @@ if (!shouldSettle) { padding = Math.max(padding, 25); } - chatAreaPaddingElement.style.padding = `${Math.max(0, padding / 2)}px`; + chatAreaPaddingElement.style.paddingBottom = `${Math.max(0, padding)}px`; tick().then(() => { if (behavior && (autoScroll || shouldSettle)) { @@ -259,16 +259,32 @@ {#if message.role === Role.User}
-
+
{@html content}
+ {#if message.references.length > 0} +
+ {#each message.references as reference} +
+
+
+
{reference.fileName}
+
{reference.size}MB
+
+
+ {/each} +
+ {/if}
{:else} {@const messageId = message.timestamp.getTime().toString()}
-
+
{#if currentStreamingMessageId === messageId}
{:else} @@ -290,7 +306,7 @@ {/if} -
+
{#if messages.length === 0}
@@ -372,7 +388,6 @@ border: var(--border-width) solid var(--background-modifier-border); border-radius: var(--radius-m); padding: 0px var(--size-4-2); - white-space: pre-wrap; } .message-bubble.assistant { @@ -382,6 +397,7 @@ .message-text-user { margin: var(--size-4-2); + white-space: pre-wrap; } .conversation-empty-state { @@ -390,7 +406,6 @@ font-size: var(--font-ui-medium); color: var(--text-muted); pointer-events: none; - user-select: none; } .streaming-content { @@ -399,7 +414,7 @@ } /* Streaming message styles */ - .fade-in-fast { + .content-fade-in { animation: reveal-fade 0.5s ease-in-out forwards; } @@ -413,4 +428,54 @@ transform: translateY(0); } } + + /* Message attachments styles */ + .message-attachments-container { + display: flex; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + gap: var(--size-4-1); + margin-bottom: var(--size-4-2); + } + + .message-attachments-container::-webkit-scrollbar { + display: none; + } + + .message-attachmanet { + display: grid; + grid-template-rows: var(--size-4-1) auto var(--size-4-1); + grid-template-columns: var(--size-4-1) auto var(--size-4-1) auto var(--size-4-1); + border: var(--border-width) solid var(--background-modifier-border); + border-radius: var(--radius-m); + flex-shrink: 0; + } + + .message-attachment-icon { + grid-row: 2; + grid-column: 2; + align-content: center; + } + + .message-attachment-info { + grid-row: 2; + grid-column: 4; + min-width: 40px; + overflow: hidden; + } + + .message-attachment-name { + display: inline-block; + white-space: nowrap; + width: 100%; + padding: 0; + font-size: var(--font-smaller); + } + + .message-attachment-size { + padding: 0; + font-size: var(--font-smallest); + color: var(--text-muted); + } \ No newline at end of file diff --git a/Components/ChatAttachments.svelte b/Components/ChatAttachments.svelte index 474b1ce..2a1d5f4 100644 --- a/Components/ChatAttachments.svelte +++ b/Components/ChatAttachments.svelte @@ -1,5 +1,6 @@