fix(react-widget): integrate with current /stream contract

This commit is contained in:
ManishMadan2882 committed 2026-08-30 06:09:38 +05:30
1 parent e0aff39a1b
commit 1281a040ef
4 files changed
+753 -262

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -11,6 +11,7 @@ interface FetchAnswerStreamingProps {
conversationId?: string | null;
apiHost?: string;
onEvent?: (event: MessageEvent) => void;
signal?: AbortSignal;
}
export interface FeedbackPayload {
@@ -31,13 +32,17 @@ export function fetchAnswerStreaming({
onEvent = () => {
console.log('Event triggered, but no handler provided.');
},
signal,
}: FetchAnswerStreamingProps): Promise<void> {
return new Promise<void>((resolve, reject) => {
const body = {
question: question,
history: JSON.stringify(history),
history: JSON.stringify(
history
.filter((item) => item.prompt && item.response)
.map(({ prompt, response }) => ({ prompt, response })),
),
conversation_id: conversationId,
model: 'default',
api_key: apiKey,
};
fetch(apiHost + '/stream', {
@@ -46,49 +51,61 @@ export function fetchAnswerStreaming({
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
signal,
})
.then((response) => {
if (!response.body) throw Error('No response body');
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let counter = 0; // eslint-disable-line @typescript-eslint/no-unused-vars
let buffer = '';
const emit = (rawLine: string) => {
const line = rawLine.trim();
if (line === '') return;
if (!line.startsWith('data:')) return;
const payload = line.substring(5).trim();
if (payload === '' || payload === '[DONE]') return;
onEvent(new MessageEvent('message', { data: payload }));
};
const processStream = ({
done,
value,
}: ReadableStreamReadResult<Uint8Array>) => {
if (done) {
if (buffer.trim() !== '') emit(buffer);
buffer = '';
resolve();
return;
}
buffer += decoder.decode(value, { stream: true });
counter += 1;
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) emit(line);
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (let line of lines) {
if (line.trim() == '') {
continue;
}
if (line.startsWith('data:')) {
line = line.substring(5);
}
const messageEvent = new MessageEvent('message', {
data: line,
});
onEvent(messageEvent); // handle each message
}
reader.read().then(processStream).catch(reject);
reader.read().then(processStream).catch(onReadError);
};
reader.read().then(processStream).catch(reject);
const onReadError = (error: unknown) => {
if (signal?.aborted || (error as Error)?.name === 'AbortError') {
resolve();
return;
}
reject(error);
};
reader.read().then(processStream).catch(onReadError);
})
.catch((error) => {
if (signal?.aborted || (error as Error)?.name === 'AbortError') {
resolve();
return;
}
console.error('Connection failed:', error);
reject(error);
});
@@ -14,6 +14,11 @@ declare module 'styled-components' {
};
/** Present only in SearchBar theme */
name?: string;
/** Gradient stops for the swept status text. */
shimmer?: {
base: string;
highlight: string;
};
/** Present only in DocsGPTWidget theme (always provided when these styled components render) */
dimensions?: {
size: string;
@@ -41,6 +46,12 @@ export interface Query {
sources?: { title: string; text: string; source: string }[];
conversationId?: string | null;
title?: string | null;
/** Accumulated from thought events. */
thought?: string;
/** Latest notice or running workflow node; drives the status line. */
notice?: string;
/** Tool names from tool_calls / tool_call. */
toolCalls?: string[];
}
export interface WidgetProps {
@@ -0,0 +1,50 @@
export interface StreamEvent {
type?: string;
[key: string]: unknown;
}
export interface ToolCallEvent {
action_name?: string;
tool_name?: string;
}
/** internal_search -> Internal Search */
export const prettifyName = (name: string): string =>
name
.split(/[_\s]+/)
.filter(Boolean)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
/**
* Status-line text for a running workflow node, or null when the node is
* scaffolding the user gains nothing from seeing. Keyed on node_type (a
* closed enum) rather than node_title, which is free text like "agent node".
*/
export const workflowStepLabel = (event: StreamEvent): string | null => {
if (event.status !== 'running') return null;
switch (event.node_type) {
case 'agent':
return 'Thinking…';
case 'code':
return 'Running code…';
case 'condition':
return 'Deciding next step…';
case 'start':
case 'end':
case 'state':
case 'note':
return null;
default:
return typeof event.node_title === 'string' && event.node_title
? `${prettifyName(event.node_title)}…`
: null;
}
};
export const toolNames = (calls: unknown): string[] => {
if (!Array.isArray(calls)) return [];
return calls
.map((call: ToolCallEvent | null) => call?.action_name ?? call?.tool_name)
.filter((name): name is string => Boolean(name));
};