mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
feat: add support for file attachments via drag and drop and paste.
refactor: standardize file type and MIME type handling across AI providers Introduce centralized MimeType enum and bidirectional mappings between file types and MIME types. Update Claude, Gemini, and OpenAI implementations to use consistent MIME type validation. Add file attachment support via drag-and-drop and paste in chat input. Expand supported file type detection to include programming languages, config files, and documentation formats.
This commit is contained in:
parent
2420cec7bf
commit
13cdd0a162
21 changed files with 1082 additions and 165 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, unknown> = {};
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -250,7 +250,7 @@
|
|||
|
||||
<div class="chat-area-wrapper">
|
||||
{#if messages.length > 0}
|
||||
<div class="top-fade"></div>
|
||||
<div class="top-fade"></div>
|
||||
{/if}
|
||||
<div class="chat-area" bind:this={chatContainer} on:scroll={handleScroll} use:observeResize>
|
||||
{#each messages as message, index}
|
||||
|
|
@ -300,7 +300,7 @@
|
|||
</div>
|
||||
|
||||
{#if messages.length > 0}
|
||||
<div class="bottom-fade"></div>
|
||||
<div class="bottom-fade"></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
@ -390,6 +390,7 @@
|
|||
font-size: var(--font-ui-medium);
|
||||
color: var(--text-muted);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.streaming-content {
|
||||
|
|
|
|||
178
Components/ChatAttachments.svelte
Normal file
178
Components/ChatAttachments.svelte
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
<script lang="ts">
|
||||
import type { Attachment } from "Conversations/Attachment";
|
||||
import { toMimeType } from "Enums/MimeType";
|
||||
import { setIcon } from "obsidian";
|
||||
import {
|
||||
isAudioFile,
|
||||
isImageFile,
|
||||
isKnownFileType,
|
||||
isTextFile,
|
||||
isVideoFile,
|
||||
MimeTypeToFileTypes,
|
||||
} from "Enums/FileType";
|
||||
|
||||
export let attachments: Attachment[] = [];
|
||||
|
||||
let removeAttachmentButtons: (HTMLButtonElement | null)[] = [];
|
||||
let iconElements: (HTMLDivElement | null)[] = [];
|
||||
let attachmentElements: (HTMLDivElement | null)[] = [];
|
||||
let selectedElement: HTMLDivElement | null = null;
|
||||
|
||||
$: if (removeAttachmentButtons.length > 0) {
|
||||
removeAttachmentButtons.forEach((button) => {
|
||||
if (button) {
|
||||
setIcon(button, "circle-x");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$: if (iconElements.length > 0) {
|
||||
iconElements.forEach((iconElement, index) => {
|
||||
if (iconElement && attachments[index]) {
|
||||
const iconName = getIconName(attachments[index]);
|
||||
setIcon(iconElement, iconName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getIconName(attachment: Attachment | null): string {
|
||||
if (attachment === null) {
|
||||
return "file";
|
||||
}
|
||||
|
||||
const fileTypes = MimeTypeToFileTypes[toMimeType(attachment.mimeType)];
|
||||
|
||||
if (fileTypes.some((fileType) => isTextFile(fileType))) {
|
||||
return "file-text";
|
||||
}
|
||||
if (fileTypes.some((fileType) => isImageFile(fileType))) {
|
||||
return "file-image";
|
||||
}
|
||||
if (fileTypes.some((fileType) => isAudioFile(fileType))) {
|
||||
return "file-music";
|
||||
}
|
||||
if (fileTypes.some((fileType) => isVideoFile(fileType))) {
|
||||
return "file-play";
|
||||
}
|
||||
if (fileTypes.some((fileType) => isKnownFileType(fileType))) {
|
||||
return "file";
|
||||
}
|
||||
return "file";
|
||||
}
|
||||
|
||||
function removeAttachment(attachment: Attachment) {
|
||||
attachments = attachments.filter((a) => a !== attachment);
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent, attachment: Attachment) {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
removeAttachment(attachment);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocus(event: FocusEvent) {
|
||||
selectedElement = event.currentTarget as HTMLDivElement;
|
||||
}
|
||||
|
||||
function handleBlur() {
|
||||
selectedElement = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="chat-attachments-container">
|
||||
{#each attachments as attachment, index}
|
||||
<div class="chat-attachmanet"
|
||||
class:selected={attachmentElements[index] === selectedElement}
|
||||
role="option"
|
||||
tabindex="0"
|
||||
aria-selected={attachmentElements[index] === selectedElement ? "true" : "false"}
|
||||
on:keydown={(e) => handleKeydown(e, attachment)}
|
||||
on:mouseover={handleFocus}
|
||||
on:mouseleave={handleBlur}
|
||||
on:focus={handleFocus}
|
||||
on:blur={handleBlur}
|
||||
bind:this={attachmentElements[index]}>
|
||||
<div
|
||||
class="chat-attachment-icon"
|
||||
bind:this={iconElements[index]}
|
||||
></div>
|
||||
<div class="chat-attachment-info">
|
||||
<div class="chat-attachment-name" aria-label="{attachment.fileName}">{attachment.fileName}</div>
|
||||
<div class="chat-attachment-size">{attachment.approximateFileSizeMB()}MB</div>
|
||||
</div>
|
||||
<button
|
||||
class="chat-attachment-remove transparent-button"
|
||||
bind:this={removeAttachmentButtons[index]}
|
||||
on:click={() => {
|
||||
removeAttachment(attachment);
|
||||
}}
|
||||
aria-label="Remove Attachment"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#chat-attachments-container {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scroll-behavior: smooth;
|
||||
gap: var(--size-4-2);
|
||||
}
|
||||
|
||||
#chat-attachments-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chat-attachmanet {
|
||||
display: grid;
|
||||
grid-template-rows: var(--size-4-2) auto var(--size-4-2);
|
||||
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;
|
||||
}
|
||||
|
||||
.chat-attachmanet.selected {
|
||||
box-shadow: inset 0px 0px 4px 1px var(--color-accent);
|
||||
}
|
||||
|
||||
.chat-attachment-icon {
|
||||
grid-row: 2;
|
||||
grid-column: 2;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.chat-attachment-info {
|
||||
grid-row: 2;
|
||||
grid-column: 4;
|
||||
min-width: 40px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-attachment-name {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
font-size: var(--font-smaller);
|
||||
}
|
||||
|
||||
.chat-attachment-size {
|
||||
padding: 0;
|
||||
font-size: var(--font-smallest);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.chat-attachment-remove {
|
||||
grid-row: 2;
|
||||
grid-column: 6;
|
||||
align-self: center;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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 @@
|
|||
</script>
|
||||
|
||||
<div id="input-container" class:edit-mode={editModeActive}>
|
||||
<div id="input-attachments-container" style:padding-top={attachments.length > 0 ? "var(--size-4-2)" : 0}>
|
||||
<ChatAttachments bind:attachments={attachments}/>
|
||||
</div>
|
||||
|
||||
<div id="diff-controls-container" style:padding-top={diffOpen ? "var(--size-4-2)" : 0} style:display={diffOpen ? "inline" : "none"}>
|
||||
<DiffControls/>
|
||||
</div>
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
|
||||
const settingsService: SettingsService = Resolve<SettingsService>(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 @@
|
|||
|
||||
<ChatInput
|
||||
bind:this={chatInput}
|
||||
bind:attachments={attachments}
|
||||
{hasNoApiKey}
|
||||
{isSubmitting}
|
||||
{editModeActive}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,14 @@ export class Attachment {
|
|||
return false;
|
||||
}
|
||||
|
||||
public approximateFileSizeMB() {
|
||||
// get the approximate MB size of the base64 string rounded to 2 decimal places
|
||||
const paddingBytes = this.base64.endsWith("==") ? 2 : this.base64.endsWith("=") ? 1 : 0;
|
||||
const byteSize = (this.base64.length * 3 / 4) - paddingBytes;
|
||||
const megaByteSize = byteSize / 1000000;
|
||||
return Math.round((megaByteSize + Number.EPSILON) * 100) / 100;
|
||||
}
|
||||
|
||||
public static isAttachmentData(this: void, data: unknown): data is {
|
||||
fileName: string;
|
||||
mimeType: string;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { Attachment } from "./Attachment";
|
|||
import type { AIFunctionResponse } from "AIClasses/FunctionDefinitions/AIFunctionResponse";
|
||||
import { Role } from "Enums/Role";
|
||||
import { AIFunction } from "Enums/AIFunction";
|
||||
import { isTextFile, FileType, getImageMimeType, isFileType } from "Enums/FileType";
|
||||
import { Exception } from "Helpers/Exception";
|
||||
import { isTextFile, toFileType } from "Enums/FileType";
|
||||
import { FileTypeToMimeType } from "Enums/MimeType";
|
||||
|
||||
export class Conversation {
|
||||
|
||||
|
|
@ -17,8 +17,9 @@ export class Conversation {
|
|||
contents: ConversationContent[] = [];
|
||||
|
||||
constructor() {
|
||||
this.created = new Date();
|
||||
this.updated = new Date();
|
||||
const timestamp = new Date();
|
||||
this.created = timestamp;
|
||||
this.updated = timestamp;
|
||||
this.title = `${StringTools.dateToString(this.created)}`;
|
||||
}
|
||||
|
||||
|
|
@ -82,24 +83,9 @@ export class Conversation {
|
|||
// 2. If there are binary files, create Attachments and add to conversation
|
||||
if (binaryResults.length > 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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|| 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<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]
|
||||
};
|
||||
259
Enums/MimeType.ts
Normal file
259
Enums/MimeType.ts
Normal file
|
|
@ -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<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
|
||||
};
|
||||
|
|
@ -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<T>(array: T[]): T[] {
|
|||
return shuffled;
|
||||
}
|
||||
|
||||
export function pathExtname(filePath: string) {
|
||||
return path.extname(filePath).substring(1).toLocaleLowerCase();
|
||||
}
|
||||
|
||||
export async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<IAIClass>(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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ArrayBuffer | Error> {
|
||||
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<TFile | Error> {
|
||||
const file: TAbstractFile | null = this.vaultService.getAbstractFileByPath(filePath, allowAccessToPluginRoot);
|
||||
if (file == null || !(file instanceof TFile)) {
|
||||
|
|
|
|||
|
|
@ -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<FileSystemService>(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<Attachment[]> {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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<ArrayBuffer | null> {
|
||||
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<TFile | Error> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue