feat(app): handle new architecture

This commit is contained in:
ItzCrazyKns
2025-11-23 19:58:46 +05:30
parent e0ba476ca4
commit 956a768a86
14 changed files with 945 additions and 508 deletions

View File

@@ -1,6 +1,14 @@
import { Message } from '@/components/ChatWindow';
export const getSuggestions = async (chatHistory: Message[]) => {
export const getSuggestions = async (chatHistory: [string, string][]) => {
const chatTurns = chatHistory.map(([role, content]) => {
if (role === 'human') {
return { role: 'user', content };
} else {
return { role: 'assistant', content };
}
});
const chatModel = localStorage.getItem('chatModelKey');
const chatModelProvider = localStorage.getItem('chatModelProviderId');
@@ -10,7 +18,7 @@ export const getSuggestions = async (chatHistory: Message[]) => {
'Content-Type': 'application/json',
},
body: JSON.stringify({
chatHistory: chatHistory,
chatHistory: chatTurns,
chatModel: {
providerId: chatModelProvider,
key: chatModel,

View File

@@ -10,6 +10,7 @@ class Classifier {
const availableIntents = IntentRegistry.getAvailableIntents({
sources: input.enabledSources,
});
const availableWidgets = WidgetRegistry.getAll();
const classificationSchema = z.object({
@@ -21,12 +22,12 @@ class Classifier {
standaloneFollowUp: z
.string()
.describe(
'A self-contained, context-independent reformulation of the user\'s question. Must include all necessary context from chat history, replace pronouns with specific nouns, and be clear enough to answer without seeing the conversation. Keep the same complexity as the original question.',
"A self-contained, context-independent reformulation of the user's question. Must include all necessary context from chat history, replace pronouns with specific nouns, and be clear enough to answer without seeing the conversation. Keep the same complexity as the original question.",
),
intents: z
.array(z.enum(availableIntents.map((i) => i.name)))
.describe(
'The intent(s) that best describe how to fulfill the user\'s query. Can include multiple intents (e.g., [\'web_search\', \'widget_response\'] for \'weather in NYC and recent news\'). Always include at least one intent when applicable.',
"The intent(s) that best describe how to fulfill the user's query. Can include multiple intents (e.g., ['web_search', 'widget_response'] for 'weather in NYC and recent news'). Always include at least one intent when applicable.",
),
widgets: z
.array(z.union(availableWidgets.map((w) => w.schema)))

View File

@@ -1,13 +1,7 @@
'use client';
import {
AssistantMessage,
ChatTurn,
Message,
SourceMessage,
SuggestionMessage,
UserMessage,
} from '@/components/ChatWindow';
import { Message } from '@/components/ChatWindow';
import { Block } from '@/lib/types';
import {
createContext,
useContext,
@@ -22,20 +16,20 @@ 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 = {
userMessage: UserMessage;
assistantMessage: AssistantMessage | undefined;
parsedAssistantMessage: string | undefined;
speechMessage: string | undefined;
sourceMessage: SourceMessage | undefined;
message: Message;
widgets: Widget[];
parsedTextBlocks: string[];
speechMessage: string;
thinkingEnded: boolean;
suggestions?: string[];
};
type ChatContext = {
messages: Message[];
chatTurns: ChatTurn[];
sections: Section[];
chatHistory: [string, string][];
files: File[];
@@ -51,6 +45,8 @@ type ChatContext = {
hasError: boolean;
chatModelProvider: ChatModelProvider;
embeddingModelProvider: EmbeddingModelProvider;
researchEnded: boolean;
setResearchEnded: (ended: boolean) => void;
setOptimizationMode: (mode: string) => void;
setFocusMode: (mode: string) => void;
setFiles: (files: File[]) => void;
@@ -204,18 +200,26 @@ const loadMessages = async (
setMessages(messages);
const chatTurns = messages.filter(
(msg): msg is ChatTurn => msg.role === 'user' || msg.role === 'assistant',
);
const history: [string, string][] = [];
messages.forEach((msg) => {
history.push(['human', msg.query]);
const history = chatTurns.map((msg) => {
return [msg.role, msg.content];
}) as [string, string][];
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 (chatTurns.length > 0) {
document.title = chatTurns[0].content;
if (messages.length > 0) {
document.title = messages[0].query;
}
const files = data.chat.files.map((file: any) => {
@@ -246,12 +250,12 @@ export const chatContext = createContext<ChatContext>({
loading: false,
messageAppeared: false,
messages: [],
chatTurns: [],
sections: [],
notFound: false,
optimizationMode: '',
chatModelProvider: { key: '', providerId: '' },
embeddingModelProvider: { key: '', providerId: '' },
researchEnded: false,
rewrite: () => {},
sendMessage: async () => {},
setFileIds: () => {},
@@ -260,6 +264,7 @@ export const chatContext = createContext<ChatContext>({
setOptimizationMode: () => {},
setChatModelProvider: () => {},
setEmbeddingModelProvider: () => {},
setResearchEnded: () => {},
});
export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
@@ -273,6 +278,8 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
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[]>([]);
@@ -305,66 +312,44 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
const messagesRef = useRef<Message[]>([]);
const chatTurns = useMemo((): ChatTurn[] => {
return messages.filter(
(msg): msg is ChatTurn => msg.role === 'user' || msg.role === 'assistant',
);
}, [messages]);
const sections = useMemo<Section[]>(() => {
const sections: Section[] = [];
return messages.map((msg) => {
const textBlocks: string[] = [];
let speechMessage = '';
let thinkingEnded = false;
let suggestions: string[] = [];
messages.forEach((msg, i) => {
if (msg.role === 'user') {
const nextUserMessageIndex = messages.findIndex(
(m, j) => j > i && m.role === 'user',
);
const sourceBlocks = msg.responseBlocks.filter(
(block): block is Block & { type: 'source' } => block.type === 'source',
);
const sources = sourceBlocks.flatMap((block) => block.data);
const aiMessage = messages.find(
(m, j) =>
j > i &&
m.role === 'assistant' &&
(nextUserMessageIndex === -1 || j < nextUserMessageIndex),
) as AssistantMessage | undefined;
const widgetBlocks = msg.responseBlocks
.filter((b) => b.type === 'widget')
.map((b) => b.data) as Widget[];
const sourceMessage = messages.find(
(m, j) =>
j > i &&
m.role === 'source' &&
m.sources &&
(nextUserMessageIndex === -1 || j < nextUserMessageIndex),
) as SourceMessage | undefined;
let thinkingEnded = false;
let processedMessage = aiMessage?.content ?? '';
let speechMessage = aiMessage?.content ?? '';
let suggestions: string[] = [];
if (aiMessage) {
msg.responseBlocks.forEach((block) => {
if (block.type === 'text') {
let processedText = block.data;
const citationRegex = /\[([^\]]+)\]/g;
const regex = /\[(\d+)\]/g;
if (processedMessage.includes('<think>')) {
const openThinkTag =
processedMessage.match(/<think>/g)?.length || 0;
if (processedText.includes('<think>')) {
const openThinkTag = processedText.match(/<think>/g)?.length || 0;
const closeThinkTag =
processedMessage.match(/<\/think>/g)?.length || 0;
processedText.match(/<\/think>/g)?.length || 0;
if (openThinkTag && !closeThinkTag) {
processedMessage += '</think> <a> </a>';
processedText += '</think> <a> </a>';
}
}
if (aiMessage.content.includes('</think>')) {
if (block.data.includes('</think>')) {
thinkingEnded = true;
}
if (
sourceMessage &&
sourceMessage.sources &&
sourceMessage.sources.length > 0
) {
processedMessage = processedMessage.replace(
if (sources.length > 0) {
processedText = processedText.replace(
citationRegex,
(_, capturedContent: string) => {
const numbers = capturedContent
@@ -379,7 +364,7 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
return `[${numStr}]`;
}
const source = sourceMessage.sources?.[number - 1];
const source = sources[number - 1];
const url = source?.metadata?.url;
if (url) {
@@ -393,37 +378,27 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
return linksHtml;
},
);
speechMessage = aiMessage.content.replace(regex, '');
speechMessage += block.data.replace(regex, '');
} else {
processedMessage = processedMessage.replace(regex, '');
speechMessage = aiMessage.content.replace(regex, '');
processedText = processedText.replace(regex, '');
speechMessage += block.data.replace(regex, '');
}
const suggestionMessage = messages.find(
(m, j) =>
j > i &&
m.role === 'suggestion' &&
(nextUserMessageIndex === -1 || j < nextUserMessageIndex),
) as SuggestionMessage | undefined;
if (suggestionMessage && suggestionMessage.suggestions.length > 0) {
suggestions = suggestionMessage.suggestions;
}
textBlocks.push(processedText);
} else if (block.type === 'suggestion') {
suggestions = block.data;
}
});
sections.push({
userMessage: msg,
assistantMessage: aiMessage,
sourceMessage: sourceMessage,
parsedAssistantMessage: processedMessage,
speechMessage,
thinkingEnded,
suggestions: suggestions,
});
}
return {
message: msg,
parsedTextBlocks: textBlocks,
speechMessage,
thinkingEnded,
suggestions,
widgets: widgetBlocks,
};
});
return sections;
}, [messages]);
useEffect(() => {
@@ -489,24 +464,17 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
const rewrite = (messageId: string) => {
const index = messages.findIndex((msg) => msg.messageId === messageId);
const chatTurnsIndex = chatTurns.findIndex(
(msg) => msg.messageId === messageId,
);
if (index === -1) return;
const message = chatTurns[chatTurnsIndex - 1];
setMessages((prev) => prev.slice(0, index));
setMessages((prev) => {
return [
...prev.slice(0, messages.length > 2 ? messages.indexOf(message) : 0),
];
});
setChatHistory((prev) => {
return [...prev.slice(0, chatTurns.length > 2 ? chatTurnsIndex - 1 : 0)];
return prev.slice(0, index * 2);
});
sendMessage(message.content, message.messageId, true);
const messageToRewrite = messages[index];
sendMessage(messageToRewrite.query, messageToRewrite.messageId, true);
};
useEffect(() => {
@@ -527,88 +495,165 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
) => {
if (loading || !message) return;
setLoading(true);
setResearchEnded(false);
setMessageAppeared(false);
if (messages.length <= 1) {
window.history.replaceState(null, '', `/c/${chatId}`);
}
let recievedMessage = '';
let added = false;
messageId = messageId ?? crypto.randomBytes(7).toString('hex');
const backendId = crypto.randomBytes(20).toString('hex');
setMessages((prevMessages) => [
...prevMessages,
{
content: message,
messageId: messageId,
chatId: chatId!,
role: 'user',
createdAt: new Date(),
},
]);
const newMessage: Message = {
messageId,
chatId: chatId!,
backendId,
query: message,
responseBlocks: [],
status: 'answering',
createdAt: new Date(),
};
setMessages((prevMessages) => [...prevMessages, newMessage]);
const receivedTextRef = { current: '' };
const messageHandler = 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 (
newMessage.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) {
return {
...msg,
responseBlocks: [...msg.responseBlocks, data.block],
};
}
return msg;
}),
);
}
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 === 'sources') {
setMessages((prevMessages) => [
...prevMessages,
{
messageId: data.messageId,
chatId: chatId!,
role: 'source',
sources: data.data,
createdAt: new Date(),
},
]);
const sourceBlock: Block = {
id: crypto.randomBytes(7).toString('hex'),
type: 'source',
data: data.data,
};
setMessages((prev) =>
prev.map((msg) => {
if (msg.messageId === messageId) {
return {
...msg,
responseBlocks: [...msg.responseBlocks, sourceBlock],
};
}
return msg;
}),
);
if (data.data.length > 0) {
setMessageAppeared(true);
}
}
if (data.type === 'message') {
if (!added) {
setMessages((prevMessages) => [
...prevMessages,
{
content: data.data,
messageId: data.messageId,
chatId: chatId!,
role: 'assistant',
createdAt: new Date(),
},
]);
added = true;
setMessageAppeared(true);
} else {
setMessages((prev) =>
prev.map((message) => {
if (
message.messageId === data.messageId &&
message.role === 'assistant'
) {
return { ...message, content: message.content + data.data };
}
receivedTextRef.current += data.data;
return message;
}),
);
}
recievedMessage += data.data;
setMessages((prev) =>
prev.map((msg) => {
if (msg.messageId === messageId) {
const existingTextBlockIndex = msg.responseBlocks.findIndex(
(b) => b.type === 'text',
);
if (existingTextBlockIndex >= 0) {
const updatedBlocks = [...msg.responseBlocks];
const existingBlock = updatedBlocks[
existingTextBlockIndex
] as Block & { type: 'text' };
updatedBlocks[existingTextBlockIndex] = {
...existingBlock,
data: existingBlock.data + data.data,
};
return { ...msg, responseBlocks: updatedBlocks };
} else {
const textBlock: Block = {
id: crypto.randomBytes(7).toString('hex'),
type: 'text',
data: data.data,
};
return {
...msg,
responseBlocks: [...msg.responseBlocks, textBlock],
};
}
}
return msg;
}),
);
setMessageAppeared(true);
}
if (data.type === 'messageEnd') {
setChatHistory((prevHistory) => [
...prevHistory,
const newHistory: [string, string][] = [
...chatHistory,
['human', message],
['assistant', recievedMessage],
]);
['assistant', receivedTextRef.current],
];
setChatHistory(newHistory);
setMessages((prev) =>
prev.map((msg) =>
msg.messageId === messageId
? { ...msg, status: 'completed' as const }
: msg,
),
);
setLoading(false);
@@ -626,38 +671,37 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
?.click();
}
/* Check if there are sources after message id's index and no suggestions */
const userMessageIndex = messagesRef.current.findIndex(
(msg) => msg.messageId === messageId && msg.role === 'user',
// Check if there are sources and no suggestions
const currentMsg = messagesRef.current.find(
(msg) => msg.messageId === messageId,
);
const sourceMessage = messagesRef.current.find(
(msg, i) => i > userMessageIndex && msg.role === 'source',
) as SourceMessage | undefined;
const suggestionMessageIndex = messagesRef.current.findIndex(
(msg, i) => i > userMessageIndex && msg.role === 'suggestion',
const hasSourceBlocks = currentMsg?.responseBlocks.some(
(block) => block.type === 'source' && block.data.length > 0,
);
const hasSuggestions = currentMsg?.responseBlocks.some(
(block) => block.type === 'suggestion',
);
if (
sourceMessage &&
sourceMessage.sources.length > 0 &&
suggestionMessageIndex == -1
) {
const suggestions = await getSuggestions(messagesRef.current);
setMessages((prev) => {
return [
...prev,
{
role: 'suggestion',
suggestions: suggestions,
chatId: chatId!,
createdAt: new Date(),
messageId: crypto.randomBytes(7).toString('hex'),
},
];
});
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;
}),
);
}
}
};
@@ -726,7 +770,6 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
<chatContext.Provider
value={{
messages,
chatTurns,
sections,
chatHistory,
files,
@@ -750,6 +793,8 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
chatModelProvider,
embeddingModelProvider,
setEmbeddingModelProvider,
researchEnded,
setResearchEnded,
}}
>
{children}

View File

@@ -1,5 +1,3 @@
import { Embeddings } from '@langchain/core/embeddings';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { ModelList, ProviderMetadata } from '../types';
import { UIConfigField } from '@/lib/config/types';
import BaseLLM from './llm';

View File

@@ -1,5 +1,3 @@
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { Embeddings } from '@langchain/core/embeddings';
import { UIConfigField } from '@/lib/config/types';
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
import { Model, ModelList, ProviderMetadata } from '../../types';