fix(assistant): the Studio's empty chat speaks of the book again

The welcome chose the journal's words whenever the Studio had not
registered as the chat's host, which also hit the Studio's own panel
before (or without) that registration. A journey's chat now speaks of the
journal only where the window or the phone's chat screen say they show it
for its journal; everywhere else, the Studio included, it stays the book's.

AssistantWindow had gone past the complexity limit choosing between the
planner's trip, the journal on screen, a picked chat and the home chat;
that choice now lives in windowChat.
This commit is contained in:
Maurice
2026-10-24 22:35:23 +02:00
parent 13449c3338
commit 8433251c86
6 changed files with 46 additions and 31 deletions
@@ -30,6 +30,7 @@ export default function AssistantSurface({
onRevealMap,
bookVersion,
onOpenTripChat,
journal,
}: AssistantSurfaceProps) {
const runtime = useAssistantRuntime(tripId);
// The phone is always compact; a window is what its width says.
@@ -43,8 +44,9 @@ export default function AssistantSurface({
onRevealMap,
bookVersion,
onOpenTripChat,
journal,
}),
[tripId, variant, density, onOpen, onFocusDay, onRevealMap, bookVersion, onOpenTripChat]
[tripId, variant, density, onOpen, onFocusDay, onRevealMap, bookVersion, onOpenTripChat, journal]
);
return (
<AssistantRuntimeProvider runtime={runtime}>
@@ -1,43 +1,34 @@
/**
* FE-AI-WELCOME-001 to FE-AI-WELCOME-002
*
* The empty chat asks what fits the chat it is: a journey's conversation on
* the journal page asks about the journal, the same conversation in TREK
* The empty chat asks what fits the chat it is: a journey's conversation
* shown for its journal asks about the journal, the same conversation in TREK
* Studio about the book, and each tells a reader what they may not change.
*/
import { afterEach, describe, expect, it } from 'vitest';
import { describe, expect, it } from 'vitest';
import { render, screen } from '../../../tests/helpers/render';
import { useAssistantStore } from '../../store/assistantStore';
import AssistantWelcome from './AssistantWelcome';
import { AssistantViewContext } from './assistantViewContext';
const INITIAL = useAssistantStore.getState();
const JOURNAL_KEY = -12;
afterEach(() => {
useAssistantStore.setState(INITIAL, true);
});
function welcome(canEdit: boolean) {
function welcome(canEdit: boolean, journal?: boolean) {
return render(
<AssistantViewContext.Provider value={{ tripId: JOURNAL_KEY, variant: 'window' }}>
<AssistantViewContext.Provider value={{ tripId: JOURNAL_KEY, variant: 'window', journal }}>
<AssistantWelcome suggestions={[]} canEdit={canEdit} withSuggestions={false} />
</AssistantViewContext.Provider>
);
}
describe('AssistantWelcome', () => {
it("FE-AI-WELCOME-001: a journey's chat on its page asks about the journal", () => {
welcome(false);
it("FE-AI-WELCOME-001: a journey's chat shown for its journal asks about the journal", () => {
welcome(false, true);
expect(screen.getByText('What should go into the journal?')).toBeInTheDocument();
expect(screen.getByText(/You can only read this journal/)).toBeInTheDocument();
});
it('FE-AI-WELCOME-002: the same chat hosted by the Studio asks about the book', () => {
useAssistantStore.setState({
studioHosts: { [JOURNAL_KEY]: { render: () => undefined, flush: async () => true } },
});
it('FE-AI-WELCOME-002: the same chat in the Studio asks about the book', () => {
welcome(true);
expect(screen.getByText('What should the book look like?')).toBeInTheDocument();
expect(screen.queryByText(/You can only/)).not.toBeInTheDocument();
@@ -1,7 +1,6 @@
import { ThreadPrimitive } from '@assistant-ui/react';
import { useTranslation } from '../../i18n';
import { useAssistantStore } from '../../store/assistantStore';
import EmptyState from '../shared/EmptyState';
import AssistantHints, { MAX_VISIBLE_HINTS } from './AssistantHints';
import { useAssistantScope, useAssistantView } from './assistantViewContext';
@@ -28,12 +27,11 @@ export default function AssistantWelcome({
withSuggestions: boolean;
}) {
const { t } = useTranslation();
const { tripId, variant } = useAssistantView();
// The Studio's chat is about a book, not a plan; the journey's chat on its own page about the journal,
// the home chat about all of the user's travels. A journey's chat the Studio does not host is the journal's.
const hosted = useAssistantStore((s) => !!s.studioHosts[tripId]);
const { tripId, variant, journal } = useAssistantView();
// The Studio's chat is about a book, not a plan; the journey's chat shown for its journal about the journal,
// the home chat about all of the user's travels.
const about = useAssistantScope().kind;
const scope: WelcomeScope = about === 'journey' && !hosted ? 'journal' : about;
const scope: WelcomeScope = about === 'journey' && journal ? 'journal' : about;
const hints = useAssistantHints(tripId, { withStays: true });
const crowded = Math.min(hints.hints.length, MAX_VISIBLE_HINTS) > SUGGESTIONS_UNDER_HINTS;
const offered = crowded ? suggestions.slice(0, SUGGESTIONS_UNDER_HINTS) : suggestions;
@@ -5,7 +5,7 @@ import { useNavigate } from 'react-router';
import type { WindowRect } from '../../hooks/useDragResize';
import { useTranslation } from '../../i18n';
import { HOME_CHAT_KEY } from '../../store/assistantChatKey';
import { HOME_CHAT_KEY, assistantScopeOf } from '../../store/assistantChatKey';
import { useAssistantStore } from '../../store/assistantStore';
import AssistantFrameEdges from './AssistantFrameEdges';
import AssistantHeader from './AssistantHeader';
@@ -172,12 +172,31 @@ function ChatWindow({
onFocusDay={onFocusDay}
onRevealMap={onRevealMap}
onOpenTripChat={onOpenTripChat}
// The window never shows the Studio: a journey's chat in it is the journal's.
journal={assistantScopeOf(trip.id).kind === 'journey'}
/>
</div>
</section>
);
}
/**
* Which chat the window shows: the page's own (the trip in the planner, else
* the journal on screen), else the one picked, else the home chat. `onScreen`
* when it is the page's own, `inPlanner` when that page is the planner.
*/
function windowChat(
trip: WindowTrip | null,
journal: WindowTrip | null,
picked: WindowTrip | null,
homeChat: WindowTrip
) {
const routeChat = trip ?? journal;
const current = routeChat ?? picked ?? homeChat;
const onScreen = !!routeChat && current.id === routeChat.id;
return { routeChat, current, onScreen, inPlanner: onScreen && trip !== null };
}
export default function AssistantWindow({ trip, onOpen, onFocusDay, onRevealMap }: AssistantWindowProps) {
const navigate = useNavigate();
const available = useAssistantStore((s) => s.status?.available === true);
@@ -201,9 +220,7 @@ export default function AssistantWindow({ trip, onOpen, onFocusDay, onRevealMap
}
const journal = useRouteJournalChat();
// The page's own chat first: the trip in the planner, else the journal on screen.
const routeChat = trip ?? journal;
const current = routeChat ?? picked ?? home.chat;
const { routeChat, current, onScreen, inPlanner } = windowChat(trip, journal, picked, home.chat);
const atHome = current.id === HOME_CHAT_KEY;
// A change in a trip the planner does not show is shown by opening that trip (a journal, its page); a tab opens on it.
const openPicked = useCallback(
@@ -235,7 +252,6 @@ export default function AssistantWindow({ trip, onOpen, onFocusDay, onRevealMap
if (!available) return null;
if (!open) return routeChat ? <AssistantLauncher variant="floating" tripId={launcherTrip} /> : null;
const onScreen = !!routeChat && current.id === routeChat.id;
let openTarget = onScreen ? onOpen && openOnScreen : openPicked;
if (atHome) openTarget = openFromHome;
let tripSwitch: { onPick?: (trip: WindowTrip) => void; onHome?: () => void } | undefined;
@@ -248,8 +264,8 @@ export default function AssistantWindow({ trip, onOpen, onFocusDay, onRevealMap
hidden={minimized}
onOpen={openTarget}
// Only the planner on screen has days to select and a map to bring back.
onFocusDay={onScreen && trip ? onFocusDay : undefined}
onRevealMap={onScreen && trip ? onRevealMap : undefined}
onFocusDay={inPlanner ? onFocusDay : undefined}
onRevealMap={inPlanner ? onRevealMap : undefined}
onOpenTripChat={atHome ? home.openTripChat : undefined}
tripSwitch={tripSwitch}
/>
@@ -39,6 +39,12 @@ export interface AssistantView {
* chat's card that offers it; left out where no chat can be switched to.
*/
onOpenTripChat?: (trip: AssistantChatTrip, question: string | null) => void;
/**
* A journey's chat shown for its journal (on the journey's page, from the
* home chat), not for its book in TREK Studio; left out in the Studio and
* for a trip. The empty chat then speaks of the journal.
*/
journal?: boolean;
}
/** A trip as the chat switches to it: its id, which is its chat's key, and its title for the head. */
@@ -317,6 +317,8 @@ export function AssistantScreen({ tripId, tripTitle, hidden, hops, onOpenTripCha
onFocusDay={hops.focusDay}
onRevealMap={hops.revealMap}
onOpenTripChat={onOpenTripChat}
// The phone's Studio has its own panel: a journey's chat here is the journal's.
journal={assistantScopeOf(tripId).kind === 'journey'}
/>
</div>