@@ -76,35 +84,73 @@ const Page = () => {
-
- {discover &&
- discover?.map((item, i) => (
-
-

-
-
- {item.title.slice(0, 100)}...
-
-
- {item.content.slice(0, 100)}...
-
-
-
- ))}
+
+ {topics.map((t, i) => (
+
setActiveTopic(t.key)}
+ >
+ {t.display}
+
+ ))}
+
+ {loading ? (
+
+ ) : (
+
+ {discover &&
+ discover?.map((item, i) => (
+
+

+
+
+ {item.title.slice(0, 100)}...
+
+
+ {item.content.slice(0, 100)}...
+
+
+
+ ))}
+
+ )}
>
);
From 45b51ab1561b869c6641beac56d09952a890c6a8 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Tue, 29 Jul 2025 13:17:07 +0530
Subject: [PATCH 23/33] feat(discover-api): handle topics
---
src/app/api/discover/route.ts | 48 ++++++++++++++++++++++-------------
src/app/discover/page.tsx | 1 -
2 files changed, 31 insertions(+), 18 deletions(-)
diff --git a/src/app/api/discover/route.ts b/src/app/api/discover/route.ts
index d0e56a6..4f141eb 100644
--- a/src/app/api/discover/route.ts
+++ b/src/app/api/discover/route.ts
@@ -1,37 +1,52 @@
import { searchSearxng } from '@/lib/searxng';
-const articleWebsites = [
- 'yahoo.com',
- 'www.exchangewire.com',
- 'businessinsider.com',
- /* 'wired.com',
- 'mashable.com',
- 'theverge.com',
- 'gizmodo.com',
- 'cnet.com',
- 'venturebeat.com', */
-];
+const websitesForTopic = {
+ tech: {
+ query: ['technology news', 'latest tech', 'AI', 'science and innovation'],
+ links: ['techcrunch.com', 'wired.com', 'theverge.com'],
+ },
+ finance: {
+ query: ['finance news', 'economy', 'stock market', 'investing'],
+ links: ['bloomberg.com', 'cnbc.com', 'marketwatch.com'],
+ },
+ art: {
+ query: ['art news', 'culture', 'modern art', 'cultural events'],
+ links: ['artnews.com', 'hyperallergic.com', 'theartnewspaper.com'],
+ },
+ sports: {
+ query: ['sports news', 'latest sports', 'cricket football tennis'],
+ links: ['espn.com', 'bbc.com/sport', 'skysports.com'],
+ },
+ entertainment: {
+ query: ['entertainment news', 'movies', 'TV shows', 'celebrities'],
+ links: ['hollywoodreporter.com', 'variety.com', 'deadline.com'],
+ },
+};
-const topics = ['AI', 'tech']; /* TODO: Add UI to customize this */
+type Topic = keyof typeof websitesForTopic;
export const GET = async (req: Request) => {
try {
const params = new URL(req.url).searchParams;
+
const mode: 'normal' | 'preview' =
(params.get('mode') as 'normal' | 'preview') || 'normal';
+ const topic: Topic = (params.get('topic') as Topic) || 'tech';
+
+ const selectedTopic = websitesForTopic[topic];
let data = [];
if (mode === 'normal') {
data = (
await Promise.all([
- ...new Array(articleWebsites.length * topics.length)
+ ...new Array(selectedTopic.links.length * selectedTopic.query.length)
.fill(0)
.map(async (_, i) => {
return (
await searchSearxng(
- `site:${articleWebsites[i % articleWebsites.length]} ${
- topics[i % topics.length]
+ `site:${selectedTopic.links[i % selectedTopic.links.length]} ${
+ selectedTopic.query[i % selectedTopic.query.length]
}`,
{
engines: ['bing news'],
@@ -45,11 +60,10 @@ export const GET = async (req: Request) => {
)
.map((result) => result)
.flat()
- .sort(() => Math.random() - 0.5);
} else {
data = (
await searchSearxng(
- `site:${articleWebsites[Math.floor(Math.random() * articleWebsites.length)]} ${topics[Math.floor(Math.random() * topics.length)]}`,
+ `site:${selectedTopic.links[Math.floor(Math.random() * selectedTopic.links.length)]} ${selectedTopic.query[Math.floor(Math.random() * selectedTopic.query.length)]}`,
{
engines: ['bing news'],
pageno: 1,
diff --git a/src/app/discover/page.tsx b/src/app/discover/page.tsx
index c7dce37..8e20e50 100644
--- a/src/app/discover/page.tsx
+++ b/src/app/discover/page.tsx
@@ -43,7 +43,6 @@ const Page = () => {
const fetchArticles = async (topic: string) => {
setLoading(true);
- console.log(topic);
try {
const res = await fetch(`/api/discover?topic=${topic}`, {
method: 'GET',
From 88be3a045bac4f0336f50050840213ce4cc7d421 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Tue, 29 Jul 2025 13:18:36 +0530
Subject: [PATCH 24/33] feat(discover): randomly sort results
---
src/app/api/discover/route.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/app/api/discover/route.ts b/src/app/api/discover/route.ts
index 4f141eb..c02d95c 100644
--- a/src/app/api/discover/route.ts
+++ b/src/app/api/discover/route.ts
@@ -60,6 +60,7 @@ export const GET = async (req: Request) => {
)
.map((result) => result)
.flat()
+ .sort(() => Math.random() - 0.5);
} else {
data = (
await searchSearxng(
From 37cd6d3ab57cbe7c4fbb62750fd764cda953d9c7 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Fri, 1 Aug 2025 20:41:07 +0530
Subject: [PATCH 25/33] feat(discover): prevent duplicate articles
---
src/app/api/discover/route.ts | 34 ++++++++++++++++++----------------
1 file changed, 18 insertions(+), 16 deletions(-)
diff --git a/src/app/api/discover/route.ts b/src/app/api/discover/route.ts
index c02d95c..415aee8 100644
--- a/src/app/api/discover/route.ts
+++ b/src/app/api/discover/route.ts
@@ -38,28 +38,30 @@ export const GET = async (req: Request) => {
let data = [];
if (mode === 'normal') {
+ const seenUrls = new Set();
+
data = (
- await Promise.all([
- ...new Array(selectedTopic.links.length * selectedTopic.query.length)
- .fill(0)
- .map(async (_, i) => {
+ await Promise.all(
+ selectedTopic.links.flatMap((link) =>
+ selectedTopic.query.map(async (query) => {
return (
- await searchSearxng(
- `site:${selectedTopic.links[i % selectedTopic.links.length]} ${
- selectedTopic.query[i % selectedTopic.query.length]
- }`,
- {
- engines: ['bing news'],
- pageno: 1,
- language: 'en',
- },
- )
+ await searchSearxng(`site:${link} ${query}`, {
+ engines: ['bing news'],
+ pageno: 1,
+ language: 'en',
+ })
).results;
}),
- ])
+ ),
+ )
)
- .map((result) => result)
.flat()
+ .filter((item) => {
+ const url = item.url?.toLowerCase().trim();
+ if (seenUrls.has(url)) return false;
+ seenUrls.add(url);
+ return true;
+ })
.sort(() => Math.random() - 0.5);
} else {
data = (
From eadbedb713d1bd26773ea357b4d5cd42563fde8c Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sat, 2 Aug 2025 17:14:34 +0530
Subject: [PATCH 26/33] feat(groq): switch to `@langchain/groq` for better
handling
---
package.json | 1 +
src/lib/providers/groq.ts | 12 +++---------
yarn.lock | 21 +++++++++++++++++++++
3 files changed, 25 insertions(+), 9 deletions(-)
diff --git a/package.json b/package.json
index 9e9137f..5715c2a 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"@langchain/community": "^0.3.49",
"@langchain/core": "^0.3.66",
"@langchain/google-genai": "^0.2.15",
+ "@langchain/groq": "^0.2.3",
"@langchain/ollama": "^0.2.3",
"@langchain/openai": "^0.6.2",
"@langchain/textsplitters": "^0.1.0",
diff --git a/src/lib/providers/groq.ts b/src/lib/providers/groq.ts
index 6a196ee..4e7db51 100644
--- a/src/lib/providers/groq.ts
+++ b/src/lib/providers/groq.ts
@@ -1,4 +1,4 @@
-import { ChatOpenAI } from '@langchain/openai';
+import { ChatGroq } from '@langchain/groq';
import { getGroqApiKey } from '../config';
import { ChatModel } from '.';
@@ -28,16 +28,10 @@ export const loadGroqChatModels = async () => {
groqChatModels.forEach((model: any) => {
chatModels[model.id] = {
displayName: model.id,
- model: new ChatOpenAI({
+ model: new ChatGroq({
apiKey: groqApiKey,
- modelName: model.id,
+ model: model.id,
temperature: 0.7,
- configuration: {
- baseURL: 'https://api.groq.com/openai/v1',
- },
- metadata: {
- 'model-type': 'groq',
- },
}) as unknown as BaseChatModel,
};
});
diff --git a/yarn.lock b/yarn.lock
index b8893e7..8a6859a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -653,6 +653,14 @@
"@google/generative-ai" "^0.24.0"
uuid "^11.1.0"
+"@langchain/groq@^0.2.3":
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/@langchain/groq/-/groq-0.2.3.tgz#3bfcbfc827cf469df3a1b5bb9799f4b0212b4625"
+ integrity sha512-r+yjysG36a0IZxTlCMr655Feumfb4IrOyA0jLLq4l7gEhVyMpYXMwyE6evseyU2LRP+7qOPbGRVpGqAIK0MsUA==
+ dependencies:
+ groq-sdk "^0.19.0"
+ zod "^3.22.4"
+
"@langchain/ollama@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@langchain/ollama/-/ollama-0.2.3.tgz#4868e66db4fc480f08c42fc652274abbab0416f0"
@@ -2732,6 +2740,19 @@ graphql@^16.11.0:
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.11.0.tgz#96d17f66370678027fdf59b2d4c20b4efaa8a633"
integrity sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==
+groq-sdk@^0.19.0:
+ version "0.19.0"
+ resolved "https://registry.yarnpkg.com/groq-sdk/-/groq-sdk-0.19.0.tgz#564ce018172dc3e2e2793398e0227a035a357d09"
+ integrity sha512-vdh5h7ORvwvOvutA80dKF81b0gPWHxu6K/GOJBOM0n6p6CSqAVLhFfeS79Ef0j/yCycDR09jqY7jkYz9dLiS6w==
+ dependencies:
+ "@types/node" "^18.11.18"
+ "@types/node-fetch" "^2.6.4"
+ abort-controller "^3.0.0"
+ agentkeepalive "^4.2.1"
+ form-data-encoder "1.7.2"
+ formdata-node "^4.3.2"
+ node-fetch "^2.6.7"
+
guid-typescript@^1.0.9:
version "1.0.9"
resolved "https://registry.yarnpkg.com/guid-typescript/-/guid-typescript-1.0.9.tgz#e35f77003535b0297ea08548f5ace6adb1480ddc"
From 113299710874842d9ce69a8835b1868593d105d8 Mon Sep 17 00:00:00 2001
From: Samuel Dockery
Date: Sun, 10 Aug 2025 07:50:31 -0700
Subject: [PATCH 27/33] feat: Add support for latest AI models from Anthropic,
Google, & OpenAI
---
src/lib/providers/anthropic.ts | 12 ++++++++++++
src/lib/providers/gemini.ts | 4 ++++
src/lib/providers/openai.ts | 14 +++++++++++++-
3 files changed, 29 insertions(+), 1 deletion(-)
diff --git a/src/lib/providers/anthropic.ts b/src/lib/providers/anthropic.ts
index 2b0f2cc..6af2115 100644
--- a/src/lib/providers/anthropic.ts
+++ b/src/lib/providers/anthropic.ts
@@ -9,6 +9,18 @@ export const PROVIDER_INFO = {
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
const anthropicChatModels: Record[] = [
+ {
+ displayName: 'Claude 4.1 Opus',
+ key: 'claude-opus-4-1-20250805',
+ },
+ {
+ displayName: 'Claude 4 Opus',
+ key: 'claude-opus-4-20250514',
+ },
+ {
+ displayName: 'Claude 4 Sonnet',
+ key: 'claude-sonnet-4-20250514',
+ },
{
displayName: 'Claude 3.7 Sonnet',
key: 'claude-3-7-sonnet-20250219',
diff --git a/src/lib/providers/gemini.ts b/src/lib/providers/gemini.ts
index a9ef4d5..418e0a4 100644
--- a/src/lib/providers/gemini.ts
+++ b/src/lib/providers/gemini.ts
@@ -17,6 +17,10 @@ const geminiChatModels: Record[] = [
displayName: 'Gemini 2.5 Flash',
key: 'gemini-2.5-flash',
},
+ {
+ displayName: 'Gemini 2.5 Flash-Lite',
+ key: 'gemini-2.5-flash-lite',
+ },
{
displayName: 'Gemini 2.5 Pro',
key: 'gemini-2.5-pro',
diff --git a/src/lib/providers/openai.ts b/src/lib/providers/openai.ts
index c857b0e..a672e53 100644
--- a/src/lib/providers/openai.ts
+++ b/src/lib/providers/openai.ts
@@ -42,6 +42,18 @@ const openaiChatModels: Record[] = [
displayName: 'GPT 4.1',
key: 'gpt-4.1',
},
+ {
+ displayName: 'GPT 5 nano',
+ key: 'gpt-5-nano',
+ },
+ {
+ displayName: 'GPT 5 mini',
+ key: 'gpt-5-mini',
+ },
+ {
+ displayName: 'GPT 5',
+ key: 'gpt-5',
+ },
];
const openaiEmbeddingModels: Record[] = [
@@ -69,7 +81,7 @@ export const loadOpenAIChatModels = async () => {
model: new ChatOpenAI({
apiKey: openaiApiKey,
modelName: model.key,
- temperature: 0.7,
+ temperature: 1,
}) as unknown as BaseChatModel,
};
});
From 3edd7d44dd5acfe7dbc416db8eb588b71cedb597 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Tue, 12 Aug 2025 21:39:14 +0530
Subject: [PATCH 28/33] feat(openai): conditionally set temperature
---
src/lib/providers/openai.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/lib/providers/openai.ts b/src/lib/providers/openai.ts
index a672e53..7e26763 100644
--- a/src/lib/providers/openai.ts
+++ b/src/lib/providers/openai.ts
@@ -81,7 +81,7 @@ export const loadOpenAIChatModels = async () => {
model: new ChatOpenAI({
apiKey: openaiApiKey,
modelName: model.key,
- temperature: 1,
+ temperature: model.key.includes('gpt-5') ? 1 : 0.7,
}) as unknown as BaseChatModel,
};
});
From 8fc78086547e414358dbbeb1d4f9d286fa2d0618 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Wed, 20 Aug 2025 20:20:50 +0530
Subject: [PATCH 29/33] feat(hooks): implement useChat hook
---
src/lib/hooks/useChat.tsx | 643 ++++++++++++++++++++++++++++++++++++++
1 file changed, 643 insertions(+)
create mode 100644 src/lib/hooks/useChat.tsx
diff --git a/src/lib/hooks/useChat.tsx b/src/lib/hooks/useChat.tsx
new file mode 100644
index 0000000..573ac6b
--- /dev/null
+++ b/src/lib/hooks/useChat.tsx
@@ -0,0 +1,643 @@
+'use client';
+
+import { Message } from '@/components/ChatWindow';
+import { createContext, useContext, useEffect, useRef, useState } from 'react';
+import crypto from 'crypto';
+import { useSearchParams } from 'next/navigation';
+import { toast } from 'sonner';
+import { Document } from '@langchain/core/documents';
+import { getSuggestions } from '../actions';
+
+type ChatContext = {
+ messages: Message[];
+ chatHistory: [string, string][];
+ files: File[];
+ fileIds: string[];
+ focusMode: string;
+ chatId: string | undefined;
+ optimizationMode: string;
+ isMessagesLoaded: boolean;
+ loading: boolean;
+ notFound: boolean;
+ messageAppeared: boolean;
+ isReady: boolean;
+ hasError: boolean;
+ setOptimizationMode: (mode: string) => void;
+ setFocusMode: (mode: string) => void;
+ setFiles: (files: File[]) => void;
+ setFileIds: (fileIds: string[]) => void;
+ sendMessage: (
+ message: string,
+ messageId?: string,
+ rewrite?: boolean,
+ ) => Promise;
+ rewrite: (messageId: string) => void;
+};
+
+export interface File {
+ fileName: string;
+ fileExtension: string;
+ fileId: string;
+}
+
+interface ChatModelProvider {
+ name: string;
+ provider: string;
+}
+
+interface EmbeddingModelProvider {
+ name: string;
+ provider: string;
+}
+
+const checkConfig = async (
+ setChatModelProvider: (provider: ChatModelProvider) => void,
+ setEmbeddingModelProvider: (provider: EmbeddingModelProvider) => void,
+ setIsConfigReady: (ready: boolean) => void,
+ setHasError: (hasError: boolean) => void,
+) => {
+ try {
+ let chatModel = localStorage.getItem('chatModel');
+ let chatModelProvider = localStorage.getItem('chatModelProvider');
+ let embeddingModel = localStorage.getItem('embeddingModel');
+ let embeddingModelProvider = localStorage.getItem('embeddingModelProvider');
+
+ const autoImageSearch = localStorage.getItem('autoImageSearch');
+ const autoVideoSearch = localStorage.getItem('autoVideoSearch');
+
+ if (!autoImageSearch) {
+ localStorage.setItem('autoImageSearch', 'true');
+ }
+
+ if (!autoVideoSearch) {
+ localStorage.setItem('autoVideoSearch', 'false');
+ }
+
+ const providers = await fetch(`/api/models`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ }).then(async (res) => {
+ if (!res.ok)
+ throw new Error(
+ `Failed to fetch models: ${res.status} ${res.statusText}`,
+ );
+ return res.json();
+ });
+
+ if (
+ !chatModel ||
+ !chatModelProvider ||
+ !embeddingModel ||
+ !embeddingModelProvider
+ ) {
+ if (!chatModel || !chatModelProvider) {
+ const chatModelProviders = providers.chatModelProviders;
+ const chatModelProvidersKeys = Object.keys(chatModelProviders);
+
+ if (!chatModelProviders || chatModelProvidersKeys.length === 0) {
+ return toast.error('No chat models available');
+ } else {
+ chatModelProvider =
+ chatModelProvidersKeys.find(
+ (provider) =>
+ Object.keys(chatModelProviders[provider]).length > 0,
+ ) || chatModelProvidersKeys[0];
+ }
+
+ if (
+ chatModelProvider === 'custom_openai' &&
+ Object.keys(chatModelProviders[chatModelProvider]).length === 0
+ ) {
+ toast.error(
+ "Looks like you haven't configured any chat model providers. Please configure them from the settings page or the config file.",
+ );
+ return setHasError(true);
+ }
+
+ chatModel = Object.keys(chatModelProviders[chatModelProvider])[0];
+ }
+
+ if (!embeddingModel || !embeddingModelProvider) {
+ const embeddingModelProviders = providers.embeddingModelProviders;
+
+ if (
+ !embeddingModelProviders ||
+ Object.keys(embeddingModelProviders).length === 0
+ )
+ return toast.error('No embedding models available');
+
+ embeddingModelProvider = Object.keys(embeddingModelProviders)[0];
+ embeddingModel = Object.keys(
+ embeddingModelProviders[embeddingModelProvider],
+ )[0];
+ }
+
+ localStorage.setItem('chatModel', chatModel!);
+ localStorage.setItem('chatModelProvider', chatModelProvider);
+ localStorage.setItem('embeddingModel', embeddingModel!);
+ localStorage.setItem('embeddingModelProvider', embeddingModelProvider);
+ } else {
+ const chatModelProviders = providers.chatModelProviders;
+ const embeddingModelProviders = providers.embeddingModelProviders;
+
+ if (
+ Object.keys(chatModelProviders).length > 0 &&
+ (!chatModelProviders[chatModelProvider] ||
+ Object.keys(chatModelProviders[chatModelProvider]).length === 0)
+ ) {
+ const chatModelProvidersKeys = Object.keys(chatModelProviders);
+ chatModelProvider =
+ chatModelProvidersKeys.find(
+ (key) => Object.keys(chatModelProviders[key]).length > 0,
+ ) || chatModelProvidersKeys[0];
+
+ localStorage.setItem('chatModelProvider', chatModelProvider);
+ }
+
+ if (
+ chatModelProvider &&
+ !chatModelProviders[chatModelProvider][chatModel]
+ ) {
+ if (
+ chatModelProvider === 'custom_openai' &&
+ Object.keys(chatModelProviders[chatModelProvider]).length === 0
+ ) {
+ toast.error(
+ "Looks like you haven't configured any chat model providers. Please configure them from the settings page or the config file.",
+ );
+ return setHasError(true);
+ }
+
+ chatModel = Object.keys(
+ chatModelProviders[
+ Object.keys(chatModelProviders[chatModelProvider]).length > 0
+ ? chatModelProvider
+ : Object.keys(chatModelProviders)[0]
+ ],
+ )[0];
+
+ localStorage.setItem('chatModel', chatModel);
+ }
+
+ if (
+ Object.keys(embeddingModelProviders).length > 0 &&
+ !embeddingModelProviders[embeddingModelProvider]
+ ) {
+ embeddingModelProvider = Object.keys(embeddingModelProviders)[0];
+ localStorage.setItem('embeddingModelProvider', embeddingModelProvider);
+ }
+
+ if (
+ embeddingModelProvider &&
+ !embeddingModelProviders[embeddingModelProvider][embeddingModel]
+ ) {
+ embeddingModel = Object.keys(
+ embeddingModelProviders[embeddingModelProvider],
+ )[0];
+ localStorage.setItem('embeddingModel', embeddingModel);
+ }
+ }
+
+ setChatModelProvider({
+ name: chatModel!,
+ provider: chatModelProvider,
+ });
+
+ setEmbeddingModelProvider({
+ name: embeddingModel!,
+ provider: embeddingModelProvider,
+ });
+
+ setIsConfigReady(true);
+ } catch (err) {
+ console.error('An error occurred while checking the configuration:', err);
+ setIsConfigReady(false);
+ setHasError(true);
+ }
+};
+
+const loadMessages = async (
+ chatId: string,
+ setMessages: (messages: Message[]) => void,
+ setIsMessagesLoaded: (loaded: boolean) => void,
+ setChatHistory: (history: [string, string][]) => void,
+ setFocusMode: (mode: 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.map((msg: any) => {
+ return {
+ ...msg,
+ ...JSON.parse(msg.metadata),
+ };
+ }) as Message[];
+
+ setMessages(messages);
+
+ const history = messages.map((msg) => {
+ return [msg.role, msg.content];
+ }) as [string, string][];
+
+ console.debug(new Date(), 'app:messages_loaded');
+
+ document.title = messages[0].content;
+
+ 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);
+ setFocusMode(data.chat.focusMode);
+ setIsMessagesLoaded(true);
+};
+
+export const chatContext = createContext({
+ chatHistory: [],
+ chatId: '',
+ fileIds: [],
+ files: [],
+ focusMode: '',
+ hasError: false,
+ isMessagesLoaded: false,
+ isReady: false,
+ loading: false,
+ messageAppeared: false,
+ messages: [],
+ notFound: false,
+ optimizationMode: '',
+ rewrite: () => {},
+ sendMessage: async () => {},
+ setFileIds: () => {},
+ setFiles: () => {},
+ setFocusMode: () => {},
+ setOptimizationMode: () => {},
+});
+
+export const ChatProvider = ({
+ children,
+ id,
+}: {
+ children: React.ReactNode;
+ id?: string;
+}) => {
+ const searchParams = useSearchParams();
+ const initialMessage = searchParams.get('q');
+
+ const [chatId, setChatId] = useState(id);
+ const [newChatCreated, setNewChatCreated] = useState(false);
+
+ const [loading, setLoading] = useState(false);
+ const [messageAppeared, setMessageAppeared] = useState(false);
+
+ const [chatHistory, setChatHistory] = useState<[string, string][]>([]);
+ const [messages, setMessages] = useState([]);
+
+ const [files, setFiles] = useState([]);
+ const [fileIds, setFileIds] = useState([]);
+
+ const [focusMode, setFocusMode] = useState('webSearch');
+ const [optimizationMode, setOptimizationMode] = useState('speed');
+
+ const [isMessagesLoaded, setIsMessagesLoaded] = useState(false);
+
+ const [notFound, setNotFound] = useState(false);
+
+ const [chatModelProvider, setChatModelProvider] = useState(
+ {
+ name: '',
+ provider: '',
+ },
+ );
+
+ const [embeddingModelProvider, setEmbeddingModelProvider] =
+ useState({
+ name: '',
+ provider: '',
+ });
+
+ const [isConfigReady, setIsConfigReady] = useState(false);
+ const [hasError, setHasError] = useState(false);
+ const [isReady, setIsReady] = useState(false);
+
+ const messagesRef = useRef([]);
+
+ useEffect(() => {
+ checkConfig(
+ setChatModelProvider,
+ setEmbeddingModelProvider,
+ setIsConfigReady,
+ setHasError,
+ );
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ if (
+ chatId &&
+ !newChatCreated &&
+ !isMessagesLoaded &&
+ messages.length === 0
+ ) {
+ loadMessages(
+ chatId,
+ setMessages,
+ setIsMessagesLoaded,
+ setChatHistory,
+ setFocusMode,
+ setNotFound,
+ setFiles,
+ setFileIds,
+ );
+ } else if (!chatId) {
+ setNewChatCreated(true);
+ setIsMessagesLoaded(true);
+ setChatId(crypto.randomBytes(20).toString('hex'));
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ messagesRef.current = messages;
+ }, [messages]);
+
+ useEffect(() => {
+ if (isMessagesLoaded && isConfigReady) {
+ setIsReady(true);
+ console.debug(new Date(), 'app:ready');
+ } else {
+ setIsReady(false);
+ }
+ }, [isMessagesLoaded, isConfigReady]);
+
+ const rewrite = (messageId: string) => {
+ const index = messages.findIndex((msg) => msg.messageId === messageId);
+
+ if (index === -1) return;
+
+ const message = messages[index - 1];
+
+ setMessages((prev) => {
+ return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)];
+ });
+ setChatHistory((prev) => {
+ return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)];
+ });
+
+ sendMessage(message.content, message.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 sendMessage: ChatContext['sendMessage'] = async (
+ message,
+ messageId,
+ rewrite = false,
+ ) => {
+ if (loading) return;
+ setLoading(true);
+ setMessageAppeared(false);
+
+ let sources: Document[] | undefined = undefined;
+ let recievedMessage = '';
+ let added = false;
+
+ messageId = messageId ?? crypto.randomBytes(7).toString('hex');
+
+ setMessages((prevMessages) => [
+ ...prevMessages,
+ {
+ content: message,
+ messageId: messageId,
+ chatId: chatId!,
+ role: 'user',
+ createdAt: new Date(),
+ },
+ ]);
+
+ const messageHandler = async (data: any) => {
+ if (data.type === 'error') {
+ toast.error(data.data);
+ setLoading(false);
+ return;
+ }
+
+ if (data.type === 'sources') {
+ sources = data.data;
+ if (!added) {
+ setMessages((prevMessages) => [
+ ...prevMessages,
+ {
+ content: '',
+ messageId: data.messageId,
+ chatId: chatId!,
+ role: 'assistant',
+ sources: sources,
+ createdAt: new Date(),
+ },
+ ]);
+ added = true;
+ }
+ setMessageAppeared(true);
+ }
+
+ if (data.type === 'message') {
+ if (!added) {
+ setMessages((prevMessages) => [
+ ...prevMessages,
+ {
+ content: data.data,
+ messageId: data.messageId,
+ chatId: chatId!,
+ role: 'assistant',
+ sources: sources,
+ createdAt: new Date(),
+ },
+ ]);
+ added = true;
+ }
+
+ setMessages((prev) =>
+ prev.map((message) => {
+ if (message.messageId === data.messageId) {
+ return { ...message, content: message.content + data.data };
+ }
+
+ return message;
+ }),
+ );
+
+ recievedMessage += data.data;
+ setMessageAppeared(true);
+ }
+
+ if (data.type === 'messageEnd') {
+ setChatHistory((prevHistory) => [
+ ...prevHistory,
+ ['human', message],
+ ['assistant', recievedMessage],
+ ]);
+
+ setLoading(false);
+
+ const lastMsg = messagesRef.current[messagesRef.current.length - 1];
+
+ const autoImageSearch = localStorage.getItem('autoImageSearch');
+ const autoVideoSearch = localStorage.getItem('autoVideoSearch');
+
+ if (autoImageSearch === 'true') {
+ document
+ .getElementById(`search-images-${lastMsg.messageId}`)
+ ?.click();
+ }
+
+ if (autoVideoSearch === 'true') {
+ document
+ .getElementById(`search-videos-${lastMsg.messageId}`)
+ ?.click();
+ }
+
+ if (
+ lastMsg.role === 'assistant' &&
+ lastMsg.sources &&
+ lastMsg.sources.length > 0 &&
+ !lastMsg.suggestions
+ ) {
+ const suggestions = await getSuggestions(messagesRef.current);
+ setMessages((prev) =>
+ prev.map((msg) => {
+ if (msg.messageId === lastMsg.messageId) {
+ return { ...msg, suggestions: suggestions };
+ }
+ return msg;
+ }),
+ );
+ }
+ }
+ };
+
+ 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,
+ focusMode: focusMode,
+ optimizationMode: optimizationMode,
+ history: rewrite
+ ? chatHistory.slice(0, messageIndex === -1 ? undefined : messageIndex)
+ : chatHistory,
+ chatModel: {
+ name: chatModelProvider.name,
+ provider: chatModelProvider.provider,
+ },
+ embeddingModel: {
+ name: embeddingModelProvider.name,
+ provider: embeddingModelProvider.provider,
+ },
+ 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 = '';
+
+ 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 (
+
+ {children}
+
+ );
+};
+
+export const useChat = () => {
+ const ctx = useContext(chatContext);
+ return ctx;
+};
From 0b15bfbe3254ebd0d92a4c591789db9f28cb27f9 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Wed, 20 Aug 2025 20:21:06 +0530
Subject: [PATCH 30/33] feat(app): switch to useChat hook
---
src/app/c/[chatId]/page.tsx | 18 +-
src/app/page.tsx | 5 +-
src/components/Chat.tsx | 38 +-
src/components/ChatWindow.tsx | 575 +-----------------
src/components/EmptyChat.tsx | 34 +-
src/components/EmptyChatMessageInput.tsx | 44 +-
src/components/MessageBox.tsx | 11 +-
src/components/MessageInput.tsx | 35 +-
src/components/MessageInputActions/Attach.tsx | 17 +-
.../MessageInputActions/AttachSmall.tsx | 15 +-
src/components/MessageInputActions/Focus.tsx | 11 +-
.../MessageInputActions/Optimization.tsx | 11 +-
src/components/Navbar.tsx | 13 +-
13 files changed, 68 insertions(+), 759 deletions(-)
diff --git a/src/app/c/[chatId]/page.tsx b/src/app/c/[chatId]/page.tsx
index aac125a..672107a 100644
--- a/src/app/c/[chatId]/page.tsx
+++ b/src/app/c/[chatId]/page.tsx
@@ -1,9 +1,17 @@
-import ChatWindow from '@/components/ChatWindow';
-import React from 'react';
+'use client';
-const Page = ({ params }: { params: Promise<{ chatId: string }> }) => {
- const { chatId } = React.use(params);
- return ;
+import ChatWindow from '@/components/ChatWindow';
+import { useParams } from 'next/navigation';
+import React from 'react';
+import { ChatProvider } from '@/lib/hooks/useChat';
+
+const Page = () => {
+ const { chatId }: { chatId: string } = useParams();
+ return (
+
+
+
+ );
};
export default Page;
diff --git a/src/app/page.tsx b/src/app/page.tsx
index e18aca9..25981b5 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,4 +1,5 @@
import ChatWindow from '@/components/ChatWindow';
+import { ChatProvider } from '@/lib/hooks/useChat';
import { Metadata } from 'next';
import { Suspense } from 'react';
@@ -11,7 +12,9 @@ const Home = () => {
return (
-
+
+
+
);
diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx
index 0cf125b..a5d8cf9 100644
--- a/src/components/Chat.tsx
+++ b/src/components/Chat.tsx
@@ -5,28 +5,11 @@ import MessageInput from './MessageInput';
import { File, Message } from './ChatWindow';
import MessageBox from './MessageBox';
import MessageBoxLoading from './MessageBoxLoading';
+import { useChat } from '@/lib/hooks/useChat';
+
+const Chat = () => {
+ const { messages, loading, messageAppeared } = useChat();
-const Chat = ({
- loading,
- messages,
- sendMessage,
- messageAppeared,
- rewrite,
- fileIds,
- setFileIds,
- files,
- setFiles,
-}: {
- messages: Message[];
- sendMessage: (message: string) => void;
- loading: boolean;
- messageAppeared: boolean;
- rewrite: (messageId: string) => void;
- fileIds: string[];
- setFileIds: (fileIds: string[]) => void;
- files: File[];
- setFiles: (files: File[]) => void;
-}) => {
const [dividerWidth, setDividerWidth] = useState(0);
const dividerRef = useRef(null);
const messageEnd = useRef(null);
@@ -72,12 +55,8 @@ const Chat = ({
key={i}
message={msg}
messageIndex={i}
- history={messages}
- loading={loading}
dividerRef={isLast ? dividerRef : undefined}
isLast={isLast}
- rewrite={rewrite}
- sendMessage={sendMessage}
/>
{!isLast && msg.role === 'assistant' && (
@@ -92,14 +71,7 @@ const Chat = ({
className="bottom-24 lg:bottom-10 fixed z-40"
style={{ width: dividerWidth }}
>
-
+
)}