-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.js
More file actions
204 lines (183 loc) · 7.5 KB
/
app.js
File metadata and controls
204 lines (183 loc) · 7.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
// app.js
require('dotenv').config();
const express = require('express');
const marked = require('marked');
const fs = require('fs').promises;
const path = require('path');
const { generateText } = require("ai");
const { createOpenRouter } = require('@openrouter/ai-sdk-provider');
const app = express();
const port = 3000;
const openrouter = createOpenRouter({
apiKey: process.env.OPENROUTER_API_KEY,
});
app.use(express.static('public'));
app.use(express.json());
// --- Marked Configuration --- //
// Keep track of paragraph IDs per request
// We need a middleware or a way to reset this per request if we rely on a global counter.
// For simplicity now, let's reset it in the parse route.
// TODO: Improve paragraph ID generation to be stable across requests/renders.
let paragraphCounter = 0;
const renderer = new marked.Renderer();
renderer.paragraph = (token) => {
// Extract text content correctly, whether input is string or token object
const text = typeof token === 'object' && token.text ? token.text : token;
// Add a check to ensure we actually have a string before parsing
if (typeof text !== 'string') {
console.error("Unexpected non-string input to paragraph renderer:", token);
// Return an empty div or placeholder to avoid crashing
return '<div>[Error rendering paragraph]</div>\n';
}
const id = `p-${paragraphCounter++}`;
return `<div class="paragraph" id="${id}">${marked.parseInline(text)}</div>\n`;
};
const options = {
gfm: true,
breaks: true,
headerIds: true,
mangle: false,
renderer: renderer,
xhtml: true,
headerPrefix: 'section-',
pedantic: false,
smartLists: true,
smartypants: false, // Changed to false to avoid potential issues with quotes/dashes in AI content
};
marked.setOptions(options);
// --- Helper Function to Read Content File --- //
async function readMarkdownFile(filename) {
const markdownPath = path.join(__dirname, 'content', `${filename}.md`);
try {
let markdownContent = await fs.readFile(markdownPath, 'utf8');
markdownContent = markdownContent.trim();
// Handle old format with DEFAULT section
if (markdownContent.includes('<!-- DEFAULT START -->')) {
const defaultSection = markdownContent.match(/<!-- DEFAULT START -->([\s\S]*?)<!-- DEFAULT END -->/);
if (defaultSection) {
markdownContent = defaultSection[1].trim();
}
}
return markdownContent;
} catch (error) {
if (error.code === 'ENOENT') {
throw new Error('File not found'); // Throw specific error type?
} else {
throw error; // Re-throw other errors
}
}
}
// --- Routes --- //
// Root route: Serve index.html
app.get('/', async (req, res) => {
try {
const indexPath = path.join(__dirname, 'public', 'index.html');
res.sendFile(indexPath);
} catch (error) {
console.error('Error serving index.html:', error);
res.status(500).send('Server error');
}
});
// API: Get content (raw or parsed)
app.get('/api/content/:filename', async (req, res) => {
const filename = req.params.filename;
const raw = req.query.raw === 'true'; // Check for ?raw=true
try {
const markdownContent = await readMarkdownFile(filename);
if (raw) {
// Return raw markdown content
res.json({ rawMarkdown: markdownContent });
} else {
// DEPRECATED: This route should ideally only return raw markdown now.
// The client should use the /api/parse-markdown endpoint.
// Keeping the parsing logic here temporarily for backward compatibility if needed,
// but prefer switching the client fully.
console.warn("Deprecated usage of /api/content/:filename for parsed HTML. Use /api/parse-markdown instead.");
paragraphCounter = 0; // Reset counter for parsing
const htmlContent = await marked.parse(markdownContent);
const formattedHtml = htmlContent.replace(/<\/div>/g, '</div>\n'); // Basic formatting
res.json({
content: formattedHtml, // Original response field
title: filename
});
}
} catch (error) {
if (error.message === 'File not found') {
res.status(404).json({ error: 'File not found' });
} else {
console.error(`Error processing /api/content/${filename}:`, error);
res.status(500).json({ error: 'Server error processing content' });
}
}
});
// API: Parse Markdown to HTML
app.post('/api/parse-markdown', async (req, res) => {
const { markdown } = req.body;
if (typeof markdown !== 'string') {
return res.status(400).json({ error: 'Request body must contain a "markdown" string property.' });
}
try {
paragraphCounter = 0; // Reset counter before each parse
const html = await marked.parse(markdown);
// Optionally format further if needed
// const formattedHtml = html.replace(/<\/div>/g, '</div>\n');
res.json({ html: html });
} catch (error) { // Catch errors during parsing
console.error("Markdown parsing error:", error);
res.status(500).json({ error: "Failed to parse markdown" });
}
});
// API: List available pages
app.get('/api/pages', async (req, res) => {
try {
const contentDir = path.join(__dirname, 'content');
const files = await fs.readdir(contentDir);
const markdownFiles = files
.filter(file => file.endsWith('.md'))
.map(file => file.replace('.md', ''));
res.json(markdownFiles);
} catch (error) {
console.error('Error listing pages:', error);
res.status(500).json({ error: 'Failed to list pages' });
}
});
// Page route: Serve the main HTML template (content is loaded client-side)
// This route might become simpler or unnecessary if everything is loaded via client-side JS
app.get('/:filename', async (req, res, next) => {
// Check if filename looks like a file extension (e.g. .js, .css) to avoid catching asset requests
if (path.extname(req.params.filename)) {
return next(); // Pass to static middleware or 404
}
// Serve the main template for any other path, client-side routing handles the rest
try {
const templatePath = path.join(__dirname, 'views', 'template.html');
// Send the template without injecting content, client will handle it.
res.sendFile(templatePath);
} catch (error) {
console.error('Error serving template:', error);
res.status(500).send('Server error');
}
});
// API: Proxy requests to OpenRouter
app.post('/api/ask-openrouter', async (req, res) => {
const { prompt } = req.body; // Destructure prompt from body
if (!prompt) {
return res.status(400).json({ error: "Prompt is required" });
}
try {
const { text } = await generateText({
model: openrouter.chat('google/gemini-flash-1.5'), // Using recommended flash model
prompt: prompt,
});
res.json({ response: text });
} catch (error) {
console.error("Error calling OpenRouter:", error);
// Attempt to parse potential API error messages
const errorMessage = error.response?.data?.error?.message || error.message || "Failed to get response from OpenRouter";
res.status(500).json({ error: errorMessage });
}
});
// --- Server Start --- //
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});