Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
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.tsxBlame777 lines · 1 contributor
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";
fc1817aClaude5
8c790e0Claude6/**
7 * Describes the most recent push to a repo, used by RepoHeader to render
8 * the Push Watch discoverability indicator.
9 *
10 * - ageMs < 5 min \u2192 pulsing red "\u25cf Live" badge
11 * - ageMs < 24 hr \u2192 dimmer "\u25cb Watch" link
12 * - otherwise \u2192 nothing shown
13 */
14export interface RecentPush {
15 sha: string;
16 ageMs: number;
17}
18
06d5ffeClaude19export const RepoHeader: FC<{
20 owner: string;
21 repo: string;
22 starCount?: number;
23 starred?: boolean;
c81ab7aClaude24 forkCount?: number;
06d5ffeClaude25 currentUser?: string | null;
c81ab7aClaude26 forkedFrom?: string | null;
71cd5ecClaude27 archived?: boolean;
28 isTemplate?: boolean;
8c790e0Claude29 /** Most recent push info for Push Watch discoverability indicator. */
30 recentPush?: RecentPush | null;
77cf834Claude31 /** 0-100 health score badge rendered after the repo name. Optional — omit to hide. */
32 healthScore?: number;
71cd5ecClaude33}> = ({
34 owner,
35 repo,
36 starCount,
37 starred,
38 forkCount,
39 currentUser,
40 forkedFrom,
41 archived,
42 isTemplate,
8c790e0Claude43 recentPush,
77cf834Claude44 healthScore,
8c790e0Claude45}) => {
46 const FIVE_MIN = 5 * 60 * 1000;
47 const TWENTY_FOUR_HR = 24 * 60 * 60 * 1000;
48 const isLive = recentPush != null && recentPush.ageMs < FIVE_MIN;
49 const isRecent = recentPush != null && recentPush.ageMs < TWENTY_FOUR_HR;
50
77cf834Claude51 const healthColor =
52 healthScore === undefined ? null :
53 healthScore >= 80 ? "#34d399" :
54 healthScore >= 50 ? "#facc15" :
55 "#f87171";
56
8c790e0Claude57 return (
58 <div class="repo-header">
59 <div>
60 <div class="repo-header-title">
61 <a href={`/${owner}`} class="owner">
62 {owner}
63 </a>
64 <span class="separator">/</span>
65 <a href={`/${owner}/${repo}`} class="name">
66 {repo}
67 </a>
68 {archived && (
69 <span
70 class="repo-header-pill repo-header-pill-archived"
71 title="Read-only: pushes and new issues/PRs disabled"
72 >
73 Archived
74 </span>
75 )}
76 {isTemplate && (
77 <span
78 class="repo-header-pill repo-header-pill-template"
79 title="This repository can be used as a template"
80 >
81 Template
82 </span>
83 )}
77cf834Claude84 {healthScore !== undefined && healthColor && (
85 <a
86 href={`/${owner}/${repo}/health`}
87 title={`Repository health score: ${healthScore}/100 — click for breakdown`}
88 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;`}
89 >
90 &#x2665; Health {healthScore}
91 </a>
92 )}
8c790e0Claude93 {isLive && recentPush && (
94 <a
95 href={`/${owner}/${repo}/push/${recentPush.sha}`}
96 class="repo-header-live-badge repo-header-live-badge--live"
97 title="Push in progress \u2014 watch live gate + deploy status"
98 aria-label="Live push \u2014 click to watch status"
99 >
100 <span class="repo-header-live-dot" aria-hidden="true">{"\u25cf"}</span>
101 Live
102 </a>
103 )}
104 {!isLive && isRecent && recentPush && (
105 <a
106 href={`/${owner}/${repo}/push/${recentPush.sha}`}
107 class="repo-header-live-badge repo-header-live-badge--recent"
108 title="Watch the most recent push's gate + deploy results"
109 aria-label="Watch most recent push"
110 >
111 <span aria-hidden="true">{"\u25cb"}</span>
112 Watch
113 </a>
114 )}
115 </div>
116 {forkedFrom && (
117 <div class="repo-header-fork">
118 forked from <a href={`/${forkedFrom}`}>{forkedFrom}</a>
119 </div>
71cd5ecClaude120 )}
c81ab7aClaude121 </div>
8c790e0Claude122 <div class="repo-header-actions">
123 {currentUser && currentUser !== owner && (
124 <form method="post" action={`/${owner}/${repo}/fork`} style="display:inline">
125 <button type="submit" class="star-btn">
126 {"\u2442"} Fork {forkCount !== undefined && forkCount > 0 ? forkCount : ""}
06d5ffeClaude127 </button>
128 </form>
8c790e0Claude129 )}
130 {starCount !== undefined && (
131 currentUser ? (
132 <form method="post" action={`/${owner}/${repo}/star`} style="display:inline">
133 <button
134 type="submit"
135 class={`star-btn${starred ? " starred" : ""}`}
136 >
137 {starred ? "\u2605" : "\u2606"} {starCount}
138 </button>
139 </form>
140 ) : (
141 <span class="star-btn">
142 {"\u2606"} {starCount}
143 </span>
144 )
145 )}
146 </div>
06d5ffeClaude147 </div>
8c790e0Claude148 );
149};
fc1817aClaude150
151export const RepoNav: FC<{
152 owner: string;
153 repo: string;
3ef4c9dClaude154 active:
155 | "code"
156 | "commits"
157 | "issues"
158 | "pulls"
159 | "releases"
eafe8c6Claude160 | "actions"
3ef4c9dClaude161 | "gates"
3cbe3d6Claude162 | "insights"
163 | "explain"
164 | "changelog"
1e162a8Claude165 | "semantic"
166 | "wiki"
0316dbbClaude167 | "projects"
ef3fd93Claude168 | "agents"
c645a86Claude169 | "discussions"
da3fc18Claude170 | "security"
bcc4020Claude171 | "settings"
7f992cdClaude172 | "debt-map"
173 | "migrate"
77cf834Claude174 | "deployments"
0224546Claude175 | "nl-search"
176 | "contributors"
177 | "pulse"
178 | "traffic";
179 /** Current authenticated user — used for owner-only tab gating. */
180 currentUser?: string | null;
181 /** Repo owner username — used for owner-only tab gating. */
182 repoOwner?: string;
183}> = ({ owner, repo, active, currentUser, repoOwner }) => (
fc1817aClaude184 <div class="repo-nav">
06d5ffeClaude185 <a href={`/${owner}/${repo}`} class={active === "code" ? "active" : ""}>
fc1817aClaude186 Code
187 </a>
79136bbClaude188 <a
189 href={`/${owner}/${repo}/issues`}
190 class={active === "issues" ? "active" : ""}
191 >
192 Issues
193 </a>
c645a86Claude194 <a
195 href={`/${owner}/${repo}/discussions`}
196 class={active === "discussions" ? "active" : ""}
197 >
198 Discussions
199 </a>
1e162a8Claude200 <a
201 href={`/${owner}/${repo}/wiki`}
202 class={active === "wiki" ? "active" : ""}
203 >
204 Wiki
205 </a>
0074234Claude206 <a
207 href={`/${owner}/${repo}/pulls`}
208 class={active === "pulls" ? "active" : ""}
209 >
210 Pull Requests
211 </a>
1e162a8Claude212 <a
213 href={`/${owner}/${repo}/projects`}
214 class={active === "projects" ? "active" : ""}
215 >
216 Projects
217 </a>
fc1817aClaude218 <a
219 href={`/${owner}/${repo}/commits`}
220 class={active === "commits" ? "active" : ""}
221 >
222 Commits
223 </a>
eafe8c6Claude224 <a
225 href={`/${owner}/${repo}/actions`}
226 class={active === "actions" ? "active" : ""}
227 >
228 Actions
229 </a>
3ef4c9dClaude230 <a
231 href={`/${owner}/${repo}/releases`}
232 class={active === "releases" ? "active" : ""}
233 >
234 Releases
235 </a>
0224546Claude236 <a
237 href={`/${owner}/${repo}/contributors`}
238 class={active === "contributors" ? "active" : ""}
239 >
240 Contributors
241 </a>
242 <a
243 href={`/${owner}/${repo}/pulse`}
244 class={active === "pulse" ? "active" : ""}
245 >
246 Pulse
247 </a>
248 {currentUser && repoOwner && currentUser === repoOwner && (
249 <a
250 href={`/${owner}/${repo}/traffic`}
251 class={active === "traffic" ? "active" : ""}
252 >
253 Traffic
254 </a>
255 )}
3ef4c9dClaude256 <a
257 href={`/${owner}/${repo}/gates`}
258 class={active === "gates" ? "active" : ""}
259 >
260 {"\u25CF"} Gates
261 </a>
da3fc18Claude262 <a
263 href={`/${owner}/${repo}/security/vulnerabilities`}
264 class={active === "security" ? "active" : ""}
265 >
266 Security
267 </a>
c9ed210Claude268 <a
269 href={`/${owner}/${repo}/cloud-deployments`}
270 class={active === "deployments" ? "active" : ""}
271 >
272 Deployments
273 </a>
2c61840Claude274 <a
275 href={`/${owner}/${repo}/pipeline`}
276 class={active === "pipeline" ? "active" : ""}
277 >
278 Pipeline
279 </a>
3ef4c9dClaude280 <a
281 href={`/${owner}/${repo}/insights`}
282 class={active === "insights" ? "active" : ""}
283 >
284 Insights
285 </a>
ef3fd93Claude286 <a
287 href={`/${owner}/${repo}/agents`}
288 class={active === "agents" ? "active" : ""}
289 >
290 Agents
291 </a>
3cbe3d6Claude292 <a
293 href={`/${owner}/${repo}/explain`}
debcf27Claude294 class={`repo-nav-ai${active === "explain" ? " active" : ""}`}
295 style="margin-left: auto"
3cbe3d6Claude296 >
297 {"\u2728"} Explain
298 </a>
debcf27Claude299 <a href={`/${owner}/${repo}/ask`} class="repo-nav-ai">
3ef4c9dClaude300 {"\u2728"} Ask AI
301 </a>
2c61840Claude302 <a
303 href={`/${owner}/${repo}/workspace`}
304 class={`repo-nav-ai${active === "workspace" ? " active" : ""}`}
305 title="AI Workspace — spec-to-PR, fix issues with AI, web editor"
306 >
307 {"✨"} Workspace
308 </a>
14c3cc8Claude309 <a
310 href={`/${owner}/${repo}/spec`}
debcf27Claude311 class="repo-nav-ai"
14c3cc8Claude312 title="Spec to PR — paste a feature spec, AI opens a draft PR"
313 >
314 {"\u2728"} Spec
315 </a>
d8ef5efClaude316 <a
317 href={`/${owner}/${repo}/ai/tests`}
debcf27Claude318 class="repo-nav-ai"
d8ef5efClaude319 title="AI Tests \u2014 generate failing test stubs from a source file"
320 >
321 {"\u2728"} Tests
322 </a>
bcc4020Claude323 <a
324 href={`/${owner}/${repo}/debt-map`}
325 class={`repo-nav-ai${active === "debt-map" ? " active" : ""}`}
326 title="AI Debt Map \u2014 visual technical debt graph with Claude analysis"
327 >
328 {"\u2593"} Debt Map
329 </a>
77cf834Claude330 <a
331 href={`/${owner}/${repo}/search/nl`}
332 class={`repo-nav-ai${active === "nl-search" ? " active" : ""}`}
333 title="Natural Language Search \u2014 search by intent, not keywords"
334 >
335 {"\u2728"} NL Search
336 </a>
b1070a5Claude337 <a
338 href={`/${owner}/${repo}/archaeology`}
339 class={`repo-nav-ai${active === "archaeology" ? " active" : ""}`}
340 title="AI Archaeology \u2014 excavate why any file exists using git history, PRs, and issues"
341 >
342 {"\ud83c\udfdb"} Archaeology
343 </a>
fc1817aClaude344 </div>
345);
346
06d5ffeClaude347export const BranchSwitcher: FC<{
348 owner: string;
349 repo: string;
350 currentRef: string;
351 branches: string[];
352 pathType: "tree" | "blob" | "commits";
353 subPath?: string;
354}> = ({ owner, repo, currentRef, branches, pathType, subPath }) => {
355 if (branches.length <= 1) {
356 return <div class="branch-selector">{currentRef}</div>;
357 }
358
359 return (
360 <div class="branch-dropdown">
361 <button class="branch-selector" type="button">
362 {currentRef} &#9662;
363 </button>
364 <div class="branch-dropdown-content">
365 {branches.map((branch) => {
366 let href: string;
367 if (pathType === "commits") {
368 href = `/${owner}/${repo}/commits/${branch}`;
369 } else if (subPath) {
370 href = `/${owner}/${repo}/${pathType}/${branch}/${subPath}`;
371 } else {
372 href = `/${owner}/${repo}/tree/${branch}`;
373 }
374 return (
375 <a
376 href={href}
377 class={branch === currentRef ? "active-branch" : ""}
378 >
379 {branch}
380 </a>
381 );
382 })}
383 </div>
384 </div>
385 );
386};
387
fc1817aClaude388export const Breadcrumb: FC<{
389 owner: string;
390 repo: string;
391 ref: string;
392 path: string;
393}> = ({ owner, repo, ref, path }) => {
394 const parts = path.split("/").filter(Boolean);
395 const crumbs: { name: string; href: string }[] = [
396 { name: repo, href: `/${owner}/${repo}/tree/${ref}` },
397 ];
398 let accumulated = "";
399 for (const part of parts) {
400 accumulated += (accumulated ? "/" : "") + part;
401 crumbs.push({
402 name: part,
403 href: `/${owner}/${repo}/tree/${ref}/${accumulated}`,
404 });
405 }
406 return (
407 <div class="breadcrumb">
408 {crumbs.map((crumb, i) => (
409 <>
410 {i > 0 && <span>/</span>}
411 {i === crumbs.length - 1 ? (
412 <strong>{crumb.name}</strong>
413 ) : (
414 <a href={crumb.href}>{crumb.name}</a>
415 )}
416 </>
417 ))}
418 </div>
419 );
420};
421
422export const FileTable: FC<{
423 entries: GitTreeEntry[];
424 owner: string;
425 repo: string;
426 ref: string;
427 path: string;
428}> = ({ entries, owner, repo, ref, path }) => (
429 <table class="file-table">
430 <tbody>
431 {entries.map((entry) => {
432 const fullPath = path ? `${path}/${entry.name}` : entry.name;
433 const href =
434 entry.type === "tree"
435 ? `/${owner}/${repo}/tree/${ref}/${fullPath}`
436 : `/${owner}/${repo}/blob/${ref}/${fullPath}`;
437 return (
438 <tr>
439 <td class="file-icon">
440 {entry.type === "tree" ? "\u{1F4C1}" : "\u{1F4C4}"}
441 </td>
442 <td class="file-name">
443 <a href={href}>{entry.name}</a>
444 </td>
445 <td style="text-align: right; color: var(--text-muted); font-size: 13px;">
446 {entry.size !== undefined ? formatSize(entry.size) : ""}
447 </td>
448 </tr>
449 );
450 })}
451 </tbody>
452 </table>
453);
454
06d5ffeClaude455export const HighlightedCode: FC<{
456 highlightedHtml: string;
457 lineCount: number;
458}> = ({ highlightedHtml, lineCount }) => {
459 const lineNums = Array.from({ length: lineCount }, (_, i) => i + 1);
460 return (
461 <div class="blob-code">
462 <table>
463 <tbody>
464 <tr>
465 <td class="line-num" style="vertical-align: top; padding-top: 0; padding-bottom: 0">
466 <pre style="margin: 0; line-height: 1.6; font-size: 13px">
467 {lineNums.map((n) => (
468 <>
469 <span>{n}</span>
470 {"\n"}
471 </>
472 ))}
473 </pre>
474 </td>
475 <td class="line-content" style="vertical-align: top; padding-top: 0; padding-bottom: 0">
476 <pre style="margin: 0; line-height: 1.6; font-size: 13px">{html([highlightedHtml] as unknown as TemplateStringsArray)}</pre>
477 </td>
478 </tr>
479 </tbody>
480 </table>
481 </div>
482 );
483};
484
485export const PlainCode: FC<{ lines: string[] }> = ({ lines }) => (
486 <div class="blob-code">
487 <table>
488 <tbody>
489 {lines.map((line, i) => (
490 <tr>
491 <td class="line-num">{i + 1}</td>
492 <td class="line-content">{line}</td>
493 </tr>
494 ))}
495 </tbody>
496 </table>
497 </div>
498);
499
fc1817aClaude500export const CommitList: FC<{
501 commits: GitCommit[];
502 owner: string;
503 repo: string;
3951454Claude504 verifications?: Record<string, { verified: boolean; reason: string }>;
505}> = ({ commits, owner, repo, verifications }) => (
fc1817aClaude506 <div class="commit-list">
3951454Claude507 {commits.map((commit) => {
508 const v = verifications?.[commit.sha];
509 return (
510 <div class="commit-item">
511 <div>
512 <div class="commit-message">
513 <a href={`/${owner}/${repo}/commit/${commit.sha}`}>
514 {commit.message}
515 </a>
516 {v?.verified && (
517 <span
518 title="Signed with a registered key"
519 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"
520 >
521 Verified
522 </span>
523 )}
524 </div>
525 <div class="commit-meta">
526 {commit.author} committed {formatRelativeDate(commit.date)}
527 </div>
fc1817aClaude528 </div>
3951454Claude529 <a
530 href={`/${owner}/${repo}/commit/${commit.sha}`}
531 class="commit-sha"
532 >
533 {commit.sha.slice(0, 7)}
534 </a>
fc1817aClaude535 </div>
3951454Claude536 );
537 })}
fc1817aClaude538 </div>
539);
540
541export const DiffView: FC<{ raw: string; files: GitDiffFile[] }> = ({
542 raw,
543 files,
544}) => {
545 const sections = parseDiff(raw);
546
547 return (
548 <div class="diff-view">
549 <div style="margin-bottom: 16px; font-size: 14px; color: var(--text-muted);">
550 Showing{" "}
551 <strong style="color: var(--text)">{files.length}</strong> changed
552 file{files.length !== 1 ? "s" : ""} with{" "}
553 <span class="stat-add">
554 +{files.reduce((s, f) => s + f.additions, 0)}
555 </span>{" "}
556 and{" "}
557 <span class="stat-del">
558 -{files.reduce((s, f) => s + f.deletions, 0)}
559 </span>
560 </div>
561 {sections.map((section) => (
562 <div class="diff-file">
563 <div class="diff-file-header">{section.path}</div>
564 <div class="diff-content">
565 {section.lines.map((line) => {
566 let cls = "line";
567 if (line.startsWith("+")) cls += " line-add";
568 else if (line.startsWith("-")) cls += " line-del";
569 else if (line.startsWith("@@")) cls += " line-hunk";
570 return <span class={cls}>{line + "\n"}</span>;
571 })}
572 </div>
573 </div>
574 ))}
575 </div>
576 );
577};
578
06d5ffeClaude579export const RepoCard: FC<{ repo: Repository; ownerName: string }> = ({
580 repo,
581 ownerName,
582}) => (
583 <div class="card">
584 <h3>
585 <a href={`/${ownerName}/${repo.name}`}>{repo.name}</a>
586 </h3>
587 {repo.description && <p>{repo.description}</p>}
588 <div class="card-meta">
589 {repo.isPrivate && <span class="badge">Private</span>}
590 <span>{"\u2606"} {repo.starCount}</span>
591 {repo.pushedAt && (
592 <span>Updated {formatRelativeDate(repo.pushedAt.toString())}</span>
593 )}
594 </div>
595 </div>
596);
597
fc1817aClaude598function parseDiff(raw: string): Array<{ path: string; lines: string[] }> {
599 const sections: Array<{ path: string; lines: string[] }> = [];
600 const diffRegex = /^diff --git a\/(.+?) b\/.+$/;
601 let current: { path: string; lines: string[] } | null = null;
602
603 for (const line of raw.split("\n")) {
604 const match = line.match(diffRegex);
605 if (match) {
606 if (current) sections.push(current);
607 current = { path: match[1], lines: [] };
608 continue;
609 }
610 if (current && !line.startsWith("diff --git")) {
611 if (
612 line.startsWith("index ") ||
613 line.startsWith("--- ") ||
614 line.startsWith("+++ ") ||
615 line.startsWith("new file") ||
616 line.startsWith("deleted file") ||
617 line.startsWith("old mode") ||
618 line.startsWith("new mode")
619 ) {
620 continue;
621 }
622 current.lines.push(line);
623 }
624 }
625 if (current) sections.push(current);
626 return sections;
627}
628
629function formatSize(bytes: number): string {
630 if (bytes < 1024) return `${bytes} B`;
631 if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
632 return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
633}
634
635function formatRelativeDate(dateStr: string): string {
636 const date = new Date(dateStr);
637 const now = new Date();
638 const diffMs = now.getTime() - date.getTime();
639 const diffMins = Math.floor(diffMs / 60000);
640 if (diffMins < 1) return "just now";
641 if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? "s" : ""} ago`;
642 const diffHours = Math.floor(diffMins / 60);
643 if (diffHours < 24)
644 return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`;
645 const diffDays = Math.floor(diffHours / 24);
646 if (diffDays < 30) return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`;
647 return date.toLocaleDateString("en-US", {
648 month: "short",
649 day: "numeric",
650 year: "numeric",
651 });
652}
6682dbeClaude653
654// ---------------------------------------------------------------------------
655// CiFailureProfileCard — Phase 2 Human-Agent Canvas
656// ---------------------------------------------------------------------------
657
658export interface CiTraceIteration {
659 iteration: number;
660 modelRoute: string;
661 errorDelta: string;
662 tokenCost: number;
663}
664
665export interface CiFailureProfileProps {
666 gateRunId: string;
667 failureTarget: string;
668 errorDelta: string;
669 mitigationHint: string;
670 traceLog: CiTraceIteration[];
671}
672
673export const CiFailureProfileCard: FC<CiFailureProfileProps> = ({
674 gateRunId,
675 failureTarget,
676 errorDelta,
677 mitigationHint,
678 traceLog,
679}) => (
680 <div class="ci-failure-profile-card">
681 <style>{`
682 .ci-failure-profile-card {
683 background: #1a1a2e;
684 border: 1px solid #2d2d4a;
685 border-radius: 8px;
686 padding: 16px;
687 margin: 12px 0;
688 font-family: monospace;
689 color: #e0e0ff;
690 }
691 .ci-failure-profile-card h3 {
692 margin: 0 0 12px;
693 color: #ff6b6b;
694 font-size: 14px;
695 text-transform: uppercase;
696 letter-spacing: 0.5px;
697 }
698 .ci-failure-profile-card .field-label {
699 color: #888;
700 font-size: 11px;
701 text-transform: uppercase;
702 margin-top: 10px;
703 }
704 .ci-failure-profile-card .field-value {
705 color: #e0e0ff;
706 font-size: 13px;
707 margin: 2px 0 6px;
708 white-space: pre-wrap;
709 word-break: break-word;
710 }
711 .ci-failure-profile-card .trace-row {
712 display: grid;
713 grid-template-columns: 40px 1fr 80px;
714 gap: 8px;
715 padding: 4px 0;
716 border-bottom: 1px solid #2d2d4a;
717 font-size: 12px;
718 }
719 .ci-failure-profile-card .feedback-row {
720 margin-top: 14px;
721 display: flex;
722 gap: 8px;
723 }
724 .ci-failure-profile-card .btn-feedback {
725 padding: 6px 12px;
726 border: none;
727 border-radius: 6px;
728 cursor: pointer;
729 font-size: 12px;
730 font-weight: bold;
731 }
732 .ci-failure-profile-card .btn-helpful {
733 background: #1a6b3c;
734 color: #7fff9e;
735 }
736 .ci-failure-profile-card .btn-hallucination {
737 background: #6b1a1a;
738 color: #ff9e9e;
739 }
740 `}</style>
741 <h3>🔍 CI Failure Profile</h3>
742 <div class="field-label">Failure Target</div>
743 <div class="field-value">{failureTarget}</div>
744 <div class="field-label">Error Delta</div>
745 <div class="field-value">{errorDelta}</div>
746 <div class="field-label">Mitigation Hint</div>
747 <div class="field-value">{mitigationHint}</div>
748 {traceLog.length > 0 && (
749 <>
750 <div class="field-label" style="margin-top:12px">Model Trace</div>
751 {traceLog.map((t) => (
752 <div class="trace-row" key={t.iteration}>
753 <span>#{t.iteration}</span>
754 <span>{t.modelRoute}</span>
755 <span>{t.tokenCost}¢</span>
756 </div>
757 ))}
758 </>
759 )}
760 <div class="feedback-row">
761 <form method="post" action="/api/flywheel-telemetry/feedback" style="display:inline">
762 <input type="hidden" name="gateRunId" value={gateRunId} />
763 <input type="hidden" name="verdict" value="helpful" />
764 <button type="submit" class="btn-feedback btn-helpful">
765 👍 Diagnostic Helpful
766 </button>
767 </form>
768 <form method="post" action="/api/flywheel-telemetry/feedback" style="display:inline">
769 <input type="hidden" name="gateRunId" value={gateRunId} />
770 <input type="hidden" name="verdict" value="hallucination" />
771 <button type="submit" class="btn-feedback btn-hallucination">
772 👎 Hallucination Flagged
773 </button>
774 </form>
775 </div>
776 </div>
777);