Skip to content

Commit

Permalink
Add inline edit dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
zeroliu committed Jan 13, 2025
1 parent 479512d commit 8a738b5
Show file tree
Hide file tree
Showing 8 changed files with 406 additions and 216 deletions.
50 changes: 26 additions & 24 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,78 +2,79 @@ import { LanguageModal } from "@/components/modals/LanguageModal";
import { ToneModal } from "@/components/modals/ToneModal";
import CopilotPlugin from "@/main";
import { Editor, Notice } from "obsidian";
import { COMMAND_IDS } from "./constants";
import { COMMAND_IDS, COMMAND_NAMES, CommandId } from "./constants";
import { getSettings } from "@/settings/model";
import { DISABLEABLE_COMMANDS } from "./constants";

export function registerBuiltInCommands(plugin: CopilotPlugin) {
// Remove all built in commands first
Object.values(COMMAND_IDS).forEach((id) => {
DISABLEABLE_COMMANDS.forEach((id) => {
// removeCommand is not available in TypeScript for some reasons
// https://docs.obsidian.md/Reference/TypeScript+API/Plugin/removeCommand
(plugin as any).removeCommand(id);
});

const addCommandIfEnabled = (id: string, callback: (editor: Editor) => void) => {
const addCommandIfEnabled = (id: CommandId, callback: (editor: Editor) => void) => {
const commandSettings = getSettings().enabledCommands[id];
if (commandSettings && commandSettings.enabled) {
plugin.addCommand({
id,
name: commandSettings.name,
name: COMMAND_NAMES[id],
editorCallback: callback,
});
}
};

addCommandIfEnabled(COMMAND_IDS.FIX_GRAMMAR, (editor) => {
plugin.processSelection(editor, "fixGrammarSpellingSelection");
plugin.processSelection(editor, COMMAND_IDS.FIX_GRAMMAR);
});

addCommandIfEnabled(COMMAND_IDS.SUMMARIZE, (editor) => {
plugin.processSelection(editor, "summarizeSelection");
plugin.processSelection(editor, COMMAND_IDS.SUMMARIZE);
});

addCommandIfEnabled(COMMAND_IDS.GENERATE_TOC, (editor) => {
plugin.processSelection(editor, "tocSelection");
plugin.processSelection(editor, COMMAND_IDS.GENERATE_TOC);
});

addCommandIfEnabled(COMMAND_IDS.GENERATE_GLOSSARY, (editor) => {
plugin.processSelection(editor, "glossarySelection");
plugin.processSelection(editor, COMMAND_IDS.GENERATE_GLOSSARY);
});

addCommandIfEnabled(COMMAND_IDS.SIMPLIFY, (editor) => {
plugin.processSelection(editor, "simplifySelection");
plugin.processSelection(editor, COMMAND_IDS.SIMPLIFY);
});

addCommandIfEnabled(COMMAND_IDS.EMOJIFY, (editor) => {
plugin.processSelection(editor, "emojifySelection");
plugin.processSelection(editor, COMMAND_IDS.EMOJIFY);
});

addCommandIfEnabled(COMMAND_IDS.REMOVE_URLS, (editor) => {
plugin.processSelection(editor, "removeUrlsFromSelection");
plugin.processSelection(editor, COMMAND_IDS.REMOVE_URLS);
});

addCommandIfEnabled(COMMAND_IDS.REWRITE_TWEET, (editor) => {
plugin.processSelection(editor, "rewriteTweetSelection");
plugin.processSelection(editor, COMMAND_IDS.REWRITE_TWEET);
});

addCommandIfEnabled(COMMAND_IDS.REWRITE_TWEET_THREAD, (editor) => {
plugin.processSelection(editor, "rewriteTweetThreadSelection");
plugin.processSelection(editor, COMMAND_IDS.REWRITE_TWEET_THREAD);
});

addCommandIfEnabled(COMMAND_IDS.MAKE_SHORTER, (editor) => {
plugin.processSelection(editor, "rewriteShorterSelection");
plugin.processSelection(editor, COMMAND_IDS.MAKE_SHORTER);
});

addCommandIfEnabled(COMMAND_IDS.MAKE_LONGER, (editor) => {
plugin.processSelection(editor, "rewriteLongerSelection");
plugin.processSelection(editor, COMMAND_IDS.MAKE_LONGER);
});

addCommandIfEnabled(COMMAND_IDS.ELI5, (editor) => {
plugin.processSelection(editor, "eli5Selection");
plugin.processSelection(editor, COMMAND_IDS.ELI5);
});

addCommandIfEnabled(COMMAND_IDS.PRESS_RELEASE, (editor) => {
plugin.processSelection(editor, "rewritePressReleaseSelection");
plugin.processSelection(editor, COMMAND_IDS.PRESS_RELEASE);
});

addCommandIfEnabled(COMMAND_IDS.TRANSLATE, (editor) => {
Expand All @@ -82,7 +83,7 @@ export function registerBuiltInCommands(plugin: CopilotPlugin) {
new Notice("Please select a language.");
return;
}
plugin.processSelection(editor, "translateSelection", language);
plugin.processSelection(editor, COMMAND_IDS.TRANSLATE, language);
}).open();
});

Expand All @@ -92,21 +93,22 @@ export function registerBuiltInCommands(plugin: CopilotPlugin) {
new Notice("Please select a tone.");
return;
}
plugin.processSelection(editor, "changeToneSelection", tone);
plugin.processSelection(editor, COMMAND_IDS.CHANGE_TONE, tone);
}).open();
});

