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
This commit is contained in:
Andrew Beal 2025-12-23 12:04:29 +00:00
parent 13cdd0a162
commit ea9cd211fc
22 changed files with 451 additions and 289 deletions

View file

@ -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)}`);

View file

@ -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: {

View file

@ -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,

View file

@ -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([{

View file

@ -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}
<div class="message-container {Role.User}" use:trackingAction={index}>
<div class="message-bubble {Role.User}">
<div class="message-text-user fade-in-fast" contenteditable="false">
<div class="message-text-user content-fade-in" contenteditable="false">
{@html content}
</div>
{#if message.references.length > 0}
<div class="message-attachments-container">
{#each message.references as reference}
<div class="message-attachmanet" aria-label="{reference.fileName}">
<div
class="message-attachment-icon"
></div>
<div class="message-attachment-info">
<div class="message-attachment-name">{reference.fileName}</div>
<div class="message-attachment-size">{reference.size}MB</div>
</div>
</div>
{/each}
</div>
{/if}
</div>
</div>
{:else}
{@const messageId = message.timestamp.getTime().toString()}
<div class="message-container {Role.Assistant}" use:trackingAction={index}>
<div class="message-bubble {Role.Assistant}">
<div class="markdown-content fade-in-fast {currentStreamingMessageId === messageId ? "streaming" : ""}">
<div class="markdown-content content-fade-in {currentStreamingMessageId === messageId ? "streaming" : ""}">
{#if currentStreamingMessageId === messageId}
<div use:streamingAction={messageId} class="streaming-content"></div>
{:else}
@ -290,7 +306,7 @@
<CancellationIndicator/>
{/if}
<div bind:this={chatAreaPaddingElement}></div>
<div bind:this={chatAreaPaddingElement} style:user-select=none></div>
{#if messages.length === 0}
<div class="conversation-empty-state">
@ -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);
}
</style>

View file

@ -1,5 +1,6 @@
<script lang="ts">
import type { Attachment } from "Conversations/Attachment";
import { MimeTypeToFileTypes } from "Enums/FileTypeMimeTypeMapping";
import { toMimeType } from "Enums/MimeType";
import { setIcon } from "obsidian";
import {
@ -8,7 +9,6 @@
isKnownFileType,
isTextFile,
isVideoFile,
MimeTypeToFileTypes,
} from "Enums/FileType";
export let attachments: Attachment[] = [];
@ -133,7 +133,9 @@
grid-template-columns: var(--size-4-2) auto var(--size-4-2) auto var(--size-4-2) auto var(--size-4-2);
border: var(--border-width) solid var(--background-modifier-border);
border-radius: var(--radius-m);
max-width: 15vw;
max-width: 33%;
min-width: 110px;
flex-shrink: 0;
}
.chat-attachmanet.selected {

View file

@ -257,7 +257,7 @@
const newAttachments = files.filter(file => !attachments.some(attachment => attachment.base64 === file.base64));
attachments = [...attachments, ...newAttachments];
if (!plainText) {
if (!plainText || plainText.includes("obsidian:")) {
return;
}

View file

@ -1,6 +1,7 @@
import type { AIProvider } from "Enums/ApiProvider";
export class Attachment {
public fileName: string;
public mimeType: string;
public base64: string;

View file

@ -5,7 +5,7 @@ import type { AIFunctionResponse } from "AIClasses/FunctionDefinitions/AIFunctio
import { Role } from "Enums/Role";
import { AIFunction } from "Enums/AIFunction";
import { isTextFile, toFileType } from "Enums/FileType";
import { FileTypeToMimeType } from "Enums/MimeType";
import { FileTypeToMimeType } from "Enums/FileTypeMimeTypeMapping";
export class Conversation {

View file

@ -1,6 +1,7 @@
import { Role } from "Enums/Role";
import { ApiErrorType } from "Types/ApiError";
import type { Attachment } from "./Attachment";
import type { Reference } from "./Reference";
type ConversationContentInit = {
role: Role;
@ -10,6 +11,7 @@ type ConversationContentInit = {
functionCall?: string;
functionResponse?: string;
attachments?: Attachment[];
references?: Reference[];
shouldDisplayContent?: boolean;
toolId?: string;
thoughtSignature?: string;
@ -24,6 +26,7 @@ export class ConversationContent {
public functionCall: string | undefined;
public functionResponse: string | undefined;
public attachments: Attachment[];
public references: Reference[];
public shouldDisplayContent: boolean;
public toolId: string | undefined;
public thoughtSignature: string | undefined;
@ -40,6 +43,7 @@ export class ConversationContent {
* @param init.functionCall - JSON string of the function call data (only set for function/tool calls)
* @param init.functionResponse - JSON string of the function call response data (only set for function/tool responses)
* @param init.attachments - Array of file attachments associated with this message (defaults to empty array)
* @param init.references - Array of file references, used to display attachment's to the user associated with attachments
* @param init.shouldDisplayContent - Whether this content should be displayed in the UI (defaults to true, false for system-generated messages)
* @param init.toolId - Unique identifier for tool calls/responses (used to match calls with their responses)
* @param init.thoughtSignature - Gemini-specific thought signature for extended thinking
@ -53,6 +57,7 @@ export class ConversationContent {
this.functionCall = init.functionCall;
this.functionResponse = init.functionResponse;
this.attachments = init.attachments ?? [];
this.references = init.references ?? [];
this.shouldDisplayContent = init.shouldDisplayContent ?? true;
this.toolId = init.toolId;
this.thoughtSignature = init.thoughtSignature;
@ -74,6 +79,7 @@ export class ConversationContent {
functionCall?: string;
functionResponse?: string;
attachments?: unknown[];
references?: unknown[];
shouldDisplayContent?: boolean;
toolId?: string;
thoughtSignature?: string;
@ -92,6 +98,7 @@ export class ConversationContent {
(!("functionCall" in data) || typeof data.functionCall === "string") &&
(!("functionResponse" in data) || typeof data.functionResponse === "string") &&
(!("attachments" in data) || Array.isArray(data.attachments)) &&
(!("references" in data) || Array.isArray(data.references)) &&
(!("shouldDisplayContent" in data) || typeof data.shouldDisplayContent === "boolean") &&
(!("toolId" in data) || typeof data.toolId === "string") &&
(!("thoughtSignature" in data) || typeof data.thoughtSignature === "string") &&

View file

@ -0,0 +1,24 @@
export class Reference {
public fileName: string;
public size: number;
public constructor(fileName: string, size: number) {
this.fileName = fileName;
this.size = size;
}
public static isReferenceData(this: void, data: unknown): data is {
fileName: string;
size: number;
} {
return (
data !== null &&
typeof data === "object" &&
"fileName" in data &&
"size" in data &&
typeof data.fileName === "string" &&
typeof data.size === "number"
);
}
}

View file

@ -1,5 +1,3 @@
import { MimeType } from "./MimeType";
export enum FileType {
// ----- Types officially supported by Obsidian -----
MD = "md",
@ -282,103 +280,4 @@ export function isVideoFile(extension: string) {
|| isFileType(extension, FileType.MP4)
|| isFileType(extension, FileType.OGV)
|| isFileType(extension, FileType.WEBM_VIDEO);
}
export const MimeTypeToFileTypes: Record<MimeType, FileType[]> = {
// Text
[MimeType.TEXT_PLAIN]: [FileType.BASE, FileType.TXT, FileType.TEXT, FileType.INI, FileType.CFG, FileType.CONF, FileType.ENV, FileType.PROPERTIES, FileType.LOG, FileType.GITIGNORE, FileType.GITATTRIBUTES, FileType.EDITORCONFIG, FileType.PRETTIERRC, FileType.ESLINTRC, FileType.BABELRC, FileType.NPMRC, FileType.YARNRC, FileType.DIFF, FileType.PATCH],
[MimeType.TEXT_MARKDOWN]: [FileType.MD],
[MimeType.TEXT_MD]: [FileType.MD],
[MimeType.TEXT_HTML]: [FileType.HTML],
[MimeType.TEXT_CSS]: [FileType.CSS],
[MimeType.TEXT_CSV]: [FileType.CSV],
[MimeType.TEXT_TSV]: [FileType.TSV],
[MimeType.TEXT_JAVASCRIPT]: [FileType.JS, FileType.MJS, FileType.CJS],
[MimeType.TEXT_TYPESCRIPT]: [FileType.TS],
[MimeType.TEXT_JSX]: [FileType.JSX],
[MimeType.TEXT_TSX]: [FileType.TSX],
[MimeType.TEXT_SASS]: [FileType.SASS],
[MimeType.TEXT_SCSS]: [FileType.SCSS],
[MimeType.TEXT_VUE]: [FileType.VUE],
[MimeType.TEXT_SVELTE]: [FileType.SVELTE],
[MimeType.TEXT_PYTHON]: [FileType.PY],
[MimeType.TEXT_RUBY]: [FileType.RB],
[MimeType.TEXT_PHP]: [FileType.PHP],
[MimeType.TEXT_JAVA]: [FileType.JAVA],
[MimeType.TEXT_JAVA_SOURCE]: [FileType.JAVA],
[MimeType.TEXT_C]: [FileType.C, FileType.H],
[MimeType.TEXT_CSRC]: [FileType.C],
[MimeType.TEXT_CHDR]: [FileType.H],
[MimeType.TEXT_CPP]: [FileType.CPP, FileType.CC, FileType.CXX, FileType.HPP, FileType.HXX],
[MimeType.TEXT_CPPSRC]: [FileType.CPP, FileType.CC, FileType.CXX],
[MimeType.TEXT_CPPHDR]: [FileType.HPP, FileType.HXX],
[MimeType.TEXT_CSHARP]: [FileType.CS],
[MimeType.TEXT_GO]: [FileType.GO],
[MimeType.TEXT_RUST]: [FileType.RS],
[MimeType.TEXT_SWIFT]: [FileType.SWIFT],
[MimeType.TEXT_KOTLIN]: [FileType.KT, FileType.KTS],
[MimeType.TEXT_SCALA]: [FileType.SCALA],
[MimeType.TEXT_R]: [FileType.R, FileType.R_UPPER],
[MimeType.TEXT_JULIA]: [FileType.JL],
[MimeType.TEXT_LUA]: [FileType.LUA],
[MimeType.TEXT_PERL]: [FileType.PL, FileType.PM],
[MimeType.TEXT_DART]: [FileType.DART],
[MimeType.TEXT_SHELL]: [FileType.SH, FileType.BASH, FileType.ZSH, FileType.FISH],
[MimeType.TEXT_SH]: [FileType.SH, FileType.BASH, FileType.ZSH, FileType.FISH],
[MimeType.TEXT_BATCH]: [FileType.BAT, FileType.CMD],
[MimeType.TEXT_POWERSHELL]: [FileType.PS1],
[MimeType.TEXT_SQL]: [FileType.SQL],
[MimeType.TEXT_GRAPHQL]: [FileType.GRAPHQL, FileType.GQL],
[MimeType.TEXT_XML]: [FileType.XML],
[MimeType.TEXT_YAML]: [FileType.YAML, FileType.YML],
// Application
[MimeType.APPLICATION_JSON]: [FileType.CANVAS, FileType.JSON],
[MimeType.APPLICATION_XML]: [FileType.XML],
[MimeType.APPLICATION_PDF]: [FileType.PDF],
[MimeType.APPLICATION_RTF]: [FileType.RTF],
[MimeType.APPLICATION_YAML]: [FileType.YAML, FileType.YML],
[MimeType.APPLICATION_TOML]: [FileType.TOML],
[MimeType.APPLICATION_TEX]: [FileType.TEX],
[MimeType.APPLICATION_LATEX]: [FileType.LATEX],
[MimeType.APPLICATION_MAKEFILE]: [FileType.MAKEFILE, FileType.MK],
[MimeType.APPLICATION_GRADLE]: [FileType.GRADLE],
[MimeType.APPLICATION_DOCKERFILE]: [FileType.DOCKERFILE],
[MimeType.APPLICATION_PYTHON_CODE]: [FileType.PY],
[MimeType.APPLICATION_JAVASCRIPT]: [FileType.JS, FileType.MJS, FileType.CJS],
[MimeType.APPLICATION_TYPESCRIPT]: [FileType.TS],
[MimeType.APPLICATION_SH]: [FileType.SH, FileType.BASH, FileType.ZSH, FileType.FISH],
// Markup formats
[MimeType.TEXT_RST]: [FileType.RST],
[MimeType.TEXT_ASCIIDOC]: [FileType.ADOC, FileType.ASCIIDOC],
[MimeType.TEXT_ORG]: [FileType.ORG],
[MimeType.TEXT_TEXTILE]: [FileType.TEXTILE],
// Images
[MimeType.IMAGE_AVIF]: [FileType.AVIF],
[MimeType.IMAGE_BMP]: [FileType.BMP],
[MimeType.IMAGE_GIF]: [FileType.GIF],
[MimeType.IMAGE_JPEG]: [FileType.JPEG, FileType.JPG],
[MimeType.IMAGE_PNG]: [FileType.PNG],
[MimeType.IMAGE_SVG]: [FileType.SVG],
[MimeType.IMAGE_WEBP]: [FileType.WEBP],
// Audio
[MimeType.AUDIO_FLAC]: [FileType.FLAC],
[MimeType.AUDIO_MP4]: [FileType.M4A],
[MimeType.AUDIO_MPEG]: [FileType.MP3],
[MimeType.AUDIO_OGG]: [FileType.OGG],
[MimeType.AUDIO_WAV]: [FileType.WAV],
[MimeType.AUDIO_WEBM]: [FileType.WEBM_AUDIO],
// Video
[MimeType.VIDEO_3GPP]: [FileType.THREE_GP],
[MimeType.VIDEO_MATROSKA]: [FileType.MKV],
[MimeType.VIDEO_QUICKTIME]: [FileType.MOV],
[MimeType.VIDEO_MP4]: [FileType.MP4],
[MimeType.VIDEO_OGG]: [FileType.OGV],
[MimeType.VIDEO_WEBM]: [FileType.WEBM, FileType.WEBM_VIDEO],
[MimeType.UNKNOWN]: [FileType.UNKNOWN]
};
}

View file

@ -0,0 +1,249 @@
import { FileType } from "./FileType";
import { MimeType } from "./MimeType";
export const FileTypeToMimeType: Record<FileType, MimeType> = {
// ----- Types officially supported by Obsidian -----
[FileType.MD]: MimeType.TEXT_MARKDOWN,
[FileType.BASE]: MimeType.TEXT_PLAIN,
[FileType.CANVAS]: MimeType.APPLICATION_JSON,
// images
[FileType.AVIF]: MimeType.IMAGE_AVIF,
[FileType.BMP]: MimeType.IMAGE_BMP,
[FileType.GIF]: MimeType.IMAGE_GIF,
[FileType.JPEG]: MimeType.IMAGE_JPEG,
[FileType.JPG]: MimeType.IMAGE_JPEG,
[FileType.PNG]: MimeType.IMAGE_PNG,
[FileType.SVG]: MimeType.IMAGE_SVG,
[FileType.WEBP]: MimeType.IMAGE_WEBP,
// Note: WEBM is used for both audio and video, defaulting to video
[FileType.WEBM]: MimeType.VIDEO_WEBM,
// audio
[FileType.FLAC]: MimeType.AUDIO_FLAC,
[FileType.M4A]: MimeType.AUDIO_MP4,
[FileType.MP3]: MimeType.AUDIO_MPEG,
[FileType.OGG]: MimeType.AUDIO_OGG,
[FileType.WAV]: MimeType.AUDIO_WAV,
[FileType.THREE_GP]: MimeType.VIDEO_3GPP,
// video
[FileType.MKV]: MimeType.VIDEO_MATROSKA,
[FileType.MOV]: MimeType.VIDEO_QUICKTIME,
[FileType.MP4]: MimeType.VIDEO_MP4,
[FileType.OGV]: MimeType.VIDEO_OGG,
// pdf
[FileType.PDF]: MimeType.APPLICATION_PDF,
// ----- Types not officially supported by Obsidian -----
// Plain text
[FileType.TXT]: MimeType.TEXT_PLAIN,
[FileType.TEXT]: MimeType.TEXT_PLAIN,
// Data formats
[FileType.JSON]: MimeType.APPLICATION_JSON,
[FileType.XML]: MimeType.APPLICATION_XML,
[FileType.CSV]: MimeType.TEXT_CSV,
[FileType.TSV]: MimeType.TEXT_TSV,
[FileType.YAML]: MimeType.APPLICATION_YAML,
[FileType.YML]: MimeType.APPLICATION_YAML,
[FileType.TOML]: MimeType.APPLICATION_TOML,
// Web languages
[FileType.HTML]: MimeType.TEXT_HTML,
[FileType.CSS]: MimeType.TEXT_CSS,
[FileType.SASS]: MimeType.TEXT_SASS,
[FileType.SCSS]: MimeType.TEXT_SCSS,
// JavaScript/TypeScript ecosystem
[FileType.JS]: MimeType.TEXT_JAVASCRIPT,
[FileType.MJS]: MimeType.TEXT_JAVASCRIPT,
[FileType.CJS]: MimeType.TEXT_JAVASCRIPT,
[FileType.TS]: MimeType.TEXT_TYPESCRIPT,
[FileType.JSX]: MimeType.TEXT_JSX,
[FileType.TSX]: MimeType.TEXT_TSX,
[FileType.VUE]: MimeType.TEXT_VUE,
[FileType.SVELTE]: MimeType.TEXT_SVELTE,
[FileType.ASTRO]: MimeType.TEXT_PLAIN,
// Shell/Scripting
[FileType.SH]: MimeType.TEXT_SHELL,
[FileType.BASH]: MimeType.TEXT_SHELL,
[FileType.ZSH]: MimeType.TEXT_SHELL,
[FileType.FISH]: MimeType.TEXT_SHELL,
[FileType.BAT]: MimeType.TEXT_BATCH,
[FileType.CMD]: MimeType.TEXT_BATCH,
[FileType.PS1]: MimeType.TEXT_POWERSHELL,
// Programming languages
[FileType.PY]: MimeType.TEXT_PYTHON,
[FileType.RB]: MimeType.TEXT_RUBY,
[FileType.PHP]: MimeType.TEXT_PHP,
[FileType.JAVA]: MimeType.TEXT_JAVA,
[FileType.C]: MimeType.TEXT_C,
[FileType.CPP]: MimeType.TEXT_CPP,
[FileType.CC]: MimeType.TEXT_CPP,
[FileType.CXX]: MimeType.TEXT_CPP,
[FileType.H]: MimeType.TEXT_C,
[FileType.HPP]: MimeType.TEXT_CPP,
[FileType.HXX]: MimeType.TEXT_CPP,
[FileType.CS]: MimeType.TEXT_CSHARP,
[FileType.GO]: MimeType.TEXT_GO,
[FileType.RS]: MimeType.TEXT_RUST,
[FileType.SWIFT]: MimeType.TEXT_SWIFT,
[FileType.KT]: MimeType.TEXT_KOTLIN,
[FileType.KTS]: MimeType.TEXT_KOTLIN,
[FileType.SCALA]: MimeType.TEXT_SCALA,
[FileType.M]: MimeType.TEXT_PLAIN,
[FileType.R]: MimeType.TEXT_R,
[FileType.R_UPPER]: MimeType.TEXT_R,
[FileType.JL]: MimeType.TEXT_JULIA,
[FileType.LUA]: MimeType.TEXT_LUA,
[FileType.PL]: MimeType.TEXT_PERL,
[FileType.PM]: MimeType.TEXT_PERL,
[FileType.DART]: MimeType.TEXT_DART,
// Markup & Documentation
[FileType.TEX]: MimeType.APPLICATION_TEX,
[FileType.LATEX]: MimeType.APPLICATION_LATEX,
[FileType.RST]: MimeType.TEXT_RST,
[FileType.ADOC]: MimeType.TEXT_ASCIIDOC,
[FileType.ASCIIDOC]: MimeType.TEXT_ASCIIDOC,
[FileType.ORG]: MimeType.TEXT_ORG,
[FileType.TEXTILE]: MimeType.TEXT_TEXTILE,
[FileType.RTF]: MimeType.APPLICATION_RTF,
// Configuration files
[FileType.INI]: MimeType.TEXT_PLAIN,
[FileType.CFG]: MimeType.TEXT_PLAIN,
[FileType.CONF]: MimeType.TEXT_PLAIN,
[FileType.ENV]: MimeType.TEXT_PLAIN,
[FileType.PROPERTIES]: MimeType.TEXT_PLAIN,
[FileType.GITIGNORE]: MimeType.TEXT_PLAIN,
[FileType.GITATTRIBUTES]: MimeType.TEXT_PLAIN,
[FileType.EDITORCONFIG]: MimeType.TEXT_PLAIN,
[FileType.PRETTIERRC]: MimeType.TEXT_PLAIN,
[FileType.ESLINTRC]: MimeType.TEXT_PLAIN,
[FileType.BABELRC]: MimeType.TEXT_PLAIN,
[FileType.NPMRC]: MimeType.TEXT_PLAIN,
[FileType.YARNRC]: MimeType.TEXT_PLAIN,
[FileType.DOCKERFILE]: MimeType.APPLICATION_DOCKERFILE,
// Query languages
[FileType.SQL]: MimeType.TEXT_SQL,
[FileType.GRAPHQL]: MimeType.TEXT_GRAPHQL,
[FileType.GQL]: MimeType.TEXT_GRAPHQL,
// Build & Development
[FileType.MAKEFILE]: MimeType.APPLICATION_MAKEFILE,
[FileType.MK]: MimeType.APPLICATION_MAKEFILE,
[FileType.GRADLE]: MimeType.APPLICATION_GRADLE,
[FileType.DIFF]: MimeType.TEXT_PLAIN,
[FileType.PATCH]: MimeType.TEXT_PLAIN,
// Logs
[FileType.LOG]: MimeType.TEXT_PLAIN,
[FileType.UNKNOWN]: MimeType.UNKNOWN
};
export const MimeTypeToFileTypes: Record<MimeType, FileType[]> = {
// Text
[MimeType.TEXT_PLAIN]: [FileType.BASE, FileType.TXT, FileType.TEXT, FileType.INI, FileType.CFG, FileType.CONF, FileType.ENV, FileType.PROPERTIES, FileType.LOG, FileType.GITIGNORE, FileType.GITATTRIBUTES, FileType.EDITORCONFIG, FileType.PRETTIERRC, FileType.ESLINTRC, FileType.BABELRC, FileType.NPMRC, FileType.YARNRC, FileType.DIFF, FileType.PATCH],
[MimeType.TEXT_MARKDOWN]: [FileType.MD],
[MimeType.TEXT_MD]: [FileType.MD],
[MimeType.TEXT_HTML]: [FileType.HTML],
[MimeType.TEXT_CSS]: [FileType.CSS],
[MimeType.TEXT_CSV]: [FileType.CSV],
[MimeType.TEXT_TSV]: [FileType.TSV],
[MimeType.TEXT_JAVASCRIPT]: [FileType.JS, FileType.MJS, FileType.CJS],
[MimeType.TEXT_TYPESCRIPT]: [FileType.TS],
[MimeType.TEXT_JSX]: [FileType.JSX],
[MimeType.TEXT_TSX]: [FileType.TSX],
[MimeType.TEXT_SASS]: [FileType.SASS],
[MimeType.TEXT_SCSS]: [FileType.SCSS],
[MimeType.TEXT_VUE]: [FileType.VUE],
[MimeType.TEXT_SVELTE]: [FileType.SVELTE],
[MimeType.TEXT_PYTHON]: [FileType.PY],
[MimeType.TEXT_RUBY]: [FileType.RB],
[MimeType.TEXT_PHP]: [FileType.PHP],
[MimeType.TEXT_JAVA]: [FileType.JAVA],
[MimeType.TEXT_JAVA_SOURCE]: [FileType.JAVA],
[MimeType.TEXT_C]: [FileType.C, FileType.H],
[MimeType.TEXT_CSRC]: [FileType.C],
[MimeType.TEXT_CHDR]: [FileType.H],
[MimeType.TEXT_CPP]: [FileType.CPP, FileType.CC, FileType.CXX, FileType.HPP, FileType.HXX],
[MimeType.TEXT_CPPSRC]: [FileType.CPP, FileType.CC, FileType.CXX],
[MimeType.TEXT_CPPHDR]: [FileType.HPP, FileType.HXX],
[MimeType.TEXT_CSHARP]: [FileType.CS],
[MimeType.TEXT_GO]: [FileType.GO],
[MimeType.TEXT_RUST]: [FileType.RS],
[MimeType.TEXT_SWIFT]: [FileType.SWIFT],
[MimeType.TEXT_KOTLIN]: [FileType.KT, FileType.KTS],
[MimeType.TEXT_SCALA]: [FileType.SCALA],
[MimeType.TEXT_R]: [FileType.R, FileType.R_UPPER],
[MimeType.TEXT_JULIA]: [FileType.JL],
[MimeType.TEXT_LUA]: [FileType.LUA],
[MimeType.TEXT_PERL]: [FileType.PL, FileType.PM],
[MimeType.TEXT_DART]: [FileType.DART],
[MimeType.TEXT_SHELL]: [FileType.SH, FileType.BASH, FileType.ZSH, FileType.FISH],
[MimeType.TEXT_SH]: [FileType.SH, FileType.BASH, FileType.ZSH, FileType.FISH],
[MimeType.TEXT_BATCH]: [FileType.BAT, FileType.CMD],
[MimeType.TEXT_POWERSHELL]: [FileType.PS1],
[MimeType.TEXT_SQL]: [FileType.SQL],
[MimeType.TEXT_GRAPHQL]: [FileType.GRAPHQL, FileType.GQL],
[MimeType.TEXT_XML]: [FileType.XML],
[MimeType.TEXT_YAML]: [FileType.YAML, FileType.YML],
// Application
[MimeType.APPLICATION_JSON]: [FileType.CANVAS, FileType.JSON],
[MimeType.APPLICATION_XML]: [FileType.XML],
[MimeType.APPLICATION_PDF]: [FileType.PDF],
[MimeType.APPLICATION_RTF]: [FileType.RTF],
[MimeType.APPLICATION_YAML]: [FileType.YAML, FileType.YML],
[MimeType.APPLICATION_TOML]: [FileType.TOML],
[MimeType.APPLICATION_TEX]: [FileType.TEX],
[MimeType.APPLICATION_LATEX]: [FileType.LATEX],
[MimeType.APPLICATION_MAKEFILE]: [FileType.MAKEFILE, FileType.MK],
[MimeType.APPLICATION_GRADLE]: [FileType.GRADLE],
[MimeType.APPLICATION_DOCKERFILE]: [FileType.DOCKERFILE],
[MimeType.APPLICATION_PYTHON_CODE]: [FileType.PY],
[MimeType.APPLICATION_JAVASCRIPT]: [FileType.JS, FileType.MJS, FileType.CJS],
[MimeType.APPLICATION_TYPESCRIPT]: [FileType.TS],
[MimeType.APPLICATION_SH]: [FileType.SH, FileType.BASH, FileType.ZSH, FileType.FISH],
// Markup formats
[MimeType.TEXT_RST]: [FileType.RST],
[MimeType.TEXT_ASCIIDOC]: [FileType.ADOC, FileType.ASCIIDOC],
[MimeType.TEXT_ORG]: [FileType.ORG],
[MimeType.TEXT_TEXTILE]: [FileType.TEXTILE],
// Images
[MimeType.IMAGE_AVIF]: [FileType.AVIF],
[MimeType.IMAGE_BMP]: [FileType.BMP],
[MimeType.IMAGE_GIF]: [FileType.GIF],
[MimeType.IMAGE_JPEG]: [FileType.JPEG, FileType.JPG],
[MimeType.IMAGE_PNG]: [FileType.PNG],
[MimeType.IMAGE_SVG]: [FileType.SVG],
[MimeType.IMAGE_WEBP]: [FileType.WEBP],
// Audio
[MimeType.AUDIO_FLAC]: [FileType.FLAC],
[MimeType.AUDIO_MP4]: [FileType.M4A],
[MimeType.AUDIO_MPEG]: [FileType.MP3],
[MimeType.AUDIO_OGG]: [FileType.OGG],
[MimeType.AUDIO_WAV]: [FileType.WAV],
[MimeType.AUDIO_WEBM]: [FileType.WEBM_AUDIO],
// Video
[MimeType.VIDEO_3GPP]: [FileType.THREE_GP],
[MimeType.VIDEO_MATROSKA]: [FileType.MKV],
[MimeType.VIDEO_QUICKTIME]: [FileType.MOV],
[MimeType.VIDEO_MP4]: [FileType.MP4],
[MimeType.VIDEO_OGG]: [FileType.OGV],
[MimeType.VIDEO_WEBM]: [FileType.WEBM, FileType.WEBM_VIDEO],
[MimeType.UNKNOWN]: [FileType.UNKNOWN]
};

View file

@ -1,5 +1,3 @@
import { FileType } from "./FileType";
export enum MimeType {
// Text
TEXT_PLAIN = "text/plain",
@ -109,151 +107,3 @@ export function toMimeType(mimeType: string): MimeType {
export function isKnownMimeType(value: string): value is MimeType {
return Object.values(MimeType).includes(value as MimeType) && value !== MimeType.UNKNOWN.toString();
}
export const FileTypeToMimeType: Record<FileType, MimeType> = {
// ----- Types officially supported by Obsidian -----
[FileType.MD]: MimeType.TEXT_MARKDOWN,
[FileType.BASE]: MimeType.TEXT_PLAIN,
[FileType.CANVAS]: MimeType.APPLICATION_JSON,
// images
[FileType.AVIF]: MimeType.IMAGE_AVIF,
[FileType.BMP]: MimeType.IMAGE_BMP,
[FileType.GIF]: MimeType.IMAGE_GIF,
[FileType.JPEG]: MimeType.IMAGE_JPEG,
[FileType.JPG]: MimeType.IMAGE_JPEG,
[FileType.PNG]: MimeType.IMAGE_PNG,
[FileType.SVG]: MimeType.IMAGE_SVG,
[FileType.WEBP]: MimeType.IMAGE_WEBP,
// Note: WEBM is used for both audio and video, defaulting to video
[FileType.WEBM]: MimeType.VIDEO_WEBM,
// audio
[FileType.FLAC]: MimeType.AUDIO_FLAC,
[FileType.M4A]: MimeType.AUDIO_MP4,
[FileType.MP3]: MimeType.AUDIO_MPEG,
[FileType.OGG]: MimeType.AUDIO_OGG,
[FileType.WAV]: MimeType.AUDIO_WAV,
[FileType.THREE_GP]: MimeType.VIDEO_3GPP,
// video
[FileType.MKV]: MimeType.VIDEO_MATROSKA,
[FileType.MOV]: MimeType.VIDEO_QUICKTIME,
[FileType.MP4]: MimeType.VIDEO_MP4,
[FileType.OGV]: MimeType.VIDEO_OGG,
// pdf
[FileType.PDF]: MimeType.APPLICATION_PDF,
// ----- Types not officially supported by Obsidian -----
// Plain text
[FileType.TXT]: MimeType.TEXT_PLAIN,
[FileType.TEXT]: MimeType.TEXT_PLAIN,
// Data formats
[FileType.JSON]: MimeType.APPLICATION_JSON,
[FileType.XML]: MimeType.APPLICATION_XML,
[FileType.CSV]: MimeType.TEXT_CSV,
[FileType.TSV]: MimeType.TEXT_TSV,
[FileType.YAML]: MimeType.APPLICATION_YAML,
[FileType.YML]: MimeType.APPLICATION_YAML,
[FileType.TOML]: MimeType.APPLICATION_TOML,
// Web languages
[FileType.HTML]: MimeType.TEXT_HTML,
[FileType.CSS]: MimeType.TEXT_CSS,
[FileType.SASS]: MimeType.TEXT_SASS,
[FileType.SCSS]: MimeType.TEXT_SCSS,
// JavaScript/TypeScript ecosystem
[FileType.JS]: MimeType.TEXT_JAVASCRIPT,
[FileType.MJS]: MimeType.TEXT_JAVASCRIPT,
[FileType.CJS]: MimeType.TEXT_JAVASCRIPT,
[FileType.TS]: MimeType.TEXT_TYPESCRIPT,
[FileType.JSX]: MimeType.TEXT_JSX,
[FileType.TSX]: MimeType.TEXT_TSX,
[FileType.VUE]: MimeType.TEXT_VUE,
[FileType.SVELTE]: MimeType.TEXT_SVELTE,
[FileType.ASTRO]: MimeType.TEXT_PLAIN,
// Shell/Scripting
[FileType.SH]: MimeType.TEXT_SHELL,
[FileType.BASH]: MimeType.TEXT_SHELL,
[FileType.ZSH]: MimeType.TEXT_SHELL,
[FileType.FISH]: MimeType.TEXT_SHELL,
[FileType.BAT]: MimeType.TEXT_BATCH,
[FileType.CMD]: MimeType.TEXT_BATCH,
[FileType.PS1]: MimeType.TEXT_POWERSHELL,
// Programming languages
[FileType.PY]: MimeType.TEXT_PYTHON,
[FileType.RB]: MimeType.TEXT_RUBY,
[FileType.PHP]: MimeType.TEXT_PHP,
[FileType.JAVA]: MimeType.TEXT_JAVA,
[FileType.C]: MimeType.TEXT_C,
[FileType.CPP]: MimeType.TEXT_CPP,
[FileType.CC]: MimeType.TEXT_CPP,
[FileType.CXX]: MimeType.TEXT_CPP,
[FileType.H]: MimeType.TEXT_C,
[FileType.HPP]: MimeType.TEXT_CPP,
[FileType.HXX]: MimeType.TEXT_CPP,
[FileType.CS]: MimeType.TEXT_CSHARP,
[FileType.GO]: MimeType.TEXT_GO,
[FileType.RS]: MimeType.TEXT_RUST,
[FileType.SWIFT]: MimeType.TEXT_SWIFT,
[FileType.KT]: MimeType.TEXT_KOTLIN,
[FileType.KTS]: MimeType.TEXT_KOTLIN,
[FileType.SCALA]: MimeType.TEXT_SCALA,
[FileType.M]: MimeType.TEXT_PLAIN,
[FileType.R]: MimeType.TEXT_R,
[FileType.R_UPPER]: MimeType.TEXT_R,
[FileType.JL]: MimeType.TEXT_JULIA,
[FileType.LUA]: MimeType.TEXT_LUA,
[FileType.PL]: MimeType.TEXT_PERL,
[FileType.PM]: MimeType.TEXT_PERL,
[FileType.DART]: MimeType.TEXT_DART,
// Markup & Documentation
[FileType.TEX]: MimeType.APPLICATION_TEX,
[FileType.LATEX]: MimeType.APPLICATION_LATEX,
[FileType.RST]: MimeType.TEXT_RST,
[FileType.ADOC]: MimeType.TEXT_ASCIIDOC,
[FileType.ASCIIDOC]: MimeType.TEXT_ASCIIDOC,
[FileType.ORG]: MimeType.TEXT_ORG,
[FileType.TEXTILE]: MimeType.TEXT_TEXTILE,
[FileType.RTF]: MimeType.APPLICATION_RTF,
// Configuration files
[FileType.INI]: MimeType.TEXT_PLAIN,
[FileType.CFG]: MimeType.TEXT_PLAIN,
[FileType.CONF]: MimeType.TEXT_PLAIN,
[FileType.ENV]: MimeType.TEXT_PLAIN,
[FileType.PROPERTIES]: MimeType.TEXT_PLAIN,
[FileType.GITIGNORE]: MimeType.TEXT_PLAIN,
[FileType.GITATTRIBUTES]: MimeType.TEXT_PLAIN,
[FileType.EDITORCONFIG]: MimeType.TEXT_PLAIN,
[FileType.PRETTIERRC]: MimeType.TEXT_PLAIN,
[FileType.ESLINTRC]: MimeType.TEXT_PLAIN,
[FileType.BABELRC]: MimeType.TEXT_PLAIN,
[FileType.NPMRC]: MimeType.TEXT_PLAIN,
[FileType.YARNRC]: MimeType.TEXT_PLAIN,
[FileType.DOCKERFILE]: MimeType.APPLICATION_DOCKERFILE,
// Query languages
[FileType.SQL]: MimeType.TEXT_SQL,
[FileType.GRAPHQL]: MimeType.TEXT_GRAPHQL,
[FileType.GQL]: MimeType.TEXT_GRAPHQL,
// Build & Development
[FileType.MAKEFILE]: MimeType.APPLICATION_MAKEFILE,
[FileType.MK]: MimeType.APPLICATION_MAKEFILE,
[FileType.GRADLE]: MimeType.APPLICATION_GRADLE,
[FileType.DIFF]: MimeType.TEXT_PLAIN,
[FileType.PATCH]: MimeType.TEXT_PLAIN,
// Logs
[FileType.LOG]: MimeType.TEXT_PLAIN,
[FileType.UNKNOWN]: MimeType.UNKNOWN
};

View file

@ -16,6 +16,7 @@ import { AbortService } from "./AbortService";
import { Exception } from "Helpers/Exception";
import { Copy } from "Enums/Copy";
import type { Attachment } from "Conversations/Attachment";
import { Reference } from "Conversations/Reference";
export interface IChatServiceCallbacks {
onSubmit: () => void;
@ -68,11 +69,12 @@ export class ChatService {
await this.abortService.abortableOperation(async () => {
const firstMessage = conversation.contents.length === 0;
conversation.contents.push(new ConversationContent({
const conversationContent = new ConversationContent({
role: Role.User,
content: formattedRequest,
displayContent: userRequest
}));
});
conversation.contents.push(conversationContent);
await this.saveConversation(conversation);
if (attachments.length > 0) {
@ -82,6 +84,9 @@ export class ChatService {
attachments: attachments,
shouldDisplayContent: false
}));
conversationContent.references = attachments.map(attachment =>
new Reference(attachment.fileName, attachment.approximateFileSizeMB()));
}
callbacks.onSubmit();

View file

@ -7,6 +7,7 @@ import { ConversationContent } from "Conversations/ConversationContent";
import { Attachment } from "Conversations/Attachment";
import { Exception } from "Helpers/Exception";
import type { IAIFileService } from "AIClasses/IAIFileService";
import { Reference } from "Conversations/Reference";
export class ConversationFileSystemService {
@ -54,6 +55,7 @@ export class ConversationFileSystemService {
functionCall: content.functionCall,
functionResponse: content.functionResponse,
attachments: content.attachments,
references: content.references,
shouldDisplayContent: content.shouldDisplayContent,
toolId: content.toolId,
thoughtSignature: content.thoughtSignature,
@ -150,6 +152,7 @@ export class ConversationFileSystemService {
conversation.contents = result.contents.map(content => {
// Reconstruct Attachment instances from plain objects
const attachments = this.deserializeAttachments(content.attachments);
const references = this.deserializeReferences(content.references);
return new ConversationContent({
role: content.role,
@ -159,6 +162,7 @@ export class ConversationFileSystemService {
functionCall: content.functionCall,
functionResponse: content.functionResponse,
attachments: attachments,
references: references,
shouldDisplayContent: content.shouldDisplayContent,
toolId: content.toolId,
thoughtSignature: content.thoughtSignature,
@ -185,6 +189,19 @@ export class ConversationFileSystemService {
));
}
private deserializeReferences(referencesData: unknown): Reference[] {
if (!Array.isArray(referencesData)) {
return [];
}
return referencesData
.filter(Reference.isReferenceData)
.map(referenceData => new Reference(
referenceData.fileName,
referenceData.size
));
}
private async attemptAIFileDeletion(conversation: Conversation) {
try {
await this.aiFileService?.refreshCache();

View file

@ -2,7 +2,7 @@ import { Exception } from "Helpers/Exception";
import { isSearchTriggerElement } from "../Enums/SearchTrigger";
import { Attachment } from "Conversations/Attachment";
import { arrayBufferToBase64 } from "obsidian";
import { FileTypeToMimeType } from "Enums/MimeType";
import { FileTypeToMimeType } from "Enums/FileTypeMimeTypeMapping";
import * as path from "path-browserify";
import { pathExtname } from "Helpers/Helpers";
import { FileType, toFileType } from "Enums/FileType";
@ -45,8 +45,10 @@ export class InputService {
}
}
// get text as sometimes uri's come through as plain text
const textUriList = this.getTextFromDataTransfer(dataTransfer);
const uriList = dataTransfer.getData("text/uri-list");
const uris = uriList.split("\n").map(uri => uri.trim())
const uris = `${uriList}\n${textUriList}`.split("\n").unique().map(uri => uri.trim())
.filter(uri => uri.length > 0 && !uri.startsWith("#"));
for (const uri of uris) {

View file

@ -61,6 +61,17 @@
/* CSS Variables for Common Components */
/* ============================== */
.fading-stream {
opacity: 0;
animation: fadeIn 1s forwards;
}
@keyframes fadeIn {
to {
opacity: 1;
}
}
.transparent-button {
border: none !important;
outline: none !important;

View file

@ -1026,7 +1026,7 @@ describe('Claude', () => {
expect(parsed).toHaveLength(1);
expect(parsed[0]).toEqual({
type: 'text',
text: 'Unsupported image format: photo.bmp'
text: 'Unsupported mime type \'image/bmp\': photo.bmp'
});
});
@ -1132,7 +1132,7 @@ describe('Claude', () => {
expect(parsed[1].type).toBe('image');
expect(parsed[2]).toEqual({
type: 'text',
text: 'Unsupported image format: bad.bmp'
text: 'Unsupported mime type \'image/bmp\': bad.bmp'
});
expect(parsed[3].type).toBe('text');
expect(parsed[3].text).toBe('doc.pdf');

View file

@ -1161,7 +1161,7 @@ describe('Gemini', () => {
expect(parsed).toHaveLength(1);
expect(parsed[0]).toEqual({
text: 'Unsupported image format: animation.gif'
text: 'Unsupported mime type \'image/gif\': animation.gif'
});
});
@ -1180,7 +1180,7 @@ describe('Gemini', () => {
expect(parsed).toHaveLength(1);
expect(parsed[0]).toEqual({
text: 'Unsupported image format: photo.bmp'
text: 'Unsupported mime type \'image/bmp\': photo.bmp'
});
});
@ -1281,7 +1281,7 @@ describe('Gemini', () => {
expect(parsed[0]).toEqual({ text: 'good.jpg' });
expect(parsed[1]).toHaveProperty('fileData');
expect(parsed[2]).toEqual({
text: 'Unsupported image format: bad.bmp'
text: 'Unsupported mime type \'image/bmp\': bad.bmp'
});
expect(parsed[3]).toEqual({ text: 'doc.pdf' });
expect(parsed[4]).toHaveProperty('fileData');

View file

@ -1061,7 +1061,7 @@ describe('OpenAI', () => {
expect(parsed[0].content).toHaveLength(1);
expect(parsed[0].content[0]).toEqual({
type: 'input_text',
text: 'Unsupported image format: photo.gif'
text: 'Unsupported mime type \'image/gif\': photo.gif'
});
});
@ -1171,7 +1171,7 @@ describe('OpenAI', () => {
expect(parsed[0].content[0].type).toBe('input_image');
expect(parsed[0].content[1]).toEqual({
type: 'input_text',
text: 'Unsupported image format: bad.bmp'
text: 'Unsupported mime type \'image/bmp\': bad.bmp'
});
expect(parsed[0].content[2].type).toBe('input_file');
});

View file

@ -1,5 +1,7 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { InputService } from '../../Services/InputService';
import { RegisterSingleton, DeregisterAllServices } from '../../Services/DependencyService';
import { Services } from '../../Services/Services';
/**
* UNIT TESTS
@ -10,11 +12,28 @@ import { InputService } from '../../Services/InputService';
describe('InputService', () => {
let service: InputService;
let mockFileSystemService: any;
beforeEach(() => {
// Setup minimal mock for FileSystemService
mockFileSystemService = {
getFile: vi.fn(),
readFile: vi.fn(),
fileExists: vi.fn()
};
// Register dependency
RegisterSingleton(Services.FileSystemService, mockFileSystemService);
service = new InputService();
});
afterEach(() => {
// Clear singleton registry to prevent memory leaks
DeregisterAllServices();
vi.restoreAllMocks();
});
describe('isPrintableKey', () => {
it('should return true for single character keys', () => {
expect(service.isPrintableKey('a')).toBe(true);
@ -132,7 +151,7 @@ describe('InputService', () => {
});
describe('getPlainTextFromClipboard', () => {
describe('getTextFromDataTransfer', () => {
it('should extract text/plain from DataTransfer', () => {
const mockClipboardData = {
getData: vi.fn((type: string) => {
@ -141,13 +160,13 @@ describe('InputService', () => {
})
} as unknown as DataTransfer;
const result = service.getPlainTextFromClipboard(mockClipboardData);
const result = service.getTextFromDataTransfer(mockClipboardData);
expect(result).toBe('Plain text content');
expect(mockClipboardData.getData).toHaveBeenCalledWith('text/plain');
});
it('should return empty string for null clipboard', () => {
const result = service.getPlainTextFromClipboard(null);
const result = service.getTextFromDataTransfer(null);
expect(result).toBe('');
});
@ -156,7 +175,7 @@ describe('InputService', () => {
getData: vi.fn(() => '')
} as unknown as DataTransfer;
const result = service.getPlainTextFromClipboard(mockClipboardData);
const result = service.getTextFromDataTransfer(mockClipboardData);
expect(result).toBe('');
});
@ -165,7 +184,7 @@ describe('InputService', () => {
getData: vi.fn(() => null)
} as unknown as DataTransfer;
const result = service.getPlainTextFromClipboard(mockClipboardData);
const result = service.getTextFromDataTransfer(mockClipboardData);
expect(result).toBe('');
});
});