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

CBIIT / crdc-datahub-ui / 23070562610

13 Mar 2026 09:07PM UTC coverage: 83.046% (+0.9%) from 82.146%
23070562610

Pull #932

github

web-flow
Merge 12ff4248b into 7804c52d3
Pull Request #932: CRDCDH-3398 Created ChatBot prototype

5874 of 6444 branches covered (91.15%)

Branch coverage included in aggregate %.

2270 of 2296 new or added lines in 21 files covered. (98.87%)

1 existing line in 1 file now uncovered.

34738 of 42459 relevant lines covered (81.82%)

246.57 hits per line

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

98.06
/src/components/ChatBot/api/knowledgeBaseClient.ts
1
import { Logger } from "@/utils";
1✔
2

3
import chatConfig from "../config/chatConfig";
1✔
4
import { storeSessionId } from "../utils/sessionStorageUtils";
1✔
5

6
export type AskKnowledgeBaseResponse = {
7
  question: string;
8
  answer: string;
9
  citations?: ChatCitation[];
10
  sessionId?: string;
11
};
12

13
type AskQuestionArgs = {
14
  question: string;
15
  sessionId?: string | null;
16
  conversationHistory?: ConversationHistory[];
17
  onChunk?: (chunk: string) => void;
18
  onCitation?: (citation: ChatCitation) => void;
19
  onPulse?: (description: string) => void;
20
  signal?: AbortSignal;
21
  url: string;
22
  typewriterDelay?: number;
23
};
24

25
type AskQuestionResult = {
26
  sessionId: string | null;
27
  citations: ChatCitation[];
28
};
29

30
/**
31
 * Emits text with a typewriter effect (character by character) using a configurable delay.
32
 *
33
 * @param {string} text - The text to emit character by character
34
 * @param {(chunk: string) => void} [onChunk] - Optional callback to invoke with each character
35
 * @param {number} [typewriterDelay=10] - Delay in milliseconds between characters (0 to disable typewriter effect)
36
 * @param {AbortSignal} [signal] - Optional abort signal to stop the typewriter effect
37
 * @returns {Promise<void>}
38
 */
39
export async function emitWithTypewriter(
47✔
40
  text: string,
47✔
41
  onChunk?: (chunk: string) => void,
47✔
42
  typewriterDelay = 10,
47✔
43
  signal?: AbortSignal
47✔
44
): Promise<void> {
47✔
45
  if (!onChunk || !text) {
47✔
46
    return;
5✔
47
  }
5✔
48

49
  // If typewriter delay is 0, emit the whole text at once
50
  if (typewriterDelay === 0) {
47✔
51
    onChunk(text);
27✔
52
    return;
27✔
53
  }
27✔
54

55
  for (let i = 0; i < text.length; i += 1) {
47✔
56
    if (signal?.aborted) {
64✔
57
      return;
4✔
58
    }
4✔
59
    onChunk(text[i]);
60✔
60
    // eslint-disable-next-line no-await-in-loop
61
    await new Promise<void>((resolve) => {
60✔
62
      setTimeout(resolve, typewriterDelay);
60✔
63
    });
60✔
64
  }
60✔
65
}
60✔
66

67
/**
68
 * Processes the streaming response from the knowledge base API with typewriter effect.
69
 *
70
 * @param {ReadableStreamDefaultReader<Uint8Array>} reader - The stream reader for the response body
71
 * @param {(chunk: string) => void} [onChunk] - Optional callback to invoke with each text chunk
72
 * @param {(citation: ChatCitation) => void} [onCitation] - Optional callback to invoke with each citation
73
 * @param {number} [typewriterDelay=10] - Delay in milliseconds between characters (0 to disable typewriter effect)
74
 * @param {AbortSignal} [signal] - Optional abort signal to stop the typewriter effect
75
 * @param {(description: string) => void} [onPulse] - Optional callback to invoke with pulse status descriptions
76
 * @returns {Promise<{ sessionId: string | null; citations: ChatCitation[] }>} The session ID and collected citations from the response
77
 */
