mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-29 21:19:25 +02:00
Update all 6 documentation files and docs_service.ts: - home.md: Add AI Chat, Knowledge Base, and Benchmark sections; replace Open WebUI references with built-in AI Chat links; expand Quick Links table with new features - getting-started.md: Update Easy Setup steps to match current wizard (Capabilities/Maps/Content/Review); replace Open WebUI section with AI Assistant and Knowledge Base sections; add Wikipedia Selector and System Benchmark docs; update GPU specs - faq.md: Add AI, Knowledge Base, Benchmark, and curated tier FAQ entries; add troubleshooting for AI Chat, Knowledge Base uploads, and benchmark submission; update all references from Open WebUI to built-in AI Chat; add Discord community link - use-cases.md: Add Knowledge Base mentions across Emergency Prep, Homeschooling, Remote Work, Privacy, and Academic Research use cases; add "Upload Relevant Documents" setup step; update privacy section to emphasize built-in AI - about.md: Fix "ultime" typo, add project evolution paragraph, add community links section - release-notes.md: Add all versions from v1.11.0 through v1.23.0 with accurate dates and changes from git history; consolidate patch versions; update Support section with Discord link - docs_service.ts: Replace alphabetical sidebar sort with custom ordering (Home > Getting Started > Use Cases > FAQ > About > Release Notes) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
154 lines
4.3 KiB
TypeScript
154 lines
4.3 KiB
TypeScript
import Markdoc from '@markdoc/markdoc'
|
|
import { streamToString } from '../../util/docs.js'
|
|
import { getFile, getFileStatsIfExists, listDirectoryContentsRecursive } from '../utils/fs.js'
|
|
import path from 'path'
|
|
import InternalServerErrorException from '#exceptions/internal_server_error_exception'
|
|
|
|
export class DocsService {
|
|
private docsPath = path.join(process.cwd(), 'docs')
|
|
|
|
private static readonly DOC_ORDER: Record<string, number> = {
|
|
'home': 1,
|
|
'getting-started': 2,
|
|
'use-cases': 3,
|
|
'faq': 4,
|
|
'about': 5,
|
|
'release-notes': 6,
|
|
}
|
|
|
|
async getDocs() {
|
|
const contents = await listDirectoryContentsRecursive(this.docsPath)
|
|
const files: Array<{ title: string; slug: string }> = []
|
|
|
|
for (const item of contents) {
|
|
if (item.type === 'file' && item.name.endsWith('.md')) {
|
|
const cleaned = this.prettify(item.name)
|
|
files.push({
|
|
title: cleaned,
|
|
slug: item.name.replace(/\.md$/, ''),
|
|
})
|
|
}
|
|
}
|
|
|
|
return files.sort((a, b) => {
|
|
const orderA = DocsService.DOC_ORDER[a.slug] ?? 999
|
|
const orderB = DocsService.DOC_ORDER[b.slug] ?? 999
|
|
return orderA - orderB
|
|
})
|
|
}
|
|
|
|
parse(content: string) {
|
|
try {
|
|
const ast = Markdoc.parse(content)
|
|
const config = this.getConfig()
|
|
const errors = Markdoc.validate(ast, config)
|
|
|
|
// Filter out attribute-undefined errors which may be caused by emojis and special characters
|
|
const criticalErrors = errors.filter((e) => e.error.id !== 'attribute-undefined')
|
|
if (criticalErrors.length > 0) {
|
|
console.error('Markdoc validation errors:', errors.map((e) => JSON.stringify(e.error)).join(', '))
|
|
throw new Error('Markdoc validation failed')
|
|
}
|
|
|
|
return Markdoc.transform(ast, config)
|
|
} catch (error) {
|
|
console.log('Error parsing Markdoc content:', error)
|
|
throw new InternalServerErrorException(`Error parsing content: ${(error as Error).message}`)
|
|
}
|
|
}
|
|
|
|
async parseFile(_filename: string) {
|
|
try {
|
|
if (!_filename) {
|
|
throw new Error('Filename is required')
|
|
}
|
|
|
|
const filename = _filename.endsWith('.md') ? _filename : `${_filename}.md`
|
|
|
|
const fileExists = await getFileStatsIfExists(path.join(this.docsPath, filename))
|
|
if (!fileExists) {
|
|
throw new Error(`File not found: ${filename}`)
|
|
}
|
|
|
|
const fileStream = await getFile(path.join(this.docsPath, filename), 'stream')
|
|
if (!fileStream) {
|
|
throw new Error(`Failed to read file stream: ${filename}`)
|
|
}
|
|
const content = await streamToString(fileStream)
|
|
return this.parse(content)
|
|
} catch (error) {
|
|
throw new InternalServerErrorException(`Error parsing file: ${(error as Error).message}`)
|
|
}
|
|
}
|
|
|
|
private prettify(filename: string) {
|
|
// Remove hyphens, underscores, and file extension
|
|
const cleaned = filename.replace(/_/g, ' ').replace(/\.md$/, '').replace(/-/g, ' ')
|
|
// Convert to Title Case
|
|
const titleCased = cleaned.replace(/\b\w/g, (char) => char.toUpperCase())
|
|
return titleCased.charAt(0).toUpperCase() + titleCased.slice(1)
|
|
}
|
|
|
|
private getConfig() {
|
|
return {
|
|
tags: {
|
|
callout: {
|
|
render: 'Callout',
|
|
attributes: {
|
|
type: {
|
|
type: String,
|
|
default: 'info',
|
|
matches: ['info', 'warning', 'error', 'success'],
|
|
},
|
|
title: {
|
|
type: String,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
nodes: {
|
|
heading: {
|
|
render: 'Heading',
|
|
attributes: {
|
|
level: { type: Number, required: true },
|
|
id: { type: String },
|
|
},
|
|
},
|
|
list: {
|
|
render: 'List',
|
|
attributes: {
|
|
ordered: { type: Boolean },
|
|
start: { type: Number },
|
|
},
|
|
},
|
|
list_item: {
|
|
render: 'ListItem',
|
|
attributes: {
|
|
marker: { type: String },
|
|
className: { type: String },
|
|
class: { type: String }
|
|
}
|
|
},
|
|
table: {
|
|
render: 'Table',
|
|
},
|
|
thead: {
|
|
render: 'TableHead',
|
|
},
|
|
tbody: {
|
|
render: 'TableBody',
|
|
},
|
|
tr: {
|
|
render: 'TableRow',
|
|
},
|
|
th: {
|
|
render: 'TableHeader',
|
|
},
|
|
td: {
|
|
render: 'TableCell',
|
|
},
|
|
},
|
|
}
|
|
}
|
|
}
|