Files
cobble/src/schemas/bot-schema.ts
T
tbenandClaude Opus 5.5 f13fa863c5 Trim page props and page the conversation's messages
Bot and workspace lists in page props leave out instructions, memory and
workspace context, and carry the last message as a short preview. The
header avatar's edit form loads the bot's instructions when it opens.
The conversation renders its latest 30 messages and loads older ones as
the thread scrolls to its top.

Also: the composer shows a placeholder until its editor loads, and
`bun start` no longer builds first.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-20 20:09:01 +01:00

157 lines
5.4 KiB
TypeScript

import { z } from "zod";
import { AppData } from "@/src/data/app-data";
import {
BotHues,
BotMessageReactions,
type BotHue,
type BotMessageReactionEmoji,
} from "@/src/dict/bot-dict";
/**
* The workspace a conversation is in; left out (or null) for a bot's own
* chat.
*/
const WorkspaceIdField = z.coerce.number().int().positive().nullish();
export const SendBotMessageRequestSchema = z.object({
bot_id: z.coerce.number().int().positive("Pick a bot to message"),
workspace_id: WorkspaceIdField,
text: z.string().trim().max(4000, "Keep messages under 4000 characters").default(""),
media_ids: z
.array(z.coerce.number().int().positive())
.max(AppData.BotAttachmentMaxFiles, `Attach up to ${AppData.BotAttachmentMaxFiles} files`)
.default([]),
/**
* The message this one replies to, quoted above it.
*/
reply_to_message_id: z.coerce.number().int().positive().nullish(),
}).refine((body) => body.text || body.media_ids.length, "Write a message or attach a file first");
export type SendBotMessageRequest = z.infer<typeof SendBotMessageRequestSchema>;
/**
* Toggling one of the fixed reactions on a message.
*/
export const ReactToBotMessageRequestSchema = z.object({
message_id: z.coerce.number().int().positive("Pick a message"),
emoji: z.enum(
BotMessageReactions.map((reaction) => reaction.value) as [
BotMessageReactionEmoji,
...BotMessageReactionEmoji[],
],
),
});
export type ReactToBotMessageRequest = z.infer<typeof ReactToBotMessageRequestSchema>;
export const BotIdRequestSchema = z.object({
bot_id: z.coerce.number().int().positive("Pick a bot"),
});
export type BotIdRequest = z.infer<typeof BotIdRequestSchema>;
export const SetBotComputerRequestSchema = z.object({
bot_id: z.coerce.number().int().positive("Pick a bot"),
computer_id: z.coerce.number().int().positive("Pick a computer"),
/**
* In a workspace this switches the workspace's default computer.
*/
workspace_id: WorkspaceIdField,
});
export type SetBotComputerRequest = z.infer<typeof SetBotComputerRequestSchema>;
/**
* Giving a bot direct access to a computer, or taking it away.
*/
export const BotComputerRequestSchema = z.object({
bot_id: z.coerce.number().int().positive("Pick a bot"),
computer_id: z.coerce.number().int().positive("Pick a computer"),
});
export type BotComputerRequest = z.infer<typeof BotComputerRequestSchema>;
export const BotSessionIdRequestSchema = z.object({
session_id: z.coerce.number().int().positive("Pick a session"),
});
export type BotSessionIdRequest = z.infer<typeof BotSessionIdRequestSchema>;
export const OlderBotMessagesRequestSchema = BotSessionIdRequestSchema.extend({
before_id: z.coerce.number().int().positive("Pick a message"),
});
export type OlderBotMessagesRequest = z.infer<typeof OlderBotMessagesRequestSchema>;
export const BotConversationRequestSchema = z.object({
bot_id: z.coerce.number().int().positive("Pick a bot"),
workspace_id: WorkspaceIdField,
});
export type BotConversationRequest = z.infer<typeof BotConversationRequestSchema>;
export const ListBotSessionsRequestSchema = BotConversationRequestSchema.extend({
query: z.string().trim().max(200).optional(),
page: z.coerce.number().int().positive().optional(),
/**
* A handful for the rail's sub-list; the modal leaves it out for a page.
*/
limit: z.coerce.number().int().positive().max(AppData.BotSessionsPageSize).optional(),
});
export type ListBotSessionsRequest = z.infer<typeof ListBotSessionsRequestSchema>;
export const CreateBotRequestSchema = z.object({
name: z.string().trim().min(1, "Give the bot a name").max(40, "Keep the name under 40 characters"),
role: z.string().trim().max(120, "Keep the role to one line").default(""),
hue: z.enum(BotHues.map((h) => h.value) as [BotHue, ...BotHue[]]),
system_prompt: z
.string()
.trim()
.min(1, "Tell the bot what it does")
.max(AppData.PromptMaxLength),
/**
* Also put it in this workspace.
*/
workspace_id: WorkspaceIdField,
});
export type CreateBotRequest = z.infer<typeof CreateBotRequestSchema>;
/**
* The coordinator keeps its name and has no hue: only its instructions
* change.
*/
export const UpdateBotRequestSchema = CreateBotRequestSchema.omit({ workspace_id: true }).extend({
bot_id: z.coerce.number().int().positive("Pick a bot"),
});
export type UpdateBotRequest = z.infer<typeof UpdateBotRequestSchema>;
export const UploadBotAttachmentRequestSchema = z.object({
/**
* The file as a base64 data URL.
*/
data_url: z.string().startsWith("data:", "Send the file as a data URL"),
name: z.string().trim().min(1, "The file needs a name").max(255),
});
export type UploadBotAttachmentRequest = z.infer<typeof UploadBotAttachmentRequestSchema>;
export const TranscribeVoiceRequestSchema = z.object({
bot_id: z.coerce.number().int().positive("Pick a bot"),
workspace_id: WorkspaceIdField,
/**
* The recording as a base64 data URL.
*/
data_url: z.string().startsWith("data:audio/", "Send the recording as an audio data URL"),
});
export type TranscribeVoiceRequest = z.infer<typeof TranscribeVoiceRequestSchema>;
export const SpeakBotMessageRequestSchema = z.object({
message_id: z.coerce.number().int().positive("Pick a message"),
});
export type SpeakBotMessageRequest = z.infer<typeof SpeakBotMessageRequestSchema>;