Cleaner event panels: hide raw feed prose, one best report sentence
- the ESPN feed description and attribution line are gone from goal
panels whenever the report narrative or shot data tells the story
(attribution lives in a tooltip); bookings keep their reason line
- one report sentence per event, ranked by event vocabulary with a
bonus when the player is the actor ('Quinones, who drove a low
shot…' beats 'goals from Quinones and Jimenez gave…')
- momentum verdicts only ever applied to goals (a clear-pressure
booking no longer claims it was 'scored against the run of play')
Routine events the report never mentions (e.g. an early booking) show
what the sources have: the reason. There is no richer feed for those.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -77,12 +77,9 @@ function ContextPanel({ e, ctx, shots, momentum, commentary, report }: {
|
||||
}) {
|
||||
const t = useT();
|
||||
const shot = goalShot(e, shots);
|
||||
const verdict = momentumVerdict(e, momentum);
|
||||
const verdict = e.kind === 'goal' ? momentumVerdict(e, momentum) : null;
|
||||
const vars = varLines(e, commentary);
|
||||
const story = reportSentences(e, report);
|
||||
// A booking's ctx falls back to the raw "is shown the red card" line when
|
||||
// the feed gives no reason — pure noise once the report explains it.
|
||||
const showCtx = !(story.length > 0 && (e.kind === 'yellow' || e.kind === 'red') && ctx === e.raw.trim());
|
||||
const situations = t.match.situations as Record<string, string>;
|
||||
const chance = shot && shot.xg != null
|
||||
? fmt(shot.distM != null ? t.match.insight.chance : t.match.insight.chanceNoDist, {
|
||||
@@ -92,14 +89,19 @@ function ContextPanel({ e, ctx, shots, momentum, commentary, report }: {
|
||||
pct: Math.round(shot.xg * 100),
|
||||
}) + (shot.xgot != null ? ` · ${shot.xgot.toFixed(2)} xGOT` : '')
|
||||
: null;
|
||||
// The raw feed line is a fallback, not a feature: a goal's prose hides once
|
||||
// the report or shot data tells the story; a booking's no-reason sentence
|
||||
// hides once the report explains it. Real reasons ("For a bad foul.") stay.
|
||||
const showCtx = e.kind === 'goal'
|
||||
? story.length === 0 && !chance
|
||||
: !(story.length > 0 && ctx === e.raw.trim());
|
||||
return (
|
||||
<div className="mt-1.5 space-y-1 whitespace-normal text-xs leading-relaxed">
|
||||
{story.map((s, j) => <p key={j} className="text-ink-soft">{s}</p>)}
|
||||
{story.map((s, j) => <p key={j} className="text-ink-soft" title={t.match.reportAttribution}>{s}</p>)}
|
||||
{chance && <p className="font-medium text-muted">{chance}</p>}
|
||||
{verdict && <p className="text-muted">{verdict === 'against' ? t.match.insight.againstRun : t.match.insight.withRun}</p>}
|
||||
{vars.map((v, j) => <p key={j} className="text-gold">{v}</p>)}
|
||||
{showCtx && <p className="text-faint">{ctx}</p>}
|
||||
{story.length > 0 && <p className="text-[10px] uppercase tracking-wide text-faint">{t.match.reportAttribution}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -185,6 +185,14 @@ describe('reportSentences', () => {
|
||||
expect(out[0]).toContain('red-carded for bringing down Gutierrez');
|
||||
});
|
||||
|
||||
it('prefers the most event-specific sentence over a generic summary', () => {
|
||||
const noisy = "Goals from Quinones and Jimenez gave Mexico the win. " +
|
||||
"They were a goal up when the ball fell to Quinones, who drove a low shot through the legs of Williams to score in the ninth minute.";
|
||||
const out = reportSentences(goal, noisy);
|
||||
expect(out).toHaveLength(1);
|
||||
expect(out[0]).toContain('drove a low shot');
|
||||
});
|
||||
|
||||
it('finds the goal narrative despite missing diacritics in the report', () => {
|
||||
const out = reportSentences(goal, story);
|
||||
expect(out[0]).toContain('drove a low shot through the legs of Williams');
|
||||
|
||||
+10
-4
@@ -222,7 +222,7 @@ const KIND_WORDS: Record<string, RegExp> = {
|
||||
* box"). Sentences must mention the player; those matching the event kind's
|
||||
* vocabulary win, so a player's card sentence beats his goal sentence.
|
||||
*/
|
||||
export function reportSentences(e: ParsedEvent, story: string, max = 2): string[] {
|
||||
export function reportSentences(e: ParsedEvent, story: string, max = 1): string[] {
|
||||
if (!story || e.kind === 'sub' || e.kind === 'other') return [];
|
||||
const last = norm(e.title).split(' ').pop() ?? '';
|
||||
if (last.length < 3) return [];
|
||||
@@ -230,10 +230,16 @@ export function reportSentences(e: ParsedEvent, story: string, max = 2): string[
|
||||
const scored = story
|
||||
.split(/(?<=[.!?])\s+/)
|
||||
.filter((s) => s.length > 20 && s.length < 400 && new RegExp(`\\b${last}\\b`, 'i').test(norm(s)))
|
||||
.map((s) => ({ s: s.trim(), hit: kindRe ? kindRe.test(s) : false }));
|
||||
.map((s) => {
|
||||
let hits = kindRe ? (s.match(new RegExp(kindRe.source, 'gi')) ?? []).length : 0;
|
||||
// The player ACTING ("Quinones, who drove a low shot") outranks the
|
||||
// player merely listed ("goals from Quinones and Jimenez gave…").
|
||||
if (kindRe && new RegExp(`\\b${last}\\b[^.]{0,60}?${kindRe.source}`, 'i').test(norm(s))) hits += 2;
|
||||
return { s: s.trim(), hits };
|
||||
});
|
||||
if (!scored.length) return [];
|
||||
const hits = scored.filter((x) => x.hit).map((x) => x.s);
|
||||
return (hits.length ? hits : [scored[0]!.s]).slice(0, max);
|
||||
scored.sort((a, b) => b.hits - a.hits);
|
||||
return scored.slice(0, max).map((x) => x.s);
|
||||
}
|
||||
|
||||
/** VAR / video-review feed lines belonging to this event (±1 minute) —
|
||||
|
||||
Reference in New Issue
Block a user