From 956a768a86daca0600c25bf6bae3a2de67d5b08c Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:58:46 +0530 Subject: [PATCH] feat(app): handle new architecture --- package.json | 3 + src/app/api/chat/route.ts | 315 ++++++---------- src/components/AssistantSteps.tsx | 197 ++++++++++ src/components/ChatWindow.tsx | 31 +- src/components/MessageActions/Copy.tsx | 9 +- src/components/MessageBox.tsx | 94 +++-- src/components/MessageSources.tsx | 4 +- src/components/Navbar.tsx | 133 +++---- src/lib/actions.ts | 12 +- src/lib/agents/search/classifier/index.ts | 5 +- src/lib/hooks/useChat.tsx | 419 ++++++++++++---------- src/lib/models/base/provider.ts | 2 - src/lib/models/providers/openai/index.ts | 2 - yarn.lock | 227 +++++++++++- 14 files changed, 945 insertions(+), 508 deletions(-) create mode 100644 src/components/AssistantSteps.tsx diff --git a/package.json b/package.json index 5752b9f..11a9ce4 100644 --- a/package.json +++ b/package.json @@ -35,9 +35,11 @@ "html-to-text": "^9.0.5", "jspdf": "^3.0.1", "langchain": "^1.0.4", + "lightweight-charts": "^5.0.9", "lucide-react": "^0.363.0", "mammoth": "^1.9.1", "markdown-to-jsx": "^7.7.2", + "mathjs": "^15.1.0", "next": "^15.2.2", "next-themes": "^0.3.0", "ollama": "^0.6.3", @@ -52,6 +54,7 @@ "sonner": "^1.4.41", "tailwind-merge": "^2.2.2", "winston": "^3.17.0", + "yahoo-finance2": "^3.10.2", "yet-another-react-lightbox": "^3.17.2", "zod": "^4.1.12" }, diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 25b8104..2ea0bf5 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,14 +1,10 @@ import crypto from 'crypto'; -import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; -import { EventEmitter } from 'stream'; -import db from '@/lib/db'; -import { chats, messages as messagesSchema } from '@/lib/db/schema'; -import { and, eq, gt } from 'drizzle-orm'; -import { getFileDetails } from '@/lib/utils/files'; -import { searchHandlers } from '@/lib/search'; import { z } from 'zod'; import ModelRegistry from '@/lib/models/registry'; import { ModelWithProvider } from '@/lib/models/types'; +import SearchAgent from '@/lib/agents/search'; +import SessionManager from '@/lib/session'; +import { ChatTurnMessage } from '@/lib/types'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -20,47 +16,25 @@ const messageSchema = z.object({ }); const chatModelSchema: z.ZodType = z.object({ - providerId: z.string({ - errorMap: () => ({ - message: 'Chat model provider id must be provided', - }), - }), - key: z.string({ - errorMap: () => ({ - message: 'Chat model key must be provided', - }), - }), + providerId: z.string({ message: 'Chat model provider id must be provided' }), + key: z.string({ message: 'Chat model key must be provided' }), }); const embeddingModelSchema: z.ZodType = z.object({ providerId: z.string({ - errorMap: () => ({ - message: 'Embedding model provider id must be provided', - }), - }), - key: z.string({ - errorMap: () => ({ - message: 'Embedding model key must be provided', - }), + message: 'Embedding model provider id must be provided', }), + key: z.string({ message: 'Embedding model key must be provided' }), }); const bodySchema = z.object({ message: messageSchema, optimizationMode: z.enum(['speed', 'balanced', 'quality'], { - errorMap: () => ({ - message: 'Optimization mode must be one of: speed, balanced, quality', - }), + message: 'Optimization mode must be one of: speed, balanced, quality', }), focusMode: z.string().min(1, 'Focus mode is required'), history: z - .array( - z.tuple([z.string(), z.string()], { - errorMap: () => ({ - message: 'History items must be tuples of two strings', - }), - }), - ) + .array(z.tuple([z.string(), z.string()])) .optional() .default([]), files: z.array(z.string()).optional().default([]), @@ -78,7 +52,7 @@ const safeValidateBody = (data: unknown) => { if (!result.success) { return { success: false, - error: result.error.errors.map((e) => ({ + error: result.error.issues.map((e: any) => ({ path: e.path.join('.'), message: e.message, })), @@ -91,151 +65,12 @@ const safeValidateBody = (data: unknown) => { }; }; -const handleEmitterEvents = async ( - stream: EventEmitter, - writer: WritableStreamDefaultWriter, - encoder: TextEncoder, - chatId: string, -) => { - let receivedMessage = ''; - const aiMessageId = crypto.randomBytes(7).toString('hex'); - - stream.on('data', (data) => { - const parsedData = JSON.parse(data); - if (parsedData.type === 'response') { - writer.write( - encoder.encode( - JSON.stringify({ - type: 'message', - data: parsedData.data, - messageId: aiMessageId, - }) + '\n', - ), - ); - - receivedMessage += parsedData.data; - } else if (parsedData.type === 'sources') { - writer.write( - encoder.encode( - JSON.stringify({ - type: 'sources', - data: parsedData.data, - messageId: aiMessageId, - }) + '\n', - ), - ); - - const sourceMessageId = crypto.randomBytes(7).toString('hex'); - - db.insert(messagesSchema) - .values({ - chatId: chatId, - messageId: sourceMessageId, - role: 'source', - sources: parsedData.data, - createdAt: new Date().toString(), - }) - .execute(); - } - }); - stream.on('end', () => { - writer.write( - encoder.encode( - JSON.stringify({ - type: 'messageEnd', - }) + '\n', - ), - ); - writer.close(); - - db.insert(messagesSchema) - .values({ - content: receivedMessage, - chatId: chatId, - messageId: aiMessageId, - role: 'assistant', - createdAt: new Date().toString(), - }) - .execute(); - }); - stream.on('error', (data) => { - const parsedData = JSON.parse(data); - writer.write( - encoder.encode( - JSON.stringify({ - type: 'error', - data: parsedData.data, - }), - ), - ); - writer.close(); - }); -}; - -const handleHistorySave = async ( - message: Message, - humanMessageId: string, - focusMode: string, - files: string[], -) => { - const chat = await db.query.chats.findFirst({ - where: eq(chats.id, message.chatId), - }); - - const fileData = files.map(getFileDetails); - - if (!chat) { - await db - .insert(chats) - .values({ - id: message.chatId, - title: message.content, - createdAt: new Date().toString(), - focusMode: focusMode, - files: fileData, - }) - .execute(); - } else if (JSON.stringify(chat.files ?? []) != JSON.stringify(fileData)) { - db.update(chats) - .set({ - files: files.map(getFileDetails), - }) - .where(eq(chats.id, message.chatId)); - } - - const messageExists = await db.query.messages.findFirst({ - where: eq(messagesSchema.messageId, humanMessageId), - }); - - if (!messageExists) { - await db - .insert(messagesSchema) - .values({ - content: message.content, - chatId: message.chatId, - messageId: humanMessageId, - role: 'user', - createdAt: new Date().toString(), - }) - .execute(); - } else { - await db - .delete(messagesSchema) - .where( - and( - gt(messagesSchema.id, messageExists.id), - eq(messagesSchema.chatId, message.chatId), - ), - ) - .execute(); - } -}; - export const POST = async (req: Request) => { try { const reqBody = (await req.json()) as Body; const parseBody = safeValidateBody(reqBody); + if (!parseBody.success) { return Response.json( { message: 'Invalid request body', error: parseBody.error }, @@ -265,48 +100,116 @@ export const POST = async (req: Request) => { ), ]); - const humanMessageId = - message.messageId ?? crypto.randomBytes(7).toString('hex'); - - const history: BaseMessage[] = body.history.map((msg) => { + const history: ChatTurnMessage[] = body.history.map((msg) => { if (msg[0] === 'human') { - return new HumanMessage({ + return { + role: 'user', content: msg[1], - }); + }; } else { - return new AIMessage({ + return { + role: 'assistant', content: msg[1], - }); + }; } }); - const handler = searchHandlers[body.focusMode]; - - if (!handler) { - return Response.json( - { - message: 'Invalid focus mode', - }, - { status: 400 }, - ); - } - - const stream = await handler.searchAndAnswer( - message.content, - history, - llm, - embedding, - body.optimizationMode, - body.files, - body.systemInstructions as string, - ); + const agent = new SearchAgent(); + const session = SessionManager.createSession(); const responseStream = new TransformStream(); const writer = responseStream.writable.getWriter(); const encoder = new TextEncoder(); - handleEmitterEvents(stream, writer, encoder, message.chatId); - handleHistorySave(message, humanMessageId, body.focusMode, body.files); + let receivedMessage = ''; + + session.addListener('data', (data: any) => { + if (data.type === 'response') { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'message', + data: data.data, + }) + '\n', + ), + ); + receivedMessage += data.data; + } else if (data.type === 'sources') { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'sources', + data: data.data, + }) + '\n', + ), + ); + } else if (data.type === 'block') { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'block', + block: data.block, + }) + '\n', + ), + ); + } else if (data.type === 'updateBlock') { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'updateBlock', + blockId: data.blockId, + patch: data.patch, + }) + '\n', + ), + ); + } else if (data.type === 'researchComplete') { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'researchComplete', + }) + '\n', + ), + ); + } + }); + + session.addListener('end', () => { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'messageEnd', + }) + '\n', + ), + ); + writer.close(); + session.removeAllListeners(); + }); + + session.addListener('error', (data: any) => { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'error', + data: data.data, + }) + '\n', + ), + ); + writer.close(); + session.removeAllListeners(); + }); + + agent.searchAsync(session, { + chatHistory: history, + followUp: message.content, + config: { + llm, + embedding: embedding, + sources: ['web'], + mode: body.optimizationMode, + }, + }); + + /* handleHistorySave(message, humanMessageId, body.focusMode, body.files); */ return new Response(responseStream.readable, { headers: { diff --git a/src/components/AssistantSteps.tsx b/src/components/AssistantSteps.tsx new file mode 100644 index 0000000..c688880 --- /dev/null +++ b/src/components/AssistantSteps.tsx @@ -0,0 +1,197 @@ +'use client'; + +import { Brain, Search, FileText, ChevronDown, ChevronUp } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useEffect, useState } from 'react'; +import { ResearchBlock, ResearchBlockSubStep } from '@/lib/types'; +import { useChat } from '@/lib/hooks/useChat'; + +const getStepIcon = (step: ResearchBlockSubStep) => { + if (step.type === 'reasoning') { + return ; + } else if (step.type === 'searching') { + return ; + } else if (step.type === 'reading') { + return ; + } + return null; +}; + +const getStepTitle = ( + step: ResearchBlockSubStep, + isStreaming: boolean, +): string => { + if (step.type === 'reasoning') { + return isStreaming && !step.reasoning ? 'Thinking...' : 'Thinking'; + } else if (step.type === 'searching') { + return `Searching ${step.searching.length} ${step.searching.length === 1 ? 'query' : 'queries'}`; + } else if (step.type === 'reading') { + return `Found ${step.reading.length} ${step.reading.length === 1 ? 'result' : 'results'}`; + } + return 'Processing'; +}; + +const AssistantSteps = ({ + block, + status, +}: { + block: ResearchBlock; + status: 'answering' | 'completed' | 'error'; +}) => { + const [isExpanded, setIsExpanded] = useState(true); + const { researchEnded, loading } = useChat(); + + useEffect(() => { + if (researchEnded) { + setIsExpanded(false); + } else if (status === 'answering') { + setIsExpanded(true); + } + }, [researchEnded, status]); + + if (!block || block.data.subSteps.length === 0) return null; + + return ( +
+ + + + {isExpanded && ( + +
+ {block.data.subSteps.map((step, index) => { + const isLastStep = index === block.data.subSteps.length - 1; + const isStreaming = loading && isLastStep && !researchEnded; + + return ( + + {/* Timeline connector */} +
+
+ {getStepIcon(step)} +
+ {index < block.data.subSteps.length - 1 && ( +
+ )} +
+ + {/* Step content */} +
+ + {getStepTitle(step, isStreaming)} + + + {step.type === 'reasoning' && ( + <> + {step.reasoning && ( +

+ {step.reasoning} +

+ )} + {isStreaming && !step.reasoning && ( +
+
+
+
+
+ )} + + )} + + {step.type === 'searching' && + step.searching.length > 0 && ( +
+ {step.searching.map((query, idx) => ( + + {query} + + ))} +
+ )} + + {step.type === 'reading' && step.reading.length > 0 && ( +
+ {step.reading.slice(0, 4).map((result, idx) => { + const url = result.metadata.url || ''; + const title = result.metadata.title || 'Untitled'; + const domain = url ? new URL(url).hostname : ''; + const faviconUrl = domain + ? `https://s2.googleusercontent.com/s2/favicons?domain=${domain}&sz=128` + : ''; + + return ( + + {faviconUrl && ( + { + e.currentTarget.style.display = 'none'; + }} + /> + )} + {title} + + ); + })} +
+ )} +
+ + ); + })} +
+ + )} + +
+ ); +}; + +export default AssistantSteps; diff --git a/src/components/ChatWindow.tsx b/src/components/ChatWindow.tsx index dc6ab01..9489219 100644 --- a/src/components/ChatWindow.tsx +++ b/src/components/ChatWindow.tsx @@ -1,14 +1,12 @@ 'use client'; -import { Document } from '@langchain/core/documents'; import Navbar from './Navbar'; import Chat from './Chat'; import EmptyChat from './EmptyChat'; -import { Settings } from 'lucide-react'; -import Link from 'next/link'; import NextError from 'next/error'; import { useChat } from '@/lib/hooks/useChat'; import SettingsButtonMobile from './Settings/SettingsButtonMobile'; +import { Block, Chunk } from '@/lib/types'; export interface BaseMessage { chatId: string; @@ -16,20 +14,27 @@ export interface BaseMessage { createdAt: Date; } +export interface Message extends BaseMessage { + backendId: string; + query: string; + responseBlocks: Block[]; + status: 'answering' | 'completed' | 'error'; +} + +export interface UserMessage extends BaseMessage { + role: 'user'; + content: string; +} + export interface AssistantMessage extends BaseMessage { role: 'assistant'; content: string; suggestions?: string[]; } -export interface UserMessage extends BaseMessage { - role: 'user'; - content: string; -} - export interface SourceMessage extends BaseMessage { role: 'source'; - sources: Document[]; + sources: Chunk[]; } export interface SuggestionMessage extends BaseMessage { @@ -37,11 +42,12 @@ export interface SuggestionMessage extends BaseMessage { suggestions: string[]; } -export type Message = +export type LegacyMessage = | AssistantMessage | UserMessage | SourceMessage | SuggestionMessage; + export type ChatTurn = UserMessage | AssistantMessage; export interface File { @@ -50,6 +56,11 @@ export interface File { fileId: string; } +export interface Widget { + widgetType: string; + params: Record; +} + const ChatWindow = () => { const { hasError, notFound, messages } = useChat(); if (hasError) { diff --git a/src/components/MessageActions/Copy.tsx b/src/components/MessageActions/Copy.tsx index f74b9f3..38ed71a 100644 --- a/src/components/MessageActions/Copy.tsx +++ b/src/components/MessageActions/Copy.tsx @@ -15,7 +15,14 @@ const Copy = ({ return (