feat(dashboard): add conversations workspace

This commit is contained in:
jjbrunton committed 2026-08-08 15:05:08 +01:00
1 parent 40e5f4c818
commit 8d4eae7ab5
8 files changed
+840

No files matched your search

@@ -0,0 +1,85 @@
import type { ZodType } from 'zod';
import {
ConversationsResponseSchema,
ConversationDetailResponseSchema,
ErrorResponseSchema,
SendReplyRequestSchema,
StartConversationRequestSchema,
StartConversationResponseSchema,
SuccessResponseSchema,
SuggestedReplyResponseSchema,
type ActiveChat,
type ConversationDetailResponse,
type SuccessResponse,
} from '../../../../../packages/api-contracts/src/index.js';
async function parseResponse<T>(response: Response, schema: ZodType<T>): Promise<T> {
const payload: unknown = await response.json();
if (!response.ok) {
const parsedError = ErrorResponseSchema.safeParse(payload);
throw new Error(parsedError.success ? parsedError.data.error : `Request failed (${response.status})`);
}
return schema.parse(payload);
}
function conversationPath(id: string): string {
return `/api/conversations/${encodeURIComponent(id)}`;
}
export interface ConversationApi {
list(): Promise<ActiveChat[]>;
detail(id: string): Promise<ConversationDetailResponse>;
suggestReply(id: string): Promise<string>;
sendReply(id: string, message: string): Promise<SuccessResponse>;
startConversation(username: string, message: string): Promise<ActiveChat>;
}
export function createConversationApi(fetchImplementation?: typeof fetch): ConversationApi {
function request(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
return (fetchImplementation ?? globalThis.fetch)(input, init);
}
return {
async list() {
return parseResponse(await request('/api/conversations'), ConversationsResponseSchema);
},
async detail(id) {
return parseResponse(await request(conversationPath(id)), ConversationDetailResponseSchema);
},
async suggestReply(id) {
const response = await parseResponse(
await request(`${conversationPath(id)}/suggest-reply`, { method: 'POST' }),
SuggestedReplyResponseSchema,
);
return response.suggestedReply;
},
async sendReply(id, message) {
const body = SendReplyRequestSchema.parse({ message: message.trim() });
return parseResponse(
await request(`${conversationPath(id)}/send-reply`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}),
SuccessResponseSchema,
);
},
async startConversation(username, message) {
const body = StartConversationRequestSchema.parse({
username: username.trim().replace(/^u\//i, ''),
message: message.trim(),
});
const response = await parseResponse(
await request('/api/conversations/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}),
StartConversationResponseSchema,
);
return response.chat;
},
};
}
export const conversationApi = createConversationApi();