diff --git a/.gitignore b/.gitignore index b3c05d5..b4c76ec 100644 --- a/.gitignore +++ b/.gitignore @@ -46,7 +46,8 @@ next-env.d.ts -# extra +# extra +extra /src/archive /src/experimental parsing-cache.json diff --git a/src/app/api/research/chat/route.ts b/src/app/api/research/chat/route.ts index fe72d1a..0e42294 100644 --- a/src/app/api/research/chat/route.ts +++ b/src/app/api/research/chat/route.ts @@ -135,6 +135,57 @@ const getErrorMessage = (error: any): string => { return "Failed to process your question"; }; +// CODE FOR TESTING PURPOSES - SIMULATED STREAMING RESPONSE + +// export async function POST(req: NextRequest) { +// try { +// const requestData: RequestData = await req.json(); +// const sessionId = requestData.sessionId || Date.now().toString(); + +// const RANDOM_BOGUS = [ +// "Flibber flabber wozzle!", +// "Gloopity glop, the blockchain bounces!", +// "Snarfle wumpus, quantum bananas dance!", +// "Ziggity zaggity, distributed spaghetti everywhere!", +// "Hocus pocus, your query just became a penguin!" +// ]; + +// const stream = new ReadableStream({ + +// async start(controller) { +// const startTime = Date.now(); +// const fiveMinutes = 1 * 10 * 1000; + +// while (Date.now() - startTime < fiveMinutes) { +// const randomIndex = Math.floor(Math.random() * RANDOM_BOGUS.length); +// const text = RANDOM_BOGUS[randomIndex] + " "; + +// controller.enqueue(text); +// console.log("printing........") +// // Small delay so it feels like typing +// await new Promise((resolve) => setTimeout(resolve, 100)); +// } + +// controller.close(); +// }, +// }); + +// return new Response(stream, { +// headers: { +// "Content-Type": "text/plain; charset=utf-8", +// "X-Session-Id": sessionId, +// }, +// }); +// } catch (error) { +// console.error("Error in chat API:", error); +// return NextResponse.json( +// { error: "Internal server error" }, +// { status: 500 } +// ); +// } +// } + + export async function POST(req: NextRequest) { try { const requestData: RequestData = await req.json(); diff --git a/src/app/research/components/chat-input.tsx b/src/app/research/components/chat-input.tsx index 01cafa5..c770ce3 100644 --- a/src/app/research/components/chat-input.tsx +++ b/src/app/research/components/chat-input.tsx @@ -35,6 +35,8 @@ interface ChatInputProps { isPreparingIndex: boolean; selectedDocuments: Document[]; onKeyDown: (e: React.KeyboardEvent) => void; + stopStreaming: () => void; + isStreaming: boolean; } export const ChatInput: React.FC = ({ @@ -45,6 +47,8 @@ export const ChatInput: React.FC = ({ isPreparingIndex, selectedDocuments, onKeyDown, + stopStreaming, + isStreaming, }) => { const { activeTool, setTool } = useTool(); const [language, setLanguage] = useState("ts"); @@ -71,6 +75,7 @@ export const ChatInput: React.FC = ({ }; onSendMessage(payload); + setInputValue(""); }; const handleToolChange = (tool: string) => { @@ -165,7 +170,7 @@ export const ChatInput: React.FC = ({ - + */} +
+ {!isStreaming ? ( + + ) : ( + + )} +
+ diff --git a/src/app/research/page.tsx b/src/app/research/page.tsx index 1f90c9c..a23d3c7 100644 --- a/src/app/research/page.tsx +++ b/src/app/research/page.tsx @@ -49,6 +49,7 @@ import { ChevronLeft, ChevronRight, GlobeIcon, Menu, MessageCircle, SquarePen } import { useRouter, useSearchParams } from "next/navigation"; import { Suspense, useCallback, useEffect, useRef, useState } from "react"; + interface Message { id: string; content: string; @@ -98,7 +99,13 @@ function useSessionId() { return { sessionId: sessionIdRef.current, resetSession }; } + + function ResearchChatPageContent() { + + console.log("ResearchChatPageContent mounted"); + + const modeOptions: Record<"research" | "code", string> = { research: "Research", code: "Code", @@ -111,7 +118,10 @@ function ResearchChatPageContent() { const [selectedDocuments, setSelectedDocuments] = useState([]); const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(""); + + const [isLoading, setIsLoading] = useState(false); + const [isPreparingIndex, setIsPreparingIndex] = useState(false); // eslint-disable-next-line @typescript-eslint/no-unused-vars const [indexError, setIndexError] = useState(false); @@ -124,6 +134,10 @@ function ResearchChatPageContent() { const [scrollOpacity, setScrollOpacity] = useState(0); const [preferWebSearch, setPreferWebSearch] = useState(false); + const [isStreaming, setIsStreaming] = useState(false); + const [serverResponding, setServerResponding] = useState(false); // ✅ new + const abortControllerRef = useRef(null); + const { sessionId, resetSession } = useSessionId(); const messagesEndRef = useRef(null); @@ -527,6 +541,21 @@ function ResearchChatPageContent() { } }; + + +const handleStop = () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + } + setIsStreaming(false); + setServerResponding(false); // ✅ stop reflects server streaming + }; + +// const handleStop = () => { +// console.log("handleStop called"); +// }; + const handleSendMessage = async (payload: { query: string; documentPaths: string[]; @@ -542,45 +571,77 @@ function ResearchChatPageContent() { timestamp: new Date().toISOString(), }; - // Create a placeholder for the assistant's response - const assistantPlaceholderMessage: Message = { - id: crypto.randomUUID(), - content: - payload.tool === "code-composer" - ? "Reading and analyzing documents to generate code. Check the preview panel to see live progress." - : "", - role: "assistant", +// Modify handleSendMessage +const handleSendMessage = async (payload: { + query: string; + documentPaths: string[]; + tool?: string; + language?: Language; + scope?: string[]; +}) => { + + const userMessage: Message = { + id: crypto.randomUUID(), + content: payload.query, + role: "user", + timestamp: new Date().toISOString(), + }; + + const assistantPlaceholderMessage: Message = { + id: crypto.randomUUID(), + content: + payload.tool === "code-composer" + ? "Reading and analyzing documents to generate code. Check the preview panel to see live progress." + : "", + role: "assistant", + timestamp: new Date().toISOString(), + isLoadingPlaceholder: payload.tool !== "code-composer", + docPaths: payload.documentPaths, + }; + + setMessages((prev) => [...prev, userMessage, assistantPlaceholderMessage]); + setInputValue(""); + setIsLoading(true); + setIsStreaming(true); + setServerResponding(true); + + const controller = new AbortController(); + abortControllerRef.current = controller; + + let earlyCodeGenerationId: string | null = null; + if (payload.tool === "code-composer") { + earlyCodeGenerationId = crypto.randomUUID(); + + const newCodeGeneration: CodeGeneration = { + id: earlyCodeGenerationId, + language: payload.language || "ts", + query: payload.query, + topic: "", + plan: "", + pseudocode: "", + implementation: "", + hasStructuredResponse: false, timestamp: new Date().toISOString(), - isLoadingPlaceholder: payload.tool !== "code-composer", - docPaths: payload.documentPaths, + isStreaming: true, + currentSection: "reading-documents", + sources: [], }; - setMessages((prev) => [...prev, userMessage, assistantPlaceholderMessage]); - setInputValue(""); - setIsLoading(true); - - let earlyCodeGenerationId: string | null = null; - if (payload.tool === "code-composer") { - earlyCodeGenerationId = crypto.randomUUID(); - - const newCodeGeneration: CodeGeneration = { - id: earlyCodeGenerationId, - language: payload.language || "ts", - query: payload.query, - topic: "", - plan: "", - pseudocode: "", - implementation: "", - hasStructuredResponse: false, - timestamp: new Date().toISOString(), - isStreaming: true, - currentSection: "reading-documents", - sources: [], - }; - - setCodeGenerations((prev) => [...prev, newCodeGeneration]); - } + setCodeGenerations((prev) => [...prev, newCodeGeneration]); + } + try { + const streamingPayload = { + ...payload, + enableStreaming: true, + sessionId, + }; + const response = await fetch("/api/research/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(streamingPayload), + signal: controller.signal, + }); try { const streamingPayload = { ...payload, @@ -607,44 +668,65 @@ function ResearchChatPageContent() { ), ); - if (earlyCodeGenerationId) { - setCodeGenerations((prev) => - prev.filter((gen) => gen.id !== earlyCodeGenerationId), - ); - } + if (!response.ok) { + setMessages((prev) => + prev.map((msg) => + msg.id === assistantPlaceholderMessage.id + ? { + ...msg, + content: "Sorry, I couldn't get a response. Please try again.", + isLoadingPlaceholder: false, + } + : msg + ) + ); - throw new Error(`Failed to send message. Status: ${response.status}`); + if (earlyCodeGenerationId) { + setCodeGenerations((prev) => + prev.filter((gen) => gen.id !== earlyCodeGenerationId) + ); } - await handleCodeComposerStream( - response, - assistantPlaceholderMessage, - payload, - earlyCodeGenerationId, - ); - } catch (error) { + throw new Error(`Failed to send message. Status: ${response.status}`); + } + + await handleCodeComposerStream( + response, + assistantPlaceholderMessage, + payload, + earlyCodeGenerationId + ); + } catch (error: any) { + if (error.name === "AbortError") { + console.log("Streaming stopped by user"); + } else { console.error("Chat error:", error); setMessages((prev) => prev.map((msg) => msg.id === assistantPlaceholderMessage.id && msg.isLoadingPlaceholder ? { - ...msg, - content: "Sorry, an error occurred. Please try again.", - isLoadingPlaceholder: false, - } - : msg, - ), + ...msg, + content: "Sorry, an error occurred. Please try again.", + isLoadingPlaceholder: false, + } + : msg + ) ); + } - if (earlyCodeGenerationId) { - setCodeGenerations((prev) => - prev.filter((gen) => gen.id !== earlyCodeGenerationId), - ); - } - } finally { - setIsLoading(false); + if (earlyCodeGenerationId) { + setCodeGenerations((prev) => + prev.filter((gen) => gen.id !== earlyCodeGenerationId) + ); } - }; + } finally { + setIsLoading(false); + setIsStreaming(false); + abortControllerRef.current = null; + } +}; + + const handleKeyDown = () => { }; @@ -1066,7 +1148,57 @@ function ResearchChatPageContent() { ))} - {mode === "code" && ( + + )} + + + Search + + + {/* */} + { + if (isStreaming) { + handleStop(); // 🔴 stop streaming if already running + } else { + handleSendMessage({ + query: inputValue, + documentPaths: selectedDocuments.map((d) => d.path), + tool: activeTool === "code-composer" ? "code-composer" : undefined, + language: language, + }); + } + }} + > + {/* {isStreaming ? "■ Stop" : "▶️ Chat"} */} + + + + + )}