refactor: centralize CSS class names in Selector enum

- Replace hardcoded class name strings with Selector enum values
- Add new selector constants for styling and UI states
- Filter cancelled request messages from saved conversations
- Improve scroll behavior when streaming ends
- Move plugin styles import to main plugin file
This commit is contained in:
Andrew Beal 2025-10-13 13:14:54 +01:00
parent c188f5727e
commit 4c21b39a28
10 changed files with 61 additions and 30 deletions

View file

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

View file

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

View file

@ -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<AIAgentPlugin>(Services.AIAgentPlugin);
let streamingMarkdownService: StreamingMarkdownService = Resolve<StreamingMarkdownService>(Services.StreamingMarkdownService);
let messageElements: Map<string, HTMLElement> = new Map<string, HTMLElement>();
@ -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 `<span class="${Selector.ApiRequestAborted}">Request has been cancelled</span>`;
}
try {
return streamingMarkdownService.formatText(message.content) || `<div>${message.content}</div>`;
} catch (err) {

3
Enums/Copy.ts Normal file
View file

@ -0,0 +1,3 @@
export enum Copy {
ApiRequestAborted = "Request has been cancelled"
}

View file

@ -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"
}

View file

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

View file

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

View file

@ -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: '<span class="ai-request-cancelled">Request has been cancelled</span>',
content: Copy.ApiRequestAborted,
isComplete: true
};
} else {
console.error("Stream request error:", error);
yield {
content: "",
isComplete: true,

View file

@ -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();

View file

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