diff --git a/client/src/components/Assistant/AssistantSurface.tsx b/client/src/components/Assistant/AssistantSurface.tsx index 74d0f5d21..2a46a3066 100644 --- a/client/src/components/Assistant/AssistantSurface.tsx +++ b/client/src/components/Assistant/AssistantSurface.tsx @@ -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 ( diff --git a/client/src/components/Assistant/AssistantWelcome.test.tsx b/client/src/components/Assistant/AssistantWelcome.test.tsx index e0bf65041..daa4a42b8 100644 --- a/client/src/components/Assistant/AssistantWelcome.test.tsx +++ b/client/src/components/Assistant/AssistantWelcome.test.tsx @@ -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( - + ); } 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(); diff --git a/client/src/components/Assistant/AssistantWelcome.tsx b/client/src/components/Assistant/AssistantWelcome.tsx index eb3bd834d..865e8edc4 100644 --- a/client/src/components/Assistant/AssistantWelcome.tsx +++ b/client/src/components/Assistant/AssistantWelcome.tsx @@ -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; diff --git a/client/src/components/Assistant/AssistantWindow.tsx b/client/src/components/Assistant/AssistantWindow.tsx index af4bfe461..18e4de54f 100644 --- a/client/src/components/Assistant/AssistantWindow.tsx +++ b/client/src/components/Assistant/AssistantWindow.tsx @@ -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'} /> ); } +/** + * 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 ? : 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} /> diff --git a/client/src/components/Assistant/assistantViewContext.ts b/client/src/components/Assistant/assistantViewContext.ts index 9a9e9316d..775f18c4c 100644 --- a/client/src/components/Assistant/assistantViewContext.ts +++ b/client/src/components/Assistant/assistantViewContext.ts @@ -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. */ diff --git a/client/src/mobile/screens/trip/assistant/MAssistantScreen.tsx b/client/src/mobile/screens/trip/assistant/MAssistantScreen.tsx index 31133a845..080513f8f 100644 --- a/client/src/mobile/screens/trip/assistant/MAssistantScreen.tsx +++ b/client/src/mobile/screens/trip/assistant/MAssistantScreen.tsx @@ -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'} />