feat: improve growth analysis waiting experience
This commit is contained in:
@@ -13,7 +13,7 @@
|
|||||||
- 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.
|
- 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.
|
- Show historical wins, up to five upcoming opportunities, up to three gentle low-period warnings, and a peer card when permitted.
|
||||||
- Each brief contains server-rendered timing/range/evidence plus validated AI headline, interpretation, offer/message suggestion, one preparation step, confidence, and limitation.
|
- 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. `INSUFFICIENT_DATA` explains the minimum evidence without presenting a forecast. `FAILED` offers safe retry without hiding an older successful report.
|
- `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.
|
||||||
- Motion is restrained, supports reduced motion, and does not block reading. All interactive targets are at least 44 px.
|
- Motion is restrained, supports reduced motion, and does not block reading. All interactive targets are at least 44 px.
|
||||||
|
|
||||||
## Copy rules
|
## Copy rules
|
||||||
@@ -28,4 +28,4 @@
|
|||||||
- Works at 360, 768, 1024, and wide desktop widths in light/dark RTL layouts.
|
- Works at 360, 768, 1024, and wide desktop widths in light/dark RTL layouts.
|
||||||
- All job, cache, refresh, partial, insufficient, unauthorized, and recoverable error states are accessible and tested.
|
- 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.
|
- Generated OpenAPI types are used; no handwritten duplicate growth DTOs or browser secrets exist.
|
||||||
- Cached navigation performs no generation request; manual refresh is explicit and limited once per merchant per Tehran day.
|
- 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.
|
||||||
|
|||||||
9
src/lib/api/schema.d.ts
vendored
9
src/lib/api/schema.d.ts
vendored
@@ -337,7 +337,7 @@ export interface components {
|
|||||||
schemas: {
|
schemas: {
|
||||||
CreateGrowthReportRequest: {
|
CreateGrowthReportRequest: {
|
||||||
terminalId?: string;
|
terminalId?: string;
|
||||||
/** @description When omitted, the request reuses the current matching report. */
|
/** @description When omitted, the request reuses the current matching report. Manual retries remain available until the requested scope has persisted AI copy. */
|
||||||
refresh?: boolean;
|
refresh?: boolean;
|
||||||
};
|
};
|
||||||
GrowthRange: {
|
GrowthRange: {
|
||||||
@@ -365,7 +365,7 @@ export interface components {
|
|||||||
/** @enum {integer} */
|
/** @enum {integer} */
|
||||||
horizonDays: 90;
|
horizonDays: 90;
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
confidence: 'HIGH' | 'MEDIUM' | 'INSUFFICIENT_DATA';
|
confidence: 'HIGH' | 'MEDIUM' | 'LOW' | 'INSUFFICIENT_DATA';
|
||||||
suppressionReason?: string;
|
suppressionReason?: string;
|
||||||
points: components['schemas']['GrowthForecastPoint'][];
|
points: components['schemas']['GrowthForecastPoint'][];
|
||||||
backtest: {
|
backtest: {
|
||||||
@@ -498,7 +498,10 @@ export interface components {
|
|||||||
failureCode?: string;
|
failureCode?: string;
|
||||||
cached: boolean;
|
cached: boolean;
|
||||||
stale: boolean;
|
stale: boolean;
|
||||||
/** Format: date-time */
|
/**
|
||||||
|
* Format: date-time
|
||||||
|
* @description Current time while no AI summary exists for the scope; otherwise the next refresh boundary for that terminal scope in the Tehran day. The all-terminals selection is a separate scope.
|
||||||
|
*/
|
||||||
nextRefreshAt: string;
|
nextRefreshAt: string;
|
||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|||||||
223
src/lib/components/GrowthAIWorkingIllustration.svelte
Normal file
223
src/lib/components/GrowthAIWorkingIllustration.svelte
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
type WorkingStatus = 'QUEUED' | 'ANALYZING' | 'GENERATING';
|
||||||
|
|
||||||
|
let { status = 'QUEUED', overlay = false }: { status?: WorkingStatus; overlay?: boolean } = $props();
|
||||||
|
|
||||||
|
const title = $derived(
|
||||||
|
overlay && status === 'QUEUED'
|
||||||
|
? 'نسخه تازه تحلیل در حال آمادهسازی است'
|
||||||
|
: status === 'GENERATING'
|
||||||
|
? 'در حال نوشتن جمعبندی تحلیلی'
|
||||||
|
: status === 'ANALYZING'
|
||||||
|
? 'در حال بررسی روندهای کسبوکار شما'
|
||||||
|
: 'تحلیل شما در صف آمادهسازی است',
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class={overlay
|
||||||
|
? 'bg-background/90 pointer-events-none absolute inset-0 z-20 flex min-h-full flex-col items-center justify-center rounded-lg px-4 py-8 text-center backdrop-blur-[2px] sm:px-8'
|
||||||
|
: 'border-border bg-card flex min-h-96 flex-col items-center justify-center rounded-lg border px-4 py-10 text-center sm:px-8'}
|
||||||
|
role="status"
|
||||||
|
aria-label={overlay
|
||||||
|
? 'هوش مصنوعی در حال بهروزرسانی تحلیل رشد است'
|
||||||
|
: 'هوش مصنوعی در حال ساخت تحلیل رشد است'}
|
||||||
|
aria-live="polite"
|
||||||
|
aria-busy="true"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class={overlay ? 'working-illustration w-full max-w-60' : 'working-illustration w-full max-w-80'}
|
||||||
|
viewBox="0 0 320 220"
|
||||||
|
role="img"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<rect class="document" x="77" y="28" width="166" height="164" rx="8" />
|
||||||
|
<path class="document-fold" d="M211 28v32h32" />
|
||||||
|
|
||||||
|
<g class="chart" transform="translate(101 104)">
|
||||||
|
<rect x="0" y="30" width="18" height="28" rx="3" />
|
||||||
|
<rect x="29" y="17" width="18" height="41" rx="3" />
|
||||||
|
<rect x="58" y="4" width="18" height="54" rx="3" />
|
||||||
|
<path d="M2 17 36 5l31-18 31 5" />
|
||||||
|
<circle cx="2" cy="17" r="4" />
|
||||||
|
<circle cx="36" cy="5" r="4" />
|
||||||
|
<circle cx="67" cy="-13" r="4" />
|
||||||
|
<circle cx="98" cy="-8" r="4" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g class="ai-core" transform="translate(160 75)">
|
||||||
|
<circle class="core-halo" r="31" />
|
||||||
|
<circle class="core" r="21" />
|
||||||
|
<path d="M-9-2h18M-7 7h14M0-12v24" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g class="data-points">
|
||||||
|
<circle cx="49" cy="72" r="5" />
|
||||||
|
<circle cx="270" cy="93" r="5" />
|
||||||
|
<circle cx="55" cy="161" r="5" />
|
||||||
|
<path d="M55 74 126 75M265 95l-73-18M60 158l49-23" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g class="writing-lines">
|
||||||
|
<path d="M104 174h112" />
|
||||||
|
<path d="M104 184h72" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<h2 class="mt-2 text-base font-bold sm:text-lg">{title}</h2>
|
||||||
|
<p class="text-muted-foreground mt-2 max-w-xl text-sm leading-7">
|
||||||
|
هوش مصنوعی دادهها را کنار هم میگذارد، الگوها را بررسی میکند و خلاصهای کاربردی از وضعیت رشد شما
|
||||||
|
میسازد.
|
||||||
|
</p>
|
||||||
|
<div class="mt-5 flex items-center gap-2" aria-hidden="true">
|
||||||
|
<span class="progress-dot"></span>
|
||||||
|
<span class="progress-dot"></span>
|
||||||
|
<span class="progress-dot"></span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.working-illustration {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.document {
|
||||||
|
fill: var(--card);
|
||||||
|
stroke: var(--border);
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-fold,
|
||||||
|
.writing-lines path {
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--border);
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart rect {
|
||||||
|
fill: color-mix(in srgb, var(--primary) 18%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart path {
|
||||||
|
fill: none;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart circle,
|
||||||
|
.data-points circle {
|
||||||
|
fill: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
animation: chart-build 1.8s cubic-bezier(0.77, 0, 0.175, 1) infinite alternate;
|
||||||
|
transform-box: fill-box;
|
||||||
|
transform-origin: center bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-halo {
|
||||||
|
fill: color-mix(in srgb, var(--primary) 12%, transparent);
|
||||||
|
animation: halo-pulse 1.8s cubic-bezier(0.77, 0, 0.175, 1) infinite alternate;
|
||||||
|
transform-box: fill-box;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.core {
|
||||||
|
fill: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-core path {
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--primary-foreground);
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-width: 2.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-points path {
|
||||||
|
fill: none;
|
||||||
|
stroke: color-mix(in srgb, var(--primary) 45%, var(--border));
|
||||||
|
stroke-dasharray: 5 6;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-points {
|
||||||
|
animation: data-gather 1.8s cubic-bezier(0.77, 0, 0.175, 1) infinite alternate;
|
||||||
|
transform-box: fill-box;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-dot {
|
||||||
|
width: 0.375rem;
|
||||||
|
height: 0.375rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--primary);
|
||||||
|
animation: dot-pulse 900ms cubic-bezier(0.77, 0, 0.175, 1) infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-dot:nth-child(2) {
|
||||||
|
animation-delay: 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-dot:nth-child(3) {
|
||||||
|
animation-delay: 300ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes chart-build {
|
||||||
|
from {
|
||||||
|
opacity: 0.55;
|
||||||
|
transform: translate(101px, 104px) scaleY(0.92);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(101px, 104px) scaleY(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes halo-pulse {
|
||||||
|
from {
|
||||||
|
opacity: 0.4;
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes data-gather {
|
||||||
|
from {
|
||||||
|
opacity: 0.45;
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dot-pulse {
|
||||||
|
from {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-35%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.chart,
|
||||||
|
.core-halo,
|
||||||
|
.data-points,
|
||||||
|
.progress-dot {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
32
src/lib/components/GrowthAIWorkingIllustration.test.ts
Normal file
32
src/lib/components/GrowthAIWorkingIllustration.test.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { render, screen } from '@testing-library/svelte';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import GrowthAIWorkingIllustration from './GrowthAIWorkingIllustration.svelte';
|
||||||
|
|
||||||
|
describe('GrowthAIWorkingIllustration', () => {
|
||||||
|
it('announces that the AI is assembling the initial growth analysis', () => {
|
||||||
|
const { container } = render(GrowthAIWorkingIllustration, { status: 'ANALYZING' });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByRole('status', { name: 'هوش مصنوعی در حال ساخت تحلیل رشد است' }).getAttribute('aria-busy'),
|
||||||
|
).toBe('true');
|
||||||
|
expect(screen.getByText('در حال بررسی روندهای کسبوکار شما')).not.toBeNull();
|
||||||
|
expect(screen.getByText(/دادهها را کنار هم میگذارد/)).not.toBeNull();
|
||||||
|
expect(container.querySelector('svg')?.getAttribute('aria-hidden')).toBe('true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('describes the writing phase when the model is generating the summary', () => {
|
||||||
|
render(GrowthAIWorkingIllustration, { status: 'GENERATING' });
|
||||||
|
|
||||||
|
expect(screen.getByText('در حال نوشتن جمعبندی تحلیلی')).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('announces refresh work while covering an existing report', () => {
|
||||||
|
render(GrowthAIWorkingIllustration, { status: 'QUEUED', overlay: true });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByRole('status', { name: 'هوش مصنوعی در حال بهروزرسانی تحلیل رشد است' }),
|
||||||
|
).not.toBeNull();
|
||||||
|
expect(screen.getByText('نسخه تازه تحلیل در حال آمادهسازی است')).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -214,6 +214,15 @@
|
|||||||
<p class="text-muted-foreground px-3 pb-4 text-xs">
|
<p class="text-muted-foreground px-3 pb-4 text-xs">
|
||||||
شروع پیشبینی: {dateLabel(startDate)}. این برآورد قطعی نیست و بر پایه الگوی دادههای کامل واردشده است.
|
شروع پیشبینی: {dateLabel(startDate)}. این برآورد قطعی نیست و بر پایه الگوی دادههای کامل واردشده است.
|
||||||
</p>
|
</p>
|
||||||
|
{#if confidence === 'LOW'}
|
||||||
|
<p
|
||||||
|
class="mx-3 mb-4 rounded-md border border-amber-300/60 bg-amber-50/50 p-3 text-xs leading-6 dark:border-amber-800/70 dark:bg-amber-950/15"
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
این پیشبینی با سابقه محدود یا الگوی پرنوسان ساخته شده است؛ بازه احتمالی را مبنای تصمیم بگیرید، نه
|
||||||
|
عدد میانی را بهعنوان نتیجه قطعی.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
<details class="border-t px-3 py-3 text-xs">
|
<details class="border-t px-3 py-3 text-xs">
|
||||||
<summary class="font-semibold">جدول دادههای نمودار</summary>
|
<summary class="font-semibold">جدول دادههای نمودار</summary>
|
||||||
<div class="mt-3 max-h-72 overflow-auto">
|
<div class="mt-3 max-h-72 overflow-auto">
|
||||||
|
|||||||
@@ -32,4 +32,14 @@ describe('GrowthForecastChart', () => {
|
|||||||
await fireEvent.click(screen.getByRole('button', { name: 'مبلغ' }));
|
await fireEvent.click(screen.getByRole('button', { name: 'مبلغ' }));
|
||||||
expect(screen.getAllByText(/ریال/).length).toBeGreaterThan(0);
|
expect(screen.getAllByText(/ریال/).length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('explains that a low-confidence forecast uses limited history', () => {
|
||||||
|
render(GrowthForecastChart, {
|
||||||
|
points,
|
||||||
|
confidence: 'LOW',
|
||||||
|
startDate: '2026-07-01T00:00:00Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText(/سابقه محدود/)).not.toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,11 +10,11 @@
|
|||||||
import { Card, CardContent } from '$lib/components/ui/card';
|
import { Card, CardContent } from '$lib/components/ui/card';
|
||||||
import { useDashboardContext } from '$lib/dashboard-context.svelte';
|
import { useDashboardContext } from '$lib/dashboard-context.svelte';
|
||||||
import { growthFeatureEnabled } from '$lib/growth-feature';
|
import { growthFeatureEnabled } from '$lib/growth-feature';
|
||||||
import { reportDisplayMode } from '$lib/growth-report-state';
|
import { refreshAvailabilityLabel, reportDisplayMode } from '$lib/growth-report-state';
|
||||||
import { formatDate } from '$lib/format';
|
import { formatDate } from '$lib/format';
|
||||||
|
import GrowthAIWorkingIllustration from './GrowthAIWorkingIllustration.svelte';
|
||||||
import GrowthForecastChart from './GrowthForecastChart.svelte';
|
import GrowthForecastChart from './GrowthForecastChart.svelte';
|
||||||
import GrowthInsights from './GrowthInsights.svelte';
|
import GrowthInsights from './GrowthInsights.svelte';
|
||||||
import GrowthReportSkeleton from './GrowthReportSkeleton.svelte';
|
|
||||||
import PageHeader from './PageHeader.svelte';
|
import PageHeader from './PageHeader.svelte';
|
||||||
|
|
||||||
const dashboard = useDashboardContext();
|
const dashboard = useDashboardContext();
|
||||||
@@ -201,7 +201,9 @@
|
|||||||
></Card
|
></Card
|
||||||
>
|
>
|
||||||
{:else if (loading && !visibleReport) || displayMode === 'loading'}
|
{:else if (loading && !visibleReport) || displayMode === 'loading'}
|
||||||
<GrowthReportSkeleton />
|
<GrowthAIWorkingIllustration
|
||||||
|
status={report?.status === 'ANALYZING' || report?.status === 'GENERATING' ? report.status : 'QUEUED'}
|
||||||
|
/>
|
||||||
{:else if displayMode === 'error' || !report || !visibleReport || !evidence}
|
{:else if displayMode === 'error' || !report || !visibleReport || !evidence}
|
||||||
<Card class="border-destructive/30 rounded-lg"
|
<Card class="border-destructive/30 rounded-lg"
|
||||||
><CardContent class="space-y-4 p-10 text-center"
|
><CardContent class="space-y-4 p-10 text-center"
|
||||||
@@ -226,7 +228,12 @@
|
|||||||
<Button class="min-h-11 rounded-md" onclick={refresh} disabled={refreshing || isRunning}
|
<Button class="min-h-11 rounded-md" onclick={refresh} disabled={refreshing || isRunning}
|
||||||
><RefreshCcwIcon class={refreshing ? 'animate-spin' : ''} /> بهروزرسانی تحلیل</Button
|
><RefreshCcwIcon class={refreshing ? 'animate-spin' : ''} /> بهروزرسانی تحلیل</Button
|
||||||
>
|
>
|
||||||
<span class="text-muted-foreground text-[11px]">نوبت بعدی: {formatDate(report.nextRefreshAt)}</span>
|
<span class="text-muted-foreground text-[11px]">
|
||||||
|
{refreshAvailabilityLabel(
|
||||||
|
visibleReport.status === 'READY' && Boolean(visibleReport.aiCopy?.briefs.length),
|
||||||
|
formatDate(report.nextRefreshAt),
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -252,13 +259,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if evidence.forecast.confidence === 'INSUFFICIENT_DATA'}
|
{#if evidence.forecast.points.length === 0}
|
||||||
<Card class="rounded-lg"
|
<Card class="rounded-lg"
|
||||||
><CardContent class="p-10 text-center"
|
><CardContent class="p-10 text-center"
|
||||||
><h2 class="font-bold">برای پیشبینی قابل اتکا هنوز داده کافی نداریم</h2>
|
><h2 class="font-bold">هنوز دادهای برای ساخت پیشبینی وجود ندارد</h2>
|
||||||
<p class="text-muted-foreground mx-auto mt-2 max-w-xl text-sm leading-7">
|
<p class="text-muted-foreground mx-auto mt-2 max-w-xl text-sm leading-7">
|
||||||
این بخش فقط زمانی پیشبینی نشان میدهد که سابقه کافی برای مقایسه روزهای مشابه و ارزیابی دقت مدل
|
پس از ثبت نخستین داده کامل، مدل پیشبینی را با درجه اطمینان متناسب نمایش میدهد.
|
||||||
وجود داشته باشد.
|
|
||||||
</p></CardContent
|
</p></CardContent
|
||||||
></Card
|
></Card
|
||||||
>
|
>
|
||||||
@@ -279,6 +285,11 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if refreshing || isRunning}<GrowthReportSkeleton refreshing />{/if}
|
{#if refreshing || isRunning}
|
||||||
|
<GrowthAIWorkingIllustration
|
||||||
|
overlay
|
||||||
|
status={report.status === 'ANALYZING' || report.status === 'GENERATING' ? report.status : 'QUEUED'}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -29,6 +29,6 @@ describe('growth forecast chart presentation', () => {
|
|||||||
it('translates forecast confidence without implying certainty', () => {
|
it('translates forecast confidence without implying certainty', () => {
|
||||||
expect(formatConfidence('HIGH')).toBe('اطمینان بالا');
|
expect(formatConfidence('HIGH')).toBe('اطمینان بالا');
|
||||||
expect(formatConfidence('MEDIUM')).toBe('اطمینان متوسط');
|
expect(formatConfidence('MEDIUM')).toBe('اطمینان متوسط');
|
||||||
expect(formatConfidence('INSUFFICIENT')).toBe('داده ناکافی');
|
expect(formatConfidence('LOW')).toBe('اطمینان پایین');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,5 +43,6 @@ export function buildForecastChart(data: ForecastChartDatum[], width: number, he
|
|||||||
export function formatConfidence(value: string): string {
|
export function formatConfidence(value: string): string {
|
||||||
if (value === 'HIGH') return 'اطمینان بالا';
|
if (value === 'HIGH') return 'اطمینان بالا';
|
||||||
if (value === 'MEDIUM') return 'اطمینان متوسط';
|
if (value === 'MEDIUM') return 'اطمینان متوسط';
|
||||||
|
if (value === 'LOW') return 'اطمینان پایین';
|
||||||
return 'داده ناکافی';
|
return 'داده ناکافی';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { reportDisplayMode } from './growth-report-state';
|
import { refreshAvailabilityLabel, reportDisplayMode } from './growth-report-state';
|
||||||
|
|
||||||
describe('growth report display state', () => {
|
describe('growth report display state', () => {
|
||||||
it('keeps an uncached queued report in a visible loading state', () => {
|
it('keeps an uncached queued report in a visible loading state', () => {
|
||||||
@@ -19,3 +19,12 @@ describe('growth report display state', () => {
|
|||||||
expect(reportDisplayMode('FAILED', false, false)).toBe('error');
|
expect(reportDisplayMode('FAILED', false, false)).toBe('error');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('growth report refresh availability', () => {
|
||||||
|
it('keeps retries visibly available until an AI summary exists', () => {
|
||||||
|
expect(refreshAvailabilityLabel(false, '۱ شهریور')).toBe(
|
||||||
|
'تا آمادهشدن توضیح هوشمند، تلاش دوباره محدود نیست.',
|
||||||
|
);
|
||||||
|
expect(refreshAvailabilityLabel(true, '۱ شهریور')).toBe('نوبت بعدی: ۱ شهریور');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -9,3 +9,9 @@ export function reportDisplayMode(
|
|||||||
if (status === 'QUEUED' || status === 'ANALYZING' || status === 'GENERATING') return 'loading';
|
if (status === 'QUEUED' || status === 'ANALYZING' || status === 'GENERATING') return 'loading';
|
||||||
return 'error';
|
return 'error';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function refreshAvailabilityLabel(hasAISummary: boolean, nextRefreshLabel: string): string {
|
||||||
|
return hasAISummary
|
||||||
|
? `نوبت بعدی: ${nextRefreshLabel}`
|
||||||
|
: 'تا آمادهشدن توضیح هوشمند، تلاش دوباره محدود نیست.';
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user