diff --git a/client/src/components/Assistant/AssistantHost.tsx b/client/src/components/Assistant/AssistantHost.tsx index b263f3f4e..99771d4f9 100644 --- a/client/src/components/Assistant/AssistantHost.tsx +++ b/client/src/components/Assistant/AssistantHost.tsx @@ -21,7 +21,8 @@ function useRouteTripId(): number | null { * map" opens the planner on the change (`?place=`, `?day=`), the same way a * notification opens a place, a tab of the trip opens as `?tab=` (a list's * own tab remembered first), and another page is simply navigated to. A day - * picked in the chat (its weather) selects that day the same way. + * picked in the chat (its weather) selects that day the same way, and "On + * map" on a card brings the planner's map back first. */ export default function AssistantHost() { const navigate = useNavigate(); @@ -53,6 +54,8 @@ export default function AssistantHost() { [navigate, openPlanner, tripId] ); const focusDay = useCallback((dayId: number) => openPlanner(`?day=${dayId}`), [openPlanner]); + // "On map" from another tab of the planner (Tours, Costs, ...) first brings the map back. + const revealMap = useCallback(() => openPlanner('?tab=plan'), [openPlanner]); - return ; + return ; } diff --git a/client/src/components/Assistant/assistantParts.ts b/client/src/components/Assistant/assistantParts.ts index 471c0be41..c5c983c44 100644 --- a/client/src/components/Assistant/assistantParts.ts +++ b/client/src/components/Assistant/assistantParts.ts @@ -1,14 +1,15 @@ -import type { - AssistantApplied, - AssistantChangeEffect, - AssistantChangeLabelKey, - AssistantChangePreview, - AssistantFoundPlace, - AssistantMessage, - AssistantMessagePart, - AssistantPlaces, - AssistantWeatherDay, - TrekCard, +import { + ASSISTANT_FOLLOW_UPS_TOOL, + type AssistantApplied, + type AssistantChangeEffect, + type AssistantChangeLabelKey, + type AssistantChangePreview, + type AssistantFoundPlace, + type AssistantMessage, + type AssistantMessagePart, + type AssistantPlaces, + type AssistantWeatherDay, + type TrekCard, } from '@trek/shared'; import { CARD_RULES } from './cards/cardLooks'; @@ -71,6 +72,8 @@ type MessageBlock = export interface CardRules { group(card: TrekCard): string; yieldsToChange(answer: TrekCard, change: TrekCard): boolean; + /** Whether an answer holding the card leaves out its found places. */ + hidesFoundPlaces?(card: TrekCard): boolean; } /** The change "+ Day 3" on a found place makes; the server holds it without a model turn. */ @@ -251,13 +254,16 @@ export function messageBlocks( else if (part.type === 'tool-call' && part.approval) changeIds.add(part.toolCallId); }); + // An answer about a tour shows the tour, not the places its searches went through to find it. + const changeCards = message.parts.flatMap((p) => (p.type === 'data-trek-change' && p.data.card ? [p.data.card] : [])); + const placesHidden = [...cards.values(), ...changeCards].some((card) => rules.hidesFoundPlaces?.(card) === true); const blocks: MessageBlock[] = []; message.parts.forEach((part, index) => { if (part.type === 'text') { if (part.text.trim()) blocks.push({ kind: 'text', key: `text-${index}`, text: part.text }); return; } - if (part.type === 'data-trek-places' && index === lastPlaces) { + if (part.type === 'data-trek-places' && index === lastPlaces && !placesHidden) { const found = allFound(message.parts); const places = found.places.filter((place) => !proposed(place, effects)); if (places.length > 0) blocks.push({ kind: 'places', key: `places-${index}`, found: { ...found, places } }); @@ -273,7 +279,8 @@ export function messageBlocks( if (card && !changeIds.has(toolCallId)) pushCard(blocks, { toolCallId, card }, rules.group(card)); return; } - if (part.type !== 'tool-call') return; + // The follow-ups the model offered are chips under the answer, never a step. + if (part.type !== 'tool-call' || part.toolName === ASSISTANT_FOLLOW_UPS_TOOL) return; const item = changeItemOf(part, previews.get(part.toolCallId) ?? null, receipts, cards); if (item) { pushChange(blocks, item, fromStrip); diff --git a/client/src/components/Assistant/cards/cardLooks.ts b/client/src/components/Assistant/cards/cardLooks.ts index f22f7a0e6..fd4ac8bed 100644 --- a/client/src/components/Assistant/cards/cardLooks.ts +++ b/client/src/components/Assistant/cards/cardLooks.ts @@ -247,6 +247,7 @@ function lookOf(card: TrekCard): CardLook { export const CARD_RULES: CardRules = { group: (card) => lookOf(card).group?.(card) ?? card.kind, yieldsToChange: (answer, change) => lookOf(answer).yieldsToChange?.(answer, change) ?? false, + hidesFoundPlaces: (card) => lookOf(card).hidesFoundPlaces === true, }; /** What the plan's map draws for a card through its kind's look; nothing for a kind that draws nothing. */ diff --git a/client/src/components/Assistant/cards/cardTypes.ts b/client/src/components/Assistant/cards/cardTypes.ts index c2946b1f9..fad5127af 100644 --- a/client/src/components/Assistant/cards/cardTypes.ts +++ b/client/src/components/Assistant/cards/cardTypes.ts @@ -73,6 +73,12 @@ export interface CardLook { lines?(card: TrekCardOf, use: RouteUse): RouteLineSpec[]; ghosts?(card: TrekCardOf): GhostPoint[]; hidesEffectGhost?(card: TrekCardOf): boolean; + /** + * Whether an answer holding this card leaves out the strip of places its + * searches found: a planned tour searched only to find its start, and a + * strip of hotels beside it would read as suggestions. + */ + hidesFoundPlaces?: boolean; } /** One part of TREK's pure look, for the kinds `K` it owns. */ diff --git a/client/src/components/Assistant/cards/tour/TourRouteActions.tsx b/client/src/components/Assistant/cards/tour/TourRouteActions.tsx index b82fa9b92..455703b62 100644 --- a/client/src/components/Assistant/cards/tour/TourRouteActions.tsx +++ b/client/src/components/Assistant/cards/tour/TourRouteActions.tsx @@ -11,6 +11,23 @@ import { useTakeProposal } from './useTourRouteCard'; /** What "Open" says: the phone opens the tour's place on the plan, the desktop the Tours tab. */ const OPEN_LABEL = { phoneKey: 'assistant.card.tour.openPhone', desktopKey: 'assistant.card.tour.open' } as const; +/** Where a tapped "Plan this" stands: being planned, added, or (held without the tap) waiting below. */ +function TakenPill({ deciding, applied }: { deciding: boolean; applied: boolean }) { + const { t } = useTranslation(); + if (deciding) { + return ( + + ); + } + return ( + + ); +} + /** * The foot of a route card: "On map" always; "Plan this" on a proposed hike * or city walk for someone who may add to the trip, off with its reason said @@ -57,11 +74,7 @@ export default function TourRouteActions({ labelKey={phone ? OPEN_LABEL.phoneKey : OPEN_LABEL.desktopKey} /> )} - {take.visible && take.state === 'taken' && ( - - )} + {take.visible && take.state === 'taken' && } {take.visible && take.state !== 'taken' && (