diff --git a/client/src/components/Assistant/cards/journey/JourneyChangeSlots.tsx b/client/src/components/Assistant/cards/journey/JourneyChangeSlots.tsx index 20a2ba317..2aacb13c9 100644 --- a/client/src/components/Assistant/cards/journey/JourneyChangeSlots.tsx +++ b/client/src/components/Assistant/cards/journey/JourneyChangeSlots.tsx @@ -1,7 +1,20 @@ import type { AssistantChangeRow, JourneyEntriesCard } from '@trek/shared'; -import { AlignLeft, CalendarDays, Clock, MapPin, Tag, Type } from 'lucide-react'; +import { + AlignLeft, + ArrowDownUp, + CalendarDays, + Clock, + LocateFixed, + MapPin, + Tag, + Tags, + ThumbsDown, + ThumbsUp, + Type, +} from 'lucide-react'; import { useTranslation } from '../../../../i18n'; +import { cardDateLabel } from '../../../Journey/journeyCard'; import type { ChangeItem } from '../../assistantParts'; import { FactChange } from '../parts/CardFacts'; import { EntryMarks, EntryStory, EntryThumb } from './JourneyEntryParts'; @@ -20,7 +33,9 @@ import { entryHoverProps, useEntryHover } from './useEntryHover'; const GONE: ReadonlySet = new Set(['failed', 'discarded', 'expired']); /** The facts a journal change row names on its faint line, in this order: when, then where. */ -const FACT_ICONS = { day: CalendarDays, time: Clock, location: MapPin } as const; +const FACT_ICONS = { day: CalendarDays, time: Clock, location: MapPin, coordinates: LocateFixed } as const; +/** The lists an update rewrites, each by its glyph: the tags and the two sides of the verdict. */ +const LIST_ICONS: Readonly> = { tags: Tags, pros: ThumbsUp, cons: ThumbsDown }; /** The words an update rewrites, named only when they change: the entry's title and its text. */ const WORD_ICONS = { name: Type, text: AlignLeft } as const; type FactRow = AssistantChangeRow & { labelKey: keyof typeof FACT_ICONS }; @@ -45,7 +60,9 @@ function StateChange({ row }: { row: AssistantChangeRow }) { if (flag) return ; const was = stateOf(row.field, row.before); const now = stateOf(row.field, row.after); - const Icon = (now ?? was)?.Icon ?? Tag; + const listed = + row.field && Object.prototype.hasOwnProperty.call(LIST_ICONS, row.field) ? LIST_ICONS[row.field] : undefined; + const Icon = (now ?? was)?.Icon ?? listed ?? Tag; const named = (tone: typeof now, value: string | null) => (tone ? t(tone.labelKey) : value); return ( + {card.entries.map((look, index) => ( +
  • +
  • + ))} + {card.more > 0 && ( +
  • {t('assistant.card.common.more', { count: card.more })}
  • + )} + + ); +} + /** The start of the entry's text and its marks, under the row; resting on them shows its place on the map. */ export function JourneyEntryBody({ card, item }: { card: JourneyEntriesCard; item: ChangeItem }) { const { t } = useTranslation(); const hover = useEntryHover(card.journeyId, item.toolCallId); const look = card.entries[0]; if (!look || GONE.has(item.state)) return null; + if (card.change === 'reordered') return ; return (
    diff --git a/client/src/components/Assistant/cards/journey/JourneyEntryChanges.test.tsx b/client/src/components/Assistant/cards/journey/JourneyEntryChanges.test.tsx new file mode 100644 index 000000000..c9f6dc51f --- /dev/null +++ b/client/src/components/Assistant/cards/journey/JourneyEntryChanges.test.tsx @@ -0,0 +1,100 @@ +/** + * FE-AI-JOURNEY-020 to FE-AI-JOURNEY-022 + * + * The parts of a journal entry the chat added later: its tags and how its + * verdict leans among its marks, an update's tags, verdict and coordinates + * old to new, and a new order of the entries drawn as the entries that move. + */ +import type { AssistantChangePreview, JourneyEntriesCard, JourneyEntryLook } from '@trek/shared'; +import { describe, expect, it } from 'vitest'; + +import { render, screen } from '../../../../../tests/helpers/render'; +import AssistantChangeCard from '../../AssistantChangeCard'; +import type { ChangeItem } from '../../assistantParts'; +import { AssistantViewContext } from '../../assistantViewContext'; + +const ENTRY: JourneyEntryLook = { + entryId: 12, + type: 'entry', + title: 'Sunset at the Spree', + date: '2026-10-21', + time: null, + place: null, + point: null, + countryCode: null, + story: null, + mood: null, + weather: null, + photoId: null, + video: false, + photoCount: 0, + visibility: 'private', + draft: false, + fromTripPlace: false, + dayIndex: null, + tags: ['river', 'evening'], + verdict: { pros: ['the light'], cons: ['cold', 'wind'], prosCount: 1, consCount: 2 }, +}; + +function journal(entries: JourneyEntryLook[], change: JourneyEntriesCard['change'], more = 0): JourneyEntriesCard { + return { kind: 'journeyEntries', journeyId: 3, journalTitle: 'Berlin', tripJournal: false, change, entries, more }; +} + +function pending( + card: JourneyEntriesCard, + verbKey: AssistantChangePreview['verbKey'], + rows: AssistantChangePreview['rows'] +) { + const preview: AssistantChangePreview = { + toolCallId: 'e1', + verbKey, + subject: 'Berlin', + rows, + dayId: null, + placeIds: [], + undoable: true, + domain: 'journey', + card, + }; + const item: ChangeItem = { toolCallId: 'e1', preview, applied: null, state: 'pending', card }; + return render( + + + + ); +} + +describe('the later parts of a journal entry', () => { + it('FE-AI-JOURNEY-020: a new entry shows its tags and how its verdict leans', () => { + pending(journal([ENTRY], 'new'), 'addJournalEntry', []); + expect(screen.getByText('river')).toBeInTheDocument(); + expect(screen.getByText('evening')).toBeInTheDocument(); + expect(screen.getByText('1 good point')).toBeInTheDocument(); + expect(screen.getByText('2 weak points')).toBeInTheDocument(); + }); + + it('FE-AI-JOURNEY-021: an update names its tags, each side of its verdict and its coordinates, old to new', () => { + const rows: AssistantChangePreview['rows'] = [ + { op: 'change', labelKey: 'field', field: 'tags', before: 'river, evening', after: 'river' }, + { op: 'change', labelKey: 'field', field: 'cons', before: null, after: 'cold' }, + { op: 'change', labelKey: 'coordinates', before: null, after: '52.5200, 13.4050' }, + ]; + pending(journal([{ ...ENTRY, tags: [], verdict: null }], 'changed'), 'updateJournalEntry', rows); + expect(screen.getByText('river, evening')).toHaveClass('line-through'); + expect(screen.getByText('cold')).toBeInTheDocument(); + expect(screen.getByText('52.5200, 13.4050')).toBeInTheDocument(); + }); + + it('FE-AI-JOURNEY-022: a new order lists the entries that move in their new places, the rest counted', () => { + const moved = [ + { ...ENTRY, entryId: 2, title: 'Museum Island', moved: true }, + { ...ENTRY, entryId: 1, title: 'Arrival', moved: true }, + ]; + pending(journal(moved, 'reordered', 3), 'reorderJournalEntries', []); + expect(screen.getByText('Put the entries in a new order')).toBeInTheDocument(); + const text = document.body.textContent ?? ''; + expect(text.indexOf('Museum Island')).toBeGreaterThan(-1); + expect(text.indexOf('Museum Island')).toBeLessThan(text.indexOf('Arrival')); + expect(screen.getByText('3 more')).toBeInTheDocument(); + }); +}); diff --git a/client/src/components/Assistant/cards/journey/JourneyEntryParts.tsx b/client/src/components/Assistant/cards/journey/JourneyEntryParts.tsx index eb2a2e23e..a216612eb 100644 --- a/client/src/components/Assistant/cards/journey/JourneyEntryParts.tsx +++ b/client/src/components/Assistant/cards/journey/JourneyEntryParts.tsx @@ -1,5 +1,5 @@ import type { JourneyEntryLook } from '@trek/shared'; -import { CalendarDays, Globe, Images, MapPin, Play, Users } from 'lucide-react'; +import { CalendarDays, Globe, Images, MapPin, Play, Tag, ThumbsDown, ThumbsUp, Users } from 'lucide-react'; import { useTranslation } from '../../../../i18n'; import { cardDateLabel, cardPlace } from '../../../Journey/journeyCard'; @@ -101,11 +101,39 @@ function WeatherMark({ weather }: { weather: string | null }) { ); } -/** How the entry reads: shared or public, a draft, its mood and weather, its photos past the first. */ +/** Its tags and how its verdict leans: the good and the weak points counted. */ +function ListMarks({ look }: { look: JourneyEntryLook }) { + const { t } = useTranslation(); + const verdict = look.verdict ?? null; + return ( + <> + {(look.tags ?? []).map((tag, index) => ( + + ))} + {verdict && verdict.prosCount > 0 && ( + + )} + {verdict && verdict.consCount > 0 && ( + + )} + + ); +} + +/** How the entry reads: shared or public, a draft, its mood and weather, its photos past the first, its tags and verdict. */ export function EntryMarks({ look }: { look: JourneyEntryLook }) { const { t } = useTranslation(); const mood = moodOf(look.mood) !== null; - if (look.visibility === 'private' && !look.draft && !mood && !look.weather && look.photoCount < 2) return null; + const lists = (look.tags?.length ?? 0) > 0 || !!look.verdict; + if (look.visibility === 'private' && !look.draft && !mood && !look.weather && look.photoCount < 2 && !lists) { + return null; + } return ( @@ -117,6 +145,7 @@ export function EntryMarks({ look }: { look: JourneyEntryLook }) { {t('assistant.card.journey.photos', { count: look.photoCount })} )} + ); } diff --git a/client/src/components/Assistant/cards/journey/journeyLook.ts b/client/src/components/Assistant/cards/journey/journeyLook.ts index 2e1b3792d..09b686baa 100644 --- a/client/src/components/Assistant/cards/journey/journeyLook.ts +++ b/client/src/components/Assistant/cards/journey/journeyLook.ts @@ -1,4 +1,5 @@ import { + ArrowDownUp, BookCheck, BookOpen, BookOpenText, @@ -29,6 +30,11 @@ export const JOURNEY_LOOK = { addJournalEntry: { Icon: BookPlus, tone: 'success', labelKey: 'assistant.change.verb.addJournalEntry' }, updateJournalEntry: { Icon: BookOpenText, tone: 'info', labelKey: 'assistant.change.verb.updateJournalEntry' }, removeJournalEntry: { Icon: BookX, tone: 'danger', labelKey: 'assistant.change.verb.removeJournalEntry' }, + reorderJournalEntries: { + Icon: ArrowDownUp, + tone: 'info', + labelKey: 'assistant.change.verb.reorderJournalEntries', + }, createJournal: { Icon: BookPlus, tone: 'success', labelKey: 'assistant.change.verb.createJournal' }, renameJournal: { Icon: Type, tone: 'info', labelKey: 'assistant.change.verb.renameJournal' }, setJournalStatus: { Icon: BookCheck, tone: 'info', labelKey: 'assistant.change.verb.setJournalStatus' }, diff --git a/server/src/nest/assistant/cards/journey/index.ts b/server/src/nest/assistant/cards/journey/index.ts index 929fde173..b2f6afae1 100644 --- a/server/src/nest/assistant/cards/journey/index.ts +++ b/server/src/nest/assistant/cards/journey/index.ts @@ -21,7 +21,17 @@ import { UNLINK_LINKED_TRIP, } from './journey-header-undo'; import { readCard } from './journey-look'; -import { DELETE_CREATED_ENTRY } from './journey-undo'; +import { + describeReorder, + describeStay, + loadJournalOrder, + loadStay, + reorderCard, + stayCard, + type JournalOrderLoaded, + type StayLoaded, +} from './journey-order'; +import { DELETE_CREATED_ENTRY, RESTORE_CHANGED_ENTRY, RESTORE_ENTRY_ORDER } from './journey-undo'; import type { AssistantChangeVerbKey, JourneyEntriesCard } from '@trek/shared'; /** @@ -85,6 +95,35 @@ function journalHeader( }; } +/** A new order of the journal's entries: the entries that move, in their new places, and the way back. */ +const reorderCards: ToolCards = { + write: { verbKey: 'reorderJournalEntries', entity: 'journey_entry' }, + load: (args, read) => loadJournalOrder(args, read), + describe: describeReorder, + change: (_ctx, args, loaded) => reorderCard(args, loaded), + appliedIds: (args) => { + const first = Array.isArray(args.orderedIds) ? args.orderedIds[0] : null; + return typeof first === 'number' ? [first] : []; + }, + undo: RESTORE_ENTRY_ORDER, +}; + +/** + * A stay Dawarich recorded taken in as an entry: the entry it writes. No way + * back: deleting the entry would leave the stay marked as taken in, with no + * way to take it in again. + */ +const stayCards: ToolCards = { + write: { verbKey: 'addJournalEntry', entity: 'journey_entry' }, + load: (args, read) => loadStay(args, read), + describe: describeStay, + change: (_ctx, args, loaded) => stayCard(args, loaded), + appliedIds: (_args, result) => { + const id = recordOf(result).createdJournalEntryId; + return typeof id === 'number' ? [id] : []; + }, +}; + export const JOURNEY_CARDS: DomainCards = { id: 'journey', tools: { @@ -108,6 +147,7 @@ export const JOURNEY_CARDS: DomainCards = { update_journey_entry: { write: { verbKey: 'updateJournalEntry', entity: 'journey_entry' }, describe: JOURNEY_DESCRIBERS.update_journey_entry, + undo: RESTORE_CHANGED_ENTRY, ...journalWrite('update_journey_entry'), }, delete_journey_entry: { @@ -115,6 +155,8 @@ export const JOURNEY_CARDS: DomainCards = { describe: JOURNEY_DESCRIBERS.delete_journey_entry, ...journalWrite('delete_journey_entry'), }, + reorder_journey_entries: reorderCards, + accept_dawarich_suggestion_as_journal_entry: stayCards, create_journey: journalHeader('create_journey', 'createJournal', DELETE_CREATED_JOURNEY), update_journey: journalHeader('update_journey', 'updateJournalSettings', RESTORE_JOURNEY_SETTINGS), delete_journey: journalHeader('delete_journey', 'deleteJournal'), diff --git a/server/src/nest/assistant/cards/journey/journey-change.ts b/server/src/nest/assistant/cards/journey/journey-change.ts index dfd5d45d8..44902ae28 100644 --- a/server/src/nest/assistant/cards/journey/journey-change.ts +++ b/server/src/nest/assistant/cards/journey/journey-change.ts @@ -47,6 +47,8 @@ const ENTRY_FIELDS = [ 'visibility', 'type', 'is_draft', + 'tags', + 'pros_cons', ] as const; /** The states of an entry an update can set, each a row of its own when it changes. */ @@ -142,6 +144,41 @@ function addStateRows(rows: Rows, before: Row, args: ToolArgs): void { } } +/** A list the user wrote (tags, a side of the verdict) as one row value: its items in order, joined. */ +function listText(value: unknown): string { + return text(Array.isArray(value) ? value.filter((item) => typeof item === 'string' && item.trim()).join(', ') : ''); +} + +/** Coordinates as a row shows them, both or none. */ +function pointText(lat: unknown, lng: unknown): string { + return typeof lat === 'number' && typeof lng === 'number' ? `${lat.toFixed(4)}, ${lng.toFixed(4)}` : ''; +} + +/** + * The rows of what an update sets beyond the words and the states: the tags, + * each side of the verdict, and the coordinates (the entry placed on the map, + * moved or taken off it), each when it changes. + */ +function addListRows(rows: Rows, before: Row, args: ToolArgs): void { + const push = (field: string, was: string, now: string) => { + if (was !== now) rows.rows.push({ op: 'change', labelKey: 'field', field, before: was || null, after: now }); + }; + if ('tags' in args) push('tags', listText(before.tags), listText(args.tags)); + if ('pros_cons' in args) { + const was = recordOf(before.pros_cons); + const now = recordOf(args.pros_cons); + push('pros', listText(was.pros), listText(now.pros)); + push('cons', listText(was.cons), listText(now.cons)); + } + if ('location_lat' in args || 'location_lng' in args) { + const lat = 'location_lat' in args ? args.location_lat : before.location_lat; + const lng = 'location_lng' in args ? args.location_lng : before.location_lng; + const was = pointText(before.location_lat, before.location_lng); + const now = pointText(lat, lng); + if (was !== now) rows.change('coordinates', was, now); + } +} + /** The fields of the entry an update sets: those its arguments name, a null among them clearing one. */ function changedFields(args: ToolArgs): Array<(typeof ENTRY_FIELDS)[number]> { return ENTRY_FIELDS.filter((field) => field in args); @@ -165,6 +202,7 @@ export const JOURNEY_DESCRIBERS = { if ('location_name' in args) rows.change('location', text(before.location_name), text(args.location_name)); if ('story' in args) rows.change('text', storyText(before.story), storyText(args.story)); addStateRows(rows, before, args); + addListRows(rows, before, args); return described(subjectOf(ctx, { ...before, ...args }), rows); }, delete_journey_entry: (ctx: PreviewContext, _args: ToolArgs, loaded: JournalLoaded | null): Described => { diff --git a/server/src/nest/assistant/cards/journey/journey-look.ts b/server/src/nest/assistant/cards/journey/journey-look.ts index 5b7c766b3..20eeff7cf 100644 --- a/server/src/nest/assistant/cards/journey/journey-look.ts +++ b/server/src/nest/assistant/cards/journey/journey-look.ts @@ -7,6 +7,7 @@ import { stripMarkdown, type JourneyEntriesCard, type JourneyEntryLook, + type JourneyEntryVerdict, } from '@trek/shared'; /** @@ -74,6 +75,30 @@ function coverOf(photos: Row[]): { photoId: number | null; video: boolean } { return { photoId: positiveId(cover.photo_id), video: cover.media_type === 'video' }; } +/** Lines of text a list field holds (tags, a side of the verdict), each read like any line of its source. */ +function linesOf(value: unknown, key: string, max: number, source: LookSource): string[] { + const items = Array.isArray(value) ? value : []; + return items.flatMap((item) => { + const line = lineOf({ [key]: item }, key, max, source); + return line === null ? [] : [line]; + }); +} + +/** The first tags, as many as a card carries. */ +function tagsOf(row: Row, source: LookSource): string[] { + return linesOf(row.tags, 'tags', JOURNEY_CARD_LIMITS.tag, source).slice(0, JOURNEY_CARD_LIMITS.tags); +} + +/** The verdict's first points on each side and how many there are; null for an entry without one. */ +function verdictOf(row: Row, source: LookSource): JourneyEntryVerdict | null { + const verdict = recordOf(row.pros_cons); + const pros = linesOf(verdict.pros, 'pros_cons', JOURNEY_CARD_LIMITS.point, source); + const cons = linesOf(verdict.cons, 'pros_cons', JOURNEY_CARD_LIMITS.point, source); + if (pros.length === 0 && cons.length === 0) return null; + const cut = JOURNEY_CARD_LIMITS.points; + return { pros: pros.slice(0, cut), cons: cons.slice(0, cut), prosCount: pros.length, consCount: cons.length }; +} + /** Both coordinates, or neither. */ function pointOf(row: Row): JourneyEntryLook['point'] { const lat = finite(row.location_lat); @@ -110,6 +135,8 @@ export function lookOf(value: unknown, source: LookSource = {}): JourneyEntryLoo draft: row.is_draft === true || row.is_draft === 1, fromTripPlace: positiveId(row.source_place_id) !== null && row.type !== 'skeleton', dayIndex: day >= 0 && day <= 9_999 ? day : null, + tags: tagsOf(row, source), + verdict: verdictOf(row, source), }; } diff --git a/server/src/nest/assistant/cards/journey/journey-order.ts b/server/src/nest/assistant/cards/journey/journey-order.ts new file mode 100644 index 000000000..e75fcfe9c --- /dev/null +++ b/server/src/nest/assistant/cards/journey/journey-order.ts @@ -0,0 +1,155 @@ +import type { CardReader, Described, PreviewContext, ToolArgs } from '../card-types'; +import { argId, recordOf, type Row } from '../card-values'; +import { formatDate, Rows, text } from '../preview-format'; +import { rowsOf } from '../undo-snapshot'; +import { journeyArgs } from './journey-header'; +import { entriesCard, lookOf } from './journey-look'; +import { JOURNEY_CARD_LIMITS, stripMarkdown, type JourneyEntriesCard, type JourneyEntryLook } from '@trek/shared'; + +/** + * Two more entry changes of the journal in the chat: a new order of its + * entries, drawn as the entries that move, in their new order; and a stay + * Dawarich recorded taken in as a new entry, drawn as the entry it writes + * from the stay and what the call corrects of it. + */ + +/** What a held reorder looked up: the journal's entries in their reading order, and the journal. */ +export interface JournalOrderLoaded { + entries: Row[]; + journeyId: number | null; + title: unknown; +} + +/** One read: the journal's entries as list_journey_entries answers them, on the journal the call names or pins. */ +export async function loadJournalOrder(args: ToolArgs, read: CardReader): Promise { + const answer = recordOf(await read('list_journey_entries', journeyArgs(args))); + const journey = recordOf(answer.journey); + const journeyId = argId(journey, 'id'); + return journeyId === null ? null : { entries: rowsOf(answer.entries), journeyId, title: journey.title }; +} + +function orderedIdsOf(args: ToolArgs): number[] { + return Array.isArray(args.orderedIds) ? args.orderedIds.filter((id): id is number => Number.isInteger(id)) : []; +} + +/** The ids the call names, in the order they stand in now and in the new one, and those that change place. */ +function orderOf(args: ToolArgs, loaded: JournalOrderLoaded) { + const next = orderedIdsOf(args); + const named = new Set(next); + const now = loaded.entries.map((entry) => entry.id).filter((id): id is number => named.has(id as number)); + const moved = new Set(next.filter((id, index) => now.indexOf(id) !== index)); + return { now, next, moved }; +} + +function nameOf(ctx: PreviewContext, entry: Row | undefined): string { + if (!entry) return ''; + const day = typeof entry.entry_date === 'string' ? formatDate(ctx, entry.entry_date) : ''; + return text(entry.title) || text(entry.location_name) || day; +} + +/** The row of a new order: the entries that move, as they stood and as they will stand. */ +export function describeReorder(ctx: PreviewContext, args: ToolArgs, loaded: JournalOrderLoaded | null): Described { + const rows = new Rows(); + if (loaded) { + const { now, next, moved } = orderOf(args, loaded); + const byId = new Map(loaded.entries.map((entry) => [entry.id, entry])); + const names = (ids: number[]) => + ids + .filter((id) => moved.has(id)) + .map((id) => nameOf(ctx, byId.get(id))) + .join(', '); + if (moved.size > 0) rows.change('order', names(now), names(next)); + } + return { subject: text(loaded?.title), rows, dayId: null, placeIds: [], domain: 'journey' }; +} + +/** The card of a new order: the entries that move in their new places, the rest of the call counted. */ +export function reorderCard(args: ToolArgs, loaded: JournalOrderLoaded | null): JourneyEntriesCard | null { + if (!loaded) return null; + const { next, moved } = orderOf(args, loaded); + const byId = new Map(loaded.entries.map((entry) => [entry.id, entry])); + const looks = next.flatMap((id): JourneyEntryLook[] => { + if (!moved.has(id)) return []; + const look = lookOf(byId.get(id)); + return look ? [{ ...look, moved: true }] : []; + }); + const kept = looks.slice(0, JOURNEY_CARD_LIMITS.entries); + const journal = { + journeyId: loaded.journeyId, + title: loaded.title, + tripJournal: false, + change: 'reordered' as const, + }; + return entriesCard(journal, kept, looks.length - kept.length); +} + +/** What a held Dawarich stay taken in as an entry looked up: the stay, and the journal it goes into. */ +export interface StayLoaded { + stay: Row | null; + journeyId: number | null; + title: unknown; +} + +/** The journal a call names as `journalId`, or none in a journey's own conversation, where the session pins it. */ +function journalArgs(args: ToolArgs): ToolArgs { + const journeyId = argId(args, 'journalId'); + return journeyId === null ? {} : { journeyId }; +} + +/** Two reads: the stays held for review, to find this one, and the journal. */ +export async function loadStay(args: ToolArgs, read: CardReader): Promise { + const suggestionId = argId(args, 'suggestionId'); + if (suggestionId === null) return null; + const listed = rowsOf(recordOf(await read('list_dawarich_suggestions', { limit: 200 })).suggestions); + const journey = recordOf(recordOf(await read('get_journey', journalArgs(args))).journey); + return { + stay: listed.find((row) => row.id === suggestionId) ?? null, + journeyId: argId(journey, 'id'), + title: journey.title, + }; +} + +/** The entry a stay becomes: the call's corrections over what the stay recorded, as the service writes it. */ +function stayEntry(args: ToolArgs, stay: Row): Row { + const name = text(args.name) || text(stay.name); + const started = typeof stay.startedAt === 'string' ? stay.startedAt : ''; + const clock = /^\d{4}-\d{2}-\d{2}T(\d{2}:\d{2})/.exec(started)?.[1] ?? null; + return { + id: null, + type: 'entry', + title: name || null, + story: typeof args.notes === 'string' ? args.notes : null, + entry_date: typeof args.date === 'string' ? args.date : stay.localDate, + entry_time: typeof args.time === 'string' ? args.time : clock, + location_name: name || null, + location_lat: stay.lat, + location_lng: stay.lng, + photos: [], + is_draft: false, + }; +} + +/** The row of a stay taken in: what the entry is about, its day, time, place and text. */ +export function describeStay(ctx: PreviewContext, args: ToolArgs, loaded: StayLoaded | null): Described { + const entry = loaded?.stay + ? stayEntry(args, loaded.stay) + : { title: args.name, story: args.notes, entry_date: args.date }; + const day = typeof entry.entry_date === 'string' ? formatDate(ctx, entry.entry_date) : ''; + const story = typeof entry.story === 'string' ? text(stripMarkdown(entry.story)) : ''; + const rows = new Rows() + .add('day', day) + .add('time', text(entry.entry_time)) + .add('location', text(entry.location_name)) + .add('text', story); + return { subject: text(entry.title) || day, rows, dayId: null, placeIds: [], domain: 'journey' }; +} + +/** The card of a stay taken in: the entry it will write, in the journal it goes into. */ +export function stayCard(args: ToolArgs, loaded: StayLoaded | null): JourneyEntriesCard | null { + if (!loaded?.stay) return null; + // What the call writes is cut only; what the stay recorded is masked like any stored text. + const fromArgs = new Set([...(text(args.name) ? ['title', 'location_name'] : []), ...(args.notes ? ['story'] : [])]); + const look = lookOf(stayEntry(args, loaded.stay), { fromArgs }); + const journal = { journeyId: loaded.journeyId, title: loaded.title, tripJournal: false, change: 'new' as const }; + return entriesCard(journal, look ? [look] : [], 0); +} diff --git a/server/src/nest/assistant/cards/journey/journey-undo.ts b/server/src/nest/assistant/cards/journey/journey-undo.ts index 55aca34bb..98ebde9a9 100644 --- a/server/src/nest/assistant/cards/journey/journey-undo.ts +++ b/server/src/nest/assistant/cards/journey/journey-undo.ts @@ -1,5 +1,6 @@ -import type { ToolCards, UndoSnapshot } from '../card-types'; +import type { ToolArgs, ToolCards, UndoSnapshot } from '../card-types'; import { idIn, readKey, recordOf } from '../card-values'; +import { journeyArgs } from './journey-header'; /** How the undo reads the created entry again: by its id, whatever the turn read before. */ function entryRead(result: unknown) { @@ -39,3 +40,49 @@ export const DELETE_CREATED_ENTRY: NonNullable = { reads: (_args, result) => [entryRead(result)], unchanged: entryUnchanged, }; + +/** + * A changed entry written back as it was, from what the tool answered it + * replaced, while nothing was written to it since and it holds the photos it + * held then. + */ +export const RESTORE_CHANGED_ENTRY: NonNullable = { + tool: 'update_journey_entry', + args: (_args, result) => { + const entryId = idIn(result, 'entry'); + const previous = recordOf(recordOf(result).previous); + return entryId === null || Object.keys(previous).length === 0 ? null : { entryId, ...previous }; + }, + reads: (_args, result) => [entryRead(result)], + unchanged: entryUnchanged, +}; + +/** The ids a reorder named, and the order the tool answered they stood in before. */ +function ordersOf(args: ToolArgs, result: unknown): { next: number[]; before: number[] } { + const ids = (value: unknown) => + Array.isArray(value) ? value.filter((id): id is number => Number.isInteger(id)) : []; + return { next: ids(args.orderedIds), before: ids(recordOf(result).previous_order) }; +} + +/** + * A new order taken back to the one before, while the entries still stand + * as it left them: each at the place the reorder gave it. + */ +export const RESTORE_ENTRY_ORDER: NonNullable = { + tool: 'reorder_journey_entries', + args: (args, result) => { + const { next, before } = ordersOf(args, result); + const same = before.length === next.length && before.every((id, index) => id === next[index]); + return before.length === 0 || same ? null : { ...journeyArgs(args), orderedIds: before }; + }, + reads: (args) => [{ tool: 'list_journey_entries', args: journeyArgs(args) }], + unchanged: (result, snapshot, call) => { + const args = call?.args ?? {}; + const { next } = ordersOf(args, result); + const read = recordOf(snapshot.reads.get(readKey('list_journey_entries', journeyArgs(args)))); + const sortOf = new Map( + (Array.isArray(read.entries) ? read.entries.map(recordOf) : []).map((entry) => [entry.id, entry.sort_order]), + ); + return next.length > 0 && next.every((id, index) => sortOf.get(id) === index); + }, +}; diff --git a/server/tests/unit/nest/assistant/cards/journey/journey-cards.test.ts b/server/tests/unit/nest/assistant/cards/journey/journey-cards.test.ts index cf35be0f6..b3a577568 100644 --- a/server/tests/unit/nest/assistant/cards/journey/journey-cards.test.ts +++ b/server/tests/unit/nest/assistant/cards/journey/journey-cards.test.ts @@ -377,7 +377,7 @@ describe('the applied change', () => { expect(ASSISTANT_WRITE_TOOLS.create_journey_entry).toEqual({ verbKey: 'addJournalEntry', entity: 'journey_entry' }); expect(ASSISTANT_WRITE_TOOLS.update_journey_entry?.verbKey).toBe('updateJournalEntry'); expect(ASSISTANT_WRITE_TOOLS.delete_journey_entry?.verbKey).toBe('removeJournalEntry'); - expect(ASSISTANT_UNDO_MAP.update_journey_entry).toBeUndefined(); + expect(ASSISTANT_UNDO_MAP.update_journey_entry?.tool).toBe('update_journey_entry'); expect(CARD_PROMPT_LINES).toEqual(expect.arrayContaining([...(JOURNEY_CARDS.prompt ?? [])])); expect(JOURNEY_CARDS.prompt).toHaveLength(2); // A trip chat sends a book request to the Studio instead of trying it. diff --git a/server/tests/unit/nest/assistant/cards/journey/journey-order.test.ts b/server/tests/unit/nest/assistant/cards/journey/journey-order.test.ts new file mode 100644 index 000000000..5f3947d92 --- /dev/null +++ b/server/tests/unit/nest/assistant/cards/journey/journey-order.test.ts @@ -0,0 +1,243 @@ +/** + * ASTCARD-JORD-001 to ASTCARD-JORD-007 + * + * The rest of a journal entry's changes in the assistant chat: an update's + * tags, verdict and coordinates on the row and the card, a new order of the + * entries with its way back, a changed entry written back as it was, and a + * Dawarich stay taken in as an entry. Every card is checked against the + * shared union through the registry's guard. + */ +import { buildTripCard } from '../../../../../../src/nest/assistant/assistant-context'; +import type { CardReader, PreviewContext, UndoSnapshot } from '../../../../../../src/nest/assistant/cards/card-types'; +import { readKey } from '../../../../../../src/nest/assistant/cards/card-values'; +import { JOURNEY_CARDS } from '../../../../../../src/nest/assistant/cards/journey'; +import type { JournalLoaded } from '../../../../../../src/nest/assistant/cards/journey/journey-change'; +import { lookOf } from '../../../../../../src/nest/assistant/cards/journey/journey-look'; +import { loadJournalOrder, loadStay } from '../../../../../../src/nest/assistant/cards/journey/journey-order'; +import { ASSISTANT_WRITE_TOOLS, changeCardOf } from '../../../../../../src/nest/assistant/cards/registry'; +import { describeToolCall } from '../../../../../../src/nest/assistant/tool-call-preview.helpers'; +import { journeyEntriesCardSchema, type JourneyEntriesCard } from '@trek/shared'; + +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('../../../../../../src/nest/audit/audit-log.logger', () => ({ + logInfo: vi.fn(), + logError: vi.fn(), + logWarn: vi.fn(), + logDebug: vi.fn(), +})); + +const ctx: PreviewContext = { + card: buildTripCard(1, 7, { trip: { title: 'Berlin' } }, null, null), + categories: new Map(), + locale: 'en', + today: '2026-10-24', + airport: () => null, +}; + +function cardOf(value: unknown): JourneyEntriesCard { + return journeyEntriesCardSchema.parse(value); +} + +function readerOf(answers: Record): { read: CardReader; asked: string[] } { + const asked: string[] = []; + const read: CardReader = (tool, args = {}) => { + asked.push(readKey(tool, args)); + return Promise.resolve(answers[readKey(tool, args)] ?? answers[tool] ?? null); + }; + return { read, asked }; +} + +function undoOf(tool: string) { + const undo = JOURNEY_CARDS.tools[tool]?.undo; + if (!undo) throw new Error(`no undo for ${tool}`); + return undo; +} + +const SPREE = { + id: 12, + journey_id: 3, + type: 'entry', + title: 'Sunset at the Spree', + entry_date: '2026-10-21', + tags: ['river', 'evening'], + pros_cons: { pros: ['the light'], cons: [] }, + location_lat: null, + location_lng: null, + photos: [], + updated_at: 5, +}; + +describe('the tags, the verdict and the coordinates of an entry', () => { + it('ASTCARD-JORD-001: an entry shows its first tags and how its verdict leans', () => { + expect(lookOf(SPREE)).toMatchObject({ + tags: ['river', 'evening'], + verdict: { pros: ['the light'], cons: [], prosCount: 1, consCount: 0 }, + }); + const many = { ...SPREE, tags: Array.from({ length: 9 }, (_, i) => `t${i}`), pros_cons: null }; + expect(lookOf(many)?.tags).toHaveLength(6); + expect(lookOf(many)?.verdict).toBeNull(); + expect(lookOf({ ...SPREE, tags: 'not a list', pros_cons: { pros: [' '], cons: [] } })).toMatchObject({ + tags: [], + verdict: null, + }); + }); + + it('ASTCARD-JORD-002: an update names the tags, each side of the verdict and the coordinates it changes', () => { + const loaded: JournalLoaded = { entry: SPREE, journeyId: 3, title: 'Berlin', tripJournal: false }; + const args = { + entryId: 12, + tags: ['river'], + pros_cons: { pros: ['the light', 'calm'], cons: ['cold'] }, + location_lat: 52.52, + location_lng: 13.405, + }; + const preview = describeToolCall('update_journey_entry', 'c1', args, ctx, loaded); + expect(preview.rows).toEqual([ + { op: 'change', labelKey: 'field', field: 'tags', before: 'river, evening', after: 'river' }, + { op: 'change', labelKey: 'field', field: 'pros', before: 'the light', after: 'the light, calm' }, + { op: 'change', labelKey: 'field', field: 'cons', before: null, after: 'cold' }, + { op: 'change', labelKey: 'coordinates', before: null, after: '52.5200, 13.4050' }, + ]); + // The card shows the entry with what the call sets, the verdict and the tags included. + const card = cardOf(changeCardOf('update_journey_entry', ctx, args, loaded)); + expect(card.entries[0]).toMatchObject({ tags: ['river'], verdict: { prosCount: 2, consCount: 1 } }); + expect(card.entries[0]?.point).toEqual({ lat: 52.52, lng: 13.405 }); + // A list set to what it holds gets no row. + expect( + describeToolCall('update_journey_entry', 'c2', { entryId: 12, tags: ['river', 'evening'] }, ctx, loaded).rows, + ).toEqual([]); + }); + + it('ASTCARD-JORD-003: a changed entry is written back from what the tool answered it replaced', () => { + const undo = undoOf('update_journey_entry'); + const result = { entry: { ...SPREE, title: 'Evening' }, previous: { title: 'Sunset at the Spree', tags: [] } }; + expect(undo.args({}, result, null)).toEqual({ entryId: 12, title: 'Sunset at the Spree', tags: [] }); + expect(undo.args({}, { entry: SPREE, previous: {} }, null)).toBeNull(); + const snapshot = (current: unknown): UndoSnapshot => ({ + summary: null, + places: null, + reads: new Map([[readKey('get_journey_entry', { entryId: 12 }), current]]), + }); + expect(undo.unchanged(result, snapshot({ entry: { ...SPREE, title: 'Evening' } }))).toBe(true); + expect(undo.unchanged(result, snapshot({ entry: { ...SPREE, updated_at: 6 } }))).toBe(false); + }); +}); + +const ENTRIES = [ + { id: 1, title: 'Arrival', entry_date: '2026-10-21', sort_order: 0, photos: [] }, + { id: 2, title: 'Museum Island', entry_date: '2026-10-21', sort_order: 1, photos: [] }, + { id: 3, title: 'Dinner', entry_date: '2026-10-21', sort_order: 2, photos: [] }, +]; + +describe('a new order of the entries', () => { + it('ASTCARD-JORD-004: reads the journal, names the entries that move, and draws them in their new places', async () => { + const { read, asked } = readerOf({ + list_journey_entries: { journey: { id: 3, title: 'Berlin' }, entries: ENTRIES }, + }); + const loaded = await loadJournalOrder({ journeyId: 3, orderedIds: [2, 1, 3] }, read); + expect(asked).toEqual([readKey('list_journey_entries', { journeyId: 3 })]); + expect(loaded).toMatchObject({ journeyId: 3, title: 'Berlin' }); + const preview = describeToolCall('reorder_journey_entries', 'c1', { orderedIds: [2, 1, 3] }, ctx, loaded); + expect(preview).toMatchObject({ verbKey: 'reorderJournalEntries', subject: 'Berlin', undoable: true }); + expect(preview.rows).toEqual([ + { op: 'change', labelKey: 'order', before: 'Arrival, Museum Island', after: 'Museum Island, Arrival' }, + ]); + const card = cardOf(preview.card); + expect(card.change).toBe('reordered'); + expect(card.entries.map((look) => [look.entryId, look.moved])).toEqual([ + [2, true], + [1, true], + ]); + expect(await loadJournalOrder({}, readerOf({}).read)).toBeNull(); + expect(ASSISTANT_WRITE_TOOLS.reorder_journey_entries).toEqual({ + verbKey: 'reorderJournalEntries', + entity: 'journey_entry', + }); + }); + + it('ASTCARD-JORD-005: is taken back to the order before while every entry still stands where it put it', () => { + const undo = undoOf('reorder_journey_entries'); + const args = { journeyId: 3, orderedIds: [2, 1, 3] }; + const result = { success: true, previous_order: [1, 2, 3] }; + expect(undo.args(args, result, null)).toEqual({ journeyId: 3, orderedIds: [1, 2, 3] }); + expect(undo.args({ orderedIds: [1, 2, 3] }, result, null)).toBeNull(); + expect(undo.args(args, { success: true }, null)).toBeNull(); + const check = (entries: unknown[]) => + undo.unchanged( + result, + { + summary: null, + places: null, + reads: new Map([[readKey('list_journey_entries', { journeyId: 3 }), { entries }]]), + }, + { args, preview: null }, + ); + expect( + check([ + { id: 2, sort_order: 0 }, + { id: 1, sort_order: 1 }, + { id: 3, sort_order: 2 }, + ]), + ).toBe(true); + expect( + check([ + { id: 1, sort_order: 0 }, + { id: 2, sort_order: 1 }, + { id: 3, sort_order: 2 }, + ]), + ).toBe(false); + }); +}); + +const STAY = { + id: 4, + name: 'Berlin Hauptbahnhof', + lat: 52.525, + lng: 13.369, + startedAt: '2026-10-20T09:15:00+02:00', + localDate: '2026-10-20', +}; + +describe('a Dawarich stay taken in as an entry', () => { + it('ASTCARD-JORD-006: reads the stay and the journal, and shows the entry the stay becomes', async () => { + const { read, asked } = readerOf({ + list_dawarich_suggestions: { suggestions: [STAY] }, + get_journey: { journey: { id: 3, title: 'Berlin' } }, + }); + const args = { suggestionId: 4, journalId: 3, notes: 'Arrived early' }; + const loaded = await loadStay(args, read); + expect(asked).toEqual([ + readKey('list_dawarich_suggestions', { limit: 200 }), + readKey('get_journey', { journeyId: 3 }), + ]); + const preview = describeToolCall('accept_dawarich_suggestion_as_journal_entry', 'c1', args, ctx, loaded); + expect(preview).toMatchObject({ verbKey: 'addJournalEntry', subject: 'Berlin Hauptbahnhof', undoable: false }); + expect(preview.rows.map((row) => [row.labelKey, row.after])).toEqual([ + ['day', 'Tue, Oct 20'], + ['time', '09:15'], + ['location', 'Berlin Hauptbahnhof'], + ['text', 'Arrived early'], + ]); + const card = cardOf(preview.card); + expect(card).toMatchObject({ journeyId: 3, journalTitle: 'Berlin', change: 'new' }); + expect(card.entries[0]).toMatchObject({ entryId: null, date: '2026-10-20', point: { lat: 52.525, lng: 13.369 } }); + }); + + it('ASTCARD-JORD-007: a correction wins over the stay, and a stay no longer listed draws no card', async () => { + const loaded = { stay: STAY, journeyId: 3, title: 'Berlin' }; + const fixed = describeToolCall( + 'accept_dawarich_suggestion_as_journal_entry', + 'c1', + { suggestionId: 4, name: 'Hbf', date: '2026-10-19', time: '20:00' }, + ctx, + loaded, + ); + expect(fixed.subject).toBe('Hbf'); + expect(fixed.rows.map((row) => row.after)).toEqual(['Mon, Oct 19', '20:00', 'Hbf']); + expect( + changeCardOf('accept_dawarich_suggestion_as_journal_entry', ctx, { suggestionId: 4 }, { ...loaded, stay: null }), + ).toBeNull(); + expect(await loadStay({}, readerOf({}).read)).toBeNull(); + }); +}); diff --git a/shared/src/assistant/cards/journey-entries.card.ts b/shared/src/assistant/cards/journey-entries.card.ts index 2df849b20..d5aa04052 100644 --- a/shared/src/assistant/cards/journey-entries.card.ts +++ b/shared/src/assistant/cards/journey-entries.card.ts @@ -12,7 +12,27 @@ import { z } from 'zod'; */ /** How much of a journal a card carries; the server clips to the same numbers. */ -export const JOURNEY_CARD_LIMITS = { entries: 8, title: 100, place: 80, story: 240, journal: 80, weather: 40 } as const; +export const JOURNEY_CARD_LIMITS = { + entries: 8, + title: 100, + place: 80, + story: 240, + journal: 80, + weather: 40, + tags: 6, + tag: 40, + points: 3, + point: 80, +} as const; + +/** The verdict on a place as a card shows it: the first points on each side, and how many each side holds. */ +export const journeyEntryVerdictSchema = z.object({ + pros: z.array(cardText(JOURNEY_CARD_LIMITS.point)).max(JOURNEY_CARD_LIMITS.points), + cons: z.array(cardText(JOURNEY_CARD_LIMITS.point)).max(JOURNEY_CARD_LIMITS.points), + prosCount: cardCountSchema, + consCount: cardCountSchema, +}); +export type JourneyEntryVerdict = z.infer; /** * One entry as a card shows it. Its texts are plain and on one line (the story @@ -52,11 +72,16 @@ export const journeyEntryLookSchema = z.object({ fromTripPlace: z.boolean(), /** Which day of the journal it falls on, counted from 0, for the day's colour; null when unknown. */ dayIndex: z.number().int().min(0).max(9_999).nullable(), + /** Its first tags. Optional, like the two below: cards stored before they existed leave them out. */ + tags: z.array(cardText(JOURNEY_CARD_LIMITS.tag)).max(JOURNEY_CARD_LIMITS.tags).optional(), + verdict: journeyEntryVerdictSchema.nullable().optional(), + /** In a new order of the journal's entries, this one changed its place. */ + moved: z.boolean().optional(), }); export type JourneyEntryLook = z.infer; -/** What a change card's entry goes through: nothing on an answer, else what the change does to it. */ -export const JOURNEY_CARD_CHANGES = ['none', 'new', 'changed', 'willDelete'] as const; +/** What a change card's entries go through: nothing on an answer, else what the change does to them. */ +export const JOURNEY_CARD_CHANGES = ['none', 'new', 'changed', 'willDelete', 'reordered'] as const; export const journeyEntriesCardSchema = z.object({ kind: z.literal('journeyEntries'), diff --git a/shared/src/assistant/cards/journey.contract.ts b/shared/src/assistant/cards/journey.contract.ts index 13e92f0d4..e4372905e 100644 --- a/shared/src/assistant/cards/journey.contract.ts +++ b/shared/src/assistant/cards/journey.contract.ts @@ -13,7 +13,7 @@ export * from './journey-header.card'; */ export const JOURNEY_CARD_SCHEMAS = [journeyEntriesCardSchema, journeyHeaderCardSchema] as const; /** - * An entry written, changed or deleted; a journal started, renamed, given + * An entry written, changed or deleted, the entries put in a new order; a journal started, renamed, given * another status or other settings, deleted; a trip linked or unlinked; its * suggestions brought back; the user's own view of it changed. */ @@ -21,6 +21,7 @@ export const JOURNEY_VERB_KEYS = [ 'addJournalEntry', 'updateJournalEntry', 'removeJournalEntry', + 'reorderJournalEntries', 'createJournal', 'renameJournal', 'setJournalStatus', diff --git a/shared/src/i18n/ar/assistantJourney.ts b/shared/src/i18n/ar/assistantJourney.ts index c0ed923d0..b4daf0f6d 100644 --- a/shared/src/i18n/ar/assistantJourney.ts +++ b/shared/src/i18n/ar/assistantJourney.ts @@ -77,5 +77,18 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.suggestionsCome.two': 'يُضاف {count} مكانان كاقتراحين', 'assistant.card.journeyHeader.suggestionsCome.few': 'تُضاف {count} أماكن كاقتراحات', 'assistant.card.journeyHeader.suggestionsCome.many': 'يُضاف {count} مكانًا كاقتراحات', + 'assistant.change.verb.reorderJournalEntries': 'إعادة ترتيب المدخلات', + 'assistant.card.journey.pros': '{count} نقطة إيجابية', + 'assistant.card.journey.pros.zero': '{count} نقاط إيجابية', + 'assistant.card.journey.pros.one': '{count} نقطة إيجابية', + 'assistant.card.journey.pros.two': '{count} نقطتان إيجابيتان', + 'assistant.card.journey.pros.few': '{count} نقاط إيجابية', + 'assistant.card.journey.pros.many': '{count} نقطة إيجابية', + 'assistant.card.journey.cons': '{count} نقطة سلبية', + 'assistant.card.journey.cons.zero': '{count} نقاط سلبية', + 'assistant.card.journey.cons.one': '{count} نقطة سلبية', + 'assistant.card.journey.cons.two': '{count} نقطتان سلبيتان', + 'assistant.card.journey.cons.few': '{count} نقاط سلبية', + 'assistant.card.journey.cons.many': '{count} نقطة سلبية', }; export default assistantJourney; diff --git a/shared/src/i18n/az/assistantJourney.ts b/shared/src/i18n/az/assistantJourney.ts index d3dd12a43..6fb912270 100644 --- a/shared/src/i18n/az/assistantJourney.ts +++ b/shared/src/i18n/az/assistantJourney.ts @@ -55,5 +55,10 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.suggestionsGo.one': '{count} açıq təklif silinir', 'assistant.card.journeyHeader.suggestionsCome': '{count} yer təklif kimi əlavə olunur', 'assistant.card.journeyHeader.suggestionsCome.one': '{count} yer təklif kimi əlavə olunur', + 'assistant.change.verb.reorderJournalEntries': 'Qeydləri yenidən sırala', + 'assistant.card.journey.pros': '{count} müsbət cəhət', + 'assistant.card.journey.pros.one': '{count} müsbət cəhət', + 'assistant.card.journey.cons': '{count} mənfi cəhət', + 'assistant.card.journey.cons.one': '{count} mənfi cəhət', }; export default assistantJourney; diff --git a/shared/src/i18n/br/assistantJourney.ts b/shared/src/i18n/br/assistantJourney.ts index 67ca6ccf4..9228195b6 100644 --- a/shared/src/i18n/br/assistantJourney.ts +++ b/shared/src/i18n/br/assistantJourney.ts @@ -53,5 +53,10 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.suggestionsGo.one': '{count} sugestão em aberto sai', 'assistant.card.journeyHeader.suggestionsCome': '{count} lugares entram como sugestões', 'assistant.card.journeyHeader.suggestionsCome.one': '{count} lugar entra como sugestão', + 'assistant.change.verb.reorderJournalEntries': 'Reordenar as entradas', + 'assistant.card.journey.pros': '{count} pontos positivos', + 'assistant.card.journey.pros.one': '{count} ponto positivo', + 'assistant.card.journey.cons': '{count} pontos negativos', + 'assistant.card.journey.cons.one': '{count} ponto negativo', }; export default assistantJourney; diff --git a/shared/src/i18n/ca/assistantJourney.ts b/shared/src/i18n/ca/assistantJourney.ts index 3b6a30cd6..745a8212d 100644 --- a/shared/src/i18n/ca/assistantJourney.ts +++ b/shared/src/i18n/ca/assistantJourney.ts @@ -53,5 +53,10 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.suggestionsGo.one': "Se'n va {count} suggeriment obert", 'assistant.card.journeyHeader.suggestionsCome': '{count} llocs entren com a suggeriments', 'assistant.card.journeyHeader.suggestionsCome.one': '{count} lloc entra com a suggeriment', + 'assistant.change.verb.reorderJournalEntries': 'Reordenar les entrades', + 'assistant.card.journey.pros': '{count} punts forts', + 'assistant.card.journey.pros.one': '{count} punt fort', + 'assistant.card.journey.cons': '{count} punts febles', + 'assistant.card.journey.cons.one': '{count} punt feble', }; export default assistantJourney; diff --git a/shared/src/i18n/cs/assistantJourney.ts b/shared/src/i18n/cs/assistantJourney.ts index 530dfa2a5..3b85d53c0 100644 --- a/shared/src/i18n/cs/assistantJourney.ts +++ b/shared/src/i18n/cs/assistantJourney.ts @@ -59,5 +59,12 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.suggestionsCome': '{count} míst přibude jako návrhy', 'assistant.card.journeyHeader.suggestionsCome.one': '{count} místo přibude jako návrh', 'assistant.card.journeyHeader.suggestionsCome.few': '{count} místa přibudou jako návrhy', + 'assistant.change.verb.reorderJournalEntries': 'Přeuspořádat záznamy', + 'assistant.card.journey.pros': '{count} plusů', + 'assistant.card.journey.pros.one': '{count} plus', + 'assistant.card.journey.pros.few': '{count} plusy', + 'assistant.card.journey.cons': '{count} minusů', + 'assistant.card.journey.cons.one': '{count} minus', + 'assistant.card.journey.cons.few': '{count} minusy', }; export default assistantJourney; diff --git a/shared/src/i18n/de/assistantJourney.ts b/shared/src/i18n/de/assistantJourney.ts index 5d6e4b054..cc29e57f9 100644 --- a/shared/src/i18n/de/assistantJourney.ts +++ b/shared/src/i18n/de/assistantJourney.ts @@ -55,5 +55,10 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.suggestionsGo.one': '{count} offener Vorschlag fällt weg', 'assistant.card.journeyHeader.suggestionsCome': '{count} Orte kommen als Vorschläge dazu', 'assistant.card.journeyHeader.suggestionsCome.one': '{count} Ort kommt als Vorschlag dazu', + 'assistant.change.verb.reorderJournalEntries': 'Einträge neu ordnen', + 'assistant.card.journey.pros': '{count} Pluspunkte', + 'assistant.card.journey.pros.one': '{count} Pluspunkt', + 'assistant.card.journey.cons': '{count} Minuspunkte', + 'assistant.card.journey.cons.one': '{count} Minuspunkt', }; export default assistantJourney; diff --git a/shared/src/i18n/en/assistantJourney.ts b/shared/src/i18n/en/assistantJourney.ts index 3eb013b88..7eace8d89 100644 --- a/shared/src/i18n/en/assistantJourney.ts +++ b/shared/src/i18n/en/assistantJourney.ts @@ -53,5 +53,10 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.suggestionsGo.one': '{count} open suggestion goes', 'assistant.card.journeyHeader.suggestionsCome': '{count} places come in as suggestions', 'assistant.card.journeyHeader.suggestionsCome.one': '{count} place comes in as a suggestion', + 'assistant.change.verb.reorderJournalEntries': 'Put the entries in a new order', + 'assistant.card.journey.pros': '{count} good points', + 'assistant.card.journey.pros.one': '{count} good point', + 'assistant.card.journey.cons': '{count} weak points', + 'assistant.card.journey.cons.one': '{count} weak point', }; export default assistantJourney; diff --git a/shared/src/i18n/es/assistantJourney.ts b/shared/src/i18n/es/assistantJourney.ts index 47a5cfcad..87f6a0ee1 100644 --- a/shared/src/i18n/es/assistantJourney.ts +++ b/shared/src/i18n/es/assistantJourney.ts @@ -54,5 +54,10 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.suggestionsGo.one': 'Se va {count} sugerencia abierta', 'assistant.card.journeyHeader.suggestionsCome': '{count} lugares entran como sugerencias', 'assistant.card.journeyHeader.suggestionsCome.one': '{count} lugar entra como sugerencia', + 'assistant.change.verb.reorderJournalEntries': 'Reordenar las entradas', + 'assistant.card.journey.pros': '{count} puntos a favor', + 'assistant.card.journey.pros.one': '{count} punto a favor', + 'assistant.card.journey.cons': '{count} puntos en contra', + 'assistant.card.journey.cons.one': '{count} punto en contra', }; export default assistantJourney; diff --git a/shared/src/i18n/et/assistantJourney.ts b/shared/src/i18n/et/assistantJourney.ts index 6906dbe16..11d6e4693 100644 --- a/shared/src/i18n/et/assistantJourney.ts +++ b/shared/src/i18n/et/assistantJourney.ts @@ -54,5 +54,10 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.suggestionsGo.one': '{count} avatud soovitus kaob', 'assistant.card.journeyHeader.suggestionsCome': '{count} kohta tuleb soovitustena juurde', 'assistant.card.journeyHeader.suggestionsCome.one': '{count} koht tuleb soovitusena juurde', + 'assistant.change.verb.reorderJournalEntries': 'Järjesta sissekanded ümber', + 'assistant.card.journey.pros': '{count} plussi', + 'assistant.card.journey.pros.one': '{count} pluss', + 'assistant.card.journey.cons': '{count} miinust', + 'assistant.card.journey.cons.one': '{count} miinus', }; export default assistantJourney; diff --git a/shared/src/i18n/fr/assistantJourney.ts b/shared/src/i18n/fr/assistantJourney.ts index a444ed6a8..d3cbafb45 100644 --- a/shared/src/i18n/fr/assistantJourney.ts +++ b/shared/src/i18n/fr/assistantJourney.ts @@ -55,5 +55,10 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.suggestionsGo.one': '{count} suggestion ouverte disparaît', 'assistant.card.journeyHeader.suggestionsCome': '{count} lieux arrivent comme suggestions', 'assistant.card.journeyHeader.suggestionsCome.one': '{count} lieu arrive comme suggestion', + 'assistant.change.verb.reorderJournalEntries': 'Réordonner les entrées', + 'assistant.card.journey.pros': '{count} points forts', + 'assistant.card.journey.pros.one': '{count} point fort', + 'assistant.card.journey.cons': '{count} points faibles', + 'assistant.card.journey.cons.one': '{count} point faible', }; export default assistantJourney; diff --git a/shared/src/i18n/gr/assistantJourney.ts b/shared/src/i18n/gr/assistantJourney.ts index a42266cef..214a5bcb8 100644 --- a/shared/src/i18n/gr/assistantJourney.ts +++ b/shared/src/i18n/gr/assistantJourney.ts @@ -55,5 +55,10 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.suggestionsGo.one': 'Φεύγει {count} ανοιχτή πρόταση', 'assistant.card.journeyHeader.suggestionsCome': '{count} μέρη μπαίνουν ως προτάσεις', 'assistant.card.journeyHeader.suggestionsCome.one': '{count} μέρος μπαίνει ως πρόταση', + 'assistant.change.verb.reorderJournalEntries': 'Αναδιάταξη καταχωρίσεων', + 'assistant.card.journey.pros': '{count} θετικά', + 'assistant.card.journey.pros.one': '{count} θετικό', + 'assistant.card.journey.cons': '{count} αρνητικά', + 'assistant.card.journey.cons.one': '{count} αρνητικό', }; export default assistantJourney; diff --git a/shared/src/i18n/hu/assistantJourney.ts b/shared/src/i18n/hu/assistantJourney.ts index 93e78f1a3..0cae69d10 100644 --- a/shared/src/i18n/hu/assistantJourney.ts +++ b/shared/src/i18n/hu/assistantJourney.ts @@ -55,5 +55,10 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.suggestionsGo.one': '{count} nyitott javaslat eltűnik', 'assistant.card.journeyHeader.suggestionsCome': '{count} hely javaslatként érkezik', 'assistant.card.journeyHeader.suggestionsCome.one': '{count} hely javaslatként érkezik', + 'assistant.change.verb.reorderJournalEntries': 'Bejegyzések átrendezése', + 'assistant.card.journey.pros': '{count} előny', + 'assistant.card.journey.pros.one': '{count} előny', + 'assistant.card.journey.cons': '{count} hátrány', + 'assistant.card.journey.cons.one': '{count} hátrány', }; export default assistantJourney; diff --git a/shared/src/i18n/id/assistantJourney.ts b/shared/src/i18n/id/assistantJourney.ts index 41fb81836..51cdaed02 100644 --- a/shared/src/i18n/id/assistantJourney.ts +++ b/shared/src/i18n/id/assistantJourney.ts @@ -48,5 +48,8 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.entriesStay': '{count} entri tertulis tetap ada', 'assistant.card.journeyHeader.suggestionsGo': '{count} saran terbuka hilang', 'assistant.card.journeyHeader.suggestionsCome': '{count} tempat masuk sebagai saran', + 'assistant.change.verb.reorderJournalEntries': 'Urutkan ulang entri', + 'assistant.card.journey.pros': '{count} hal positif', + 'assistant.card.journey.cons': '{count} hal negatif', }; export default assistantJourney; diff --git a/shared/src/i18n/it/assistantJourney.ts b/shared/src/i18n/it/assistantJourney.ts index 82c3f40d1..400a8493f 100644 --- a/shared/src/i18n/it/assistantJourney.ts +++ b/shared/src/i18n/it/assistantJourney.ts @@ -54,5 +54,10 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.suggestionsGo.one': 'Sparisce {count} suggerimento aperto', 'assistant.card.journeyHeader.suggestionsCome': '{count} luoghi arrivano come suggerimenti', 'assistant.card.journeyHeader.suggestionsCome.one': '{count} luogo arriva come suggerimento', + 'assistant.change.verb.reorderJournalEntries': 'Riordina le voci', + 'assistant.card.journey.pros': '{count} punti a favore', + 'assistant.card.journey.pros.one': '{count} punto a favore', + 'assistant.card.journey.cons': '{count} punti a sfavore', + 'assistant.card.journey.cons.one': '{count} punto a sfavore', }; export default assistantJourney; diff --git a/shared/src/i18n/ja/assistantJourney.ts b/shared/src/i18n/ja/assistantJourney.ts index 54d5216ae..729788c8b 100644 --- a/shared/src/i18n/ja/assistantJourney.ts +++ b/shared/src/i18n/ja/assistantJourney.ts @@ -47,5 +47,8 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.entriesStay': '書かれたエントリー{count}件は残ります', 'assistant.card.journeyHeader.suggestionsGo': '未記入の提案{count}件はなくなります', 'assistant.card.journeyHeader.suggestionsCome': '{count}か所が提案として加わります', + 'assistant.change.verb.reorderJournalEntries': 'エントリーを並べ替える', + 'assistant.card.journey.pros': '良い点 {count} 件', + 'assistant.card.journey.cons': '残念な点 {count} 件', }; export default assistantJourney; diff --git a/shared/src/i18n/ko/assistantJourney.ts b/shared/src/i18n/ko/assistantJourney.ts index 4948d6946..010ebfe09 100644 --- a/shared/src/i18n/ko/assistantJourney.ts +++ b/shared/src/i18n/ko/assistantJourney.ts @@ -47,5 +47,8 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.entriesStay': '작성된 항목 {count}개는 남아요', 'assistant.card.journeyHeader.suggestionsGo': '열린 제안 {count}개는 사라져요', 'assistant.card.journeyHeader.suggestionsCome': '장소 {count}곳이 제안으로 들어와요', + 'assistant.change.verb.reorderJournalEntries': '항목 순서 바꾸기', + 'assistant.card.journey.pros': '좋은 점 {count}개', + 'assistant.card.journey.cons': '아쉬운 점 {count}개', }; export default assistantJourney; diff --git a/shared/src/i18n/nl/assistantJourney.ts b/shared/src/i18n/nl/assistantJourney.ts index 3a48bea57..bc776bd7e 100644 --- a/shared/src/i18n/nl/assistantJourney.ts +++ b/shared/src/i18n/nl/assistantJourney.ts @@ -54,5 +54,10 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.suggestionsGo.one': '{count} open suggestie verdwijnt', 'assistant.card.journeyHeader.suggestionsCome': '{count} plekken komen erbij als suggestie', 'assistant.card.journeyHeader.suggestionsCome.one': '{count} plek komt erbij als suggestie', + 'assistant.change.verb.reorderJournalEntries': 'Berichten opnieuw ordenen', + 'assistant.card.journey.pros': '{count} pluspunten', + 'assistant.card.journey.pros.one': '{count} pluspunt', + 'assistant.card.journey.cons': '{count} minpunten', + 'assistant.card.journey.cons.one': '{count} minpunt', }; export default assistantJourney; diff --git a/shared/src/i18n/pl/assistantJourney.ts b/shared/src/i18n/pl/assistantJourney.ts index 258d377f5..1fe81e17a 100644 --- a/shared/src/i18n/pl/assistantJourney.ts +++ b/shared/src/i18n/pl/assistantJourney.ts @@ -66,5 +66,14 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.suggestionsCome.one': '{count} miejsce dojdzie jako sugestia', 'assistant.card.journeyHeader.suggestionsCome.few': '{count} miejsca dojdą jako sugestie', 'assistant.card.journeyHeader.suggestionsCome.many': '{count} miejsc dojdzie jako sugestie', + 'assistant.change.verb.reorderJournalEntries': 'Zmień kolejność wpisów', + 'assistant.card.journey.pros': '{count} plusa', + 'assistant.card.journey.pros.one': '{count} plus', + 'assistant.card.journey.pros.few': '{count} plusy', + 'assistant.card.journey.pros.many': '{count} plusów', + 'assistant.card.journey.cons': '{count} minusa', + 'assistant.card.journey.cons.one': '{count} minus', + 'assistant.card.journey.cons.few': '{count} minusy', + 'assistant.card.journey.cons.many': '{count} minusów', }; export default assistantJourney; diff --git a/shared/src/i18n/ru/assistantJourney.ts b/shared/src/i18n/ru/assistantJourney.ts index cabfe103f..3ee1c5db5 100644 --- a/shared/src/i18n/ru/assistantJourney.ts +++ b/shared/src/i18n/ru/assistantJourney.ts @@ -66,5 +66,14 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.suggestionsCome.one': '{count} место появится как предложение', 'assistant.card.journeyHeader.suggestionsCome.few': '{count} места появятся как предложения', 'assistant.card.journeyHeader.suggestionsCome.many': '{count} мест появятся как предложения', + 'assistant.change.verb.reorderJournalEntries': 'Изменить порядок записей', + 'assistant.card.journey.pros': '{count} плюса', + 'assistant.card.journey.pros.one': '{count} плюс', + 'assistant.card.journey.pros.few': '{count} плюса', + 'assistant.card.journey.pros.many': '{count} плюсов', + 'assistant.card.journey.cons': '{count} минуса', + 'assistant.card.journey.cons.one': '{count} минус', + 'assistant.card.journey.cons.few': '{count} минуса', + 'assistant.card.journey.cons.many': '{count} минусов', }; export default assistantJourney; diff --git a/shared/src/i18n/sk/assistantJourney.ts b/shared/src/i18n/sk/assistantJourney.ts index c291ad92b..c8b7045e4 100644 --- a/shared/src/i18n/sk/assistantJourney.ts +++ b/shared/src/i18n/sk/assistantJourney.ts @@ -59,5 +59,12 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.suggestionsCome': '{count} miest pribudne ako návrhy', 'assistant.card.journeyHeader.suggestionsCome.one': '{count} miesto pribudne ako návrh', 'assistant.card.journeyHeader.suggestionsCome.few': '{count} miesta pribudnú ako návrhy', + 'assistant.change.verb.reorderJournalEntries': 'Preusporiadať záznamy', + 'assistant.card.journey.pros': '{count} plusov', + 'assistant.card.journey.pros.one': '{count} plus', + 'assistant.card.journey.pros.few': '{count} plusy', + 'assistant.card.journey.cons': '{count} mínusov', + 'assistant.card.journey.cons.one': '{count} mínus', + 'assistant.card.journey.cons.few': '{count} mínusy', }; export default assistantJourney; diff --git a/shared/src/i18n/sv/assistantJourney.ts b/shared/src/i18n/sv/assistantJourney.ts index adacafae8..33f1b2e71 100644 --- a/shared/src/i18n/sv/assistantJourney.ts +++ b/shared/src/i18n/sv/assistantJourney.ts @@ -53,5 +53,10 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.suggestionsGo.one': '{count} öppet förslag försvinner', 'assistant.card.journeyHeader.suggestionsCome': '{count} platser kommer in som förslag', 'assistant.card.journeyHeader.suggestionsCome.one': '{count} plats kommer in som förslag', + 'assistant.change.verb.reorderJournalEntries': 'Ordna om inläggen', + 'assistant.card.journey.pros': '{count} fördelar', + 'assistant.card.journey.pros.one': '{count} fördel', + 'assistant.card.journey.cons': '{count} nackdelar', + 'assistant.card.journey.cons.one': '{count} nackdel', }; export default assistantJourney; diff --git a/shared/src/i18n/th/assistantJourney.ts b/shared/src/i18n/th/assistantJourney.ts index 2a962a4b9..51f698b32 100644 --- a/shared/src/i18n/th/assistantJourney.ts +++ b/shared/src/i18n/th/assistantJourney.ts @@ -47,5 +47,8 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.entriesStay': 'รายการที่เขียนแล้ว {count} รายการยังอยู่', 'assistant.card.journeyHeader.suggestionsGo': 'คำแนะนำที่ยังไม่เขียน {count} รายการจะหายไป', 'assistant.card.journeyHeader.suggestionsCome': 'สถานที่ {count} แห่งจะเข้ามาเป็นคำแนะนำ', + 'assistant.change.verb.reorderJournalEntries': 'จัดลำดับรายการใหม่', + 'assistant.card.journey.pros': 'ข้อดี {count} ข้อ', + 'assistant.card.journey.cons': 'ข้อเสีย {count} ข้อ', }; export default assistantJourney; diff --git a/shared/src/i18n/tr/assistantJourney.ts b/shared/src/i18n/tr/assistantJourney.ts index da11a595f..8d5d50f70 100644 --- a/shared/src/i18n/tr/assistantJourney.ts +++ b/shared/src/i18n/tr/assistantJourney.ts @@ -54,5 +54,10 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.suggestionsGo.one': '{count} açık öneri kalkar', 'assistant.card.journeyHeader.suggestionsCome': '{count} yer öneri olarak eklenir', 'assistant.card.journeyHeader.suggestionsCome.one': '{count} yer öneri olarak eklenir', + 'assistant.change.verb.reorderJournalEntries': 'Kayıtları yeniden sırala', + 'assistant.card.journey.pros': '{count} artı', + 'assistant.card.journey.pros.one': '{count} artı', + 'assistant.card.journey.cons': '{count} eksi', + 'assistant.card.journey.cons.one': '{count} eksi', }; export default assistantJourney; diff --git a/shared/src/i18n/uk/assistantJourney.ts b/shared/src/i18n/uk/assistantJourney.ts index 949b49699..70b45f869 100644 --- a/shared/src/i18n/uk/assistantJourney.ts +++ b/shared/src/i18n/uk/assistantJourney.ts @@ -66,5 +66,14 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.suggestionsCome.one': '{count} місце з’явиться як пропозиція', 'assistant.card.journeyHeader.suggestionsCome.few': '{count} місця з’являться як пропозиції', 'assistant.card.journeyHeader.suggestionsCome.many': '{count} місць з’являться як пропозиції', + 'assistant.change.verb.reorderJournalEntries': 'Змінити порядок записів', + 'assistant.card.journey.pros': '{count} плюса', + 'assistant.card.journey.pros.one': '{count} плюс', + 'assistant.card.journey.pros.few': '{count} плюси', + 'assistant.card.journey.pros.many': '{count} плюсів', + 'assistant.card.journey.cons': '{count} мінуса', + 'assistant.card.journey.cons.one': '{count} мінус', + 'assistant.card.journey.cons.few': '{count} мінуси', + 'assistant.card.journey.cons.many': '{count} мінусів', }; export default assistantJourney; diff --git a/shared/src/i18n/vi/assistantJourney.ts b/shared/src/i18n/vi/assistantJourney.ts index 6dc40e0b9..b7d337bdf 100644 --- a/shared/src/i18n/vi/assistantJourney.ts +++ b/shared/src/i18n/vi/assistantJourney.ts @@ -48,5 +48,8 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.entriesStay': '{count} mục đã viết vẫn được giữ', 'assistant.card.journeyHeader.suggestionsGo': '{count} gợi ý chưa viết sẽ bị bỏ', 'assistant.card.journeyHeader.suggestionsCome': '{count} địa điểm được thêm vào làm gợi ý', + 'assistant.change.verb.reorderJournalEntries': 'Sắp xếp lại các mục', + 'assistant.card.journey.pros': '{count} điểm cộng', + 'assistant.card.journey.cons': '{count} điểm trừ', }; export default assistantJourney; diff --git a/shared/src/i18n/zh-TW/assistantJourney.ts b/shared/src/i18n/zh-TW/assistantJourney.ts index c0d215cad..edd781202 100644 --- a/shared/src/i18n/zh-TW/assistantJourney.ts +++ b/shared/src/i18n/zh-TW/assistantJourney.ts @@ -47,5 +47,8 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.entriesStay': '已寫的 {count} 則條目會保留', 'assistant.card.journeyHeader.suggestionsGo': '{count} 則未寫的建議會被移除', 'assistant.card.journeyHeader.suggestionsCome': '{count} 個地點會作為建議加入', + 'assistant.change.verb.reorderJournalEntries': '重新排列條目', + 'assistant.card.journey.pros': '{count} 個優點', + 'assistant.card.journey.cons': '{count} 個不足', }; export default assistantJourney; diff --git a/shared/src/i18n/zh/assistantJourney.ts b/shared/src/i18n/zh/assistantJourney.ts index 0d74c8d1f..55af3157e 100644 --- a/shared/src/i18n/zh/assistantJourney.ts +++ b/shared/src/i18n/zh/assistantJourney.ts @@ -47,5 +47,8 @@ const assistantJourney: TranslationStrings = { 'assistant.card.journeyHeader.entriesStay': '已写的 {count} 条条目会保留', 'assistant.card.journeyHeader.suggestionsGo': '{count} 条未写的建议会被移除', 'assistant.card.journeyHeader.suggestionsCome': '{count} 个地点会作为建议加入', + 'assistant.change.verb.reorderJournalEntries': '重新排列条目', + 'assistant.card.journey.pros': '{count} 个优点', + 'assistant.card.journey.cons': '{count} 个不足', }; export default assistantJourney;