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
15 changes: 11 additions & 4 deletions studio/src/components/TweetsTab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState, useMemo } from "react";
import { Heart, Repeat2, MessageSquare, Eye, Wand2, X } from "lucide-react";
import { Chip, SentimentChip } from "./primitives.jsx";
import { TONE_DEFS, TONE_COLOR } from "../lib/lexicons.js";
import { MAX_VISIBLE, capNotice } from "../lib/display.js";

/* ============================================================
* TWEETS TAB
Expand All @@ -14,7 +15,7 @@ export function TweetsTab({ tweets, selection, onJumpToCaptions }) {
const [sortKey, setSortKey] = useState("engagement");
const [onlySelected, setOnlySelected] = useState(false);

const filtered = useMemo(() => {
const matched = useMemo(() => {
const needle = q.trim().toLowerCase();
return tweets
.filter(
Expand All @@ -34,10 +35,12 @@ export function TweetsTab({ tweets, selection, onJumpToCaptions }) {
if (sortKey === "date")
return (b.date || "").localeCompare(a.date || "");
return 0;
})
.slice(0, 500);
});
}, [tweets, q, sentFilter, toneFilter, sortKey, onlySelected, selection]);

const filtered = useMemo(() => matched.slice(0, MAX_VISIBLE), [matched]);
const notice = capNotice(matched.length);

const visibleIds = useMemo(() => filtered.map((t) => t.uid), [filtered]);
const allVisibleSelected =
visibleIds.length > 0 && visibleIds.every((id) => selection.has(id));
Expand Down Expand Up @@ -102,10 +105,14 @@ export function TweetsTab({ tweets, selection, onJumpToCaptions }) {
Selected only
</label>
<span className="text-xs text-slate-500 ml-auto">
{filtered.length} of {tweets.length}
{matched.length} of {tweets.length}
</span>
</div>

{notice && (
<div className="text-xs text-slate-500 -mt-1 px-1">{notice}</div>
)}

<div className="flex flex-wrap items-center gap-2 bg-indigo-50/60 border border-indigo-100 rounded-xl p-3 text-sm">
<span className="font-medium text-indigo-900">
{selection.count} selected
Expand Down
13 changes: 13 additions & 0 deletions studio/src/lib/display.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* ============================================================
* DISPLAY HELPERS
* ============================================================ */

// The Tweets tab renders at most this many rows for performance. The cap was
// previously applied silently; capNotice surfaces it so users don't think
// matches are missing.
export const MAX_VISIBLE = 500;

export function capNotice(matchCount, cap = MAX_VISIBLE) {
if (matchCount <= cap) return null;
return `Showing the top ${cap} of ${matchCount.toLocaleString()} matches`;
}
24 changes: 24 additions & 0 deletions studio/src/lib/display.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, it, expect } from "vitest";
import { capNotice, MAX_VISIBLE } from "./display.js";

describe("capNotice", () => {
it("returns null when the match count is at or below the cap", () => {
expect(capNotice(0, 500)).toBeNull();
expect(capNotice(500, 500)).toBeNull();
});

it("returns a notice when matches exceed the cap", () => {
expect(capNotice(501, 500)).toBe("Showing the top 500 of 501 matches");
});

it("formats large match counts with thousands separators", () => {
expect(capNotice(12345, 500)).toBe("Showing the top 500 of 12,345 matches");
});

it("defaults to MAX_VISIBLE when no cap is passed", () => {
expect(capNotice(MAX_VISIBLE)).toBeNull();
expect(capNotice(MAX_VISIBLE + 1)).toBe(
`Showing the top ${MAX_VISIBLE} of ${(MAX_VISIBLE + 1).toLocaleString()} matches`,
);
});
});
Loading