Conversation
…, and security fixes
feat: add chat functionality with AI assistant and content indexing
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds an AI chat assistant feature (RAG over site content) to the DocuBase Astro template, including client UI, a chat page, an API endpoint, and indexing scripts to populate Upstash Vector.
Changes:
- Introduces homepage “Ask Anything” entrypoint and a reusable floating chat widget UI.
- Adds
/chat/[uid]conversation page and/api/chatSSE streaming endpoint backed by Upstash Vector + Google Gemini. - Adds content indexing scripts (
index-content,index-watch) plus new env vars and dependency updates.
Reviewed changes
Copilot reviewed 17 out of 19 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| template/src/types/index.ts | Re-exports new chat-related types. |
| template/src/types/constants.ts | Adds chat/indexing constants and roles. |
| template/src/types/components.ts | Removes Hero button props from the public type. |
| template/src/types/chat.ts | Adds chat + indexing-related TypeScript types. |
| template/src/pages/index.astro | Adds the AskAnything section to the homepage. |
| template/src/pages/chat/[uid].astro | Adds a dedicated chat page with streaming UI rendering. |
| template/src/pages/api/chat.ts | Adds RAG + Gemini SSE chat endpoint. |
| template/src/components/Hero.astro | Removes the CTA button from the hero component. |
| template/src/components/ChatWidget.astro | Adds a floating modal chat widget with streaming updates. |
| template/src/components/AskAnything.astro | Adds homepage form that navigates into chat. |
| scripts/index-watch.ts | Adds a dev watcher to upsert/delete vectors on content changes. |
| scripts/index-content.ts | Adds batch indexing with a local manifest to avoid re-uploading unchanged chunks. |
| README.md | Documents the AI chat assistant setup and behavior. |
| package.json | Adds dependencies + indexing scripts and bumps version to 1.2.0. |
| pnpm-lock.yaml | Locks new dependencies for Gemini, Upstash Vector, Vercel adapter, tsx, etc. |
| astro.config.mjs | Adds Vercel adapter configuration. |
| CHANGELOG.md | Notes the new AI chat assistant release. |
| .gitignore | Ignores generated index manifest and template dist/node_modules. |
| .env.example | Adds required env vars for Upstash Vector + Gemini. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| export default defineConfig({ | ||
| site: 'https://docubase-docs.vercel.app', | ||
| output: 'static', |
There was a problem hiding this comment.
output: 'static' is incompatible with the newly added non-prerendered routes (/api/chat, /chat/[uid]). With a static build those endpoints won't be deployed/served, even with the Vercel adapter configured. Switch to output: 'hybrid' (or server) to enable SSR for these routes, or remove the adapter + dynamic routes if static-only is intended.
| output: 'static', | |
| output: 'hybrid', |
| function buildSystemPrompt(contexts: ChatContext[]): string { | ||
| const contextText = contexts | ||
| .map((ctx, i) => `[${i + 1}] ${ctx.title} - ${ctx.section}\nURL: ${ctx.url}`) | ||
| .join('\n\n'); | ||
|
|
||
| return `You are a helpful documentation assistant for ${SITE_TITLE}, a documentation template built with Astro. | ||
| Your role is to answer questions based on the documentation content provided. | ||
|
|
||
| Guidelines: | ||
| - Be concise and helpful | ||
| - Reference specific documentation sections when relevant | ||
| - If the answer isn't in the provided context, say so honestly | ||
| - Include relevant URLs when they would be helpful | ||
| - Format responses using markdown for readability | ||
| - For code examples, use proper code blocks with language tags | ||
|
|
||
| Relevant documentation sections: | ||
| ${contextText} | ||
|
|
||
| Answer the user's question based on the above context. If the question cannot be answered from the provided context, let them know and suggest they check the documentation directly.`; |
There was a problem hiding this comment.
buildSystemPrompt() says it will answer based on provided documentation content, but the prompt only includes titles/sections/URLs and never includes ctx.content (the actual retrieved chunks). This effectively disables RAG and will cause hallucinations. Include the retrieved chunk text in the prompt (e.g., per context entry) or otherwise pass it to Gemini as grounded context.
| html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(_, linkText, url) { | ||
| var safeUrl = sanitizeUrl(url); | ||
| if (!safeUrl) return linkText; | ||
| return '<a href="' + safeUrl + '" class="text-blue-600 dark:text-blue-400 underline hover:no-underline" rel="noopener noreferrer">' + linkText + '</a>'; | ||
| }); |
There was a problem hiding this comment.
parseMarkdown() injects safeUrl directly into an href="..." attribute without escaping quotes. A crafted markdown link URL containing " can break out of the attribute and lead to XSS. Escape attribute-special characters (at least " and ') or build the anchor with DOM APIs (createElement('a') + setAttribute) after validating the protocol.
| // Links - with URL sanitization | ||
| html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(_, linkText, url) { | ||
| var safeUrl = sanitizeUrl(url); | ||
| if (!safeUrl) return linkText; | ||
| return '<a href="' + safeUrl + '" class="text-blue-600 dark:text-blue-400 underline hover:no-underline" rel="noopener noreferrer">' + linkText + '</a>'; | ||
| }); |
There was a problem hiding this comment.
parseMarkdown() injects safeUrl into href without escaping quotes, so a malicious URL containing " can break the attribute and execute script (XSS). Escaping "/' (or using DOM APIs to create/set the anchor element) is needed in addition to the protocol check.
| function init() { | ||
| if (window[INIT_FLAG]) return; | ||
| window[INIT_FLAG] = true; | ||
|
|
||
| var form = document.getElementById('ask-form'); | ||
| var input = document.getElementById('ask-input'); | ||
| var chips = document.querySelectorAll('.suggestion-chip'); | ||
|
|
||
| if (form && input) { | ||
| form.addEventListener('submit', function(e) { | ||
| e.preventDefault(); | ||
| navigateToChat(input.value); | ||
| }); | ||
|
|
||
| input.addEventListener('keydown', function(e) { | ||
| if (e.key === 'Enter' && !e.shiftKey) { | ||
| e.preventDefault(); | ||
| navigateToChat(input.value); | ||
| } | ||
| }); | ||
|
|
||
| input.addEventListener('input', function() { | ||
| this.style.height = 'auto'; | ||
| this.style.height = Math.min(this.scrollHeight, TEXTAREA_MAX_HEIGHT) + 'px'; | ||
| }); | ||
| } | ||
|
|
||
| chips.forEach(function(chip) { | ||
| chip.addEventListener('click', function() { | ||
| var text = chip.textContent.trim(); | ||
| navigateToChat(text); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| function reset() { | ||
| window[INIT_FLAG] = false; | ||
| init(); | ||
| } | ||
|
|
||
| init(); | ||
| document.addEventListener('astro:page-load', reset); | ||
| })(); |
There was a problem hiding this comment.
reset() clears the global init flag and then calls init(), which re-attaches event listeners on every astro:page-load. This will accumulate duplicate submit/keydown/input handlers over time. Prefer keeping the one-time INIT_FLAG guard and make the astro:page-load handler idempotent, or use an AbortController signal pattern (as in Search.astro) to remove and re-add listeners safely.
| function reset() { | ||
| window[INIT_FLAG] = false; | ||
| initChat(); | ||
| } | ||
|
|
||
| initChat(); | ||
| document.addEventListener('astro:page-load', reset); |
There was a problem hiding this comment.
reset() sets window[INIT_FLAG] = false and then calls initChat(), which re-attaches DOM/global listeners on every astro:page-load (duplicates will accumulate). The existing codebase pattern is to keep the init guard and make the page-load init idempotent (or use an AbortController signal pattern like Search.astro) so listeners are not duplicated.
| function reset() { | |
| window[INIT_FLAG] = false; | |
| initChat(); | |
| } | |
| initChat(); | |
| document.addEventListener('astro:page-load', reset); | |
| initChat(); | |
| document.addEventListener('astro:page-load', initChat); |
| function watchDirectory(dir: string): void { | ||
| if (!fs.existsSync(dir)) return; | ||
|
|
||
| fs.watch(dir, { recursive: true }, (eventType, filename) => { | ||
| if (!filename) return; | ||
| const filePath = path.join(dir, filename); | ||
| handleFileChange(filePath, eventType); | ||
| }); | ||
| } |
There was a problem hiding this comment.
fs.watch(..., { recursive: true }) is only fully supported on macOS/Windows; on Linux it is not recursive (and may throw depending on Node version). This makes the watcher unreliable in common CI/dev environments. Consider using a cross-platform watcher (e.g., chokidar) or explicitly document the platform limitation / implement manual directory recursion.
| 3. That's it! Content is automatically indexed on every build. | ||
|
|
||
| ### How It Works | ||
|
|
||
| - Content is automatically indexed when you run `pnpm run build` | ||
| - Users can ask questions via the chat widget or "Ask Anything" input | ||
| - The AI searches your docs and provides relevant answers with sources | ||
| - No manual indexing required - just write docs and deploy |
There was a problem hiding this comment.
The README claims content is "automatically indexed on every build" and specifically when running pnpm run build, but build does not run index-content (only build:with-index does). Update this section to match the actual scripts (or change the build script to include indexing) to avoid users deploying without a populated vector index.
| - 8cceec4: Add AI chat assistant with RAG-powered documentation search | ||
| - Add AskAnything component for homepage chat input | ||
| - Add ChatWidget floating chat button component | ||
| - Add dedicated chat page with conversation history | ||
| - Add chat API endpoint with Google Gemini integration | ||
| - Add Upstash Vector for semantic search | ||
| - Add index-content scripts for development and production | ||
| - Fix: XSS protection for AI-generated markdown links | ||
| - Fix: Proper SSE format for streaming responses | ||
| - Fix: Initialization guards for event listeners | ||
|
|
||
| - 8cceec4: AI chat assistant with RAG-powered documentation search | ||
|
|
There was a problem hiding this comment.
The 1.2.0 changelog entry repeats the same bullet twice (both keyed to 8cceec4). This looks like an accidental duplicate and makes the changelog noisy; remove the redundant line or replace it with distinct information.
| let result; | ||
| let lastError; | ||
|
|
||
| for (const modelName of CHAT.MODELS) { | ||
| try { | ||
| const model = genAI.getGenerativeModel({ | ||
| model: modelName, | ||
| systemInstruction: systemPrompt, | ||
| }); | ||
| const chat = model.startChat({ history: chatHistory }); | ||
| result = await chat.sendMessageStream(message); | ||
| console.log(`Using model: ${modelName}`); | ||
| break; | ||
| } catch (error) { | ||
| lastError = error; | ||
| const errorMessage = error instanceof Error ? error.message : ''; |
There was a problem hiding this comment.
lastError is assigned in the model fallback loop but never read. In strict TypeScript configurations (and many lint setups) this will fail the build due to an unused variable. Remove lastError or use it in the final 429 response / logging so failures are diagnosable.
No description provided.