Skip to content

Commit 13adc61

Browse files
authored
refactor(ui): event handling refactor (#805)
1 parent 7bbe174 commit 13adc61

File tree

7 files changed

+282
-144
lines changed

7 files changed

+282
-144
lines changed

packages/ragbits-chat/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# CHANGELOG
22

33
## Unreleased
4+
- Refactor chat handlers in the UI to use registry (#805)
45
- Add auth token storage and automatic logout on 401 (#802)
56
- Improve user settings storage when history is disabled (#799)
67
- Remove redundant test for `/api/config` endpoint (#795)

scripts/generate_typescript_from_json_schema.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -203,18 +203,11 @@ def _generate_chat_response_union_type() -> str:
203203
("MessageUsageChatResponse", "usage", "Record<string, MessageUsage>"),
204204
]
205205

206-
exported_response_interfaces = [
206+
internal_response_interfaces = [
207207
("ChunkedChatResponse", "chunked_content", "ChunkedContent"),
208208
]
209209

210-
for interface_name, response_type, content_type in response_interfaces:
211-
lines.append(f"interface {interface_name} {{")
212-
lines.append(f" type: '{response_type}'")
213-
lines.append(f" content: {content_type}")
214-
lines.append("}")
215-
lines.append("")
216-
217-
for interface_name, response_type, content_type in exported_response_interfaces:
210+
for interface_name, response_type, content_type in [*response_interfaces, *internal_response_interfaces]:
218211
lines.append(f"export interface {interface_name} {{")
219212
lines.append(f" type: '{response_type}'")
220213
lines.append(f" content: {content_type}")

typescript/@ragbits/api-client/src/autogen.types.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -414,52 +414,52 @@ export interface User {
414414
/**
415415
* Specific chat response types
416416
*/
417-
interface TextChatResponse {
417+
export interface TextChatResponse {
418418
type: 'text'
419419
content: string
420420
}
421421

422-
interface ReferenceChatResponse {
422+
export interface ReferenceChatResponse {
423423
type: 'reference'
424424
content: Reference
425425
}
426426

427-
interface MessageIdChatResponse {
427+
export interface MessageIdChatResponse {
428428
type: 'message_id'
429429
content: string
430430
}
431431

432-
interface ConversationIdChatResponse {
432+
export interface ConversationIdChatResponse {
433433
type: 'conversation_id'
434434
content: string
435435
}
436436

437-
interface StateUpdateChatResponse {
437+
export interface StateUpdateChatResponse {
438438
type: 'state_update'
439439
content: ServerState
440440
}
441441

442-
interface LiveUpdateChatResponse {
442+
export interface LiveUpdateChatResponse {
443443
type: 'live_update'
444444
content: LiveUpdate
445445
}
446446

447-
interface FollowupMessagesChatResponse {
447+
export interface FollowupMessagesChatResponse {
448448
type: 'followup_messages'
449449
content: string[]
450450
}
451451

452-
interface ImageChatResponse {
452+
export interface ImageChatResponse {
453453
type: 'image'
454454
content: Image
455455
}
456456

457-
interface ClearMessageResponse {
457+
export interface ClearMessageResponse {
458458
type: 'clear_message'
459459
content: never
460460
}
461461

462-
interface MessageUsageChatResponse {
462+
export interface MessageUsageChatResponse {
463463
type: 'usage'
464464
content: Record<string, MessageUsage>
465465
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { ChatResponse, ChatResponseType } from "@ragbits/api-client-react";
2+
import { Conversation, HistoryStore } from "../../../../types/history";
3+
import {
4+
handleAfterConversationId,
5+
handleConversationId,
6+
handleFollowupMessages,
7+
handleStateUpdate,
8+
} from "./nonMessageHandlers";
9+
import {
10+
handleClearMessage,
11+
handleImage,
12+
handleLiveUpdate,
13+
handleMessageId,
14+
handleReference,
15+
handleText,
16+
handleUsage,
17+
} from "./messageHandlers";
18+
19+
type BaseHandlerContext = {
20+
conversationIdRef: { current: string };
21+
messageId: string;
22+
};
23+
24+
export type PrimaryHandler<
25+
T extends ChatResponse,
26+
Ctx extends object = object,
27+
> = (response: T, draft: Conversation, ctx: BaseHandlerContext) => Ctx | void;
28+
29+
export type AfterHandler<
30+
T extends ChatResponse,
31+
Ctx extends object = object,
32+
> = (response: T, draft: HistoryStore, ctx: BaseHandlerContext & Ctx) => void;
33+
34+
export type HandlerEntry<
35+
T extends ChatResponse,
36+
Ctx extends object = object,
37+
> = {
38+
handle: PrimaryHandler<T, Ctx>;
39+
after?: AfterHandler<T, Ctx>;
40+
};
41+
42+
class ChatResponseHandlerRegistry {
43+
private handlers = new Map<ChatResponseType, unknown>();
44+
45+
register<
46+
K extends ChatResponseType,
47+
R extends Extract<ChatResponse, { type: K }>,
48+
Ctx extends object = object,
49+
>(type: K, entry: HandlerEntry<R, Ctx>) {
50+
if (this.handlers.has(type)) {
51+
console.warn(
52+
`Handler for ${String(type)} already registered - overwriting.`,
53+
);
54+
}
55+
this.handlers.set(type, entry);
56+
}
57+
58+
get<
59+
K extends ChatResponseType,
60+
R extends Extract<ChatResponse, { type: K }>,
61+
Ctx extends object = object,
62+
>(type: K): HandlerEntry<R, Ctx> {
63+
const raw = this.handlers.get(type);
64+
if (!raw) {
65+
throw new Error(`No handler registered for type: ${String(type)}`);
66+
}
67+
return raw as HandlerEntry<R, Ctx>;
68+
}
69+
}
70+
71+
export const ChatHandlerRegistry = new ChatResponseHandlerRegistry();
72+
ChatHandlerRegistry.register(ChatResponseType.StateUpdate, {
73+
handle: handleStateUpdate,
74+
});
75+
ChatHandlerRegistry.register(ChatResponseType.ConversationId, {
76+
handle: handleConversationId,
77+
after: handleAfterConversationId,
78+
});
79+
ChatHandlerRegistry.register(ChatResponseType.FollowupMessages, {
80+
handle: handleFollowupMessages,
81+
});
82+
83+
ChatHandlerRegistry.register(ChatResponseType.Text, {
84+
handle: handleText,
85+
});
86+
ChatHandlerRegistry.register(ChatResponseType.Reference, {
87+
handle: handleReference,
88+
});
89+
ChatHandlerRegistry.register(ChatResponseType.MessageId, {
90+
handle: handleMessageId,
91+
});
92+
ChatHandlerRegistry.register(ChatResponseType.LiveUpdate, {
93+
handle: handleLiveUpdate,
94+
});
95+
ChatHandlerRegistry.register(ChatResponseType.Image, {
96+
handle: handleImage,
97+
});
98+
ChatHandlerRegistry.register(ChatResponseType.ClearMessage, {
99+
handle: handleClearMessage,
100+
});
101+
ChatHandlerRegistry.register(ChatResponseType.Usage, {
102+
handle: handleUsage,
103+
});
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import {
2+
ClearMessageResponse,
3+
ImageChatResponse,
4+
LiveUpdateChatResponse,
5+
LiveUpdateType,
6+
MessageIdChatResponse,
7+
MessageUsageChatResponse,
8+
ReferenceChatResponse,
9+
TextChatResponse,
10+
} from "@ragbits/api-client-react";
11+
import { PrimaryHandler } from "./eventHandlerRegistry";
12+
import { produce } from "immer";
13+
14+
export const handleText: PrimaryHandler<TextChatResponse> = (
15+
response,
16+
draft,
17+
ctx,
18+
) => {
19+
const message = draft.history[ctx.messageId];
20+
message.content += response.content;
21+
};
22+
23+
export const handleReference: PrimaryHandler<ReferenceChatResponse> = (
24+
response,
25+
draft,
26+
ctx,
27+
) => {
28+
const message = draft.history[ctx.messageId];
29+
message.references = [...(message.references ?? []), response.content];
30+
};
31+
32+
export const handleMessageId: PrimaryHandler<MessageIdChatResponse> = (
33+
response,
34+
draft,
35+
ctx,
36+
) => {
37+
const message = draft.history[ctx.messageId];
38+
message.serverId = response.content;
39+
};
40+
41+
export const handleLiveUpdate: PrimaryHandler<LiveUpdateChatResponse> = (
42+
response,
43+
draft,
44+
ctx,
45+
) => {
46+
const message = draft.history[ctx.messageId];
47+
48+
const { update_id, content, type } = response.content;
49+
const liveUpdates = produce(message.liveUpdates ?? {}, (draft) => {
50+
if (type === LiveUpdateType.Start && update_id in draft) {
51+
console.error(
52+
`Got duplicate start event for update_id: ${update_id}. Ignoring the event.`,
53+
);
54+
}
55+
56+
draft[update_id] = content;
57+
});
58+
message.liveUpdates = liveUpdates;
59+
};
60+
61+
export const handleImage: PrimaryHandler<ImageChatResponse> = (
62+
response,
63+
draft,
64+
ctx,
65+
) => {
66+
const message = draft.history[ctx.messageId];
67+
const image = response.content;
68+
message.images = produce(message.images ?? {}, (draft) => {
69+
if (draft[image.id]) {
70+
console.error(
71+
`Got duplicate image event for image_id: ${image.id}. Ignoring the event.`,
72+
);
73+
}
74+
75+
draft[image.id] = image.url;
76+
});
77+
};
78+
79+
export const handleClearMessage: PrimaryHandler<ClearMessageResponse> = (
80+
_,
81+
draft,
82+
ctx,
83+
) => {
84+
const message = draft.history[ctx.messageId];
85+
draft.history[ctx.messageId] = {
86+
id: message.id,
87+
role: message.role,
88+
content: "",
89+
};
90+
};
91+
92+
export const handleUsage: PrimaryHandler<MessageUsageChatResponse> = (
93+
response,
94+
draft,
95+
ctx,
96+
) => {
97+
const message = draft.history[ctx.messageId];
98+
message.usage = response.content;
99+
};
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {
2+
ConversationIdChatResponse,
3+
FollowupMessagesChatResponse,
4+
StateUpdateChatResponse,
5+
} from "@ragbits/api-client-react";
6+
import { AfterHandler, PrimaryHandler } from "./eventHandlerRegistry";
7+
8+
export const handleStateUpdate: PrimaryHandler<StateUpdateChatResponse> = (
9+
{ content },
10+
draft,
11+
) => {
12+
draft.serverState = content;
13+
};
14+
15+
export const handleConversationId: PrimaryHandler<
16+
ConversationIdChatResponse,
17+
{ originalConversationId: string }
18+
> = ({ content }, draft, ctx) => {
19+
const originalConversationId = ctx.conversationIdRef.current;
20+
draft.conversationId = content;
21+
// Update the ref to propagate the change
22+
ctx.conversationIdRef.current = content;
23+
return { originalConversationId };
24+
};
25+
26+
export const handleAfterConversationId: AfterHandler<
27+
ConversationIdChatResponse,
28+
{ originalConversationId: string }
29+
> = (_, draft, { originalConversationId, conversationIdRef }) => {
30+
const oldConversation = draft.conversations[originalConversationId];
31+
32+
if (!oldConversation) {
33+
throw new Error("Received events for non-existent conversation");
34+
}
35+
36+
draft.conversations[conversationIdRef.current] = oldConversation;
37+
if (draft.currentConversation === originalConversationId) {
38+
draft.currentConversation = conversationIdRef.current;
39+
}
40+
delete draft.conversations[originalConversationId];
41+
};
42+
43+
export const handleFollowupMessages: PrimaryHandler<
44+
FollowupMessagesChatResponse
45+
> = ({ content }, draft) => {
46+
draft.followupMessages = content;
47+
};

0 commit comments

Comments
 (0)