78
export async function processStreamingResponse(
32✔
79
  reader: ReadableStreamDefaultReader<Uint8Array>,
32✔
80
  onChunk?: (chunk: string) => void,
32✔
81
  onCitation?: (citation: ChatCitation) => void,
32✔
82
  typewriterDelay = 10,
32✔
83
  signal?: AbortSignal,
32✔
84
  onPulse?: (description: string) => void
32✔
85
): Promise<{ sessionId: string | null; citations: ChatCitation[] }> {
32✔
86
  const decoder = new TextDecoder();
32✔
87
  let buffer = "";
32✔
88
  let currentSessionId: string | null = null;
32✔
89
  const citations: ChatCitation[] = [];
32✔
90
  const seenCitationLinks = new Set<string>();
32✔
91
  let done = false;
32✔
92

93
  while (!done) {
32✔
94
    // eslint-disable-next-line no-await-in-loop
95
    const result = await reader.read();
98✔
96
    done = result.done;
97✔
97

98
    if (done) {
98✔
99
      break;
31✔
100
    }
31✔
101

102
    buffer += decoder.decode(result.value, { stream: true });
66✔
103
    const lines = buffer.split("\n");
66✔
104
    buffer = lines.pop() || "";
98✔
105

106
    for (const line of lines) {
98✔
107
      if (typeof line !== "string" || !line?.trim()) {
72✔
108
        Logger.error("[KnowledgeBase] Received non-string or empty line:", line);
7✔
109
        // eslint-disable-next-line no-continue
110
        continue;
7✔
111
      }
7✔
112

113
      try {
65✔
114
        const parsed = JSON.parse(line);
65✔
115

116
        switch (parsed.type) {
65✔
117
          case "session":
72✔
118
            if (currentSessionId === null && parsed.sessionId) {
17✔
119
              Logger.info("[KnowledgeBase] Received session ID:", parsed.sessionId);
16✔
120
              currentSessionId = parsed.sessionId;
16✔
121
            }
16✔
122
            break;
17✔
123

124
          case "response":
72✔
125
            if (parsed.output) {
37✔
126
              // eslint-disable-next-line no-await-in-loop
127
              await emitWithTypewriter(parsed.output, onChunk, typewriterDelay, signal);
36✔
128
            }
36✔
129
            break;
37✔
130

131
          case "citations":
72✔
132
            if (!Array.isArray(parsed.citations)) {
8!
NEW
133
              break;
×
NEW
134
            }
×
135

136
            for (const citation of parsed.citations) {
8✔
137
              const link = citation.documentLink ?? "";
9!
138
              if (link && seenCitationLinks.has(link)) {
9✔
139
                // eslint-disable-next-line no-continue
140
                continue;
2✔
141
              }
2✔
142
              if (link) {
7✔
143
                seenCitationLinks.add(link);
7✔
144
              }
7✔
145

146
              citations.push(citation);
7✔
147
              onCitation?.(citation);
7✔
148
            }
9✔
149
            break;
8✔
150

151
          case "pulse":
72✔
152
            if (parsed.description) {
1✔
153
              Logger.info("[KnowledgeBase] Pulse:", parsed.description);
1✔
154
              onPulse?.(parsed.description);
1✔
155
            }
1✔
156
            break;
1✔
157

158
          default:
72✔
159
            Logger.info("[KnowledgeBase] Unknown event type:", parsed?.type, parsed);
1✔
160
            break;
1✔
161
        }
72✔
162

163
        if (parsed.error) {
72✔
164
          Logger.error("[KnowledgeBase] Stream error:", parsed.error);
1✔
165
        }
1✔
166
      } catch (e) {
72✔
167
        Logger.error("[KnowledgeBase] Failed to parse line:", line, e);
1✔
168
      }
1✔
169
    }
72✔
170
  }
66✔
171

172
  return { sessionId: currentSessionId, citations };
31✔
173
}
31✔
174

175
/**
176
 * Sends a question to the knowledge base API and streams the response.
177
 *
178
 * @param {AskQuestionArgs} args - The question arguments
179
 * @param {string} args.question - The question text to send
180
 * @param {string | null} [args.sessionId] - Optional session ID to continue a conversation
181
 * @param {(chunk: string) => void} [args.onChunk] - Optional callback invoked with each text chunk as it arrives
182
 * @param {(citation: ChatCitation) => void} [args.onCitation] - Optional callback invoked with each citation as it arrives
183
 * @param {AbortSignal} [args.signal] - Optional abort signal to cancel the request
184
 * @param {string} args.url - The knowledge base API endpoint URL
185
 * @param {number} [args.typewriterDelay=10] - Delay in milliseconds between characters (0 to disable typewriter effect)
186
 * @returns {Promise<AskQuestionResult>} The session ID and citations from the response
187
 * @throws {Error} If the URL is not provided or the HTTP request fails
188
 */
189
export async function askQuestion({
18✔
190
  question,
18✔
191
  sessionId = null,
18✔
192
  conversationHistory = [],
18✔
193
  onChunk,
18✔
194
  onCitation,
18✔
195
  onPulse,
18✔
196
  signal,
18✔
197
  url,
18✔
198
  typewriterDelay = 10,
18✔
199
}: AskQuestionArgs): Promise<AskQuestionResult> {
18✔
200
  if (!url) {
18✔
201
    throw new Error("Knowledge base URL is required but was not provided");
1✔
202
  }
1✔
203

204
  try {
17✔
205
    const truncatedQuestion = question.slice(0, chatConfig.maxInputTextLength);
17✔
206
    const truncatedHistory = conversationHistory.slice(-chatConfig.maxConversationHistoryLength);
17✔
207

208
    const response = await fetch(url, {
17✔
209
      method: "POST",
17✔
210
      headers: { "Content-Type": "application/json" },
17✔
211
      body: JSON.stringify({
17✔
212
        question: truncatedQuestion,
17✔
213
        sessionId,
17✔
214
        conversationHistory: truncatedHistory,
17✔
215
      }),
17✔
216
      signal,
17✔
217
    });
17✔
218

219
    if (!response.ok) {
18✔
220
      throw new Error(`HTTP error! status: ${response.status}`);
1✔
221
    }
1✔
222

223
    const reader = response.body.getReader();
14✔
224
    const { sessionId: currentSessionId, citations } = await processStreamingResponse(
14✔
225
      reader,
14✔
226
      onChunk,
14✔
227
      onCitation,
14✔
228
      typewriterDelay,
14✔
229
      signal,
14✔
230
      onPulse
14✔
231
    );
14✔
232

233
    if (currentSessionId) {
18✔
234
      storeSessionId(currentSessionId);
12✔
235
    }
12✔
236

237
    if (citations.length > 0) {
18✔
238
      Logger.info("[KnowledgeBase] Citations:", citations);
2✔
239
    }
2✔
240

241
    return { sessionId: currentSessionId, citations };
13✔
242
  } catch (error) {
18✔
243
    Logger.error("[KnowledgeBase] Error:", error);
4✔
244
    throw error;
4✔
245
  }
4✔
246
}
18✔
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