* Refactor AI course * Add AI course generation functionality * Add basic error handling * AI Document content * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * feat: regenerate guide * wip * wip * wip * wip * wip * fix: form ui * feat: update guide ui * refactor: update the course card * fix: term and redirects * Guide page UI improvements * Loading chip refactor * AI guide changes * Improve UI for ai guide content * Add AI guide * AI Guide chat * fix: stop streaming * fix: chat responsiveness * UI improvements for ai library * Guide listing UI update * User guides listing * Library guides listing UI * Library guides listing UI * Featured courses listing UI update * Staff picks UI changes * Community page UI design * Explore courses listing functionality * Improve UI for explore page * Implement guides functionality --------- Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
95 lines
2.4 KiB
TypeScript
95 lines
2.4 KiB
TypeScript
export const CHAT_RESPONSE_PREFIX = {
|
|
message: '0',
|
|
details: 'd',
|
|
} as const;
|
|
|
|
const NEWLINE = '\n'.charCodeAt(0);
|
|
|
|
function concatChunks(chunks: Uint8Array[], totalLength: number) {
|
|
const concatenatedChunks = new Uint8Array(totalLength);
|
|
|
|
let offset = 0;
|
|
for (const chunk of chunks) {
|
|
concatenatedChunks.set(chunk, offset);
|
|
offset += chunk.length;
|
|
}
|
|
chunks.length = 0;
|
|
|
|
return concatenatedChunks;
|
|
}
|
|
|
|
export async function readChatStream(
|
|
stream: ReadableStream<Uint8Array>,
|
|
{
|
|
onMessage,
|
|
onMessageEnd,
|
|
onDetails,
|
|
}: {
|
|
onMessage?: (message: string) => Promise<void>;
|
|
onMessageEnd?: (message: string) => Promise<void>;
|
|
onDetails?: (details: any) => Promise<void> | void;
|
|
},
|
|
) {
|
|
const reader = stream.getReader();
|
|
const decoder = new TextDecoder('utf-8');
|
|
const chunks: Uint8Array[] = [];
|
|
|
|
let totalLength = 0;
|
|
let result = '';
|
|
|
|
while (true) {
|
|
const { value } = await reader.read();
|
|
if (value) {
|
|
chunks.push(value);
|
|
totalLength += value.length;
|
|
if (value[value.length - 1] !== NEWLINE) {
|
|
// if the last character is not a new line, we need to wait for the next chunk
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (chunks.length === 0) {
|
|
// end of stream
|
|
break;
|
|
}
|
|
|
|
const concatenatedChunks = concatChunks(chunks, totalLength);
|
|
totalLength = 0;
|
|
|
|
const streamParts = decoder
|
|
.decode(concatenatedChunks, { stream: true })
|
|
.split('\n')
|
|
.filter((line) => line !== '')
|
|
.map((line) => {
|
|
const separatorIndex = line.indexOf(':');
|
|
if (separatorIndex === -1) {
|
|
throw new Error('Invalid line: ' + line + '. No separator found.');
|
|
}
|
|
|
|
const prefix = line.slice(0, separatorIndex);
|
|
const content = line.slice(separatorIndex + 1);
|
|
|
|
switch (prefix) {
|
|
case CHAT_RESPONSE_PREFIX.message:
|
|
return { type: 'message', content: JSON.parse(content) };
|
|
case CHAT_RESPONSE_PREFIX.details:
|
|
return { type: 'details', content: JSON.parse(content) };
|
|
default:
|
|
throw new Error('Invalid prefix: ' + prefix);
|
|
}
|
|
});
|
|
|
|
for (const part of streamParts) {
|
|
if (part.type === 'message') {
|
|
result += part.content;
|
|
await onMessage?.(result);
|
|
} else if (part.type === 'details') {
|
|
await onDetails?.(part.content);
|
|
}
|
|
}
|
|
}
|
|
|
|
await onMessageEnd?.(result);
|
|
reader.releaseLock();
|
|
}
|