fix(FN-3808): increase create-room member role typography
- Increase the member role text sizing in CreateRoomModal for better readability - Update CreateRoomModal.css with the adjusted typography value - Keep the change scoped to create-room role styling only Fusion-Task-Id: FN-3808
This commit is contained in:
@@ -24,6 +24,15 @@ import type {
|
||||
ChatSessionCreateInput,
|
||||
ChatSessionUpdateInput,
|
||||
ChatMessagesFilter,
|
||||
ChatRoom,
|
||||
ChatRoomCreateInput,
|
||||
ChatRoomMember,
|
||||
ChatRoomMessage,
|
||||
ChatRoomMessageCreateInput,
|
||||
ChatRoomMessagesFilter,
|
||||
ChatRoomStatus,
|
||||
ChatRoomUpdateInput,
|
||||
RoomMemberRole,
|
||||
} from "./chat-types.js";
|
||||
|
||||
// ── Event Types ─────────────────────────────────────────────────────
|
||||
@@ -41,6 +50,22 @@ export interface ChatStoreEvents {
|
||||
"chat:message:deleted": [messageId: string];
|
||||
/** Emitted when a message is updated (e.g., attachment appended) */
|
||||
"chat:message:updated": [message: ChatMessage];
|
||||
/** Emitted when a room is created */
|
||||
"chat:room:created": [room: ChatRoom];
|
||||
/** Emitted when a room is updated */
|
||||
"chat:room:updated": [room: ChatRoom];
|
||||
/** Emitted when a room is deleted */
|
||||
"chat:room:deleted": [roomId: string];
|
||||
/** Emitted when a room member is added */
|
||||
"chat:room:member:added": [member: ChatRoomMember];
|
||||
/** Emitted when a room member is removed */
|
||||
"chat:room:member:removed": [payload: { roomId: string; agentId: string }];
|
||||
/** Emitted when a room message is added */
|
||||
"chat:room:message:added": [message: ChatRoomMessage];
|
||||
/** Emitted when a room message is updated */
|
||||
"chat:room:message:updated": [message: ChatRoomMessage];
|
||||
/** Emitted when a room message is deleted */
|
||||
"chat:room:message:deleted": [messageId: string];
|
||||
}
|
||||
|
||||
// ── Row Interfaces ───────────────────────────────────────────────────
|
||||
@@ -71,6 +96,38 @@ interface ChatMessageRow {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface ChatRoomRow {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string | null;
|
||||
projectId: string | null;
|
||||
createdBy: string | null;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface ChatRoomMemberRow {
|
||||
roomId: string;
|
||||
agentId: string;
|
||||
role: string;
|
||||
addedAt: string;
|
||||
}
|
||||
|
||||
interface ChatRoomMessageRow {
|
||||
id: string;
|
||||
roomId: string;
|
||||
role: string;
|
||||
content: string;
|
||||
thinkingOutput: string | null;
|
||||
metadata: string | null;
|
||||
attachments: string | null;
|
||||
senderAgentId: string | null;
|
||||
mentions: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// ── ChatStore Class ─────────────────────────────────────────────────
|
||||
|
||||
export class ChatStore extends EventEmitter<ChatStoreEvents> {
|
||||
@@ -118,6 +175,56 @@ export class ChatStore extends EventEmitter<ChatStoreEvents> {
|
||||
};
|
||||
}
|
||||
|
||||
private rowToRoom(row: ChatRoomRow): ChatRoom {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
slug: row.slug,
|
||||
description: row.description ?? null,
|
||||
projectId: row.projectId ?? null,
|
||||
createdBy: row.createdBy ?? null,
|
||||
status: row.status as ChatRoomStatus,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
private rowToRoomMember(row: ChatRoomMemberRow): ChatRoomMember {
|
||||
return {
|
||||
roomId: row.roomId,
|
||||
agentId: row.agentId,
|
||||
role: row.role as RoomMemberRole,
|
||||
addedAt: row.addedAt,
|
||||
};
|
||||
}
|
||||
|
||||
private rowToRoomMessage(row: ChatRoomMessageRow): ChatRoomMessage {
|
||||
return {
|
||||
id: row.id,
|
||||
roomId: row.roomId,
|
||||
role: row.role as ChatMessageRole,
|
||||
content: row.content,
|
||||
thinkingOutput: row.thinkingOutput ?? null,
|
||||
metadata: fromJson<Record<string, unknown>>(row.metadata) ?? null,
|
||||
attachments: fromJson<ChatAttachment[]>(row.attachments) ?? undefined,
|
||||
senderAgentId: row.senderAgentId ?? null,
|
||||
mentions: fromJson<string[]>(row.mentions) ?? [],
|
||||
createdAt: row.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
private normalizeRoomName(name: string): string {
|
||||
return name.trim().replace(/^#+/, "").trim();
|
||||
}
|
||||
|
||||
private buildRoomSlug(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
// ── Session CRUD Operations ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -554,4 +661,308 @@ export class ChatStore extends EventEmitter<ChatStoreEvents> {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
createRoom(input: ChatRoomCreateInput & { memberAgentIds?: string[] }): ChatRoom {
|
||||
const normalizedName = this.normalizeRoomName(input.name);
|
||||
if (!normalizedName) throw new Error("Room name cannot be empty");
|
||||
|
||||
const slug = this.buildRoomSlug(normalizedName);
|
||||
if (!slug) throw new Error("Room name must include letters or numbers");
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const room: ChatRoom = {
|
||||
id: `room-${randomUUID().slice(0, 8)}`,
|
||||
name: normalizedName,
|
||||
slug,
|
||||
description: input.description ?? null,
|
||||
projectId: input.projectId ?? null,
|
||||
createdBy: input.createdBy ?? null,
|
||||
status: "active",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
const existingSlug = this.db.prepare(
|
||||
"SELECT id FROM chat_rooms WHERE projectId IS ? AND slug = ?",
|
||||
).get(room.projectId, room.slug) as { id: string } | undefined;
|
||||
if (existingSlug) {
|
||||
throw new Error(`Room slug ${room.slug} already exists in this project`);
|
||||
}
|
||||
|
||||
const memberIds = new Set((input.memberAgentIds ?? []).map((id) => id.trim()).filter(Boolean));
|
||||
|
||||
this.db.transaction(() => {
|
||||
this.db.prepare(`
|
||||
INSERT INTO chat_rooms (id, name, slug, description, projectId, createdBy, status, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
room.id,
|
||||
room.name,
|
||||
room.slug,
|
||||
room.description,
|
||||
room.projectId,
|
||||
room.createdBy,
|
||||
room.status,
|
||||
room.createdAt,
|
||||
room.updatedAt,
|
||||
);
|
||||
|
||||
const insertMember = this.db.prepare(`
|
||||
INSERT INTO chat_room_members (roomId, agentId, role, addedAt)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
for (const agentId of memberIds) {
|
||||
const role: RoomMemberRole = room.createdBy !== null && agentId === room.createdBy ? "owner" : "member";
|
||||
insertMember.run(room.id, agentId, role, now);
|
||||
}
|
||||
});
|
||||
|
||||
const insertedMembers = this.listRoomMembers(room.id);
|
||||
this.db.bumpLastModified();
|
||||
this.emit("chat:room:created", room);
|
||||
for (const member of insertedMembers) {
|
||||
this.emit("chat:room:member:added", member);
|
||||
}
|
||||
return room;
|
||||
}
|
||||
|
||||
getRoom(id: string): ChatRoom | undefined {
|
||||
const row = this.db.prepare("SELECT * FROM chat_rooms WHERE id = ?").get(id) as ChatRoomRow | undefined;
|
||||
return row ? this.rowToRoom(row) : undefined;
|
||||
}
|
||||
|
||||
getRoomBySlug(projectId: string | null, slug: string): ChatRoom | undefined {
|
||||
const row = this.db.prepare("SELECT * FROM chat_rooms WHERE projectId IS ? AND slug = ?").get(projectId, slug) as ChatRoomRow | undefined;
|
||||
return row ? this.rowToRoom(row) : undefined;
|
||||
}
|
||||
|
||||
listRooms(options?: { projectId?: string; status?: ChatRoomStatus }): ChatRoom[] {
|
||||
const whereClauses: string[] = [];
|
||||
const params: string[] = [];
|
||||
if (options?.projectId) {
|
||||
whereClauses.push("projectId = ?");
|
||||
params.push(options.projectId);
|
||||
}
|
||||
if (options?.status) {
|
||||
whereClauses.push("status = ?");
|
||||
params.push(options.status);
|
||||
}
|
||||
const whereSql = whereClauses.length ? `WHERE ${whereClauses.join(" AND ")}` : "";
|
||||
const rows = this.db.prepare(`SELECT * FROM chat_rooms ${whereSql} ORDER BY updatedAt DESC`).all(...params) as ChatRoomRow[];
|
||||
return rows.map((row) => this.rowToRoom(row));
|
||||
}
|
||||
|
||||
updateRoom(id: string, input: ChatRoomUpdateInput): ChatRoom | undefined {
|
||||
const existing = this.getRoom(id);
|
||||
if (!existing) return undefined;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const setClauses: string[] = ["updatedAt = ?"];
|
||||
const params: Array<string | null> = [now];
|
||||
|
||||
if (input.name !== undefined) {
|
||||
const normalizedName = this.normalizeRoomName(input.name);
|
||||
if (!normalizedName) throw new Error("Room name cannot be empty");
|
||||
const slug = this.buildRoomSlug(normalizedName);
|
||||
if (!slug) throw new Error("Room name must include letters or numbers");
|
||||
|
||||
const existingSlug = this.db.prepare(
|
||||
"SELECT id FROM chat_rooms WHERE projectId IS ? AND slug = ? AND id != ?",
|
||||
).get(existing.projectId, slug, id) as { id: string } | undefined;
|
||||
if (existingSlug) {
|
||||
throw new Error(`Room slug ${slug} already exists in this project`);
|
||||
}
|
||||
|
||||
setClauses.push("name = ?", "slug = ?");
|
||||
params.push(normalizedName, slug);
|
||||
}
|
||||
if (input.description !== undefined) {
|
||||
setClauses.push("description = ?");
|
||||
params.push(input.description);
|
||||
}
|
||||
if (input.status !== undefined) {
|
||||
setClauses.push("status = ?");
|
||||
params.push(input.status);
|
||||
}
|
||||
|
||||
params.push(id);
|
||||
this.db.prepare(`UPDATE chat_rooms SET ${setClauses.join(", ")} WHERE id = ?`).run(...params);
|
||||
|
||||
const updated = this.getRoom(id)!;
|
||||
this.db.bumpLastModified();
|
||||
this.emit("chat:room:updated", updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
deleteRoom(id: string): boolean {
|
||||
const existing = this.getRoom(id);
|
||||
if (!existing) return false;
|
||||
|
||||
this.db.prepare("DELETE FROM chat_rooms WHERE id = ?").run(id);
|
||||
this.db.bumpLastModified();
|
||||
this.emit("chat:room:deleted", id);
|
||||
return true;
|
||||
}
|
||||
|
||||
addRoomMember(roomId: string, agentId: string, role: RoomMemberRole = "member"): ChatRoomMember {
|
||||
const now = new Date().toISOString();
|
||||
const result = this.db.prepare(`
|
||||
INSERT OR IGNORE INTO chat_room_members (roomId, agentId, role, addedAt)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`).run(roomId, agentId, role, now);
|
||||
|
||||
const member = this.db.prepare("SELECT * FROM chat_room_members WHERE roomId = ? AND agentId = ?").get(roomId, agentId) as ChatRoomMemberRow | undefined;
|
||||
if (!member) throw new Error(`Failed to load room member ${agentId}`);
|
||||
const mapped = this.rowToRoomMember(member);
|
||||
|
||||
if (result.changes > 0) {
|
||||
this.db.bumpLastModified();
|
||||
this.emit("chat:room:member:added", mapped);
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
|
||||
removeRoomMember(roomId: string, agentId: string): boolean {
|
||||
const result = this.db.prepare("DELETE FROM chat_room_members WHERE roomId = ? AND agentId = ?").run(roomId, agentId);
|
||||
const removed = result.changes > 0;
|
||||
if (removed) {
|
||||
this.db.bumpLastModified();
|
||||
this.emit("chat:room:member:removed", { roomId, agentId });
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
listRoomMembers(roomId: string): ChatRoomMember[] {
|
||||
const rows = this.db.prepare("SELECT * FROM chat_room_members WHERE roomId = ? ORDER BY addedAt ASC").all(roomId) as ChatRoomMemberRow[];
|
||||
return rows.map((row) => this.rowToRoomMember(row));
|
||||
}
|
||||
|
||||
listRoomsForAgent(agentId: string, options?: { projectId?: string; status?: ChatRoomStatus }): ChatRoom[] {
|
||||
const whereClauses: string[] = ["m.agentId = ?"];
|
||||
const params: string[] = [agentId];
|
||||
if (options?.projectId) {
|
||||
whereClauses.push("r.projectId = ?");
|
||||
params.push(options.projectId);
|
||||
}
|
||||
if (options?.status) {
|
||||
whereClauses.push("r.status = ?");
|
||||
params.push(options.status);
|
||||
}
|
||||
const rows = this.db.prepare(`
|
||||
SELECT r.* FROM chat_rooms r
|
||||
INNER JOIN chat_room_members m ON m.roomId = r.id
|
||||
WHERE ${whereClauses.join(" AND ")}
|
||||
ORDER BY r.updatedAt DESC
|
||||
`).all(...params) as ChatRoomRow[];
|
||||
return rows.map((row) => this.rowToRoom(row));
|
||||
}
|
||||
|
||||
addRoomMessage(roomId: string, input: ChatRoomMessageCreateInput): ChatRoomMessage {
|
||||
const room = this.getRoom(roomId);
|
||||
if (!room) {
|
||||
throw new Error(`Chat room ${roomId} not found`);
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const message: ChatRoomMessage = {
|
||||
id: `rmsg-${randomUUID().slice(0, 8)}`,
|
||||
roomId,
|
||||
role: input.role,
|
||||
content: input.content,
|
||||
thinkingOutput: input.thinkingOutput ?? null,
|
||||
metadata: input.metadata ?? null,
|
||||
attachments: input.attachments,
|
||||
senderAgentId: input.senderAgentId ?? null,
|
||||
mentions: input.mentions ?? [],
|
||||
createdAt: now,
|
||||
};
|
||||
|
||||
this.db.prepare(`
|
||||
INSERT INTO chat_room_messages (id, roomId, role, content, thinkingOutput, metadata, attachments, senderAgentId, mentions, createdAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
message.id,
|
||||
message.roomId,
|
||||
message.role,
|
||||
message.content,
|
||||
message.thinkingOutput,
|
||||
toJsonNullable(message.metadata),
|
||||
toJsonNullable(message.attachments),
|
||||
message.senderAgentId,
|
||||
toJsonNullable(message.mentions),
|
||||
message.createdAt,
|
||||
);
|
||||
|
||||
this.db.prepare("UPDATE chat_rooms SET updatedAt = ? WHERE id = ?").run(now, roomId);
|
||||
this.db.bumpLastModified();
|
||||
this.emit("chat:room:message:added", message);
|
||||
return message;
|
||||
}
|
||||
|
||||
getRoomMessages(roomId: string, filter?: ChatRoomMessagesFilter): ChatRoomMessage[] {
|
||||
const whereClauses: string[] = ["roomId = ?"];
|
||||
const params: Array<string | number> = [roomId];
|
||||
if (filter?.before) {
|
||||
whereClauses.push("createdAt < ?");
|
||||
params.push(filter.before);
|
||||
}
|
||||
|
||||
const rows = this.db.prepare(`
|
||||
SELECT * FROM chat_room_messages
|
||||
WHERE ${whereClauses.join(" AND ")}
|
||||
ORDER BY createdAt ASC
|
||||
LIMIT ? OFFSET ?
|
||||
`).all(...params, filter?.limit ?? 100, filter?.offset ?? 0) as ChatRoomMessageRow[];
|
||||
|
||||
return rows.map((row) => this.rowToRoomMessage(row));
|
||||
}
|
||||
|
||||
getRoomMessage(id: string): ChatRoomMessage | undefined {
|
||||
const row = this.db.prepare("SELECT * FROM chat_room_messages WHERE id = ?").get(id) as ChatRoomMessageRow | undefined;
|
||||
return row ? this.rowToRoomMessage(row) : undefined;
|
||||
}
|
||||
|
||||
deleteRoomMessage(id: string): boolean {
|
||||
const message = this.getRoomMessage(id);
|
||||
if (!message) return false;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
this.db.prepare("DELETE FROM chat_room_messages WHERE id = ?").run(id);
|
||||
this.db.prepare("UPDATE chat_rooms SET updatedAt = ? WHERE id = ?").run(now, message.roomId);
|
||||
|
||||
this.db.bumpLastModified();
|
||||
this.emit("chat:room:message:deleted", id);
|
||||
|
||||
const updatedRoom = this.getRoom(message.roomId);
|
||||
if (updatedRoom) {
|
||||
this.emit("chat:room:updated", updatedRoom);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
addRoomMessageAttachment(roomId: string, messageId: string, attachment: ChatAttachment): ChatRoomMessage {
|
||||
const message = this.getRoomMessage(messageId);
|
||||
if (!message || message.roomId !== roomId) {
|
||||
throw new Error(`Message ${messageId} not found in room ${roomId}`);
|
||||
}
|
||||
|
||||
const updatedAttachments = [...(message.attachments ?? []), attachment];
|
||||
this.db.prepare("UPDATE chat_room_messages SET attachments = ? WHERE id = ?").run(
|
||||
toJsonNullable(updatedAttachments),
|
||||
messageId,
|
||||
);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
this.db.prepare("UPDATE chat_rooms SET updatedAt = ? WHERE id = ?").run(now, roomId);
|
||||
|
||||
const updated = this.getRoomMessage(messageId);
|
||||
if (!updated) {
|
||||
throw new Error(`Failed to update room message ${messageId}`);
|
||||
}
|
||||
|
||||
this.db.bumpLastModified();
|
||||
this.emit("chat:room:message:updated", updated);
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user