Add wiki-link support, API key validation, and refactor AI provider initialization

Implement clickable wiki-links in assistant responses with WorkSpaceService for note navigation. Add API key validation with visual feedback in ChatWindow, automatically opening settings when empty. Extract settings helper to reduce code duplication. Refactor Gemini class to resolve API key from plugin settings rather than constructor parameter. Update system prompt with wiki-link usage guidelines. Remove unused ODB cache implementation and loadExternalCSS helper. Improve UI with enhanced empty state styling and input textarea scrolling behavior.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Andrew Beal 2025-10-08 22:06:25 +01:00
parent f5a3a513ec
commit d910239a7f
20 changed files with 547 additions and 529 deletions

79
AIAgentSettingTab.ts Normal file
View file

@ -0,0 +1,79 @@
import { AIProvider } from "Enums/ApiProvider";
import type AIAgentPlugin from "main";
import { PluginSettingTab, Setting, App, setIcon, setTooltip } from "obsidian";
export class AIAgentSettingTab extends PluginSettingTab {
plugin: AIAgentPlugin;
apiKeySetting: Setting | null = null;
constructor(app: App, plugin: AIAgentPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName("API Provider")
.setDesc("Select the API provider to use.")
.addDropdown((dropdown) =>
dropdown
.addOption(AIProvider.Gemini, AIProvider.Gemini)
.addOption(AIProvider.OpenAI, AIProvider.OpenAI)
.setValue(this.plugin.settings.apiProvider)
.onChange(async (value) => {
this.plugin.settings.apiProvider = value;
await this.plugin.saveSettings();
})
);
let inputEl: HTMLInputElement;
this.apiKeySetting = new Setting(containerEl)
.setName("API Key")
.setDesc("Enter your API key here.")
.addText(text => {
text.setPlaceholder("Enter your API key")
.setValue(this.plugin.settings.apiKey)
.onChange(async (value) => {
this.plugin.settings.apiKey = value;
await this.plugin.saveSettings();
this.highlightApiKey();
});
text.inputEl.type = "password";
inputEl = text.inputEl;
})
.addExtraButton(button => {
button
.setTooltip("Show API Key")
.onClick(() => {
if (inputEl.type === "password") {
inputEl.type = "text";
setIcon(button.extraSettingsEl, "eye-off");
setTooltip(button.extraSettingsEl, "Hide API Key");
} else {
inputEl.type = "password";
setIcon(button.extraSettingsEl, "eye");
setTooltip(button.extraSettingsEl, "Show API Key");
}
});
setIcon(button.extraSettingsEl, "eye");
});
this.highlightApiKey();
}
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");
} else {
this.apiKeySetting.settingEl.removeClass("api-key-setting-error");
this.apiKeySetting.settingEl.addClass("api-key-setting-ok");
}
}
}
}

View file

