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(); + }, + }); };