├── .cursor └── rules │ ├── api.mdc │ ├── main.mdc │ ├── obsidian.mdc │ ├── roam.mdc │ └── website.mdc ├── .github └── workflows │ ├── database-deploy.yaml │ ├── publish-obsidian.yml │ ├── roam-main.yaml │ ├── roam-pr.yaml │ └── roam-release.yaml ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── STYLE_GUIDE.md ├── apps ├── obsidian │ ├── .env.example │ ├── README.md │ ├── docs │ │ └── media │ │ │ ├── BRAT.png │ │ │ ├── add-beta-plugin.png │ │ │ ├── add-datacore.png │ │ │ ├── add-discourse-graph.png │ │ │ ├── add-node-types.png │ │ │ ├── add-relationship.png │ │ │ ├── choose-discourse-relations.png │ │ │ ├── choose-template.png │ │ │ ├── command.png │ │ │ ├── create-template-file.png │ │ │ ├── discourse-relations.png │ │ │ ├── final-relation.png │ │ │ ├── new-folder.png │ │ │ ├── node-created.png │ │ │ ├── node-menu.png │ │ │ ├── open-dg-context.png │ │ │ ├── relation-types.png │ │ │ ├── relationship-created.png │ │ │ ├── right-click-menu.png │ │ │ ├── search.png │ │ │ ├── select.png │ │ │ └── template.png │ ├── manifest.json │ ├── package.json │ ├── postcss.config.js │ ├── scripts │ │ ├── build.ts │ │ ├── compile.ts │ │ └── dev.ts │ ├── src │ │ ├── components │ │ │ ├── AppContext.tsx │ │ │ ├── ConfirmationModal.tsx │ │ │ ├── DiscourseContextView.tsx │ │ │ ├── DropdownSelect.tsx │ │ │ ├── NodeTypeModal.tsx │ │ │ ├── NodeTypeSettings.tsx │ │ │ ├── PluginContext.tsx │ │ │ ├── RelationshipSection.tsx │ │ │ ├── RelationshipSettings.tsx │ │ │ ├── RelationshipTypeSettings.tsx │ │ │ ├── SearchBar.tsx │ │ │ └── Settings.tsx │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── services │ │ │ └── QueryEngine.ts │ │ ├── styles │ │ │ └── style.css │ │ ├── types.ts │ │ └── utils │ │ │ ├── createNodeFromSelectedText.ts │ │ │ ├── generateUid.ts │ │ │ ├── getDiscourseNodeFormatExpression.ts │ │ │ ├── registerCommands.ts │ │ │ ├── templates.ts │ │ │ ├── validateNodeFormat.ts │ │ │ └── validateNodeType.ts │ ├── styles.css │ ├── tailwind.config.ts │ └── tsconfig.json ├── roam │ ├── .env.example │ ├── .gitignore │ ├── README.md │ ├── build.sh │ ├── docs │ │ ├── examples.md │ │ ├── media │ │ │ ├── discourse-graph-linked-ref.jpg │ │ │ ├── discourse-graph-stop.png │ │ │ ├── discourse-graph-stopped.png │ │ │ ├── settings-browse-extensions.png │ │ │ ├── settings-enable-discourse-graph.png │ │ │ ├── settings-install-query-builder.png │ │ │ └── settings-roam-depot.png │ │ └── query-builder.md │ ├── package-lock.json │ ├── package.json │ ├── patches │ │ ├── @playwright+test+1.29.0.patch │ │ ├── @tldraw+primitives+2.0.0-canary.ffda4cfb.patch │ │ ├── @tldraw+tldraw++@tldraw+editor+2.0.0-canary.ffda4cfb.patch │ │ ├── @tldraw+tldraw++@tldraw+ui+2.0.0-canary.ffda4cfb.patch │ │ └── @tldraw+tlschema+2.0.0-canary.ffda4cfb.patch │ ├── scripts │ │ ├── build.ts │ │ ├── compile.ts │ │ ├── deploy.ts │ │ ├── dev.ts │ │ └── publish.ts │ ├── src │ │ ├── components │ │ │ ├── BirdEatsBugs.tsx │ │ │ ├── DiscourseContext.tsx │ │ │ ├── DiscourseContextOverlay.tsx │ │ │ ├── DiscourseNodeMenu.tsx │ │ │ ├── DiscourseNodeSearchMenu.tsx │ │ │ ├── Export.tsx │ │ │ ├── ExportDiscourseContext.tsx │ │ │ ├── ExportGithub.tsx │ │ │ ├── ImportDialog.tsx │ │ │ ├── LivePreview.tsx │ │ │ ├── QueryBuilder.tsx │ │ │ ├── QueryDrawer.tsx │ │ │ ├── QueryEditor.tsx │ │ │ ├── ResizableDrawer.tsx │ │ │ ├── SelectDialog.tsx │ │ │ ├── canvas │ │ │ │ ├── CanvasDrawer.tsx │ │ │ │ ├── CanvasReferences.tsx │ │ │ │ ├── DiscourseNodeUtil.tsx │ │ │ │ ├── DiscourseRelationsUtil.tsx │ │ │ │ ├── LabelDialog.tsx │ │ │ │ └── Tldraw.tsx │ │ │ ├── index.ts │ │ │ ├── results-view │ │ │ │ ├── Charts.tsx │ │ │ │ ├── Inputs.tsx │ │ │ │ ├── Kanban.tsx │ │ │ │ ├── ResultsTable.tsx │ │ │ │ ├── ResultsView.tsx │ │ │ │ └── Timeline.tsx │ │ │ └── settings │ │ │ │ ├── DefaultFilters.tsx │ │ │ │ ├── DiscourseNodeAttributes.tsx │ │ │ │ ├── DiscourseNodeCanvasSettings.tsx │ │ │ │ ├── DiscourseNodeConfigPanel.tsx │ │ │ │ ├── DiscourseNodeIndex.tsx │ │ │ │ ├── DiscourseNodeSpecification.tsx │ │ │ │ ├── DiscourseRelationConfigPanel.tsx │ │ │ │ ├── ExportSettings.tsx │ │ │ │ ├── GeneralSettings.tsx │ │ │ │ ├── HomePersonalSettings.tsx │ │ │ │ ├── NodeConfig.tsx │ │ │ │ ├── QueryPagesPanel.tsx │ │ │ │ ├── QuerySettings.tsx │ │ │ │ └── Settings.tsx │ │ ├── data │ │ │ ├── defaultDiscourseNodes.ts │ │ │ ├── defaultDiscourseRelations.ts │ │ │ ├── toastMessages.tsx │ │ │ └── userSettings.ts │ │ ├── index.ts │ │ ├── styles │ │ │ ├── discourseGraphStyles.css │ │ │ ├── settingsStyles.css │ │ │ └── styles.css │ │ ├── types.d.ts │ │ └── utils │ │ │ ├── calcCanvasNodeSizeAndImg.ts │ │ │ ├── canvasUiOverrides.ts │ │ │ ├── compileDatalog.ts │ │ │ ├── conditionToDatalog.ts │ │ │ ├── configPageTabs.ts │ │ │ ├── createDiscourseNode.ts │ │ │ ├── createInitialTldrawProps.ts │ │ │ ├── createSettingsPanel.ts │ │ │ ├── deriveDiscourseNodeAttribute.ts │ │ │ ├── discourseConfigRef.ts │ │ │ ├── discourseNodeFormatToDatalog.ts │ │ │ ├── extensionSettings.ts │ │ │ ├── findDiscourseNode.ts │ │ │ ├── fireQuery.ts │ │ │ ├── formatUtils.ts │ │ │ ├── gatherDatalogVariablesFromClause.ts │ │ │ ├── getBlockProps.ts │ │ │ ├── getDiscourseContextResults.ts │ │ │ ├── getDiscourseNodeFormatExpression.ts │ │ │ ├── getDiscourseNodes.ts │ │ │ ├── getDiscourseRelationLabels.ts │ │ │ ├── getDiscourseRelationTriples.ts │ │ │ ├── getDiscourseRelations.ts │ │ │ ├── getExportSettings.ts │ │ │ ├── getExportTypes.ts │ │ │ ├── getPageMetadata.ts │ │ │ ├── getPlainTitleFromSpecification.ts │ │ │ ├── getQBClauses.ts │ │ │ ├── graphViewNodeStyling.ts │ │ │ ├── importDiscourseGraph.ts │ │ │ ├── initializeDiscourseNodes.ts │ │ │ ├── initializeObserversAndListeners.ts │ │ │ ├── isCanvasPage.ts │ │ │ ├── isDiscourseNode.ts │ │ │ ├── isDiscourseNodeConfigPage.ts │ │ │ ├── isFlagEnabled.ts │ │ │ ├── isPageUid.ts │ │ │ ├── isQueryPage.ts │ │ │ ├── listActiveQueries.ts │ │ │ ├── loadImage.ts │ │ │ ├── matchDiscourseNode.ts │ │ │ ├── measureCanvasNodeText.ts │ │ │ ├── pageRefObserverHandlers.ts │ │ │ ├── parseQuery.ts │ │ │ ├── parseResultSettings.ts │ │ │ ├── postProcessResults.ts │ │ │ ├── predefinedSelections.ts │ │ │ ├── refreshConfigTree.ts │ │ │ ├── registerCommandPaletteCommands.ts │ │ │ ├── registerDiscourseDatalogTranslators.ts │ │ │ ├── registerSmartBlock.ts │ │ │ ├── renderLinkedReferenceAdditions.ts │ │ │ ├── renderNodeConfigPage.ts │ │ │ ├── replaceDatalogVariables.ts │ │ │ ├── resolveQueryBuilderRef.ts │ │ │ ├── runQuery.ts │ │ │ ├── sendErrorEmail.ts │ │ │ ├── setQueryPages.ts │ │ │ ├── toCellValue.ts │ │ │ ├── triplesToBlocks.ts │ │ │ ├── types.ts │ │ │ └── useRoamStore.ts │ ├── tailwind.config.ts │ ├── tests │ │ └── extension.test.ts │ └── tsconfig.json └── website │ ├── .env.example │ ├── .eslintrc.js │ ├── .gitignore │ ├── app │ ├── (docs) │ │ ├── docs │ │ │ ├── page.tsx │ │ │ └── roam │ │ │ │ ├── [slug] │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── navigation.ts │ │ │ │ ├── page.tsx │ │ │ │ └── pages │ │ │ │ ├── base-grammar.md │ │ │ │ ├── creating-discourse-nodes.md │ │ │ │ ├── creating-discourse-relationships.md │ │ │ │ ├── discourse-attributes.md │ │ │ │ ├── discourse-context-overlay.md │ │ │ │ ├── discourse-context.md │ │ │ │ ├── enhanced-zettelkasten.md │ │ │ │ ├── exploring-discourse-graph.md │ │ │ │ ├── extending-personalizing-graph.md │ │ │ │ ├── getting-started.md │ │ │ │ ├── grammar.md │ │ │ │ ├── installation-roam-depot.md │ │ │ │ ├── installation.md │ │ │ │ ├── lab-notebooks.md │ │ │ │ ├── literature-reviewing.md │ │ │ │ ├── node-index.md │ │ │ │ ├── nodes.md │ │ │ │ ├── operators-relations.md │ │ │ │ ├── querying-discourse-graph.md │ │ │ │ ├── reading-clubs.md │ │ │ │ ├── relations-patterns.md │ │ │ │ ├── research-roadmapping.md │ │ │ │ ├── sharing-discourse-graph.md │ │ │ │ └── what-is-discourse-graph.md │ │ └── layout.tsx │ ├── (home) │ │ ├── blog │ │ │ ├── [slug] │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ ├── posts │ │ │ │ └── EXAMPLE.md │ │ │ └── readBlogs.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── PostHogPageView.tsx │ ├── api │ │ ├── embeddings │ │ │ └── openai │ │ │ │ └── small │ │ │ │ └── route.ts │ │ ├── errors │ │ │ ├── EmailTemplate.tsx │ │ │ └── route.tsx │ │ ├── llm │ │ │ ├── anthropic │ │ │ │ └── chat │ │ │ │ │ └── route.ts │ │ │ ├── gemini │ │ │ │ └── chat │ │ │ │ │ └── route.ts │ │ │ └── openai │ │ │ │ └── chat │ │ │ │ └── route.ts │ │ └── supabase │ │ │ ├── account │ │ │ ├── [id].ts │ │ │ └── route.ts │ │ │ ├── content-embedding │ │ │ ├── [id].ts │ │ │ ├── batch │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ │ ├── content │ │ │ ├── [id].ts │ │ │ ├── batch │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ │ ├── document │ │ │ ├── [id].ts │ │ │ └── route.ts │ │ │ ├── person │ │ │ ├── [id].ts │ │ │ └── route.ts │ │ │ ├── platform │ │ │ ├── [id].ts │ │ │ └── route.ts │ │ │ ├── similarity-rank │ │ │ └── route.ts │ │ │ └── space │ │ │ ├── [id].ts │ │ │ └── route.ts │ ├── components │ │ ├── DocsHeader.tsx │ │ ├── DocsLayout.tsx │ │ ├── Logo.tsx │ │ ├── Navigation.tsx │ │ ├── PrevNextLinks.tsx │ │ ├── Prose.tsx │ │ ├── TableOfContents.tsx │ │ └── TeamPerson.tsx │ ├── data │ │ └── constants.ts │ ├── fonts │ │ ├── GeistMonoVF.woff │ │ └── GeistVF.woff │ ├── globals.css │ ├── icon.ico │ ├── providers.tsx │ ├── types │ │ ├── llm.ts │ │ └── schema.tsx │ └── utils │ │ ├── getFileContent.ts │ │ ├── getHtmlFromMarkdown.ts │ │ ├── getProcessedMarkdownFile.ts │ │ ├── getSections.ts │ │ ├── llm │ │ ├── cors.ts │ │ ├── handler.ts │ │ └── providers.ts │ │ └── supabase │ │ ├── apiUtils.ts │ │ ├── dbUtils.ts │ │ ├── server.ts │ │ └── validators.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── public │ ├── MATSU_lab_journal_club_graph_view.png │ ├── bg │ │ └── smile1.svg │ ├── discord.svg │ ├── docs │ │ └── roam │ │ │ ├── argument-map.png │ │ │ ├── bans-hate-speech.png │ │ │ ├── browse-roam-depot.png │ │ │ ├── command-palette-export.png │ │ │ ├── command-palette-query-drawer.png │ │ │ ├── discourse-context-filter.gif │ │ │ ├── discourse-context-group-by-target.gif │ │ │ ├── discourse-context-overlay-score.png │ │ │ ├── discourse-context-overlay.gif │ │ │ ├── find-in-roam-depot.png │ │ │ ├── install-instruction-roam-depot.png │ │ │ ├── load-from-url1.png │ │ │ ├── load-from-url2.png │ │ │ ├── node-template.png │ │ │ ├── query-drawer-advanced.png │ │ │ ├── query-drawer-advanced2.png │ │ │ ├── query-drawer-advanced3.png │ │ │ ├── query-drawer-informs.png │ │ │ ├── query-drawer-naming.gif │ │ │ ├── query-drawer-save-to-page.png │ │ │ ├── query-drawer-supports.png │ │ │ ├── query-drawer.png │ │ │ ├── relation-informs.png │ │ │ ├── relation-opposes.png │ │ │ ├── relation-supports.png │ │ │ ├── roam-depot-settings.png │ │ │ ├── roam-depot-sidebar.png │ │ │ ├── settings-discourse-attributes.png │ │ │ ├── settings-discourse-context-overlay.png │ │ │ ├── settings-export-frontmatter.png │ │ │ ├── settings-export.png │ │ │ ├── settings-node-index.png │ │ │ └── settings-relation.png │ ├── github.svg │ ├── logo-screenshot-48.png │ ├── section1.webp │ ├── section2.webp │ ├── section3.webp │ ├── section4.webp │ ├── section5a.webp │ ├── section5b.webp │ ├── section6.webp │ ├── smile-black.svg │ ├── social │ │ ├── github.svg │ │ ├── website.png │ │ └── x.png │ ├── supporter-logos │ │ ├── Chan_Zuckerberg_Initiative.svg │ │ ├── Metagov.svg │ │ ├── PL_Research.svg │ │ ├── Schmidt_Futures.svg │ │ └── The_Navigation_Fund.svg │ └── team │ │ ├── david.png │ │ ├── joel.png │ │ ├── john.jpeg │ │ ├── maparent.jpg │ │ ├── matt.png │ │ ├── michael.png │ │ ├── sid.jpg │ │ └── trang.png │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── vercel.json ├── package-lock.json ├── package.json ├── packages ├── database │ ├── .sqruff │ ├── README.md │ ├── example.md │ ├── package.json │ ├── schema.puml │ ├── schema.svg │ ├── schema.yaml │ ├── scripts │ │ ├── deploy.ts │ │ └── lint.ts │ ├── supabase │ │ ├── .gitignore │ │ ├── config.toml │ │ ├── migrations │ │ │ ├── 20250504195841_remote_schema.sql │ │ │ ├── 20250504202930_content_tables.sql │ │ │ ├── 20250506174523_content_idx_id.sql │ │ │ ├── 20250512142307_sync_table.sql │ │ │ ├── 20250513173724_content_concept_key.sql │ │ │ ├── 20250517154122_plpgsql_linting.sql │ │ │ ├── 20250520132747_restrict_search_by_document.sql │ │ │ ├── 20250520133551_nodes_needing_sync.sql │ │ │ ├── 20250522193823_rename_discourse_space.sql │ │ │ ├── 20250526150535_uniqueness.sql │ │ │ └── 20250530161244_content_constraints.sql │ │ └── schemas │ │ │ ├── account.sql │ │ │ ├── agent.sql │ │ │ ├── base.sql │ │ │ ├── concept.sql │ │ │ ├── content.sql │ │ │ ├── contributor.sql │ │ │ ├── embedding.sql │ │ │ ├── extensions.sql │ │ │ ├── space.sql │ │ │ └── sync.sql │ └── types.gen.ts ├── eslint-config │ ├── README.md │ ├── library.js │ ├── next.js │ ├── package.json │ └── react-internal.js ├── tailwind-config │ ├── package.json │ ├── tailwind.config.ts │ └── tsconfig.json ├── types │ ├── index.ts │ └── package.json ├── typescript-config │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json └── ui │ ├── .eslintrc.js │ ├── components.json │ ├── package.json │ ├── src │ ├── components │ │ └── ui │ │ │ └── card.tsx │ ├── globals.css │ └── lib │ │ └── utils.ts │ ├── tailwind.config.ts │ ├── tsconfig.json │ ├── tsconfig.lint.json │ └── turbo │ └── generators │ ├── config.ts │ └── templates │ └── component.hbs └── turbo.json /.cursor/rules/api.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: apps/website/app/api/** 4 | alwaysApply: false 5 | --- 6 | You are working on the api routes for Discourse Graph which uses NextJS app router. 7 | -------------------------------------------------------------------------------- /.cursor/rules/obsidian.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: apps/obsidian/** 4 | alwaysApply: false 5 | --- 6 | You are working on the Obsidian plugin that implements the Discourse Graph protocol. 7 | 8 | ## dependencies 9 | prefer existing depedencies from [package.json](mdc:apps/obsidian/package.json) 10 | 11 | ## Obsidian Style Guide 12 | Use the obsidian style guide from help.obsidian.md/style-guide and docs.obsidian.md/Developer+policies 13 | 14 | ### Icons 15 | platform-native UI 16 | Lucide and custom Obsidian icons can be used alongside detailed elements to provide a visual representation of a feature. 17 | 18 | Example: In the ribbon on the left, select Create new canvas ( lucide-layout-dashboard.svg > icon ) to create a canvas in the same folder as the active file. 19 | 20 | Guidelines for icons 21 | 22 | Store icons in the Attachments/icons folder. 23 | Add the prefix lucide- before the Lucide icon name. 24 | Add the prefix obsidian-icon- before the Obsidian icon name. 25 | Example: The icon for creating a new canvas should be named lucide-layout-dashboard. 26 | 27 | Use the SVG version of the icons available. 28 | Icons should be 18 pixels in width, 18 pixels in height, and have a stroke width of 1.5. You can adjust these settings in the SVG data. 29 | Adjusting size and stroke in an SVG 30 | Utilize the icon anchor in embedded images, to tweak the spacing around the icon so that it aligns neatly with the text in the vicinity. 31 | Icons should be surrounded by parenthesis. ( lucide-cog.svg > icon ) 32 | Example: ( ![[lucide-cog.svg#icon]] ) -------------------------------------------------------------------------------- /.cursor/rules/roam.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: apps/roam/** 4 | alwaysApply: false 5 | --- 6 | You are working on the Roam Research extension that implements the Discourse Graph protocol. 7 | 8 | ## dependencies 9 | prefer existing depedencies from [package.json](mdc:apps/roam/package.json) 10 | 11 | ## Roam Style Guide 12 | 13 | platform-native UI - use BlueprintJS 3 components and Tailwind CSS. 14 | Use the roamAlphaApi docs from https://roamresearch.com/#/app/developer-documentation/page/tIaOPdXCj 15 | Use Roam Depot/Extension API docs from https://roamresearch.com/#/app/developer-documentation/page/y31lhjIqU 16 | 17 | -------------------------------------------------------------------------------- /.cursor/rules/website.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: apps/website/** 4 | alwaysApply: false 5 | --- 6 | You are working on the public-facing website for Discourse Graph. 7 | 8 | ## dependencies 9 | prefer existing depedencies from [package.json](mdc:apps/website/package.json) 10 | -------------------------------------------------------------------------------- /.github/workflows/database-deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Supabase deploy Function 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | env: 11 | SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} 12 | SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID_PROD }} 13 | SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD_PROD }} 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: "20" 19 | - run: npm ci 20 | - uses: supabase/setup-cli@v1 21 | with: 22 | version: latest 23 | - run: npx turbo deploy -F @repo/database 24 | -------------------------------------------------------------------------------- /.github/workflows/roam-main.yaml: -------------------------------------------------------------------------------- 1 | name: Main - Roam To Blob Storage 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: main 6 | paths: 7 | - "apps/roam/**" 8 | - "packages/tailwind-config/**" 9 | - "packages/ui/**" 10 | 11 | env: 12 | BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }} 13 | 14 | jobs: 15 | deploy: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout Code 19 | uses: actions/checkout@v4 20 | 21 | - name: Setup Node.js environment 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 20 25 | cache: "npm" 26 | 27 | - name: Install Dependencies 28 | run: npm install 29 | 30 | - name: Deploy 31 | run: npx turbo run deploy --filter=roam 32 | -------------------------------------------------------------------------------- /.github/workflows/roam-pr.yaml: -------------------------------------------------------------------------------- 1 | name: PR - Roam To Blob Storage 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - synchronize 7 | paths: 8 | - "apps/roam/**" 9 | - "packages/tailwind-config/**" 10 | - "packages/ui/**" 11 | env: 12 | BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }} 13 | GITHUB_HEAD_REF: ${{ github.head_ref }} 14 | GITHUB_REF_NAME: ${{ github.ref_name }} 15 | 16 | jobs: 17 | deploy: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout Code 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup Node.js environment 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | cache: "npm" 28 | 29 | - name: Install Dependencies 30 | run: npm install 31 | 32 | - name: Deploy 33 | run: npx turbo run deploy --filter=roam 34 | -------------------------------------------------------------------------------- /.github/workflows/roam-release.yaml: -------------------------------------------------------------------------------- 1 | name: Update Roam Extension Metadata 2 | on: 3 | workflow_dispatch: 4 | 5 | env: 6 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 7 | APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} 8 | APP_ID: ${{ secrets.APP_ID }} 9 | 10 | jobs: 11 | update-extension: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | steps: 17 | - name: Checkout Code 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup Node.js environment 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: 20 24 | cache: "npm" 25 | 26 | - name: Install Dependencies 27 | run: npm install 28 | 29 | - name: Update Roam Depot Extension 30 | run: npx turbo run publish --filter=roam 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # Local env files 9 | .env 10 | .env.* 11 | 12 | # Testing 13 | coverage 14 | 15 | # Turbo 16 | .turbo 17 | 18 | # Vercel 19 | .vercel 20 | 21 | # Build Outputs 22 | .next/ 23 | out/ 24 | build 25 | dist 26 | 27 | 28 | # Debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | 33 | # Misc 34 | .DS_Store 35 | *.pem 36 | *.ipynb 37 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/.npmrc -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "prettier.requireConfig": true, 4 | "prettier.prettierPath": "./node_modules/prettier", 5 | "editor.defaultFormatter": "esbenp.prettier-vscode" 6 | } 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We appreciate your interest in helping improve [Discourse Graphs](https://github.com/DiscourseGraphs/discourse-graph)! Contributions, whether in code or documentation, are always welcome. 🙌 4 | 5 | ## Start With an Issue 6 | 7 | Before diving into a pull request, it’s a good idea to [open an issue](https://github.com/DiscourseGraphs/discourse-graph/issues/new/) to discuss what you have in mind. This ensures your work aligns with the project’s goals and reduces the chance of duplicating ongoing efforts. 8 | 9 | If you’re uncertain about the value of your proposed change, don’t hesitate to create an issue anyway 😄. We’ll review it together, and once we agree on next steps, you can confidently move forward. 10 | 11 | ## Making Your Changes 12 | 13 | Here’s how to contribute: 14 | 15 | 1. [Fork](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) and [clone](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository) the repository. 16 | 2. [Create a new branch](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/managing-branches) for your updates. 17 | 3. Implement your changes, making sure your code: 18 | - Is formatted with [Prettier](https://prettier.io) 19 | - Follows our [Style Guide](./STYLE_GUIDE.md) 20 | - Passes all [TypeScript](https://www.typescriptlang.org/) type checks 21 | 4. Write tests that validate your change and/or fix. 22 | 23 | 5. Push your branch and open a pull request. 🚀 24 | -------------------------------------------------------------------------------- /apps/obsidian/.env.example: -------------------------------------------------------------------------------- 1 | # OBSIDIAN_PLUGIN_PATH="path/to/your/obsidian/plugins/folder" -------------------------------------------------------------------------------- /apps/obsidian/docs/media/BRAT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/BRAT.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/add-beta-plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/add-beta-plugin.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/add-datacore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/add-datacore.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/add-discourse-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/add-discourse-graph.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/add-node-types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/add-node-types.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/add-relationship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/add-relationship.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/choose-discourse-relations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/choose-discourse-relations.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/choose-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/choose-template.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/command.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/create-template-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/create-template-file.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/discourse-relations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/discourse-relations.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/final-relation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/final-relation.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/new-folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/new-folder.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/node-created.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/node-created.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/node-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/node-menu.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/open-dg-context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/open-dg-context.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/relation-types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/relation-types.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/relationship-created.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/relationship-created.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/right-click-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/right-click-menu.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/search.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/select.png -------------------------------------------------------------------------------- /apps/obsidian/docs/media/template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/docs/media/template.png -------------------------------------------------------------------------------- /apps/obsidian/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "@discourse-graph/obsidian", 3 | "name": "Discourse Graph", 4 | "version": "0.1.0", 5 | "minAppVersion": "1.7.0", 6 | "description": "Discourse Graph Plugin for Obsidian", 7 | "author": "Discourse Graphs", 8 | "authorUrl": "https://discoursegraphs.com", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /apps/obsidian/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@discourse-graphs/obsidian", 3 | "version": "0.1.0", 4 | "description": "Discourse Graph Plugin for obsidian.md", 5 | "main": "dist/main.js", 6 | "private": true, 7 | "scripts": { 8 | "dev": "tsx scripts/dev.ts", 9 | "build": "tsx scripts/build.ts" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "Apache-2.0", 14 | "devDependencies": { 15 | "@types/node": "^16.11.6", 16 | "@typescript-eslint/eslint-plugin": "5.29.0", 17 | "@typescript-eslint/parser": "5.29.0", 18 | "autoprefixer": "^10.4.21", 19 | "builtin-modules": "3.3.0", 20 | "esbuild": "0.17.3", 21 | "obsidian": "^1.7.2", 22 | "postcss": "^8.5.3", 23 | "tailwindcss": "^3.4.17", 24 | "tslib": "2.4.0", 25 | "tsx": "^4.19.2", 26 | "typescript": "4.7.4" 27 | }, 28 | "dependencies": { 29 | "react": "^19.0.0", 30 | "react-dom": "^19.0.0", 31 | "tailwindcss-animate": "^1.0.7" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/obsidian/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/obsidian/scripts/build.ts: -------------------------------------------------------------------------------- 1 | import { compile } from "./compile"; 2 | 3 | const build = async () => { 4 | process.env = { 5 | ...process.env, 6 | NODE_ENV: process.env.NODE_ENV || "production", 7 | }; 8 | 9 | console.log("Compiling ..."); 10 | try { 11 | await compile({}); 12 | console.log("Compiling complete"); 13 | } catch (error) { 14 | console.error("Build failed on compile:", error); 15 | process.exit(1); 16 | } 17 | }; 18 | 19 | const main = async () => { 20 | try { 21 | await build(); 22 | } catch (error) { 23 | console.error(error); 24 | process.exit(1); 25 | } 26 | }; 27 | if (require.main === module) main(); 28 | -------------------------------------------------------------------------------- /apps/obsidian/scripts/dev.ts: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import dotenv from "dotenv"; 3 | import { compile, args } from "./compile"; 4 | 5 | dotenv.config(); 6 | 7 | const dev = () => { 8 | process.env.NODE_ENV = process.env.NODE_ENV || "development"; 9 | return new Promise((resolve) => { 10 | compile({ 11 | opts: args, 12 | builder: (opts: esbuild.BuildOptions) => 13 | esbuild.context(opts).then((esb) => { 14 | esb.watch(); 15 | // Cleanup on process termination 16 | const cleanup = () => { 17 | esb.dispose(); 18 | resolve(0); 19 | }; 20 | process.on('SIGINT', cleanup); 21 | process.on('SIGTERM', cleanup); 22 | return esb; 23 | }), 24 | }); 25 | process.on("exit", resolve); 26 | }); 27 | }; 28 | 29 | const main = async () => { 30 | try { 31 | await dev(); 32 | } catch (error) { 33 | console.error(error); 34 | process.exit(1); 35 | } 36 | }; 37 | if (require.main === module) main(); 38 | -------------------------------------------------------------------------------- /apps/obsidian/src/components/AppContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react"; 2 | import { App } from "obsidian"; 3 | 4 | export const AppContext = createContext(undefined); 5 | 6 | export const useApp = (): App | undefined => { 7 | return useContext(AppContext); 8 | }; 9 | 10 | export const ContextProvider = ({ 11 | app, 12 | children, 13 | }: { 14 | app: App; 15 | children: React.ReactNode; 16 | }) => { 17 | return {children}; 18 | }; 19 | -------------------------------------------------------------------------------- /apps/obsidian/src/components/ConfirmationModal.tsx: -------------------------------------------------------------------------------- 1 | import { App, Modal } from "obsidian"; 2 | 3 | interface ConfirmationModalProps { 4 | title: string; 5 | message: string; 6 | onConfirm: () => void; 7 | } 8 | 9 | export class ConfirmationModal extends Modal { 10 | private title: string; 11 | private message: string; 12 | private onConfirm: () => void; 13 | 14 | constructor(app: App, { title, message, onConfirm }: ConfirmationModalProps) { 15 | super(app); 16 | this.title = title; 17 | this.message = message; 18 | this.onConfirm = onConfirm; 19 | } 20 | 21 | onOpen() { 22 | const { contentEl } = this; 23 | 24 | contentEl.createEl("h2", { text: this.title }); 25 | contentEl.createEl("p", { text: this.message }); 26 | 27 | const buttonContainer = contentEl.createDiv({ 28 | cls: "modal-button-container", 29 | }); 30 | 31 | buttonContainer 32 | .createEl("button", { 33 | text: "Cancel", 34 | cls: "mod-normal", 35 | }) 36 | .addEventListener("click", () => { 37 | this.close(); 38 | }); 39 | 40 | buttonContainer 41 | .createEl("button", { 42 | text: "Confirm", 43 | cls: "mod-warning", 44 | }) 45 | .addEventListener("click", () => { 46 | this.onConfirm(); 47 | this.close(); 48 | }); 49 | } 50 | 51 | onClose() { 52 | const { contentEl } = this; 53 | contentEl.empty(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /apps/obsidian/src/components/DropdownSelect.tsx: -------------------------------------------------------------------------------- 1 | import { DropdownComponent } from "obsidian"; 2 | import { useEffect, useRef } from "react"; 3 | 4 | type DropdownSelectProps = { 5 | options: T[]; 6 | onSelect: (item: T | null) => void; 7 | placeholder?: string; 8 | getItemText: (item: T) => string; 9 | }; 10 | 11 | const DropdownSelect = ({ 12 | options, 13 | onSelect, 14 | placeholder = "Select...", 15 | getItemText, 16 | }: DropdownSelectProps) => { 17 | const containerRef = useRef(null); 18 | const dropdownRef = useRef(null); 19 | 20 | useEffect(() => { 21 | if (!containerRef.current) return; 22 | 23 | if (!dropdownRef.current) { 24 | dropdownRef.current = new DropdownComponent(containerRef.current); 25 | } 26 | 27 | const dropdown = dropdownRef.current; 28 | const currentValue = dropdown.getValue(); 29 | 30 | dropdown.selectEl.empty(); 31 | 32 | dropdown.addOption("", placeholder); 33 | 34 | options.forEach((option) => { 35 | const text = getItemText(option); 36 | dropdown.addOption(text, text); 37 | }); 38 | 39 | if ( 40 | currentValue && 41 | options.some((opt) => getItemText(opt) === currentValue) 42 | ) { 43 | dropdown.setValue(currentValue); 44 | } 45 | 46 | const onChangeHandler = (value: string) => { 47 | const selectedOption = 48 | options.find((opt) => getItemText(opt) === value) || null; 49 | dropdown.setValue(value); 50 | onSelect(selectedOption); 51 | }; 52 | 53 | dropdown.onChange(onChangeHandler); 54 | 55 | if (options && options.length === 1 && !currentValue) { 56 | dropdown.setValue(getItemText(options[0] as T)); 57 | } 58 | 59 | return () => { 60 | dropdown.onChange(() => {}); 61 | }; 62 | }, [options, onSelect, getItemText, placeholder]); 63 | 64 | useEffect(() => { 65 | return () => { 66 | if (dropdownRef.current) { 67 | dropdownRef.current.selectEl.empty(); 68 | dropdownRef.current = null; 69 | } 70 | }; 71 | }, []); 72 | 73 | return
; 74 | }; 75 | 76 | export default DropdownSelect; 77 | -------------------------------------------------------------------------------- /apps/obsidian/src/components/NodeTypeModal.tsx: -------------------------------------------------------------------------------- 1 | import { App, Editor, SuggestModal, TFile, Notice } from "obsidian"; 2 | import { DiscourseNode } from "~/types"; 3 | import { processTextToDiscourseNode } from "~/utils/createNodeFromSelectedText"; 4 | 5 | export class NodeTypeModal extends SuggestModal { 6 | constructor( 7 | app: App, 8 | private editor: Editor, 9 | private nodeTypes: DiscourseNode[], 10 | ) { 11 | super(app); 12 | } 13 | 14 | getItemText(item: DiscourseNode): string { 15 | return item.name; 16 | } 17 | 18 | getSuggestions() { 19 | const query = this.inputEl.value.toLowerCase(); 20 | return this.nodeTypes.filter((node) => 21 | this.getItemText(node).toLowerCase().includes(query), 22 | ); 23 | } 24 | 25 | renderSuggestion(nodeType: DiscourseNode, el: HTMLElement) { 26 | el.createEl("div", { text: nodeType.name }); 27 | } 28 | 29 | async onChooseSuggestion(nodeType: DiscourseNode) { 30 | await processTextToDiscourseNode({ 31 | app: this.app, 32 | editor: this.editor, 33 | nodeType, 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /apps/obsidian/src/components/PluginContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, ReactNode } from "react"; 2 | import type DiscourseGraphPlugin from "~/index"; 3 | 4 | export const PluginContext = createContext( 5 | undefined, 6 | ); 7 | 8 | export const usePlugin = (): DiscourseGraphPlugin => { 9 | const plugin = useContext(PluginContext); 10 | if (!plugin) { 11 | throw new Error("usePlugin must be used within a PluginProvider"); 12 | } 13 | return plugin; 14 | }; 15 | 16 | export const PluginProvider = ({ 17 | plugin, 18 | children, 19 | }: { 20 | plugin: DiscourseGraphPlugin; 21 | children: ReactNode; 22 | }) => { 23 | return ( 24 | {children} 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /apps/obsidian/src/constants.ts: -------------------------------------------------------------------------------- 1 | import { DiscourseNode, DiscourseRelationType, Settings } from "~/types"; 2 | import generateUid from "~/utils/generateUid"; 3 | 4 | export const DEFAULT_NODE_TYPES: Record = { 5 | Question: { 6 | id: generateUid("node"), 7 | name: "Question", 8 | format: "QUE - {content}", 9 | }, 10 | Claim: { 11 | id: generateUid("node"), 12 | name: "Claim", 13 | format: "CLM - {content}", 14 | }, 15 | Evidence: { 16 | id: generateUid("node"), 17 | name: "Evidence", 18 | format: "EVD - {content}", 19 | }, 20 | }; 21 | export const DEFAULT_RELATION_TYPES: Record = { 22 | supports: { 23 | id: generateUid("relation"), 24 | label: "supports", 25 | complement: "is supported by", 26 | }, 27 | opposes: { 28 | id: generateUid("relation"), 29 | label: "opposes", 30 | complement: "is opposed by", 31 | }, 32 | informs: { 33 | id: generateUid("relation"), 34 | label: "informs", 35 | complement: "is informed by", 36 | }, 37 | }; 38 | 39 | export const DEFAULT_SETTINGS: Settings = { 40 | nodeTypes: Object.values(DEFAULT_NODE_TYPES), 41 | relationTypes: Object.values(DEFAULT_RELATION_TYPES), 42 | discourseRelations: [ 43 | { 44 | sourceId: DEFAULT_NODE_TYPES.Evidence!.id, 45 | destinationId: DEFAULT_NODE_TYPES.Question!.id, 46 | relationshipTypeId: DEFAULT_RELATION_TYPES.informs!.id, 47 | }, 48 | { 49 | sourceId: DEFAULT_NODE_TYPES.Evidence!.id, 50 | destinationId: DEFAULT_NODE_TYPES.Claim!.id, 51 | relationshipTypeId: DEFAULT_RELATION_TYPES.supports!.id, 52 | }, 53 | { 54 | sourceId: DEFAULT_NODE_TYPES.Evidence!.id, 55 | destinationId: DEFAULT_NODE_TYPES.Claim!.id, 56 | relationshipTypeId: DEFAULT_RELATION_TYPES.opposes!.id, 57 | }, 58 | ], 59 | }; 60 | -------------------------------------------------------------------------------- /apps/obsidian/src/styles/style.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/obsidian/src/styles/style.css -------------------------------------------------------------------------------- /apps/obsidian/src/types.ts: -------------------------------------------------------------------------------- 1 | export type DiscourseNode = { 2 | id: string; 3 | name: string; 4 | format: string; 5 | template?: string; 6 | shortcut?: string; 7 | color?: string; 8 | }; 9 | 10 | export type DiscourseRelationType = { 11 | id: string; 12 | label: string; 13 | complement: string; 14 | }; 15 | 16 | export type DiscourseRelation = { 17 | sourceId: string; 18 | destinationId: string; 19 | relationshipTypeId: string; 20 | }; 21 | 22 | export type Settings = { 23 | nodeTypes: DiscourseNode[]; 24 | discourseRelations: DiscourseRelation[]; 25 | relationTypes: DiscourseRelationType[]; 26 | }; 27 | 28 | export const VIEW_TYPE_DISCOURSE_CONTEXT = "discourse-context-view"; -------------------------------------------------------------------------------- /apps/obsidian/src/utils/generateUid.ts: -------------------------------------------------------------------------------- 1 | import { nanoid } from "nanoid"; 2 | 3 | const generateUid = (prefix = "dg") => { 4 | return `${prefix}_${nanoid()}`; 5 | }; 6 | 7 | export default generateUid; 8 | -------------------------------------------------------------------------------- /apps/obsidian/src/utils/getDiscourseNodeFormatExpression.ts: -------------------------------------------------------------------------------- 1 | export const getDiscourseNodeFormatExpression = (format: string) => 2 | format 3 | ? new RegExp( 4 | `^${format 5 | .replace(/(\[|\]|\?|\.|\+)/g, "\\$1") 6 | .replace(/{[a-zA-Z]+}/g, "(.*?)")}$`, 7 | "s", 8 | ) 9 | : /$^/; 10 | -------------------------------------------------------------------------------- /apps/obsidian/src/utils/registerCommands.ts: -------------------------------------------------------------------------------- 1 | import { Editor, Notice } from "obsidian"; 2 | import type DiscourseGraphPlugin from "~/index"; 3 | import { NodeTypeModal } from "~/components/NodeTypeModal"; 4 | 5 | export const registerCommands = (plugin: DiscourseGraphPlugin) => { 6 | plugin.addCommand({ 7 | id: "open-node-type-menu", 8 | name: "Open Node Type Menu", 9 | hotkeys: [{ modifiers: ["Mod"], key: "\\" }], 10 | editorCallback: (editor: Editor) => { 11 | if (!editor.getSelection()) { 12 | new Notice("Please select some text to create a discourse node", 3000); 13 | return; 14 | } 15 | 16 | new NodeTypeModal(plugin.app, editor, plugin.settings.nodeTypes).open(); 17 | }, 18 | }); 19 | 20 | plugin.addCommand({ 21 | id: "toggle-discourse-context", 22 | name: "Toggle Discourse Context", 23 | callback: () => { 24 | plugin.toggleDiscourseContextView(); 25 | }, 26 | }); 27 | 28 | plugin.addCommand({ 29 | id: "open-discourse-graph-settings", 30 | name: "Open Discourse Graph Settings", 31 | callback: () => { 32 | // plugin.app.setting is an unofficial API 33 | const setting = (plugin.app as any).setting; 34 | setting.open(); 35 | setting.openTabById(plugin.manifest.id); 36 | }, 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /apps/obsidian/src/utils/validateNodeFormat.ts: -------------------------------------------------------------------------------- 1 | export const validateNodeFormat = ( 2 | format: string, 3 | ): { 4 | isValid: boolean; 5 | error?: string; 6 | } => { 7 | if (!format) { 8 | return { 9 | isValid: false, 10 | error: "Format cannot be empty", 11 | }; 12 | } 13 | 14 | if (format.includes("[[") || format.includes("]]")) { 15 | return { 16 | isValid: false, 17 | error: "Format should not contain double brackets [[ or ]]", 18 | }; 19 | } 20 | 21 | const hasVariable = /{[a-zA-Z]+}/.test(format); 22 | if (!hasVariable) { 23 | return { 24 | isValid: false, 25 | error: "Format must contain at least one variable in {varName} format", 26 | }; 27 | } 28 | 29 | return { isValid: true }; 30 | }; 31 | -------------------------------------------------------------------------------- /apps/obsidian/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind components; 2 | @tailwind utilities; 3 | 4 | .accent-border-bottom { 5 | border-bottom: 2px solid var(--interactive-accent) !important; 6 | } 7 | 8 | .dg-h1 { 9 | @apply text-3xl font-bold mb-4; 10 | } 11 | .dg-h2 { 12 | @apply text-2xl font-bold mb-3; 13 | } 14 | .dg-h3 { 15 | @apply text-xl font-semibold mb-2; 16 | } 17 | .dg-h4 { 18 | @apply text-lg font-bold mb-2; 19 | } 20 | -------------------------------------------------------------------------------- /apps/obsidian/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Pick = { 4 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 5 | theme: { 6 | extend: { 7 | // Map Obsidian CSS variables to Tailwind classes 8 | colors: { 9 | // Background colors 10 | primary: "var(--background-primary)", 11 | secondary: "var(--background-secondary)", 12 | tertiary: "var(--background-tertiary)", 13 | 14 | // Text colors 15 | normal: "var(--text-normal)", 16 | muted: "var(--text-muted)", 17 | "accent-text": "var(--text-accent)", 18 | error: "var(--text-error)", 19 | "on-accent": "var(--text-on-accent)", 20 | 21 | // Interactive elements 22 | accent: { 23 | DEFAULT: "var(--interactive-accent)", 24 | hover: "var(--interactive-accent-hover)", 25 | }, 26 | 27 | // Modifiers 28 | "modifier-border": "var(--background-modifier-border)", 29 | "modifier-form-field": "var(--background-modifier-form-field)", 30 | "modifier-error": "var(--background-modifier-error)", 31 | "modifier-hover": "var(--background-modifier-hover)", 32 | }, 33 | borderColor: { 34 | DEFAULT: "var(--background-modifier-border)", 35 | }, 36 | }, 37 | }, 38 | }; 39 | 40 | export default config; 41 | -------------------------------------------------------------------------------- /apps/obsidian/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/react-library.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "inlineSourceMap": true, 6 | "inlineSources": true, 7 | "module": "ESNext", 8 | "target": "ES6", 9 | "allowJs": true, 10 | "noImplicitAny": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "isolatedModules": true, 14 | "strictNullChecks": true, 15 | "lib": ["DOM", "ES5", "ES6", "ES7"] 16 | }, 17 | "include": ["**/*.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /apps/roam/.env.example: -------------------------------------------------------------------------------- 1 | # BLOB_READ_WRITE_TOKEN= -------------------------------------------------------------------------------- /apps/roam/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .env 4 | extension.js 5 | extension.css 6 | CHANGELOG.md -------------------------------------------------------------------------------- /apps/roam/README.md: -------------------------------------------------------------------------------- 1 | # Discourse Graphs 2 | 3 | The Discourse Graph extension enables Roam users to seamlessly add additional semantic structure to their notes, including specified page types and link types that model scientific discourse, to enable more complex and structured knowledge synthesis work, such as a complex interdisciplinary literature review, and enhanced collaboration with others on this work. 4 | 5 | For more information about Discourse Graphs, check out our website at [https://discoursegraphs.com](https://discoursegraphs.com) 6 | 7 | ## Table of Contents 8 | 9 | **WIP** 10 | 11 | - [Discourse Graphs](https://discoursegraphs.com/docs/roam) documentation 12 | - [Query Builder](https://github.com/DiscourseGraphs/discourse-graph/blob/main/apps/roam/docs/query-builder.md) documentation 13 | 14 | ## Nomenclature 15 | 16 | There are some important terms to know and have exact definitions on since they will be used throughout the docs. 17 | 18 | - `Page` - A Page is anything in Roam that was created with `[[brackets]]`, `#hashtag`, `#[[hashtag with brackets]]`, or `Attribute::`. Clicking on these links in your graph takes you to its designated page, each with its own unique title, and they have no parent. 19 | - `Block` - A bullet or line of text in Roam. While you can also go to pages that have a zoomed in block view, their content is not unique, and they always have one parent. 20 | - `Node` - A superset of `Block`s and `Page`s. 21 | 22 | ## Analytics & Privacy 23 | 24 | The Discourse Graphs extension uses PostHog analytics to help us understand how the extension is being used and improve it. We take privacy seriously and: 25 | 26 | - Do not track encrypted graphs 27 | - Do not track offline graphs 28 | - Do not automatically capture page views or clicks 29 | - Only track specific user actions like creating Discourse Graph nodes, running queries, and using the canvas 30 | 31 | This minimal tracking helps us understand how researchers use the extension so we can make it better while respecting your privacy. 32 | -------------------------------------------------------------------------------- /apps/roam/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | npm install 4 | npx turbo run build --filter=roam 5 | cp dist/* . -------------------------------------------------------------------------------- /apps/roam/docs/media/discourse-graph-linked-ref.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/roam/docs/media/discourse-graph-linked-ref.jpg -------------------------------------------------------------------------------- /apps/roam/docs/media/discourse-graph-stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/roam/docs/media/discourse-graph-stop.png -------------------------------------------------------------------------------- /apps/roam/docs/media/discourse-graph-stopped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/roam/docs/media/discourse-graph-stopped.png -------------------------------------------------------------------------------- /apps/roam/docs/media/settings-browse-extensions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/roam/docs/media/settings-browse-extensions.png -------------------------------------------------------------------------------- /apps/roam/docs/media/settings-enable-discourse-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/roam/docs/media/settings-enable-discourse-graph.png -------------------------------------------------------------------------------- /apps/roam/docs/media/settings-install-query-builder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/roam/docs/media/settings-install-query-builder.png -------------------------------------------------------------------------------- /apps/roam/docs/media/settings-roam-depot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/roam/docs/media/settings-roam-depot.png -------------------------------------------------------------------------------- /apps/roam/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roam", 3 | "version": "0.13.0", 4 | "description": "Discourse Graph Plugin for roamresearch.com", 5 | "scripts": { 6 | "postinstall": "patch-package", 7 | "dev": "tsx scripts/dev.ts", 8 | "build": "tsx scripts/build.ts", 9 | "deploy": "tsx scripts/deploy.ts", 10 | "publish": "tsx scripts/publish.ts" 11 | }, 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@repo/tailwind-config": "*", 15 | "@types/contrast-color": "^1.0.0", 16 | "@types/react-vertical-timeline-component": "^3.3.3", 17 | "axios": "^0.27.2", 18 | "esbuild": "0.17.14", 19 | "patch-package": "^6.5.1", 20 | "tsx": "^4.19.2" 21 | }, 22 | "//": "axios dep temporary - need to fix the dep in underlying libraries", 23 | "tags": [ 24 | "queries", 25 | "widgets" 26 | ], 27 | "dependencies": { 28 | "@octokit/auth-app": "^7.1.4", 29 | "@octokit/core": "^6.1.3", 30 | "@repo/types": "*", 31 | "@tldraw/tldraw": "^2.0.0-alpha.12", 32 | "@vercel/blob": "^0.27.0", 33 | "contrast-color": "^1.0.1", 34 | "cytoscape-navigator": "^2.0.1", 35 | "nanoid": "2.0.4", 36 | "posthog-js": "^1.203.2", 37 | "react-charts": "^3.0.0-beta.48", 38 | "react-draggable": "^4.4.5", 39 | "react-in-viewport": "^1.0.0-alpha.20", 40 | "react-vertical-timeline-component": "^3.5.2", 41 | "roamjs-components": "^0.85.1", 42 | "signia-react": "^0.1.1" 43 | }, 44 | "overrides": { 45 | "@tldraw/tldraw": { 46 | "react": "^17.0.2", 47 | "react-dom": "^17.0.2" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /apps/roam/patches/@playwright+test+1.29.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@playwright/test/lib/transform.js b/node_modules/@playwright/test/lib/transform.js 2 | index 9910124..948f529 100644 3 | --- a/node_modules/@playwright/test/lib/transform.js 4 | +++ b/node_modules/@playwright/test/lib/transform.js 5 | @@ -149,6 +149,17 @@ function js2ts(resolved) { 6 | if (!_fs.default.existsSync(resolved) && _fs.default.existsSync(tsResolved)) return tsResolved; 7 | } 8 | } 9 | + 10 | +function mebuild(filename, outfile) { 11 | + require("esbuild").buildSync({ 12 | + entryPoints: [filename], 13 | + format: "cjs", 14 | + outfile, 15 | + sourcemap: "inline", 16 | + }); 17 | + return {code: _fs.default.readFileSync(outfile).toString()}; 18 | +} 19 | + 20 | function transformHook(code, filename, moduleUrl) { 21 | // If we are not TypeScript and there is no applicable preprocessor - bail out. 22 | const isModule = !!moduleUrl; 23 | @@ -166,7 +177,9 @@ function transformHook(code, filename, moduleUrl) { 24 | const { 25 | babelTransform 26 | } = require('./babelBundle'); 27 | - const result = babelTransform(filename, isTypeScript, isModule, hasPreprocessor ? scriptPreprocessor : undefined, [require.resolve('./tsxTransform')]); 28 | + const result =filename.endsWith('.tsx') ? 29 | + mebuild(filename,codePath) 30 | + : babelTransform(filename, isTypeScript, isModule, hasPreprocessor ? scriptPreprocessor : undefined, [require.resolve('./tsxTransform')]); 31 | if (result.code) { 32 | _fs.default.mkdirSync(_path.default.dirname(cachePath), { 33 | recursive: true 34 | -------------------------------------------------------------------------------- /apps/roam/patches/@tldraw+primitives+2.0.0-canary.ffda4cfb.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@tldraw/primitives/dist/esm/lib/freehand/setStrokePointRadii.mjs b/node_modules/@tldraw/primitives/dist/esm/lib/freehand/setStrokePointRadii.mjs 2 | index 0bbfddb..ea8a571 100644 3 | --- a/node_modules/@tldraw/primitives/dist/esm/lib/freehand/setStrokePointRadii.mjs 4 | +++ b/node_modules/@tldraw/primitives/dist/esm/lib/freehand/setStrokePointRadii.mjs 5 | @@ -12,7 +12,8 @@ function setStrokePointRadii(strokePoints, options) { 6 | } = options; 7 | const { easing: taperStartEase = EASINGS.easeOutQuad } = start; 8 | const { easing: taperEndEase = EASINGS.easeOutCubic } = end; 9 | - const totalLength = strokePoints[strokePoints.length - 1].runningLength; 10 | + const totalLength = strokePoints[strokePoints.length - 1]?.runningLength || 0; 11 | + if (!totalLength) return strokePoints; 12 | let firstRadius; 13 | let prevPressure = strokePoints[0].pressure; 14 | let strokePoint; 15 | -------------------------------------------------------------------------------- /apps/roam/patches/@tldraw+tldraw++@tldraw+ui+2.0.0-canary.ffda4cfb.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@tldraw/tldraw/node_modules/@tldraw/ui/dist/cjs/index.d.ts b/node_modules/@tldraw/tldraw/node_modules/@tldraw/ui/dist/cjs/index.d.ts 2 | index 0f93cb5..0e42591 100644 3 | --- a/node_modules/@tldraw/tldraw/node_modules/@tldraw/ui/dist/cjs/index.d.ts 4 | +++ b/node_modules/@tldraw/tldraw/node_modules/@tldraw/ui/dist/cjs/index.d.ts 5 | @@ -737,6 +737,7 @@ export declare interface ToolItem { 6 | meta?: { 7 | [key: string]: any; 8 | }; 9 | + style?: CSSProperties 10 | } 11 | 12 | /** @public */ 13 | diff --git a/node_modules/@tldraw/tldraw/node_modules/@tldraw/ui/dist/esm/lib/components/Toolbar/Toolbar.mjs b/node_modules/@tldraw/tldraw/node_modules/@tldraw/ui/dist/esm/lib/components/Toolbar/Toolbar.mjs 14 | index 8c9610d..03a9b7c 100644 15 | --- a/node_modules/@tldraw/tldraw/node_modules/@tldraw/ui/dist/esm/lib/components/Toolbar/Toolbar.mjs 16 | +++ b/node_modules/@tldraw/tldraw/node_modules/@tldraw/ui/dist/esm/lib/components/Toolbar/Toolbar.mjs 17 | @@ -151,7 +151,7 @@ const OverflowToolsContent = track(function OverflowToolsContent2({ 18 | toolbarItems 19 | }) { 20 | const msg = useTranslation(); 21 | - return /* @__PURE__ */ jsx("div", { className: "tlui-button-grid__four tlui-button-grid__reverse", children: toolbarItems.map(({ toolItem: { id, meta, kbd, label, onSelect, icon } }) => { 22 | + return /* @__PURE__ */ jsx("div", { className: "tlui-button-grid__four tlui-button-grid__reverse", children: toolbarItems.map(({ toolItem: { id, meta, kbd, label, onSelect, icon, style } }) => { 23 | return /* @__PURE__ */ jsx( 24 | M.Item, 25 | { 26 | @@ -162,7 +162,8 @@ const OverflowToolsContent = track(function OverflowToolsContent2({ 27 | "aria-label": label, 28 | onClick: onSelect, 29 | title: label ? `${msg(label)} ${kbd ? kbdStr(kbd) : ""}` : "", 30 | - icon 31 | + icon, 32 | + style 33 | }, 34 | id 35 | ); 36 | @@ -188,7 +189,8 @@ function ToolbarButton({ 37 | onTouchStart: (e) => { 38 | e.preventDefault(); 39 | item.onSelect(); 40 | - } 41 | + }, 42 | + style: item.style, 43 | }, 44 | item.id 45 | ); 46 | -------------------------------------------------------------------------------- /apps/roam/patches/@tldraw+tlschema+2.0.0-canary.ffda4cfb.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@tldraw/tlschema/dist/esm/shapes/TLEmbedShape.mjs b/node_modules/@tldraw/tlschema/dist/esm/shapes/TLEmbedShape.mjs 2 | index 0c0783a..1136d46 100644 3 | --- a/node_modules/@tldraw/tlschema/dist/esm/shapes/TLEmbedShape.mjs 4 | +++ b/node_modules/@tldraw/tlschema/dist/esm/shapes/TLEmbedShape.mjs 5 | @@ -78,7 +78,7 @@ const EMBED_DEFINITIONS = [ 6 | { 7 | type: "tldraw", 8 | title: "tldraw", 9 | - hostnames: ["beta.tldraw.com", "lite.tldraw.com"], 10 | + hostnames: ["tldraw.com", "beta.tldraw.com", "lite.tldraw.com"], 11 | minWidth: 300, 12 | minHeight: 300, 13 | width: 720, 14 | -------------------------------------------------------------------------------- /apps/roam/scripts/build.ts: -------------------------------------------------------------------------------- 1 | import { compile } from "./compile"; 2 | 3 | const build = async () => { 4 | process.env = { 5 | ...process.env, 6 | NODE_ENV: process.env.NODE_ENV || "production", 7 | }; 8 | 9 | console.log("Compiling ..."); 10 | try { 11 | await compile({}); 12 | console.log("Compiling complete"); 13 | } catch (error) { 14 | console.error("Build failed on compile:", error); 15 | process.exit(1); 16 | } 17 | }; 18 | 19 | const main = async () => { 20 | try { 21 | await build(); 22 | } catch (error) { 23 | console.error(error); 24 | process.exit(1); 25 | } 26 | }; 27 | if (require.main === module) main(); 28 | -------------------------------------------------------------------------------- /apps/roam/scripts/dev.ts: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import dotenv from "dotenv"; 3 | import { compile, args } from "./compile"; 4 | 5 | dotenv.config(); 6 | 7 | const dev = () => { 8 | process.env.NODE_ENV = process.env.NODE_ENV || "development"; 9 | return new Promise((resolve) => { 10 | compile({ 11 | opts: args, 12 | builder: (opts: esbuild.BuildOptions) => 13 | esbuild.context(opts).then((esb) => esb.watch()), 14 | }); 15 | process.on("exit", resolve); 16 | }); 17 | }; 18 | 19 | const main = async () => { 20 | try { 21 | await dev(); 22 | } catch (error) { 23 | console.error(error); 24 | process.exit(1); 25 | } 26 | }; 27 | if (require.main === module) main(); 28 | -------------------------------------------------------------------------------- /apps/roam/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DefaultFilters } from "./settings/DefaultFilters"; 2 | export { default as DiscourseContext } from "./DiscourseContext"; 3 | export { default as DiscourseContextOverlay } from "./DiscourseContextOverlay"; 4 | export { default as DiscourseNodeAttributes } from "./settings/DiscourseNodeAttributes"; 5 | export { default as DiscourseNodeCanvasSettings } from "./settings/DiscourseNodeCanvasSettings"; 6 | export { default as DiscourseNodeIndex } from "./settings/DiscourseNodeIndex"; 7 | export { default as DiscourseNodeMenu } from "./DiscourseNodeMenu"; 8 | export { default as DiscourseNodeSpecification } from "./settings/DiscourseNodeSpecification"; 9 | export { default as Export } from "./Export"; 10 | export { render as ExportDiscourseContext } from "./ExportDiscourseContext"; 11 | export { ExportGithub as ExportGithub } from "./ExportGithub"; 12 | export { default as ImportDialog } from "./ImportDialog"; 13 | export { default as LivePreview } from "./LivePreview"; 14 | export { render as renderQueryDrawer } from "./QueryDrawer"; 15 | export { openQueryDrawer as QueryDrawer } from "./QueryDrawer"; 16 | export { default as QueryEditor } from "./QueryEditor"; 17 | export { default as QueryBuilder } from "./QueryBuilder"; 18 | export { default as QueryPagesPanel } from "./settings/QueryPagesPanel"; 19 | export { default as ResizableDrawer } from "./ResizableDrawer"; 20 | export { default as ResultsView } from "./results-view/ResultsView"; 21 | export { default as Timeline } from "./results-view/Timeline"; 22 | -------------------------------------------------------------------------------- /apps/roam/src/components/results-view/Charts.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Chart, AxisOptions, AxisOptionsBase } from "react-charts"; 3 | import { Result } from "roamjs-components/types/query-builder"; 4 | import { Column } from "~/utils/types"; 5 | 6 | type ChartData = [Result[string], Result[string]]; 7 | 8 | const Charts = ({ 9 | data, 10 | type, 11 | columns, 12 | }: { 13 | type: AxisOptionsBase["elementType"]; 14 | data: Result[]; 15 | columns: Column[]; 16 | }): JSX.Element => { 17 | const chartData = React.useMemo( 18 | () => 19 | columns.slice(1).map((col) => { 20 | return { 21 | label: col.key, 22 | data: data.map((d) => [d[columns[0].key], d[col.key]] as ChartData), 23 | }; 24 | }), 25 | [data, columns], 26 | ); 27 | const primaryAxis = React.useMemo>( 28 | () => ({ 29 | primary: true, 30 | type: "timeLocal", 31 | position: "bottom" as const, 32 | getValue: ([d]) => 33 | d instanceof Date 34 | ? d 35 | : typeof d === "string" 36 | ? window.roamAlphaAPI.util.pageTitleToDate(d) 37 | : new Date(d), 38 | }), 39 | [], 40 | ); 41 | const secondaryAxes = React.useMemo[]>( 42 | () => 43 | columns.slice(1).map(() => ({ 44 | type: "linear", 45 | position: "left" as const, 46 | getValue: (d) => Number(d[1]) || 0, 47 | elementType: type, 48 | })), 49 | [type], 50 | ); 51 | 52 | return Object.keys(primaryAxis).length !== 0 && !secondaryAxes.length ? ( 53 |

54 | You need to have at least two selections for this layout 55 | to work, where the first is a selection that returns{" "} 56 | date values and all subsequent selections return{" "} 57 | numeric values. 58 |

59 | ) : ( 60 |
61 | 62 |
63 | ); 64 | }; 65 | 66 | export default Charts; 67 | -------------------------------------------------------------------------------- /apps/roam/src/components/settings/DiscourseNodeIndex.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { Spinner } from "@blueprintjs/core"; 3 | import ExtensionApiContextProvider from "roamjs-components/components/ExtensionApiContext"; 4 | import type { OnloadArgs } from "roamjs-components/types/native"; 5 | import type { DiscourseNode } from "~/utils/getDiscourseNodes"; 6 | import QueryBuilder from "~/components/QueryBuilder"; 7 | import parseQuery, { DEFAULT_RETURN_NODE } from "~/utils/parseQuery"; 8 | import createBlock from "roamjs-components/writes/createBlock"; 9 | 10 | const NodeIndex = ({ 11 | parentUid, 12 | node, 13 | onloadArgs, 14 | }: { 15 | parentUid: string; 16 | node: DiscourseNode; 17 | onloadArgs: OnloadArgs; 18 | }) => { 19 | const initialQueryArgs = React.useMemo( 20 | () => parseQuery(parentUid), 21 | [parentUid], 22 | ); 23 | const [showQuery, setShowQuery] = React.useState( 24 | !!initialQueryArgs.conditions.length, 25 | ); 26 | useEffect(() => { 27 | if (!showQuery) { 28 | createBlock({ 29 | parentUid: initialQueryArgs.conditionsNodesUid, 30 | node: { 31 | text: "clause", 32 | children: [ 33 | { 34 | text: "source", 35 | children: [{ text: DEFAULT_RETURN_NODE }], 36 | }, 37 | { 38 | text: "relation", 39 | children: [{ text: "is a" }], 40 | }, 41 | { 42 | text: "target", 43 | children: [ 44 | { 45 | text: node.text, 46 | }, 47 | ], 48 | }, 49 | ], 50 | }, 51 | }).then(() => setShowQuery(true)); 52 | } 53 | }, [parentUid, initialQueryArgs, showQuery]); 54 | return ( 55 | 56 | {showQuery ? : } 57 | 58 | ); 59 | }; 60 | 61 | export default NodeIndex; 62 | -------------------------------------------------------------------------------- /apps/roam/src/components/settings/GeneralSettings.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import TextPanel from "roamjs-components/components/ConfigPanels/TextPanel"; 3 | import { getFormattedConfigTree } from "~/utils/discourseConfigRef"; 4 | import refreshConfigTree from "~/utils/refreshConfigTree"; 5 | import { DEFAULT_CANVAS_PAGE_FORMAT } from "~/index"; 6 | 7 | const DiscourseGraphHome = () => { 8 | const settings = useMemo(() => { 9 | refreshConfigTree(); 10 | return getFormattedConfigTree(); 11 | }, []); 12 | 13 | return ( 14 |
15 | 23 | 32 |
33 | ); 34 | }; 35 | 36 | export default DiscourseGraphHome; 37 | -------------------------------------------------------------------------------- /apps/roam/src/data/defaultDiscourseNodes.ts: -------------------------------------------------------------------------------- 1 | import { DiscourseNode } from "~/utils/getDiscourseNodes"; 2 | 3 | const INITIAL_NODE_VALUES: Partial[] = [ 4 | { 5 | type: "_CLM-node", 6 | format: "[[CLM]] - {content}", 7 | text: "Claim", 8 | shortcut: "C", 9 | graphOverview: true, 10 | canvasSettings: { 11 | color: "7DA13E", 12 | }, 13 | }, 14 | { 15 | type: "_QUE-node", 16 | format: "[[QUE]] - {content}", 17 | text: "Question", 18 | shortcut: "Q", 19 | graphOverview: true, 20 | canvasSettings: { 21 | color: "99890e", 22 | }, 23 | }, 24 | { 25 | type: "_EVD-node", 26 | format: "[[EVD]] - {content} - {Source}", 27 | text: "Evidence", 28 | shortcut: "E", 29 | graphOverview: true, 30 | canvasSettings: { 31 | color: "DB134A", 32 | }, 33 | }, 34 | { 35 | type: "_SRC-node", 36 | format: "@{content}", 37 | text: "Source", 38 | shortcut: "S", 39 | graphOverview: true, 40 | canvasSettings: { 41 | color: "9E9E9E", 42 | }, 43 | }, 44 | ]; 45 | 46 | export default INITIAL_NODE_VALUES; 47 | -------------------------------------------------------------------------------- /apps/roam/src/data/toastMessages.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const queryBuilderLoadedToast = ( 4 |
5 |

6 | Discourse Graph Not Loaded 7 |

8 |

9 | The Query Builder extension is already loaded elsewhere. 10 |

11 |

Having both loaded at the same time may cause issues.

12 |

13 | Please disable Query Builder 14 |
15 | then reload your graph. 16 |

17 |
18 | ); 19 | -------------------------------------------------------------------------------- /apps/roam/src/data/userSettings.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_PAGE_SIZE_KEY = "default-page-size"; 2 | export const DEFAULT_CANVAS_PAGE_FORMAT_KEY = "default-canvas-page-format"; 3 | export const HIDE_METADATA_KEY = "hide-metadata"; 4 | export const DEFAULT_FILTERS_KEY = "default-filters"; 5 | export const QUERY_BUILDER_SETTINGS_KEY = "query-builder-settings"; 6 | -------------------------------------------------------------------------------- /apps/roam/src/styles/settingsStyles.css: -------------------------------------------------------------------------------- 1 | .bp3-tab-copy { 2 | overflow: hidden; 3 | text-overflow: ellipsis; 4 | white-space: nowrap; 5 | word-wrap: normal; 6 | color: #182026; 7 | cursor: pointer; 8 | -webkit-box-flex: 0; 9 | -ms-flex: 0 0 auto; 10 | flex: 0 0 auto; 11 | font-size: 14px; 12 | line-height: 30px; 13 | max-width: 100%; 14 | position: relative; 15 | vertical-align: top; 16 | 17 | border-radius: 3px; 18 | padding: 0 10px; 19 | width: 100%; 20 | } 21 | -------------------------------------------------------------------------------- /apps/roam/src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" { 2 | const value: string; 3 | export default value; 4 | } 5 | 6 | declare module "cytoscape-navigator" { 7 | const value: (cy: cytoscape) => void; 8 | export default value; 9 | } 10 | 11 | declare module "react-in-viewport/dist/es/lib/useInViewport" { 12 | import { useInViewport } from "react-in-viewport"; 13 | export default useInViewport; 14 | } 15 | 16 | declare module "*.css" { 17 | const content: string; 18 | export default content; 19 | } 20 | -------------------------------------------------------------------------------- /apps/roam/src/utils/createInitialTldrawProps.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TLInstance, 3 | TLUser, 4 | TLDocument, 5 | TLPage, 6 | TLUserDocument, 7 | TLUserPresence, 8 | } from "@tldraw/tldraw"; 9 | import getCurrentUserUid from "roamjs-components/queries/getCurrentUserUid"; 10 | 11 | export const createInitialTldrawProps = () => { 12 | const instanceId = TLInstance.createId(); 13 | const userId = TLUser.createCustomId(getCurrentUserUid()); 14 | const documentId = TLDocument.createCustomId("document"); 15 | const pageId = TLPage.createId(); 16 | const userDocument = TLUserDocument.createId(); 17 | const userPresence = TLUserPresence.createId(); 18 | 19 | const userRecord: TLUser = { 20 | ...TLUser.createDefaultProperties(), 21 | typeName: "user", 22 | id: userId, 23 | }; 24 | const instanceRecord: TLInstance = { 25 | ...TLInstance.createDefaultProperties(), 26 | currentPageId: pageId, 27 | userId: userId, 28 | typeName: "instance", 29 | id: instanceId, 30 | }; 31 | const documentRecord: TLDocument = { 32 | ...TLDocument.createDefaultProperties(), 33 | typeName: "document", 34 | id: documentId, 35 | }; 36 | const pageRecord: TLPage = { 37 | // ...TLPage.createDefaultProperties(), doesn't add anything? 38 | index: "a1", 39 | name: "Page 1", 40 | typeName: "page", 41 | id: pageId, 42 | }; 43 | const userDocumentRecord: TLUserDocument = { 44 | ...TLUserDocument.createDefaultProperties(), 45 | userId: userId, 46 | typeName: "user_document", 47 | id: userDocument, 48 | }; 49 | const userPresenceRecord: TLUserPresence = { 50 | ...TLUserPresence.createDefaultProperties(), 51 | typeName: "user_presence", 52 | id: userPresence, 53 | userId: userId, 54 | }; 55 | 56 | const props = { 57 | [userId]: userRecord, 58 | [instanceId]: instanceRecord, 59 | ["document:document"]: documentRecord, 60 | [pageId]: pageRecord, 61 | [userDocument]: userDocumentRecord, 62 | [userPresence]: userPresenceRecord, 63 | }; 64 | return props; 65 | }; 66 | -------------------------------------------------------------------------------- /apps/roam/src/utils/createSettingsPanel.ts: -------------------------------------------------------------------------------- 1 | import { OnloadArgs } from "roamjs-components/types"; 2 | import { NodeMenuTriggerComponent } from "~/components/DiscourseNodeMenu"; 3 | import { SettingsPanel } from "~/components/settings/Settings"; 4 | 5 | export const createSettingsPanel = (onloadArgs: OnloadArgs) => { 6 | const { extensionAPI } = onloadArgs; 7 | extensionAPI.settings.panel.create({ 8 | tabTitle: "Discourse Graphs", 9 | settings: [ 10 | { 11 | id: "settings-popup", 12 | name: "Settings", 13 | description: "", 14 | action: { 15 | type: "reactComponent", 16 | component: () => SettingsPanel({ onloadArgs }), 17 | }, 18 | }, 19 | ], 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /apps/roam/src/utils/discourseConfigRef.ts: -------------------------------------------------------------------------------- 1 | import type { RoamBasicNode } from "roamjs-components/types"; 2 | import { 3 | getExportSettingsAndUids, 4 | StringSetting, 5 | ExportConfigWithUids, 6 | getUidAndStringSetting, 7 | } from "./getExportSettings"; 8 | import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/utils/renderNodeConfigPage"; 9 | import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; 10 | 11 | const configTreeRef: { 12 | tree: RoamBasicNode[]; 13 | nodes: { [uid: string]: { text: string; children: RoamBasicNode[] } }; 14 | } = { tree: [], nodes: {} }; 15 | 16 | type FormattedConfigTree = { 17 | settingsUid: string; 18 | grammarUid: string; 19 | relationsUid: string; 20 | nodesUid: string; 21 | trigger: StringSetting; 22 | export: ExportConfigWithUids; 23 | canvasPageFormat: StringSetting; 24 | }; 25 | 26 | export const getFormattedConfigTree = (): FormattedConfigTree => { 27 | const settingsUid = getPageUidByPageTitle(DISCOURSE_CONFIG_PAGE_TITLE); 28 | const grammarNode = configTreeRef.tree.find( 29 | (node) => node.text === "grammar", 30 | ); 31 | const relationsNode = grammarNode?.children.find( 32 | (node) => node.text === "relations", 33 | ); 34 | const nodesNode = grammarNode?.children.find((node) => node.text === "nodes"); 35 | 36 | return { 37 | settingsUid, 38 | grammarUid: grammarNode?.uid || "", 39 | relationsUid: relationsNode?.uid || "", 40 | nodesUid: nodesNode?.uid || "", 41 | trigger: getUidAndStringSetting({ 42 | tree: configTreeRef.tree, 43 | text: "trigger", 44 | }), 45 | export: getExportSettingsAndUids(), 46 | canvasPageFormat: getUidAndStringSetting({ 47 | tree: configTreeRef.tree, 48 | text: "Canvas Page Format", 49 | }), 50 | }; 51 | }; 52 | export default configTreeRef; 53 | -------------------------------------------------------------------------------- /apps/roam/src/utils/discourseNodeFormatToDatalog.ts: -------------------------------------------------------------------------------- 1 | import { DatalogClause } from "roamjs-components/types/native"; 2 | import conditionToDatalog from "./conditionToDatalog"; 3 | import getDiscourseNodeFormatExpression from "./getDiscourseNodeFormatExpression"; 4 | import type { DiscourseNode } from "./getDiscourseNodes"; 5 | import replaceDatalogVariables from "./replaceDatalogVariables"; 6 | 7 | const discourseNodeFormatToDatalog = ({ 8 | freeVar, 9 | ...node 10 | }: DiscourseNode & { 11 | freeVar: string; 12 | }): DatalogClause[] => { 13 | if (node.specification.length) { 14 | const clauses = node.specification.flatMap(conditionToDatalog); 15 | return replaceDatalogVariables([{ from: node.text, to: freeVar }], clauses); 16 | } 17 | return conditionToDatalog({ 18 | source: freeVar, 19 | relation: "has title", 20 | target: `/${getDiscourseNodeFormatExpression(node.format).source}/`, 21 | type: "clause", 22 | uid: window.roamAlphaAPI.util.generateUID(), 23 | }); 24 | }; 25 | 26 | export default discourseNodeFormatToDatalog; 27 | -------------------------------------------------------------------------------- /apps/roam/src/utils/extensionSettings.ts: -------------------------------------------------------------------------------- 1 | import getExtensionAPI from "roamjs-components/util/extensionApiContext"; 2 | 3 | export function getSetting(key: string, defaultValue?: T): T { 4 | const extensionAPI = getExtensionAPI(); 5 | const value = extensionAPI.settings.get(key); 6 | 7 | if (value !== undefined && value !== null) { 8 | return value as T; 9 | } 10 | return defaultValue as T; 11 | } 12 | 13 | export function setSetting(key: string, value: T): void { 14 | const extensionAPI = getExtensionAPI(); 15 | extensionAPI.settings.set(key, value); 16 | } 17 | -------------------------------------------------------------------------------- /apps/roam/src/utils/findDiscourseNode.ts: -------------------------------------------------------------------------------- 1 | import getDiscourseNodes, { DiscourseNode } from "./getDiscourseNodes"; 2 | import matchDiscourseNode from "./matchDiscourseNode"; 3 | 4 | const discourseNodeTypeCache: Record = {}; 5 | 6 | const findDiscourseNode = (uid = "", nodes = getDiscourseNodes()) => { 7 | if (typeof discourseNodeTypeCache[uid] !== "undefined") { 8 | return discourseNodeTypeCache[uid]; 9 | } 10 | 11 | const matchingNode = nodes.find((node) => 12 | matchDiscourseNode({ ...node, uid }), 13 | ); 14 | 15 | discourseNodeTypeCache[uid] = matchingNode || false; 16 | return discourseNodeTypeCache[uid]; 17 | }; 18 | export default findDiscourseNode; 19 | -------------------------------------------------------------------------------- /apps/roam/src/utils/gatherDatalogVariablesFromClause.ts: -------------------------------------------------------------------------------- 1 | import { DatalogClause } from "roamjs-components/types/native"; 2 | 3 | const gatherDatalogVariablesFromClause = ( 4 | clause: DatalogClause, 5 | ): Set => { 6 | if ( 7 | clause.type === "data-pattern" || 8 | clause.type === "fn-expr" || 9 | clause.type === "pred-expr" || 10 | clause.type === "rule-expr" 11 | ) { 12 | return new Set( 13 | [...clause.arguments] 14 | .filter((v) => v.type === "variable") 15 | .map((v) => v.value), 16 | ); 17 | } else if ( 18 | clause.type === "not-clause" || 19 | clause.type === "or-clause" || 20 | clause.type === "and-clause" 21 | ) { 22 | return new Set( 23 | clause.clauses.flatMap((c) => 24 | Array.from(gatherDatalogVariablesFromClause(c)), 25 | ), 26 | ); 27 | } else if ( 28 | clause.type === "not-join-clause" || 29 | clause.type === "or-join-clause" 30 | ) { 31 | return new Set(clause.variables.map((c) => c.value)); 32 | } 33 | return new Set(); 34 | }; 35 | 36 | export default gatherDatalogVariablesFromClause; 37 | -------------------------------------------------------------------------------- /apps/roam/src/utils/getBlockProps.ts: -------------------------------------------------------------------------------- 1 | export type json = 2 | | string 3 | | number 4 | | boolean 5 | | null 6 | | json[] 7 | | { [key: string]: json }; 8 | 9 | export const normalizeProps = (props: json): json => 10 | typeof props === "object" 11 | ? props === null 12 | ? null 13 | : Array.isArray(props) 14 | ? props.map(normalizeProps) 15 | : Object.fromEntries( 16 | Object.entries(props).map(([k, v]) => [ 17 | k.replace(/^:+/, ""), 18 | typeof v === "object" && v !== null && !Array.isArray(v) 19 | ? normalizeProps(v) 20 | : Array.isArray(v) 21 | ? v.map(normalizeProps) 22 | : v, 23 | ]), 24 | ) 25 | : props; 26 | 27 | const getBlockProps = (uid: string) => 28 | normalizeProps( 29 | (window.roamAlphaAPI.pull("[:block/props]", [":block/uid", uid])?.[ 30 | ":block/props" 31 | ] || {}) as Record, 32 | ) as Record; 33 | 34 | export default getBlockProps; 35 | -------------------------------------------------------------------------------- /apps/roam/src/utils/getDiscourseNodeFormatExpression.ts: -------------------------------------------------------------------------------- 1 | const getDiscourseNodeFormatExpression = (format: string) => 2 | format 3 | ? new RegExp( 4 | `^${format 5 | .replace(/(\[|\]|\?|\.|\+)/g, "\\$1") 6 | .replace(/{[a-zA-Z]+}/g, "(.*?)")}$`, 7 | "s", 8 | ) 9 | : /$^/; 10 | 11 | export default getDiscourseNodeFormatExpression; 12 | -------------------------------------------------------------------------------- /apps/roam/src/utils/getDiscourseRelationLabels.ts: -------------------------------------------------------------------------------- 1 | import getDiscourseRelations from "./getDiscourseRelations"; 2 | 3 | const getDiscourseRelationLabels = (relations = getDiscourseRelations()) => 4 | Array.from(new Set(relations.flatMap((r) => [r.label, r.complement]))).filter( 5 | (s) => !!s, 6 | ); 7 | 8 | export default getDiscourseRelationLabels; 9 | -------------------------------------------------------------------------------- /apps/roam/src/utils/getDiscourseRelationTriples.ts: -------------------------------------------------------------------------------- 1 | import getDiscourseRelations from "./getDiscourseRelations"; 2 | 3 | const getDiscourseRelationTriples = (relations = getDiscourseRelations()) => 4 | Array.from( 5 | new Set( 6 | relations.flatMap((r) => [ 7 | JSON.stringify([r.label, r.source, r.destination]), 8 | JSON.stringify([r.complement, r.destination, r.source]), 9 | ]), 10 | ), 11 | ) 12 | .map((s) => JSON.parse(s)) 13 | .map(([relation, source, target]: string[]) => ({ 14 | relation, 15 | source, 16 | target, 17 | })); 18 | 19 | export default getDiscourseRelationTriples; 20 | -------------------------------------------------------------------------------- /apps/roam/src/utils/getPageMetadata.ts: -------------------------------------------------------------------------------- 1 | import normalizePageTitle from "roamjs-components/queries/normalizePageTitle"; 2 | import getDisplayNameByUid from "roamjs-components/queries/getDisplayNameByUid"; 3 | 4 | const displayNameCache: Record = {}; 5 | const getDisplayName = (s: string) => { 6 | if (displayNameCache[s]) { 7 | return displayNameCache[s]; 8 | } 9 | const value = getDisplayNameByUid(s); 10 | displayNameCache[s] = value; 11 | setTimeout(() => delete displayNameCache[s], 120000); 12 | return value; 13 | }; 14 | 15 | const getPageMetadata = (title: string, cacheKey?: string) => { 16 | const results = window.roamAlphaAPI.q( 17 | `[:find (pull ?p [:create/time :block/uid]) (pull ?cu [:user/uid]) :where [?p :node/title "${normalizePageTitle( 18 | title, 19 | )}"] [?p :create/user ?cu]]`, 20 | ) as [[{ time: number; uid: string }, { uid: string }]]; 21 | if (results.length) { 22 | const [[{ time: createdTime, uid: id }, { uid }]] = results; 23 | 24 | const displayName = getDisplayName(uid); 25 | const date = new Date(createdTime); 26 | return { displayName, date, id }; 27 | } 28 | return { 29 | displayName: "Unknown", 30 | date: new Date(), 31 | id: "", 32 | }; 33 | }; 34 | 35 | export default getPageMetadata; 36 | -------------------------------------------------------------------------------- /apps/roam/src/utils/getPlainTitleFromSpecification.ts: -------------------------------------------------------------------------------- 1 | import { Condition, QBClause } from "./types"; 2 | 3 | export const getPlainTitleFromSpecification = ({ 4 | specification, 5 | text, 6 | }: { 7 | specification: Condition[]; 8 | text: string; 9 | }) => { 10 | // Assumptions: 11 | // - Conditions are properly ordered 12 | // - There is a 'has title' condition somewhere 13 | const titleCondition = specification.find( 14 | (s): s is QBClause => 15 | s.type === "clause" && s.relation === "has title" && s.source === text, 16 | ); 17 | if (!titleCondition) return ""; 18 | return titleCondition.target 19 | .replace(/^\/(\^)?/, "") 20 | .replace(/(\$)?\/$/, "") 21 | .replace(/\\\[/g, "[") 22 | .replace(/\\\]/g, "]") 23 | .replace(/\(\.[\*\+](\?)?\)/g, ""); 24 | }; 25 | -------------------------------------------------------------------------------- /apps/roam/src/utils/getQBClauses.ts: -------------------------------------------------------------------------------- 1 | import { Condition, QBClauseData } from "./types"; 2 | 3 | const getQBClauses = (cs: Condition[]): QBClauseData[] => 4 | cs.flatMap((c) => { 5 | switch (c.type) { 6 | case "not or": 7 | case "or": 8 | return getQBClauses(c.conditions.flat()); 9 | case "clause": 10 | case "not": 11 | default: 12 | return c; 13 | } 14 | }); 15 | 16 | export default getQBClauses; 17 | -------------------------------------------------------------------------------- /apps/roam/src/utils/graphViewNodeStyling.ts: -------------------------------------------------------------------------------- 1 | import { formatHexColor } from "~/components/settings/DiscourseNodeCanvasSettings"; 2 | import getDiscourseNodes from "./getDiscourseNodes"; 3 | import { getPlainTitleFromSpecification } from "./getPlainTitleFromSpecification"; 4 | 5 | type SigmaRenderer = { 6 | setSetting: (settingName: string, value: any) => void; 7 | getSetting: (settingName: string) => any; 8 | }; 9 | type nodeData = { 10 | x: number; 11 | y: number; 12 | label: string; 13 | size: number; 14 | }; 15 | 16 | export const addGraphViewNodeStyling = () => { 17 | window.roamAlphaAPI.ui.graphView.wholeGraph.addCallback({ 18 | label: "discourse-node-styling", 19 | callback: ({ "sigma-renderer": sigma }) => { 20 | const sig = sigma as SigmaRenderer; 21 | graphViewNodeStyling({ sig }); 22 | }, 23 | }); 24 | }; 25 | 26 | const graphViewNodeStyling = ({ sig }: { sig: SigmaRenderer }) => { 27 | const allNodes = getDiscourseNodes(); 28 | const prefixColors = allNodes.map((n) => { 29 | const formattedTitle = getPlainTitleFromSpecification({ 30 | specification: n.specification, 31 | text: n.text, 32 | }); 33 | const formattedBackgroundColor = formatHexColor(n.canvasSettings.color); 34 | 35 | return { 36 | prefix: formattedTitle, 37 | color: formattedBackgroundColor, 38 | showInGraphOverview: n.graphOverview, 39 | }; 40 | }); 41 | 42 | const originalReducer = sig.getSetting("nodeReducer"); 43 | sig.setSetting("nodeReducer", (id: string, nodeData: nodeData) => { 44 | let modifiedData = originalReducer 45 | ? originalReducer(id, nodeData) 46 | : nodeData; 47 | 48 | const { label } = modifiedData; 49 | 50 | for (const { prefix, color, showInGraphOverview } of prefixColors) { 51 | if (prefix && showInGraphOverview && label.startsWith(prefix)) { 52 | return { 53 | ...modifiedData, 54 | color, 55 | }; 56 | } 57 | } 58 | 59 | return modifiedData; 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /apps/roam/src/utils/importDiscourseGraph.ts: -------------------------------------------------------------------------------- 1 | import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; 2 | import type { InputTextNode } from "roamjs-components/types"; 3 | import createPage from "roamjs-components/writes/createPage"; 4 | 5 | const pruneNodes = (nodes: InputTextNode[]): InputTextNode[] => 6 | nodes 7 | .filter((n) => !getPageTitleByPageUid(n.uid || "")) 8 | .map((n) => ({ ...n, children: pruneNodes(n.children || []) })); 9 | 10 | const importDiscourseGraph = ({ 11 | title, 12 | grammar: _grammar, 13 | nodes, 14 | relations, 15 | }: { 16 | title: string; 17 | grammar: { source: string; label: string; destination: string }[]; 18 | nodes: InputTextNode[]; 19 | relations: { source: string; label: string; target: string }[]; 20 | }) => { 21 | const pagesByUids = Object.fromEntries( 22 | nodes.map(({ uid, text }) => [uid, text]), 23 | ); 24 | return createPage({ 25 | title, 26 | tree: relations.map(({ source, target, label }) => ({ 27 | text: `[[${pagesByUids[source]}]]`, 28 | children: [ 29 | { 30 | text: label, 31 | children: [ 32 | { 33 | text: `[[${pagesByUids[target]}]]`, 34 | }, 35 | ], 36 | }, 37 | ], 38 | })), 39 | }).then(() => 40 | Promise.all( 41 | pruneNodes(nodes).map((node) => 42 | createPage({ title: node.text, tree: node.children, uid: node.uid }), 43 | ), 44 | ), 45 | ); 46 | }; 47 | 48 | export default importDiscourseGraph; 49 | -------------------------------------------------------------------------------- /apps/roam/src/utils/initializeDiscourseNodes.ts: -------------------------------------------------------------------------------- 1 | import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; 2 | import { createPage } from "roamjs-components/writes"; 3 | import INITIAL_NODE_VALUES from "~/data/defaultDiscourseNodes"; 4 | import getDiscourseNodes, { excludeDefaultNodes } from "./getDiscourseNodes"; 5 | 6 | const initializeDiscourseNodes = async () => { 7 | const nodes = getDiscourseNodes().filter(excludeDefaultNodes); 8 | if (nodes.length === 0) { 9 | await Promise.all( 10 | INITIAL_NODE_VALUES.map( 11 | (n) => 12 | getPageUidByPageTitle(`discourse-graph/nodes/${n.text}`) || 13 | createPage({ 14 | title: `discourse-graph/nodes/${n.text}`, 15 | uid: n.type, 16 | tree: [ 17 | { text: "Format", children: [{ text: n.format || "" }] }, 18 | { text: "Shortcut", children: [{ text: n.shortcut || "" }] }, 19 | { text: "Graph Overview" }, 20 | { 21 | text: "Canvas", 22 | children: [ 23 | { 24 | text: "color", 25 | children: [{ text: n.canvasSettings?.color || "" }], 26 | }, 27 | ], 28 | }, 29 | ], 30 | }), 31 | ), 32 | ); 33 | } 34 | }; 35 | 36 | export default initializeDiscourseNodes; 37 | -------------------------------------------------------------------------------- /apps/roam/src/utils/isCanvasPage.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_CANVAS_PAGE_FORMAT } from ".."; 2 | import { getFormattedConfigTree } from "./discourseConfigRef"; 3 | 4 | export const isCanvasPage = ({ title }: { title: string }) => { 5 | const { canvasPageFormat } = getFormattedConfigTree(); 6 | const format = canvasPageFormat.value || DEFAULT_CANVAS_PAGE_FORMAT; 7 | const canvasRegex = new RegExp(`^${format}$`.replace(/\*/g, ".+")); 8 | return canvasRegex.test(title); 9 | }; 10 | 11 | export const isCurrentPageCanvas = ({ 12 | title, 13 | h1, 14 | }: { 15 | title: string; 16 | h1: HTMLHeadingElement; 17 | }) => { 18 | return isCanvasPage({ title }) && !!h1.closest(".roam-article"); 19 | }; 20 | -------------------------------------------------------------------------------- /apps/roam/src/utils/isDiscourseNode.ts: -------------------------------------------------------------------------------- 1 | import getDiscourseNodes from "./getDiscourseNodes"; 2 | import findDiscourseNode from "./findDiscourseNode"; 3 | 4 | const isDiscourseNode = (uid: string) => { 5 | const nodes = getDiscourseNodes(); 6 | const node = findDiscourseNode(uid, nodes); 7 | if (!node) return false; 8 | return node.backedBy !== "default"; 9 | }; 10 | 11 | export default isDiscourseNode; 12 | -------------------------------------------------------------------------------- /apps/roam/src/utils/isDiscourseNodeConfigPage.ts: -------------------------------------------------------------------------------- 1 | import { NODE_CONFIG_PAGE_TITLE } from "~/utils/renderNodeConfigPage"; 2 | 3 | export const isDiscourseNodeConfigPage = (title: string) => 4 | title.startsWith(NODE_CONFIG_PAGE_TITLE); 5 | -------------------------------------------------------------------------------- /apps/roam/src/utils/isFlagEnabled.ts: -------------------------------------------------------------------------------- 1 | import type { RoamBasicNode } from "roamjs-components/types/native"; 2 | import getSubTree from "roamjs-components/util/getSubTree"; 3 | import toFlexRegex from "roamjs-components/util/toFlexRegex"; 4 | import discourseConfigRef from "./discourseConfigRef"; 5 | 6 | const isFlagEnabled = (flag: string, inputTree?: RoamBasicNode[]): boolean => { 7 | const flagParts = flag.split("."); 8 | const tree = inputTree || discourseConfigRef.tree; 9 | if (flagParts.length === 1) 10 | return tree.some((t) => toFlexRegex(flag).test(t.text)); 11 | else 12 | return isFlagEnabled( 13 | flagParts.slice(1).join("."), 14 | getSubTree({ tree, key: flagParts[0] }).children, 15 | ); 16 | }; 17 | 18 | export default isFlagEnabled; 19 | -------------------------------------------------------------------------------- /apps/roam/src/utils/isPageUid.ts: -------------------------------------------------------------------------------- 1 | export const isPageUid = (uid: string) => 2 | !!window.roamAlphaAPI.pull("[:node/title]", [":block/uid", uid])?.[ 3 | ":node/title" 4 | ]; 5 | -------------------------------------------------------------------------------- /apps/roam/src/utils/isQueryPage.ts: -------------------------------------------------------------------------------- 1 | import { OnloadArgs } from "roamjs-components/types"; 2 | import { getQueryPages } from "~/components/settings/QueryPagesPanel"; 3 | 4 | export const isQueryPage = ({ 5 | title, 6 | onloadArgs, 7 | }: { 8 | title: string; 9 | onloadArgs: OnloadArgs; 10 | }): boolean => { 11 | const { extensionAPI } = onloadArgs; 12 | const queryPages = getQueryPages(extensionAPI); 13 | 14 | const matchesQueryPage = queryPages.some((queryPage) => { 15 | const escapedPattern = queryPage 16 | .replace(/\*/g, ".*") 17 | .replace(/([()])/g, "\\$1"); 18 | const regex = new RegExp(`^${escapedPattern}$`); 19 | return regex.test(title); 20 | }); 21 | 22 | return matchesQueryPage; 23 | }; 24 | -------------------------------------------------------------------------------- /apps/roam/src/utils/listActiveQueries.ts: -------------------------------------------------------------------------------- 1 | import { PullBlock } from "roamjs-components/types"; 2 | import { getQueryPages } from "~/components/settings/QueryPagesPanel"; 3 | import { OnloadArgs } from "roamjs-components/types"; 4 | 5 | export const listActiveQueries = (extensionAPI: OnloadArgs["extensionAPI"]) => 6 | ( 7 | window.roamAlphaAPI.data.fast.q( 8 | `[:find (pull ?b [:block/uid]) :where [or-join [?b] 9 | [and [?b :block/string ?s] [[clojure.string/includes? ?s "{{query block}}"]] ] 10 | ${getQueryPages(extensionAPI).map( 11 | (p) => 12 | `[and [?b :node/title ?t] [[re-pattern "^${p.replace( 13 | /\*/, 14 | ".*", 15 | )}$"] ?regex] [[re-find ?regex ?t]]]`, 16 | )} 17 | ]]`, 18 | ) as [PullBlock][] 19 | ).map((b) => ({ uid: b[0][":block/uid"] || "" })); 20 | -------------------------------------------------------------------------------- /apps/roam/src/utils/loadImage.ts: -------------------------------------------------------------------------------- 1 | export const loadImage = ( 2 | url: string, 3 | ): Promise<{ width: number; height: number }> => { 4 | return new Promise((resolve, reject) => { 5 | const img = new Image(); 6 | 7 | img.onload = () => { 8 | resolve({ width: img.width, height: img.height }); 9 | }; 10 | 11 | img.onerror = () => { 12 | reject(new Error("Failed to load image")); 13 | }; 14 | 15 | setTimeout(() => { 16 | reject(new Error("Failed to load image: timeout")); 17 | }, 10000); 18 | 19 | img.src = url; 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /apps/roam/src/utils/matchDiscourseNode.ts: -------------------------------------------------------------------------------- 1 | import compileDatalog from "./compileDatalog"; 2 | import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; 3 | import normalizePageTitle from "roamjs-components/queries/normalizePageTitle"; 4 | import conditionToDatalog from "./conditionToDatalog"; 5 | import getDiscourseNodeFormatExpression from "./getDiscourseNodeFormatExpression"; 6 | import type { DiscourseNode } from "./getDiscourseNodes"; 7 | import replaceDatalogVariables from "./replaceDatalogVariables"; 8 | 9 | const matchDiscourseNode = ({ 10 | format, 11 | specification, 12 | text, 13 | ...rest 14 | }: Pick & 15 | ( 16 | | { 17 | title: string; 18 | } 19 | | { uid: string } 20 | )) => { 21 | // Handle specification with single "has title" clause 22 | if ( 23 | specification.length === 1 && 24 | specification[0].type === "clause" && 25 | specification[0].relation === "has title" 26 | ) { 27 | const title = 28 | "title" in rest ? rest.title : getPageTitleByPageUid(rest.uid); 29 | const regex = new RegExp(specification[0].target.slice(1, -1)); 30 | return !specification[0].not && regex.test(title); 31 | } 32 | 33 | // Handle any other specification 34 | if (specification?.length) { 35 | const where = replaceDatalogVariables( 36 | [{ from: text, to: "node" }], 37 | specification.flatMap((c) => conditionToDatalog(c)), 38 | ).map((c) => compileDatalog(c, 0)); 39 | const firstClause = 40 | "title" in rest 41 | ? `[or-join [?node] [?node :node/title "${normalizePageTitle( 42 | rest.title, 43 | )}"] [?node :block/string "${normalizePageTitle(rest.title)}"]]` 44 | : `[?node :block/uid "${rest.uid}"]`; 45 | return !!window.roamAlphaAPI.data.fast.q( 46 | `[:find ?node :where ${firstClause} ${where.join(" ")}]`, 47 | ).length; 48 | } 49 | 50 | // Fallback to format expression 51 | const title = "title" in rest ? rest.title : getPageTitleByPageUid(rest.uid); 52 | return getDiscourseNodeFormatExpression(format).test(title); 53 | }; 54 | 55 | export default matchDiscourseNode; 56 | -------------------------------------------------------------------------------- /apps/roam/src/utils/measureCanvasNodeText.ts: -------------------------------------------------------------------------------- 1 | type DefaultStyles = { 2 | fontFamily: string; 3 | fontStyle: string; 4 | fontWeight: string; 5 | fontSize: number; 6 | lineHeight: number; 7 | width: string; 8 | minWidth?: string; 9 | maxWidth?: string; 10 | padding: string; 11 | text: string; 12 | }; 13 | 14 | // node_modules\@tldraw\tldraw\node_modules\@tldraw\editor\dist\cjs\lib\app\managers\TextManager.js 15 | export const measureCanvasNodeText = (opts: DefaultStyles) => { 16 | const fixNewLines = /\r?\n|\r/g; 17 | 18 | const normalizeTextForDom = (text: string) => { 19 | return text 20 | .replace(fixNewLines, "\n") 21 | .split("\n") 22 | .map((x) => x || " ") 23 | .join("\n"); 24 | }; 25 | 26 | const measureText = (opts: DefaultStyles) => { 27 | const elm = document.createElement("div"); 28 | document.body.appendChild(elm); 29 | elm.setAttribute("dir", "ltr"); 30 | elm.style.setProperty("font-family", opts.fontFamily); 31 | elm.style.setProperty("font-style", opts.fontStyle); 32 | elm.style.setProperty("font-weight", opts.fontWeight); 33 | elm.style.setProperty("font-size", opts.fontSize + "px"); 34 | elm.style.setProperty( 35 | "line-height", 36 | opts.lineHeight * opts.fontSize + "px", 37 | ); 38 | elm.style.setProperty("width", opts.width); 39 | elm.style.setProperty("min-width", opts.minWidth ?? null); 40 | elm.style.setProperty("max-width", opts.maxWidth ?? null); 41 | elm.style.setProperty("padding", opts.padding); 42 | 43 | elm.textContent = normalizeTextForDom(opts.text); 44 | const rect = elm.getBoundingClientRect(); 45 | elm.remove(); 46 | return { 47 | x: 0, 48 | y: 0, 49 | w: rect.width, 50 | h: rect.height, 51 | }; 52 | }; 53 | 54 | const { w, h } = measureText(opts); 55 | 56 | return { w, h }; 57 | }; 58 | -------------------------------------------------------------------------------- /apps/roam/src/utils/refreshConfigTree.ts: -------------------------------------------------------------------------------- 1 | import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid"; 2 | import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; 3 | import getDiscourseRelationLabels from "./getDiscourseRelationLabels"; 4 | import discourseConfigRef from "./discourseConfigRef"; 5 | import registerDiscourseDatalogTranslators from "./registerDiscourseDatalogTranslators"; 6 | import { unregisterDatalogTranslator } from "./conditionToDatalog"; 7 | import type { PullBlock } from "roamjs-components/types/native"; 8 | import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/utils/renderNodeConfigPage"; 9 | 10 | const getPagesStartingWithPrefix = (prefix: string) => 11 | ( 12 | window.roamAlphaAPI.data.fast.q( 13 | `[:find (pull ?b [:block/uid :node/title]) :where [?b :node/title ?title] [(clojure.string/starts-with? ?title "${prefix}")]]`, 14 | ) as [PullBlock][] 15 | ).map((r) => ({ 16 | title: r[0][":node/title"] || "", 17 | uid: r[0][":block/uid"] || "", 18 | })); 19 | 20 | const refreshConfigTree = () => { 21 | getDiscourseRelationLabels().forEach((key) => 22 | unregisterDatalogTranslator({ key }), 23 | ); 24 | discourseConfigRef.tree = getBasicTreeByParentUid( 25 | getPageUidByPageTitle(DISCOURSE_CONFIG_PAGE_TITLE), 26 | ); 27 | const pages = getPagesStartingWithPrefix("discourse-graph/nodes"); 28 | discourseConfigRef.nodes = Object.fromEntries( 29 | pages.map(({ title, uid }) => { 30 | return [ 31 | uid, 32 | { 33 | text: title.substring("discourse-graph/nodes/".length), 34 | children: getBasicTreeByParentUid(uid), 35 | }, 36 | ]; 37 | }), 38 | ); 39 | registerDiscourseDatalogTranslators(); 40 | }; 41 | 42 | export default refreshConfigTree; 43 | -------------------------------------------------------------------------------- /apps/roam/src/utils/renderLinkedReferenceAdditions.ts: -------------------------------------------------------------------------------- 1 | import { createElement } from "react"; 2 | import { getPageTitleValueByHtmlElement } from "roamjs-components/dom"; 3 | import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; 4 | import renderWithUnmount from "roamjs-components/util/renderWithUnmount"; 5 | import { DiscourseContext } from "~/components"; 6 | import CanvasReferences from "~/components/canvas/CanvasReferences"; 7 | import isDiscourseNode from "./isDiscourseNode"; 8 | import { OnloadArgs } from "roamjs-components/types"; 9 | 10 | export const renderLinkedReferenceAdditions = async ( 11 | div: HTMLDivElement, 12 | onloadArgs: OnloadArgs, 13 | ) => { 14 | const isMainWindow = !!div.closest(".roam-article"); 15 | const uid = isMainWindow 16 | ? await window.roamAlphaAPI.ui.mainWindow.getOpenPageOrBlockUid() 17 | : getPageUidByPageTitle(getPageTitleValueByHtmlElement(div)); 18 | if ( 19 | uid && 20 | isDiscourseNode(uid) && 21 | !div.getAttribute("data-roamjs-discourse-context") 22 | ) { 23 | div.setAttribute("data-roamjs-discourse-context", "true"); 24 | const parent = div.firstElementChild; 25 | if (parent) { 26 | const insertBefore = parent.firstElementChild; 27 | 28 | const p = document.createElement("div"); 29 | parent.insertBefore(p, insertBefore); 30 | renderWithUnmount( 31 | createElement(DiscourseContext, { 32 | uid, 33 | results: [], 34 | onloadArgs, 35 | }), 36 | p, 37 | onloadArgs, 38 | ); 39 | 40 | const canvasP = document.createElement("div"); 41 | parent.insertBefore(canvasP, insertBefore); 42 | renderWithUnmount( 43 | createElement(CanvasReferences, { 44 | uid, 45 | }), 46 | canvasP, 47 | onloadArgs, 48 | ); 49 | } 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /apps/roam/src/utils/resolveQueryBuilderRef.ts: -------------------------------------------------------------------------------- 1 | import isLiveBlock from "roamjs-components/queries/isLiveBlock"; 2 | import extractRef from "roamjs-components/util/extractRef"; 3 | import { getQueryPages } from "~/components/settings/QueryPagesPanel"; 4 | import { OnloadArgs } from "roamjs-components/types"; 5 | 6 | const resolveQueryBuilderRef = ({ 7 | queryRef, 8 | extensionAPI, 9 | }: { 10 | queryRef: string; 11 | extensionAPI: OnloadArgs["extensionAPI"]; 12 | }) => { 13 | const parentUid = isLiveBlock(extractRef(queryRef)) 14 | ? extractRef(queryRef) 15 | : window.roamAlphaAPI.data.fast 16 | .q( 17 | `[:find ?uid :where [?b :block/uid ?uid] [or-join [?b] 18 | [and [?b :block/string ?s] [[clojure.string/includes? ?s "{{query block:${queryRef}}}"]] ] 19 | ${getQueryPages(extensionAPI).map( 20 | (p) => `[and [?b :node/title "${p.replace(/\*/, queryRef)}"]]`, 21 | )} 22 | [and [?b :node/title "${queryRef}"]] 23 | ]]`, 24 | )[0] 25 | ?.toString() || ""; 26 | return parentUid; 27 | }; 28 | 29 | export default resolveQueryBuilderRef; 30 | -------------------------------------------------------------------------------- /apps/roam/src/utils/runQuery.ts: -------------------------------------------------------------------------------- 1 | import type { OnloadArgs } from "roamjs-components/types/native"; 2 | import fireQuery, { QueryArgs } from "./fireQuery"; 3 | import parseQuery from "./parseQuery"; 4 | import parseResultSettings from "./parseResultSettings"; 5 | import postProcessResults from "./postProcessResults"; 6 | import { Column } from "./types"; 7 | 8 | const runQuery = ({ 9 | parentUid, 10 | extensionAPI, 11 | inputs, 12 | }: { 13 | parentUid: string; 14 | extensionAPI: OnloadArgs["extensionAPI"]; 15 | inputs?: QueryArgs["inputs"]; 16 | }) => { 17 | const queryArgs = Object.assign(parseQuery(parentUid), { inputs }); 18 | return fireQuery(queryArgs).then((results) => { 19 | const settings = parseResultSettings( 20 | parentUid, 21 | [ 22 | { 23 | key: "text", 24 | uid: window.roamAlphaAPI.util.generateUID(), 25 | selection: "node", 26 | } as Column, 27 | ].concat( 28 | queryArgs.selections.map((s) => ({ 29 | key: s.label, 30 | uid: s.uid, 31 | selection: s.text, 32 | })), 33 | ), 34 | extensionAPI, 35 | ); 36 | return postProcessResults(results, settings); 37 | }); 38 | }; 39 | 40 | export default runQuery; 41 | -------------------------------------------------------------------------------- /apps/roam/src/utils/sendErrorEmail.ts: -------------------------------------------------------------------------------- 1 | import { getNodeEnv } from "roamjs-components/util/env"; 2 | import { ErrorEmailProps } from "@repo/types"; 3 | 4 | const sendErrorEmail = async ({ 5 | error, 6 | type, 7 | context, 8 | }: { 9 | error: Error; 10 | type: string; 11 | context?: Record; 12 | }) => { 13 | const url = 14 | getNodeEnv() === "development" 15 | ? "http://localhost:3000/api/errors" 16 | : "https://discoursegraphs.com/api/errors"; 17 | const payload: ErrorEmailProps = { 18 | errorMessage: error.message, 19 | errorStack: error.stack || "", 20 | type, 21 | app: "Roam", 22 | graphName: window.roamAlphaAPI?.graph?.name || "unknown", 23 | context, 24 | }; 25 | 26 | try { 27 | const response = await fetch(url, { 28 | method: "POST", 29 | headers: { 30 | "Content-Type": "application/json", 31 | }, 32 | body: JSON.stringify(payload), 33 | }); 34 | 35 | if (!response.ok) { 36 | const errorMessage = await response.text(); 37 | console.error(`Failed to send error email: ${errorMessage}`); 38 | } 39 | } catch (err) { 40 | console.error(`Error sending request: ${err}`); 41 | } 42 | }; 43 | 44 | export default sendErrorEmail; 45 | -------------------------------------------------------------------------------- /apps/roam/src/utils/setQueryPages.ts: -------------------------------------------------------------------------------- 1 | import { OnloadArgs } from "roamjs-components/types"; 2 | 3 | export const setQueryPages = (onloadArgs: OnloadArgs) => { 4 | const queryPages = onloadArgs.extensionAPI.settings.get("query-pages"); 5 | const queryPageArray = Array.isArray(queryPages) 6 | ? queryPages 7 | : typeof queryPages === "object" 8 | ? [] 9 | : typeof queryPages === "string" && queryPages 10 | ? [queryPages] 11 | : []; 12 | if (!queryPageArray.includes("discourse-graph/queries/*")) { 13 | onloadArgs.extensionAPI.settings.set("query-pages", [ 14 | ...queryPageArray, 15 | "discourse-graph/queries/*", 16 | ]); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /apps/roam/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | type QBBase = { 2 | uid: string; 3 | }; 4 | export type QBClauseData = { 5 | relation: string; 6 | source: string; 7 | target: string; 8 | not?: boolean; 9 | } & QBBase; 10 | export type QBNestedData = { 11 | conditions: Condition[][]; 12 | } & QBBase; 13 | export type QBClause = QBClauseData & { 14 | type: "clause"; 15 | }; 16 | export type QBNot = QBClauseData & { 17 | type: "not"; 18 | }; 19 | export type QBOr = QBNestedData & { 20 | type: "or"; 21 | }; 22 | export type QBNor = QBNestedData & { 23 | type: "not or"; 24 | }; 25 | export type Condition = QBClause | QBNot | QBOr | QBNor; 26 | 27 | export type Selection = { 28 | text: string; 29 | label: string; 30 | uid: string; 31 | }; 32 | 33 | export type ExportTypes = { 34 | name: string; 35 | callback: (args: { 36 | filename: string; 37 | includeDiscourseContext: boolean; 38 | isExportDiscourseGraph: boolean; 39 | }) => Promise<{ title: string; content: string }[]>; 40 | }[]; 41 | 42 | export type Result = { 43 | text: string; 44 | uid: string; 45 | } & Record<`${string}-uid`, string> & 46 | Record; 47 | 48 | export type Column = { key: string; uid: string; selection: string }; 49 | -------------------------------------------------------------------------------- /apps/roam/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | // tailwind config is required for editor support 2 | 3 | import type { Config } from "tailwindcss"; 4 | // import sharedConfig from "@repo/tailwind-config"; 5 | 6 | const config: Pick = { 7 | content: [], 8 | // presets: [sharedConfig], 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /apps/roam/tests/extension.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import root from "../src"; 3 | 4 | const extensionSettings: Record = {}; 5 | const commandPalette: Record unknown> = {}; 6 | 7 | // TODO - solve playwright's horrendous file resolution 8 | test.skip("End to end flow of Query Builder & Discourse Graphs", () => { 9 | if (!root) throw new Error("Root not found"); 10 | const { onload } = root; 11 | const unload = onload({ 12 | extension: { 13 | version: "test", 14 | }, 15 | extensionAPI: { 16 | settings: { 17 | get: (key: string) => extensionSettings[key], 18 | getAll: () => extensionSettings, 19 | set: async (key: string, value: unknown) => { 20 | extensionSettings[key] = value; 21 | }, 22 | panel: { 23 | create: (config) => {}, 24 | }, 25 | }, 26 | ui: { 27 | commandPalette: { 28 | addCommand: async (config) => { 29 | commandPalette[config.label] = config.callback; 30 | }, 31 | removeCommand: async (config) => { 32 | delete commandPalette[config.label]; 33 | }, 34 | }, 35 | }, 36 | }, 37 | }); 38 | expect(unload).toBeTruthy(); 39 | }); 40 | -------------------------------------------------------------------------------- /apps/roam/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "src/types.d.ts", "tailwind.config.ts", "tests"], 3 | "exclude": ["node_modules"], 4 | "extends": "@repo/typescript-config/react-library.json", 5 | "compilerOptions": { 6 | "baseUrl": ".", 7 | "outDir": "dist", 8 | "target": "ESNext", 9 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 10 | "allowJs": false, 11 | "esModuleInterop": false, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "forceConsistentCasingInFileNames": true, 15 | 16 | "jsx": "react", 17 | "noUncheckedIndexedAccess": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/website/.env.example: -------------------------------------------------------------------------------- 1 | # RESEND_API_KEY= 2 | # NEXT_PUBLIC_POSTHOG_KEY=phc_KllQh2hOMmXJ3YwdiLHswb1CfOaEWzoC30mA0u3ZJgp 3 | # NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com -------------------------------------------------------------------------------- /apps/website/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ["@repo/eslint-config/next.js"], 5 | parser: "@typescript-eslint/parser", 6 | parserOptions: { 7 | project: true, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /apps/website/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for committing if needed) 33 | .env 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "next/image"; 3 | 4 | export default function DocsPage() { 5 | return ( 6 |
7 |
8 |

9 | Future Home of All the Docs 10 |

11 |

12 | For now, here are the{" "} 13 | 14 | Roam Docs{" "} 15 | 16 |

17 |
18 | 29 |
30 |
31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from "~/components/DocsLayout"; 2 | import { navigation } from "./navigation"; 3 | 4 | export default function RootLayout({ 5 | children, 6 | }: { 7 | children: React.ReactNode; 8 | }) { 9 | return {children}; 10 | } 11 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/navigation.ts: -------------------------------------------------------------------------------- 1 | import { NavigationList } from "~/components/Navigation"; 2 | 3 | const ROOT = "/docs/roam"; 4 | 5 | export const navigation: NavigationList = [ 6 | { 7 | title: "🏠 Welcome!", 8 | links: [ 9 | { title: "Getting Started", href: `${ROOT}/getting-started` }, 10 | { title: "Installation", href: `${ROOT}/installation` }, 11 | ], 12 | }, 13 | { 14 | title: "🗺️ GUIDES", 15 | links: [ 16 | { 17 | title: "Creating Nodes", 18 | href: `${ROOT}/creating-discourse-nodes`, 19 | }, 20 | { 21 | title: "Creating Relationships", 22 | href: `${ROOT}/creating-discourse-relationships`, 23 | }, 24 | { 25 | title: "Exploring", 26 | href: `${ROOT}/exploring-discourse-graph`, 27 | }, 28 | { 29 | title: "Querying", 30 | href: `${ROOT}/querying-discourse-graph`, 31 | }, 32 | { 33 | title: "Extending", 34 | href: `${ROOT}/extending-personalizing-graph`, 35 | }, 36 | { 37 | title: "Sharing", 38 | href: `${ROOT}/sharing-discourse-graph`, 39 | }, 40 | ], 41 | }, 42 | { 43 | title: "🧱 FUNDAMENTALS", 44 | links: [ 45 | { 46 | title: "What is a Discourse Graph?", 47 | href: `${ROOT}/what-is-discourse-graph`, 48 | }, 49 | { 50 | title: "Grammar", 51 | href: `${ROOT}/grammar`, 52 | }, 53 | ], 54 | }, 55 | { 56 | title: "🚢 USE CASES", 57 | links: [ 58 | { 59 | title: "Literature Reviewing", 60 | href: `${ROOT}/literature-reviewing`, 61 | }, 62 | { 63 | title: "Zettelkasten", 64 | href: `${ROOT}/enhanced-zettelkasten`, 65 | }, 66 | { 67 | title: "Reading Clubs / Seminars", 68 | href: `${ROOT}/reading-clubs`, 69 | }, 70 | { 71 | title: "Lab notebooks", 72 | href: `${ROOT}/lab-notebooks`, 73 | }, 74 | { 75 | title: "Product / Research Roadmapping", 76 | href: `${ROOT}/research-roadmapping`, 77 | }, 78 | ], 79 | }, 80 | ]; 81 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect, notFound } from "next/navigation"; 2 | import { navigation } from "./navigation"; 3 | 4 | export default function Page() { 5 | const firstSection = navigation[0]; 6 | const firstLink = firstSection?.links[0]; 7 | 8 | if (!firstLink?.href) { 9 | notFound(); 10 | } 11 | 12 | redirect(firstLink.href); 13 | } 14 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/pages/base-grammar.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Base Grammar" 3 | date: "2025-01-01" 4 | author: "" 5 | published: true 6 | --- 7 | 8 | ## Base grammar 9 | 10 | This is what ships with the extension. 11 | 12 | ### Nodes 13 | 14 | - QUE - Question 15 | - CLM - Claim 16 | - EVD - Evidence 17 | - Source 18 | 19 | ### Relations 20 | 21 | - EVD Informs QUE 22 | - EVD Supports CLM 23 | - EVD Opposes CLM 24 | 25 | Motivation for this base grammar is described in this [article](https://oasislab.pubpub.org/pub/54t0y9mk/release/3). 26 | 27 | This base grammar may be most useful for projects that interact with empirical evidence where you want to clearly distinguish between theory and evidence, and retain provenance to the source citations if you want to get more context. 28 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/pages/creating-discourse-nodes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Creating Discourse Nodes" 3 | date: "2025-01-01" 4 | author: "" 5 | published: true 6 | --- 7 | 8 | The extension makes it easy to factor parts of your notes into formal nodes of a discourse graph (claims, evidence, etc.). 9 | 10 | Steps to create a discourse graph node: 11 | 12 | 1. Select the text you want to turn into a formal discourse graph node 13 | 2. Press the trigger hotkey (default is `\`) to open up the Node Menu 14 | 3. Press the appropriate shortcut key (e.g., E for Evidence) 15 | 16 | The system will create a new page with the text as title, appropriate metadata, and template you have defined. 17 | 18 | ## Demo 19 | 20 | https://www.loom.com/share/471fcf7dc13246439cb35feedb110470 21 | 22 | You can customize the template for specific nodes in the Settings Panel. 23 | 24 | ![](/docs/roam/node-template.png) 25 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/pages/creating-discourse-relationships.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Creating Discourse Relationships" 3 | date: "2025-01-01" 4 | author: "" 5 | published: true 6 | --- 7 | 8 | One of the main features of the Discourse Graph extension is the ability to **create formal discourse relations between nodes just by writing and outlining!** 9 | 10 | The extension has an in-built [grammar](./grammar) that enables it to recognize when certain patterns of writing and outlining are meant to express particular discourse relations (e.g., support, oppose, inform) between discourse nodes. When it recognizes these patterns, it "writes" them to a formal discourse graph data structure, that you can then use to explore or query your discourse graph. 11 | 12 | ## Stock Patterns 13 | 14 | - Take a look at [Relations Patterns](./relations-patterns) 15 | 16 | ### Verifying relations 17 | 18 | You can verify any created relations by checking the [discourse context](./discourse-context) of the claim, evidence, or question page. 19 | 20 | Or by running a [query](./querying-discourse-graph) for the specific relation. 21 | 22 | ## Digging deeper 23 | 24 | Want to recognize other patterns that aren't listed here? Or don't like these? You can [change them](./extending-personalizing-graph)! But you might first want to [understand how the grammar works](./grammar). 25 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/pages/discourse-context.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Discourse Context" 3 | date: "2025-01-01" 4 | author: "" 5 | published: true 6 | --- 7 | 8 | - [Discourse Context](./discourse-context) 9 | - [Discourse Context Overlay](./discourse-context-overlay) 10 | - [Discourse Attributes](./discourse-attributes) 11 | - [Node Index](./node-index) 12 | 13 | The discourse context component adds a "higher-signal" linked references section to each discourse node, that allows you to explore _discourse_ relations (e.g., inform, support, etc.) between this node and other nodes in your discourse graph. 14 | 15 | ## Group by target node 16 | 17 | ![](/docs/roam/discourse-context-group-by-target.gif) 18 | 19 | ## Filter results 20 | 21 | ![](/docs/roam/discourse-context-filter.gif) 22 | 23 | ## Demo 24 | 25 | https://www.loom.com/share/0c66e95d0c71426e8090a8bc1cbf8544 26 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/pages/enhanced-zettelkasten.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Enhanced Zettelkasten" 3 | date: "2025-01-01" 4 | author: "" 5 | published: true 6 | --- 7 | 8 | Maarten Van Doorn has integrated the discourse graph into his take on applying the zettelkasten to his research. 9 | 10 | Here is a video overview: 11 | 12 | [https://www.youtube.com/watch?v=mNzUAICf4Rk](https://www.youtube.com/watch?v=mNzUAICf4Rk) 13 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/pages/exploring-discourse-graph.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Exploring Your Discourse Graph" 3 | date: "2025-01-01" 4 | author: "" 5 | published: true 6 | --- 7 | 8 | The extension adds features to enable you to seamlessly explore your discourse graph to enhance your thinking. 9 | 10 | - [Discourse Context](./discourse-context) 11 | - [Discourse Context Overlay](./discourse-context-overlay) 12 | - [Discourse Attributes](./discourse-attributes) 13 | - [Node Index](./node-index) 14 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/pages/extending-personalizing-graph.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Extending and Personalizing Your Discourse Graph" 3 | date: "2025-01-01" 4 | author: "" 5 | published: true 6 | --- 7 | 8 | You can define new nodes and relation patterns to fit your own needs! 9 | 10 | Some users have extended the grammar quite extensively! 11 | 12 | For example, here is a video from a user who extended the grammar to cover structured international law research: 13 | 14 | [https://www.youtube.com/watch?v=rST7cKMO_Ds](https://www.youtube.com/watch?v=rST7cKMO_Ds) 15 | 16 | Others have also extended the grammar to integrate with their primary results from their own experiments, to richly contextualize each new result and experiment with questions shared with past research. 17 | 18 | Detailed instructions coming soon! 19 | 20 | For now, here is a quick demo video of creating an incredibly useful "consistent with" relation pattern that recognizes when any pair of evidence both supports the same thing, implying that they are consistent with each other. 21 | 22 | [https://www.loom.com/share/cb9e526a98764e95a459a6db2b66e46a](https://www.loom.com/share/cb9e526a98764e95a459a6db2b66e46a) 23 | 24 | To extend the grammar, it will likely be useful to [learn more about the fundamentals of the extension's grammar and how it works](./grammar). 25 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/pages/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Getting Started" 3 | date: "2025-01-01" 4 | author: "" 5 | published: true 6 | --- 7 | 8 | The Discourse Graph extension enables [Roam](https://roamresearch.com/) users to seamlessly add additional semantic structure to their notes, including specified page types and link types that [model scientific discourse](./what-is-discourse-graph), to enable more complex and structured [knowledge synthesis work](https://oasislab.pubpub.org/pub/54t0y9mk/release/3), such as a complex interdisciplinary literature review, and enhanced collaboration with others on this work. 9 | 10 | ## Overview 11 | 12 | Here is a relatively brief walkthrough of some of the main features of the extension, including creating discourse nodes and relations, and [using](./exploring-your-discourse-graph), [querying](./querying-your-discourse-graph), and [sharing](./sharing-your-discourse-graph) the discourse graph. This is done in the context of an actual ongoing literature review. 13 | 14 | [https://www.loom.com/share/2ec80422301c451b888b65ee1d283b40](https://www.loom.com/share/2ec80422301c451b888b65ee1d283b40) 15 | 16 | ## Guides: Jump right in 17 | 18 | Follow our handy guides to get started on the basics as quickly as possible: 19 | 20 | - [Creating Discourse Nodes](./creating-discourse-nodes) 21 | - [Creating Discourse Relationships](./creating-discourse-relationships) 22 | - [Exploring your Discourse Graph](./exploring-discourse-graph) 23 | - [Querying your Discourse Graph](./querying-discourse-graph) 24 | - [Extending and Personalizing your Discourse Graph](./extending-personalizing-discourse-graph) 25 | 26 | ## Fundamentals: Dive a little deeper 27 | 28 | Learn the fundamentals of the Discourse Graph extension to get a deeper understanding of our main features: 29 | 30 | - [What is a Discourse Graph?](./what-is-discourse-graph) 31 | - [The Discourse Graph Extension Grammar](./grammar) 32 | - [The Base Grammar: Questions, Claims, and Evidence](./base-grammar) 33 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/pages/grammar.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Extension Grammar" 3 | date: "2025-01-01" 4 | author: "" 5 | published: true 6 | --- 7 | 8 | - [Nodes](./nodes) 9 | - [Operators and Relations](./operators-relations) 10 | - [Relations Patterns](./relations-patterns) 11 | - [Base Grammar](./base-grammar) 12 | 13 | The basic technical intuition behind the extension is that it creates a mapping between the following two elements: 14 | 15 | - **Human-readable, natural writing patterns in Roam** such as pages, blocks, page/block references, and indentation 16 | - **Discourse graph nodes and relations** such as questions, claims, evidence, and relations like support/oppose/inform 17 | 18 | This works because writing in Roam is instantiated as transactions on an underlying [Datomic](https://docs.datomic.com/cloud/whatis/data-model.html) database, queryable via [the Datomic dialect of Datalog](http://www.learndatalogtoday.org/). At it's most basic level, the extension's discourse graph grammar is defined in terms of higher-order combinations of Datalog queries. 19 | 20 | The purpose of this mapping is to make it possible to write and outline as naturally as possible, to help your own thinking, and then have the extension [recognize](./creating-discourse-relationships) (and "make real") the important relationships in your thinking that you have specified, so you and others can build on it in the near or later future as you continue your work. 21 | 22 | For this reason, if you want to better understand how the extension works, and especially if you want to [extend or customize its grammar](./extending-personalizing-graph), it is very useful to get a robust intuition for how Datomic and Datalog work. 23 | 24 | Here are two helpful entry points for this: 25 | 26 | - [https://www.zsolt.blog/2021/01/Roam-Data-Structure-Query.html](https://www.zsolt.blog/2021/01/Roam-Data-Structure-Query.html) 27 | - [http://www.learndatalogtoday.org/](http://www.learndatalogtoday.org/) 28 | 29 | ## Relation grammar building blocks 30 | 31 | [Nodes](./nodes) 32 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/pages/installation-roam-depot.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Installation" 3 | date: "2025-01-01" 4 | author: "" 5 | published: false 6 | --- 7 | 8 | The Discourse Graphs extension can be installed via Roam Depot: 9 | 10 | ![](/docs/roam/roam-depot-sidebar.png) 11 | 12 | ![](/docs/roam/roam-depot-settings.png) 13 | 14 | Then type in "Discourse Graphs" in the search bar and click install. 15 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/pages/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Installation" 3 | date: "2025-01-01" 4 | author: "" 5 | published: true 6 | --- 7 | 8 | The Discourse Graphs extension is now available for installation via Roam Depot. 9 | 10 | Search for Discourse Graph in Roam Depot 11 | 12 | ![](/docs/roam/browse-roam-depot.png) 13 | ![](/docs/roam/find-in-roam-depot.png) 14 | ![](/docs/roam/install-instruction-roam-depot.png) 15 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/pages/lab-notebooks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Lab Notebooks" 3 | date: "2025-01-01" 4 | author: "" 5 | published: true 6 | --- 7 | 8 | Description coming soon! 9 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/pages/literature-reviewing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Literature Reviewing" 3 | date: "2025-01-01" 4 | author: "" 5 | published: true 6 | --- 7 | 8 | This is currently the most common use case. 9 | 10 | Lukas Kawerau (aka Cortex Futura) has a course that integrates the extension into a complete system for academic literature reviewing and writing: [https://learn.cortexfutura.com/p/cite-to-write-v2](https://learn.cortexfutura.com/p/cite-to-write-v2) 11 | 12 | Lukas gives a short overview of how in this tweet thread: 13 | 14 | [https://x.com/cortexfutura/status/1441795897680011276](https://x.com/cortexfutura/status/1441795897680011276) 15 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/pages/node-index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Node Index" 3 | date: "2025-01-01" 4 | author: "" 5 | published: true 6 | --- 7 | 8 | - [Discourse Context](./discourse-context) 9 | - [Discourse Context Overlay](./discourse-context-overlay) 10 | - [Discourse Attributes](./discourse-attributes) 11 | - [Node Index](./node-index) 12 | 13 | The extension has node settings tabs for each node you define, which provides a query over all of your nodes of that type. For example, you can go to the Claim tab to see all the Claim nodes in your graph. 14 | 15 | ![](/docs/roam/settings-node-index.png) 16 | 17 | This can be quite handy in multiplayer settings, for example, to quickly view the latest question nodes that have been added to the graph, or who authored them, by modifying the default index query, just like a [regular query](./querying-discourse-graph). 18 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/pages/nodes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Nodes" 3 | date: "2025-01-01" 4 | author: "" 5 | published: true 6 | --- 7 | 8 | - `discourse node` 9 | 10 | - as defined in your grammar (e.g., base grammar will include CLM or EVD, for example) 11 | 12 | - `block` 13 | - `page` 14 | 15 | It is not possible to directly specify that a source or target node in a relation pattern is a `block` or `page`. These are variables that are defined implicitly at the moment, by what incoming and/or outgoing relations it connects to. 16 | 17 | For example, if a node's incoming relation is `references`, that implies it is a page. Similarly, if the node's incoming relation is `has child` or `has ancestor`, that implies the node is a block. 18 | 19 | When in doubt, check the preview of your relation pattern to ensure you're correctly expressing your intentions! 20 | 21 | ## Next 22 | 23 | - [Operators and Relations](./operators-relations) 24 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/pages/reading-clubs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Reading Clubs" 3 | date: "2025-01-01" 4 | author: "" 5 | published: true 6 | --- 7 | 8 | Description coming soon! 9 | 10 | [Get in touch](mailto:joechan@umd.edu) if you're interested in joining a Reading Club! 11 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/pages/relations-patterns.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Relations and Patterns" 3 | date: "2025-01-01" 4 | author: "" 5 | published: true 6 | --- 7 | 8 | ## Stock Patterns 9 | 10 | The extension ships with the ability to recognize three such writing/outlining patterns. Give them a try! 11 | 12 | - [Informed](#question-informed-by-evidence) 13 | - [Supported](#claim-supported-by-evidence) 14 | - [Opposed](#claim-opposed-by-evidence) 15 | 16 | ### Question Informed by Evidence 17 | 18 | - Go into a Question page. 19 | 20 | - Create a block, and reference an evidence page. 21 | 22 | Like this: 23 | 24 | ![](/docs/roam/relation-informs.png) 25 | 26 | The system now formally recognizes that this piece of evidence **informs** the question (and equivalently, the question is **informed by** that evidence)! 27 | 28 | ### Claim Supported by Evidence 29 | 30 | Create a block anywhere, and reference a claim page. We'll call this the claim block. 31 | 32 | Indent a block underneath the claim block. And reference the page `[[SupportedBy]]`. We'll call this the connecting block. 33 | 34 | Indent a block underneath the connecting block. And reference an evidence page. 35 | 36 | Like this: 37 | 38 | ![](/docs/roam/relation-supports.png) 39 | 40 | The system now formally recognizes that this piece of evidence **supports** that claim (and equivalently, the claim is **supported by** that evidence)! 41 | 42 | ### Claim Opposed by Evidence 43 | 44 | Create a block anywhere and reference a claim page. We'll call this the claim block. 45 | 46 | Indent a block underneath the claim block. And reference the page `[[OpposedBy]]`. We'll call this the connecting block. 47 | 48 | Indent a block underneath the connecting block. And reference an evidence page. 49 | 50 | Like this: 51 | 52 | ![](/docs/roam/relation-opposes.png) 53 | 54 | The system now formally recognizes that this piece of evidence **opposes** that claim (and equivalently, the claim is **opposed by** that evidence)! 55 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/pages/research-roadmapping.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Research Roadmapping" 3 | date: "2025-01-01" 4 | author: "" 5 | published: true 6 | --- 7 | 8 | Description coming soon! 9 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/docs/roam/pages/what-is-discourse-graph.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "What is Discourse Graph" 3 | date: "2025-01-01" 4 | author: "" 5 | published: true 6 | --- 7 | 8 | **Discourse graphs** are an information model for bodies of knowledge that emphasize discourse moves (such as questions, claims, and evidence), and relations (such as support or opposition), rather than papers or sources as the main units. 9 | 10 | To give an intuition for what it is, here is a figure of a visual representation of a simple discourse graph for a body of literature on bans and antisocial behavior in online forums. You may recognize similarities to things like argument maps. 11 | 12 | ![](/docs/roam/argument-map.png) 13 | 14 | Consider how that information model foregrounds the conceptual building blocks and relationships that are important for synthesis, compared to a typical "[iTunes for papers](http://joelchan.me/assets/pdf/2019-cscw-beyond-itunes-for-papers.pdf)" model that foregrounds documents in a way that forces us to drudge through [tedious extraction work](https://dl.acm.org/doi/abs/10.1145/3295750.3298937) before we can do the thinking we want to do! 15 | 16 | ![](/docs/roam/bans-hate-speech.png) 17 | 18 | This information model (theoretically!) has high potential for augmenting individual and collective "research-grade" synthesis (e.g., lit reviews for a dissertation or grant proposal). 19 | 20 | Discourse graphs are not a new idea (you can read more about it in [this short (academic-focused) write-up](http://joelchan.me/assets/pdf/Discourse_Graphs_for_Augmented_Knowledge_Synthesis_What_and_Why.pdf) or this more [practically-oriented article](https://oasislab.pubpub.org/pub/54t0y9mk/release/3)), but the potential to use it for everyday research work is! 21 | -------------------------------------------------------------------------------- /apps/website/app/(docs)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import clsx from "clsx"; 4 | import { customScrollbar } from "~/components/DocsLayout"; 5 | import { DESCRIPTION } from "~/data/constants"; 6 | import "~/globals.css"; 7 | 8 | const inter = Inter({ 9 | subsets: ["latin"], 10 | display: "swap", 11 | variable: "--font-inter", 12 | }); 13 | 14 | export const metadata: Metadata = { 15 | title: { 16 | template: "%s - Docs", 17 | default: "Discourse Graphs - Documentation", 18 | }, 19 | description: DESCRIPTION, 20 | }; 21 | 22 | export default function RootLayout({ 23 | children, 24 | }: { 25 | children: React.ReactNode; 26 | }) { 27 | return ( 28 | 33 | 34 |
{children}
35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /apps/website/app/(home)/blog/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { getAllBlogs } from "./readBlogs"; 3 | 4 | export default async function BlogIndex() { 5 | const blogs = await getAllBlogs(); 6 | return ( 7 |
8 |
9 |
10 |
11 |

All Updates

12 |
13 |
14 |
    15 | {blogs.length === 0 ? ( 16 |

    17 | No updates yet! Check back soon. 😊 18 |

    19 | ) : ( 20 | blogs.map((blog) => ( 21 |
  • 25 |
    26 | 30 | {blog.title} 31 | 32 |

    33 | {blog.date} 34 |

    35 |
    36 |
    37 | by {blog.author} 38 |
    39 |
  • 40 | )) 41 | )} 42 |
43 |
44 |
45 |
46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /apps/website/app/(home)/blog/posts/EXAMPLE.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Example Post - How to Create Blog Posts" 3 | date: "2024-01-01" 4 | author: "Devs" 5 | published: false # Set to true to make this post visible 6 | --- 7 | 8 | # How to Create Blog Posts 9 | 10 | This is a template post that shows how to create new blog posts. Keep this file as a reference and create new posts by following these instructions. 11 | 12 | ### Creating Blog Posts 13 | 14 | 1. [Fork](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) and [clone](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository) the repository. 15 | 2. [Create a new branch](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/managing-branches) for your update. 16 | 3. Copy this file (`EXAMPLE.md`) to create a new post 17 | 4. Rename the file to your desired URL slug (e.g., `my-new-post.md`) 18 | 5. Update the frontmatter and content 19 | 6. Set `published: true` when ready 20 | 7. Push your branch and open a pull request. 🚀 21 | 22 | ### Required Frontmatter 23 | 24 | Every post must start with frontmatter in YAML format: 25 | 26 | ```yaml 27 | --- 28 | title: "Your Post Title" 29 | date: "YYYY-MM-DD" 30 | author: "Author's name" 31 | published: true # Set to true to make the post visible 32 | --- 33 | ``` 34 | -------------------------------------------------------------------------------- /apps/website/app/(home)/blog/readBlogs.tsx: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs/promises"; 3 | import matter from "gray-matter"; 4 | import { BLOG_PATH } from "~/data/constants"; 5 | import { PageSchema, type PageData } from "~/types/schema"; 6 | 7 | const BLOG_DIRECTORY = path.join(process.cwd(), BLOG_PATH); 8 | 9 | async function validateBlogDirectory(): Promise { 10 | try { 11 | const stats = await fs.stat(BLOG_DIRECTORY); 12 | return stats.isDirectory(); 13 | } catch { 14 | console.log("No app/blog/posts directory found."); 15 | return false; 16 | } 17 | } 18 | 19 | async function processBlogFile(filename: string): Promise { 20 | try { 21 | const filePath = path.resolve(BLOG_DIRECTORY, filename); 22 | const fileContent = await fs.readFile(filePath, "utf-8"); 23 | const { data } = matter(fileContent); 24 | const validatedData = PageSchema.parse(data); 25 | 26 | return { 27 | slug: filename.replace(/\.md$/, ""), 28 | ...validatedData, 29 | }; 30 | } catch (error) { 31 | console.error(`Error processing blog file ${filename}:`, error); 32 | return null; 33 | } 34 | } 35 | 36 | export async function getAllBlogs(): Promise { 37 | try { 38 | const directoryExists = await validateBlogDirectory(); 39 | if (!directoryExists) return []; 40 | 41 | const files = await fs.readdir(BLOG_DIRECTORY); 42 | const blogs = await Promise.all( 43 | files.filter((filename) => filename.endsWith(".md")).map(processBlogFile), 44 | ); 45 | const validBlogs = blogs.filter((blog): blog is PageData => blog !== null); 46 | return validBlogs.filter((blog) => blog.published); 47 | } catch (error) { 48 | console.error("Error reading blog directory:", error); 49 | return []; 50 | } 51 | } 52 | 53 | export async function getLatestBlogs(): Promise { 54 | const blogs = await getAllBlogs(); 55 | return blogs 56 | .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) 57 | .slice(0, 3); 58 | } 59 | -------------------------------------------------------------------------------- /apps/website/app/PostHogPageView.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { usePathname, useSearchParams } from "next/navigation"; 4 | import { useEffect, Suspense } from "react"; 5 | import { usePostHog } from "posthog-js/react"; 6 | 7 | function PostHogPageView() { 8 | const pathname = usePathname(); 9 | const searchParams = useSearchParams(); 10 | const posthog = usePostHog(); 11 | 12 | useEffect(() => { 13 | if (pathname && posthog) { 14 | let url = window.origin + pathname; 15 | if (searchParams.toString()) { 16 | url = url + `?${searchParams.toString()}`; 17 | } 18 | posthog.capture("$pageview", { 19 | $current_url: url, 20 | }); 21 | } 22 | }, [pathname, searchParams, posthog]); 23 | 24 | return null; 25 | } 26 | 27 | export default function SuspendedPostHogPageView() { 28 | return ( 29 | 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /apps/website/app/api/errors/EmailTemplate.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorEmailProps } from "@repo/types"; 2 | import React from "react"; 3 | 4 | // TODO: use react.email 5 | export const EmailTemplate = ({ 6 | errorMessage, 7 | errorStack, 8 | type, 9 | app, 10 | graphName, 11 | context, 12 | }: ErrorEmailProps) => { 13 | return ( 14 |
15 |

Error Report

16 | 17 |

Type: {type}

18 |

App: {app}

19 |

Graph Name: {graphName}

20 | 21 |
22 |

Error Details

23 |
{errorMessage}
24 |
25 | 26 | {context != null && Object.keys(context).length > 0 && ( 27 |
28 |

Additional Data

29 |
{JSON.stringify(context, null, 2)}
30 |
31 | )} 32 | 33 | {errorStack && ( 34 |
35 |

Stack Trace

36 |
{errorStack}
37 |
38 | )} 39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /apps/website/app/api/llm/anthropic/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | import { handleLLMRequest, handleOptionsRequest } from "~/utils/llm/handler"; 3 | import { anthropicConfig } from "~/utils/llm/providers"; 4 | 5 | export const preferredRegion = "auto"; 6 | export const maxDuration = 300; 7 | export const runtime = "nodejs"; 8 | 9 | export const POST = (request: NextRequest): Promise => { 10 | return handleLLMRequest(request, anthropicConfig); 11 | }; 12 | 13 | export const OPTIONS = (request: NextRequest): Promise => { 14 | return handleOptionsRequest(request); 15 | }; 16 | -------------------------------------------------------------------------------- /apps/website/app/api/llm/gemini/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | import { handleLLMRequest, handleOptionsRequest } from "~/utils/llm/handler"; 3 | import { geminiConfig } from "~/utils/llm/providers"; 4 | 5 | export const preferredRegion = "auto"; 6 | export const maxDuration = 300; 7 | export const runtime = "nodejs"; 8 | 9 | export const POST = (request: NextRequest): Promise => { 10 | return handleLLMRequest(request, geminiConfig); 11 | }; 12 | 13 | export const OPTIONS = (request: NextRequest): Promise => { 14 | return handleOptionsRequest(request); 15 | }; 16 | -------------------------------------------------------------------------------- /apps/website/app/api/llm/openai/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | import { handleLLMRequest, handleOptionsRequest } from "~/utils/llm/handler"; 3 | import { openaiConfig } from "~/utils/llm/providers"; 4 | 5 | export const runtime = "nodejs"; 6 | export const preferredRegion = "auto"; 7 | export const maxDuration = 300; 8 | 9 | export const POST = (request: NextRequest): Promise => { 10 | return handleLLMRequest(request, openaiConfig); 11 | }; 12 | 13 | export const OPTIONS = (request: NextRequest): Promise => { 14 | return handleOptionsRequest(request); 15 | }; 16 | -------------------------------------------------------------------------------- /apps/website/app/api/supabase/account/[id].ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultOptionsHandler, 3 | makeDefaultGetHandler, 4 | makeDefaultDeleteHandler, 5 | } from "~/utils/supabase/apiUtils"; 6 | 7 | export const GET = makeDefaultGetHandler("Account"); 8 | 9 | export const OPTIONS = defaultOptionsHandler; 10 | 11 | export const DELETE = makeDefaultDeleteHandler("Account"); 12 | -------------------------------------------------------------------------------- /apps/website/app/api/supabase/content-embedding/[id].ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultOptionsHandler, 3 | makeDefaultGetHandler, 4 | makeDefaultDeleteHandler, 5 | } from "~/utils/supabase/apiUtils"; 6 | 7 | // TODO: Make model agnostic 8 | 9 | export const GET = makeDefaultGetHandler( 10 | "ContentEmbedding_openai_text_embedding_3_small_1536", 11 | "targetId", 12 | ); 13 | 14 | export const DELETE = makeDefaultDeleteHandler( 15 | "ContentEmbedding_openai_text_embedding_3_small_1536", 16 | "targetId", 17 | ); 18 | 19 | export const OPTIONS = defaultOptionsHandler; 20 | -------------------------------------------------------------------------------- /apps/website/app/api/supabase/content/[id].ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultOptionsHandler, 3 | makeDefaultGetHandler, 4 | makeDefaultDeleteHandler, 5 | } from "~/utils/supabase/apiUtils"; 6 | 7 | export const GET = makeDefaultGetHandler("Content"); 8 | 9 | export const OPTIONS = defaultOptionsHandler; 10 | 11 | export const DELETE = makeDefaultDeleteHandler("Content"); 12 | -------------------------------------------------------------------------------- /apps/website/app/api/supabase/content/batch/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse, NextRequest } from "next/server"; 2 | import type { PostgrestResponse } from "@supabase/supabase-js"; 3 | import { createClient } from "~/utils/supabase/server"; 4 | import { 5 | createApiResponse, 6 | handleRouteError, 7 | defaultOptionsHandler, 8 | asPostgrestFailure, 9 | } from "~/utils/supabase/apiUtils"; 10 | import { validateAndInsertBatch } from "~/utils/supabase/dbUtils"; 11 | import { 12 | contentInputValidation, 13 | type ContentDataInput, 14 | type ContentRecord, 15 | } from "~/utils/supabase/validators"; 16 | 17 | const batchInsertContentProcess = async ( 18 | supabase: Awaited>, 19 | contentItems: ContentDataInput[], 20 | ): Promise> => { 21 | return validateAndInsertBatch<"Content">({ 22 | supabase, 23 | tableName: "Content", 24 | items: contentItems, 25 | uniqueOn: ["space_id", "source_local_id"], 26 | inputValidator: contentInputValidation, 27 | }); 28 | }; 29 | 30 | export const POST = async (request: NextRequest): Promise => { 31 | const supabase = await createClient(); 32 | 33 | try { 34 | const body: ContentDataInput[] = await request.json(); 35 | if (!Array.isArray(body)) { 36 | return createApiResponse( 37 | request, 38 | asPostgrestFailure( 39 | "Request body must be an array of content items.", 40 | "array", 41 | ), 42 | ); 43 | } 44 | 45 | const result = await batchInsertContentProcess(supabase, body); 46 | 47 | return createApiResponse(request, result); 48 | } catch (e: unknown) { 49 | return handleRouteError(request, e, "/api/supabase/content/batch"); 50 | } 51 | }; 52 | 53 | export const OPTIONS = defaultOptionsHandler; 54 | -------------------------------------------------------------------------------- /apps/website/app/api/supabase/content/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse, NextRequest } from "next/server"; 2 | import type { PostgrestSingleResponse } from "@supabase/supabase-js"; 3 | 4 | import { createClient } from "~/utils/supabase/server"; 5 | import { getOrCreateEntity } from "~/utils/supabase/dbUtils"; 6 | import { 7 | createApiResponse, 8 | handleRouteError, 9 | defaultOptionsHandler, 10 | asPostgrestFailure, 11 | } from "~/utils/supabase/apiUtils"; 12 | import { contentInputValidation } from "~/utils/supabase/validators"; 13 | import { Tables, TablesInsert } from "@repo/database/types.gen.ts"; 14 | 15 | type ContentDataInput = TablesInsert<"Content">; 16 | type ContentRecord = Tables<"Content">; 17 | 18 | const processAndUpsertContentEntry = async ( 19 | supabasePromise: ReturnType, 20 | data: ContentDataInput, 21 | ): Promise> => { 22 | const error = contentInputValidation(data); 23 | if (error !== null) return asPostgrestFailure(error, "invalid"); 24 | 25 | const supabase = await supabasePromise; 26 | 27 | // If no solid matchCriteria for a "get", getOrCreateEntity will likely proceed to "create". 28 | // If there are unique constraints other than (space_id, source_local_id), it will handle race conditions. 29 | 30 | const result = await getOrCreateEntity<"Content">({ 31 | supabase, 32 | tableName: "Content", 33 | insertData: data, 34 | uniqueOn: ["space_id", "source_local_id"], 35 | }); 36 | 37 | return result; 38 | }; 39 | 40 | export const POST = async (request: NextRequest): Promise => { 41 | const supabasePromise = createClient(); 42 | 43 | try { 44 | const body: ContentDataInput = await request.json(); 45 | const result = await processAndUpsertContentEntry(supabasePromise, body); 46 | return createApiResponse(request, result); 47 | } catch (e: unknown) { 48 | return handleRouteError(request, e, "/api/supabase/content"); 49 | } 50 | }; 51 | 52 | export const OPTIONS = defaultOptionsHandler; 53 | -------------------------------------------------------------------------------- /apps/website/app/api/supabase/document/[id].ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultOptionsHandler, 3 | makeDefaultGetHandler, 4 | makeDefaultDeleteHandler, 5 | } from "~/utils/supabase/apiUtils"; 6 | 7 | export const GET = makeDefaultGetHandler("Document"); 8 | 9 | export const OPTIONS = defaultOptionsHandler; 10 | 11 | export const DELETE = makeDefaultDeleteHandler("Document"); 12 | -------------------------------------------------------------------------------- /apps/website/app/api/supabase/person/[id].ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultOptionsHandler, 3 | makeDefaultGetHandler, 4 | makeDefaultDeleteHandler, 5 | } from "~/utils/supabase/apiUtils"; 6 | 7 | export const GET = makeDefaultGetHandler("Person"); 8 | 9 | export const OPTIONS = defaultOptionsHandler; 10 | 11 | export const DELETE = makeDefaultDeleteHandler("Person"); 12 | -------------------------------------------------------------------------------- /apps/website/app/api/supabase/platform/[id].ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultOptionsHandler, 3 | makeDefaultGetHandler, 4 | makeDefaultDeleteHandler, 5 | } from "~/utils/supabase/apiUtils"; 6 | 7 | export const GET = makeDefaultGetHandler("Platform"); 8 | 9 | export const OPTIONS = defaultOptionsHandler; 10 | 11 | export const DELETE = makeDefaultDeleteHandler("Platform"); 12 | -------------------------------------------------------------------------------- /apps/website/app/api/supabase/similarity-rank/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse, NextRequest } from "next/server"; 2 | import { createClient } from "~/utils/supabase/server"; 3 | import { 4 | createApiResponse, 5 | handleRouteError, 6 | defaultOptionsHandler, 7 | asPostgrestFailure, 8 | } from "~/utils/supabase/apiUtils"; 9 | 10 | type SimilarityRankInput = { 11 | embedding: number[]; 12 | subsetRoamUids: string[]; 13 | }; 14 | 15 | export const POST = async (request: NextRequest): Promise => { 16 | try { 17 | const body: SimilarityRankInput = await request.json(); 18 | const { embedding, subsetRoamUids } = body; 19 | 20 | if ( 21 | !Array.isArray(embedding) || 22 | !Array.isArray(subsetRoamUids) || 23 | !subsetRoamUids.length 24 | ) { 25 | return createApiResponse( 26 | request, 27 | asPostgrestFailure( 28 | "Missing required fields: embedding and subsetRoamUids", 29 | "invalid", 30 | ), 31 | ); 32 | } 33 | const supabase = await createClient(); 34 | const response = await supabase.rpc("match_embeddings_for_subset_nodes", { 35 | p_query_embedding: JSON.stringify(embedding), 36 | p_subset_roam_uids: subsetRoamUids, 37 | }); 38 | 39 | return createApiResponse(request, response); 40 | } catch (e: unknown) { 41 | return handleRouteError(request, e, "/api/supabase/similarity-rank"); 42 | } 43 | }; 44 | 45 | export const OPTIONS = defaultOptionsHandler; 46 | -------------------------------------------------------------------------------- /apps/website/app/api/supabase/space/[id].ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultOptionsHandler, 3 | makeDefaultGetHandler, 4 | makeDefaultDeleteHandler, 5 | } from "~/utils/supabase/apiUtils"; 6 | 7 | export const GET = makeDefaultGetHandler("Space"); 8 | 9 | export const OPTIONS = defaultOptionsHandler; 10 | 11 | export const DELETE = makeDefaultDeleteHandler("Space"); 12 | -------------------------------------------------------------------------------- /apps/website/app/components/DocsHeader.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { usePathname } from "next/navigation"; 4 | import { NavigationList } from "./Navigation"; 5 | 6 | export function DocsHeader({ 7 | title, 8 | navigation = [], 9 | }: { 10 | title?: string; 11 | navigation?: NavigationList; 12 | }) { 13 | let pathname = usePathname(); 14 | let section = navigation.find((section) => 15 | section.links.find((link) => link.href === pathname), 16 | ); 17 | 18 | if (!title && !section) { 19 | return null; 20 | } 21 | 22 | return ( 23 |
24 | {section && ( 25 |

26 | {section.title} 27 |

28 | )} 29 | {title && ( 30 |

31 | {title} 32 |

33 | )} 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /apps/website/app/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "next/image"; 3 | 4 | export function Logo() { 5 | return ( 6 | 7 | Discourse Graphs Logo 13 | 14 | 15 | Discourse Graphs 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /apps/website/app/components/Prose.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | 3 | export function Prose({ 4 | as, 5 | className, 6 | ...props 7 | }: React.ComponentPropsWithoutRef & { 8 | as?: T; 9 | }) { 10 | let Component = as ?? "div"; 11 | 12 | return ( 13 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /apps/website/app/data/constants.ts: -------------------------------------------------------------------------------- 1 | import { TeamMember } from "~/components/TeamPerson"; 2 | 3 | export const DESCRIPTION = 4 | "Discourse Graphs are a tool and ecosystem for collaborative knowledge synthesis, enabling researchers to map ideas and arguments in a modular, composable graph format."; 5 | export const SHORT_DESCRIPTION = 6 | "A tool and ecosystem for collaborative knowledge synthesis"; 7 | export const BLOG_PATH = "app/(home)/blog/posts"; 8 | 9 | export const TEAM_MEMBERS: TeamMember[] = [ 10 | { 11 | name: "Joel Chan", 12 | title: "Research", 13 | image: "/team/joel.png", 14 | }, 15 | { 16 | name: "Matthew Akamatsu", 17 | title: "Research", 18 | image: "/team/matt.png", 19 | }, 20 | { 21 | name: "Michael Gartner", 22 | title: "Tech Lead", 23 | image: "/team/michael.png", 24 | }, 25 | { 26 | name: "John Morabito", 27 | title: "UX Lead", 28 | image: "/team/john.jpeg", 29 | }, 30 | { 31 | name: "Siddharth Yadav", 32 | title: "Developer", 33 | image: "/team/sid.jpg", 34 | }, 35 | { 36 | name: "Trang Doan", 37 | title: "Developer", 38 | image: "/team/trang.png", 39 | }, 40 | { 41 | name: "Marc-Antoine Parent", 42 | title: "Developer", 43 | image: "/team/maparent.jpg", 44 | }, 45 | { 46 | name: "David Vargas", 47 | title: "Advisor", 48 | image: "/team/david.png", 49 | }, 50 | ]; 51 | -------------------------------------------------------------------------------- /apps/website/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /apps/website/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /apps/website/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | a { 6 | @apply text-secondary hover:text-secondary/60 transition-colors; 7 | } 8 | 9 | html { 10 | @apply scroll-smooth; 11 | } 12 | 13 | :root { 14 | --background: #ffffff; 15 | --foreground: #171717; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | :root { 20 | --background: #0a0a0a; 21 | --foreground: #ededed; 22 | } 23 | } 24 | 25 | body { 26 | color: var(--foreground); 27 | background: var(--background); 28 | font-family: Arial, Helvetica, sans-serif; 29 | } 30 | -------------------------------------------------------------------------------- /apps/website/app/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/app/icon.ico -------------------------------------------------------------------------------- /apps/website/app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import posthog from "posthog-js"; 4 | import { PostHogProvider as PHProvider } from "posthog-js/react"; 5 | import { useEffect } from "react"; 6 | import PostHogPageView from "./PostHogPageView"; 7 | 8 | if ( 9 | !process.env.NEXT_PUBLIC_POSTHOG_KEY || 10 | !process.env.NEXT_PUBLIC_POSTHOG_HOST 11 | ) { 12 | throw new Error("PostHog environment variables are not set"); 13 | } 14 | 15 | export function PostHogProvider({ children }: { children: React.ReactNode }) { 16 | useEffect(() => { 17 | posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { 18 | api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST!, 19 | capture_pageview: false, 20 | }); 21 | }, []); 22 | 23 | return ( 24 | 25 | 26 | {children} 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /apps/website/app/types/llm.ts: -------------------------------------------------------------------------------- 1 | export type Message = { 2 | role: string; 3 | content: string; 4 | }; 5 | 6 | export type Settings = { 7 | model: string; 8 | maxTokens: number; 9 | temperature: number; 10 | safetySettings?: Array<{ 11 | category: string; 12 | threshold: string; 13 | }>; 14 | }; 15 | 16 | export type RequestBody = { 17 | documents: Message[]; 18 | passphrase?: string; 19 | settings: Settings; 20 | }; 21 | 22 | export const CONTENT_TYPE_JSON = "application/json"; 23 | export const CONTENT_TYPE_TEXT = "text/plain"; 24 | 25 | export type LLMProviderConfig = { 26 | apiKeyEnvVar: string; 27 | apiUrl: string | ((settings: Settings) => string); 28 | apiHeaders: (apiKey: string) => Record; 29 | formatRequestBody: (messages: Message[], settings: Settings) => any; 30 | extractResponseText: (responseData: any) => string | null; 31 | errorMessagePath: string; 32 | }; 33 | -------------------------------------------------------------------------------- /apps/website/app/types/schema.tsx: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const PageSchema = z.object({ 4 | title: z.string(), 5 | published: z.boolean().default(false), 6 | date: z.string(), 7 | author: z.string(), 8 | }); 9 | export type PageFrontmatter = z.infer; 10 | export type PageData = PageFrontmatter & { 11 | slug: string; 12 | }; 13 | 14 | export type Node = { 15 | type: string; 16 | attributes: Record; 17 | children?: Node[]; 18 | }; 19 | export type HeadingNode = Node & { 20 | type: "heading"; 21 | attributes: { 22 | level: 1 | 2 | 3 | 4 | 5 | 6; 23 | id?: string; 24 | [key: string]: unknown; 25 | }; 26 | }; 27 | export type H2Node = HeadingNode & { 28 | attributes: { 29 | level: 2; 30 | }; 31 | }; 32 | export type H3Node = HeadingNode & { 33 | attributes: { 34 | level: 3; 35 | }; 36 | }; 37 | export type Section = H2Node["attributes"] & { 38 | id: string; 39 | title: string; 40 | children: Array; 41 | }; 42 | export type Subsection = H3Node["attributes"] & { 43 | id: string; 44 | title: string; 45 | children?: undefined; 46 | }; 47 | -------------------------------------------------------------------------------- /apps/website/app/utils/getFileContent.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs/promises"; 3 | 4 | type Props = { 5 | filename: string; 6 | directory: string; 7 | }; 8 | 9 | export const getFileContent = async ({ 10 | filename, 11 | directory, 12 | }: Props): Promise => { 13 | try { 14 | const safeFilename = path.basename(filename); 15 | const filePath = path.join(directory, safeFilename); 16 | const fileContent = await fs.readFile(filePath, "utf-8"); 17 | return fileContent; 18 | } catch (error) { 19 | throw error instanceof Error 20 | ? new Error(`Failed to read file ${filename}: ${error.message}`, { 21 | cause: error, 22 | }) 23 | : new Error(`Failed to read file ${filename}: Unknown error`); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /apps/website/app/utils/getHtmlFromMarkdown.ts: -------------------------------------------------------------------------------- 1 | import { unified } from "unified"; 2 | import remarkParse from "remark-parse"; 3 | import remarkRehype from "remark-rehype"; 4 | import rehypeStringify from "rehype-stringify"; 5 | import { toString } from "mdast-util-to-string"; 6 | import { visit } from "unist-util-visit"; 7 | import type { Root } from "mdast"; 8 | 9 | function remarkHeadingId() { 10 | return (tree: Root) => { 11 | visit(tree, "heading", (node) => { 12 | const text = toString(node); 13 | const id = text 14 | .toLowerCase() 15 | .replace(/[^a-z0-9]+/g, "-") 16 | .replace(/(^-|-$)/g, ""); 17 | 18 | node.data = { 19 | hName: `h${node.depth}`, 20 | hProperties: { id }, 21 | }; 22 | }); 23 | }; 24 | } 25 | 26 | export async function getHtmlFromMarkdown(markdown: string): Promise { 27 | if (!markdown) { 28 | throw new Error('Markdown content is required'); 29 | } 30 | 31 | try { 32 | const htmlString = await unified() 33 | .use(remarkParse) 34 | .use(remarkHeadingId) 35 | .use(remarkRehype) 36 | .use(rehypeStringify) 37 | .process(markdown); 38 | return htmlString.toString(); 39 | } catch (error) { 40 | console.error('Error processing markdown:', error); 41 | throw new Error('Failed to process markdown content'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /apps/website/app/utils/getProcessedMarkdownFile.ts: -------------------------------------------------------------------------------- 1 | import { getHtmlFromMarkdown } from "~/utils/getHtmlFromMarkdown"; 2 | import { getFileContent } from "~/utils/getFileContent"; 3 | import { notFound } from "next/navigation"; 4 | import { PageFrontmatter, PageSchema } from "~/types/schema"; 5 | import matter from "gray-matter"; 6 | 7 | type Props = { 8 | slug: string; 9 | directory: string; 10 | }; 11 | 12 | type ProcessedMarkdownPage = { 13 | data: PageFrontmatter; 14 | contentHtml: string; 15 | }; 16 | 17 | export const getProcessedMarkdownFile = async ({ 18 | slug, 19 | directory, 20 | }: Props): Promise => { 21 | try { 22 | if (!slug || !directory) { 23 | throw new Error("Both slug and directory are required"); 24 | } 25 | 26 | // Prevent directory traversal 27 | if (slug.includes("..") || directory.includes("..")) { 28 | throw new Error("Invalid path"); 29 | } 30 | 31 | const fileContent = await getFileContent({ 32 | filename: `${slug}.md`, 33 | directory, 34 | }); 35 | const { data: rawData, content } = matter(fileContent); 36 | const data = PageSchema.parse(rawData); 37 | 38 | if (!data.published) { 39 | console.log(`Post ${slug} is not published`); 40 | return notFound(); 41 | } 42 | 43 | const contentHtml = await getHtmlFromMarkdown(content); 44 | 45 | return { data, contentHtml }; 46 | } catch (error) { 47 | console.error("Error loading blog post:", error); 48 | return notFound(); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /apps/website/app/utils/llm/cors.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | 3 | const allowedOrigins = ["https://roamresearch.com", "http://localhost:3000"]; 4 | 5 | const isVercelPreviewUrl = (origin: string): boolean => 6 | origin.includes(".vercel.app") || origin.includes("discourse-graph"); 7 | 8 | const isAllowedOrigin = (origin: string): boolean => 9 | allowedOrigins.some((allowed) => origin.startsWith(allowed)) || 10 | isVercelPreviewUrl(origin); 11 | 12 | export default function cors(req: NextRequest, res: Response) { 13 | const origin = req.headers.get("origin"); 14 | const originIsAllowed = origin && isAllowedOrigin(origin); 15 | 16 | if (req.method === "OPTIONS") { 17 | return new Response(null, { 18 | status: 204, 19 | headers: { 20 | ...(originIsAllowed ? { "Access-Control-Allow-Origin": origin } : {}), 21 | "Access-Control-Allow-Methods": "GET, POST, OPTIONS", 22 | "Access-Control-Allow-Headers": 23 | "Content-Type, Authorization, x-vercel-protection-bypass", 24 | "Access-Control-Max-Age": "86400", 25 | }, 26 | }); 27 | } 28 | 29 | if (originIsAllowed) { 30 | res.headers.set("Access-Control-Allow-Origin", origin as string); 31 | res.headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); 32 | res.headers.set( 33 | "Access-Control-Allow-Headers", 34 | "Content-Type, Authorization, x-vercel-protection-bypass", 35 | ); 36 | } 37 | 38 | return res; 39 | } 40 | -------------------------------------------------------------------------------- /apps/website/app/utils/supabase/server.ts: -------------------------------------------------------------------------------- 1 | import { createServerClient, type CookieOptions } from "@supabase/ssr"; 2 | import { cookies } from "next/headers"; 3 | import { Database } from "@repo/database/types.gen.ts"; 4 | 5 | export const createClient = async () => { 6 | const cookieStore = await cookies(); 7 | const supabaseUrl = process.env.SUPABASE_URL; 8 | const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; 9 | 10 | if (!supabaseUrl || !supabaseKey) { 11 | throw new Error("Missing required Supabase environment variables"); 12 | } 13 | 14 | // following https://supabase.com/docs/guides/auth/server-side/creating-a-client?queryGroups=environment&environment=server 15 | return createServerClient(supabaseUrl, supabaseKey, { 16 | cookies: { 17 | getAll() { 18 | return cookieStore.getAll(); 19 | }, 20 | setAll( 21 | cookiesToSet: { 22 | name: string; 23 | value: string; 24 | options: CookieOptions; 25 | }[], 26 | ) { 27 | try { 28 | cookiesToSet.forEach( 29 | ({ 30 | name, 31 | value, 32 | options, 33 | }: { 34 | name: string; 35 | value: string; 36 | options: CookieOptions; 37 | }) => cookieStore.set(name, value, options), 38 | ); 39 | } catch { 40 | // The `setAll` method was called from a Server Component. 41 | // This can be ignored if you have middleware refreshing 42 | // user sessions. 43 | } 44 | }, 45 | }, 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /apps/website/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig = { 4 | reactStrictMode: true, 5 | serverRuntimeConfig: { 6 | maxDuration: 300, 7 | }, 8 | }; 9 | export default nextConfig; 10 | -------------------------------------------------------------------------------- /apps/website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.1.0", 4 | "description": "https://discoursegraphs.com NextJs Website", 5 | "private": true, 6 | "license": "Apache-2.0", 7 | "scripts": { 8 | "dev": "next dev", 9 | "build": "next build", 10 | "start": "next start", 11 | "lint": "next lint" 12 | }, 13 | "dependencies": { 14 | "@repo/types": "*", 15 | "@repo/ui": "*", 16 | "@sindresorhus/slugify": "^2.2.1", 17 | "@supabase/ssr": "^0.6.1", 18 | "gray-matter": "^4.0.3", 19 | "next": "^15.0.3", 20 | "openai": "^4.98.0", 21 | "react": "19.0.0-rc-66855b96-20241106", 22 | "react-dom": "19.0.0-rc-66855b96-20241106", 23 | "rehype-parse": "^9.0.1", 24 | "rehype-stringify": "^10.0.1", 25 | "remark": "^15.0.1", 26 | "remark-html": "^16.0.1", 27 | "remark-rehype": "^11.1.1", 28 | "resend": "^4.0.1", 29 | "zod": "^3.24.1" 30 | }, 31 | "devDependencies": { 32 | "@repo/database": "*", 33 | "@repo/eslint-config": "*", 34 | "@repo/tailwind-config": "*", 35 | "@repo/typescript-config": "*", 36 | "@tailwindcss/typography": "^0.5.15", 37 | "@types/node": "^20", 38 | "@types/react": "^18", 39 | "@types/react-dom": "^18", 40 | "autoprefixer": "^10.4.20", 41 | "postcss": "^8.4.49", 42 | "tailwindcss": "^3.4.16", 43 | "typescript": "^5" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /apps/website/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /apps/website/public/MATSU_lab_journal_club_graph_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/MATSU_lab_journal_club_graph_view.png -------------------------------------------------------------------------------- /apps/website/public/discord.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/website/public/docs/roam/argument-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/argument-map.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/bans-hate-speech.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/bans-hate-speech.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/browse-roam-depot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/browse-roam-depot.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/command-palette-export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/command-palette-export.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/command-palette-query-drawer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/command-palette-query-drawer.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/discourse-context-filter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/discourse-context-filter.gif -------------------------------------------------------------------------------- /apps/website/public/docs/roam/discourse-context-group-by-target.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/discourse-context-group-by-target.gif -------------------------------------------------------------------------------- /apps/website/public/docs/roam/discourse-context-overlay-score.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/discourse-context-overlay-score.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/discourse-context-overlay.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/discourse-context-overlay.gif -------------------------------------------------------------------------------- /apps/website/public/docs/roam/find-in-roam-depot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/find-in-roam-depot.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/install-instruction-roam-depot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/install-instruction-roam-depot.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/load-from-url1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/load-from-url1.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/load-from-url2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/load-from-url2.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/node-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/node-template.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/query-drawer-advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/query-drawer-advanced.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/query-drawer-advanced2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/query-drawer-advanced2.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/query-drawer-advanced3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/query-drawer-advanced3.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/query-drawer-informs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/query-drawer-informs.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/query-drawer-naming.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/query-drawer-naming.gif -------------------------------------------------------------------------------- /apps/website/public/docs/roam/query-drawer-save-to-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/query-drawer-save-to-page.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/query-drawer-supports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/query-drawer-supports.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/query-drawer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/query-drawer.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/relation-informs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/relation-informs.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/relation-opposes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/relation-opposes.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/relation-supports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/relation-supports.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/roam-depot-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/roam-depot-settings.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/roam-depot-sidebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/roam-depot-sidebar.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/settings-discourse-attributes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/settings-discourse-attributes.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/settings-discourse-context-overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/settings-discourse-context-overlay.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/settings-export-frontmatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/settings-export-frontmatter.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/settings-export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/settings-export.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/settings-node-index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/settings-node-index.png -------------------------------------------------------------------------------- /apps/website/public/docs/roam/settings-relation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/docs/roam/settings-relation.png -------------------------------------------------------------------------------- /apps/website/public/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /apps/website/public/logo-screenshot-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/logo-screenshot-48.png -------------------------------------------------------------------------------- /apps/website/public/section1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/section1.webp -------------------------------------------------------------------------------- /apps/website/public/section2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/section2.webp -------------------------------------------------------------------------------- /apps/website/public/section3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/section3.webp -------------------------------------------------------------------------------- /apps/website/public/section4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/section4.webp -------------------------------------------------------------------------------- /apps/website/public/section5a.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/section5a.webp -------------------------------------------------------------------------------- /apps/website/public/section5b.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/section5b.webp -------------------------------------------------------------------------------- /apps/website/public/section6.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/section6.webp -------------------------------------------------------------------------------- /apps/website/public/smile-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/website/public/social/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/website/public/social/website.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/social/website.png -------------------------------------------------------------------------------- /apps/website/public/social/x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/social/x.png -------------------------------------------------------------------------------- /apps/website/public/team/david.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/team/david.png -------------------------------------------------------------------------------- /apps/website/public/team/joel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/team/joel.png -------------------------------------------------------------------------------- /apps/website/public/team/john.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/team/john.jpeg -------------------------------------------------------------------------------- /apps/website/public/team/maparent.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/team/maparent.jpg -------------------------------------------------------------------------------- /apps/website/public/team/matt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/team/matt.png -------------------------------------------------------------------------------- /apps/website/public/team/michael.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/team/michael.png -------------------------------------------------------------------------------- /apps/website/public/team/sid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/team/sid.jpg -------------------------------------------------------------------------------- /apps/website/public/team/trang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiscourseGraphs/discourse-graph/901e4738633ed8a084740ab446b6b0c0920694af/apps/website/public/team/trang.png -------------------------------------------------------------------------------- /apps/website/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import sharedConfig from "@repo/tailwind-config"; 3 | 4 | const config: Pick = { 5 | content: [ 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | presets: [sharedConfig], 10 | plugins: [require('@tailwindcss/typography')], 11 | }; 12 | 13 | export default config; 14 | -------------------------------------------------------------------------------- /apps/website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/nextjs.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "~/*": ["./app/*"] 6 | }, 7 | "plugins": [ 8 | { 9 | "name": "next" 10 | } 11 | ] 12 | }, 13 | "include": [ 14 | "next-env.d.ts", 15 | "next.config.mjs", 16 | "**/*.ts", 17 | "**/*.tsx", 18 | ".next/types/**/*.ts", 19 | "postcss.config.mjs" 20 | ], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /apps/website/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/releases/:path*", 5 | "destination": "https://6b4k1ntlti17rkf1.public.blob.vercel-storage.com/releases/:path*" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discourse-graph", 3 | "private": true, 4 | "license": "Apache-2.0", 5 | "scripts": { 6 | "build": "turbo build", 7 | "dev": "turbo dev", 8 | "lint": "turbo lint", 9 | "deploy": "turbo deploy", 10 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 11 | }, 12 | "devDependencies": { 13 | "prettier": "^3.4.2", 14 | "prettier-plugin-tailwindcss": "^0.6.9", 15 | "turbo": "^2.3.0", 16 | "typescript": "5.5.4" 17 | }, 18 | "engines": { 19 | "node": ">=18" 20 | }, 21 | "packageManager": "npm@10.8.2", 22 | "workspaces": [ 23 | "apps/*", 24 | "packages/*" 25 | ], 26 | "prettier": { 27 | "plugins": [ 28 | "prettier-plugin-tailwindcss" 29 | ], 30 | "tailwindConfig": "./packages/tailwind-config/tailwind.config.ts" 31 | }, 32 | "dependencies": { 33 | "posthog-js": "^1.203.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/database/.sqruff: -------------------------------------------------------------------------------- 1 | [sqruff] 2 | dialect = postgres 3 | exclude_rules = CP05,LT05 4 | 5 | [sqruff:indentation] 6 | indent_unit = space 7 | tab_space_size = 4 8 | indented_joins = True 9 | -------------------------------------------------------------------------------- /packages/database/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/database", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "Apache-2.0", 6 | "type": "module", 7 | "exports": { 8 | "./types.gen.ts": "./types.gen.ts" 9 | }, 10 | "scripts": { 11 | "init": "supabase login", 12 | "dev": "supabase start", 13 | "stop": "supabase stop", 14 | "check-types": "npm run lint && supabase stop && npm run dbdiff", 15 | "lint": "tsx scripts/lint.ts", 16 | "lint:fix": "tsx scripts/lint.ts -f", 17 | "build": "if [ $HOME != '/vercel' ]; then\n supabase start && supabase migrations up && (supabase gen types typescript --local --schema public > types.gen.ts)\nfi", 18 | "gentypes:production": "supabase start && supabase gen types typescript --project-id \"$SUPABASE_PROJECT_ID\" --schema public > types.gen.ts", 19 | "dbdiff": "supabase stop && supabase db diff", 20 | "dbdiff:save": "supabase stop && supabase db diff -f", 21 | "deploy": "tsx scripts/deploy.ts", 22 | "deploy:functions": "tsx scripts/lint.ts -f" 23 | }, 24 | "devDependencies": { 25 | "supabase": "^2.22.12", 26 | "tsx": "^4.19.2" 27 | }, 28 | "dependencies": {} 29 | } 30 | -------------------------------------------------------------------------------- /packages/database/scripts/lint.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "node:child_process"; 2 | 3 | const main = () => { 4 | try { 5 | exec("which sqruff", (err, stdout, stderr) => { 6 | if (err) { 7 | console.error("Could not find sqruff, you may want to install it."); 8 | // Fail gracefully 9 | process.exit(0); 10 | } 11 | const command = 12 | process.argv.length == 3 && process.argv[2] == "-f" ? "fix" : "lint"; 13 | exec(`sqruff ${command} supabase/schemas`, {}, (err, stdout, stderr) => { 14 | console.log(`${stdout}`); 15 | console.log(`${stderr}`); 16 | process.exit(err ? err.code : 0); 17 | }); 18 | }); 19 | } catch (error) { 20 | console.error("error:", error); 21 | process.exit(1); 22 | } 23 | }; 24 | if (import.meta.url === `file://${process.argv[1]}`) main(); 25 | -------------------------------------------------------------------------------- /packages/database/supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | 5 | # dotenvx 6 | .env.keys 7 | .env.local 8 | .env.*.local 9 | -------------------------------------------------------------------------------- /packages/database/supabase/migrations/20250504195841_remote_schema.sql: -------------------------------------------------------------------------------- 1 | create extension if not exists "pg_jsonschema" with schema "extensions"; 2 | 3 | create extension if not exists "pg_stat_monitor" with schema "extensions"; 4 | 5 | create extension if not exists "pgroonga" with schema "extensions"; 6 | 7 | create extension if not exists "vector" with schema "extensions"; 8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/database/supabase/migrations/20250506174523_content_idx_id.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX "Content_space_and_id" ON "Content" (space_id, source_local_id) WHERE 2 | source_local_id IS NOT NULL; 3 | -------------------------------------------------------------------------------- /packages/database/supabase/migrations/20250520132747_restrict_search_by_document.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION public.match_content_embeddings(query_embedding vector, match_threshold double precision, match_count integer, current_document_id integer DEFAULT NULL::integer) 2 | RETURNS TABLE(content_id bigint, roam_uid text, text_content text, similarity double precision) 3 | LANGUAGE sql 4 | STABLE 5 | AS $function$ 6 | SELECT 7 | c.id AS content_id, 8 | c.source_local_id AS roam_uid, 9 | c.text AS text_content, 10 | 1 - (ce.vector <=> query_embedding) AS similarity 11 | FROM public."ContentEmbedding_openai_text_embedding_3_small_1536" AS ce 12 | JOIN public."Content" AS c ON ce.target_id = c.id 13 | WHERE 1 - (ce.vector <=> query_embedding) > match_threshold 14 | AND ce.obsolete = FALSE 15 | AND (current_document_id IS NULL OR c.document_id = current_document_id) 16 | ORDER BY 17 | ce.vector <=> query_embedding ASC 18 | LIMIT match_count; 19 | $function$; 20 | 21 | -- Supabase wants to replace this function for no obvious reason. Letting it. 22 | 23 | CREATE OR REPLACE FUNCTION public.match_embeddings_for_subset_nodes(p_query_embedding vector, p_subset_roam_uids text[]) 24 | RETURNS TABLE(content_id bigint, roam_uid text, text_content text, similarity double precision) 25 | LANGUAGE sql 26 | STABLE 27 | AS $function$ 28 | WITH subset_content_with_embeddings AS ( 29 | -- Step 1: Identify content and fetch embeddings ONLY for the nodes in the provided Roam UID subset 30 | SELECT 31 | c.id AS content_id, 32 | c.source_local_id AS roam_uid, 33 | c.text AS text_content, 34 | ce.vector AS embedding_vector 35 | FROM public."Content" AS c 36 | JOIN public."ContentEmbedding_openai_text_embedding_3_small_1536" AS ce ON c.id = ce.target_id 37 | WHERE 38 | c.source_local_id = ANY(p_subset_roam_uids) -- Filter Content by the provided Roam UIDs 39 | AND ce.obsolete = FALSE 40 | ) 41 | SELECT 42 | ss_ce.content_id, 43 | ss_ce.roam_uid, 44 | ss_ce.text_content, 45 | 1 - (ss_ce.embedding_vector <=> p_query_embedding) AS similarity 46 | FROM subset_content_with_embeddings AS ss_ce 47 | ORDER BY similarity DESC; -- Order by calculated similarity, highest first 48 | $function$; 49 | -------------------------------------------------------------------------------- /packages/database/supabase/migrations/20250520133551_nodes_needing_sync.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION public.get_nodes_needing_sync(nodes_from_roam jsonb) 2 | RETURNS TABLE(uid_to_sync text) 3 | LANGUAGE plpgsql 4 | AS $function$ 5 | DECLARE 6 | node_info jsonb; 7 | roam_node_uid TEXT; 8 | roam_node_edit_epoch_ms BIGINT; 9 | content_db_last_modified_epoch_ms BIGINT; 10 | BEGIN 11 | FOR node_info IN SELECT * FROM jsonb_array_elements(nodes_from_roam) 12 | LOOP 13 | roam_node_uid := (node_info->>'uid')::text; 14 | roam_node_edit_epoch_ms := (node_info->>'roam_edit_time')::bigint; 15 | 16 | -- Get the last_modified time from your Content table for the current node, converting it to epoch milliseconds 17 | -- Assumes your 'last_modified' column in 'Content' is a timestamp type 18 | SELECT EXTRACT(EPOCH FROM c.last_modified) * 1000 19 | INTO content_db_last_modified_epoch_ms 20 | FROM public."Content" c -- Ensure "Content" matches your table name exactly (case-sensitive if quoted) 21 | WHERE c.source_local_id = roam_node_uid; 22 | 23 | IF NOT FOUND THEN 24 | -- Node does not exist in Supabase Content table, so it needs sync 25 | uid_to_sync := roam_node_uid; 26 | RETURN NEXT; 27 | ELSE 28 | -- Node exists, compare timestamps 29 | IF roam_node_edit_epoch_ms > content_db_last_modified_epoch_ms THEN 30 | uid_to_sync := roam_node_uid; 31 | RETURN NEXT; 32 | END IF; 33 | END IF; 34 | END LOOP; 35 | RETURN; 36 | END; 37 | $function$ 38 | ; 39 | -------------------------------------------------------------------------------- /packages/database/supabase/migrations/20250522193823_rename_discourse_space.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE public."DiscoursePlatform" RENAME TO "Platform"; 2 | ALTER TABLE public."Platform" RENAME CONSTRAINT "DiscoursePlatform_pkey" TO "Platform_pkey"; 3 | 4 | ALTER TABLE public."DiscourseSpace" RENAME TO "Space"; 5 | ALTER TABLE public."Space" RENAME CONSTRAINT "DiscourseSpace_pkey" TO "Space_pkey"; 6 | ALTER TABLE public."Space" RENAME COLUMN discourse_platform_id TO platform_id; 7 | ALTER TABLE PUBLIC."Space" RENAME CONSTRAINT "DiscourseSpace_discourse_platform_id_fkey" TO "Space_platform_id_fkey"; 8 | 9 | COMMENT ON TABLE public."Space" IS 10 | 'A space on a platform representing a community engaged in a conversation'; 11 | COMMENT ON TABLE public."Account" IS 'A user account on a platform'; 12 | -------------------------------------------------------------------------------- /packages/database/supabase/migrations/20250530161244_content_constraints.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX IF EXISTS "public"."document_space_and_local_id_idx"; 2 | CREATE UNIQUE INDEX document_space_and_local_id_idx ON public."Document" USING btree (space_id, source_local_id) NULLS DISTINCT; 3 | 4 | DROP INDEX IF EXISTS "public"."content_space_and_local_id_idx"; 5 | CREATE UNIQUE INDEX content_space_and_local_id_idx ON public."Content" USING btree (space_id, source_local_id) NULLS DISTINCT; 6 | -------------------------------------------------------------------------------- /packages/database/supabase/schemas/contributor.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS public.content_contributors ( 2 | content_id bigint NOT NULL, 3 | contributor_id bigint NOT NULL 4 | ); 5 | 6 | ALTER TABLE ONLY public.content_contributors 7 | ADD CONSTRAINT content_contributors_pkey PRIMARY KEY ( 8 | content_id, contributor_id 9 | ); 10 | 11 | ALTER TABLE ONLY public.content_contributors 12 | ADD CONSTRAINT content_contributors_content_id_fkey FOREIGN KEY ( 13 | content_id 14 | ) REFERENCES public."Content" (id) ON UPDATE CASCADE ON DELETE CASCADE; 15 | 16 | ALTER TABLE ONLY public.content_contributors 17 | ADD CONSTRAINT content_contributors_contributor_id_fkey FOREIGN KEY ( 18 | contributor_id 19 | ) REFERENCES public."Agent" (id) ON UPDATE CASCADE ON DELETE CASCADE; 20 | 21 | ALTER TABLE public.content_contributors OWNER TO "postgres"; 22 | 23 | 24 | CREATE TABLE IF NOT EXISTS public.concept_contributors ( 25 | concept_id bigint NOT NULL, 26 | contributor_id bigint NOT NULL 27 | ); 28 | 29 | ALTER TABLE public.concept_contributors OWNER TO "postgres"; 30 | 31 | ALTER TABLE ONLY public.concept_contributors 32 | ADD CONSTRAINT concept_contributors_concept_id_fkey FOREIGN KEY ( 33 | concept_id 34 | ) REFERENCES public."Concept" (id) ON UPDATE CASCADE ON DELETE CASCADE; 35 | 36 | ALTER TABLE ONLY public.concept_contributors 37 | ADD CONSTRAINT concept_contributors_contributor_id_fkey FOREIGN KEY ( 38 | contributor_id 39 | ) REFERENCES public."Agent" (id) ON UPDATE CASCADE ON DELETE CASCADE; 40 | 41 | ALTER TABLE ONLY public.concept_contributors 42 | ADD CONSTRAINT concept_contributors_pkey PRIMARY KEY ( 43 | concept_id, contributor_id 44 | ); 45 | 46 | GRANT ALL ON TABLE public.concept_contributors TO anon; 47 | GRANT ALL ON TABLE public.concept_contributors TO authenticated; 48 | GRANT ALL ON TABLE public.concept_contributors TO service_role; 49 | 50 | GRANT ALL ON TABLE public.content_contributors TO anon; 51 | GRANT ALL ON TABLE public.content_contributors TO authenticated; 52 | GRANT ALL ON TABLE public.content_contributors TO service_role; 53 | -------------------------------------------------------------------------------- /packages/database/supabase/schemas/extensions.sql: -------------------------------------------------------------------------------- 1 | CREATE SCHEMA IF NOT EXISTS extensions; 2 | CREATE SCHEMA IF NOT EXISTS graphql; 3 | CREATE SCHEMA IF NOT EXISTS vault; 4 | 5 | CREATE EXTENSION IF NOT EXISTS pg_cron WITH SCHEMA pg_catalog; 6 | CREATE EXTENSION IF NOT EXISTS pgroonga WITH SCHEMA extensions; 7 | CREATE EXTENSION IF NOT EXISTS pg_graphql WITH SCHEMA graphql; 8 | CREATE EXTENSION IF NOT EXISTS pg_jsonschema WITH SCHEMA extensions; 9 | CREATE EXTENSION IF NOT EXISTS pg_stat_monitor WITH SCHEMA extensions; 10 | CREATE EXTENSION IF NOT EXISTS pg_stat_statements WITH SCHEMA extensions; 11 | CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA extensions; 12 | CREATE EXTENSION IF NOT EXISTS pgjwt WITH SCHEMA extensions; 13 | CREATE EXTENSION IF NOT EXISTS supabase_vault WITH SCHEMA vault; 14 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA extensions; 15 | CREATE EXTENSION IF NOT EXISTS vector WITH SCHEMA extensions; 16 | -------------------------------------------------------------------------------- /packages/database/supabase/schemas/space.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS public."Platform" ( 2 | id bigint DEFAULT nextval( 3 | 'public."entity_id_seq"'::regclass 4 | ) NOT NULL, 5 | name character varying NOT NULL, 6 | url character varying NOT NULL 7 | ); 8 | 9 | ALTER TABLE ONLY public."Platform" 10 | ADD CONSTRAINT "Platform_pkey" PRIMARY KEY (id); 11 | 12 | CREATE UNIQUE INDEX platform_url_idx ON public."Platform" USING btree (url); 13 | 14 | COMMENT ON TABLE public."Platform" IS 15 | 'A data platform where discourse happens'; 16 | 17 | CREATE TABLE IF NOT EXISTS public."Space" ( 18 | id bigint DEFAULT nextval( 19 | 'public."entity_id_seq"'::regclass 20 | ) NOT NULL, 21 | url character varying NOT NULL, 22 | name character varying NOT NULL, 23 | platform_id bigint NOT NULL 24 | ); 25 | 26 | ALTER TABLE ONLY public."Space" 27 | ADD CONSTRAINT "Space_pkey" PRIMARY KEY (id); 28 | 29 | CREATE UNIQUE INDEX space_url_idx ON public."Space" USING btree (url); 30 | 31 | ALTER TABLE ONLY public."Space" 32 | ADD CONSTRAINT "Space_platform_id_fkey" FOREIGN KEY ( 33 | platform_id 34 | ) REFERENCES public."Platform" ( 35 | id 36 | ) ON UPDATE CASCADE ON DELETE CASCADE; 37 | 38 | COMMENT ON TABLE public."Space" IS 39 | 'A space on a platform representing a community engaged in a conversation'; 40 | 41 | ALTER TABLE public."Platform" OWNER TO "postgres"; 42 | ALTER TABLE public."Space" OWNER TO "postgres"; 43 | 44 | GRANT ALL ON TABLE public."Platform" TO anon; 45 | GRANT ALL ON TABLE public."Platform" TO authenticated; 46 | GRANT ALL ON TABLE public."Platform" TO service_role; 47 | 48 | GRANT ALL ON TABLE public."Space" TO anon; 49 | GRANT ALL ON TABLE public."Space" TO authenticated; 50 | GRANT ALL ON TABLE public."Space" TO service_role; 51 | -------------------------------------------------------------------------------- /packages/eslint-config/README.md: -------------------------------------------------------------------------------- 1 | # `@turbo/eslint-config` 2 | 3 | Collection of internal eslint configurations. 4 | -------------------------------------------------------------------------------- /packages/eslint-config/library.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /** @type {import("eslint").Linter.Config} */ 6 | module.exports = { 7 | extends: ["eslint:recommended", "prettier", "turbo"], 8 | plugins: ["only-warn"], 9 | globals: { 10 | React: true, 11 | JSX: true, 12 | }, 13 | env: { 14 | node: true, 15 | }, 16 | settings: { 17 | "import/resolver": { 18 | typescript: { 19 | project, 20 | }, 21 | }, 22 | }, 23 | ignorePatterns: [ 24 | // Ignore dotfiles 25 | ".*.js", 26 | "node_modules/", 27 | "dist/", 28 | ], 29 | overrides: [ 30 | { 31 | files: ["*.js?(x)", "*.ts?(x)"], 32 | }, 33 | ], 34 | }; 35 | -------------------------------------------------------------------------------- /packages/eslint-config/next.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /** @type {import("eslint").Linter.Config} */ 6 | module.exports = { 7 | extends: [ 8 | "eslint:recommended", 9 | "prettier", 10 | require.resolve("@vercel/style-guide/eslint/next"), 11 | "turbo", 12 | ], 13 | globals: { 14 | React: true, 15 | JSX: true, 16 | }, 17 | env: { 18 | node: true, 19 | browser: true, 20 | }, 21 | plugins: ["only-warn"], 22 | settings: { 23 | "import/resolver": { 24 | typescript: { 25 | project, 26 | }, 27 | }, 28 | }, 29 | ignorePatterns: [ 30 | // Ignore dotfiles 31 | ".*.js", 32 | "node_modules/", 33 | ], 34 | overrides: [{ files: ["*.js?(x)", "*.ts?(x)"] }], 35 | }; 36 | -------------------------------------------------------------------------------- /packages/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/eslint-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "Apache-2.0", 6 | "files": [ 7 | "library.js", 8 | "next.js", 9 | "react-internal.js" 10 | ], 11 | "devDependencies": { 12 | "@vercel/style-guide": "^5.2.0", 13 | "eslint-config-turbo": "^2.0.0", 14 | "eslint-config-prettier": "^9.1.0", 15 | "eslint-plugin-only-warn": "^1.1.0", 16 | "@typescript-eslint/parser": "^7.1.0", 17 | "@typescript-eslint/eslint-plugin": "^7.1.0", 18 | "typescript": "5.5.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/eslint-config/react-internal.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /* 6 | * This is a custom ESLint configuration for use with 7 | * internal (bundled by their consumer) libraries 8 | * that utilize React. 9 | */ 10 | 11 | /** @type {import("eslint").Linter.Config} */ 12 | module.exports = { 13 | extends: ["eslint:recommended", "prettier", "turbo"], 14 | plugins: ["only-warn"], 15 | globals: { 16 | React: true, 17 | }, 18 | env: { 19 | browser: true, 20 | }, 21 | settings: { 22 | "import/resolver": { 23 | typescript: { 24 | project, 25 | }, 26 | }, 27 | }, 28 | ignorePatterns: [ 29 | // Ignore dotfiles 30 | ".*.js", 31 | "node_modules/", 32 | "dist/", 33 | ], 34 | overrides: [ 35 | // Force ESLint to detect .tsx files 36 | { files: ["*.js?(x)", "*.ts?(x)"] }, 37 | ], 38 | }; 39 | -------------------------------------------------------------------------------- /packages/tailwind-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/tailwind-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "Apache-2.0", 6 | "exports": { 7 | ".": "./tailwind.config.ts" 8 | }, 9 | "devDependencies": { 10 | "@repo/typescript-config": "*", 11 | "tailwindcss": "^3.4.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/tailwind-config/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | // We want each package to be responsible for its own content. 2 | import type { Config } from "tailwindcss"; 3 | 4 | const config: Omit = { 5 | darkMode: ["class"], 6 | theme: { 7 | extend: { 8 | colors: { 9 | primary: { 10 | DEFAULT: "#FF8C4B", 11 | 12 | rgb: "255, 140, 75", 13 | }, 14 | secondary: { 15 | DEFAULT: "#5F57C0", 16 | 17 | rgb: "95, 87, 192", 18 | }, 19 | neutral: { 20 | dark: "#1F1F1F", 21 | light: "#F1F1F1", 22 | }, 23 | }, 24 | }, 25 | }, 26 | plugins: [require("tailwindcss-animate")], 27 | }; 28 | export default config; 29 | -------------------------------------------------------------------------------- /packages/tailwind-config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/base.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/types/index.ts: -------------------------------------------------------------------------------- 1 | export type ErrorEmailProps = { 2 | errorMessage: string; 3 | errorStack: string; 4 | type: string; // To identify the type of error, eg "Export Dialog Failed" 5 | app: "Roam" | "Obsidian"; 6 | graphName: string; 7 | context?: Record; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/types", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "Apache-2.0", 6 | "main": "./index.ts", 7 | "types": "./index.ts" 8 | } 9 | -------------------------------------------------------------------------------- /packages/typescript-config/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "paths": { 5 | "~/*": ["src/*"] 6 | }, 7 | "allowSyntheticDefaultImports": true, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "esModuleInterop": true, 11 | "incremental": false, 12 | "isolatedModules": true, 13 | "lib": ["es2022", "DOM", "DOM.Iterable"], 14 | "module": "NodeNext", 15 | "moduleDetection": "force", 16 | "moduleResolution": "NodeNext", 17 | "noUncheckedIndexedAccess": true, 18 | "resolveJsonModule": true, 19 | "skipLibCheck": true, 20 | "strict": true, 21 | "target": "ES2022" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/typescript-config/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./base.json", 4 | "compilerOptions": { 5 | "plugins": [{ "name": "next" }], 6 | "module": "ESNext", 7 | "moduleResolution": "Bundler", 8 | "allowJs": true, 9 | "jsx": "preserve", 10 | "noEmit": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/typescript-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/typescript-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "Apache-2.0", 6 | "publishConfig": { 7 | "access": "public" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/typescript-config/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./base.json", 4 | "compilerOptions": { 5 | "jsx": "react-jsx" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: true, 3 | extends: ["@repo/eslint-config/react-internal.js"], 4 | parser: "@typescript-eslint/parser", 5 | parserOptions: { 6 | project: "./tsconfig.lint.json", 7 | tsconfigRootDir: __dirname, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/ui/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@repo/ui/components", 15 | "ui": "@repo/ui/components/ui", 16 | "utils": "@repo/ui/lib/utils", 17 | "lib": "@repo/ui/lib", 18 | "hooks": "@repo/ui/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/ui", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "Apache-2.0", 6 | "type": "module", 7 | "exports": { 8 | "./globals.css": "./src/globals.css", 9 | "./postcss.config": "./postcss.config.mjs", 10 | "./tailwind.config": "./tailwind.config.ts", 11 | "./lib/*": "./src/lib/*.ts", 12 | "./hooks/*": [ 13 | "./src/hooks/*.ts", 14 | "./src/hooks/*.tsx" 15 | ], 16 | "./components/*": "./src/components/*.tsx" 17 | }, 18 | "scripts": { 19 | "lint": "eslint . --max-warnings 0", 20 | "generate:component": "turbo gen react-component", 21 | "check-types": "tsc --noEmit", 22 | "ui": "npx shadcn@latest" 23 | }, 24 | "devDependencies": { 25 | "@repo/tailwind-config": "*", 26 | "@repo/eslint-config": "*", 27 | "@repo/typescript-config": "*", 28 | "@turbo/gen": "^1.12.4", 29 | "@types/eslint": "^8.56.5", 30 | "@types/node": "^20.11.24", 31 | "@types/react": "18.3.0", 32 | "@types/react-dom": "18.3.1", 33 | "eslint": "^8.57.0", 34 | "typescript": "5.5.4" 35 | }, 36 | "dependencies": { 37 | "class-variance-authority": "^0.7.1", 38 | "clsx": "^2.1.1", 39 | "lucide-react": "^0.468.0", 40 | "react": "19.0.0-rc-5c56b873-20241107", 41 | "react-dom": "19.0.0-rc-5c56b873-20241107", 42 | "tailwind-merge": "^2.5.5", 43 | "tailwindcss-animate": "^1.0.7" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/ui/src/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 47.4% 11.2%; 9 | --muted: 210 40% 96.1%; 10 | --muted-foreground: 215.4 16.3% 46.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 222.2 47.4% 11.2%; 13 | --border: 214.3 31.8% 91.4%; 14 | --input: 214.3 31.8% 91.4%; 15 | --card: 0 0% 100%; 16 | --card-foreground: 222.2 47.4% 11.2%; 17 | --primary: 222.2 47.4% 11.2%; 18 | --primary-foreground: 210 40% 98%; 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | --accent: 210 40% 96.1%; 22 | --accent-foreground: 222.2 47.4% 11.2%; 23 | --destructive: 0 100% 50%; 24 | --destructive-foreground: 210 40% 98%; 25 | --ring: 215 20.2% 65.1%; 26 | --radius: 0.5rem; 27 | } 28 | 29 | .dark { 30 | --background: 224 71% 4%; 31 | --foreground: 213 31% 91%; 32 | --muted: 223 47% 11%; 33 | --muted-foreground: 215.4 16.3% 56.9%; 34 | --accent: 216 34% 17%; 35 | --accent-foreground: 210 40% 98%; 36 | --popover: 224 71% 4%; 37 | --popover-foreground: 215 20.2% 65.1%; 38 | --border: 216 34% 17%; 39 | --input: 216 34% 17%; 40 | --card: 224 71% 4%; 41 | --card-foreground: 213 31% 91%; 42 | --primary: 210 40% 98%; 43 | --primary-foreground: 222.2 47.4% 1.2%; 44 | --secondary: 222.2 47.4% 11.2%; 45 | --secondary-foreground: 210 40% 98%; 46 | --destructive: 0 63% 31%; 47 | --destructive-foreground: 210 40% 98%; 48 | --ring: 216 34% 17%; 49 | } 50 | } 51 | 52 | @layer base { 53 | * { 54 | @apply border-border; 55 | } 56 | body { 57 | @apply font-sans antialiased bg-background text-foreground; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/ui/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /packages/ui/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import sharedConfig from "@repo/tailwind-config"; 3 | 4 | const config: Pick = { 5 | content: ["./src/**/*.tsx"], 6 | prefix: "ui-", 7 | presets: [sharedConfig], 8 | }; 9 | 10 | export default config; 11 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/react-library.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@repo/ui/*": ["./src/*"] 7 | } 8 | }, 9 | "include": ["src"], 10 | "exclude": ["node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.lint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/react-library.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src", "turbo"], 7 | "exclude": ["node_modules", "dist"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/ui/turbo/generators/config.ts: -------------------------------------------------------------------------------- 1 | import type { PlopTypes } from "@turbo/gen"; 2 | 3 | // Learn more about Turborepo Generators at https://turbo.build/repo/docs/core-concepts/monorepos/code-generation 4 | 5 | export default function generator(plop: PlopTypes.NodePlopAPI): void { 6 | // A simple generator to add a new React component to the internal UI library 7 | plop.setGenerator("react-component", { 8 | description: "Adds a new react component", 9 | prompts: [ 10 | { 11 | type: "input", 12 | name: "name", 13 | message: "What is the name of the component?", 14 | }, 15 | ], 16 | actions: [ 17 | { 18 | type: "add", 19 | path: "src/{{kebabCase name}}.tsx", 20 | templateFile: "templates/component.hbs", 21 | }, 22 | { 23 | type: "append", 24 | path: "package.json", 25 | pattern: /"exports": {(?)/g, 26 | template: ' "./{{kebabCase name}}": "./src/{{kebabCase name}}.tsx",', 27 | }, 28 | ], 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /packages/ui/turbo/generators/templates/component.hbs: -------------------------------------------------------------------------------- 1 | export const {{ pascalCase name }} = ({ children }: { children: React.ReactNode }) => { 2 | return ( 3 |
4 |

{{ pascalCase name }} Component

5 | {children} 6 |
7 | ); 8 | }; 9 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "ui": "tui", 4 | "tasks": { 5 | "build": { 6 | "passThroughEnv": [ 7 | "RESEND_API_KEY", 8 | "OPENAI_API_KEY", 9 | "ANTHROPIC_API_KEY", 10 | "GEMINI_API_KEY", 11 | "NODE_ENV", 12 | "BLOB_READ_WRITE_TOKEN" 13 | ], 14 | "dependsOn": ["^build"], 15 | "inputs": ["$TURBO_DEFAULT$", ".env*"], 16 | "outputs": [".next/**", "!.next/cache/**", "dist/**"] 17 | }, 18 | "lint": { 19 | "dependsOn": ["^lint"] 20 | }, 21 | "check-types": { 22 | "dependsOn": ["^check-types"] 23 | }, 24 | "dev": { 25 | "passThroughEnv": [ 26 | "OBSIDIAN_PLUGIN_PATH", 27 | "NODE_ENV", 28 | "SUPABASE_URL", 29 | "SUPABASE_ANON_KEY", 30 | "SUPABASE_SERVICE_ROLE_KEY", 31 | "POSTGRES_URL", 32 | "OPENAI_API_KEY", 33 | "ANTHROPIC_API_KEY", 34 | "GEMINI_API_KEY" 35 | ], 36 | "cache": false, 37 | "persistent": true, 38 | "inputs": ["$TURBO_DEFAULT$", ".env*"] 39 | }, 40 | "deploy": { 41 | "cache": false, 42 | "inputs": ["$TURBO_DEFAULT$", ".env*"], 43 | "passThroughEnv": [ 44 | "BLOB_READ_WRITE_TOKEN", 45 | "GITHUB_REF_NAME", 46 | "GITHUB_HEAD_REF", 47 | "NODE_ENV", 48 | "SUPABASE_PROJECT_ID", 49 | "SUPABASE_DB_PASSWORD", 50 | "SUPABASE_ACCESS_TOKEN", 51 | "SUPABASE_URL", 52 | "SUPABASE_ANON_KEY", 53 | "SUPABASE_SERVICE_ROLE_KEY", 54 | "POSTGRES_URL", 55 | "OPENAI_API_KEY", 56 | "ANTHROPIC_API_KEY", 57 | "GEMINI_API_KEY" 58 | ] 59 | }, 60 | "publish": { 61 | "cache": false, 62 | "inputs": ["$TURBO_DEFAULT$", ".env*"], 63 | "passThroughEnv": [ 64 | "GITHUB_TOKEN", 65 | "APP_PRIVATE_KEY", 66 | "APP_ID", 67 | "NODE_ENV" 68 | ] 69 | } 70 | } 71 | } 72 | --------------------------------------------------------------------------------