• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CBIIT / crdc-datahub-ui / 21080480968

16 Jan 2026 08:51PM UTC coverage: 80.151% (+0.8%) from 79.35%
21080480968

Pull #932

github

web-flow
Merge 2838b9429 into 9dfdabde2
Pull Request #932: CRDCDH-3398 Created ChatBot prototype

5536 of 6059 branches covered (91.37%)

Branch coverage included in aggregate %.

1710 of 1753 new or added lines in 19 files covered. (97.55%)

1 existing line in 1 file now uncovered.

32566 of 41479 relevant lines covered (78.51%)

223.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.45
/src/components/ChatBot/context/ChatConversationContext.tsx
1
import React, {
1✔
2
  createContext,
3
  useCallback,
4
  useContext,
5
  useEffect,
6
  useMemo,
7
  useReducer,
8
  useRef,
9
} from "react";
10

11
import { askQuestion } from "../api/knowledgeBaseClient";
1✔
12
import chatConfig from "../config/chatConfig";
1✔
13
import { createChatMessage, createId, isAbortError } from "../utils/chatUtils";
1✔
14
import { clearStoredSessionId, getStoredSessionId } from "../utils/sessionStorageUtils";
1✔
15

16
import { useChatBotContext } from "./ChatBotContext";
1✔
17

18
type ChatState = {
19
  messages: ChatMessage[];
20
  inputValue: string;
21
  status: ChatStatus;
22
};
23

24
type ChatAction =
25
  | { type: "input_changed"; value: string }
26
  | { type: "input_cleared" }
27
  | { type: "message_added"; message: ChatMessage }
28
  | { type: "status_changed"; status: ChatStatus }
29
  | { type: "conversation_reset" };
30

31
export type ChatConversationActions = {
32
  greetingTimestamp: Date;
33
  messages: ChatMessage[];
34
  inputValue: string;
35
  isBotTyping: boolean;
36
  setInputValue: (value: string) => void;
37
  sendMessage: () => void;
38
  handleKeyDown: React.KeyboardEventHandler<HTMLDivElement>;
39
  endConversation: () => void;
40
};
41

42
/**
43
 * Chat reducer to manage chat state transitions.
44
 *
45
 * @param state - The current chat state
46
 * @param action - The Action to process
47
 * @returns The updated chat state
48
 */
49
export const chatReducer = (state: ChatState, action: ChatAction): ChatState => {
1✔
50
  switch (action.type) {
341✔
51
    case "input_changed": {
341✔
52
      return { ...state, inputValue: action.value };
231✔
53
    }
231✔
54
    case "input_cleared": {
341✔
55
      return { ...state, inputValue: "" };
22✔
56
    }
22✔
57
    case "message_added": {
341✔
58
      const existingIndex = state.messages.findIndex((msg) => msg.id === action.message.id);
38✔
59
      if (existingIndex !== -1) {
38✔
60
        const updatedMessages = [...state.messages];
4✔
61
        updatedMessages[existingIndex] = action.message;
4✔
62
        return { ...state, messages: updatedMessages };
4✔
63
      }
4✔
64

65
      return { ...state, messages: [...state.messages, action.message] };
34✔
66
    }
34✔
67
    case "status_changed": {
341✔
68
      return { ...state, status: action.status };
45✔
69
    }
45✔
70
    case "conversation_reset": {
341✔
71
      return {
4✔
72
        messages: [
4✔
73
          createChatMessage({
4✔
74
            text: chatConfig.initialMessage,
4✔
75
            sender: "bot",
4✔
76
            senderName: chatConfig.supportBotName,
4✔
77
          }),
4✔
78
        ],
79
        inputValue: "",
4✔
80
        status: "idle",
4✔
81
      };
4✔
82
    }
4✔
83
    default: {
341✔
84
      return state;
1✔
85
    }
1✔
86
  }
341✔
87
};
341✔
88

89
/**
90
 * Custom hook to manage chat conversation state and behavior.
91
 *
92
 * @returns {ChatConversationActions} An object containing chat state and action handlers.
93
 */
