mirror of
https://github.com/fbarrca/obsidian-inlineAI.git
synced 2026-07-22 11:50:24 +00:00
Add basic message history
This commit is contained in:
parent
13d0a1ebf9
commit
031efb3ae4
3 changed files with 168 additions and 22 deletions
48
src/api.ts
48
src/api.ts
|
|
@ -8,6 +8,14 @@ import { App, MarkdownView, Notice } from "obsidian";
|
|||
import { EditorView } from "@codemirror/view";
|
||||
import { setGeneratedResponseEffect } from "./modules/AIExtension";
|
||||
import { parseCommand } from "./modules/commands/parser";
|
||||
import { MessageQueue } from "./modules/messageHistory/queue";
|
||||
|
||||
const MESSAGE_HISTORY_LIMIT = 20;
|
||||
|
||||
export type HistoryMessage = {
|
||||
mode: string;
|
||||
userPrompt: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Class to manage interactions with different chat APIs.
|
||||
|
|
@ -16,7 +24,7 @@ export class ChatApiManager {
|
|||
private chatClient: ChatOpenAI | ChatOllama | ChatGoogleGenerativeAI | null;
|
||||
private app: App;
|
||||
private settings: InlineAISettings;
|
||||
|
||||
private messageHistory: MessageQueue<HistoryMessage>;
|
||||
/**
|
||||
* Initializes the ChatApiManager with the given settings.
|
||||
* @param settings - Configuration settings for the chat API.
|
||||
|
|
@ -26,6 +34,7 @@ export class ChatApiManager {
|
|||
this.app = app;
|
||||
this.chatClient = this.initializeChatClient(settings);
|
||||
this.settings = settings;
|
||||
this.messageHistory = new MessageQueue<HistoryMessage>(MESSAGE_HISTORY_LIMIT);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -138,25 +147,14 @@ export class ChatApiManager {
|
|||
return "⚠️ Failed to process request.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles user input and generates a response using the cursor API.
|
||||
* @param userRequest - The user's request to process.
|
||||
* @returns The AI-generated response or an error message.
|
||||
*/
|
||||
public async callCursor(userRequest: string): Promise<string> {
|
||||
const systemPrompt = "You are a helpful assistant. Please help the user with the following request:";
|
||||
return this.handleEditorUpdate(systemPrompt, userRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Processes selected text using the specified prompt and transformation.
|
||||
* @param prompt - The transformation prompt (e.g., "Add Emojis").
|
||||
* @param userPrompt - The transformation prompt (e.g., "Add Emojis").
|
||||
* @param selectedText - The selected text to transform.
|
||||
* @returns The transformed text or an error message.
|
||||
*/
|
||||
public async callSelection(prompt: string, selectedText: string): Promise<string> {
|
||||
prompt = parseCommand(prompt, this.settings.commandPrefix, this.settings.customCommands);
|
||||
public async callSelection(userPrompt: string, selectedText: string): Promise<string> {
|
||||
userPrompt = parseCommand(userPrompt, this.settings.commandPrefix, this.settings.customCommands);
|
||||
|
||||
let isCursor = false;
|
||||
if (selectedText.trim().length === 0) {
|
||||
|
|
@ -164,21 +162,23 @@ export class ChatApiManager {
|
|||
}
|
||||
|
||||
const systemPrompt = isCursor ? this.settings.cursorPrompt : this.settings.selectionPrompt;
|
||||
let userPrompt = ``;
|
||||
let finalUserPrompt = ``;
|
||||
const mode = isCursor ? "cursor" : "selection";
|
||||
this.messageHistory.enqueue({ mode, userPrompt });
|
||||
|
||||
if (isCursor) {
|
||||
userPrompt = `
|
||||
**Task:** ${prompt}
|
||||
finalUserPrompt = `
|
||||
**Task:** ${userPrompt}
|
||||
**Output:**`;
|
||||
} else {
|
||||
userPrompt = `
|
||||
**Task:** ${prompt}
|
||||
finalUserPrompt = `
|
||||
**Task:** ${userPrompt}
|
||||
**Input:**
|
||||
${selectedText}
|
||||
|
||||
**Output:**`;
|
||||
}
|
||||
return this.handleEditorUpdate(systemPrompt, userPrompt);
|
||||
return this.handleEditorUpdate(systemPrompt, finalUserPrompt);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -193,4 +193,8 @@ export class ChatApiManager {
|
|||
}
|
||||
this.chatClient = newChatClient;
|
||||
}
|
||||
|
||||
public getMessageHistory(): HistoryMessage[] {
|
||||
return this.messageHistory.getItems();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ class FloatingWidget extends WidgetType {
|
|||
private acceptButton!: HTMLButtonElement;
|
||||
private discardButton!: HTMLButtonElement;
|
||||
|
||||
// Track the current index in message history for up/down navigation
|
||||
private messageHistoryIndex: number | null = null;
|
||||
|
||||
constructor(chatApiManager: ChatApiManager, selectionInfo: SelectionInfo | null, plugin: InlineAIChatPlugin) {
|
||||
super();
|
||||
this.chatApiManager = chatApiManager;
|
||||
|
|
@ -111,6 +114,7 @@ class FloatingWidget extends WidgetType {
|
|||
|
||||
this.textFieldView = undefined;
|
||||
this.outerEditorView = null;
|
||||
this.messageHistoryIndex = null;
|
||||
}
|
||||
|
||||
private dismissTooltip() {
|
||||
|
|
@ -159,7 +163,81 @@ class FloatingWidget extends WidgetType {
|
|||
},
|
||||
preventDefault: true,
|
||||
},
|
||||
{
|
||||
key: "ArrowUp",
|
||||
run: () => {
|
||||
const messageHistory = this.chatApiManager.getMessageHistory();
|
||||
if (messageHistory.length === 0 || !this.textFieldView) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If not started, set to last index
|
||||
if (this.messageHistoryIndex === null) {
|
||||
this.messageHistoryIndex = messageHistory.length - 1;
|
||||
} else if (this.messageHistoryIndex > 0) {
|
||||
this.messageHistoryIndex--;
|
||||
}
|
||||
// Clamp to 0
|
||||
if (this.messageHistoryIndex < 0) this.messageHistoryIndex = 0;
|
||||
|
||||
const msg = messageHistory[this.messageHistoryIndex].userPrompt;
|
||||
this.textFieldView.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: this.textFieldView.state.doc.length,
|
||||
insert: msg,
|
||||
}
|
||||
});
|
||||
// Move cursor to end
|
||||
this.textFieldView.dispatch({
|
||||
selection: { anchor: msg.length, head: msg.length }
|
||||
});
|
||||
return true;
|
||||
},
|
||||
preventDefault: true,
|
||||
},
|
||||
{
|
||||
key: "ArrowDown",
|
||||
run: () => {
|
||||
const messageHistory = this.chatApiManager.getMessageHistory();
|
||||
if (messageHistory.length === 0 || !this.textFieldView) {
|
||||
return true;
|
||||
}
|
||||
if (this.messageHistoryIndex === null) {
|
||||
// Do nothing if not in history navigation
|
||||
return true;
|
||||
}
|
||||
if (this.messageHistoryIndex < messageHistory.length - 1) {
|
||||
this.messageHistoryIndex++;
|
||||
const msg = messageHistory[this.messageHistoryIndex].userPrompt;
|
||||
this.textFieldView.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: this.textFieldView.state.doc.length,
|
||||
insert: msg,
|
||||
}
|
||||
});
|
||||
// Move cursor to end
|
||||
this.textFieldView.dispatch({
|
||||
selection: { anchor: msg.length, head: msg.length }
|
||||
});
|
||||
} else {
|
||||
// If at the end, clear the field and exit history navigation
|
||||
this.messageHistoryIndex = null;
|
||||
this.textFieldView.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: this.textFieldView.state.doc.length,
|
||||
insert: "",
|
||||
}
|
||||
});
|
||||
}
|
||||
return true;
|
||||
},
|
||||
preventDefault: true,
|
||||
},
|
||||
]),
|
||||
|
||||
// 3) Enable slash-command autocompletion
|
||||
slashCommandAutocompletion({
|
||||
prefix: this.plugin.settings.commandPrefix,
|
||||
|
|
@ -174,6 +252,13 @@ class FloatingWidget extends WidgetType {
|
|||
parent: editorDom,
|
||||
})
|
||||
|
||||
// Reset messageHistoryIndex when user types or changes input
|
||||
this.textFieldView.dom.addEventListener("input", () => {
|
||||
// Only reset if the input is not the result of an up/down navigation
|
||||
// (i.e., if the value doesn't match the current history entry)
|
||||
// But for simplicity, always reset if user types
|
||||
this.messageHistoryIndex = null;
|
||||
});
|
||||
}
|
||||
|
||||
private createSubmitButton() {
|
||||
|
|
@ -223,6 +308,8 @@ class FloatingWidget extends WidgetType {
|
|||
this.toggleLoading(false);
|
||||
});
|
||||
|
||||
// Reset message history navigation after submit
|
||||
this.messageHistoryIndex = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
55
src/modules/messageHistory/queue.ts
Normal file
55
src/modules/messageHistory/queue.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* MessageQueue is a generic fixed-length queue for storing recent chat messages.
|
||||
* - When a new item is enqueued and the queue is full, the oldest item is removed.
|
||||
* - Duplicate consecutive items are not added.
|
||||
* - Provides standard queue operations: enqueue, dequeue, peek, size, isEmpty, and getItems.
|
||||
*/
|
||||
export class MessageQueue<T> {
|
||||
private items: T[] = [];
|
||||
private maxLength: number;
|
||||
|
||||
constructor(maxLength: number) {
|
||||
this.maxLength = maxLength;
|
||||
}
|
||||
|
||||
// Adds an item to the queue
|
||||
enqueue(item: T): void {
|
||||
// If the new item is exactly the same as the previous one, do not add it
|
||||
if (this.items.length > 0) {
|
||||
const lastItem = this.items[this.items.length - 1];
|
||||
if (lastItem === item) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.items.length >= this.maxLength) {
|
||||
// Remove the oldest item if the queue is full
|
||||
this.items.shift();
|
||||
}
|
||||
this.items.push(item);
|
||||
}
|
||||
|
||||
// Removes and returns the item at the front of the queue
|
||||
dequeue(): T | undefined {
|
||||
return this.items.shift();
|
||||
}
|
||||
|
||||
// Returns the item at the front of the queue without removing it
|
||||
peek(): T | undefined {
|
||||
return this.items[0];
|
||||
}
|
||||
|
||||
// Returns the current size of the queue
|
||||
size(): number {
|
||||
return this.items.length;
|
||||
}
|
||||
|
||||
// Checks if the queue is empty
|
||||
isEmpty(): boolean {
|
||||
return this.items.length === 0;
|
||||
}
|
||||
|
||||
// Returns the items in the queue
|
||||
getItems(): T[] {
|
||||
return [...this.items];
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue