diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 2f8432e2be..606ab9ce0a 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -241,6 +241,9 @@ jobs: run: bash scripts/generate-hooks.sh continue-on-error: true + - name: Build AI assistant knowledge manifests + run: node scripts/build-ai-docs-manifests.js + - name: Upload prepared docs source uses: actions/upload-artifact@v4 with: diff --git a/.gitignore b/.gitignore index b954186ea2..a64c3342ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules/ build/ .docusaurus/ +static/ai/*.jsonl wp-hooks-docs/ .hooks-tmp/ .cache-loader/ diff --git a/scripts/build-ai-docs-manifests.js b/scripts/build-ai-docs-manifests.js new file mode 100755 index 0000000000..84c7a86819 --- /dev/null +++ b/scripts/build-ai-docs-manifests.js @@ -0,0 +1,139 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +const rootDir = process.cwd(); +const docsDir = path.join(rootDir, 'docs'); +const outputDir = path.join(rootDir, 'static', 'ai'); +const publicBaseUrl = 'https://ultimatemultisite.com/docs/'; + +const docsManifestPath = path.join(outputDir, 'ums-docs-manifest.jsonl'); +const codeManifestPath = path.join(outputDir, 'ums-code-reference-manifest.jsonl'); +const markdownExtensions = new Set(['.md', '.mdx']); + +function listMarkdownFiles(dir) { + const entries = fs.readdirSync(dir, {withFileTypes: true}); + const files = []; + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...listMarkdownFiles(fullPath)); + continue; + } + + if (entry.isFile() && markdownExtensions.has(path.extname(entry.name))) { + files.push(fullPath); + } + } + + return files.sort(); +} + +function extractFrontmatter(content) { + if (!content.startsWith('---\n')) { + return {}; + } + + const end = content.indexOf('\n---\n', 4); + if (end === -1) { + return {}; + } + + const metadata = {}; + const lines = content.slice(4, end).split('\n'); + for (const line of lines) { + const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/); + if (!match) { + continue; + } + + const value = match[2].trim().replace(/^['"]|['"]$/g, ''); + metadata[match[1]] = value; + } + + return metadata; +} + +function routeFor(relativePath) { + let route = relativePath.replace(/\\/g, '/').replace(/\.(md|mdx)$/i, ''); + + if (route.endsWith('/index')) { + route = route.slice(0, -'/index'.length); + } else if (route === 'index') { + route = ''; + } + + return route; +} + +function publicUrlFor(route) { + return route ? `${publicBaseUrl}${route}` : publicBaseUrl; +} + +function titleFor(content, frontmatter, relativePath) { + if (frontmatter.title) { + return frontmatter.title; + } + + const heading = content.match(/^#\s+(.+)$/m); + if (heading) { + return heading[1].trim(); + } + + return path.basename(relativePath, path.extname(relativePath)); +} + +function isCodeReference(relativePath) { + const normalized = relativePath.replace(/\\/g, '/'); + + return ( + normalized.startsWith('developer/') || + normalized.startsWith('addons/superdav-ai-agent/') || + normalized.startsWith('addons/gratis-ai-agent/') || + /\/hooks\//.test(normalized) + ); +} + +function recordFor(filePath) { + const relativePath = path.relative(docsDir, filePath).replace(/\\/g, '/'); + const content = fs.readFileSync(filePath, 'utf8'); + const frontmatter = extractFrontmatter(content); + const route = routeFor(relativePath); + + return { + id: `docs:${route || 'index'}`, + path: relativePath, + title: titleFor(content, frontmatter, relativePath), + route: publicUrlFor(route), + url: publicUrlFor(route), + locale: 'en', + metadata: { + path: relativePath, + route, + source: 'docusaurus', + code_reference: isCodeReference(relativePath), + }, + content, + }; +} + +function writeJsonl(filePath, records) { + fs.writeFileSync( + filePath, + `${records.map((record) => JSON.stringify(record)).join('\n')}\n`, + 'utf8' + ); +} + +fs.mkdirSync(outputDir, {recursive: true}); + +const records = listMarkdownFiles(docsDir).map(recordFor); +const codeReferenceRecords = records.filter((record) => record.metadata.code_reference); + +writeJsonl(docsManifestPath, records); +writeJsonl(codeManifestPath, codeReferenceRecords); + +console.log(`Wrote ${records.length} docs records to ${path.relative(rootDir, docsManifestPath)}`); +console.log(`Wrote ${codeReferenceRecords.length} code-reference records to ${path.relative(rootDir, codeManifestPath)}`); diff --git a/src/theme/Root.js b/src/theme/Root.js new file mode 100644 index 0000000000..4a4bd39ac9 --- /dev/null +++ b/src/theme/Root.js @@ -0,0 +1,45 @@ +import React, {useEffect} from 'react'; + +const API_BASE = 'https://ultimatemultisite.com/wp-json/sd-ai-agent/v1'; +const SCRIPT_SRC = 'https://ultimatemultisite.com/app/plugins/superdav-ai-agent/build/embed-widget.js'; +const STYLE_HREF = 'https://ultimatemultisite.com/app/plugins/superdav-ai-agent/build/style-embed-widget.css'; +const SCRIPT_ID = 'ums-docs-assistant-script'; +const STYLE_ID = 'ums-docs-assistant-style'; + +function appendAssistantStylesheet() { + if (document.getElementById(STYLE_ID)) { + return; + } + + const stylesheet = document.createElement('link'); + stylesheet.id = STYLE_ID; + stylesheet.rel = 'stylesheet'; + stylesheet.href = STYLE_HREF; + document.head.appendChild(stylesheet); +} + +function appendAssistantScript() { + if (document.getElementById(SCRIPT_ID)) { + return; + } + + const script = document.createElement('script'); + script.id = SCRIPT_ID; + script.src = SCRIPT_SRC; + script.defer = true; + script.dataset.apiBase = API_BASE; + script.dataset.embedId = 'ums-docs'; + script.dataset.theme = document.documentElement.dataset.theme || 'light'; + script.dataset.locale = document.documentElement.lang || 'en'; + script.dataset.greeting = 'Ask me about Ultimate Multisite docs.'; + document.body.appendChild(script); +} + +export default function Root({children}) { + useEffect(() => { + appendAssistantStylesheet(); + appendAssistantScript(); + }, []); + + return <>{children}; +}