File size: 9,374 Bytes
43d2f79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ee8e66a
43d2f79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2277ae0
 
 
43d2f79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ee8e66a
 
43d2f79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
/**
 * ReadmeView — on-demand README generator, rendered as a full-height view
 * alongside Chat and Diagram in the main content area.
 *
 * Design principles:
 *   - Matches the DiagramView pattern exactly: fills the main pane, no modal
 *   - Scrollable markdown panel with a sticky action bar at the top
 *   - Progress bar during generation, same visual language as tour/diagram loading
 *   - Copy + Regenerate in the action bar — accessible, not tucked in a corner
 */

import { useState, useEffect, useRef, useCallback } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { streamReadme } from "../api";

/**
 * TerminalBlock — wraps every fenced code block in a macOS-style terminal window.
 * ReactMarkdown passes the <code> element as children of <pre>; we intercept
 * the <pre> render and fish the language out of code's className prop.
 */
function TerminalBlock({ children }) {
  const codeChild = Array.isArray(children) ? children[0] : children;
  const lang = codeChild?.props?.className?.replace("language-", "") ?? null;
  return (
    <div className="readme-terminal">
      <div className="readme-terminal-bar">
        <span className="readme-terminal-dots">
          <span /><span /><span />
        </span>
        {lang && <span className="readme-terminal-lang">{lang}</span>}
      </div>
      <pre className="readme-terminal-pre">{children}</pre>
    </div>
  );
}

const MD_COMPONENTS = { pre: TerminalBlock };

const STAGE_LABELS = {
  loading:    "Analysing repository…",
  generating: "Generating README…",
};

