Skip to content

Commit a80e465

Browse files
Merge pull request #66 from programmerShinobi/feat/phase3-chat-advisor
[feat](insights): group the landing into four lenses + three activities
2 parents 01560cc + 59894ad commit a80e465

6 files changed

Lines changed: 143 additions & 38 deletions

File tree

scripts/check-bundle-size.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { gzipSync } from 'node:zlib';
1616
// This reads the real initial set from index.html rather than guessing by filename, so a new lazy or
1717
// shared async chunk can't silently be mis-counted. Headroom catches a real regression; raise the
1818
// budgets deliberately (with a note) if the app grows.
19-
const JS_INITIAL_BUDGET_KB = 121; // 120→121 for Phase 3 (chat + copilot) 2026-07-19: both features are LAZY chunks; the only initial delta is their dynamic-import stubs + a few refs of wiring (~0.3kB gzip, negligible FCP). NFR ceiling is far higher.
19+
const JS_INITIAL_BUDGET_KB = 122; // 120→121 for Phase 3 (chat + copilot) 2026-07-19: both features are LAZY chunks; the only initial delta is their dynamic-import stubs + a few refs of wiring (~0.3kB gzip, negligible FCP). 121→122 on 2026-07-25 for the Insights landing's grouping + "open this when" copy: those 11 dict keys are only READ on a lazy tab, but `dict` is one object literal, so Vite cannot tree-shake properties and every key costs first paint wherever it is used — an accepted, known property of this i18n design, paid here as ~0.6kB gzip. NFR ceiling is far higher.
2020
const JS_TOTAL_BUDGET_KB = 284; // raised 200→260 (Insights bilingualisation 2026-07-15); 260→268 for the Phase 3 Chat Advisor 2026-07-19 (adapter + hook + panel, all in a LAZY chunk — the FAB is lazy too, so the initial budget is untouched); 268→278 for the 2026-07-23 scenario-coverage expansion (cost/ops, risk catalog, sensitivity, migration, dimension/factor/QA lookups, app-usage FAQ — all pure data-driven text in the same lazy chunk; NFR cap is 300); 278→281 for the 2026-07-25 Chat Advisor capability browser + "How to use this" pane (bilingual help copy, same lazy chunk — initial JS unchanged at 120.8/121, so first paint is unaffected); 281→284 for the 2026-07-25 role-based "Start here" section in the Guide (six bilingual role paths — prose only, in the already-lazy ManualBook chunk that loads on demand; initial JS still 120.8/121, NFR cap 300)
2121
const CSS_BUDGET_KB = 29; // 25→27 (Fase 2g polish 2026-07-18); 27→29 for Phase 3 2026-07-19 (chat panel + copilot overlay/launcher/Dos-Don'ts cards); still under the ~30kB NFR ceiling
2222

src/components/insights/LearnView.test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,24 @@ describe('LearnView', () => {
1919
return { onOpenAdvisor, onLoadLab };
2020
};
2121

22+
// The landing groups its seven sections into "four lenses on the same 21 architectures" + "three
23+
// activities". Rendering them as one flat grid read as seven separate libraries to get through —
24+
// wrong, and the main reason the tab felt daunting. These pin the structure and the routing line.
25+
it('groups the landing into the four lenses and the three activities, never one flat list', () => {
26+
render();
27+
expect(screen.getByRole('heading', { name: 'Read about the architectures' })).toBeInTheDocument();
28+
expect(screen.getByRole('heading', { name: 'Build the skill' })).toBeInTheDocument();
29+
// The point of the grouping: state that the four are one subject read four ways.
30+
expect(screen.getByText(/Four ways of reading the same 21 architectures/)).toBeInTheDocument();
31+
});
32+
33+
it('tells the reader when each section is the right one to open, not just what it holds', () => {
34+
render();
35+
// One "Open this when…" line per section: the descriptions say what a section CONTAINS, which
36+
// is not the question someone staring at seven cards is actually asking.
37+
expect(screen.getAllByText(/^Open this when/)).toHaveLength(AVAILABLE_SECTIONS.length);
38+
});
39+
2240
it('every architecture has a Catalog, Playbook, Review, and Library entry (parity with the model)', () => {
2341
for (const dim of DIMENSION_ORDER) {
2442
for (const opt of DIMENSIONS[dim].options) {

src/components/insights/LearnView.tsx

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
IconScale,
1818
} from '@tabler/icons-react';
1919
import { useI18n } from '../../i18n/I18nContext';
20-
import { AVAILABLE_SECTIONS, sectionMeta } from '../../config/sections';
20+
import { AVAILABLE_SECTIONS, sectionMeta, SECTION_GROUPS, SECTION_WHEN } from '../../config/sections';
2121
import { contentBySection, contentBySlug, docBody, docTitle, docTldr } from '../../lib/content';
2222
import { renderMarkdown } from '../../lib/markdown';
2323
import { DIMENSIONS, DIMENSION_ORDER } from '../../config/dimensions';
@@ -599,39 +599,58 @@ export default function LearnView({ onOpenAdvisor, onLoadLab, initialTarget }: P
599599
<h1 style={{ fontSize: 'var(--aa-fs-xl)', fontWeight: 600, marginBottom: '6px' }}>{t('learn.title')}</h1>
600600
<p style={{ fontSize: '13px', color: 'var(--color-text-secondary)', marginBottom: '6px', maxWidth: '72ch' }}>{t('learn.intro')}</p>
601601
<p style={{ fontSize: '12px', color: 'var(--color-text-tertiary)', marginBottom: '18px' }}>{t('learn.readingHint')}</p>
602-
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(min(260px, 100%), 1fr))', gap: '14px' }}>
603-
{AVAILABLE_SECTIONS.map((s) => {
604-
const Icon = s.icon;
605-
const isLens = (LENSES as string[]).includes(s.id);
606-
const guides = s.id === 'catalog' ? 0 : contentBySection(s.id).length;
607-
return (
608-
<button key={s.id} type="button" className="learn-card aa-reveal" style={{ ...cardBase, padding: 'var(--aa-panel-pad)', display: 'flex', flexDirection: 'column', gap: '10px' }} onClick={() => setSection(s.id)}>
609-
<span className="learn-chip">
610-
<Icon size={19} aria-hidden />
611-
</span>
612-
<span style={{ fontSize: '16px', fontWeight: 600 }}>{t(s.label)}</span>
613-
<span style={{ fontSize: '12.5px', color: 'var(--color-text-secondary)', lineHeight: 1.5, flexGrow: 1 }}>{t(s.desc)}</span>
614-
<span style={{ display: 'flex', alignItems: 'center', gap: '5px', fontSize: '12px', color: 'var(--color-text-info)', fontWeight: 600, flexWrap: 'wrap' }}>
615-
{isLens ? (
616-
<>
617-
{totalArchitectures} {t('learn.architectures')}
618-
{guides > 0 && <span style={{ color: 'var(--color-text-tertiary)' }}>· {guides} {t('learn.guidesWord')}</span>}
619-
</>
620-
) : s.id === 'roadmap' ? (
621-
<>{LEARNING_PATHS.length} {t('learn.paths')}</>
622-
) : s.id === 'academy' ? (
623-
<>{ACADEMY_QUIZZES.length} {t('learn.modules')}</>
624-
) : s.id === 'lab' ? (
625-
<>{LAB_EXPERIMENTS.length} {t('learn.experiments')}</>
626-
) : (
627-
<>{guides} {t('learn.articles')}</>
628-
)}
629-
<IconArrowRight size={13} aria-hidden />
630-
</span>
631-
</button>
632-
);
633-
})}
634-
</div>
602+
603+
{/* The seven sections are NOT seven peers, and rendering them as one flat grid was the single
604+
biggest source of confusion here: four of them are the SAME 21 architectures read from four
605+
angles, while the other three are activities you do. Someone who cannot see that boundary
606+
reads "Catalog, Playbook, Review, Library" as four separate libraries to get through, which
607+
is both wrong and daunting. Grouping states the structure the codebase already knew. */}
608+
{SECTION_GROUPS.map((group) => {
609+
const sections = AVAILABLE_SECTIONS.filter((s) => group.ids.includes(s.id));
610+
if (sections.length === 0) return null;
611+
return (
612+
<section key={group.id} style={{ marginBottom: '26px' }}>
613+
<h2 style={{ fontSize: '14px', fontWeight: 600, marginBottom: '3px' }}>{t(group.label)}</h2>
614+
<p style={{ fontSize: '12.5px', color: 'var(--color-text-secondary)', lineHeight: 1.5, marginBottom: '12px', maxWidth: '72ch' }}>{t(group.desc)}</p>
615+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(min(260px, 100%), 1fr))', gap: '14px' }}>
616+
{sections.map((s) => {
617+
const Icon = s.icon;
618+
const isLens = (LENSES as string[]).includes(s.id);
619+
const guides = s.id === 'catalog' ? 0 : contentBySection(s.id).length;
620+
return (
621+
<button key={s.id} type="button" className="learn-card aa-reveal" style={{ ...cardBase, padding: 'var(--aa-panel-pad)', display: 'flex', flexDirection: 'column', gap: '10px' }} onClick={() => setSection(s.id)}>
622+
<span className="learn-chip">
623+
<Icon size={19} aria-hidden />
624+
</span>
625+
<span style={{ fontSize: '16px', fontWeight: 600 }}>{t(s.label)}</span>
626+
<span style={{ fontSize: '12.5px', color: 'var(--color-text-secondary)', lineHeight: 1.5 }}>{t(s.desc)}</span>
627+
{/* "Open this when…" — the descriptions say what a section CONTAINS; this says
628+
when it is the right one to pick, which is the actual question being asked. */}
629+
<span style={{ fontSize: '12px', color: 'var(--color-text-tertiary)', lineHeight: 1.5, flexGrow: 1 }}>{t(SECTION_WHEN[s.id])}</span>
630+
<span style={{ display: 'flex', alignItems: 'center', gap: '5px', fontSize: '12px', color: 'var(--color-text-info)', fontWeight: 600, flexWrap: 'wrap' }}>
631+
{isLens ? (
632+
<>
633+
{totalArchitectures} {t('learn.architectures')}
634+
{guides > 0 && <span style={{ color: 'var(--color-text-tertiary)' }}>· {guides} {t('learn.guidesWord')}</span>}
635+
</>
636+
) : s.id === 'roadmap' ? (
637+
<>{LEARNING_PATHS.length} {t('learn.paths')}</>
638+
) : s.id === 'academy' ? (
639+
<>{ACADEMY_QUIZZES.length} {t('learn.modules')}</>
640+
) : s.id === 'lab' ? (
641+
<>{LAB_EXPERIMENTS.length} {t('learn.experiments')}</>
642+
) : (
643+
<>{guides} {t('learn.articles')}</>
644+
)}
645+
<IconArrowRight size={13} aria-hidden />
646+
</span>
647+
</button>
648+
);
649+
})}
650+
</div>
651+
</section>
652+
);
653+
})}
635654
</div>
636655
);
637656
}

src/components/overlays/ManualBook.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -386,10 +386,22 @@ export function ManualBook({ open, onClose, levels, weights }: Props) {
386386
'Bagian analisis menambah radar trade-off, sensitivitas ("perubahan tunggal apa yang membalik pilihan?"), pembanding skenario pin-A/B, peringatan anti-pattern, jalur migrasi, daftar risiko, fitness function, indikator biaya/operasional, dan sketsa C4.',
387387
)}
388388

389-
<strong className="aa-manual-subhead">{L('Insights — seven ways into the same knowledge', 'Insightstujuh pintu ke pengetahuan yang sama')}</strong>
389+
<strong className="aa-manual-subhead">{L('Insights — four lenses plus three ways to practise', 'Wawasanempat lensa plus tiga cara berlatih')}</strong>
390390
{p(
391-
'Catalog (discover each of the 21 architectures), Playbook (step-by-step implementation), Review (structured evaluation with a verdict), Library (reference: concepts & terminology + evergreen articles), Roadmap (learning paths), Academy (quizzes with explanations), and Lab (experiments that load a prepared scenario into the real engine). Everything is bilingual and cross-linked.',
392-
'Katalog (kenali 21 arsitektur), Playbook (implementasi langkah demi langkah), Review (evaluasi terstruktur dengan verdict), Pustaka (referensi: konsep & terminologi + artikel evergreen), Peta Belajar (jalur belajar), Akademi (kuis dengan penjelasan), dan Lab (eksperimen yang memuat skenario siap pakai ke mesin sungguhan). Semuanya dwibahasa dan saling bertaut.',
391+
'The seven cards on this tab are not seven separate bodies of material, and reading them that way is what makes the tab look enormous. Four of them — Catalog, Playbook, Review and Library — are the SAME 21 architectures read from four angles: every architecture appears in all four, and each page has a switcher to jump between them. The other three are things you do rather than read.',
392+
'Tujuh kartu di tab ini bukan tujuh kumpulan materi yang terpisah, dan membacanya seperti itulah yang membuat tab ini terasa raksasa. Empat di antaranya — Katalog, Playbook, Review, dan Pustaka — adalah 21 arsitektur YANG SAMA dibaca dari empat sudut: tiap arsitektur muncul di keempatnya, dan tiap halaman punya pengalih untuk melompat antar sudut. Tiga sisanya adalah hal yang Anda kerjakan, bukan dibaca.',
393+
)}
394+
{p(
395+
'The four lenses, by the question they answer: Catalog — what is this and where is it used, when you do not yet recognise the name. Playbook — how do I actually build it, once the decision is made: prerequisites, ordered steps, best practices and the pitfalls people hit. Review — how do these two compare, when you are weighing options: pros and cons, performance, scalability, developer experience, and a verdict. Library — what does this term mean, for definitions, concepts and standards.',
396+
'Empat lensa itu, menurut pertanyaan yang dijawabnya: Katalog — ini apa dan dipakai di mana, ketika Anda belum mengenali namanya. Playbook — bagaimana membangunnya, setelah keputusan diambil: prasyarat, langkah berurutan, praktik terbaik, dan jebakan yang sering ditemui. Review — bagaimana kedua opsi ini dibandingkan, ketika Anda sedang menimbang: pro dan kontra, performa, skalabilitas, pengalaman pengembang, dan sebuah verdik. Pustaka — istilah ini artinya apa, untuk definisi, konsep, dan standar.',
397+
)}
398+
{p(
399+
'The three practice sections: Roadmap gives you an ordered route through the lenses, for when choosing a starting point is itself the obstacle. Academy asks questions and marks them on your device, so you find out what you actually retained rather than what felt familiar; a wrong answer links back to the page that teaches it. Lab is the one to open if you learn by doing — each experiment loads a prepared scenario into the real scoring engine, so you change one factor and watch the ranking move, which teaches a trade-off far better than a paragraph about it.',
400+
'Tiga seksi latihan: Peta Belajar memberi rute berurutan menyusuri lensa-lensanya, untuk saat memilih titik mulai justru jadi hambatannya. Akademi mengajukan soal dan menilainya di perangkat Anda, sehingga Anda tahu apa yang benar-benar tersimpan, bukan apa yang sekadar terasa familier; jawaban salah bertaut balik ke halaman yang mengajarkannya. Lab adalah yang dibuka kalau Anda belajar dengan mempraktikkan — tiap eksperimen memuat skenario siap pakai ke mesin skor sungguhan, jadi Anda ubah satu faktor lalu melihat peringkatnya bergerak, dan itu mengajarkan sebuah pertukaran jauh lebih baik daripada satu paragraf tentangnya.',
401+
)}
402+
{p(
403+
'Two things worth knowing before you start. The Guided / Expert switch in the header changes the reading depth everywhere on this tab: newcomers get plain language, experts get mechanism, evidence and cited sources. And the articles themselves are in English — a deliberate product decision, since the primary literature is — while all the navigation around them follows your chosen language.',
404+
'Dua hal yang perlu diketahui sebelum mulai. Sakelar Terpandu / Ahli di header mengubah kedalaman bacaan di seluruh tab ini: pemula mendapat bahasa sederhana, ahli mendapat mekanisme, bukti, dan sumber terkutip. Dan artikel-artikelnya sendiri berbahasa Inggris — keputusan produk yang disengaja, karena literatur primernya memang begitu — sementara seluruh navigasi di sekelilingnya mengikuti bahasa pilihan Anda.',
393405
)}
394406

395407
<strong className="aa-manual-subhead">{L('Save, export & share', 'Simpan, ekspor & bagikan')}</strong>

src/config/sections.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,43 @@ export const SECTIONS: SectionMeta[] = [
3535

3636
export const AVAILABLE_SECTIONS = SECTIONS.filter((s) => s.available);
3737

38+
/**
39+
* The two kinds of section, made explicit for the Insights landing.
40+
*
41+
* Catalog / Playbook / Review / Library are FOUR ANGLES ON THE SAME 21 ARCHITECTURES — the same
42+
* subject read four ways, not four separate libraries. Roadmap / Academy / Lab are activities you
43+
* do. Presenting all seven as one flat grid hid that boundary and made the tab look like seven
44+
* bodies of material to get through, which is both wrong and discouraging.
45+
*/
46+
export interface SectionGroup {
47+
id: 'lenses' | 'practice';
48+
label: DictKey;
49+
desc: DictKey;
50+
ids: SectionId[];
51+
}
52+
53+
export const SECTION_GROUPS: SectionGroup[] = [
54+
{ id: 'lenses', label: 'learn.group.lenses', desc: 'learn.group.lenses.desc', ids: ['catalog', 'playbook', 'review', 'library'] },
55+
{ id: 'practice', label: 'learn.group.practice', desc: 'learn.group.practice.desc', ids: ['roadmap', 'academy', 'lab'] },
56+
];
57+
58+
/**
59+
* "Open this when…" — one line per section naming the SITUATION it answers.
60+
*
61+
* The `desc` fields say what a section contains; that is not the question a person on this page is
62+
* asking. They are asking which one to click, and that is decided by what they are trying to do
63+
* right now, so these are phrased as circumstances rather than contents.
64+
*/
65+
export const SECTION_WHEN: Record<SectionId, DictKey> = {
66+
catalog: 'learn.when.catalog',
67+
playbook: 'learn.when.playbook',
68+
review: 'learn.when.review',
69+
library: 'learn.when.library',
70+
roadmap: 'learn.when.roadmap',
71+
academy: 'learn.when.academy',
72+
lab: 'learn.when.lab',
73+
};
74+
3875
export function sectionMeta(id: SectionId): SectionMeta | undefined {
3976
return SECTIONS.find((s) => s.id === id);
4077
}

0 commit comments

Comments
 (0)