feat: visualize ranked event opportunities

This commit is contained in:
2026-08-22 18:44:59 +08:00
parent f3f8a0bb99
commit 2f856db441
6 changed files with 682 additions and 94 deletions

View File

@@ -12,8 +12,14 @@
- Header states the 90-day horizon starts after the latest complete dataset date. A daily `به‌روزرسانی تحلیل` action shows `nextRefreshAt`, loading, and typed rate-limit feedback.
- Render a responsive, keyboard-accessible forecast chart with count/amount mode, hover/focus points, P10–P90 band, axes/units, chart summary, and a non-visual data alternative.
- Show historical wins, up to five upcoming opportunities, up to three gentle low-period warnings, and a peer card when permitted.
- Rank up to three upcoming events as a shared-baseline priority histogram. Rank 1 is the physical-right column in RTL. Each column's literal height equals its integer planning-priority percentage; the displayed percentages are positive, descend by rank, and total exactly `100`. When only one or two relevant candidates exist, say so explicitly and divide the total only among the available candidates.
- State visibly that event priority is a relative planning allocation, not a probability of growth or guaranteed demand. Repeat rank and percentage in text so height and color are never the only carriers of meaning.
- Pair the compact histogram with one evidence card per displayed event. On desktop, up to three cards form RTL columns aligned by rank; on mobile, the compact histogram keeps its shared baseline and evidence cards stack for readable analysis.
- Prefer evidence from the merchant's matching prior occasion window. Show the prior start/end dates, observation count, paid-session uplift, amount uplift, and an explicit promote, prepare, or caution stance. Material decline must produce a visible caution recommendation.
- When matching historical evidence is unavailable, ground the explanation in onboarding profile signals. Show the matched category, niche, or sales-channel signals and, when provided, the reason history was unavailable.
- Label every event explanation as either `بر پایه تجربه شما` or `بر پایه شناخت کسب‌وکار`. The label must agree with both deterministic evidence and the recommendation's `insightBasis`.
- Each brief contains server-rendered timing/range/evidence plus validated AI headline, interpretation, offer/message suggestion, one preparation step, confidence, and limitation.
- `PARTIAL` keeps deterministic charts/cards and explains that the written AI interpretation was unavailable. `LOW` keeps the forecast visible with an explicit limited-history or high-variance warning. `INSUFFICIENT_DATA` is reserved for scopes with no historical observations. `FAILED` offers safe retry without hiding an older successful report.
- `PARTIAL` keeps deterministic charts/cards and visibly explains that the written AI interpretation was unavailable. `INSUFFICIENT_EVENT_CANDIDATES` additionally states the exact number of available events instead of implying that three were ranked. `LOW` keeps the forecast visible with an explicit limited-history or high-variance warning. `INSUFFICIENT_DATA` is reserved for scopes with no historical observations. `FAILED` offers safe retry without hiding an older successful report.
- Motion is restrained, supports reduced motion, and does not block reading. All interactive targets are at least 44 px.
## Copy rules
@@ -21,11 +27,25 @@
- Use natural professional Persian, Persian `ی`/`ک`, appropriate نیم‌فاصله, Persian digits, and Rial.
- Keep tone constructive: describe opportunity, preparation, and uncertainty; do not shame the merchant or claim guaranteed outcomes.
- Every recommendation visibly cites its deterministic evidence card. AI is labeled advisory.
- Event reasons explain why the occasion is relevant to this merchant and must not turn planning priority into a forecast claim.
- No chat, notifications, campaign execution, user memory, provider/model names, token usage, or internal failure details appear in merchant UI.
## Event priority contract
- `occasionKey` is the stable identity used to join an upcoming occurrence to prior occurrences; `eventVariant` is one of `PRIMARY`, `CAMPAIGN_WINDOW`, or `OPERATIONAL_WINDOW` and distinguishes materially different variants without changing that identity.
- `HISTORICAL` is preferred whenever matching prior-window evidence is eligible. `BUSINESS_PROFILE` is used only when that evidence is unavailable.
- `historicalEvidence` contains deterministic dates, sample size, basis-point uplifts, and material-win/material-decline flags. AI copy may interpret these values but may not invent or alter them.
- `profileEvidence.matchedSignals` contains onboarding-derived evidence and may be empty. Empty signals produce truthful general-profile copy rather than invented alignment chips. `historyUnavailableReason` is an optional typed code and is always translated to natural Persian; raw backend codes never appear.
- Deterministic events form a discriminated evidence union: `HISTORICAL` requires `historicalEvidence`, while `BUSINESS_PROFILE` requires `profileEvidence`. Recommendation basis must match the deterministic event basis; mismatched recommendation copy is discarded rather than overriding evidence authority.
- `priorityPercent` is an integer from `1` to `98`. A valid AI recommendation collection contains exactly three items, is sorted descending, and totals exactly `100`; reports with fewer candidates omit AI copy and use the explicit limited-candidate `PARTIAL` state.
- Upcoming-event filtering compares inclusive calendar dates in `Asia/Tehran`, so an event remains visible through its entire `endDate` in the merchant calendar.
- `stance` is one of `PROMOTE`, `PREPARE`, or `CAUTION`; color always appears with its Persian text label.
## Acceptance
- Works at 360, 768, 1024, and wide desktop widths in light/dark RTL layouts.
- The event chart keeps three readable columns at 360 px, places rank 1 physically right, and exposes an equivalent ordered text alternative to assistive technology.
- Event evidence and reasons are visible without hover or pointer interaction, and histogram height transitions are removed under `prefers-reduced-motion`.
- All job, cache, refresh, partial, insufficient, unauthorized, and recoverable error states are accessible and tested.
- Generated OpenAPI types are used; no handwritten duplicate growth DTOs or browser secrets exist.
- Cached navigation performs no generation request. Manual retry remains available while the current scope has no persisted AI-written summary; after one exists, refresh is limited once per merchant per Tehran day.