# InsightsCard

> A meeting's numbers (sentiment, stat tiles, per-speaker talk share, an over-talk hint), all derived on the client from the transcript.

## Facts

- **Product**: Max
- **Family**: Analytics
- **Global**: `window.DigitalCrew.InsightsCard`
- **Source**: `max-agent: src/features/meeting-hub/ui/insights-card.tsx`
- **Import in the app**: `import { InsightsCard } from "@/features/meeting-hub/ui/insights-card";`
- **Live preview**: /design/components/insights-card/preview.html
- **Page**: /design/components/insights-card

## Guidelines

**Use it for** a recorded meeting's insights panel in the meeting hub.

**What you provide**: `detail: MeetingSessionDetailDto`. The card reads three parts of it:
- `currentTranscript.segments`: `sequenceNumber`, `participantId` or `speakerLabel`, `startMs` and `endMs`, and `text`.
- `participants`: `id`, `displayName` or `email`, `isInternal`, `providerIdentity`.
- `currentSummary.sentiment`: `{ overall: "positive" | "neutral" | "negative", rationale }`.

**Behaviour**:
- Speaking time, pace and time-based shares appear only when at least 50% of segments carry timings. Otherwise share is counted in words.
- Talk balance needs at least one speaker matched to a participant.
- An internal share above 65%, with an external speaker present, shows the amber hint.
- A speaker who never matched a participant keeps their raw label.
- An empty transcript shows "Insights appear once this meeting has a transcript."

**Anatomy**:
- A [`Card`](/design/components/card.md). The sentiment is an outline [`Badge`](/design/components/badge.md) (`h-5 px-1.5 text-[10px]`): emerald for positive, muted for neutral, red for negative.
- Stat tiles sit in `grid-cols-2 sm:grid-cols-3 gap-2`; each tile is `rounded-md border-border/60 px-2.5 py-1.5` with a `text-[10px]` uppercase label.
- Share bars are `h-1.5 rounded-full` on `bg-muted`, at least 2% wide. Their colours cycle through eight `-500` hues, starting blue, emerald, amber.
- The hint is `border-amber-500/40 bg-amber-500/10`.

**Don't** present the sentiment as fact: it is model output. Don't turn a diarisation label into a person.

## API

```ts
export declare function InsightsCard({ detail }: {
    detail: MeetingSessionDetailDto;
}): JSX.Element;
```

## Example

