Copy page button on docs
This commit is contained in:
parent
a1f22dabd2
commit
29c0890f22
5 changed files with 268 additions and 0 deletions
|
|
@ -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.
|
||||
|
|
|
|||
162
hindsight-docs/src/components/CopyPageButton/index.tsx
Normal file
162
hindsight-docs/src/components/CopyPageButton/index.tsx
Normal 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 ``;
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
21
hindsight-docs/src/theme/DocItem/Content/index.tsx
Normal file
21
hindsight-docs/src/theme/DocItem/Content/index.tsx
Normal 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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
20
hindsight-docs/src/theme/DocItem/Content/styles.module.css
Normal file
20
hindsight-docs/src/theme/DocItem/Content/styles.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue