mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
refactor: centralize icon handling and improve attachment UI
- Move icon name logic to Attachment and Reference classes - Create reusable setElementIcon Svelte action in ElementHelper - Update ChatArea and ChatAttachments to use new icon helper - Add visual separator and background color for message attachments - Fix duplicate URIs in drag-and-drop file handling - Improve attachment icon centering with flexbox layout
This commit is contained in:
parent
dde2120612
commit
d2e7b47c65
6 changed files with 119 additions and 53 deletions
|
|
@ -11,7 +11,7 @@
|
|||
import { tick } from "svelte";
|
||||
import { Selector } from "Enums/Selector";
|
||||
import { Exception } from "Helpers/Exception";
|
||||
import { getOuterHeight } from "Helpers/ElementHelper";
|
||||
import { getOuterHeight, setElementIcon } from "Helpers/ElementHelper";
|
||||
|
||||
export let cancelling: boolean = false;
|
||||
export let messages: ConversationContent[] = [];
|
||||
|
|
@ -263,12 +263,13 @@
|
|||
{@html content}
|
||||
</div>
|
||||
{#if message.references.length > 0}
|
||||
<hr class="message-attachment-break"/>
|
||||
<div class="message-attachments-container">
|
||||
{#each message.references as reference}
|
||||
<div class="message-attachmanet" aria-label="{reference.fileName}">
|
||||
<div
|
||||
class="message-attachment-icon"
|
||||
|
||||
use:setElementIcon={reference.getIconName()}
|
||||
></div>
|
||||
<div class="message-attachment-info">
|
||||
<div class="message-attachment-name">{reference.fileName}</div>
|
||||
|
|
@ -430,12 +431,18 @@
|
|||
}
|
||||
|
||||
/* Message attachments styles */
|
||||
.message-attachment-break {
|
||||
color: var(--background-secondary-alt);
|
||||
margin: 0 0 var(--size-4-2) 0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.message-attachments-container {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scroll-behavior: smooth;
|
||||
gap: var(--size-4-1);
|
||||
gap: var(--size-4-2);
|
||||
margin-bottom: var(--size-4-2);
|
||||
}
|
||||
|
||||
|
|
@ -445,8 +452,9 @@
|
|||
|
||||
.message-attachmanet {
|
||||
display: grid;
|
||||
grid-template-rows: var(--size-4-1) auto var(--size-4-1);
|
||||
grid-template-columns: var(--size-4-1) auto var(--size-4-1) auto var(--size-4-1);
|
||||
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);
|
||||
background-color: var(--background-secondary-alt);
|
||||
border: var(--border-width) solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
flex-shrink: 0;
|
||||
|
|
@ -455,7 +463,9 @@
|
|||
.message-attachment-icon {
|
||||
grid-row: 2;
|
||||
grid-column: 2;
|
||||
align-content: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.message-attachment-info {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,11 @@
|
|||
<script lang="ts">
|
||||
import type { Attachment } from "Conversations/Attachment";
|
||||
import { MimeTypeToFileTypes } from "Enums/FileTypeMimeTypeMapping";
|
||||
import { toMimeType } from "Enums/MimeType";
|
||||
import { setElementIcon } from "Helpers/ElementHelper";
|
||||
import { setIcon } from "obsidian";
|
||||
import {
|
||||
isAudioFile,
|
||||
isImageFile,
|
||||
isKnownFileType,
|
||||
isTextFile,
|
||||
isVideoFile,
|
||||
} from "Enums/FileType";
|
||||
|
||||
export let attachments: Attachment[] = [];
|
||||
|
||||
let removeAttachmentButtons: (HTMLButtonElement | null)[] = [];
|
||||
let iconElements: (HTMLDivElement | null)[] = [];
|
||||
let attachmentElements: (HTMLDivElement | null)[] = [];
|
||||
let selectedElement: HTMLDivElement | null = null;
|
||||
|
||||
|
|
@ -26,40 +17,6 @@
|
|||
});
|
||||
}
|
||||
|
||||
$: 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);
|
||||
}
|
||||
|
|
@ -95,7 +52,7 @@
|
|||
bind:this={attachmentElements[index]}>
|
||||
<div
|
||||
class="chat-attachment-icon"
|
||||
bind:this={iconElements[index]}
|
||||
use:setElementIcon={attachment.getIconName()}
|
||||
></div>
|
||||
<div class="chat-attachment-info">
|
||||
<div class="chat-attachment-name" aria-label="{attachment.fileName}">{attachment.fileName}</div>
|
||||
|
|
@ -131,6 +88,7 @@
|
|||
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);
|
||||
background-color: var(--background-secondary-alt);
|
||||
border: var(--border-width) solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
max-width: 33%;
|
||||
|
|
@ -145,7 +103,9 @@
|
|||
.chat-attachment-icon {
|
||||
grid-row: 2;
|
||||
grid-column: 2;
|
||||
align-content: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chat-attachment-info {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import type { AIProvider } from "Enums/ApiProvider";
|
||||
import { isAudioFile, isImageFile, isKnownFileType, isTextFile, isVideoFile } from "Enums/FileType";
|
||||
import { MimeTypeToFileTypes } from "Enums/FileTypeMimeTypeMapping";
|
||||
import { toMimeType } from "Enums/MimeType";
|
||||
|
||||
export class Attachment {
|
||||
|
||||
|
|
@ -43,6 +46,27 @@ export class Attachment {
|
|||
return Math.round((megaByteSize + Number.EPSILON) * 100) / 100;
|
||||
}
|
||||
|
||||
public getIconName(): string {
|
||||
const fileTypes = MimeTypeToFileTypes[toMimeType(this.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";
|
||||
}
|
||||
|
||||
public static isAttachmentData(this: void, data: unknown): data is {
|
||||
fileName: string;
|
||||
mimeType: string;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import { isAudioFile, isImageFile, isKnownFileType, isTextFile, isVideoFile } from "Enums/FileType";
|
||||
import { pathExtname } from "Helpers/Helpers";
|
||||
|
||||
export class Reference {
|
||||
|
||||
public fileName: string;
|
||||
|
|
@ -8,6 +11,27 @@ export class Reference {
|
|||
this.size = size;
|
||||
}
|
||||
|
||||
public getIconName(): string {
|
||||
const extension = pathExtname(this.fileName);
|
||||
|
||||
if (isTextFile(extension)) {
|
||||
return "file-text";
|
||||
}
|
||||
if (isImageFile(extension)) {
|
||||
return "file-image";
|
||||
}
|
||||
if (isAudioFile(extension)) {
|
||||
return "file-music";
|
||||
}
|
||||
if (isVideoFile(extension)) {
|
||||
return "file-play";
|
||||
}
|
||||
if (isKnownFileType(extension)) {
|
||||
return "file";
|
||||
}
|
||||
return "file";
|
||||
}
|
||||
|
||||
public static isReferenceData(this: void, data: unknown): data is {
|
||||
fileName: string;
|
||||
size: number;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,20 @@
|
|||
import { setIcon } from "obsidian";
|
||||
|
||||
export function getOuterHeight(element: HTMLElement): number {
|
||||
const marginTop = parseFloat(getComputedStyle(element).marginTop) || 0;
|
||||
const marginBottom = parseFloat(getComputedStyle(element).marginBottom) || 0;
|
||||
return element.offsetHeight + marginTop + marginBottom;
|
||||
}
|
||||
|
||||
export function setElementIcon(element: HTMLDivElement, iconName: string) {
|
||||
if (element) {
|
||||
setIcon(element, iconName);
|
||||
}
|
||||
return {
|
||||
update(newIconName: string) {
|
||||
if (element) {
|
||||
setIcon(element, newIconName);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -48,9 +48,12 @@ export class InputService {
|
|||
// get text as sometimes uri's come through as plain text
|
||||
const textUriList = this.getTextFromDataTransfer(dataTransfer);
|
||||
const uriList = dataTransfer.getData("text/uri-list");
|
||||
const uris = `${uriList}\n${textUriList}`.split("\n").unique().map(uri => uri.trim())
|
||||
|
||||
const allUris = `${uriList}\n${textUriList}`.split("\n").map(uri => uri.trim())
|
||||
.filter(uri => uri.length > 0 && !uri.startsWith("#"));
|
||||
|
||||
const uris = this.removeDuplicateUris(allUris);
|
||||
|
||||
for (const uri of uris) {
|
||||
try {
|
||||
const url = new URL(uri);
|
||||
|
|
@ -390,4 +393,34 @@ export class InputService {
|
|||
return traverse(element);
|
||||
}
|
||||
|
||||
private removeDuplicateUris(uris: string[]): string[] {
|
||||
const seenFiles = new Set<string>();
|
||||
const uniqueUris: string[] = [];
|
||||
|
||||
for (const uri of uris) {
|
||||
try {
|
||||
const url = new URL(uri);
|
||||
const fileParam = url.searchParams.get("file");
|
||||
|
||||
if (fileParam) {
|
||||
// Normalize by decoding the file parameter to handle different URL encodings
|
||||
const normalizedFile = decodeURIComponent(fileParam);
|
||||
|
||||
if (!seenFiles.has(normalizedFile)) {
|
||||
seenFiles.add(normalizedFile);
|
||||
uniqueUris.push(uri);
|
||||
}
|
||||
} else {
|
||||
// If no file parameter, include the URI anyway
|
||||
uniqueUris.push(uri);
|
||||
}
|
||||
} catch {
|
||||
// If URL parsing fails, include the URI anyway
|
||||
uniqueUris.push(uri);
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueUris;
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue