Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: robust metadata yielding #52

Merged
merged 4 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@magicul/react-chat-stream",
"description": "A React hook that lets you easily integrate your custom ChatGPT-like chat in React.",
"version": "0.5.1",
"version": "0.5.2",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"homepage": "https://github.com/XD2Sketch/react-chat-stream#readme",
Expand Down
55 changes: 45 additions & 10 deletions src/hooks/useChatStream.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChangeEvent, FormEvent, useState } from 'react';
import { decodeStreamToJson, getStream } from '../utils/streams';
import { UseChatStreamChatMessage, UseChatStreamInput } from '../types';
import { extractJsonFromEnd } from '../utils/json';
import { extractJsons } from '../utils/json';

const BOT_ERROR_MESSAGE = 'Something went wrong fetching AI response.';

Expand Down Expand Up @@ -50,13 +50,6 @@ const useChatStream = (input: UseChatStreamInput) => {
continue;
}

if (input.options.useMetadata) {
const metadata = extractJsonFromEnd(chunk);
if (metadata) {
return { ...initialMessage, content: response, metadata: metadata };
}
}

// Stream characters one by one based on the characters per second that is set.
for (const char of chunk) {
appendMessageToChat(char);
Expand All @@ -71,6 +64,43 @@ const useChatStream = (input: UseChatStreamInput) => {
return { ...initialMessage, content: response };
};

const fetchAndUpdateAIResponseWithMetadata = async (message: string) => {
const charactersPerSecond = input.options.fakeCharactersPerSecond;
const stream = await getStream(message, input.options, input.method);
const initialMessage = addMessage({ content: '', role: 'bot' });
let response = '';
let metadata = null;

for await (const chunk of decodeStreamToJson(stream)) {
const jsonObjects = extractJsons(chunk);

for (const parsedChunk of jsonObjects) {
if (parsedChunk.type === 'content') {
gcalcedo marked this conversation as resolved.
Show resolved Hide resolved
const content = parsedChunk.data;

if (!charactersPerSecond) {
appendMessageToChat(content);
response += content;
continue;
}

for (const char of content) {
appendMessageToChat(char);
response += char;

if (charactersPerSecond > 0) {
await new Promise(resolve => setTimeout(resolve, 1000 / charactersPerSecond));
}
}
} else if (parsedChunk.type === 'metadata') {
metadata = parsedChunk.data;
}
}
}

return { ...initialMessage, content: response, metadata };
};

const submitMessage = async (message: string) => resetInputAndGetResponse(message);

const resetInputAndGetResponse = async (message?: string) => {
Expand All @@ -80,8 +110,13 @@ const useChatStream = (input: UseChatStreamInput) => {
setFormInput('');

try {
const addedMessage = await fetchAndUpdateAIResponse(message ?? formInput);
await input.handlers.onMessageAdded?.(addedMessage);
if (input.options.useMetadata) {
const addedMessage = await fetchAndUpdateAIResponseWithMetadata(message ?? formInput);
gcalcedo marked this conversation as resolved.
Show resolved Hide resolved
await input.handlers.onMessageAdded?.(addedMessage);
} else {
const addedMessage = await fetchAndUpdateAIResponse(message ?? formInput);
await input.handlers.onMessageAdded?.(addedMessage);
}
} catch {
const addedMessage = addMessage({ content: BOT_ERROR_MESSAGE, role: 'bot' });
await input.handlers.onMessageAdded?.(addedMessage);
Expand Down
38 changes: 23 additions & 15 deletions src/utils/json.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
export const extractJsonFromEnd = (chunk: string) => {
const chunkTrimmed = chunk.trim();
export const extractJsons = (chunk: string) => {
const jsonObjects = [];
const braceStack = [];
let currentJsonStart = null;

const jsonObjectRegex = /({[^]*})\s*$/;
const match = chunkTrimmed.match(jsonObjectRegex);
for (let i = 0; i < chunk.length; i++) {
const char = chunk[i];

if (!match) {
return null;
}

const jsonStr = match[1];
try {
const parsedData = JSON.parse(jsonStr);
if (typeof parsedData === 'object' && parsedData !== null && !Array.isArray(parsedData)) {
return parsedData;
if (char === '{') {
if (braceStack.length === 0) {
currentJsonStart = i;
}
braceStack.push('{');
} else if (char === '}') {
braceStack.pop();
if (braceStack.length === 0 && currentJsonStart !== null) {
const potentialJson = chunk.substring(currentJsonStart, i + 1);
try {
const parsedJson = JSON.parse(potentialJson);
jsonObjects.push(parsedJson);
} catch {}
currentJsonStart = null;
}
}
} catch {}
}

return null;
return jsonObjects;
};
Loading