Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 64 additions & 3 deletions apps/app/src/components/markdown/markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,34 @@ function sanitizeMarkdownHtml(value: string) {
"start",
"style",
"target",
// Whitelist custom attribute to prevent DOMPurify from stripping the copy button identifier
"data-openwork-copy-code",
],
});
}

function generateCodeBlockContainer(content: string){
return (
`
<div data-openwork-shiki="true" class="my-4 overflow-y-clip rounded-lg border border-border/70 bg-gray-1/80 p-4 pt-0 text-xs leading-6">
<div class="sticky z-[2] top-0 bg-gray-1">
<div class="w-full flex items-center justify-end py-2">
<button data-openwork-copy-code="">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-md">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
</div>
</div>
<div class="overflow-x-scroll">
${content}
</div>
</div>
`
)
}

const baseMarkedOptions = {
async: false,
breaks: false,
Expand Down Expand Up @@ -258,7 +282,8 @@ const baseMarkedOptions = {
return `<blockquote class="my-4 rounded-r-lg border-l border-border bg-muted/40 pl-4 italic text-muted-foreground">${this.parser.parse(tokens)}</blockquote>`;
},
code({ text, lang }) {
return `<pre class="my-4 overflow-x-auto rounded-[18px] border border-border/70 bg-gray-1/80 px-4 py-3 text-xs leading-6 text-muted-foreground"><code${codeLanguageClass(lang)}>${escapeHtml(text)}</code></pre>`;
const rawCode = `<pre class="overflow-x-auto text-xs leading-6 text-muted-foreground"><code${codeLanguageClass(lang)}>${escapeHtml(text)}</code></pre>`
return generateCodeBlockContainer(rawCode)
},
codespan({ text }) {
return `<code class="rounded-md bg-gray-2/70 px-1.5 py-0.5 font-mono text-sm text-foreground">${escapeHtml(text)}</code>`;
Expand All @@ -267,7 +292,7 @@ const baseMarkedOptions = {
if (!raw.startsWith("~~")) {
return escapeHtml(raw);
}

return `<del>${this.parser.parseInline(tokens)}</del>`;
},
link({ href, title, tokens }) {
Expand Down Expand Up @@ -352,7 +377,8 @@ const highlightedMarkdownParser = new Marked({
],
});
},
container: `<div data-openwork-shiki="true" class="my-4 overflow-hidden rounded-lg border border-border/70 bg-gray-1/80 p-4 text-xs leading-6">%s</div>`,
// Custom container wrapping code blocks with a sticky copy header
container: generateCodeBlockContainer("%s"),
}),
);

Expand Down Expand Up @@ -445,6 +471,41 @@ function MarkdownBlockInner({
const handleClick = (event: MouseEvent) => {
if (!(event.target instanceof Element)) return;

// Handle copying code from code blocks when clicking the copy button
const copyButton = event.target.closest("[data-openwork-copy-code]");
if (copyButton instanceof HTMLButtonElement) {
const originalHtml = copyButton.innerHTML;
event.preventDefault();
event.stopPropagation();
// Copy code block function handler
const codeBlock = copyButton.closest("[data-openwork-shiki]")?.querySelector("code");
if (codeBlock instanceof HTMLElement) {
const textToCopy = codeBlock.textContent ?? "";
navigator.clipboard.writeText(textToCopy).then(() => {
// On succes change svg to tick mark with current color
copyButton.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 6L9 17L4 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`;
setTimeout(() => {
copyButton.innerHTML = originalHtml;
}, 500);
}).catch(() => {
// on failure change svg to red X mark
copyButton.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="#FF0000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`;
setTimeout(() => {
copyButton.innerHTML = originalHtml;
}, 500);
});
}
return;
}

const chevron = event.target.closest("[data-openwork-link-chevron]");
if (chevron instanceof HTMLElement) {
event.preventDefault();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1290,8 +1290,10 @@ export function SessionSurface(props: SessionSurfaceProps) {
onScroll={sessionScroll.handleScroll}
// Extra top padding while the find bar is open so it never covers
// the first message (short transcripts cannot scroll it clear).
className={`absolute inset-0 overflow-x-hidden overflow-y-auto overscroll-y-contain px-3 pb-4 sm:px-5 ${findOpen ? "pt-16" : "pt-4"}`}
className={`absolute inset-0 overflow-x-hidden overflow-y-auto overscroll-y-contain px-3 pb-4 sm:px-5`}
>
{/* Removed the padding from the scroll container and added a spacer div to adjust the top spacing when the find bar is open*/}
{findOpen? <div className="h-16"></div>:<div className="h-4"></div>}
{/* Chat column: tighter than the composer (800px) so messages
keep a comfortable reading width and don't feel "too big". */}
<div ref={contentRef} className="mx-auto w-full max-w-[720px]">
Expand Down