botdocs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/bin/botdocs.js +6 -0
- package/dist/src/builder/chunker.d.ts +41 -0
- package/dist/src/builder/chunker.js +139 -0
- package/dist/src/builder/embedder.d.ts +26 -0
- package/dist/src/builder/embedder.js +75 -0
- package/dist/src/builder/index.d.ts +5 -0
- package/dist/src/builder/index.js +142 -0
- package/dist/src/builder/markdown-processor.d.ts +26 -0
- package/dist/src/builder/markdown-processor.js +161 -0
- package/dist/src/builder/site-generator.d.ts +24 -0
- package/dist/src/builder/site-generator.js +152 -0
- package/dist/src/builder/template-engine.d.ts +30 -0
- package/dist/src/builder/template-engine.js +77 -0
- package/dist/src/builder/vector-db-builder.d.ts +20 -0
- package/dist/src/builder/vector-db-builder.js +63 -0
- package/dist/src/cli/index.d.ts +1 -0
- package/dist/src/cli/index.js +47 -0
- package/dist/src/cli/options.d.ts +7 -0
- package/dist/src/cli/options.js +5 -0
- package/dist/src/types/config.d.ts +24 -0
- package/dist/src/types/config.js +16 -0
- package/dist/src/types/document.d.ts +20 -0
- package/dist/src/types/document.js +1 -0
- package/dist/src/types/vector-db.d.ts +21 -0
- package/dist/src/types/vector-db.js +1 -0
- package/dist-client/assets/chatbox-CmTTiB2Z.js +1 -0
- package/dist-client/assets/rag-engine-L2vj3Y0G.js +7 -0
- package/dist-client/bundle.js +1 -0
- package/package.json +71 -0
- package/src/styles/chat.css +420 -0
- package/src/styles/main.css +532 -0
- package/src/styles/themes.css +78 -0
- package/src/templates/doc-page.html +27 -0
- package/src/templates/index.html +28 -0
- package/src/templates/layout.html +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 usr-wwelsh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Botdocs
|
|
2
|
+
|
|
3
|
+
Convert markdown documentation into beautiful static sites with AI-powered semantic search — no backend required.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Markdown to HTML** - Converts `.md` files into polished static sites
|
|
8
|
+
- **Semantic Search** - Client-side vector search using Transformers.js
|
|
9
|
+
- **Dark Mode** - Built-in theme switching
|
|
10
|
+
- **Deep Links** - Search results link directly to sections
|
|
11
|
+
- **No Backend** - Everything runs in the browser
|
|
12
|
+
- **Fast** - Syntax highlighting with Shiki
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Install globally via npm:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g botdocs
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Generate site from markdown
|
|
26
|
+
botdocs ./docs
|
|
27
|
+
|
|
28
|
+
# Disable chatbot
|
|
29
|
+
botdocs ./docs --no-chat
|
|
30
|
+
|
|
31
|
+
# Custom output directory
|
|
32
|
+
botdocs ./docs -o ./public
|
|
33
|
+
|
|
34
|
+
# Verbose logging
|
|
35
|
+
botdocs ./docs -v
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
Create `botdocs.config.json` in your docs directory:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"title": "My Documentation",
|
|
45
|
+
"description": "Project docs",
|
|
46
|
+
"chat": { "enabled": true },
|
|
47
|
+
"build": {
|
|
48
|
+
"chunkSize": 500,
|
|
49
|
+
"chunkOverlap": 50,
|
|
50
|
+
"topK": 5
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Front Matter
|
|
56
|
+
|
|
57
|
+
```markdown
|
|
58
|
+
---
|
|
59
|
+
title: Getting Started
|
|
60
|
+
description: Quick start guide
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
# Your content here
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## How It Works
|
|
67
|
+
|
|
68
|
+
1. **Build**: Parses markdown → generates embeddings → creates `vector-db.json`
|
|
69
|
+
2. **Runtime**: User query → embed → search vector DB → return relevant chunks
|
|
70
|
+
3. **No LLM**: Pure semantic search, not AI text generation
|
|
71
|
+
|
|
72
|
+
## Architecture
|
|
73
|
+
|
|
74
|
+
- **Embedding Model**: `e5-small-v2` (384-dim vectors, 2.2x faster than all-MiniLM-L6-v2)
|
|
75
|
+
- **Search**: Cosine similarity, client-side only
|
|
76
|
+
- **Browser Bundle**: ~825KB (includes Transformers.js)
|
|
77
|
+
- **Deployment**: Fully static, works on any host
|
|
78
|
+
|
|
79
|
+
## Development
|
|
80
|
+
|
|
81
|
+
Building from source:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
git clone https://github.com/usr-wwelsh/botdocs.git
|
|
85
|
+
cd botdocs
|
|
86
|
+
npm install
|
|
87
|
+
npm run build && npm run build:client
|
|
88
|
+
botdocs ./test-docs
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT © [usr-wwelsh](https://github.com/usr-wwelsh)
|
package/bin/botdocs.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ProcessedDocument } from '../types/document.js';
|
|
2
|
+
import { ChunkMetadata } from '../types/vector-db.js';
|
|
3
|
+
export interface TextChunk {
|
|
4
|
+
text: string;
|
|
5
|
+
metadata: ChunkMetadata;
|
|
6
|
+
}
|
|
7
|
+
export interface ChunkerOptions {
|
|
8
|
+
maxChunkSize: number;
|
|
9
|
+
chunkOverlap: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Text chunker that splits documents by headings while respecting token limits
|
|
13
|
+
*/
|
|
14
|
+
export declare class Chunker {
|
|
15
|
+
private options;
|
|
16
|
+
constructor(options?: Partial<ChunkerOptions>);
|
|
17
|
+
/**
|
|
18
|
+
* Chunk a document into semantically meaningful pieces
|
|
19
|
+
*/
|
|
20
|
+
chunkDocument(doc: ProcessedDocument): TextChunk[];
|
|
21
|
+
/**
|
|
22
|
+
* Create a chunk with metadata
|
|
23
|
+
*/
|
|
24
|
+
private createChunk;
|
|
25
|
+
/**
|
|
26
|
+
* Convert heading text to anchor ID (matches markdown-it-anchor behavior)
|
|
27
|
+
*/
|
|
28
|
+
private slugify;
|
|
29
|
+
/**
|
|
30
|
+
* Estimate token count (rough approximation: ~4 chars = 1 token)
|
|
31
|
+
*/
|
|
32
|
+
private estimateTokens;
|
|
33
|
+
/**
|
|
34
|
+
* Get last N tokens worth of lines for overlap
|
|
35
|
+
*/
|
|
36
|
+
private getOverlapLines;
|
|
37
|
+
/**
|
|
38
|
+
* Chunk multiple documents
|
|
39
|
+
*/
|
|
40
|
+
chunkDocuments(documents: ProcessedDocument[]): TextChunk[];
|
|
41
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text chunker that splits documents by headings while respecting token limits
|
|
3
|
+
*/
|
|
4
|
+
export class Chunker {
|
|
5
|
+
options;
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
this.options = {
|
|
8
|
+
maxChunkSize: options.maxChunkSize || 500,
|
|
9
|
+
chunkOverlap: options.chunkOverlap || 50,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Chunk a document into semantically meaningful pieces
|
|
14
|
+
*/
|
|
15
|
+
chunkDocument(doc) {
|
|
16
|
+
const chunks = [];
|
|
17
|
+
const lines = doc.content.split('\n');
|
|
18
|
+
let currentChunk = [];
|
|
19
|
+
let currentHeading;
|
|
20
|
+
let inCodeBlock = false;
|
|
21
|
+
let codeBlockLines = [];
|
|
22
|
+
for (let i = 0; i < lines.length; i++) {
|
|
23
|
+
const line = lines[i];
|
|
24
|
+
// Handle code blocks
|
|
25
|
+
if (line.trim().startsWith('```')) {
|
|
26
|
+
if (!inCodeBlock) {
|
|
27
|
+
// Start of code block
|
|
28
|
+
inCodeBlock = true;
|
|
29
|
+
codeBlockLines = [line];
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
// End of code block
|
|
33
|
+
inCodeBlock = false;
|
|
34
|
+
codeBlockLines.push(line);
|
|
35
|
+
// Add complete code block to current chunk
|
|
36
|
+
currentChunk.push(...codeBlockLines);
|
|
37
|
+
codeBlockLines = [];
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (inCodeBlock) {
|
|
42
|
+
codeBlockLines.push(line);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
// Detect headings
|
|
46
|
+
const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
|
|
47
|
+
if (headingMatch) {
|
|
48
|
+
// Save current chunk if it exists
|
|
49
|
+
if (currentChunk.length > 0) {
|
|
50
|
+
chunks.push(this.createChunk(currentChunk.join('\n'), doc, currentHeading));
|
|
51
|
+
}
|
|
52
|
+
// Start new chunk with this heading
|
|
53
|
+
currentHeading = headingMatch[2];
|
|
54
|
+
currentChunk = [line];
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
currentChunk.push(line);
|
|
58
|
+
// Check if chunk is getting too large
|
|
59
|
+
const tokenCount = this.estimateTokens(currentChunk.join('\n'));
|
|
60
|
+
if (tokenCount >= this.options.maxChunkSize) {
|
|
61
|
+
// Split chunk
|
|
62
|
+
const chunkText = currentChunk.join('\n');
|
|
63
|
+
chunks.push(this.createChunk(chunkText, doc, currentHeading));
|
|
64
|
+
// Create overlap for next chunk
|
|
65
|
+
const overlapLines = this.getOverlapLines(currentChunk, this.options.chunkOverlap);
|
|
66
|
+
currentChunk = overlapLines;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Add final chunk
|
|
71
|
+
if (currentChunk.length > 0) {
|
|
72
|
+
chunks.push(this.createChunk(currentChunk.join('\n'), doc, currentHeading));
|
|
73
|
+
}
|
|
74
|
+
return chunks.filter((chunk) => chunk.text.trim().length > 0);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Create a chunk with metadata
|
|
78
|
+
*/
|
|
79
|
+
createChunk(text, doc, heading) {
|
|
80
|
+
return {
|
|
81
|
+
text: text.trim(),
|
|
82
|
+
metadata: {
|
|
83
|
+
sourceFile: doc.relativePath,
|
|
84
|
+
title: doc.metadata.title || 'Untitled',
|
|
85
|
+
heading,
|
|
86
|
+
headingId: heading ? this.slugify(heading) : undefined,
|
|
87
|
+
url: doc.url,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Convert heading text to anchor ID (matches markdown-it-anchor behavior)
|
|
93
|
+
*/
|
|
94
|
+
slugify(text) {
|
|
95
|
+
return text
|
|
96
|
+
.toLowerCase()
|
|
97
|
+
.trim()
|
|
98
|
+
.replace(/[\s+]/g, '-')
|
|
99
|
+
.replace(/[^\w\-]+/g, '')
|
|
100
|
+
.replace(/\-\-+/g, '-')
|
|
101
|
+
.replace(/^-+/, '')
|
|
102
|
+
.replace(/-+$/, '');
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Estimate token count (rough approximation: ~4 chars = 1 token)
|
|
106
|
+
*/
|
|
107
|
+
estimateTokens(text) {
|
|
108
|
+
return Math.ceil(text.length / 4);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get last N tokens worth of lines for overlap
|
|
112
|
+
*/
|
|
113
|
+
getOverlapLines(lines, targetTokens) {
|
|
114
|
+
const result = [];
|
|
115
|
+
let tokenCount = 0;
|
|
116
|
+
// Work backwards from end
|
|
117
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
118
|
+
const line = lines[i];
|
|
119
|
+
const lineTokens = this.estimateTokens(line);
|
|
120
|
+
if (tokenCount + lineTokens > targetTokens) {
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
result.unshift(line);
|
|
124
|
+
tokenCount += lineTokens;
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Chunk multiple documents
|
|
130
|
+
*/
|
|
131
|
+
chunkDocuments(documents) {
|
|
132
|
+
const allChunks = [];
|
|
133
|
+
for (const doc of documents) {
|
|
134
|
+
const chunks = this.chunkDocument(doc);
|
|
135
|
+
allChunks.push(...chunks);
|
|
136
|
+
}
|
|
137
|
+
return allChunks;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare class Embedder {
|
|
2
|
+
private model;
|
|
3
|
+
private modelName;
|
|
4
|
+
private dimension;
|
|
5
|
+
constructor(modelName?: string);
|
|
6
|
+
/**
|
|
7
|
+
* Initialize the embedding model
|
|
8
|
+
*/
|
|
9
|
+
initialize(): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Generate embedding for a single text
|
|
12
|
+
*/
|
|
13
|
+
embed(text: string): Promise<number[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Generate embeddings for multiple texts (batched for efficiency)
|
|
16
|
+
*/
|
|
17
|
+
embedBatch(texts: string[], batchSize?: number, onProgress?: (current: number, total: number) => void): Promise<number[][]>;
|
|
18
|
+
/**
|
|
19
|
+
* Get embedding dimension
|
|
20
|
+
*/
|
|
21
|
+
getDimension(): number;
|
|
22
|
+
/**
|
|
23
|
+
* Get model name
|
|
24
|
+
*/
|
|
25
|
+
getModelName(): string;
|
|
26
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { pipeline, env } from '@xenova/transformers';
|
|
2
|
+
// Disable local model loading - use Hugging Face
|
|
3
|
+
env.allowLocalModels = false;
|
|
4
|
+
export class Embedder {
|
|
5
|
+
model;
|
|
6
|
+
modelName;
|
|
7
|
+
dimension;
|
|
8
|
+
constructor(modelName = 'Xenova/e5-small-v2') {
|
|
9
|
+
this.modelName = modelName;
|
|
10
|
+
this.dimension = 384; // e5-small-v2 produces 384-dimensional embeddings
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Initialize the embedding model
|
|
14
|
+
*/
|
|
15
|
+
async initialize() {
|
|
16
|
+
console.log(`Loading embedding model: ${this.modelName}...`);
|
|
17
|
+
this.model = await pipeline('feature-extraction', this.modelName);
|
|
18
|
+
console.log('Embedding model loaded');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Generate embedding for a single text
|
|
22
|
+
*/
|
|
23
|
+
async embed(text) {
|
|
24
|
+
if (!this.model) {
|
|
25
|
+
await this.initialize();
|
|
26
|
+
}
|
|
27
|
+
// Prepend "passage: " prefix for e5 models
|
|
28
|
+
const prefixedText = `passage: ${text}`;
|
|
29
|
+
// Generate embedding
|
|
30
|
+
const output = await this.model(prefixedText, {
|
|
31
|
+
pooling: 'mean',
|
|
32
|
+
normalize: true,
|
|
33
|
+
});
|
|
34
|
+
// Convert to array
|
|
35
|
+
const embedding = Array.from(output.data);
|
|
36
|
+
return embedding;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Generate embeddings for multiple texts (batched for efficiency)
|
|
40
|
+
*/
|
|
41
|
+
async embedBatch(texts, batchSize = 10, onProgress) {
|
|
42
|
+
if (!this.model) {
|
|
43
|
+
await this.initialize();
|
|
44
|
+
}
|
|
45
|
+
const embeddings = [];
|
|
46
|
+
const total = texts.length;
|
|
47
|
+
for (let i = 0; i < texts.length; i += batchSize) {
|
|
48
|
+
const batch = texts.slice(i, i + batchSize);
|
|
49
|
+
// Process batch in parallel
|
|
50
|
+
const batchEmbeddings = await Promise.all(batch.map((text) => this.embed(text)));
|
|
51
|
+
embeddings.push(...batchEmbeddings);
|
|
52
|
+
// Report progress
|
|
53
|
+
const current = Math.min(i + batchSize, total);
|
|
54
|
+
if (onProgress) {
|
|
55
|
+
onProgress(current, total);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
console.log(`Embedded ${current}/${total} chunks`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return embeddings;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get embedding dimension
|
|
65
|
+
*/
|
|
66
|
+
getDimension() {
|
|
67
|
+
return this.dimension;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get model name
|
|
71
|
+
*/
|
|
72
|
+
getModelName() {
|
|
73
|
+
return this.modelName;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { defaultConfig } from '../types/config.js';
|
|
2
|
+
import { SiteGenerator } from './site-generator.js';
|
|
3
|
+
import { VectorDBBuilder } from './vector-db-builder.js';
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, copyFileSync } from 'fs';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import { join, dirname, resolve } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { exec } from 'child_process';
|
|
9
|
+
import { promisify } from 'util';
|
|
10
|
+
const execAsync = promisify(exec);
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
/**
|
|
14
|
+
* Main build orchestrator
|
|
15
|
+
*/
|
|
16
|
+
export async function build(options) {
|
|
17
|
+
const { inputDir, outputDir, chatEnabled, configPath, verbose } = options;
|
|
18
|
+
// Validate input directory
|
|
19
|
+
if (!existsSync(inputDir)) {
|
|
20
|
+
throw new Error(`Input directory does not exist: ${inputDir}`);
|
|
21
|
+
}
|
|
22
|
+
// Load config
|
|
23
|
+
const config = loadConfig(configPath, inputDir);
|
|
24
|
+
// Override chat enabled setting if specified in CLI
|
|
25
|
+
if (chatEnabled !== undefined) {
|
|
26
|
+
config.chat = config.chat || {};
|
|
27
|
+
config.chat.enabled = chatEnabled;
|
|
28
|
+
}
|
|
29
|
+
if (verbose) {
|
|
30
|
+
console.log('Configuration:', JSON.stringify(config, null, 2));
|
|
31
|
+
}
|
|
32
|
+
// Prepare output directory
|
|
33
|
+
fs.ensureDirSync(outputDir);
|
|
34
|
+
if (verbose) {
|
|
35
|
+
console.log(`Cleaning output directory: ${outputDir}`);
|
|
36
|
+
}
|
|
37
|
+
// Create site generator
|
|
38
|
+
const generator = new SiteGenerator();
|
|
39
|
+
// Generate site
|
|
40
|
+
const documents = await generator.generate(inputDir, outputDir, config);
|
|
41
|
+
// Create assets directories
|
|
42
|
+
const assetsDir = join(outputDir, 'assets');
|
|
43
|
+
fs.ensureDirSync(join(assetsDir, 'css'));
|
|
44
|
+
fs.ensureDirSync(join(assetsDir, 'js'));
|
|
45
|
+
if (verbose) {
|
|
46
|
+
console.log(`Processed ${documents.length} documents`);
|
|
47
|
+
}
|
|
48
|
+
// Phase 2: Generate vector database (if chat enabled)
|
|
49
|
+
if (config.chat?.enabled) {
|
|
50
|
+
const vectorDBBuilder = new VectorDBBuilder({
|
|
51
|
+
chunkSize: config.build?.chunkSize,
|
|
52
|
+
chunkOverlap: config.build?.chunkOverlap,
|
|
53
|
+
});
|
|
54
|
+
await vectorDBBuilder.build(documents, outputDir, verbose);
|
|
55
|
+
}
|
|
56
|
+
// Phase 3: Build client-side code with Vite
|
|
57
|
+
console.log('Building client-side code...');
|
|
58
|
+
// From dist/src/builder, go up 3 levels to project root
|
|
59
|
+
const projectRoot = resolve(__dirname, '../../..');
|
|
60
|
+
try {
|
|
61
|
+
// Skip rebuild if already built
|
|
62
|
+
const distClientDir = join(projectRoot, 'dist-client');
|
|
63
|
+
const bundleExists = existsSync(join(distClientDir, 'bundle.js'));
|
|
64
|
+
if (!bundleExists) {
|
|
65
|
+
await execAsync('npm run build:client', { cwd: projectRoot });
|
|
66
|
+
}
|
|
67
|
+
// Copy bundled JS files
|
|
68
|
+
if (existsSync(distClientDir)) {
|
|
69
|
+
const jsOutputDir = join(assetsDir, 'js');
|
|
70
|
+
if (verbose) {
|
|
71
|
+
console.log(`Copying client code from: ${distClientDir}`);
|
|
72
|
+
console.log(`Copying to: ${jsOutputDir}`);
|
|
73
|
+
}
|
|
74
|
+
// Copy bundle.js
|
|
75
|
+
const sourceBundlePath = join(distClientDir, 'bundle.js');
|
|
76
|
+
if (existsSync(sourceBundlePath)) {
|
|
77
|
+
copyFileSync(sourceBundlePath, join(jsOutputDir, 'bundle.js'));
|
|
78
|
+
}
|
|
79
|
+
// Copy bundle.js.map if exists
|
|
80
|
+
const sourceMapPath = join(distClientDir, 'bundle.js.map');
|
|
81
|
+
if (existsSync(sourceMapPath)) {
|
|
82
|
+
copyFileSync(sourceMapPath, join(jsOutputDir, 'bundle.js.map'));
|
|
83
|
+
}
|
|
84
|
+
// Copy assets folder (for code-split chunks)
|
|
85
|
+
const distAssetsDir = join(distClientDir, 'assets');
|
|
86
|
+
if (existsSync(distAssetsDir)) {
|
|
87
|
+
const outputAssetsDir = join(jsOutputDir, 'assets');
|
|
88
|
+
fs.copySync(distAssetsDir, outputAssetsDir);
|
|
89
|
+
if (verbose) {
|
|
90
|
+
console.log('Copied code-split chunks');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (verbose) {
|
|
94
|
+
console.log('Client code copy complete');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else if (verbose) {
|
|
98
|
+
console.log(`dist-client directory not found: ${distClientDir}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
console.warn('Client build skipped (run npm run build:client manually)');
|
|
103
|
+
if (verbose) {
|
|
104
|
+
console.error(error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Phase 4: Copy styles
|
|
108
|
+
console.log('Copying styles...');
|
|
109
|
+
// From dist/src/builder, go to project root, then to src/styles
|
|
110
|
+
const stylesDir = resolve(__dirname, '../../../src/styles');
|
|
111
|
+
const outputCssDir = join(assetsDir, 'css');
|
|
112
|
+
// Combine all CSS files into one bundle
|
|
113
|
+
const cssFiles = ['main.css', 'themes.css', 'chat.css'];
|
|
114
|
+
let bundledCss = '';
|
|
115
|
+
for (const cssFile of cssFiles) {
|
|
116
|
+
const cssPath = join(stylesDir, cssFile);
|
|
117
|
+
if (existsSync(cssPath)) {
|
|
118
|
+
bundledCss += readFileSync(cssPath, 'utf-8') + '\n\n';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
writeFileSync(join(outputCssDir, 'bundle.css'), bundledCss, 'utf-8');
|
|
122
|
+
if (verbose) {
|
|
123
|
+
console.log('Styles copied');
|
|
124
|
+
}
|
|
125
|
+
console.log('Site generation complete!');
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Load configuration from file or use defaults
|
|
129
|
+
*/
|
|
130
|
+
function loadConfig(configPath, inputDir) {
|
|
131
|
+
// Try explicit config path
|
|
132
|
+
if (configPath && existsSync(configPath)) {
|
|
133
|
+
return JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
134
|
+
}
|
|
135
|
+
// Try default config in input directory
|
|
136
|
+
const defaultConfigPath = join(inputDir, 'botdocs.config.json');
|
|
137
|
+
if (existsSync(defaultConfigPath)) {
|
|
138
|
+
return JSON.parse(readFileSync(defaultConfigPath, 'utf-8'));
|
|
139
|
+
}
|
|
140
|
+
// Use defaults
|
|
141
|
+
return { ...defaultConfig };
|
|
142
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ProcessedDocument } from '../types/document.js';
|
|
2
|
+
export declare class MarkdownProcessor {
|
|
3
|
+
private md;
|
|
4
|
+
private shikiInitialized;
|
|
5
|
+
constructor();
|
|
6
|
+
private escapeHtml;
|
|
7
|
+
private setupShiki;
|
|
8
|
+
/**
|
|
9
|
+
* Process a markdown file and extract front matter
|
|
10
|
+
*/
|
|
11
|
+
processFile(filePath: string, inputDir: string, content: string): Promise<ProcessedDocument>;
|
|
12
|
+
/**
|
|
13
|
+
* Extract title from markdown content (first h1) or filename
|
|
14
|
+
*/
|
|
15
|
+
private extractTitle;
|
|
16
|
+
/**
|
|
17
|
+
* Generate URL from relative file path
|
|
18
|
+
* e.g., "getting-started.md" -> "/getting-started.html"
|
|
19
|
+
* e.g., "api/overview.md" -> "/api/overview.html"
|
|
20
|
+
*/
|
|
21
|
+
private generateUrl;
|
|
22
|
+
/**
|
|
23
|
+
* Render markdown string to HTML
|
|
24
|
+
*/
|
|
25
|
+
render(markdown: string): string;
|
|
26
|
+
}
|