Files
TREK/client/src/repo/assistantRepo.ts
T

103 lines
3.9 KiB
TypeScript

import {
assistantAddFoundPlaceResponseSchema,
assistantThreadListResponseSchema,
assistantThreadResponseSchema,
collabMessageResponseSchema,
type AssistantAddFoundPlaceRequest,
type AssistantChatRequest,
type AssistantDecisionsRequest,
type AssistantShareRequest,
type AssistantTakeProposalRequest,
} from '@trek/shared';
import { assistantApi } from '../api/assistant';
import { requireOnline } from './onlineOnly';
/**
* The trip assistant's conversations.
*
* **Deliberately online-only, and no Dexie table.** Every answer comes from a
* model on the other side of the server, so there is nothing to do offline,
* and a cached copy of a chat would be one more place the trip data the model
* read lives on the device. Each call refuses with `OnlineOnlyError` before a
* request is made; the view says why instead of hanging on a timeout.
*
* The planner learns about applied changes the normal way, through the
* WebSocket broadcast the tools send, so nothing here touches trip data.
*/
export const assistantRepo = {
async threads(tripId: number) {
requireOnline();
return assistantThreadListResponseSchema.parse(await assistantApi.threads(tripId)).threads;
},
async thread(tripId: number, threadId: number) {
requireOnline();
return assistantThreadResponseSchema.parse(await assistantApi.thread(tripId, threadId));
},
async deleteThread(tripId: number, threadId: number): Promise<void> {
requireOnline();
await assistantApi.deleteThread(tripId, threadId);
},
/** A new message; resolves once the server accepted it, with the stream of the answer. */
async send(tripId: number, body: AssistantChatRequest, signal: AbortSignal) {
requireOnline();
return assistantApi.chat(tripId, body, signal);
},
/** The decisions on the changes a step holds; the stream carries their results and the rest of the turn. */
async decide(tripId: number, threadId: number, body: AssistantDecisionsRequest, signal: AbortSignal) {
requireOnline();
return assistantApi.decide(tripId, threadId, body, signal);
},
/** A booking file for the chat to read; the stream proposes each booking found in it. */
async attach(
tripId: number,
file: File,
fields: { threadId: number | null; locale: string; timeZone: string },
signal: AbortSignal
) {
requireOnline();
const form = new FormData();
form.append('file', file);
if (fields.threadId !== null) form.append('threadId', String(fields.threadId));
form.append('locale', fields.locale);
form.append('timeZone', fields.timeZone);
return assistantApi.attach(tripId, form, signal);
},
async undo(tripId: number, toolCallId: string): Promise<void> {
requireOnline();
await assistantApi.undo(tripId, toolCallId);
},
/** A place a search found, held as a change for a day; no model runs for it. */
async addFoundPlace(tripId: number, threadId: number, body: AssistantAddFoundPlaceRequest) {
requireOnline();
return assistantAddFoundPlaceResponseSchema.parse(await assistantApi.addFoundPlace(tripId, threadId, body));
},
/** A proposal an answer showed as a card ("Plan this"), held as a change; no model runs for it. */
async takeProposal(tripId: number, threadId: number, body: AssistantTakeProposalRequest) {
requireOnline();
return assistantAddFoundPlaceResponseSchema.parse(await assistantApi.takeProposal(tripId, threadId, body));
},
/**
* An answer posted to the trip chat, as the preview showed it. Online only
* like the rest: a chat message sent later would land out of order between
* the live ones.
*/
async share(tripId: number, threadId: number, messageId: string, body: AssistantShareRequest) {
requireOnline();
return collabMessageResponseSchema.parse(await assistantApi.share(tripId, threadId, messageId, body));
},
async stop(tripId: number): Promise<void> {
requireOnline();
await assistantApi.stop(tripId);
},
};