mirror of
https://github.com/fbarrca/obsidian-inlineAI.git
synced 2026-07-22 05:34:48 +00:00
Reformat the code with the new prettier config
This commit is contained in:
parent
41e73ea28a
commit
ad60944974
17 changed files with 1191 additions and 998 deletions
35
.github/workflows/format.yml
vendored
35
.github/workflows/format.yml
vendored
|
|
@ -1,26 +1,25 @@
|
|||
name: Format Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
push:
|
||||
branches: [main, master]
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
|
||||
jobs:
|
||||
prettier:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
prettier:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Prettier check
|
||||
run: npx prettier --check .
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Prettier check
|
||||
run: npx prettier --check .
|
||||
|
|
|
|||
88
.github/workflows/release.yml
vendored
88
.github/workflows/release.yml
vendored
|
|
@ -1,57 +1,57 @@
|
|||
name: Build and Release Obsidian Plugin
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*' # Triggers on any tag
|
||||
push:
|
||||
tags:
|
||||
- "*" # Triggers on any tag
|
||||
|
||||
env:
|
||||
PLUGIN_NAME: obsidian-inlineAI
|
||||
PLUGIN_NAME: obsidian-inlineAI
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
runs-on: ubuntu-latest
|
||||
build-and-release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# 1. Checkout the repository
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
steps:
|
||||
# 1. Checkout the repository
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# 2. Set up Node.js environment with caching
|
||||
- name: Set Up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 'lts/*' # Use the latest LTS version
|
||||
cache: 'npm'
|
||||
# 2. Set up Node.js environment with caching
|
||||
- name: Set Up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "lts/*" # Use the latest LTS version
|
||||
cache: "npm"
|
||||
|
||||
# 3. Install dependencies and build the project
|
||||
- name: Install and Build
|
||||
run: |
|
||||
npm ci
|
||||
npm run build --if-present
|
||||
# 3. Install dependencies and build the project
|
||||
- name: Install and Build
|
||||
run: |
|
||||
npm ci
|
||||
npm run build --if-present
|
||||
|
||||
# 4. Prepare release assets
|
||||
- name: Prepare Release Assets
|
||||
run: |
|
||||
mkdir -p $PLUGIN_NAME
|
||||
cp main.js manifest.json styles.css $PLUGIN_NAME
|
||||
zip -r ${PLUGIN_NAME}.zip $PLUGIN_NAME
|
||||
# 4. Prepare release assets
|
||||
- name: Prepare Release Assets
|
||||
run: |
|
||||
mkdir -p $PLUGIN_NAME
|
||||
cp main.js manifest.json styles.css $PLUGIN_NAME
|
||||
zip -r ${PLUGIN_NAME}.zip $PLUGIN_NAME
|
||||
|
||||
# 5. Retrieve the tag name
|
||||
- name: Get Tag Name
|
||||
id: get_tag
|
||||
run: echo "tag_name=$(git describe --tags --abbrev=0)" >> $GITHUB_OUTPUT
|
||||
# 5. Retrieve the tag name
|
||||
- name: Get Tag Name
|
||||
id: get_tag
|
||||
run: echo "tag_name=$(git describe --tags --abbrev=0)" >> $GITHUB_OUTPUT
|
||||
|
||||
# 6. Create a new release and upload all assets in one step
|
||||
- name: Create Release and Upload Assets
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
tag: ${{ steps.get_tag.outputs.tag_name }}
|
||||
name: ${{ steps.get_tag.outputs.tag_name }}
|
||||
artifacts: |
|
||||
${{ env.PLUGIN_NAME }}.zip
|
||||
main.js
|
||||
manifest.json
|
||||
styles.css
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# 6. Create a new release and upload all assets in one step
|
||||
- name: Create Release and Upload Assets
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
tag: ${{ steps.get_tag.outputs.tag_name }}
|
||||
name: ${{ steps.get_tag.outputs.tag_name }}
|
||||
artifacts: |
|
||||
${{ env.PLUGIN_NAME }}.zip
|
||||
main.js
|
||||
manifest.json
|
||||
styles.css
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
|||
|
|
@ -1,2 +1 @@
|
|||
{}
|
||||
|
||||
|
|
|
|||
11
README.md
11
README.md
|
|
@ -59,15 +59,14 @@
|
|||
### Initial Setup
|
||||
|
||||
1. **Set up your API key**:
|
||||
- Open the plugin settings in Obsidian.
|
||||
- Enter your API key for OpenAI, Ollama, Gemini or any supported model provider.
|
||||
|
||||
- Open the plugin settings in Obsidian.
|
||||
- Enter your API key for OpenAI, Ollama, Gemini or any supported model provider.
|
||||
2. **Choose a model**:
|
||||
- Supported models include `gpt-4`, `llama3.2`, and others.
|
||||
|
||||
- Supported models include `gpt-4`, `llama3.2`, and others.
|
||||
3. **Configure prompts**:
|
||||
|
||||
- Define system and transformation prompts in settings for customized interactions.
|
||||
- Define system and transformation prompts in settings for customized interactions.
|
||||
|
||||
### How to Use
|
||||
|
||||
|
|
@ -97,6 +96,7 @@ Want to contribute? Here’s how:
|
|||
git clone https://github.com/FBarrca/obsidian-inlineai.git
|
||||
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
|
|
@ -104,6 +104,7 @@ Want to contribute? Here’s how:
|
|||
npm install
|
||||
|
||||
```
|
||||
|
||||
3. Build the plugin:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -8,4 +8,4 @@
|
|||
"authorUrl": "https://github.com/FBarrca/",
|
||||
"fundingUrl": "https://www.buymeacoffee.com/FBarrCa",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
366
src/api.ts
366
src/api.ts
|
|
@ -2,7 +2,11 @@
|
|||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { ChatOllama } from "@langchain/ollama";
|
||||
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
|
||||
import { SystemMessage, HumanMessage, AIMessage } from "@langchain/core/messages";
|
||||
import {
|
||||
SystemMessage,
|
||||
HumanMessage,
|
||||
AIMessage,
|
||||
} from "@langchain/core/messages";
|
||||
import { InlineAISettings } from "./settings";
|
||||
import { App, MarkdownView, Notice } from "obsidian";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
|
|
@ -13,196 +17,228 @@ import { MessageQueue } from "./modules/messageHistory/queue";
|
|||
const MESSAGE_HISTORY_LIMIT = 20;
|
||||
|
||||
export type HistoryMessage = {
|
||||
mode: string;
|
||||
userPrompt: string;
|
||||
mode: string;
|
||||
userPrompt: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Class to manage interactions with different chat APIs.
|
||||
*/
|
||||
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.
|
||||
* @param app - The Obsidian App instance.
|
||||
*/
|
||||
constructor(settings: InlineAISettings, app: App) {
|
||||
this.app = app;
|
||||
this.chatClient = this.initializeChatClient(settings);
|
||||
this.settings = settings;
|
||||
this.messageHistory = new MessageQueue<HistoryMessage>(MESSAGE_HISTORY_LIMIT);
|
||||
}
|
||||
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.
|
||||
* @param app - The Obsidian App instance.
|
||||
*/
|
||||
constructor(settings: InlineAISettings, app: App) {
|
||||
this.app = app;
|
||||
this.chatClient = this.initializeChatClient(settings);
|
||||
this.settings = settings;
|
||||
this.messageHistory = new MessageQueue<HistoryMessage>(
|
||||
MESSAGE_HISTORY_LIMIT,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the appropriate chat client based on the provider specified in settings.
|
||||
* @param settings - Configuration settings for the chat API.
|
||||
* @returns An instance of ChatOpenAI, ChatOllama, or null if initialization fails.
|
||||
*/
|
||||
private initializeChatClient(settings: InlineAISettings): ChatOpenAI | ChatOllama | ChatGoogleGenerativeAI | null {
|
||||
try {
|
||||
if (settings.messageHistory) {
|
||||
this.messageHistory = new MessageQueue<HistoryMessage>(MESSAGE_HISTORY_LIMIT);
|
||||
} else {
|
||||
this.messageHistory = new MessageQueue<HistoryMessage>(0);
|
||||
}
|
||||
|
||||
switch (settings.provider) {
|
||||
case "openai":
|
||||
if (!settings.apiKey) {
|
||||
new Notice("⚠️ OpenAI API key is required. Please check your settings.");
|
||||
return null;
|
||||
}
|
||||
return new ChatOpenAI({
|
||||
modelName: settings.model,
|
||||
temperature: 0,
|
||||
apiKey: settings.apiKey,
|
||||
});
|
||||
/**
|
||||
* Initializes the appropriate chat client based on the provider specified in settings.
|
||||
* @param settings - Configuration settings for the chat API.
|
||||
* @returns An instance of ChatOpenAI, ChatOllama, or null if initialization fails.
|
||||
*/
|
||||
private initializeChatClient(
|
||||
settings: InlineAISettings,
|
||||
): ChatOpenAI | ChatOllama | ChatGoogleGenerativeAI | null {
|
||||
try {
|
||||
if (settings.messageHistory) {
|
||||
this.messageHistory = new MessageQueue<HistoryMessage>(
|
||||
MESSAGE_HISTORY_LIMIT,
|
||||
);
|
||||
} else {
|
||||
this.messageHistory = new MessageQueue<HistoryMessage>(0);
|
||||
}
|
||||
|
||||
case "ollama":
|
||||
return new ChatOllama({
|
||||
model: settings.model,
|
||||
});
|
||||
case "gemini":
|
||||
return new ChatGoogleGenerativeAI({
|
||||
model: settings.model,
|
||||
apiKey: settings.apiKey,
|
||||
});
|
||||
case "custom":
|
||||
if (!settings.apiKey || !settings.customURL) {
|
||||
new Notice("⚠️ API key and custom base URL are required for custom providers.");
|
||||
return null;
|
||||
}
|
||||
return new ChatOpenAI({
|
||||
modelName: settings.model,
|
||||
temperature: 0,
|
||||
openAIApiKey: settings.apiKey,
|
||||
// 'configuration.basePath' is the recognized property
|
||||
configuration: {
|
||||
baseURL: settings.customURL.trim(),
|
||||
},
|
||||
});
|
||||
switch (settings.provider) {
|
||||
case "openai":
|
||||
if (!settings.apiKey) {
|
||||
new Notice(
|
||||
"⚠️ OpenAI API key is required. Please check your settings.",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return new ChatOpenAI({
|
||||
modelName: settings.model,
|
||||
temperature: 0,
|
||||
apiKey: settings.apiKey,
|
||||
});
|
||||
|
||||
default:
|
||||
new Notice(`⚠️ Unsupported provider: ${settings.provider}`);
|
||||
return null;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error initializing chat client:", error);
|
||||
new Notice(`❌ Error initializing chat client: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
case "ollama":
|
||||
return new ChatOllama({
|
||||
model: settings.model,
|
||||
});
|
||||
case "gemini":
|
||||
return new ChatGoogleGenerativeAI({
|
||||
model: settings.model,
|
||||
apiKey: settings.apiKey,
|
||||
});
|
||||
case "custom":
|
||||
if (!settings.apiKey || !settings.customURL) {
|
||||
new Notice(
|
||||
"⚠️ API key and custom base URL are required for custom providers.",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return new ChatOpenAI({
|
||||
modelName: settings.model,
|
||||
temperature: 0,
|
||||
openAIApiKey: settings.apiKey,
|
||||
// 'configuration.basePath' is the recognized property
|
||||
configuration: {
|
||||
baseURL: settings.customURL.trim(),
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Calls the chat API with the provided content and context.
|
||||
* @param systemMessage - The system message to send to the chat API.
|
||||
* @param message - The user's message to send to the chat API.
|
||||
* @returns A promise that resolves with the generated content or an error message.
|
||||
*/
|
||||
public async callApi(systemMessage: string, message: string): Promise<string> {
|
||||
if (!this.chatClient) {
|
||||
new Notice("⚠️ Chat client is not initialized. Please check your settings.");
|
||||
return "⚠️ Chat client is not available.";
|
||||
}
|
||||
default:
|
||||
new Notice(`⚠️ Unsupported provider: ${settings.provider}`);
|
||||
return null;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error initializing chat client:", error);
|
||||
new Notice(`❌ Error initializing chat client: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const messages = [
|
||||
new SystemMessage(systemMessage),
|
||||
new HumanMessage(message),
|
||||
];
|
||||
/**
|
||||
* Calls the chat API with the provided content and context.
|
||||
* @param systemMessage - The system message to send to the chat API.
|
||||
* @param message - The user's message to send to the chat API.
|
||||
* @returns A promise that resolves with the generated content or an error message.
|
||||
*/
|
||||
public async callApi(
|
||||
systemMessage: string,
|
||||
message: string,
|
||||
): Promise<string> {
|
||||
if (!this.chatClient) {
|
||||
new Notice(
|
||||
"⚠️ Chat client is not initialized. Please check your settings.",
|
||||
);
|
||||
return "⚠️ Chat client is not available.";
|
||||
}
|
||||
|
||||
try {
|
||||
const aiMessage: AIMessage = await this.chatClient.invoke(messages);
|
||||
return aiMessage.content.toString();
|
||||
} catch (error: any) {
|
||||
console.error("Error calling the chat model:", error);
|
||||
new Notice(`❌ Error calling the chat model: ${error.message}`);
|
||||
return "⚠️ Failed to generate a response. Please try again later.";
|
||||
}
|
||||
}
|
||||
const messages = [
|
||||
new SystemMessage(systemMessage),
|
||||
new HumanMessage(message),
|
||||
];
|
||||
|
||||
/**
|
||||
* Handles user input and updates the editor with the response.
|
||||
* @param systemPrompt - The system prompt to send to the chat API.
|
||||
* @param userRequest - The user's request to process.
|
||||
* @returns The AI-generated response or an error message.
|
||||
*/
|
||||
private async handleEditorUpdate(systemPrompt: string, userRequest: string): Promise<string> {
|
||||
try {
|
||||
const response = await this.callApi(systemPrompt, userRequest);
|
||||
if (!response) return "⚠️ No response generated.";
|
||||
try {
|
||||
const aiMessage: AIMessage = await this.chatClient.invoke(messages);
|
||||
return aiMessage.content.toString();
|
||||
} catch (error: any) {
|
||||
console.error("Error calling the chat model:", error);
|
||||
new Notice(`❌ Error calling the chat model: ${error.message}`);
|
||||
return "⚠️ Failed to generate a response. Please try again later.";
|
||||
}
|
||||
}
|
||||
|
||||
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (!markdownView) {
|
||||
new Notice("⚠️ No active Markdown editor found.");
|
||||
return "";
|
||||
}
|
||||
/**
|
||||
* Handles user input and updates the editor with the response.
|
||||
* @param systemPrompt - The system prompt to send to the chat API.
|
||||
* @param userRequest - The user's request to process.
|
||||
* @returns The AI-generated response or an error message.
|
||||
*/
|
||||
private async handleEditorUpdate(
|
||||
systemPrompt: string,
|
||||
userRequest: string,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const response = await this.callApi(systemPrompt, userRequest);
|
||||
if (!response) return "⚠️ No response generated.";
|
||||
|
||||
const mainEditorView = (markdownView.editor as any).cm as EditorView;
|
||||
mainEditorView?.dispatch({
|
||||
effects: setGeneratedResponseEffect.of({ airesponse: response, prompt: userRequest }),
|
||||
});
|
||||
const markdownView =
|
||||
this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (!markdownView) {
|
||||
new Notice("⚠️ No active Markdown editor found.");
|
||||
return "";
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
console.error("Error processing request:", error);
|
||||
new Notice(`❌ Error processing request: ${error.message}`);
|
||||
return "⚠️ Failed to process request.";
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Processes selected text using the specified prompt and transformation.
|
||||
* @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(userPrompt: string, selectedText: string): Promise<string> {
|
||||
userPrompt = parseCommand(userPrompt, this.settings.commandPrefix, this.settings.customCommands);
|
||||
const mainEditorView = (markdownView.editor as any)
|
||||
.cm as EditorView;
|
||||
mainEditorView?.dispatch({
|
||||
effects: setGeneratedResponseEffect.of({
|
||||
airesponse: response,
|
||||
prompt: userRequest,
|
||||
}),
|
||||
});
|
||||
|
||||
let isCursor = false;
|
||||
if (selectedText.trim().length === 0) {
|
||||
isCursor = true;
|
||||
}
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
console.error("Error processing request:", error);
|
||||
new Notice(`❌ Error processing request: ${error.message}`);
|
||||
return "⚠️ Failed to process request.";
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Processes selected text using the specified prompt and transformation.
|
||||
* @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(
|
||||
userPrompt: string,
|
||||
selectedText: string,
|
||||
): Promise<string> {
|
||||
userPrompt = parseCommand(
|
||||
userPrompt,
|
||||
this.settings.commandPrefix,
|
||||
this.settings.customCommands,
|
||||
);
|
||||
|
||||
const systemPrompt = isCursor ? this.settings.cursorPrompt : this.settings.selectionPrompt;
|
||||
let finalUserPrompt = ``;
|
||||
const mode = isCursor ? "cursor" : "selection";
|
||||
if (this.settings.messageHistory) {
|
||||
this.messageHistory.enqueue({ mode, userPrompt });
|
||||
}
|
||||
let isCursor = false;
|
||||
if (selectedText.trim().length === 0) {
|
||||
isCursor = true;
|
||||
}
|
||||
|
||||
if (isCursor) {
|
||||
finalUserPrompt = `
|
||||
const systemPrompt = isCursor
|
||||
? this.settings.cursorPrompt
|
||||
: this.settings.selectionPrompt;
|
||||
let finalUserPrompt = ``;
|
||||
const mode = isCursor ? "cursor" : "selection";
|
||||
if (this.settings.messageHistory) {
|
||||
this.messageHistory.enqueue({ mode, userPrompt });
|
||||
}
|
||||
|
||||
if (isCursor) {
|
||||
finalUserPrompt = `
|
||||
**Task:** ${userPrompt}
|
||||
**Output:**`;
|
||||
} else {
|
||||
finalUserPrompt = `
|
||||
} else {
|
||||
finalUserPrompt = `
|
||||
**Task:** ${userPrompt}
|
||||
**Input:**
|
||||
${selectedText}
|
||||
|
||||
**Output:**`;
|
||||
}
|
||||
return this.handleEditorUpdate(systemPrompt, finalUserPrompt);
|
||||
}
|
||||
}
|
||||
return this.handleEditorUpdate(systemPrompt, finalUserPrompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the manager's settings and reinitializes the chat client.
|
||||
* @param settings - New configuration settings for the chat API.
|
||||
*/
|
||||
public updateSettings(settings: InlineAISettings): void {
|
||||
this.settings = settings;
|
||||
const newChatClient = this.initializeChatClient(settings);
|
||||
if (!newChatClient) {
|
||||
return;
|
||||
}
|
||||
this.chatClient = newChatClient;
|
||||
}
|
||||
/**
|
||||
* Updates the manager's settings and reinitializes the chat client.
|
||||
* @param settings - New configuration settings for the chat API.
|
||||
*/
|
||||
public updateSettings(settings: InlineAISettings): void {
|
||||
this.settings = settings;
|
||||
const newChatClient = this.initializeChatClient(settings);
|
||||
if (!newChatClient) {
|
||||
return;
|
||||
}
|
||||
this.chatClient = newChatClient;
|
||||
}
|
||||
|
||||
public getMessageHistory(): HistoryMessage[] {
|
||||
return this.messageHistory.getItems();
|
||||
}
|
||||
public getMessageHistory(): HistoryMessage[] {
|
||||
return this.messageHistory.getItems();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ It is **very important** that you follow the examples. Do not add anything at th
|
|||
| John | 30 | Engineer|
|
||||
|
||||
---
|
||||
`
|
||||
`;
|
||||
|
||||
export const cursorPrompt = `
|
||||
You are an advanced language model specialized in following specific instructions to create and process markdown documents. Always use **Obsidian-flavored Markdown** syntax in your responses whenever applicable.
|
||||
|
|
|
|||
79
src/main.ts
79
src/main.ts
|
|
@ -1,11 +1,24 @@
|
|||
// main.ts
|
||||
import { Plugin, MarkdownView, App } from "obsidian";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { InlineAISettings, DEFAULT_SETTINGS, InlineAISettingsTab } from "./settings";
|
||||
import { acceptTooltipEffect, commandEffect, dismissTooltipEffect, FloatingTooltipExtension } from "./modules/WidgetExtension";
|
||||
import {
|
||||
InlineAISettings,
|
||||
DEFAULT_SETTINGS,
|
||||
InlineAISettingsTab,
|
||||
} from "./settings";
|
||||
import {
|
||||
acceptTooltipEffect,
|
||||
commandEffect,
|
||||
dismissTooltipEffect,
|
||||
FloatingTooltipExtension,
|
||||
} from "./modules/WidgetExtension";
|
||||
import { ChatApiManager } from "./api";
|
||||
import { generatedResponseState } from "./modules/AIExtension";
|
||||
import { buildSelectionHiglightState, currentSelectionState, setSelectionInfoEffect } from "./modules/SelectionState";
|
||||
import {
|
||||
buildSelectionHiglightState,
|
||||
currentSelectionState,
|
||||
setSelectionInfoEffect,
|
||||
} from "./modules/SelectionState";
|
||||
import { diffExtension } from "./modules/diffExtension";
|
||||
|
||||
export default class InlineAIChatPlugin extends Plugin {
|
||||
|
|
@ -21,7 +34,7 @@ export default class InlineAIChatPlugin extends Plugin {
|
|||
generatedResponseState,
|
||||
currentSelectionState,
|
||||
buildSelectionHiglightState,
|
||||
diffExtension
|
||||
diffExtension,
|
||||
]);
|
||||
|
||||
// Add command to show tooltip
|
||||
|
|
@ -29,9 +42,11 @@ export default class InlineAIChatPlugin extends Plugin {
|
|||
id: "show-cursor-tooltip",
|
||||
name: "Show cursor tooltip",
|
||||
callback: () => {
|
||||
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
const markdownView =
|
||||
this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (markdownView) {
|
||||
const cmEditor = (markdownView.editor as any).cm as EditorView;
|
||||
const cmEditor = (markdownView.editor as any)
|
||||
.cm as EditorView;
|
||||
|
||||
// Grab the main selection range
|
||||
const { from, to } = cmEditor.state.selection.main;
|
||||
|
|
@ -39,13 +54,22 @@ export default class InlineAIChatPlugin extends Plugin {
|
|||
|
||||
if (from !== to) {
|
||||
// If there is a real selection, store it
|
||||
const selectedText = cmEditor.state.doc.sliceString(from, to);
|
||||
const selectedText = cmEditor.state.doc.sliceString(
|
||||
from,
|
||||
to,
|
||||
);
|
||||
effects.push(
|
||||
setSelectionInfoEffect.of({ from, to, text: selectedText })
|
||||
setSelectionInfoEffect.of({
|
||||
from,
|
||||
to,
|
||||
text: selectedText,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
// If no selection, store cursor position instead of null
|
||||
effects.push(setSelectionInfoEffect.of({ from, to, text: "" }));
|
||||
effects.push(
|
||||
setSelectionInfoEffect.of({ from, to, text: "" }),
|
||||
);
|
||||
}
|
||||
|
||||
// Also trigger the overlay
|
||||
|
|
@ -55,18 +79,22 @@ export default class InlineAIChatPlugin extends Plugin {
|
|||
cmEditor.dispatch({ effects });
|
||||
}
|
||||
},
|
||||
hotkeys: [
|
||||
],
|
||||
hotkeys: [],
|
||||
});
|
||||
this.addCommand({
|
||||
id: "accept-tooltip",
|
||||
name: "Accept tooltip suggestion",
|
||||
callback: () => {
|
||||
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
const markdownView =
|
||||
this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (markdownView) {
|
||||
const cmEditor = (markdownView.editor as any).cm as EditorView;
|
||||
const cmEditor = (markdownView.editor as any)
|
||||
.cm as EditorView;
|
||||
|
||||
const response = cmEditor.state.field(generatedResponseState, false);
|
||||
const response = cmEditor.state.field(
|
||||
generatedResponseState,
|
||||
false,
|
||||
);
|
||||
if (response) {
|
||||
cmEditor.dispatch({
|
||||
effects: acceptTooltipEffect.of(null),
|
||||
|
|
@ -76,25 +104,28 @@ export default class InlineAIChatPlugin extends Plugin {
|
|||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
this.addCommand({
|
||||
id: "discard-tooltip",
|
||||
name: "Discard tooltip suggestion",
|
||||
callback: () => {
|
||||
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
const markdownView =
|
||||
this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (markdownView) {
|
||||
const cmEditor = (markdownView.editor as any).cm as EditorView;
|
||||
const response = cmEditor.state.field(generatedResponseState, false);
|
||||
const cmEditor = (markdownView.editor as any)
|
||||
.cm as EditorView;
|
||||
const response = cmEditor.state.field(
|
||||
generatedResponseState,
|
||||
false,
|
||||
);
|
||||
if (response) {
|
||||
cmEditor.dispatch({
|
||||
effects: dismissTooltipEffect.of(null),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
// Add settings tab
|
||||
|
|
@ -106,7 +137,11 @@ export default class InlineAIChatPlugin extends Plugin {
|
|||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
this.settings = Object.assign(
|
||||
{},
|
||||
DEFAULT_SETTINGS,
|
||||
await this.loadData(),
|
||||
);
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
|
|
|
|||
|
|
@ -1,38 +1,36 @@
|
|||
import {
|
||||
StateEffect,
|
||||
StateField,
|
||||
} from "@codemirror/state";
|
||||
import { StateEffect, StateField } from "@codemirror/state";
|
||||
|
||||
import { dismissTooltipEffect } from "./WidgetExtension";
|
||||
|
||||
// Custom structure that has a airesponse and context fields
|
||||
export interface AIResponse {
|
||||
airesponse: string;
|
||||
prompt: string;
|
||||
airesponse: string;
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* State Effect to set the AI response.
|
||||
*/
|
||||
export const setGeneratedResponseEffect = StateEffect.define<AIResponse | null>();
|
||||
export const setGeneratedResponseEffect =
|
||||
StateEffect.define<AIResponse | null>();
|
||||
|
||||
// State field of type text to store the response from the API
|
||||
export const generatedResponseState = StateField.define<AIResponse | null>({
|
||||
create() {
|
||||
return null;
|
||||
},
|
||||
update(value, tr) {
|
||||
// Check if the transaction contains an effect to set the response
|
||||
if (tr.effects.some((e) => e.is(setGeneratedResponseEffect))) {
|
||||
const effect = tr.effects.find((e) => e.is(setGeneratedResponseEffect));
|
||||
return effect ? effect.value : value;
|
||||
}
|
||||
// if we geta dismmisTooltipEffect we should clear the response
|
||||
if (tr.effects.some((e) => e.is(dismissTooltipEffect))) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
create() {
|
||||
return null;
|
||||
},
|
||||
update(value, tr) {
|
||||
// Check if the transaction contains an effect to set the response
|
||||
if (tr.effects.some((e) => e.is(setGeneratedResponseEffect))) {
|
||||
const effect = tr.effects.find((e) =>
|
||||
e.is(setGeneratedResponseEffect),
|
||||
);
|
||||
return effect ? effect.value : value;
|
||||
}
|
||||
// if we geta dismmisTooltipEffect we should clear the response
|
||||
if (tr.effects.some((e) => e.is(dismissTooltipEffect))) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,62 +8,67 @@ import { dismissTooltipEffect } from "./WidgetExtension";
|
|||
* Interface describing the selection range and text.
|
||||
*/
|
||||
export interface SelectionInfo {
|
||||
from: number;
|
||||
to: number;
|
||||
text: string;
|
||||
from: number;
|
||||
to: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Effect used to set or clear the selection info.
|
||||
*/
|
||||
export const setSelectionInfoEffect = StateEffect.define<SelectionInfo | null>();
|
||||
export const setSelectionInfoEffect =
|
||||
StateEffect.define<SelectionInfo | null>();
|
||||
|
||||
/**
|
||||
* Field that holds the most recently preserved selection info.
|
||||
*/
|
||||
export const currentSelectionState = StateField.define<SelectionInfo | null>({
|
||||
create() {
|
||||
return null;
|
||||
},
|
||||
update(value, tr) {
|
||||
// Look for a setSelectionInfoEffect in this transaction
|
||||
const effect = tr.effects.find(e => e.is(setSelectionInfoEffect));
|
||||
if (effect) {
|
||||
return effect.value;
|
||||
} else if (tr.effects.some(e => e.is(dismissTooltipEffect))) {
|
||||
return null;
|
||||
}
|
||||
create() {
|
||||
return null;
|
||||
},
|
||||
update(value, tr) {
|
||||
// Look for a setSelectionInfoEffect in this transaction
|
||||
const effect = tr.effects.find((e) => e.is(setSelectionInfoEffect));
|
||||
if (effect) {
|
||||
return effect.value;
|
||||
} else if (tr.effects.some((e) => e.is(dismissTooltipEffect))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
return value;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Decoration to highlight the selected text.
|
||||
*/
|
||||
const highlightDecoration = Decoration.mark({
|
||||
class: "cm-selectionBackground", // CSS class for highlighting
|
||||
class: "cm-selectionBackground", // CSS class for highlighting
|
||||
});
|
||||
|
||||
/**
|
||||
* StateField that manages the decoration set for highlighting.
|
||||
*/
|
||||
export const buildSelectionHiglightState = StateField.define<DecorationSet>({
|
||||
create(state) {
|
||||
const info = state.field(currentSelectionState);
|
||||
if (info && info.from !== info.to) {
|
||||
console.log("info", info);
|
||||
return Decoration.set([highlightDecoration.range(info.from, info.to)]);
|
||||
}
|
||||
return Decoration.none;
|
||||
},
|
||||
update(decos, tr) {
|
||||
// Check if selectionInfoField has changed
|
||||
const info = tr.state.field(currentSelectionState);
|
||||
if (info && info.from !== info.to) {
|
||||
return Decoration.set([highlightDecoration.range(info.from, info.to)]);
|
||||
}
|
||||
return Decoration.none;
|
||||
},
|
||||
provide: f => EditorView.decorations.from(f),
|
||||
create(state) {
|
||||
const info = state.field(currentSelectionState);
|
||||
if (info && info.from !== info.to) {
|
||||
console.log("info", info);
|
||||
return Decoration.set([
|
||||
highlightDecoration.range(info.from, info.to),
|
||||
]);
|
||||
}
|
||||
return Decoration.none;
|
||||
},
|
||||
update(decos, tr) {
|
||||
// Check if selectionInfoField has changed
|
||||
const info = tr.state.field(currentSelectionState);
|
||||
if (info && info.from !== info.to) {
|
||||
return Decoration.set([
|
||||
highlightDecoration.range(info.from, info.to),
|
||||
]);
|
||||
}
|
||||
return Decoration.none;
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,460 +1,501 @@
|
|||
// modules/WidgetExtension.ts
|
||||
import {
|
||||
EditorState,
|
||||
StateEffect,
|
||||
StateField,
|
||||
} from "@codemirror/state";
|
||||
import { EditorState, StateEffect, StateField } from "@codemirror/state";
|
||||
|
||||
import {
|
||||
EditorView,
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
WidgetType,
|
||||
placeholder,
|
||||
keymap,
|
||||
EditorView,
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
WidgetType,
|
||||
placeholder,
|
||||
keymap,
|
||||
} from "@codemirror/view";
|
||||
|
||||
import { setIcon } from "obsidian";
|
||||
import { ChatApiManager } from "../api";
|
||||
|
||||
import { currentSelectionState, SelectionInfo } from "./SelectionState";
|
||||
import { createSlashCommandHighlighter, slashCommandAutocompletion } from "./commands/source";
|
||||
import {
|
||||
createSlashCommandHighlighter,
|
||||
slashCommandAutocompletion,
|
||||
} from "./commands/source";
|
||||
import InlineAIChatPlugin from "src/main";
|
||||
|
||||
|
||||
|
||||
// Some existing exports
|
||||
export const commandEffect = StateEffect.define<null>();
|
||||
export const dismissTooltipEffect = StateEffect.define<null>();
|
||||
export const acceptTooltipEffect = StateEffect.define<null>();
|
||||
|
||||
class FloatingWidget extends WidgetType {
|
||||
private chatApiManager: ChatApiManager;
|
||||
private selectionInfo: SelectionInfo | null;
|
||||
private plugin: InlineAIChatPlugin;
|
||||
private chatApiManager: ChatApiManager;
|
||||
private selectionInfo: SelectionInfo | null;
|
||||
private plugin: InlineAIChatPlugin;
|
||||
|
||||
private outerEditorView: EditorView | null = null;
|
||||
private outerEditorView: EditorView | null = null;
|
||||
|
||||
private dom: HTMLElement;
|
||||
private innerDom: HTMLElement;
|
||||
private dom: HTMLElement;
|
||||
private innerDom: HTMLElement;
|
||||
|
||||
private textFieldView?: EditorView;
|
||||
// Primary Action Buttons
|
||||
private submitButton!: HTMLButtonElement;
|
||||
private loaderElement!: HTMLElement;
|
||||
private textFieldView?: EditorView;
|
||||
// Primary Action Buttons
|
||||
private submitButton!: HTMLButtonElement;
|
||||
private loaderElement!: HTMLElement;
|
||||
|
||||
//Secondary Action Buttons
|
||||
private acceptButton!: HTMLButtonElement;
|
||||
private discardButton!: HTMLButtonElement;
|
||||
//Secondary Action Buttons
|
||||
private acceptButton!: HTMLButtonElement;
|
||||
private discardButton!: HTMLButtonElement;
|
||||
|
||||
// Track the current index in message history for up/down navigation
|
||||
private messageHistoryIndex: number | null = null;
|
||||
// Track the current index in message history for up/down navigation
|
||||
private messageHistoryIndex: number | null = null;
|
||||
|
||||
// Store references to event listeners for cleanup
|
||||
private onClickOutside: ((event: MouseEvent) => void) | null = null;
|
||||
private onEscape: ((event: KeyboardEvent) => void) | null = null;
|
||||
private escapeWindows: Window[] = [];
|
||||
// Store references to event listeners for cleanup
|
||||
private onClickOutside: ((event: MouseEvent) => void) | null = null;
|
||||
private onEscape: ((event: KeyboardEvent) => void) | null = null;
|
||||
private escapeWindows: Window[] = [];
|
||||
|
||||
constructor(chatApiManager: ChatApiManager, selectionInfo: SelectionInfo | null, plugin: InlineAIChatPlugin) {
|
||||
super();
|
||||
this.chatApiManager = chatApiManager;
|
||||
this.selectionInfo = selectionInfo;
|
||||
this.plugin = plugin;
|
||||
constructor(
|
||||
chatApiManager: ChatApiManager,
|
||||
selectionInfo: SelectionInfo | null,
|
||||
plugin: InlineAIChatPlugin,
|
||||
) {
|
||||
super();
|
||||
this.chatApiManager = chatApiManager;
|
||||
this.selectionInfo = selectionInfo;
|
||||
this.plugin = plugin;
|
||||
|
||||
// Create main DOM structure using createEl
|
||||
this.dom = createEl("div", { cls: "cm-cursor-overlay", attr: { style: "user-select: none;" } });
|
||||
this.innerDom = this.dom.createEl("div", { cls: "cm-cursor-overlay-inner" });
|
||||
}
|
||||
// Create main DOM structure using createEl
|
||||
this.dom = createEl("div", {
|
||||
cls: "cm-cursor-overlay",
|
||||
attr: { style: "user-select: none;" },
|
||||
});
|
||||
this.innerDom = this.dom.createEl("div", {
|
||||
cls: "cm-cursor-overlay-inner",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Overriding toDOM(view: EditorView) instead of just toDOM().
|
||||
*/
|
||||
public override toDOM(view: EditorView): HTMLElement {
|
||||
// Capture the outer EditorView
|
||||
this.outerEditorView = view;
|
||||
/**
|
||||
* Overriding toDOM(view: EditorView) instead of just toDOM().
|
||||
*/
|
||||
public override toDOM(view: EditorView): HTMLElement {
|
||||
// Capture the outer EditorView
|
||||
this.outerEditorView = view;
|
||||
|
||||
this.createPencilIcon();
|
||||
this.createInputField();
|
||||
this.createSubmitButton();
|
||||
this.createLoader();
|
||||
this.createPencilIcon();
|
||||
this.createInputField();
|
||||
this.createSubmitButton();
|
||||
this.createLoader();
|
||||
|
||||
setTimeout(() => {
|
||||
this.textFieldView?.focus();
|
||||
}, 0);
|
||||
setTimeout(() => {
|
||||
this.textFieldView?.focus();
|
||||
}, 0);
|
||||
|
||||
// Setup "click outside" and "Escape" dismissal
|
||||
this.onClickOutside = (event: MouseEvent) => {
|
||||
if (!this.dom.contains(event.target as Node)) {
|
||||
this.dismissTooltip();
|
||||
}
|
||||
};
|
||||
// Setup "click outside" and "Escape" dismissal
|
||||
this.onClickOutside = (event: MouseEvent) => {
|
||||
if (!this.dom.contains(event.target as Node)) {
|
||||
this.dismissTooltip();
|
||||
}
|
||||
};
|
||||
|
||||
// Escape key: listen on all windows
|
||||
this.onEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
this.dismissTooltip();
|
||||
}
|
||||
};
|
||||
// Escape key: listen on all windows
|
||||
this.onEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
this.dismissTooltip();
|
||||
}
|
||||
};
|
||||
|
||||
// Always add to main window
|
||||
window.addEventListener("mousedown", this.onClickOutside);
|
||||
window.addEventListener("keydown", this.onEscape);
|
||||
// Always add to main window
|
||||
window.addEventListener("mousedown", this.onClickOutside);
|
||||
window.addEventListener("keydown", this.onEscape);
|
||||
|
||||
// Try to add to all popout windows (if any)
|
||||
this.escapeWindows = [window];
|
||||
// @ts-ignore
|
||||
if (window.app && window.app.openPopouts instanceof Function) {
|
||||
// Obsidian 1.4+ openPopouts returns array of popout windows
|
||||
// @ts-ignore
|
||||
const popouts: Window[] = window.app.openPopouts();
|
||||
for (const popout of popouts) {
|
||||
try {
|
||||
popout.addEventListener("keydown", this.onEscape!);
|
||||
this.escapeWindows.push(popout);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
} else if ((window as any).app && Array.isArray((window as any).app?.workspace?.getWindows?.())) {
|
||||
// Legacy: app.workspace.getWindows() returns array of windows
|
||||
const popouts: Window[] = (window as any).app.workspace.getWindows();
|
||||
for (const popout of popouts) {
|
||||
try {
|
||||
popout.addEventListener("keydown", this.onEscape!);
|
||||
this.escapeWindows.push(popout);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
// Try to add to all popout windows (if any)
|
||||
this.escapeWindows = [window];
|
||||
// @ts-ignore
|
||||
if (window.app && window.app.openPopouts instanceof Function) {
|
||||
// Obsidian 1.4+ openPopouts returns array of popout windows
|
||||
// @ts-ignore
|
||||
const popouts: Window[] = window.app.openPopouts();
|
||||
for (const popout of popouts) {
|
||||
try {
|
||||
popout.addEventListener("keydown", this.onEscape!);
|
||||
this.escapeWindows.push(popout);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
(window as any).app &&
|
||||
Array.isArray((window as any).app?.workspace?.getWindows?.())
|
||||
) {
|
||||
// Legacy: app.workspace.getWindows() returns array of windows
|
||||
const popouts: Window[] = (
|
||||
window as any
|
||||
).app.workspace.getWindows();
|
||||
for (const popout of popouts) {
|
||||
try {
|
||||
popout.addEventListener("keydown", this.onEscape!);
|
||||
this.escapeWindows.push(popout);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
this.dom.addEventListener("destroy", () => {
|
||||
document.removeEventListener("mousedown", this.onClickOutside!);
|
||||
for (const win of this.escapeWindows) {
|
||||
try {
|
||||
win.removeEventListener("keydown", this.onEscape!);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
this.escapeWindows = [];
|
||||
});
|
||||
// Cleanup
|
||||
this.dom.addEventListener("destroy", () => {
|
||||
document.removeEventListener("mousedown", this.onClickOutside!);
|
||||
for (const win of this.escapeWindows) {
|
||||
try {
|
||||
win.removeEventListener("keydown", this.onEscape!);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
this.escapeWindows = [];
|
||||
});
|
||||
|
||||
return this.dom;
|
||||
}
|
||||
return this.dom;
|
||||
}
|
||||
|
||||
public override destroy(): void {
|
||||
this.textFieldView?.destroy();
|
||||
this.innerDom.empty();
|
||||
public override destroy(): void {
|
||||
this.textFieldView?.destroy();
|
||||
this.innerDom.empty();
|
||||
|
||||
this.submitButton.remove();
|
||||
this.loaderElement.remove();
|
||||
this.submitButton.remove();
|
||||
this.loaderElement.remove();
|
||||
|
||||
if (this.acceptButton) this.acceptButton.remove();
|
||||
if (this.discardButton) this.discardButton.remove();
|
||||
if (this.acceptButton) this.acceptButton.remove();
|
||||
if (this.discardButton) this.discardButton.remove();
|
||||
|
||||
this.textFieldView = undefined;
|
||||
this.outerEditorView = null;
|
||||
this.messageHistoryIndex = null;
|
||||
}
|
||||
this.textFieldView = undefined;
|
||||
this.outerEditorView = null;
|
||||
this.messageHistoryIndex = null;
|
||||
}
|
||||
|
||||
private dismissTooltip() {
|
||||
if (this.outerEditorView) {
|
||||
this.outerEditorView.dispatch({
|
||||
effects: dismissTooltipEffect.of(null),
|
||||
});
|
||||
}
|
||||
}
|
||||
private dismissTooltip() {
|
||||
if (this.outerEditorView) {
|
||||
this.outerEditorView.dispatch({
|
||||
effects: dismissTooltipEffect.of(null),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private createPencilIcon() {
|
||||
if (!this.innerDom.querySelector(".cm-pencil-icon")) {
|
||||
const icon = this.innerDom.createEl("div", {
|
||||
cls: "cm-pencil-icon",
|
||||
});
|
||||
setIcon(icon, "pencil");
|
||||
}
|
||||
}
|
||||
|
||||
private createPencilIcon() {
|
||||
if (!this.innerDom.querySelector(".cm-pencil-icon")) {
|
||||
const icon = this.innerDom.createEl("div", { cls: "cm-pencil-icon" });
|
||||
setIcon(icon, "pencil");
|
||||
}
|
||||
}
|
||||
private createInputField() {
|
||||
const editorDom = this.innerDom.createEl("div", {
|
||||
cls: "cm-tooltip-editor",
|
||||
attr: { style: "user-select: text;" },
|
||||
});
|
||||
|
||||
private createInputField() {
|
||||
const editorDom = this.innerDom.createEl("div", { cls: "cm-tooltip-editor", attr: { style: "user-select: text;" } });
|
||||
this.textFieldView = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc: "",
|
||||
extensions: [
|
||||
// 1) Show a placeholder in the input field
|
||||
placeholder("Ask copilot"),
|
||||
// 2) Add key bindings (including default ones for typical editor commands)
|
||||
keymap.of([
|
||||
{
|
||||
key: "Enter",
|
||||
run: () => {
|
||||
this.submitAction();
|
||||
return true;
|
||||
},
|
||||
preventDefault: true,
|
||||
},
|
||||
{
|
||||
key: "Mod-a",
|
||||
run: (view) => {
|
||||
const doc = view.state.doc;
|
||||
view.dispatch({
|
||||
selection: { anchor: 0, head: doc.length },
|
||||
});
|
||||
return true;
|
||||
},
|
||||
preventDefault: true,
|
||||
},
|
||||
{
|
||||
key: "ArrowUp",
|
||||
run: () => {
|
||||
const messageHistory =
|
||||
this.chatApiManager.getMessageHistory();
|
||||
if (
|
||||
messageHistory.length === 0 ||
|
||||
!this.textFieldView
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.textFieldView = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc: "",
|
||||
extensions: [
|
||||
// 1) Show a placeholder in the input field
|
||||
placeholder("Ask copilot"),
|
||||
// 2) Add key bindings (including default ones for typical editor commands)
|
||||
keymap.of([
|
||||
{
|
||||
key: "Enter",
|
||||
run: () => {
|
||||
this.submitAction()
|
||||
return true
|
||||
},
|
||||
preventDefault: true,
|
||||
},
|
||||
{
|
||||
key: "Mod-a",
|
||||
run: (view) => {
|
||||
const doc = view.state.doc;
|
||||
view.dispatch({
|
||||
selection: { anchor: 0, head: doc.length },
|
||||
});
|
||||
return true;
|
||||
},
|
||||
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;
|
||||
|
||||
// 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,
|
||||
},
|
||||
]),
|
||||
|
||||
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,
|
||||
customCommands: this.plugin.settings.customCommands,
|
||||
}),
|
||||
createSlashCommandHighlighter({
|
||||
prefix: this.plugin.settings.commandPrefix,
|
||||
customCommands: this.plugin.settings.customCommands,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
parent: editorDom,
|
||||
});
|
||||
|
||||
// 3) Enable slash-command autocompletion
|
||||
slashCommandAutocompletion({
|
||||
prefix: this.plugin.settings.commandPrefix,
|
||||
customCommands: this.plugin.settings.customCommands
|
||||
}),
|
||||
createSlashCommandHighlighter({
|
||||
prefix: this.plugin.settings.commandPrefix,
|
||||
customCommands: this.plugin.settings.customCommands
|
||||
})
|
||||
],
|
||||
}),
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
// 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() {
|
||||
this.submitButton = this.innerDom.createEl("button", {
|
||||
cls: "submit-button tooltip-button",
|
||||
text: "Submit",
|
||||
});
|
||||
setIcon(this.submitButton, "send-horizontal");
|
||||
|
||||
private createSubmitButton() {
|
||||
this.submitButton = this.innerDom.createEl("button", {
|
||||
cls: "submit-button tooltip-button",
|
||||
text: "Submit",
|
||||
});
|
||||
setIcon(this.submitButton, "send-horizontal");
|
||||
this.submitButton.onclick = () => {
|
||||
this.submitAction();
|
||||
};
|
||||
}
|
||||
|
||||
this.submitButton.onclick = () => {
|
||||
this.submitAction();
|
||||
};
|
||||
}
|
||||
private createLoader() {
|
||||
this.loaderElement = this.innerDom.createEl("div", { cls: "loader" });
|
||||
this.toggleLoading(false);
|
||||
}
|
||||
|
||||
private createLoader() {
|
||||
this.loaderElement = this.innerDom.createEl("div", { cls: "loader" });
|
||||
this.toggleLoading(false);
|
||||
}
|
||||
/**
|
||||
* Handles the submit action by calling the AI with the user input and the selected text.
|
||||
*/
|
||||
private submitAction() {
|
||||
const userPrompt = this.textFieldView?.state.doc.toString() ?? "";
|
||||
|
||||
/**
|
||||
* Handles the submit action by calling the AI with the user input and the selected text.
|
||||
*/
|
||||
private submitAction() {
|
||||
const userPrompt = this.textFieldView?.state.doc.toString() ?? "";
|
||||
if (!userPrompt.trim()) {
|
||||
console.warn("Empty input. Submission aborted.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userPrompt.trim()) {
|
||||
console.warn("Empty input. Submission aborted.");
|
||||
return;
|
||||
}
|
||||
// Grab the selected text from the stored selection info
|
||||
const selectedText = this.selectionInfo?.text ?? "";
|
||||
|
||||
// Grab the selected text from the stored selection info
|
||||
const selectedText = this.selectionInfo?.text ?? "";
|
||||
// Show loader
|
||||
this.toggleLoading(true);
|
||||
|
||||
// Show loader
|
||||
this.toggleLoading(true);
|
||||
this.chatApiManager
|
||||
.callSelection(userPrompt, selectedText)
|
||||
.then((aiResponse) => {
|
||||
this.showActionButtons();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error calling AI:", error);
|
||||
})
|
||||
.finally(() => {
|
||||
// Hide loader
|
||||
this.toggleLoading(false);
|
||||
});
|
||||
|
||||
this.chatApiManager
|
||||
.callSelection(userPrompt, selectedText)
|
||||
.then((aiResponse) => {
|
||||
this.showActionButtons();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error calling AI:", error);
|
||||
})
|
||||
.finally(() => {
|
||||
// Hide loader
|
||||
this.toggleLoading(false);
|
||||
});
|
||||
// Reset message history navigation after submit
|
||||
this.messageHistoryIndex = null;
|
||||
}
|
||||
|
||||
// Reset message history navigation after submit
|
||||
this.messageHistoryIndex = null;
|
||||
}
|
||||
/**
|
||||
* Toggles the visibility of the submit button and loader.
|
||||
* @param isLoading - Whether to show the loader.
|
||||
*/
|
||||
private toggleLoading(isLoading: boolean) {
|
||||
if (isLoading) {
|
||||
this.submitButton.classList.add("hidden");
|
||||
this.loaderElement.classList.remove("hidden");
|
||||
} else {
|
||||
this.submitButton.classList.remove("hidden");
|
||||
this.loaderElement.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the visibility of the submit button and loader.
|
||||
* @param isLoading - Whether to show the loader.
|
||||
*/
|
||||
private toggleLoading(isLoading: boolean) {
|
||||
if (isLoading) {
|
||||
this.submitButton.classList.add("hidden");
|
||||
this.loaderElement.classList.remove("hidden");
|
||||
} else {
|
||||
this.submitButton.classList.remove("hidden");
|
||||
this.loaderElement.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Transitions the widget to show Accept, Discard, and Reload buttons.
|
||||
*/
|
||||
private showActionButtons() {
|
||||
this.submitButton.classList.add("hidden");
|
||||
this.createAcceptButton();
|
||||
this.createDiscardButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the Accept button.
|
||||
*/
|
||||
private createAcceptButton() {
|
||||
if (!this.acceptButton) {
|
||||
this.acceptButton = this.innerDom.createEl("button", {
|
||||
cls: "accept-button tooltip-button primary-action",
|
||||
text: "Accept",
|
||||
});
|
||||
setIcon(this.acceptButton, "check");
|
||||
|
||||
/**
|
||||
* Transitions the widget to show Accept, Discard, and Reload buttons.
|
||||
*/
|
||||
private showActionButtons() {
|
||||
this.submitButton.classList.add("hidden");
|
||||
this.createAcceptButton();
|
||||
this.createDiscardButton();
|
||||
}
|
||||
this.acceptButton.onclick = () => {
|
||||
this.acceptAction();
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the Accept button.
|
||||
*/
|
||||
private createAcceptButton() {
|
||||
if (!this.acceptButton) {
|
||||
this.acceptButton = this.innerDom.createEl("button", {
|
||||
cls: "accept-button tooltip-button primary-action",
|
||||
text: "Accept",
|
||||
});
|
||||
setIcon(this.acceptButton, "check");
|
||||
this.innerDom.appendChild(this.acceptButton);
|
||||
}
|
||||
}
|
||||
|
||||
this.acceptButton.onclick = () => {
|
||||
this.acceptAction();
|
||||
};
|
||||
/**
|
||||
* Creates the Discard button.
|
||||
*/
|
||||
private createDiscardButton() {
|
||||
if (!this.discardButton) {
|
||||
this.discardButton = this.innerDom.createEl("button", {
|
||||
cls: "discard-button tooltip-button",
|
||||
text: "Discard",
|
||||
});
|
||||
setIcon(this.discardButton, "cross");
|
||||
|
||||
this.innerDom.appendChild(this.acceptButton);
|
||||
}
|
||||
}
|
||||
this.discardButton.onclick = () => {
|
||||
this.discardAction();
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the Discard button.
|
||||
*/
|
||||
private createDiscardButton() {
|
||||
if (!this.discardButton) {
|
||||
this.discardButton = this.innerDom.createEl("button", {
|
||||
cls: "discard-button tooltip-button",
|
||||
text: "Discard",
|
||||
});
|
||||
setIcon(this.discardButton, "cross");
|
||||
this.innerDom.appendChild(this.discardButton);
|
||||
}
|
||||
}
|
||||
|
||||
this.discardButton.onclick = () => {
|
||||
this.discardAction();
|
||||
};
|
||||
/**
|
||||
* Handles the Accept action.
|
||||
* Confirms the result, applies changes, and closes the tooltip.
|
||||
*/
|
||||
private acceptAction() {
|
||||
if (this.outerEditorView) {
|
||||
this.outerEditorView.dispatch({
|
||||
effects: acceptTooltipEffect.of(null),
|
||||
});
|
||||
}
|
||||
this.dismissTooltip();
|
||||
}
|
||||
|
||||
this.innerDom.appendChild(this.discardButton);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles the Accept action.
|
||||
* Confirms the result, applies changes, and closes the tooltip.
|
||||
*/
|
||||
private acceptAction() {
|
||||
if (this.outerEditorView) {
|
||||
this.outerEditorView.dispatch({
|
||||
effects: acceptTooltipEffect.of(null),
|
||||
});
|
||||
}
|
||||
this.dismissTooltip();
|
||||
}
|
||||
|
||||
private discardAction() {
|
||||
this.dismissTooltip();
|
||||
}
|
||||
private discardAction() {
|
||||
this.dismissTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build decorations for the first non-empty selection range.
|
||||
*/
|
||||
function renderFloatingWidget(
|
||||
state: EditorState,
|
||||
chatApiManager: ChatApiManager,
|
||||
plugin: InlineAIChatPlugin
|
||||
state: EditorState,
|
||||
chatApiManager: ChatApiManager,
|
||||
plugin: InlineAIChatPlugin,
|
||||
): DecorationSet {
|
||||
const firstSelectedRange = state.selection.ranges.find((range) => !range.empty) ?? state.selection.main;
|
||||
const firstSelectedRange =
|
||||
state.selection.ranges.find((range) => !range.empty) ??
|
||||
state.selection.main;
|
||||
|
||||
const selectionInfo = state.field(currentSelectionState, false) ?? null;
|
||||
const selectionInfo = state.field(currentSelectionState, false) ?? null;
|
||||
|
||||
const deco = Decoration.widget({
|
||||
widget: new FloatingWidget(chatApiManager, selectionInfo, plugin),
|
||||
above: true,
|
||||
inline: true,
|
||||
side: -9999,
|
||||
}).range(firstSelectedRange.from);
|
||||
const deco = Decoration.widget({
|
||||
widget: new FloatingWidget(chatApiManager, selectionInfo, plugin),
|
||||
above: true,
|
||||
inline: true,
|
||||
|
||||
return Decoration.set([deco]);
|
||||
side: -1,
|
||||
}).range(firstSelectedRange.from);
|
||||
|
||||
return Decoration.set([deco]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -467,30 +508,36 @@ function renderFloatingWidget(
|
|||
* When the user dismisses the tooltip, it clears the decoration set.
|
||||
* Otherwise, it returns the existing decoration set.
|
||||
*/
|
||||
function FloatingTooltipState(chatApiManager: ChatApiManager, plugin: InlineAIChatPlugin) {
|
||||
return StateField.define<DecorationSet>({
|
||||
create(state) {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(decorations, tr) {
|
||||
// Recompute if the user triggers the command
|
||||
if (tr.effects.some((e) => e.is(commandEffect))) {
|
||||
return renderFloatingWidget(tr.state, chatApiManager, plugin);
|
||||
}
|
||||
// Or dismiss it
|
||||
if (tr.effects.some((e) => e.is(dismissTooltipEffect))) {
|
||||
return Decoration.none;
|
||||
}
|
||||
// Otherwise, return the existing overlay
|
||||
return decorations;
|
||||
},
|
||||
provide: (field) => EditorView.decorations.from(field),
|
||||
});
|
||||
function FloatingTooltipState(
|
||||
chatApiManager: ChatApiManager,
|
||||
plugin: InlineAIChatPlugin,
|
||||
) {
|
||||
return StateField.define<DecorationSet>({
|
||||
create(state) {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(decorations, tr) {
|
||||
// Recompute if the user triggers the command
|
||||
if (tr.effects.some((e) => e.is(commandEffect))) {
|
||||
return renderFloatingWidget(tr.state, chatApiManager, plugin);
|
||||
}
|
||||
// Or dismiss it
|
||||
if (tr.effects.some((e) => e.is(dismissTooltipEffect))) {
|
||||
return Decoration.none;
|
||||
}
|
||||
// Otherwise, return the existing overlay
|
||||
return decorations;
|
||||
},
|
||||
provide: (field) => EditorView.decorations.from(field),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension enabling selection overlay widgets.
|
||||
*/
|
||||
export function FloatingTooltipExtension(chatApiManager: ChatApiManager, plugin: InlineAIChatPlugin) {
|
||||
return [FloatingTooltipState(chatApiManager, plugin)];
|
||||
export function FloatingTooltipExtension(
|
||||
chatApiManager: ChatApiManager,
|
||||
plugin: InlineAIChatPlugin,
|
||||
) {
|
||||
return [FloatingTooltipState(chatApiManager, plugin)];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import { SlashCommand } from "./source";
|
||||
|
||||
|
||||
export function parseCommand(userInput: string, prefix: string, customCommands: SlashCommand[]): string {
|
||||
for (const command of customCommands) {
|
||||
const commandPattern = `${prefix}${command.keyword}`;
|
||||
if (userInput.includes(commandPattern)) {
|
||||
return userInput.replace(commandPattern, command.prompt);
|
||||
}
|
||||
}
|
||||
return userInput; // Return original text if no command matches
|
||||
|
||||
export function parseCommand(
|
||||
userInput: string,
|
||||
prefix: string,
|
||||
customCommands: SlashCommand[],
|
||||
): string {
|
||||
for (const command of customCommands) {
|
||||
const commandPattern = `${prefix}${command.keyword}`;
|
||||
if (userInput.includes(commandPattern)) {
|
||||
return userInput.replace(commandPattern, command.prompt);
|
||||
}
|
||||
}
|
||||
return userInput; // Return original text if no command matches
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,87 +1,118 @@
|
|||
import { autocompletion, CompletionContext } from "@codemirror/autocomplete"
|
||||
import { Extension, Range } from "@codemirror/state"
|
||||
import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view"
|
||||
import { autocompletion, CompletionContext } from "@codemirror/autocomplete";
|
||||
import { Extension, Range } from "@codemirror/state";
|
||||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
} from "@codemirror/view";
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* Interface describing a slash command.
|
||||
*/
|
||||
export interface SlashCommand {
|
||||
keyword: string;
|
||||
prompt: string;
|
||||
keyword: string;
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
// Factory function that creates a completion source with custom parameters
|
||||
function createSlashCommandSource(options: {
|
||||
prefix: string,
|
||||
customCommands: SlashCommand[]
|
||||
} = {
|
||||
prefix: '/',
|
||||
customCommands: []
|
||||
}) {
|
||||
const { prefix, customCommands } = options;
|
||||
return (context: CompletionContext) => {
|
||||
let word = context.matchBefore(new RegExp(`^\\${prefix}\\w*`))
|
||||
if (!word || (word.from == word.to && !context.explicit))
|
||||
return null
|
||||
return {
|
||||
from: word.from+1,
|
||||
options: (customCommands).map(cmd => ({
|
||||
label: cmd.keyword,
|
||||
type: undefined,
|
||||
detail: cmd.prompt,
|
||||
}))
|
||||
}
|
||||
}
|
||||
function createSlashCommandSource(
|
||||
options: {
|
||||
prefix: string;
|
||||
customCommands: SlashCommand[];
|
||||
} = {
|
||||
prefix: "/",
|
||||
customCommands: [],
|
||||
},
|
||||
) {
|
||||
const { prefix, customCommands } = options;
|
||||
return (context: CompletionContext) => {
|
||||
let word = context.matchBefore(new RegExp(`^\\${prefix}\\w*`));
|
||||
if (!word || (word.from == word.to && !context.explicit)) return null;
|
||||
return {
|
||||
from: word.from + 1,
|
||||
options: customCommands.map((cmd) => ({
|
||||
label: cmd.keyword,
|
||||
type: undefined,
|
||||
detail: cmd.prompt,
|
||||
})),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Create the extension that uses our custom completion source.
|
||||
export function slashCommandAutocompletion(options: { prefix: string, customCommands: SlashCommand[] } = { prefix: '/', customCommands: [] }) {
|
||||
return autocompletion({
|
||||
override: [createSlashCommandSource(options)],
|
||||
tooltipClass: () => "tooltip-autocomplete",
|
||||
optionClass: () => "completion-label",
|
||||
icons: false,
|
||||
}
|
||||
)
|
||||
export function slashCommandAutocompletion(
|
||||
options: { prefix: string; customCommands: SlashCommand[] } = {
|
||||
prefix: "/",
|
||||
customCommands: [],
|
||||
},
|
||||
) {
|
||||
return autocompletion({
|
||||
override: [createSlashCommandSource(options)],
|
||||
tooltipClass: () => "tooltip-autocomplete",
|
||||
optionClass: () => "completion-label",
|
||||
icons: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Create a decoration for highlighting slash commands
|
||||
const slashCommandMark = Decoration.mark({ class: "cm-slash-command" })
|
||||
const slashCommandMark = Decoration.mark({ class: "cm-slash-command" });
|
||||
|
||||
export function createSlashCommandHighlighter({ prefix, customCommands }: { prefix: string, customCommands: SlashCommand[] }) {
|
||||
return ViewPlugin.fromClass(class {
|
||||
decorations: DecorationSet
|
||||
export function createSlashCommandHighlighter({
|
||||
prefix,
|
||||
customCommands,
|
||||
}: {
|
||||
prefix: string;
|
||||
customCommands: SlashCommand[];
|
||||
}) {
|
||||
return ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: DecorationSet;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.buildDecorations(view)
|
||||
}
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.buildDecorations(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged) {
|
||||
this.decorations = this.buildDecorations(update.view)
|
||||
}
|
||||
}
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged) {
|
||||
this.decorations = this.buildDecorations(update.view);
|
||||
}
|
||||
}
|
||||
|
||||
buildDecorations(view: EditorView) {
|
||||
const keywords = customCommands.map(cmd => cmd.keyword).join('|')
|
||||
const regexp = new RegExp(`\\${prefix}(${keywords})\\b`, 'g')
|
||||
buildDecorations(view: EditorView) {
|
||||
const keywords = customCommands
|
||||
.map((cmd) => cmd.keyword)
|
||||
.join("|");
|
||||
const regexp = new RegExp(`\\${prefix}(${keywords})\\b`, "g");
|
||||
|
||||
const decorations = view.visibleRanges.flatMap(({ from, to }) => {
|
||||
const text = view.state.doc.sliceString(from, to)
|
||||
return [...text.matchAll(regexp)]
|
||||
.map(match =>
|
||||
match.index !== undefined
|
||||
? slashCommandMark.range(from + match.index, from + match.index + 1 + match[1].length)
|
||||
: null
|
||||
)
|
||||
.filter((dec): dec is Range<Decoration> => dec !== null)
|
||||
})
|
||||
|
||||
const decorations = view.visibleRanges.flatMap(
|
||||
({ from, to }) => {
|
||||
const text = view.state.doc.sliceString(from, to);
|
||||
return [...text.matchAll(regexp)]
|
||||
.map((match) =>
|
||||
match.index !== undefined
|
||||
? slashCommandMark.range(
|
||||
from + match.index,
|
||||
from +
|
||||
match.index +
|
||||
1 +
|
||||
match[1].length,
|
||||
)
|
||||
: null,
|
||||
)
|
||||
.filter(
|
||||
(dec): dec is Range<Decoration> => dec !== null,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return Decoration.set(decorations)
|
||||
}
|
||||
}, {
|
||||
decorations: v => v.decorations
|
||||
})
|
||||
return Decoration.set(decorations);
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,20 @@
|
|||
// modules/diffExtension.ts
|
||||
import { EditorState, StateField, RangeSetBuilder } from "@codemirror/state";
|
||||
import {
|
||||
EditorState,
|
||||
StateField,
|
||||
RangeSetBuilder,
|
||||
} from "@codemirror/state";
|
||||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
WidgetType,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
WidgetType,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
} from "@codemirror/view";
|
||||
import DiffMatchPatch from "diff-match-patch";
|
||||
|
||||
import { acceptTooltipEffect, dismissTooltipEffect } from "./WidgetExtension";
|
||||
import { generatedResponseState, setGeneratedResponseEffect } from "./AIExtension";
|
||||
import {
|
||||
generatedResponseState,
|
||||
setGeneratedResponseEffect,
|
||||
} from "./AIExtension";
|
||||
import { currentSelectionState } from "./SelectionState";
|
||||
|
||||
/**
|
||||
|
|
@ -23,30 +22,30 @@ import { currentSelectionState } from "./SelectionState";
|
|||
* Improves accessibility by using appropriate ARIA attributes.
|
||||
*/
|
||||
class ChangeContentWidget extends WidgetType {
|
||||
constructor(
|
||||
private readonly content: string,
|
||||
private readonly type: 'added' | 'removed'
|
||||
) {
|
||||
super();
|
||||
}
|
||||
constructor(
|
||||
private readonly content: string,
|
||||
private readonly type: "added" | "removed",
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
toDOM(): HTMLElement {
|
||||
const wrapper = document.createElement("span");
|
||||
wrapper.className = `cm-change-widget cm-change-${this.type}`;
|
||||
wrapper.textContent = this.content;
|
||||
toDOM(): HTMLElement {
|
||||
const wrapper = document.createElement("span");
|
||||
wrapper.className = `cm-change-widget cm-change-${this.type}`;
|
||||
wrapper.textContent = this.content;
|
||||
|
||||
// Accessibility: Provide ARIA label
|
||||
wrapper.setAttribute(
|
||||
"aria-label",
|
||||
this.type === 'added' ? "Added content" : "Removed content"
|
||||
);
|
||||
return wrapper;
|
||||
}
|
||||
// Accessibility: Provide ARIA label
|
||||
wrapper.setAttribute(
|
||||
"aria-label",
|
||||
this.type === "added" ? "Added content" : "Removed content",
|
||||
);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
ignoreEvent(): boolean {
|
||||
// Decide whether to ignore events on the widget
|
||||
return false;
|
||||
}
|
||||
ignoreEvent(): boolean {
|
||||
// Decide whether to ignore events on the widget
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -56,58 +55,57 @@ class ChangeContentWidget extends WidgetType {
|
|||
* @returns A DecorationSet with the appropriate widgets.
|
||||
*/
|
||||
function generateDiffView(state: EditorState): DecorationSet {
|
||||
try {
|
||||
// Retrieve the AI response and the current context text from the state
|
||||
const response = state.field(generatedResponseState);
|
||||
const context = state.field(currentSelectionState);
|
||||
try {
|
||||
// Retrieve the AI response and the current context text from the state
|
||||
const response = state.field(generatedResponseState);
|
||||
const context = state.field(currentSelectionState);
|
||||
|
||||
const aiText: string = response?.airesponse ?? "";
|
||||
const contextText: string = context?.text ?? "";
|
||||
const aiText: string = response?.airesponse ?? "";
|
||||
const contextText: string = context?.text ?? "";
|
||||
|
||||
// Use diff_match_patch instead of diffWords
|
||||
const dmp = new DiffMatchPatch();
|
||||
let diffs = dmp.diff_main(contextText, aiText);
|
||||
// Use diff_match_patch instead of diffWords
|
||||
const dmp = new DiffMatchPatch();
|
||||
let diffs = dmp.diff_main(contextText, aiText);
|
||||
|
||||
// Perform semantic cleanup
|
||||
dmp.diff_cleanupSemantic(diffs);
|
||||
// Perform semantic cleanup
|
||||
dmp.diff_cleanupSemantic(diffs);
|
||||
|
||||
// Initialize RangeSetBuilder for efficient decoration construction
|
||||
const builder = new RangeSetBuilder<Decoration>();
|
||||
let currentPos = context?.from ?? 0;
|
||||
// Initialize RangeSetBuilder for efficient decoration construction
|
||||
const builder = new RangeSetBuilder<Decoration>();
|
||||
let currentPos = context?.from ?? 0;
|
||||
|
||||
diffs.forEach(([op, text]) => {
|
||||
const length = text.length;
|
||||
|
||||
diffs.forEach(([op, text]) => {
|
||||
const length = text.length;
|
||||
if (op === DiffMatchPatch.DIFF_INSERT) {
|
||||
// AI text added
|
||||
const widget = new ChangeContentWidget(text, "added");
|
||||
builder.add(
|
||||
currentPos,
|
||||
currentPos,
|
||||
Decoration.widget({ widget, side: 1 }),
|
||||
);
|
||||
} else if (op === DiffMatchPatch.DIFF_DELETE) {
|
||||
// Context text removed
|
||||
const widget = new ChangeContentWidget(text, "removed");
|
||||
// Attach the widget over the removed range
|
||||
builder.add(
|
||||
currentPos,
|
||||
currentPos + length,
|
||||
Decoration.widget({ widget, side: -1 }),
|
||||
);
|
||||
currentPos += length;
|
||||
} else {
|
||||
// No change, move currentPos forward
|
||||
currentPos += length;
|
||||
}
|
||||
});
|
||||
|
||||
if (op === DiffMatchPatch.DIFF_INSERT) {
|
||||
// AI text added
|
||||
const widget = new ChangeContentWidget(text, "added");
|
||||
builder.add(
|
||||
currentPos,
|
||||
currentPos,
|
||||
Decoration.widget({ widget, side: 1 })
|
||||
);
|
||||
} else if (op === DiffMatchPatch.DIFF_DELETE) {
|
||||
// Context text removed
|
||||
const widget = new ChangeContentWidget(text, "removed");
|
||||
// Attach the widget over the removed range
|
||||
builder.add(
|
||||
currentPos,
|
||||
currentPos + length,
|
||||
Decoration.widget({ widget, side: -1 })
|
||||
);
|
||||
currentPos += length;
|
||||
} else {
|
||||
// No change, move currentPos forward
|
||||
currentPos += length;
|
||||
}
|
||||
});
|
||||
|
||||
return builder.finish();
|
||||
} catch (error) {
|
||||
console.error("Error generating diff view:", error);
|
||||
return Decoration.none;
|
||||
}
|
||||
return builder.finish();
|
||||
} catch (error) {
|
||||
console.error("Error generating diff view:", error);
|
||||
return Decoration.none;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -117,69 +115,70 @@ function generateDiffView(state: EditorState): DecorationSet {
|
|||
* @param view - The EditorView instance.
|
||||
*/
|
||||
function dispatchAIChanges(state: EditorState, view: EditorView): void {
|
||||
try {
|
||||
// Grab the AI text and selection info (original context)
|
||||
const response = state.field(generatedResponseState);
|
||||
const context = state.field(currentSelectionState);
|
||||
try {
|
||||
// Grab the AI text and selection info (original context)
|
||||
const response = state.field(generatedResponseState);
|
||||
const context = state.field(currentSelectionState);
|
||||
|
||||
const aiText: string = response?.airesponse ?? "";
|
||||
const selectionFrom = context?.from ?? 0;
|
||||
const selectionTo = context?.to ?? 0;
|
||||
const aiText: string = response?.airesponse ?? "";
|
||||
const selectionFrom = context?.from ?? 0;
|
||||
const selectionTo = context?.to ?? 0;
|
||||
|
||||
// Dispatch the transaction to apply the AI changes
|
||||
view.dispatch({
|
||||
changes: { from: selectionFrom, to: selectionTo, insert: aiText },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error applying diff changes:", error);
|
||||
}
|
||||
// Dispatch the transaction to apply the AI changes
|
||||
view.dispatch({
|
||||
changes: { from: selectionFrom, to: selectionTo, insert: aiText },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error applying diff changes:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export const diffDecorationState = StateField.define<DecorationSet>({
|
||||
create(): DecorationSet {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(decorations: DecorationSet, tr) {
|
||||
// Check if we got the AI response effect
|
||||
if (tr.effects.some(e => e.is(setGeneratedResponseEffect))) {
|
||||
return generateDiffView(tr.state);
|
||||
}
|
||||
create(): DecorationSet {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(decorations: DecorationSet, tr) {
|
||||
// Check if we got the AI response effect
|
||||
if (tr.effects.some((e) => e.is(setGeneratedResponseEffect))) {
|
||||
return generateDiffView(tr.state);
|
||||
}
|
||||
|
||||
// Check if the dismiss tooltip effect is present
|
||||
const hasDismissEffect = tr.effects.some(e => e.is(dismissTooltipEffect));
|
||||
if (hasDismissEffect) {
|
||||
return Decoration.none;
|
||||
}
|
||||
// Check if the dismiss tooltip effect is present
|
||||
const hasDismissEffect = tr.effects.some((e) =>
|
||||
e.is(dismissTooltipEffect),
|
||||
);
|
||||
if (hasDismissEffect) {
|
||||
return Decoration.none;
|
||||
}
|
||||
|
||||
// Retain the existing decorations if no relevant changes
|
||||
return decorations;
|
||||
},
|
||||
provide: (field) => EditorView.decorations.from(field),
|
||||
// Retain the existing decorations if no relevant changes
|
||||
return decorations;
|
||||
},
|
||||
provide: (field) => EditorView.decorations.from(field),
|
||||
});
|
||||
|
||||
/**
|
||||
* Plugin to handle applying diff changes when the accept tooltip effect is triggered.
|
||||
*/
|
||||
const applyDiffPlugin = ViewPlugin.fromClass(class {
|
||||
update(update: ViewUpdate) {
|
||||
// Iterate through all transactions in the update
|
||||
for (const transaction of update.transactions) {
|
||||
for (const effect of transaction.effects) {
|
||||
if (effect.is(acceptTooltipEffect)) {
|
||||
// Apply the diff changes by dispatching the transaction
|
||||
setTimeout(() => {
|
||||
dispatchAIChanges(update.state, update.view);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const applyDiffPlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
update(update: ViewUpdate) {
|
||||
// Iterate through all transactions in the update
|
||||
for (const transaction of update.transactions) {
|
||||
for (const effect of transaction.effects) {
|
||||
if (effect.is(acceptTooltipEffect)) {
|
||||
// Apply the diff changes by dispatching the transaction
|
||||
setTimeout(() => {
|
||||
dispatchAIChanges(update.state, update.view);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Exported extension to be included in the EditorView.
|
||||
*/
|
||||
export const diffExtension = [
|
||||
diffDecorationState,
|
||||
applyDiffPlugin,
|
||||
];
|
||||
export const diffExtension = [diffDecorationState, applyDiffPlugin];
|
||||
|
|
|
|||
|
|
@ -5,51 +5,51 @@
|
|||
* - Provides standard queue operations: enqueue, dequeue, peek, size, isEmpty, and getItems.
|
||||
*/
|
||||
export class MessageQueue<T> {
|
||||
private items: T[] = [];
|
||||
private maxLength: number;
|
||||
private items: T[] = [];
|
||||
private maxLength: number;
|
||||
|
||||
constructor(maxLength: number) {
|
||||
this.maxLength = maxLength;
|
||||
}
|
||||
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);
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
// 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 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;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
// Checks if the queue is empty
|
||||
isEmpty(): boolean {
|
||||
return this.items.length === 0;
|
||||
}
|
||||
|
||||
// Returns the items in the queue
|
||||
getItems(): T[] {
|
||||
return [...this.items];
|
||||
}
|
||||
// Returns the items in the queue
|
||||
getItems(): T[] {
|
||||
return [...this.items];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export const DEFAULT_SETTINGS: InlineAISettings = {
|
|||
cursorPrompt: cursorPrompt,
|
||||
customCommands: [],
|
||||
commandPrefix: "/",
|
||||
messageHistory: false
|
||||
messageHistory: false,
|
||||
};
|
||||
|
||||
export class InlineAISettingsTab extends PluginSettingTab {
|
||||
|
|
@ -50,7 +50,9 @@ export class InlineAISettingsTab extends PluginSettingTab {
|
|||
// Provider setting
|
||||
new Setting(containerEl)
|
||||
.setName("Provider")
|
||||
.setDesc("Choose between OpenAI, Ollama, or a custom OpenAI-compatible endpoint.")
|
||||
.setDesc(
|
||||
"Choose between OpenAI, Ollama, or a custom OpenAI-compatible endpoint.",
|
||||
)
|
||||
.addDropdown((dropdown) =>
|
||||
dropdown
|
||||
.addOption("openai", "OpenAI")
|
||||
|
|
@ -59,10 +61,14 @@ export class InlineAISettingsTab extends PluginSettingTab {
|
|||
.addOption("custom", "Custom/OpenAI-compatible")
|
||||
.setValue(this.plugin.settings.provider)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.provider = value as "openai" | "ollama" | "custom" | "gemini";
|
||||
this.plugin.settings.provider = value as
|
||||
| "openai"
|
||||
| "ollama"
|
||||
| "custom"
|
||||
| "gemini";
|
||||
await this.saveSettings();
|
||||
this.display(); // Refresh UI to show/hide API key field
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// Model setting
|
||||
|
|
@ -79,7 +85,11 @@ export class InlineAISettingsTab extends PluginSettingTab {
|
|||
});
|
||||
|
||||
// API Key setting (conditionally displayed for OpenAI-supported endpoints)
|
||||
if (this.plugin.settings.provider === "openai" || this.plugin.settings.provider === "custom" || this.plugin.settings.provider === "gemini") {
|
||||
if (
|
||||
this.plugin.settings.provider === "openai" ||
|
||||
this.plugin.settings.provider === "custom" ||
|
||||
this.plugin.settings.provider === "gemini"
|
||||
) {
|
||||
new Setting(containerEl)
|
||||
.setName("API key")
|
||||
.setDesc("Enter your API key.")
|
||||
|
|
@ -97,7 +107,9 @@ export class InlineAISettingsTab extends PluginSettingTab {
|
|||
if (this.plugin.settings.provider === "custom") {
|
||||
new Setting(containerEl)
|
||||
.setName("Custom endpoint")
|
||||
.setDesc("Enter your OpenAI-compatible base URL (e.g. https://api.groq.com/openai/v1).")
|
||||
.setDesc(
|
||||
"Enter your OpenAI-compatible base URL (e.g. https://api.groq.com/openai/v1).",
|
||||
)
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("https://api.mycustomhost.com/v1")
|
||||
.setValue(this.plugin.settings.customURL || "")
|
||||
|
|
@ -113,12 +125,16 @@ export class InlineAISettingsTab extends PluginSettingTab {
|
|||
// Selection Prompt setting
|
||||
new Setting(containerEl)
|
||||
.setName("Selection prompt")
|
||||
.setDesc("System Prompt used when the tooltip is triggered with selected text.")
|
||||
.setDesc(
|
||||
"System Prompt used when the tooltip is triggered with selected text.",
|
||||
)
|
||||
.addTextArea((textarea) => {
|
||||
textarea.setPlaceholder("e.g., Summarize the selected text.")
|
||||
textarea
|
||||
.setPlaceholder("e.g., Summarize the selected text.")
|
||||
.setValue(this.plugin.settings.selectionPrompt)
|
||||
.inputEl.addEventListener("blur", async () => {
|
||||
this.plugin.settings.selectionPrompt = textarea.getValue();
|
||||
this.plugin.settings.selectionPrompt =
|
||||
textarea.getValue();
|
||||
await this.saveSettings();
|
||||
});
|
||||
textarea.inputEl.classList.add("wide-text-settings");
|
||||
|
|
@ -127,9 +143,14 @@ export class InlineAISettingsTab extends PluginSettingTab {
|
|||
// Cursor Prompt setting
|
||||
new Setting(containerEl)
|
||||
.setName("Cursor prompt")
|
||||
.setDesc("System Prompt used when the tooltip is triggered with selected text.")
|
||||
.setDesc(
|
||||
"System Prompt used when the tooltip is triggered with selected text.",
|
||||
)
|
||||
.addTextArea((textarea) => {
|
||||
textarea.setPlaceholder("e.g., Generate text based on cursor position.")
|
||||
textarea
|
||||
.setPlaceholder(
|
||||
"e.g., Generate text based on cursor position.",
|
||||
)
|
||||
.setValue(this.plugin.settings.cursorPrompt)
|
||||
.inputEl.addEventListener("blur", async () => {
|
||||
this.plugin.settings.cursorPrompt = textarea.getValue();
|
||||
|
|
@ -141,28 +162,37 @@ export class InlineAISettingsTab extends PluginSettingTab {
|
|||
// Message History setting
|
||||
new Setting(containerEl)
|
||||
.setName("Message History")
|
||||
.setDesc("Enable message history, you can navigate through the history using the up/down arrow keys.")
|
||||
.setDesc(
|
||||
"Enable message history, you can navigate through the history using the up/down arrow keys.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(this.plugin.settings.messageHistory)
|
||||
toggle
|
||||
.setValue(this.plugin.settings.messageHistory)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.messageHistory = value;
|
||||
await this.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Custom Commands Section
|
||||
containerEl.createEl("h3", { text: "Custom Commands" });
|
||||
containerEl.createEl("p", { text: "Add your own custom commands. Triggered with the prefix defined in the Command Prefix setting." });
|
||||
containerEl.createEl("p", {
|
||||
text: "Add your own custom commands. Triggered with the prefix defined in the Command Prefix setting.",
|
||||
});
|
||||
|
||||
// Command Prefix setting
|
||||
new Setting(containerEl)
|
||||
.setName("Command Prefix")
|
||||
.setDesc("The prefix used to trigger custom commands (e.g., /, !, #)")
|
||||
.setDesc(
|
||||
"The prefix used to trigger custom commands (e.g., /, !, #)",
|
||||
)
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("/")
|
||||
.setValue(this.plugin.settings.commandPrefix)
|
||||
.inputEl.addEventListener("blur", async () => {
|
||||
this.plugin.settings.commandPrefix = text.getValue().charAt(0);
|
||||
this.plugin.settings.commandPrefix = text
|
||||
.getValue()
|
||||
.charAt(0);
|
||||
await this.saveSettings();
|
||||
this.display();
|
||||
});
|
||||
|
|
@ -177,15 +207,18 @@ export class InlineAISettingsTab extends PluginSettingTab {
|
|||
text.setValue(command.keyword)
|
||||
.setPlaceholder("Command name")
|
||||
.inputEl.addEventListener("blur", async () => {
|
||||
this.plugin.settings.customCommands[index].keyword = text.getValue();
|
||||
this.plugin.settings.customCommands[index].keyword =
|
||||
text.getValue();
|
||||
await this.saveSettings();
|
||||
});
|
||||
})
|
||||
.addTextArea((textarea) => {
|
||||
textarea.setValue(command.prompt)
|
||||
textarea
|
||||
.setValue(command.prompt)
|
||||
.setPlaceholder("Command prompt")
|
||||
.inputEl.addEventListener("blur", async () => {
|
||||
this.plugin.settings.customCommands[index].prompt = textarea.getValue();
|
||||
this.plugin.settings.customCommands[index].prompt =
|
||||
textarea.getValue();
|
||||
await this.saveSettings();
|
||||
});
|
||||
})
|
||||
|
|
@ -194,25 +227,29 @@ export class InlineAISettingsTab extends PluginSettingTab {
|
|||
.setIcon("trash")
|
||||
.setTooltip("Delete this command")
|
||||
.onClick(async () => {
|
||||
this.plugin.settings.customCommands.splice(index, 1);
|
||||
this.plugin.settings.customCommands.splice(
|
||||
index,
|
||||
1,
|
||||
);
|
||||
await this.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Add new command button
|
||||
new Setting(containerEl).addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("Add Command")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
this.plugin.settings.customCommands.push({ keyword: "new_command", prompt: "" });
|
||||
this.plugin.settings.customCommands.push({
|
||||
keyword: "new_command",
|
||||
prompt: "",
|
||||
});
|
||||
await this.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
styles.css
21
styles.css
|
|
@ -79,12 +79,16 @@
|
|||
@keyframes l5 {
|
||||
0%,
|
||||
33% {
|
||||
box-shadow: 10px 0 var(--text-accent), -10px 0 var(--text-faint);
|
||||
box-shadow:
|
||||
10px 0 var(--text-accent),
|
||||
-10px 0 var(--text-faint);
|
||||
background: var(--text-accent);
|
||||
}
|
||||
66%,
|
||||
100% {
|
||||
box-shadow: 10px 0 var(--text-faint), -10px 0 var(--text-accent);
|
||||
box-shadow:
|
||||
10px 0 var(--text-faint),
|
||||
-10px 0 var(--text-accent);
|
||||
background: var(--text-faint);
|
||||
}
|
||||
}
|
||||
|
|
@ -118,7 +122,7 @@
|
|||
height: 10em;
|
||||
}
|
||||
|
||||
.tooltip-autocomplete{
|
||||
.tooltip-autocomplete {
|
||||
background-color: var(--background-primary-alt) !important;
|
||||
color: var(--text-normal) !important;
|
||||
width: 101.9% !important;
|
||||
|
|
@ -130,11 +134,11 @@
|
|||
}
|
||||
|
||||
.tooltip-autocomplete li[aria-selected] {
|
||||
background-color: var(--text-selection)!important;
|
||||
background-color: var(--text-selection) !important;
|
||||
color: var(--text-accent) !important;
|
||||
}
|
||||
|
||||
.completion-label{
|
||||
.completion-label {
|
||||
font-size: var(--font-ui-small);
|
||||
font-weight: var(--font-normal);
|
||||
font-family: Arial, Helvetica, sans-serif !important;
|
||||
|
|
@ -144,12 +148,11 @@
|
|||
|
||||
.cm-tooltip.cm-tooltip-below {
|
||||
top: 100%;
|
||||
margin-top: 0.8em;
|
||||
margin-top: 0.8em;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
/* Slash command highlighting */
|
||||
.cm-slash-command {
|
||||
color: var(--text-accent);
|
||||
|
||||
}
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue