Skip to content

Instantly share code, notes, and snippets.

@jalehman
Created January 31, 2026 23:42
Show Gist options
  • Select an option

  • Save jalehman/5d36ae15a693380dc2b9bed97a8baac4 to your computer and use it in GitHub Desktop.

Select an option

Save jalehman/5d36ae15a693380dc2b9bed97a8baac4 to your computer and use it in GitHub Desktop.
OpenClaw Annotations v2 - Mobile Support
/**
* OpenClaw Preview Annotations
*
* Inject into any HTML preview to enable inline feedback.
* Pure client-side - no backend required.
* Works on desktop and mobile (iOS Safari, etc.)
*
* Usage: Include this script at the end of your HTML body.
*/
(function() {
'use strict';
// Generate or retrieve preview ID
const PREVIEW_ID = new URLSearchParams(window.location.search).get('preview_id')
|| window.location.pathname.split('/').pop().replace('.html', '')
|| 'preview_' + Date.now();
const STORAGE_KEY = `openclaw_annotations_${PREVIEW_ID}`;
// State
let annotations = [];
let annotationCounter = 0;
let currentSelection = null;
// Load existing annotations
function loadAnnotations() {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
annotations = JSON.parse(stored);
annotationCounter = annotations.length;
}
} catch (e) {
console.warn('Failed to load annotations:', e);
}
}
// Save annotations
function saveAnnotations() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(annotations));
} catch (e) {
console.warn('Failed to save annotations:', e);
}
}
// Get text context around selection
function getTextContext(text, fullText, chars = 80) {
const startIdx = fullText.indexOf(text);
if (startIdx === -1) return text;
const contextStart = Math.max(0, startIdx - chars);
const contextEnd = Math.min(fullText.length, startIdx + text.length + chars);
let context = '';
if (contextStart > 0) context += '...';
context += fullText.slice(contextStart, contextEnd);
if (contextEnd < fullText.length) context += '...';
return context;
}
// Inject styles
function injectStyles() {
const style = document.createElement('style');
style.textContent = `
/* Floating annotate button - appears when text is selected */
.openclaw-annotate-btn {
position: fixed;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: white;
border: none;
padding: 12px 20px;
border-radius: 25px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.4);
z-index: 10000;
display: none;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
transition: transform 0.15s, box-shadow 0.15s;
-webkit-tap-highlight-color: transparent;
}
.openclaw-annotate-btn:active {
transform: scale(0.95);
}
.openclaw-annotate-btn.visible {
display: block;
animation: openclaw-pop 0.2s ease;
}
@keyframes openclaw-pop {
from { opacity: 0; transform: scale(0.8); }
to { opacity: 1; transform: scale(1); }
}
/* Popover */
.openclaw-popover {
position: fixed;
background: #1a1a2e;
border: 1px solid #444;
border-radius: 16px;
padding: 16px;
box-shadow: 0 10px 50px rgba(0,0,0,0.6);
z-index: 10001;
width: calc(100vw - 32px);
max-width: 380px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
color: #e0e0e0;
display: none;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.openclaw-popover.visible {
display: block;
animation: openclaw-slide 0.25s ease;
}
@keyframes openclaw-slide {
from { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
.openclaw-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 10000;
display: none;
}
.openclaw-overlay.visible {
display: block;
}
.openclaw-popover-header {
display: flex;
gap: 8px;
margin-bottom: 12px;
justify-content: center;
}
.openclaw-reaction-btn {
padding: 10px 16px;
border: 2px solid #444;
border-radius: 10px;
background: #252525;
cursor: pointer;
font-size: 20px;
transition: all 0.15s;
-webkit-tap-highlight-color: transparent;
}
.openclaw-reaction-btn:active {
transform: scale(0.9);
}
.openclaw-reaction-btn.selected {
border-color: #6366f1;
background: rgba(99, 102, 241, 0.2);
transform: scale(1.1);
}
.openclaw-selected-text {
background: #252525;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
font-size: 14px;
color: #aaa;
max-height: 80px;
overflow: hidden;
border-left: 4px solid #6366f1;
line-height: 1.5;
}
.openclaw-textarea {
width: 100%;
min-height: 100px;
padding: 12px;
border: 2px solid #444;
border-radius: 10px;
background: #252525;
color: #e0e0e0;
font-size: 16px;
resize: none;
font-family: inherit;
-webkit-appearance: none;
}
.openclaw-textarea:focus {
outline: none;
border-color: #6366f1;
}
.openclaw-textarea::placeholder {
color: #666;
}
.openclaw-popover-actions {
display: flex;
gap: 10px;
margin-top: 12px;
}
.openclaw-btn {
flex: 1;
padding: 14px 20px;
border-radius: 10px;
border: none;
cursor: pointer;
font-size: 15px;
font-weight: 600;
transition: all 0.15s;
-webkit-tap-highlight-color: transparent;
}
.openclaw-btn:active {
transform: scale(0.95);
}
.openclaw-btn-primary {
background: #6366f1;
color: white;
}
.openclaw-btn-secondary {
background: #333;
color: #ccc;
}
/* Floating toolbar */
.openclaw-toolbar {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 10px;
z-index: 9999;
}
.openclaw-toolbar-btn {
padding: 14px 24px;
border-radius: 30px;
border: none;
cursor: pointer;
font-size: 15px;
font-weight: 600;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
-webkit-tap-highlight-color: transparent;
}
.openclaw-toolbar-btn:active {
transform: scale(0.95);
}
.openclaw-send-btn {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: white;
}
.openclaw-clear-btn {
background: #333;
color: #aaa;
}
.openclaw-count {
background: rgba(255,255,255,0.2);
padding: 3px 10px;
border-radius: 12px;
font-size: 13px;
}
/* Export modal */
.openclaw-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.8);
z-index: 10002;
display: none;
align-items: center;
justify-content: center;
padding: 16px;
}
.openclaw-modal.visible {
display: flex;
}
.openclaw-modal-content {
background: #1a1a2e;
border-radius: 16px;
padding: 20px;
width: 100%;
max-width: 500px;
max-height: 80vh;
overflow: auto;
border: 1px solid #333;
}
.openclaw-modal h2 {
margin: 0 0 16px;
color: #fff;
font-size: 1.2rem;
}
.openclaw-export-preview {
background: #0d0d0d;
border-radius: 10px;
padding: 16px;
font-family: 'SF Mono', Monaco, monospace;
font-size: 12px;
white-space: pre-wrap;
max-height: 250px;
overflow: auto;
margin-bottom: 16px;
border: 1px solid #333;
line-height: 1.5;
}
.openclaw-modal-actions {
display: flex;
gap: 10px;
}
/* Toast */
.openclaw-toast {
position: fixed;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
background: #1a1a2e;
border: 1px solid #333;
padding: 14px 24px;
border-radius: 10px;
color: #e0e0e0;
font-size: 15px;
z-index: 10003;
animation: openclaw-pop 0.3s ease;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
/* Instructions hint */
.openclaw-hint {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(99, 102, 241, 0.9);
color: white;
padding: 10px 20px;
border-radius: 20px;
font-size: 14px;
z-index: 9998;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.openclaw-hint.visible {
opacity: 1;
}
`;
document.head.appendChild(style);
}
// Show toast notification
function showToast(message, duration = 2000) {
const existing = document.querySelector('.openclaw-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.className = 'openclaw-toast';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), duration);
}
// Generate export text
function generateExport() {
if (annotations.length === 0) return 'No annotations yet.';
const lines = [
`## Preview Feedback`,
`**Preview:** ${document.title || PREVIEW_ID}`,
`**Date:** ${new Date().toLocaleString()}`,
`**Annotations:** ${annotations.length}`,
'',
'---',
''
];
annotations.forEach((ann, idx) => {
const reaction = ann.reaction ? `${ann.reaction} ` : '';
lines.push(`### ${reaction}${idx + 1}. ${ann.comment ? '' : '(no comment)'}`);
lines.push('');
lines.push(`> ${ann.selectedText}`);
lines.push('');
if (ann.comment) {
lines.push(ann.comment);
lines.push('');
}
lines.push('---');
lines.push('');
});
return lines.join('\n');
}
// Escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Initialize
function init() {
injectStyles();
loadAnnotations();
// Create elements
const annotateBtn = document.createElement('button');
annotateBtn.className = 'openclaw-annotate-btn';
annotateBtn.textContent = '✏️ Annotate';
document.body.appendChild(annotateBtn);
const overlay = document.createElement('div');
overlay.className = 'openclaw-overlay';
document.body.appendChild(overlay);
const popover = document.createElement('div');
popover.className = 'openclaw-popover';
popover.innerHTML = `
<div class="openclaw-selected-text"></div>
<div class="openclaw-popover-header">
<button class="openclaw-reaction-btn" data-reaction="👍">👍</button>
<button class="openclaw-reaction-btn" data-reaction="❌">❌</button>
<button class="openclaw-reaction-btn" data-reaction="❓">❓</button>
</div>
<textarea class="openclaw-textarea" placeholder="Add a comment, question, or suggestion..."></textarea>
<div class="openclaw-popover-actions">
<button class="openclaw-btn openclaw-btn-secondary openclaw-cancel-btn">Cancel</button>
<button class="openclaw-btn openclaw-btn-primary openclaw-save-btn">Save</button>
</div>
`;
document.body.appendChild(popover);
const toolbar = document.createElement('div');
toolbar.className = 'openclaw-toolbar';
toolbar.innerHTML = `
<button class="openclaw-toolbar-btn openclaw-clear-btn">Clear</button>
<button class="openclaw-toolbar-btn openclaw-send-btn">
📤 Send to OpenClaw
<span class="openclaw-count">${annotations.length}</span>
</button>
`;
document.body.appendChild(toolbar);
const hint = document.createElement('div');
hint.className = 'openclaw-hint';
hint.textContent = 'Select text to annotate';
document.body.appendChild(hint);
// Show hint briefly on load
setTimeout(() => hint.classList.add('visible'), 500);
setTimeout(() => hint.classList.remove('visible'), 3000);
let currentAnnotation = null;
let hideButtonTimeout = null;
// Update toolbar count
function updateCount() {
toolbar.querySelector('.openclaw-count').textContent = annotations.length;
}
// Check for text selection
function checkSelection() {
const selection = window.getSelection();
const text = selection.toString().trim();
if (text.length > 2 && text.length < 1000) {
currentSelection = {
text: text,
context: text
};
// Position button near bottom of viewport (mobile-friendly)
annotateBtn.style.bottom = '90px';
annotateBtn.style.left = '50%';
annotateBtn.style.transform = 'translateX(-50%)';
annotateBtn.classList.add('visible');
// Clear any pending hide
if (hideButtonTimeout) {
clearTimeout(hideButtonTimeout);
hideButtonTimeout = null;
}
} else {
// Delay hiding to allow tap on button
if (!hideButtonTimeout) {
hideButtonTimeout = setTimeout(() => {
annotateBtn.classList.remove('visible');
hideButtonTimeout = null;
}, 300);
}
}
}
// Listen for selection changes (works on mobile!)
document.addEventListener('selectionchange', checkSelection);
// Annotate button click
annotateBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
if (!currentSelection) return;
currentAnnotation = {
id: ++annotationCounter,
selectedText: currentSelection.text.length > 200
? currentSelection.text.slice(0, 200) + '...'
: currentSelection.text,
fullText: currentSelection.text,
reaction: null,
comment: '',
createdAt: new Date().toISOString()
};
// Populate popover
popover.querySelector('.openclaw-selected-text').textContent = currentAnnotation.selectedText;
popover.querySelector('.openclaw-textarea').value = '';
popover.querySelectorAll('.openclaw-reaction-btn').forEach(b => b.classList.remove('selected'));
// Show popover
overlay.classList.add('visible');
popover.classList.add('visible');
annotateBtn.classList.remove('visible');
// Clear selection
window.getSelection().removeAllRanges();
});
// Reaction buttons
popover.querySelectorAll('.openclaw-reaction-btn').forEach(btn => {
btn.addEventListener('click', () => {
const reaction = btn.dataset.reaction;
popover.querySelectorAll('.openclaw-reaction-btn').forEach(b => b.classList.remove('selected'));
if (currentAnnotation.reaction === reaction) {
currentAnnotation.reaction = null;
} else {
btn.classList.add('selected');
currentAnnotation.reaction = reaction;
}
});
});
// Save button
popover.querySelector('.openclaw-save-btn').addEventListener('click', () => {
currentAnnotation.comment = popover.querySelector('.openclaw-textarea').value.trim();
if (!currentAnnotation.reaction && !currentAnnotation.comment) {
showToast('Add a reaction or comment');
return;
}
annotations.push(currentAnnotation);
saveAnnotations();
updateCount();
overlay.classList.remove('visible');
popover.classList.remove('visible');
showToast('✓ Annotation saved');
});
// Cancel button
popover.querySelector('.openclaw-cancel-btn').addEventListener('click', () => {
overlay.classList.remove('visible');
popover.classList.remove('visible');
});
// Close on overlay tap
overlay.addEventListener('click', () => {
overlay.classList.remove('visible');
popover.classList.remove('visible');
});
// Send button - show export modal
toolbar.querySelector('.openclaw-send-btn').addEventListener('click', () => {
if (annotations.length === 0) {
showToast('No annotations to send');
return;
}
const exportText = generateExport();
const modal = document.createElement('div');
modal.className = 'openclaw-modal visible';
modal.innerHTML = `
<div class="openclaw-modal-content">
<h2>📤 Send Feedback</h2>
<div class="openclaw-export-preview">${escapeHtml(exportText)}</div>
<div class="openclaw-modal-actions">
<button class="openclaw-btn openclaw-btn-secondary">Close</button>
<button class="openclaw-btn openclaw-btn-primary">Copy to Clipboard</button>
</div>
</div>
`;
document.body.appendChild(modal);
const buttons = modal.querySelectorAll('.openclaw-btn');
buttons[0].addEventListener('click', () => modal.remove());
buttons[1].addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(exportText);
showToast('✓ Copied! Paste in chat');
setTimeout(() => modal.remove(), 1000);
} catch (e) {
// Fallback
const textarea = document.createElement('textarea');
textarea.value = exportText;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
textarea.remove();
showToast('✓ Copied! Paste in chat');
setTimeout(() => modal.remove(), 1000);
}
});
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.remove();
});
});
// Clear button
toolbar.querySelector('.openclaw-clear-btn').addEventListener('click', () => {
if (annotations.length === 0) {
showToast('No annotations');
return;
}
if (confirm('Clear all annotations?')) {
annotations = [];
saveAnnotations();
updateCount();
showToast('Cleared');
}
});
console.log('🐾 OpenClaw Annotations loaded. Select text to annotate.');
}
// Run on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment