diff options
| author | Clawd <ai@clawd.bot> | 2026-02-28 13:48:34 -0800 |
|---|---|---|
| committer | Clawd <ai@clawd.bot> | 2026-02-28 13:48:34 -0800 |
| commit | c386a1acfab0db99af57a9a18a49c72b89184f15 (patch) | |
| tree | 1b4c81ea2b992be3e19789ca21e7bb9f4f99bbc5 /renderer/src/components/DocumentPane.tsx | |
| parent | afe1cd0918e182b8107ffa81b9f9d6cdec4615ae (diff) | |
Replace custom markdown renderer with react-markdown
- Add react-markdown and remark-gfm dependencies
- Remove hacky regex-based renderMarkdown function
- Full GFM support: tables, task lists, strikethrough, autolinks
- Update CSS for react-markdown output (task lists, blockquotes, hr)
- Cleaner, more maintainable code
Diffstat (limited to 'renderer/src/components/DocumentPane.tsx')
| -rw-r--r-- | renderer/src/components/DocumentPane.tsx | 183 |
1 files changed, 59 insertions, 124 deletions
diff --git a/renderer/src/components/DocumentPane.tsx b/renderer/src/components/DocumentPane.tsx index c2e1b2c..95d7d03 100644 --- a/renderer/src/components/DocumentPane.tsx +++ b/renderer/src/components/DocumentPane.tsx | |||
| @@ -1,4 +1,6 @@ | |||
| 1 | import React, { useState, useMemo } from "react"; | 1 | import React, { useState } from "react"; |
| 2 | import ReactMarkdown from "react-markdown"; | ||
| 3 | import remarkGfm from "remark-gfm"; | ||
| 2 | import type { Phase } from "../types"; | 4 | import type { Phase } from "../types"; |
| 3 | 5 | ||
| 4 | interface DocumentPaneProps { | 6 | interface DocumentPaneProps { |
| @@ -9,108 +11,6 @@ interface DocumentPaneProps { | |||
| 9 | showOnboarding?: boolean; | 11 | showOnboarding?: boolean; |
| 10 | } | 12 | } |
| 11 | 13 | ||
| 12 | function renderTable(tableLines: string[]): string { | ||
| 13 | if (tableLines.length < 2) return tableLines.join("\n"); | ||
| 14 | |||
| 15 | const parseRow = (line: string): string[] => { | ||
| 16 | return line | ||
| 17 | .split("|") | ||
| 18 | .slice(1, -1) // Remove empty first/last from |col|col| | ||
| 19 | .map((cell) => cell.trim()); | ||
| 20 | }; | ||
| 21 | |||
| 22 | const headerCells = parseRow(tableLines[0]); | ||
| 23 | // Skip separator row (index 1) | ||
| 24 | const bodyRows = tableLines.slice(2); | ||
| 25 | |||
| 26 | let html = '<table><thead><tr>'; | ||
| 27 | headerCells.forEach((cell) => { | ||
| 28 | html += `<th>${cell}</th>`; | ||
| 29 | }); | ||
| 30 | html += '</tr></thead><tbody>'; | ||
| 31 | |||
| 32 | bodyRows.forEach((row) => { | ||
| 33 | if (row.trim()) { | ||
| 34 | html += '<tr>'; | ||
| 35 | parseRow(row).forEach((cell) => { | ||
| 36 | html += `<td>${cell}</td>`; | ||
| 37 | }); | ||
| 38 | html += '</tr>'; | ||
| 39 | } | ||
| 40 | }); | ||
| 41 | |||
| 42 | html += '</tbody></table>'; | ||
| 43 | return html; | ||
| 44 | } | ||
| 45 | |||
| 46 | function renderMarkdown(md: string): string { | ||
| 47 | // First, handle tables (before other transformations) | ||
| 48 | const lines = md.split("\n"); | ||
| 49 | const processedLines: string[] = []; | ||
| 50 | let tableBuffer: string[] = []; | ||
| 51 | let inTable = false; | ||
| 52 | |||
| 53 | for (const line of lines) { | ||
| 54 | const isTableLine = /^\|.*\|$/.test(line.trim()); | ||
| 55 | |||
| 56 | if (isTableLine) { | ||
| 57 | inTable = true; | ||
| 58 | tableBuffer.push(line); | ||
| 59 | } else { | ||
| 60 | if (inTable && tableBuffer.length > 0) { | ||
| 61 | processedLines.push(renderTable(tableBuffer)); | ||
| 62 | tableBuffer = []; | ||
| 63 | inTable = false; | ||
| 64 | } | ||
| 65 | processedLines.push(line); | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | // Handle table at end of content | ||
| 70 | if (tableBuffer.length > 0) { | ||
| 71 | processedLines.push(renderTable(tableBuffer)); | ||
| 72 | } | ||
| 73 | |||
| 74 | let result = processedLines.join("\n"); | ||
| 75 | |||
| 76 | return ( | ||
| 77 | result | ||
| 78 | // Headers | ||
| 79 | .replace(/^### (.*$)/gm, "<h3>$1</h3>") | ||
| 80 | .replace(/^## (.*$)/gm, "<h2>$1</h2>") | ||
| 81 | .replace(/^# (.*$)/gm, "<h1>$1</h1>") | ||
| 82 | // Bold/italic | ||
| 83 | .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>") | ||
| 84 | .replace(/\*([^*]+)\*/g, "<em>$1</em>") | ||
| 85 | // Code blocks | ||
| 86 | .replace( | ||
| 87 | /```(\w*)\n([\s\S]*?)```/g, | ||
| 88 | '<pre><code class="language-$1">$2</code></pre>' | ||
| 89 | ) | ||
| 90 | .replace(/`([^`]+)`/g, "<code>$1</code>") | ||
| 91 | // Lists | ||
| 92 | .replace(/^- \[x\] (.*$)/gm, '<li class="task done">☑ $1</li>') | ||
| 93 | .replace(/^- \[ \] (.*$)/gm, '<li class="task">☐ $1</li>') | ||
| 94 | .replace(/^- (.*$)/gm, "<li>$1</li>") | ||
| 95 | // Review comments (highlight them) | ||
| 96 | .replace(/(\/\/ REVIEW:.*$)/gm, '<mark class="review">$1</mark>') | ||
| 97 | .replace(/(\/\/ NOTE:.*$)/gm, '<mark class="note">$1</mark>') | ||
| 98 | // Paragraphs | ||
| 99 | .replace(/\n\n/g, "</p><p>") | ||
| 100 | .replace(/^(.+)$/gm, "<p>$1</p>") | ||
| 101 | // Clean up | ||
| 102 | .replace(/<p><\/p>/g, "") | ||
| 103 | .replace(/<p>(<h[1-3]>)/g, "$1") | ||
| 104 | .replace(/(<\/h[1-3]>)<\/p>/g, "$1") | ||
| 105 | .replace(/<p>(<pre>)/g, "$1") | ||
| 106 | .replace(/(<\/pre>)<\/p>/g, "$1") | ||
| 107 | .replace(/<p>(<li)/g, "$1") | ||
| 108 | .replace(/(<\/li>)<\/p>/g, "$1") | ||
| 109 | .replace(/<p>(<table>)/g, "$1") | ||
| 110 | .replace(/(<\/table>)<\/p>/g, "$1") | ||
| 111 | ); | ||
| 112 | } | ||
| 113 | |||
| 114 | export function DocumentPane({ | 14 | export function DocumentPane({ |
| 115 | content, | 15 | content, |
| 116 | onChange, | 16 | onChange, |
| @@ -119,7 +19,6 @@ export function DocumentPane({ | |||
| 119 | showOnboarding, | 19 | showOnboarding, |
| 120 | }: DocumentPaneProps) { | 20 | }: DocumentPaneProps) { |
| 121 | const [isEditing, setIsEditing] = useState(false); | 21 | const [isEditing, setIsEditing] = useState(false); |
| 122 | const renderedHtml = useMemo(() => renderMarkdown(content), [content]); | ||
| 123 | 22 | ||
| 124 | if (showOnboarding) { | 23 | if (showOnboarding) { |
| 125 | return ( | 24 | return ( |
| @@ -130,27 +29,61 @@ export function DocumentPane({ | |||
| 130 | <div className="document-content rendered onboarding"> | 29 | <div className="document-content rendered onboarding"> |
| 131 | <h1>Claude Flow</h1> | 30 | <h1>Claude Flow</h1> |
| 132 | <p> | 31 | <p> |
| 133 | A structured workflow for AI-assisted coding: <strong>Research → Plan → Implement</strong>. | 32 | A structured workflow for AI-assisted coding:{" "} |
| 33 | <strong>Research → Plan → Implement</strong>. | ||
| 134 | </p> | 34 | </p> |
| 135 | 35 | ||
| 136 | <h2>Setup</h2> | 36 | <h2>Setup</h2> |
| 137 | <p>Export your Anthropic API key:</p> | 37 | <p>Export your Anthropic API key:</p> |
| 138 | <pre><code>export ANTHROPIC_API_KEY=your-key-here</code></pre> | 38 | <pre> |
| 139 | <p>Get one at <a href="https://platform.claude.com" target="_blank" rel="noopener">platform.claude.com</a></p> | 39 | <code>export ANTHROPIC_API_KEY=your-key-here</code> |
| 40 | </pre> | ||
| 41 | <p> | ||
| 42 | Get one at{" "} | ||
| 43 | <a | ||
| 44 | href="https://platform.claude.com" | ||
| 45 | target="_blank" | ||
| 46 | rel="noopener" | ||
| 47 | > | ||
| 48 | platform.claude.com | ||
| 49 | </a> | ||
| 50 | </p> | ||
| 140 | 51 | ||
| 141 | <h2>Getting Started</h2> | 52 | <h2>Getting Started</h2> |
| 142 | <ol> | 53 | <ol> |
| 143 | <li><strong>Add a project</strong> — Select a codebase folder</li> | 54 | <li> |
| 144 | <li><strong>Create a session</strong> — Start a new task</li> | 55 | <strong>Add a project</strong> — Select a codebase folder |
| 145 | <li><strong>Describe your work</strong> — Tell Claude what you want to build</li> | 56 | </li> |
| 57 | <li> | ||
| 58 | <strong>Create a session</strong> — Start a new task | ||
| 59 | </li> | ||
| 60 | <li> | ||
| 61 | <strong>Describe your work</strong> — Tell Claude what you want to | ||
| 62 | build | ||
| 63 | </li> | ||
| 146 | </ol> | 64 | </ol> |
| 147 | 65 | ||
| 148 | <h2>Workflow</h2> | 66 | <h2>Workflow</h2> |
| 149 | <p><strong>Research:</strong> Claude analyzes your codebase and writes findings to <code>research.md</code>. Add notes like <code>// REVIEW: check this</code> — click <strong>Review</strong> when done.</p> | 67 | <p> |
| 150 | <p><strong>Plan:</strong> Claude drafts an implementation plan in <code>plan.md</code> with code snippets and a TODO list. Iterate the same way.</p> | 68 | <strong>Research:</strong> Claude analyzes your codebase and writes |
| 151 | <p><strong>Implement:</strong> Claude executes the plan, marking tasks complete as it goes.</p> | 69 | findings to <code>research.md</code>. Add notes like{" "} |
| 70 | <code>// REVIEW: check this</code> — click <strong>Review</strong>{" "} | ||
| 71 | when done. | ||
| 72 | </p> | ||
| 73 | <p> | ||
| 74 | <strong>Plan:</strong> Claude drafts an implementation plan in{" "} | ||
| 75 | <code>plan.md</code> with code snippets and a TODO list. Iterate the | ||
| 76 | same way. | ||
| 77 | </p> | ||
| 78 | <p> | ||
| 79 | <strong>Implement:</strong> Claude executes the plan, marking tasks | ||
| 80 | complete as it goes. | ||
| 81 | </p> | ||
| 152 | 82 | ||
| 153 | <p className="onboarding-tip">Iterate on research and plan docs as long as you want. Click <strong>Submit</strong> when happy to move to the next phase.</p> | 83 | <p className="onboarding-tip"> |
| 84 | Iterate on research and plan docs as long as you want. Click{" "} | ||
| 85 | <strong>Submit</strong> when happy to move to the next phase. | ||
| 86 | </p> | ||
| 154 | </div> | 87 | </div> |
| 155 | </div> | 88 | </div> |
| 156 | ); | 89 | ); |
| @@ -163,10 +96,9 @@ export function DocumentPane({ | |||
| 163 | <span>plan.md</span> | 96 | <span>plan.md</span> |
| 164 | <span className="badge">Implementing...</span> | 97 | <span className="badge">Implementing...</span> |
| 165 | </div> | 98 | </div> |
| 166 | <div | 99 | <div className="document-content rendered"> |
| 167 | className="document-content rendered" | 100 | <ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown> |
| 168 | dangerouslySetInnerHTML={{ __html: renderedHtml }} | 101 | </div> |
| 169 | /> | ||
| 170 | </div> | 102 | </div> |
| 171 | ); | 103 | ); |
| 172 | } | 104 | } |
| @@ -193,13 +125,16 @@ export function DocumentPane({ | |||
| 193 | ) : ( | 125 | ) : ( |
| 194 | <div | 126 | <div |
| 195 | className="document-content rendered" | 127 | className="document-content rendered" |
| 196 | dangerouslySetInnerHTML={{ | ||
| 197 | __html: | ||
| 198 | renderedHtml || | ||
| 199 | '<p class="empty">Document will appear here after Claude generates it...</p>', | ||
| 200 | }} | ||
| 201 | onClick={() => !disabled && setIsEditing(true)} | 128 | onClick={() => !disabled && setIsEditing(true)} |
| 202 | /> | 129 | > |
| 130 | {content ? ( | ||
| 131 | <ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown> | ||
| 132 | ) : ( | ||
| 133 | <p className="empty"> | ||
| 134 | Document will appear here after Claude generates it... | ||
| 135 | </p> | ||
| 136 | )} | ||
| 137 | </div> | ||
| 203 | )} | 138 | )} |
| 204 | </div> | 139 | </div> |
| 205 | ); | 140 | ); |
