mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-09-15 14:01:31 +00:00
Compare commits
50 Commits
v1.11.0-rc
...
b8e4152e77
Author | SHA1 | Date | |
---|---|---|---|
|
b8e4152e77 | ||
|
c8ac9279bd | ||
|
6f367c34a8 | ||
|
328b12ffbe | ||
|
d8486e90bb | ||
|
238bcaff2b | ||
|
6f7c55b783 | ||
|
83a0cffe1b | ||
|
829ae59944 | ||
|
a546eb18a1 | ||
|
ff1ca56157 | ||
|
30725b5d6d | ||
|
8dc54efbdd | ||
|
72f26b4370 | ||
|
f680188905 | ||
|
0b15bfbe32 | ||
|
8fc7808654 | ||
|
0dc17286b9 | ||
|
3edd7d44dd | ||
|
1132997108 | ||
|
eadbedb713 | ||
|
37cd6d3ab5 | ||
|
88be3a045b | ||
|
45b51ab156 | ||
|
3bee01cfa7 | ||
|
567c6a8758 | ||
|
81a91da743 | ||
|
70a61ee1eb | ||
|
9d89a4413b | ||
|
6ea17d54c6 | ||
|
11a828b073 | ||
|
37022fb11e | ||
|
dd50d4927b | ||
|
fdaf3af3af | ||
|
3f2a8f862c | ||
|
58c7be6e95 | ||
|
829b4e7134 | ||
|
77870b39cc | ||
|
8e0ae9b867 | ||
|
543f1df5ce | ||
|
341aae4587 | ||
|
7f62907385 | ||
|
7c4aa683a2 | ||
|
b48b0eeb0e | ||
|
cddc793915 | ||
|
94e6db10bb | ||
|
65fc881356 | ||
|
26e1d5fec3 | ||
|
66be87b688 | ||
|
f7b4e32218 |
15
README.md
15
README.md
@@ -53,7 +53,7 @@ Want to know more about its architecture and how it works? You can read it [here
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Local LLMs**: You can make use local LLMs such as Llama3 and Mixtral using Ollama.
|
- **Local LLMs**: You can utilize local LLMs such as Qwen, DeepSeek, Llama, and Mistral.
|
||||||
- **Two Main Modes:**
|
- **Two Main Modes:**
|
||||||
- **Copilot Mode:** (In development) Boosts search by generating different queries to find more relevant internet sources. Like normal search instead of just using the context by SearxNG, it visits the top matches and tries to find relevant sources to the user's query directly from the page.
|
- **Copilot Mode:** (In development) Boosts search by generating different queries to find more relevant internet sources. Like normal search instead of just using the context by SearxNG, it visits the top matches and tries to find relevant sources to the user's query directly from the page.
|
||||||
- **Normal Mode:** Processes your query and performs a web search.
|
- **Normal Mode:** Processes your query and performs a web search.
|
||||||
@@ -87,6 +87,7 @@ There are mainly 2 ways of installing Perplexica - With Docker, Without Docker.
|
|||||||
4. Rename the `sample.config.toml` file to `config.toml`. For Docker setups, you need only fill in the following fields:
|
4. Rename the `sample.config.toml` file to `config.toml`. For Docker setups, you need only fill in the following fields:
|
||||||
|
|
||||||
- `OPENAI`: Your OpenAI API key. **You only need to fill this if you wish to use OpenAI's models**.
|
- `OPENAI`: Your OpenAI API key. **You only need to fill this if you wish to use OpenAI's models**.
|
||||||
|
- `CUSTOM_OPENAI`: Your OpenAI-API-compliant local server URL, model name, and API key. You should run your local server with host set to `0.0.0.0`, take note of which port number it is running on, and then use that port number to set `API_URL = http://host.docker.internal:PORT_NUMBER`. You must specify the model name, such as `MODEL_NAME = "unsloth/DeepSeek-R1-0528-Qwen3-8B-GGUF:Q4_K_XL"`. Finally, set `API_KEY` to the appropriate value. If you have not defined an API key, just put anything you want in-between the quotation marks: `API_KEY = "whatever-you-want-but-not-blank"` **You only need to configure these settings if you want to use a local OpenAI-compliant server, such as Llama.cpp's [`llama-server`](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md)**.
|
||||||
- `OLLAMA`: Your Ollama API URL. You should enter it as `http://host.docker.internal:PORT_NUMBER`. If you installed Ollama on port 11434, use `http://host.docker.internal:11434`. For other ports, adjust accordingly. **You need to fill this if you wish to use Ollama's models instead of OpenAI's**.
|
- `OLLAMA`: Your Ollama API URL. You should enter it as `http://host.docker.internal:PORT_NUMBER`. If you installed Ollama on port 11434, use `http://host.docker.internal:11434`. For other ports, adjust accordingly. **You need to fill this if you wish to use Ollama's models instead of OpenAI's**.
|
||||||
- `GROQ`: Your Groq API key. **You only need to fill this if you wish to use Groq's hosted models**.
|
- `GROQ`: Your Groq API key. **You only need to fill this if you wish to use Groq's hosted models**.
|
||||||
- `ANTHROPIC`: Your Anthropic API key. **You only need to fill this if you wish to use Anthropic models**.
|
- `ANTHROPIC`: Your Anthropic API key. **You only need to fill this if you wish to use Anthropic models**.
|
||||||
@@ -120,7 +121,17 @@ There are mainly 2 ways of installing Perplexica - With Docker, Without Docker.
|
|||||||
|
|
||||||
See the [installation documentation](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/installation) for more information like updating, etc.
|
See the [installation documentation](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/installation) for more information like updating, etc.
|
||||||
|
|
||||||
### Ollama Connection Errors
|
### Troubleshooting
|
||||||
|
|
||||||
|
#### Local OpenAI-API-Compliant Servers
|
||||||
|
|
||||||
|
If Perplexica tells you that you haven't configured any chat model providers, ensure that:
|
||||||
|
|
||||||
|
1. Your server is running on `0.0.0.0` (not `127.0.0.1`) and on the same port you put in the API URL.
|
||||||
|
2. You have specified the correct model name loaded by your local LLM server.
|
||||||
|
3. You have specified the correct API key, or if one is not defined, you have put *something* in the API key field and not left it empty.
|
||||||
|
|
||||||
|
#### Ollama Connection Errors
|
||||||
|
|
||||||
If you're encountering an Ollama connection error, it is likely due to the backend being unable to connect to Ollama's API. To fix this issue you can:
|
If you're encountering an Ollama connection error, it is likely due to the backend being unable to connect to Ollama's API. To fix this issue you can:
|
||||||
|
|
||||||
|
14
package.json
14
package.json
@@ -15,11 +15,13 @@
|
|||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
"@icons-pack/react-simple-icons": "^12.3.0",
|
"@icons-pack/react-simple-icons": "^12.3.0",
|
||||||
"@langchain/anthropic": "^0.3.15",
|
"@langchain/anthropic": "^0.3.24",
|
||||||
"@langchain/community": "^0.3.36",
|
"@langchain/community": "^0.3.49",
|
||||||
"@langchain/core": "^0.3.42",
|
"@langchain/core": "^0.3.66",
|
||||||
"@langchain/google-genai": "^0.1.12",
|
"@langchain/google-genai": "^0.2.15",
|
||||||
"@langchain/openai": "^0.0.25",
|
"@langchain/groq": "^0.2.3",
|
||||||
|
"@langchain/ollama": "^0.2.3",
|
||||||
|
"@langchain/openai": "^0.6.2",
|
||||||
"@langchain/textsplitters": "^0.1.0",
|
"@langchain/textsplitters": "^0.1.0",
|
||||||
"@tailwindcss/typography": "^0.5.12",
|
"@tailwindcss/typography": "^0.5.12",
|
||||||
"@xenova/transformers": "^2.17.2",
|
"@xenova/transformers": "^2.17.2",
|
||||||
@@ -31,7 +33,7 @@
|
|||||||
"drizzle-orm": "^0.40.1",
|
"drizzle-orm": "^0.40.1",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"jspdf": "^3.0.1",
|
"jspdf": "^3.0.1",
|
||||||
"langchain": "^0.1.30",
|
"langchain": "^0.3.30",
|
||||||
"lucide-react": "^0.363.0",
|
"lucide-react": "^0.363.0",
|
||||||
"mammoth": "^1.9.1",
|
"mammoth": "^1.9.1",
|
||||||
"markdown-to-jsx": "^7.7.2",
|
"markdown-to-jsx": "^7.7.2",
|
||||||
|
@@ -1,11 +1,7 @@
|
|||||||
import prompts from '@/lib/prompts';
|
|
||||||
import MetaSearchAgent from '@/lib/search/metaSearchAgent';
|
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
|
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
|
||||||
import { EventEmitter } from 'stream';
|
import { EventEmitter } from 'stream';
|
||||||
import {
|
import {
|
||||||
chatModelProviders,
|
|
||||||
embeddingModelProviders,
|
|
||||||
getAvailableChatModelProviders,
|
getAvailableChatModelProviders,
|
||||||
getAvailableEmbeddingModelProviders,
|
getAvailableEmbeddingModelProviders,
|
||||||
} from '@/lib/providers';
|
} from '@/lib/providers';
|
||||||
@@ -138,6 +134,8 @@ const handleHistorySave = async (
|
|||||||
where: eq(chats.id, message.chatId),
|
where: eq(chats.id, message.chatId),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fileData = files.map(getFileDetails);
|
||||||
|
|
||||||
if (!chat) {
|
if (!chat) {
|
||||||
await db
|
await db
|
||||||
.insert(chats)
|
.insert(chats)
|
||||||
@@ -146,9 +144,15 @@ const handleHistorySave = async (
|
|||||||
title: message.content,
|
title: message.content,
|
||||||
createdAt: new Date().toString(),
|
createdAt: new Date().toString(),
|
||||||
focusMode: focusMode,
|
focusMode: focusMode,
|
||||||
files: files.map(getFileDetails),
|
files: fileData,
|
||||||
})
|
})
|
||||||
.execute();
|
.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({
|
const messageExists = await db.query.messages.findFirst({
|
||||||
@@ -223,7 +227,7 @@ export const POST = async (req: Request) => {
|
|||||||
|
|
||||||
if (body.chatModel?.provider === 'custom_openai') {
|
if (body.chatModel?.provider === 'custom_openai') {
|
||||||
llm = new ChatOpenAI({
|
llm = new ChatOpenAI({
|
||||||
openAIApiKey: getCustomOpenaiApiKey(),
|
apiKey: getCustomOpenaiApiKey(),
|
||||||
modelName: getCustomOpenaiModelName(),
|
modelName: getCustomOpenaiModelName(),
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
configuration: {
|
configuration: {
|
||||||
|
@@ -11,6 +11,7 @@ import {
|
|||||||
getAimlApiKey,
|
getAimlApiKey,
|
||||||
getLMStudioApiEndpoint,
|
getLMStudioApiEndpoint,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
|
getOllamaApiKey,
|
||||||
} from '@/lib/config';
|
} from '@/lib/config';
|
||||||
import {
|
import {
|
||||||
getAvailableChatModelProviders,
|
getAvailableChatModelProviders,
|
||||||
@@ -53,6 +54,7 @@ export const GET = async (req: Request) => {
|
|||||||
|
|
||||||
config['openaiApiKey'] = getOpenaiApiKey();
|
config['openaiApiKey'] = getOpenaiApiKey();
|
||||||
config['ollamaApiUrl'] = getOllamaApiEndpoint();
|
config['ollamaApiUrl'] = getOllamaApiEndpoint();
|
||||||
|
config['ollamaApiKey'] = getOllamaApiKey();
|
||||||
config['lmStudioApiUrl'] = getLMStudioApiEndpoint();
|
config['lmStudioApiUrl'] = getLMStudioApiEndpoint();
|
||||||
config['anthropicApiKey'] = getAnthropicApiKey();
|
config['anthropicApiKey'] = getAnthropicApiKey();
|
||||||
config['groqApiKey'] = getGroqApiKey();
|
config['groqApiKey'] = getGroqApiKey();
|
||||||
@@ -93,6 +95,7 @@ export const POST = async (req: Request) => {
|
|||||||
},
|
},
|
||||||
OLLAMA: {
|
OLLAMA: {
|
||||||
API_URL: config.ollamaApiUrl,
|
API_URL: config.ollamaApiUrl,
|
||||||
|
API_KEY: config.ollamaApiKey,
|
||||||
},
|
},
|
||||||
DEEPSEEK: {
|
DEEPSEEK: {
|
||||||
API_KEY: config.deepseekApiKey,
|
API_KEY: config.deepseekApiKey,
|
||||||
|
@@ -1,55 +1,77 @@
|
|||||||
import { searchSearxng } from '@/lib/searxng';
|
import { searchSearxng } from '@/lib/searxng';
|
||||||
|
|
||||||
const articleWebsites = [
|
const websitesForTopic = {
|
||||||
'yahoo.com',
|
tech: {
|
||||||
'www.exchangewire.com',
|
query: ['technology news', 'latest tech', 'AI', 'science and innovation'],
|
||||||
'businessinsider.com',
|
links: ['techcrunch.com', 'wired.com', 'theverge.com'],
|
||||||
/* 'wired.com',
|
},
|
||||||
'mashable.com',
|
finance: {
|
||||||
'theverge.com',
|
query: ['finance news', 'economy', 'stock market', 'investing'],
|
||||||
'gizmodo.com',
|
links: ['bloomberg.com', 'cnbc.com', 'marketwatch.com'],
|
||||||
'cnet.com',
|
},
|
||||||
'venturebeat.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) => {
|
export const GET = async (req: Request) => {
|
||||||
try {
|
try {
|
||||||
const params = new URL(req.url).searchParams;
|
const params = new URL(req.url).searchParams;
|
||||||
|
|
||||||
const mode: 'normal' | 'preview' =
|
const mode: 'normal' | 'preview' =
|
||||||
(params.get('mode') as 'normal' | 'preview') || 'normal';
|
(params.get('mode') as 'normal' | 'preview') || 'normal';
|
||||||
|
const topic: Topic = (params.get('topic') as Topic) || 'tech';
|
||||||
|
|
||||||
|
const selectedTopic = websitesForTopic[topic];
|
||||||
|
|
||||||
let data = [];
|
let data = [];
|
||||||
|
|
||||||
if (mode === 'normal') {
|
if (mode === 'normal') {
|
||||||
|
const seenUrls = new Set();
|
||||||
|
|
||||||
data = (
|
data = (
|
||||||
await Promise.all([
|
await Promise.all(
|
||||||
...new Array(articleWebsites.length * topics.length)
|
selectedTopic.links.flatMap((link) =>
|
||||||
.fill(0)
|
selectedTopic.query.map(async (query) => {
|
||||||
.map(async (_, i) => {
|
|
||||||
return (
|
return (
|
||||||
await searchSearxng(
|
await searchSearxng(`site:${link} ${query}`, {
|
||||||
`site:${articleWebsites[i % articleWebsites.length]} ${
|
engines: ['bing news'],
|
||||||
topics[i % topics.length]
|
pageno: 1,
|
||||||
}`,
|
language: 'en',
|
||||||
{
|
})
|
||||||
engines: ['bing news'],
|
|
||||||
pageno: 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
).results;
|
).results;
|
||||||
}),
|
}),
|
||||||
])
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.map((result) => result)
|
|
||||||
.flat()
|
.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);
|
.sort(() => Math.random() - 0.5);
|
||||||
} else {
|
} else {
|
||||||
data = (
|
data = (
|
||||||
await searchSearxng(
|
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 },
|
{
|
||||||
|
engines: ['bing news'],
|
||||||
|
pageno: 1,
|
||||||
|
language: 'en',
|
||||||
|
},
|
||||||
)
|
)
|
||||||
).results;
|
).results;
|
||||||
}
|
}
|
||||||
|
@@ -49,7 +49,7 @@ export const POST = async (req: Request) => {
|
|||||||
|
|
||||||
if (body.chatModel?.provider === 'custom_openai') {
|
if (body.chatModel?.provider === 'custom_openai') {
|
||||||
llm = new ChatOpenAI({
|
llm = new ChatOpenAI({
|
||||||
openAIApiKey: getCustomOpenaiApiKey(),
|
apiKey: getCustomOpenaiApiKey(),
|
||||||
modelName: getCustomOpenaiModelName(),
|
modelName: getCustomOpenaiModelName(),
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
configuration: {
|
configuration: {
|
||||||
|
@@ -81,8 +81,7 @@ export const POST = async (req: Request) => {
|
|||||||
if (body.chatModel?.provider === 'custom_openai') {
|
if (body.chatModel?.provider === 'custom_openai') {
|
||||||
llm = new ChatOpenAI({
|
llm = new ChatOpenAI({
|
||||||
modelName: body.chatModel?.name || getCustomOpenaiModelName(),
|
modelName: body.chatModel?.name || getCustomOpenaiModelName(),
|
||||||
openAIApiKey:
|
apiKey: body.chatModel?.customOpenAIKey || getCustomOpenaiApiKey(),
|
||||||
body.chatModel?.customOpenAIKey || getCustomOpenaiApiKey(),
|
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
configuration: {
|
configuration: {
|
||||||
baseURL:
|
baseURL:
|
||||||
|
@@ -48,7 +48,7 @@ export const POST = async (req: Request) => {
|
|||||||
|
|
||||||
if (body.chatModel?.provider === 'custom_openai') {
|
if (body.chatModel?.provider === 'custom_openai') {
|
||||||
llm = new ChatOpenAI({
|
llm = new ChatOpenAI({
|
||||||
openAIApiKey: getCustomOpenaiApiKey(),
|
apiKey: getCustomOpenaiApiKey(),
|
||||||
modelName: getCustomOpenaiModelName(),
|
modelName: getCustomOpenaiModelName(),
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
configuration: {
|
configuration: {
|
||||||
|
@@ -49,7 +49,7 @@ export const POST = async (req: Request) => {
|
|||||||
|
|
||||||
if (body.chatModel?.provider === 'custom_openai') {
|
if (body.chatModel?.provider === 'custom_openai') {
|
||||||
llm = new ChatOpenAI({
|
llm = new ChatOpenAI({
|
||||||
openAIApiKey: getCustomOpenaiApiKey(),
|
apiKey: getCustomOpenaiApiKey(),
|
||||||
modelName: getCustomOpenaiModelName(),
|
modelName: getCustomOpenaiModelName(),
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
configuration: {
|
configuration: {
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
export const POST = async (req: Request) => {
|
export const POST = async (req: Request) => {
|
||||||
try {
|
try {
|
||||||
const body: { lat: number; lng: number } = await req.json();
|
const body: {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
measureUnit: 'Imperial' | 'Metric';
|
||||||
|
} = await req.json();
|
||||||
|
|
||||||
if (!body.lat || !body.lng) {
|
if (!body.lat || !body.lng) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
@@ -12,7 +16,9 @@ export const POST = async (req: Request) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`https://api.open-meteo.com/v1/forecast?latitude=${body.lat}&longitude=${body.lng}¤t=weather_code,temperature_2m,is_day,relative_humidity_2m,wind_speed_10m&timezone=auto`,
|
`https://api.open-meteo.com/v1/forecast?latitude=${body.lat}&longitude=${body.lng}¤t=weather_code,temperature_2m,is_day,relative_humidity_2m,wind_speed_10m&timezone=auto${
|
||||||
|
body.measureUnit === 'Metric' ? '' : '&temperature_unit=fahrenheit'
|
||||||
|
}${body.measureUnit === 'Metric' ? '' : '&wind_speed_unit=mph'}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -33,12 +39,16 @@ export const POST = async (req: Request) => {
|
|||||||
humidity: number;
|
humidity: number;
|
||||||
windSpeed: number;
|
windSpeed: number;
|
||||||
icon: string;
|
icon: string;
|
||||||
|
temperatureUnit: 'C' | 'F';
|
||||||
|
windSpeedUnit: 'm/s' | 'mph';
|
||||||
} = {
|
} = {
|
||||||
temperature: data.current.temperature_2m,
|
temperature: data.current.temperature_2m,
|
||||||
condition: '',
|
condition: '',
|
||||||
humidity: data.current.relative_humidity_2m,
|
humidity: data.current.relative_humidity_2m,
|
||||||
windSpeed: data.current.wind_speed_10m,
|
windSpeed: data.current.wind_speed_10m,
|
||||||
icon: '',
|
icon: '',
|
||||||
|
temperatureUnit: body.measureUnit === 'Metric' ? 'C' : 'F',
|
||||||
|
windSpeedUnit: body.measureUnit === 'Metric' ? 'm/s' : 'mph',
|
||||||
};
|
};
|
||||||
|
|
||||||
const code = data.current.weather_code;
|
const code = data.current.weather_code;
|
||||||
|
@@ -1,9 +1,17 @@
|
|||||||
import ChatWindow from '@/components/ChatWindow';
|
'use client';
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
const Page = ({ params }: { params: Promise<{ chatId: string }> }) => {
|
import ChatWindow from '@/components/ChatWindow';
|
||||||
const { chatId } = React.use(params);
|
import { useParams } from 'next/navigation';
|
||||||
return <ChatWindow id={chatId} />;
|
import React from 'react';
|
||||||
|
import { ChatProvider } from '@/lib/hooks/useChat';
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
const { chatId }: { chatId: string } = useParams();
|
||||||
|
return (
|
||||||
|
<ChatProvider id={chatId}>
|
||||||
|
<ChatWindow />
|
||||||
|
</ChatProvider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Page;
|
export default Page;
|
||||||
|
@@ -4,6 +4,7 @@ import { Search } from 'lucide-react';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
interface Discover {
|
interface Discover {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -12,60 +13,66 @@ interface Discover {
|
|||||||
thumbnail: string;
|
thumbnail: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const topics: { key: string; display: string }[] = [
|
||||||
|
{
|
||||||
|
display: 'Tech & Science',
|
||||||
|
key: 'tech',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'Finance',
|
||||||
|
key: 'finance',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'Art & Culture',
|
||||||
|
key: 'art',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'Sports',
|
||||||
|
key: 'sports',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'Entertainment',
|
||||||
|
key: 'entertainment',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const [discover, setDiscover] = useState<Discover[] | null>(null);
|
const [discover, setDiscover] = useState<Discover[] | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [activeTopic, setActiveTopic] = useState<string>(topics[0].key);
|
||||||
|
|
||||||
|
const fetchArticles = async (topic: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/discover?topic=${topic}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(data.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.blogs = data.blogs.filter((blog: Discover) => blog.thumbnail);
|
||||||
|
|
||||||
|
setDiscover(data.blogs);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Error fetching data:', err.message);
|
||||||
|
toast.error('Error fetching data');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
fetchArticles(activeTopic);
|
||||||
try {
|
}, [activeTopic]);
|
||||||
const res = await fetch(`/api/discover`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await res.json();
|
return (
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(data.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
data.blogs = data.blogs.filter((blog: Discover) => blog.thumbnail);
|
|
||||||
|
|
||||||
setDiscover(data.blogs);
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('Error fetching data:', err.message);
|
|
||||||
toast.error('Error fetching data');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return loading ? (
|
|
||||||
<div className="flex flex-row items-center justify-center min-h-screen">
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
className="w-8 h-8 text-light-200 fill-light-secondary dark:text-[#202020] animate-spin dark:fill-[#ffffff3b]"
|
|
||||||
viewBox="0 0 100 101"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M100 50.5908C100.003 78.2051 78.1951 100.003 50.5908 100C22.9765 99.9972 0.997224 78.018 1 50.4037C1.00281 22.7993 22.8108 0.997224 50.4251 1C78.0395 1.00281 100.018 22.8108 100 50.4251ZM9.08164 50.594C9.06312 73.3997 27.7909 92.1272 50.5966 92.1457C73.4023 92.1642 92.1298 73.4365 92.1483 50.6308C92.1669 27.8251 73.4392 9.0973 50.6335 9.07878C27.8278 9.06026 9.10003 27.787 9.08164 50.594Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M93.9676 39.0409C96.393 38.4037 97.8624 35.9116 96.9801 33.5533C95.1945 28.8227 92.871 24.3692 90.0681 20.348C85.6237 14.1775 79.4473 9.36872 72.0454 6.45794C64.6435 3.54717 56.3134 2.65431 48.3133 3.89319C45.869 4.27179 44.3768 6.77534 45.014 9.20079C45.6512 11.6262 48.1343 13.0956 50.5786 12.717C56.5073 11.8281 62.5542 12.5399 68.0406 14.7911C73.527 17.0422 78.2187 20.7487 81.5841 25.4923C83.7976 28.5886 85.4467 32.059 86.4416 35.7474C87.1273 38.1189 89.5423 39.6781 91.9676 39.0409Z"
|
|
||||||
fill="currentFill"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-col pt-4">
|
<div className="flex flex-col pt-4">
|
||||||
@@ -76,35 +83,73 @@ const Page = () => {
|
|||||||
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
|
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-4 pb-28 lg:pb-8 w-full justify-items-center lg:justify-items-start">
|
<div className="flex flex-row items-center space-x-2 overflow-x-auto">
|
||||||
{discover &&
|
{topics.map((t, i) => (
|
||||||
discover?.map((item, i) => (
|
<div
|
||||||
<Link
|
key={i}
|
||||||
href={`/?q=Summary: ${item.url}`}
|
className={cn(
|
||||||
key={i}
|
'border-[0.1px] rounded-full text-sm px-3 py-1 text-nowrap transition duration-200 cursor-pointer',
|
||||||
className="max-w-sm rounded-lg overflow-hidden bg-light-secondary dark:bg-dark-secondary hover:-translate-y-[1px] transition duration-200"
|
activeTopic === t.key
|
||||||
target="_blank"
|
? 'text-cyan-300 bg-cyan-300/30 border-cyan-300/60'
|
||||||
>
|
: 'border-black/30 dark:border-white/30 text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white hover:border-black/40 dark:hover:border-white/40 hover:bg-black/5 dark:hover:bg-white/5',
|
||||||
<img
|
)}
|
||||||
className="object-cover w-full aspect-video"
|
onClick={() => setActiveTopic(t.key)}
|
||||||
src={
|
>
|
||||||
new URL(item.thumbnail).origin +
|
<span>{t.display}</span>
|
||||||
new URL(item.thumbnail).pathname +
|
</div>
|
||||||
`?id=${new URL(item.thumbnail).searchParams.get('id')}`
|
))}
|
||||||
}
|
|
||||||
alt={item.title}
|
|
||||||
/>
|
|
||||||
<div className="px-6 py-4">
|
|
||||||
<div className="font-bold text-lg mb-2">
|
|
||||||
{item.title.slice(0, 100)}...
|
|
||||||
</div>
|
|
||||||
<p className="text-black-70 dark:text-white/70 text-sm">
|
|
||||||
{item.content.slice(0, 100)}...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex flex-row items-center justify-center min-h-screen">
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="w-8 h-8 text-light-200 fill-light-secondary dark:text-[#202020] animate-spin dark:fill-[#ffffff3b]"
|
||||||
|
viewBox="0 0 100 101"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M100 50.5908C100.003 78.2051 78.1951 100.003 50.5908 100C22.9765 99.9972 0.997224 78.018 1 50.4037C1.00281 22.7993 22.8108 0.997224 50.4251 1C78.0395 1.00281 100.018 22.8108 100 50.4251ZM9.08164 50.594C9.06312 73.3997 27.7909 92.1272 50.5966 92.1457C73.4023 92.1642 92.1298 73.4365 92.1483 50.6308C92.1669 27.8251 73.4392 9.0973 50.6335 9.07878C27.8278 9.06026 9.10003 27.787 9.08164 50.594Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M93.9676 39.0409C96.393 38.4037 97.8624 35.9116 96.9801 33.5533C95.1945 28.8227 92.871 24.3692 90.0681 20.348C85.6237 14.1775 79.4473 9.36872 72.0454 6.45794C64.6435 3.54717 56.3134 2.65431 48.3133 3.89319C45.869 4.27179 44.3768 6.77534 45.014 9.20079C45.6512 11.6262 48.1343 13.0956 50.5786 12.717C56.5073 11.8281 62.5542 12.5399 68.0406 14.7911C73.527 17.0422 78.2187 20.7487 81.5841 25.4923C83.7976 28.5886 85.4467 32.059 86.4416 35.7474C87.1273 38.1189 89.5423 39.6781 91.9676 39.0409Z"
|
||||||
|
fill="currentFill"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-4 pb-28 pt-5 lg:pb-8 w-full justify-items-center lg:justify-items-start">
|
||||||
|
{discover &&
|
||||||
|
discover?.map((item, i) => (
|
||||||
|
<Link
|
||||||
|
href={`/?q=Summary: ${item.url}`}
|
||||||
|
key={i}
|
||||||
|
className="max-w-sm rounded-lg overflow-hidden bg-light-secondary dark:bg-dark-secondary hover:-translate-y-[1px] transition duration-200"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="object-cover w-full aspect-video"
|
||||||
|
src={
|
||||||
|
new URL(item.thumbnail).origin +
|
||||||
|
new URL(item.thumbnail).pathname +
|
||||||
|
`?id=${new URL(item.thumbnail).searchParams.get('id')}`
|
||||||
|
}
|
||||||
|
alt={item.title}
|
||||||
|
/>
|
||||||
|
<div className="px-6 py-4">
|
||||||
|
<div className="font-bold text-lg mb-2">
|
||||||
|
{item.title.slice(0, 100)}...
|
||||||
|
</div>
|
||||||
|
<p className="text-black-70 dark:text-white/70 text-sm">
|
||||||
|
{item.content.slice(0, 100)}...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import ChatWindow from '@/components/ChatWindow';
|
import ChatWindow from '@/components/ChatWindow';
|
||||||
|
import { ChatProvider } from '@/lib/hooks/useChat';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
@@ -11,7 +12,9 @@ const Home = () => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<ChatWindow />
|
<ChatProvider>
|
||||||
|
<ChatWindow />
|
||||||
|
</ChatProvider>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -21,6 +21,7 @@ interface SettingsType {
|
|||||||
anthropicApiKey: string;
|
anthropicApiKey: string;
|
||||||
geminiApiKey: string;
|
geminiApiKey: string;
|
||||||
ollamaApiUrl: string;
|
ollamaApiUrl: string;
|
||||||
|
ollamaApiKey: string;
|
||||||
lmStudioApiUrl: string;
|
lmStudioApiUrl: string;
|
||||||
deepseekApiKey: string;
|
deepseekApiKey: string;
|
||||||
aimlApiKey: string;
|
aimlApiKey: string;
|
||||||
@@ -148,6 +149,9 @@ const Page = () => {
|
|||||||
const [automaticImageSearch, setAutomaticImageSearch] = useState(false);
|
const [automaticImageSearch, setAutomaticImageSearch] = useState(false);
|
||||||
const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false);
|
const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false);
|
||||||
const [systemInstructions, setSystemInstructions] = useState<string>('');
|
const [systemInstructions, setSystemInstructions] = useState<string>('');
|
||||||
|
const [measureUnit, setMeasureUnit] = useState<'Imperial' | 'Metric'>(
|
||||||
|
'Metric',
|
||||||
|
);
|
||||||
const [savingStates, setSavingStates] = useState<Record<string, boolean>>({});
|
const [savingStates, setSavingStates] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -210,6 +214,10 @@ const Page = () => {
|
|||||||
|
|
||||||
setSystemInstructions(localStorage.getItem('systemInstructions')!);
|
setSystemInstructions(localStorage.getItem('systemInstructions')!);
|
||||||
|
|
||||||
|
setMeasureUnit(
|
||||||
|
localStorage.getItem('measureUnit')! as 'Imperial' | 'Metric',
|
||||||
|
);
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -368,6 +376,8 @@ const Page = () => {
|
|||||||
localStorage.setItem('embeddingModel', value);
|
localStorage.setItem('embeddingModel', value);
|
||||||
} else if (key === 'systemInstructions') {
|
} else if (key === 'systemInstructions') {
|
||||||
localStorage.setItem('systemInstructions', value);
|
localStorage.setItem('systemInstructions', value);
|
||||||
|
} else if (key === 'measureUnit') {
|
||||||
|
localStorage.setItem('measureUnit', value.toString());
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to save:', err);
|
console.error('Failed to save:', err);
|
||||||
@@ -416,13 +426,35 @@ const Page = () => {
|
|||||||
) : (
|
) : (
|
||||||
config && (
|
config && (
|
||||||
<div className="flex flex-col space-y-6 pb-28 lg:pb-8">
|
<div className="flex flex-col space-y-6 pb-28 lg:pb-8">
|
||||||
<SettingsSection title="Appearance">
|
<SettingsSection title="Preferences">
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
Theme
|
Theme
|
||||||
</p>
|
</p>
|
||||||
<ThemeSwitcher />
|
<ThemeSwitcher />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
|
Measurement Units
|
||||||
|
</p>
|
||||||
|
<Select
|
||||||
|
value={measureUnit ?? undefined}
|
||||||
|
onChange={(e) => {
|
||||||
|
setMeasureUnit(e.target.value as 'Imperial' | 'Metric');
|
||||||
|
saveConfig('measureUnit', e.target.value);
|
||||||
|
}}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: 'Metric',
|
||||||
|
value: 'Metric',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Imperial',
|
||||||
|
value: 'Imperial',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection title="Automatic Search">
|
<SettingsSection title="Automatic Search">
|
||||||
@@ -516,7 +548,7 @@ const Page = () => {
|
|||||||
<SettingsSection title="System Instructions">
|
<SettingsSection title="System Instructions">
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
<Textarea
|
<Textarea
|
||||||
value={systemInstructions}
|
value={systemInstructions ?? undefined}
|
||||||
isSaving={savingStates['systemInstructions']}
|
isSaving={savingStates['systemInstructions']}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSystemInstructions(e.target.value);
|
setSystemInstructions(e.target.value);
|
||||||
@@ -787,6 +819,25 @@ const Page = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
|
Ollama API Key (Can be left blank)
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Ollama API Key"
|
||||||
|
value={config.ollamaApiKey}
|
||||||
|
isSaving={savingStates['ollamaApiKey']}
|
||||||
|
onChange={(e) => {
|
||||||
|
setConfig((prev) => ({
|
||||||
|
...prev!,
|
||||||
|
ollamaApiKey: e.target.value,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
onSave={(value) => saveConfig('ollamaApiKey', value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
GROQ API Key
|
GROQ API Key
|
||||||
|
@@ -5,28 +5,11 @@ import MessageInput from './MessageInput';
|
|||||||
import { File, Message } from './ChatWindow';
|
import { File, Message } from './ChatWindow';
|
||||||
import MessageBox from './MessageBox';
|
import MessageBox from './MessageBox';
|
||||||
import MessageBoxLoading from './MessageBoxLoading';
|
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 [dividerWidth, setDividerWidth] = useState(0);
|
||||||
const dividerRef = useRef<HTMLDivElement | null>(null);
|
const dividerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const messageEnd = useRef<HTMLDivElement | null>(null);
|
const messageEnd = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -72,12 +55,8 @@ const Chat = ({
|
|||||||
key={i}
|
key={i}
|
||||||
message={msg}
|
message={msg}
|
||||||
messageIndex={i}
|
messageIndex={i}
|
||||||
history={messages}
|
|
||||||
loading={loading}
|
|
||||||
dividerRef={isLast ? dividerRef : undefined}
|
dividerRef={isLast ? dividerRef : undefined}
|
||||||
isLast={isLast}
|
isLast={isLast}
|
||||||
rewrite={rewrite}
|
|
||||||
sendMessage={sendMessage}
|
|
||||||
/>
|
/>
|
||||||
{!isLast && msg.role === 'assistant' && (
|
{!isLast && msg.role === 'assistant' && (
|
||||||
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
|
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
|
||||||
@@ -92,14 +71,7 @@ const Chat = ({
|
|||||||
className="bottom-24 lg:bottom-10 fixed z-40"
|
className="bottom-24 lg:bottom-10 fixed z-40"
|
||||||
style={{ width: dividerWidth }}
|
style={{ width: dividerWidth }}
|
||||||
>
|
>
|
||||||
<MessageInput
|
<MessageInput />
|
||||||
loading={loading}
|
|
||||||
sendMessage={sendMessage}
|
|
||||||
fileIds={fileIds}
|
|
||||||
setFileIds={setFileIds}
|
|
||||||
files={files}
|
|
||||||
setFiles={setFiles}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,17 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import { Document } from '@langchain/core/documents';
|
import { Document } from '@langchain/core/documents';
|
||||||
import Navbar from './Navbar';
|
import Navbar from './Navbar';
|
||||||
import Chat from './Chat';
|
import Chat from './Chat';
|
||||||
import EmptyChat from './EmptyChat';
|
import EmptyChat from './EmptyChat';
|
||||||
import crypto from 'crypto';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
|
||||||
import { getSuggestions } from '@/lib/actions';
|
|
||||||
import { Settings } from 'lucide-react';
|
import { Settings } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import NextError from 'next/error';
|
import NextError from 'next/error';
|
||||||
|
import { useChat } from '@/lib/hooks/useChat';
|
||||||
|
|
||||||
export type Message = {
|
export type Message = {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
@@ -29,539 +25,8 @@ export interface File {
|
|||||||
fileId: string;
|
fileId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatModelProvider {
|
const ChatWindow = () => {
|
||||||
name: string;
|
const { hasError, isReady, notFound, messages } = useChat();
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ChatWindow = ({ id }: { id?: string }) => {
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const initialMessage = searchParams.get('q');
|
|
||||||
|
|
||||||
const [chatId, setChatId] = useState<string | undefined>(id);
|
|
||||||
const [newChatCreated, setNewChatCreated] = useState(false);
|
|
||||||
|
|
||||||
const [chatModelProvider, setChatModelProvider] = useState<ChatModelProvider>(
|
|
||||||
{
|
|
||||||
name: '',
|
|
||||||
provider: '',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const [embeddingModelProvider, setEmbeddingModelProvider] =
|
|
||||||
useState<EmbeddingModelProvider>({
|
|
||||||
name: '',
|
|
||||||
provider: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isConfigReady, setIsConfigReady] = useState(false);
|
|
||||||
const [hasError, setHasError] = useState(false);
|
|
||||||
const [isReady, setIsReady] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
checkConfig(
|
|
||||||
setChatModelProvider,
|
|
||||||
setEmbeddingModelProvider,
|
|
||||||
setIsConfigReady,
|
|
||||||
setHasError,
|
|
||||||
);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [messageAppeared, setMessageAppeared] = useState(false);
|
|
||||||
|
|
||||||
const [chatHistory, setChatHistory] = useState<[string, string][]>([]);
|
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
|
||||||
|
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
|
||||||
const [fileIds, setFileIds] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const [focusMode, setFocusMode] = useState('webSearch');
|
|
||||||
const [optimizationMode, setOptimizationMode] = useState('speed');
|
|
||||||
|
|
||||||
const [isMessagesLoaded, setIsMessagesLoaded] = useState(false);
|
|
||||||
|
|
||||||
const [notFound, setNotFound] = useState(false);
|
|
||||||
|
|
||||||
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
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const messagesRef = useRef<Message[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
messagesRef.current = messages;
|
|
||||||
}, [messages]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isMessagesLoaded && isConfigReady) {
|
|
||||||
setIsReady(true);
|
|
||||||
console.debug(new Date(), 'app:ready');
|
|
||||||
} else {
|
|
||||||
setIsReady(false);
|
|
||||||
}
|
|
||||||
}, [isMessagesLoaded, isConfigReady]);
|
|
||||||
|
|
||||||
const sendMessage = async (message: string, messageId?: string) => {
|
|
||||||
if (loading) return;
|
|
||||||
if (!isConfigReady) {
|
|
||||||
toast.error('Cannot send message before the configuration is ready');
|
|
||||||
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 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: 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...');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isReady && initialMessage && isConfigReady) {
|
|
||||||
sendMessage(initialMessage);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [isConfigReady, isReady, initialMessage]);
|
|
||||||
|
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -586,31 +51,11 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
<div>
|
<div>
|
||||||
{messages.length > 0 ? (
|
{messages.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<Navbar chatId={chatId!} messages={messages} />
|
<Navbar />
|
||||||
<Chat
|
<Chat />
|
||||||
loading={loading}
|
|
||||||
messages={messages}
|
|
||||||
sendMessage={sendMessage}
|
|
||||||
messageAppeared={messageAppeared}
|
|
||||||
rewrite={rewrite}
|
|
||||||
fileIds={fileIds}
|
|
||||||
setFileIds={setFileIds}
|
|
||||||
files={files}
|
|
||||||
setFiles={setFiles}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<EmptyChat
|
<EmptyChat />
|
||||||
sendMessage={sendMessage}
|
|
||||||
focusMode={focusMode}
|
|
||||||
setFocusMode={setFocusMode}
|
|
||||||
optimizationMode={optimizationMode}
|
|
||||||
setOptimizationMode={setOptimizationMode}
|
|
||||||
fileIds={fileIds}
|
|
||||||
setFileIds={setFileIds}
|
|
||||||
files={files}
|
|
||||||
setFiles={setFiles}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@@ -5,27 +5,7 @@ import Link from 'next/link';
|
|||||||
import WeatherWidget from './WeatherWidget';
|
import WeatherWidget from './WeatherWidget';
|
||||||
import NewsArticleWidget from './NewsArticleWidget';
|
import NewsArticleWidget from './NewsArticleWidget';
|
||||||
|
|
||||||
const EmptyChat = ({
|
const EmptyChat = () => {
|
||||||
sendMessage,
|
|
||||||
focusMode,
|
|
||||||
setFocusMode,
|
|
||||||
optimizationMode,
|
|
||||||
setOptimizationMode,
|
|
||||||
fileIds,
|
|
||||||
setFileIds,
|
|
||||||
files,
|
|
||||||
setFiles,
|
|
||||||
}: {
|
|
||||||
sendMessage: (message: string) => void;
|
|
||||||
focusMode: string;
|
|
||||||
setFocusMode: (mode: string) => void;
|
|
||||||
optimizationMode: string;
|
|
||||||
setOptimizationMode: (mode: string) => void;
|
|
||||||
fileIds: string[];
|
|
||||||
setFileIds: (fileIds: string[]) => void;
|
|
||||||
files: File[];
|
|
||||||
setFiles: (files: File[]) => void;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute w-full flex flex-row items-center justify-end mr-5 mt-5">
|
<div className="absolute w-full flex flex-row items-center justify-end mr-5 mt-5">
|
||||||
@@ -38,17 +18,7 @@ const EmptyChat = ({
|
|||||||
<h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8">
|
<h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8">
|
||||||
Research begins here.
|
Research begins here.
|
||||||
</h2>
|
</h2>
|
||||||
<EmptyChatMessageInput
|
<EmptyChatMessageInput />
|
||||||
sendMessage={sendMessage}
|
|
||||||
focusMode={focusMode}
|
|
||||||
setFocusMode={setFocusMode}
|
|
||||||
optimizationMode={optimizationMode}
|
|
||||||
setOptimizationMode={setOptimizationMode}
|
|
||||||
fileIds={fileIds}
|
|
||||||
setFileIds={setFileIds}
|
|
||||||
files={files}
|
|
||||||
setFiles={setFiles}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col w-full gap-4 mt-2 sm:flex-row sm:justify-center">
|
<div className="flex flex-col w-full gap-4 mt-2 sm:flex-row sm:justify-center">
|
||||||
<div className="flex-1 w-full">
|
<div className="flex-1 w-full">
|
||||||
|
@@ -1,34 +1,15 @@
|
|||||||
import { ArrowRight } from 'lucide-react';
|
import { ArrowRight } from 'lucide-react';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
import CopilotToggle from './MessageInputActions/Copilot';
|
|
||||||
import Focus from './MessageInputActions/Focus';
|
import Focus from './MessageInputActions/Focus';
|
||||||
import Optimization from './MessageInputActions/Optimization';
|
import Optimization from './MessageInputActions/Optimization';
|
||||||
import Attach from './MessageInputActions/Attach';
|
import Attach from './MessageInputActions/Attach';
|
||||||
import { File } from './ChatWindow';
|
import { useChat } from '@/lib/hooks/useChat';
|
||||||
|
|
||||||
const EmptyChatMessageInput = ({
|
const EmptyChatMessageInput = () => {
|
||||||
sendMessage,
|
const { sendMessage } = useChat();
|
||||||
focusMode,
|
|
||||||
setFocusMode,
|
/* const [copilotEnabled, setCopilotEnabled] = useState(false); */
|
||||||
optimizationMode,
|
|
||||||
setOptimizationMode,
|
|
||||||
fileIds,
|
|
||||||
setFileIds,
|
|
||||||
files,
|
|
||||||
setFiles,
|
|
||||||
}: {
|
|
||||||
sendMessage: (message: string) => void;
|
|
||||||
focusMode: string;
|
|
||||||
setFocusMode: (mode: string) => void;
|
|
||||||
optimizationMode: string;
|
|
||||||
setOptimizationMode: (mode: string) => void;
|
|
||||||
fileIds: string[];
|
|
||||||
setFileIds: (fileIds: string[]) => void;
|
|
||||||
files: File[];
|
|
||||||
setFiles: (files: File[]) => void;
|
|
||||||
}) => {
|
|
||||||
const [copilotEnabled, setCopilotEnabled] = useState(false);
|
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
|
|
||||||
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
@@ -84,20 +65,11 @@ const EmptyChatMessageInput = ({
|
|||||||
/>
|
/>
|
||||||
<div className="flex flex-row items-center justify-between mt-4">
|
<div className="flex flex-row items-center justify-between mt-4">
|
||||||
<div className="flex flex-row items-center space-x-2 lg:space-x-4">
|
<div className="flex flex-row items-center space-x-2 lg:space-x-4">
|
||||||
<Focus focusMode={focusMode} setFocusMode={setFocusMode} />
|
<Focus />
|
||||||
<Attach
|
<Attach showText />
|
||||||
fileIds={fileIds}
|
|
||||||
setFileIds={setFileIds}
|
|
||||||
files={files}
|
|
||||||
setFiles={setFiles}
|
|
||||||
showText
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center space-x-1 sm:space-x-4">
|
<div className="flex flex-row items-center space-x-1 sm:space-x-4">
|
||||||
<Optimization
|
<Optimization />
|
||||||
optimizationMode={optimizationMode}
|
|
||||||
setOptimizationMode={setOptimizationMode}
|
|
||||||
/>
|
|
||||||
<button
|
<button
|
||||||
disabled={message.trim().length === 0}
|
disabled={message.trim().length === 0}
|
||||||
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 disabled:bg-[#e0e0dc] dark:disabled:bg-[#ececec21] hover:bg-opacity-85 transition duration-100 rounded-full p-2"
|
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 disabled:bg-[#e0e0dc] dark:disabled:bg-[#ececec21] hover:bg-opacity-85 transition duration-100 rounded-full p-2"
|
||||||
|
@@ -20,32 +20,36 @@ import SearchImages from './SearchImages';
|
|||||||
import SearchVideos from './SearchVideos';
|
import SearchVideos from './SearchVideos';
|
||||||
import { useSpeech } from 'react-text-to-speech';
|
import { useSpeech } from 'react-text-to-speech';
|
||||||
import ThinkBox from './ThinkBox';
|
import ThinkBox from './ThinkBox';
|
||||||
|
import { useChat } from '@/lib/hooks/useChat';
|
||||||
|
|
||||||
const ThinkTagProcessor = ({ children }: { children: React.ReactNode }) => {
|
const ThinkTagProcessor = ({
|
||||||
return <ThinkBox content={children as string} />;
|
children,
|
||||||
|
thinkingEnded,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
thinkingEnded: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ThinkBox content={children as string} thinkingEnded={thinkingEnded} />
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MessageBox = ({
|
const MessageBox = ({
|
||||||
message,
|
message,
|
||||||
messageIndex,
|
messageIndex,
|
||||||
history,
|
|
||||||
loading,
|
|
||||||
dividerRef,
|
dividerRef,
|
||||||
isLast,
|
isLast,
|
||||||
rewrite,
|
|
||||||
sendMessage,
|
|
||||||
}: {
|
}: {
|
||||||
message: Message;
|
message: Message;
|
||||||
messageIndex: number;
|
messageIndex: number;
|
||||||
history: Message[];
|
|
||||||
loading: boolean;
|
|
||||||
dividerRef?: MutableRefObject<HTMLDivElement | null>;
|
dividerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
isLast: boolean;
|
isLast: boolean;
|
||||||
rewrite: (messageId: string) => void;
|
|
||||||
sendMessage: (message: string) => void;
|
|
||||||
}) => {
|
}) => {
|
||||||
|
const { loading, messages: history, sendMessage, rewrite } = useChat();
|
||||||
|
|
||||||
const [parsedMessage, setParsedMessage] = useState(message.content);
|
const [parsedMessage, setParsedMessage] = useState(message.content);
|
||||||
const [speechMessage, setSpeechMessage] = useState(message.content);
|
const [speechMessage, setSpeechMessage] = useState(message.content);
|
||||||
|
const [thinkingEnded, setThinkingEnded] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const citationRegex = /\[([^\]]+)\]/g;
|
const citationRegex = /\[([^\]]+)\]/g;
|
||||||
@@ -61,6 +65,10 @@ const MessageBox = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.role === 'assistant' && message.content.includes('</think>')) {
|
||||||
|
setThinkingEnded(true);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
message.role === 'assistant' &&
|
message.role === 'assistant' &&
|
||||||
message?.sources &&
|
message?.sources &&
|
||||||
@@ -88,7 +96,7 @@ const MessageBox = ({
|
|||||||
if (url) {
|
if (url) {
|
||||||
return `<a href="${url}" target="_blank" className="bg-light-secondary dark:bg-dark-secondary px-1 rounded ml-1 no-underline text-xs text-black/70 dark:text-white/70 relative">${numStr}</a>`;
|
return `<a href="${url}" target="_blank" className="bg-light-secondary dark:bg-dark-secondary px-1 rounded ml-1 no-underline text-xs text-black/70 dark:text-white/70 relative">${numStr}</a>`;
|
||||||
} else {
|
} else {
|
||||||
return `[${numStr}]`;
|
return ``;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.join('');
|
.join('');
|
||||||
@@ -99,6 +107,14 @@ const MessageBox = ({
|
|||||||
);
|
);
|
||||||
setSpeechMessage(message.content.replace(regex, ''));
|
setSpeechMessage(message.content.replace(regex, ''));
|
||||||
return;
|
return;
|
||||||
|
} else if (
|
||||||
|
message.role === 'assistant' &&
|
||||||
|
message?.sources &&
|
||||||
|
message.sources.length === 0
|
||||||
|
) {
|
||||||
|
setParsedMessage(processedMessage.replace(regex, ''));
|
||||||
|
setSpeechMessage(message.content.replace(regex, ''));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSpeechMessage(message.content.replace(regex, ''));
|
setSpeechMessage(message.content.replace(regex, ''));
|
||||||
@@ -111,6 +127,9 @@ const MessageBox = ({
|
|||||||
overrides: {
|
overrides: {
|
||||||
think: {
|
think: {
|
||||||
component: ThinkTagProcessor,
|
component: ThinkTagProcessor,
|
||||||
|
props: {
|
||||||
|
thinkingEnded: thinkingEnded,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@@ -6,22 +6,11 @@ import Attach from './MessageInputActions/Attach';
|
|||||||
import CopilotToggle from './MessageInputActions/Copilot';
|
import CopilotToggle from './MessageInputActions/Copilot';
|
||||||
import { File } from './ChatWindow';
|
import { File } from './ChatWindow';
|
||||||
import AttachSmall from './MessageInputActions/AttachSmall';
|
import AttachSmall from './MessageInputActions/AttachSmall';
|
||||||
|
import { useChat } from '@/lib/hooks/useChat';
|
||||||
|
|
||||||
|
const MessageInput = () => {
|
||||||
|
const { loading, sendMessage } = useChat();
|
||||||
|
|
||||||
const MessageInput = ({
|
|
||||||
sendMessage,
|
|
||||||
loading,
|
|
||||||
fileIds,
|
|
||||||
setFileIds,
|
|
||||||
files,
|
|
||||||
setFiles,
|
|
||||||
}: {
|
|
||||||
sendMessage: (message: string) => void;
|
|
||||||
loading: boolean;
|
|
||||||
fileIds: string[];
|
|
||||||
setFileIds: (fileIds: string[]) => void;
|
|
||||||
files: File[];
|
|
||||||
setFiles: (files: File[]) => void;
|
|
||||||
}) => {
|
|
||||||
const [copilotEnabled, setCopilotEnabled] = useState(false);
|
const [copilotEnabled, setCopilotEnabled] = useState(false);
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const [textareaRows, setTextareaRows] = useState(1);
|
const [textareaRows, setTextareaRows] = useState(1);
|
||||||
@@ -79,14 +68,7 @@ const MessageInput = ({
|
|||||||
mode === 'multi' ? 'flex-col rounded-lg' : 'flex-row rounded-full',
|
mode === 'multi' ? 'flex-col rounded-lg' : 'flex-row rounded-full',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{mode === 'single' && (
|
{mode === 'single' && <AttachSmall />}
|
||||||
<AttachSmall
|
|
||||||
fileIds={fileIds}
|
|
||||||
setFileIds={setFileIds}
|
|
||||||
files={files}
|
|
||||||
setFiles={setFiles}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<TextareaAutosize
|
<TextareaAutosize
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={message}
|
value={message}
|
||||||
@@ -113,12 +95,7 @@ const MessageInput = ({
|
|||||||
)}
|
)}
|
||||||
{mode === 'multi' && (
|
{mode === 'multi' && (
|
||||||
<div className="flex flex-row items-center justify-between w-full pt-2">
|
<div className="flex flex-row items-center justify-between w-full pt-2">
|
||||||
<AttachSmall
|
<AttachSmall />
|
||||||
fileIds={fileIds}
|
|
||||||
setFileIds={setFileIds}
|
|
||||||
files={files}
|
|
||||||
setFiles={setFiles}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-row items-center space-x-4">
|
<div className="flex flex-row items-center space-x-4">
|
||||||
<CopilotToggle
|
<CopilotToggle
|
||||||
copilotEnabled={copilotEnabled}
|
copilotEnabled={copilotEnabled}
|
||||||
|
@@ -7,21 +7,11 @@ import {
|
|||||||
} from '@headlessui/react';
|
} from '@headlessui/react';
|
||||||
import { CopyPlus, File, LoaderCircle, Plus, Trash } from 'lucide-react';
|
import { CopyPlus, File, LoaderCircle, Plus, Trash } from 'lucide-react';
|
||||||
import { Fragment, useRef, useState } from 'react';
|
import { Fragment, useRef, useState } from 'react';
|
||||||
import { File as FileType } from '../ChatWindow';
|
import { useChat } from '@/lib/hooks/useChat';
|
||||||
|
|
||||||
|
const Attach = ({ showText }: { showText?: boolean }) => {
|
||||||
|
const { files, setFiles, setFileIds, fileIds } = useChat();
|
||||||
|
|
||||||
const Attach = ({
|
|
||||||
fileIds,
|
|
||||||
setFileIds,
|
|
||||||
showText,
|
|
||||||
files,
|
|
||||||
setFiles,
|
|
||||||
}: {
|
|
||||||
fileIds: string[];
|
|
||||||
setFileIds: (fileIds: string[]) => void;
|
|
||||||
showText?: boolean;
|
|
||||||
files: FileType[];
|
|
||||||
setFiles: (files: FileType[]) => void;
|
|
||||||
}) => {
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const fileInputRef = useRef<any>();
|
const fileInputRef = useRef<any>();
|
||||||
|
|
||||||
@@ -142,8 +132,8 @@ const Attach = ({
|
|||||||
key={i}
|
key={i}
|
||||||
className="flex flex-row items-center justify-start w-full space-x-3 p-3"
|
className="flex flex-row items-center justify-start w-full space-x-3 p-3"
|
||||||
>
|
>
|
||||||
<div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md">
|
<div className="bg-light-100 dark:bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md">
|
||||||
<File size={16} className="text-white/70" />
|
<File size={16} className="text-black/70 dark:text-white/70" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
{file.fileName.length > 25
|
{file.fileName.length > 25
|
||||||
|
@@ -8,18 +8,11 @@ import {
|
|||||||
import { CopyPlus, File, LoaderCircle, Plus, Trash } from 'lucide-react';
|
import { CopyPlus, File, LoaderCircle, Plus, Trash } from 'lucide-react';
|
||||||
import { Fragment, useRef, useState } from 'react';
|
import { Fragment, useRef, useState } from 'react';
|
||||||
import { File as FileType } from '../ChatWindow';
|
import { File as FileType } from '../ChatWindow';
|
||||||
|
import { useChat } from '@/lib/hooks/useChat';
|
||||||
|
|
||||||
|
const AttachSmall = () => {
|
||||||
|
const { files, setFiles, setFileIds, fileIds } = useChat();
|
||||||
|
|
||||||
const AttachSmall = ({
|
|
||||||
fileIds,
|
|
||||||
setFileIds,
|
|
||||||
files,
|
|
||||||
setFiles,
|
|
||||||
}: {
|
|
||||||
fileIds: string[];
|
|
||||||
setFileIds: (fileIds: string[]) => void;
|
|
||||||
files: FileType[];
|
|
||||||
setFiles: (files: FileType[]) => void;
|
|
||||||
}) => {
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const fileInputRef = useRef<any>();
|
const fileInputRef = useRef<any>();
|
||||||
|
|
||||||
@@ -114,8 +107,8 @@ const AttachSmall = ({
|
|||||||
key={i}
|
key={i}
|
||||||
className="flex flex-row items-center justify-start w-full space-x-3 p-3"
|
className="flex flex-row items-center justify-start w-full space-x-3 p-3"
|
||||||
>
|
>
|
||||||
<div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md">
|
<div className="bg-light-100 dark:bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md">
|
||||||
<File size={16} className="text-white/70" />
|
<File size={16} className="text-black/70 dark:text-white/70" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
{file.fileName.length > 25
|
{file.fileName.length > 25
|
||||||
|
@@ -15,6 +15,7 @@ import {
|
|||||||
} from '@headlessui/react';
|
} from '@headlessui/react';
|
||||||
import { SiReddit, SiYoutube } from '@icons-pack/react-simple-icons';
|
import { SiReddit, SiYoutube } from '@icons-pack/react-simple-icons';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
|
import { useChat } from '@/lib/hooks/useChat';
|
||||||
|
|
||||||
const focusModes = [
|
const focusModes = [
|
||||||
{
|
{
|
||||||
@@ -55,13 +56,9 @@ const focusModes = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const Focus = ({
|
const Focus = () => {
|
||||||
focusMode,
|
const { focusMode, setFocusMode } = useChat();
|
||||||
setFocusMode,
|
|
||||||
}: {
|
|
||||||
focusMode: string;
|
|
||||||
setFocusMode: (mode: string) => void;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg mt-[6.5px]">
|
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg mt-[6.5px]">
|
||||||
<PopoverButton
|
<PopoverButton
|
||||||
|
@@ -7,6 +7,7 @@ import {
|
|||||||
Transition,
|
Transition,
|
||||||
} from '@headlessui/react';
|
} from '@headlessui/react';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
|
import { useChat } from '@/lib/hooks/useChat';
|
||||||
|
|
||||||
const OptimizationModes = [
|
const OptimizationModes = [
|
||||||
{
|
{
|
||||||
@@ -34,13 +35,9 @@ const OptimizationModes = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const Optimization = ({
|
const Optimization = () => {
|
||||||
optimizationMode,
|
const { optimizationMode, setOptimizationMode } = useChat();
|
||||||
setOptimizationMode,
|
|
||||||
}: {
|
|
||||||
optimizationMode: string;
|
|
||||||
setOptimizationMode: (mode: string) => void;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
|
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
|
||||||
<PopoverButton
|
<PopoverButton
|
||||||
|
@@ -10,6 +10,7 @@ import {
|
|||||||
Transition,
|
Transition,
|
||||||
} from '@headlessui/react';
|
} from '@headlessui/react';
|
||||||
import jsPDF from 'jspdf';
|
import jsPDF from 'jspdf';
|
||||||
|
import { useChat } from '@/lib/hooks/useChat';
|
||||||
|
|
||||||
const downloadFile = (filename: string, content: string, type: string) => {
|
const downloadFile = (filename: string, content: string, type: string) => {
|
||||||
const blob = new Blob([content], { type });
|
const blob = new Blob([content], { type });
|
||||||
@@ -118,16 +119,12 @@ const exportAsPDF = (messages: Message[], title: string) => {
|
|||||||
doc.save(`${title || 'chat'}.pdf`);
|
doc.save(`${title || 'chat'}.pdf`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Navbar = ({
|
const Navbar = () => {
|
||||||
chatId,
|
|
||||||
messages,
|
|
||||||
}: {
|
|
||||||
messages: Message[];
|
|
||||||
chatId: string;
|
|
||||||
}) => {
|
|
||||||
const [title, setTitle] = useState<string>('');
|
const [title, setTitle] = useState<string>('');
|
||||||
const [timeAgo, setTimeAgo] = useState<string>('');
|
const [timeAgo, setTimeAgo] = useState<string>('');
|
||||||
|
|
||||||
|
const { messages, chatId } = useChat();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messages.length > 0) {
|
if (messages.length > 0) {
|
||||||
const newTitle =
|
const newTitle =
|
||||||
@@ -206,7 +203,7 @@ const Navbar = ({
|
|||||||
</PopoverPanel>
|
</PopoverPanel>
|
||||||
</Transition>
|
</Transition>
|
||||||
</Popover>
|
</Popover>
|
||||||
<DeleteChat redirect chatId={chatId} chats={[]} setChats={() => {}} />
|
<DeleteChat redirect chatId={chatId!} chats={[]} setChats={() => {}} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -1,15 +1,23 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { ChevronDown, ChevronUp, BrainCircuit } from 'lucide-react';
|
import { ChevronDown, ChevronUp, BrainCircuit } from 'lucide-react';
|
||||||
|
|
||||||
interface ThinkBoxProps {
|
interface ThinkBoxProps {
|
||||||
content: string;
|
content: string;
|
||||||
|
thinkingEnded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThinkBox = ({ content }: ThinkBoxProps) => {
|
const ThinkBox = ({ content, thinkingEnded }: ThinkBoxProps) => {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (thinkingEnded) {
|
||||||
|
setIsExpanded(false);
|
||||||
|
} else {
|
||||||
|
setIsExpanded(true);
|
||||||
|
}
|
||||||
|
}, [thinkingEnded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="my-4 bg-light-secondary/50 dark:bg-dark-secondary/50 rounded-xl border border-light-200 dark:border-dark-200 overflow-hidden">
|
<div className="my-4 bg-light-secondary/50 dark:bg-dark-secondary/50 rounded-xl border border-light-200 dark:border-dark-200 overflow-hidden">
|
||||||
|
@@ -9,7 +9,10 @@ const WeatherWidget = () => {
|
|||||||
humidity: 0,
|
humidity: 0,
|
||||||
windSpeed: 0,
|
windSpeed: 0,
|
||||||
icon: '',
|
icon: '',
|
||||||
|
temperatureUnit: 'C',
|
||||||
|
windSpeedUnit: 'm/s',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -73,6 +76,7 @@ const WeatherWidget = () => {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
lat: location.latitude,
|
lat: location.latitude,
|
||||||
lng: location.longitude,
|
lng: location.longitude,
|
||||||
|
measureUnit: localStorage.getItem('measureUnit') ?? 'Metric',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,6 +95,8 @@ const WeatherWidget = () => {
|
|||||||
humidity: data.humidity,
|
humidity: data.humidity,
|
||||||
windSpeed: data.windSpeed,
|
windSpeed: data.windSpeed,
|
||||||
icon: data.icon,
|
icon: data.icon,
|
||||||
|
temperatureUnit: data.temperatureUnit,
|
||||||
|
windSpeedUnit: data.windSpeedUnit,
|
||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
@@ -125,7 +131,7 @@ const WeatherWidget = () => {
|
|||||||
className="h-10 w-auto"
|
className="h-10 w-auto"
|
||||||
/>
|
/>
|
||||||
<span className="text-base font-semibold text-black dark:text-white">
|
<span className="text-base font-semibold text-black dark:text-white">
|
||||||
{data.temperature}°C
|
{data.temperature}°{data.temperatureUnit}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col justify-between flex-1 h-full py-1">
|
<div className="flex flex-col justify-between flex-1 h-full py-1">
|
||||||
@@ -135,7 +141,7 @@ const WeatherWidget = () => {
|
|||||||
</span>
|
</span>
|
||||||
<span className="flex items-center text-xs text-black/60 dark:text-white/60">
|
<span className="flex items-center text-xs text-black/60 dark:text-white/60">
|
||||||
<Wind className="w-3 h-3 mr-1" />
|
<Wind className="w-3 h-3 mr-1" />
|
||||||
{data.windSpeed} km/h
|
{data.windSpeed} {data.windSpeedUnit}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-black/60 dark:text-white/60 mt-1">
|
<span className="text-xs text-black/60 dark:text-white/60 mt-1">
|
||||||
|
@@ -3,32 +3,18 @@ import {
|
|||||||
RunnableMap,
|
RunnableMap,
|
||||||
RunnableLambda,
|
RunnableLambda,
|
||||||
} from '@langchain/core/runnables';
|
} from '@langchain/core/runnables';
|
||||||
import { PromptTemplate } from '@langchain/core/prompts';
|
import { ChatPromptTemplate } from '@langchain/core/prompts';
|
||||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
import formatChatHistoryAsString from '../utils/formatHistory';
|
||||||
import { BaseMessage } from '@langchain/core/messages';
|
import { BaseMessage } from '@langchain/core/messages';
|
||||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
import { StringOutputParser } from '@langchain/core/output_parsers';
|
||||||
import { searchSearxng } from '../searxng';
|
import { searchSearxng } from '../searxng';
|
||||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
import LineOutputParser from '../outputParsers/lineOutputParser';
|
||||||
|
|
||||||
const imageSearchChainPrompt = `
|
const imageSearchChainPrompt = `
|
||||||
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search the web for images.
|
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search the web for images.
|
||||||
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation.
|
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation.
|
||||||
|
Output only the rephrased query wrapped in an XML <query> element. Do not include any explanation or additional text.
|
||||||
Example:
|
|
||||||
1. Follow up question: What is a cat?
|
|
||||||
Rephrased: A cat
|
|
||||||
|
|
||||||
2. Follow up question: What is a car? How does it works?
|
|
||||||
Rephrased: Car working
|
|
||||||
|
|
||||||
3. Follow up question: How does an AC work?
|
|
||||||
Rephrased: AC working
|
|
||||||
|
|
||||||
Conversation:
|
|
||||||
{chat_history}
|
|
||||||
|
|
||||||
Follow up question: {query}
|
|
||||||
Rephrased question:
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type ImageSearchChainInput = {
|
type ImageSearchChainInput = {
|
||||||
@@ -54,12 +40,39 @@ const createImageSearchChain = (llm: BaseChatModel) => {
|
|||||||
return input.query;
|
return input.query;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
PromptTemplate.fromTemplate(imageSearchChainPrompt),
|
ChatPromptTemplate.fromMessages([
|
||||||
|
['system', imageSearchChainPrompt],
|
||||||
|
[
|
||||||
|
'user',
|
||||||
|
'<conversation>\n</conversation>\n<follow_up>\nWhat is a cat?\n</follow_up>',
|
||||||
|
],
|
||||||
|
['assistant', '<query>A cat</query>'],
|
||||||
|
|
||||||
|
[
|
||||||
|
'user',
|
||||||
|
'<conversation>\n</conversation>\n<follow_up>\nWhat is a car? How does it work?\n</follow_up>',
|
||||||
|
],
|
||||||
|
['assistant', '<query>Car working</query>'],
|
||||||
|
[
|
||||||
|
'user',
|
||||||
|
'<conversation>\n</conversation>\n<follow_up>\nHow does an AC work?\n</follow_up>',
|
||||||
|
],
|
||||||
|
['assistant', '<query>AC working</query>'],
|
||||||
|
[
|
||||||
|
'user',
|
||||||
|
'<conversation>{chat_history}</conversation>\n<follow_up>\n{query}\n</follow_up>',
|
||||||
|
],
|
||||||
|
]),
|
||||||
llm,
|
llm,
|
||||||
strParser,
|
strParser,
|
||||||
RunnableLambda.from(async (input: string) => {
|
RunnableLambda.from(async (input: string) => {
|
||||||
input = input.replace(/<think>.*?<\/think>/g, '');
|
const queryParser = new LineOutputParser({
|
||||||
|
key: 'query',
|
||||||
|
});
|
||||||
|
|
||||||
|
return await queryParser.parse(input);
|
||||||
|
}),
|
||||||
|
RunnableLambda.from(async (input: string) => {
|
||||||
const res = await searchSearxng(input, {
|
const res = await searchSearxng(input, {
|
||||||
engines: ['bing images', 'google images'],
|
engines: ['bing images', 'google images'],
|
||||||
});
|
});
|
||||||
|
@@ -3,33 +3,19 @@ import {
|
|||||||
RunnableMap,
|
RunnableMap,
|
||||||
RunnableLambda,
|
RunnableLambda,
|
||||||
} from '@langchain/core/runnables';
|
} from '@langchain/core/runnables';
|
||||||
import { PromptTemplate } from '@langchain/core/prompts';
|
import { ChatPromptTemplate } from '@langchain/core/prompts';
|
||||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
import formatChatHistoryAsString from '../utils/formatHistory';
|
||||||
import { BaseMessage } from '@langchain/core/messages';
|
import { BaseMessage } from '@langchain/core/messages';
|
||||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
import { StringOutputParser } from '@langchain/core/output_parsers';
|
||||||
import { searchSearxng } from '../searxng';
|
import { searchSearxng } from '../searxng';
|
||||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
import LineOutputParser from '../outputParsers/lineOutputParser';
|
||||||
|
|
||||||
const VideoSearchChainPrompt = `
|
const videoSearchChainPrompt = `
|
||||||
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search Youtube for videos.
|
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search Youtube for videos.
|
||||||
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation.
|
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation.
|
||||||
|
Output only the rephrased query wrapped in an XML <query> element. Do not include any explanation or additional text.
|
||||||
Example:
|
`;
|
||||||
1. Follow up question: How does a car work?
|
|
||||||
Rephrased: How does a car work?
|
|
||||||
|
|
||||||
2. Follow up question: What is the theory of relativity?
|
|
||||||
Rephrased: What is theory of relativity
|
|
||||||
|
|
||||||
3. Follow up question: How does an AC work?
|
|
||||||
Rephrased: How does an AC work
|
|
||||||
|
|
||||||
Conversation:
|
|
||||||
{chat_history}
|
|
||||||
|
|
||||||
Follow up question: {query}
|
|
||||||
Rephrased question:
|
|
||||||
`;
|
|
||||||
|
|
||||||
type VideoSearchChainInput = {
|
type VideoSearchChainInput = {
|
||||||
chat_history: BaseMessage[];
|
chat_history: BaseMessage[];
|
||||||
@@ -55,12 +41,37 @@ const createVideoSearchChain = (llm: BaseChatModel) => {
|
|||||||
return input.query;
|
return input.query;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
PromptTemplate.fromTemplate(VideoSearchChainPrompt),
|
ChatPromptTemplate.fromMessages([
|
||||||
|
['system', videoSearchChainPrompt],
|
||||||
|
[
|
||||||
|
'user',
|
||||||
|
'<conversation>\n</conversation>\n<follow_up>\nHow does a car work?\n</follow_up>',
|
||||||
|
],
|
||||||
|
['assistant', '<query>How does a car work?</query>'],
|
||||||
|
[
|
||||||
|
'user',
|
||||||
|
'<conversation>\n</conversation>\n<follow_up>\nWhat is the theory of relativity?\n</follow_up>',
|
||||||
|
],
|
||||||
|
['assistant', '<query>Theory of relativity</query>'],
|
||||||
|
[
|
||||||
|
'user',
|
||||||
|
'<conversation>\n</conversation>\n<follow_up>\nHow does an AC work?\n</follow_up>',
|
||||||
|
],
|
||||||
|
['assistant', '<query>AC working</query>'],
|
||||||
|
[
|
||||||
|
'user',
|
||||||
|
'<conversation>{chat_history}</conversation>\n<follow_up>\n{query}\n</follow_up>',
|
||||||
|
],
|
||||||
|
]),
|
||||||
llm,
|
llm,
|
||||||
strParser,
|
strParser,
|
||||||
RunnableLambda.from(async (input: string) => {
|
RunnableLambda.from(async (input: string) => {
|
||||||
input = input.replace(/<think>.*?<\/think>/g, '');
|
const queryParser = new LineOutputParser({
|
||||||
|
key: 'query',
|
||||||
|
});
|
||||||
|
return await queryParser.parse(input);
|
||||||
|
}),
|
||||||
|
RunnableLambda.from(async (input: string) => {
|
||||||
const res = await searchSearxng(input, {
|
const res = await searchSearxng(input, {
|
||||||
engines: ['youtube'],
|
engines: ['youtube'],
|
||||||
});
|
});
|
||||||
@@ -92,8 +103,8 @@ const handleVideoSearch = (
|
|||||||
input: VideoSearchChainInput,
|
input: VideoSearchChainInput,
|
||||||
llm: BaseChatModel,
|
llm: BaseChatModel,
|
||||||
) => {
|
) => {
|
||||||
const VideoSearchChain = createVideoSearchChain(llm);
|
const videoSearchChain = createVideoSearchChain(llm);
|
||||||
return VideoSearchChain.invoke(input);
|
return videoSearchChain.invoke(input);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handleVideoSearch;
|
export default handleVideoSearch;
|
||||||
|
@@ -31,6 +31,7 @@ interface Config {
|
|||||||
};
|
};
|
||||||
OLLAMA: {
|
OLLAMA: {
|
||||||
API_URL: string;
|
API_URL: string;
|
||||||
|
API_KEY: string;
|
||||||
};
|
};
|
||||||
DEEPSEEK: {
|
DEEPSEEK: {
|
||||||
API_KEY: string;
|
API_KEY: string;
|
||||||
@@ -86,6 +87,8 @@ export const getSearxngApiEndpoint = () =>
|
|||||||
|
|
||||||
export const getOllamaApiEndpoint = () => loadConfig().MODELS.OLLAMA.API_URL;
|
export const getOllamaApiEndpoint = () => loadConfig().MODELS.OLLAMA.API_URL;
|
||||||
|
|
||||||
|
export const getOllamaApiKey = () => loadConfig().MODELS.OLLAMA.API_KEY;
|
||||||
|
|
||||||
export const getDeepseekApiKey = () => loadConfig().MODELS.DEEPSEEK.API_KEY;
|
export const getDeepseekApiKey = () => loadConfig().MODELS.DEEPSEEK.API_KEY;
|
||||||
|
|
||||||
export const getAimlApiKey = () => loadConfig().MODELS.AIMLAPI.API_KEY;
|
export const getAimlApiKey = () => loadConfig().MODELS.AIMLAPI.API_KEY;
|
||||||
|
643
src/lib/hooks/useChat.tsx
Normal file
643
src/lib/hooks/useChat.tsx
Normal file
@@ -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<void>;
|
||||||
|
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<ChatContext>({
|
||||||
|
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<string | undefined>(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<Message[]>([]);
|
||||||
|
|
||||||
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
|
const [fileIds, setFileIds] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const [focusMode, setFocusMode] = useState('webSearch');
|
||||||
|
const [optimizationMode, setOptimizationMode] = useState('speed');
|
||||||
|
|
||||||
|
const [isMessagesLoaded, setIsMessagesLoaded] = useState(false);
|
||||||
|
|
||||||
|
const [notFound, setNotFound] = useState(false);
|
||||||
|
|
||||||
|
const [chatModelProvider, setChatModelProvider] = useState<ChatModelProvider>(
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
provider: '',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const [embeddingModelProvider, setEmbeddingModelProvider] =
|
||||||
|
useState<EmbeddingModelProvider>({
|
||||||
|
name: '',
|
||||||
|
provider: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isConfigReady, setIsConfigReady] = useState(false);
|
||||||
|
const [hasError, setHasError] = useState(false);
|
||||||
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
|
||||||
|
const messagesRef = useRef<Message[]>([]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<chatContext.Provider
|
||||||
|
value={{
|
||||||
|
messages,
|
||||||
|
chatHistory,
|
||||||
|
files,
|
||||||
|
fileIds,
|
||||||
|
focusMode,
|
||||||
|
chatId,
|
||||||
|
hasError,
|
||||||
|
isMessagesLoaded,
|
||||||
|
isReady,
|
||||||
|
loading,
|
||||||
|
messageAppeared,
|
||||||
|
notFound,
|
||||||
|
optimizationMode,
|
||||||
|
setFileIds,
|
||||||
|
setFiles,
|
||||||
|
setFocusMode,
|
||||||
|
setOptimizationMode,
|
||||||
|
rewrite,
|
||||||
|
sendMessage,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</chatContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useChat = () => {
|
||||||
|
const ctx = useContext(chatContext);
|
||||||
|
return ctx;
|
||||||
|
};
|
@@ -38,7 +38,7 @@ export const loadAimlApiChatModels = async () => {
|
|||||||
chatModels[model.id] = {
|
chatModels[model.id] = {
|
||||||
displayName: model.name || model.id,
|
displayName: model.name || model.id,
|
||||||
model: new ChatOpenAI({
|
model: new ChatOpenAI({
|
||||||
openAIApiKey: apiKey,
|
apiKey: apiKey,
|
||||||
modelName: model.id,
|
modelName: model.id,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
configuration: {
|
configuration: {
|
||||||
@@ -76,7 +76,7 @@ export const loadAimlApiEmbeddingModels = async () => {
|
|||||||
embeddingModels[model.id] = {
|
embeddingModels[model.id] = {
|
||||||
displayName: model.name || model.id,
|
displayName: model.name || model.id,
|
||||||
model: new OpenAIEmbeddings({
|
model: new OpenAIEmbeddings({
|
||||||
openAIApiKey: apiKey,
|
apiKey: apiKey,
|
||||||
modelName: model.id,
|
modelName: model.id,
|
||||||
configuration: {
|
configuration: {
|
||||||
baseURL: API_URL,
|
baseURL: API_URL,
|
||||||
|
@@ -9,6 +9,18 @@ export const PROVIDER_INFO = {
|
|||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
|
||||||
const anthropicChatModels: Record<string, string>[] = [
|
const anthropicChatModels: Record<string, string>[] = [
|
||||||
|
{
|
||||||
|
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',
|
displayName: 'Claude 3.7 Sonnet',
|
||||||
key: 'claude-3-7-sonnet-20250219',
|
key: 'claude-3-7-sonnet-20250219',
|
||||||
|
@@ -31,7 +31,7 @@ export const loadDeepseekChatModels = async () => {
|
|||||||
chatModels[model.key] = {
|
chatModels[model.key] = {
|
||||||
displayName: model.displayName,
|
displayName: model.displayName,
|
||||||
model: new ChatOpenAI({
|
model: new ChatOpenAI({
|
||||||
openAIApiKey: deepseekApiKey,
|
apiKey: deepseekApiKey,
|
||||||
modelName: model.key,
|
modelName: model.key,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
configuration: {
|
configuration: {
|
||||||
|
@@ -14,16 +14,16 @@ import { Embeddings } from '@langchain/core/embeddings';
|
|||||||
|
|
||||||
const geminiChatModels: Record<string, string>[] = [
|
const geminiChatModels: Record<string, string>[] = [
|
||||||
{
|
{
|
||||||
displayName: 'Gemini 2.5 Flash Preview 05-20',
|
displayName: 'Gemini 2.5 Flash',
|
||||||
key: 'gemini-2.5-flash-preview-05-20',
|
key: 'gemini-2.5-flash',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Gemini 2.5 Pro Preview',
|
displayName: 'Gemini 2.5 Flash-Lite',
|
||||||
key: 'gemini-2.5-pro-preview-05-06',
|
key: 'gemini-2.5-flash-lite',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Gemini 2.5 Pro Experimental',
|
displayName: 'Gemini 2.5 Pro',
|
||||||
key: 'gemini-2.5-pro-preview-05-06',
|
key: 'gemini-2.5-pro',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Gemini 2.0 Flash',
|
displayName: 'Gemini 2.0 Flash',
|
||||||
@@ -75,7 +75,7 @@ export const loadGeminiChatModels = async () => {
|
|||||||
displayName: model.displayName,
|
displayName: model.displayName,
|
||||||
model: new ChatGoogleGenerativeAI({
|
model: new ChatGoogleGenerativeAI({
|
||||||
apiKey: geminiApiKey,
|
apiKey: geminiApiKey,
|
||||||
modelName: model.key,
|
model: model.key,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
}) as unknown as BaseChatModel,
|
}) as unknown as BaseChatModel,
|
||||||
};
|
};
|
||||||
@@ -108,7 +108,7 @@ export const loadGeminiEmbeddingModels = async () => {
|
|||||||
|
|
||||||
return embeddingModels;
|
return embeddingModels;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error loading OpenAI embeddings models: ${err}`);
|
console.error(`Error loading Gemini embeddings models: ${err}`);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { ChatOpenAI } from '@langchain/openai';
|
import { ChatGroq } from '@langchain/groq';
|
||||||
import { getGroqApiKey } from '../config';
|
import { getGroqApiKey } from '../config';
|
||||||
import { ChatModel } from '.';
|
import { ChatModel } from '.';
|
||||||
|
|
||||||
@@ -28,13 +28,10 @@ export const loadGroqChatModels = async () => {
|
|||||||
groqChatModels.forEach((model: any) => {
|
groqChatModels.forEach((model: any) => {
|
||||||
chatModels[model.id] = {
|
chatModels[model.id] = {
|
||||||
displayName: model.id,
|
displayName: model.id,
|
||||||
model: new ChatOpenAI({
|
model: new ChatGroq({
|
||||||
openAIApiKey: groqApiKey,
|
apiKey: groqApiKey,
|
||||||
modelName: model.id,
|
model: model.id,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
configuration: {
|
|
||||||
baseURL: 'https://api.groq.com/openai/v1',
|
|
||||||
},
|
|
||||||
}) as unknown as BaseChatModel,
|
}) as unknown as BaseChatModel,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@@ -118,9 +118,13 @@ export const getAvailableChatModelProviders = async () => {
|
|||||||
[customOpenAiModelName]: {
|
[customOpenAiModelName]: {
|
||||||
displayName: customOpenAiModelName,
|
displayName: customOpenAiModelName,
|
||||||
model: new ChatOpenAI({
|
model: new ChatOpenAI({
|
||||||
openAIApiKey: customOpenAiApiKey,
|
apiKey: customOpenAiApiKey,
|
||||||
modelName: customOpenAiModelName,
|
modelName: customOpenAiModelName,
|
||||||
temperature: 0.7,
|
...((() => {
|
||||||
|
const temperatureRestrictedModels = ['gpt-5-nano','gpt-5','gpt-5-mini','o1', 'o3', 'o3-mini', 'o4-mini'];
|
||||||
|
const isTemperatureRestricted = temperatureRestrictedModels.some(restrictedModel => customOpenAiModelName.includes(restrictedModel));
|
||||||
|
return isTemperatureRestricted ? {} : { temperature: 0.7 };
|
||||||
|
})()),
|
||||||
configuration: {
|
configuration: {
|
||||||
baseURL: customOpenAiApiUrl,
|
baseURL: customOpenAiApiUrl,
|
||||||
},
|
},
|
||||||
|
@@ -47,7 +47,7 @@ export const loadLMStudioChatModels = async () => {
|
|||||||
chatModels[model.id] = {
|
chatModels[model.id] = {
|
||||||
displayName: model.name || model.id,
|
displayName: model.name || model.id,
|
||||||
model: new ChatOpenAI({
|
model: new ChatOpenAI({
|
||||||
openAIApiKey: 'lm-studio',
|
apiKey: 'lm-studio',
|
||||||
configuration: {
|
configuration: {
|
||||||
baseURL: ensureV1Endpoint(endpoint),
|
baseURL: ensureV1Endpoint(endpoint),
|
||||||
},
|
},
|
||||||
@@ -83,7 +83,7 @@ export const loadLMStudioEmbeddingsModels = async () => {
|
|||||||
embeddingsModels[model.id] = {
|
embeddingsModels[model.id] = {
|
||||||
displayName: model.name || model.id,
|
displayName: model.name || model.id,
|
||||||
model: new OpenAIEmbeddings({
|
model: new OpenAIEmbeddings({
|
||||||
openAIApiKey: 'lm-studio',
|
apiKey: 'lm-studio',
|
||||||
configuration: {
|
configuration: {
|
||||||
baseURL: ensureV1Endpoint(endpoint),
|
baseURL: ensureV1Endpoint(endpoint),
|
||||||
},
|
},
|
||||||
|
@@ -1,16 +1,17 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { getKeepAlive, getOllamaApiEndpoint } from '../config';
|
import { getKeepAlive, getOllamaApiEndpoint, getOllamaApiKey } from '../config';
|
||||||
import { ChatModel, EmbeddingModel } from '.';
|
import { ChatModel, EmbeddingModel } from '.';
|
||||||
|
|
||||||
export const PROVIDER_INFO = {
|
export const PROVIDER_INFO = {
|
||||||
key: 'ollama',
|
key: 'ollama',
|
||||||
displayName: 'Ollama',
|
displayName: 'Ollama',
|
||||||
};
|
};
|
||||||
import { ChatOllama } from '@langchain/community/chat_models/ollama';
|
import { ChatOllama } from '@langchain/ollama';
|
||||||
import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama';
|
import { OllamaEmbeddings } from '@langchain/ollama';
|
||||||
|
|
||||||
export const loadOllamaChatModels = async () => {
|
export const loadOllamaChatModels = async () => {
|
||||||
const ollamaApiEndpoint = getOllamaApiEndpoint();
|
const ollamaApiEndpoint = getOllamaApiEndpoint();
|
||||||
|
const ollamaApiKey = getOllamaApiKey();
|
||||||
|
|
||||||
if (!ollamaApiEndpoint) return {};
|
if (!ollamaApiEndpoint) return {};
|
||||||
|
|
||||||
@@ -33,6 +34,9 @@ export const loadOllamaChatModels = async () => {
|
|||||||
model: model.model,
|
model: model.model,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
keepAlive: getKeepAlive(),
|
keepAlive: getKeepAlive(),
|
||||||
|
...(ollamaApiKey
|
||||||
|
? { headers: { Authorization: `Bearer ${ollamaApiKey}` } }
|
||||||
|
: {}),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -46,6 +50,7 @@ export const loadOllamaChatModels = async () => {
|
|||||||
|
|
||||||
export const loadOllamaEmbeddingModels = async () => {
|
export const loadOllamaEmbeddingModels = async () => {
|
||||||
const ollamaApiEndpoint = getOllamaApiEndpoint();
|
const ollamaApiEndpoint = getOllamaApiEndpoint();
|
||||||
|
const ollamaApiKey = getOllamaApiKey();
|
||||||
|
|
||||||
if (!ollamaApiEndpoint) return {};
|
if (!ollamaApiEndpoint) return {};
|
||||||
|
|
||||||
@@ -66,6 +71,9 @@ export const loadOllamaEmbeddingModels = async () => {
|
|||||||
model: new OllamaEmbeddings({
|
model: new OllamaEmbeddings({
|
||||||
baseUrl: ollamaApiEndpoint,
|
baseUrl: ollamaApiEndpoint,
|
||||||
model: model.model,
|
model: model.model,
|
||||||
|
...(ollamaApiKey
|
||||||
|
? { headers: { Authorization: `Bearer ${ollamaApiKey}` } }
|
||||||
|
: {}),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@@ -26,6 +26,10 @@ const openaiChatModels: Record<string, string>[] = [
|
|||||||
displayName: 'GPT-4 omni',
|
displayName: 'GPT-4 omni',
|
||||||
key: 'gpt-4o',
|
key: 'gpt-4o',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'GPT-4o (2024-05-13)',
|
||||||
|
key: 'gpt-4o-2024-05-13',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'GPT-4 omni mini',
|
displayName: 'GPT-4 omni mini',
|
||||||
key: 'gpt-4o-mini',
|
key: 'gpt-4o-mini',
|
||||||
@@ -42,6 +46,34 @@ const openaiChatModels: Record<string, string>[] = [
|
|||||||
displayName: 'GPT 4.1',
|
displayName: 'GPT 4.1',
|
||||||
key: 'gpt-4.1',
|
key: 'gpt-4.1',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'GPT 5 nano',
|
||||||
|
key: 'gpt-5-nano',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'GPT 5',
|
||||||
|
key: 'gpt-5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'GPT 5 Mini',
|
||||||
|
key: 'gpt-5-mini',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'o1',
|
||||||
|
key: 'o1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'o3',
|
||||||
|
key: 'o3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'o3 Mini',
|
||||||
|
key: 'o3-mini',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'o4 Mini',
|
||||||
|
key: 'o4-mini',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const openaiEmbeddingModels: Record<string, string>[] = [
|
const openaiEmbeddingModels: Record<string, string>[] = [
|
||||||
@@ -64,13 +96,23 @@ export const loadOpenAIChatModels = async () => {
|
|||||||
const chatModels: Record<string, ChatModel> = {};
|
const chatModels: Record<string, ChatModel> = {};
|
||||||
|
|
||||||
openaiChatModels.forEach((model) => {
|
openaiChatModels.forEach((model) => {
|
||||||
|
// Models that only support temperature = 1
|
||||||
|
const temperatureRestrictedModels = ['gpt-5-nano','gpt-5','gpt-5-mini','o1', 'o3', 'o3-mini', 'o4-mini'];
|
||||||
|
const isTemperatureRestricted = temperatureRestrictedModels.some(restrictedModel => model.key.includes(restrictedModel));
|
||||||
|
|
||||||
|
const modelConfig: any = {
|
||||||
|
apiKey: openaiApiKey,
|
||||||
|
modelName: model.key,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only add temperature if the model supports it
|
||||||
|
if (!isTemperatureRestricted) {
|
||||||
|
modelConfig.temperature = 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
chatModels[model.key] = {
|
chatModels[model.key] = {
|
||||||
displayName: model.displayName,
|
displayName: model.displayName,
|
||||||
model: new ChatOpenAI({
|
model: new ChatOpenAI(modelConfig) as unknown as BaseChatModel,
|
||||||
openAIApiKey: openaiApiKey,
|
|
||||||
modelName: model.key,
|
|
||||||
temperature: 0.7,
|
|
||||||
}) as unknown as BaseChatModel,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -93,7 +135,7 @@ export const loadOpenAIEmbeddingModels = async () => {
|
|||||||
embeddingModels[model.key] = {
|
embeddingModels[model.key] = {
|
||||||
displayName: model.displayName,
|
displayName: model.displayName,
|
||||||
model: new OpenAIEmbeddings({
|
model: new OpenAIEmbeddings({
|
||||||
openAIApiKey: openaiApiKey,
|
apiKey: openaiApiKey,
|
||||||
modelName: model.key,
|
modelName: model.key,
|
||||||
}) as unknown as Embeddings,
|
}) as unknown as Embeddings,
|
||||||
};
|
};
|
||||||
|
@@ -1,8 +1,11 @@
|
|||||||
import { BaseMessage } from '@langchain/core/messages';
|
import { BaseMessage, isAIMessage } from '@langchain/core/messages';
|
||||||
|
|
||||||
const formatChatHistoryAsString = (history: BaseMessage[]) => {
|
const formatChatHistoryAsString = (history: BaseMessage[]) => {
|
||||||
return history
|
return history
|
||||||
.map((message) => `${message._getType()}: ${message.content}`)
|
.map(
|
||||||
|
(message) =>
|
||||||
|
`${isAIMessage(message) ? 'AI' : 'User'}: ${message.content}`,
|
||||||
|
)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user