diff --git a/docusaurus/docs/ai-toolbar-translations.md b/docusaurus/docs/ai-toolbar-translations.md new file mode 100644 index 0000000000..efee64c83c --- /dev/null +++ b/docusaurus/docs/ai-toolbar-translations.md @@ -0,0 +1,51 @@ +# AI Toolbar Prompt Translations + +The AI toolbar (ChatGPT/Claude buttons) relies on a shared set of translated prompt strings. If you’d like to tweak an existing translation or contribute a new language, follow this guide. + +## Where prompts live + +- Edit `src/components/AiToolbar/config/aiPromptTemplates.js`. +- Keys are ISO language tags (e.g., `en`, `fr`, `pt-BR`). +- Values are strings that may contain placeholders such as `{{url}}`. + +```js +export const aiPromptTemplates = { + 'en': 'Read from {{url}} so I can ask questions about it.', + 'fr': 'Lis {{url}} pour que je puisse poser des questions à son sujet.', + // Add new entries here … +}; +``` + +## Rules to follow + +1. **Keep placeholders intact.** `{{url}}` is required. +2. **Use clear, neutral tone.** Mirror the English template. +3. **One translation per language.** No duplicates. +4. **Prefer UTF-8 text.** Don’t escape unless necessary. + +## Testing locally + +1. Change `navigator.language` via browser DevTools. +2. Reload the docs page and click the toolbar button. +3. Confirm the new tab or copied prompt shows your translation. + +## Validation script + +Run the built-in check before opening a PR: + +```bash +node scripts/validate-prompts.js +# or, if Node isn’t available in your environment: +python scripts/validate-prompts.py +``` + +Both ensure the `{{url}}` placeholder is present and no translation is empty. + +## Adding a new language + +1. Add the entry in `aiPromptTemplates.js`. +2. Update this document if helpful (e.g., to credit contributors). +3. (Optional) Add a test case if you create one in the future. +4. Include a screenshot/gif in your PR showing the prompt in action. + +Thanks for helping improve the AI tooling experience! 🙌 diff --git a/docusaurus/docs/contributing/ai-toolbar-translations.md b/docusaurus/docs/contributing/ai-toolbar-translations.md new file mode 100644 index 0000000000..cf9e85fe49 --- /dev/null +++ b/docusaurus/docs/contributing/ai-toolbar-translations.md @@ -0,0 +1,54 @@ +--- +id: ai-toolbar-translations +title: Update AI Toolbar Prompts +--- + +The AI toolbar (ChatGPT and Claude buttons) uses a set of translated prompt strings. If you want to tweak a translation or add a new language, use this guide. + +## Location + +- Edit `src/components/AiToolbar/config/aiPromptTemplates.js` in this repository. +- Keys are ISO language tags (use lowercase, include region if needed, e.g. `pt-BR`). +- Values are plain strings with placeholders like `{{url}}`. + +```js +export const aiPromptTemplates = { + 'en': 'Read from {{url}} so I can ask questions about it.', + 'fr': 'Lis {{url}} pour que je puisse poser des questions à son sujet.', + // … +}; +``` + +## Contribution Rules + +1. **Keep placeholders**: `{{url}}` is mandatory so the current page URL is injected. +2. **Preserve meaning**: Translate the English prompt faithfully; keep a neutral tone. +3. **Avoid duplicates**: one entry per language tag. +4. **Use UTF-8 characters** wherever possible; avoid HTML entities. + +## Testing Your Translation + +1. Override `navigator.language` via DevTools (Chrome Sensors panel or Firefox locale settings). +2. Reload any docs page with the toolbar. +3. Click “Open with ChatGPT/Claude” and confirm your translation appears in the prompt or clipboard. + +## Validation Script + +Before opening a pull request, run the placeholder check: + +```bash +node scripts/validate-prompts.js +# If Node isn’t available, run: +python scripts/validate-prompts.py +``` + +The script ensures that each translation keeps the required placeholders and isn’t empty. + +## Adding a New Language + +1. Add your entry in `aiPromptTemplates.js`. +2. Run the validation script. +3. Include a screenshot or clip in your PR showing the translation in action (optional but helpful). +4. Update this doc if you want to note any language-specific tips. + +Thanks for helping improve the AI tooling experience! 🙌 diff --git a/docusaurus/package.json b/docusaurus/package.json index 9ac09cd2eb..a4a6126e4c 100644 --- a/docusaurus/package.json +++ b/docusaurus/package.json @@ -15,8 +15,10 @@ "release-notes": "bash ./scripts/release-notes-script.sh", "redirections-analysis": "node ./scripts/redirection-analysis/redirect-analyzer.js", "generate-llms": "node scripts/generate-llms.js", - "dev:with-llms": "yarn generate-llms && docusaurus start --port 8080 --no-open", - "build:with-llms": "yarn generate-llms && docusaurus build", + "dev:with-llms": "yarn generate-llms && node scripts/generate-llms-code.js --anchors --output static/llms-code.txt && node scripts/validate-llms-code.js --path static/llms-code.txt --strict --check-files --verify-anchors --project-root .. && docusaurus start --port 8080 --no-open", + "build:with-llms": "yarn generate-llms && node scripts/generate-llms-code.js --anchors --output static/llms-code.txt && node scripts/validate-llms-code.js --path static/llms-code.txt --strict --check-files --verify-anchors --project-root .. && docusaurus build", + "llms:generate-and-validate": "yarn generate-llms && node scripts/generate-llms-code.js --anchors --output static/llms-code.txt && node scripts/validate-llms-code.js --path static/llms-code.txt --strict --check-files --verify-anchors --project-root ..", + "validate:llms-code": "node scripts/validate-llms-code.js --path static/llms-code.txt --strict --check-files --verify-anchors --project-root ..", "meilisearch:update-order": "node -r dotenv/config scripts/meilisearch/add-category-order.js" }, "dependencies": { diff --git a/docusaurus/scripts/generate-llms-code.js b/docusaurus/scripts/generate-llms-code.js new file mode 100644 index 0000000000..98371706c4 --- /dev/null +++ b/docusaurus/scripts/generate-llms-code.js @@ -0,0 +1,816 @@ +#!/usr/bin/env node + +// Prefer optional deps; fall back to built-ins for sandboxed runs +let fs = null; +try { + fs = require('fs-extra'); +} catch (e) { + fs = require('fs'); + // polyfills to mimic fs-extra subset used here + fs.ensureDir = async (dir) => fs.promises.mkdir(dir, { recursive: true }); + fs.pathExistsSync = (p) => fs.existsSync(p); + fs.writeFile = fs.promises.writeFile.bind(fs.promises); + fs.readFile = fs.promises.readFile.bind(fs.promises); +} +const path = require('path'); + +let matter = null; +try { + matter = require('gray-matter'); +} catch (e) { + // Minimal frontmatter parser fallback + matter = (raw) => { + if (raw.startsWith('---')) { + const end = raw.indexOf('\n---', 3); + if (end !== -1) { + const body = raw.slice(end + 4); + return { data: {}, content: body }; + } + } + return { data: {}, content: raw }; + }; +} + +const DEFAULT_DOCS = [ + 'cms/admin-panel-customization/bundlers', + 'cms/backend-customization/middlewares', + 'cms/features/api-tokens', +]; + +const DEFAULT_OUTPUT = path.join('static', 'llms-code.txt'); +const BASE_URL = 'https://docs.strapi.io'; +const HEADING_REGEX = /^(#{1,6})\s+(.*)/; + +const cleanInlineText = (value) => { + if (!value) { + return ''; + } + + return value + .replace(/```[\s\S]*?```/g, '') + .replace(/`([^`]+)`/g, '$1') + .replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') + .replace(/\*\*([^*]+)\*\*/g, '$1') + .replace(/__([^_]+)__/g, '$1') + .replace(/\*([^*]+)\*/g, '$1') + .replace(/_([^_]+)_/g, '$1') + .replace(/<[^>]+>/g, ' ') + .replace(/\s+/g, ' ') + .trim(); +}; + +const summarizeDescription = (raw, fallback) => { + const cleaned = cleanInlineText(raw) + .replace(/^[0-9]+[.)]\s*/, '') + .replace(/^[-*•]\s*/, '') + .trim(); + + if (!cleaned || !/[a-zA-Z]/.test(cleaned)) { + return { description: fallback, useCase: null, fallbackUsed: true }; + } + + const sentences = cleaned.split(/(?<=[.!?])\s+/); + const description = (sentences[0] || cleaned).trim(); + let useCase = null; + + for (const sentence of sentences.slice(1)) { + const lower = sentence.toLowerCase(); + if (lower.includes('use ') || lower.includes('when ') || lower.includes('recommended')) { + useCase = sentence.trim(); + break; + } + } + + return { description, useCase, fallbackUsed: false }; +}; + +const parseArgs = () => { + const args = process.argv.slice(2); + const docs = []; + let output = DEFAULT_OUTPUT; + let anchors = false; + let checkFiles = false; + let projectRoot = process.cwd(); + + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + + if (arg === '--docs') { + const value = args[i + 1]; + i += 1; + if (value) { + value.split(',').map((item) => item.trim()).filter(Boolean).forEach((item) => docs.push(item)); + } + } else if (arg === '--output') { + const value = args[i + 1]; + i += 1; + if (value) { + output = value; + } + } else if (arg === '--anchors' || arg === '--with-anchors') { + anchors = true; + } else if (arg === '--help' || arg === '-h') { + console.log('Usage: node generate-llms-code.js [--docs docA,docB] [--output path/to/file]'); + process.exit(0); + } else if (arg === '--check-files') { + checkFiles = true; + } else if (arg === '--project-root') { + const value = args[i + 1]; + i += 1; + if (value) { + projectRoot = value; + } + } else { + docs.push(arg); + } + } + + return { + docs: docs.length > 0 ? docs : DEFAULT_DOCS, + output, + anchors, + checkFiles, + projectRoot, + }; +}; + +class DocusaurusLlmsCodeGenerator { + constructor(config = {}) { + this.docsDir = config.docsDir || 'docs'; + this.sidebarPath = config.sidebarPath || 'sidebars.js'; + this.outputPath = config.outputPath || DEFAULT_OUTPUT; + this.docIds = config.docIds || DEFAULT_DOCS; + this.includeSectionAnchors = Boolean(config.includeSectionAnchors); + this.includeFileChecks = Boolean(config.includeFileChecks); + this.projectRoot = config.projectRoot || process.cwd(); + } + + normalizeTitlePath(value) { + if (!value || typeof value !== 'string') return null; + let v = value.trim(); + if (/^path\s*:/i.test(v)) { + v = v.replace(/^path\s*:\s*/i, ''); + } + if (v.includes(' or ')) { + v = v.split(' or ')[0].trim(); + } + return v || null; + } + + parseFenceInfo(info = '') { + const options = {}; + if (!info) { + return { language: '', options }; + } + + const tokens = info.split(/\s+/).filter(Boolean); + const language = tokens.shift() || ''; + + tokens.forEach((token) => { + const [key, rawValue] = token.split('='); + if (!key) { + return; + } + if (rawValue === undefined) { + options[key] = true; + return; + } + const value = rawValue.replace(/^"|"$/g, '').replace(/^'|'$/g, ''); + options[key] = value; + }); + + return { language, options }; + } + + async generate() { + try { + console.log('🔍 Collecting code snippets...'); + + const pages = []; + + for (const docId of this.docIds) { + const filePath = this.findDocFile(docId); + if (!filePath) { + console.warn(`⚠️ Unable to locate file for ${docId}`); + continue; + } + + const parsed = await this.parseDocument(filePath); + const fm = parsed.data || parsed.frontmatter || {}; + const title = fm.title || this.deriveTitleFromId(docId); + const extracted = this.extractCodeSnippets(docId, title, parsed.content); + const snippets = extracted.snippets || []; + const sectionAnchors = extracted.sectionAnchors || {}; + + if (snippets.length === 0) { + console.warn(`ℹ️ Skipping ${docId}: no code snippets found.`); + continue; + } + + pages.push({ docId, title, snippets, sectionAnchors }); + } + + if (pages.length === 0) { + console.warn('⚠️ No pages with code snippets were collected.'); + } + + const output = this.formatOutput(pages); + + // Support stdout preview when --output - is provided + if (this.outputPath === '-' || this.outputPath === '/dev/stdout') { + process.stdout.write(output); + console.log('\n✅ Printed llms-code to stdout'); + return; + } + + await fs.ensureDir(path.dirname(this.outputPath)); + await fs.writeFile(this.outputPath, output, 'utf-8'); + + console.log(`✅ Wrote ${this.outputPath}`); + } catch (error) { + console.error('❌ Error while generating llms-code:', error); + throw error; + } + } + + findDocFile(docId) { + const candidates = [ + path.join(this.docsDir, `${docId}.md`), + path.join(this.docsDir, `${docId}.mdx`), + path.join(this.docsDir, docId, 'index.md'), + path.join(this.docsDir, docId, 'index.mdx'), + ]; + + for (const candidate of candidates) { + if (fs.pathExistsSync(candidate)) { + return candidate; + } + } + + return null; + } + + async parseDocument(filePath) { + const raw = await fs.readFile(filePath, 'utf-8'); + return matter(raw); + } + + deriveTitleFromId(docId) { + const parts = docId.split('/'); + return parts[parts.length - 1] + .replace(/-/g, ' ') + .replace(/\b\w/g, (match) => match.toUpperCase()); + } + + formatLanguageName(language = '') { + const lower = language.toLowerCase(); + switch (lower) { + case 'js': + case 'javascript': + return 'JavaScript'; + case 'ts': + case 'typescript': + return 'TypeScript'; + case 'bash': + case 'sh': + return 'Bash'; + case 'powershell': + case 'pwsh': + return 'PowerShell'; + case 'fish': + return 'Fish'; + case 'yaml': + case 'yml': + return 'YAML'; + case 'json': + return 'JSON'; + case 'tsx': + return 'TSX'; + case 'jsx': + return 'JSX'; + default: + return language.toUpperCase(); + } + } + + // Resolve language from fence, file path, and code content (content-first heuristics) + resolveLanguage(fenceLanguage = '', filePath = '', code = '') { + const ext = (filePath || '').split('/').pop() || ''; + const extLower = (ext.split('.').pop() || '').toLowerCase(); + const fence = (fenceLanguage || '').toLowerCase(); + const head = (code || '').split('\n').map((l) => l.trim()).filter(Boolean).slice(0, 10); + + // Content-first heuristics + const first = head[0] || ''; + if (/^#!\/.+\b(bash|sh|env\s+bash|env\s+sh)\b/.test(first)) return 'bash'; + if (/^FROM\s+\S+/i.test(first) || head.some((l) => /^(RUN|CMD|ENTRYPOINT|COPY|ADD|WORKDIR|ENV|EXPOSE|USER)\b/i.test(l))) return 'dockerfile'; + // SQL: require clear SQL shape; avoid JS objects like `delete:` keys + if (head.some((l) => /(^select\b.+\bfrom\b)|(^insert\b\s+into\b)|(^update\b\s+\w+\b)|(^delete\b\s+from\b)|(^create\b\s+(table|index|view)\b)|(^alter\b\s+table\b)|(^drop\b\s+(table|index|view)\b)|(^with\b\s+\w+\s+as\b)/i.test(l))) return 'sql'; + if (/^(query|mutation|subscription|fragment|schema)\b/.test(first)) return 'graphql'; + // JS/TS module cues before YAML/JSON + if (/(?:^|\b)(module\.exports|require\(["']|exports?\.|console\.log\()/.test(code)) return extLower.startsWith('ts') ? 'ts' : 'js'; + if (/(?:^|\b)(import\s+[^;]+from\s+["'][^"']+["']|export\s+(default|const|function|class)\b)/.test(code)) return extLower.startsWith('ts') ? 'ts' : 'js'; + // YAML detection: frontmatter or multiple key: value lines without JS syntax + const yamlKeyLines = head.filter((l) => /^\w[\w-]*\s*:\s*\S/.test(l)).length; + if (first === '---' || (yamlKeyLines >= 2 && !/[{}();]/.test(head.join(' ')))) return 'yaml'; + // JSON detection: leading brace/bracket and key: value patterns, but avoid JS/TS + if (/^[\[{]/.test(first) && head.some((l) => /"?\w+"?\s*:\s*/.test(l)) && !/(module\.exports|import\s|export\s)/.test(code)) return 'json'; + if (head.length > 0 && head.every((l) => /^(?:\$\s+)?(npm|yarn|pnpm|npx|strapi|node|cd|cp|mv|rm|mkdir|curl|wget|git|docker|kubectl|helm|openssl|grep|sed|awk|touch|chmod|chown|tee|cat)\b/.test(l) || l.startsWith('#'))) return 'bash'; + if (head.some((l) => /^(param\s*\(|Write-Host\b|Get-Item\b|Set-Item\b|New-Object\b)/i.test(l))) return 'powershell'; + if (head.some((l) => /^(function\s+\w+|set\s+-l\s+\w+|end\s*$)/.test(l))) return 'fish'; + if (/(export\s+(interface|type)\b|:\s*\w+<|\bimplements\b|\bas\s+const\b)/.test(code)) return 'ts'; + if (/from\s+['"][^'"\n]+\.ts['"]/i.test(code)) return 'ts'; + + // Extension-derived mapping + const extToLang = { + js: 'js', jsx: 'jsx', + ts: 'ts', tsx: 'tsx', + json: 'json', yml: 'yaml', yaml: 'yaml', + sh: 'bash', bash: 'bash', zsh: 'bash', + graphql: 'graphql', gql: 'graphql', + sql: 'sql', + env: 'dotenv', + dockerfile: 'dockerfile', ps1: 'powershell', psm1: 'powershell', fish: 'fish', + html: 'html', css: 'css', scss: 'scss', + py: 'python', rb: 'ruby', go: 'go', php: 'php', java: 'java', + c: 'c', h: 'c', cc: 'cpp', cpp: 'cpp', cxx: 'cpp', cs: 'csharp', + ini: 'ini', toml: 'toml', md: 'md', mdx: 'mdx', + }; + + + let preferred = ''; + if (/^dockerfile$/i.test(ext)) preferred = 'dockerfile'; + else if (/^\.env(\..+)?$/i.test(ext)) preferred = 'dotenv'; + else preferred = extToLang[extLower] || ''; + + // If no fence language, adopt the extension-derived language + if (!fence && preferred) return preferred; + + // JS/TS family resolution: prefer file extension if it contradicts fence + const family = (lang) => (lang.startsWith('ts') ? 'ts' : (lang.startsWith('js') ? 'js' : lang)); + if (preferred && family(fence) !== family(preferred)) return preferred; + + // Fall back to fence or preferred + return fenceLanguage || preferred || ''; + } + + // Slugify heading text similarly to GitHub/Docusaurus and dedupe within a page + slugify(text, seen) { + if (!text) return ''; + let slug = String(text) + .toLowerCase() + .normalize('NFKD') + .replace(/[\u0300-\u036f]/g, '') // strip diacritics + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .replace(/-{2,}/g, '-'); + if (!seen.has(slug)) { + seen.set(slug, 0); + return slug; + } + const n = seen.get(slug) + 1; + seen.set(slug, n); + return `${slug}-${n}`; + } + + normalizeOutputPath(p) { + if (!p) return p; + if (/^https?:\/\//i.test(p)) return p; + let out = p; + if (out.startsWith('//')) out = out.replace(/^\/+/, '/'); + out = out.replace(/([^:])\/\/+/, '$1/'); + return out; + } + + resolveAbsolutePathForCheck(relPath = '') { + if (!relPath) return null; + const clean = relPath.replace(/^\/+/, ''); + const candidate = path.join(this.projectRoot, clean); + return candidate; + } + + fileExists(relPath = '') { + try { + const abs = this.resolveAbsolutePathForCheck(relPath); + if (!abs) return false; + return fs.pathExistsSync ? fs.pathExistsSync(abs) : fs.existsSync(abs); + } catch (e) { + return false; + } + } + + buildFallbackDescription(snippet) { + const langLabel = snippet.language ? this.formatLanguageName(snippet.language) : 'code'; + const section = snippet.section || 'this section'; + return `Example showing how to work with ${section} using ${langLabel}.`; + } + + currentSection(sections, defaultTitle) { + if (!sections || sections.length === 0) { + return defaultTitle; + } + return sections[sections.length - 1]; + } + + extractContextDescription(buffer, sections, defaultTitle) { + const descriptionLines = []; + + for (let index = buffer.length - 1; index >= 0 && descriptionLines.length < 5; index -= 1) { + const entry = buffer[index]; + + if (entry === '\n') { + if (descriptionLines.length > 0) { + break; + } + continue; + } + + if (entry.startsWith('```')) { + break; + } + + if (entry.trim().startsWith('#')) { + break; + } + + if (entry.trim()) { + descriptionLines.push(entry); + } + } + + descriptionLines.reverse(); + const rawDescription = descriptionLines.join('\n'); + const fallback = `Code example from "${this.currentSection(sections, defaultTitle)}"`; + + return summarizeDescription(rawDescription, fallback); + } + + extractCodeSnippets(docId, title, content) { + const sections = []; + const contextBuffer = []; + const snippets = []; + const sectionAnchors = {}; + // Track MDX Tabs/TabItem context to group language variants + let inTabs = false; + let tabsCounter = 0; + let currentTabsGroupId = null; // from + let currentTabLabel = null; // from + let currentTabValue = null; // from + let currentVariantGroupId = null; // computed group key for variants + + let inCode = false; + let codeLanguage = ''; + let codeLines = []; + + const lines = content.split('\n'); + + lines.forEach((line) => { + const trimmed = line.trim(); + + if (trimmed.startsWith('```')) { + const info = trimmed.slice(3).trim(); + const { language, options } = this.parseFenceInfo(info); + if (!inCode) { + inCode = true; + codeLines = []; + codeLanguage = language || ''; + + const { description, useCase, fallbackUsed } = this.extractContextDescription( + contextBuffer, + sections, + title, + ); + + snippets.push({ + section: this.currentSection(sections, title), + language: codeLanguage, + options, + description, + useCase, + fallbackUsed, + code: null, + filePath: this.normalizeTitlePath(options?.title) || null, + variantGroupId: currentVariantGroupId, + tabLabel: currentTabLabel, + tabValue: currentTabValue, + context: [...contextBuffer], + }); + } else { + inCode = false; + if (snippets.length > 0) { + const current = snippets[snippets.length - 1]; + current.code = codeLines.join('\n').trimEnd(); + if (!current.filePath) { + const titleAttr = this.normalizeTitlePath(current.options?.title); + if (titleAttr) { + current.filePath = titleAttr; + } else { + // Prefer nearby "path:" hints before generic filename regex + const ctxPath = this.inferFilePathFromContext(current.context, { preferHints: true }); + current.filePath = ctxPath; + } + } + } + codeLines = []; + codeLanguage = ''; + } + return; + } + + if (inCode) { + codeLines.push(line); + return; + } + + // Detect MDX Tabs and TabItem wrappers to group variants + // start + const tabsOpen = trimmed.match(/^]*)>/); + if (tabsOpen) { + inTabs = true; + tabsCounter += 1; + const attrs = tabsOpen[1] || ''; + const gidMatch = attrs.match(/groupId\s*=\s*(?:"([^"]+)"|'([^']+)')/); + const gid = gidMatch ? (gidMatch[1] || gidMatch[2]) : 'default'; + currentTabsGroupId = gid; + currentVariantGroupId = `${docId}::${this.currentSection(sections, title)}::tabs${tabsCounter}:${gid}`; + return; + } + // end + if (/^<\/Tabs>\s*$/.test(trimmed)) { + inTabs = false; + currentTabsGroupId = null; + currentVariantGroupId = null; + return; + } + // start + const tabItemOpen = trimmed.match(/^]*)>/); + if (tabItemOpen) { + const attrs = tabItemOpen[1] || ''; + const labelMatch = attrs.match(/label\s*=\s*(?:"([^"]+)"|'([^']+)')/); + const valueMatch = attrs.match(/value\s*=\s*(?:"([^"]+)"|'([^']+)')/); + currentTabLabel = labelMatch ? (labelMatch[1] || labelMatch[2]) : null; + currentTabValue = valueMatch ? (valueMatch[1] || valueMatch[2]) : null; + // Ensure we have a variant group during TabItem even if attrs missing + if (!currentVariantGroupId) { + tabsCounter += 1; + currentVariantGroupId = `${docId}::${this.currentSection(sections, title)}::tabs${tabsCounter}:${currentTabsGroupId || 'default'}`; + } + return; + } + // end + if (/^<\/TabItem>\s*$/.test(trimmed)) { + currentTabLabel = null; + currentTabValue = null; + return; + } + + const headingMatch = trimmed.match(HEADING_REGEX); + if (headingMatch) { + const level = headingMatch[1].length; + const rawHeading = headingMatch[2]; + const customAnchorMatch = rawHeading.match(/\{#([A-Za-z0-9\-_]+)\}/); + const customAnchor = customAnchorMatch ? customAnchorMatch[1] : null; + const headingBase = rawHeading.replace(/\{#([A-Za-z0-9\-_]+)\}/, '').trim(); + const headingText = cleanInlineText(headingBase) || `Heading level ${level}`; + + while (sections.length >= level) { + sections.pop(); + } + + sections.push(headingText); + if (customAnchor) { + sectionAnchors[headingText] = customAnchor; + } + // Reset any ongoing Tabs grouping when changing section + inTabs = false; + currentTabsGroupId = null; + currentVariantGroupId = null; + currentTabLabel = null; + currentTabValue = null; + contextBuffer.push('\n'); + return; + } + + if (trimmed === '') { + contextBuffer.push('\n'); + } else { + contextBuffer.push(trimmed); + } + + if (contextBuffer.length > 50) { + contextBuffer.splice(0, contextBuffer.length - 50); + } + }); + + return { snippets: snippets.filter((snippet) => Boolean(snippet.code)), sectionAnchors }; + } + + deriveSnippetTitle(snippet, index) { + const baseDescription = snippet.description || ''; + + if (!snippet.fallbackUsed && baseDescription) { + const sentence = baseDescription.split(/(?<=[.!?])/)[0].trim(); + if (sentence) { + return sentence.length > 80 ? `${sentence.slice(0, 77)}…` : sentence; + } + } + + if (snippet.language) { + const langName = this.formatLanguageName(snippet.language); + return `Code example ${index + 1}: ${langName} version`; + } + + return `Code example ${index + 1}`; + } + + formatOutput(pages) { + const lines = []; + + pages.forEach((page) => { + lines.push(`# ${page.title}`); + lines.push(`Source: ${BASE_URL}/${page.docId}`); + lines.push(''); + + const snippetsBySection = page.snippets.reduce((acc, snippet) => { + if (!acc.has(snippet.section)) { + acc.set(snippet.section, []); + } + acc.get(snippet.section).push(snippet); + return acc; + }, new Map()); + + const seenSlugs = new Map(); + snippetsBySection.forEach((sectionSnippets, sectionName) => { + lines.push(`## ${sectionName}`); + if (this.includeSectionAnchors) { + const custom = page.sectionAnchors && page.sectionAnchors[sectionName]; + const anchor = custom || this.slugify(sectionName, seenSlugs); + lines.push(`Source: ${BASE_URL}/${page.docId}#${anchor}`); + } + lines.push(''); + + const groups = this.groupVariantSnippets(sectionSnippets); + + groups.forEach((group, index) => { + const primary = group.find((snippet) => !snippet.fallbackUsed) || group[0]; + const description = primary.fallbackUsed + ? this.buildFallbackDescription(primary) + : primary.description; + + lines.push(`### Example ${index + 1}`); + lines.push(`Description: ${description}`); + + const primaryFile = this.normalizeOutputPath(primary.filePath || this.inferFilePathFromContext(primary.context)); + if (primaryFile) { + const suffix = this.includeFileChecks && !this.fileExists(primaryFile) ? ' (missing)' : ''; + lines.push(`File: ${primaryFile}${suffix}`); + } + + if (primary.useCase) { + lines.push(`Use Case: ${primary.useCase}`); + } + + lines.push(''); + + group.forEach((variant, variantIndex) => { + if (variantIndex > 0) { + lines.push('---'); + } + + const resolvedFile = this.normalizeOutputPath( + variant.filePath || this.inferFilePathFromContext(variant.context) || primaryFile + ); + + const chosenLang = this.resolveLanguage(variant.language, resolvedFile, variant.code); + const language = chosenLang + ? `Language: ${this.formatLanguageName(chosenLang)}` + : 'Language: (unspecified)'; + lines.push(language); + + if (resolvedFile && resolvedFile !== primaryFile) { + const suffix = this.includeFileChecks && !this.fileExists(resolvedFile) ? ' (missing)' : ''; + lines.push(`File: ${resolvedFile}${suffix}`); + } + + // Visual separation before the code fence to avoid accidental inline rendering + lines.push(''); + + // Proper fenced code block without spurious leading newlines + const fence = chosenLang ? `\`\`\`${chosenLang}` : '```'; + lines.push(fence); + lines.push(variant.code); + lines.push('```'); + lines.push(''); + }); + + lines.push(''); + }); + + lines.push(''); + }); + + lines.push(''); + }); + + return lines.join('\n').trim() + '\n'; + } + + groupVariantSnippets(snippets) { + // First, group by explicit variantGroupId when present (MDX Tabs), + // otherwise fall back to old behavior of grouping consecutive snippets + const explicitGroups = new Map(); + const sequentialGroups = []; + let currentGroup = []; + + snippets.forEach((snippet) => { + if (snippet.variantGroupId) { + if (!explicitGroups.has(snippet.variantGroupId)) { + explicitGroups.set(snippet.variantGroupId, []); + } + explicitGroups.get(snippet.variantGroupId).push(snippet); + // Flush any ongoing sequential group before switching context + if (currentGroup.length > 0) { + sequentialGroups.push(currentGroup); + currentGroup = []; + } + return; + } + + if (currentGroup.length === 0) { + currentGroup.push(snippet); + return; + } + + const previous = currentGroup[currentGroup.length - 1]; + const sameDescription = (snippet.description || '') === (previous.description || ''); + const sameFile = (snippet.filePath || '') === (previous.filePath || ''); + + if (sameDescription && sameFile) { + currentGroup.push(snippet); + } else { + sequentialGroups.push(currentGroup); + currentGroup = [snippet]; + } + }); + + if (currentGroup.length > 0) { + sequentialGroups.push(currentGroup); + } + + const groups = [...explicitGroups.values(), ...sequentialGroups]; + return groups; + } + + inferFilePathFromContext(buffer = [], opts = { preferHints: false }) { + // 1) Prefer explicit "path:" hints nearby (last ~20 lines) + if (opts && opts.preferHints) { + const start = Math.max(0, buffer.length - 20); + for (let index = buffer.length - 1; index >= start; index -= 1) { + const entry = buffer[index]; + if (typeof entry !== 'string') { + continue; + } + const hint = entry.match(/(?:^|\b)path\s*:\s*([^\s,;]+[^\s]*)/i); + if (hint && hint[1]) { + const normalized = this.normalizeTitlePath(hint[0]); + if (normalized) { + return normalized; + } + // Fallback to raw capture + return hint[1]; + } + } + } + + // 2) Fallback: scan for any obvious file-like tokens with known extensions + for (let index = buffer.length - 1; index >= 0; index -= 1) { + const entry = buffer[index]; + if (typeof entry !== 'string') { + continue; + } + const match = entry.match(/(?:\.|\/)[^\s]*\.(?:js|ts|jsx|tsx|json|ya?ml)/i); + if (match) { + return match[0]; + } + } + return null; + } +} + +if (require.main === module) { + const { docs, output, anchors, checkFiles, projectRoot } = parseArgs(); + const generator = new DocusaurusLlmsCodeGenerator({ + docIds: docs, + outputPath: output, + includeSectionAnchors: anchors, + includeFileChecks: checkFiles, + projectRoot, + }); + + generator.generate().catch((error) => { + console.error(error); + process.exit(1); + }); +} + +module.exports = DocusaurusLlmsCodeGenerator; diff --git a/docusaurus/scripts/generate-llms.js b/docusaurus/scripts/generate-llms.js index 9ae9a1df05..9bf611fd4c 100644 --- a/docusaurus/scripts/generate-llms.js +++ b/docusaurus/scripts/generate-llms.js @@ -118,11 +118,13 @@ class DocusaurusLlmsGenerator { const { data: frontmatter, content } = matter(fileContent); const pageUrl = this.generatePageUrl(docId); - + const tldr = this.extractTldr(content); + pages.push({ id: docId, title: frontmatter.title || this.getTitleFromContent(content) || docId, - description: frontmatter.description || this.extractDescription(content), + description: + tldr || frontmatter.description || this.extractDescription(content), url: pageUrl, content: this.cleanContent(content), frontmatter @@ -160,6 +162,42 @@ class DocusaurusLlmsGenerator { return ''; } + extractTldr(content) { + const match = content.match(/([\s\S]*?)<\/Tldr>/i); + + if (!match) { + return null; + } + + const raw = match[1].trim(); + + if (!raw) { + return null; + } + + return this.sanitizeInlineMarkdown(raw); + } + + sanitizeInlineMarkdown(text) { + return text + // Remove fenced code blocks inside TLDR (rare but safe) + .replace(/```[\s\S]*?```/g, '') + // Strip inline code + .replace(/`([^`]+)`/g, '$1') + // Turn markdown links into plain text + .replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') + // Bold and italic markers + .replace(/\*\*([^*]+)\*\*/g, '$1') + .replace(/__([^_]+)__/g, '$1') + .replace(/\*([^*]+)\*/g, '$1') + .replace(/_([^_]+)_/g, '$1') + // Strip residual HTML tags (including MDX components) + .replace(/<[^>]+>/g, ' ') + // Collapse whitespace and trim + .replace(/\s+/g, ' ') + .trim(); + } + cleanContent(content) { return content // Deletes frontmatter metadata diff --git a/docusaurus/scripts/validate-llms-code.js b/docusaurus/scripts/validate-llms-code.js new file mode 100644 index 0000000000..ba54716891 --- /dev/null +++ b/docusaurus/scripts/validate-llms-code.js @@ -0,0 +1,436 @@ +#!/usr/bin/env node + +/** + * llms-code validator + * + * Validates the generated static/llms-code.txt structure and metadata: + * - Section structure and required fields + * - Balanced code fences and language tags + * - Recognized languages + * - File path presence and existence (when --check-files) + * - Source URL format and optional anchor syntax + * - Optional anchor verification against source MD/MDX (when --verify-anchors) + * + * Exit code: + * - 0 when clean (no errors; warnings ignored unless --strict) + * - 1 when errors found (or warnings found with --strict) + */ + +let fs = null; +try { + fs = require('fs-extra'); +} catch { + fs = require('fs'); + fs.ensureDir = async (dir) => fs.promises.mkdir(dir, { recursive: true }); + fs.pathExistsSync = (p) => fs.existsSync(p); + fs.writeFile = fs.promises.writeFile.bind(fs.promises); + fs.readFile = fs.promises.readFile.bind(fs.promises); +} +const path = require('path'); +const { URL } = require('url'); + +const DEFAULT_INPUT = path.join('static', 'llms-code.txt'); +const DEFAULT_PROJECT_ROOT = path.resolve('..'); +const BASE_HOSTS = new Set([ + 'docs.strapi.io', + 'localhost', + '127.0.0.1', +]); + +const RECOGNIZED_LANGS = new Set([ + 'javascript', 'typescript', 'tsx', 'jsx', + 'json', 'yaml', 'yml', + 'bash', 'sh', 'zsh', 'fish', + 'powershell', 'ps1', + 'sql', 'dockerfile', + 'toml', 'ini', 'env', 'diff', +]); + +const DISPLAY_LANG_MAP = new Map([ + ['javascript', 'javascript'], + ['typescript', 'typescript'], + ['tsx', 'tsx'], + ['jsx', 'jsx'], + ['json', 'json'], + ['yaml', 'yaml'], + ['yml', 'yml'], + ['bash', 'bash'], + ['shell', 'bash'], + ['sh', 'sh'], + ['zsh', 'zsh'], + ['fish', 'fish'], + ['powershell', 'powershell'], + ['ps1', 'ps1'], + ['sql', 'sql'], + ['dockerfile', 'dockerfile'], + ['toml', 'toml'], + ['ini', 'ini'], + ['env', 'env'], + ['diff', 'diff'], +]); + +function parseArgs() { + const args = process.argv.slice(2); + let inputPath = DEFAULT_INPUT; + let strict = false; + let report = 'text'; + let checkFiles = false; + let verifyAnchors = false; + let projectRoot = DEFAULT_PROJECT_ROOT; + + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + const next = args[i + 1]; + + if (arg === '--path') { + inputPath = next || inputPath; + i += 1; + } else if (arg === '--strict') { + strict = true; + } else if (arg === '--report') { + report = (next || report).toLowerCase(); + i += 1; + } else if (arg === '--check-files') { + checkFiles = true; + } else if (arg === '--verify-anchors') { + verifyAnchors = true; + } else if (arg === '--project-root') { + projectRoot = next ? path.resolve(next) : projectRoot; + i += 1; + } else if (arg === '--help' || arg === '-h') { + console.log(`Usage:\n node scripts/validate-llms-code.js [--path static/llms-code.txt] [--strict]\n [--check-files] [--verify-anchors]\n [--project-root ..] [--report json|text]`); + process.exit(0); + } + } + + return { inputPath, strict, report, checkFiles, verifyAnchors, projectRoot }; +} + +function slugifyHeading(text) { + if (!text) return ''; + const cleaned = String(text) + .replace(/\{#([A-Za-z0-9\-_]+)\}\s*$/, '') + .replace(/`([^`]+)`/g, '$1') + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') + .replace(/<[^>]+>/g, ' ') + .replace(/[.,\/#!$%^&*;:{}=_`~()"'?<>\[\]|+]/g, ' ') + .replace(/\s+/g, ' ') + .trim() + .toLowerCase(); + + return cleaned + .replace(/[^a-z0-9\s-]/g, '') + .trim() + .replace(/\s+/g, '-'); +} + +function extractHeadingId(line) { + const m = line.match(/^\s*#{1,6}\s+(.+?)\s*(\{#([A-Za-z0-9\-_]+)\})?\s*$/); + if (!m) return null; + const [, title, , custom] = m; + if (custom) return custom; + return slugifyHeading(title); +} + +function collectAnchorsFromDoc(content) { + const lines = content.split(/\r?\n/); + const anchors = new Set(); + for (const ln of lines) { + const id = extractHeadingId(ln); + if (id) anchors.add(id); + } + return anchors; +} + +function findDocFileForUrl(sourceUrl, projectRoot) { + let url; + try { + url = new URL(sourceUrl); + } catch { + return null; + } + const pathname = url.pathname.replace(/\/+$/, ''); + if (!pathname || pathname === '/') return null; + + const candidatesRoots = [ + path.join(projectRoot, 'docusaurus', 'docs'), + path.join(projectRoot, 'docs'), + path.join(projectRoot, 'website', 'docs'), + path.join(projectRoot, 'content', 'docs'), + ]; + + const suffixes = ['', '.mdx', '.md', '/index.mdx', '/index.md']; + + for (const root of candidatesRoots) { + for (const suf of suffixes) { + const attempt = path.join(root, pathname + suf); + if (fs.existsSync(attempt)) return attempt; + } + } + return null; +} + +function isAbsoluteHttpUrl(u) { + try { + const parsed = new URL(u); + return parsed.protocol === 'http:' || parsed.protocol === 'https:'; + } catch { + return false; + } +} + +function normalizeDisplayLang(name) { + if (!name) return null; + const key = String(name).trim().toLowerCase(); + return DISPLAY_LANG_MAP.get(key) || null; +} + +function fileExistsMaybe(projectRoot, filePath) { + if (!filePath || filePath === 'N/A') return false; + const cleaned = filePath.replace(/\s+\(file not found\)\s*$/, ''); + const resolved = path.isAbsolute(cleaned) + ? cleaned + : path.resolve(projectRoot, cleaned); + return fs.existsSync(resolved); +} + +function splitSectionsByHeading(inputText) { + const lines = inputText.split(/\r?\n/); + const sections = []; + let current = null; + + const flush = () => { + if (current) { + current.endLine = current.startLine + current.lines.length - 1; + sections.push(current); + current = null; + } + }; + + for (let i = 0; i < lines.length; i += 1) { + const line = lines[i]; + if (line.startsWith('## ')) { + flush(); + current = { + titleLine: line, + title: line.slice(3).trim(), + startLine: i + 1, + lines: [line], + }; + } else if (current) { + current.lines.push(line); + } + } + flush(); + return sections; +} + +function validateSection(section, opts) { + const { checkFiles, verifyAnchors, projectRoot } = opts; + + const diagnostics = []; + const lines = section.lines; + const title = section.title; + + const push = (severity, message, relLineIdx = 0) => { + diagnostics.push({ + severity, + message, + section: title, + line: section.startLine + relLineIdx, + }); + }; + + if (!lines[0] || !lines[0].startsWith('## ')) { + push('error', 'Section does not start with "## " heading', 0); + return diagnostics; + } + + let idx = 1; + const findLineIndex = (re, start) => { + for (let i = start; i < lines.length; i += 1) { + if (re.test(lines[i])) return i; + } + return -1; + }; + + while (idx < lines.length && lines[idx].trim() === '') idx += 1; + + const descIdx = lines[idx] && /^Description:\s*/i.test(lines[idx]) ? idx : -1; + if (descIdx === -1) { + push('error', 'Missing "Description:" line', idx); + return diagnostics; + } + const desc = lines[descIdx].replace(/^Description:\s*/i, '').trim(); + if (!desc) { + push('error', '"Description:" is empty', descIdx); + } else if (/^(tbd|todo|n\/a|1|none)$/i.test(desc)) { + push('warning', 'Description appears placeholder-like', descIdx); + } + idx = descIdx + 1; + + while (idx < lines.length && lines[idx].trim() === '') idx += 1; + + if (!lines[idx] || !/^\(Source:\s*.+\)$/.test(lines[idx])) { + push('error', 'Missing or malformed "(Source: ...)" line', idx); + return diagnostics; + } + const sourceLine = lines[idx]; + const sourceUrl = sourceLine.replace(/^\(Source:\s*/i, '').replace(/\)\s*$/, '').trim(); + if (!isAbsoluteHttpUrl(sourceUrl)) { + push('error', 'Source is not an absolute URL', idx); + } else { + try { + const u = new URL(sourceUrl); + if (!BASE_HOSTS.has(u.hostname)) { + push('warning', `Source host not in known set: ${u.hostname}`, idx); + } + } catch { + push('error', 'Source URL failed to parse', idx); + } + } + + let sourceAnchor = null; + try { + const u = new URL(sourceUrl); + sourceAnchor = u.hash ? u.hash.replace(/^#/, '') : null; + } catch {} + idx += 1; + + let sawAnyVariant = false; + while (idx < lines.length) { + if (lines[idx].startsWith('## ')) break; + while (idx < lines.length && lines[idx].trim() === '') idx += 1; + if (idx >= lines.length || lines[idx].startsWith('## ')) break; + + if (lines[idx].trim() === '---') { + idx += 1; + while (idx < lines.length && lines[idx].trim() === '') idx += 1; + } + + if (!lines[idx] || !/^Language:\s*/i.test(lines[idx])) { + if (!sawAnyVariant) push('error', 'Missing "Language:" before code block', idx); + break; + } + const displayLangRaw = lines[idx].replace(/^Language:\s*/i, '').trim(); + const canonicalLang = normalizeDisplayLang(displayLangRaw); + if (!canonicalLang) { + push('error', `Unrecognized language: ${displayLangRaw}`, idx); + } else if (!RECOGNIZED_LANGS.has(canonicalLang)) { + push('warning', `Language recognized but not in allowlist: ${canonicalLang}`, idx); + } + idx += 1; + + if (!lines[idx] || !/^File path:\s*/i.test(lines[idx])) { + push('error', 'Missing "File path:" line', idx); + break; + } + const filePathValue = lines[idx].replace(/^File path:\s*/i, '').trim(); + if (!filePathValue) { + push('error', '"File path:" is empty', idx); + } else if (checkFiles) { + const exists = fileExistsMaybe(projectRoot, filePathValue); + if (!exists) { + push('error', `Referenced file does not exist: ${filePathValue}`, idx); + } + } + idx += 1; + + while (idx < lines.length && lines[idx].trim() === '') idx += 1; + + const fenceStart = lines[idx] || ''; + const fenceStartMatch = fenceStart.match(/^```([a-z0-9]+)\s*$/i); + if (!fenceStartMatch) { + push('error', 'Missing opening code fence ```', idx); + break; + } + const fenceLang = fenceStartMatch[1].toLowerCase(); + if (canonicalLang && fenceLang !== canonicalLang) { + push('error', `Fence language "${fenceLang}" does not match declared Language "${displayLangRaw}"`, idx); + } + idx += 1; + + let closed = false; + while (idx < lines.length) { + if (/^```/.test(lines[idx])) { + closed = true; + idx += 1; + break; + } + idx += 1; + } + if (!closed) { + push('error', 'Unclosed code fence', idx); + break; + } + + sawAnyVariant = true; + } + + if (!sawAnyVariant) { + push('error', 'No code example variants found in section', 0); + } + + if (verifyAnchors && sourceAnchor) { + const docFile = findDocFileForUrl(sourceUrl, projectRoot); + if (!docFile) { + push('warning', `Could not locate local doc file for source to verify anchor: ${sourceUrl}`, 0); + } else { + try { + const raw = fs.readFileSync(docFile, 'utf8'); + const anchors = collectAnchorsFromDoc(raw); + if (!anchors.has(sourceAnchor)) { + push('error', `Anchor "#${sourceAnchor}" not found in ${path.relative(projectRoot, docFile)}`, 0); + } + } catch (e) { + push('warning', `Failed reading doc for anchor verification: ${e.message}`, 0); + } + } + } + + return diagnostics; +} + +(async function main() { + const args = parseArgs(); + const { inputPath, strict, report, checkFiles, verifyAnchors, projectRoot } = args; + + if (!fs.existsSync(inputPath)) { + console.error(`ERROR: Input file not found: ${inputPath}`); + process.exit(1); + } + + const raw = await fs.readFile(inputPath, 'utf8'); + const sections = splitSectionsByHeading(raw); + + const diagnostics = []; + for (const sec of sections) { + const diags = validateSection(sec, { checkFiles, verifyAnchors, projectRoot }); + diagnostics.push(...diags); + } + + const errors = diagnostics.filter(d => d.severity === 'error'); + const warnings = diagnostics.filter(d => d.severity === 'warning'); + + if (report === 'json') { + console.log(JSON.stringify({ errors, warnings, count: diagnostics.length }, null, 2)); + } else { + for (const d of diagnostics) { + const tag = d.severity.toUpperCase(); + const loc = d.line ? `:${d.line}` : ''; + console.log(`[${tag}] ${d.section}${loc} - ${d.message}`); + } + if (diagnostics.length === 0) { + console.log('llms-code validation passed: no issues found.'); + } else { + console.log(`\nSummary: ${errors.length} error(s), ${warnings.length} warning(s)`); + } + } + + const exitWithError = errors.length > 0 || (strict && warnings.length > 0); + process.exit(exitWithError ? 1 : 0); +})().catch((e) => { + console.error('Validator crashed:', e); + process.exit(1); +}); + diff --git a/docusaurus/scripts/validate-prompts.js b/docusaurus/scripts/validate-prompts.js new file mode 100755 index 0000000000..3694b080a8 --- /dev/null +++ b/docusaurus/scripts/validate-prompts.js @@ -0,0 +1,58 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +const PROMPT_FILE = path.join(__dirname, '..', 'src', 'components', 'AiToolbar', 'config', 'aiPromptTemplates.js'); +const REQUIRED_PLACEHOLDERS = ['{{url}}']; + +function loadPromptMap(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + const match = content.match(/export const aiPromptTemplates = (\{[\s\S]*?\});/); + if (!match) { + throw new Error('Unable to locate aiPromptTemplates export.'); + } + // eslint-disable-next-line no-new-func + const factory = new Function(`return (${match[1]});`); + return factory(); +} + +function main() { + try { + const prompts = loadPromptMap(PROMPT_FILE); + const languages = Object.keys(prompts); + + if (languages.length === 0) { + throw new Error('No prompt translations found.'); + } + + for (const lang of languages) { + const value = prompts[lang]; + if (typeof value !== 'string') { + throw new Error(`Prompt for language "${lang}" must be a string.`); + } + + const trimmed = value.trim(); + if (trimmed.length === 0) { + throw new Error(`Prompt for language "${lang}" is empty.`); + } + + if (trimmed !== value) { + console.warn(`Warning: prompt for "${lang}" has leading/trailing whitespace.`); + } + + for (const placeholder of REQUIRED_PLACEHOLDERS) { + if (!value.includes(placeholder)) { + throw new Error(`Prompt for "${lang}" is missing placeholder ${placeholder}.`); + } + } + } + + console.log(`Validated ${languages.length} prompt translations.`); + } catch (error) { + console.error(error.message); + process.exit(1); + } +} + +main(); diff --git a/docusaurus/src/components/AiToolbar/AiToolbar.jsx b/docusaurus/src/components/AiToolbar/AiToolbar.jsx index c94964d621..401e15c5cd 100644 --- a/docusaurus/src/components/AiToolbar/AiToolbar.jsx +++ b/docusaurus/src/components/AiToolbar/AiToolbar.jsx @@ -124,7 +124,7 @@ const AiToolbar = () => {