feat(assistant): a card framework every addon package plugs into

This commit is contained in:
Maurice
2026-10-22 12:14:24 +02:00
parent d2a2728cde
commit 3967ca2790
514 changed files with 9868 additions and 1274 deletions
@@ -0,0 +1,44 @@
data: {"type":"start","messageId":"msg-12"}
data: {"type":"tool-output-available","toolCallId":"7c1e2d3a-0b4f-4e8a-9d6c-1f2e3a4b5c6d","output":{"tour":{"place_id":77,"name":"Ridge walk"}}}
data: {"type":"data-trek-applied","data":{"toolCallId":"7c1e2d3a-0b4f-4e8a-9d6c-1f2e3a4b5c6d","entity":"tour","ids":[77],"dayId":32,"undoUntil":"2026-06-11T09:15:00Z"}}
data: {"type":"data-trek-card","data":{"toolCallId":"7c1e2d3a-0b4f-4e8a-9d6c-1f2e3a4b5c6d","card":{"kind":"tourRoute"}}}
data: {"type":"start-step"}
data: {"type":"tool-input-start","toolCallId":"toolu_02","toolName":"list_tours"}
data: {"type":"tool-input-available","toolCallId":"toolu_02","toolName":"list_tours","input":{}}
data: {"type":"tool-output-available","toolCallId":"toolu_02","output":{"tours":[{"place_id":77,"name":"Ridge walk"}]}}
data: {"type":"data-trek-card","data":{"toolCallId":"toolu_02","card":{"kind":"tourList"}}}
data: {"type":"finish-step"}
data: {"type":"start-step"}
data: {"type":"text-start","id":"txt-0"}
data: {"type":"text-delta","id":"txt-0","delta":"The ridge walk is on Friday now. "}
data: {"type":"text-delta","id":"txt-0","delta":"A lake loop would fit on Saturday."}
data: {"type":"text-end","id":"txt-0"}
data: {"type":"tool-input-start","toolCallId":"9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d","toolName":"create_hike"}
data: {"type":"tool-input-available","toolCallId":"9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d","toolName":"create_hike","input":{"name":"Lake loop","round_trip":true}}
data: {"type":"tool-approval-request","approvalId":"9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d","toolCallId":"9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d"}
data: {"type":"data-trek-change","data":{"toolCallId":"9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d","verbKey":"addTour","subject":"Lake loop","rows":[{"op":"add","labelKey":"route","before":null,"after":"Car park → Lake → Car park"}],"dayId":null,"placeIds":[],"undoable":true,"card":{"kind":"tourRoute"}}}
data: {"type":"data-trek-quota","data":{"used":4,"limit":50}}
data: {"type":"finish","finishReason":"tool-calls"}
data: [DONE]
@@ -5,6 +5,7 @@ import {
assistantAddFoundPlaceResponseSchema,
assistantAttachmentRequestSchema,
assistantChangePreviewSchema,
assistantChangePreviewWriteSchema,
assistantChatRequestSchema,
assistantDecisionsRequestSchema,
assistantFoundPlaceSchema,
@@ -122,7 +123,13 @@ describe('assistantChangePreviewSchema', () => {
expect(
assistantChangePreviewSchema.safeParse({ ...preview, rows: [{ ...preview.rows[0], labelKey: 'evil' }] }).success,
).toBe(false);
expect(assistantChangePreviewSchema.safeParse({ ...preview, verbKey: 'deleteTrip' }).success).toBe(false);
expect(assistantChangePreviewWriteSchema.safeParse({ ...preview, verbKey: 'deleteTrip' }).success).toBe(false);
});
it('reads a verb a later build added as a plain update, and writes none it does not know', () => {
expect(assistantChangePreviewSchema.parse({ ...preview, verbKey: 'laterVerb' }).verbKey).toBe('genericUpdate');
expect(assistantChangePreviewWriteSchema.parse(preview).verbKey).toBe('addPlace');
expect(assistantChangePreviewWriteSchema.safeParse({ ...preview, card: { kind: 'nope' } }).success).toBe(false);
});
it('holds at most twelve rows', () => {
+173 -44
View File
@@ -1,4 +1,31 @@
import { idParamSchema, idSchema } from '../common/primitives.schema';
import {
assistantCardSchema,
assistantCardWriteSchema,
ATLAS_APPLIED_ENTITIES,
ATLAS_VERB_KEYS,
BOOKING_APPLIED_ENTITIES,
BOOKING_VERB_KEYS,
BUDGET_APPLIED_ENTITIES,
BUDGET_VERB_KEYS,
COLLAB_APPLIED_ENTITIES,
COLLAB_VERB_KEYS,
COLLECTION_APPLIED_ENTITIES,
COLLECTION_VERB_KEYS,
JOURNEY_APPLIED_ENTITIES,
JOURNEY_VERB_KEYS,
PACKING_APPLIED_ENTITIES,
PACKING_VERB_KEYS,
ROADTRIP_APPLIED_ENTITIES,
ROADTRIP_VERB_KEYS,
storedTrekCardSchema,
TOUR_APPLIED_ENTITIES,
TOUR_VERB_KEYS,
trekCardSchema,
VACAY_APPLIED_ENTITIES,
VACAY_VERB_KEYS,
type AssistantCard,
} from './cards';
import { z } from 'zod';
@@ -48,9 +75,10 @@ export type AssistantErrorCode = z.infer<typeof assistantErrorCodeSchema>;
/**
* What a change does, one per writing tool. The client labels the card with
* `assistant.change.verb.<key>`.
* `assistant.change.verb.<key>`. These are the core verbs; each part of TREK
* adds its own in its cards contract (`cards/<pkg>.contract.ts`).
*/
export const ASSISTANT_CHANGE_VERB_KEYS = [
export const CORE_VERB_KEYS = [
'addPlace',
'addPlaceToDay',
'updatePlace',
@@ -76,6 +104,19 @@ export const ASSISTANT_CHANGE_VERB_KEYS = [
'genericUpdate',
'genericRemove',
] as const;
export const ASSISTANT_CHANGE_VERB_KEYS = [
...CORE_VERB_KEYS,
...TOUR_VERB_KEYS,
...ROADTRIP_VERB_KEYS,
...BUDGET_VERB_KEYS,
...BOOKING_VERB_KEYS,
...COLLAB_VERB_KEYS,
...COLLECTION_VERB_KEYS,
...ATLAS_VERB_KEYS,
...VACAY_VERB_KEYS,
...JOURNEY_VERB_KEYS,
...PACKING_VERB_KEYS,
] as const;
export const assistantChangeVerbKeySchema = z.enum(ASSISTANT_CHANGE_VERB_KEYS);
export type AssistantChangeVerbKey = z.infer<typeof assistantChangeVerbKeySchema>;
@@ -233,6 +274,19 @@ export const assistantAddFoundPlaceRequestSchema = assistantClientCtxSchema
.strict();
export type AssistantAddFoundPlaceRequest = z.infer<typeof assistantAddFoundPlaceRequestSchema>;
/**
* POST /threads/:threadId/proposals: take a proposal an answer showed as a
* card ("Plan this"). `toolCallId` names the read whose card it was; the
* server holds the matching change like a found place, from what it stored,
* and answers like the found places route.
*/
export const assistantTakeProposalRequestSchema = assistantClientCtxSchema
.extend({
toolCallId: z.string().min(1).max(64),
})
.strict();
export type AssistantTakeProposalRequest = z.infer<typeof assistantTakeProposalRequestSchema>;
// ── Change previews ─────────────────────────────────────────────────────────
/**
@@ -269,8 +323,8 @@ export const assistantChangeRowSchema = z.object({
});
export type AssistantChangeRow = z.infer<typeof assistantChangeRowSchema>;
/** A held writing call as the user sees it before deciding. */
export const assistantChangePreviewSchema = z.object({
/** A held writing call as the server writes it: every verb and card one this build knows. */
export const assistantChangePreviewWriteSchema = z.object({
toolCallId: z.string(),
verbKey: assistantChangeVerbKeySchema,
// What the change is about: the place, note or item name.
@@ -286,28 +340,63 @@ export const assistantChangePreviewSchema = z.object({
// The tool behind a card without one of its own, and the part of TREK it works in.
tool: z.string().max(80).optional(),
domain: z.string().max(40).optional(),
// What the change will do, drawn as a card; absent on older rows and on cards without one.
card: trekCardSchema.nullable().optional(),
});
/**
* A held writing call as the user sees it before deciding, and as it is read
* back: a verb a later build added reads as a plain update and a card this
* build cannot draw as none, so an older client or a rolled back server still
* shows the change instead of dropping it.
*/
export const assistantChangePreviewSchema = assistantChangePreviewWriteSchema.extend({
verbKey: assistantChangeVerbKeySchema.catch('genericUpdate'),
card: storedTrekCardSchema.optional(),
});
export type AssistantChangePreview = z.infer<typeof assistantChangePreviewSchema>;
/** An applied change: what it touched, and until when it can be undone. */
export const assistantAppliedSchema = z.object({
/** What an applied change can touch in the core; each part of TREK adds its own in its cards contract. */
export const CORE_APPLIED_ENTITIES = [
'place',
'assignment',
'day_note',
'packing',
'todo',
'budget',
'reservation',
'memory',
'tour',
'other',
] as const;
const assistantAppliedEntitySchema = z.enum([
...CORE_APPLIED_ENTITIES,
...TOUR_APPLIED_ENTITIES,
...ROADTRIP_APPLIED_ENTITIES,
...BUDGET_APPLIED_ENTITIES,
...BOOKING_APPLIED_ENTITIES,
...COLLAB_APPLIED_ENTITIES,
...COLLECTION_APPLIED_ENTITIES,
...ATLAS_APPLIED_ENTITIES,
...VACAY_APPLIED_ENTITIES,
...JOURNEY_APPLIED_ENTITIES,
...PACKING_APPLIED_ENTITIES,
]);
/** An applied change as the server writes it: what it touched, and until when it can be undone. */
const assistantAppliedWriteSchema = z.object({
toolCallId: z.string(),
entity: z.enum([
'place',
'assignment',
'day_note',
'packing',
'todo',
'budget',
'reservation',
'memory',
'tour',
'other',
]),
entity: assistantAppliedEntitySchema,
ids: z.array(idSchema),
dayId: idSchema.nullable(),
undoUntil: z.string().nullable(),
});
/** An applied change as it is read back: an entity a later build added reads as `other`, with nothing to open. */
export const assistantAppliedSchema = assistantAppliedWriteSchema.extend({
entity: assistantAppliedEntitySchema.catch('other'),
});
export type AssistantApplied = z.infer<typeof assistantAppliedSchema>;
// ── Cards in an answer ──────────────────────────────────────────────────────
@@ -453,34 +542,73 @@ export const assistantToolApprovalSchema = z.object({
});
export type AssistantToolApproval = z.infer<typeof assistantToolApprovalSchema>;
/** One stored part of a message, in the order the model produced it. */
export const assistantMessagePartSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('text'), text: z.string() }),
// A file the user handed the assistant. Only its description is kept, never its content.
z.object({
type: z.literal('attachment'),
name: z.string(),
mime: z.string(),
size: z.number().int().nonnegative(),
}),
z.object({
type: z.literal('tool-call'),
toolCallId: z.string(),
toolName: z.string(),
args: z.record(z.string(), z.unknown()),
result: z.unknown().optional(),
isError: z.boolean().optional(),
approval: assistantToolApprovalSchema.optional(),
}),
z.object({ type: z.literal('data-trek-change'), data: assistantChangePreviewSchema }),
z.object({ type: z.literal('data-trek-applied'), data: assistantAppliedSchema }),
z.object({ type: z.literal('data-trek-places'), data: assistantPlacesSchema }),
z.object({ type: z.literal('data-trek-weather'), data: assistantWeatherSchema }),
// Only on a user message.
z.object({ type: z.literal('data-trek-mentions'), data: assistantMentionsSchema }),
]);
/** The parts of a message, with the change, receipt and card parts read or written as given. */
function messagePartSchema(
change: z.ZodType<AssistantChangePreview>,
applied: z.ZodType<AssistantApplied>,
card: z.ZodType<AssistantCard>,
) {
return z.discriminatedUnion('type', [
z.object({ type: z.literal('text'), text: z.string() }),
// A file the user handed the assistant. Only its description is kept, never its content.
z.object({
type: z.literal('attachment'),
name: z.string(),
mime: z.string(),
size: z.number().int().nonnegative(),
}),
z.object({
type: z.literal('tool-call'),
toolCallId: z.string(),
toolName: z.string(),
args: z.record(z.string(), z.unknown()),
result: z.unknown().optional(),
isError: z.boolean().optional(),
approval: assistantToolApprovalSchema.optional(),
}),
z.object({ type: z.literal('data-trek-change'), data: change }),
z.object({ type: z.literal('data-trek-applied'), data: applied }),
z.object({ type: z.literal('data-trek-places'), data: assistantPlacesSchema }),
z.object({ type: z.literal('data-trek-weather'), data: assistantWeatherSchema }),
// The card of one tool call: a read's beside the answer, an applied write's on the message that proposed it.
z.object({ type: z.literal('data-trek-card'), data: card }),
// Only on a user message.
z.object({ type: z.literal('data-trek-mentions'), data: assistantMentionsSchema }),
]);
}
/** One stored part of a message, in the order the model produced it, as a reader takes it. */
export const assistantMessagePartSchema = messagePartSchema(
assistantChangePreviewSchema,
assistantAppliedSchema,
assistantCardSchema,
);
export type AssistantMessagePart = z.infer<typeof assistantMessagePartSchema>;
/** The parts as the server writes them: a verb, entity or card this build cannot name is a bug in the writer. */
export const assistantMessagePartWriteSchema = messagePartSchema(
assistantChangePreviewWriteSchema,
assistantAppliedWriteSchema,
assistantCardWriteSchema,
);
/**
* A message's parts as they are read back: a `data-trek-*` part this build
* does not know, or one that no longer reads, is dropped, so a part a later
* build added never blanks the message around it. Any other broken part
* still fails the whole list. A known part reads through its tolerant fields
* (a later verb, entity or card kind); parts are written strictly, with
* `assistantMessagePartWriteSchema`.
*/
export const assistantStoredPartsSchema = z
.array(
z.union([
assistantMessagePartSchema,
z.object({ type: z.string().startsWith('data-trek-') }).transform(() => null),
]),
)
.transform((parts) => parts.filter((part): part is AssistantMessagePart => part !== null));
export const assistantMessageSchema = z.object({
id: z.string(),
role: z.enum(['user', 'assistant']),
@@ -539,6 +667,7 @@ export const uiStreamChunkSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('data-trek-applied'), data: assistantAppliedSchema }),
z.object({ type: z.literal('data-trek-places'), data: assistantPlacesSchema }),
z.object({ type: z.literal('data-trek-weather'), data: assistantWeatherSchema }),
z.object({ type: z.literal('data-trek-card'), data: assistantCardSchema }),
// Live progress of a running step; the message never keeps it.
z.object({
type: z.literal('data-trek-activity'),
@@ -0,0 +1,5 @@
import { z } from 'zod';
/** Entries of the bucket list. Only its kind so far; the atlas cards give it its fields. */
export const atlasBucketCardSchema = z.object({ kind: z.literal('atlasBucket') });
export type AtlasBucketCard = z.infer<typeof atlasBucketCardSchema>;
@@ -0,0 +1,5 @@
import { z } from 'zod';
/** One country of the atlas. Only its kind so far; the atlas cards give it its fields. */
export const atlasCountryCardSchema = z.object({ kind: z.literal('atlasCountry') });
export type AtlasCountryCard = z.infer<typeof atlasCountryCardSchema>;
@@ -0,0 +1,8 @@
import { z } from 'zod';
/**
* The countries and regions a user has been to. Only its kind so far; the atlas cards give it its
* fields.
*/
export const atlasPassportCardSchema = z.object({ kind: z.literal('atlasPassport') });
export type AtlasPassportCard = z.infer<typeof atlasPassportCardSchema>;
@@ -0,0 +1,18 @@
import { atlasBucketCardSchema } from './atlas-bucket.card';
import { atlasCountryCardSchema } from './atlas-country.card';
import { atlasPassportCardSchema } from './atlas-passport.card';
export * from './atlas-bucket.card';
export * from './atlas-country.card';
export * from './atlas-passport.card';
/**
* What the atlas brings to the assistant chat: the kinds of card it draws,
* the verbs of its change cards and what an applied change of its
* touches, beyond the core ones. cards/index.ts and assistant.schema.ts
* spread these into the card union and the shared enums, so a new kind, verb
* or entity is added here and nowhere else.
*/
export const ATLAS_CARD_SCHEMAS = [atlasPassportCardSchema, atlasCountryCardSchema, atlasBucketCardSchema] as const;
export const ATLAS_VERB_KEYS = [] as const;
export const ATLAS_APPLIED_ENTITIES = [] as const;
@@ -0,0 +1,8 @@
import { z } from 'zod';
/**
* Bookings: flights, trains, stays and other reservations. Only its kind so far; the booking cards
* give it its fields.
*/
export const bookingCardSchema = z.object({ kind: z.literal('booking') });
export type BookingCard = z.infer<typeof bookingCardSchema>;
@@ -0,0 +1,14 @@
import { bookingCardSchema } from './booking.card';
export * from './booking.card';
/**
* What the bookings bring to the assistant chat: the kinds of card they draw,
* the verbs of their change cards and what an applied change of theirs
* touches, beyond the core ones. cards/index.ts and assistant.schema.ts
* spread these into the card union and the shared enums, so a new kind, verb
* or entity is added here and nowhere else.
*/
export const BOOKING_CARD_SCHEMAS = [bookingCardSchema] as const;
export const BOOKING_VERB_KEYS = [] as const;
export const BOOKING_APPLIED_ENTITIES = [] as const;
@@ -0,0 +1,25 @@
import { costPerPersonCardSchema } from './cost-per-person.card';
import { expensesCardSchema } from './expenses.card';
import { paymentsCardSchema } from './payments.card';
import { settlementCardSchema } from './settlement.card';
export * from './cost-per-person.card';
export * from './expenses.card';
export * from './payments.card';
export * from './settlement.card';
/**
* What the budget brings to the assistant chat: the kinds of card it draws,
* the verbs of its change cards and what an applied change of its
* touches, beyond the core ones. cards/index.ts and assistant.schema.ts
* spread these into the card union and the shared enums, so a new kind, verb
* or entity is added here and nowhere else.
*/
export const BUDGET_CARD_SCHEMAS = [
expensesCardSchema,
paymentsCardSchema,
settlementCardSchema,
costPerPersonCardSchema,
] as const;
export const BUDGET_VERB_KEYS = [] as const;
export const BUDGET_APPLIED_ENTITIES = [] as const;
+58
View File
@@ -0,0 +1,58 @@
import { idSchema } from '../../common/primitives.schema';
import { z } from 'zod';
/**
* The pieces every card of the assistant chat is built from. A kind uses
* these, `idSchema` and its domain's own enums, nothing else, so every card
* bounds its text the same way and none of them carries a URL or a path: a
* picture travels as a key the client turns into TREK's own address.
*/
/** One line of text, clipped and masked by the server before it is sent. */
export const cardText = (max: number) => z.string().min(1).max(max);
export const cardLatSchema = z.number().min(-90).max(90);
export const cardLngSchema = z.number().min(-180).max(180);
/** A point of a line to draw, `[lat, lng]` at five decimals; single points are `{ lat, lng }` objects. */
export const cardPointSchema = z.tuple([cardLatSchema, cardLngSchema]);
export const cardDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
export const cardClockSchema = z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/);
export const cardInstantSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
/** A colour the data carries (a member's, a category's); the client draws it through safeHexColor. */
export const cardHexColorSchema = z.string().regex(/^#[0-9a-fA-F]{6}$/);
export const cardCurrencySchema = z.string().regex(/^[A-Z]{3}$/);
export const cardMoneySchema = z.object({ amount: z.number().min(-1e9).max(1e9), currency: cardCurrencySchema });
export const cardCountSchema = z.number().int().min(0).max(1_000_000);
/** A stored file name (an avatar, a place image): the client builds TREK's own path. Never a URL, never a path. */
export const cardFileKeySchema = z.string().regex(/^(?!.*\.\.)[A-Za-z0-9][\w.-]{0,199}$/);
/** A Google place id for TREK's photo proxy, like a found place's photo key without `coords:`. */
export const cardPhotoKeySchema = z.string().regex(/^[\w-]{1,200}$/);
/** A traveller as a card shows them: the client compares `id` with the signed-in user, so no card says "me". */
export const cardPersonSchema = z.object({
id: idSchema,
name: cardText(80),
avatarKey: cardFileKeySchema.nullable(),
});
/** A trip day: its id for links, its number or date for words. */
export const cardDaySchema = z.object({
id: idSchema.nullable(),
number: z.number().int().min(1).max(999).nullable(),
date: cardDateSchema.nullable(),
});
/** The most one card may weigh, as JSON text; a heavier one is not sent at all. */
export const MAX_CARD_CHARS = 24_000;
export type CardPoint = z.infer<typeof cardPointSchema>;
export type CardMoney = z.infer<typeof cardMoneySchema>;
export type CardPerson = z.infer<typeof cardPersonSchema>;
export type CardDay = z.infer<typeof cardDaySchema>;
@@ -0,0 +1,8 @@
import { z } from 'zod';
/**
* Charging stops for an electric car along a drive. Only its kind so far; the road trip cards give it
* its fields.
*/
export const chargingCardSchema = z.object({ kind: z.literal('charging') });
export type ChargingCard = z.infer<typeof chargingCardSchema>;
@@ -0,0 +1,8 @@
import { z } from 'zod';
/**
* Messages of the trip chat, as the chat shows them. Only its kind so far; the trip chat cards give it
* its fields.
*/
export const collabChatCardSchema = z.object({ kind: z.literal('collabChat') });
export type CollabChatCard = z.infer<typeof collabChatCardSchema>;
@@ -0,0 +1,5 @@
import { z } from 'zod';
/** Shared notes of the trip. Only its kind so far; the trip chat cards give it its fields. */
export const collabNotesCardSchema = z.object({ kind: z.literal('collabNotes') });
export type CollabNotesCard = z.infer<typeof collabNotesCardSchema>;
@@ -0,0 +1,8 @@
import { z } from 'zod';
/**
* Polls of the trip chat with their options. Only its kind so far; the trip chat cards give it its
* fields.
*/
export const collabPollsCardSchema = z.object({ kind: z.literal('collabPolls') });
export type CollabPollsCard = z.infer<typeof collabPollsCardSchema>;
@@ -0,0 +1,18 @@
import { collabChatCardSchema } from './collab-chat.card';
import { collabNotesCardSchema } from './collab-notes.card';
import { collabPollsCardSchema } from './collab-polls.card';
export * from './collab-chat.card';
export * from './collab-notes.card';
export * from './collab-polls.card';
/**
* What the trip chat brings to the assistant chat: the kinds of card it draws,
* the verbs of its change cards and what an applied change of its
* touches, beyond the core ones. cards/index.ts and assistant.schema.ts
* spread these into the card union and the shared enums, so a new kind, verb
* or entity is added here and nowhere else.
*/
export const COLLAB_CARD_SCHEMAS = [collabChatCardSchema, collabPollsCardSchema, collabNotesCardSchema] as const;
export const COLLAB_VERB_KEYS = [] as const;
export const COLLAB_APPLIED_ENTITIES = [] as const;
@@ -0,0 +1,5 @@
import { z } from 'zod';
/** The collections a user keeps. Only its kind so far; the collections cards give it its fields. */
export const collectionListsCardSchema = z.object({ kind: z.literal('collectionLists') });
export type CollectionListsCard = z.infer<typeof collectionListsCardSchema>;
@@ -0,0 +1,5 @@
import { z } from 'zod';
/** Places of a collection. Only its kind so far; the collections cards give it its fields. */
export const collectionPlacesCardSchema = z.object({ kind: z.literal('collectionPlaces') });
export type CollectionPlacesCard = z.infer<typeof collectionPlacesCardSchema>;
@@ -0,0 +1,16 @@
import { collectionListsCardSchema } from './collection-lists.card';
import { collectionPlacesCardSchema } from './collection-places.card';
export * from './collection-lists.card';
export * from './collection-places.card';
/**
* What the collections bring to the assistant chat: the kinds of card they draw,
* the verbs of their change cards and what an applied change of theirs
* touches, beyond the core ones. cards/index.ts and assistant.schema.ts
* spread these into the card union and the shared enums, so a new kind, verb
* or entity is added here and nowhere else.
*/
export const COLLECTION_CARD_SCHEMAS = [collectionPlacesCardSchema, collectionListsCardSchema] as const;
export const COLLECTION_VERB_KEYS = [] as const;
export const COLLECTION_APPLIED_ENTITIES = [] as const;
@@ -0,0 +1,8 @@
import { z } from 'zod';
/**
* What lies along a drive, within reach of its route. Only its kind so far; the road trip cards give
* it its fields.
*/
export const corridorCardSchema = z.object({ kind: z.literal('corridor') });
export type CorridorCard = z.infer<typeof corridorCardSchema>;
@@ -0,0 +1,5 @@
import { z } from 'zod';
/** What the trip costs each traveller. Only its kind so far; the budget cards give it its fields. */
export const costPerPersonCardSchema = z.object({ kind: z.literal('costPerPerson') });
export type CostPerPersonCard = z.infer<typeof costPerPersonCardSchema>;
@@ -0,0 +1,5 @@
import { z } from 'zod';
/** One day's route between its places. Only its kind so far; the road trip cards give it its fields. */
export const dayRouteCardSchema = z.object({ kind: z.literal('dayRoute') });
export type DayRouteCard = z.infer<typeof dayRouteCardSchema>;
@@ -0,0 +1,8 @@
import { z } from 'zod';
/**
* Expenses of the trip, with who paid and who shares them. Only its kind so far; the budget cards give
* it its fields.
*/
export const expensesCardSchema = z.object({ kind: z.literal('expenses') });
export type ExpensesCard = z.infer<typeof expensesCardSchema>;
+209
View File
@@ -0,0 +1,209 @@
import {
ASSISTANT_CHANGE_VERB_KEYS,
assistantAppliedSchema,
assistantChangePreviewSchema,
assistantMessagePartSchema,
assistantMessagePartWriteSchema,
assistantStoredPartsSchema,
assistantTakeProposalRequestSchema,
CORE_APPLIED_ENTITIES,
CORE_VERB_KEYS,
uiStreamChunkSchema,
UI_STREAM_DONE,
} from '../assistant.schema';
import { cardFileKeySchema, cardPersonSchema, cardText, MAX_CARD_CHARS } from './card-parts';
import { assistantCardSchema, storedTrekCardSchema, trekCardSchema } from './index';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
import { z } from 'zod';
const CALL = 'call_1';
const preview = {
toolCallId: CALL,
verbKey: 'addTour',
subject: 'Lake loop',
rows: [],
dayId: null,
placeIds: [],
undoable: true,
};
/** Every kind as its package declares it today, built from its own literal. */
const KINDS = trekCardSchema.options.map((option) => option.shape.kind.value);
/**
* A kind as the packages build theirs, from the card parts, with bounds to
* cross: the read tolerance is the same for every kind, so these cases hold
* whatever fields the packages give their own.
*/
const probeSchema = z
.discriminatedUnion('kind', [
z.object({ kind: z.literal('probe'), title: cardText(8), people: z.array(cardPersonSchema).max(2) }),
])
.nullable()
.catch(null);
describe('the card union', () => {
it('CARDS-001: holds every kind once, each read as itself', () => {
expect(new Set(KINDS).size).toBe(KINDS.length);
expect(KINDS).toEqual(expect.arrayContaining(['tourRoute', 'tourList', 'packingList', 'todoList']));
for (const kind of KINDS) {
const read = storedTrekCardSchema.parse({ kind });
if (read !== null) expect(read.kind).toBe(kind);
}
});
it('CARDS-002: reads an unknown kind, a missing kind or no card at all as null', () => {
for (const card of [{ kind: 'bookPage' }, {}, null, undefined, 'tourRoute', 7]) {
expect(storedTrekCardSchema.parse(card)).toBeNull();
}
});
it('CARDS-003: reads a field past its bound as null, and drops keys a kind does not declare', () => {
expect(probeSchema.parse({ kind: 'probe', title: 'Far too long a title', people: [] })).toBeNull();
const four = [1, 2, 3, 4].map((id) => ({ id, name: `P${id}`, avatarKey: null }));
expect(probeSchema.parse({ kind: 'probe', title: 'Walk', people: four })).toBeNull();
expect(probeSchema.parse({ kind: 'probe', title: 'Walk', people: [], url: 'https://x.test' })).toEqual({
kind: 'probe',
title: 'Walk',
people: [],
});
});
it('CARDS-004: a stored file key is a file name, never a path, a URL or a way up', () => {
expect(cardFileKeySchema.safeParse('a1b2c3.jpg').success).toBe(true);
for (const key of ['../secret', 'avatars/a.jpg', 'https://x.test/a.jpg', '.hidden', 'a..b', '']) {
expect(cardFileKeySchema.safeParse(key).success).toBe(false);
}
expect(MAX_CARD_CHARS).toBe(24_000);
});
});
describe('cards in parts, chunks and previews', () => {
it('CARDS-010: a card part reads an unknown, a kindless or a broken card as null and keeps the part', () => {
for (const card of [{ kind: 'bookPage', page: 3 }, { title: 'x' }, 'nope']) {
const part = assistantMessagePartSchema.parse({ type: 'data-trek-card', data: { toolCallId: CALL, card } });
expect(part).toEqual({ type: 'data-trek-card', data: { toolCallId: CALL, card: null } });
}
});
it('CARDS-011: a card chunk reads the same way, and a known card comes through with extra keys dropped', () => {
const unknown = uiStreamChunkSchema.parse({
type: 'data-trek-card',
data: { toolCallId: CALL, card: { kind: 'x' } },
});
expect(unknown).toEqual({ type: 'data-trek-card', data: { toolCallId: CALL, card: null } });
const known = assistantCardSchema.parse({ toolCallId: CALL, card: { kind: KINDS[0], later: 1 }, extra: true });
expect(known).not.toHaveProperty('extra');
if (known.card !== null) expect(known.card).not.toHaveProperty('later');
});
it('CARDS-012: a change preview reads without a card, and with a broken one as null', () => {
expect(assistantChangePreviewSchema.parse(preview)).not.toHaveProperty('card');
expect(assistantChangePreviewSchema.parse({ ...preview, card: { kind: 'nope' } }).card).toBeNull();
expect(assistantChangePreviewSchema.parse({ ...preview, card: { title: 'no kind' } }).card).toBeNull();
});
it('CARDS-014: a verb or an entity a later build added reads as a plain update and as other', () => {
const later = { ...preview, verbKey: 'laterVerb' };
expect(assistantChangePreviewSchema.parse(later).verbKey).toBe('genericUpdate');
const receipt = { toolCallId: CALL, entity: 'later_entity', ids: [4], dayId: null, undoUntil: null };
expect(assistantAppliedSchema.parse(receipt).entity).toBe('other');
const parts = assistantStoredPartsSchema.parse([
{ type: 'data-trek-change', data: later },
{ type: 'data-trek-applied', data: receipt },
]);
expect(parts.map((part) => part.type)).toEqual(['data-trek-change', 'data-trek-applied']);
});
it('CARDS-013: a card is always under its call, whose id is 1 to 64 characters', () => {
expect(assistantCardSchema.safeParse({ toolCallId: '', card: null }).success).toBe(false);
expect(assistantCardSchema.safeParse({ toolCallId: 'x'.repeat(65), card: null }).success).toBe(false);
});
});
describe('stored parts', () => {
it('CARDS-020: drop a data part this build does not know, or one that no longer reads, and keep the rest', () => {
const parts = assistantStoredPartsSchema.parse([
{ type: 'text', text: 'Here is the walk.' },
{ type: 'data-trek-book-page', data: { page: 3 } },
{ type: 'data-trek-places', data: { toolCallId: CALL } },
{ type: 'data-trek-card', data: { toolCallId: CALL, card: { kind: 'future' } } },
{ type: 'data-trek-change', data: preview },
]);
expect(parts.map((part) => part.type)).toEqual(['text', 'data-trek-card', 'data-trek-change']);
});
it('CARDS-021: still refuse a broken part that is not a data part', () => {
expect(assistantStoredPartsSchema.safeParse([{ type: 'text', text: 5 }]).success).toBe(false);
expect(assistantStoredPartsSchema.safeParse([{ type: 'tool-call' }]).success).toBe(false);
});
it('CARDS-022: are written strictly: an unknown part is no part', () => {
expect(assistantMessagePartSchema.safeParse({ type: 'data-trek-book-page', data: {} }).success).toBe(false);
expect(assistantMessagePartWriteSchema.safeParse({ type: 'data-trek-book-page', data: {} }).success).toBe(false);
});
it('CARDS-023: are written strictly: no verb, entity or card kind this build does not know', () => {
const write = (part: unknown) => assistantMessagePartWriteSchema.safeParse(part).success;
expect(write({ type: 'data-trek-change', data: preview })).toBe(true);
expect(write({ type: 'data-trek-change', data: { ...preview, verbKey: 'laterVerb' } })).toBe(false);
expect(write({ type: 'data-trek-change', data: { ...preview, card: { kind: 'nope' } } })).toBe(false);
const receipt = { toolCallId: CALL, entity: 'later_entity', ids: [4], dayId: null, undoUntil: null };
expect(write({ type: 'data-trek-applied', data: receipt })).toBe(false);
expect(write({ type: 'data-trek-card', data: { toolCallId: CALL, card: { kind: 'nope' } } })).toBe(false);
expect(write({ type: 'data-trek-card', data: { toolCallId: CALL, card: null } })).toBe(true);
});
});
describe('the enums the packages extend', () => {
it('CARDS-030: keep every core verb and entity, the hike verbs among them', () => {
expect(ASSISTANT_CHANGE_VERB_KEYS).toEqual(expect.arrayContaining([...CORE_VERB_KEYS]));
expect(ASSISTANT_CHANGE_VERB_KEYS).toEqual(expect.arrayContaining(['addTour', 'updateTour', 'importTour']));
expect(new Set(ASSISTANT_CHANGE_VERB_KEYS).size).toBe(ASSISTANT_CHANGE_VERB_KEYS.length);
for (const entity of CORE_APPLIED_ENTITIES) {
expect(assistantAppliedSchema.shape.entity.safeParse(entity).success).toBe(true);
}
});
});
describe('assistantTakeProposalRequestSchema', () => {
it('CARDS-040: names the read whose proposal is taken, with the turn context and nothing else', () => {
const ctx = { locale: 'en', timeZone: 'Europe/Berlin' };
expect(assistantTakeProposalRequestSchema.parse({ ...ctx, toolCallId: CALL })).toEqual({
...ctx,
toolCallId: CALL,
});
expect(assistantTakeProposalRequestSchema.safeParse({ ...ctx, toolCallId: '' }).success).toBe(false);
expect(assistantTakeProposalRequestSchema.safeParse({ ...ctx, toolCallId: CALL, args: {} }).success).toBe(false);
expect(assistantTakeProposalRequestSchema.safeParse({ toolCallId: CALL }).success).toBe(false);
});
});
describe('ui-stream.cards.v1 fixture', () => {
const text = readFileSync(resolve(__dirname, '../__fixtures__/ui-stream.cards.v1.txt'), 'utf8');
const events = text
.split('\n\n')
.filter(Boolean)
.map((event) => event.slice('data: '.length));
const chunks = events.slice(0, -1).map((event) => uiStreamChunkSchema.parse(JSON.parse(event)));
it('CARDS-050: is a stream of valid chunks closed by [DONE]', () => {
expect(events.at(-1)).toBe(UI_STREAM_DONE);
expect(chunks.length).toBeGreaterThan(10);
});
it('CARDS-051: carries an applied write card after its receipt, a read card after its answer, a held one in its preview', () => {
const types = chunks.map((chunk) => chunk.type);
const cards = chunks.flatMap((chunk) => (chunk.type === 'data-trek-card' ? [chunk.data.toolCallId] : []));
const applied = chunks.find((chunk) => chunk.type === 'data-trek-applied');
const read = chunks.find((chunk) => chunk.type === 'tool-input-start' && chunk.toolName === 'list_tours');
expect(applied?.type === 'data-trek-applied' && applied.data.toolCallId).toBe(cards[0]);
expect(read?.type === 'tool-input-start' && read.toolCallId).toBe(cards[1]);
expect(types.indexOf('data-trek-applied')).toBeLessThan(types.indexOf('data-trek-card'));
const change = chunks.find((chunk) => chunk.type === 'data-trek-change');
expect(change?.type === 'data-trek-change' && 'card' in change.data).toBe(true);
});
});
+66
View File
@@ -0,0 +1,66 @@
import { ATLAS_CARD_SCHEMAS } from './atlas.contract';
import { BOOKING_CARD_SCHEMAS } from './booking.contract';
import { BUDGET_CARD_SCHEMAS } from './budget.contract';
import { COLLAB_CARD_SCHEMAS } from './collab.contract';
import { COLLECTION_CARD_SCHEMAS } from './collection.contract';
import { JOURNEY_CARD_SCHEMAS } from './journey.contract';
import { PACKING_CARD_SCHEMAS } from './packing.contract';
import { ROADTRIP_CARD_SCHEMAS } from './roadtrip.contract';
import { TOUR_CARD_SCHEMAS } from './tour.contract';
import { VACAY_CARD_SCHEMAS } from './vacay.contract';
import { z } from 'zod';
export * from './card-parts';
export * from './atlas.contract';
export * from './booking.contract';
export * from './budget.contract';
export * from './collab.contract';
export * from './collection.contract';
export * from './journey.contract';
export * from './packing.contract';
export * from './roadtrip.contract';
export * from './tour.contract';
export * from './vacay.contract';
/**
* The cards of the assistant chat: one per tool call, drawn beside an answer
* (a read's result) or inside a change card (what a held write will do, and
* what it did once applied). Each part of TREK owns its kinds in its
* `<pkg>.contract.ts`; the server builds every card from the tool's own
* answer and checks it against this union before it is sent.
*/
export const trekCardSchema = z.discriminatedUnion('kind', [
...TOUR_CARD_SCHEMAS,
...ROADTRIP_CARD_SCHEMAS,
...BUDGET_CARD_SCHEMAS,
...BOOKING_CARD_SCHEMAS,
...COLLAB_CARD_SCHEMAS,
...COLLECTION_CARD_SCHEMAS,
...ATLAS_CARD_SCHEMAS,
...VACAY_CARD_SCHEMAS,
...JOURNEY_CARD_SCHEMAS,
...PACKING_CARD_SCHEMAS,
]);
export type TrekCard = z.infer<typeof trekCardSchema>;
export type TrekCardKind = TrekCard['kind'];
export type TrekCardOf<K extends TrekCardKind> = Extract<TrekCard, { kind: K }>;
/**
* How a stored or streamed card is read. A kind this build does not know, a
* missing field or a value past a bound reads as null and the card is left
* out, never the message around it: a newer server may write kinds an older
* client or a rolled back server has not heard of (verified with zod 4.6.5).
* Keys a kind does not declare are dropped, so a field added later reads too.
*/
export const storedTrekCardSchema = trekCardSchema.nullable().catch(null);
/** The card of one tool call, under that call's id: a read's, or a held or applied write's. */
export const assistantCardSchema = z.object({
toolCallId: z.string().min(1).max(64),
card: storedTrekCardSchema,
});
export type AssistantCard = z.infer<typeof assistantCardSchema>;
/** The same as the server writes it: a card the union refuses is a bug in its builder, never a silent null. */
export const assistantCardWriteSchema = assistantCardSchema.extend({ card: trekCardSchema.nullable() });
@@ -0,0 +1,5 @@
import { z } from 'zod';
/** Entries of a travel journal. Only its kind so far; the travel journal cards give it its fields. */
export const journeyEntriesCardSchema = z.object({ kind: z.literal('journeyEntries') });
export type JourneyEntriesCard = z.infer<typeof journeyEntriesCardSchema>;
@@ -0,0 +1,14 @@
import { journeyEntriesCardSchema } from './journey-entries.card';
export * from './journey-entries.card';
/**
* What the travel journal brings to the assistant chat: the kinds of card it draws,
* the verbs of its change cards and what an applied change of its
* touches, beyond the core ones. cards/index.ts and assistant.schema.ts
* spread these into the card union and the shared enums, so a new kind, verb
* or entity is added here and nowhere else.
*/
export const JOURNEY_CARD_SCHEMAS = [journeyEntriesCardSchema] as const;
export const JOURNEY_VERB_KEYS = [] as const;
export const JOURNEY_APPLIED_ENTITIES = [] as const;
@@ -0,0 +1,8 @@
import { z } from 'zod';
/**
* A packing list: the trip's, a tour's or a template's. Only its kind so far; the packing and to-do
* cards give it its fields.
*/
export const packingListCardSchema = z.object({ kind: z.literal('packingList') });
export type PackingListCard = z.infer<typeof packingListCardSchema>;
@@ -0,0 +1,16 @@
import { packingListCardSchema } from './packing-list.card';
import { todoListCardSchema } from './todo-list.card';
export * from './packing-list.card';
export * from './todo-list.card';
/**
* What the packing and to-do lists bring to the assistant chat: the kinds of card they draw,
* the verbs of their change cards and what an applied change of theirs
* touches, beyond the core ones. cards/index.ts and assistant.schema.ts
* spread these into the card union and the shared enums, so a new kind, verb
* or entity is added here and nowhere else.
*/
export const PACKING_CARD_SCHEMAS = [packingListCardSchema, todoListCardSchema] as const;
export const PACKING_VERB_KEYS = [] as const;
export const PACKING_APPLIED_ENTITIES = [] as const;
@@ -0,0 +1,5 @@
import { z } from 'zod';
/** Payments between the travellers. Only its kind so far; the budget cards give it its fields. */
export const paymentsCardSchema = z.object({ kind: z.literal('payments') });
export type PaymentsCard = z.infer<typeof paymentsCardSchema>;
@@ -0,0 +1,8 @@
import { z } from 'zod';
/**
* A road trip's drive with its days and stops. Only its kind so far; the road trip cards give it its
* fields.
*/
export const roadtripCardSchema = z.object({ kind: z.literal('roadtrip') });
export type RoadtripCard = z.infer<typeof roadtripCardSchema>;
@@ -0,0 +1,28 @@
import { chargingCardSchema } from './charging.card';
import { corridorCardSchema } from './corridor.card';
import { dayRouteCardSchema } from './day-route.card';
import { roadtripCardSchema } from './roadtrip.card';
import { transitCardSchema } from './transit.card';
export * from './charging.card';
export * from './corridor.card';
export * from './day-route.card';
export * from './roadtrip.card';
export * from './transit.card';
/**
* What the road trip brings to the assistant chat: the kinds of card it draws,
* the verbs of its change cards and what an applied change of its
* touches, beyond the core ones. cards/index.ts and assistant.schema.ts
* spread these into the card union and the shared enums, so a new kind, verb
* or entity is added here and nowhere else.
*/
export const ROADTRIP_CARD_SCHEMAS = [
roadtripCardSchema,
corridorCardSchema,
chargingCardSchema,
transitCardSchema,
dayRouteCardSchema,
] as const;
export const ROADTRIP_VERB_KEYS = [] as const;
export const ROADTRIP_APPLIED_ENTITIES = [] as const;
@@ -0,0 +1,5 @@
import { z } from 'zod';
/** Who pays whom to settle up. Only its kind so far; the budget cards give it its fields. */
export const settlementCardSchema = z.object({ kind: z.literal('settlement') });
export type SettlementCard = z.infer<typeof settlementCardSchema>;
@@ -0,0 +1,5 @@
import { z } from 'zod';
/** To-dos of the trip. Only its kind so far; the packing and to-do cards give it its fields. */
export const todoListCardSchema = z.object({ kind: z.literal('todoList') });
export type TodoListCard = z.infer<typeof todoListCardSchema>;
@@ -0,0 +1,5 @@
import { z } from 'zod';
/** The tours of a trip or of one of its days. Only its kind so far; the tours cards give it its fields. */
export const tourListCardSchema = z.object({ kind: z.literal('tourList') });
export type TourListCard = z.infer<typeof tourListCardSchema>;
@@ -0,0 +1,8 @@
import { z } from 'zod';
/**
* A tour's route: a hike, a city walk, a paddle or a ski plan the assistant proposed, or a saved tour
* it read. Only its kind so far; the tours cards give it its fields.
*/
export const tourRouteCardSchema = z.object({ kind: z.literal('tourRoute') });
export type TourRouteCard = z.infer<typeof tourRouteCardSchema>;
@@ -0,0 +1,18 @@
import { tourListCardSchema } from './tour-list.card';
import { tourRouteCardSchema } from './tour-route.card';
export * from './tour-list.card';
export * from './tour-route.card';
/**
* What the tours bring to the assistant chat: the kinds of card they draw,
* the verbs of their change cards and what an applied change of theirs
* touches, beyond the core ones. cards/index.ts and assistant.schema.ts
* spread these into the card union and the shared enums, so a new kind, verb
* or entity is added here and nowhere else.
*/
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;
/** 'tour' is a core entity already. */
export const TOUR_APPLIED_ENTITIES = [] as const;
@@ -0,0 +1,8 @@
import { z } from 'zod';
/**
* A public transport journey with its legs and times. Only its kind so far; the road trip cards give
* it its fields.
*/
export const transitCardSchema = z.object({ kind: z.literal('transit') });
export type TransitCard = z.infer<typeof transitCardSchema>;
+8
View File
@@ -0,0 +1,8 @@
import { z } from 'zod';
/**
* Vacation days on a month calendar, with the days left. Only its kind so far; the vacation planner
* cards give it its fields.
*/
export const vacayCardSchema = z.object({ kind: z.literal('vacay') });
export type VacayCard = z.infer<typeof vacayCardSchema>;
@@ -0,0 +1,14 @@
import { vacayCardSchema } from './vacay.card';
export * from './vacay.card';
/**
* What the vacation planner brings to the assistant chat: the kinds of card it draws,
* the verbs of its change cards and what an applied change of its
* touches, beyond the core ones. cards/index.ts and assistant.schema.ts
* spread these into the card union and the shared enums, so a new kind, verb
* or entity is added here and nowhere else.
*/
export const VACAY_CARD_SCHEMAS = [vacayCardSchema] as const;
export const VACAY_VERB_KEYS = [] as const;
export const VACAY_APPLIED_ENTITIES = [] as const;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantAtlas: TranslationStrings = {};
export default assistantAtlas;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantBooking: TranslationStrings = {};
export default assistantBooking;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantBudget: TranslationStrings = {};
export default assistantBudget;
+19
View File
@@ -0,0 +1,19 @@
import type { TranslationStrings } from '../types';
const assistantCard: TranslationStrings = {
'assistant.card.common.open': 'فتح',
'assistant.card.common.more': '{count} أخرى',
'assistant.card.common.more.zero': '{count} أخرى',
'assistant.card.common.more.one': '{count} أخرى',
'assistant.card.common.more.two': '{count} أخرى',
'assistant.card.common.more.few': '{count} أخرى',
'assistant.card.common.more.many': '{count} أخرى',
'assistant.card.common.new': 'جديد',
'assistant.card.common.removed': 'أُزيل',
'assistant.card.common.changed': 'تغيّر',
'assistant.card.common.willDelete': 'سيُحذف',
'assistant.card.common.noLonger': 'لم يعد: {names}',
'assistant.card.common.you': 'أنت',
'assistant.card.common.showDay': 'عرض هذا اليوم في الخطة',
};
export default assistantCard;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantCollab: TranslationStrings = {};
export default assistantCollab;
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantCollection: TranslationStrings = {};
export default assistantCollection;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantJourney: TranslationStrings = {};
export default assistantJourney;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantPacking: TranslationStrings = {};
export default assistantPacking;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantRoadtrip: TranslationStrings = {};
export default assistantRoadtrip;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantTour: TranslationStrings = {};
export default assistantTour;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantVacay: TranslationStrings = {};
export default assistantVacay;
+22
View File
@@ -1,6 +1,17 @@
import admin from './admin';
import airport from './airport';
import assistant from './assistant';
import assistantAtlas from './assistantAtlas';
import assistantBooking from './assistantBooking';
import assistantBudget from './assistantBudget';
import assistantCard from './assistantCard';
import assistantCollab from './assistantCollab';
import assistantCollection from './assistantCollection';
import assistantJourney from './assistantJourney';
import assistantPacking from './assistantPacking';
import assistantRoadtrip from './assistantRoadtrip';
import assistantTour from './assistantTour';
import assistantVacay from './assistantVacay';
import atlas from './atlas';
import backup from './backup';
import budget from './budget';
@@ -118,5 +129,16 @@ const locale = {
...docsync,
...ferratas,
...assistant,
...assistantCard,
...assistantTour,
...assistantRoadtrip,
...assistantBudget,
...assistantBooking,
...assistantCollab,
...assistantCollection,
...assistantAtlas,
...assistantVacay,
...assistantJourney,
...assistantPacking,
};
export default locale;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantAtlas: TranslationStrings = {};
export default assistantAtlas;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantBooking: TranslationStrings = {};
export default assistantBooking;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantBudget: TranslationStrings = {};
export default assistantBudget;
+15
View File
@@ -0,0 +1,15 @@
import type { TranslationStrings } from '../types';
const assistantCard: TranslationStrings = {
'assistant.card.common.open': 'Aç',
'assistant.card.common.more': 'daha {count}',
'assistant.card.common.more.one': 'daha {count}',
'assistant.card.common.new': 'Yeni',
'assistant.card.common.removed': 'Çıxarıldı',
'assistant.card.common.changed': 'Dəyişdi',
'assistant.card.common.willDelete': 'Silinəcək',
'assistant.card.common.noLonger': 'Artıq yoxdur: {names}',
'assistant.card.common.you': 'Siz',
'assistant.card.common.showDay': 'Bu günü planda göstər',
};
export default assistantCard;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantCollab: TranslationStrings = {};
export default assistantCollab;
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantCollection: TranslationStrings = {};
export default assistantCollection;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantJourney: TranslationStrings = {};
export default assistantJourney;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantPacking: TranslationStrings = {};
export default assistantPacking;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantRoadtrip: TranslationStrings = {};
export default assistantRoadtrip;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantTour: TranslationStrings = {};
export default assistantTour;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantVacay: TranslationStrings = {};
export default assistantVacay;
+22
View File
@@ -1,6 +1,17 @@
import admin from './admin';
import airport from './airport';
import assistant from './assistant';
import assistantAtlas from './assistantAtlas';
import assistantBooking from './assistantBooking';
import assistantBudget from './assistantBudget';
import assistantCard from './assistantCard';
import assistantCollab from './assistantCollab';
import assistantCollection from './assistantCollection';
import assistantJourney from './assistantJourney';
import assistantPacking from './assistantPacking';
import assistantRoadtrip from './assistantRoadtrip';
import assistantTour from './assistantTour';
import assistantVacay from './assistantVacay';
import atlas from './atlas';
import backup from './backup';
import budget from './budget';
@@ -118,5 +129,16 @@ const locale = {
...docsync,
...ferratas,
...assistant,
...assistantCard,
...assistantTour,
...assistantRoadtrip,
...assistantBudget,
...assistantBooking,
...assistantCollab,
...assistantCollection,
...assistantAtlas,
...assistantVacay,
...assistantJourney,
...assistantPacking,
};
export default locale;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantAtlas: TranslationStrings = {};
export default assistantAtlas;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantBooking: TranslationStrings = {};
export default assistantBooking;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantBudget: TranslationStrings = {};
export default assistantBudget;
+15
View File
@@ -0,0 +1,15 @@
import type { TranslationStrings } from '../types';
const assistantCard: TranslationStrings = {
'assistant.card.common.open': 'Abrir',
'assistant.card.common.more': 'mais {count}',
'assistant.card.common.more.one': 'mais {count}',
'assistant.card.common.new': 'Novo',
'assistant.card.common.removed': 'Removido',
'assistant.card.common.changed': 'Alterado',
'assistant.card.common.willDelete': 'Será excluído',
'assistant.card.common.noLonger': 'Não mais: {names}',
'assistant.card.common.you': 'Você',
'assistant.card.common.showDay': 'Mostrar este dia no plano',
};
export default assistantCard;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantCollab: TranslationStrings = {};
export default assistantCollab;
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantCollection: TranslationStrings = {};
export default assistantCollection;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantJourney: TranslationStrings = {};
export default assistantJourney;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantPacking: TranslationStrings = {};
export default assistantPacking;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantRoadtrip: TranslationStrings = {};
export default assistantRoadtrip;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantTour: TranslationStrings = {};
export default assistantTour;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantVacay: TranslationStrings = {};
export default assistantVacay;
+22
View File
@@ -1,6 +1,17 @@
import admin from './admin';
import airport from './airport';
import assistant from './assistant';
import assistantAtlas from './assistantAtlas';
import assistantBooking from './assistantBooking';
import assistantBudget from './assistantBudget';
import assistantCard from './assistantCard';
import assistantCollab from './assistantCollab';
import assistantCollection from './assistantCollection';
import assistantJourney from './assistantJourney';
import assistantPacking from './assistantPacking';
import assistantRoadtrip from './assistantRoadtrip';
import assistantTour from './assistantTour';
import assistantVacay from './assistantVacay';
import atlas from './atlas';
import backup from './backup';
import budget from './budget';
@@ -118,5 +129,16 @@ const locale = {
...docsync,
...ferratas,
...assistant,
...assistantCard,
...assistantTour,
...assistantRoadtrip,
...assistantBudget,
...assistantBooking,
...assistantCollab,
...assistantCollection,
...assistantAtlas,
...assistantVacay,
...assistantJourney,
...assistantPacking,
};
export default locale;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantAtlas: TranslationStrings = {};
export default assistantAtlas;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantBooking: TranslationStrings = {};
export default assistantBooking;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantBudget: TranslationStrings = {};
export default assistantBudget;
+15
View File
@@ -0,0 +1,15 @@
import type { TranslationStrings } from '../types';
const assistantCard: TranslationStrings = {
'assistant.card.common.open': 'Obrir',
'assistant.card.common.more': '{count} més',
'assistant.card.common.more.one': '{count} més',
'assistant.card.common.new': 'Nou',
'assistant.card.common.removed': 'Tret',
'assistant.card.common.changed': 'Canviat',
'assistant.card.common.willDelete': "S'eliminarà",
'assistant.card.common.noLonger': 'Ja no: {names}',
'assistant.card.common.you': 'Tu',
'assistant.card.common.showDay': 'Mostra aquest dia al pla',
};
export default assistantCard;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantCollab: TranslationStrings = {};
export default assistantCollab;
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantCollection: TranslationStrings = {};
export default assistantCollection;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantJourney: TranslationStrings = {};
export default assistantJourney;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantPacking: TranslationStrings = {};
export default assistantPacking;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantRoadtrip: TranslationStrings = {};
export default assistantRoadtrip;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantTour: TranslationStrings = {};
export default assistantTour;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantVacay: TranslationStrings = {};
export default assistantVacay;
+22
View File
@@ -1,6 +1,17 @@
import admin from './admin';
import airport from './airport';
import assistant from './assistant';
import assistantAtlas from './assistantAtlas';
import assistantBooking from './assistantBooking';
import assistantBudget from './assistantBudget';
import assistantCard from './assistantCard';
import assistantCollab from './assistantCollab';
import assistantCollection from './assistantCollection';
import assistantJourney from './assistantJourney';
import assistantPacking from './assistantPacking';
import assistantRoadtrip from './assistantRoadtrip';
import assistantTour from './assistantTour';
import assistantVacay from './assistantVacay';
import atlas from './atlas';
import backup from './backup';
import budget from './budget';
@@ -118,5 +129,16 @@ const locale = {
...docsync,
...ferratas,
...assistant,
...assistantCard,
...assistantTour,
...assistantRoadtrip,
...assistantBudget,
...assistantBooking,
...assistantCollab,
...assistantCollection,
...assistantAtlas,
...assistantVacay,
...assistantJourney,
...assistantPacking,
};
export default locale;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantAtlas: TranslationStrings = {};
export default assistantAtlas;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantBooking: TranslationStrings = {};
export default assistantBooking;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantBudget: TranslationStrings = {};
export default assistantBudget;
+16
View File
@@ -0,0 +1,16 @@
import type { TranslationStrings } from '../types';
const assistantCard: TranslationStrings = {
'assistant.card.common.open': 'Otevřít',
'assistant.card.common.more': '{count} dalších',
'assistant.card.common.more.one': '{count} další',
'assistant.card.common.more.few': '{count} další',
'assistant.card.common.new': 'Nové',
'assistant.card.common.removed': 'Odebráno',
'assistant.card.common.changed': 'Změněno',
'assistant.card.common.willDelete': 'Bude smazáno',
'assistant.card.common.noLonger': 'Už ne: {names}',
'assistant.card.common.you': 'Vy',
'assistant.card.common.showDay': 'Zobrazit tento den v plánu',
};
export default assistantCard;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantCollab: TranslationStrings = {};
export default assistantCollab;
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantCollection: TranslationStrings = {};
export default assistantCollection;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantJourney: TranslationStrings = {};
export default assistantJourney;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantPacking: TranslationStrings = {};
export default assistantPacking;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantRoadtrip: TranslationStrings = {};
export default assistantRoadtrip;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantTour: TranslationStrings = {};
export default assistantTour;
+4
View File
@@ -0,0 +1,4 @@
import type { TranslationStrings } from '../types';
const assistantVacay: TranslationStrings = {};
export default assistantVacay;
+22
View File
@@ -1,6 +1,17 @@
import admin from './admin';
import airport from './airport';
import assistant from './assistant';
import assistantAtlas from './assistantAtlas';
import assistantBooking from './assistantBooking';
import assistantBudget from './assistantBudget';
import assistantCard from './assistantCard';
import assistantCollab from './assistantCollab';
import assistantCollection from './assistantCollection';
import assistantJourney from './assistantJourney';
import assistantPacking from './assistantPacking';
import assistantRoadtrip from './assistantRoadtrip';
import assistantTour from './assistantTour';
import assistantVacay from './assistantVacay';
import atlas from './atlas';
import backup from './backup';
import budget from './budget';
@@ -118,5 +129,16 @@ const locale = {
...docsync,
...ferratas,
...assistant,
...assistantCard,
...assistantTour,
...assistantRoadtrip,
...assistantBudget,
...assistantBooking,
...assistantCollab,
...assistantCollection,
...assistantAtlas,
...assistantVacay,
...assistantJourney,
...assistantPacking,
};
export default locale;

Some files were not shown because too many files have changed in this diff Show More