export default function ReadmeView({ repo, contextualAt, onClose }) {
  const [status,    setStatus]    = useState("idle");
  const [progress,  setProgress]  = useState(0);
  const [message,   setMessage]   = useState("");
  const [content,   setContent]   = useState(null);
  const [fromCache, setFromCache] = useState(false);
  const [error,     setError]     = useState(null);
  const [copied,    setCopied]    = useState(false);
  const [rawMode,   setRawMode]   = useState(false);  // preview vs markdown source
  const cancelRef = useRef(null);

  const generate = useCallback((force = false) => {
    cancelRef.current?.();
    setStatus("loading");
    setProgress(0);
    setContent(null);
    setError(null);

    cancelRef.current = streamReadme(repo, {
      force,
      onProgress: ({ stage, progress: p, message: m }) => {
        setStatus(stage || "loading");
        setProgress(p ?? 0);
        setMessage(m || STAGE_LABELS[stage] || "Working…");
      },
      onDone: ({ content: md, from_cache }) => {
        setContent(md);
        setFromCache(from_cache);
        setStatus("done");
        setProgress(1);
      },
      onError: (msg) => {
        setError(msg);
        setStatus("error");
      },
    });
  }, [repo]);

  useEffect(() => {
    generate(false);
    return () => cancelRef.current?.();
  }, [generate]);

  function handleCopy() {
    if (!content) return;
    navigator.clipboard.writeText(content).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), 1800);
    });
  }

  const isLoading = status === "loading" || status === "generating";

  return (
    <div className="readme-view">
      {/* Sticky action bar */}
      <div className="readme-view-bar">
        <div className="readme-view-bar-left">
          {onClose && (
            <button
              className="readme-bar-btn"
              onClick={onClose}
              title="Back"
              style={{ marginRight: 4 }}
            >
              <svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                <path d="M10 3L5 8l5 5"/>
              </svg>
            </button>
          )}
          <span style={{ fontSize: 12, fontWeight: 600, color: "var(--text-2)" }}>README.md</span>
          {fromCache && status === "done" && (
            <span className="readme-cache-badge" title="Loaded from cache — click Regenerate to refresh">
              cached
            </span>
          )}
          {contextualAt && status === "done" && (
            <span className="repo-contextual" title={`Contextual retrieval appliedre-indexed ${contextualAt}`}></span>
          )}
          {isLoading && (
            <span className="readme-view-status">
              <span className="spinner" style={{ width: 10, height: 10 }} />
              {message || STAGE_LABELS[status]}
            </span>
          )}
        </div>

        <div className="readme-view-bar-right">
          {status === "done" && (
            <>
              {/* Preview / Markdown source toggle — reuses the app-wide view-toggle system */}
              <div className="view-toggle" style={{ padding: "2px", gap: "2px" }}>
                <button
                  className={`view-btn${!rawMode ? " active" : ""}`}
                  style={{ padding: "3px 12px", fontSize: 12 }}
                  onClick={() => setRawMode(false)}
                >Preview</button>
                <button
                  className={`view-btn${rawMode ? " active" : ""}`}
                  style={{ padding: "3px 12px", fontSize: 12 }}
                  onClick={() => setRawMode(true)}
                >Markdown</button>
              </div>
              <div style={{ width: 1, height: 14, background: "var(--border-strong)", margin: "0 6px" }} />
              <button
                className="readme-bar-btn"
                onClick={handleCopy}
                title={copied ? "Copied!" : "Copy markdown"}
              >
                {copied ? (
                  <>
                    <svg width="11" height="11" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
                      <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
                    </svg>
                    Copied
                  </>
                ) : (
                  <>
                    <svg width="11" height="11" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
                      <path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25z"/>
                      <path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25z"/>
                    </svg>
                    Copy
                  </>
                )}
              </button>
              <button
                className="readme-bar-btn"
                onClick={() => generate(true)}
                title="Regenerate README"
              >
                ↺ Regenerate
              </button>
            </>
          )}
        </div>
      </div>

      {/* Thin progress bar */}
      {isLoading && (
        <div className="readme-progress-wrap">
          <div className="readme-progress-bar" style={{ width: `${Math.max(progress * 100, 6)}%` }} />
        </div>
      )}

      {/* Quality tip — only shown when contextual retrieval hasn't been applied yet */}
      {status === "done" && !contextualAt && (
        <div className="readme-quality-note">
          <svg width="11" height="11" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" style={{ flexShrink: 0, opacity: 0.5 }}>
            <path d="M8 1a7 7 0 100 14A7 7 0 008 1zm0 3.25a.75.75 0 110 1.5.75.75 0 010-1.5zM7.25 7h1.5v4.5h-1.5V7z"/>
          </svg>
          Quality improves when the repo is indexed with contextual retrieval enabled
          (<code>CONTEXTUAL_TOP_N&gt;0</code>). Re-index to get richer README output.
        </div>
      )}

      {/* Content area */}
      <div className="readme-view-body">
        {isLoading && (
          <div className="readme-view-placeholder">
            <div className="readme-view-placeholder-icon">
              <svg width="28" height="28" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" opacity="0.3" aria-hidden="true">
                <rect x="2" y="1" width="9" height="13" rx="1"/>
                <path d="M5 5h4M5 7h4M5 9h2"/>
                <path d="M9 1v3h3"/>
              </svg>
            </div>
            <p>{message || STAGE_LABELS[status]}</p>
          </div>
        )}

        {status === "error" && (
          <div className="readme-view-placeholder">
            <p style={{ color: "var(--text-2)" }}>{error}</p>
            <button className="readme-bar-btn" style={{ marginTop: 12 }} onClick={() => generate(true)}>
              Try again
            </button>
          </div>
        )}

        {content && (
          rawMode ? (
            /* Raw markdown source — monospace, line-numbered feel */
            <pre className="readme-raw">{content}</pre>
          ) : (
            <div className="readme-content">
              <ReactMarkdown remarkPlugins={[remarkGfm]} components={MD_COMPONENTS}>{content}</ReactMarkdown>
            </div>
          )
        )}
      </div>
    </div>
  );
}