Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions frontend/src/components/insights/ShareBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,47 @@ function formatTimestamp(value: string | null | undefined): string | null {
return date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
}

export function ShareBanner({ window, share, isCreating, onCreateShare }: ShareBannerProps) {
export function ShareBanner({ window: insightWindow, share, isCreating, onCreateShare }: ShareBannerProps) {
const toast = useToast();
const browserWindow =
typeof globalThis !== "undefined" && typeof (globalThis as { window?: Window }).window !== "undefined"
? (globalThis as { window: Window }).window
: null;

const generatedLabel = useMemo(() => formatTimestamp(share?.generated_at), [share?.generated_at]);
const expiresLabel = useMemo(() => formatTimestamp(share?.cache_expires_at), [share?.cache_expires_at]);

const resolvedShareUrl = useMemo(() => {
if (!share?.url) {
return null;
}
if (!browserWindow) {
return share.url;
}
try {
const currentOrigin = browserWindow.location.origin;
const target = share.url.startsWith("http") ? new URL(share.url) : new URL(share.url, currentOrigin);
if (target.origin !== currentOrigin) {
target.protocol = browserWindow.location.protocol;
target.host = browserWindow.location.host;
}
return target.toString();
} catch {
return share.url;
}
}, [browserWindow, share?.url]);

const handleCopy = useCallback(async () => {
if (!share?.url) {
return;
}
const url = resolvedShareUrl ?? share.url;
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(share.url);
await navigator.clipboard.writeText(url);
} else {
const tempInput = document.createElement("input");
tempInput.value = share.url;
tempInput.value = url;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand("copy");
Expand All @@ -56,7 +81,7 @@ export function ShareBanner({ window, share, isCreating, onCreateShare }: ShareB
variant: "error",
});
}
}, [share?.url, toast]);
}, [resolvedShareUrl, share?.url, toast]);

return (
<section className="rounded-2xl border border-border-glass/60 bg-surface-panel/60 p-4">
Expand All @@ -65,7 +90,7 @@ export function ShareBanner({ window, share, isCreating, onCreateShare }: ShareB
<p className="text-xs uppercase tracking-[0.28em] text-text-secondary/70">Share timeline</p>
<h2 className="text-lg font-semibold text-text-primary">Generate a read-only link</h2>
<p className="text-sm text-text-secondary/80">
Share the {window === "7d" ? "seven day" : "twenty four hour"} insight snapshot with stakeholders without granting dashboard access.
Share the {insightWindow === "7d" ? "seven day" : "twenty four hour"} insight snapshot with stakeholders without granting dashboard access.
</p>
{share ? (
<div className="flex flex-col gap-1 text-xs text-text-secondary/70">
Expand All @@ -78,8 +103,8 @@ export function ShareBanner({ window, share, isCreating, onCreateShare }: ShareB
<div className="flex w-full flex-col gap-3 sm:w-auto sm:min-w-[22rem]">
{share ? (
<div className="flex items-center gap-2 rounded-xl border border-border-glass/60 bg-surface-glass/50 px-3 py-2 text-sm">
<span className="truncate" title={share.url}>
{share.url}
<span className="truncate" title={resolvedShareUrl ?? share.url}>
{resolvedShareUrl ?? share.url}
</span>
<button
type="button"
Expand All @@ -95,7 +120,7 @@ export function ShareBanner({ window, share, isCreating, onCreateShare }: ShareB
<button
type="button"
onClick={() => {
void onCreateShare(window);
void onCreateShare(insightWindow);
}}
className={cn(
"inline-flex items-center justify-center rounded-full border border-border-glass/70 px-4 py-2 text-sm font-semibold transition",
Expand Down
21 changes: 16 additions & 5 deletions frontend/src/utils/insightsReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,38 @@ function safeTopLabels(labels: InsightTopLabel[]): InsightTopLabel[] {
return labels.slice(0, 5);
}

function describeSeverityCategory(value: number | null | undefined): string {
if (value === null || value === undefined) {
return "Mixed";
}
if (value >= 1.5) {
return "High";
}
if (value >= 0.5) {
return "Medium";
}
return "Low";
}

function renderTopLabels(labels: InsightTopLabel[]): string {
if (!labels.length) {
return '<p class="section-body muted">No top labels captured in this window.</p>';
}
const items = labels
.map((label) => {
const avgSeverity = label.avg_severity !== null && label.avg_severity !== undefined
? `${label.avg_severity.toFixed(1)}`
: "-";
const dominantSeverity = describeSeverityCategory(label.avg_severity);
return `<tr>
<td class="label-name">${escapeHtml(label.label)}</td>
<td class="label-count">${label.count}</td>
<td class="label-severity">${avgSeverity}</td>
<td class="label-severity">${escapeHtml(dominantSeverity)}</td>
</tr>`;
})
.join("");
return `
<div class="table-container">
<table aria-label="Top labels table">
<thead>
<tr><th scope="col">Label</th><th scope="col">Count</th><th scope="col">Avg. Severity</th></tr>
<tr><th scope="col">Label</th><th scope="col">Count</th><th scope="col">Dominant Severity</th></tr>
</thead>
<tbody>${items}</tbody>
</table>
Expand Down
4 changes: 2 additions & 2 deletions frontend/tests/insights/insights-report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ describe("buildInsightReportDocument", () => {

expect(html).toContain("ClipNotes Insight Report");
expect(html).toContain("Severity distribution");
expect(html).toContain("Top focus areas");
expect(html).toContain("Top focus areas");
expect(html).toContain("Timeline breakdown");
expect(html).toContain("Latency");
expect(html).toContain("High");
expect(html).toContain("Dominant Severity");
});

it("escapes unsafe characters in rendered content", () => {
Expand Down