add passages

This commit is contained in:
celeste 2023-07-03 02:34:18 -04:00
parent 6aed1444e4
commit f3aed396f0
7 changed files with 256 additions and 9 deletions

View file

@ -14,6 +14,10 @@ export interface LoomSettings {
ocpApiKey: string;
ocpUrl: string;
passageFolder: string;
defaultPassageSeparator: string;
defaultPassageFrontmatter: string;
provider: Provider;
model: string;
maxTokens: number;

87
main.ts
View file

@ -1,4 +1,10 @@
import { LoomView, LoomSiblingsView, LoomEditorPlugin, loomEditorPluginSpec } from './views';
import {
LoomView,
LoomSiblingsView,
LoomEditorPlugin,
loomEditorPluginSpec,
MakePromptFromPassagesModal,
} from './views';
import { PROVIDERS, Provider, LoomSettings, Node, NoteState } from './common';
import {
@ -23,6 +29,7 @@ import p50k from "gpt-tokenizer/esm/model/text-davinci-003";
import r50k from "gpt-tokenizer/esm/model/davinci";
import * as fs from "fs";
import { toRoman } from "roman-numerals";
import { v4 as uuidv4 } from "uuid";
const untildify = require("untildify") as any;
@ -48,6 +55,10 @@ const DEFAULT_SETTINGS: LoomSettings = {
ocpApiKey: "",
ocpUrl: "",
passageFolder: "",
defaultPassageSeparator: "\\n\\n---\\n\\n",
defaultPassageFrontmatter: "%r:\\n",
provider: "ocp",
model: "code-davinci-002",
maxTokens: 60,
@ -494,6 +505,24 @@ export default class LoomPlugin extends Plugin {
}),
});
const getState = () => this.withFile((file) => this.state[file.path]);
const getSettings = () => this.settings;
this.addCommand({
id: "make-prompt-from-passages",
name: "Make prompt from passages",
callback: () => {
if (this.settings.passageFolder.trim() === "") {
new Notice("Please set the passage folder in settings");
return;
}
new MakePromptFromPassagesModal(
this.app,
getSettings,
).open();
}
});
this.addCommand({
id: "open-pane",
name: "Open Loom pane",
@ -518,9 +547,6 @@ export default class LoomPlugin extends Plugin {
callback: () => this.wftsar((file) => (this.state[file.path].hoisted = [])),
});
const getState = () => this.withFile((file) => this.state[file.path]);
const getSettings = () => this.settings;
this.registerView(
"loom",
(leaf) => new LoomView(leaf, getState, getSettings)
@ -921,6 +947,46 @@ export default class LoomPlugin extends Plugin {
)
);
this.registerEvent(
this.app.workspace.on(
// @ts-expect-error
"loom:make-prompt-from-passages",
(
passages: string[],
rawSeparator: string,
rawFrontmatter: string,
) => this.wftsar((file) => {
const separator = rawSeparator.replace(/\\n/g, "\n");
const frontmatter = (index: number) => rawFrontmatter
.replace(/%n/g, (index + 1).toString())
.replace(/%r/g, toRoman(index + 1))
.replace(/\\n/g, "\n");
const passageTexts = passages.map((passage, index) => {
return Object.entries(this.state[passage].nodes)
.filter(([, node]) => node.parentId === null)
.map(([, node]) => frontmatter(index) + node.text);
});
const text = `${passageTexts.join(separator)}${separator}${frontmatter(passages.length)}`;
const state = this.state[file.path];
const currentNode = state.nodes[state.current];
let id;
if (currentNode.text === "" && currentNode.parentId === null) {
this.state[file.path].nodes[state.current].text = text;
id = state.current;
} else {
const [newId, newNode] = this.newNode(text, null);
this.state[file.path].nodes[newId] = newNode;
id = newId;
}
this.app.workspace.trigger("loom:switch-to", id);
})
)
);
const onFileOpen = (file: TFile) => {
if (file.extension !== "md") return;
@ -1396,6 +1462,19 @@ class LoomSettingTab extends PluginSettingTab {
idSetting("OpenAI organization ID", "openaiOrganization");
apiKeySetting("Azure", "azureApiKey")
idSetting("Azure resource endpoint", "azureEndpoint");
new Setting(containerEl)
.setName("Passage folder location")
.setDesc("Passages can be quickly combined into a multipart prompt")
.addText((text) =>
text.setValue(this.plugin.settings.passageFolder).onChange(async (value) => {
this.plugin.settings.passageFolder = value;
await this.plugin.save();
})
);
idSetting("Default passage separator", "defaultPassageSeparator");
idSetting("Default passage frontmatter", "defaultPassageFrontmatter");
idSetting("Model", "model");
intSetting("Length (in tokens)", "maxTokens");

View file

@ -1,7 +1,7 @@
{
"id": "loom",
"name": "Loom",
"version": "1.13.1",
"version": "1.14.0",
"minAppVersion": "0.15.0",
"description": "Loom in Obsidian",
"author": "celeste",

16
package-lock.json generated
View file

@ -1,21 +1,23 @@
{
"name": "obsidian-loom",
"version": "1.12.2",
"version": "1.13.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "obsidian-loom",
"version": "1.12.2",
"version": "1.13.1",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.9.3",
"@types/lodash": "^4.14.191",
"@types/roman-numerals": "^0.3.0",
"azure-openai": "^0.9.4",
"cohere-ai": "^6.1.0",
"gpt-tokenizer": "^2.1.1",
"openai": "^3.2.0",
"roman-numerals": "^0.3.2",
"untildify": "^4.0.0",
"uuid": "^9.0.0"
},
@ -195,6 +197,11 @@
"integrity": "sha512-vzLe5NaNMjIE3mcddFVGlAXN1LEWueUsMsOJWaT6wWMJGyljHAWHznqfnKUQWGzu7TLPrGvWdNAsvQYW+C0xtw==",
"dev": true
},
"node_modules/@types/roman-numerals": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@types/roman-numerals/-/roman-numerals-0.3.0.tgz",
"integrity": "sha512-sfO4vwDEH5hpm9GHQMkcJaRUWgcrlgJn9YLQv+6l9JBQk+Xe4nx9zfbHgS+3x1sMwZUnFc0sgTY0eAHr9Foqhw=="
},
"node_modules/@types/semver": {
"version": "7.3.13",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz",
@ -1655,6 +1662,11 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/roman-numerals": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/roman-numerals/-/roman-numerals-0.3.2.tgz",
"integrity": "sha512-aTmaKtxczT3K4ZhtJDauOZ6LY3e1xNDf2SBn7aRY/ErAtCCuVYuwNQbPuh+SWTjGmbg9p5Nlt8sJbQNkto8uyg=="
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-loom",
"version": "1.13.1",
"version": "1.14.0",
"description": "Loom in Obsidian",
"main": "main.js",
"scripts": {
@ -27,10 +27,12 @@
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.9.3",
"@types/lodash": "^4.14.191",
"@types/roman-numerals": "^0.3.0",
"azure-openai": "^0.9.4",
"cohere-ai": "^6.1.0",
"gpt-tokenizer": "^2.1.1",
"openai": "^3.2.0",
"roman-numerals": "^0.3.2",
"untildify": "^4.0.0",
"uuid": "^9.0.0"
}

View file

@ -252,6 +252,37 @@ body {
border-right: 2px dashed #333;
}
/* "make prompt from passages" modal */
.loom__passage-list {
max-height: 10em;
overflow-y: auto;
}
.loom__passage {
display: flex;
align-items: center;
padding: 0.35em 0 0.35em 0.6em;
}
.loom__passage:hover {
background-color: var(--nav-item-background-active);
}
.loom__selected-passages-title {
font-weight: bold;
margin: 1em 0 0.5em;
}
.loom__selected-passage-list {
margin-bottom: 0.75em;
}
.loom__no-passages-selected {
color: var(--text-faint);
font-size: var(--font-ui-small);
padding: 0.35em 0 0.35em 0.6em;
}
/* hidden -- must be at end */
.hidden {

121
views.ts
View file

@ -1,5 +1,5 @@
import { LoomSettings, NoteState } from "./common";
import { ItemView, Menu, WorkspaceLeaf, setIcon } from "obsidian";
import { App, ItemView, Menu, Modal, Setting, WorkspaceLeaf, setIcon } from "obsidian";
import { Range } from "@codemirror/state";
import {
Decoration,
@ -636,3 +636,122 @@ export const loomEditorPluginSpec: PluginSpec<LoomEditorPlugin> = {
},
},
};
export class MakePromptFromPassagesModal extends Modal {
getSettings: () => LoomSettings;
constructor(app: App, getSettings: () => LoomSettings) {
super(app);
this.getSettings = getSettings;
}
onOpen() {
this.contentEl.createDiv({
cls: "modal-title",
text: "Make prompt from passages",
});
const pathPrefix = this.getSettings().passageFolder.trim().replace(/\/?$/, "/");
const passages = this.app.vault.getFiles().filter((file) =>
file.path.startsWith(pathPrefix) && file.extension === "md"
).sort((a, b) => b.stat.mtime - a.stat.mtime);
let selectedPassages: string[] = [];
const unselectedContainer = this.contentEl.createDiv({
cls: "loom__passage-list",
});
this.contentEl.createDiv({
cls: "loom__selected-passages-title",
text: "Selected passages",
});
const selectedContainer = this.contentEl.createDiv({
cls: "loom__passage-list loom__selected-passage-list",
});
let button: HTMLElement;
const cleanName = (name: string) => name.slice(pathPrefix.length, -3);
const renderPassageList = () => {
unselectedContainer.empty();
selectedContainer.empty();
const unselectedPassages = passages.filter(
(passage) => !selectedPassages.includes(passage.path)
);
for (const passage of unselectedPassages) {
const passageContainer = unselectedContainer.createDiv({
cls: "tree-item-self loom__passage"
});
passageContainer.createSpan({
cls: "tree-item-inner",
text: cleanName(passage.path),
});
passageContainer.addEventListener("click", () => {
selectedPassages.push(passage.path)
renderPassageList();
});
}
if (selectedPassages.length === 0) {
selectedContainer.createDiv({
cls: "loom__no-passages-selected",
text: "No passages selected.",
});
}
for (const passage of selectedPassages) {
const passageContainer = selectedContainer.createDiv({
cls: "tree-item-self loom__passage",
});
passageContainer.createSpan({
cls: "tree-item-inner",
text: cleanName(passage),
});
passageContainer.addEventListener("click", () => {
selectedPassages = selectedPassages.filter((p) => p !== passage);
renderPassageList();
});
}
};
let separator = this.getSettings().defaultPassageSeparator;
let passageFrontmatter = this.getSettings().defaultPassageFrontmatter;
new Setting(this.contentEl)
.setName("Separator")
.setDesc("Use \\n to denote a newline.")
.addText((text) =>
text.setValue(separator).onChange((value) => (separator = value)));
new Setting(this.contentEl)
.setName("Passage frontmatter")
.setDesc("This will be added before each passage and at the end. %n: 1, 2, 3..., %r: I, II, III...")
.addText((text) =>
text.setValue(passageFrontmatter).onChange((value) => (passageFrontmatter = value)));
const buttonContainer = this.contentEl.createDiv({
cls: "modal-button-container",
});
button = buttonContainer.createEl("button", {
cls: "mod-cta",
text: "Submit",
});
button.addEventListener("click", () => {
if (selectedPassages.length === 0) return;
this.app.workspace.trigger(
"loom:make-prompt-from-passages",
selectedPassages,
separator,
passageFrontmatter,
);
this.close();
});
renderPassageList();
}
onClose() {
this.contentEl.empty();
}
}