1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
|
import React, { useState, useMemo } from "react";
import type { Phase } from "../types";
interface DocumentPaneProps {
content: string;
onChange: (content: string) => void;
phase: Phase;
disabled: boolean;
}
function renderMarkdown(md: string): string {
return (
md
// Headers
.replace(/^### (.*$)/gm, "<h3>$1</h3>")
.replace(/^## (.*$)/gm, "<h2>$1</h2>")
.replace(/^# (.*$)/gm, "<h1>$1</h1>")
// Bold/italic
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
.replace(/\*([^*]+)\*/g, "<em>$1</em>")
// Code blocks
.replace(
/```(\w*)\n([\s\S]*?)```/g,
'<pre><code class="language-$1">$2</code></pre>'
)
.replace(/`([^`]+)`/g, "<code>$1</code>")
// Lists
.replace(/^- \[x\] (.*$)/gm, '<li class="task done">☑ $1</li>')
.replace(/^- \[ \] (.*$)/gm, '<li class="task">☐ $1</li>')
.replace(/^- (.*$)/gm, "<li>$1</li>")
// Review comments (highlight them)
.replace(/(\/\/ REVIEW:.*$)/gm, '<mark class="review">$1</mark>')
.replace(/(\/\/ NOTE:.*$)/gm, '<mark class="note">$1</mark>')
// Paragraphs
.replace(/\n\n/g, "</p><p>")
.replace(/^(.+)$/gm, "<p>$1</p>")
// Clean up
.replace(/<p><\/p>/g, "")
.replace(/<p>(<h[1-3]>)/g, "$1")
.replace(/(<\/h[1-3]>)<\/p>/g, "$1")
.replace(/<p>(<pre>)/g, "$1")
.replace(/(<\/pre>)<\/p>/g, "$1")
.replace(/<p>(<li)/g, "$1")
.replace(/(<\/li>)<\/p>/g, "$1")
);
}
export function DocumentPane({
content,
onChange,
phase,
disabled,
}: DocumentPaneProps) {
const [isEditing, setIsEditing] = useState(false);
const renderedHtml = useMemo(() => renderMarkdown(content), [content]);
if (phase === "implement") {
return (
<div className="document-pane">
<div className="document-header">
<span>plan.md</span>
<span className="badge">Implementing...</span>
</div>
<div
className="document-content rendered"
dangerouslySetInnerHTML={{ __html: renderedHtml }}
/>
</div>
);
}
const filename = phase === "research" ? "research.md" : "plan.md";
return (
<div className="document-pane">
<div className="document-header">
<span>{filename}</span>
<button onClick={() => setIsEditing(!isEditing)}>
{isEditing ? "Preview" : "Edit"}
</button>
</div>
{isEditing ? (
<textarea
className="document-content editing"
value={content}
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
placeholder={`${filename} will appear here...`}
/>
) : (
<div
className="document-content rendered"
dangerouslySetInnerHTML={{
__html:
renderedHtml ||
'<p class="empty">Document will appear here after Claude generates it...</p>',
}}
onClick={() => !disabled && setIsEditing(true)}
/>
)}
</div>
);
}
|