mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2026-01-03 01:56:56 +00:00
828 lines
22 KiB
TypeScript
828 lines
22 KiB
TypeScript
'use client';
|
|
|
|
import { Message } from '@/components/ChatWindow';
|
|
import { Block } from '@/lib/types';
|
|
import {
|
|
createContext,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
import crypto from 'crypto';
|
|
import { useParams, useSearchParams } from 'next/navigation';
|
|
import { toast } from 'sonner';
|
|
import { getSuggestions } from '../actions';
|
|
import { MinimalProvider } from '../models/types';
|
|
import { getAutoMediaSearch } from '../config/clientRegistry';
|
|
import { applyPatch } from 'rfc6902';
|
|
import { Widget } from '@/components/ChatWindow';
|
|
|
|
export type Section = {
|
|
message: Message;
|
|
widgets: Widget[];
|
|
parsedTextBlocks: string[];
|
|
speechMessage: string;
|
|
thinkingEnded: boolean;
|
|
suggestions?: string[];
|
|
};
|
|
|
|
type ChatContext = {
|
|
messages: Message[];
|
|
sections: Section[];
|
|
chatHistory: [string, string][];
|
|
files: File[];
|
|
fileIds: string[];
|
|
sources: string[];
|
|
chatId: string | undefined;
|
|
optimizationMode: string;
|
|
isMessagesLoaded: boolean;
|
|
loading: boolean;
|
|
notFound: boolean;
|
|
messageAppeared: boolean;
|
|
isReady: boolean;
|
|
hasError: boolean;
|
|
chatModelProvider: ChatModelProvider;
|
|
embeddingModelProvider: EmbeddingModelProvider;
|
|
researchEnded: boolean;
|
|
setResearchEnded: (ended: boolean) => void;
|
|
setOptimizationMode: (mode: string) => void;
|
|
setSources: (sources: string[]) => void;
|
|
setFiles: (files: File[]) => void;
|
|
setFileIds: (fileIds: string[]) => void;
|
|
sendMessage: (
|
|
message: string,
|
|
messageId?: string,
|
|
rewrite?: boolean,
|
|
) => Promise<void>;
|
|
rewrite: (messageId: string) => void;
|
|
setChatModelProvider: (provider: ChatModelProvider) => void;
|
|
setEmbeddingModelProvider: (provider: EmbeddingModelProvider) => void;
|
|
};
|
|
|
|
export interface File {
|
|
fileName: string;
|
|
fileExtension: string;
|
|
fileId: string;
|
|
}
|
|
|
|
interface ChatModelProvider {
|
|
key: string;
|
|
providerId: string;
|
|
}
|
|
|
|
interface EmbeddingModelProvider {
|
|
key: string;
|
|
providerId: string;
|
|
}
|
|
|
|
const checkConfig = async (
|
|
setChatModelProvider: (provider: ChatModelProvider) => void,
|
|
setEmbeddingModelProvider: (provider: EmbeddingModelProvider) => void,
|
|
setIsConfigReady: (ready: boolean) => void,
|
|
setHasError: (hasError: boolean) => void,
|
|
) => {
|
|
try {
|
|
let chatModelKey = localStorage.getItem('chatModelKey');
|
|
let chatModelProviderId = localStorage.getItem('chatModelProviderId');
|
|
let embeddingModelKey = localStorage.getItem('embeddingModelKey');
|
|
let embeddingModelProviderId = localStorage.getItem(
|
|
'embeddingModelProviderId',
|
|
);
|
|
|
|
const res = await fetch(`/api/providers`, {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
if (!res.ok) {
|
|
throw new Error(
|
|
`Provider fetching failed with status code ${res.status}`,
|
|
);
|
|
}
|
|
|
|
const data = await res.json();
|
|
const providers: MinimalProvider[] = data.providers;
|
|
|
|
if (providers.length === 0) {
|
|
throw new Error(
|
|
'No chat model providers found, please configure them in the settings page.',
|
|
);
|
|
}
|
|
|
|
const chatModelProvider =
|
|
providers.find((p) => p.id === chatModelProviderId) ??
|
|
providers.find((p) => p.chatModels.length > 0);
|
|
|
|
if (!chatModelProvider) {
|
|
throw new Error(
|
|
'No chat models found, pleae configure them in the settings page.',
|
|
);
|
|
}
|
|
|
|
chatModelProviderId = chatModelProvider.id;
|
|
|
|
const chatModel =
|
|
chatModelProvider.chatModels.find((m) => m.key === chatModelKey) ??
|
|
chatModelProvider.chatModels[0];
|
|
chatModelKey = chatModel.key;
|
|
|
|
const embeddingModelProvider =
|
|
providers.find((p) => p.id === embeddingModelProviderId) ??
|
|
providers.find((p) => p.embeddingModels.length > 0);
|
|
|
|
if (!embeddingModelProvider) {
|
|
throw new Error(
|
|
'No embedding models found, pleae configure them in the settings page.',
|
|
);
|
|
}
|
|
|
|
embeddingModelProviderId = embeddingModelProvider.id;
|
|
|
|
const embeddingModel =
|
|
embeddingModelProvider.embeddingModels.find(
|
|
(m) => m.key === embeddingModelKey,
|
|
) ?? embeddingModelProvider.embeddingModels[0];
|
|
embeddingModelKey = embeddingModel.key;
|
|
|
|
localStorage.setItem('chatModelKey', chatModelKey);
|
|
localStorage.setItem('chatModelProviderId', chatModelProviderId);
|
|
localStorage.setItem('embeddingModelKey', embeddingModelKey);
|
|
localStorage.setItem('embeddingModelProviderId', embeddingModelProviderId);
|
|
|
|
setChatModelProvider({
|
|
key: chatModelKey,
|
|
providerId: chatModelProviderId,
|
|
});
|
|
|
|
setEmbeddingModelProvider({
|
|
key: embeddingModelKey,
|
|
providerId: embeddingModelProviderId,
|
|
});
|
|
|
|
setIsConfigReady(true);
|
|
} catch (err: any) {
|
|
console.error('An error occurred while checking the configuration:', err);
|
|
toast.error(err.message);
|
|
setIsConfigReady(false);
|
|
setHasError(true);
|
|
}
|
|
};
|
|
|
|
const loadMessages = async (
|
|
chatId: string,
|
|
setMessages: (messages: Message[]) => void,
|
|
setIsMessagesLoaded: (loaded: boolean) => void,
|
|
setChatHistory: (history: [string, string][]) => void,
|
|
setSources: (sources: string[]) => void,
|
|
setNotFound: (notFound: boolean) => void,
|
|
setFiles: (files: File[]) => void,
|
|
setFileIds: (fileIds: string[]) => void,
|
|
) => {
|
|
const res = await fetch(`/api/chats/${chatId}`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
if (res.status === 404) {
|
|
setNotFound(true);
|
|
setIsMessagesLoaded(true);
|
|
return;
|
|
}
|
|
|
|
const data = await res.json();
|
|
|
|
const messages = data.messages as Message[];
|
|
|
|
setMessages(messages);
|
|
|
|
const history: [string, string][] = [];
|
|
messages.forEach((msg) => {
|
|
history.push(['human', msg.query]);
|
|
|
|
const textBlocks = msg.responseBlocks
|
|
.filter(
|
|
(block): block is Block & { type: 'text' } => block.type === 'text',
|
|
)
|
|
.map((block) => block.data)
|
|
.join('\n');
|
|
|
|
if (textBlocks) {
|
|
history.push(['assistant', textBlocks]);
|
|
}
|
|
});
|
|
|
|
console.debug(new Date(), 'app:messages_loaded');
|
|
|
|
if (messages.length > 0) {
|
|
document.title = messages[0].query;
|
|
}
|
|
|
|
const files = data.chat.files.map((file: any) => {
|
|
return {
|
|
fileName: file.name,
|
|
fileExtension: file.name.split('.').pop(),
|
|
fileId: file.fileId,
|
|
};
|
|
});
|
|
|
|
setFiles(files);
|
|
setFileIds(files.map((file: File) => file.fileId));
|
|
|
|
setChatHistory(history);
|
|
setSources(data.chat.sources);
|
|
setIsMessagesLoaded(true);
|
|
};
|
|
|
|
export const chatContext = createContext<ChatContext>({
|
|
chatHistory: [],
|
|
chatId: '',
|
|
fileIds: [],
|
|
files: [],
|
|
sources: [],
|
|
hasError: false,
|
|
isMessagesLoaded: false,
|
|
isReady: false,
|
|
loading: false,
|
|
messageAppeared: false,
|
|
messages: [],
|
|
sections: [],
|
|
notFound: false,
|
|
optimizationMode: '',
|
|
chatModelProvider: { key: '', providerId: '' },
|
|
embeddingModelProvider: { key: '', providerId: '' },
|
|
researchEnded: false,
|
|
rewrite: () => {},
|
|
sendMessage: async () => {},
|
|
setFileIds: () => {},
|
|
setFiles: () => {},
|
|
setSources: () => {},
|
|
setOptimizationMode: () => {},
|
|
setChatModelProvider: () => {},
|
|
setEmbeddingModelProvider: () => {},
|
|
setResearchEnded: () => {},
|
|
});
|
|
|
|
export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
|
|
const params: { chatId: string } = useParams();
|
|
|
|
const searchParams = useSearchParams();
|
|
const initialMessage = searchParams.get('q');
|
|
|
|
const [chatId, setChatId] = useState<string | undefined>(params.chatId);
|
|
const [newChatCreated, setNewChatCreated] = useState(false);
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
const [messageAppeared, setMessageAppeared] = useState(false);
|
|
|
|
const [researchEnded, setResearchEnded] = useState(false);
|
|
|
|
const [chatHistory, setChatHistory] = useState<[string, string][]>([]);
|
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
|
|
const [files, setFiles] = useState<File[]>([]);
|
|
const [fileIds, setFileIds] = useState<string[]>([]);
|
|
|
|
const [sources, setSources] = useState<string[]>(['web']);
|
|
const [optimizationMode, setOptimizationMode] = useState('speed');
|
|
|
|
const [isMessagesLoaded, setIsMessagesLoaded] = useState(false);
|
|
|
|
const [notFound, setNotFound] = useState(false);
|
|
|
|
const [chatModelProvider, setChatModelProvider] = useState<ChatModelProvider>(
|
|
{
|
|
key: '',
|
|
providerId: '',
|
|
},
|
|
);
|
|
|
|
const [embeddingModelProvider, setEmbeddingModelProvider] =
|
|
useState<EmbeddingModelProvider>({
|
|
key: '',
|
|
providerId: '',
|
|
});
|
|
|
|
const [isConfigReady, setIsConfigReady] = useState(false);
|
|
const [hasError, setHasError] = useState(false);
|
|
const [isReady, setIsReady] = useState(false);
|
|
|
|
const messagesRef = useRef<Message[]>([]);
|
|
|
|
const sections = useMemo<Section[]>(() => {
|
|
return messages.map((msg) => {
|
|
const textBlocks: string[] = [];
|
|
let speechMessage = '';
|
|
let thinkingEnded = false;
|
|
let suggestions: string[] = [];
|
|
|
|
const sourceBlocks = msg.responseBlocks.filter(
|
|
(block): block is Block & { type: 'source' } => block.type === 'source',
|
|
);
|
|
const sources = sourceBlocks.flatMap((block) => block.data);
|
|
|
|
const widgetBlocks = msg.responseBlocks
|
|
.filter((b) => b.type === 'widget')
|
|
.map((b) => b.data) as Widget[];
|
|
|
|
msg.responseBlocks.forEach((block) => {
|
|
if (block.type === 'text') {
|
|
let processedText = block.data;
|
|
const citationRegex = /\[([^\]]+)\]/g;
|
|
const regex = /\[(\d+)\]/g;
|
|
|
|
if (processedText.includes('<think>')) {
|
|
const openThinkTag = processedText.match(/<think>/g)?.length || 0;
|
|
const closeThinkTag =
|
|
processedText.match(/<\/think>/g)?.length || 0;
|
|
|
|
if (openThinkTag && !closeThinkTag) {
|
|
processedText += '</think> <a> </a>';
|
|
}
|
|
}
|
|
|
|
if (block.data.includes('</think>')) {
|
|
thinkingEnded = true;
|
|
}
|
|
|
|
if (sources.length > 0) {
|
|
processedText = processedText.replace(
|
|
citationRegex,
|
|
(_, capturedContent: string) => {
|
|
const numbers = capturedContent
|
|
.split(',')
|
|
.map((numStr) => numStr.trim());
|
|
|
|
const linksHtml = numbers
|
|
.map((numStr) => {
|
|
const number = parseInt(numStr);
|
|
|
|
if (isNaN(number) || number <= 0) {
|
|
return `[${numStr}]`;
|
|
}
|
|
|
|
const source = sources[number - 1];
|
|
const url = source?.metadata?.url;
|
|
|
|
if (url) {
|
|
return `<citation href="${url}">${numStr}</citation>`;
|
|
} else {
|
|
return ``;
|
|
}
|
|
})
|
|
.join('');
|
|
|
|
return linksHtml;
|
|
},
|
|
);
|
|
speechMessage += block.data.replace(regex, '');
|
|
} else {
|
|
processedText = processedText.replace(regex, '');
|
|
speechMessage += block.data.replace(regex, '');
|
|
}
|
|
|
|
textBlocks.push(processedText);
|
|
} else if (block.type === 'suggestion') {
|
|
suggestions = block.data;
|
|
}
|
|
});
|
|
|
|
return {
|
|
message: msg,
|
|
parsedTextBlocks: textBlocks,
|
|
speechMessage,
|
|
thinkingEnded,
|
|
suggestions,
|
|
widgets: widgetBlocks,
|
|
};
|
|
});
|
|
}, [messages]);
|
|
|
|
const checkReconnect = async () => {
|
|
setIsReady(true);
|
|
console.debug(new Date(), 'app:ready');
|
|
|
|
if (messages.length > 0) {
|
|
const lastMsg = messages[messages.length - 1];
|
|
|
|
if (lastMsg.status === 'answering') {
|
|
setLoading(true);
|
|
setResearchEnded(false);
|
|
setMessageAppeared(false);
|
|
|
|
const res = await fetch(`/api/reconnect/${lastMsg.backendId}`, {
|
|
method: 'POST',
|
|
});
|
|
|
|
if (!res.body) throw new Error('No response body');
|
|
|
|
const reader = res.body?.getReader();
|
|
const decoder = new TextDecoder('utf-8');
|
|
|
|
let partialChunk = '';
|
|
|
|
const messageHandler = getMessageHandler(lastMsg);
|
|
|
|
while (true) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
|
|
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);
|
|
}
|
|
partialChunk = '';
|
|
} catch (error) {
|
|
console.warn('Incomplete JSON, waiting for next chunk...');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
checkConfig(
|
|
setChatModelProvider,
|
|
setEmbeddingModelProvider,
|
|
setIsConfigReady,
|
|
setHasError,
|
|
);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (params.chatId && params.chatId !== chatId) {
|
|
setChatId(params.chatId);
|
|
setMessages([]);
|
|
setChatHistory([]);
|
|
setFiles([]);
|
|
setFileIds([]);
|
|
setIsMessagesLoaded(false);
|
|
setNotFound(false);
|
|
setNewChatCreated(false);
|
|
}
|
|
}, [params.chatId, chatId]);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
chatId &&
|
|
!newChatCreated &&
|
|
!isMessagesLoaded &&
|
|
messages.length === 0
|
|
) {
|
|
loadMessages(
|
|
chatId,
|
|
setMessages,
|
|
setIsMessagesLoaded,
|
|
setChatHistory,
|
|
setSources,
|
|
setNotFound,
|
|
setFiles,
|
|
setFileIds,
|
|
);
|
|
} else if (!chatId) {
|
|
setNewChatCreated(true);
|
|
setIsMessagesLoaded(true);
|
|
setChatId(crypto.randomBytes(20).toString('hex'));
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [chatId, isMessagesLoaded, newChatCreated, messages.length]);
|
|
|
|
useEffect(() => {
|
|
messagesRef.current = messages;
|
|
}, [messages]);
|
|
|
|
useEffect(() => {
|
|
if (isMessagesLoaded && isConfigReady && newChatCreated) {
|
|
setIsReady(true);
|
|
console.debug(new Date(), 'app:ready');
|
|
} else if (isMessagesLoaded && isConfigReady && !newChatCreated) {
|
|
checkReconnect();
|
|
} else {
|
|
setIsReady(false);
|
|
}
|
|
}, [isMessagesLoaded, isConfigReady, newChatCreated]);
|
|
|
|
const rewrite = (messageId: string) => {
|
|
const index = messages.findIndex((msg) => msg.messageId === messageId);
|
|
|
|
if (index === -1) return;
|
|
|
|
setMessages((prev) => prev.slice(0, index));
|
|
|
|
setChatHistory((prev) => {
|
|
return prev.slice(0, index * 2);
|
|
});
|
|
|
|
const messageToRewrite = messages[index];
|
|
sendMessage(messageToRewrite.query, messageToRewrite.messageId, true);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (isReady && initialMessage && isConfigReady) {
|
|
if (!isConfigReady) {
|
|
toast.error('Cannot send message before the configuration is ready');
|
|
return;
|
|
}
|
|
sendMessage(initialMessage);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [isConfigReady, isReady, initialMessage]);
|
|
|
|
const getMessageHandler = (message: Message) => {
|
|
const messageId = message.messageId;
|
|
|
|
return async (data: any) => {
|
|
if (data.type === 'error') {
|
|
toast.error(data.data);
|
|
setLoading(false);
|
|
setMessages((prev) =>
|
|
prev.map((msg) =>
|
|
msg.messageId === messageId
|
|
? { ...msg, status: 'error' as const }
|
|
: msg,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (data.type === 'researchComplete') {
|
|
setResearchEnded(true);
|
|
if (
|
|
message.responseBlocks.find(
|
|
(b) => b.type === 'source' && b.data.length > 0,
|
|
)
|
|
) {
|
|
setMessageAppeared(true);
|
|
}
|
|
}
|
|
|
|
if (data.type === 'block') {
|
|
setMessages((prev) =>
|
|
prev.map((msg) => {
|
|
if (msg.messageId === messageId) {
|
|
const exists = msg.responseBlocks.findIndex(
|
|
(b) => b.id === data.block.id,
|
|
);
|
|
|
|
if (exists !== -1) {
|
|
const existingBlocks = [...msg.responseBlocks];
|
|
existingBlocks[exists] = data.block;
|
|
|
|
return {
|
|
...msg,
|
|
responseBlocks: existingBlocks,
|
|
};
|
|
}
|
|
|
|
return {
|
|
...msg,
|
|
responseBlocks: [...msg.responseBlocks, data.block],
|
|
};
|
|
}
|
|
return msg;
|
|
}),
|
|
);
|
|
|
|
if (
|
|
(data.block.type === 'source' && data.block.data.length > 0) ||
|
|
data.block.type === 'text'
|
|
) {
|
|
setMessageAppeared(true);
|
|
}
|
|
}
|
|
|
|
if (data.type === 'updateBlock') {
|
|
setMessages((prev) =>
|
|
prev.map((msg) => {
|
|
if (msg.messageId === messageId) {
|
|
const updatedBlocks = msg.responseBlocks.map((block) => {
|
|
if (block.id === data.blockId) {
|
|
const updatedBlock = { ...block };
|
|
applyPatch(updatedBlock, data.patch);
|
|
return updatedBlock;
|
|
}
|
|
return block;
|
|
});
|
|
return { ...msg, responseBlocks: updatedBlocks };
|
|
}
|
|
return msg;
|
|
}),
|
|
);
|
|
}
|
|
|
|
if (data.type === 'messageEnd') {
|
|
const currentMsg = messagesRef.current.find(
|
|
(msg) => msg.messageId === messageId,
|
|
);
|
|
|
|
const newHistory: [string, string][] = [
|
|
...chatHistory,
|
|
['human', message.query],
|
|
[
|
|
'assistant',
|
|
currentMsg?.responseBlocks.find((b) => b.type === 'text')?.data ||
|
|
'',
|
|
],
|
|
];
|
|
|
|
setChatHistory(newHistory);
|
|
|
|
setMessages((prev) =>
|
|
prev.map((msg) =>
|
|
msg.messageId === messageId
|
|
? { ...msg, status: 'completed' as const }
|
|
: msg,
|
|
),
|
|
);
|
|
|
|
setLoading(false);
|
|
|
|
const lastMsg = messagesRef.current[messagesRef.current.length - 1];
|
|
|
|
const autoMediaSearch = getAutoMediaSearch();
|
|
|
|
if (autoMediaSearch) {
|
|
document
|
|
.getElementById(`search-images-${lastMsg.messageId}`)
|
|
?.click();
|
|
|
|
document
|
|
.getElementById(`search-videos-${lastMsg.messageId}`)
|
|
?.click();
|
|
}
|
|
|
|
// Check if there are sources and no suggestions
|
|
|
|
const hasSourceBlocks = currentMsg?.responseBlocks.some(
|
|
(block) => block.type === 'source' && block.data.length > 0,
|
|
);
|
|
const hasSuggestions = currentMsg?.responseBlocks.some(
|
|
(block) => block.type === 'suggestion',
|
|
);
|
|
|
|
if (hasSourceBlocks && !hasSuggestions) {
|
|
const suggestions = await getSuggestions(newHistory);
|
|
const suggestionBlock: Block = {
|
|
id: crypto.randomBytes(7).toString('hex'),
|
|
type: 'suggestion',
|
|
data: suggestions,
|
|
};
|
|
|
|
setMessages((prev) =>
|
|
prev.map((msg) => {
|
|
if (msg.messageId === messageId) {
|
|
return {
|
|
...msg,
|
|
responseBlocks: [...msg.responseBlocks, suggestionBlock],
|
|
};
|
|
}
|
|
return msg;
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
const sendMessage: ChatContext['sendMessage'] = async (
|
|
message,
|
|
messageId,
|
|
rewrite = false,
|
|
) => {
|
|
if (loading || !message) return;
|
|
setLoading(true);
|
|
setResearchEnded(false);
|
|
setMessageAppeared(false);
|
|
|
|
if (messages.length <= 1) {
|
|
window.history.replaceState(null, '', `/c/${chatId}`);
|
|
}
|
|
|
|
messageId = messageId ?? crypto.randomBytes(7).toString('hex');
|
|
const backendId = crypto.randomBytes(20).toString('hex');
|
|
|
|
const newMessage: Message = {
|
|
messageId,
|
|
chatId: chatId!,
|
|
backendId,
|
|
query: message,
|
|
responseBlocks: [],
|
|
status: 'answering',
|
|
createdAt: new Date(),
|
|
};
|
|
|
|
setMessages((prevMessages) => [...prevMessages, newMessage]);
|
|
|
|
const messageIndex = messages.findIndex((m) => m.messageId === messageId);
|
|
|
|
const res = await fetch('/api/chat', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
content: message,
|
|
message: {
|
|
messageId: messageId,
|
|
chatId: chatId!,
|
|
content: message,
|
|
},
|
|
chatId: chatId!,
|
|
files: fileIds,
|
|
sources: sources,
|
|
optimizationMode: optimizationMode,
|
|
history: rewrite
|
|
? chatHistory.slice(0, messageIndex === -1 ? undefined : messageIndex)
|
|
: chatHistory,
|
|
chatModel: {
|
|
key: chatModelProvider.key,
|
|
providerId: chatModelProvider.providerId,
|
|
},
|
|
embeddingModel: {
|
|
key: embeddingModelProvider.key,
|
|
providerId: embeddingModelProvider.providerId,
|
|
},
|
|
systemInstructions: localStorage.getItem('systemInstructions'),
|
|
}),
|
|
});
|
|
|
|
if (!res.body) throw new Error('No response body');
|
|
|
|
const reader = res.body?.getReader();
|
|
const decoder = new TextDecoder('utf-8');
|
|
|
|
let partialChunk = '';
|
|
|
|
const messageHandler = getMessageHandler(newMessage);
|
|
|
|
while (true) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
|
|
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);
|
|
}
|
|
partialChunk = '';
|
|
} catch (error) {
|
|
console.warn('Incomplete JSON, waiting for next chunk...');
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<chatContext.Provider
|
|
value={{
|
|
messages,
|
|
sections,
|
|
chatHistory,
|
|
files,
|
|
fileIds,
|
|
sources,
|
|
chatId,
|
|
hasError,
|
|
isMessagesLoaded,
|
|
isReady,
|
|
loading,
|
|
messageAppeared,
|
|
notFound,
|
|
optimizationMode,
|
|
setFileIds,
|
|
setFiles,
|
|
setSources,
|
|
setOptimizationMode,
|
|
rewrite,
|
|
sendMessage,
|
|
setChatModelProvider,
|
|
chatModelProvider,
|
|
embeddingModelProvider,
|
|
setEmbeddingModelProvider,
|
|
researchEnded,
|
|
setResearchEnded,
|
|
}}
|
|
>
|
|
{children}
|
|
</chatContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useChat = () => {
|
|
const ctx = useContext(chatContext);
|
|
return ctx;
|
|
};
|