```html
<div id="root" class="p-6"></div>
<script>
(function () {
  var h = React.createElement, DC = window.DigitalCrew, I = DC.Icons;
  function participant(id, name, email, role, isInternal, relation) {
    return {
      id: id, providerIdentity: null, identityReviewedAt: null, displayName: name, email: email,
      role: role, isInternal: isInternal, prospectId: null,
      matchStatus: isInternal ? "unresolved" : "confirmed", matchConfidence: isInternal ? null : 0.96,
      relationType: relation, contact: null
    };
  }
  var PARTICIPANTS = [
    participant("p-sarah", "Sarah Lindqvist", "sarah@digitalcrew.ai", "organizer", true, "employee"),
    participant("p-claire", "Claire Dubois", "claire.dubois@northwind.com", "external", false, "prospect"),
    participant("p-tom", "Tom Becker", "tom.becker@northwind.com", "external", false, "prospect")
  ];
  // TranscriptSegmentDto rows. Timings follow a ~140 wpm speaking pace so the
  // time-based stats (speaking time, pace, talk share) are shown.
  var LINES = [
    ["p-sarah", "Thanks for making the time, Claire, and good to meet you, Tom. Before we dive in, what made you look at outbound automation this quarter?"],
    ["p-claire", "We doubled the SDR team last year and reply rates went the other way. The board now wants three times pipeline coverage by January, and hiring more reps is not the answer."],
    ["p-sarah", "That is the pattern we see most often. Max runs the research and the first touches across email and LinkedIn, then hands warm replies to your reps with the context already written up. Teams your size usually start with two Digital Workers on one segment and expand once the reply data is in. The workers follow your playbook, your tone and your exclusions, and every message they send is logged against the prospect, so your reps pick up a thread instead of starting one."],
    ["p-tom", "How does it handle the HubSpot sequences we already have running? We have about forty live ones."],
    ["p-sarah", "It reads the enrolments and skips anyone already in a sequence, so nobody gets a double touch. Replies land in HubSpot on the contact as well. Shall we walk through the sync settings with you and your RevOps team on Thursday?"],
    ["p-claire", "Yes, let's do that. And what would a pilot cost for the DACH segment?"],
    ["p-sarah", "One segment and two workers is the Growth plan, billed monthly with no annual commitment for the pilot. I will send the numbers in writing today, with the reply benchmarks we see for logistics."],
    ["p-tom", "That works for us."],
    ["p-sarah", "Great. Is Thursday at ten good for the technical session?"],
    ["p-claire", "Thursday at ten is fine. Send the invite to both of us."]
  ];
  var clock = 0;
  var SEGMENTS = LINES.map(function (l, i) {
    var words = l[1].split(/\s+/).length, start = clock, end = start + Math.round((words / 140) * 60000);
    clock = end + 700;
    return { sequenceNumber: i + 1, speakerLabel: null, participantId: l[0], startMs: start, endMs: end, text: l[1], confidence: 0.94 };
  });
  var DETAIL = {
    id: "mtg_7f3a", title: "Northwind Traders intro call", platform: "google_meet", source: "calendar",
    status: "ready", startedAt: "2026-09-24T09:00:00Z", endedAt: "2026-09-24T09:04:00Z", participantCount: 3,
    hasTranscript: true, hasSummary: true, hasRecording: true, openTaskCount: 2,
    correlationId: "corr_7f3a", captureProvider: "vexa", calendarEventId: "evt_91", bookingMeetingId: null,
    stages: [], participants: PARTICIPANTS,
    currentTranscript: { id: "tr_2", versionNumber: 1, versionType: "raw", createdByType: "provider", changeSummary: null, createdAt: "2026-09-24T09:12:00Z", segments: SEGMENTS },
    currentTranscriptVersionNumber: 1,
    currentSummary: {
      id: "sum_4", versionNumber: 1, status: "draft", summary: null, keyDecisions: [], discussionPoints: [],
      risksOpenQuestions: [], nextSteps: [], sourceTranscriptVersionId: "tr_2", sourceTranscriptVersionNumber: 1,
      generatedAt: "2026-09-24T09:14:00Z",
      sentiment: { overall: "positive", rationale: "Both buyers engaged on pricing and agreed to a technical follow-up on Thursday." }
    },
    summaryStale: false, recordingAvailable: true, recordingId: "rec_7f3a", recordingUrl: null,
    recordingMimeType: "video/webm", recordingPartCount: 1
  };
  function App() {
    return h("div", { className: "max-w-md" }, h(DC.InsightsCard, { detail: DETAIL }));
  }
  ReactDOM.createRoot(document.getElementById("root")).render(h(DC.TooltipProvider, { delayDuration: 80 }, h(App)));
})();
</script>
```

## More in Analytics

- [ActivityChart](/design/components/activity-chart.md): A day-bucketed Recharts chart in Max's glass panel: each series is drawn as an area, a line or a bar, with a dot legend and a popover tooltip.
- [ChartContainer](/design/components/chart-container.md): The shell every Crew OS chart sits in: it wraps a Recharts 2 chart in a `ResponsiveContainer`, turns a config object into per-series colour variables and styles axes, grid, tooltip and legend with the theme tokens.
- [FunnelChart](/design/components/funnel-chart.md): A vertical column funnel with a summary panel.
- [QualificationContent](/design/components/qualification-content.md): The body of a BANT qualification snapshot: a score, a verdict, a confirmed count, and one tile per criterion with its status, rationale and first piece of transcript evidence.
