diff --git a/AIAgentSettingTab.ts b/AIAgentSettingTab.ts index 7949296..c9f4471 100644 --- a/AIAgentSettingTab.ts +++ b/AIAgentSettingTab.ts @@ -1,5 +1,6 @@ import { AIProvider } from "Enums/ApiProvider"; import { Path } from "Enums/Path"; +import { Selector } from "Enums/Selector"; import type AIAgentPlugin from "main"; import { PluginSettingTab, Setting, App, setIcon, setTooltip } from "obsidian"; @@ -77,18 +78,18 @@ export class AIAgentSettingTab extends PluginSettingTab { this.plugin.settings.exclusions = value.split("\n").map(line => line.trim()).filter(line => line.length > 0); await this.plugin.saveSettings(); }); - text.inputEl.classList.add("ai-exclusions-input"); + text.inputEl.classList.add(Selector.AIExclusionsInput); }); } private highlightApiKey(): void { if (this.apiKeySetting) { if (this.plugin.settings.apiKey.trim() === "") { - this.apiKeySetting.settingEl.removeClass("api-key-setting-ok"); - this.apiKeySetting.settingEl.addClass("api-key-setting-error"); + this.apiKeySetting.settingEl.removeClass(Selector.ApiKeySettingOk); + this.apiKeySetting.settingEl.addClass(Selector.ApiKeySettingError); } else { - this.apiKeySetting.settingEl.removeClass("api-key-setting-error"); - this.apiKeySetting.settingEl.addClass("api-key-setting-ok"); + this.apiKeySetting.settingEl.removeClass(Selector.ApiKeySettingError); + this.apiKeySetting.settingEl.addClass(Selector.ApiKeySettingOk); } } } diff --git a/AIClasses/SystemPrompt.ts b/AIClasses/SystemPrompt.ts index d81aa04..f23522a 100644 --- a/AIClasses/SystemPrompt.ts +++ b/AIClasses/SystemPrompt.ts @@ -15,10 +15,12 @@ You are a knowledgeable AI assistant with specialized access to the user's Obsid ## Core Capabilities +**When referencing vault contents and user notes, always use Obsidian wiki-link syntax: [[note name]]** + You can help users with: -- Finding and referencing information from their notes using wiki-link syntax +- Finding and referencing information from their notes - Creating, updating, and organizing notes -- Working with tags, links, and folder structures +- Working with tags, links, graphs, and folder structures - General questions, problem-solving, and explanations across any domain - Programming, writing, and creative tasks @@ -100,7 +102,6 @@ When performing vault searches or reading multiple files: - Process descriptions unless they explain why results are limited **Natural Integration:** -- When referencing vault contents, **ALWAYS** use Obsidian wiki-link syntax: [[note name]] - Seamlessly combine vault information with your general knowledge - Always prefer vault content over generic information when available diff --git a/Components/ChatArea.svelte b/Components/ChatArea.svelte index 6c26d60..8a91b76 100644 --- a/Components/ChatArea.svelte +++ b/Components/ChatArea.svelte @@ -8,6 +8,9 @@ import { Role } from "Enums/Role"; import type { ConversationContent } from "Conversations/ConversationContent"; import { tick } from "svelte"; + import type AIAgentPlugin from "main"; + import { Copy } from "Enums/Copy"; + import { Selector } from "Enums/Selector"; export let messages: ConversationContent[] = []; export let currentThought: string | null = null; @@ -20,17 +23,26 @@ chatContainer.offsetHeight - parseFloat(getComputedStyle(chatContainer).padding) * 2) { // Recalculate padding when streaming ends to fix race condition with streaming indicator removal assistantMessageAction(lastAssistantMessageElement); - requestAnimationFrame(() => { - requestAnimationFrame(() => { - chatContainer.scroll({ top: chatContainer.scrollHeight, behavior: "smooth" }); - }); - }); + + // use an interval to complete scrolling once the dom has finished updating + const scrollInterval: number = plugin.registerInterval(window.setInterval(() => { + tick().then(() => { + chatContainer.scroll({ top: chatContainer.scrollHeight, behavior: "smooth" }); + }); + }, 50)); + + setTimeout(() => { + if (scrollInterval) { + clearInterval(scrollInterval); + } + }, 1000); } } let thoughtElement: HTMLElement | undefined; let streamingElement: HTMLElement | undefined; + let plugin: AIAgentPlugin = Resolve(Services.AIAgentPlugin); let streamingMarkdownService: StreamingMarkdownService = Resolve(Services.StreamingMarkdownService); let messageElements: Map = new Map(); @@ -123,6 +135,11 @@ // For assistant messages that aren't streaming, use traditional parsing if (!isCurrentlyStreaming) { + // Check if this is a cancelled request message + if (message.content === Copy.ApiRequestAborted) { + return `Request has been cancelled`; + } + try { return streamingMarkdownService.formatText(message.content) || `
${message.content}
`; } catch (err) { diff --git a/Enums/Copy.ts b/Enums/Copy.ts new file mode 100644 index 0000000..d26d980 --- /dev/null +++ b/Enums/Copy.ts @@ -0,0 +1,3 @@ +export enum Copy { + ApiRequestAborted = "Request has been cancelled" +} \ No newline at end of file diff --git a/Enums/Selector.ts b/Enums/Selector.ts index 2c24f44..09f1dd8 100644 --- a/Enums/Selector.ts +++ b/Enums/Selector.ts @@ -1,3 +1,8 @@ export enum Selector { MarkDownLink = "ai-agent-internal-markdown-link", + AIExclusionsInput = "ai-exclusions-input", + ApiKeySettingOk = "api-key-setting-ok", + ApiKeySettingError = "api-key-setting-error", + ApiRequestAborted = "api-request-aborted", + ConversationHistoryModal = "conversation-history-modal" } \ No newline at end of file diff --git a/Modals/ConversationHistoryModal.ts b/Modals/ConversationHistoryModal.ts index 85e391e..61b2bb2 100644 --- a/Modals/ConversationHistoryModal.ts +++ b/Modals/ConversationHistoryModal.ts @@ -8,6 +8,7 @@ import type { ConversationFileSystemService } from 'Services/ConversationFileSys import type { FileSystemService } from 'Services/FileSystemService'; import { dateToString } from 'Helpers/Helpers'; import { conversationStore } from 'Stores/conversationStore'; +import { Selector } from 'Enums/Selector'; interface ListItem { id: string; @@ -50,8 +51,8 @@ export class ConversationHistoryModal extends Modal { onOpen() { const { contentEl, modalEl, containerEl } = this; - containerEl.addClass('conversation-history-modal'); - modalEl.addClass('conversation-history-modal'); + containerEl.addClass(Selector.ConversationHistoryModal); + modalEl.addClass(Selector.ConversationHistoryModal); this.component = mount(ConversationHistoryModalSvelte, { target: contentEl, @@ -82,7 +83,7 @@ export class ConversationHistoryModal extends Modal { const currentPath = this.conversationFileSystemService.getCurrentConversationPath(); for (const item of itemsToDelete) { - const deleted = await this.fileSystemService.deleteFile(item.filePath); + const deleted = await this.fileSystemService.deleteFile(item.filePath, true); if (deleted && currentPath === item.filePath) { shouldResetChat = true; } diff --git a/Services/ConversationFileSystemService.ts b/Services/ConversationFileSystemService.ts index 9347c1a..cf3a1c6 100644 --- a/Services/ConversationFileSystemService.ts +++ b/Services/ConversationFileSystemService.ts @@ -4,6 +4,7 @@ import { FileSystemService } from "./FileSystemService"; import { Services } from "./Services"; import { Conversation } from "Conversations/Conversation"; import { ConversationContent } from "Conversations/ConversationContent"; +import { Copy } from "Enums/Copy"; export class ConversationFileSystemService { @@ -26,14 +27,16 @@ export class ConversationFileSystemService { const conversationData = { title: conversation.title, created: conversation.created.toISOString(), - contents: conversation.contents.map(content => ({ - id: content.id, - role: content.role, - content: content.content, - timestamp: content.timestamp.toISOString(), - isFunctionCall: content.isFunctionCall, - isFunctionCallResponse: content.isFunctionCallResponse - })) + contents: conversation.contents + .filter(content => content.content !== Copy.ApiRequestAborted) + .map(content => ({ + id: content.id, + role: content.role, + content: content.content, + timestamp: content.timestamp.toISOString(), + isFunctionCall: content.isFunctionCall, + isFunctionCallResponse: content.isFunctionCallResponse + })) }; await this.fileSystemService.writeObjectToFile(this.currentConversationPath, conversationData, true); diff --git a/Services/StreamingService.ts b/Services/StreamingService.ts index cc39690..32ba790 100644 --- a/Services/StreamingService.ts +++ b/Services/StreamingService.ts @@ -1,4 +1,5 @@ import type { AIFunctionCall } from "AIClasses/AIFunctionCall"; +import { Copy } from "Enums/Copy"; export interface StreamChunk { content: string; @@ -69,14 +70,11 @@ export class StreamingService { } catch (error) { // Don't log abort errors as they're intentional if (error instanceof Error && error.name === 'AbortError') { - console.log("Stream request aborted by user"); yield { - content: "", - isComplete: true, - error: 'Request has been cancelled', + content: Copy.ApiRequestAborted, + isComplete: true }; } else { - console.error("Stream request error:", error); yield { content: "", isComplete: true, diff --git a/main.ts b/main.ts index fa8cfa2..0deadb1 100644 --- a/main.ts +++ b/main.ts @@ -23,6 +23,8 @@ export default class AIAgentPlugin extends Plugin { async onload() { // KaTeX CSS is bundled with the plugin to comply with CSP require('katex/dist/katex.min.css'); + // Plugin styles + require('./styles.css'); await this.loadSettings(); diff --git a/styles.css b/styles.css index e41db4e..d235c2c 100644 --- a/styles.css +++ b/styles.css @@ -87,7 +87,7 @@ background-color: var(--color-base-35); } -.ai-request-cancelled { +.api-request-aborted { color: var(--color-red); border: var(--color-red); border-style: solid;