# Event Log Viewer

Event Log Viewer [#event-log-viewer]

The **Event Log Viewer** renders a devtools event stream newest first: relative timestamps (absolute on hover), namespace-colored type badges (`vectordb:` emerald, `embedding:` sky, `model` violet, `queue` amber, `pipeline` fuchsia, `storage` orange), and the serialized payload per entry. A case-insensitive substring filter narrows by event type, the visible list is capped (default 100, overridable via `maxVisible`) with an overflow line for older hidden matches, and the "no events yet" and "no events matching filter" empty states are distinct. A Clear affordance renders only when `onClear` is provided. It preserves the filter + cap semantics of the deprecated `@localmode/devtools` widget's Events panel. Values are passed in as props — works with any backend; recommended data source: `useDevToolsEvents` from `@localmode/devtools/react` (on-device, optional).

Preview [#preview]

```tsx
'use client';

import { useState } from 'react';
import {
  EventLogViewer,
  type DevToolsEventLike,
} from '@/components/event-log-viewer';

/**
 * Demo for {@link EventLogViewer}. Renders a fixture event stream shaped like
 * the real devtools bridge buffer (embedding + vectordb namespaces, oldest
 * first) with `maxVisible` lowered to 10 so the overflow line is visible.
 * Type into the filter (e.g. `search` or `embedding`) to see substring
 * filtering and the no-match empty state; Clear empties the log to show the
 * no-events state. The real app passes `useDevToolsEvents()` output straight
 * in — zero network, zero model bytes here.
 */
function buildFixtureEvents(): DevToolsEventLike[] {
  const now = Date.now();
  const at = (secondsAgo: number) => new Date(now - secondsAgo * 1000).toISOString();
  let id = 0;
  const event = (
    type: string,
    data: Record<string, unknown>,
    secondsAgo: number,
  ): DevToolsEventLike => ({ id: ++id, type, data, timestamp: at(secondsAgo) });

  return [
    event('embedding:modelLoad', { modelId: 'Xenova/bge-small-en-v1.5', durationMs: 2140 }, 340),
    event('vectordb:open', { collection: 'docs' }, 322),
    event('embedding:embedStart', { modelId: 'Xenova/bge-small-en-v1.5', count: 24 }, 300),
    event(
      'embedding:embedComplete',
      { modelId: 'Xenova/bge-small-en-v1.5', count: 24, durationMs: 412 },
      296,
    ),
    event('vectordb:addMany', { collection: 'docs', count: 24 }, 292),
    event('vectordb:search', { collection: 'docs', k: 5, durationMs: 11 }, 210),
    event('vectordb:search', { collection: 'docs', k: 5, durationMs: 9 }, 180),
    event('embedding:embedStart', { modelId: 'Xenova/bge-small-en-v1.5', count: 1 }, 121),
    event(
      'embedding:embedComplete',
      { modelId: 'Xenova/bge-small-en-v1.5', count: 1, durationMs: 38 },
      120,
    ),
    event('vectordb:search', { collection: 'docs', k: 10, durationMs: 14 }, 119),
    event('vectordb:delete', { collection: 'docs', id: 'doc-7' }, 90),
    event(
      'vectordb:error',
      { collection: 'scratch', operation: 'add', message: 'QuotaExceededError' },
      64,
    ),
    event('embedding:embedStart', { modelId: 'Xenova/bge-small-en-v1.5', count: 3 }, 12),
    event(
      'embedding:embedComplete',
      { modelId: 'Xenova/bge-small-en-v1.5', count: 3, durationMs: 96 },
      11,
    ),
  ];
}

export default function EventLogViewerDemo() {
  const [events, setEvents] = useState<DevToolsEventLike[]>(buildFixtureEvents);

  return (
    <div className="flex w-full max-w-xl flex-col gap-3">
      <EventLogViewer events={events} maxVisible={10} onClear={() => setEvents([])} />
      {events.length === 0 && (
        <button
          type="button"
          onClick={() => setEvents(buildFixtureEvents())}
          className="inline-flex h-8 w-fit items-center rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90"
        >
          Restore fixture events
        </button>
      )}
    </div>
  );
}
```

Installation [#installation]

```bash
npx shadcn@latest add @localmode/ui/devtools/event-log-viewer
```

Data source & dependencies [#data-source--dependencies]

**Data source:** renders whatever `events` you pass — works with any backend or event bus. Recommended producer: the `useDevToolsEvents` hook from `@localmode/devtools/react` (on-device, optional); its `DevToolsEvent[]` output matches `DevToolsEventLike` field-for-field, so it feeds the `events` prop with no mapping layer.

* `lucide-react` — icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)

Files installed [#files-installed]

* `event-log-viewer.tsx` — the `EventLogViewer` component
* `lib/utils.ts` — the `cn()` helper (if not already present)

Props [#props]

**EventLogViewer**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `events` | `array` | — | **Required.** The event stream, oldest first (the order `useDevToolsEvents` returns). Rendered newest first. |
| `maxVisible` | `number` | `100` | Maximum number of events visible at once, applied after filtering and keeping the newest. An overflow line reports how many older matches are hidden. |
| `filter` | `string` | — | Controlled filter value. Omit to let the component manage its own filter state internally. |
| `onFilterChange` | `function` | — | Called with the new filter text whenever the filter input changes. |
| `onClear` | `function` | — | When provided, a Clear button renders and invokes this callback. |

**DevToolsEventLike**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `number` | — | **Required.** Monotonically increasing event ID. |
| `type` | `string` | — | **Required.** Namespaced event type (e.g. `'vectordb:add'`, `'embedding:embedComplete'`). |
| `data` | `Record<string, unknown>` | — | **Required.** Event payload. |
| `timestamp` | `string` | — | **Required.** ISO 8601 timestamp. |

Examples [#examples]

Bound to the devtools event stream [#bound-to-the-devtools-event-stream]

Enable devtools once at app init (`enableDevTools()` from `@localmode/devtools`), then the hook streams the bridge's event buffer — oldest first, exactly what `events` expects:

```tsx
import { useDevToolsEvents } from '@localmode/devtools/react';
import { EventLogViewer } from '@/components/event-log-viewer';

function EventsTab() {
  const events = useDevToolsEvents();
  return <EventLogViewer events={events} maxVisible={100} />;
}
```

Pre-narrowed to one namespace [#pre-narrowed-to-one-namespace]

Pass the hook's own `types` filter to scope the stream before it reaches the viewer (the in-component filter then narrows further):

```tsx
const events = useDevToolsEvents({ types: ['vectordb'] });

<EventLogViewer events={events} />
```

Controlled filter with a clear affordance [#controlled-filter-with-a-clear-affordance]

Own the filter in the parent (e.g. to sync it with a URL param) and wire `onClear` to whatever resets your buffer:

```tsx
const [filter, setFilter] = useState('');

<EventLogViewer
  events={events}
  filter={filter}
  onFilterChange={setFilter}
  onClear={() => setEvents([])}
/>
```

Customization [#customization]

Type badges are colored by the namespace before the `:` in the event type — the `NAMESPACE_BADGES` map in the copied file is yours to extend with your own namespaces (unknown namespaces fall back to a neutral badge). The list scrolls internally past `max-h-96`; the overflow line reports matches hidden by `maxVisible`. Styled entirely with shadcn/ui CSS-variable utilities so it inherits your theme — because you own the copied file, every class, cap, and empty-state message is yours to change.