From f3e05f48e1a52adb540a6228caa6f456d61662fa Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 23 Oct 2026 17:03:01 +0200 Subject: [PATCH] feat(assistant): found places open the place form, notes kept on its own, city walk stops checked off - Found places: the strip shows only when the answer names one of them, the named ones first. A click on a card, or "Add to trip", opens the planner's place form filled in with the place on the day in focus, its Google details (photos, hours, description) loading beside it; "+ Day" and the day grid are gone. - Trip notes: remember_for_trip runs at once without a card. The assistant keeps lasting facts the user mentions (who travels, needs, likes, budget, pace) on its own and offers the unsure ones as a follow-up; the chat shows "Noted for this trip: ..." with the notes a tap away, and reads them again so the dialog never saves over the new note. - City walks: set_city_walk_visit checks a station off on its day (visited, skipped, or the mark taken back), with day_edit as in the day plan, a change card naming the station and its walk, and an undo that restores the status before while the station is as the change left it. - Card heads are a step smaller (32px tile, smaller title and count), and the dot beside "Working on it" in the header is gone. --- .../components/Assistant/AssistantHeader.tsx | 6 +- .../components/Assistant/AssistantMessage.tsx | 4 + .../Assistant/AssistantPlaceCard.tsx | 123 ++---------------- .../Assistant/AssistantPlaceStrip.test.tsx | 38 +++--- .../Assistant/AssistantRemembered.test.tsx | 49 +++++++ .../Assistant/AssistantRemembered.tsx | 49 +++++++ client/src/components/Assistant/assistant.css | 20 +-- .../Assistant/assistantParts.test.ts | 41 +++++- .../components/Assistant/assistantParts.ts | 47 ++++++- .../Assistant/cards/parts/CardHeader.tsx | 14 +- .../Assistant/cards/tour/TourRouteHead.tsx | 2 +- .../Assistant/cards/tour/tourLook.ts | 6 +- .../Assistant/useAssistantPlaceStrip.ts | 105 +++------------ .../Planner/PlaceFormModal.helpers.ts | 5 +- .../src/components/Planner/PlaceFormModal.tsx | 2 +- .../tripPlanner/useAssistantPlaceDraft.ts | 35 +++++ .../pages/tripPlanner/usePlaceFormOpeners.ts | 3 +- .../pages/tripPlanner/usePlannerDialogs.ts | 2 +- .../src/pages/tripPlanner/useTripPlanner.ts | 2 + client/src/store/assistantStore.ts | 12 ++ server/src/nest/assistant/assistant-memory.ts | 33 ++++- server/src/nest/assistant/assistant.prompt.ts | 2 +- .../src/nest/assistant/assistant.service.ts | 12 +- server/src/nest/assistant/cards/tour/index.ts | 7 + .../assistant/cards/tour/tour-visit.cards.ts | 84 ++++++++++++ server/src/nest/tours/tours.mcp.ts | 81 +++++++++++- .../nest/assistant/assistant-memory.test.ts | 35 ++++- .../cards/tour/tour-visit.cards.test.ts | 103 +++++++++++++++ server/tests/unit/nest/tours.mcp.test.ts | 52 ++++++++ shared/scripts/i18n-parity.mjs | 1 - shared/src/assistant/cards/tour.contract.ts | 3 +- shared/src/i18n/ar/assistant.ts | 8 +- shared/src/i18n/az/assistant.ts | 8 +- shared/src/i18n/br/assistant.ts | 8 +- shared/src/i18n/ca/assistant.ts | 8 +- shared/src/i18n/cs/assistant.ts | 8 +- shared/src/i18n/de/assistant.ts | 8 +- shared/src/i18n/en/assistant.ts | 8 +- shared/src/i18n/es/assistant.ts | 8 +- shared/src/i18n/et/assistant.ts | 8 +- shared/src/i18n/fr/assistant.ts | 8 +- shared/src/i18n/gr/assistant.ts | 8 +- shared/src/i18n/hu/assistant.ts | 8 +- shared/src/i18n/id/assistant.ts | 8 +- shared/src/i18n/it/assistant.ts | 8 +- shared/src/i18n/ja/assistant.ts | 8 +- shared/src/i18n/ko/assistant.ts | 8 +- shared/src/i18n/nl/assistant.ts | 8 +- shared/src/i18n/pl/assistant.ts | 8 +- shared/src/i18n/ru/assistant.ts | 8 +- shared/src/i18n/sk/assistant.ts | 8 +- shared/src/i18n/sv/assistant.ts | 8 +- shared/src/i18n/th/assistant.ts | 8 +- shared/src/i18n/tr/assistant.ts | 8 +- shared/src/i18n/uk/assistant.ts | 8 +- shared/src/i18n/vi/assistant.ts | 8 +- shared/src/i18n/zh-TW/assistant.ts | 8 +- shared/src/i18n/zh/assistant.ts | 8 +- 58 files changed, 843 insertions(+), 351 deletions(-) create mode 100644 client/src/components/Assistant/AssistantRemembered.test.tsx create mode 100644 client/src/components/Assistant/AssistantRemembered.tsx create mode 100644 client/src/pages/tripPlanner/useAssistantPlaceDraft.ts create mode 100644 server/src/nest/assistant/cards/tour/tour-visit.cards.ts create mode 100644 server/tests/unit/nest/assistant/cards/tour/tour-visit.cards.test.ts diff --git a/client/src/components/Assistant/AssistantHeader.tsx b/client/src/components/Assistant/AssistantHeader.tsx index d58b95b9c..691a2905d 100644 --- a/client/src/components/Assistant/AssistantHeader.tsx +++ b/client/src/components/Assistant/AssistantHeader.tsx @@ -103,11 +103,7 @@ export default function AssistantHeader({ tripId, thread, tripTitle, onClose, dr
{thread.isRunning ? ( // While it works, the line over the title says what it does right now. -

-

{live?.text ?? t('assistant.step.working')}

) : ( diff --git a/client/src/components/Assistant/AssistantMessage.tsx b/client/src/components/Assistant/AssistantMessage.tsx index d300f2efa..e286699ed 100644 --- a/client/src/components/Assistant/AssistantMessage.tsx +++ b/client/src/components/Assistant/AssistantMessage.tsx @@ -11,6 +11,7 @@ import AssistantFollowUps from './AssistantFollowUps'; import AssistantMarkdown from './AssistantMarkdown'; import AssistantMentionText from './AssistantMentionText'; import AssistantPlaceStrip from './AssistantPlaceStrip'; +import AssistantRemembered from './AssistantRemembered'; import AssistantShareDialog from './AssistantShareDialog'; import AssistantToolSteps from './AssistantToolSteps'; import AssistantWeatherStrip from './AssistantWeatherStrip'; @@ -129,6 +130,9 @@ function Answer({ message }: { message: StoredMessage }) { return ; } if (block.kind === 'weather') return ; + if (block.kind === 'remembered') { + return ; + } if (block.kind === 'cards') return ; return ; })} diff --git a/client/src/components/Assistant/AssistantPlaceCard.tsx b/client/src/components/Assistant/AssistantPlaceCard.tsx index 6ca589074..30fbb10bd 100644 --- a/client/src/components/Assistant/AssistantPlaceCard.tsx +++ b/client/src/components/Assistant/AssistantPlaceCard.tsx @@ -1,12 +1,10 @@ import type { AssistantFoundPlace } from '@trek/shared'; -import { Check, ChevronDown, LoaderCircle, MapPin, Plus, Star } from 'lucide-react'; +import { Check, MapPin, Plus, Star } from 'lucide-react'; import { createElement, type ReactNode, useRef } from 'react'; import { useTranslation } from '../../i18n'; import { getCategoryIcon } from '../shared/categoryIcons'; -import { PopoverListPanel } from '../shared/dialogParts'; import { Tooltip } from '../shared/Tooltip'; -import { usePopoverList } from '../shared/usePopoverList'; import { useAssistantView } from './assistantViewContext'; import type { useAssistantPlaceStrip } from './useAssistantPlaceStrip'; import { useFoundPlacePhoto } from './useFoundPlacePhoto'; @@ -16,110 +14,14 @@ type Strip = ReturnType; const ACTION = 'inline-flex items-center gap-1 rounded-lg bg-surface-secondary text-caption font-medium text-content transition-colors hover:bg-surface-hover disabled:cursor-default disabled:opacity-50 disabled:hover:bg-surface-secondary'; -/** "+ Day 3" for the day in focus, a list of the trip's days beside it; "Pick a day" when no day is in focus. */ -function SuggestButton({ place, strip, tall }: { place: AssistantFoundPlace; strip: Strip; tall: boolean }) { +/** "Add to trip": opens the planner's place form filled in with the place, its details loading beside it. */ +function AddButton({ place, strip, tall }: { place: AssistantFoundPlace; strip: Strip; tall: boolean }) { const { t } = useTranslation(); - const { days, focusDay, disabled, blockedKey, adding } = strip; - const list = usePopoverList(focusDay ? days.findIndex((d) => d.id === focusDay.id) : 0, { - estimatedHeight: Math.ceil(days.length / 7) * 44 + 16, - }); - const { open, toggle, close, triggerRef, listId } = list; - const height = tall ? 'h-10' : 'h-7'; - const busy = adding === place.ref; - - const pick = (dayId: number) => { - close(); - void strip.suggest(place, dayId); - }; - - const buttons = ( - - {focusDay ? ( - <> - - - - ) : ( - - )} - - ); - return ( - <> - {blockedKey ? ( - - {/* A disabled button gets no pointer events; the wrapper carries the reason. */} - {buttons} - - ) : ( - buttons - )} - {/* A grid of day tiles, not a list: a month-long trip still fits in a few rows. */} - -
- {days.map((day) => { - const current = day.id === focusDay?.id; - return ( - - - - ); - })} -
-
- + ); } @@ -219,7 +121,7 @@ function PlaceAction({ ); } - return strip.canSuggest ? : null; + return strip.canAdd ? : null; } /** @@ -227,9 +129,10 @@ function PlaceAction({ * with the category it most likely belongs in on it (or that category's icon * on its colour), its name, address and rating, whether it is open now while * the trip runs, how far it is from the last stop of the day in focus, and one - * way on: "+ Day 3" for a new place, "On map" for one the trip has. A click on - * the card shows it on the map, and resting the pointer on it pins it there - * for a moment. + * way on: "Add to trip" for a new place, "On map" for one the trip has. A click + * on a new place opens the place form filled in with it (one the trip has, or + * any where nothing can be added, shows on the map), and resting the pointer on + * it pins it there for a moment. */ export default function AssistantPlaceCard({ place, @@ -259,7 +162,7 @@ export default function AssistantPlaceCard({ > + + )} + {editing && tripId !== null && setEditing(false)} />} +
+ ); +} diff --git a/client/src/components/Assistant/assistant.css b/client/src/components/Assistant/assistant.css index fa9158b73..b86341454 100644 --- a/client/src/components/Assistant/assistant.css +++ b/client/src/components/Assistant/assistant.css @@ -168,20 +168,6 @@ animation: trek-assistant-busy 1.8s ease-in-out infinite; } -/* The dot beside what the assistant does right now: a soft beat, a halo going out. */ -@keyframes trek-assistant-pulse { - 0% { - box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 45%, transparent); - } - 70%, - 100% { - box-shadow: 0 0 0 6px transparent; - } -} -.trek-assistant-pulse { - animation: trek-assistant-pulse 1.4s ease-out infinite; -} - @media (prefers-reduced-motion: reduce) { .trek-assistant-pop, .trek-assistant-enter { @@ -190,8 +176,7 @@ .trek-assistant-settle { transition: none; } - .trek-assistant-busy, - .trek-assistant-pulse { + .trek-assistant-busy { animation: none; } .trek-assistant-shimmer { @@ -211,8 +196,7 @@ [data-reduce-motion] .trek-assistant-settle { transition: none; } -[data-reduce-motion] .trek-assistant-busy, -[data-reduce-motion] .trek-assistant-pulse { +[data-reduce-motion] .trek-assistant-busy { animation: none; } [data-reduce-motion] .trek-assistant-shimmer { diff --git a/client/src/components/Assistant/assistantParts.test.ts b/client/src/components/Assistant/assistantParts.test.ts index c94652df7..0c1274590 100644 --- a/client/src/components/Assistant/assistantParts.test.ts +++ b/client/src/components/Assistant/assistantParts.test.ts @@ -200,7 +200,7 @@ describe('messageBlocks: found places and weather', () => { read('s2', []), places('s2', [found('s2:0', { name: 'Place s1:0' }), found('s2:1', { googlePlaceId: 'ChIJa', lat: 40 })], 3), places('s3', [found('s3:0', { lat: 40 })]), - { type: 'text', text: 'Three good ones.' }, + { type: 'text', text: 'Three good ones, Place s3:0 first.' }, ]), false ); @@ -208,10 +208,47 @@ describe('messageBlocks: found places and weather', () => { expect(blocks[0]).toMatchObject({ steps: [{ toolCallId: 's1' }, { toolCallId: 's2' }] }); // The same name at the same spot, or the same Google id, shows once. expect(blocks[1]).toMatchObject({ - found: { toolCallId: 's3', more: 5, places: [{ ref: 's1:0' }, { ref: 's1:1' }, { ref: 's3:0' }] }, + found: { toolCallId: 's3', more: 5, places: [{ ref: 's3:0' }, { ref: 's1:0' }, { ref: 's1:1' }] }, }); }); + it('FE-AI-PARTS-008c: a note kept in the trip notes is a line of its own, a refused or running one a step', () => { + const kept = (id: string, extra: Record): AssistantMessagePart => + ({ + type: 'tool-call', + toolCallId: id, + toolName: 'remember_for_trip', + args: { note: ' Vegetarian ' }, + ...extra, + }) as AssistantMessagePart; + const blocks = messageBlocks( + message([ + kept('m1', { result: { notes: 'Vegetarian' } }), + kept('m2', { result: 'The trip notes are full.', isError: true }), + ]), + false + ); + expect(blocks).toMatchObject([ + { kind: 'remembered', note: 'Vegetarian' }, + { kind: 'steps', steps: [{ toolCallId: 'm2' }] }, + ]); + expect(messageBlocks(message([kept('m3', {})]), true)).toMatchObject([{ kind: 'steps' }]); + }); + + it('FE-AI-PARTS-008b: an answer that names none of the places it found shows no strip', () => { + const blocks = messageBlocks( + message([places('s1', [found('s1:0', { name: 'Café Ñandú' })]), { type: 'text', text: 'It rains all day.' }]), + false + ); + expect(blocks.map((b) => b.kind)).toEqual(['text']); + // Named with other accents and case, it is the same place. + const named = messageBlocks( + message([places('s1', [found('s1:0', { name: 'Café Ñandú' })]), { type: 'text', text: 'Try cafe nandu.' }]), + false + ); + expect(named.map((b) => b.kind)).toEqual(['places', 'text']); + }); + it('FE-AI-PARTS-009: drops the places a change of the model proposes, and the strip if none is left', () => { const blocks = messageBlocks( message([ diff --git a/client/src/components/Assistant/assistantParts.ts b/client/src/components/Assistant/assistantParts.ts index c5c983c44..7cd14e929 100644 --- a/client/src/components/Assistant/assistantParts.ts +++ b/client/src/components/Assistant/assistantParts.ts @@ -66,6 +66,8 @@ type MessageBlock = | { kind: 'change'; key: string; items: ChangeItem[] } | { kind: 'places'; key: string; found: AssistantPlaces } | { kind: 'weather'; key: string; days: AssistantWeatherDay[] } + // A note the model kept in the trip notes on its own, as a quiet line. + | { kind: 'remembered'; key: string; note: string } | CardsBlock; /** How cards are laid out: the group each joins, and which ones give way to a change they served. */ @@ -93,6 +95,37 @@ function proposed(place: AssistantFoundPlace, effects: readonly AssistantChangeE }); } +/** A name as it is compared with the answer's words: lower case, without accents or marks. */ +function folded(text: string): string { + return text.normalize('NFKD').replace(/\p{M}/gu, '').toLocaleLowerCase(); +} + +/** + * The found places the answer names, first, then the rest; null when the + * answer is written and names none of them. A search the answer does not + * draw on is the model's own groundwork and shows no strip. + */ +function namedFirst(places: readonly AssistantFoundPlace[], text: string): AssistantFoundPlace[] | null { + if (text.trim() === '') return [...places]; + const words = folded(text); + const named = places.filter((place) => { + const name = folded(place.name).trim(); + return name.length >= 3 && words.includes(name); + }); + if (named.length === 0) return null; + return [...named, ...places.filter((place) => !named.includes(place))]; +} + +/** The tool that keeps a note in the trip notes; kept at once, without a card. */ +const REMEMBER_TOOL = 'remember_for_trip'; + +/** The note a call kept, once it is kept; null for any other call, one still running or one refused. */ +function rememberedNote(part: Extract): string | null { + if (part.toolName !== REMEMBER_TOOL || part.approval || part.result === undefined || part.isError) return null; + const note = (part.args as { note?: unknown }).note; + return typeof note === 'string' && note.trim() !== '' ? note.trim() : null; +} + /** Every day the answer's weather steps looked up, one entry per date (the later look wins), by date. */ function weatherDays(parts: readonly AssistantMessagePart[]): AssistantWeatherDay[] { const byDate = new Map(); @@ -265,8 +298,13 @@ export function messageBlocks( } 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 } }); + const text = message.parts.map((p) => (p.type === 'text' ? p.text : '')).join(' '); + const places = namedFirst( + found.places.filter((place) => !proposed(place, effects)), + text + ); + if (places && places.length > 0) + blocks.push({ kind: 'places', key: `places-${index}`, found: { ...found, places } }); return; } if (part.type === 'data-trek-weather' && index === firstWeather) { @@ -286,6 +324,11 @@ export function messageBlocks( pushChange(blocks, item, fromStrip); return; } + const note = rememberedNote(part); + if (note !== null) { + blocks.push({ kind: 'remembered', key: `remembered-${part.toolCallId}`, note }); + return; + } const step: ToolStep = { toolCallId: part.toolCallId, toolName: part.toolName, diff --git a/client/src/components/Assistant/cards/parts/CardHeader.tsx b/client/src/components/Assistant/cards/parts/CardHeader.tsx index bc180ee7f..bea04ed1a 100644 --- a/client/src/components/Assistant/cards/parts/CardHeader.tsx +++ b/client/src/components/Assistant/cards/parts/CardHeader.tsx @@ -48,14 +48,14 @@ export default function CardHeader({ children?: ReactNode; }) { return ( -
-
+
+
{tile ?? ( )}
@@ -71,14 +71,14 @@ export default function CardHeader({ {title} {count !== undefined && ( {count} diff --git a/client/src/components/Assistant/cards/tour/TourRouteHead.tsx b/client/src/components/Assistant/cards/tour/TourRouteHead.tsx index 6fc0d6a91..a73eefdd4 100644 --- a/client/src/components/Assistant/cards/tour/TourRouteHead.tsx +++ b/client/src/components/Assistant/cards/tour/TourRouteHead.tsx @@ -160,7 +160,7 @@ export default function TourRouteHead({ card, onShow }: { card: TourRouteCard; o const { t } = useTranslation(); const name = routeName(card, t); const tile = ( - + ); diff --git a/client/src/components/Assistant/cards/tour/tourLook.ts b/client/src/components/Assistant/cards/tour/tourLook.ts index c36be4f77..9f8d89588 100644 --- a/client/src/components/Assistant/cards/tour/tourLook.ts +++ b/client/src/components/Assistant/cards/tour/tourLook.ts @@ -1,5 +1,5 @@ import { type AssistantApplied, cumulativeMetres, type TourRouteCard, type TrekCard } from '@trek/shared'; -import { Route } from 'lucide-react'; +import { Circle, CircleCheck, CircleSlash, Route } from 'lucide-react'; import type { RouteLineSpec, RouteUse } from '../../../../types/assistantMap'; import type { EntityTarget, PackLook } from '../cardTypes'; @@ -86,6 +86,9 @@ export const TOUR_LOOK = { verbs: { addTour: { Icon: Route, tone: 'success', labelKey: 'assistant.change.verb.addTour' }, updateTour: { Icon: Route, tone: 'info', labelKey: 'assistant.change.verb.updateTour' }, + markVisited: { Icon: CircleCheck, tone: 'success', labelKey: 'assistant.change.verb.markVisited' }, + markSkipped: { Icon: CircleSlash, tone: 'info', labelKey: 'assistant.change.verb.markSkipped' }, + markPlanned: { Icon: Circle, tone: 'info', labelKey: 'assistant.change.verb.markPlanned' }, }, tools: {}, steps: { @@ -96,6 +99,7 @@ export const TOUR_LOOK = { list_tours: { stepKey: 'assistant.step.readingTours' }, find_tours_for_day: { stepKey: 'assistant.step.readingTours' }, search_tour_pois: { stepKey: 'assistant.step.tourPois' }, + get_city_walk_progress: { stepKey: 'assistant.step.readingTour' }, }, entities: { tour: { diff --git a/client/src/components/Assistant/useAssistantPlaceStrip.ts b/client/src/components/Assistant/useAssistantPlaceStrip.ts index c99434767..5860eacb3 100644 --- a/client/src/components/Assistant/useAssistantPlaceStrip.ts +++ b/client/src/components/Assistant/useAssistantPlaceStrip.ts @@ -1,38 +1,18 @@ import { type AssistantFoundPlace, categoryIdForKind } from '@trek/shared'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback } from 'react'; import { useShallow } from 'zustand/react/shallow'; -import { useNetworkMode } from '../../hooks/useNetworkMode'; -import { useTranslation } from '../../i18n'; import { chatOf, useAssistantStore } from '../../store/assistantStore'; import { useCanDo } from '../../store/permissionsStore'; import { useSettingsStore } from '../../store/settingsStore'; import { useTripStore } from '../../store/tripStore'; import type { Assignment } from '../../types'; -import { dayLabel } from '../../utils/dayLabel'; import { haversineKm } from '../../utils/geo'; import { safeHexColor } from '../../utils/safeColor'; import { formatDistance } from '../../utils/units'; -import { hasUndecided } from './assistantParts'; import { useAssistantScope, useAssistantView } from './assistantViewContext'; import { useCardPoint } from './cards/parts/useCardPoint'; -/** A day a found place can be suggested for, named the way the day list names it. */ -interface StripDay { - id: number; - /** Its place in the trip, 1 for the first day: "+ Day 3". */ - n: number; - label: string; - /** Its weekday, short ("Sat"), for the day grid; null for a day without a date. */ - weekday: string | null; -} - -/** The short weekday of a `YYYY-MM-DD` date, read as that calendar day wherever the reader is. */ -function weekdayOf(date: string | null | undefined, locale: string): string | null { - if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) return null; - return new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' }).format(new Date(`${date}T00:00:00Z`)); -} - /** The trip category a found place most likely belongs in, for its line and its icon. */ interface StripCategory { name: string; @@ -54,20 +34,17 @@ function lastSpot(stops: readonly Assignment[] | undefined): { lat: number; lng: /** * What the strip of found places needs, the same for the window and the - * phone: the trip's days to suggest a place for, the day in focus, how far a - * place is from that day's last stop, its likely category, whether a - * suggestion can be made now and why not, and the map's part (the pin while - * the pointer rests on a card, the camera on a click). + * phone: the day in focus, how far a place is from that day's last stop, its + * likely category, whether a place can be added, and the map's part (the pin + * while the pointer rests on a card, the camera on a click). * - * Suggesting writes nothing: the server holds the place as a change card to - * decide like any other. It waits while another change is undecided, because - * one decision answers all open cards. + * Adding opens the planner's own place form, filled in with the place and on + * the day in focus: the traveller sees everything about it and saves it as + * any other place, so nothing waits for the chat's open decisions. */ export function useAssistantPlaceStrip() { - const { t, locale } = useTranslation(); - const { tripId } = useAssistantView(); + const { tripId, onRevealMap } = useAssistantView(); const scope = useAssistantScope(); - const { offline } = useNetworkMode(); const can = useCanDo(); const unit = useSettingsStore((s) => s.settings.distance_unit ?? 'metric'); const { trip, days, assignments, categories } = useTripStore( @@ -81,44 +58,15 @@ export function useAssistantPlaceStrip() { }; }) ); - const { focusDayId, isRunning, threadId, waiting } = useAssistantStore( - useShallow((s) => { - const chat = chatOf(s, tripId); - return { - focusDayId: chat.focus?.dayId ?? null, - isRunning: chat.isRunning, - threadId: chat.threadId, - waiting: hasUndecided(chat.messages), - }; - }) - ); - const addFoundPlace = useAssistantStore((s) => s.addFoundPlace); + const focusDayId = useAssistantStore((s) => chatOf(s, tripId).focus?.dayId ?? null); + const openPlaceDraft = useAssistantStore((s) => s.openPlaceDraft); const point = useCardPoint(); - const [adding, setAdding] = useState(null); - const stripDays = useMemo( - (): StripDay[] => - (days ?? []).map((day, index) => ({ - id: day.id, - n: index + 1, - label: dayLabel(day, index, t, locale), - weekday: weekdayOf(day.date, locale), - })), - [days, t, locale] - ); - const focusDay = stripDays.find((d) => d.id === focusDayId) ?? null; + const focusDay = (days ?? []).find((d) => d.id === focusDayId) ?? null; const from = focusDay && assignments ? lastSpot(assignments[String(focusDay.id)]) : null; - /** - * Whether "+ Day" shows at all: a trip's chat (a journey's has no days), the - * planner holds the trip and the user may add to it. - */ - const canSuggest = scope.kind === 'trip' && !!trip && can('place_edit', trip) && stripDays.length > 0; - /** Why "+ Day" is off right now, as a tooltip; null when it is on. */ - let blockedKey: string | null = null; - if (offline) blockedKey = 'assistant.error.offline'; - else if (waiting) blockedKey = 'assistant.composer.decideFirst'; - const disabled = blockedKey !== null || isRunning || threadId === null || adding !== null; + /** Whether "Add to trip" shows at all: a trip's chat (a journey's has no days), the planner holds the trip and the user may add to it. */ + const canAdd = scope.kind === 'trip' && !!trip && can('place_edit', trip); const categoryOf = (place: AssistantFoundPlace): StripCategory | null => { const id = categoryIdForKind(place.kind, categories); @@ -130,14 +78,11 @@ export function useAssistantPlaceStrip() { const distanceOf = (place: AssistantFoundPlace): string | null => from ? formatDistance(haversineKm(from, place), unit) : null; - const suggest = async (place: AssistantFoundPlace, dayId: number) => { - if (disabled) return; - setAdding(place.ref); - try { - await addFoundPlace(tripId, place.ref, dayId); - } finally { - setAdding(null); - } + /** Opens the place form with it; on the phone the chat steps aside first. */ + const open = (place: AssistantFoundPlace) => { + if (!canAdd || !trip) return; + onRevealMap?.(); + openPlaceDraft(trip.id, place, focusDay?.id ?? null); }; /** Shows the place on the map; on the phone the chat steps aside first. */ @@ -151,17 +96,5 @@ export function useAssistantPlaceStrip() { [hoverPoint] ); - return { - days: stripDays, - focusDay, - canSuggest, - disabled, - blockedKey, - adding, - categoryOf, - distanceOf, - suggest, - show, - hover, - }; + return { canAdd, categoryOf, distanceOf, open, show, hover }; } diff --git a/client/src/components/Planner/PlaceFormModal.helpers.ts b/client/src/components/Planner/PlaceFormModal.helpers.ts index 24e7c3a3f..8c595d272 100644 --- a/client/src/components/Planner/PlaceFormModal.helpers.ts +++ b/client/src/components/Planner/PlaceFormModal.helpers.ts @@ -136,6 +136,8 @@ export interface PlacePrefill { website?: string phone?: string osm_id?: string + /** A place Google knows, such as one the assistant found: the details column reads it by this id. */ + google_place_id?: string } /** A blank form at a prefilled position, with what the POI already knew. */ @@ -149,6 +151,7 @@ export function prefillForm(prefill: PlacePrefill, extra: Partial website: prefill.website || '', phone: prefill.phone || '', osm_id: prefill.osm_id, + google_place_id: prefill.google_place_id, ...extra, } } @@ -170,7 +173,7 @@ export function openingDetailsSelection( } } if (prefill) { - return { placeId: prefill.osm_id || undefined, lat: prefill.lat, lng: prefill.lng, name: prefill.name || '' } + return { placeId: prefill.google_place_id || prefill.osm_id || undefined, lat: prefill.lat, lng: prefill.lng, name: prefill.name || '' } } return null } diff --git a/client/src/components/Planner/PlaceFormModal.tsx b/client/src/components/Planner/PlaceFormModal.tsx index 1f8db6e58..01a18e020 100644 --- a/client/src/components/Planner/PlaceFormModal.tsx +++ b/client/src/components/Planner/PlaceFormModal.tsx @@ -64,7 +64,7 @@ interface PlaceFormModalProps { onClose: () => void onSave: (data: PlaceSubmitData, files?: File[]) => Promise<{ id: number } | void> | void place: Place | null - prefillCoords?: { lat: number; lng: number; name?: string; address?: string; website?: string; phone?: string; osm_id?: string; stop_type?: RoadtripStopType | null; duration_minutes?: number; category?: string } | null + prefillCoords?: { lat: number; lng: number; name?: string; address?: string; website?: string; phone?: string; osm_id?: string; google_place_id?: string; stop_type?: RoadtripStopType | null; duration_minutes?: number; category?: string } | null tripId: number categories: Category[] onCategoryCreated: (category: { name: string; color?: string; icon?: string }) => Promise | undefined diff --git a/client/src/pages/tripPlanner/useAssistantPlaceDraft.ts b/client/src/pages/tripPlanner/useAssistantPlaceDraft.ts new file mode 100644 index 000000000..66cf72552 --- /dev/null +++ b/client/src/pages/tripPlanner/useAssistantPlaceDraft.ts @@ -0,0 +1,35 @@ +import { useEffect } from 'react'; + +import { useAssistantStore } from '../../store/assistantStore'; +import type { PlaceFormOpeners } from './usePlaceFormOpeners'; + +/** + * A place the assistant found, opened in the place form when its card is + * clicked: filled in with what the search knew, the details column reading + * the rest (photos, hours, description) by its Google id. The form is the + * planner's own, so the place is saved as any other, on the day in focus. + */ +export function useAssistantPlaceDraft(tripId: number, openAddPlaceFromPoi: PlaceFormOpeners['openAddPlaceFromPoi']) { + const draft = useAssistantStore((s) => (s.placeDraft?.tripId === tripId ? s.placeDraft : null)); + const takePlaceDraft = useAssistantStore((s) => s.takePlaceDraft); + + useEffect(() => { + if (!draft) return; + takePlaceDraft(draft.seq); + const { place } = draft; + openAddPlaceFromPoi( + { + lat: place.lat, + lng: place.lng, + name: place.name, + address: place.address, + website: null, + phone: null, + osm_id: place.osmId ?? '', + google_place_id: place.googlePlaceId, + poi_type: place.kind, + }, + draft.dayId + ); + }, [draft, openAddPlaceFromPoi, takePlaceDraft]); +} diff --git a/client/src/pages/tripPlanner/usePlaceFormOpeners.ts b/client/src/pages/tripPlanner/usePlaceFormOpeners.ts index 6cc58849f..170eb2374 100644 --- a/client/src/pages/tripPlanner/usePlaceFormOpeners.ts +++ b/client/src/pages/tripPlanner/usePlaceFormOpeners.ts @@ -76,7 +76,7 @@ export function usePlaceFormOpeners(options: PlaceFormOpenersOptions) { // Open the Add-Place form pre-filled from an OSM "explore" POI marker: all the // data already comes from the POI, so no reverse-geocode is needed. const openAddPlaceFromPoi = useCallback(( - poi: { lat: number; lng: number; name: string; address: string | null; website: string | null; phone: string | null; osm_id: string; category?: string | null; poi_type?: string | null }, + poi: { lat: number; lng: number; name: string; address: string | null; website: string | null; phone: string | null; osm_id: string; google_place_id?: string | null; category?: string | null; poi_type?: string | null }, dayId?: number | null, /** Index within that day. Omitted, the place is appended, which is what every caller did before. */ position?: number | null, @@ -100,6 +100,7 @@ export function usePlaceFormOpeners(options: PlaceFormOpenersOptions) { // A plugin POI's `plugin::` rides along as it is. The server never // takes that prefix for a Google place id, so the details column makes no Google call. osm_id: poi.osm_id, + google_place_id: poi.google_place_id || undefined, // What the map search filed it under, so the form can preselect a category (#2282). category: poi.poi_type || poi.category || undefined, stop_type: stop?.stopType ?? null, diff --git a/client/src/pages/tripPlanner/usePlannerDialogs.ts b/client/src/pages/tripPlanner/usePlannerDialogs.ts index cc6b603ed..bdb2840a1 100644 --- a/client/src/pages/tripPlanner/usePlannerDialogs.ts +++ b/client/src/pages/tripPlanner/usePlannerDialogs.ts @@ -27,7 +27,7 @@ export function usePlannerDialogs(options: PlannerDialogsOptions) { const { tripId, tripActions, searchParams, setSearchParams } = options const [showPlaceForm, setShowPlaceForm] = useState(false) const [editingPlace, setEditingPlace] = useState(null) - const [prefillCoords, setPrefillCoords] = useState<{ lat: number; lng: number; name?: string; address?: string; website?: string; phone?: string; osm_id?: string; stop_type?: RoadtripStopType | null; duration_minutes?: number; category?: string } | null>(null) + const [prefillCoords, setPrefillCoords] = useState<{ lat: number; lng: number; name?: string; address?: string; website?: string; phone?: string; osm_id?: string; google_place_id?: string; stop_type?: RoadtripStopType | null; duration_minutes?: number; category?: string } | null>(null) const [editingAssignmentId, setEditingAssignmentId] = useState(null) // Day context of the open form. Set only by the day-scoped entry points (the // mobile day toolbar, a long-press on the mobile map); every other opener diff --git a/client/src/pages/tripPlanner/useTripPlanner.ts b/client/src/pages/tripPlanner/useTripPlanner.ts index 30cccda0e..44aa6554d 100644 --- a/client/src/pages/tripPlanner/useTripPlanner.ts +++ b/client/src/pages/tripPlanner/useTripPlanner.ts @@ -52,6 +52,7 @@ import { usePlannerDialogs } from './usePlannerDialogs' import { usePlaceDeepLink } from './usePlaceDeepLink' import { useAssistantDeepLink } from './useAssistantDeepLink' import { useAssistantCompareView, useAssistantMapLink } from './useAssistantMapLink' +import { useAssistantPlaceDraft } from './useAssistantPlaceDraft' import { usePlannerMapView } from './usePlannerMapView' import { useMapPlaces } from './useMapPlaces' import { useRoadtripFeed } from './useRoadtripFeed' @@ -593,6 +594,7 @@ export function useTripPlanner() { setPrefillCoords, setEditingPlace, setEditingAssignmentId, setPlaceFormDayId, setPlaceFormPosition, setShowPlaceForm, roadtripActive, roadtripFeedActive, roadtripRoutes, roadtripCorridor, manualStopTargetFor, isTourPlace, }) + useAssistantPlaceDraft(tripId, openAddPlaceFromPoi) const { handleReorder, handleReorderDays, handleUpdateDayTitle, handleMoveToDay, handleRemoveAssignment, diff --git a/client/src/store/assistantStore.ts b/client/src/store/assistantStore.ts index cdc600a79..33ee8e267 100644 --- a/client/src/store/assistantStore.ts +++ b/client/src/store/assistantStore.ts @@ -6,6 +6,7 @@ import { type AssistantDecision, type AssistantErrorCode, type AssistantFocus, + type AssistantFoundPlace, type AssistantMapView, type AssistantMention, type AssistantMessage, @@ -149,6 +150,8 @@ interface AssistantState { routeFocus: { tripId: number; ref: string; focus: RouteProfileFocus; source: 'card' | 'map' } | null; /** A line a card asks the map to draw while the pointer rests on it (a day of a drive, a saved tour). */ routePreview: { tripId: number; ref: string; lines: LatLng[][]; color: string } | null; + /** A found place to open in the planner's place form, filled in; `seq` tells a new one from the same place asked again. */ + placeDraft: { tripId: number; seq: number; place: AssistantFoundPlace; dayId: number | null } | null; /** The route card the phone shows over the map. */ routePeek: { tripId: number; toolCallId: string } | null; /** Where the floating window sits after its last move or resize, for the map to frame around it. */ @@ -207,6 +210,9 @@ interface AssistantState { setMapHover: (tripId: number, hoveredId: string | null, source: AssistantMapSource) => void; requestCamera: (tripId: number, points: LatLng[], mode: 'fit' | 'pan', why: 'user' | 'auto') => void; + /** Opens a found place in the planner's place form; the planner takes it with `takePlaceDraft`. */ + openPlaceDraft: (tripId: number, place: AssistantFoundPlace, dayId: number | null) => void; + takePlaceDraft: (seq: number) => void; setPreviewPin: (preview: AssistantState['preview']) => void; setWindowRect: (rect: WindowRect | null) => void; setMapView: (mapView: AssistantState['mapView']) => void; @@ -820,6 +826,11 @@ function mapActions(ctx: Ctx) { }, requestCamera: (tripId: number, points: LatLng[], mode: 'fit' | 'pan', why: 'user' | 'auto') => requestCameraOn(ctx, tripId, points, mode, why), + openPlaceDraft: (tripId: number, place: AssistantFoundPlace, dayId: number | null) => + ctx.set({ placeDraft: { tripId, seq: (ctx.get().placeDraft?.seq ?? 0) + 1, place, dayId } }), + takePlaceDraft: (seq: number) => { + if (ctx.get().placeDraft?.seq === seq) ctx.set({ placeDraft: null }); + }, setPreviewPin: (preview: AssistantState['preview']) => ctx.set({ preview }), setWindowRect: (rect: WindowRect | null) => ctx.set({ windowRect: rect }), setMapView: (mapView: AssistantState['mapView']) => ctx.set({ mapView }), @@ -923,6 +934,7 @@ export const useAssistantStore = create((set, get) => { mapLink: null, camera: null, preview: null, + placeDraft: null, windowRect: null, mapView: null, studioHosts: {}, diff --git a/server/src/nest/assistant/assistant-memory.ts b/server/src/nest/assistant/assistant-memory.ts index 10c7f362b..d839d89fa 100644 --- a/server/src/nest/assistant/assistant-memory.ts +++ b/server/src/nest/assistant/assistant-memory.ts @@ -1,5 +1,6 @@ import type { AiToolSpec } from '../ai-connections/ai-chat-client.interface'; import type { SettingsService } from '../settings/settings.service'; +import type { AgentToolPort } from './assistant-agent'; import { MAX_MEMORY_CHARS, tripMemoryOf } from './assistant-context'; import { REMEMBER_TOOL } from './assistant-tools.constants'; import type { ToolRun } from './assistant-tools.service'; @@ -7,13 +8,21 @@ import type { ToolRun } from './assistant-tools.service'; /** The longest single note the model may keep. */ const MAX_NOTE_CHARS = 500; -/** How the model is offered the trip notes: one short fact per call, confirmed by the user like any change. */ +/** + * How the model is offered the trip notes: one short fact per call, kept at + * once without a card (the chat shows it as kept, and the notes are the + * user's to edit in the chat menu). + */ export const REMEMBER_SPEC: AiToolSpec = { name: REMEMBER_TOOL, description: "Keep a fact or wish about this trip in the user's trip notes, which come along with every later conversation " + - 'about this trip (for example "we travel as two", "vegetarian", "no museums before 10"). Call it whenever the ' + - 'user asks you to remember something, instead of saying you cannot. The user confirms it like any change.', + 'about this trip (for example "we travel as two", "vegetarian", "no museums before 10", "Anna has a bad knee"). ' + + 'It is kept at once, without asking: call it on your own whenever the user tells you something that will ' + + 'matter for later suggestions on this trip (who travels, needs, likes and dislikes, budget, pace, fixed ' + + 'times), and whenever they ask you to remember something, instead of saying you cannot. Keep nothing ' + + 'that only matters for this one answer, nothing already in , and nothing private beyond what the ' + + 'trip needs (no passport or card numbers). Say in a few words that you noted it.', parameters: { type: 'object', properties: { @@ -58,3 +67,21 @@ export async function rememberForTrip( await settings.upsertSetting(userId, 'assistant_memory', { ...all, [String(tripId)]: next }); return { ok: true, value: { notes: next } }; } + +/** + * The turn's tools with the trip notes kept at once: remember_for_trip runs + * as a read (the loop carries on with its answer) through `remember`, every + * other tool as the port has it. Without a trip the port stays as it is. + */ +export function rememberingPort( + port: AgentToolPort, + remember: ((args: Record) => Promise) | null, +): AgentToolPort { + if (!remember) return port; + return { + specs: () => port.specs(), + kindOf: (name) => (name === REMEMBER_TOOL && port.kindOf(name) !== null ? 'read' : port.kindOf(name)), + pinArgs: (name, args) => (name === REMEMBER_TOOL ? { args } : port.pinArgs(name, args)), + call: (name, args) => (name === REMEMBER_TOOL ? remember(args) : port.call(name, args)), + }; +} diff --git a/server/src/nest/assistant/assistant.prompt.ts b/server/src/nest/assistant/assistant.prompt.ts index 399b244e1..ec8606b00 100644 --- a/server/src/nest/assistant/assistant.prompt.ts +++ b/server/src/nest/assistant/assistant.prompt.ts @@ -30,7 +30,7 @@ How you work: - Besides the open trip you can work in the rest of TREK: tours (hikes, bike tours, city walks), the road trip, the travel journal, the vacation planner, the atlas, collections, the trip's chat, polls and notes, files and the user's settings. Their tools are not loaded at first: call use_addon_tools with the groups you need, then use them in the same reply. Tools without a trip work on the user's own data across trips. - Propose a deletion only when the user asks for it; it cannot be undone, and the card says so. - You cannot share, invite people, publish links or calendar feeds, change who can see a trip, leave, copy or delete the trip, or make a new one. If asked, say so in one sentence and where in TREK the user can do it. -- When the user asks you to remember something for this trip (who travels, what they like or avoid), keep it with remember_for_trip; it comes back as in every later conversation. Never say you cannot remember things. +- Keep what will matter later with remember_for_trip on your own, without asking: who travels, needs, likes and dislikes, budget, pace, fixed times the user mentions, and anything they ask you to remember. It is kept at once and comes back as in every later conversation; say briefly that you noted it. Where you are not sure it lasts, offer it as a follow-up instead ("Remember: …"). Never say you cannot remember things. - Expenses and their ids come from list_budget_items. Change an existing one with update_budget_item; never add a second entry to correct one. For what each person's share comes to, use get_budget_per_person; for who pays whom, get_settlement_summary. - For the wind on a day (a paddle, a sail, a ridge walk), use get_wind_forecast: it has the gusts and the direction hour by hour. - End every answer with one suggest_follow_ups call, alone in its step after your text: up to three short messages the user would plausibly send next, about what you just discussed (a hike you planned: an easier variant, what to pack; never the day plan when the answer was not about it). When you asked the user a question, offer its likely answers instead ("Yes, plan it anyway", "Rather in the Harz"). Skip it only while changes wait for the user's decision. diff --git a/server/src/nest/assistant/assistant.service.ts b/server/src/nest/assistant/assistant.service.ts index a56250d2d..461ac6492 100644 --- a/server/src/nest/assistant/assistant.service.ts +++ b/server/src/nest/assistant/assistant.service.ts @@ -38,7 +38,7 @@ import { mentionItems } from './assistant-cards.helpers'; import { todayIn } from './assistant-context'; import { DISCARDED, NOT_RUN, type Outcome, withCancelled, withOutcomes } from './assistant-decisions.helpers'; import { messageIdOf, outputText, partsOf, toChatHistory, toThreadMessages } from './assistant-history'; -import { rememberForTrip } from './assistant-memory'; +import { rememberForTrip, rememberingPort } from './assistant-memory'; import { toolOutputForProvider } from './assistant-output'; import { AssistantScopes, findScopedCall, scopedThreads, tripScope } from './assistant-scope'; import { AssistantStreamRegistry } from './assistant-stream.registry'; @@ -812,6 +812,14 @@ export class AssistantService { return new Map(lists.flat().map((call) => [call.id, call])); } + /** How a trip's turn keeps a note at once; a journey's has no trip notes. */ + private remembering(userId: number, scope: AssistantScope) { + if (scope.kind !== 'trip') return null; + const { tripId } = scope; + return (args: Record) => + this.inContext(() => rememberForTrip(this.settings, userId, tripId, args)); + } + /** * The turn itself, once the stream is open: the tool session, the trip or * book card, the history, then the agent loop. Whatever fails after this @@ -848,7 +856,7 @@ export class AssistantService { const outcome = await runAgentLoop( { client: this.chatClients.create(spec.connection), - tools: seeing.tools, + tools: rememberingPort(seeing.tools, this.remembering(userId, spec.scope)), store: this.stepStore(spec), describe: this.cardContext.forTurn(gate.reader, card, spec.locale, today).describe, refusalOf: gate.refusalOf, diff --git a/server/src/nest/assistant/cards/tour/index.ts b/server/src/nest/assistant/cards/tour/index.ts index 2494c9247..4afc7d673 100644 --- a/server/src/nest/assistant/cards/tour/index.ts +++ b/server/src/nest/assistant/cards/tour/index.ts @@ -15,6 +15,7 @@ import { tourListCard } from './tour-lists.cards'; import { proposalCard, savedTourCard } from './tour-route.cards'; import { cityWalkCard, paddleCard, skiCard } from './tour-sport.cards'; import { DELETE_CREATED_TOUR, DELETE_IMPORTED_TOUR } from './tour-undo'; +import { describeVisit, loadVisit, SET_VISIT_UNDO } from './tour-visit.cards'; /** * The tours in the assistant chat. A tour is a place with a route: its id is @@ -73,6 +74,12 @@ export const TOUR_CARDS: DomainCards = { undo: DELETE_CREATED_TOUR, }, update_tour: updateTour, + set_city_walk_visit: { + write: { verbKey: 'markVisited', entity: 'tour' }, + load: loadVisit, + describe: describeVisit, + undo: SET_VISIT_UNDO, + }, import_tour_gpx: { write: { verbKey: 'importTour', entity: 'tour' }, describe: TOUR_DESCRIBERS.import_tour_gpx, diff --git a/server/src/nest/assistant/cards/tour/tour-visit.cards.ts b/server/src/nest/assistant/cards/tour/tour-visit.cards.ts new file mode 100644 index 000000000..32d5582c8 --- /dev/null +++ b/server/src/nest/assistant/cards/tour/tour-visit.cards.ts @@ -0,0 +1,84 @@ +import type { Described, PreviewContext, ToolArgs, ToolCards, UndoCall, UndoSnapshot } from '../card-types'; +import { argId, readKey, recordOf, type Row } from '../card-values'; +import { dayLabel, Rows, text } from '../preview-format'; +import type { AssistantChangeVerbKey } from '@trek/shared'; + +/** + * A station of a city walk checked off on its day (set_city_walk_visit): the + * card names the station and the walk, the verb says what the mark does, and + * the way back is the same tool with the status the station had before, + * while the station still has the status the change gave it. + */ + +const PROGRESS_TOOL = 'get_city_walk_progress'; + +const VERB_OF: Readonly> = { + visited: 'markVisited', + skipped: 'markSkipped', + planned: 'markPlanned', +}; + +/** What the card and its undo read: the walk on that day, as get_city_walk_progress answers it. */ +function progressArgs(args: ToolArgs): Row { + const read: Row = { placeId: args.placeId, dayId: args.dayId }; + if (args.assignmentId !== undefined) read.assignmentId = args.assignmentId; + return read; +} + +/** The station a call names, from the walk as read. */ +function stationIn(progress: unknown, key: unknown): Row | null { + const stations = recordOf(progress).stations; + if (!Array.isArray(stations) || typeof key !== 'string') return null; + return (stations.map(recordOf).find((station) => station.key === key) ?? null) as Row | null; +} + +export async function loadVisit(args: ToolArgs, read: (tool: string, args?: ToolArgs) => Promise) { + if (argId(args, 'placeId') === null || argId(args, 'dayId') === null) return null; + return await read(PROGRESS_TOOL, progressArgs(args)); +} + +export function describeVisit(ctx: PreviewContext, args: ToolArgs, loaded: unknown): Described { + const placeId = argId(args, 'placeId'); + const dayId = argId(args, 'dayId'); + const walk = placeId === null ? '' : (ctx.card.places.get(placeId)?.name ?? ''); + const station = stationIn(loaded, args.stationKey); + const name = text(station?.name); + const status = typeof args.status === 'string' ? args.status : ''; + return { + subject: name || walk, + rows: new Rows().add('route', name ? walk : '').add('day', dayLabel(ctx, dayId)), + dayId, + placeIds: placeId === null ? [] : [placeId], + verbKey: VERB_OF[status], + // Marking a station with the status it has already changes nothing to take back. + undoable: station === null || station.status !== status, + }; +} + +/** The walk as the undo read it before it checks. */ +function readProgress(snapshot: UndoSnapshot, call: UndoCall | undefined): unknown { + return call ? snapshot.reads.get(readKey(PROGRESS_TOOL, progressArgs(call.args))) : undefined; +} + +export const SET_VISIT_UNDO: NonNullable = { + tool: 'set_city_walk_visit', + args: (args, result) => { + const answer = recordOf(result); + const before = answer.previous_status; + if (typeof before !== 'string' || typeof answer.station_key !== 'string') return null; + const assignmentId = recordOf(answer.walk).assignment_id; + return { + placeId: args.placeId, + dayId: args.dayId, + stationKey: answer.station_key, + status: before, + ...(typeof assignmentId === 'number' ? { assignmentId } : {}), + }; + }, + reads: (args) => [{ tool: PROGRESS_TOOL, args: progressArgs(args) }], + unchanged: (result, snapshot, call) => { + const answer = recordOf(result); + const station = stationIn(readProgress(snapshot, call), answer.station_key); + return station !== null && station.status === answer.status; + }, +}; diff --git a/server/src/nest/tours/tours.mcp.ts b/server/src/nest/tours/tours.mcp.ts index 2f612981e..2dd24b171 100644 --- a/server/src/nest/tours/tours.mcp.ts +++ b/server/src/nest/tours/tours.mcp.ts @@ -1,9 +1,18 @@ import { ADDON_IDS } from '../../addons'; import type { TourDayRow } from '../../db/repositories/Tours.repository'; -import { noAccess } from '../../mcp/tools/_shared'; -import { McpController, Tool, TOOL_ANNOTATIONS_READONLY, errorResult, ok, type McpContext } from '../../nest-mcp'; +import { noAccess, permissionDenied } from '../../mcp/tools/_shared'; +import { + McpController, + Tool, + TOOL_ANNOTATIONS_READONLY, + TOOL_ANNOTATIONS_WRITE, + errorResult, + ok, + type McpContext, +} from '../../nest-mcp'; import { addonGate } from '../addons/addon-gate'; import { AddonsService } from '../addons/addons.service'; +import { McpToolGuardsService } from '../mcp-shared/mcp-tool-guards.service'; import { TripAccessService } from '../trip-membership/trip-access.service'; import { withoutLine } from './tour-activities.helpers'; import { TourGpxExportService } from './tour-gpx-export.service'; @@ -15,10 +24,13 @@ import { ToursService } from './tours.service'; import { NotFoundException } from '@nestjs/common'; import { idSchema, + stationKeySchema, tourPoiSearchRequestSchema, + tourVisitUpdateRequestSchema, type TourDetailResponse, type TourListItem, type TourPoiSearchRequest, + type TourVisitUpdateRequest, } from '@trek/shared'; type TourDay = Pick; @@ -101,6 +113,7 @@ export class ToursMcp { private readonly gpxExport: TourGpxExportService, private readonly poiSearch: TourPoiSearchService, private readonly visits: TourVisitsService, + private readonly guards: McpToolGuardsService, // Read by the addon gate of every tool here. readonly addons: AddonsService, ) {} @@ -206,6 +219,70 @@ export class ToursMcp { } } + @Tool({ + name: 'set_city_walk_visit', + description: + 'Check off a station of a city walk (tour_type walk) on one day it is on, as the travellers tap it in the day plan: visited, skipped, or planned to take a mark back. Use it when the travellers say where they are or what they did ("we are at the museum", "skip the tower"); stationKey is the key of the station from get_city_walk_progress or get_tour_plan. The mark belongs to that day only, as every visit does. The answer gives the status before and after, and the walk as it now stands that day (the same fields as get_city_walk_progress): read get_city_walk_remaining_time next for what is left. Needs the right to edit the trip days, as checking off in the day plan does. Nothing about the route changes.', + inputSchema: { + tripId: idSchema, + placeId: idSchema.describe('The place id of the city walk, as list_tours gives it'), + dayId: idSchema.describe('The day the walk is on, as find_tours_for_day gives it'), + stationKey: stationKeySchema.describe('The key of the station, as get_city_walk_progress gives it'), + status: tourVisitUpdateRequestSchema.shape.status.describe( + 'visited for a station seen, skipped for one left out, planned to take a mark back', + ), + assignmentId: idSchema + .optional() + .describe( + 'For a walk put on that day more than once: the assignment of the run, one of that walk on that day; without it the first run of the day', + ), + }, + annotations: TOOL_ANNOTATIONS_WRITE, + when: addonGate(ADDON_IDS.TOURS), + access: { group: 'trips', mode: 'write' }, + }) + async setCityWalkVisit( + { + tripId, + placeId, + dayId, + stationKey, + status, + assignmentId, + }: { + tripId: number; + placeId: number; + dayId: number; + stationKey: string; + status: TourVisitUpdateRequest['status']; + assignmentId?: number; + }, + ctx: McpContext, + ) { + if (!(await this.access.findAccessible(tripId, ctx.userId))) return noAccess(); + // Checking off in the day plan is what REST asks 'day_edit' for. + if (!(await this.guards.hasTripPermission('day_edit', tripId, ctx.userId))) return permissionDenied(); + let before: WalkProgress; + try { + before = await this.visits.progress(tripId, placeId, dayId, assignmentId); + } catch (err: unknown) { + if (err instanceof NotFoundException) return errorResult('No city walk of this trip is on that day.'); + throw err; + } + const station = before.stations.find((one) => one.key === stationKey); + if (!station) + return errorResult('This walk has no station with that key on that day: read get_city_walk_progress.'); + await this.visits.update(String(tripId), before.assignment_id, stationKey, { status }); + const after = await this.visits.progress(tripId, placeId, dayId, before.assignment_id); + return ok({ + station_key: stationKey, + name: station.name, + previous_status: station.status, + status, + walk: after, + }); + } + @Tool({ name: 'get_city_walk_remaining_time', description: diff --git a/server/tests/unit/nest/assistant/assistant-memory.test.ts b/server/tests/unit/nest/assistant/assistant-memory.test.ts index 285313830..7f0d753b8 100644 --- a/server/tests/unit/nest/assistant/assistant-memory.test.ts +++ b/server/tests/unit/nest/assistant/assistant-memory.test.ts @@ -3,7 +3,13 @@ * same note is not added twice, notes that would run past the limit are * refused whole, and other trips' notes stay as they are. */ -import { noteOf, REMEMBER_SPEC, rememberForTrip } from '../../../../src/nest/assistant/assistant-memory'; +import type { AgentToolPort } from '../../../../src/nest/assistant/assistant-agent'; +import { + noteOf, + REMEMBER_SPEC, + rememberForTrip, + rememberingPort, +} from '../../../../src/nest/assistant/assistant-memory'; import type { SettingsService } from '../../../../src/nest/settings/settings.service'; import { describe, expect, it, vi } from 'vitest'; @@ -54,4 +60,31 @@ describe('the trip notes', () => { expect(noteOf({ note: 'y'.repeat(600) })).toHaveLength(500); expect(REMEMBER_SPEC.parameters).toMatchObject({ required: ['note'], additionalProperties: false }); }); + + it('ASTMEM-005: in a trip turn the note is kept at once as a read; other tools and a journey keep the port as it is', async () => { + const port: AgentToolPort = { + specs: () => [REMEMBER_SPEC], + kindOf: (name) => (name === 'remember_for_trip' || name === 'create_place' ? 'write' : null), + pinArgs: (_name, args) => ({ args: { ...args, tripId: 4 } }), + call: vi.fn(async () => ({ ok: true, value: 'port' })), + }; + const remember = vi.fn(async () => ({ ok: true, value: { notes: 'Vegetarian' } })); + const kept = rememberingPort(port, remember); + expect(kept.kindOf('remember_for_trip')).toBe('read'); + expect(kept.kindOf('create_place')).toBe('write'); + expect(kept.kindOf('nope')).toBeNull(); + expect(kept.pinArgs('remember_for_trip', { note: 'x' })).toEqual({ args: { note: 'x' } }); + expect(kept.pinArgs('create_place', {})).toEqual({ args: { tripId: 4 } }); + expect(await kept.call('remember_for_trip', { note: 'Vegetarian' })).toEqual({ + ok: true, + value: { notes: 'Vegetarian' }, + }); + expect(await kept.call('create_place', {})).toEqual({ ok: true, value: 'port' }); + expect(kept.specs()).toEqual([REMEMBER_SPEC]); + expect(remember).toHaveBeenCalledTimes(1); + expect(rememberingPort(port, null)).toBe(port); + // A session that does not hold the tool (a journey's) never gains it. + const journey = rememberingPort({ ...port, kindOf: () => null }, remember); + expect(journey.kindOf('remember_for_trip')).toBeNull(); + }); }); diff --git a/server/tests/unit/nest/assistant/cards/tour/tour-visit.cards.test.ts b/server/tests/unit/nest/assistant/cards/tour/tour-visit.cards.test.ts new file mode 100644 index 000000000..656032521 --- /dev/null +++ b/server/tests/unit/nest/assistant/cards/tour/tour-visit.cards.test.ts @@ -0,0 +1,103 @@ +/** + * ASTCARD-TOUR-050 to ASTCARD-TOUR-052 + * + * A station of a city walk checked off on its day (set_city_walk_visit): the + * card names the station and its walk with the verb of the mark, and the way + * back sets the status the station had before, while it still has the one the + * change gave it. + */ +import { buildTripCard } from '../../../../../../src/nest/assistant/assistant-context'; +import { unchangedSinceApplied } from '../../../../../../src/nest/assistant/assistant-undo.helpers'; +import { loadFor } from '../../../../../../src/nest/assistant/cards/card-context'; +import type { PreviewContext } from '../../../../../../src/nest/assistant/cards/card-types'; +import { readKey } from '../../../../../../src/nest/assistant/cards/card-values'; +import { ASSISTANT_UNDO_MAP, ASSISTANT_WRITE_TOOLS } from '../../../../../../src/nest/assistant/cards/registry'; +import { describeToolCall } from '../../../../../../src/nest/assistant/tool-call-preview.helpers'; +import { assistantChangePreviewSchema } from '@trek/shared'; + +import { describe, expect, it, vi } from 'vitest'; + +const logMock = vi.hoisted(() => ({ logInfo: vi.fn(), logError: vi.fn(), logWarn: vi.fn(), logDebug: vi.fn() })); +vi.mock('../../../../../../src/nest/audit/audit-log.logger', () => logMock); + +const card = buildTripCard( + 1, + 7, + { trip: { title: 'Lisbon' }, days: [{ id: 31, day_number: 1, date: '2026-11-11', assignments: [] }] }, + { places: [{ id: 61, name: 'Old town walk', lat: 38.71, lng: -9.14 }] }, + null, +); +const ctx: PreviewContext = { card, categories: new Map(), locale: 'en-GB', today: '2026-11-01', airport: () => null }; + +const PROGRESS = { + place_id: 61, + day_id: 31, + assignment_id: 90, + stations: [ + { key: 'stop-se', name: 'Sé Cathedral', status: 'visited' }, + { key: 'stop-castle', name: 'Castle', status: 'planned' }, + ], +}; +const ARGS = { placeId: 61, dayId: 31, stationKey: 'stop-castle', status: 'skipped' }; +const RESULT = { + station_key: 'stop-castle', + name: 'Castle', + previous_status: 'planned', + status: 'skipped', + walk: { assignment_id: 90 }, +}; + +describe('set_city_walk_visit in the chat', () => { + it('ASTCARD-TOUR-050: the card names the station, its walk and day, with the verb of the mark', async () => { + const read = vi.fn(async () => PROGRESS); + const loaded = await loadFor('set_city_walk_visit', ARGS, read); + expect(read).toHaveBeenCalledWith('get_city_walk_progress', { placeId: 61, dayId: 31 }); + const preview = describeToolCall('set_city_walk_visit', 'tc-1', ARGS, ctx, loaded); + expect(assistantChangePreviewSchema.safeParse(preview).success).toBe(true); + expect(preview).toMatchObject({ subject: 'Castle', verbKey: 'markSkipped', dayId: 31, placeIds: [61] }); + expect(preview.rows.map((row) => row.labelKey)).toEqual(['route', 'day']); + expect(ASSISTANT_WRITE_TOOLS.set_city_walk_visit).toMatchObject({ entity: 'tour' }); + // Without the walk read, the card still names the walk and says what the mark does. + const blind = describeToolCall('set_city_walk_visit', 'tc-2', { ...ARGS, status: 'visited' }, ctx, null); + expect(blind).toMatchObject({ subject: 'Old town walk', verbKey: 'markVisited' }); + expect(await loadFor('set_city_walk_visit', { stationKey: 'x' }, read)).toBeNull(); + }); + + it('ASTCARD-TOUR-051: a mark the station already has offers no way back', () => { + const same = describeToolCall( + 'set_city_walk_visit', + 'tc-3', + { ...ARGS, stationKey: 'stop-se', status: 'visited' }, + ctx, + PROGRESS, + ); + expect(same.undoable).toBe(false); + }); + + it('ASTCARD-TOUR-052: the way back sets the status before, while the station still has the one the change gave it', () => { + const undo = ASSISTANT_UNDO_MAP.set_city_walk_visit; + expect(undo.tool).toBe('set_city_walk_visit'); + expect(undo.args(ARGS, RESULT, null)).toEqual({ + placeId: 61, + dayId: 31, + stationKey: 'stop-castle', + status: 'planned', + assignmentId: 90, + }); + expect(undo.args(ARGS, { status: 'skipped' }, null)).toBeNull(); + const at = (status: string) => ({ + summary: { days: [] }, + places: { places: [] }, + reads: new Map([ + [ + readKey('get_city_walk_progress', { placeId: 61, dayId: 31 }), + { ...PROGRESS, stations: [{ key: 'stop-castle', status }] }, + ], + ]), + }); + const call = { args: ARGS, preview: null }; + expect(unchangedSinceApplied('set_city_walk_visit', RESULT, at('skipped'), call)).toBe(true); + expect(unchangedSinceApplied('set_city_walk_visit', RESULT, at('visited'), call)).toBe(false); + expect(unchangedSinceApplied('set_city_walk_visit', RESULT, at('skipped'))).toBe(false); + }); +}); diff --git a/server/tests/unit/nest/tours.mcp.test.ts b/server/tests/unit/nest/tours.mcp.test.ts index 72bdf8e0c..5ebbeef4b 100644 --- a/server/tests/unit/nest/tours.mcp.test.ts +++ b/server/tests/unit/nest/tours.mcp.test.ts @@ -10,6 +10,7 @@ import { TourWaypoints } from '../../../src/db/entities/TourWaypoints.entity'; import { Tours } from '../../../src/db/entities/Tours.entity'; import type { McpContext } from '../../../src/nest-mcp'; import { getEntry, type ClassRef } from '../../../src/nest-mcp/metadata'; +import { McpToolGuardsService } from '../../../src/nest/mcp-shared/mcp-tool-guards.service'; import { PlacesMcp } from '../../../src/nest/places/places.mcp'; import { TourLibraryMcp } from '../../../src/nest/tours/tour-library.mcp'; import { TOUR_PLAN_LINE_POINTS } from '../../../src/nest/tours/tour-plan-read'; @@ -249,6 +250,57 @@ describe('ToursMcp', () => { expect(answers).toEqual(notThere.map(() => ({ error: 'No city walk of this trip is on that day.' }))); }); + it('TOURS-MCP-033: set_city_walk_visit checks a station off on its day, says what it was, and takes a mark back', async () => { + const { member, trip, day1, walk, onDay1 } = await walkFixture(); + const at = { tripId: trip.id, placeId: walk.id, dayId: day1.id }; + const marked = read( + await mcp.setCityWalkVisit({ ...at, stationKey: 'stop-park', status: 'visited' }, ctx(member.id)), + ); + expect(marked).toMatchObject({ + station_key: 'stop-park', + name: null, + previous_status: 'planned', + status: 'visited', + walk: { assignment_id: onDay1.id }, + }); + expect(marked.walk.stations.map((station: { status: string }) => station.status)).toEqual([ + 'visited', + 'skipped', + 'visited', + ]); + const back = read( + await mcp.setCityWalkVisit({ ...at, stationKey: 'stop-market', status: 'planned' }, ctx(member.id)), + ); + expect(back).toMatchObject({ name: null, previous_status: 'skipped', status: 'planned' }); + const orm = await createTestOrm(db); + expect(await countRows(orm, TourStationVisits, { station_key: 'stop-market' })).toBe(0); + await orm.close(); + }); + + it('TOURS-MCP-034: set_city_walk_visit refuses an outsider, a member without day_edit, an unknown station and a day without the walk', async () => { + const { user, outsider, trip, day1, day2, walk } = await walkFixture(); + const at = { tripId: trip.id, placeId: walk.id, dayId: day1.id, status: 'visited' as const }; + expect(read(await mcp.setCityWalkVisit({ ...at, stationKey: 'stop-park' }, ctx(outsider.id)))).toEqual({ + error: 'Trip not found or access denied.', + }); + const guards = t.get(McpToolGuardsService); + const denied = vi.spyOn(guards, 'hasTripPermission').mockResolvedValueOnce(false); + try { + expect(read(await mcp.setCityWalkVisit({ ...at, stationKey: 'stop-park' }, ctx(user.id)))).toEqual({ + error: 'You do not have permission to perform this action on this trip.', + }); + expect(denied).toHaveBeenCalledWith('day_edit', trip.id, user.id); + } finally { + denied.mockRestore(); + } + expect(read(await mcp.setCityWalkVisit({ ...at, stationKey: 'stop-nowhere' }, ctx(user.id))).error).toMatch( + /no station with that key/, + ); + expect(read(await mcp.setCityWalkVisit({ ...at, dayId: day2.id, stationKey: 'stop-park' }, ctx(user.id)))).toEqual({ + error: 'No city walk of this trip is on that day.', + }); + }); + it('TOURS-MCP-010: a failure other than a missing walk is not passed off as no walk', async () => { const { user, trip, day1, walk } = await walkFixture(); const failure = new Error('database gone'); diff --git a/shared/scripts/i18n-parity.mjs b/shared/scripts/i18n-parity.mjs index d86814ed5..bdc20a12f 100644 --- a/shared/scripts/i18n-parity.mjs +++ b/shared/scripts/i18n-parity.mjs @@ -86,7 +86,6 @@ function checkPluralForms(locale, keys, groups) { * ungrouped count string fails the check, so the list cannot go stale. */ export const NOT_PLURAL = [ - { key: 'assistant.place.addToShort', because: 'the number of the day a found place would go on ("+ Day 3")' }, { key: 'assistant.card.tour.days', because: 'the numbers of the days a tour lies on ("Days 2, 4")' }, { key: 'collab.polls.optionPlaceholder', because: 'the position of a poll option ("Option 3")' }, { key: 'collections.importOnDay', because: 'the number of the day a place is planned on' }, diff --git a/shared/src/assistant/cards/tour.contract.ts b/shared/src/assistant/cards/tour.contract.ts index 90b3d3d30..683f845ca 100644 --- a/shared/src/assistant/cards/tour.contract.ts +++ b/shared/src/assistant/cards/tour.contract.ts @@ -13,6 +13,7 @@ export * from './tour-route.card'; */ export const TOUR_CARD_SCHEMAS = [tourRouteCardSchema, tourListCardSchema] as const; /** addTour, updateTour and importTour sit in the core list already. */ -export const TOUR_VERB_KEYS = [] as const; +/** A station of a city walk checked off on its day: visited, skipped, or the mark taken back. */ +export const TOUR_VERB_KEYS = ['markVisited', 'markSkipped', 'markPlanned'] as const; /** 'tour' is a core entity already. */ export const TOUR_APPLIED_ENTITIES = [] as const; diff --git a/shared/src/i18n/ar/assistant.ts b/shared/src/i18n/ar/assistant.ts index 0e5167f23..9530212b6 100644 --- a/shared/src/i18n/ar/assistant.ts +++ b/shared/src/i18n/ar/assistant.ts @@ -298,9 +298,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllFailed.few': 'تعذّر التراجع عن {count} تغييرات', 'assistant.change.undoAllFailed.many': 'تعذّر التراجع عن {count} تغييرًا', 'assistant.place.strip': 'الأماكن التي وُجدت', - 'assistant.place.addTo': 'أضف إلى {day}', - 'assistant.place.addToShort': '+ اليوم {n}', - 'assistant.place.pickDay': 'اختر يومًا', + 'assistant.place.add': 'أضف إلى الرحلة', 'assistant.place.suggested': 'تم الاقتراح', 'assistant.place.inTrip': 'موجود في رحلتك بالفعل', 'assistant.place.showOnMap': 'على الخريطة', @@ -440,6 +438,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'يوم', 'assistant.memory.title': 'ملاحظات الرحلة', 'assistant.memory.edit': 'تعديل ملاحظات الرحلة', + 'assistant.memory.kept': 'تم الحفظ لهذه الرحلة:', 'assistant.memory.sub': 'يراعيها المساعد في كل اقتراح لهذه الرحلة.', 'assistant.memory.quick': 'رغبات شائعة', 'assistant.memory.notes': 'ملاحظاتك', @@ -484,6 +483,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'إزالة', 'assistant.change.verb.addTour': 'إضافة جولة', 'assistant.change.verb.updateTour': 'تعديل الجولة', + 'assistant.change.verb.markVisited': 'وضع علامة كمُزار', + 'assistant.change.verb.markSkipped': 'تخطي المحطة', + 'assistant.change.verb.markPlanned': 'إلغاء العلامة', 'assistant.change.verb.importTour': 'إضافة جولة من ملف', 'assistant.change.label.field': 'القيمة', 'assistant.change.label.difficulty': 'أقصى مستوى لصعوبة الدرب', diff --git a/shared/src/i18n/az/assistant.ts b/shared/src/i18n/az/assistant.ts index 9ae7b4b77..cfc765ef1 100644 --- a/shared/src/i18n/az/assistant.ts +++ b/shared/src/i18n/az/assistant.ts @@ -274,9 +274,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllFailed': '{count} dəyişikliyi geri almaq mümkün olmadı', 'assistant.change.undoAllFailed.one': '{count} dəyişikliyi geri almaq mümkün olmadı', 'assistant.place.strip': 'Tapılan yerlər', - 'assistant.place.addTo': '{day} gününə əlavə et', - 'assistant.place.addToShort': '+ {n}. gün', - 'assistant.place.pickDay': 'Gün seçin', + 'assistant.place.add': 'Səyahətə əlavə et', 'assistant.place.suggested': 'Təklif edildi', 'assistant.place.inTrip': 'Artıq səyahətinizdədir', 'assistant.place.showOnMap': 'Xəritədə', @@ -377,6 +375,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'Gün', 'assistant.memory.title': 'Səyahət qeydləri', 'assistant.memory.edit': 'Səyahət qeydlərini redaktə et', + 'assistant.memory.kept': 'Bu səyahət üçün qeyd edildi:', 'assistant.memory.sub': 'Assistent bu səyahətdə hər təklifdə bunları nəzərə alır.', 'assistant.memory.quick': 'Tez-tez istəklər', 'assistant.memory.notes': 'Qeydlərin', @@ -407,6 +406,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Sil', 'assistant.change.verb.addTour': 'Tur əlavə et', 'assistant.change.verb.updateTour': 'Turu dəyiş', + 'assistant.change.verb.markVisited': 'Ziyarət edilmiş kimi qeyd et', + 'assistant.change.verb.markSkipped': 'Dayanacağı keç', + 'assistant.change.verb.markPlanned': 'Qeydi geri al', 'assistant.change.verb.importTour': 'Fayldan tur əlavə et', 'assistant.change.label.field': 'Dəyər', 'assistant.change.label.difficulty': 'Maksimum cığır çətinliyi', diff --git a/shared/src/i18n/br/assistant.ts b/shared/src/i18n/br/assistant.ts index 4b926d6c6..f050977d3 100644 --- a/shared/src/i18n/br/assistant.ts +++ b/shared/src/i18n/br/assistant.ts @@ -273,9 +273,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllFailed': 'Não foi possível desfazer {count} mudanças', 'assistant.change.undoAllFailed.one': 'Não foi possível desfazer {count} mudança', 'assistant.place.strip': 'Lugares encontrados', - 'assistant.place.addTo': 'Adicionar a {day}', - 'assistant.place.addToShort': '+ Dia {n}', - 'assistant.place.pickDay': 'Escolher dia', + 'assistant.place.add': 'Adicionar à viagem', 'assistant.place.suggested': 'Sugerido', 'assistant.place.inTrip': 'Já está na sua viagem', 'assistant.place.showOnMap': 'No mapa', @@ -377,6 +375,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'Dia', 'assistant.memory.title': 'Notas da viagem', 'assistant.memory.edit': 'Editar notas da viagem', + 'assistant.memory.kept': 'Anotado para esta viagem:', 'assistant.memory.sub': 'O assistente leva isso em conta em todas as sugestões para esta viagem.', 'assistant.memory.quick': 'Pedidos comuns', 'assistant.memory.notes': 'Suas notas', @@ -407,6 +406,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Remover', 'assistant.change.verb.addTour': 'Adicionar passeio', 'assistant.change.verb.updateTour': 'Alterar passeio', + 'assistant.change.verb.markVisited': 'Marcar como visitado', + 'assistant.change.verb.markSkipped': 'Pular parada', + 'assistant.change.verb.markPlanned': 'Desfazer marcação', 'assistant.change.verb.importTour': 'Adicionar passeio a partir de arquivo', 'assistant.change.label.field': 'Valor', 'assistant.change.label.difficulty': 'Dificuldade máxima da trilha', diff --git a/shared/src/i18n/ca/assistant.ts b/shared/src/i18n/ca/assistant.ts index 9c75bccc6..07f957837 100644 --- a/shared/src/i18n/ca/assistant.ts +++ b/shared/src/i18n/ca/assistant.ts @@ -276,9 +276,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllFailed': 'No s’han pogut desfer {count} canvis', 'assistant.change.undoAllFailed.one': 'No s’ha pogut desfer {count} canvi', 'assistant.place.strip': 'Llocs trobats', - 'assistant.place.addTo': 'Afegeix a {day}', - 'assistant.place.addToShort': '+ Dia {n}', - 'assistant.place.pickDay': 'Tria un dia', + 'assistant.place.add': 'Afegeix al viatge', 'assistant.place.suggested': 'Proposat', 'assistant.place.inTrip': 'Ja és al teu viatge', 'assistant.place.showOnMap': 'Al mapa', @@ -380,6 +378,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'Dia', 'assistant.memory.title': 'Notes del viatge', 'assistant.memory.edit': 'Edita les notes del viatge', + 'assistant.memory.kept': 'Anotat per a aquest viatge:', 'assistant.memory.sub': "L'assistent les té en compte en cada proposta d'aquest viatge.", 'assistant.memory.quick': 'Desitjos habituals', 'assistant.memory.notes': 'Les teves notes', @@ -410,6 +409,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Elimina', 'assistant.change.verb.addTour': 'Afegeix una excursió', 'assistant.change.verb.updateTour': 'Canvia l’excursió', + 'assistant.change.verb.markVisited': 'Marca com a visitat', + 'assistant.change.verb.markSkipped': 'Salta la parada', + 'assistant.change.verb.markPlanned': 'Desfés la marca', 'assistant.change.verb.importTour': 'Afegeix una excursió des d’un fitxer', 'assistant.change.label.field': 'Valor', 'assistant.change.label.difficulty': 'Dificultat màxima del sender', diff --git a/shared/src/i18n/cs/assistant.ts b/shared/src/i18n/cs/assistant.ts index 3172acd25..1f85c650b 100644 --- a/shared/src/i18n/cs/assistant.ts +++ b/shared/src/i18n/cs/assistant.ts @@ -279,9 +279,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllFailed.one': '{count} změnu nešlo vrátit', 'assistant.change.undoAllFailed.few': '{count} změny nešlo vrátit', 'assistant.place.strip': 'Nalezená místa', - 'assistant.place.addTo': 'Přidat do {day}', - 'assistant.place.addToShort': '+ Den {n}', - 'assistant.place.pickDay': 'Vybrat den', + 'assistant.place.add': 'Přidat do cesty', 'assistant.place.suggested': 'Navrženo', 'assistant.place.inTrip': 'Už je ve tvém výletu', 'assistant.place.showOnMap': 'Na mapě', @@ -392,6 +390,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'Den', 'assistant.memory.title': 'Poznámky k cestě', 'assistant.memory.edit': 'Upravit poznámky k cestě', + 'assistant.memory.kept': 'Zapamatováno pro tuto cestu:', 'assistant.memory.sub': 'Asistent na ně myslí u každého návrhu pro tuto cestu.', 'assistant.memory.quick': 'Častá přání', 'assistant.memory.notes': 'Tvoje poznámky', @@ -425,6 +424,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Odebrat', 'assistant.change.verb.addTour': 'Přidat výlet', 'assistant.change.verb.updateTour': 'Změnit výlet', + 'assistant.change.verb.markVisited': 'Označit jako navštívené', + 'assistant.change.verb.markSkipped': 'Přeskočit zastávku', + 'assistant.change.verb.markPlanned': 'Zrušit označení', 'assistant.change.verb.importTour': 'Přidat výlet ze souboru', 'assistant.change.label.field': 'Hodnota', 'assistant.change.label.difficulty': 'Maximální obtížnost stezky', diff --git a/shared/src/i18n/de/assistant.ts b/shared/src/i18n/de/assistant.ts index 946c0701c..3e9408dfc 100644 --- a/shared/src/i18n/de/assistant.ts +++ b/shared/src/i18n/de/assistant.ts @@ -273,9 +273,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllFailed': '{count} Änderungen ließen sich nicht rückgängig machen', 'assistant.change.undoAllFailed.one': '{count} Änderung ließ sich nicht rückgängig machen', 'assistant.place.strip': 'Gefundene Orte', - 'assistant.place.addTo': 'Zu {day} hinzufügen', - 'assistant.place.addToShort': '+ Tag {n}', - 'assistant.place.pickDay': 'Tag wählen', + 'assistant.place.add': 'Zur Reise hinzufügen', 'assistant.place.suggested': 'Vorgeschlagen', 'assistant.place.inTrip': 'Schon im Trip', 'assistant.place.showOnMap': 'Auf Karte', @@ -378,6 +376,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'Tag', 'assistant.memory.title': 'Merkzettel', 'assistant.memory.edit': 'Merkzettel bearbeiten', + 'assistant.memory.kept': 'Gemerkt:', 'assistant.memory.sub': 'Der Assistent beachtet das bei jedem Vorschlag für diesen Trip.', 'assistant.memory.quick': 'Häufige Wünsche', 'assistant.memory.notes': 'Deine Notizen', @@ -407,6 +406,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Entfernen', 'assistant.change.verb.addTour': 'Tour hinzufügen', 'assistant.change.verb.updateTour': 'Tour ändern', + 'assistant.change.verb.markVisited': 'Als besucht abhaken', + 'assistant.change.verb.markSkipped': 'Stopp überspringen', + 'assistant.change.verb.markPlanned': 'Haken zurücknehmen', 'assistant.change.verb.importTour': 'Tour aus Datei hinzufügen', 'assistant.change.label.field': 'Wert', 'assistant.change.label.difficulty': 'Maximale Wegschwierigkeit', diff --git a/shared/src/i18n/en/assistant.ts b/shared/src/i18n/en/assistant.ts index b84eaf102..6885bbb0f 100644 --- a/shared/src/i18n/en/assistant.ts +++ b/shared/src/i18n/en/assistant.ts @@ -269,9 +269,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllFailed': '{count} changes could not be undone', 'assistant.change.undoAllFailed.one': '{count} change could not be undone', 'assistant.place.strip': 'Places found', - 'assistant.place.addTo': 'Add to {day}', - 'assistant.place.addToShort': '+ Day {n}', - 'assistant.place.pickDay': 'Pick a day', + 'assistant.place.add': 'Add to trip', 'assistant.place.suggested': 'Suggested', 'assistant.place.inTrip': 'Already in your trip', 'assistant.place.showOnMap': 'On map', @@ -372,6 +370,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'Day', 'assistant.memory.title': 'Trip notes', 'assistant.memory.edit': 'Edit trip notes', + 'assistant.memory.kept': 'Noted for this trip:', 'assistant.memory.sub': 'The assistant keeps these in mind for every suggestion on this trip.', 'assistant.memory.quick': 'Common wishes', 'assistant.memory.notes': 'Your notes', @@ -401,6 +400,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Remove', 'assistant.change.verb.addTour': 'Add tour', 'assistant.change.verb.updateTour': 'Change tour', + 'assistant.change.verb.markVisited': 'Mark as visited', + 'assistant.change.verb.markSkipped': 'Skip stop', + 'assistant.change.verb.markPlanned': 'Take mark back', 'assistant.change.verb.importTour': 'Add tour from file', 'assistant.change.label.field': 'Value', 'assistant.change.label.difficulty': 'Maximum trail difficulty', diff --git a/shared/src/i18n/es/assistant.ts b/shared/src/i18n/es/assistant.ts index 6a940a3df..867b9239f 100644 --- a/shared/src/i18n/es/assistant.ts +++ b/shared/src/i18n/es/assistant.ts @@ -272,9 +272,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllFailed': 'No se pudieron deshacer {count} cambios', 'assistant.change.undoAllFailed.one': 'No se pudo deshacer {count} cambio', 'assistant.place.strip': 'Lugares encontrados', - 'assistant.place.addTo': 'Añadir a {day}', - 'assistant.place.addToShort': '+ Día {n}', - 'assistant.place.pickDay': 'Elegir día', + 'assistant.place.add': 'Añadir al viaje', 'assistant.place.suggested': 'Propuesto', 'assistant.place.inTrip': 'Ya está en tu viaje', 'assistant.place.showOnMap': 'En el mapa', @@ -375,6 +373,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'Día', 'assistant.memory.title': 'Notas del viaje', 'assistant.memory.edit': 'Editar notas del viaje', + 'assistant.memory.kept': 'Anotado para este viaje:', 'assistant.memory.sub': 'El asistente las tiene en cuenta en cada sugerencia de este viaje.', 'assistant.memory.quick': 'Deseos habituales', 'assistant.memory.notes': 'Tus notas', @@ -405,6 +404,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Quitar', 'assistant.change.verb.addTour': 'Añadir excursión', 'assistant.change.verb.updateTour': 'Cambiar excursión', + 'assistant.change.verb.markVisited': 'Marcar como visitado', + 'assistant.change.verb.markSkipped': 'Saltar parada', + 'assistant.change.verb.markPlanned': 'Quitar marca', 'assistant.change.verb.importTour': 'Añadir excursión desde archivo', 'assistant.change.label.field': 'Valor', 'assistant.change.label.difficulty': 'Dificultad máxima del sendero', diff --git a/shared/src/i18n/et/assistant.ts b/shared/src/i18n/et/assistant.ts index 0ce79bd4f..1f51d02e7 100644 --- a/shared/src/i18n/et/assistant.ts +++ b/shared/src/i18n/et/assistant.ts @@ -273,9 +273,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllFailed': '{count} muudatust ei õnnestunud tagasi võtta', 'assistant.change.undoAllFailed.one': '{count} muudatust ei õnnestunud tagasi võtta', 'assistant.place.strip': 'Leitud kohad', - 'assistant.place.addTo': 'Lisa päevale {day}', - 'assistant.place.addToShort': '+ Päev {n}', - 'assistant.place.pickDay': 'Vali päev', + 'assistant.place.add': 'Lisa reisile', 'assistant.place.suggested': 'Soovitatud', 'assistant.place.inTrip': 'Juba sinu reisil', 'assistant.place.showOnMap': 'Kaardil', @@ -377,6 +375,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'Päev', 'assistant.memory.title': 'Reisimärkmed', 'assistant.memory.edit': 'Muuda reisimärkmeid', + 'assistant.memory.kept': 'Selle reisi jaoks meelde jäetud:', 'assistant.memory.sub': 'Assistent arvestab neid selle reisi iga ettepaneku juures.', 'assistant.memory.quick': 'Sagedased soovid', 'assistant.memory.notes': 'Sinu märkmed', @@ -406,6 +405,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Eemalda', 'assistant.change.verb.addTour': 'Lisa retk', 'assistant.change.verb.updateTour': 'Muuda retke', + 'assistant.change.verb.markVisited': 'Märgi külastatuks', + 'assistant.change.verb.markSkipped': 'Jäta peatus vahele', + 'assistant.change.verb.markPlanned': 'Võta märge tagasi', 'assistant.change.verb.importTour': 'Lisa retk failist', 'assistant.change.label.field': 'Väärtus', 'assistant.change.label.difficulty': 'Raja suurim raskusaste', diff --git a/shared/src/i18n/fr/assistant.ts b/shared/src/i18n/fr/assistant.ts index f57a6fc2d..5cfbc15d9 100644 --- a/shared/src/i18n/fr/assistant.ts +++ b/shared/src/i18n/fr/assistant.ts @@ -277,9 +277,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllFailed': '{count} modifications n’ont pas pu être annulées', 'assistant.change.undoAllFailed.one': '{count} modification n’a pas pu être annulée', 'assistant.place.strip': 'Lieux trouvés', - 'assistant.place.addTo': 'Ajouter à {day}', - 'assistant.place.addToShort': '+ Jour {n}', - 'assistant.place.pickDay': 'Choisir un jour', + 'assistant.place.add': 'Ajouter au voyage', 'assistant.place.suggested': 'Proposé', 'assistant.place.inTrip': 'Déjà dans votre voyage', 'assistant.place.showOnMap': 'Sur la carte', @@ -382,6 +380,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'Jour', 'assistant.memory.title': 'Pense-bête', 'assistant.memory.edit': 'Modifier le pense-bête', + 'assistant.memory.kept': 'Noté pour ce voyage :', 'assistant.memory.sub': "L'assistant en tient compte pour chaque suggestion de ce voyage.", 'assistant.memory.quick': 'Souhaits fréquents', 'assistant.memory.notes': 'Vos notes', @@ -412,6 +411,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Supprimer', 'assistant.change.verb.addTour': 'Ajouter une sortie', 'assistant.change.verb.updateTour': 'Modifier la sortie', + 'assistant.change.verb.markVisited': 'Marquer comme visité', + 'assistant.change.verb.markSkipped': 'Sauter l’étape', + 'assistant.change.verb.markPlanned': 'Retirer la marque', 'assistant.change.verb.importTour': 'Ajouter une sortie depuis un fichier', 'assistant.change.label.field': 'Valeur', 'assistant.change.label.difficulty': 'Difficulté maximale du sentier', diff --git a/shared/src/i18n/gr/assistant.ts b/shared/src/i18n/gr/assistant.ts index 3b3a5ea58..137a040cb 100644 --- a/shared/src/i18n/gr/assistant.ts +++ b/shared/src/i18n/gr/assistant.ts @@ -278,9 +278,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllFailed': 'Δεν ήταν δυνατή η αναίρεση {count} αλλαγών', 'assistant.change.undoAllFailed.one': 'Δεν ήταν δυνατή η αναίρεση {count} αλλαγής', 'assistant.place.strip': 'Μέρη που βρέθηκαν', - 'assistant.place.addTo': 'Προσθήκη σε {day}', - 'assistant.place.addToShort': '+ Ημέρα {n}', - 'assistant.place.pickDay': 'Επιλογή ημέρας', + 'assistant.place.add': 'Προσθήκη στο ταξίδι', 'assistant.place.suggested': 'Προτάθηκε', 'assistant.place.inTrip': 'Ήδη στο ταξίδι σας', 'assistant.place.showOnMap': 'Στον χάρτη', @@ -382,6 +380,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'Μέρα', 'assistant.memory.title': 'Σημειώσεις ταξιδιού', 'assistant.memory.edit': 'Επεξεργασία σημειώσεων', + 'assistant.memory.kept': 'Σημειώθηκε για αυτό το ταξίδι:', 'assistant.memory.sub': 'Ο βοηθός τις λαμβάνει υπόψη σε κάθε πρόταση για αυτό το ταξίδι.', 'assistant.memory.quick': 'Συχνές επιθυμίες', 'assistant.memory.notes': 'Οι σημειώσεις σου', @@ -411,6 +410,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Αφαίρεση', 'assistant.change.verb.addTour': 'Προσθήκη εκδρομής', 'assistant.change.verb.updateTour': 'Αλλαγή εκδρομής', + 'assistant.change.verb.markVisited': 'Σήμανση ως επισκέψιμο', + 'assistant.change.verb.markSkipped': 'Παράλειψη στάσης', + 'assistant.change.verb.markPlanned': 'Αναίρεση σήμανσης', 'assistant.change.verb.importTour': 'Προσθήκη εκδρομής από αρχείο', 'assistant.change.label.field': 'Τιμή', 'assistant.change.label.difficulty': 'Μέγιστη δυσκολία μονοπατιού', diff --git a/shared/src/i18n/hu/assistant.ts b/shared/src/i18n/hu/assistant.ts index aa94a9bf7..3d83405e8 100644 --- a/shared/src/i18n/hu/assistant.ts +++ b/shared/src/i18n/hu/assistant.ts @@ -278,9 +278,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllFailed': '{count} módosítást nem sikerült visszavonni', 'assistant.change.undoAllFailed.one': '{count} módosítást nem sikerült visszavonni', 'assistant.place.strip': 'Talált helyek', - 'assistant.place.addTo': 'Hozzáadás ehhez: {day}', - 'assistant.place.addToShort': '+ {n}. nap', - 'assistant.place.pickDay': 'Nap választása', + 'assistant.place.add': 'Hozzáadás az úthoz', 'assistant.place.suggested': 'Javasolva', 'assistant.place.inTrip': 'Már az utadban van', 'assistant.place.showOnMap': 'Térképen', @@ -383,6 +381,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'Nap', 'assistant.memory.title': 'Útijegyzet', 'assistant.memory.edit': 'Útijegyzet szerkesztése', + 'assistant.memory.kept': 'Megjegyezve ehhez az úthoz:', 'assistant.memory.sub': 'Az asszisztens minden javaslatnál figyelembe veszi ezt az utazáson.', 'assistant.memory.quick': 'Gyakori kívánságok', 'assistant.memory.notes': 'Jegyzeteid', @@ -412,6 +411,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Eltávolítás', 'assistant.change.verb.addTour': 'Túra hozzáadása', 'assistant.change.verb.updateTour': 'Túra módosítása', + 'assistant.change.verb.markVisited': 'Megjelölés meglátogatottként', + 'assistant.change.verb.markSkipped': 'Megálló kihagyása', + 'assistant.change.verb.markPlanned': 'Jelölés visszavonása', 'assistant.change.verb.importTour': 'Túra hozzáadása fájlból', 'assistant.change.label.field': 'Érték', 'assistant.change.label.difficulty': 'Ösvény legnagyobb nehézsége', diff --git a/shared/src/i18n/id/assistant.ts b/shared/src/i18n/id/assistant.ts index 912a28bb6..36cbb69da 100644 --- a/shared/src/i18n/id/assistant.ts +++ b/shared/src/i18n/id/assistant.ts @@ -265,9 +265,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllPartial': '{done} diurungkan, {stale} sudah berubah sejak itu', 'assistant.change.undoAllFailed': '{count} perubahan tidak bisa diurungkan', 'assistant.place.strip': 'Tempat yang ditemukan', - 'assistant.place.addTo': 'Tambahkan ke {day}', - 'assistant.place.addToShort': '+ Hari {n}', - 'assistant.place.pickDay': 'Pilih hari', + 'assistant.place.add': 'Tambah ke perjalanan', 'assistant.place.suggested': 'Diusulkan', 'assistant.place.inTrip': 'Sudah ada di perjalananmu', 'assistant.place.showOnMap': 'Di peta', @@ -361,6 +359,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'Hari', 'assistant.memory.title': 'Catatan perjalanan', 'assistant.memory.edit': 'Ubah catatan perjalanan', + 'assistant.memory.kept': 'Dicatat untuk perjalanan ini:', 'assistant.memory.sub': 'Asisten memperhatikannya di setiap saran untuk perjalanan ini.', 'assistant.memory.quick': 'Keinginan umum', 'assistant.memory.notes': 'Catatanmu', @@ -386,6 +385,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Hapus', 'assistant.change.verb.addTour': 'Tambah tur', 'assistant.change.verb.updateTour': 'Ubah tur', + 'assistant.change.verb.markVisited': 'Tandai sudah dikunjungi', + 'assistant.change.verb.markSkipped': 'Lewati perhentian', + 'assistant.change.verb.markPlanned': 'Batalkan tanda', 'assistant.change.verb.importTour': 'Tambah tur dari file', 'assistant.change.label.field': 'Nilai', 'assistant.change.label.difficulty': 'Tingkat kesulitan jalur maksimum', diff --git a/shared/src/i18n/it/assistant.ts b/shared/src/i18n/it/assistant.ts index f8625ea3a..30d9ed262 100644 --- a/shared/src/i18n/it/assistant.ts +++ b/shared/src/i18n/it/assistant.ts @@ -276,9 +276,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllFailed': 'Non è stato possibile annullare {count} modifiche', 'assistant.change.undoAllFailed.one': 'Non è stato possibile annullare {count} modifica', 'assistant.place.strip': 'Luoghi trovati', - 'assistant.place.addTo': 'Aggiungi a {day}', - 'assistant.place.addToShort': '+ Giorno {n}', - 'assistant.place.pickDay': 'Scegli un giorno', + 'assistant.place.add': 'Aggiungi al viaggio', 'assistant.place.suggested': 'Proposto', 'assistant.place.inTrip': 'Già nel tuo viaggio', 'assistant.place.showOnMap': 'Sulla mappa', @@ -383,6 +381,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'Giorno', 'assistant.memory.title': 'Promemoria', 'assistant.memory.edit': 'Modifica il promemoria', + 'assistant.memory.kept': 'Annotato per questo viaggio:', 'assistant.memory.sub': "L'assistente ne tiene conto in ogni proposta per questo viaggio.", 'assistant.memory.quick': 'Desideri frequenti', 'assistant.memory.notes': 'Le tue note', @@ -413,6 +412,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Rimuovi', 'assistant.change.verb.addTour': 'Aggiungi escursione', 'assistant.change.verb.updateTour': 'Modifica escursione', + 'assistant.change.verb.markVisited': 'Segna come visitato', + 'assistant.change.verb.markSkipped': 'Salta tappa', + 'assistant.change.verb.markPlanned': 'Togli il segno', 'assistant.change.verb.importTour': 'Aggiungi escursione da file', 'assistant.change.label.field': 'Valore', 'assistant.change.label.difficulty': 'Difficoltà massima del sentiero', diff --git a/shared/src/i18n/ja/assistant.ts b/shared/src/i18n/ja/assistant.ts index faaa569aa..685923f96 100644 --- a/shared/src/i18n/ja/assistant.ts +++ b/shared/src/i18n/ja/assistant.ts @@ -264,9 +264,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllPartial': '{done}件を元に戻し、{stale}件はその後変更されています', 'assistant.change.undoAllFailed': '{count}件の変更を元に戻せませんでした', 'assistant.place.strip': '見つかった場所', - 'assistant.place.addTo': '{day}に追加', - 'assistant.place.addToShort': '+ {n}日目', - 'assistant.place.pickDay': '日を選ぶ', + 'assistant.place.add': '旅行に追加', 'assistant.place.suggested': '提案済み', 'assistant.place.inTrip': 'すでに旅行に含まれています', 'assistant.place.showOnMap': '地図で表示', @@ -357,6 +355,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': '日', 'assistant.memory.title': '旅のメモ', 'assistant.memory.edit': '旅のメモを編集', + 'assistant.memory.kept': 'この旅行のメモに追加:', 'assistant.memory.sub': 'アシスタントはこの旅のすべての提案でこれを考慮します。', 'assistant.memory.quick': 'よくある希望', 'assistant.memory.notes': 'あなたのメモ', @@ -381,6 +380,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': '削除', 'assistant.change.verb.addTour': 'ツアーを追加', 'assistant.change.verb.updateTour': 'ツアーを変更', + 'assistant.change.verb.markVisited': '訪問済みにする', + 'assistant.change.verb.markSkipped': 'スポットをスキップ', + 'assistant.change.verb.markPlanned': 'チェックを外す', 'assistant.change.verb.importTour': 'ファイルからツアーを追加', 'assistant.change.label.field': '値', 'assistant.change.label.difficulty': '登山道の難易度上限', diff --git a/shared/src/i18n/ko/assistant.ts b/shared/src/i18n/ko/assistant.ts index 1a8f926cb..579cfeece 100644 --- a/shared/src/i18n/ko/assistant.ts +++ b/shared/src/i18n/ko/assistant.ts @@ -261,9 +261,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllPartial': '{done}개 되돌림, {stale}개는 그사이 변경됨', 'assistant.change.undoAllFailed': '변경 {count}개를 되돌리지 못했습니다', 'assistant.place.strip': '찾은 장소', - 'assistant.place.addTo': '{day}에 추가', - 'assistant.place.addToShort': '+ {n}일차', - 'assistant.place.pickDay': '날짜 선택', + 'assistant.place.add': '여행에 추가', 'assistant.place.suggested': '제안됨', 'assistant.place.inTrip': '이미 여행에 있음', 'assistant.place.showOnMap': '지도에서', @@ -353,6 +351,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': '일', 'assistant.memory.title': '여행 메모', 'assistant.memory.edit': '여행 메모 편집', + 'assistant.memory.kept': '이 여행에 기억함:', 'assistant.memory.sub': '어시스턴트가 이 여행의 모든 제안에서 이를 반영합니다.', 'assistant.memory.quick': '자주 찾는 요청', 'assistant.memory.notes': '내 메모', @@ -377,6 +376,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': '삭제', 'assistant.change.verb.addTour': '투어 추가', 'assistant.change.verb.updateTour': '투어 변경', + 'assistant.change.verb.markVisited': '방문함으로 표시', + 'assistant.change.verb.markSkipped': '정류장 건너뛰기', + 'assistant.change.verb.markPlanned': '표시 취소', 'assistant.change.verb.importTour': '파일에서 투어 추가', 'assistant.change.label.field': '값', 'assistant.change.label.difficulty': '탐방로 최대 난이도', diff --git a/shared/src/i18n/nl/assistant.ts b/shared/src/i18n/nl/assistant.ts index d575c9769..a5c40961f 100644 --- a/shared/src/i18n/nl/assistant.ts +++ b/shared/src/i18n/nl/assistant.ts @@ -274,9 +274,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllFailed': '{count} wijzigingen konden niet ongedaan worden gemaakt', 'assistant.change.undoAllFailed.one': '{count} wijziging kon niet ongedaan worden gemaakt', 'assistant.place.strip': 'Gevonden plekken', - 'assistant.place.addTo': 'Toevoegen aan {day}', - 'assistant.place.addToShort': '+ Dag {n}', - 'assistant.place.pickDay': 'Kies een dag', + 'assistant.place.add': 'Aan reis toevoegen', 'assistant.place.suggested': 'Voorgesteld', 'assistant.place.inTrip': 'Al in je reis', 'assistant.place.showOnMap': 'Op de kaart', @@ -379,6 +377,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'Dag', 'assistant.memory.title': 'Reisnotities', 'assistant.memory.edit': 'Reisnotities bewerken', + 'assistant.memory.kept': 'Onthouden voor deze reis:', 'assistant.memory.sub': 'De assistent houdt hier bij elk voorstel voor deze reis rekening mee.', 'assistant.memory.quick': 'Veelgevraagde wensen', 'assistant.memory.notes': 'Je notities', @@ -408,6 +407,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Verwijderen', 'assistant.change.verb.addTour': 'Tocht toevoegen', 'assistant.change.verb.updateTour': 'Tocht wijzigen', + 'assistant.change.verb.markVisited': 'Als bezocht markeren', + 'assistant.change.verb.markSkipped': 'Stop overslaan', + 'assistant.change.verb.markPlanned': 'Markering terugnemen', 'assistant.change.verb.importTour': 'Tocht uit bestand toevoegen', 'assistant.change.label.field': 'Waarde', 'assistant.change.label.difficulty': 'Maximale moeilijkheid van het pad', diff --git a/shared/src/i18n/pl/assistant.ts b/shared/src/i18n/pl/assistant.ts index 2de12001a..c2b950c10 100644 --- a/shared/src/i18n/pl/assistant.ts +++ b/shared/src/i18n/pl/assistant.ts @@ -289,9 +289,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllFailed.few': 'Nie udało się cofnąć {count} zmian', 'assistant.change.undoAllFailed.many': 'Nie udało się cofnąć {count} zmian', 'assistant.place.strip': 'Znalezione miejsca', - 'assistant.place.addTo': 'Dodaj do: {day}', - 'assistant.place.addToShort': '+ Dzień {n}', - 'assistant.place.pickDay': 'Wybierz dzień', + 'assistant.place.add': 'Dodaj do podróży', 'assistant.place.suggested': 'Zaproponowano', 'assistant.place.inTrip': 'Już w twojej podróży', 'assistant.place.showOnMap': 'Na mapie', @@ -413,6 +411,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'Dzień', 'assistant.memory.title': 'Notatki podróży', 'assistant.memory.edit': 'Edytuj notatki podróży', + 'assistant.memory.kept': 'Zapamiętane dla tej podróży:', 'assistant.memory.sub': 'Asystent uwzględnia je w każdej propozycji dla tej podróży.', 'assistant.memory.quick': 'Częste życzenia', 'assistant.memory.notes': 'Twoje notatki', @@ -450,6 +449,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Usuń', 'assistant.change.verb.addTour': 'Dodaj wycieczkę', 'assistant.change.verb.updateTour': 'Zmień wycieczkę', + 'assistant.change.verb.markVisited': 'Oznacz jako odwiedzone', + 'assistant.change.verb.markSkipped': 'Pomiń przystanek', + 'assistant.change.verb.markPlanned': 'Cofnij oznaczenie', 'assistant.change.verb.importTour': 'Dodaj wycieczkę z pliku', 'assistant.change.label.field': 'Wartość', 'assistant.change.label.difficulty': 'Maksymalna trudność szlaku', diff --git a/shared/src/i18n/ru/assistant.ts b/shared/src/i18n/ru/assistant.ts index 7b65aa9df..d44b814aa 100644 --- a/shared/src/i18n/ru/assistant.ts +++ b/shared/src/i18n/ru/assistant.ts @@ -288,9 +288,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllFailed.few': 'Не удалось отменить {count} изменения', 'assistant.change.undoAllFailed.many': 'Не удалось отменить {count} изменений', 'assistant.place.strip': 'Найденные места', - 'assistant.place.addTo': 'Добавить в {day}', - 'assistant.place.addToShort': '+ День {n}', - 'assistant.place.pickDay': 'Выбрать день', + 'assistant.place.add': 'Добавить в поездку', 'assistant.place.suggested': 'Предложено', 'assistant.place.inTrip': 'Уже в вашей поездке', 'assistant.place.showOnMap': 'На карте', @@ -413,6 +411,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'День', 'assistant.memory.title': 'Заметки к поездке', 'assistant.memory.edit': 'Изменить заметки к поездке', + 'assistant.memory.kept': 'Запомнено для этой поездки:', 'assistant.memory.sub': 'Ассистент учитывает их в каждом предложении для этой поездки.', 'assistant.memory.quick': 'Частые пожелания', 'assistant.memory.notes': 'Ваши заметки', @@ -450,6 +449,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Удалить', 'assistant.change.verb.addTour': 'Добавить тур', 'assistant.change.verb.updateTour': 'Изменить тур', + 'assistant.change.verb.markVisited': 'Отметить как посещённое', + 'assistant.change.verb.markSkipped': 'Пропустить остановку', + 'assistant.change.verb.markPlanned': 'Снять отметку', 'assistant.change.verb.importTour': 'Добавить тур из файла', 'assistant.change.label.field': 'Значение', 'assistant.change.label.difficulty': 'Максимальная сложность тропы', diff --git a/shared/src/i18n/sk/assistant.ts b/shared/src/i18n/sk/assistant.ts index dedaa2062..62d3a34af 100644 --- a/shared/src/i18n/sk/assistant.ts +++ b/shared/src/i18n/sk/assistant.ts @@ -279,9 +279,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllFailed.one': '{count} zmenu sa nepodarilo vrátiť', 'assistant.change.undoAllFailed.few': '{count} zmeny sa nepodarilo vrátiť', 'assistant.place.strip': 'Nájdené miesta', - 'assistant.place.addTo': 'Pridať do {day}', - 'assistant.place.addToShort': '+ Deň {n}', - 'assistant.place.pickDay': 'Vybrať deň', + 'assistant.place.add': 'Pridať do cesty', 'assistant.place.suggested': 'Navrhnuté', 'assistant.place.inTrip': 'Už je v tvojom výlete', 'assistant.place.showOnMap': 'Na mape', @@ -392,6 +390,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'Deň', 'assistant.memory.title': 'Poznámky k ceste', 'assistant.memory.edit': 'Upraviť poznámky k ceste', + 'assistant.memory.kept': 'Zapamätané pre túto cestu:', 'assistant.memory.sub': 'Asistent na ne myslí pri každom návrhu pre túto cestu.', 'assistant.memory.quick': 'Časté želania', 'assistant.memory.notes': 'Tvoje poznámky', @@ -425,6 +424,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Odstrániť', 'assistant.change.verb.addTour': 'Pridať túru', 'assistant.change.verb.updateTour': 'Zmeniť túru', + 'assistant.change.verb.markVisited': 'Označiť ako navštívené', + 'assistant.change.verb.markSkipped': 'Preskočiť zastávku', + 'assistant.change.verb.markPlanned': 'Zrušiť označenie', 'assistant.change.verb.importTour': 'Pridať túru zo súboru', 'assistant.change.label.field': 'Hodnota', 'assistant.change.label.difficulty': 'Maximálna náročnosť chodníka', diff --git a/shared/src/i18n/sv/assistant.ts b/shared/src/i18n/sv/assistant.ts index a8c894529..c4a1e7dee 100644 --- a/shared/src/i18n/sv/assistant.ts +++ b/shared/src/i18n/sv/assistant.ts @@ -274,9 +274,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllFailed': '{count} ändringar kunde inte ångras', 'assistant.change.undoAllFailed.one': '{count} ändring kunde inte ångras', 'assistant.place.strip': 'Hittade platser', - 'assistant.place.addTo': 'Lägg till på {day}', - 'assistant.place.addToShort': '+ Dag {n}', - 'assistant.place.pickDay': 'Välj dag', + 'assistant.place.add': 'Lägg till i resan', 'assistant.place.suggested': 'Föreslagen', 'assistant.place.inTrip': 'Redan i din resa', 'assistant.place.showOnMap': 'På kartan', @@ -377,6 +375,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'Dag', 'assistant.memory.title': 'Resanteckningar', 'assistant.memory.edit': 'Redigera resanteckningar', + 'assistant.memory.kept': 'Antecknat för resan:', 'assistant.memory.sub': 'Assistenten tänker på detta i varje förslag för resan.', 'assistant.memory.quick': 'Vanliga önskemål', 'assistant.memory.notes': 'Dina anteckningar', @@ -406,6 +405,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Ta bort', 'assistant.change.verb.addTour': 'Lägg till tur', 'assistant.change.verb.updateTour': 'Ändra tur', + 'assistant.change.verb.markVisited': 'Markera som besökt', + 'assistant.change.verb.markSkipped': 'Hoppa över stopp', + 'assistant.change.verb.markPlanned': 'Ta tillbaka markering', 'assistant.change.verb.importTour': 'Lägg till tur från fil', 'assistant.change.label.field': 'Värde', 'assistant.change.label.difficulty': 'Högsta svårighetsgrad för leden', diff --git a/shared/src/i18n/th/assistant.ts b/shared/src/i18n/th/assistant.ts index 9ed60e615..1b3b612a6 100644 --- a/shared/src/i18n/th/assistant.ts +++ b/shared/src/i18n/th/assistant.ts @@ -261,9 +261,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllPartial': 'เลิกทำแล้ว {done} รายการ มีการเปลี่ยนในระหว่างนั้น {stale} รายการ', 'assistant.change.undoAllFailed': 'เลิกทำ {count} การเปลี่ยนแปลงไม่ได้', 'assistant.place.strip': 'สถานที่ที่พบ', - 'assistant.place.addTo': 'เพิ่มใน {day}', - 'assistant.place.addToShort': '+ วันที่ {n}', - 'assistant.place.pickDay': 'เลือกวัน', + 'assistant.place.add': 'เพิ่มในทริป', 'assistant.place.suggested': 'เสนอแล้ว', 'assistant.place.inTrip': 'อยู่ในทริปของคุณแล้ว', 'assistant.place.showOnMap': 'บนแผนที่', @@ -353,6 +351,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'วัน', 'assistant.memory.title': 'บันทึกทริป', 'assistant.memory.edit': 'แก้ไขบันทึกทริป', + 'assistant.memory.kept': 'จดไว้สำหรับทริปนี้:', 'assistant.memory.sub': 'ผู้ช่วยจะคำนึงถึงสิ่งนี้ในทุกข้อเสนอของทริปนี้', 'assistant.memory.quick': 'ความต้องการที่พบบ่อย', 'assistant.memory.notes': 'บันทึกของคุณ', @@ -378,6 +377,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'ลบ', 'assistant.change.verb.addTour': 'เพิ่มทัวร์', 'assistant.change.verb.updateTour': 'แก้ไขทัวร์', + 'assistant.change.verb.markVisited': 'ทำเครื่องหมายว่าไปแล้ว', + 'assistant.change.verb.markSkipped': 'ข้ามจุดแวะ', + 'assistant.change.verb.markPlanned': 'ยกเลิกเครื่องหมาย', 'assistant.change.verb.importTour': 'เพิ่มทัวร์จากไฟล์', 'assistant.change.label.field': 'ค่า', 'assistant.change.label.difficulty': 'ระดับความยากสูงสุดของทางเดิน', diff --git a/shared/src/i18n/tr/assistant.ts b/shared/src/i18n/tr/assistant.ts index bb88e223f..1e54a9f90 100644 --- a/shared/src/i18n/tr/assistant.ts +++ b/shared/src/i18n/tr/assistant.ts @@ -272,9 +272,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllFailed': '{count} değişiklik geri alınamadı', 'assistant.change.undoAllFailed.one': '{count} değişiklik geri alınamadı', 'assistant.place.strip': 'Bulunan yerler', - 'assistant.place.addTo': '{day} gününe ekle', - 'assistant.place.addToShort': '+ {n}. gün', - 'assistant.place.pickDay': 'Gün seç', + 'assistant.place.add': 'Seyahate ekle', 'assistant.place.suggested': 'Önerildi', 'assistant.place.inTrip': 'Zaten seyahatinde', 'assistant.place.showOnMap': 'Haritada', @@ -376,6 +374,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'Gün', 'assistant.memory.title': 'Gezi notları', 'assistant.memory.edit': 'Gezi notlarını düzenle', + 'assistant.memory.kept': 'Bu seyahat için not edildi:', 'assistant.memory.sub': 'Asistan bu gezideki her öneride bunları dikkate alır.', 'assistant.memory.quick': 'Sık istekler', 'assistant.memory.notes': 'Notların', @@ -406,6 +405,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Kaldır', 'assistant.change.verb.addTour': 'Tur ekle', 'assistant.change.verb.updateTour': 'Turu değiştir', + 'assistant.change.verb.markVisited': 'Ziyaret edildi olarak işaretle', + 'assistant.change.verb.markSkipped': 'Durağı atla', + 'assistant.change.verb.markPlanned': 'İşareti geri al', 'assistant.change.verb.importTour': 'Dosyadan tur ekle', 'assistant.change.label.field': 'Değer', 'assistant.change.label.difficulty': 'En yüksek patika zorluğu', diff --git a/shared/src/i18n/uk/assistant.ts b/shared/src/i18n/uk/assistant.ts index 3811b5a4a..85d5650b6 100644 --- a/shared/src/i18n/uk/assistant.ts +++ b/shared/src/i18n/uk/assistant.ts @@ -288,9 +288,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllFailed.few': 'Не вдалося скасувати {count} зміни', 'assistant.change.undoAllFailed.many': 'Не вдалося скасувати {count} змін', 'assistant.place.strip': 'Знайдені місця', - 'assistant.place.addTo': 'Додати до {day}', - 'assistant.place.addToShort': '+ День {n}', - 'assistant.place.pickDay': 'Вибрати день', + 'assistant.place.add': 'Додати до подорожі', 'assistant.place.suggested': 'Запропоновано', 'assistant.place.inTrip': 'Уже у вашій подорожі', 'assistant.place.showOnMap': 'На мапі', @@ -411,6 +409,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'День', 'assistant.memory.title': 'Нотатки до подорожі', 'assistant.memory.edit': 'Змінити нотатки до подорожі', + 'assistant.memory.kept': 'Запам’ятано для цієї подорожі:', 'assistant.memory.sub': 'Асистент враховує їх у кожній пропозиції для цієї подорожі.', 'assistant.memory.quick': 'Часті побажання', 'assistant.memory.notes': 'Ваші нотатки', @@ -448,6 +447,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Видалити', 'assistant.change.verb.addTour': 'Додати тур', 'assistant.change.verb.updateTour': 'Змінити тур', + 'assistant.change.verb.markVisited': 'Позначити як відвідане', + 'assistant.change.verb.markSkipped': 'Пропустити зупинку', + 'assistant.change.verb.markPlanned': 'Зняти позначку', 'assistant.change.verb.importTour': 'Додати тур із файлу', 'assistant.change.label.field': 'Значення', 'assistant.change.label.difficulty': 'Максимальна складність стежки', diff --git a/shared/src/i18n/vi/assistant.ts b/shared/src/i18n/vi/assistant.ts index fae02002b..9e6f05abb 100644 --- a/shared/src/i18n/vi/assistant.ts +++ b/shared/src/i18n/vi/assistant.ts @@ -263,9 +263,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllPartial': 'Đã hoàn tác {done}, {stale} đã đổi trong lúc đó', 'assistant.change.undoAllFailed': 'Không thể hoàn tác {count} thay đổi', 'assistant.place.strip': 'Địa điểm tìm được', - 'assistant.place.addTo': 'Thêm vào {day}', - 'assistant.place.addToShort': '+ Ngày {n}', - 'assistant.place.pickDay': 'Chọn ngày', + 'assistant.place.add': 'Thêm vào chuyến đi', 'assistant.place.suggested': 'Đã đề xuất', 'assistant.place.inTrip': 'Đã có trong chuyến đi', 'assistant.place.showOnMap': 'Trên bản đồ', @@ -356,6 +354,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': 'Ngày', 'assistant.memory.title': 'Ghi chú chuyến đi', 'assistant.memory.edit': 'Sửa ghi chú chuyến đi', + 'assistant.memory.kept': 'Đã ghi nhớ cho chuyến đi:', 'assistant.memory.sub': 'Trợ lý sẽ lưu ý điều này trong mọi đề xuất cho chuyến đi.', 'assistant.memory.quick': 'Mong muốn phổ biến', 'assistant.memory.notes': 'Ghi chú của bạn', @@ -381,6 +380,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': 'Xóa', 'assistant.change.verb.addTour': 'Thêm chuyến đi', 'assistant.change.verb.updateTour': 'Sửa chuyến đi', + 'assistant.change.verb.markVisited': 'Đánh dấu đã ghé', + 'assistant.change.verb.markSkipped': 'Bỏ qua điểm dừng', + 'assistant.change.verb.markPlanned': 'Bỏ đánh dấu', 'assistant.change.verb.importTour': 'Thêm chuyến đi từ tệp', 'assistant.change.label.field': 'Giá trị', 'assistant.change.label.difficulty': 'Độ khó tối đa của đường mòn', diff --git a/shared/src/i18n/zh-TW/assistant.ts b/shared/src/i18n/zh-TW/assistant.ts index 35c35ab9c..7dc0f4d74 100644 --- a/shared/src/i18n/zh-TW/assistant.ts +++ b/shared/src/i18n/zh-TW/assistant.ts @@ -258,9 +258,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllPartial': '已復原 {done} 項,{stale} 項在這段期間已被修改', 'assistant.change.undoAllFailed': '有 {count} 項修改無法復原', 'assistant.place.strip': '找到的地點', - 'assistant.place.addTo': '加入{day}', - 'assistant.place.addToShort': '+ 第 {n} 天', - 'assistant.place.pickDay': '選擇日期', + 'assistant.place.add': '加入行程', 'assistant.place.suggested': '已建議', 'assistant.place.inTrip': '已在行程中', 'assistant.place.showOnMap': '在地圖上', @@ -349,6 +347,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': '天', 'assistant.memory.title': '行程備忘', 'assistant.memory.edit': '編輯行程備忘', + 'assistant.memory.kept': '已為本次行程記下:', 'assistant.memory.sub': '助理會在此行程的每個建議中考慮這些。', 'assistant.memory.quick': '常見需求', 'assistant.memory.notes': '你的備忘', @@ -373,6 +372,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': '刪除', 'assistant.change.verb.addTour': '新增路線', 'assistant.change.verb.updateTour': '修改路線', + 'assistant.change.verb.markVisited': '標記為已造訪', + 'assistant.change.verb.markSkipped': '跳過站點', + 'assistant.change.verb.markPlanned': '取消標記', 'assistant.change.verb.importTour': '從檔案新增路線', 'assistant.change.label.field': '值', 'assistant.change.label.difficulty': '步道最高難度', diff --git a/shared/src/i18n/zh/assistant.ts b/shared/src/i18n/zh/assistant.ts index 5cc552206..7b80a626a 100644 --- a/shared/src/i18n/zh/assistant.ts +++ b/shared/src/i18n/zh/assistant.ts @@ -258,9 +258,7 @@ const assistant: TranslationStrings = { 'assistant.change.undoAllPartial': '已撤销 {done} 项,{stale} 项在此期间已被修改', 'assistant.change.undoAllFailed': '有 {count} 项修改无法撤销', 'assistant.place.strip': '找到的地点', - 'assistant.place.addTo': '添加到{day}', - 'assistant.place.addToShort': '+ 第 {n} 天', - 'assistant.place.pickDay': '选择日期', + 'assistant.place.add': '添加到行程', 'assistant.place.suggested': '已建议', 'assistant.place.inTrip': '已在行程中', 'assistant.place.showOnMap': '在地图上', @@ -349,6 +347,7 @@ const assistant: TranslationStrings = { 'assistant.week.day': '天', 'assistant.memory.title': '行程备忘', 'assistant.memory.edit': '编辑行程备忘', + 'assistant.memory.kept': '已为本次行程记下:', 'assistant.memory.sub': '助手会在此行程的每个建议中考虑这些。', 'assistant.memory.quick': '常见需求', 'assistant.memory.notes': '你的备忘', @@ -373,6 +372,9 @@ const assistant: TranslationStrings = { 'assistant.change.verb.genericRemove': '删除', 'assistant.change.verb.addTour': '添加路线', 'assistant.change.verb.updateTour': '修改路线', + 'assistant.change.verb.markVisited': '标记为已游览', + 'assistant.change.verb.markSkipped': '跳过站点', + 'assistant.change.verb.markPlanned': '取消标记', 'assistant.change.verb.importTour': '从文件添加路线', 'assistant.change.label.field': '值', 'assistant.change.label.difficulty': '步道最高难度',