plugin.addCommand({
id: COMMAND_IDS.COUNT_TOKENS,
name: "Count words and tokens in selection",
editorCallback: (editor: Editor) => {
plugin.processSelection(editor, "countTokensSelection");
id: COMMAND_IDS.COUNT_WORD_AND_TOKENS_SELECTION,
name: COMMAND_NAMES[COMMAND_IDS.COUNT_WORD_AND_TOKENS_SELECTION],
editorCallback: async (editor: Editor) => {
const { wordCount, tokenCount } = await plugin.countSelectionWordsAndTokens(editor);
new Notice(`Selected text contains ${wordCount} words and ${tokenCount} tokens.`);
},
});

plugin.addCommand({
id: COMMAND_IDS.COUNT_TOTAL_VAULT_TOKENS,
name: "Count total tokens in your vault",
name: COMMAND_NAMES[COMMAND_IDS.COUNT_TOTAL_VAULT_TOKENS],
callback: async () => {
const totalTokens = await plugin.countTotalTokens();
new Notice(`Total tokens in your vault: ${totalTokens}`);
Expand Down
122 changes: 56 additions & 66 deletions src/components/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ import { ChainType } from "@/chainFactory";
import { updateChatMemory } from "@/chatUtils";
import ChatInput from "@/components/chat-components/ChatInput";
import ChatMessages from "@/components/chat-components/ChatMessages";
import { ABORT_REASON, AI_SENDER, EVENT_NAMES, LOADING_MESSAGES, USER_SENDER } from "@/constants";
import { InlineEditModal } from "@/components/modals/InlineEditModal";
import {
ABORT_REASON,
COMMAND_IDS,
CommandId,
EVENT_NAMES,
LOADING_MESSAGES,
USER_SENDER,
} from "@/constants";
import { AppContext, EventTargetContext } from "@/context";
import { ContextProcessor } from "@/contextProcessor";
import { CustomPromptProcessor } from "@/customPromptProcessor";
Expand Down Expand Up @@ -436,123 +444,100 @@ ${chatContent}`;
[addMessage, chainManager.memoryManager, chatHistory, clearMessages, handleRegenerate]
);

useEffect(() => {
async function handleSelection(event: CustomEvent) {
const wordCount = event.detail.selectedText.split(" ").length;
const tokenCount = await chainManager.chatModelManager.countTokens(event.detail.selectedText);
const tokenCountMessage: ChatMessage = {
sender: AI_SENDER,
message: `The selected text contains ${wordCount} words and ${tokenCount} tokens.`,
isVisible: true,
timestamp: formatDateTime(new Date()),
};
addMessage(tokenCountMessage);
}

eventTarget?.addEventListener("countTokensSelection", handleSelection);

// Cleanup function to remove the event listener when the component unmounts
return () => {
eventTarget?.removeEventListener("countTokensSelection", handleSelection);
};
}, [addMessage, chainManager.chatModelManager, eventTarget]);

// Create an effect for each event type (Copilot command on selected text)
const createEffect = (
eventType: string,
commandId: CommandId,
promptFn: (selectedText: string, eventSubtype?: string) => string | Promise<string>,
options: CreateEffectOptions = {}
) => {
return () => {
const {
isVisible = false,
ignoreSystemMessage = true, // Ignore system message by default for commands
} = options;
const handleSelection = async (event: CustomEvent) => {
const messageWithPrompt = await promptFn(
event.detail.selectedText,
event.detail.eventSubtype
);

// Create a user message with the selected text
const promptMessage: ChatMessage = {
message: messageWithPrompt,
sender: USER_SENDER,
isVisible: isVisible,
isVisible: false,
timestamp: formatDateTime(new Date()),
};

if (isVisible) {
addMessage(promptMessage);
const selectedText = event.detail.selectedText;

if (selectedText) {
new InlineEditModal(app, {
selectedText,
commandId,
promptMessage,
chainManager,
onInsert: (message: string) => {
handleInsertAtCursor(message);
},
onReplace: (message: string) => {
handleInsertAtCursor(message, true);
},
}).open();
}

setLoading(true);
await getAIResponse(
promptMessage,
chainManager,
addMessage,
setCurrentAiMessage,
setAbortController,
{
debug: settings.debug,
ignoreSystemMessage,
}
);
setLoading(false);
};

eventTarget?.addEventListener(eventType, handleSelection);
eventTarget?.addEventListener(commandId, handleSelection);

// Cleanup function to remove the event listener when the component unmounts
return () => {
eventTarget?.removeEventListener(eventType, handleSelection);
eventTarget?.removeEventListener(commandId, handleSelection);
};
};
};

// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(createEffect("fixGrammarSpellingSelection", fixGrammarSpellingSelectionPrompt), []);
useEffect(createEffect(COMMAND_IDS.FIX_GRAMMAR, fixGrammarSpellingSelectionPrompt), []);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(createEffect("summarizeSelection", summarizePrompt), []);
useEffect(createEffect(COMMAND_IDS.SUMMARIZE, summarizePrompt), []);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(createEffect("tocSelection", tocPrompt), []);
useEffect(createEffect(COMMAND_IDS.GENERATE_TOC, tocPrompt), []);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(createEffect("glossarySelection", glossaryPrompt), []);
useEffect(createEffect(COMMAND_IDS.GENERATE_GLOSSARY, glossaryPrompt), []);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(createEffect("simplifySelection", simplifyPrompt), []);
useEffect(createEffect(COMMAND_IDS.SIMPLIFY, simplifyPrompt), []);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(createEffect("emojifySelection", emojifyPrompt), []);
useEffect(createEffect(COMMAND_IDS.EMOJIFY, emojifyPrompt), []);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(createEffect("removeUrlsFromSelection", removeUrlsFromSelectionPrompt), []);
useEffect(createEffect(COMMAND_IDS.REMOVE_URLS, removeUrlsFromSelectionPrompt), []);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(
createEffect("rewriteTweetSelection", rewriteTweetSelectionPrompt, { custom_temperature: 0.2 }),
createEffect(COMMAND_IDS.REWRITE_TWEET, rewriteTweetSelectionPrompt, {
custom_temperature: 0.2,
}),
[]
);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(
createEffect("rewriteTweetThreadSelection", rewriteTweetThreadSelectionPrompt, {
createEffect(COMMAND_IDS.REWRITE_TWEET_THREAD, rewriteTweetThreadSelectionPrompt, {
custom_temperature: 0.2,
}),
[]
);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(createEffect("rewriteShorterSelection", rewriteShorterSelectionPrompt), []);
useEffect(createEffect(COMMAND_IDS.MAKE_SHORTER, rewriteShorterSelectionPrompt), []);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(createEffect("rewriteLongerSelection", rewriteLongerSelectionPrompt), []);
useEffect(createEffect(COMMAND_IDS.MAKE_LONGER, rewriteLongerSelectionPrompt), []);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(createEffect("eli5Selection", eli5SelectionPrompt), []);
useEffect(createEffect(COMMAND_IDS.ELI5, eli5SelectionPrompt), []);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(createEffect("rewritePressReleaseSelection", rewritePressReleaseSelectionPrompt), []);
useEffect(createEffect(COMMAND_IDS.PRESS_RELEASE, rewritePressReleaseSelectionPrompt), []);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(
createEffect("translateSelection", (selectedText, language) =>
createEffect(COMMAND_IDS.TRANSLATE, (selectedText, language) =>
createTranslateSelectionPrompt(language)(selectedText)
),
[]
);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(
createEffect("changeToneSelection", (selectedText, tone) =>
createEffect(COMMAND_IDS.CHANGE_TONE, (selectedText, tone) =>
createChangeToneSelectionPrompt(tone)(selectedText)
),
[]
Expand All @@ -562,7 +547,7 @@ ${chatContent}`;
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(
createEffect(
"applyCustomPrompt",
COMMAND_IDS.APPLY_CUSTOM_PROMPT,
async (selectedText, customPrompt) => {
if (!customPrompt) {
return selectedText;
Expand All @@ -580,7 +565,7 @@ ${chatContent}`;
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(
createEffect(
"applyAdhocPrompt",
COMMAND_IDS.APPLY_ADHOC_PROMPT,
async (selectedText, customPrompt) => {
if (!customPrompt) {
return selectedText;
Expand All @@ -597,7 +582,7 @@ ${chatContent}`;
);

const handleInsertAtCursor = useCallback(
async (message: string) => {
async (message: string, replace: boolean = false) => {
let leaf = app.workspace.getMostRecentLeaf();
if (!leaf) {
new Notice("No active leaf found.");
Expand All @@ -615,8 +600,13 @@ ${chatContent}`;
}

const editor = leaf.view.editor;
const cursor = editor.getCursor();
editor.replaceRange(message, cursor);
const cursorFrom = editor.getCursor("from");
const cursorTo = editor.getCursor("to");
if (replace) {
editor.replaceRange(message, cursorFrom, cursorTo);
} else {
editor.replaceRange(message, cursorTo);
}
new Notice("Message inserted into the active note.");
},
[app.workspace]
Expand Down
Loading

0 comments on commit 8a738b5

Please sign in to comment.