Give every user their own bot face
A bot's face and status line live in bot_user_faces, one row per bot and user, set by jobs for the user whose conversation they're in. Needs-you and working only show to that user; every bot read overlays the viewer's face (botFaceSql) and pushes go to the user a face change is for. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -42,8 +42,6 @@ export default async function createBot({ user_id, name, role, hue, system_promp
|
||||
kind: "worker",
|
||||
hue,
|
||||
role: role || undefined,
|
||||
face: "idle",
|
||||
status_line: "",
|
||||
system_prompt,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -55,8 +55,6 @@ export default async function ensureCoordinatorBot({ user_id }: Params) {
|
||||
user_id: Number(user_id),
|
||||
name: CoordinatorBotName,
|
||||
kind: "coordinator",
|
||||
face: "idle",
|
||||
status_line: "",
|
||||
system_prompt: CoordinatorSystemPrompt,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BotFace } from "@/src/dict/bot-dict";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
user_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A bot's face and status line as one user sees it: idle when it has
|
||||
* none for them.
|
||||
*/
|
||||
export default async function grabBotFace({ bot_id, user_id }: Params) {
|
||||
const row = (
|
||||
await BunSQLite.sql<{ face: BotFace; status_line: string }>({
|
||||
sql: `SELECT face, status_line FROM bot_user_faces WHERE bot_id = ? AND user_id = ? LIMIT 1`,
|
||||
values: [bot_id, user_id],
|
||||
})
|
||||
).payload?.[0];
|
||||
|
||||
return { face: (row?.face || "idle") as BotFace, status_line: row?.status_line || "" };
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { BotWithLastMessage } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import chatBotsSql from "@/src/utils/crew-access/chat-bots-sql";
|
||||
import workspaceBotsSql from "@/src/utils/crew-access/workspace-bots-sql";
|
||||
import botFaceSql from "@/src/utils/bot-face-sql";
|
||||
import grabCrewRole from "../access/grab-crew-role";
|
||||
|
||||
type Params = {
|
||||
@@ -18,8 +19,8 @@ type Params = {
|
||||
* The bots a user chats with, for the rail: in their own chat
|
||||
* (`chatBotsSql`) or in one workspace (`workspaceBotsSql`). The
|
||||
* coordinator is pinned first, then workers by latest activity. Each row
|
||||
* carries the user's own conversation there: its unread flag, latest
|
||||
* activity and latest message.
|
||||
* carries the user's own face (`botFaceSql`) and conversation there: its
|
||||
* unread flag, latest activity and latest message.
|
||||
*/
|
||||
export default async function grabBots({
|
||||
user_id,
|
||||
@@ -29,10 +30,13 @@ export default async function grabBots({
|
||||
const role = await grabCrewRole({ user_id });
|
||||
const access = scope ? workspaceBotsSql({ role, workspace_id: scope }) : chatBotsSql({ role, user_id });
|
||||
|
||||
const face = botFaceSql({ user_id });
|
||||
|
||||
let sql = ``;
|
||||
|
||||
sql += `SELECT\n`;
|
||||
sql += ` b.*,\n`;
|
||||
sql += ` ${face.sql},\n`;
|
||||
sql += ` COALESCE(t.unread, 0) AS unread,\n`;
|
||||
sql += ` t.last_activity_at AS last_activity_at,\n`;
|
||||
sql += ` lm.text AS last_message_text,\n`;
|
||||
@@ -55,7 +59,7 @@ export default async function grabBots({
|
||||
|
||||
const res = await BunSQLite.sql<BotWithLastMessage>({
|
||||
sql,
|
||||
values: [scope, user_id, scope, user_id, ...access.values],
|
||||
values: [...face.values, scope, user_id, scope, user_id, ...access.values],
|
||||
});
|
||||
|
||||
return res.payload || [];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
|
||||
import type { BotWithFace } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import chatBotsSql from "@/src/utils/crew-access/chat-bots-sql";
|
||||
import botFaceSql from "@/src/utils/bot-face-sql";
|
||||
import grabCrewRole from "../access/grab-crew-role";
|
||||
|
||||
type Params = {
|
||||
@@ -9,17 +10,20 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* A bot only if the user chats with it in its own chat (`chatBotsSql`).
|
||||
* A bot, with the user's own face (`botFaceSql`), only if the user chats
|
||||
* with it in its own chat (`chatBotsSql`).
|
||||
*/
|
||||
export default async function grabChatBot({
|
||||
bot_id,
|
||||
user_id,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_BOTS | null> {
|
||||
}: Params): Promise<BotWithFace | null> {
|
||||
const access = chatBotsSql({ role: await grabCrewRole({ user_id }), user_id });
|
||||
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS>({
|
||||
sql: `SELECT b.* FROM bots b WHERE b.id = ? AND ${access.sql} LIMIT 1`,
|
||||
values: [bot_id, ...access.values],
|
||||
const face = botFaceSql({ user_id });
|
||||
|
||||
const res = await BunSQLite.sql<BotWithFace>({
|
||||
sql: `SELECT b.*, ${face.sql} FROM bots b WHERE b.id = ? AND ${access.sql} LIMIT 1`,
|
||||
values: [...face.values, bot_id, ...access.values],
|
||||
});
|
||||
|
||||
return res.payload?.[0] || null;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
|
||||
import type { BotWithFace } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import chatBotsSql from "@/src/utils/crew-access/chat-bots-sql";
|
||||
import botFaceSql from "@/src/utils/bot-face-sql";
|
||||
import grabCrewRole from "../access/grab-crew-role";
|
||||
|
||||
type Params = {
|
||||
@@ -9,23 +10,26 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* A bot only if the user reaches it somewhere: in its own chat
|
||||
* (`chatBotsSql`) or in a workspace they're in. Super admins and admins
|
||||
* reach every worker; only the super admin reaches the coordinator.
|
||||
* A bot, with the user's own face (`botFaceSql`), only if the user
|
||||
* reaches it somewhere: in its own chat (`chatBotsSql`) or in a workspace
|
||||
* they're in. Super admins and admins reach every worker; only the super
|
||||
* admin reaches the coordinator.
|
||||
*/
|
||||
export default async function grabUserBot({
|
||||
bot_id,
|
||||
user_id,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_BOTS | null> {
|
||||
}: Params): Promise<BotWithFace | null> {
|
||||
const access = chatBotsSql({ role: await grabCrewRole({ user_id }), user_id });
|
||||
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS>({
|
||||
sql: `SELECT b.* FROM bots b WHERE b.id = ? AND (${access.sql} OR (b.kind != 'coordinator' AND b.id IN (
|
||||
const face = botFaceSql({ user_id });
|
||||
|
||||
const res = await BunSQLite.sql<BotWithFace>({
|
||||
sql: `SELECT b.*, ${face.sql} FROM bots b WHERE b.id = ? AND (${access.sql} OR (b.kind != 'coordinator' AND b.id IN (
|
||||
SELECT wb.bot_id FROM workspace_bots wb
|
||||
JOIN workspace_users wu ON wu.workspace_id = wb.workspace_id AND wu.user_id = ?
|
||||
JOIN workspaces w ON w.id = wb.workspace_id AND w.archived = 0
|
||||
))) LIMIT 1`,
|
||||
values: [bot_id, ...access.values, user_id],
|
||||
values: [...face.values, bot_id, ...access.values, user_id],
|
||||
});
|
||||
|
||||
return res.payload?.[0] || null;
|
||||
|
||||
@@ -3,23 +3,40 @@ import type { BotFace } from "@/src/dict/bot-dict";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
/**
|
||||
* The user who sees it: the one whose conversation the job is in.
|
||||
*/
|
||||
user_id: number | string;
|
||||
face: BotFace;
|
||||
status_line?: string;
|
||||
};
|
||||
|
||||
export type BotFaceChange = {
|
||||
id: number;
|
||||
user_id: number;
|
||||
face: BotFace;
|
||||
status_line: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets a bot's face (its status) and status line. Callers must check the
|
||||
* Sets a bot's face (its status) and status line as one user sees it
|
||||
* (`bot_user_faces`). Other users keep their own. Callers must check the
|
||||
* bot first.
|
||||
*/
|
||||
export default async function setBotFace({
|
||||
bot_id,
|
||||
user_id,
|
||||
face,
|
||||
status_line = "",
|
||||
}: Params) {
|
||||
}: Params): Promise<BotFaceChange> {
|
||||
const now = Date.now();
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE bots SET face = ?, status_line = ?, updated_at = ? WHERE id = ?`,
|
||||
values: [face, status_line, Date.now(), bot_id],
|
||||
sql: `INSERT INTO bot_user_faces (created_at, updated_at, bot_id, user_id, face, status_line)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(bot_id, user_id) DO UPDATE SET face = excluded.face, status_line = excluded.status_line, updated_at = excluded.updated_at`,
|
||||
values: [now, now, bot_id, user_id, face, status_line],
|
||||
});
|
||||
|
||||
return { id: Number(bot_id), face, status_line };
|
||||
return { id: Number(bot_id), user_id: Number(user_id), face, status_line };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user