diff --git a/apps/obsidian/src/components/DiscourseContextView.tsx b/apps/obsidian/src/components/DiscourseContextView.tsx
new file mode 100644
index 000000000..9dcdd6368
--- /dev/null
+++ b/apps/obsidian/src/components/DiscourseContextView.tsx
@@ -0,0 +1,141 @@
+import { ItemView, TFile, WorkspaceLeaf } from "obsidian";
+import { createRoot, Root } from "react-dom/client";
+import DiscourseGraphPlugin from "~/index";
+import { getDiscourseNodeFormatExpression } from "~/utils/getDiscourseNodeFormatExpression";
+import { VIEW_TYPE_DISCOURSE_CONTEXT } from "~/types";
+
+interface DiscourseContextProps {
+ activeFile: TFile | null;
+ plugin: DiscourseGraphPlugin;
+}
+
+const DiscourseContext = ({ activeFile, plugin }: DiscourseContextProps) => {
+ const extractContentFromTitle = (
+ format: string | undefined,
+ title: string,
+ ): string => {
+ if (!format) return "";
+ const regex = getDiscourseNodeFormatExpression(format);
+ const match = title.match(regex);
+ return match?.[1] ?? title;
+ };
+
+ const renderContent = () => {
+ if (!activeFile) {
+ return
No file is open
;
+ }
+
+ const fileMetadata = plugin.app.metadataCache.getFileCache(activeFile);
+ if (!fileMetadata) {
+ return File metadata not available
;
+ }
+
+ const frontmatter = fileMetadata.frontmatter;
+ if (!frontmatter) {
+ return No discourse node data found
;
+ }
+
+ if (!frontmatter.nodeTypeId) {
+ return Not a discourse node (no nodeTypeId)
;
+ }
+
+ const nodeType = plugin.settings.nodeTypes.find(
+ (type) => type.id === frontmatter.nodeTypeId,
+ );
+
+ if (!nodeType) {
+ return Unknown node type: {frontmatter.nodeTypeId}
;
+ }
+ return (
+
+
+ {nodeType.name || "Unnamed Node Type"}
+
+
+ {nodeType.format && (
+
+ Content:
+ {extractContentFromTitle(nodeType.format, activeFile.basename)}
+
+ )}
+
+ );
+ };
+
+ return (
+
+
Discourse Context
+ {renderContent()}
+
+ );
+};
+
+export class DiscourseContextView extends ItemView {
+ private plugin: DiscourseGraphPlugin;
+ private activeFile: TFile | null = null;
+ private root: Root | null = null;
+
+ constructor(leaf: WorkspaceLeaf, plugin: DiscourseGraphPlugin) {
+ super(leaf);
+ this.plugin = plugin;
+ }
+
+ setActiveFile(file: TFile | null): void {
+ this.activeFile = file;
+ this.updateView();
+ }
+
+ getViewType(): string {
+ return VIEW_TYPE_DISCOURSE_CONTEXT;
+ }
+
+ getDisplayText(): string {
+ return "Discourse Context";
+ }
+
+ getIcon(): string {
+ return "telescope";
+ }
+
+ async onOpen(): Promise {
+ const container = this.containerEl.children[1];
+ if (container) {
+ container.empty();
+ container.addClass("discourse-context-container");
+
+ this.root = createRoot(container);
+
+ this.activeFile = this.app.workspace.getActiveFile();
+
+ this.updateView();
+
+ this.registerEvent(
+ this.app.workspace.on("file-open", (file) => {
+ this.activeFile = file;
+ this.updateView();
+ }),
+ );
+ }
+ }
+
+ updateView(): void {
+ if (this.root) {
+ this.root.render(
+ ,
+ );
+ }
+ }
+
+ async onClose(): Promise {
+ if (this.root) {
+ this.root.unmount();
+ this.root = null;
+ }
+ }
+}
diff --git a/apps/obsidian/src/components/NodeTypeModal.tsx b/apps/obsidian/src/components/NodeTypeModal.tsx
index baa9fdfc8..bcb8aac07 100644
--- a/apps/obsidian/src/components/NodeTypeModal.tsx
+++ b/apps/obsidian/src/components/NodeTypeModal.tsx
@@ -1,4 +1,4 @@
-import { App, Editor, SuggestModal } from "obsidian";
+import { App, Editor, SuggestModal, TFile, Notice } from "obsidian";
import { DiscourseNode } from "../types";
import { getDiscourseNodeFormatExpression } from "../utils/getDiscourseNodeFormatExpression";
@@ -25,8 +25,38 @@ export class NodeTypeModal extends SuggestModal {
renderSuggestion(nodeType: DiscourseNode, el: HTMLElement) {
el.createEl("div", { text: nodeType.name });
}
+ async createDiscourseNode(
+ title: string,
+ nodeType: DiscourseNode,
+ ): Promise {
+ try {
+ const instanceId = `${nodeType.id}-${Date.now()}`;
+ const filename = `${title}.md`;
- onChooseSuggestion(nodeType: DiscourseNode) {
+ await this.app.vault.create(filename, "");
+
+ const newFile = this.app.vault.getAbstractFileByPath(filename);
+ if (!(newFile instanceof TFile)) {
+ throw new Error("Failed to create new file");
+ }
+
+ await this.app.fileManager.processFrontMatter(newFile, (fm) => {
+ fm.nodeTypeId = nodeType.id;
+ fm.nodeInstanceId = instanceId;
+ });
+
+ new Notice(`Created discourse node: ${title}`);
+ return newFile;
+ } catch (error: unknown) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Unknown error";
+ new Notice(`Error creating discourse node: ${errorMessage}`, 5000);
+ console.error("Failed to create discourse node:", error);
+ return null;
+ }
+ }
+
+ async onChooseSuggestion(nodeType: DiscourseNode) {
const selectedText = this.editor.getSelection();
const regex = getDiscourseNodeFormatExpression(nodeType.format);
@@ -39,6 +69,9 @@ export class NodeTypeModal extends SuggestModal {
nodeFormat[2]?.replace(/\\/g, "");
if (!nodeFormat) return;
- this.editor.replaceSelection(`[[${formattedNodeName}]]`);
+ const newFile = await this.createDiscourseNode(formattedNodeName, nodeType);
+ if (newFile) {
+ this.editor.replaceSelection(`[[${formattedNodeName}]]`);
+ }
}
}
diff --git a/apps/obsidian/src/index.ts b/apps/obsidian/src/index.ts
index 39586a0f7..d7546666b 100644
--- a/apps/obsidian/src/index.ts
+++ b/apps/obsidian/src/index.ts
@@ -1,7 +1,9 @@
import { Plugin } from "obsidian";
import { SettingsTab } from "~/components/Settings";
-import { Settings } from "./types";
-import { registerCommands } from "./utils/registerCommands";
+import { Settings } from "~/types";
+import { registerCommands } from "~/utils/registerCommands";
+import { DiscourseContextView } from "~/components/DiscourseContextView";
+import { VIEW_TYPE_DISCOURSE_CONTEXT } from "~/types";
const DEFAULT_SETTINGS: Settings = {
nodeTypes: [],
@@ -16,9 +18,47 @@ export default class DiscourseGraphPlugin extends Plugin {
await this.loadSettings();
registerCommands(this);
this.addSettingTab(new SettingsTab(this.app, this));
+
+ this.registerView(
+ VIEW_TYPE_DISCOURSE_CONTEXT,
+ (leaf) => new DiscourseContextView(leaf, this),
+ );
+
+ this.addRibbonIcon("telescope", "Toggle Discourse Context", () => {
+ this.toggleDiscourseContextView();
+ });
}
- onunload() {}
+ toggleDiscourseContextView() {
+ const { workspace } = this.app;
+ const existingLeaf = workspace.getLeavesOfType(
+ VIEW_TYPE_DISCOURSE_CONTEXT,
+ )[0];
+
+ if (existingLeaf) {
+ existingLeaf.detach();
+ } else {
+ const activeFile = workspace.getActiveFile();
+ const leaf = workspace.getRightLeaf(false);
+ if (leaf) {
+ const layoutChangeHandler = () => {
+ const view = leaf.view;
+ if (view instanceof DiscourseContextView) {
+ view.setActiveFile(activeFile);
+ workspace.off("layout-change", layoutChangeHandler);
+ }
+ };
+
+ workspace.on("layout-change", layoutChangeHandler);
+
+ leaf.setViewState({
+ type: VIEW_TYPE_DISCOURSE_CONTEXT,
+ active: true,
+ });
+ workspace.revealLeaf(leaf);
+ }
+ }
+ }
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
diff --git a/apps/obsidian/src/types.ts b/apps/obsidian/src/types.ts
index fae4c112a..5ef3fb2c7 100644
--- a/apps/obsidian/src/types.ts
+++ b/apps/obsidian/src/types.ts
@@ -23,3 +23,5 @@ export type Settings = {
discourseRelations: DiscourseRelation[];
relationTypes: DiscourseRelationType[];
};
+
+export const VIEW_TYPE_DISCOURSE_CONTEXT = "discourse-context-view";
\ No newline at end of file
diff --git a/apps/obsidian/src/utils/registerCommands.ts b/apps/obsidian/src/utils/registerCommands.ts
index 1f9fa566e..43b1bc6d6 100644
--- a/apps/obsidian/src/utils/registerCommands.ts
+++ b/apps/obsidian/src/utils/registerCommands.ts
@@ -52,4 +52,12 @@ export const registerCommands = (plugin: DiscourseGraphPlugin) => {
new NodeTypeModal(plugin.app, editor, plugin.settings.nodeTypes).open();
},
});
+
+ plugin.addCommand({
+ id: "toggle-discourse-context",
+ name: "Toggle Discourse Context",
+ callback: () => {
+ plugin.toggleDiscourseContextView();
+ },
+ });
};