Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
node_modules/
build/
.docusaurus/
static/ai/*.jsonl
wp-hooks-docs/
.hooks-tmp/
.cache-loader/
Expand Down
139 changes: 139 additions & 0 deletions scripts/build-ai-docs-manifests.js
Original file line number Diff line number Diff line change
@@ -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)}`);
45 changes: 45 additions & 0 deletions src/theme/Root.js
Original file line number Diff line number Diff line change
@@ -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}</>;
}