mirror of
https://github.com/cosmicoptima/loom.git
synced 2026-07-22 07:40:25 +00:00
add passages
This commit is contained in:
parent
6aed1444e4
commit
f3aed396f0
7 changed files with 256 additions and 9 deletions
|
|
@ -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
87
main.ts
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
16
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
31
styles.css
31
styles.css
|
|
@ -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
121
views.ts
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue