Skip to content

Commit 0959096

Browse files
[fix](chat,copilot,guide): fix Q&A mismatches, full mutual exclusion, modernize the Guide
Chat Advisor — five question/answer mismatches (a short generic keyword shadowing a more specific, different intent), all now regression-tested: - "why not serverless?" hit the privacy blurb ("server" ⊂ "serverless") - "instal aplikasi offline" hit privacy, not the PWA-install answer - "daftar semua anti-pattern" (ID "list all") hit privacy - "what is Cost efficiency?" returned scenario cost/ops, not the QA - "what is an anti-pattern?" ran a scenario check, not the glossary term Fixed by narrowing keywords, checking the specific FAQ table before broad single-word intents, a qaInfo() pre-check on the cost branch, and an isConceptual() guard routing "what is X" to the glossary. Full mutual exclusion (owner report): opening any one of {Chat Advisor, Copilot tour, Guide/Manual, command palette, shortcuts, Compare} now closes every other — never two open or highlighted at once. Wiring in App.tsx (derived chatCloseSignal, handleChatOpenChange, handleTourStart, suspendCopilot); ChatFab self-reports open state + closes on signal; Copilot stops + hides its launcher when suspended and reports tour start. Floating-target fix: the tour's final step highlights the fixed Chat Advisor FAB. A new `floating` flag on that step makes useCopilot skip the scroll-into-view and CopilotOverlay skip the mobile bottom sheet (which otherwise covered the very button it points at). Locked in by a test. Guide / Manual ("Panduan"): - Content mismatch fixed — §7 "feature map" claimed to cover everything but omitted the Chat Advisor and the "Guide me" tour; both added. - Modernized the dated inline-styled modal into a glass shell with a gradient book-mark header, a sticky jump-to-section table of contents, numbered gradient section badges, and card sub-sections (new .aa-manual-* classes; inline styles removed). The TOC uses scrollIntoView, NOT href="#id" anchors, so it never mutates location.hash (reserved for share-state). Close-button label → "Close the guide" (test updated). 184 unit tests (up from 175), 14 e2e, 6 guards, bundle within budget all green. README + DECISIONS updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 82084df commit 0959096

17 files changed

Lines changed: 552 additions & 79 deletions

