mirror of
https://github.com/cosmicoptima/loom.git
synced 2026-07-22 07:40:25 +00:00
The Rewrite
This commit is contained in:
parent
98e9b03c7a
commit
618b563949
7 changed files with 1241 additions and 1569 deletions
45
common.ts
Normal file
45
common.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
export const PROVIDERS = ["cohere", "textsynth", "ocp", "openai", "openai-chat", "azure", "azure-chat"];
|
||||
export type Provider = (typeof PROVIDERS)[number];
|
||||
|
||||
export interface LoomSettings {
|
||||
openaiApiKey: string;
|
||||
openaiOrganization: string;
|
||||
|
||||
cohereApiKey: string;
|
||||
textsynthApiKey: string;
|
||||
|
||||
azureApiKey: string;
|
||||
azureEndpoint: string;
|
||||
|
||||
ocpApiKey: string;
|
||||
ocpUrl: string;
|
||||
|
||||
provider: Provider;
|
||||
model: string;
|
||||
maxTokens: number;
|
||||
temperature: number;
|
||||
topP: number;
|
||||
frequencyPenalty: number;
|
||||
presencePenalty: number;
|
||||
n: number;
|
||||
|
||||
showSettings: boolean;
|
||||
showNodeBorders: boolean;
|
||||
showExport: boolean;
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
text: string;
|
||||
parentId: string | null;
|
||||
collapsed: boolean;
|
||||
unread: boolean;
|
||||
bookmarked: boolean;
|
||||
lastVisited?: number;
|
||||
}
|
||||
|
||||
export interface NoteState {
|
||||
current: string;
|
||||
hoisted: string[];
|
||||
nodes: Record<string, Node>;
|
||||
generating: string | null;
|
||||
}
|
||||
29
package-lock.json
generated
29
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "obsidian-loom",
|
||||
"version": "1.10.2",
|
||||
"version": "1.12.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-loom",
|
||||
"version": "1.10.2",
|
||||
"version": "1.12.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.2.0",
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
"@types/lodash": "^4.14.191",
|
||||
"azure-openai": "^0.9.4",
|
||||
"cohere-ai": "^6.1.0",
|
||||
"gpt3-tokenizer": "^1.1.5",
|
||||
"gpt-tokenizer": "^2.1.1",
|
||||
"openai": "^3.2.0",
|
||||
"untildify": "^4.0.0",
|
||||
"uuid": "^9.0.0"
|
||||
|
|
@ -471,11 +471,6 @@
|
|||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/array-keyed-map": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/array-keyed-map/-/array-keyed-map-2.1.3.tgz",
|
||||
"integrity": "sha512-JIUwuFakO+jHjxyp4YgSiKXSZeC0U+R1jR94bXWBcVlFRBycqXlb+kH9JHxBGcxnVuSqx5bnn0Qz9xtSeKOjiA=="
|
||||
},
|
||||
"node_modules/array-union": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
||||
|
|
@ -1154,15 +1149,12 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gpt3-tokenizer": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/gpt3-tokenizer/-/gpt3-tokenizer-1.1.5.tgz",
|
||||
"integrity": "sha512-O9iCL8MqGR0Oe9wTh0YftzIbysypNQmS5a5JG3cB3M4LMYjlAVvNnf8LUzVY9MrI7tj+YLY356uHtO2lLX2HpA==",
|
||||
"node_modules/gpt-tokenizer": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/gpt-tokenizer/-/gpt-tokenizer-2.1.1.tgz",
|
||||
"integrity": "sha512-WlX+vj6aPaZ71U6Bf18fem+5k58zlgh2a4nbc7KHy6aGVIyq3nCh709b/8momu34sV/5t/SpzWi8LayWD9uyDw==",
|
||||
"dependencies": {
|
||||
"array-keyed-map": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"rfc4648": "^1.5.2"
|
||||
}
|
||||
},
|
||||
"node_modules/grapheme-splitter": {
|
||||
|
|
@ -1643,6 +1635,11 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rfc4648": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.2.tgz",
|
||||
"integrity": "sha512-tLOizhR6YGovrEBLatX1sdcuhoSCXddw3mqNVAcKxGJ+J0hFeJ+SjeWCv5UPA/WU3YzWPPuCVYgXBKZUPGpKtg=="
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
"@types/lodash": "^4.14.191",
|
||||
"azure-openai": "^0.9.4",
|
||||
"cohere-ai": "^6.1.0",
|
||||
"gpt3-tokenizer": "^1.1.5",
|
||||
"gpt-tokenizer": "^2.1.1",
|
||||
"openai": "^3.2.0",
|
||||
"untildify": "^4.0.0",
|
||||
"uuid": "^9.0.0"
|
||||
|
|
|
|||
340
styles.css
340
styles.css
|
|
@ -36,69 +36,137 @@ body {
|
|||
--ancestor-editor-background: #111;
|
||||
}
|
||||
|
||||
.loom {
|
||||
/* all views */
|
||||
|
||||
.loom__view {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.loom-buttons {
|
||||
/* nav buttons */
|
||||
|
||||
.loom__nav-buttons {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.loom-zport {
|
||||
/* alt export field */
|
||||
|
||||
.loom__alt-export-field {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5em;
|
||||
margin-bottom: 0.8em;
|
||||
}
|
||||
|
||||
.loom-zport input {
|
||||
.loom__alt-export-field input {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.loom-zport button {
|
||||
.loom__alt-export-field button {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.loom-vspacer {
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
/* settings */
|
||||
|
||||
.loom-settings {
|
||||
.loom__settings {
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loom-setting {
|
||||
.loom__setting {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.loom-setting label {
|
||||
.loom__setting label {
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.loom-setting input {
|
||||
.loom__setting input {
|
||||
margin-bottom: 0.6em;
|
||||
}
|
||||
.loom__setting select {
|
||||
margin-bottom: 0.6em;
|
||||
}
|
||||
|
||||
.loom-setting select {
|
||||
margin-bottom: 0.6em;
|
||||
/* all tree headers */
|
||||
|
||||
.loom__tree-header {
|
||||
padding: 0.35em;
|
||||
}
|
||||
|
||||
.loom-collapse {
|
||||
.loom__tree-header-text {
|
||||
color: var(--text-normal);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
/* bookmarks header */
|
||||
|
||||
.loom__bookmarks {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.loom__bookmarks-count {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
/* nodes */
|
||||
|
||||
.loom__node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
.loom__node.is-active {
|
||||
background-color: var(--nav-item-background-active);
|
||||
}
|
||||
|
||||
.loom__collapse-button {
|
||||
margin-left: 0.6em;
|
||||
}
|
||||
|
||||
.loom-collapse.is-collapsed {
|
||||
.loom__collapse-button.loom__is-collapsed {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.loom-node-footer {
|
||||
.loom__node-bookmark-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 0.3em;
|
||||
margin-right: -0.3em;
|
||||
}
|
||||
|
||||
.loom__node-unread {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.loom__node-unread-indicator {
|
||||
width: 0.3em;
|
||||
height: 0.3em;
|
||||
border-radius: 50%;
|
||||
background-color: var(--nav-item-color);
|
||||
margin-left: 0.6em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.loom__node-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0.35em 0 0.35em 0.6em;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.loom__node-buttons {
|
||||
display: none;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
.loom__node:hover .loom__node-buttons {
|
||||
display: inline-flex !important;
|
||||
}
|
||||
|
||||
.loom__node-button {
|
||||
display: inline-flex !important;
|
||||
margin-left: 0.1em;
|
||||
--icon-size: 1.1rem;
|
||||
}
|
||||
|
||||
.loom__node-footer {
|
||||
color: var(--text-faint);
|
||||
font-size: 0.9em;
|
||||
margin-left: calc(0.85em + 1px);
|
||||
|
|
@ -112,135 +180,10 @@ body {
|
|||
border-left: 1px solid var(--color-base-35);
|
||||
}
|
||||
|
||||
.loom-hoist-button:hover {
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.loom-node-footer-text {
|
||||
.loom__node-footer-text {
|
||||
margin-left: 0.35em;
|
||||
}
|
||||
|
||||
.loom-children {
|
||||
margin-left: 0.85em;
|
||||
padding-left: 0.6em;
|
||||
border-left: 1px solid var(--color-base-35);
|
||||
}
|
||||
|
||||
.loom-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.loom-node.is-active {
|
||||
background-color: var(--nav-item-background-active);
|
||||
}
|
||||
|
||||
.loom-node-red {
|
||||
background-color: rgba(var(--color-red-rgb), 0.2) !important;
|
||||
}
|
||||
.loom-node-red.is-active {
|
||||
background-color: rgba(var(--color-red-rgb), 0.3) !important;
|
||||
}
|
||||
|
||||
.loom-node-orange {
|
||||
background-color: rgba(var(--color-orange-rgb), 0.2) !important;
|
||||
}
|
||||
.loom-node-orange.is-active {
|
||||
background-color: rgba(var(--color-orange-rgb), 0.3) !important;
|
||||
}
|
||||
|
||||
.loom-node-yellow {
|
||||
background-color: rgba(var(--color-yellow-rgb), 0.2) !important;
|
||||
}
|
||||
.loom-node-yellow.is-active {
|
||||
background-color: rgba(var(--color-yellow-rgb), 0.3) !important;
|
||||
}
|
||||
|
||||
.loom-node-green {
|
||||
background-color: rgba(var(--color-green-rgb), 0.2) !important;
|
||||
}
|
||||
.loom-node-green.is-active {
|
||||
background-color: rgba(var(--color-green-rgb), 0.3) !important;
|
||||
}
|
||||
|
||||
.loom-node-blue {
|
||||
background-color: rgba(var(--color-blue-rgb), 0.2) !important;
|
||||
}
|
||||
.loom-node-blue.is-active {
|
||||
background-color: rgba(var(--color-blue-rgb), 0.3) !important;
|
||||
}
|
||||
|
||||
.loom-node-purple {
|
||||
background-color: rgba(var(--color-purple-rgb), 0.2) !important;
|
||||
}
|
||||
.loom-node-purple.is-active {
|
||||
background-color: rgba(var(--color-purple-rgb), 0.3) !important;
|
||||
}
|
||||
|
||||
.loom-node-inner {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0.35em 0 0.35em 0.6em;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.loom-node-bookmark {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 0.3em;
|
||||
margin-right: -0.3em;
|
||||
}
|
||||
|
||||
.loom-node-unread {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.loom-node-unread-indicator {
|
||||
width: 0.3em;
|
||||
height: 0.3em;
|
||||
border-radius: 50%;
|
||||
background-color: var(--nav-item-color);
|
||||
margin-left: 0.6em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.loom-icon {
|
||||
display: inline-flex !important;
|
||||
margin-left: 0.1em;
|
||||
--icon-size: 1.1rem;
|
||||
}
|
||||
|
||||
.loom-icons:not(.is-mobile) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loom-node:hover .loom-icons {
|
||||
display: inline-flex !important;
|
||||
}
|
||||
|
||||
.loom-spacer {
|
||||
padding-right: 0.6em;
|
||||
}
|
||||
|
||||
.loom-section {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.loom-section-header {
|
||||
padding: 0.35em;
|
||||
}
|
||||
|
||||
.loom-section-header-inner {
|
||||
color: var(--text-normal);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
.loom-section-count {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
|
|
@ -249,58 +192,27 @@ body {
|
|||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.rotating svg {
|
||||
.loom__node-generating-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.loom__node-generating-icon svg {
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
|
||||
.rotating {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.obsidian-button {
|
||||
-webkit-app-region: no-drag;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.loom__node-show-more:hover {
|
||||
color: var(--text-normal);
|
||||
font-size: var(--font-ui-small);
|
||||
border-radius: var(--button-radius);
|
||||
border: 0;
|
||||
padding: var(--size-4-1) var(--size-4-3);
|
||||
height: var(--input-height);
|
||||
font-weight: var(--input-font-weight);
|
||||
cursor: var(--cursor);
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
background-color: var(--interactive-normal);
|
||||
box-shadow: var(--input-shadow);
|
||||
}
|
||||
|
||||
.obsidian-button:hover {
|
||||
background-color: var(--interactive-hover);
|
||||
.loom__node-children {
|
||||
margin-left: 0.85em;
|
||||
padding-left: 0.6em;
|
||||
border-left: 1px solid var(--color-base-35);
|
||||
}
|
||||
|
||||
.loom-import-button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
/* nodes -- siblings view */
|
||||
|
||||
.loom-bct {
|
||||
color: var(--ancestor-editor-color);
|
||||
}
|
||||
|
||||
.theme-dark .loom-bct-border {
|
||||
border-right: 2px dashed #333;
|
||||
}
|
||||
|
||||
.loom-bct-hover {
|
||||
background-color: var(--ancestor-editor-background);
|
||||
}
|
||||
|
||||
.loom-sibling {
|
||||
.loom__sibling {
|
||||
color: var(--nav-item-color);
|
||||
font-size: 0.9em;
|
||||
|
||||
|
|
@ -309,21 +221,39 @@ body {
|
|||
padding: 0.8em;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.loom-sibling:hover {
|
||||
.loom__sibling:hover {
|
||||
color: var(--nav-item-color-active);
|
||||
}
|
||||
|
||||
.loom-sibling.is-active {
|
||||
.loom__sibling.is-active {
|
||||
background-color: var(--nav-item-background-active);
|
||||
color: var(--nav-item-color-active);
|
||||
}
|
||||
|
||||
.loom-sibling-divider {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.loom-sibling-ellipsis {
|
||||
.loom__sibling-ellipsis {
|
||||
color: var(--text-faint);
|
||||
margin-right: 0.3em;
|
||||
}
|
||||
|
||||
.loom__sibling-separator {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* nodes -- editor */
|
||||
|
||||
.loom__editor-node {
|
||||
color: var(--ancestor-editor-color);
|
||||
}
|
||||
|
||||
.loom__editor-node-hover {
|
||||
background-color: var(--ancestor-editor-background);
|
||||
}
|
||||
|
||||
.theme-dark .loom__editor-node-border {
|
||||
border-right: 2px dashed #333;
|
||||
}
|
||||
|
||||
/* hidden -- must be at end */
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@
|
|||
"ES5",
|
||||
"ES6",
|
||||
"ES7"
|
||||
]
|
||||
],
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
|
|
|
|||
639
views.ts
Normal file
639
views.ts
Normal file
|
|
@ -0,0 +1,639 @@
|
|||
import { LoomSettings, NoteState } from "./common";
|
||||
import { ItemView, Menu, WorkspaceLeaf, setIcon } from "obsidian";
|
||||
import { Range } from "@codemirror/state";
|
||||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
PluginSpec,
|
||||
PluginValue,
|
||||
WidgetType,
|
||||
} from "@codemirror/view";
|
||||
const dialog = require("electron").remote.dialog;
|
||||
|
||||
export class LoomView extends ItemView {
|
||||
getNoteState: () => NoteState | null;
|
||||
getSettings: () => LoomSettings;
|
||||
|
||||
constructor(
|
||||
leaf: WorkspaceLeaf,
|
||||
getNoteState: () => NoteState | null,
|
||||
getSettings: () => LoomSettings
|
||||
) {
|
||||
super(leaf);
|
||||
|
||||
this.getNoteState = getNoteState;
|
||||
this.getSettings = getSettings;
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
const state = this.getNoteState();
|
||||
const settings = this.getSettings();
|
||||
|
||||
const scroll = this.containerEl.scrollTop;
|
||||
|
||||
this.containerEl.empty();
|
||||
this.containerEl.addClass("loom__view");
|
||||
|
||||
this.renderNavButtons(settings);
|
||||
const container = this.containerEl.createDiv({ cls: "outline" });
|
||||
if (settings.showExport) this.renderAltExportInterface(container);
|
||||
if (settings.showSettings) this.renderSettings(container, settings);
|
||||
|
||||
if (!state) {
|
||||
container.createDiv({ cls: "pane-empty", text: "No note selected." });
|
||||
return;
|
||||
}
|
||||
this.renderBookmarks(container, state);
|
||||
this.renderTree(container, state);
|
||||
|
||||
this.containerEl.scrollTop = scroll;
|
||||
}
|
||||
|
||||
renderNavButtons(settings: LoomSettings) {
|
||||
const navButtonsContainer = this.containerEl.createDiv({ cls: "nav-buttons-container loom__nav-buttons" });
|
||||
|
||||
// buttons to toggle 1) settings 2) node borders in the editor
|
||||
|
||||
const settingNavButton = (
|
||||
setting: string,
|
||||
value: boolean,
|
||||
icon: string,
|
||||
label: string
|
||||
) => {
|
||||
const button = navButtonsContainer.createDiv({
|
||||
cls: `clickable-icon nav-action-button${value ? " is-active" : ""}`,
|
||||
attr: { "aria-label": label },
|
||||
});
|
||||
setIcon(button, icon);
|
||||
button.addEventListener("click", () =>
|
||||
this.app.workspace.trigger("loom:set-setting", setting, !value)
|
||||
);
|
||||
};
|
||||
|
||||
settingNavButton(
|
||||
"showSettings",
|
||||
settings.showSettings,
|
||||
"settings",
|
||||
"Show settings"
|
||||
);
|
||||
settingNavButton(
|
||||
"showNodeBorders",
|
||||
settings.showNodeBorders,
|
||||
"separator-vertical",
|
||||
"Show node borders in the editor"
|
||||
);
|
||||
|
||||
// the import button
|
||||
|
||||
const importInput = navButtonsContainer.createEl("input", {
|
||||
cls: "hidden",
|
||||
attr: { type: "file", id: "loom__import-input" },
|
||||
});
|
||||
|
||||
const importNavButton = navButtonsContainer.createEl("label", {
|
||||
cls: "clickable-icon nav-action-button",
|
||||
attr: { "aria-label": "Import JSON", for: "loom__import-input" },
|
||||
});
|
||||
setIcon(importNavButton, "import");
|
||||
|
||||
importInput.addEventListener("change", () => {
|
||||
// @ts-expect-error
|
||||
const path = importInput.files?.[0].path;
|
||||
if (path) this.app.workspace.trigger("loom:import", path);
|
||||
});
|
||||
|
||||
// the export button
|
||||
|
||||
const exportNavButton = navButtonsContainer.createDiv({
|
||||
cls: `clickable-icon nav-action-button${settings.showExport ? " is-active" : ""}`,
|
||||
attr: { "aria-label": "Export to JSON" },
|
||||
});
|
||||
setIcon(exportNavButton, "download");
|
||||
|
||||
exportNavButton.addEventListener("click", (event) => {
|
||||
if (event.shiftKey) {
|
||||
this.app.workspace.trigger("loom:set-setting", "showExport", !settings.showExport);
|
||||
return;
|
||||
}
|
||||
dialog
|
||||
.showSaveDialog({ title: "Export to JSON", filters: [{ extensions: ["json"] }] })
|
||||
.then((result: any) => {
|
||||
if (result && result.filePath)
|
||||
this.app.workspace.trigger("loom:export", result.filePath);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderAltExportInterface(container: HTMLElement) {
|
||||
const exportContainer = container.createDiv({ cls: "loom__alt-export-field" });
|
||||
const exportInput = exportContainer.createEl("input", {
|
||||
attr: { type: "text", placeholder: "Path to export to" },
|
||||
});
|
||||
const exportButton = exportContainer.createEl("button", {});
|
||||
setIcon(exportButton, "download");
|
||||
|
||||
exportButton.addEventListener("click", () => {
|
||||
if (exportInput.value) this.app.workspace.trigger("loom:export", exportInput.value);
|
||||
});
|
||||
}
|
||||
|
||||
renderSettings(container: HTMLElement, settings: LoomSettings) {
|
||||
const settingsContainer = container.createDiv({ cls: "loom__settings" });
|
||||
|
||||
// provider dropdown
|
||||
|
||||
const providerContainer = settingsContainer.createDiv({ cls: "loom__setting" });
|
||||
providerContainer.createEl("label", { text: "Provider" });
|
||||
const providerDropdown = providerContainer.createEl("select");
|
||||
|
||||
const providers = [
|
||||
{ label: "OpenAI code-davinci-002 proxy", value: "ocp" },
|
||||
{ label: "Cohere", value: "cohere" },
|
||||
{ label: "TextSynth", value: "textsynth" },
|
||||
{ label: "OpenAI", value: "openai" },
|
||||
{ label: "OpenAI (Chat)", value: "openai-chat" },
|
||||
{ label: "Azure", value: "azure" },
|
||||
{ label: "Azure (Chat)", value: "azure-chat" },
|
||||
];
|
||||
providers.forEach((provider) =>
|
||||
providerDropdown.createEl("option", {
|
||||
text: provider.label,
|
||||
attr: {
|
||||
value: provider.value,
|
||||
selected: provider.value === settings.provider ? "selected" : null,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
providerDropdown.addEventListener("change", () =>
|
||||
this.app.workspace.trigger("loom:set-setting", "provider", providerDropdown.value)
|
||||
);
|
||||
|
||||
// other settings
|
||||
|
||||
const setting = (
|
||||
label: string,
|
||||
setting: string,
|
||||
value: string,
|
||||
type: "string" | "int" | "float"
|
||||
) => {
|
||||
const parsers = {
|
||||
"string": (value: string) => value,
|
||||
"int": (value: string) => parseInt(value),
|
||||
"float": (value: string) => parseFloat(value),
|
||||
};
|
||||
|
||||
const settingContainer = settingsContainer.createDiv({ cls: "loom__setting" });
|
||||
settingContainer.createEl("label", { text: label });
|
||||
const settingInput = settingContainer.createEl("input", {
|
||||
type: type === "string" ? "text" : "number", value
|
||||
});
|
||||
settingInput.addEventListener("blur", () =>
|
||||
this.app.workspace.trigger(
|
||||
"loom:set-setting", setting, parsers[type](settingInput.value)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
setting("Model", "model", settings.model, "string");
|
||||
setting("Length (in tokens)", "maxTokens", String(settings.maxTokens), "int");
|
||||
setting("Number of completions", "n", String(settings.n), "int");
|
||||
setting("Temperature", "temperature", String(settings.temperature), "float");
|
||||
setting("Top p", "topP", String(settings.topP), "float");
|
||||
setting("Frequency penalty", "frequencyPenalty", String(settings.frequencyPenalty), "float");
|
||||
setting("Presence penalty", "presencePenalty", String(settings.presencePenalty), "float");
|
||||
}
|
||||
|
||||
renderBookmarks(container: HTMLElement, state: NoteState) {
|
||||
const bookmarks = Object.entries(state.nodes).filter(([, node]) => node.bookmarked);
|
||||
|
||||
const bookmarksContainer = container.createDiv({ cls: "loom__bookmarks" });
|
||||
|
||||
const bookmarksHeader = bookmarksContainer.createDiv({
|
||||
cls: "tree-item-self loom__tree-header"
|
||||
});
|
||||
bookmarksHeader.createSpan({
|
||||
cls: "tree-item-inner loom__tree-header-text", text: "Bookmarks"
|
||||
});
|
||||
bookmarksHeader.createSpan({
|
||||
cls: "tree-item-flair-outer loom__bookmarks-count",
|
||||
text: String(bookmarks.length)
|
||||
});
|
||||
|
||||
for (const [id,] of bookmarks)
|
||||
this.renderNode(bookmarksContainer, state, id, false);
|
||||
}
|
||||
|
||||
renderTree(container: HTMLElement, state: NoteState) {
|
||||
const treeHeader = container.createDiv({
|
||||
cls: "tree-item-self loom__tree-header"
|
||||
});
|
||||
treeHeader.createSpan({
|
||||
cls: "tree-item-inner loom__tree-header-text",
|
||||
text: state.hoisted.length > 0 ? "Hoisted node" : "All nodes"
|
||||
});
|
||||
|
||||
if (state.hoisted.length > 0)
|
||||
this.renderNode(container, state, state.hoisted[state.hoisted.length - 1], true);
|
||||
else {
|
||||
const rootIds = Object.entries(state.nodes)
|
||||
.filter(([, node]) => node.parentId === null)
|
||||
.map(([id]) => id);
|
||||
for (const rootId of rootIds)
|
||||
this.renderNode(container, state, rootId, true);
|
||||
}
|
||||
}
|
||||
|
||||
renderNode(
|
||||
container: HTMLElement,
|
||||
state: NoteState,
|
||||
id: string,
|
||||
inTree: boolean
|
||||
) {
|
||||
const node = state.nodes[id];
|
||||
|
||||
const branchContainer = container.createDiv({});
|
||||
|
||||
const nodeContainer = branchContainer.createDiv({
|
||||
cls: "is-clickable outgoing-link-item tree-item-self loom__node",
|
||||
attr: { id: inTree ? `loom__node-${id}` : null },
|
||||
});
|
||||
if (id === state.current) nodeContainer.addClass("is-active");
|
||||
if (node.unread) nodeContainer.addClass("loom__node-unread");
|
||||
if (node.color) nodeContainer.addClass(`loom__node-${node.color}`);
|
||||
|
||||
const children = Object.entries(state.nodes)
|
||||
.filter(([, node]) => node.parentId === id)
|
||||
.map(([id]) => id);
|
||||
|
||||
// if the node has children, add an expand/collapse button
|
||||
|
||||
if (inTree && children.length > 0) {
|
||||
const collapseButton = nodeContainer.createDiv({
|
||||
cls: "collapse-icon loom__collapse-button"
|
||||
});
|
||||
if (node.collapsed) collapseButton.addClass("loom__is-collapsed");
|
||||
setIcon(collapseButton, "right-triangle");
|
||||
|
||||
collapseButton.addEventListener("click", () =>
|
||||
this.app.workspace.trigger("loom:toggle-collapse", id)
|
||||
);
|
||||
}
|
||||
|
||||
// if the node is bookmarked, add a bookmark icon
|
||||
|
||||
if (node.bookmarked) {
|
||||
const bookmarkIcon = nodeContainer.createDiv({ cls: "loom__node-bookmark-icon" });
|
||||
setIcon(bookmarkIcon, "bookmark");
|
||||
}
|
||||
|
||||
// if the node is unread, add an unread indicator
|
||||
|
||||
if (node.unread) nodeContainer.createDiv({ cls: "loom__node-unread-indicator" });
|
||||
|
||||
// add the node's text
|
||||
|
||||
const nodeText = nodeContainer.createEl(node.text.trim() ? "span" : "em", {
|
||||
cls: "tree-item-inner loom__node-text",
|
||||
text: node.text.trim() || "No text",
|
||||
});
|
||||
nodeText.addEventListener("click", () =>
|
||||
this.app.workspace.trigger("loom:switch-to", id)
|
||||
);
|
||||
|
||||
// define `showMenu`,
|
||||
// which is triggered by pressing the menu button or right-clicking the node
|
||||
|
||||
const rootNodes = Object.entries(state.nodes)
|
||||
.filter(([, node]) => node.parentId === null)
|
||||
const deletable = rootNodes.length !== 1 || rootNodes[0][0] !== id;
|
||||
|
||||
const showMenu = () => {
|
||||
const menu = new Menu();
|
||||
|
||||
const menuItem = (name: string, icon: string, callback: () => void) =>
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(name);
|
||||
item.setIcon(icon);
|
||||
item.onClick(callback);
|
||||
});
|
||||
|
||||
const zeroArgMenuItem = (name: string, icon: string, event: string) =>
|
||||
menuItem(name, icon, () => this.app.workspace.trigger(event));
|
||||
const selfArgMenuItem = (name: string, icon: string, event: string) =>
|
||||
menuItem(name, icon, () => this.app.workspace.trigger(event, id));
|
||||
|
||||
if (state.hoisted[state.hoisted.length - 1] === id)
|
||||
zeroArgMenuItem("Unhoist", "arrow-down", "loom:unhoist");
|
||||
else
|
||||
selfArgMenuItem("Hoist", "arrow-up", "loom:hoist");
|
||||
|
||||
if (node.bookmarked)
|
||||
selfArgMenuItem("Remove bookmark", "bookmark", "loom:toggle-bookmark");
|
||||
else
|
||||
selfArgMenuItem("Bookmark", "bookmark-minus", "loom:toggle-bookmark");
|
||||
|
||||
menu.addSeparator();
|
||||
selfArgMenuItem("Create child", "plus", "loom:create-child");
|
||||
selfArgMenuItem("Create sibling", "list-plus", "loom:create-sibling");
|
||||
|
||||
menu.addSeparator();
|
||||
selfArgMenuItem("Delete all children", "x", "loom:clear-children");
|
||||
selfArgMenuItem("Delete all siblings", "list-x", "loom:clear-siblings");
|
||||
|
||||
if (node.parentId !== null) {
|
||||
menu.addSeparator();
|
||||
selfArgMenuItem("Merge with parent", "arrow-up-left", "loom:merge-with-parent");
|
||||
}
|
||||
|
||||
if (deletable) {
|
||||
menu.addSeparator();
|
||||
selfArgMenuItem("Delete", "trash", "loom:delete");
|
||||
}
|
||||
|
||||
const rect = nodeContainer.getBoundingClientRect();
|
||||
menu.showAtPosition({ x: rect.right, y: rect.top });
|
||||
}
|
||||
|
||||
nodeContainer.addEventListener("contextmenu", (event) => {
|
||||
event.preventDefault();
|
||||
showMenu();
|
||||
});
|
||||
|
||||
// add buttons on hover
|
||||
|
||||
const nodeButtonsContainer = nodeContainer.createDiv({
|
||||
cls: "loom__node-buttons"
|
||||
});
|
||||
|
||||
const button = (label: string, icon: string, callback: () => void) => {
|
||||
const button_ = nodeButtonsContainer.createDiv({
|
||||
cls: "loom__node-button",
|
||||
attr: { "aria-label": label },
|
||||
});
|
||||
setIcon(button_, icon);
|
||||
button_.addEventListener("click", callback);
|
||||
};
|
||||
|
||||
button("Show menu", "menu", showMenu);
|
||||
|
||||
if (state.hoisted[state.hoisted.length - 1] === id)
|
||||
button("Unhoist", "arrow-down", () => this.app.workspace.trigger("loom:unhoist"));
|
||||
else button("Hoist", "arrow-up", () => this.app.workspace.trigger("loom:hoist", id));
|
||||
|
||||
if (node.bookmarked)
|
||||
button(
|
||||
"Remove bookmark",
|
||||
"bookmark-minus",
|
||||
() => this.app.workspace.trigger("loom:toggle-bookmark", id)
|
||||
);
|
||||
else
|
||||
button("Bookmark", "bookmark", () =>
|
||||
this.app.workspace.trigger("loom:toggle-bookmark", id)
|
||||
);
|
||||
|
||||
if (deletable)
|
||||
button("Delete", "trash", () => this.app.workspace.trigger("loom:delete", id));
|
||||
|
||||
// indicate if loom is currently generating children for this node
|
||||
|
||||
if (inTree && state.generating === id) {
|
||||
const generatingContainer = branchContainer.createDiv({
|
||||
cls: "loom__node-footer"
|
||||
});
|
||||
const generatingIcon = generatingContainer.createDiv({
|
||||
cls: "loom__node-generating-icon"
|
||||
});
|
||||
setIcon(generatingIcon, "loader-2");
|
||||
generatingContainer.createSpan({
|
||||
cls: "loom__node-footer-text",
|
||||
text: "Generating..."
|
||||
});
|
||||
}
|
||||
|
||||
// if in a tree, and if the node isn't collapsed, render its children
|
||||
|
||||
if (!inTree || node.collapsed) return;
|
||||
|
||||
if (branchContainer.offsetWidth < 150) {
|
||||
if (children.length > 0) {
|
||||
const showMore = branchContainer.createDiv({
|
||||
cls: "loom__node-footer loom__node-show-more"
|
||||
});
|
||||
setIcon(showMore, "arrow-up");
|
||||
showMore.createSpan({
|
||||
cls: "loom__node-footer-text",
|
||||
text: "Show more...",
|
||||
});
|
||||
|
||||
showMore.addEventListener("click", () =>
|
||||
this.app.workspace.trigger("loom:hoist", id)
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const childrenContainer = branchContainer.createDiv({
|
||||
cls: "loom__node-children"
|
||||
});
|
||||
for (const childId of children)
|
||||
this.renderNode(childrenContainer, state, childId, true);
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return "loom";
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return "Loom";
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return "network";
|
||||
}
|
||||
}
|
||||
|
||||
export class LoomSiblingsView extends ItemView {
|
||||
getNoteState: () => NoteState | null;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, getNoteState: () => NoteState | null) {
|
||||
super(leaf);
|
||||
this.getNoteState = getNoteState;
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
const scroll = this.containerEl.scrollTop;
|
||||
|
||||
this.containerEl.empty();
|
||||
this.containerEl.addClass("loom__view");
|
||||
const container = this.containerEl.createDiv({ cls: "outline" });
|
||||
|
||||
const state = this.getNoteState();
|
||||
|
||||
if (state === null) {
|
||||
container.createDiv({
|
||||
cls: "pane-empty",
|
||||
text: "No note selected.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const parentId = state.nodes[state.current].parentId;
|
||||
const siblings = Object.entries(state.nodes).filter(
|
||||
([, node]) => node.parentId === parentId
|
||||
);
|
||||
|
||||
let currentNodeContainer = null;
|
||||
for (const i in siblings) {
|
||||
const [id, node] = siblings[i];
|
||||
|
||||
const nodeContainer = container.createDiv({
|
||||
cls: `loom__sibling${id === state.current ? " is-active" : ""}`,
|
||||
});
|
||||
if (parentId !== null)
|
||||
nodeContainer.createSpan({
|
||||
text: "…",
|
||||
cls: "loom__sibling-ellipsis",
|
||||
});
|
||||
nodeContainer.createSpan({ text: node.text.trim() });
|
||||
nodeContainer.addEventListener("click", () =>
|
||||
this.app.workspace.trigger("loom:switch-to", id)
|
||||
);
|
||||
|
||||
if (parseInt(i) !== siblings.length - 1)
|
||||
container.createEl("hr", { cls: "loom__sibling-separator" });
|
||||
|
||||
if (id === state.current) currentNodeContainer = nodeContainer;
|
||||
}
|
||||
|
||||
this.containerEl.scrollTop = scroll;
|
||||
|
||||
if (currentNodeContainer !== null)
|
||||
currentNodeContainer.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return "loom-siblings";
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return "Siblings";
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return "layout-list";
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoomEditorPluginState {
|
||||
ancestorLengths: [string, number][];
|
||||
showNodeBorders: boolean;
|
||||
}
|
||||
|
||||
export class LoomEditorPlugin implements PluginValue {
|
||||
decorations: DecorationSet;
|
||||
state: LoomEditorPluginState;
|
||||
|
||||
constructor() {
|
||||
this.decorations = Decoration.none;
|
||||
this.state = { ancestorLengths: [], showNodeBorders: false };
|
||||
}
|
||||
|
||||
update() {
|
||||
let decorations: Range<Decoration>[] = [];
|
||||
|
||||
const addRange = (from: number, to: number, id: string) => {
|
||||
try {
|
||||
const range = Decoration.mark({
|
||||
class: `loom__editor-node loom__editor-node-${id}`,
|
||||
}).range(from, to);
|
||||
decorations.push(range);
|
||||
} catch (e) {
|
||||
// this happens if the range is empty. it's ok. it's fine,
|
||||
}
|
||||
};
|
||||
|
||||
const addBorder = (at: number) => {
|
||||
const range = Decoration.widget({
|
||||
widget: new LoomNodeBorderWidget(),
|
||||
side: -1,
|
||||
}).range(at, at);
|
||||
decorations.push(range);
|
||||
};
|
||||
|
||||
let i = 0;
|
||||
for (const [id, length] of this.state.ancestorLengths) {
|
||||
addRange(i, i + length, id);
|
||||
i += length;
|
||||
if (this.state.showNodeBorders) addBorder(i);
|
||||
}
|
||||
|
||||
this.decorations = Decoration.set(decorations);
|
||||
}
|
||||
}
|
||||
|
||||
class LoomNodeBorderWidget extends WidgetType {
|
||||
toDOM() {
|
||||
const el = document.createElement("span");
|
||||
el.classList.add("loom__editor-node-border");
|
||||
return el;
|
||||
}
|
||||
|
||||
eq() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export const loomEditorPluginSpec: PluginSpec<LoomEditorPlugin> = {
|
||||
decorations: (plugin: LoomEditorPlugin) => plugin.decorations,
|
||||
eventHandlers: {
|
||||
mouseover: (event: MouseEvent, _view: EditorView) => {
|
||||
if (event.button !== 0) return false;
|
||||
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.classList.contains("loom__editor-node")) return false;
|
||||
|
||||
const className = target.classList[target.classList.length - 1];
|
||||
for (const el of [].slice.call(
|
||||
document.getElementsByClassName(className)
|
||||
))
|
||||
el.classList.add("loom__editor-node-hover");
|
||||
|
||||
return true;
|
||||
},
|
||||
mouseout: (event: MouseEvent, _view: EditorView) => {
|
||||
if (event.button !== 0) return false;
|
||||
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.classList.contains("loom__editor-node")) return false;
|
||||
|
||||
const className = target.classList[target.classList.length - 1];
|
||||
for (const el of [].slice.call(
|
||||
document.getElementsByClassName(className)
|
||||
))
|
||||
el.classList.remove("loom__editor-node-hover");
|
||||
|
||||
return true;
|
||||
},
|
||||
mousedown: (event: MouseEvent, _view: EditorView) => {
|
||||
if (event.button !== 0 || !event.shiftKey) return false;
|
||||
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.classList.contains("loom__editor-node")) return false;
|
||||
|
||||
// the second last element, since the last is `loom__editor-node-hover`
|
||||
const className = target.classList[target.classList.length - 2];
|
||||
const id = className.split("-").slice(2).join("-");
|
||||
app.workspace.trigger("loom:switch-to", id);
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
Loading…
Reference in a new issue