@ -7,24 +7,23 @@ import type { Conversation } from "Conversations/Conversation";
import { Role } from "Enums/Role";
import { AIProviderURL } from "Enums/ApiProvider";
import { AIFunctionCall } from "AIClasses/AIFunctionCall";
import type { AIFunctionDefinitions } from "AIClasses/FunctionDefinitions/AIFunctionDefinitions";
import type { IAIFunctionDefinition } from "AIClasses/FunctionDefinitions/IAIFunctionDefinition";
import type AIAgentPlugin from "main";
import type { AIFunctionDefinitions } from "AIClasses/FunctionDefinitions/AIFunctionDefinitions";
export class Gemini implements IAIClass {
private readonly REQUEST_WEB_SEARCH: string = "request_web_search";
private readonly apiKey: string;
private readonly aiPrompt: IPrompt;
private readonly streamingService: StreamingService;
private readonly aiFunctionDefinitions: AIFunctionDefinitions;
private readonly aiPrompt: IPrompt = Resolve(Services.IPrompt);
private readonly plugin: AIAgentPlugin = Resolve(Services.AIAgentPlugin);
private readonly streamingService: StreamingService = Resolve(Services.StreamingService);
private readonly aiFunctionDefinitions: AIFunctionDefinitions = Resolve(Services.AIFunctionDefinitions);
private accumulatedFunctionName: string | null = null;
private accumulatedFunctionArgs: Record<string, any> = {};
public constructor(apiKey: string) {
this.apiKey = apiKey;
this.aiPrompt = Resolve(Services.IPrompt);
this.streamingService = Resolve(Services.StreamingService);
this.aiFunctionDefinitions = Resolve(Services.AIFunctionDefinitions);
public constructor() {
this.apiKey = this.plugin.settings.apiKey;
}
public async* streamRequest(conversation: Conversation): AsyncGenerator<StreamChunk, void, unknown> {

View file

@ -14,6 +14,18 @@ When users need help with their Obsidian vault, you have access to tools that al
- Assist with Markdown formatting specific to Obsidian
- Help with plugins and vault configuration
When referencing notes from the user's vault, ALWAYS use Obsidian wiki-link syntax: [[note name]].
Examples:
- "I found relevant information in [[project ideas]]"
- "This relates to what you mentioned in [[meeting notes 2024-10-07]]"
- "See [[research paper]] for more details"
Guidelines:
- Use just the note name without file extension
- If uncertain about exact name, use your best guess - the system will try to match it
- You can reference notes in subfolders as [[folder/note name]]
### General Assistance
You are also capable of helping with:
- General questions and conversations

View file

@ -249,6 +249,7 @@
.conversation-empty-state {
margin: auto;
font-style: italic;
font-size: var(--font-ui-medium);
color: var(--text-muted);
pointer-events: none;
}

View file

@ -4,9 +4,9 @@
import { Services } from "Services/Services";
import ChatArea from "./ChatArea.svelte";
import type { IAIClass } from "AIClasses/IAIClass";
import { tick } from "svelte";
import { tick, onMount } from "svelte";
import { ConversationFileSystemService } from "Services/ConversationFileSystemService";
import { setIcon } from "obsidian";
import { Notice, setIcon } from "obsidian";
import { conversationStore } from "../Stores/conversationStore";
import { Role } from "Enums/Role";
import { Conversation } from "Conversations/Conversation";
@ -14,10 +14,17 @@
import type { AIFunctionCall } from "AIClasses/AIFunctionCall";
import type { AIFunctionService } from "Services/AIFunctionService";
import type { AIFunctionResponse } from "AIClasses/FunctionDefinitions/AIFunctionResponse";
import type AIAgentPlugin from "main";
import { openPluginSettings } from "Helpers/Helpers";
import { Selector } from "Enums/Selector";
import type { WorkSpaceService } from "Services/WorkSpaceService";
let plugin: AIAgentPlugin = Resolve(Services.AIAgentPlugin);
let ai: IAIClass = Resolve(Services.IAIClass);
let conversationService: ConversationFileSystemService = Resolve(Services.ConversationFileSystemService);
let aiFunctionService: AIFunctionService = Resolve(Services.AIFunctionService);
let workSpaceService: WorkSpaceService = Resolve(Services.WorkSpaceService);
let semaphore: Semaphore = new Semaphore(1, false);
let textareaElement: HTMLTextAreaElement;
@ -25,6 +32,7 @@
let submitButton: HTMLButtonElement;
let userRequest = "";
let hasNoApiKey = false;
let isSubmitting = false;
let isStreaming = false;
@ -32,12 +40,51 @@
let currentThought: string | null = null;
onMount(() => {
if (chatContainer) {
plugin.registerDomEvent(chatContainer, 'click', handleLinkClick);
}
});
async function handleLinkClick(evt: MouseEvent) {
const target = evt.target as HTMLElement;
const link = target.closest(`.${Selector.MarkDownLink}`) as HTMLAnchorElement | null;
if (!link) {
return;
}
const href = link.getAttribute('href');
if (!href || !href.startsWith('#/page/')) {
return;
}
evt.preventDefault();
evt.stopPropagation();
const encodedPath = href.replace('#/page/', '');
const notePath = decodeURIComponent(encodedPath);
await workSpaceService.openNote(notePath);
}
function handleNoApiKey(): boolean {
hasNoApiKey = plugin.settings.apiKey.trim() == "";
if (hasNoApiKey) {
openPluginSettings(plugin);
}
return hasNoApiKey;
}
async function handleSubmit() {
if (!await semaphore.wait()) {
return;
}
try {
if (handleNoApiKey()) {
return;
}
if (userRequest.trim() === "" || isSubmitting) {
return;
}
@ -103,6 +150,7 @@
}
if (chunk.content) {
currentThought = null;
accumulatedContent += chunk.content;
conversation.contents = conversation.contents.map((msg, messageIndex) =>
messageIndex === aiMessageIndex
@ -193,6 +241,7 @@
<div id="input-container">
<textarea
id="input"
class:error={hasNoApiKey}
bind:this={textareaElement}
bind:value={userRequest}
on:keydown={handleKeydown}
@ -247,12 +296,13 @@
grid-row: 2;
grid-column: 2;
min-height: var(--input-height);
max-height: var(--dialog-max-height);
max-height: 30vh;
border-radius: var(--input-radius);
font-weight: var(--input-font-weight);
border-width: var(--input-border-width);
resize: none;
overflow-y: auto;
scroll-behavior: smooth;
color: var(--font-interface-theme);
transition: border-color 0.3s ease-out;
}
@ -262,6 +312,15 @@
transition: border-color 0.3s ease-out;
}
#input.error,
#input.error:focus {
border-color: var(--color-red);
}
#input::-webkit-scrollbar {
display: none;
}
#submit {
grid-row: 2;
grid-column: 4;

View file

@ -6,6 +6,7 @@
import { ConversationFileSystemService } from '../Services/ConversationFileSystemService';
import { conversationStore } from '../Stores/conversationStore';
import type { ConversationHistoryModal } from 'Modals/ConversationHistoryModal';
import { openPluginSettings } from 'Helpers/Helpers';
export let leaf: WorkspaceLeaf;
@ -28,10 +29,7 @@
}
function openSettings() {
// @ts-ignore - accessing internal API
plugin.app.setting.open();
// @ts-ignore - accessing internal API
plugin.app.setting.openTabById(plugin.manifest.id);
openPluginSettings(plugin);
}
function closePlugin() {

3
Enums/Selector.ts Normal file
View file

@ -0,0 +1,3 @@
export enum Selector {
MarkDownLink = "ai-agent-internal-markdown-link",
}

View file

@ -1,4 +1,11 @@
import { type Vault } from "obsidian";
import type AIAgentPlugin from "main";
export function openPluginSettings(plugin: AIAgentPlugin) {
// @ts-ignore - accessing internal API
plugin.app.setting.open();
// @ts-ignore - accessing internal API
plugin.app.setting.openTabById(plugin.manifest.id);
}
export function isValidJson(str: string): boolean {
try {
@ -9,18 +16,6 @@ export function isValidJson(str: string): boolean {
return true;
}
export function loadExternalCSS(href: string): Promise<void> {
return new Promise((resolve, reject) => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = href;
link.onload = () => resolve();
link.onerror = reject;
document.head.appendChild(link);
});
}
export function dateToString(date: Date, includeTime: boolean = true): string {
if (includeTime) {
return date.toLocaleString('sv-SE', {

View file

@ -1,157 +0,0 @@
// import { version as uuidVersion } from 'uuid';
// import { validate as uuidValidate } from 'uuid';
// import type { OdbCache } from './OdbCache';
// import { Resolve } from 'Services/DependencyService';
// import { Services } from 'Services/Services';
// import type AIAgentPlugin from 'main';
// import { TAbstractFile, TFile, type Vault } from 'obsidian';
// interface DynamicProps {
// [key: string]: any;
// }
// export class DynamicRecord {
// public readonly type: string;
// public readonly schema: object;
// public readonly objectId: string;
// public readonly recordPath: string;
// public props: DynamicProps;
// private vault: Vault;
// private odbCache: OdbCache;
// public constructor(recordPath: string, schema: object, record: { [key: string]: any }) {
// this.odbCache = Resolve<OdbCache>(Services.OdbCache);
// this.vault = Resolve<AIAgentPlugin>(Services.AIAgentPlugin).app.vault;
// this.schema = schema;
// this.recordPath = recordPath;
// if (record[DynamicRecordProp.Type] === undefined || record[DynamicRecordProp.ObjectId] === undefined) {
// throw new Error(`Invalid record format: missing ${DynamicRecordProp.Type} or ${DynamicRecordProp.ObjectId}`);
// }
// this.type = record[DynamicRecordProp.Type];
// this.objectId = record[DynamicRecordProp.ObjectId];
// let parsedRecord: Record<string,any> = {};
// for (let key in schema) {
// if (!(key in record) || key === DynamicRecordProp.Type || key === DynamicRecordProp.ObjectId) {
// continue;
// }
// parsedRecord[key] = this.parse(record[key]);
// }
// this.props = this.createProxy(parsedRecord);
// }
// public save() {
// let file: TAbstractFile | null = this.vault.getAbstractFileByPath(this.recordPath);
// if (file == null || !(file instanceof TAbstractFile)) {
// this.vault.create(this.recordPath, this.toFileContent());
// }
// this.vault.modify(file as TFile, this.toFileContent());
// }
// public delete() {
// let file: TAbstractFile | null = this.vault.getAbstractFileByPath(this.recordPath);
// if (file == null || !(file instanceof TAbstractFile)) {
// return;
// }
// this.vault.delete(file as TFile);
// }
// public move(newPath: string) {
// let file: TAbstractFile | null = this.vault.getAbstractFileByPath(this.recordPath);
// if (file == null || !(file instanceof TAbstractFile)) {
// return;
// }
// this.vault.rename(file as TFile, newPath);
// }
// private parse(data: any): any {
// if (typeof data === null) {
// return data;
// }
// if (Array.isArray(data)) {
// let arr: any[] = [];
// for (let item of data) {
// arr.push(this.parse(item));
// }
// return arr;
// }
// if (typeof data === "object") {
// let obj: Record<string,any> = {};
// for (let key in data) {
// obj[key] = this.parse(data[key]);
// }
// return this.createProxy(obj);
// }
// return data;
// }
// private createProxy(data: object): DynamicProps {
// return new Proxy(data, {
// get: (target, prop, receiver) => {
// if (this.isObjectId(String(prop))) {
// return this.odbCache.getRecord(String(prop));
// }
// return Reflect.get(target, prop, receiver);
// },
// set: (target, prop, value, receiver) => {
// return Reflect.set(target, prop, value, receiver);
// }
// });
// }
// private isObjectId(key: string): boolean {
// return uuidValidate(key) && uuidVersion(key) === 4;
// }
// private toFileContent(): string {
// let content: Record<string,any> = {};
// content[DynamicRecordProp.Type] = this.type;
// content[DynamicRecordProp.ObjectId] = this.objectId;
// // use the schema when saving to file in case the AI did something funky
// for (let key in this.schema) {
// if (!(key in this.props) || key === DynamicRecordProp.Type || key === DynamicRecordProp.ObjectId) {
// continue;
// }
// content[key] = this.serialise(this.props[key]);
// }
// return JSON.stringify(content, null, 4);
// }
// private serialise(value: any): any {
// if (value instanceof DynamicRecord) {
// return value.objectId;
// }
// if (Array.isArray(value)) {
// let arr: any[] = [];
// for (let item of value) {
// arr.push(this.serialise(item));
// }
// return arr;
// }
// if (typeof value === "object") {
// let obj: Record<string,any> = {};
// for (let key in value) {
// obj[key] = this.serialise(value[key]);
// }
// return obj;
// }
// return value;
// }
// }

View file

@ -1,131 +0,0 @@
// import { TAbstractFile, TFile, TFolder, Vault } from "obsidian";
// import { DynamicRecord } from "./DynamicRecord";
// import { Resolve } from "Services/DependencyService";
// import type AIAgentPlugin from "main";
// import { Services } from "Services/Services";
// import { Path } from "Enums/Path";
// import { DynamicRecordProp } from "Enums/DynamicRecord";
// import { FileAction } from "Enums/FileAction";
// import { isValidJson } from "Helpers/Helpers";
// export class OdbCache {
// private vault: Vault;
// private schemas: Map<string, object> = new Map<string, object>();
// private cache: Map<string, DynamicRecord> = new Map<string, DynamicRecord>();
// public constructor() {
// this.vault = Resolve<AIAgentPlugin>(Services.AIAgentPlugin).app.vault;
// }
// public getSchemas(): Map<string, object> {
// return this.schemas;
// }
// public getRecord(objectId: string): DynamicRecord | null {
// return this.cache.get(objectId) ?? null;
// }
// public async buildCache() {
// await this.loadSchemas();
// let recordDir: TAbstractFile | null = this.vault.getAbstractFileByPath(Path.Records);
// if (!(recordDir instanceof TFolder)) {
// return;
// }
// this.recursiveBuild(recordDir);
// }
// public async onFileChanged(file: TAbstractFile, fileAction: FileAction) {
// if (file instanceof TFolder) {
// for (let child of file.children) {
// this.onFileChanged(child, fileAction);
// }
// return;
// }
// for (let [_, record] of this.cache) {
// if (record.recordPath === file.path) {
// switch (fileAction) {
// case FileAction.Create:
// this.addToCache(file as TFile);
// break;
// case FileAction.Modify, FileAction.Rename:
// this.removeFromCache(record.objectId);
// this.addToCache(file as TFile);
// break;
// case FileAction.Delete:
// this.removeFromCache(record.objectId);
// break;
// }
// }
// }
// }
// private async recursiveBuild(child : TAbstractFile) {
// if (child instanceof TFolder) {
// for (let subChild of child.children) {
// await this.recursiveBuild(subChild);
// }
// } else if (child instanceof TFile && child.extension === "json") {
// await this.addToCache(child);
// }
// }
// private async createDynamicRecord(file: TFile): Promise<DynamicRecord> {
// let contents: { [key: string]: any } = JSON.parse(await this.vault.read(file));
// if (contents[DynamicRecordProp.Type] === undefined || contents[DynamicRecordProp.ObjectId] === undefined) {
// throw new Error(`Invalid record format in file ${file.path}: missing ${DynamicRecordProp.Type} or ${DynamicRecordProp.ObjectId}`);
// }
// let schema: object | undefined = this.schemas.get(contents[DynamicRecordProp.Type]);
// if (schema === undefined) {
// throw new Error(`No schema found for record type ${contents[DynamicRecordProp.Type]} in file ${file.path}`);
// }
// return new DynamicRecord(file.path, schema, contents);
// }
// private async loadSchemas() {
// let schemaDir: TAbstractFile | null = this.vault.getAbstractFileByPath(Path.Schemas);
// if (!(schemaDir instanceof TFolder)) {
// return;
// }
// for (let child of schemaDir.children) {
// if (!(child instanceof TFile) || child.extension !== "json") {
// continue;
// }
// let contents: string = await this.vault.read(child);
// if (!isValidJson(contents)) {
// console.warn(`Invalid schema format in file ${child.path}`);
// continue;
// }
// let schema: Record<string,any> = JSON.parse(contents);
// if (schema[DynamicRecordProp.Type] === undefined || schema[DynamicRecordProp.ObjectId] === undefined) {
// throw new Error(`Invalid schema format in file ${child.path}: missing ${DynamicRecordProp.Type} or ${DynamicRecordProp.ObjectId}`);
// }
// this.schemas.set(schema[DynamicRecordProp.Type], schema);
// }
// }
// private async addToCache(file: TFile) {
// let record: DynamicRecord = await this.createDynamicRecord(file);
// this.cache.set(record.objectId, record);
// }
// private async removeFromCache(objectId: string) {
// this.cache.delete(objectId);
// }
// }

View file

@ -12,6 +12,13 @@ export class FileSystemService {
this.vault = Resolve<AIAgentPlugin>(Services.AIAgentPlugin).app.vault;
}
public getVaultFileListForMarkDown() {
const files: TFile[] = this.vault.getMarkdownFiles();
return files.map(file => {
return file.path.replace(/\.md$/, "");
});
}
public async readFile(filePath: string): Promise<string | null> {
const file: TAbstractFile | null = this.vault.getAbstractFileByPath(filePath);
if (file && file instanceof TFile) {

View file

@ -1,6 +1,5 @@
import { AIProvider } from "Enums/ApiProvider";
import type AIAgentPlugin from "main";
//import { OdbCache } from "ODB/Core/OdbCache";
import { RegisterSingleton, RegisterTransient } from "./DependencyService";
import { Services } from "./Services";
import { AIPrompt, type IPrompt } from "AIClasses/IPrompt";
@ -15,11 +14,12 @@ import type { App } from "obsidian";
import { AIFunctionService } from "./AIFunctionService";
import { StreamingService } from "./StreamingService";
import { AIFunctionDefinitions } from "AIClasses/FunctionDefinitions/AIFunctionDefinitions";
import { WorkSpaceService } from "./WorkSpaceService";
export function RegisterDependencies(plugin: AIAgentPlugin) {
RegisterSingleton(Services.MessageService, new MessageService());
RegisterSingleton(Services.AIAgentPlugin, plugin);
//RegisterSingleton(Services.OdbCache, new OdbCache());
RegisterSingleton(Services.MessageService, new MessageService());
RegisterSingleton(Services.WorkSpaceService, new WorkSpaceService());
RegisterSingleton(Services.FileSystemService, new FileSystemService());
RegisterSingleton(Services.ConversationFileSystemService, new ConversationFileSystemService());
@ -36,7 +36,7 @@ export function RegisterDependencies(plugin: AIAgentPlugin) {
export function RegisterAiProvider(plugin: AIAgentPlugin) {
if (plugin.settings.apiProvider == AIProvider.Gemini) {
RegisterSingleton<IAIClass>(Services.IAIClass, new Gemini(plugin.settings.apiKey));
RegisterSingleton<IAIClass>(Services.IAIClass, new Gemini());
}
}

View file

@ -1,7 +1,7 @@
export class Services {
static MessageService = Symbol("MessageService");
static AIAgentPlugin = Symbol("AIAgentPlugin");
static OdbCache = Symbol("OdbCache");
static WorkSpaceService = Symbol("WorkSpaceService");
static FileSystemService = Symbol("FileSystemService");
static ConversationFileSystemService = Symbol("ConversationFileSystemService");
static StreamingService = Symbol("StreamingService");

View file

@ -7,6 +7,11 @@ import remarkRehype from 'remark-rehype';
import rehypeKatex from 'rehype-katex';
import rehypeHighlight from 'rehype-highlight';
import rehypeStringify from 'rehype-stringify';
import wikiLinkPlugin from 'remark-wiki-link';
import type { FileSystemService } from './FileSystemService';
import { Resolve } from './DependencyService';
import { Services } from './Services';
import { Selector } from 'Enums/Selector';
interface StreamingState {
element: HTMLElement;
@ -16,6 +21,8 @@ interface StreamingState {
}
export class StreamingMarkdownService {
private readonly fileSystemService: FileSystemService = Resolve(Services.FileSystemService);
private readonly processor: Processor<any, any, any, any, any> | null = null;
private streamingStates = new Map<string, StreamingState>();
@ -44,6 +51,12 @@ export class StreamingMarkdownService {
allowDangerousHtml: false,
allowDangerousCharacters: false,
closeSelfClosing: true
})
.use(wikiLinkPlugin, {
permalinks: this.fileSystemService.getVaultFileListForMarkDown(),
wikiLinkClassName: Selector.MarkDownLink,
pageResolver: (pageName: string) => [pageName],
hrefTemplate: (permalink: string) => `#/page/${encodeURIComponent(permalink)}`
});
}

View file

@ -1,75 +1,75 @@
import type { AIFunctionCall } from "AIClasses/AIFunctionCall";
export interface StreamChunk {
content: string;
isComplete: boolean;
error?: string;
functionCall?: AIFunctionCall;
}
export class StreamingService {
public async* streamRequest(
url: string,
requestBody: unknown,
parseStreamChunk: (chunk: string) => StreamChunk
): AsyncGenerator<StreamChunk, void, unknown> {
try {
const response = await fetch(
url,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
}
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API request failed: ${response.status} - ${errorText}`);
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error("Response body is not readable");
}
const decoder = new TextDecoder();
let buffer = "";
let lastChunkWasComplete = false;
content: string;
isComplete: boolean;
error?: string;
functionCall?: AIFunctionCall;
}
while (true) {
const { done, value } = await reader.read();
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || ""; // Keep potentially incomplete line in buffer
for (const line of lines) {
if (line.trim().startsWith("data:")) {
const jsonStr = line.trim().substring(5);
const chunk = parseStreamChunk(jsonStr);
lastChunkWasComplete = chunk.isComplete;
yield chunk;
}
}
export class StreamingService {
public async* streamRequest(
url: string,
requestBody: unknown,
parseStreamChunk: (chunk: string) => StreamChunk
): AsyncGenerator<StreamChunk, void, unknown> {
try {
const response = await fetch(
url,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
}
);
if (done) {
break;
}
}
if (!lastChunkWasComplete) {
yield { content: "", isComplete: true };
}
} catch (error) {
console.error("Stream request error:", error);
yield {
content: "",
isComplete: true,
error: error instanceof Error ? error.message : "Unknown error",
};
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API request failed: ${response.status} - ${errorText}`);
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error("Response body is not readable");
}
const decoder = new TextDecoder();
let buffer = "";
let lastChunkWasComplete = false;
while (true) {
const { done, value } = await reader.read();
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || ""; // Keep potentially incomplete line in buffer
for (const line of lines) {
if (line.trim().startsWith("data:")) {
const jsonStr = line.trim().substring(5);
const chunk = parseStreamChunk(jsonStr);
lastChunkWasComplete = chunk.isComplete;
yield chunk;
}
}
if (done) {
break;
}
}
if (!lastChunkWasComplete) {
yield { content: "", isComplete: true };
}
} catch (error) {
console.error("Stream request error:", error);
yield {
content: "",
isComplete: true,
error: error instanceof Error ? error.message : "Unknown error",
};
}
}
}
}

View file

@ -0,0 +1,17 @@
import type AIAgentPlugin from "main";
import { Resolve } from "./DependencyService";
import { Services } from "./Services";
import type { TFile, WorkspaceLeaf } from "obsidian";
export class WorkSpaceService {
private readonly plugin: AIAgentPlugin = Resolve(Services.AIAgentPlugin);
public async openNote(noteName: string) {
const file: TFile | null = this.plugin.app.metadataCache.getFirstLinkpathDest(noteName, "");
const leaf: WorkspaceLeaf = this.plugin.app.workspace.getLeaf(false);
if (file) {
await leaf.openFile(file);
}
}
}

115
main.ts
View file

@ -1,13 +1,9 @@
import { App, Editor, MarkdownView, WorkspaceLeaf, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
import { Resolve } from './Services/DependencyService';
import { WorkspaceLeaf, Plugin } from 'obsidian';
import { AIProvider } from './Enums/ApiProvider';
import { MainView, VIEW_TYPE_MAIN } from 'Views/MainView';
import { Services } from 'Services/Services';
// import { OdbCache } from 'ODB/Core/OdbCache';
// import { FileAction } from 'Enums/FileAction';
import { Path } from 'Enums/Path';
import { RegisterAiProvider, RegisterDependencies } from 'Services/ServiceRegistration';
import { AIAgentSettingTab } from 'AIAgentSettingTab';
interface AIAgentSettings {
apiProvider: string;
@ -30,8 +26,6 @@ export default class AIAgentPlugin extends Plugin {
RegisterDependencies(this);
CreateDirectories(this);
this.registerView(
VIEW_TYPE_MAIN,
(leaf) => new MainView(leaf)
@ -53,40 +47,7 @@ export default class AIAgentPlugin extends Plugin {
const statusBarItemEl = this.addStatusBarItem();
statusBarItemEl.setText('Status Bar Text');
// This adds an editor command that can perform some operation on the current editor instance
this.addCommand({
id: 'sample-editor-command',
name: 'Sample editor command',
editorCallback: (editor: Editor, view: MarkdownView) => {
console.log(editor.getSelection());
editor.replaceSelection('Sample Editor Command');
}
});
this.addSettingTab(new AIAgentSettingTab(this.app, this));
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
// Using this function will automatically remove the event listener when this plugin is disabled.
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
console.log('click', evt);
});
// let odbCache: OdbCache = Resolve<OdbCache>(Services.OdbCache)
// await odbCache.buildCache();
// this.registerEvent(
// this.app.vault.on(FileAction.Create, (file) => odbCache.onFileChanged(file, FileAction.Create))
// );
// this.registerEvent(
// this.app.vault.on(FileAction.Modify, (file) => odbCache.onFileChanged(file, FileAction.Modify))
// );
// this.registerEvent(
// this.app.vault.on(FileAction.Delete, (file) => odbCache.onFileChanged(file, FileAction.Delete))
// );
// this.registerEvent(
// this.app.vault.on(FileAction.Rename, (file) => odbCache.onFileChanged(file, FileAction.Rename))
// );
}
async onunload() {
@ -99,16 +60,12 @@ export default class AIAgentPlugin extends Plugin {
const leaves = workspace.getLeavesOfType(VIEW_TYPE_MAIN);
if (leaves.length > 0) {
// A leaf with our view already exists, use that
leaf = leaves[0];
} else {
// Our view could not be found in the workspace, create a new leaf
// in the right sidebar for it
leaf = workspace.getRightLeaf(false);
await leaf?.setViewState({ type: VIEW_TYPE_MAIN, active: true });
}
// "Reveal" the leaf in case it is in a collapsed sidebar
if (leaf != null) {
workspace.revealLeaf(leaf);
}
@ -122,70 +79,4 @@ export default class AIAgentPlugin extends Plugin {
await this.saveData(this.settings);
RegisterAiProvider(this);
}
}
class SampleModal extends Modal {
constructor(app: App) {
super(app);
}
onOpen() {
const { contentEl } = this;
contentEl.setText('Woah!');
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
class AIAgentSettingTab extends PluginSettingTab {
plugin: AIAgentPlugin;
constructor(app: App, plugin: AIAgentPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName("API Provider")
.setDesc("Select the API provider to use.")
.addDropdown((dropdown) =>
dropdown
.addOption(AIProvider.Gemini, AIProvider.Gemini)
.addOption(AIProvider.OpenAI, AIProvider.OpenAI)
.setValue(this.plugin.settings.apiProvider)
.onChange(async (value) => {
this.plugin.settings.apiProvider = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("API Key")
.setDesc("Enter your API key here.")
.addText(text => text
.setPlaceholder("Enter your API key")
.setValue(this.plugin.settings.apiKey)
.onChange(async (value) => {
this.plugin.settings.apiKey = value;
await this.plugin.saveSettings();
}));
}
}
function CreateDirectories(plugin: AIAgentPlugin) {
this.app.workspace.onLayoutReady(async () => {
const vault = plugin.app.vault;
if (vault.getAbstractFileByPath(Path.Root) == null) {
vault.createFolder(Path.Root);
}
});
}
}

259
package-lock.json generated
View file

@ -32,6 +32,7 @@
"remark-rehype": "^11.1.2",
"remark-relative-links": "^0.0.6",
"remark-toc": "^9.0.0",
"remark-wiki-link": "^2.0.1",
"svelte-exmarkdown": "^5.0.2",
"unified": "^11.0.5",
"uuid": "^11.1.0"
@ -52,6 +53,15 @@
"typescript": "~5.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
@ -587,9 +597,9 @@
}
},
"node_modules/@google/genai": {
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.21.0.tgz",
"integrity": "sha512-k47DECR8BF9z7IJxQd3reKuH2eUnOH5NlJWSe+CKM6nbXx+wH3hmtWQxUQR9M8gzWW1EvFuRVgjQssEIreNZsw==",
"version": "1.22.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.22.0.tgz",
"integrity": "sha512-siETS3zTm3EGpTT4+BFc1z20xXBYfueD3gCYfxkOjuAKRk8lt8TJevDHi3zepn1oSI6NhG/LZvy0i+Q3qheObg==",
"license": "Apache-2.0",
"dependencies": {
"google-auth-library": "^9.14.2",
@ -900,9 +910,9 @@
}
},
"node_modules/@types/express-serve-static-core": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz",
"integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz",
"integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -985,6 +995,28 @@
"license": "MIT"
},
"node_modules/@types/send": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz",
"integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/serve-static": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz",
"integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
"@types/node": "*",
"@types/send": "<1"
}
},
"node_modules/@types/serve-static/node_modules/@types/send": {
"version": "0.17.5",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
"integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==",
@ -995,18 +1027,6 @@
"@types/node": "*"
}
},
"node_modules/@types/serve-static": {
"version": "1.15.8",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz",
"integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
"@types/node": "*",
"@types/send": "*"
}
},
"node_modules/@types/tern": {
"version": "0.23.9",
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
@ -1599,6 +1619,16 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/character-reference-invalid": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz",
"integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@ -3240,6 +3270,30 @@
"node": ">= 0.10"
}
},
"node_modules/is-alphabetical": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
"integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-alphanumerical": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz",
"integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==",
"license": "MIT",
"dependencies": {
"is-alphabetical": "^1.0.0",
"is-decimal": "^1.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-buffer": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
@ -3263,6 +3317,16 @@
"node": ">=4"
}
},
"node_modules/is-decimal": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz",
"integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -3286,6 +3350,16 @@
"node": ">=0.10.0"
}
},
"node_modules/is-hexadecimal": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
"integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@ -3423,9 +3497,9 @@
}
},
"node_modules/katex": {
"version": "0.16.22",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz",
"integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==",
"version": "0.16.23",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.23.tgz",
"integrity": "sha512-7VlC1hsEEolL9xNO05v9VjrvWZePkCVBJqj8ruICxYjZfHaHbaU53AlP+PODyFIXEnaEIEWi3wJy7FPZ95JAVg==",
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"
@ -4107,6 +4181,70 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-wiki-link": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/mdast-util-wiki-link/-/mdast-util-wiki-link-0.1.2.tgz",
"integrity": "sha512-DTcDyOxKDo3pB3fc0zQlD8myfQjYkW4hazUKI9PUyhtoj9JBeHC2eIdlVXmaT22bZkFAVU2d47B6y2jVKGoUQg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.1",
"mdast-util-to-markdown": "^0.6.5"
}
},
"node_modules/mdast-util-wiki-link/node_modules/@types/unist": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT"
},
"node_modules/mdast-util-wiki-link/node_modules/longest-streak": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz",
"integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/mdast-util-wiki-link/node_modules/mdast-util-to-markdown": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz",
"integrity": "sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==",
"license": "MIT",
"dependencies": {
"@types/unist": "^2.0.0",
"longest-streak": "^2.0.0",
"mdast-util-to-string": "^2.0.0",
"parse-entities": "^2.0.0",
"repeat-string": "^1.0.0",
"zwitch": "^1.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-wiki-link/node_modules/mdast-util-to-string": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz",
"integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-wiki-link/node_modules/zwitch": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz",
"integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/media-typer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
@ -4707,6 +4845,15 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-wiki-link": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/micromark-extension-wiki-link/-/micromark-extension-wiki-link-0.0.4.tgz",
"integrity": "sha512-dJc8AfnoU8BHkN+7fWZvIS20SMsMS1ZlxQUn6We67MqeKbOiEDZV5eEvCpwqGBijbJbxX3Kxz879L4K9HIiOvw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.1"
}
},
"node_modules/micromark-factory-destination": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
@ -5338,6 +5485,44 @@
"node": ">=6"
}
},
"node_modules/parse-entities": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz",
"integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==",
"license": "MIT",
"dependencies": {
"character-entities": "^1.0.0",
"character-entities-legacy": "^1.0.0",
"character-reference-invalid": "^1.0.0",
"is-alphanumerical": "^1.0.0",
"is-decimal": "^1.0.0",
"is-hexadecimal": "^1.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/parse-entities/node_modules/character-entities": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
"integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/parse-entities/node_modules/character-entities-legacy": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
"integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
@ -5985,6 +6170,26 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-wiki-link": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/remark-wiki-link/-/remark-wiki-link-2.0.1.tgz",
"integrity": "sha512-F8Eut1E7GWfFm4ZDTI6/4ejeZEHZgnVk6E933Yqd/ssYsc4AyI32aGakxwsGcEzbbE7dkWi1EfLlGAdGgOZOsA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.4.4",
"mdast-util-wiki-link": "^0.1.2",
"micromark-extension-wiki-link": "^0.0.4"
}
},
"node_modules/repeat-string": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
"integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
"license": "MIT",
"engines": {
"node": ">=0.10"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@ -6104,9 +6309,9 @@
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
@ -6378,9 +6583,9 @@
}
},
"node_modules/svelte": {
"version": "5.39.8",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.39.8.tgz",
"integrity": "sha512-KfZ3hCITdxIXTOvrea4nFZX2o+47HPTChKeocgj9BwJQYqWrviVCcPj4boXHF5yf8+eBKqhHY8xii//XaakKXA==",
"version": "5.39.10",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.39.10.tgz",
"integrity": "sha512-Q3gqCGIgl4r0CR7OaWYjVo22nqFmLLSfn1MiWNFaITamvqhGBD3kyqk51EKuO4Nd1z8QliO5KIz7a2Ka9Rxilw==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.4",

View file

@ -51,6 +51,7 @@
"remark-rehype": "^11.1.2",
"remark-relative-links": "^0.0.6",
"remark-toc": "^9.0.0",
"remark-wiki-link": "^2.0.1",
"svelte-exmarkdown": "^5.0.2",
"unified": "^11.0.5",
"uuid": "^11.1.0"

View file

@ -29,6 +29,32 @@
padding-top: 0;
}
/* ============================== */
/* Settings API Key Highlighting */
/* ============================== */
.api-key-setting-ok input[type="text"],
.api-key-setting-ok input[type="password"],
.api-key-setting-ok textarea {
border-color: var(--color-green) !important;
}
.api-key-setting-error input[type="text"],
.api-key-setting-error input[type="password"],
.api-key-setting-error textarea {
border-color: var(--color-red) !important;
}
.api-key-setting-error .setting-item-control {
animation: shake 0.5s ease-in-out;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
/* ============================== */
/* CSS Variables for Common Components */
/* ============================== */