Copy page button on docs

This commit is contained in:
Chris Latimer 2026-02-09 14:48:08 -07:00
parent a1f22dabd2
commit 29c0890f22
5 changed files with 268 additions and 0 deletions

View file

@ -1,4 +1,14 @@
---
title: How We Solved Memory Conflicts in Hindsight
description: Learn how Hindsight handles contradictory information by tracking temporal evolution and preserving history in its memory consolidation system.
authors: [hindsight]
tags: [engineering, memory-systems, conflict-resolution]
image: /img/blog/2026-02-09/consolidation-pipeline.png
date: 2026-02-09
---
# How We Solved Memory Conflicts in Hindsight
One of the hardest problems we tackled in Hindsight was dealing with contradictions. When you're building a memory system for AI agents, reality isn't static. It evolves.
A CRM agent might learn that "Acme Corp is a key prospect" in January, then encounter "Acme Corp is now a paying customer" in March. Naive approaches either lose the history or drown in duplicate facts.

View file

@ -0,0 +1,162 @@
import React, { useState, useCallback } from 'react';
import styles from './styles.module.css';
export default function CopyPageButton(): JSX.Element | null {
const [copied, setCopied] = useState(false);
const copyPageAsMarkdown = useCallback(async () => {
try {
// Get the page content
const contentElement = document.querySelector('.markdown');
if (!contentElement) return;
// Convert HTML to markdown-like text
let markdown = '';
// Add title
const title = document.querySelector('h1')?.textContent;
if (title) {
markdown += `# ${title}\n\n`;
}
// Extract text content from the markdown container
const extractMarkdown = (element: Element): string => {
let text = '';
const processNode = (node: Node): string => {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent || '';
}
if (node.nodeType === Node.ELEMENT_NODE) {
const el = node as Element;
const tagName = el.tagName.toLowerCase();
const children = Array.from(el.childNodes).map(processNode).join('');
switch (tagName) {
case 'h1':
return `# ${children}\n\n`;
case 'h2':
return `## ${children}\n\n`;
case 'h3':
return `### ${children}\n\n`;
case 'h4':
return `#### ${children}\n\n`;
case 'h5':
return `##### ${children}\n\n`;
case 'h6':
return `###### ${children}\n\n`;
case 'p':
return `${children}\n\n`;
case 'ul':
return `${children}\n`;
case 'ol':
return `${children}\n`;
case 'li':
const parent = el.parentElement;
const isOrdered = parent?.tagName.toLowerCase() === 'ol';
if (isOrdered) {
const index = Array.from(parent?.children || []).indexOf(el) + 1;
return `${index}. ${children}\n`;
}
return `- ${children}\n`;
case 'code':
const isBlock = el.parentElement?.tagName.toLowerCase() === 'pre';
if (isBlock) {
const lang = el.className.replace('language-', '');
return `\`\`\`${lang}\n${children}\n\`\`\`\n\n`;
}
return `\`${children}\``;
case 'pre':
return children; // Already handled by code block
case 'blockquote':
return children.split('\n').map(line => `> ${line}`).join('\n') + '\n\n';
case 'a':
const href = el.getAttribute('href') || '';
return `[${children}](${href})`;
case 'strong':
case 'b':
return `**${children}**`;
case 'em':
case 'i':
return `*${children}*`;
case 'br':
return '\n';
case 'hr':
return '---\n\n';
case 'table':
return `${children}\n`;
case 'thead':
case 'tbody':
return children;
case 'tr':
return `${children}|\n`;
case 'th':
case 'td':
return `| ${children} `;
case 'img':
const src = el.getAttribute('src') || '';
const alt = el.getAttribute('alt') || '';
return `![${alt}](${src})`;
default:
return children;
}
}
return '';
};
Array.from(element.childNodes).forEach(node => {
text += processNode(node);
});
return text;
};
// Skip the title h1 if it's already added
const contentToCopy = Array.from(contentElement.children)
.filter(child => !(child.tagName === 'H1' && child.textContent === title))
.map(child => extractMarkdown(child))
.join('');
markdown += contentToCopy;
// Clean up excessive newlines
markdown = markdown.replace(/\n{3,}/g, '\n\n').trim();
// Copy to clipboard
await navigator.clipboard.writeText(markdown);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (error) {
console.error('Failed to copy page content:', error);
}
}, []);
return (
<button
className={`${styles.copyPageButton} ${copied ? styles.copied : ''}`}
onClick={copyPageAsMarkdown}
aria-label="Copy page as markdown"
title="Copy page as markdown"
>
{copied ? (
<>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425a.247.247 0 0 1 .02-.022Z"/>
</svg>
<span className={styles.buttonText}>Copied!</span>
</>
) : (
<>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H6z"/>
<path d="M2 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1h1v1a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h1v1H2z"/>
</svg>
<span className={styles.buttonText}>Copy page</span>
</>
)}
</button>
);
}

View file

@ -0,0 +1,55 @@
.copyPageButton {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: transparent;
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 6px;
color: var(--ifm-font-color-base);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.copyPageButton:hover {
background-color: var(--ifm-color-emphasis-100);
border-color: var(--ifm-color-emphasis-400);
}
.copyPageButton:active {
transform: translateY(1px);
}
.copyPageButton.copied {
background-color: var(--ifm-color-success-contrast-background);
border-color: var(--ifm-color-success);
color: var(--ifm-color-success-darkest);
}
.copyPageButton.copied:hover {
background-color: var(--ifm-color-success-contrast-background);
border-color: var(--ifm-color-success);
}
.buttonText {
margin: 0 4px;
}
/* Dark mode adjustments */
[data-theme='dark'] .copyPageButton {
border-color: var(--ifm-color-emphasis-400);
}
[data-theme='dark'] .copyPageButton:hover {
background-color: var(--ifm-color-emphasis-200);
border-color: var(--ifm-color-emphasis-500);
}
[data-theme='dark'] .copyPageButton.copied {
background-color: var(--ifm-color-success-dark);
border-color: var(--ifm-color-success);
color: var(--ifm-color-success-contrast-foreground);
}

View file

@ -0,0 +1,21 @@
import React from 'react';
import DocItemContent from '@theme-original/DocItem/Content';
import type DocItemContentType from '@theme/DocItem/Content';
import type { WrapperProps } from '@docusaurus/types';
import CopyPageButton from '@site/src/components/CopyPageButton';
import styles from './styles.module.css';
type Props = WrapperProps<typeof DocItemContentType>;
export default function DocItemContentWrapper(props: Props): JSX.Element {
return (
<>
<div className={styles.docItemHeader}>
<div className={styles.docItemActions}>
<CopyPageButton />
</div>
</div>
<DocItemContent {...props} />
</>
);
}

View file

@ -0,0 +1,20 @@
.docItemHeader {
display: flex;
justify-content: flex-end;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--ifm-color-emphasis-200);
}
.docItemActions {
display: flex;
gap: 8px;
align-items: center;
}
@media (max-width: 768px) {
.docItemHeader {
margin-bottom: 0.75rem;
}
}