1. Niyə axın UX‑i məhz ChatGPT App‑də vacibdir
Adi vebdə istifadəçilər artıq fayl yükləmə progress barına, fırlanan spinnerə və skeleton‑ekrana öyrəşiblər. Amma ChatGPT tətbiqlərində sizin əlavə bir “rəqibiniz” var: mətnin real vaxtda stream olunmasını bacaran modelin özü. Əgər vidcet həmin an izahatsız statik spinner göstərirsə, hiss olaraq uduzur — GPT “canlıdır”, App isə “donub qalır”.
Uzun əməliyyatlar üçün UX bir neçə vəzifəni birdən həll edir. İlk öncə, istifadəçi narahatlığını azaldır: “donub qaldı, yoxsa hələ düşünür?” sualındansa, o, statusları, mərhələləri, faizləri və hətta ilk nəticələri görür. İkincisi, etibarı artırır: App açıq‑aydın nə etdiyini göstərdikdə (rəyləri analiz edir, qiymətləri müqayisə edir, hədiyyələri filtrdən keçirir), bu, məhz operational transparency — əməliyyatların şəffaflığını yaradır. İstifadəçi anlayır: “kapotun altında” sehr deyil, aydın addımlar ardıcıllığı var.
Və nəhayət, axın UX‑i təkcə irəliləyiş deyil. Bu, həm də idarəetmədir. Ağır hədiyyə seçimini dayandırmaq, parametrləri dəyişmək və dərhal yenidən başlatmaq imkanı — “mən idarə edirəm, serverdən mərhəmət gözləmirəm” hissinin vacib hissəsidir.
Bu mühazirədə biz:
- uzunmüddətli tapşırıq üçün sadə vəziyyət modelini layihələndirəcəyik (pending / in_progress / partial_ready / …);
- onu vidcet üçün React vəziyyətinə çevirəcəyik;
- irəliləyişi və qismən nəticələri necə dürüst göstərməyi anlayacağıq;
- belə tapşırıqların ləğvini səliqəli şəkildə reallaşdıracağıq.
Bunların hamısını — bizim GiftGenius nümunəsində.
2. GiftGenius‑da uzun əməliyyatın vəziyyət modeli
Hadisə axınını if (event.type === …) qarışığına çevirməmək üçün, uzun tapşırıq haqqında müştəridə sonlu avtomat (state machine) kimi düşünmək rahatdır. GiftGenius üçün biz nəzəriyyədən artıq sizə tanış olan bu məntiqi vəziyyətlərdən istifadə edəcəyik: pending, in_progress, partial_ready, completed, failed, canceled və gözləmə vəziyyəti idle.
Onları cədvəldə toplayaq:
| Status | Backend‑də nə deməkdir | İstifadəçi vidcetdə nə görür |
|---|---|---|
|
Hələ iş (job) yoxdur | Adi forma, düymə “Hədiyyə seç” |
|
Job yaradılıb, worker‑in başlanmasını gözləyirik | Düymə deaktivdir, yüngül spinner |
|
Worker işləyir, job.progress göndərir | Progress bar və ya addımlar “Addım 1 / 3” |
|
İlk nəticələr var, iş davam edir | Artıq ilk hədiyyələr görünür + hələ də progress |
|
job.completed gəldi | Hədiyyələrin yekun siyahısı, CTA (“Almaq”) |
|
job.failed gəldi | Xəta mesajı + “Yenidən cəhd et” düyməsi |
|
job.canceled və ya cancel‑bayraq gəldi | “Seçim dayandırıldı” mətni + “Yenidən başla” |
Bu model MCP hadisələrinə də əla oturur. Məsələn, job.started pending‑dən in_progress‑a keçirir, job.progress isə ya sadəcə in_progress vəziyyətində faizləri yeniləyir, ya da “bizdə ilk kartlar peyda oldu” deyib sizi partial_ready vəziyyətinə keçirir. job.completed, job.failed və job.canceled hekayəni bağlayır.
Bu, kiçik bir vəziyyət avtomatı kimi görünür:
stateDiagram-v2
[*] --> idle
idle --> pending: job yarat
pending --> in_progress: job.started
in_progress --> partial_ready: ilk qismən nəticələr
partial_ready --> completed: job.completed
in_progress --> completed: job.completed (qismən olmadan)
in_progress --> failed: job.failed
partial_ready --> failed: job.failed
in_progress --> canceled: job.canceled
partial_ready --> canceled: job.canceled
failed --> idle: yenidən işə salma
canceled --> idle: yenidən işə salma
Vidcet kodunda bunu sadə bir tiplə göstərə bilərik:
type JobStatus =
| 'idle'
| 'pending'
| 'in_progress'
| 'partial_ready'
| 'completed'
| 'failed'
| 'canceled';
interface GiftJobState {
status: JobStatus;
percent?: number;
stage?: string;
error?: string;
}
Hələlik bu, yalnız məlumat formasından ibarətdir. Sonra biz onu MCP‑dən və ya stream‑dən gələn hadisələr daxil olduqca məzmunla dolduracağıq.
3. Vidcet vəziyyəti: React komponenti axını “dinləyir”
Vəziyyət modelimizi GiftGenius vidcetinin React koduna keçirək. Saxlamalı olduğumuz şeylər:
- hansı hadisələrin bu tapşırığa aid olduğunu bilmək üçün cari jobId;
- tapşırığın vəziyyəti (status, percent, stage);
- qismən nəticələr massivi (hədiyyə kartları);
- düymələr üçün bayraqlar: ləğv etmək olarmı, yenidən başlatmaq olarmı.
Bunu tək bir interfeyslə təsvir edək:
interface GiftSuggestion {
id: string;
title: string;
price: string;
}
interface GiftWidgetState extends GiftJobState {
jobId?: string;
partialGifts: GiftSuggestion[];
}
Komponentdə ilkinləşdirmə çox sadə görünə bilər:
const [state, setState] = useState<GiftWidgetState>({
status: 'idle',
partialGifts: [],
});
Sonra iki əsas məqamımız var.
Birincisi, tapşırığın işə salınması. Bu, Apps SDK vasitəsilə MCP alətinin (callTool) çağırışı və ya job yaradan və jobId qaytaran backend‑ə HTTP sorğusu ola bilər. Bu mühazirədə async‑pipeline‑ın necə qurulduğuna dərindən varmırıq — bunu növbələr və worker‑lər mövzusunda edəcəyik. İndi bizə yalnız UI‑ın artıq yaradılmış jobId‑a reaksiyası vacibdir.
İkincisi, həmin jobId üzrə hadisələrə abunə olmaq. Təcrübədə bu, useJobEvents(jobId) kimi bir hook və ya daxilində SSE bağlantısı və ya MCP‑müştəri istifadə edən, amma çöldə bizə normal JS obyektləri verən subscribeToJobEvents sarğısı ola bilər. Aşağıda sadəlik üçün useEffect daxilində subscribeToJobEvents variantını göstəririk:
useEffect(() => {
if (!state.jobId) return;
const unsubscribe = subscribeToJobEvents(state.jobId, handleEvent);
return () => unsubscribe();
}, [state.jobId]);
Burada handleEvent sadəcə hadisənin tipindən asılı olaraq state‑i yeniləyir. Aşağıda onun emal etdiyi üç hadisə qrupunu növbə ilə nəzərdən keçirəcəyik: irəliləyiş, qismən nəticələr və tapşırığın ləğvi.
4. İrəliləyişin vizualizasiyası: faizlər, mərhələlər və dürüstlük
UX‑də irəliləyiş iki cür olur: müəyyən (determinate) və müəyyən olunmayan (indeterminate). Birinci halda siz həqiqətən işin nə qədərinin görüldüyünü bilirsiniz: məsələn, workflow‑da 4 addım var və ya 100 fayldan 30‑u emal edilib. İkinci halda isə səmimi şəkildə nə qədər gözləmək qaldığını bilmədiyinizi deyirsiniz və saxta “73 %” əvəzinə “düşünürük” animasiyası göstərirsiniz.
GiftGenius‑da məntiq belə ola bilər. Əgər backend həqiqətən irəliləyişi hesablayırsa — məsələn, collect_sources, analyze_preferences, rank_candidates, enrich_descriptions addımları varsa — onda job.progress hadisəsində stepCurrent, stepTotal, statusText və (opsional) məntiqli percent olan payload qaytara bilərsiniz.
TS‑də hadisə tipi:
interface JobProgressPayload {
stepCurrent: number;
stepTotal: number;
percent?: number;
statusText: string;
}
interface JobEvent {
type:
| 'job.started'
| 'job.progress'
| 'job.partial_result'
| 'job.completed'
| 'job.failed'
| 'job.canceled';
jobId: string;
payload?: any;
}
Komponentdə irəliləyiş emalçısı:
function handleJobProgress(payload: JobProgressPayload) {
setState(prev => ({
...prev,
status: prev.status === 'idle' ? 'in_progress' : prev.status,
percent: payload.percent,
stage: `${payload.stepCurrent} / ${payload.stepTotal}: ${payload.statusText}`,
}));
}
JSX‑də həm progress barı, həm də mərhələ mətnini çəkə bilərsiniz:
{(state.status === 'pending' || state.status === 'in_progress' || state.status === 'partial_ready') && (
<div>
{typeof state.percent === 'number'
? <progress value={state.percent} max={100} />
: <div className="spinner" />}
{state.stage && <p>{state.stage}</p>}
</div>
)}
Burada bir psixoloji nüans var. Dürüst faiziniz yoxdursa, 30 saniyə “donmuş” 99 % əvəzinə sadəcə “Addım 2 / 3: üstünlükləri təhlil edirik” mətnini və müəyyən olunmayan progress barı (indeterminate bar animasiyası) göstərmək daha yaxşıdır. Bu cür hibrid (mərhələlər + indeterminate göstərici) qalan işin dəqiq hesablanması çətin olan AI əməliyyatları üçün əla işləyir.
5. Qismən nəticələr: hər şey ideal olana qədər gözləməyə ehtiyac yoxdur
Axın UX‑in ən xoş hissəsi — qismən nəticələrdir. Əgər 5–7 saniyəyə artıq ilk uyğun hədiyyələriniz varsa, istifadəçini niyə gözlədəsiniz? Onları dərhal göstərə, qalanını isə sonra yükləyə bilərsiniz.
GiftGenius‑da bu belə görünə bilər. Backend iş getdikcə ya xüsusi job.partial_result hadisələri, ya da məsələn, yeni tövsiyə dəsti ilə resource.updated göndərir. Hər belə hadisə mövcud olanlara əlavə edilən hədiyyələr massivini gətirir.
Şərti payload forması:
interface PartialResultPayload {
gifts: GiftSuggestion[];
isFinalChunk?: boolean;
}
Emalçı:
function handlePartialResult(payload: PartialResultPayload) {
setState(prev => ({
...prev,
status: 'partial_ready',
partialGifts: [...prev.partialGifts, ...payload.gifts],
}));
}
JSX‑də isə tapşırıq başa çatıb‑çatmamasından asılı olmayaraq kartları render edirsiniz:
<section>
{state.partialGifts.map(gift => (
<GiftCard key={gift.id} gift={gift} />
))}
{(state.status === 'in_progress' || state.status === 'partial_ready') && (
<p>Biz hələ də daha çox variant axtarırıq…</p>
)}
</section>
Burada xatırlanmalı olan bir neçə vacib UX nüansı var.
Birincisi, kəskin layout sıçrayışlarından (layout shift) qaçmağa çalışın. Əgər yeni hədiyyələri siyahının yuxarısına əlavə edirsinizsə, istifadəçi oxuduğu yeri itirəcək. Onları siyahının sonuna (append‑only) əlavə etmək və görünüşü yumşaq animasiya ilə göstərmək daha təhlükəsizdir.
İkincisi, refinement strategiyasından istifadə edirsinizsə (öncə sürətli ilkin siyahı, sonra onu “cilalayıb” yenidən sıralamaq), interaktivliyə diqqətlə yanaşmaq lazımdır. Nəticələr “qaralama” ikən “Almaq” düyməsinə icazə verməyin və ya belə siyahını açıq şəkildə “ilkin” kimi qeyd edin. Əks halda istifadəçi hədiyyə seçəcək, bir saniyə sonra isə o, yoxa çıxacaq və ya qiyməti dəyişəcək — bu, UX fəlakətidir.
Üçüncüsü, partial_ready vəziyyəti vizual olaraq completed‑dən fərqlənməlidir. İstifadəçi siyahının hələ də dolduğunu anlamalıdır: ya “Seçim davam edir” mətni ilə, ya küncdə kiçik spinnerlə, ya da yeni kartların neytral vurğulanması ilə.
6. Uzun əməliyyatların ləğvi: UX və texnika
İstifadəçiyə ağır hədiyyə seçimini başlatmaq hüququ verirsinizsə, demək olar ki, həmişə ona bunu dayandırmaq hüququnu da verməlisiniz. Ləğv — təkcə LLM və worker resurslarına qənaət deyil, həm də nəzarət hissidir: “nə baş verdiyini özüm müəyyən edirəm”.
UX baxımından ləğv düyməsi kifayət qədər nəzərə çarpan olmalıdır, amma ekranın ortasında qışqıran qırmızı panel şəklində yox. Yaxşı işləyən cütlük: əsas “Seçimi dayandır” düyməsi və kiçik ikinci mətn “istənilən an yenidən başlada bilərsiniz”. İstifadəçiyə nəyin ləğv olunduğu — cari analiz, tətbiqin tamamı yox — aydın olmalıdır.
Texniki baxımdan iki ləğv səviyyəniz var.
Birincisi, frontend-də ləğv: lokal fetch çağırışını yarıda kəsə və ya SSE bağlantısını bağlaya bilərsiniz. Bu, trafiki qənaət edir, amma təkbaşına backend‑də worker‑i dayandırmır.
İkincisi, job‑un həqiqi ləğvi: MCP aləti və ya POST /jobs/{jobId}/cancel HTTP endpoint vasitəsilə tapşırığı canceled kimi qeyd etmək və worker‑ə düzgün tamamlanmaq şansı vermək. Bu zaman server job.canceled hadisəsi göndərir və siz onu vidcetdə emal edirsiniz.
Vidcet baxımından görünüş:
async function handleCancelClick() {
if (!state.jobId) return;
// UI‑nin optimist yenilənməsi
setState(prev => ({ ...prev, status: 'canceled' }));
try {
await cancelJobOnServer(state.jobId); // MCP tool və ya HTTP
} catch (e) {
// Serverdə ləğv alınmadısa — statusu geri qaytaraq
setState(prev => ({ ...prev, status: 'in_progress' }));
}
}
Və düymə:
<button
onClick={handleCancelClick}
disabled={
state.status !== 'pending' &&
state.status !== 'in_progress' &&
state.status !== 'partial_ready'
}
>
Seçimi dayandır
</button>
Burada biz optimist UI istifadə edirik: serverdən təsdiq gözləmədən dərhal canceled vəziyyətinə keçiririk. Bu, ləğv saniyələr çəkə bildikdə faydalıdır — istifadəçi hərəkətinin qəbul olunduğunu dərhal görür. Amma server worker sona çatmağa macal tapıbsa, yenə də job.completed və ya job.failed qaytara bilər. Hadisə emalçısında belə “gecikmiş” finaları süzmək və məsələn, artıq canceled vəziyyətini üzərinə yazmamaq lazımdır.
Daha konservativ yanaşma — pessimist UI: əvvəlcə “Ləğv edirik…” vəziyyətini göstəririk, düyməni bloklayırıq və yalnız job.canceled gəldikdən sonra tapşırığı canceled vəziyyətinə keçiririk. Bu, reallaşdırması daha sadə, amma vizual olaraq daha az çevikdir. Yanaşmanı backend‑inizin SLA‑sından asılı olaraq seçə bilərsiniz.
7. Hamısını birləşdirək: GiftGenius üçün mini irəliləyiş paneli
İndi ayrı hissələri birləşdirək. Artıq biz yazdıq:
- irəliləyiş emalçısı handleJobProgress,
- qismən nəticələr emalçısı handlePartialResult,
- və ləğv emalçısı handleCancelClick.
Əslində bu, əvvəlki bölmədəki ümumi handleEvent‑in özüdür: o, job.progress, job.partial_result, job.canceled və digər hadisələrə reaksiya verir və tək bir komponentin vəziyyətini yeniləyir. Qalır bütün bunları kiçik GiftJobPanel komponentinə bükmək; o:
- hədiyyə seçimini işə salır;
- jobId üzrə hadisələri dinləyir;
- irəliləyişi göstərir;
- qismən nəticələri render edir;
- tapşırığı ləğv etməyə imkan verir.
Apps SDK / MCP ilə inteqrasiyanın detallarını xeyli sadələşdirək və vəziyyət məntiqinə fokuslanaq.
export function GiftJobPanel() {
const [state, setState] = useState<GiftWidgetState>({
status: 'idle',
partialGifts: [],
});
useEffect(() => {
if (!state.jobId) return;
const unsub = subscribeToJobEvents(state.jobId, event => {
switch (event.type) {
case 'job.started':
setState(prev => ({ ...prev, status: 'in_progress' }));
break;
case 'job.progress':
handleJobProgress(event.payload);
break;
case 'job.partial_result':
handlePartialResult(event.payload);
break;
case 'job.completed':
setState(prev => ({ ...prev, status: 'completed' }));
break;
case 'job.failed':
setState(prev => ({
...prev,
status: 'failed',
error: event.payload?.message ?? 'Nəsə səhv oldu',
}));
break;
case 'job.canceled':
setState(prev => ({ ...prev, status: 'canceled' }));
break;
}
});
return () => unsub();
}, [state.jobId]);
Tapşırığın işə salınması MCP aləti start_gift_search vasitəsilə reallaşdırıla bilər:
async function handleStartClick() {
setState({
status: 'pending',
partialGifts: [],
});
const jobId = await startGiftSearchOnServer(/* istifadəçi parametrləri */);
setState(prev => ({ ...prev, jobId }));
}
Sonra JSX‑də:
return (
<div>
{state.status === 'idle' && (
<button onClick={handleStartClick}>Hədiyyə seç</button>
)}
{['pending', 'in_progress', 'partial_ready'].includes(state.status) && (
<ProgressSection state={state} onCancel={handleCancelClick} />
)}
<GiftsList gifts={state.partialGifts} status={state.status} />
{state.status === 'failed' && (
<ErrorSection error={state.error} onRetry={handleStartClick} />
)}
{state.status === 'canceled' && (
<p>Seçim dayandırıldı. İstəsəniz, başqa parametrlərlə yenidən başlada bilərsiniz.</p>
)}
</div>
);
ProgressSection, GiftsList, ErrorSection kimi ayrı alt‑komponentlər əsas komponenti “spagetti”yə çevirməməyə kömək edir. Amma əsas fikir eynidir: bütün vidcet birbaşa MCP hadisələrinə və artıq bildiyiniz axın kanallarına uyğun gələn tək bir anlaşılan vəziyyət modeli ilə idarə olunur.
8. ChatGPT dialoqu ilə bir az əlaqə haqqında
Bu mühazirə vidcetə fokuslansa da, istifadəçinin hələ də modellə dialoqda olduğunu unutmaq olmaz. Yaxşı ssenari belə görünür: GPT istifadəçiyə GiftGenius‑u işə saldığını bildirir, sonra vidcet irəliləyişi göstərir, GPT isə bunu mətnlə dəstəkləyir: “İndi genişləndirilmiş hədiyyə seçimini işə saldım, siyahının tədricən necə dolduğunu görəcəksiniz”.
Seçim bitdikdən sonra ChatGPT ToolOutput‑dan nəticəni götürüb insan dilinə yaxın xülasə qura bilər: “10 variant tapdım, qısa icmal budur, tam siyahı isə aşağıdakı vidcetdədir”. Mətn axını ilə axın UI‑ının bu dueti bütöv təcrübə yaradır.
Bu əlaqə workflow və commerce modullarında daha da vacib olacaq; burada hər uzun addım (səbətin analizi, mövcudluğun yoxlanması, ödənişin gözlənməsi) həm mətndə, həm də interfeysdə anlaşılan olmalıdır.
9. Axın UX‑ində tipik səhvlər
Səhv №1: “Mətnsiz əbədi spinner”.
Ən çox rast gəlinən anti‑pattern — sadəcə animasiyanı fırladıb nələrin baş verdiyini heç cür izah etməməkdir. İstifadəçi sistemin faydalı bir şey edib‑etmədiyini, yoxsa donub qaldığını anlamır. Sadə mərhələ mətni ilə (“Populyar hədiyyələri toplayırıq…”, “Rəyləri analiz edirik”) bunu düzəltmək olar; daha yaxşısı — vidcet vəziyyətində artıq saxladığınız aydın pending, in_progress, partial_ready statuslarıdır.
Səhv №2: Saxta irəliləyiş faizləri.
“Etibarı artırmaq” üçün uydurma irəliləyiş çəkmək cəhdi (havadan “73 %”) adətən əks effekt verir. İstifadəçi tez başa düşür ki, 99 % 20 saniyə qala bilər və göstəriciyə inamını itirir. Dürüst metrika yoxdursa, aldatmaqdansa mərhələlərdən və müəyyən olunmayan progress barından istifadə edin.
Səhv №3: Hər şeyi pozan qismən nəticələr.
Bəzən qismən nəticələri hər hadisədə tam yenidən yığılan siyahı kimi reallaşdırırlar: o, gah yox olur, gah da yenidən qarışır. Nəticədə istifadəçi karta klikləyir, o isə qəfil aşağı qaçır. Belə “titrəmə” xüsusilə commerce ssenarilərində qorxuludur. Kartları ehtiyatla əlavə etmək (çox vaxt — yalnız sona), açarları qorumaq və layout sıçrayışlarını minimallaşdırmaq daha doğrudur.
Səhv №4: Heç nəyi ləğv etməyən ləğv.
Elə də olur ki, vidcetdə “Ləğv et” düyməsi var, amma bu, UI‑ı gizlətməkdən başqa bir şey etmir və serverdəki real job‑u dayandırmır. Nəticədə resurslar xərclənməyə davam edir, gecikmiş job.completed gəlir, istifadəçi isə artıq hər şeyin dayandığını düşünür. Həqiqi ləğv həm frontend‑i (düymələri söndürmək, stream‑i dayandırmaq), həm də backend‑i (worker‑ə cancel siqnalı vermək və job.canceled hadisəsini almaq) əhatə etməlidir.
Səhv №5: Finalı görməməzlikdən gəlmək və “boş” xəta ekranı.
Bəzən job.completed sonra vidcet heç bir növbəti addım təklif etmədən hədiyyə siyahısını göstərir, job.failed zamanı isə yalnız “Xəta 500” kimi texniki mesaj verir. Hər iki halda UX yarımçıq qalır. Düzgünü — sonda qısa xülasə və aydın CTA (“Seçimi yadda saxla”, “Alışa keç”) vermək, xətada isə insan dilində izah və “Yenidən cəhd et” və ya “Parametrləri dəyiş” düymələri təqdim etməkdir; istifadəçini status kodu ilə tək‑tənha qoymayın.
GO TO FULL VERSION