///
// Teaching example: CTS (CommonTools TypeScript) generates JSON Schemas from the
// TypeScript types below. Recipes use typed inputs/outputs, and handlers take
// explicit `Cell` parameters; the transform emits the correct asCell schema
// automatically where mutation is required.
import {
Cell,
cell,
Default,
derive,
handler,
NAME,
navigateTo,
recipe,
str,
UI,
} from "commontools";
// User object for better data modeling
interface User {
name: string;
}
// Data types for the chat messages. CTS will derive the matching schema.
interface ChatMessage {
author: User;
message: string;
timestamp: number;
}
// Recipe input (typed). The UI receives a reactive reference that supports
// mapping (`messages.map(...)`). Writes should be performed in handlers.
type MainRecipeInput = {
messages: Default;
};
type UserSessionInput = {
messages: Default;
user: User;
};
// Session recipe result (typed): return User object alongside [NAME] and [UI].
// CTS will carry these in the recipe result schema.
type UserSessionResult = {
messages: Default;
user: User;
};
// Event payload type for ct-message-input's ct-send event
type InputEventType = {
detail: {
message: string;
};
};
// Simple messenger-style time format (e.g., 3:05 PM). No full date per message.
function formatTime(ts: number): string {
const d = new Date(ts);
return d.toLocaleTimeString(undefined, {
hour: "numeric",
minute: "2-digit",
});
}
// Handler to send a new chat message.
const sendMessage = handler<
InputEventType,
{
messages: Cell;
user: Cell;
}
>((event, { messages, user }) => {
const text = event.detail?.message?.trim();
if (!text) return;
messages.push({
author: user.get(),
message: text,
timestamp: Date.now(),
});
});
// Handler to set/update the username (local-only field)
const setUsername = handler<
InputEventType,
{
user: Cell;
}
>((event, { user }) => {
const name = (event.detail?.message ?? "").trim();
// Update only the "name" field to avoid clearing other properties
user.key("name").set(name);
});
// User Session Recipe - Individual instance with local state
export const UserSession = recipe<
UserSessionInput,
UserSessionResult
>(
"User Chat Session",
({ messages, user }) => {
return {
[NAME]: str`Chat Session` as any,
[UI]: (
),
messages,
user,
};
},
);
// Handler to create a new user session. Receives typed parameters so the handler
// can pass the reactive references directly into the child recipe and
// keep all sessions linked to the same underlying state.
const createUserSession = handler<
never,
{
messages: Default;
}
>((_, { messages }) => {
const sessionCharm = UserSession({
messages: messages as any,
user: cell({ name: "" }),
});
return navigateTo(sessionCharm);
});
// Main chat recipe: a state container with a button to spawn per-user sessions.
// All sessions get the same `messages` reference so changes are shared.
export default recipe(
"Main Chat State Container",
({ messages }) => {
return {
[NAME]: "Chat State Container",
[UI]: (