From 015d5da8222929cd05dec0c3250eaa3b21bb88db Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Wed, 1 Apr 2026 03:17:49 +0200 Subject: [PATCH] feat(inbox): match Cloud signal card wording and structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Consistent SignalCardHeader: colored dot + "Product · Type" + Weight badge - Source-specific cards: error tracking (fingerprint), GitHub (labels, link), Zendesk (priority/status/tags), LLM analytics (model/provider/trace) - Cloud-exact source line labels and product colors - Type guards dispatch signals to correct card variant --- .../features/inbox/components/SignalCard.tsx | 496 +++++++++++++----- 1 file changed, 372 insertions(+), 124 deletions(-) diff --git a/apps/code/src/renderer/features/inbox/components/SignalCard.tsx b/apps/code/src/renderer/features/inbox/components/SignalCard.tsx index 22a0fff82..34a6f3b9b 100644 --- a/apps/code/src/renderer/features/inbox/components/SignalCard.tsx +++ b/apps/code/src/renderer/features/inbox/components/SignalCard.tsx @@ -1,10 +1,15 @@ import { ArrowSquareOutIcon, + BrainIcon, BugIcon, CaretDownIcon, CaretRightIcon, GithubLogoIcon, + KanbanIcon, TagIcon, + TicketIcon, + VideoIcon, + WarningIcon, } from "@phosphor-icons/react"; import { Badge, Box, Flex, Text } from "@radix-ui/themes"; import type { Signal } from "@shared/types"; @@ -12,10 +17,71 @@ import { useState } from "react"; const COLLAPSE_THRESHOLD = 300; -interface SignalCardProps { - signal: Signal; +// ── Source line labels (matching PostHog Cloud's signalCardSourceLine) ──────── + +const ERROR_TRACKING_TYPE_LABELS: Record = { + issue_created: "New issue", + issue_reopened: "Issue reopened", + issue_spiking: "Volume spike", +}; + +function signalCardSourceLine(signal: { + source_product: string; + source_type: string; +}): string { + const { source_product, source_type } = signal; + + if (source_product === "error_tracking") { + const typeLabel = + ERROR_TRACKING_TYPE_LABELS[source_type] ?? source_type.replace(/_/g, " "); + return `Error tracking · ${typeLabel}`; + } + if ( + source_product === "session_replay" && + source_type === "session_segment_cluster" + ) { + return "Session replay · Session segment cluster"; + } + if ( + source_product === "session_replay" && + source_type === "session_analysis_cluster" + ) { + return "Session replay · Session analysis cluster"; + } + if (source_product === "llm_analytics" && source_type === "evaluation") { + return "LLM analytics · Evaluation"; + } + if (source_product === "zendesk" && source_type === "ticket") { + return "Zendesk · Ticket"; + } + if (source_product === "github" && source_type === "issue") { + return "GitHub · Issue"; + } + if (source_product === "linear" && source_type === "issue") { + return "Linear · Issue"; + } + + const productLabel = source_product.replace(/_/g, " "); + const typeLabel = source_type.replace(/_/g, " "); + return `${productLabel} · ${typeLabel}`; } +// ── Source product color (matching Cloud's known product colors) ────────────── + +const SOURCE_PRODUCT_ICONS: Record< + string, + { icon: React.ReactNode; color: string } +> = { + session_replay: { icon: , color: "var(--amber-9)" }, + error_tracking: { icon: , color: "var(--red-9)" }, + llm_analytics: { icon: , color: "var(--purple-9)" }, + github: { icon: , color: "var(--gray-11)" }, + linear: { icon: , color: "var(--blue-9)" }, + zendesk: { icon: , color: "var(--green-9)" }, +}; + +// ── Shared utilities ───────────────────────────────────────────────────────── + interface GitHubLabelObject { name: string; color?: string; @@ -24,11 +90,26 @@ interface GitHubLabelObject { interface GitHubIssueExtra { html_url?: string; number?: number; - state?: string; labels?: string | GitHubLabelObject[]; created_at?: string; - updated_at?: string; - locked?: boolean; +} + +interface ZendeskTicketExtra { + url?: string; + priority?: string; + status?: string; + tags?: string[]; +} + +interface LlmEvalExtra { + evaluation_id?: string; + trace_id?: string; + model?: string; + provider?: string; +} + +interface ErrorTrackingExtra { + fingerprint?: string; } function resolveLabels( @@ -57,18 +138,6 @@ function resolveLabels( return []; } -function splitTitleBody(content: string): { title: string; body: string } { - const firstNewline = content.indexOf("\n"); - if (firstNewline === -1) return { title: content, body: "" }; - return { - title: content.slice(0, firstNewline).trim(), - body: content - .slice(firstNewline + 1) - .replace(/^[\n]+/, "") - .trim(), - }; -} - function truncateBody(body: string, maxLength = COLLAPSE_THRESHOLD): string { if (body.length <= maxLength) return body; const truncated = body.slice(0, maxLength); @@ -77,6 +146,82 @@ function truncateBody(body: string, maxLength = COLLAPSE_THRESHOLD): string { return `${truncated.slice(0, cutPoint)}…`; } +function parseExtra(raw: Record): Record { + if (typeof raw === "string") { + try { + return JSON.parse(raw) as Record; + } catch { + return {}; + } + } + return raw; +} + +// ── Type guards ────────────────────────────────────────────────────────────── + +function isGithubIssueExtra( + extra: Record, +): extra is Record & GitHubIssueExtra { + return "html_url" in extra && "number" in extra; +} + +function isZendeskTicketExtra( + extra: Record, +): extra is Record & ZendeskTicketExtra { + return "url" in extra && "priority" in extra; +} + +function isLlmEvalExtra( + extra: Record, +): extra is Record & LlmEvalExtra { + return "evaluation_id" in extra && "trace_id" in extra; +} + +function isErrorTrackingExtra( + extra: Record, +): extra is Record & ErrorTrackingExtra { + return typeof extra.fingerprint === "string"; +} + +// ── Shared components ──────────────────────────────────────────────────────── + +function SignalCardHeader({ signal }: { signal: Signal }) { + const productInfo = SOURCE_PRODUCT_ICONS[signal.source_product]; + + return ( + + + {productInfo?.icon ?? ( + + )} + + + {signalCardSourceLine(signal)} + + + + Weight: {signal.weight.toFixed(1)} + + + ); +} + function CollapsibleBody({ body }: { body: string }) { const [expanded, setExpanded] = useState(false); const isLong = body.length > COLLAPSE_THRESHOLD; @@ -108,154 +253,257 @@ function CollapsibleBody({ body }: { body: string }) { ); } -function parseExtra(raw: Record): GitHubIssueExtra { - if (typeof raw === "string") { - try { - return JSON.parse(raw) as GitHubIssueExtra; - } catch { - return {}; - } - } - return raw as GitHubIssueExtra; -} +// ── Source-specific cards ──────────────────────────────────────────────────── -function GitHubIssueSignalCard({ signal }: SignalCardProps) { - const extra = parseExtra(signal.extra); +function GitHubIssueSignalCard({ + signal, + extra, +}: { + signal: Signal; + extra: GitHubIssueExtra; +}) { const labels = resolveLabels(extra.labels); const issueUrl = extra.html_url ?? null; - const issueNumber = extra.number ?? null; - const { title, body } = splitTitleBody(signal.content); - - const titleContent = ( - <> - {issueNumber ? `#${issueNumber} ` : ""} - {title} - - ); return ( - + + + - - {issueUrl ? ( + + #{extra.number} + + {labels.map((label) => ( + + + {label.name} + + ))} + + {issueUrl && ( - {titleContent} + View on GitHub + - ) : ( - + {extra.created_at && ( + + Opened: {new Date(extra.created_at).toLocaleString()} + + )} + + ); +} + +function ZendeskTicketSignalCard({ + signal, + extra, +}: { + signal: Signal; + extra: ZendeskTicketExtra; +}) { + return ( + + + + + {extra.priority && ( + + Priority: {extra.priority} + + )} + {extra.status && ( + + Status: {extra.status} + + )} + {extra.tags?.map((tag) => ( + - {titleContent} - - )} - {issueUrl && ( + {tag} + + ))} + + {extra.url && ( + Open )} - - - {labels.length > 0 && ( - - - {labels.map((label) => ( - - {label.name} - - ))} - - )} - - {body && } - - - - w:{signal.weight.toFixed(2)} - - - {new Date(signal.timestamp).toLocaleString()} - - - ); } -function DefaultSignalCard({ signal }: SignalCardProps) { +function LlmEvalSignalCard({ + signal, + extra, +}: { + signal: Signal; + extra: LlmEvalExtra; +}) { return ( - + + + - - - - {signal.source_product} - - - {signal.source_type} - - + {extra.model && Model: {extra.model}} + {extra.model && extra.provider && ·} + {extra.provider && Provider: {extra.provider}} + {extra.trace_id && ( + + Trace:{" "} + {extra.trace_id.slice(0, 12)}... + + )} + + ); +} - - +function ErrorTrackingSignalCard({ + signal, + extra, +}: { + signal: Signal; + extra: ErrorTrackingExtra; +}) { + const fingerprint = extra.fingerprint ?? ""; + const fingerprintShort = + fingerprint.length > 14 ? `${fingerprint.slice(0, 14)}…` : fingerprint; - - - w:{signal.weight.toFixed(2)} - - - {new Date(signal.timestamp).toLocaleString()} - + return ( + + + + + + + + Fingerprint{" "} + + {fingerprintShort} + + + + {/* No "View issue" link in Code — error tracking lives in Cloud */} ); } -export function SignalCard({ signal }: SignalCardProps) { - if (signal.source_product === "github") { - return ; +function GenericSignalCard({ signal }: { signal: Signal }) { + return ( + + + + + {new Date(signal.timestamp).toLocaleString()} + + + ); +} + +// ── Main export ────────────────────────────────────────────────────────────── + +export function SignalCard({ signal }: { signal: Signal }) { + const extra = parseExtra(signal.extra); + + if ( + signal.source_product === "error_tracking" && + isErrorTrackingExtra(extra) + ) { + return ; + } + if (signal.source_product === "github" && isGithubIssueExtra(extra)) { + return ; + } + if (signal.source_product === "zendesk" && isZendeskTicketExtra(extra)) { + return ; + } + if (signal.source_product === "llm_analytics" && isLlmEvalExtra(extra)) { + return ; } - return ; + return ; }