94
const useChatConversation = (): ChatConversationActions => {
1✔
95
  const { knowledgeBaseUrl } = useChatBotContext();
325✔
96
  const greetingTimestampRef = useRef<Date>(new Date());
325✔
97

98
  const [state, dispatch] = useReducer(chatReducer, {
325✔
99
    messages: [
325✔
100
      createChatMessage({
325✔
101
        text: chatConfig.initialMessage,
325✔
102
        sender: "bot",
325✔
103
        senderName: chatConfig.supportBotName,
325✔
104
      }),
325✔
105
    ],
106
    inputValue: "",
325✔
107
    status: "idle",
325✔
108
  });
325✔
109

110
  const stateRef = useRef(state);
325✔
111
  stateRef.current = state;
325✔
112

113
  const activeRequestRef = useRef<{
325✔
114
    requestId: string;
115
    abortController: AbortController;
116
  } | null>(null);
325✔
117

118
  useEffect(
325✔
119
    () => () => {
325✔
120
      activeRequestRef.current?.abortController.abort();
53✔
121
      activeRequestRef.current = null;
53✔
122
    },
53✔
123
    []
325✔
124
  );
325✔
125

126
  /**
127
   * Handles errors that occur during bot reply requests.
128
   */
129
  const handleReplyError = useCallback((error: unknown, requestId: string): void => {
325✔
130
    const active = activeRequestRef.current;
4✔
131
    if (!active || active.requestId !== requestId) {
4✔
132
      return;
1✔
133
    }
1✔
134

135
    if (active.abortController.signal.aborted || isAbortError(error)) {
4✔
136
      return;
1✔
137
    }
1✔
138

139
    dispatch({
2✔
140
      type: "message_added",
2✔
141
      message: createChatMessage({
2✔
142
        text: "Sorry, an unexpected error occurred. Please try again later.",
2✔
143
        sender: "bot",
2✔
144
        senderName: chatConfig.supportBotName,
2✔
145
        variant: "error",
2✔
146
      }),
2✔
147
    });
2✔
148

149
    dispatch({ type: "status_changed", status: "idle" });
2✔
150
  }, []);
325✔
151

152
  /**
153
   * Executes the bot reply request with streaming support.
154
   */
155
  const runReply = useCallback(
325✔
156
    async (
325✔
157
      userMessage: string,
22✔
158
      requestId: string,
22✔
159
      abortController: AbortController
22✔
160
    ): Promise<void> => {
22✔
161
      try {
22✔
162
        const botMessageId = createId("bot_msg_");
22✔
163
        let accumulatedText = "";
22✔
164
        const allCitations: ChatCitation[] = [];
22✔
165
        let firstChunkReceived = false;
22✔
166

167
        await askQuestion({
22✔
168
          question: userMessage,
22✔
169
          sessionId: getStoredSessionId(),
22✔
170
          signal: abortController.signal,
22✔
171
          url: knowledgeBaseUrl,
22✔
172
          onChunk: (chunk: string) => {
22✔
173
            if (!firstChunkReceived) {
14✔
174
              dispatch({ type: "status_changed", status: "idle" });
10✔
175
              firstChunkReceived = true;
10✔
176
            }
10✔
177

178
            accumulatedText += chunk;
14✔
179
            dispatch({
14✔
180
              type: "message_added",
14✔
181
              message: createChatMessage({
14✔
182
                id: botMessageId,
14✔
183
                text: accumulatedText,
14✔
184
                sender: "bot",
14✔
185
                senderName: chatConfig.supportBotName,
14✔
186
              }),
14✔
187
            });
14✔
188
          },
14✔
189
          onCitation: (citation) => {
22✔
NEW
190
            allCitations?.push(citation);
×
NEW
191
          },
×
192
        });
22✔
193

194
        const active = activeRequestRef.current;
18✔
195
        if (!active || active.requestId !== requestId || active.abortController.signal.aborted) {
22✔
196
          return;
7✔
197
        }
7✔
198

199
        // Add citations to existing bot message if they exist
200
        if (allCitations?.length > 0) {
22!
NEW
201
          dispatch({
×
NEW
202
            type: "message_added",
×
NEW
203
            message: createChatMessage({
×
NEW
204
              id: botMessageId,
×
NEW
205
              text: accumulatedText,
×
NEW
206
              sender: "bot",
×
NEW
207
              senderName: chatConfig.supportBotName,
×
NEW
208
              citations: allCitations,
×
NEW
209
            }),
×
NEW
210
          });
×
NEW
211
        }
✔
212

213
        dispatch({ type: "status_changed", status: "idle" });
11✔
214
      } catch (error) {
22✔
215
        handleReplyError(error, requestId);
4✔
216
      }
4✔
217
    },
22✔
218
    [knowledgeBaseUrl, handleReplyError]
325✔
219
  );
325✔
220

221
  /**
222
   * Updates the input field value in the chat state.
223
   */
224
  const setInputValue = useCallback((value: string): void => {
325✔
225
    dispatch({ type: "input_changed", value });
231✔
226
  }, []);
325✔
227

228
  /**
229
   * Sends the current input message to the bot.
230
   */
231
  const sendMessage = useCallback((): void => {
325✔
232
    const { current } = stateRef;
24✔
233
    const value = current.inputValue?.trim();
24✔
234

235
    if (!value) {
24✔
236
      return;
2✔
237
    }
2✔
238

239
    if (current.status === "bot_typing") {
24!
NEW
240
      return;
×
NEW
241
    }
✔
242

243
    dispatch({
22✔
244
      type: "message_added",
22✔
245
      message: createChatMessage({
22✔
246
        text: value,
22✔
247
        sender: "user",
22✔
248
        senderName: chatConfig.userDisplayName,
22✔
249
      }),
22✔
250
    });
22✔
251

252
    dispatch({ type: "input_cleared" });
22✔
253
    dispatch({ type: "status_changed", status: "bot_typing" });
22✔
254

255
    activeRequestRef.current?.abortController.abort();
24✔
256

257
    const abortController = new AbortController();
24✔
258
    const requestId = createId("bot_reply_");
24✔
259
    activeRequestRef.current = { requestId, abortController };
24✔
260

261
    runReply(value, requestId, abortController).catch((error: unknown) => {
24✔
NEW
262
      if (!isAbortError(error)) {
×
NEW
263
        dispatch({ type: "status_changed", status: "idle" });
×
NEW
264
      }
×
265
    });
24✔
266
  }, [runReply]);
325✔
267

268
  /**
269
   * Handles keyboard events in the chat input.
270
   */
271
  const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> = useCallback(
325✔
272
    (event) => {
325✔
273
      if (event.key !== "Enter") {
239✔
274
        return;
237✔
275
      }
237✔
276

277
      if (event.shiftKey) {
239✔
278
        return;
1✔
279
      }
1✔
280

281
      event.preventDefault();
1✔
282
      sendMessage();
1✔
283
    },
239✔
284
    [sendMessage]
325✔
285
  );
325✔
286

287
  /**
288
   * Ends the current conversation and resets to initial state.
289
   */
290
  const endConversation = useCallback((): void => {
325✔
291
    clearStoredSessionId();
4✔
292
    activeRequestRef.current?.abortController.abort();
4✔
293
    activeRequestRef.current = null;
4✔
294
    greetingTimestampRef.current = new Date();
4✔
295
    dispatch({ type: "conversation_reset" });
4✔
296
  }, []);
325✔
297

298
  return {
325✔
299
    greetingTimestamp: greetingTimestampRef.current,
325✔
300
    messages: state.messages,
325✔
301
    inputValue: state.inputValue,
325✔
302
    isBotTyping: state.status === "bot_typing",
325✔
303
    setInputValue,
325✔
304
    sendMessage,
325✔
305
    handleKeyDown,
325✔
306
    endConversation,
325✔
307
  };
325✔
308
};
325✔
309

310
type ChatConversationContextValue = ChatConversationActions;
311

312
const ChatConversationContext = createContext<ChatConversationContextValue | null>(null);
1✔
313

314
export const useChatConversationContext = (): ChatConversationContextValue => {
1✔
315
  const context = useContext(ChatConversationContext);
341✔
316

317
  if (!context) {
341!
NEW
318
    throw new Error("useChatConversationContext must be used within ChatConversationProvider");
×
NEW
319
  }
×
320

321
  return context;
341✔
322
};
341✔
323

324
export type ChatConversationProviderProps = {
325
  children: React.ReactNode;
326
};
327

328
export const ChatConversationProvider: React.FC<ChatConversationProviderProps> = ({ children }) => {
1✔
329
  const conversationHook = useChatConversation();
325✔
330

331
  const value = useMemo<ChatConversationContextValue>(() => conversationHook, [conversationHook]);
325✔
332

333
  return (
325✔
334
    <ChatConversationContext.Provider value={value}>{children}</ChatConversationContext.Provider>
325✔
335
  );
336
};
325✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc