aboutsummaryrefslogtreecommitdiffstats
path: root/renderer/src/components/DocumentPane.tsx
blob: e44b0fb180fdca29bf5caefed4d58f56ad58abf4 (plain)
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>
  );
}