Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

components.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

components.tsxBlame877 lines · 2 contributors
fc1817aClaude1import type { FC } from "hono/jsx";
06d5ffeClaude2import { html } from "hono/html";
fc1817aClaude3import type { GitCommit, GitTreeEntry, GitDiffFile } from "../git/repository";
06d5ffeClaude4import type { Repository } from "../db/schema";
82c3a38ccanty labs5import { parseDiff, pairLines } from "../lib/diff";
6import type { DiffLine, ParsedFile, SplitRow } from "../lib/diff";
fc1817aClaude7
8c790e0Claude8/**
9 * Describes the most recent push to a repo, used by RepoHeader to render
10 * the Push Watch discoverability indicator.
11 *
12 * - ageMs < 5 min \u2192 pulsing red "\u25cf Live" badge
13 * - ageMs < 24 hr \u2192 dimmer "\u25cb Watch" link
14 * - otherwise \u2192 nothing shown
15 */
16export interface RecentPush {
17 sha: string;
18 ageMs: number;
19}
20
06d5ffeClaude21export const RepoHeader: FC<{
22 owner: string;
23 repo: string;
24 starCount?: number;
25 starred?: boolean;
c81ab7aClaude26 forkCount?: number;
06d5ffeClaude27 currentUser?: string | null;
c81ab7aClaude28 forkedFrom?: string | null;
71cd5ecClaude29 archived?: boolean;
30 isTemplate?: boolean;
8c790e0Claude31 /** Most recent push info for Push Watch discoverability indicator. */
32 recentPush?: RecentPush | null;
77cf834Claude33 /** 0-100 health score badge rendered after the repo name. Optional — omit to hide. */
34 healthScore?: number;
71cd5ecClaude35}> = ({
36 owner,
37 repo,
38 starCount,
39 starred,
40 forkCount,
41 currentUser,
42 forkedFrom,
43 archived,
44 isTemplate,
8c790e0Claude45 recentPush,
77cf834Claude46 healthScore,
8c790e0Claude47}) => {
48 const FIVE_MIN = 5 * 60 * 1000;
49 const TWENTY_FOUR_HR = 24 * 60 * 60 * 1000;
50 const isLive = recentPush != null && recentPush.ageMs < FIVE_MIN;
51 const isRecent = recentPush != null && recentPush.ageMs < TWENTY_FOUR_HR;
52
77cf834Claude53 const healthColor =
54 healthScore === undefined ? null :
55 healthScore >= 80 ? "#34d399" :
56 healthScore >= 50 ? "#facc15" :
57 "#f87171";
58
8c790e0Claude59 return (
60 <div class="repo-header">
61 <div>
62 <div class="repo-header-title">
63 <a href={`/${owner}`} class="owner">
64 {owner}
65 </a>
66 <span class="separator">/</span>
67 <a href={`/${owner}/${repo}`} class="name">
68 {repo}
69 </a>
70 {archived && (
71 <span
72 class="repo-header-pill repo-header-pill-archived"
73 title="Read-only: pushes and new issues/PRs disabled"
74 >
75 Archived
76 </span>
77 )}
78 {isTemplate && (
79 <span
80 class="repo-header-pill repo-header-pill-template"
81 title="This repository can be used as a template"
82 >
83 Template
84 </span>
85 )}
77cf834Claude86 {healthScore !== undefined && healthColor && (
87 <a
88 href={`/${owner}/${repo}/health`}
89 title={`Repository health score: ${healthScore}/100 — click for breakdown`}
90 style={`display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:9999px;font-size:11px;font-weight:700;text-decoration:none;color:${healthColor};background:${healthColor}22;border:1px solid ${healthColor}44;`}
91 >
92 &#x2665; Health {healthScore}
93 </a>
94 )}
8c790e0Claude95 {isLive && recentPush && (
96 <a
97 href={`/${owner}/${repo}/push/${recentPush.sha}`}
98 class="repo-header-live-badge repo-header-live-badge--live"
99 title="Push in progress \u2014 watch live gate + deploy status"
100 aria-label="Live push \u2014 click to watch status"
101 >
102 <span class="repo-header-live-dot" aria-hidden="true">{"\u25cf"}</span>
103 Live
104 </a>
105 )}
106 {!isLive && isRecent && recentPush && (
107 <a
108 href={`/${owner}/${repo}/push/${recentPush.sha}`}
109 class="repo-header-live-badge repo-header-live-badge--recent"
110 title="Watch the most recent push's gate + deploy results"
111 aria-label="Watch most recent push"
112 >
113 <span aria-hidden="true">{"\u25cb"}</span>
114 Watch
115 </a>
116 )}
117 </div>
118 {forkedFrom && (
119 <div class="repo-header-fork">
120 forked from <a href={`/${forkedFrom}`}>{forkedFrom}</a>
121 </div>
71cd5ecClaude122 )}
c81ab7aClaude123 </div>
8c790e0Claude124 <div class="repo-header-actions">
125 {currentUser && currentUser !== owner && (
126 <form method="post" action={`/${owner}/${repo}/fork`} style="display:inline">
127 <button type="submit" class="star-btn">
128 {"\u2442"} Fork {forkCount !== undefined && forkCount > 0 ? forkCount : ""}
06d5ffeClaude129 </button>
130 </form>
8c790e0Claude131 )}
132 {starCount !== undefined && (
133 currentUser ? (
134 <form method="post" action={`/${owner}/${repo}/star`} style="display:inline">
135 <button
136 type="submit"
137 class={`star-btn${starred ? " starred" : ""}`}
138 >
139 {starred ? "\u2605" : "\u2606"} {starCount}
140 </button>
141 </form>
142 ) : (
143 <span class="star-btn">
144 {"\u2606"} {starCount}
145 </span>
146 )
147 )}
148 </div>
06d5ffeClaude149 </div>
8c790e0Claude150 );
151};
fc1817aClaude152
153export const RepoNav: FC<{
154 owner: string;
155 repo: string;
3ef4c9dClaude156 active:
157 | "code"
158 | "commits"
159 | "issues"
160 | "pulls"
161 | "releases"
eafe8c6Claude162 | "actions"
3ef4c9dClaude163 | "gates"
3cbe3d6Claude164 | "insights"
165 | "explain"
166 | "changelog"
1e162a8Claude167 | "semantic"
168 | "wiki"
0316dbbClaude169 | "projects"
ef3fd93Claude170 | "agents"
c645a86Claude171 | "discussions"
da3fc18Claude172 | "security"
bcc4020Claude173 | "settings"
7f992cdClaude174 | "debt-map"
175 | "migrate"
77cf834Claude176 | "deployments"
0224546Claude177 | "nl-search"
178 | "contributors"
179 | "pulse"
180 | "traffic";
181 /** Current authenticated user — used for owner-only tab gating. */
182 currentUser?: string | null;
183 /** Repo owner username — used for owner-only tab gating. */
184 repoOwner?: string;
185}> = ({ owner, repo, active, currentUser, repoOwner }) => (
fc1817aClaude186 <div class="repo-nav">
06d5ffeClaude187 <a href={`/${owner}/${repo}`} class={active === "code" ? "active" : ""}>
fc1817aClaude188 Code
189 </a>
79136bbClaude190 <a
191 href={`/${owner}/${repo}/issues`}
192 class={active === "issues" ? "active" : ""}
193 >
194 Issues
195 </a>
c645a86Claude196 <a
197 href={`/${owner}/${repo}/discussions`}
198 class={active === "discussions" ? "active" : ""}
199 >
200 Discussions
201 </a>
1e162a8Claude202 <a
203 href={`/${owner}/${repo}/wiki`}
204 class={active === "wiki" ? "active" : ""}
205 >
206 Wiki
207 </a>
0074234Claude208 <a
209 href={`/${owner}/${repo}/pulls`}
210 class={active === "pulls" ? "active" : ""}
211 >
212 Pull Requests
213 </a>
1e162a8Claude214 <a
215 href={`/${owner}/${repo}/projects`}
216 class={active === "projects" ? "active" : ""}
217 >
218 Projects
219 </a>
fc1817aClaude220 <a
221 href={`/${owner}/${repo}/commits`}
222 class={active === "commits" ? "active" : ""}
223 >
224 Commits
225 </a>
eafe8c6Claude226 <a
227 href={`/${owner}/${repo}/actions`}
228 class={active === "actions" ? "active" : ""}
229 >
230 Actions
231 </a>
3ef4c9dClaude232 <a
233 href={`/${owner}/${repo}/releases`}
234 class={active === "releases" ? "active" : ""}
235 >
236 Releases
237 </a>
0224546Claude238 <a
239 href={`/${owner}/${repo}/contributors`}
240 class={active === "contributors" ? "active" : ""}
241 >
242 Contributors
243 </a>
244 <a
245 href={`/${owner}/${repo}/pulse`}
246 class={active === "pulse" ? "active" : ""}
247 >
248 Pulse
249 </a>
250 {currentUser && repoOwner && currentUser === repoOwner && (
251 <a
252 href={`/${owner}/${repo}/traffic`}
253 class={active === "traffic" ? "active" : ""}
254 >
255 Traffic
256 </a>
257 )}
3ef4c9dClaude258 <a
259 href={`/${owner}/${repo}/gates`}
260 class={active === "gates" ? "active" : ""}
261 >
262 {"\u25CF"} Gates
263 </a>
da3fc18Claude264 <a
265 href={`/${owner}/${repo}/security/vulnerabilities`}
266 class={active === "security" ? "active" : ""}
267 >
268 Security
269 </a>
c9ed210Claude270 <a
271 href={`/${owner}/${repo}/cloud-deployments`}
272 class={active === "deployments" ? "active" : ""}
273 >
274 Deployments
275 </a>
2c61840Claude276 <a
277 href={`/${owner}/${repo}/pipeline`}
278 class={active === "pipeline" ? "active" : ""}
279 >
280 Pipeline
281 </a>
3ef4c9dClaude282 <a
283 href={`/${owner}/${repo}/insights`}
284 class={active === "insights" ? "active" : ""}
285 >
286 Insights
287 </a>
ef3fd93Claude288 <a
289 href={`/${owner}/${repo}/agents`}
290 class={active === "agents" ? "active" : ""}
291 >
292 Agents
293 </a>
3cbe3d6Claude294 <a
295 href={`/${owner}/${repo}/explain`}
debcf27Claude296 class={`repo-nav-ai${active === "explain" ? " active" : ""}`}
297 style="margin-left: auto"
3cbe3d6Claude298 >
299 {"\u2728"} Explain
300 </a>
debcf27Claude301 <a href={`/${owner}/${repo}/ask`} class="repo-nav-ai">
3ef4c9dClaude302 {"\u2728"} Ask AI
303 </a>
2c61840Claude304 <a
305 href={`/${owner}/${repo}/workspace`}
306 class={`repo-nav-ai${active === "workspace" ? " active" : ""}`}
307 title="AI Workspace — spec-to-PR, fix issues with AI, web editor"
308 >
309 {"✨"} Workspace
310 </a>
14c3cc8Claude311 <a
312 href={`/${owner}/${repo}/spec`}
debcf27Claude313 class="repo-nav-ai"
14c3cc8Claude314 title="Spec to PR — paste a feature spec, AI opens a draft PR"
315 >
316 {"\u2728"} Spec
317 </a>
d8ef5efClaude318 <a
319 href={`/${owner}/${repo}/ai/tests`}
debcf27Claude320 class="repo-nav-ai"
d8ef5efClaude321 title="AI Tests \u2014 generate failing test stubs from a source file"
322 >
323 {"\u2728"} Tests
324 </a>
bcc4020Claude325 <a
326 href={`/${owner}/${repo}/debt-map`}
327 class={`repo-nav-ai${active === "debt-map" ? " active" : ""}`}
328 title="AI Debt Map \u2014 visual technical debt graph with Claude analysis"
329 >
330 {"\u2593"} Debt Map
331 </a>
77cf834Claude332 <a
333 href={`/${owner}/${repo}/search/nl`}
334 class={`repo-nav-ai${active === "nl-search" ? " active" : ""}`}
335 title="Natural Language Search \u2014 search by intent, not keywords"
336 >
337 {"\u2728"} NL Search
338 </a>
b1070a5Claude339 <a
340 href={`/${owner}/${repo}/archaeology`}
341 class={`repo-nav-ai${active === "archaeology" ? " active" : ""}`}
342 title="AI Archaeology \u2014 excavate why any file exists using git history, PRs, and issues"
343 >
344 {"\ud83c\udfdb"} Archaeology
345 </a>
fc1817aClaude346 </div>
347);
348
06d5ffeClaude349export const BranchSwitcher: FC<{
350 owner: string;
351 repo: string;
352 currentRef: string;
353 branches: string[];
354 pathType: "tree" | "blob" | "commits";
355 subPath?: string;
356}> = ({ owner, repo, currentRef, branches, pathType, subPath }) => {
357 if (branches.length <= 1) {
358 return <div class="branch-selector">{currentRef}</div>;
359 }
360
361 return (
362 <div class="branch-dropdown">
363 <button class="branch-selector" type="button">
364 {currentRef} &#9662;
365 </button>
366 <div class="branch-dropdown-content">
367 {branches.map((branch) => {
368 let href: string;
369 if (pathType === "commits") {
370 href = `/${owner}/${repo}/commits/${branch}`;
371 } else if (subPath) {
372 href = `/${owner}/${repo}/${pathType}/${branch}/${subPath}`;
373 } else {
374 href = `/${owner}/${repo}/tree/${branch}`;
375 }
376 return (
377 <a
378 href={href}
379 class={branch === currentRef ? "active-branch" : ""}
380 >
381 {branch}
382 </a>
383 );
384 })}
385 </div>
386 </div>
387 );
388};
389
fc1817aClaude390export const Breadcrumb: FC<{
391 owner: string;
392 repo: string;
393 ref: string;
394 path: string;
395}> = ({ owner, repo, ref, path }) => {
396 const parts = path.split("/").filter(Boolean);
397 const crumbs: { name: string; href: string }[] = [
398 { name: repo, href: `/${owner}/${repo}/tree/${ref}` },
399 ];
400 let accumulated = "";
401 for (const part of parts) {
402 accumulated += (accumulated ? "/" : "") + part;
403 crumbs.push({
404 name: part,
405 href: `/${owner}/${repo}/tree/${ref}/${accumulated}`,
406 });
407 }
408 return (
409 <div class="breadcrumb">
410 {crumbs.map((crumb, i) => (
411 <>
412 {i > 0 && <span>/</span>}
413 {i === crumbs.length - 1 ? (
414 <strong>{crumb.name}</strong>
415 ) : (
416 <a href={crumb.href}>{crumb.name}</a>
417 )}
418 </>
419 ))}
420 </div>
421 );
422};
423
424export const FileTable: FC<{
425 entries: GitTreeEntry[];
426 owner: string;
427 repo: string;
428 ref: string;
429 path: string;
430}> = ({ entries, owner, repo, ref, path }) => (
431 <table class="file-table">
432 <tbody>
433 {entries.map((entry) => {
434 const fullPath = path ? `${path}/${entry.name}` : entry.name;
435 const href =
436 entry.type === "tree"
437 ? `/${owner}/${repo}/tree/${ref}/${fullPath}`
438 : `/${owner}/${repo}/blob/${ref}/${fullPath}`;
439 return (
440 <tr>
441 <td class="file-icon">
442 {entry.type === "tree" ? "\u{1F4C1}" : "\u{1F4C4}"}
443 </td>
444 <td class="file-name">
445 <a href={href}>{entry.name}</a>
446 </td>
447 <td style="text-align: right; color: var(--text-muted); font-size: 13px;">
448 {entry.size !== undefined ? formatSize(entry.size) : ""}
449 </td>
450 </tr>
451 );
452 })}
453 </tbody>
454 </table>
455);
456
06d5ffeClaude457export const HighlightedCode: FC<{
458 highlightedHtml: string;
459 lineCount: number;
460}> = ({ highlightedHtml, lineCount }) => {
461 const lineNums = Array.from({ length: lineCount }, (_, i) => i + 1);
462 return (
463 <div class="blob-code">
464 <table>
465 <tbody>
466 <tr>
467 <td class="line-num" style="vertical-align: top; padding-top: 0; padding-bottom: 0">
468 <pre style="margin: 0; line-height: 1.6; font-size: 13px">
469 {lineNums.map((n) => (
470 <>
471 <span>{n}</span>
472 {"\n"}
473 </>
474 ))}
475 </pre>
476 </td>
477 <td class="line-content" style="vertical-align: top; padding-top: 0; padding-bottom: 0">
478 <pre style="margin: 0; line-height: 1.6; font-size: 13px">{html([highlightedHtml] as unknown as TemplateStringsArray)}</pre>
479 </td>
480 </tr>
481 </tbody>
482 </table>
483 </div>
484 );
485};
486
487export const PlainCode: FC<{ lines: string[] }> = ({ lines }) => (
488 <div class="blob-code">
489 <table>
490 <tbody>
491 {lines.map((line, i) => (
492 <tr>
493 <td class="line-num">{i + 1}</td>
494 <td class="line-content">{line}</td>
495 </tr>
496 ))}
497 </tbody>
498 </table>
499 </div>
500);
501
fc1817aClaude502export const CommitList: FC<{
503 commits: GitCommit[];
504 owner: string;
505 repo: string;
3951454Claude506 verifications?: Record<string, { verified: boolean; reason: string }>;
507}> = ({ commits, owner, repo, verifications }) => (
fc1817aClaude508 <div class="commit-list">
3951454Claude509 {commits.map((commit) => {
510 const v = verifications?.[commit.sha];
511 return (
512 <div class="commit-item">
513 <div>
514 <div class="commit-message">
515 <a href={`/${owner}/${repo}/commit/${commit.sha}`}>
516 {commit.message}
517 </a>
518 {v?.verified && (
519 <span
520 title="Signed with a registered key"
521 style="margin-left:8px;font-size:10px;padding:1px 6px;border-radius:3px;background:var(--green,#2ea043);color:#fff;text-transform:uppercase;letter-spacing:.4px"
522 >
523 Verified
524 </span>
525 )}
526 </div>
527 <div class="commit-meta">
528 {commit.author} committed {formatRelativeDate(commit.date)}
529 </div>
fc1817aClaude530 </div>
3951454Claude531 <a
532 href={`/${owner}/${repo}/commit/${commit.sha}`}
533 class="commit-sha"
534 >
535 {commit.sha.slice(0, 7)}
536 </a>
fc1817aClaude537 </div>
3951454Claude538 );
539 })}
fc1817aClaude540 </div>
541);
542
82c3a38ccanty labs543// ---- Diff viewer ----------------------------------------------------------
544
545const _diffScript = `
546function diffToggleView(id,mode){
547 var v=document.getElementById(id);if(!v)return;
548 v.querySelectorAll('.diff-table-inline').forEach(function(t){t.hidden=(mode==='split');});
549 v.querySelectorAll('.diff-table-split').forEach(function(t){t.hidden=(mode!=='split');});
550 v.querySelectorAll('.diff-view-btn').forEach(function(b){b.classList.remove('active');});
551 var btn=v.querySelector('.diff-view-btn-'+mode);if(btn)btn.classList.add('active');
552}
553function diffToggleFile(id){
554 var body=document.getElementById(id);if(!body)return;
555 body.hidden=!body.hidden;
556 var hdr=body.previousElementSibling;if(hdr)hdr.classList.toggle('collapsed',body.hidden);
557}
558`;
559
fc1817aClaude560export const DiffView: FC<{ raw: string; files: GitDiffFile[] }> = ({
561 raw,
562 files,
563}) => {
82c3a38ccanty labs564 const parsed = parseDiff(raw);
565 const totalAdd = files.reduce((s, f) => s + f.additions, 0);
566 const totalDel = files.reduce((s, f) => s + f.deletions, 0);
567
568 if (parsed.length === 0 && files.length === 0) {
569 return (
570 <div class="diff-view">
571 <p style="color:var(--text-muted);font-size:14px">No changes.</p>
572 </div>
573 );
574 }
fc1817aClaude575
576 return (
82c3a38ccanty labs577 <div class="diff-view" id="diff-view-main">
578 <script dangerouslySetInnerHTML={{ __html: _diffScript }} />
579
580 {/* Toolbar */}
581 <div class="diff-toolbar">
582 <div class="diff-summary">
583 <strong>{files.length}</strong> file{files.length !== 1 ? "s" : ""} changed,{" "}
584 <span class="stat-add">+{totalAdd}</span>{" "}
585 <span class="stat-del">-{totalDel}</span>
586 </div>
587 <div class="diff-view-toggle">
588 <button class="diff-view-btn diff-view-btn-inline active" onclick="diffToggleView('diff-view-main','inline')" type="button">Inline</button>
589 <button class="diff-view-btn diff-view-btn-split" onclick="diffToggleView('diff-view-main','split')" type="button">Split</button>
590 </div>
fc1817aClaude591 </div>
82c3a38ccanty labs592
593 {/* File jump list (shown when > 1 file) */}
594 {parsed.length > 1 && (
595 <div class="diff-jump-list">
596 {parsed.map((f, i) => {
597 const isAdd = f.additions > 0 && f.deletions === 0;
598 const isDel = f.additions === 0 && f.deletions > 0;
599 return (
600 <a class="diff-jump-item" href={`#diff-file-${i}`}>
601 <span class={isAdd ? "stat-add" : isDel ? "stat-del" : "diff-jump-mod"}>
602 {isAdd ? "+" : isDel ? "−" : "~"}
603 </span>{" "}
604 {f.path.split("/").pop() || f.path}
605 </a>
606 );
607 })}
fc1817aClaude608 </div>
82c3a38ccanty labs609 )}
610
611 {/* Per-file blocks */}
612 {parsed.map((file, idx) => {
613 const bodyId = `diff-body-${idx}`;
614 const splitRows = pairLines(file.lines);
615 return (
616 <div class="diff-file" id={`diff-file-${idx}`}>
617 <div class="diff-file-header" onclick={`diffToggleFile('${bodyId}')`}>
618 <span class="diff-file-path" title={file.path}>{file.path}</span>
619 <span class="diff-file-meta">
620 {file.isBinary
621 ? <span style="color:var(--text-muted)">binary</span>
622 : <><span class="stat-add">+{file.additions}</span>{" "}<span class="stat-del">-{file.deletions}</span></>
623 }
624 <span class="diff-file-chevron">▾</span>
625 </span>
626 </div>
627 <div id={bodyId}>
628 {file.isBinary ? (
629 <div class="diff-binary">Binary file changed</div>
630 ) : (
631 <>
632 {/* Inline table (default) */}
633 <table class="diff-table diff-table-inline">
634 <tbody>
635 {file.lines.map((line) => {
636 if (line.type === "hunk") {
637 return (
638 <tr class="diff-row diff-row-hunk">
639 <td class="diff-ln diff-ln-old"></td>
640 <td class="diff-ln diff-ln-new"></td>
641 <td class="diff-cell">{line.content}</td>
642 </tr>
643 );
644 }
645 const sigil = line.type === "add" ? "+" : line.type === "del" ? "-" : " ";
646 return (
647 <tr class={`diff-row diff-row-${line.type}${line.wsOnly ? " diff-row-ws" : ""}`}>
648 <td class="diff-ln diff-ln-old">{line.oldLine ?? ""}</td>
649 <td class="diff-ln diff-ln-new">{line.newLine ?? ""}</td>
650 <td class="diff-cell">
651 <span class="diff-sigil">{sigil}</span>
652 {line.content}
653 {line.wsOnly && <span class="diff-ws-badge" title="whitespace-only change">ws</span>}
654 </td>
655 </tr>
656 );
657 })}
658 </tbody>
659 </table>
660 {/* Split table (hidden until toggled) */}
661 <table class="diff-table diff-table-split" hidden>
662 <tbody>
663 {splitRows.map((row) => {
664 const isHunk = row.left?.type === "hunk" || row.right?.type === "hunk";
665 if (isHunk) {
666 const hunk = row.left ?? row.right;
667 return (
668 <tr class="diff-row diff-row-hunk">
669 <td class="diff-ln diff-ln-old"></td>
670 <td class="diff-cell">{hunk?.content ?? ""}</td>
671 <td class="diff-ln diff-ln-new"></td>
672 <td class="diff-cell"></td>
673 </tr>
674 );
675 }
676 const isCtx = row.left?.type === "ctx";
677 const L = row.left;
678 const R = row.right;
679 const leftCls = `diff-cell${L ? (isCtx ? " diff-cell-ctx" : " diff-cell-del") : " diff-cell-empty"}${L?.wsOnly ? " diff-cell-ws" : ""}`;
680 const rightCls = `diff-cell${R ? (isCtx ? " diff-cell-ctx" : " diff-cell-add") : " diff-cell-empty"}${R?.wsOnly ? " diff-cell-ws" : ""}`;
681 return (
682 <tr class="diff-row diff-split-row">
683 <td class="diff-ln diff-ln-old">{L?.oldLine ?? ""}</td>
684 <td class={leftCls}>
685 {L && <span class="diff-sigil">{isCtx ? " " : "-"}</span>}
686 {L?.content ?? ""}
687 {L?.wsOnly && <span class="diff-ws-badge" title="whitespace-only change">ws</span>}
688 </td>
689 <td class="diff-ln diff-ln-new">{R?.newLine ?? ""}</td>
690 <td class={rightCls}>
691 {R && <span class="diff-sigil">{isCtx ? " " : "+"}</span>}
692 {isCtx ? L?.content ?? "" : R?.content ?? ""}
693 {R?.wsOnly && !isCtx && <span class="diff-ws-badge" title="whitespace-only change">ws</span>}
694 </td>
695 </tr>
696 );
697 })}
698 </tbody>
699 </table>
700 </>
701 )}
702 </div>
703 </div>
704 );
705 })}
fc1817aClaude706 </div>
707 );
708};
709
06d5ffeClaude710export const RepoCard: FC<{ repo: Repository; ownerName: string }> = ({
711 repo,
712 ownerName,
713}) => (
714 <div class="card">
715 <h3>
716 <a href={`/${ownerName}/${repo.name}`}>{repo.name}</a>
717 </h3>
718 {repo.description && <p>{repo.description}</p>}
719 <div class="card-meta">
720 {repo.isPrivate && <span class="badge">Private</span>}
721 <span>{"\u2606"} {repo.starCount}</span>
722 {repo.pushedAt && (
723 <span>Updated {formatRelativeDate(repo.pushedAt.toString())}</span>
724 )}
725 </div>
726 </div>
727);
728
fc1817aClaude729function formatSize(bytes: number): string {
730 if (bytes < 1024) return `${bytes} B`;
731 if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
732 return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
733}
734
735function formatRelativeDate(dateStr: string): string {
736 const date = new Date(dateStr);
737 const now = new Date();
738 const diffMs = now.getTime() - date.getTime();
739 const diffMins = Math.floor(diffMs / 60000);
740 if (diffMins < 1) return "just now";
741 if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? "s" : ""} ago`;
742 const diffHours = Math.floor(diffMins / 60);
743 if (diffHours < 24)
744 return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`;
745 const diffDays = Math.floor(diffHours / 24);
746 if (diffDays < 30) return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`;
747 return date.toLocaleDateString("en-US", {
748 month: "short",
749 day: "numeric",
750 year: "numeric",
751 });
752}
6682dbeClaude753
754// ---------------------------------------------------------------------------
755// CiFailureProfileCard — Phase 2 Human-Agent Canvas
756// ---------------------------------------------------------------------------
757
758export interface CiTraceIteration {
759 iteration: number;
760 modelRoute: string;
761 errorDelta: string;
762 tokenCost: number;
763}
764
765export interface CiFailureProfileProps {
766 gateRunId: string;
767 failureTarget: string;
768 errorDelta: string;
769 mitigationHint: string;
770 traceLog: CiTraceIteration[];
771}
772
773export const CiFailureProfileCard: FC<CiFailureProfileProps> = ({
774 gateRunId,
775 failureTarget,
776 errorDelta,
777 mitigationHint,
778 traceLog,
779}) => (
780 <div class="ci-failure-profile-card">
781 <style>{`
782 .ci-failure-profile-card {
783 background: #1a1a2e;
784 border: 1px solid #2d2d4a;
785 border-radius: 8px;
786 padding: 16px;
787 margin: 12px 0;
788 font-family: monospace;
789 color: #e0e0ff;
790 }
791 .ci-failure-profile-card h3 {
792 margin: 0 0 12px;
793 color: #ff6b6b;
794 font-size: 14px;
795 text-transform: uppercase;
796 letter-spacing: 0.5px;
797 }
798 .ci-failure-profile-card .field-label {
799 color: #888;
800 font-size: 11px;
801 text-transform: uppercase;
802 margin-top: 10px;
803 }
804 .ci-failure-profile-card .field-value {
805 color: #e0e0ff;
806 font-size: 13px;
807 margin: 2px 0 6px;
808 white-space: pre-wrap;
809 word-break: break-word;
810 }
811 .ci-failure-profile-card .trace-row {
812 display: grid;
813 grid-template-columns: 40px 1fr 80px;
814 gap: 8px;
815 padding: 4px 0;
816 border-bottom: 1px solid #2d2d4a;
817 font-size: 12px;
818 }
819 .ci-failure-profile-card .feedback-row {
820 margin-top: 14px;
821 display: flex;
822 gap: 8px;
823 }
824 .ci-failure-profile-card .btn-feedback {
825 padding: 6px 12px;
826 border: none;
827 border-radius: 6px;
828 cursor: pointer;
829 font-size: 12px;
830 font-weight: bold;
831 }
832 .ci-failure-profile-card .btn-helpful {
833 background: #1a6b3c;
834 color: #7fff9e;
835 }
836 .ci-failure-profile-card .btn-hallucination {
837 background: #6b1a1a;
838 color: #ff9e9e;
839 }
840 `}</style>
841 <h3>🔍 CI Failure Profile</h3>
842 <div class="field-label">Failure Target</div>
843 <div class="field-value">{failureTarget}</div>
844 <div class="field-label">Error Delta</div>
845 <div class="field-value">{errorDelta}</div>
846 <div class="field-label">Mitigation Hint</div>
847 <div class="field-value">{mitigationHint}</div>
848 {traceLog.length > 0 && (
849 <>
850 <div class="field-label" style="margin-top:12px">Model Trace</div>
851 {traceLog.map((t) => (
852 <div class="trace-row" key={t.iteration}>
853 <span>#{t.iteration}</span>
854 <span>{t.modelRoute}</span>
855 <span>{t.tokenCost}¢</span>
856 </div>
857 ))}
858 </>
859 )}
860 <div class="feedback-row">
861 <form method="post" action="/api/flywheel-telemetry/feedback" style="display:inline">
862 <input type="hidden" name="gateRunId" value={gateRunId} />
863 <input type="hidden" name="verdict" value="helpful" />
864 <button type="submit" class="btn-feedback btn-helpful">
865 👍 Diagnostic Helpful
866 </button>
867 </form>
868 <form method="post" action="/api/flywheel-telemetry/feedback" style="display:inline">
869 <input type="hidden" name="gateRunId" value={gateRunId} />
870 <input type="hidden" name="verdict" value="hallucination" />
871 <button type="submit" class="btn-feedback btn-hallucination">
872 👎 Hallucination Flagged
873 </button>
874 </form>
875 </div>
876 </div>
877);