Add status updates when generating chat completions, add system theme support, add custom openai model embedding support, and fix various bugs.
This commit is contained in:
parent
cc821315f8
commit
1077c1b703
13 changed files with 347 additions and 66 deletions
|
|
@ -16,6 +16,7 @@ const Chat = ({
|
|||
setFileIds,
|
||||
files,
|
||||
setFiles,
|
||||
statusText,
|
||||
}: {
|
||||
messages: Message[];
|
||||
sendMessage: (message: string) => void;
|
||||
|
|
@ -26,6 +27,7 @@ const Chat = ({
|
|||
setFileIds: (fileIds: string[]) => void;
|
||||
files: File[];
|
||||
setFiles: (files: File[]) => void;
|
||||
statusText?: string;
|
||||
}) => {
|
||||
const [dividerWidth, setDividerWidth] = useState(0);
|
||||
const dividerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
|
@ -78,6 +80,7 @@ const Chat = ({
|
|||
isLast={isLast}
|
||||
rewrite={rewrite}
|
||||
sendMessage={sendMessage}
|
||||
statusText={statusText}
|
||||
/>
|
||||
{!isLast && msg.role === 'assistant' && (
|
||||
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
|
||||
|
|
@ -85,7 +88,9 @@ const Chat = ({
|
|||
</Fragment>
|
||||
);
|
||||
})}
|
||||
{loading && !messageAppeared && <MessageBoxLoading />}
|
||||
{loading && !messageAppeared && (
|
||||
<MessageBoxLoading statusText={statusText} />
|
||||
)}
|
||||
<div ref={messageEnd} className="h-0" />
|
||||
{dividerWidth > 0 && (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -313,6 +313,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||
const [isMessagesLoaded, setIsMessagesLoaded] = useState(false);
|
||||
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
const [statusText, setStatusText] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
|
@ -367,6 +368,11 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||
|
||||
setLoading(true);
|
||||
setMessageAppeared(false);
|
||||
setStatusText(
|
||||
focusMode === 'writingAssistant'
|
||||
? 'Waiting for chat completion...'
|
||||
: 'Searching web...'
|
||||
);
|
||||
|
||||
let sources: Document[] | undefined = undefined;
|
||||
let recievedMessage = '';
|
||||
|
|
@ -386,13 +392,19 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||
]);
|
||||
|
||||
const messageHandler = async (data: any) => {
|
||||
if (data.type === 'status') {
|
||||
if (typeof data.data === 'string') setStatusText(data.data);
|
||||
return;
|
||||
}
|
||||
if (data.type === 'error') {
|
||||
toast.error(data.data);
|
||||
setStatusText('Chat completion failed.');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === 'sources') {
|
||||
setStatusText('Generating answer...');
|
||||
sources = data.data;
|
||||
if (!added) {
|
||||
setMessages((prevMessages) => [
|
||||
|
|
@ -412,6 +424,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||
}
|
||||
|
||||
if (data.type === 'message') {
|
||||
setStatusText('Generating answer...');
|
||||
if (!added) {
|
||||
setMessages((prevMessages) => [
|
||||
...prevMessages,
|
||||
|
|
@ -442,6 +455,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||
}
|
||||
|
||||
if (data.type === 'messageEnd') {
|
||||
setStatusText(undefined);
|
||||
setChatHistory((prevHistory) => [
|
||||
...prevHistory,
|
||||
['human', message],
|
||||
|
|
@ -519,31 +533,61 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||
}),
|
||||
});
|
||||
|
||||
if (!res.body) throw new Error('No response body');
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
toast.error(json.message || `Request failed: ${res.status} ${res.statusText}`);
|
||||
} catch {
|
||||
toast.error(`Request failed: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
setStatusText('Chat completion failed.');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!res.body) {
|
||||
toast.error('No response body');
|
||||
setStatusText('Chat completion failed.');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = res.body?.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
|
||||
let partialChunk = '';
|
||||
try {
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
partialChunk += decoder.decode(value, { stream: true });
|
||||
|
||||
partialChunk += decoder.decode(value, { stream: true });
|
||||
|
||||
try {
|
||||
const messages = partialChunk.split('\n');
|
||||
for (const msg of messages) {
|
||||
if (!msg.trim()) continue;
|
||||
const json = JSON.parse(msg);
|
||||
messageHandler(json);
|
||||
try {
|
||||
const messages = partialChunk.split('\n');
|
||||
for (const msg of messages) {
|
||||
if (!msg.trim()) continue;
|
||||
const json = JSON.parse(msg);
|
||||
messageHandler(json);
|
||||
}
|
||||
partialChunk = '';
|
||||
} catch (error) {
|
||||
console.warn('Incomplete JSON, waiting for next chunk...');
|
||||
}
|
||||
partialChunk = '';
|
||||
} catch (error) {
|
||||
console.warn('Incomplete JSON, waiting for next chunk...');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Streaming error', e);
|
||||
toast.error('Chat streaming failed.');
|
||||
setStatusText('Chat completion failed.');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: if the stream ended without 'messageEnd' or explicit error,
|
||||
// ensure the UI doesn't stay in a loading state indefinitely.
|
||||
setStatusText(undefined);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const rewrite = (messageId: string) => {
|
||||
|
|
@ -605,6 +649,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||
setFileIds={setFileIds}
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
statusText={statusText}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ const EmptyChat = ({
|
|||
<div className="flex flex-col items-center justify-center min-h-screen max-w-screen-sm mx-auto p-2 space-y-4">
|
||||
<div className="flex flex-col items-center justify-center w-full space-y-8">
|
||||
<h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8">
|
||||
Research begins here.
|
||||
Ask away...
|
||||
</h2>
|
||||
<EmptyChatMessageInput
|
||||
sendMessage={sendMessage}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
'use client';
|
||||
|
||||
import { Check, ClipboardList } from 'lucide-react';
|
||||
import { Message } from '../ChatWindow';
|
||||
import { useState } from 'react';
|
||||
|
|
@ -13,11 +15,37 @@ const Copy = ({
|
|||
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
const contentToCopy = `${initialMessage}${message.sources && message.sources.length > 0 && `\n\nCitations:\n${message.sources?.map((source: any, i: any) => `[${i + 1}] ${source.metadata.url}`).join(`\n`)}`}`;
|
||||
navigator.clipboard.writeText(contentToCopy);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1000);
|
||||
onClick={async () => {
|
||||
const citations =
|
||||
message.sources && message.sources.length > 0
|
||||
? `\n\nCitations:\n${message.sources
|
||||
?.map((source: any, i: number) => {
|
||||
const url = source?.metadata?.url ?? '';
|
||||
return `[${i + 1}] ${url}`;
|
||||
})
|
||||
.join('\n')}`
|
||||
: '';
|
||||
const contentToCopy = `${initialMessage}${citations}`;
|
||||
|
||||
try {
|
||||
if (navigator?.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(contentToCopy);
|
||||
} else {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = contentToCopy;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-9999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1200);
|
||||
} catch (err) {
|
||||
console.error('Copy failed', err);
|
||||
}
|
||||
}}
|
||||
className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ const MessageBox = ({
|
|||
isLast,
|
||||
rewrite,
|
||||
sendMessage,
|
||||
statusText,
|
||||
}: {
|
||||
message: Message;
|
||||
messageIndex: number;
|
||||
|
|
@ -51,6 +52,7 @@ const MessageBox = ({
|
|||
isLast: boolean;
|
||||
rewrite: (messageId: string) => void;
|
||||
sendMessage: (message: string) => void;
|
||||
statusText?: string;
|
||||
}) => {
|
||||
const [parsedMessage, setParsedMessage] = useState(message.content);
|
||||
const [speechMessage, setSpeechMessage] = useState(message.content);
|
||||
|
|
@ -182,7 +184,7 @@ const MessageBox = ({
|
|||
size={20}
|
||||
/>
|
||||
<h3 className="text-black dark:text-white font-medium text-xl">
|
||||
Answer
|
||||
{loading && isLast && statusText ? statusText : 'Answer'}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
const MessageBoxLoading = () => {
|
||||
const MessageBoxLoading = ({ statusText }: { statusText?: string }) => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-2 w-full lg:w-9/12 bg-light-primary dark:bg-dark-primary animate-pulse rounded-lg py-3">
|
||||
<div className="h-2 rounded-full w-full bg-light-secondary dark:bg-dark-secondary" />
|
||||
<div className="h-2 rounded-full w-9/12 bg-light-secondary dark:bg-dark-secondary" />
|
||||
<div className="h-2 rounded-full w-10/12 bg-light-secondary dark:bg-dark-secondary" />
|
||||
{statusText && (
|
||||
<div className="mt-3 text-xs text-black/70 dark:text-white/70 not-italic animate-none">
|
||||
{statusText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
'use client';
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
const ThemeProviderComponent = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const ThemeProviderComponent = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<ThemeProvider attribute="class" enableSystem={false} defaultTheme="dark">
|
||||
<ThemeProvider attribute="class" enableSystem={true} defaultTheme="system">
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,44 +1,19 @@
|
|||
'use client';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import Select from '../ui/Select';
|
||||
|
||||
type Theme = 'dark' | 'light' | 'system';
|
||||
|
||||
const ThemeSwitcher = ({ className }: { className?: string }) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const isTheme = useCallback((t: Theme) => t === theme, [theme]);
|
||||
|
||||
const handleThemeSwitch = (theme: Theme) => {
|
||||
setTheme(theme);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTheme('system')) {
|
||||
const preferDarkScheme = window.matchMedia(
|
||||
'(prefers-color-scheme: dark)',
|
||||
);
|
||||
|
||||
const detectThemeChange = (event: MediaQueryListEvent) => {
|
||||
const theme: Theme = event.matches ? 'dark' : 'light';
|
||||
setTheme(theme);
|
||||
};
|
||||
|
||||
preferDarkScheme.addEventListener('change', detectThemeChange);
|
||||
|
||||
return () => {
|
||||
preferDarkScheme.removeEventListener('change', detectThemeChange);
|
||||
};
|
||||
}
|
||||
}, [isTheme, setTheme, theme]);
|
||||
|
||||
// Avoid Hydration Mismatch
|
||||
if (!mounted) {
|
||||
return null;
|
||||
|
|
@ -48,8 +23,9 @@ const ThemeSwitcher = ({ className }: { className?: string }) => {
|
|||
<Select
|
||||
className={className}
|
||||
value={theme}
|
||||
onChange={(e) => handleThemeSwitch(e.target.value as Theme)}
|
||||
onChange={(e: ChangeEvent<HTMLSelectElement>) => setTheme(e.target.value as Theme)}
|
||||
options={[
|
||||
{ value: 'system', label: 'System' },
|
||||
{ value: 'light', label: 'Light' },
|
||||
{ value: 'dark', label: 'Dark' },
|
||||
]}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue