diff --git a/README.md b/README.md
index 7bead05..9333655 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,8 @@
![logseq gpt3 openai demo](docs/summarize.gif)
+Follow me on Twitter for updates and examples of how I use this plugin: [@bsunter](https://twitter.com/bsunter)
+[![](docs/follow.png)](https://twitter.com/bsunter)
# Usage
## `gpt`
@@ -33,6 +35,8 @@ To bring up the gpt popup, use the keyboard shortcut `cmd+g`, or select `gpt` fr
If you are currently in a block, the plugin will use the text in the block as input to the prompt.
+You can click and drag or shift+click to select multiple blocks to use as input to the prompt.
+
If you are not in a block, the plugin won't add any additional input text to your prompt, and will append the results of the prompt to the bottom of the page.
After selecting the prompt and generating a response, a preview of the response will be shown in the popup. You can click the `Insert` button or press the enter key to insert the response into the page.
@@ -120,7 +124,10 @@ This will generate an image using the DALL-E model, save the image to the `asset
![logseq dalle](docs/dalle.gif)
+#### Select Multiple Blocks
+You can click and drag or shift+click to select multiple blocks to use as input to the prompt.
+![multi select](docs/multi-select.gif)
### ChatGPT Guidance
You can adjust the `chatPrompt` setting to adjust how ChatGPT should respond to your input. By default, the setting is set to `Do not refer to yourself in your answers. Do not say as an AI language model...` to prevent the model from including unnecessary text in the response.
@@ -140,22 +147,12 @@ Use the `Inject Prefix` options in the setting to set the prefix. You can add a
![inject tag](docs/inject-quote.gif)
+
+
### OpenAI Examples
[See here for example usages](https://beta.openai.com/examples).
-## 📝 Table of Contents
-
-- [About](#about)
-- [Examples with GIFs](#examples)
-- [Getting Started](#getting_started)
-- [Deployment](#deployment)
-- [Usage](#usage)
-- [Built Using](#built_using)
-- [TODO](../TODO.md)
-- [Contributing](../CONTRIBUTING.md)
-- [Authors](#authors)
-- [Acknowledgments](#acknowledgement)
## About
diff --git a/docs/follow.png b/docs/follow.png
new file mode 100644
index 0000000..b14092a
Binary files /dev/null and b/docs/follow.png differ
diff --git a/docs/multi-select.gif b/docs/multi-select.gif
new file mode 100644
index 0000000..b95ff09
Binary files /dev/null and b/docs/multi-select.gif differ
diff --git a/package-lock.json b/package-lock.json
index 239f5a4..8f0d6fe 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,12 +14,13 @@
"clsx": "^1.2.1",
"exponential-backoff": "^3.1.0",
"fuse.js": "^6.6.2",
- "openai": "^3.0.0",
+ "openai": "^3.2.1",
"postcss": "^8.4.20",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.2.4",
- "toml": "^3.0.0"
+ "toml": "^3.0.0",
+ "use-immer": "^0.8.1"
},
"devDependencies": {
"@ladle/react": "^2.4.5",
@@ -4049,6 +4050,16 @@
"node": ">= 4"
}
},
+ "node_modules/immer": {
+ "version": "9.0.19",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.19.tgz",
+ "integrity": "sha512-eY+Y0qcsB4TZKwgQzLaE/lqYMlKhv5J9dyd2RhhtGhNo2njPXDqU9XPfcNfa3MIDsdtZt5KlkIsirlo4dHsWdQ==",
+ "peer": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -10753,6 +10764,15 @@
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
"dev": true
},
+ "node_modules/use-immer": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/use-immer/-/use-immer-0.8.1.tgz",
+ "integrity": "sha512-OfTFf1pL+ICjjcLPn9+ZnaJO/Yg4MBzYZtACEe2mZ/W2A5col28PNUnwowOAaBuOogACOK/37TU17KgsIhUpOw==",
+ "peerDependencies": {
+ "immer": ">=2.0.0",
+ "react": "^16.8.0 || ^17.0.1 || ^18.0.0"
+ }
+ },
"node_modules/util": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
@@ -13984,6 +14004,12 @@
"integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
"dev": true
},
+ "immer": {
+ "version": "9.0.19",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.19.tgz",
+ "integrity": "sha512-eY+Y0qcsB4TZKwgQzLaE/lqYMlKhv5J9dyd2RhhtGhNo2njPXDqU9XPfcNfa3MIDsdtZt5KlkIsirlo4dHsWdQ==",
+ "peer": true
+ },
"import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -18686,6 +18712,12 @@
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
"dev": true
},
+ "use-immer": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/use-immer/-/use-immer-0.8.1.tgz",
+ "integrity": "sha512-OfTFf1pL+ICjjcLPn9+ZnaJO/Yg4MBzYZtACEe2mZ/W2A5col28PNUnwowOAaBuOogACOK/37TU17KgsIhUpOw==",
+ "requires": {}
+ },
"util": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
diff --git a/package.json b/package.json
index 60aaecf..4aed54a 100644
--- a/package.json
+++ b/package.json
@@ -34,12 +34,14 @@
"clsx": "^1.2.1",
"exponential-backoff": "^3.1.0",
"fuse.js": "^6.6.2",
+ "immer": "^9.0.19",
"openai": "^3.2.1",
"postcss": "^8.4.20",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.2.4",
- "toml": "^3.0.0"
+ "toml": "^3.0.0",
+ "use-immer": "^0.8.1"
},
"logseq": {
"name": "logseq-plugin-gpt3-openai",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4da7c37..1e696fd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -19,6 +19,7 @@ specifiers:
conventional-changelog-conventionalcommits: ^5.0.0
exponential-backoff: ^3.1.0
fuse.js: ^6.6.2
+ immer: ^9.0.19
openai: ^3.2.1
postcss: ^8.4.20
react: ^18.2.0
@@ -27,6 +28,7 @@ specifiers:
tailwindcss: ^3.2.4
toml: ^3.0.0
typescript: ^4.5.4
+ use-immer: ^0.8.1
vite: ^4.0.4
vite-plugin-logseq: ^1.1.2
@@ -37,12 +39,14 @@ dependencies:
clsx: 1.2.1
exponential-backoff: 3.1.0
fuse.js: 6.6.2
+ immer: 9.0.19
openai: 3.2.1
postcss: 8.4.20
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
tailwindcss: 3.2.4
toml: 3.0.0
+ use-immer: 0.8.1_immer@9.0.19+react@18.2.0
devDependencies:
'@ladle/react': 2.4.5_biqbaboplfbrettd7655fr4n2y
@@ -3589,6 +3593,10 @@ packages:
engines: {node: '>= 4'}
dev: true
+ /immer/9.0.19:
+ resolution: {integrity: sha512-eY+Y0qcsB4TZKwgQzLaE/lqYMlKhv5J9dyd2RhhtGhNo2njPXDqU9XPfcNfa3MIDsdtZt5KlkIsirlo4dHsWdQ==}
+ dev: false
+
/import-fresh/3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}
@@ -5503,6 +5511,16 @@ packages:
resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==}
dev: true
+ /use-immer/0.8.1_immer@9.0.19+react@18.2.0:
+ resolution: {integrity: sha512-OfTFf1pL+ICjjcLPn9+ZnaJO/Yg4MBzYZtACEe2mZ/W2A5col28PNUnwowOAaBuOogACOK/37TU17KgsIhUpOw==}
+ peerDependencies:
+ immer: '>=2.0.0'
+ react: ^16.8.0 || ^17.0.1 || ^18.0.0
+ dependencies:
+ immer: 9.0.19
+ react: 18.2.0
+ dev: false
+
/util-deprecate/1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
diff --git a/src/main.tsx b/src/main.tsx
index 14720ab..a9db4d4 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -8,6 +8,7 @@ import { loadUserCommands, loadBuiltInCommands } from "./lib/prompts";
import { getOpenaiSettings, settingsSchema } from "./lib/settings";
import { runDalleBlock, runGptBlock, runGptPage } from "./lib/rawCommands";
import { BlockEntity } from "@logseq/libs/dist/LSPlugin.user";
+import { useImmer } from 'use-immer';
logseq.useSettingsSchema(settingsSchema);
@@ -35,10 +36,34 @@ async function main() {
logseq.ready(main).catch(console.error);
+type singleBlockSelected = {
+ type: "singleBlockSelected";
+ block: BlockEntity;
+};
+
+type multipleBlocksSelected = {
+ type: "multipleBlocksSelected";
+ blocks: BlockEntity[];
+};
+
+type noBlockSelected = {
+ type: "noBlockSelected";
+};
+
+type AppState = {
+ selection: (singleBlockSelected | multipleBlocksSelected | noBlockSelected);
+}
+
+const defaultAppState: AppState = {
+ selection: {
+ type: "noBlockSelected",
+ },
+};
+
const LogseqApp = () => {
const [builtInCommands, setBuiltInCommands] = useState([]);
const [userCommands, setUserCommands] = useState([]);
- const [activeBlock, setActiveBlock] = useState(null);
+ const [appState, updateAppState] = useImmer(defaultAppState);
const openUI = async () => {
const reloadedUserCommands = await loadUserCommands();
@@ -76,15 +101,30 @@ const LogseqApp = () => {
const activeText = await logseq.Editor.getEditingCursorPosition();
const currentBlock = await logseq.Editor.getCurrentBlock();
const currentPage = await logseq.Editor.getCurrentPage();
-
- if (!activeText && !currentPage) {
+ const selectedBlocks = await logseq.Editor.getSelectedBlocks();
+ if (selectedBlocks && selectedBlocks.length > 0) {
+ updateAppState(draft => {
+ draft.selection = {
+ type: "multipleBlocksSelected",
+ blocks: selectedBlocks,
+ };
+ });
+ } else if (!activeText && !currentPage) {
logseq.App.showMsg("Put cursor in block or navigate to specific page to use keyboard shortcut", "warning");
- return;
- }
- if (activeText && currentBlock){
- setActiveBlock(currentBlock);
+ return;
+ } else if (activeText && currentBlock) {
+ updateAppState(draft => {
+ draft.selection = {
+ type: "singleBlockSelected",
+ block: currentBlock,
+ };
+ });
} else {
- setActiveBlock(null);
+ updateAppState(draft => {
+ draft.selection = {
+ type: "noBlockSelected",
+ };
+ });
}
openUI();
}
@@ -96,7 +136,12 @@ const LogseqApp = () => {
logseq.Editor.registerBlockContextMenuItem("gpt", async (b) => {
const block = await logseq.Editor.getBlock(b.uuid);
if (block) {
- setActiveBlock(block);
+ updateAppState(draft => {
+ draft.selection = {
+ type: "singleBlockSelected",
+ block: block,
+ };
+ });
openUI();
}
});
@@ -104,7 +149,12 @@ const LogseqApp = () => {
logseq.Editor.registerSlashCommand("gpt", async (b) => {
const block = await logseq.Editor.getBlock(b.uuid);
if (block) {
- setActiveBlock(block);
+ updateAppState(draft => {
+ draft.selection = {
+ type: "singleBlockSelected",
+ block: block,
+ };
+ });
openUI();
}
});
@@ -127,7 +177,15 @@ const LogseqApp = () => {
const allCommands = [...builtInCommands, ...userCommands];
const handleCommand = async (command: Command): Promise => {
- const inputText = activeBlock?.content || "";
+ let inputText;
+ if (appState.selection.type === "singleBlockSelected") {
+ inputText = appState.selection.block.content;
+ } else if (appState.selection.type === "multipleBlocksSelected") {
+ inputText = appState.selection.blocks.map(b => b.content).join("\n");
+ } else {
+ inputText = "";
+ }
+
const openAISettings = getOpenaiSettings();
const response = await openAI(command.prompt + inputText, openAISettings);
if (response) {
@@ -142,42 +200,61 @@ const LogseqApp = () => {
if (getOpenaiSettings().injectPrefix) {
result = getOpenaiSettings().injectPrefix + result;
}
- if (activeBlock) {
- if (activeBlock.content.length > 0) {
- logseq.Editor.insertBlock(activeBlock.uuid, result, {
+ if (appState.selection.type === "singleBlockSelected") {
+ if (appState.selection.block.content.length > 0) {
+ logseq.Editor.insertBlock(appState.selection.block.uuid, result, {
sibling: false,
});
} else {
- logseq.Editor.updateBlock(activeBlock.uuid, result);
+ logseq.Editor.updateBlock(appState.selection.block.uuid, result);
}
- } else {
+ } else if (appState.selection.type === "multipleBlocksSelected") {
+ const lastBlock = appState.selection.blocks[appState.selection.blocks.length - 1];
+ logseq.Editor.insertBlock(lastBlock.uuid, result, {
+ sibling: true,
+ });
+ } else if (appState.selection.type === "noBlockSelected"){
const currentPage = await logseq.Editor.getCurrentPage();
if (currentPage) {
logseq.Editor.appendBlockInPage(currentPage.uuid, result);
}
+ } else {
+ console.error("Unknown selection type");
}
+
logseq.hideMainUI({ restoreEditingCursor: true });
};
const onReplace = async (text: string) => {
let result = text;
- // if (getOpenaiSettings().injectPrefix) {
- // result = getOpenaiSettings().injectPrefix + result;
- // }
- if (activeBlock) {
- logseq.Editor.updateBlock(activeBlock.uuid, result);
- } else {
+ if (getOpenaiSettings().injectPrefix) {
+ result = getOpenaiSettings().injectPrefix + result;
+ }
+
+ if (appState.selection.type === "singleBlockSelected") {
+ logseq.Editor.updateBlock(appState.selection.block.uuid, result);
+ } else if (appState.selection.type === "multipleBlocksSelected") {
+ const firstBlock = appState.selection.blocks[0];
+ logseq.Editor.updateBlock(firstBlock.uuid, result);
+ if (appState.selection.blocks.length > 1) {
+ const remainingBlocks = appState.selection.blocks.slice(1);
+ const blocksToRemove = remainingBlocks.map(b => logseq.Editor.removeBlock(b.uuid));
+ await Promise.all(blocksToRemove);
+ }
+ } else if (appState.selection.type === "noBlockSelected"){
const currentPage = await logseq.Editor.getCurrentPage();
if (currentPage) {
- logseq.Editor.appendBlockInPage(currentPage, result);
+ logseq.Editor.appendBlockInPage(currentPage.uuid, result);
}
+ } else {
+ console.error("Unknown selection type");
}
+
logseq.hideMainUI({ restoreEditingCursor: true });
};
const onClose = () => {
logseq.hideMainUI({ restoreEditingCursor: true });
- setActiveBlock(null);
};
return (