diff --git a/AIClasses/Claude/Claude.ts b/AIClasses/Claude/Claude.ts index b91c272..0a9356b 100644 --- a/AIClasses/Claude/Claude.ts +++ b/AIClasses/Claude/Claude.ts @@ -10,11 +10,21 @@ import type { ConversationContent } from "Conversations/ConversationContent"; 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"; export class Claude extends BaseAIClass { private readonly STOP_REASON_TOOL_USE: string = "tool_use"; - private readonly SUPPORTED_IMAGE_TYPES: string[] = ["image/jpeg", "image/png", "image/gif", "image/webp"]; + + private readonly SUPPORTED_MIMETYPES = [ + MimeType.TEXT_PLAIN, + MimeType.APPLICATION_PDF, + MimeType.IMAGE_JPEG, + MimeType.IMAGE_PNG, + MimeType.IMAGE_GIF, + MimeType.IMAGE_WEBP + ]; private accumulatedFunctionName: string | null = null; private accumulatedFunctionArgs: string = ""; @@ -257,27 +267,28 @@ export class Claude extends BaseAIClass { public formatBinaryFiles(attachments: Attachment[]): string { const contentBlocks = attachments.flatMap(attachment => { - // Check for uploaded file ID const fileID = attachment.getFileID(this.provider); if (!fileID) { - // Skip - upload failed, error message added in extractContents() - return []; + return []; // Skip - upload failed, error message added in extractContents() } - let blockType: string; - if (attachment.mimeType === "application/pdf") { - blockType = "document"; - } else { - blockType = "image"; - if (!this.SUPPORTED_IMAGE_TYPES.includes(attachment.mimeType)) { - return [{ type: "text", text: `Unsupported image format: ${attachment.fileName}` }]; - } + const mimeType = toMimeType(attachment.mimeType); + + let isPlainText = false; + + // This content can be sent up with the 'MimeType.TEXT_PLAIN' mime type + if (MimeTypeToFileTypes[mimeType].some(fileType => isTextFile(fileType))) { + isPlainText = true; + } + + if (!isPlainText && !this.isSupportedMimeType(mimeType)) { + return [{ type: "text", text: `Unsupported mime type '${mimeType}': ${attachment.fileName}` }]; } return [ {type: "text", text: attachment.fileName}, { - type: blockType, + type: isPlainText || mimeType === MimeType.APPLICATION_PDF ? "document" : "image", source: { type: "file", file_id: fileID @@ -287,4 +298,8 @@ export class Claude extends BaseAIClass { }); return JSON.stringify(contentBlocks); } -} + + private isSupportedMimeType(mimeType: MimeType): boolean { + return this.SUPPORTED_MIMETYPES.includes(mimeType); + } +} \ No newline at end of file diff --git a/AIClasses/Gemini/Gemini.ts b/AIClasses/Gemini/Gemini.ts index 0f0eda8..553c9e0 100644 --- a/AIClasses/Gemini/Gemini.ts +++ b/AIClasses/Gemini/Gemini.ts @@ -10,11 +10,59 @@ import type { IAIFunctionDefinition } from "AIClasses/FunctionDefinitions/IAIFun 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"; export class Gemini extends BaseAIClass { private readonly REQUEST_WEB_SEARCH: string = "request_web_search"; - private readonly SUPPORTED_IMAGE_TYPES: string[] = ["image/jpeg", "image/png"]; + + private readonly SUPPORTED_MIMETYPES = [ + // Common Text + MimeType.TEXT_PLAIN, + MimeType.TEXT_HTML, + MimeType.TEXT_CSS, + MimeType.TEXT_CSV, + MimeType.TEXT_MD, + MimeType.TEXT_MARKDOWN, + MimeType.TEXT_XML, + MimeType.APPLICATION_RTF, + // Images + MimeType.IMAGE_JPEG, + MimeType.IMAGE_PNG, + // Data Formats + MimeType.APPLICATION_JSON, + MimeType.APPLICATION_XML, + // Scripting + MimeType.TEXT_PYTHON, + MimeType.APPLICATION_PYTHON_CODE, + MimeType.TEXT_JAVASCRIPT, + MimeType.APPLICATION_JAVASCRIPT, + MimeType.TEXT_TYPESCRIPT, + MimeType.APPLICATION_TYPESCRIPT, + MimeType.TEXT_SH, + MimeType.APPLICATION_SH, + // C-Family + MimeType.TEXT_C, + MimeType.TEXT_CPP, + MimeType.TEXT_CSRC, + MimeType.TEXT_CPPSRC, + MimeType.TEXT_CHDR, + MimeType.TEXT_CPPHDR, + // Java/JVM + MimeType.TEXT_JAVA, + MimeType.TEXT_JAVA_SOURCE, + MimeType.TEXT_KOTLIN, + MimeType.TEXT_SCALA, + // Others + MimeType.TEXT_GO, + MimeType.TEXT_RUST, + MimeType.TEXT_SWIFT, + MimeType.TEXT_RUBY, + MimeType.TEXT_PHP, + MimeType.TEXT_YAML, + MimeType.APPLICATION_YAML + ]; private accumulatedFunctionName: string | null = null; private accumulatedFunctionArgs: Record = {}; @@ -261,31 +309,38 @@ export class Gemini extends BaseAIClass { const parts: unknown[] = []; for (const attachment of attachments) { - // Check for uploaded file ID const fileID = attachment.getFileID(this.provider); if (!fileID) { - // Skip - upload failed, error message added in extractContents() + continue; // Skip - upload failed, error message added in extractContents() + } + + const mimeType = toMimeType(attachment.mimeType); + + let isPlainText = false; + + // This content can be sent up with the 'MimeType.TEXT_PLAIN' mime type + if (MimeTypeToFileTypes[mimeType].some(fileType => isTextFile(fileType))) { + isPlainText = true; + } + + if (!isPlainText && !this.isSupportedMimeType(mimeType)) { + parts.push({ text: `Unsupported mime type '${mimeType}': ${attachment.fileName}` }); continue; } - // Validate image types (Gemini only supports JPEG and PNG) - if (attachment.mimeType.startsWith('image/')) { - if (!this.SUPPORTED_IMAGE_TYPES.includes(attachment.mimeType)) { - parts.push({ text: `Unsupported image format: ${attachment.fileName}` }); - continue; - } - } - - // Add filename and file data parts.push({text: attachment.fileName}); parts.push({ fileData: { - mimeType: attachment.mimeType, - fileUri: fileID // Format: "files/abc123" + mimeType: mimeType, + fileUri: fileID } }); } return JSON.stringify(parts); } + + private isSupportedMimeType(mimeType: MimeType): boolean { + return this.SUPPORTED_MIMETYPES.includes(mimeType); + } } \ No newline at end of file diff --git a/AIClasses/OpenAI/OpenAI.ts b/AIClasses/OpenAI/OpenAI.ts index 7e76f77..f5ebf14 100644 --- a/AIClasses/OpenAI/OpenAI.ts +++ b/AIClasses/OpenAI/OpenAI.ts @@ -10,10 +10,18 @@ import type { IAIFunctionDefinition } from "AIClasses/FunctionDefinitions/IAIFun import type { ResponseEvent, ResponseOutputTextDelta, ResponseOutputItemDone, ResponseErrorEvent, ResponseFailedEvent, OpenAIFunctionTool, ResponsesAPIInput } from "./OpenAITypes"; import { Exception } from "Helpers/Exception"; import { ApiErrorType } from "Types/ApiError"; +import { MimeType, toMimeType } from "Enums/MimeType"; +import { isTextFile, MimeTypeToFileTypes } from "Enums/FileType"; export class OpenAI extends BaseAIClass { - private readonly SUPPORTED_IMAGE_TYPES: string[] = ["image/jpeg", "image/png", "image/webp"]; + private readonly SUPPORTED_MIMETYPES = [ + MimeType.TEXT_PLAIN, + MimeType.APPLICATION_PDF, + MimeType.IMAGE_JPEG, + MimeType.IMAGE_PNG, + MimeType.IMAGE_WEBP + ]; public constructor() { super(AIProvider.OpenAI); @@ -309,35 +317,29 @@ export class OpenAI extends BaseAIClass { const contentBlocks: unknown[] = []; for (const attachment of attachments) { - // Check for uploaded file ID const fileID = attachment.getFileID(this.provider); if (!fileID) { - // Skip - upload failed, error message added in extractContents() + continue; // Skip - upload failed, error message added in extractContents() + } + + const mimeType = toMimeType(attachment.mimeType); + + let isPlainText = false; + + //This content can be sent up with the 'MimeType.TEXT_PLAIN' mime type + if (MimeTypeToFileTypes[mimeType].some(fileType => isTextFile(fileType))) { + isPlainText = true; + } + + if (!isPlainText && !this.isSupportedMimeType(mimeType)) { + contentBlocks.push([{ type: "input_text", text: `Unsupported mime type '${mimeType}': ${attachment.fileName}` }]); continue; } - if (attachment.mimeType === "application/pdf") { - // Use file ID format for PDFs - contentBlocks.push({ - type: "input_file", - file_id: fileID - }); - } else { - // Images - if (!this.SUPPORTED_IMAGE_TYPES.includes(attachment.mimeType)) { - contentBlocks.push({ - type: "input_text", - text: `Unsupported image format: ${attachment.fileName}` - }); - continue; - } - - // Use file ID format for images - contentBlocks.push({ - type: "input_image", - file_id: fileID - }); - } + contentBlocks.push({ + type: isPlainText || mimeType === MimeType.APPLICATION_PDF ? "input_file" : "input_image", + file_id: fileID + }); } return JSON.stringify([{ @@ -345,4 +347,8 @@ export class OpenAI extends BaseAIClass { content: contentBlocks }]); } + + private isSupportedMimeType(mimeType: MimeType): boolean { + return this.SUPPORTED_MIMETYPES.includes(mimeType); + } } \ No newline at end of file diff --git a/Components/ChatArea.svelte b/Components/ChatArea.svelte index 4696e87..2eac1a2 100644 --- a/Components/ChatArea.svelte +++ b/Components/ChatArea.svelte @@ -250,7 +250,7 @@
{#if messages.length > 0} -
+
{/if}
{#each messages as message, index} @@ -300,7 +300,7 @@
{#if messages.length > 0} -
+
{/if}
@@ -390,6 +390,7 @@ font-size: var(--font-ui-medium); color: var(--text-muted); pointer-events: none; + user-select: none; } .streaming-content { diff --git a/Components/ChatAttachments.svelte b/Components/ChatAttachments.svelte new file mode 100644 index 0000000..474b1ce --- /dev/null +++ b/Components/ChatAttachments.svelte @@ -0,0 +1,178 @@ + + +
+ {#each attachments as attachment, index} +
handleKeydown(e, attachment)} + on:mouseover={handleFocus} + on:mouseleave={handleBlur} + on:focus={handleFocus} + on:blur={handleBlur} + bind:this={attachmentElements[index]}> +
+
+
{attachment.fileName}
+
{attachment.approximateFileSizeMB()}MB
+
+ +
+ {/each} +
+ + diff --git a/Components/ChatInput.svelte b/Components/ChatInput.svelte index 8138ee7..933b130 100644 --- a/Components/ChatInput.svelte +++ b/Components/ChatInput.svelte @@ -14,6 +14,10 @@ import type { EventService } from "Services/EventService"; import { Event } from "Enums/Event"; import type { DiffService } from "Services/DiffService"; + import type { Attachment } from "Conversations/Attachment"; + import ChatAttachments from "./ChatAttachments.svelte"; + + export let attachments: Attachment[] = []; export let hasNoApiKey: boolean; export let isSubmitting: boolean; @@ -35,8 +39,9 @@ let submitButton: HTMLButtonElement; let editModeButton: HTMLButtonElement; - let userInstructionActive = false; - let userRequest = ""; + let userInstructionActive: boolean = false; + + let userRequest: string = ""; let diffOpen: boolean = false; @@ -227,19 +232,6 @@ } } - function handlePaste(e: ClipboardEvent) { - e.preventDefault(); - - const plainText = inputService.getPlainTextFromClipboard(e.clipboardData); - - if (!plainText) { - return; - } - - inputService.insertTextAtCursor(plainText); - handleInput(); - } - function handleCopy(e: ClipboardEvent) { e.preventDefault(); @@ -253,10 +245,17 @@ e.clipboardData?.setData("text/plain", selectedText); } - function handleDrop(e: DragEvent) { + async function handleDataTransfer(e: ClipboardEvent | DragEvent) { e.preventDefault(); + e.stopPropagation(); - const plainText = e.dataTransfer?.getData("text/plain") || ""; + const dataTransfer = e instanceof ClipboardEvent ? e.clipboardData : e.dataTransfer; + + const files = await inputService.getFilesFromDataTransfer(dataTransfer); + const plainText = inputService.getTextFromDataTransfer(dataTransfer); + + const newAttachments = files.filter(file => !attachments.some(attachment => attachment.base64 === file.base64)); + attachments = [...attachments, ...newAttachments]; if (!plainText) { return; @@ -266,6 +265,25 @@ handleInput(); } + function handleDragOver(e: DragEvent) { + e.preventDefault(); + e.stopPropagation(); + if (e.dataTransfer) { + e.dataTransfer.dropEffect = "copy"; + e.dataTransfer.effectAllowed = "copy"; + } + } + + function handleDragEnter(e: DragEvent) { + e.preventDefault(); + e.stopPropagation(); + } + + function handleDragLeave(e: DragEvent) { + e.preventDefault(); + e.stopPropagation(); + } + function handleCursorPositionChange() { if (!$searchState.active || $searchState.position === null) { return; @@ -284,6 +302,10 @@
+
0 ? "var(--size-4-2)" : 0}> + +
+
@@ -313,9 +335,12 @@ on:keydown={handleKeydown} on:beforeinput={handleBeforeInput} on:input={handleInput} - on:paste={handlePaste} on:copy={handleCopy} - on:drop={handleDrop} + on:paste={handleDataTransfer} + on:drop={handleDataTransfer} + on:dragover={handleDragOver} + on:dragenter={handleDragEnter} + on:dragleave={handleDragLeave} on:click={handleCursorPositionChange} on:keyup={handleCursorPositionChange} on:focusout={handleFocusOut} @@ -355,7 +380,7 @@ grid-row: 2; grid-column: 1; display: grid; - grid-template-rows: auto auto var(--size-4-3) 1fr var(--size-4-3); + grid-template-rows: auto auto auto var(--size-4-3) 1fr var(--size-4-3); grid-template-columns: var(--size-4-3) auto var(--size-4-2) 1fr var(--size-4-2) auto var(--size-4-2) auto var(--size-4-3); border-radius: var(--modal-radius); background-color: var(--background-primary); @@ -366,23 +391,28 @@ transition: border-color 0.5s ease-out; } - #diff-controls-container { + #input-attachments-container { grid-row: 1; grid-column: 2 / 9; } - #input-search-results-container { + #diff-controls-container { grid-row: 2; grid-column: 2 / 9; } + #input-search-results-container { + grid-row: 3; + grid-column: 2 / 9; + } + #user-instruction-container { - grid-row: 2; + grid-row: 3; grid-column: 2 / 9; } #user-instruction-button { - grid-row: 4; + grid-row: 5; grid-column: 2; border-radius: var(--button-radius); align-self: end; @@ -398,7 +428,7 @@ } #input-field { - grid-row: 4; + grid-row: 5; grid-column: 4; height: 100%; max-height: 30vh; @@ -459,7 +489,7 @@ } #edit-mode-button { - grid-row: 4; + grid-row: 5; grid-column: 6; border-radius: var(--button-radius); align-self: end; @@ -471,7 +501,7 @@ } #submit-button { - grid-row: 4; + grid-row: 5; grid-column: 8; border-radius: var(--button-radius); padding-left: var(--size-4-5); diff --git a/Components/ChatSearchResults.svelte b/Components/ChatSearchResults.svelte index c973f2c..738c51a 100644 --- a/Components/ChatSearchResults.svelte +++ b/Components/ChatSearchResults.svelte @@ -45,7 +45,7 @@ case SearchTrigger.Tag: return "tag"; default: - return "file-question-mark"; + return "file"; } } @@ -121,7 +121,6 @@ padding: var(--size-2-2) var(--size-4-2); cursor: pointer; position: relative; - z-index: 10; } .input-search-result-icon { diff --git a/Components/ChatWindow.svelte b/Components/ChatWindow.svelte index 79fb923..519bccf 100644 --- a/Components/ChatWindow.svelte +++ b/Components/ChatWindow.svelte @@ -15,6 +15,7 @@ import type { SettingsService } from "Services/SettingsService"; import { Copy } from "Enums/Copy"; import { AbortService } from "Services/AbortService"; + import type { Attachment } from "Conversations/Attachment"; const plugin: VaultkeeperAIPlugin = Resolve(Services.VaultkeeperAIPlugin); const settingsService: SettingsService = Resolve(Services.SettingsService); @@ -34,6 +35,7 @@ let currentStreamingMessageId: string | null = null; let conversation: Conversation = new Conversation(); + let attachments: Attachment[] = []; let currentThought: string | null = null; @@ -97,10 +99,11 @@ const currentRequest = userRequest; - await chatService.submit(conversation, editModeActive, currentRequest, formattedRequest, { + await chatService.submit(conversation, editModeActive, currentRequest, formattedRequest, attachments, { onSubmit: () => { chatArea.updateChatAreaLayout("smooth"); isSubmitting = true; + attachments = []; }, onStreamingUpdate: (streamingId) => { conversation = conversation; @@ -172,6 +175,7 @@ 0) { const attachments = binaryResults.map(file => { - // Extract filename from path const fileName = file.path.split('/').pop() || file.path; - - // Determine mimeType based on file.type - let mimeType: string; - if (isFileType(file.type, FileType.PDF)) { - mimeType = "application/pdf"; - } else { - // For images, derive from extension - const extension = fileName.split('.').pop()?.toLowerCase() || ''; - try { - mimeType = getImageMimeType(extension); - } catch (error) { - Exception.log(error); - Exception.throw(error); - } - } - + const mimeType = FileTypeToMimeType[toFileType(file.type)]; + return new Attachment(fileName, mimeType, file.contents); }); diff --git a/Enums/FileType.ts b/Enums/FileType.ts index 089d9be..10c3dda 100644 --- a/Enums/FileType.ts +++ b/Enums/FileType.ts @@ -1,13 +1,10 @@ -import { Exception } from "Helpers/Exception"; +import { MimeType } from "./MimeType"; export enum FileType { - // ----- Types officially supported by Obsidian ----- - MD = "md", BASE = "base", CANVAS = "canvas", - // images AVIF = "avif", BMP = "bmp", @@ -17,9 +14,7 @@ export enum FileType { PNG = "png", SVG = "svg", WEBP = "webp", - WEBM = "webm", - // audio FLAC = "flac", M4A = "m4a", @@ -28,66 +23,136 @@ export enum FileType { WAV = "wav", WEBM_AUDIO = WEBM, THREE_GP = "3gp", - // video MKV = "mkv", MOV = "mov", MP4 = "mp4", OGV = "ogv", WEBM_VIDEO = WEBM, - // pdf PDF = "pdf", - + // ----- Types not officially supported by Obsidian ----- - + // Plain text TXT = "txt", + TEXT = "text", + + // Data formats JSON = "json", XML = "xml", - HTML = "html", CSV = "csv", + TSV = "tsv", YAML = "yaml", YML = "yml", + TOML = "toml", + + // Web languages + HTML = "html", CSS = "css", + SASS = "sass", + SCSS = "scss", + + // JavaScript/TypeScript ecosystem JS = "js", + MJS = "mjs", + CJS = "cjs", TS = "ts", JSX = "jsx", TSX = "tsx", - SASS = "sass", - SCSS = "scss", + VUE = "vue", + SVELTE = "svelte", + ASTRO = "astro", + + // Shell/Scripting + SH = "sh", + BASH = "bash", + ZSH = "zsh", + FISH = "fish", + BAT = "bat", + CMD = "cmd", + PS1 = "ps1", + + // Programming languages + PY = "py", + RB = "rb", + PHP = "php", + JAVA = "java", + C = "c", + CPP = "cpp", + CC = "cc", + CXX = "cxx", + H = "h", + HPP = "hpp", + HXX = "hxx", + CS = "cs", + GO = "go", + RS = "rs", + SWIFT = "swift", + KT = "kt", + KTS = "kts", + SCALA = "scala", + M = "m", + R = "r", + R_UPPER = "R", + JL = "jl", + LUA = "lua", + PL = "pl", + PM = "pm", + DART = "dart", + + // Markup & Documentation TEX = "tex", + LATEX = "latex", + RST = "rst", + ADOC = "adoc", + ASCIIDOC = "asciidoc", + ORG = "org", + TEXTILE = "textile", + RTF = "rtf", + + // Configuration files INI = "ini", CFG = "cfg", CONF = "conf", + ENV = "env", + PROPERTIES = "properties", + GITIGNORE = "gitignore", + GITATTRIBUTES = "gitattributes", + EDITORCONFIG = "editorconfig", + PRETTIERRC = "prettierrc", + ESLINTRC = "eslintrc", + BABELRC = "babelrc", + NPMRC = "npmrc", + YARNRC = "yarnrc", + DOCKERFILE = "dockerfile", + + // Query languages + SQL = "sql", + GRAPHQL = "graphql", + GQL = "gql", + + // Build & Development + MAKEFILE = "makefile", + MK = "mk", + GRADLE = "gradle", + DIFF = "diff", + PATCH = "patch", + + // Logs LOG = "log", - TOML = "toml", - RTF = "rtf", + + UNKNOWN = "unknown" } -export function getImageMimeType(extension: string): string { - switch (extension.toLowerCase()) { - case FileType.AVIF as string: - return "image/avif"; - case FileType.BMP as string: - return "image/bmp"; - case FileType.GIF as string: - return "image/gif"; - case FileType.JPEG as string: - case FileType.JPG as string: - return "image/jpeg"; - case FileType.PNG as string: - return "image/png"; - case FileType.SVG as string: - return "image/svg+xml"; - case FileType.WEBP as string: - return "image/webp"; - default: - Exception.throw(`Image type not supported: ${extension}`); +export function toFileType(fileType: string): FileType { + if (isKnownFileType(fileType)) { + return fileType; } + return FileType.UNKNOWN; } export function isKnownFileType(value: string): value is FileType { - return Object.values(FileType).includes(value as FileType); + return Object.values(FileType).includes(value as FileType) && value !== FileType.UNKNOWN.toString(); } export function isFileType(value: string, fileType: FileType) { @@ -103,24 +168,217 @@ export function isTextFile(extension: string) { || isFileType(extension, FileType.BASE) || isFileType(extension, FileType.CANVAS) || isFileType(extension, FileType.TXT) + || isFileType(extension, FileType.TEXT) || isFileType(extension, FileType.JSON) || isFileType(extension, FileType.XML) - || isFileType(extension, FileType.HTML) || isFileType(extension, FileType.CSV) + || isFileType(extension, FileType.TSV) || isFileType(extension, FileType.YAML) || isFileType(extension, FileType.YML) + || isFileType(extension, FileType.TOML) + || isFileType(extension, FileType.HTML) || isFileType(extension, FileType.CSS) + || isFileType(extension, FileType.SASS) + || isFileType(extension, FileType.SCSS) || isFileType(extension, FileType.JS) + || isFileType(extension, FileType.MJS) + || isFileType(extension, FileType.CJS) || isFileType(extension, FileType.TS) || isFileType(extension, FileType.JSX) || isFileType(extension, FileType.TSX) - || isFileType(extension, FileType.SASS) - || isFileType(extension, FileType.SCSS) + || isFileType(extension, FileType.VUE) + || isFileType(extension, FileType.SVELTE) + || isFileType(extension, FileType.ASTRO) + || isFileType(extension, FileType.SH) + || isFileType(extension, FileType.BASH) + || isFileType(extension, FileType.ZSH) + || isFileType(extension, FileType.FISH) + || isFileType(extension, FileType.BAT) + || isFileType(extension, FileType.CMD) + || isFileType(extension, FileType.PS1) + || isFileType(extension, FileType.PY) + || isFileType(extension, FileType.RB) + || isFileType(extension, FileType.PHP) + || isFileType(extension, FileType.JAVA) + || isFileType(extension, FileType.C) + || isFileType(extension, FileType.CPP) + || isFileType(extension, FileType.CC) + || isFileType(extension, FileType.CXX) + || isFileType(extension, FileType.H) + || isFileType(extension, FileType.HPP) + || isFileType(extension, FileType.HXX) + || isFileType(extension, FileType.CS) + || isFileType(extension, FileType.GO) + || isFileType(extension, FileType.RS) + || isFileType(extension, FileType.SWIFT) + || isFileType(extension, FileType.KT) + || isFileType(extension, FileType.KTS) + || isFileType(extension, FileType.SCALA) + || isFileType(extension, FileType.M) + || isFileType(extension, FileType.R) + || isFileType(extension, FileType.R_UPPER) + || isFileType(extension, FileType.JL) + || isFileType(extension, FileType.LUA) + || isFileType(extension, FileType.PL) + || isFileType(extension, FileType.PM) + || isFileType(extension, FileType.DART) || isFileType(extension, FileType.TEX) + || isFileType(extension, FileType.LATEX) + || isFileType(extension, FileType.RST) + || isFileType(extension, FileType.ADOC) + || isFileType(extension, FileType.ASCIIDOC) + || isFileType(extension, FileType.ORG) + || isFileType(extension, FileType.TEXTILE) + || isFileType(extension, FileType.RTF) || isFileType(extension, FileType.INI) || isFileType(extension, FileType.CFG) || isFileType(extension, FileType.CONF) - || isFileType(extension, FileType.LOG) - || isFileType(extension, FileType.TOML) - || isFileType(extension, FileType.RTF); -} \ No newline at end of file + || isFileType(extension, FileType.ENV) + || isFileType(extension, FileType.PROPERTIES) + || isFileType(extension, FileType.GITIGNORE) + || isFileType(extension, FileType.GITATTRIBUTES) + || isFileType(extension, FileType.EDITORCONFIG) + || isFileType(extension, FileType.PRETTIERRC) + || isFileType(extension, FileType.ESLINTRC) + || isFileType(extension, FileType.BABELRC) + || isFileType(extension, FileType.NPMRC) + || isFileType(extension, FileType.YARNRC) + || isFileType(extension, FileType.DOCKERFILE) + || isFileType(extension, FileType.SQL) + || isFileType(extension, FileType.GRAPHQL) + || isFileType(extension, FileType.GQL) + || isFileType(extension, FileType.MAKEFILE) + || isFileType(extension, FileType.MK) + || isFileType(extension, FileType.GRADLE) + || isFileType(extension, FileType.DIFF) + || isFileType(extension, FileType.PATCH) + || isFileType(extension, FileType.LOG); +} + +export function isImageFile(extension: string) { + return isFileType(extension, FileType.AVIF) + || isFileType(extension, FileType.BMP) + || isFileType(extension, FileType.GIF) + || isFileType(extension, FileType.JPEG) + || isFileType(extension, FileType.JPG) + || isFileType(extension, FileType.PNG) + || isFileType(extension, FileType.SVG) + || isFileType(extension, FileType.WEBP); +} + +export function isAudioFile(extension: string) { + return isFileType(extension, FileType.FLAC) + || isFileType(extension, FileType.M4A) + || isFileType(extension, FileType.MP3) + || isFileType(extension, FileType.OGG) + || isFileType(extension, FileType.WAV) + || isFileType(extension, FileType.WEBM_AUDIO) + || isFileType(extension, FileType.THREE_GP); +} + +export function isVideoFile(extension: string) { + return isFileType(extension, FileType.MKV) + || isFileType(extension, FileType.MOV) + || isFileType(extension, FileType.MP4) + || isFileType(extension, FileType.OGV) + || isFileType(extension, FileType.WEBM_VIDEO); +} + +export const MimeTypeToFileTypes: Record = { + // 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] +}; \ No newline at end of file diff --git a/Enums/MimeType.ts b/Enums/MimeType.ts new file mode 100644 index 0000000..3f8f95b --- /dev/null +++ b/Enums/MimeType.ts @@ -0,0 +1,259 @@ +import { FileType } from "./FileType"; + +export enum MimeType { + // Text + TEXT_PLAIN = "text/plain", + TEXT_MARKDOWN = "text/markdown", + TEXT_MD = "text/md", + TEXT_HTML = "text/html", + TEXT_CSS = "text/css", + TEXT_CSV = "text/csv", + TEXT_TSV = "text/tab-separated-values", + TEXT_JAVASCRIPT = "text/javascript", + TEXT_TYPESCRIPT = "text/typescript", + TEXT_JSX = "text/jsx", + TEXT_TSX = "text/tsx", + TEXT_SASS = "text/x-sass", + TEXT_SCSS = "text/x-scss", + TEXT_VUE = "text/x-vue", + TEXT_SVELTE = "text/x-svelte", + TEXT_PYTHON = "text/x-python", + TEXT_RUBY = "text/x-ruby", + TEXT_PHP = "text/x-php", + TEXT_JAVA = "text/x-java", + TEXT_JAVA_SOURCE = "text/x-java-source", + TEXT_C = "text/x-c", + TEXT_CSRC = "text/x-csrc", + TEXT_CHDR = "text/x-chdr", + TEXT_CPP = "text/x-c++", + TEXT_CPPSRC = "text/x-c++src", + TEXT_CPPHDR = "text/x-c++hdr", + TEXT_CSHARP = "text/x-csharp", + TEXT_GO = "text/x-go", + TEXT_RUST = "text/x-rust", + TEXT_SWIFT = "text/x-swift", + TEXT_KOTLIN = "text/x-kotlin", + TEXT_SCALA = "text/x-scala", + TEXT_R = "text/x-r", + TEXT_JULIA = "text/x-julia", + TEXT_LUA = "text/x-lua", + TEXT_PERL = "text/x-perl", + TEXT_DART = "text/x-dart", + TEXT_SHELL = "text/x-shellscript", + TEXT_SH = "text/x-sh", + TEXT_BATCH = "text/x-batch", + TEXT_POWERSHELL = "text/x-powershell", + TEXT_SQL = "text/x-sql", + TEXT_GRAPHQL = "text/x-graphql", + TEXT_XML = "text/xml", + TEXT_YAML = "text/x-yaml", + + // Application + APPLICATION_JSON = "application/json", + APPLICATION_XML = "application/xml", + APPLICATION_PDF = "application/pdf", + APPLICATION_RTF = "application/rtf", + APPLICATION_YAML = "application/x-yaml", + APPLICATION_TOML = "application/toml", + APPLICATION_TEX = "application/x-tex", + APPLICATION_LATEX = "application/x-latex", + APPLICATION_MAKEFILE = "text/x-makefile", + APPLICATION_GRADLE = "text/x-gradle", + APPLICATION_DOCKERFILE = "text/x-dockerfile", + APPLICATION_PYTHON_CODE = "application/x-python-code", + APPLICATION_JAVASCRIPT = "application/x-javascript", + APPLICATION_TYPESCRIPT = "application/x-typescript", + APPLICATION_SH = "application/x-sh", + + // Markup formats + TEXT_RST = "text/x-rst", + TEXT_ASCIIDOC = "text/x-asciidoc", + TEXT_ORG = "text/x-org", + TEXT_TEXTILE = "text/x-textile", + + // Images + IMAGE_AVIF = "image/avif", + IMAGE_BMP = "image/bmp", + IMAGE_GIF = "image/gif", + IMAGE_JPEG = "image/jpeg", + IMAGE_PNG = "image/png", + IMAGE_SVG = "image/svg+xml", + IMAGE_WEBP = "image/webp", + + // Audio + AUDIO_FLAC = "audio/flac", + AUDIO_MP4 = "audio/mp4", + AUDIO_MPEG = "audio/mpeg", + AUDIO_OGG = "audio/ogg", + AUDIO_WAV = "audio/wav", + AUDIO_WEBM = "audio/webm", + + // Video + VIDEO_3GPP = "video/3gpp", + VIDEO_MATROSKA = "video/x-matroska", + VIDEO_QUICKTIME = "video/quicktime", + VIDEO_MP4 = "video/mp4", + VIDEO_OGG = "video/ogg", + VIDEO_WEBM = "video/webm", + + UNKNOWN = "unknown" +} + +export function toMimeType(mimeType: string): MimeType { + if (isKnownMimeType(mimeType)) { + return mimeType; + } + return MimeType.UNKNOWN; +} + +export function isKnownMimeType(value: string): value is MimeType { + return Object.values(MimeType).includes(value as MimeType) && value !== MimeType.UNKNOWN.toString(); +} + +export const FileTypeToMimeType: Record = { + // ----- 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 +}; diff --git a/Helpers/Helpers.ts b/Helpers/Helpers.ts index cecd9cc..a6ae347 100644 --- a/Helpers/Helpers.ts +++ b/Helpers/Helpers.ts @@ -1,4 +1,5 @@ import type VaultkeeperAIPlugin from "main"; +import * as path from "path-browserify"; export function openPluginSettings(plugin: VaultkeeperAIPlugin) { if (!("setting" in plugin.app) || typeof plugin.app.setting !== "object" || plugin.app.setting === null) { @@ -39,6 +40,10 @@ export function shuffleArray(array: T[]): T[] { return shuffled; } +export function pathExtname(filePath: string) { + return path.extname(filePath).substring(1).toLocaleLowerCase(); +} + export async function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } \ No newline at end of file diff --git a/Services/AIFunctionService.ts b/Services/AIFunctionService.ts index bd9e961..b37b46a 100644 --- a/Services/AIFunctionService.ts +++ b/Services/AIFunctionService.ts @@ -8,7 +8,7 @@ import type { ISearchMatch } from "../Helpers/SearchTypes"; import { AbortService } from "./AbortService"; import { normalizePath, TAbstractFile, TFile } from "obsidian"; import { Exception } from "Helpers/Exception"; -import * as path from "path-browserify"; +import { pathExtname } from "Helpers/Helpers"; import { SearchVaultFilesArgsSchema, ReadVaultFilesArgsSchema, @@ -162,7 +162,7 @@ export class AIFunctionService { return { path: filePath, error: result.message } } return { - type: path.extname(filePath).substring(1).toLocaleLowerCase(), + type: pathExtname(filePath), path: filePath, contents: result } diff --git a/Services/ChatService.ts b/Services/ChatService.ts index 9216f81..4a2a6e0 100644 --- a/Services/ChatService.ts +++ b/Services/ChatService.ts @@ -15,6 +15,7 @@ import { Event } from "Enums/Event"; import { AbortService } from "./AbortService"; import { Exception } from "Helpers/Exception"; import { Copy } from "Enums/Copy"; +import type { Attachment } from "Conversations/Attachment"; export interface IChatServiceCallbacks { onSubmit: () => void; @@ -50,7 +51,7 @@ export class ChatService { this.ai = Resolve(Services.IAIClass); } - public async submit(conversation: Conversation, allowDestructiveActions: boolean, userRequest: string, formattedRequest: string, callbacks: IChatServiceCallbacks) { + public async submit(conversation: Conversation, allowDestructiveActions: boolean, userRequest: string, formattedRequest: string, attachments: Attachment[], callbacks: IChatServiceCallbacks) { if (!await this.semaphore.wait()) { return; } @@ -65,6 +66,8 @@ export class ChatService { this.abortService.initialiseAbortController(); await this.abortService.abortableOperation(async () => { + const firstMessage = conversation.contents.length === 0; + conversation.contents.push(new ConversationContent({ role: Role.User, content: formattedRequest, @@ -72,10 +75,19 @@ export class ChatService { })); await this.saveConversation(conversation); + if (attachments.length > 0) { + // Add any attachments that came from paste / drop + conversation.contents.push(new ConversationContent({ + role: Role.User, + attachments: attachments, + shouldDisplayContent: false + })); + } + callbacks.onSubmit(); callbacks.onStreamingUpdate(null); - if (conversation.contents.length === 1) { + if (firstMessage) { this.onNameChanged?.(conversation.title); // on change for initial conversation name await this.namingService.requestName(conversation, formattedRequest, this.onNameChanged); } diff --git a/Services/FileSystemService.ts b/Services/FileSystemService.ts index 23fa92c..cef8ef4 100644 --- a/Services/FileSystemService.ts +++ b/Services/FileSystemService.ts @@ -28,6 +28,21 @@ export class FileSystemService { return Exception.new(`Path is a folder, not a file: ${filePath}`); } + public async readBinaryFile(filePath: string, allowAccessToPluginRoot: boolean = false): Promise { + const file: TAbstractFile | null = this.vaultService.getAbstractFileByPath(filePath, allowAccessToPluginRoot); + if (file == null) { + return Exception.new(`File does not exist: ${filePath}`); + } + if (file instanceof TFile) { + const arrayBuffer = await this.vaultService.readBinaryData(file, allowAccessToPluginRoot); + if (!arrayBuffer) { + return Exception.new(`Failed to read binary dta for: ${filePath}`); + } + return arrayBuffer; + } + return Exception.new(`Path is a folder, not a file: ${filePath}`); + } + public async writeFile(filePath: string, content: string, allowAccessToPluginRoot: boolean = false, requiresConfirmation: boolean = true): Promise { const file: TAbstractFile | null = this.vaultService.getAbstractFileByPath(filePath, allowAccessToPluginRoot); if (file == null || !(file instanceof TFile)) { diff --git a/Services/InputService.ts b/Services/InputService.ts index dd92e94..fa75873 100644 --- a/Services/InputService.ts +++ b/Services/InputService.ts @@ -1,13 +1,85 @@ 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 * as path from "path-browserify"; +import { pathExtname } from "Helpers/Helpers"; +import { FileType, toFileType } from "Enums/FileType"; +import type { FileSystemService } from "./FileSystemService"; +import { Resolve } from "./DependencyService"; +import { Services } from "./Services"; export class InputService { - public getPlainTextFromClipboard(clipboardData: DataTransfer | null): string { - if (!clipboardData) { + private readonly fileSystemService: FileSystemService; + + public constructor() { + this.fileSystemService = Resolve(Services.FileSystemService); + } + + public getTextFromDataTransfer(dataTransfer: DataTransfer | null): string { + if (!dataTransfer) { return ""; } - return clipboardData.getData("text/plain") || ""; + return dataTransfer.getData("text/plain") || ""; + } + + public async getFilesFromDataTransfer(dataTransfer: DataTransfer | null): Promise { + const attachments: Attachment[] = []; + + if (!dataTransfer) { + return attachments; + } + + // files from external source (dragged from outside Obsidian) + const files = dataTransfer.files; + if (files) { + for (let i = 0; i < files.length; i++) { + const file = files[i]; + attachments.push(new Attachment( + file.name, + FileTypeToMimeType[toFileType(pathExtname(file.name))], + arrayBufferToBase64(await file.arrayBuffer()) + )); + } + } + + const uriList = dataTransfer.getData("text/uri-list"); + const uris = uriList.split("\n").map(uri => uri.trim()) + .filter(uri => uri.length > 0 && !uri.startsWith("#")); + + for (const uri of uris) { + try { + const url = new URL(uri); + const fileParam = url.searchParams.get("file"); + + if (fileParam) { + let filePath = decodeURIComponent(fileParam); + + let extension = pathExtname(filePath); + // Obsidian doesn't include extension for markdown files + if (extension.trim() === "") { + extension = FileType.MD; + filePath = `${filePath}.${extension}`; + } + + const arrayBuffer = await this.fileSystemService.readBinaryFile(filePath); + + if (arrayBuffer instanceof ArrayBuffer) { + attachments.push(new Attachment( + path.basename(filePath), + FileTypeToMimeType[toFileType(extension)], + arrayBufferToBase64(arrayBuffer) + )); + } + } + } catch (error) { + Exception.log(error); + } + } + + return attachments; } public sanitizeToPlainText(element: HTMLElement) { diff --git a/Services/VaultService.ts b/Services/VaultService.ts index f414855..b8da6d3 100644 --- a/Services/VaultService.ts +++ b/Services/VaultService.ts @@ -3,7 +3,7 @@ import { Resolve } from "./DependencyService"; import { Services } from "./Services"; import type VaultkeeperAIPlugin from "main"; import { Path } from "Enums/Path"; -import { randomSample, shuffleArray } from "Helpers/Helpers"; +import { pathExtname, randomSample, shuffleArray } from "Helpers/Helpers"; import { StringTools } from "Helpers/StringTools"; import type { IPageText, ISearchMatch, ISearchSnippet } from "../Helpers/SearchTypes"; import type { SanitiserService } from "./SanitiserService"; @@ -87,13 +87,24 @@ export class VaultService { } if (isBinaryFile(file.extension.toLowerCase())) { - const arrayBuffer = await this.vault.readBinary(file); - return arrayBufferToBase64(arrayBuffer); + const arrayBuffer = await this.readBinaryData(file, allowAccessToPluginRoot); + if (arrayBuffer) { + return arrayBufferToBase64(arrayBuffer); + } } return await this.vault.read(file); } + public async readBinaryData(file: TFile, allowAccessToPluginRoot: boolean = false): Promise { + const filePath = this.sanitiserService.sanitize(file.path); + if (this.isExclusion(filePath, allowAccessToPluginRoot)) { + Exception.log(`Plugin attempted to read a file that is in the exclusions list: ${filePath}`); + return null; + } + return await this.vault.readBinary(file); + } + public async create(filePath: string, content: string, allowAccessToPluginRoot: boolean = false, requiresConfirmation: boolean = true): Promise { filePath = this.sanitiserService.sanitize(filePath); if (this.isExclusion(filePath, allowAccessToPluginRoot)) { @@ -101,7 +112,7 @@ export class VaultService { return Exception.new(`Failed to create file, permission denied: ${filePath}`); } - if (path.extname(filePath) === "pdf") { + if (isFileType(pathExtname(filePath), FileType.PDF)) { return Exception.new("Creating PDF files is not supported"); } @@ -142,7 +153,7 @@ export class VaultService { return Exception.new(`File does not exist: ${filePath}`); } - if (isFileType(path.extname(filePath), FileType.PDF)) { + if (isFileType(pathExtname(filePath), FileType.PDF)) { return Exception.new("Creating PDF files is not supported"); } @@ -179,7 +190,7 @@ export class VaultService { return currentContent; } - if (isFileType(path.extname(filePath), FileType.PDF)) { + if (isFileType(pathExtname(filePath), FileType.PDF)) { await this.fileManager.trashFile(file); } diff --git a/Styles/custom_styles.css b/Styles/custom_styles.css index 3eb0c19..b05e2dd 100644 --- a/Styles/custom_styles.css +++ b/Styles/custom_styles.css @@ -61,6 +61,13 @@ /* CSS Variables for Common Components */ /* ============================== */ +.transparent-button { + border: none !important; + outline: none !important; + box-shadow: none !important; + background-color: transparent !important; +} + .search-trigger { color: var(--interactive-accent); cursor: default; diff --git a/__tests__/AIClasses/BaseAIClass.test.ts b/__tests__/AIClasses/BaseAIClass.test.ts index 8c2ad57..15d5ab3 100644 --- a/__tests__/AIClasses/BaseAIClass.test.ts +++ b/__tests__/AIClasses/BaseAIClass.test.ts @@ -6,9 +6,6 @@ import { ConversationContent } from '../../Conversations/ConversationContent'; import { Role } from '../../Enums/Role'; import { RegisterSingleton, DeregisterAllServices } from '../../Services/DependencyService'; import { Services } from '../../Services/Services'; -import { StreamingService } from '../../Services/StreamingService'; -import { SettingsService } from '../../Services/SettingsService'; -import { AIFunctionDefinitions } from '../../AIClasses/FunctionDefinitions/AIFunctionDefinitions'; import { AbortService } from '../../Services/AbortService'; import { AIProvider } from '../../Enums/ApiProvider'; diff --git a/__tests__/Conversations/Conversation.test.ts b/__tests__/Conversations/Conversation.test.ts index e6fa7ea..6b71f33 100644 --- a/__tests__/Conversations/Conversation.test.ts +++ b/__tests__/Conversations/Conversation.test.ts @@ -314,7 +314,6 @@ describe('Conversation', () => { const mostRecent = conversation.contents[conversation.contents.length - 1]; if (mostRecent) { mostRecent.functionCall = 'test'; - mostRecent.isFunctionCall = true; } }).not.toThrow(); expect(conversation.contents).toHaveLength(0);