mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
* feat: add docs chat prototype and related scripts
- Introduced a minimal documentation chatbot that builds a search index from markdown files and serves responses via an API.
- Added scripts for building the index and serving the chat API.
- Updated package.json with new commands for chat index building and serving.
- Created a new Vercel configuration file for deployment.
- Added a README for the docs chat prototype detailing usage and integration.
* feat: enhance docs chat with vector-based RAG pipeline
- Added vector index building and serving capabilities to the docs chat.
- Introduced new scripts for generating embeddings and serving the chat API using vector search.
- Updated package.json with new commands for vector index operations.
- Enhanced README with instructions for the new RAG pipeline and legacy keyword pipeline.
- Removed outdated Vercel configuration file.
* feat: enhance chat widget with markdown rendering and style updates
- Integrated dynamic loading of markdown rendering for chat responses.
- Implemented a fallback for markdown rendering to ensure consistent display.
- Updated CSS variables for improved theming and visual consistency.
- Enhanced chat bubble and input styles for better user experience.
- Added new styles for markdown content in chat bubbles, including code blocks and lists.
* feat: add copy buttons to chat widget for enhanced user interaction
- Implemented copy buttons for chat responses and code blocks in the chat widget.
- Updated CSS styles for improved visibility and interaction of copy buttons.
- Adjusted textarea height for better user experience.
- Enhanced functionality to allow users to easily copy text from chat bubbles and code snippets.
* feat: update chat widget styles for improved user experience
- Changed accent color for better visibility.
- Enhanced preformatted text styles for code blocks, including padding and word wrapping.
- Adjusted positioning and styles of copy buttons for chat responses and code snippets.
- Improved hover effects for copy buttons to enhance interactivity.
* feat: enhance chat widget styles for better responsiveness and scrollbar design
- Updated chat panel dimensions for improved adaptability on various screen sizes.
- Added custom scrollbar styles for better aesthetics and usability.
- Adjusted chat bubble styles for enhanced visibility and interaction.
- Improved layout for expanded chat widget on smaller screens.
* feat: refine chat widget code block styles and copy button functionality
- Adjusted padding and margin for preformatted text in chat responses for better visual consistency.
- Introduced a compact style for single-line code blocks to enhance layout.
- Updated copy button logic to skip short code blocks, improving user experience when copying code snippets.
* feat: add resize handle functionality to chat widget for adjustable panel width
- Implemented a draggable resize handle for the chat widget's sidebar, allowing users to adjust the panel width.
- Added CSS styles for the resize handle, including hover effects and responsive behavior.
- Integrated drag-to-resize logic to maintain user-set width across interactions.
- Ensured the panel resets to default width when closed, enhancing user experience.
* feat: implement rate limiting and error handling in chat API
- Added rate limiting functionality to the chat API, allowing a maximum number of requests per IP within a specified time window.
- Implemented error handling for rate limit exceeded responses, including appropriate headers and retry instructions.
- Enhanced error handling for other API errors, providing user-friendly messages for various failure scenarios.
- Updated README to include new environment variables for rate limiting configuration.
* feat: integrate Upstash Vector for enhanced document retrieval in chat API
- Implemented Upstash Vector as a cloud-based storage solution for document chunks, replacing the local LanceDB option.
- Added auto-detection of storage mode based on environment variables for seamless integration.
- Updated the chat API to utilize the new retrieval mechanism, enhancing response accuracy and performance.
- Enhanced README with setup instructions for Upstash and updated environment variable requirements.
- Introduced new scripts and configurations for managing the vector index and API interactions.
* feat: add create-markdown-preview.js for markdown rendering
- Introduced a new script for framework-agnostic HTML rendering of markdown content.
- The script includes various parsing functions to handle different markdown elements.
- Updated the chat widget to load the vendored version of @create-markdown/preview for improved markdown rendering.
* docs: update README for Upstash Vector index setup and environment variables
- Enhanced instructions for creating a Vector index in Upstash, including detailed settings and important notes.
- Clarified environment variable requirements for both Upstash and LanceDB modes.
- Improved formatting and organization of setup steps for better readability.
- Added health check and API endpoint details for clearer usage guidance.
* feat: add TRUST_PROXY environment variable for IP address handling
- Introduced the TRUST_PROXY variable to control the trust of X-Forwarded-For headers when behind a reverse proxy.
- Updated the README to document the new environment variable and its default value.
- Enhanced the getClientIP function to conditionally trust proxy headers based on the TRUST_PROXY setting.
* feat: add ALLOWED_ORIGINS environment variable for CORS configuration
- Introduced the ALLOWED_ORIGINS variable to specify allowed origins for CORS, enhancing security and flexibility.
- Updated the README to document the new environment variable and its usage.
- Refactored CORS handling in the server code to utilize the ALLOWED_ORIGINS setting for dynamic origin control.
* fix: ensure complete markdown rendering in chat widget
- Added logic to flush any remaining buffered bytes from the decoder, ensuring that all text is rendered correctly in the assistant bubble.
- Updated the assistant bubble's innerHTML to reflect the complete markdown content after streaming completes.
* feat: enhance DocsStore with improved vector handling and similarity conversion
- Added a constant for the distance metric used in vector searches, clarifying the assumption of L2 distance.
- Updated the createTable method to ensure all chunk properties are correctly mapped during table creation.
- Improved the similarity score calculation by providing a clear explanation of the conversion from L2 distance, ensuring accurate ranking of results.
* chore: fix code formatting
* Revert "chore: fix code formatting"
This reverts commit 6721f5b0b7.
* chore: format code for improved readability
- Reformatted code in serve.ts to enhance readability by adjusting indentation and line breaks.
- Ensured consistent style for function return types and object properties throughout the file.
* feat: Update API URL selection logic in chat widget
- Enhanced the API URL configuration to prioritize explicit settings, defaulting to localhost for development and using a production URL otherwise.
- Improved clarity in the code by adding comments to explain the logic behind the API URL selection.
* chore: Update documentation structure for improved organization
- Changed the path for the "Start Here" page to "start/index" for better clarity.
- Reformatted the "Web & Interfaces" and "Help" groups to use multi-line arrays for improved readability.
* feat: Enhance markdown preview integration and improve chat widget asset loading
- Wrapped the markdown preview functionality in an IIFE to expose a global API for easier integration.
- Updated the chat widget to load the markdown preview library dynamically, checking for existing instances to avoid duplicate loads.
- Adjusted asset paths in the chat widget to ensure correct loading based on the environment (local or production).
- Added CORS headers in the Vercel configuration for improved API accessibility.
* fix: Update chat API URL to include '/api' for correct endpoint access
- Modified the chat configuration and widget files to append '/api' to the API URL, ensuring proper endpoint usage in production and local environments.
* refactor: Simplify docs-chat configuration and remove unused scripts
- Removed outdated scripts and configurations related to the docs-chat feature, including build and serve scripts, as well as the associated package.json and README files.
- Streamlined the API URL configuration in the chat widget for better clarity and maintainability.
- Updated the package.json to remove unnecessary scripts related to the now-deleted functionality.
* refactor: Update documentation structure for improved clarity
- Changed the path for the "Start Here" page from "start/index" to "index" to enhance navigation and organization within the documentation.
* chore: Remove unused dependencies from package.json and pnpm-lock.yaml
- Deleted `@lancedb/lancedb`, `@upstash/vector`, and `openai` from both package.json and pnpm-lock.yaml to streamline the project and reduce bloat.
* chore: Clean up .gitignore by removing obsolete entries
- Deleted unused entries related to the docs-chat vector database from .gitignore to maintain a cleaner configuration.
* chore: Remove deprecated chat configuration and markdown preview script
- Deleted the `create-markdown-preview.js` script and the `docs-chat-config.js` file to eliminate unused assets and streamline the project.
- Updated the `docs-chat-widget.js` to directly reference the markdown library from a CDN, enhancing maintainability.
* chore: Update markdown rendering in chat widget to use marked library
- Replaced the deprecated `create-markdown-preview` library with the `marked` library for markdown rendering.
- Adjusted the script loading mechanism to fetch `marked` from a CDN, improving performance and maintainability.
- Enhanced the markdown rendering function to ensure security by disabling HTML pass-through and opening links in new tabs.
* Delete docs/start/index.md
668 lines
21 KiB
JavaScript
668 lines
21 KiB
JavaScript
(() => {
|
||
if (document.getElementById("docs-chat-root")) return;
|
||
|
||
// Determine if we're on the docs site or embedded elsewhere
|
||
const hostname = window.location.hostname;
|
||
const isDocsSite = hostname === "localhost" || hostname === "127.0.0.1" ||
|
||
hostname.includes("docs.openclaw") || hostname.endsWith(".mintlify.app");
|
||
const assetsBase = isDocsSite ? "" : "https://docs.openclaw.ai";
|
||
const apiBase = "https://claw-api.openknot.ai/api";
|
||
|
||
// Load marked for markdown rendering (via CDN)
|
||
let markedReady = false;
|
||
const loadMarkdownLib = () => {
|
||
if (window.marked) {
|
||
markedReady = true;
|
||
return;
|
||
}
|
||
const script = document.createElement("script");
|
||
script.src = "https://cdn.jsdelivr.net/npm/marked@15.0.6/marked.min.js";
|
||
script.onload = () => {
|
||
if (window.marked) {
|
||
markedReady = true;
|
||
}
|
||
};
|
||
script.onerror = () => console.warn("Failed to load marked library");
|
||
document.head.appendChild(script);
|
||
};
|
||
loadMarkdownLib();
|
||
|
||
// Markdown renderer with fallback before module loads
|
||
const renderMarkdown = (text) => {
|
||
if (markedReady && window.marked) {
|
||
// Configure marked for security: disable HTML pass-through
|
||
const html = window.marked.parse(text, { async: false, gfm: true, breaks: true });
|
||
// Open links in new tab by rewriting <a> tags
|
||
return html.replace(/<a href="/g, '<a target="_blank" rel="noopener" href="');
|
||
}
|
||
// Fallback: escape HTML and preserve newlines
|
||
return text
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/\n/g, "<br>");
|
||
};
|
||
|
||
const style = document.createElement("style");
|
||
style.textContent = `
|
||
#docs-chat-root { position: fixed; right: 20px; bottom: 20px; z-index: 9999; font-family: var(--font-body, system-ui, -apple-system, sans-serif); }
|
||
#docs-chat-root.docs-chat-expanded { right: 0; bottom: 0; top: 0; }
|
||
/* Thin scrollbar styling */
|
||
#docs-chat-root ::-webkit-scrollbar { width: 6px; height: 6px; }
|
||
#docs-chat-root ::-webkit-scrollbar-track { background: transparent; }
|
||
#docs-chat-root ::-webkit-scrollbar-thumb { background: var(--docs-chat-panel-border); border-radius: 3px; }
|
||
#docs-chat-root ::-webkit-scrollbar-thumb:hover { background: var(--docs-chat-muted); }
|
||
#docs-chat-root * { scrollbar-width: thin; scrollbar-color: var(--docs-chat-panel-border) transparent; }
|
||
:root {
|
||
--docs-chat-accent: var(--accent, #ff7d60);
|
||
--docs-chat-text: #1a1a1a;
|
||
--docs-chat-muted: #555;
|
||
--docs-chat-panel: rgba(255, 255, 255, 0.92);
|
||
--docs-chat-panel-border: rgba(0, 0, 0, 0.1);
|
||
--docs-chat-surface: rgba(250, 250, 250, 0.95);
|
||
--docs-chat-shadow: 0 18px 50px rgba(0,0,0,0.15);
|
||
--docs-chat-code-bg: rgba(0, 0, 0, 0.05);
|
||
--docs-chat-assistant-bg: #f5f5f5;
|
||
}
|
||
html[data-theme="dark"] {
|
||
--docs-chat-text: #e8e8e8;
|
||
--docs-chat-muted: #aaa;
|
||
--docs-chat-panel: rgba(28, 28, 30, 0.95);
|
||
--docs-chat-panel-border: rgba(255, 255, 255, 0.12);
|
||
--docs-chat-surface: rgba(38, 38, 40, 0.95);
|
||
--docs-chat-shadow: 0 18px 50px rgba(0,0,0,0.5);
|
||
--docs-chat-code-bg: rgba(255, 255, 255, 0.08);
|
||
--docs-chat-assistant-bg: #2a2a2c;
|
||
}
|
||
#docs-chat-button {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
background: linear-gradient(140deg, rgba(255,90,54,0.25), rgba(255,90,54,0.06));
|
||
color: var(--docs-chat-text);
|
||
border: 1px solid rgba(255,90,54,0.4);
|
||
border-radius: 999px;
|
||
padding: 10px 14px;
|
||
cursor: pointer;
|
||
box-shadow: 0 8px 30px rgba(255,90,54, 0.08);
|
||
backdrop-filter: blur(12px);
|
||
-webkit-backdrop-filter: blur(12px);
|
||
font-family: var(--font-pixel, var(--font-body, system-ui, sans-serif));
|
||
}
|
||
#docs-chat-button span { font-weight: 600; letter-spacing: 0.04em; font-size: 14px; }
|
||
.docs-chat-logo { width: 20px; height: 20px; }
|
||
#docs-chat-panel {
|
||
width: min(440px, calc(100vw - 40px));
|
||
height: min(696px, calc(100vh - 80px));
|
||
background: var(--docs-chat-panel);
|
||
color: var(--docs-chat-text);
|
||
border-radius: 16px;
|
||
border: 1px solid var(--docs-chat-panel-border);
|
||
box-shadow: var(--docs-chat-shadow);
|
||
display: none;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
backdrop-filter: blur(16px);
|
||
-webkit-backdrop-filter: blur(16px);
|
||
}
|
||
#docs-chat-root.docs-chat-expanded #docs-chat-panel {
|
||
width: min(512px, 100vw);
|
||
height: 100vh;
|
||
height: 100dvh;
|
||
border-radius: 18px 0 0 18px;
|
||
padding-top: env(safe-area-inset-top, 0);
|
||
padding-bottom: env(safe-area-inset-bottom, 0);
|
||
}
|
||
@media (max-width: 520px) {
|
||
#docs-chat-root.docs-chat-expanded #docs-chat-panel {
|
||
width: 100vw;
|
||
border-radius: 0;
|
||
}
|
||
#docs-chat-root.docs-chat-expanded { right: 0; left: 0; bottom: 0; top: 0; }
|
||
}
|
||
#docs-chat-header {
|
||
padding: 12px 14px;
|
||
font-weight: 600;
|
||
font-family: var(--font-pixel, var(--font-body, system-ui, sans-serif));
|
||
letter-spacing: 0.03em;
|
||
border-bottom: 1px solid var(--docs-chat-panel-border);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
#docs-chat-header-title { display: inline-flex; align-items: center; gap: 8px; }
|
||
#docs-chat-header-title span { color: var(--docs-chat-text); font-size: 15px; }
|
||
#docs-chat-header-actions { display: inline-flex; align-items: center; gap: 6px; }
|
||
.docs-chat-icon-button {
|
||
border: 1px solid var(--docs-chat-panel-border);
|
||
background: transparent;
|
||
color: inherit;
|
||
border-radius: 8px;
|
||
width: 30px;
|
||
height: 30px;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
line-height: 1;
|
||
}
|
||
#docs-chat-messages { flex: 1; padding: 12px 14px; overflow: auto; background: transparent; }
|
||
#docs-chat-input {
|
||
display: flex;
|
||
gap: 8px;
|
||
padding: 12px 14px;
|
||
border-top: 1px solid var(--docs-chat-panel-border);
|
||
background: var(--docs-chat-surface);
|
||
backdrop-filter: blur(10px);
|
||
-webkit-backdrop-filter: blur(10px);
|
||
}
|
||
#docs-chat-input textarea {
|
||
flex: 1;
|
||
resize: none;
|
||
border: 1px solid var(--docs-chat-panel-border);
|
||
border-radius: 10px;
|
||
padding: 9px 10px;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
font-family: inherit;
|
||
color: var(--docs-chat-text);
|
||
background: var(--docs-chat-surface);
|
||
min-height: 42px;
|
||
max-height: 120px;
|
||
overflow-y: auto;
|
||
}
|
||
#docs-chat-input textarea::placeholder { color: var(--docs-chat-muted); }
|
||
#docs-chat-send {
|
||
background: var(--docs-chat-accent);
|
||
color: #fff;
|
||
border: none;
|
||
border-radius: 10px;
|
||
padding: 8px 14px;
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
font-family: inherit;
|
||
font-size: 14px;
|
||
transition: opacity 0.15s ease;
|
||
}
|
||
#docs-chat-send:hover { opacity: 0.9; }
|
||
#docs-chat-send:active { opacity: 0.8; }
|
||
.docs-chat-bubble {
|
||
margin-bottom: 10px;
|
||
padding: 10px 14px;
|
||
border-radius: 12px;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
max-width: 92%;
|
||
}
|
||
.docs-chat-user {
|
||
background: rgba(255, 125, 96, 0.15);
|
||
color: var(--docs-chat-text);
|
||
border: 1px solid rgba(255, 125, 96, 0.3);
|
||
align-self: flex-end;
|
||
white-space: pre-wrap;
|
||
margin-left: auto;
|
||
}
|
||
html[data-theme="dark"] .docs-chat-user {
|
||
background: rgba(255, 125, 96, 0.18);
|
||
border-color: rgba(255, 125, 96, 0.35);
|
||
}
|
||
.docs-chat-assistant {
|
||
background: var(--docs-chat-assistant-bg);
|
||
color: var(--docs-chat-text);
|
||
border: 1px solid var(--docs-chat-panel-border);
|
||
}
|
||
/* Markdown content styling for chat bubbles */
|
||
.docs-chat-assistant p { margin: 0 0 10px 0; }
|
||
.docs-chat-assistant p:last-child { margin-bottom: 0; }
|
||
.docs-chat-assistant code {
|
||
background: var(--docs-chat-code-bg);
|
||
padding: 2px 6px;
|
||
border-radius: 5px;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||
font-size: 0.9em;
|
||
}
|
||
.docs-chat-assistant pre {
|
||
background: var(--docs-chat-code-bg);
|
||
padding: 10px 12px;
|
||
border-radius: 8px;
|
||
overflow-x: auto;
|
||
margin: 6px 0;
|
||
font-size: 0.9em;
|
||
max-width: 100%;
|
||
white-space: pre;
|
||
word-wrap: normal;
|
||
}
|
||
.docs-chat-assistant pre::-webkit-scrollbar-thumb { background: transparent; }
|
||
.docs-chat-assistant pre:hover::-webkit-scrollbar-thumb { background: var(--docs-chat-panel-border); }
|
||
@media (hover: none) {
|
||
.docs-chat-assistant pre { -webkit-overflow-scrolling: touch; }
|
||
.docs-chat-assistant pre::-webkit-scrollbar-thumb { background: var(--docs-chat-panel-border); }
|
||
}
|
||
.docs-chat-assistant pre code {
|
||
background: transparent;
|
||
padding: 0;
|
||
font-size: inherit;
|
||
white-space: pre;
|
||
word-wrap: normal;
|
||
display: block;
|
||
}
|
||
/* Compact single-line code blocks */
|
||
.docs-chat-assistant pre.compact {
|
||
margin: 4px 0;
|
||
padding: 6px 10px;
|
||
}
|
||
/* Longer code blocks with copy button need extra top padding */
|
||
.docs-chat-assistant pre:not(.compact) {
|
||
padding-top: 28px;
|
||
}
|
||
.docs-chat-assistant a {
|
||
color: var(--docs-chat-accent);
|
||
text-decoration: underline;
|
||
text-underline-offset: 2px;
|
||
}
|
||
.docs-chat-assistant a:hover { opacity: 0.8; }
|
||
.docs-chat-assistant ul, .docs-chat-assistant ol {
|
||
margin: 8px 0;
|
||
padding-left: 18px;
|
||
list-style: none;
|
||
}
|
||
.docs-chat-assistant li {
|
||
margin: 4px 0;
|
||
position: relative;
|
||
padding-left: 14px;
|
||
}
|
||
.docs-chat-assistant li::before {
|
||
content: "•";
|
||
position: absolute;
|
||
left: 0;
|
||
color: var(--docs-chat-muted);
|
||
}
|
||
.docs-chat-assistant strong { font-weight: 600; }
|
||
.docs-chat-assistant em { font-style: italic; }
|
||
.docs-chat-assistant h1, .docs-chat-assistant h2, .docs-chat-assistant h3 {
|
||
font-weight: 600;
|
||
margin: 12px 0 6px 0;
|
||
line-height: 1.3;
|
||
}
|
||
.docs-chat-assistant h1 { font-size: 1.2em; }
|
||
.docs-chat-assistant h2 { font-size: 1.1em; }
|
||
.docs-chat-assistant h3 { font-size: 1.05em; }
|
||
.docs-chat-assistant blockquote {
|
||
border-left: 3px solid var(--docs-chat-accent);
|
||
margin: 10px 0;
|
||
padding: 4px 12px;
|
||
color: var(--docs-chat-muted);
|
||
background: var(--docs-chat-code-bg);
|
||
border-radius: 0 6px 6px 0;
|
||
}
|
||
.docs-chat-assistant hr {
|
||
border: none;
|
||
height: 1px;
|
||
background: var(--docs-chat-panel-border);
|
||
margin: 12px 0;
|
||
}
|
||
/* Copy buttons */
|
||
.docs-chat-assistant { position: relative; padding-top: 28px; }
|
||
.docs-chat-copy-response {
|
||
position: absolute;
|
||
top: 8px;
|
||
right: 8px;
|
||
background: var(--docs-chat-surface);
|
||
border: 1px solid var(--docs-chat-panel-border);
|
||
border-radius: 5px;
|
||
padding: 4px 8px;
|
||
font-size: 11px;
|
||
cursor: pointer;
|
||
color: var(--docs-chat-muted);
|
||
transition: color 0.15s ease, background 0.15s ease;
|
||
}
|
||
.docs-chat-copy-response:hover {
|
||
color: var(--docs-chat-text);
|
||
background: var(--docs-chat-code-bg);
|
||
}
|
||
.docs-chat-assistant pre {
|
||
position: relative;
|
||
}
|
||
.docs-chat-copy-code {
|
||
position: absolute;
|
||
top: 8px;
|
||
right: 8px;
|
||
background: var(--docs-chat-surface);
|
||
border: 1px solid var(--docs-chat-panel-border);
|
||
border-radius: 4px;
|
||
padding: 3px 7px;
|
||
font-size: 10px;
|
||
cursor: pointer;
|
||
color: var(--docs-chat-muted);
|
||
transition: color 0.15s ease, background 0.15s ease;
|
||
z-index: 1;
|
||
}
|
||
.docs-chat-copy-code:hover {
|
||
color: var(--docs-chat-text);
|
||
background: var(--docs-chat-code-bg);
|
||
}
|
||
/* Resize handle - left edge of expanded panel */
|
||
#docs-chat-resize-handle {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 6px;
|
||
cursor: ew-resize;
|
||
z-index: 10;
|
||
display: none;
|
||
}
|
||
#docs-chat-root.docs-chat-expanded #docs-chat-resize-handle { display: block; }
|
||
#docs-chat-resize-handle::after {
|
||
content: "";
|
||
position: absolute;
|
||
left: 1px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 4px;
|
||
height: 40px;
|
||
border-radius: 2px;
|
||
background: var(--docs-chat-panel-border);
|
||
opacity: 0;
|
||
transition: opacity 0.15s ease, background 0.15s ease;
|
||
}
|
||
#docs-chat-resize-handle:hover::after,
|
||
#docs-chat-resize-handle.docs-chat-dragging::after {
|
||
opacity: 1;
|
||
background: var(--docs-chat-accent);
|
||
}
|
||
@media (max-width: 520px) {
|
||
#docs-chat-resize-handle { display: none !important; }
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
|
||
const root = document.createElement("div");
|
||
root.id = "docs-chat-root";
|
||
|
||
const button = document.createElement("button");
|
||
button.id = "docs-chat-button";
|
||
button.type = "button";
|
||
button.innerHTML =
|
||
`<img class="docs-chat-logo" src="${assetsBase}/assets/pixel-lobster.svg" alt="OpenClaw">` +
|
||
`<span>Ask Molty</span>`;
|
||
|
||
const panel = document.createElement("div");
|
||
panel.id = "docs-chat-panel";
|
||
panel.style.display = "none";
|
||
|
||
// Resize handle for expandable sidebar width (desktop only)
|
||
const resizeHandle = document.createElement("div");
|
||
resizeHandle.id = "docs-chat-resize-handle";
|
||
|
||
const header = document.createElement("div");
|
||
header.id = "docs-chat-header";
|
||
header.innerHTML =
|
||
`<div id="docs-chat-header-title">` +
|
||
`<img class="docs-chat-logo" src="${assetsBase}/assets/pixel-lobster.svg" alt="OpenClaw">` +
|
||
`<span>OpenClaw Docs</span>` +
|
||
`</div>` +
|
||
`<div id="docs-chat-header-actions"></div>`;
|
||
const headerActions = header.querySelector("#docs-chat-header-actions");
|
||
const expand = document.createElement("button");
|
||
expand.type = "button";
|
||
expand.className = "docs-chat-icon-button";
|
||
expand.setAttribute("aria-label", "Expand");
|
||
expand.textContent = "⤢";
|
||
const clear = document.createElement("button");
|
||
clear.type = "button";
|
||
clear.className = "docs-chat-icon-button";
|
||
clear.setAttribute("aria-label", "Clear chat");
|
||
clear.textContent = "⌫";
|
||
const close = document.createElement("button");
|
||
close.type = "button";
|
||
close.className = "docs-chat-icon-button";
|
||
close.setAttribute("aria-label", "Close");
|
||
close.textContent = "×";
|
||
headerActions.appendChild(expand);
|
||
headerActions.appendChild(clear);
|
||
headerActions.appendChild(close);
|
||
|
||
const messages = document.createElement("div");
|
||
messages.id = "docs-chat-messages";
|
||
|
||
const inputWrap = document.createElement("div");
|
||
inputWrap.id = "docs-chat-input";
|
||
const textarea = document.createElement("textarea");
|
||
textarea.rows = 1;
|
||
textarea.placeholder = "Ask about OpenClaw Docs...";
|
||
|
||
// Auto-expand textarea as user types (up to max-height set in CSS)
|
||
const autoExpand = () => {
|
||
textarea.style.height = "auto";
|
||
textarea.style.height = Math.min(textarea.scrollHeight, 224) + "px";
|
||
};
|
||
textarea.addEventListener("input", autoExpand);
|
||
|
||
const send = document.createElement("button");
|
||
send.id = "docs-chat-send";
|
||
send.type = "button";
|
||
send.textContent = "Send";
|
||
|
||
inputWrap.appendChild(textarea);
|
||
inputWrap.appendChild(send);
|
||
|
||
panel.appendChild(resizeHandle);
|
||
panel.appendChild(header);
|
||
panel.appendChild(messages);
|
||
panel.appendChild(inputWrap);
|
||
|
||
root.appendChild(button);
|
||
root.appendChild(panel);
|
||
document.body.appendChild(root);
|
||
|
||
// Add copy buttons to assistant bubble
|
||
const addCopyButtons = (bubble, rawText) => {
|
||
// Add copy response button
|
||
const copyResponse = document.createElement("button");
|
||
copyResponse.className = "docs-chat-copy-response";
|
||
copyResponse.textContent = "Copy";
|
||
copyResponse.type = "button";
|
||
copyResponse.addEventListener("click", async () => {
|
||
try {
|
||
await navigator.clipboard.writeText(rawText);
|
||
copyResponse.textContent = "Copied!";
|
||
setTimeout(() => (copyResponse.textContent = "Copy"), 1500);
|
||
} catch (e) {
|
||
copyResponse.textContent = "Failed";
|
||
}
|
||
});
|
||
bubble.appendChild(copyResponse);
|
||
|
||
// Add copy buttons to code blocks (skip short/single-line blocks)
|
||
bubble.querySelectorAll("pre").forEach((pre) => {
|
||
const code = pre.querySelector("code") || pre;
|
||
const text = code.textContent || "";
|
||
const lineCount = text.split("\n").length;
|
||
const isShort = lineCount <= 2 && text.length < 100;
|
||
|
||
if (isShort) {
|
||
pre.classList.add("compact");
|
||
return; // Skip copy button for compact blocks
|
||
}
|
||
|
||
const copyCode = document.createElement("button");
|
||
copyCode.className = "docs-chat-copy-code";
|
||
copyCode.textContent = "Copy";
|
||
copyCode.type = "button";
|
||
copyCode.addEventListener("click", async (e) => {
|
||
e.stopPropagation();
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
copyCode.textContent = "Copied!";
|
||
setTimeout(() => (copyCode.textContent = "Copy"), 1500);
|
||
} catch (err) {
|
||
copyCode.textContent = "Failed";
|
||
}
|
||
});
|
||
pre.appendChild(copyCode);
|
||
});
|
||
};
|
||
|
||
const addBubble = (text, role, isMarkdown = false) => {
|
||
const bubble = document.createElement("div");
|
||
bubble.className =
|
||
"docs-chat-bubble " +
|
||
(role === "user" ? "docs-chat-user" : "docs-chat-assistant");
|
||
if (isMarkdown && role === "assistant") {
|
||
bubble.innerHTML = renderMarkdown(text);
|
||
} else {
|
||
bubble.textContent = text;
|
||
}
|
||
messages.appendChild(bubble);
|
||
messages.scrollTop = messages.scrollHeight;
|
||
return bubble;
|
||
};
|
||
|
||
let isExpanded = false;
|
||
let customWidth = null; // User-set width via drag
|
||
const MIN_WIDTH = 320;
|
||
const MAX_WIDTH = 800;
|
||
|
||
// Drag-to-resize logic
|
||
let isDragging = false;
|
||
let startX, startWidth;
|
||
|
||
resizeHandle.addEventListener("mousedown", (e) => {
|
||
if (!isExpanded) return;
|
||
isDragging = true;
|
||
startX = e.clientX;
|
||
startWidth = panel.offsetWidth;
|
||
resizeHandle.classList.add("docs-chat-dragging");
|
||
document.body.style.cursor = "ew-resize";
|
||
document.body.style.userSelect = "none";
|
||
e.preventDefault();
|
||
});
|
||
|
||
document.addEventListener("mousemove", (e) => {
|
||
if (!isDragging) return;
|
||
// Panel is on right, so dragging left increases width
|
||
const delta = startX - e.clientX;
|
||
const newWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidth + delta));
|
||
customWidth = newWidth;
|
||
panel.style.width = newWidth + "px";
|
||
});
|
||
|
||
document.addEventListener("mouseup", () => {
|
||
if (!isDragging) return;
|
||
isDragging = false;
|
||
resizeHandle.classList.remove("docs-chat-dragging");
|
||
document.body.style.cursor = "";
|
||
document.body.style.userSelect = "";
|
||
});
|
||
|
||
const setOpen = (isOpen) => {
|
||
panel.style.display = isOpen ? "flex" : "none";
|
||
button.style.display = isOpen ? "none" : "inline-flex";
|
||
root.classList.toggle("docs-chat-expanded", isOpen && isExpanded);
|
||
if (!isOpen) {
|
||
panel.style.width = ""; // Reset to CSS default when closed
|
||
} else if (isExpanded && customWidth) {
|
||
panel.style.width = customWidth + "px";
|
||
}
|
||
if (isOpen) textarea.focus();
|
||
};
|
||
|
||
const setExpanded = (next) => {
|
||
isExpanded = next;
|
||
expand.textContent = isExpanded ? "⤡" : "⤢";
|
||
expand.setAttribute("aria-label", isExpanded ? "Collapse" : "Expand");
|
||
if (panel.style.display !== "none") {
|
||
root.classList.toggle("docs-chat-expanded", isExpanded);
|
||
if (isExpanded && customWidth) {
|
||
panel.style.width = customWidth + "px";
|
||
} else if (!isExpanded) {
|
||
panel.style.width = ""; // Reset to CSS default
|
||
}
|
||
}
|
||
};
|
||
|
||
button.addEventListener("click", () => setOpen(true));
|
||
expand.addEventListener("click", () => setExpanded(!isExpanded));
|
||
clear.addEventListener("click", () => {
|
||
messages.innerHTML = "";
|
||
});
|
||
close.addEventListener("click", () => {
|
||
setOpen(false);
|
||
root.classList.remove("docs-chat-expanded");
|
||
});
|
||
|
||
const sendMessage = async () => {
|
||
const text = textarea.value.trim();
|
||
if (!text) return;
|
||
textarea.value = "";
|
||
textarea.style.height = "auto"; // Reset height after sending
|
||
addBubble(text, "user");
|
||
const assistantBubble = addBubble("...", "assistant");
|
||
assistantBubble.innerHTML = "";
|
||
|
||
let fullText = "";
|
||
try {
|
||
const response = await fetch(`${apiBase}/chat`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ message: text }),
|
||
});
|
||
|
||
// Handle rate limiting
|
||
if (response.status === 429) {
|
||
const retryAfter = response.headers.get("Retry-After") || "60";
|
||
fullText = `You're asking questions too quickly. Please wait ${retryAfter} seconds before trying again.`;
|
||
assistantBubble.innerHTML = renderMarkdown(fullText);
|
||
addCopyButtons(assistantBubble, fullText);
|
||
return;
|
||
}
|
||
|
||
// Handle other errors
|
||
if (!response.ok) {
|
||
try {
|
||
const errorData = await response.json();
|
||
fullText = errorData.error || "Something went wrong. Please try again.";
|
||
} catch {
|
||
fullText = "Something went wrong. Please try again.";
|
||
}
|
||
assistantBubble.innerHTML = renderMarkdown(fullText);
|
||
addCopyButtons(assistantBubble, fullText);
|
||
return;
|
||
}
|
||
|
||
if (!response.body) {
|
||
fullText = await response.text();
|
||
assistantBubble.innerHTML = renderMarkdown(fullText);
|
||
addCopyButtons(assistantBubble, fullText);
|
||
return;
|
||
}
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
while (true) {
|
||
const { value, done } = await reader.read();
|
||
if (done) break;
|
||
fullText += decoder.decode(value, { stream: true });
|
||
// Re-render markdown on each chunk for live preview
|
||
assistantBubble.innerHTML = renderMarkdown(fullText);
|
||
messages.scrollTop = messages.scrollHeight;
|
||
}
|
||
// Flush any remaining buffered bytes (partial UTF-8 sequences)
|
||
fullText += decoder.decode();
|
||
assistantBubble.innerHTML = renderMarkdown(fullText);
|
||
// Add copy buttons after streaming completes
|
||
addCopyButtons(assistantBubble, fullText);
|
||
} catch (err) {
|
||
fullText = "Failed to reach docs chat API.";
|
||
assistantBubble.innerHTML = renderMarkdown(fullText);
|
||
addCopyButtons(assistantBubble, fullText);
|
||
}
|
||
};
|
||
|
||
send.addEventListener("click", sendMessage);
|
||
textarea.addEventListener("keydown", (event) => {
|
||
if (event.key === "Enter" && !event.shiftKey) {
|
||
event.preventDefault();
|
||
sendMessage();
|
||
}
|
||
});
|
||
})();
|