DECISIONS.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,3 +557,68 @@ Nothing in the underlying architecture changed: `getChatAdapter()`, `ChatAdapter
557557
`localAdvisorAdapter` (its internal `id: 'local-advisor'`), and the Adapter Pattern itself keep
558558
their names — "chat" and "advisor" already described the mechanism accurately; only the
559559
AI-implying product-facing brand needed to go.
560+
561+
## Chat Advisor & Copilot — full mutual exclusion + floating-target fixes, 2026-07-23
562+
563+
Owner report (hand-testing on iPhone SE): the two floating features and the existing overlays could
564+
visibly collide. Three concrete UI/UX defects, all fixed:
565+
566+
1. **Opening the Chat Advisor covered the "Pandu saya" (Guide me) launcher**, and starting the tour
567+
left the chat panel open underneath the tour card. Fixed with **full mutual exclusion**: opening
568+
any ONE of {Chat Advisor panel, Copilot tour, Manual/Guide book, command palette, shortcuts,
569+
Compare} now closes every other one — never two open or highlighted at once. Wiring lives in
570+
`App.tsx`: `handleChatOpenChange` (chat opening clears `overlay`), `handleTourStart` (tour
571+
starting bumps `chatCloseSignal` **and** clears `overlay`), a derived `chatCloseSignal` string
572+
(changes whenever `overlay` opens or the tour starts — **derived, not a setState-in-effect**, to
573+
satisfy the `react-hooks/set-state-in-effect` rule), and `suspendCopilot = chatOpen || overlay
574+
!== null` passed to `<Copilot suspended>`. `ChatFab` self-reports its open state via
575+
`onOpenChange` and closes itself when `closeSignal` changes; `Copilot` stops the tour + hides its
576+
launcher when `suspended`, and calls `onTourStart` when the run begins.
577+
578+
2. **When the tour highlighted the Chat Advisor FAB (last step), the mobile bottom sheet covered the
579+
very button it was pointing at**, and the Pre-Flight Check tried to scroll a fixed-position
580+
element into view (pointless page motion). Fixed with a `floating: true` flag on that tour step
581+
(threaded through `CopilotStep → CopilotStepView`): `useCopilot` skips the scroll adjustment for
582+
a floating target, and `CopilotOverlay` skips the mobile bottom-sheet layout for it (falling back
583+
to the clamped placement that flips above/beside the target). Verified iPhone-SE: the card sits
584+
clear of the FAB (no overlap), spotlight visible.
585+
586+
3. The FAB button itself intentionally **stays visible** during the tour's final step (the step
587+
highlights it); only the chat *panel* is force-closed. After the tour ends, clicking the FAB
588+
opens the panel normally and re-hides the launcher — verified.
589+
590+
## Chat Advisor Q&A mismatches + Guide (Panduan) modernization, 2026-07-23
591+
592+
More owner hand-testing surfaced answers that didn't match the question, plus a dated-looking Guide.
593+
594+
**Chat Advisor — five question/answer MISMATCHES fixed** (each a short generic keyword shadowing a
595+
more specific, conceptually different intent — all now covered by regression tests in
596+
`chat.test.ts`):
597+
- "why not **serverless**?" returned the privacy blurb — bare `server` is a substring of
598+
"serverless"; narrowed to `server call` / `no server` / `a server`.
599+
- "instal **aplikasi offline**" returned the privacy blurb — `offline` shadowed the PWA-install FAQ;
600+
fixed by checking the (more specific, multi-word) FAQ table **before** the broad single-word
601+
intents, and dropping `aplikasi offline` from the PWA keywords.
602+
- "**daftar** semua anti-pattern" returned the privacy blurb — Indonesian `daftar` means "list" (the
603+
app has no sign-up), and collided with the catalog's "list all"; removed from privacy keywords.
604+
- "what is **Cost efficiency**?" returned the scenario's cost/ops numbers instead of the quality
605+
attribute — `cost`/`biaya` is a substring of that QA's name; the cost/ops branch now checks
606+
`qaInfo()` first.
607+
- "what is an **anti-pattern**?" (the concept) returned a scenario-specific check — added an
608+
`isConceptual()` guard ("what is / define …", without "my/this scenario") that routes conceptual
609+
phrasings to the GLOSSARY term for both the anti-pattern and sensitivity intents.
610+
- The underlying lesson (documented for future intents): **order intents most-specific first**; a
611+
bare keyword must never be checked before a multi-word phrase or exact label that contains it.
612+
613+
**Guide / Manual ("Panduan") — content fix + UI modernization:**
614+
- **Content mismatch fixed.** §7 "the feature map" claimed to cover *everything in the app* but
615+
omitted the two features added this session. Added subsections for the **Chat Advisor** (grounded,
616+
offline, Advisor-tab-only, not an LLM) and the **"Guide me" interactive tour**, and noted the
617+
mutual-exclusion behavior. All other facts re-verified against the current app.
618+
- **Modernized the dated look** without new deps or bundle regression: the flat inline-styled modal
619+
became a glass shell (`aa-glass`) with a gradient book-mark header + subtitle, a **sticky
620+
jump-to-section table of contents** (9 pills), numbered gradient section badges, and card-styled
621+
sub-sections — all via new `.aa-manual-*` classes in `index.css` (inline styles removed). The TOC
622+
uses `scrollIntoView`, **not** `href="#id"` anchors, so it never mutates `location.hash` (which
623+
this app reserves for share-state — an anchor jump would have clobbered a share link). Verified
624+
desktop + mobile; the close button's aria-label became "Close the guide" (test updated).

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,11 @@ flowchart LR
213213
- **Nav-harmony & clean teardown** — switching tabs mid-tour stops it; "Start Over" resets the tour
214214
alongside the chat; every observer/listener/timer is cleaned up (zero leaks). The launcher and
215215
the whole overlay are **lazy-loaded**, so the initial JS budget is untouched.
216+
- **One floating thing at a time** — opening any of {Chat Advisor, this tour, the Guide/Manual,
217+
command palette, shortcuts, Compare} closes every other one, so nothing is ever covered by or
218+
highlighted behind another. The tour's final step targets the Chat Advisor FAB, a *fixed* control,
219+
so that step skips the scroll-into-view + mobile bottom sheet (which would otherwise cover the very
220+
button it points at) via a `floating` flag on the step.
216221

217222
## Run it locally
218223

src/App.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,14 @@ export default function App() {
126126
const chatResetRef = useRef<(() => void) | null>(null);
127127
// Registered by the Copilot so "Start Over" hard-resets the guided tour (anti-contamination).
128128
const copilotResetRef = useRef<(() => void) | null>(null);
129+
// Full mutual exclusion between every floating/overlay UI (owner report): the Chat Advisor panel
130+
// and the Copilot tour. `chatOpen` mirrors ChatFab's own open state (it self-reports via
131+
// onOpenChange — App can't set it directly). `chatCloseTick` is bumped to force ChatFab closed
132+
// from outside (tour starting, or any `overlay` opening); ChatFab watches it and calls its own
133+
// setOpen(false) in response. The `overlay` half of this (Manual/Guide · palette · shortcuts ·
134+
// Compare) is wired in just below, once `overlay`/`setOverlay` exist.
135+
const [chatOpen, setChatOpen] = useState(false);
136+
const [chatCloseTick, setChatCloseTick] = useState(0);
129137

130138
const scenario: ScenarioState = { v: 1, mode, lang, levels, selections, overrides };
131139
const exportInput: ExportInput = { levels, overrides, selections: effective, lang };
@@ -171,6 +179,24 @@ export default function App() {
171179

172180
const { status: exportStatus, setStatus: setExportStatus, run } = useExportActions(exportInput, scenario, weights);
173181
const [overlay, setOverlay] = useState<'palette' | 'shortcuts' | 'manual' | 'compare' | null>(null);
182+
183+
// Finish the mutual-exclusion wiring (owner report: opening any ONE of Chat Advisor / Copilot
184+
// tour / Manual-Guide / palette / shortcuts / Compare must close every other one — never two open
185+
// or highlighted together). `chatCloseSignal` is a plain DERIVED value (no effect, no
186+
// setState-in-effect) that changes whenever `overlay` opens OR the tour starts — ChatFab watches
187+
// it and closes itself in response. Opening chat or starting the tour closes `overlay` directly
188+
// (both are ordinary event handlers, not effects).
189+
const chatCloseSignal = `${overlay ?? ''}|${chatCloseTick}`;
190+
const handleChatOpenChange = (open: boolean) => {
191+
setChatOpen(open);
192+
if (open) setOverlay(null);
193+
};
194+
const handleTourStart = () => {
195+
setChatCloseTick((n) => n + 1);
196+
setOverlay(null);
197+
};
198+
const suspendCopilot = chatOpen || overlay !== null;
199+
174200
const [snapA, setSnapA] = usePersistedState<ScenarioState | null>('aa.snapA', null);
175201
const [snapB, setSnapB] = usePersistedState<ScenarioState | null>('aa.snapB', null);
176202

@@ -254,7 +280,12 @@ export default function App() {
254280
unmounts it; conversation history still persists (localStorage) and resumes on return. */}
255281
{FEATURES.chat && mainView === 'advisor' && (
256282
<Suspense fallback={null}>
257-
<ChatFab contextInput={{ levels, overrides, mode, lang }} registerReset={(fn) => (chatResetRef.current = fn)} />
283+
<ChatFab
284+
contextInput={{ levels, overrides, mode, lang }}
285+
registerReset={(fn) => (chatResetRef.current = fn)}
286+
onOpenChange={handleChatOpenChange}
287+
closeSignal={chatCloseSignal}
288+
/>
258289
</Suspense>
259290
)}
260291
{/* Interactive Copilot / guided tutorial (Phase 3) — a lazy, pluggable feature module. */}
@@ -265,6 +296,8 @@ export default function App() {
265296
lang={lang}
266297
topPick={rankings.D1[0]?.name}
267298
registerReset={(fn) => (copilotResetRef.current = fn)}
299+
suspended={suspendCopilot}
300+
onTourStart={handleTourStart}
268301
/>
269302
</Suspense>
270303
<MobileChrome mainView={mainView} onNavigate={navigate} theme={theme} onToggleTheme={toggleTheme} mode={mode} onSetMode={setMode} />

src/components/chat/ChatFab.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,19 @@ interface Props {
1111
contextInput: ChatContextInput;
1212
/** App registers the mounted chat's reset() so "Start Over" wipes it in-tab. */
1313
registerReset: (reset: (() => void) | null) => void;
14+
/** Reports open/close so App can mutually exclude every other overlay/floating feature (owner
15+
* report: nothing may ever be visibly open/highlighted at the same time as anything else). */
16+
onOpenChange?: (open: boolean) => void;
17+
/** A value that CHANGES whenever App wants this panel closed from outside (the Copilot tour
18+
* starting, or the Manual/Guide · palette · shortcuts · Compare overlay opening) — closes the
19+
* panel so it never covers (or gets covered by) another one. The FAB button itself stays
20+
* visible: the tour's own last step highlights it. */
21+
closeSignal?: string | number;
1422
}
1523

1624
// The floating chat launcher (Advisor "Chat Advisor"). Toggles the lazy panel; keeps its own open
1725
// state. Esc closes. Opening never resets chat; closing keeps the session (persisted).
18-
export function ChatFab({ contextInput, registerReset }: Readonly<Props>) {
26+
export function ChatFab({ contextInput, registerReset, onOpenChange, closeSignal }: Readonly<Props>) {
1927
const { t } = useI18n();
2028
const [open, setOpen] = useState(false);
2129
const [everOpened, setEverOpened] = useState(false);
@@ -30,6 +38,21 @@ export function ChatFab({ contextInput, registerReset }: Readonly<Props>) {
3038
return () => window.removeEventListener('keydown', onKey);
3139
}, [open]);
3240

41+
useEffect(() => {
42+
onOpenChange?.(open);
43+
}, [open, onOpenChange]);
44+
45+
// Mutual exclusion (owner report): the Copilot tour starting closes this panel. Skips the very
46+
// first render so mounting never force-closes an already-open panel.
47+
const mounted = useRef(false);
48+
useEffect(() => {
49+
if (!mounted.current) {
50+
mounted.current = true;
51+
return;
52+
}
53+
setOpen(false);
54+
}, [closeSignal]);
55+
3356
const toggle = () => {
3457
setOpen((v) => !v);
3558
setEverOpened(true);

0 commit comments

Comments
 (0)