Compare commits
74 Commits
v1.11.0-rc
...
238bcaff2b
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
57407112fb | ||
|
b280cc2e01 | ||
|
e6ebf892c5 | ||
|
b754641058 | ||
|
722f4f760e | ||
|
01e04a209f | ||
|
0299fd1ea0 | ||
|
cf8dec53ca | ||
|
d5c012d748 | ||
|
2ccbd9a44c | ||
|
ccd89d48d9 | ||
|
87d788ddef | ||
|
809b625a34 | ||
|
95c753a549 | ||
|
0bb8b7ec5c | ||
|
c6d084f5dc | ||
|
0024ce36c8 | ||
|
c44e746807 | ||
|
b1826066f4 | ||
|
b0b8acc45b | ||
|
e2b9ffc072 | ||
|
68c43ea372 | ||
|
3b46baca4f | ||
|
772e461c08 | ||
|
5c6018a0f9 | ||
|
0b7989c3d3 | ||
|
8cfcc3e39c | ||
|
9eba4b7373 | ||
|
91306dc0c7 |
0
.assets/manifest.json
Normal file
2
.gitignore
vendored
@@ -37,3 +37,5 @@ Thumbs.db
|
|||||||
# Db
|
# Db
|
||||||
db.sqlite
|
db.sqlite
|
||||||
/searxng
|
/searxng
|
||||||
|
|
||||||
|
certificates
|
24
README.md
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
[](https://discord.gg/26aArMy8tT)
|
[](https://discord.gg/26aArMy8tT)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -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,9 +87,13 @@ 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**.
|
||||||
|
- `Gemini`: Your Gemini API key. **You only need to fill this if you wish to use Google's models**.
|
||||||
|
- `DEEPSEEK`: Your Deepseek API key. **Only needed if you want Deepseek models.**
|
||||||
|
- `AIMLAPI`: Your AI/ML API key. **Only needed if you want to use AI/ML API models and embeddings.**
|
||||||
|
|
||||||
**Note**: You can change these after starting Perplexica from the settings dialog.
|
**Note**: You can change these after starting Perplexica from the settings dialog.
|
||||||
|
|
||||||
@@ -111,13 +115,23 @@ There are mainly 2 ways of installing Perplexica - With Docker, Without Docker.
|
|||||||
2. Clone the repository and rename the `sample.config.toml` file to `config.toml` in the root directory. Ensure you complete all required fields in this file.
|
2. Clone the repository and rename the `sample.config.toml` file to `config.toml` in the root directory. Ensure you complete all required fields in this file.
|
||||||
3. After populating the configuration run `npm i`.
|
3. After populating the configuration run `npm i`.
|
||||||
4. Install the dependencies and then execute `npm run build`.
|
4. Install the dependencies and then execute `npm run build`.
|
||||||
5. Finally, start the app by running `npm rum start`
|
5. Finally, start the app by running `npm run start`
|
||||||
|
|
||||||
**Note**: Using Docker is recommended as it simplifies the setup process, especially for managing environment variables and dependencies.
|
**Note**: Using Docker is recommended as it simplifies the setup process, especially for managing environment variables and dependencies.
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
@@ -132,7 +146,7 @@ If you're encountering an Ollama connection error, it is likely due to the backe
|
|||||||
|
|
||||||
3. **Linux Users - Expose Ollama to Network:**
|
3. **Linux Users - Expose Ollama to Network:**
|
||||||
|
|
||||||
- Inside `/etc/systemd/system/ollama.service`, you need to add `Environment="OLLAMA_HOST=0.0.0.0"`. Then restart Ollama by `systemctl restart ollama`. For more information see [Ollama docs](https://github.com/ollama/ollama/blob/main/docs/faq.md#setting-environment-variables-on-linux)
|
- Inside `/etc/systemd/system/ollama.service`, you need to add `Environment="OLLAMA_HOST=0.0.0.0:11434"`. (Change the port number if you are using a different one.) Then reload the systemd manager configuration with `systemctl daemon-reload`, and restart Ollama by `systemctl restart ollama`. For more information see [Ollama docs](https://github.com/ollama/ollama/blob/main/docs/faq.md#setting-environment-variables-on-linux)
|
||||||
|
|
||||||
- Ensure that the port (default is 11434) is not blocked by your firewall.
|
- Ensure that the port (default is 11434) is not blocked by your firewall.
|
||||||
|
|
||||||
|
@@ -41,6 +41,6 @@ To update Perplexica to the latest version, follow these steps:
|
|||||||
3. Check for changes in the configuration files. If the `sample.config.toml` file contains new fields, delete your existing `config.toml` file, rename `sample.config.toml` to `config.toml`, and update the configuration accordingly.
|
3. Check for changes in the configuration files. If the `sample.config.toml` file contains new fields, delete your existing `config.toml` file, rename `sample.config.toml` to `config.toml`, and update the configuration accordingly.
|
||||||
4. After populating the configuration run `npm i`.
|
4. After populating the configuration run `npm i`.
|
||||||
5. Install the dependencies and then execute `npm run build`.
|
5. Install the dependencies and then execute `npm run build`.
|
||||||
6. Finally, start the app by running `npm rum start`
|
6. Finally, start the app by running `npm run start`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
16
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "perplexica-frontend",
|
"name": "perplexica-frontend",
|
||||||
"version": "1.11.0-rc1",
|
"version": "1.11.0-rc2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "ItzCrazyKns",
|
"author": "ItzCrazyKns",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -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",
|
||||||
|
BIN
public/icon-100.png
Normal file
After Width: | Height: | Size: 916 B |
BIN
public/icon-50.png
Normal file
After Width: | Height: | Size: 515 B |
BIN
public/icon.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
public/screenshots/p1.png
Normal file
After Width: | Height: | Size: 183 KiB |
BIN
public/screenshots/p1_small.png
Normal file
After Width: | Height: | Size: 130 KiB |
BIN
public/screenshots/p2.png
Normal file
After Width: | Height: | Size: 627 KiB |
BIN
public/screenshots/p2_small.png
Normal file
After Width: | Height: | Size: 202 KiB |
@@ -25,6 +25,9 @@ API_URL = "" # Ollama API URL - http://host.docker.internal:11434
|
|||||||
[MODELS.DEEPSEEK]
|
[MODELS.DEEPSEEK]
|
||||||
API_KEY = ""
|
API_KEY = ""
|
||||||
|
|
||||||
|
[MODELS.AIMLAPI]
|
||||||
|
API_KEY = "" # Required to use AI/ML API chat and embedding models
|
||||||
|
|
||||||
[MODELS.LM_STUDIO]
|
[MODELS.LM_STUDIO]
|
||||||
API_URL = "" # LM Studio API URL - http://host.docker.internal:1234
|
API_URL = "" # LM Studio API URL - http://host.docker.internal:1234
|
||||||
|
|
||||||
|
@@ -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: {
|
||||||
|
@@ -8,8 +8,10 @@ import {
|
|||||||
getOllamaApiEndpoint,
|
getOllamaApiEndpoint,
|
||||||
getOpenaiApiKey,
|
getOpenaiApiKey,
|
||||||
getDeepseekApiKey,
|
getDeepseekApiKey,
|
||||||
|
getAimlApiKey,
|
||||||
getLMStudioApiEndpoint,
|
getLMStudioApiEndpoint,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
|
getOllamaApiKey,
|
||||||
} from '@/lib/config';
|
} from '@/lib/config';
|
||||||
import {
|
import {
|
||||||
getAvailableChatModelProviders,
|
getAvailableChatModelProviders,
|
||||||
@@ -52,11 +54,13 @@ 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();
|
||||||
config['geminiApiKey'] = getGeminiApiKey();
|
config['geminiApiKey'] = getGeminiApiKey();
|
||||||
config['deepseekApiKey'] = getDeepseekApiKey();
|
config['deepseekApiKey'] = getDeepseekApiKey();
|
||||||
|
config['aimlApiKey'] = getAimlApiKey();
|
||||||
config['customOpenaiApiUrl'] = getCustomOpenaiApiUrl();
|
config['customOpenaiApiUrl'] = getCustomOpenaiApiUrl();
|
||||||
config['customOpenaiApiKey'] = getCustomOpenaiApiKey();
|
config['customOpenaiApiKey'] = getCustomOpenaiApiKey();
|
||||||
config['customOpenaiModelName'] = getCustomOpenaiModelName();
|
config['customOpenaiModelName'] = getCustomOpenaiModelName();
|
||||||
@@ -91,10 +95,14 @@ 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,
|
||||||
},
|
},
|
||||||
|
AIMLAPI: {
|
||||||
|
API_KEY: config.aimlApiKey,
|
||||||
|
},
|
||||||
LM_STUDIO: {
|
LM_STUDIO: {
|
||||||
API_URL: config.lmStudioApiUrl,
|
API_URL: config.lmStudioApiUrl,
|
||||||
},
|
},
|
||||||
|
@@ -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]} ${
|
|
||||||
topics[i % topics.length]
|
|
||||||
}`,
|
|
||||||
{
|
|
||||||
engines: ['bing news'],
|
engines: ['bing news'],
|
||||||
pageno: 1,
|
pageno: 1,
|
||||||
},
|
language: 'en',
|
||||||
)
|
})
|
||||||
).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,14 +13,38 @@ 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);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchArticles = async (topic: string) => {
|
||||||
const fetchData = async () => {
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/discover`, {
|
const res = await fetch(`/api/discover?topic=${topic}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -43,10 +68,39 @@ const Page = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchData();
|
useEffect(() => {
|
||||||
}, []);
|
fetchArticles(activeTopic);
|
||||||
|
}, [activeTopic]);
|
||||||
|
|
||||||
return loading ? (
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-col pt-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Search />
|
||||||
|
<h1 className="text-3xl font-medium p-2">Discover</h1>
|
||||||
|
</div>
|
||||||
|
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center space-x-2 overflow-x-auto">
|
||||||
|
{topics.map((t, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={cn(
|
||||||
|
'border-[0.1px] rounded-full text-sm px-3 py-1 text-nowrap transition duration-200 cursor-pointer',
|
||||||
|
activeTopic === t.key
|
||||||
|
? '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',
|
||||||
|
)}
|
||||||
|
onClick={() => setActiveTopic(t.key)}
|
||||||
|
>
|
||||||
|
<span>{t.display}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
<div className="flex flex-row items-center justify-center min-h-screen">
|
<div className="flex flex-row items-center justify-center min-h-screen">
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -66,17 +120,7 @@ const Page = () => {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</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">
|
||||||
<div>
|
|
||||||
<div className="flex flex-col pt-4">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Search />
|
|
||||||
<h1 className="text-3xl font-medium p-2">Discover</h1>
|
|
||||||
</div>
|
|
||||||
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
|
|
||||||
</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">
|
|
||||||
{discover &&
|
{discover &&
|
||||||
discover?.map((item, i) => (
|
discover?.map((item, i) => (
|
||||||
<Link
|
<Link
|
||||||
@@ -105,6 +149,7 @@ const Page = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@@ -11,3 +11,11 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
||||||
|
select,
|
||||||
|
textarea,
|
||||||
|
input {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
54
src/app/manifest.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { MetadataRoute } from 'next';
|
||||||
|
|
||||||
|
export default function manifest(): MetadataRoute.Manifest {
|
||||||
|
return {
|
||||||
|
name: 'Perplexica - Chat with the internet',
|
||||||
|
short_name: 'Perplexica',
|
||||||
|
description:
|
||||||
|
'Perplexica is an AI powered chatbot that is connected to the internet.',
|
||||||
|
start_url: '/',
|
||||||
|
display: 'standalone',
|
||||||
|
background_color: '#0a0a0a',
|
||||||
|
theme_color: '#0a0a0a',
|
||||||
|
screenshots: [
|
||||||
|
{
|
||||||
|
src: '/screenshots/p1.png',
|
||||||
|
form_factor: 'wide',
|
||||||
|
sizes: '2560x1600',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/screenshots/p2.png',
|
||||||
|
form_factor: 'wide',
|
||||||
|
sizes: '2560x1600',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/screenshots/p1_small.png',
|
||||||
|
form_factor: 'narrow',
|
||||||
|
sizes: '828x1792',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/screenshots/p2_small.png',
|
||||||
|
form_factor: 'narrow',
|
||||||
|
sizes: '828x1792',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: '/icon-50.png',
|
||||||
|
sizes: '50x50',
|
||||||
|
type: 'image/png' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/icon-100.png',
|
||||||
|
sizes: '100x100',
|
||||||
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/icon.png',
|
||||||
|
sizes: '440x440',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'any',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
@@ -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>
|
||||||
|
<ChatProvider>
|
||||||
<ChatWindow />
|
<ChatWindow />
|
||||||
|
</ChatProvider>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -21,8 +21,10 @@ 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;
|
||||||
customOpenaiApiKey: string;
|
customOpenaiApiKey: string;
|
||||||
customOpenaiApiUrl: string;
|
customOpenaiApiUrl: string;
|
||||||
customOpenaiModelName: string;
|
customOpenaiModelName: string;
|
||||||
@@ -147,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(() => {
|
||||||
@@ -209,6 +214,10 @@ const Page = () => {
|
|||||||
|
|
||||||
setSystemInstructions(localStorage.getItem('systemInstructions')!);
|
setSystemInstructions(localStorage.getItem('systemInstructions')!);
|
||||||
|
|
||||||
|
setMeasureUnit(
|
||||||
|
localStorage.getItem('measureUnit')! as 'Imperial' | 'Metric',
|
||||||
|
);
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -367,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);
|
||||||
@@ -415,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">
|
||||||
@@ -515,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);
|
||||||
@@ -786,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
|
||||||
@@ -862,6 +914,25 @@ const Page = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
|
AI/ML API Key
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="AI/ML API Key"
|
||||||
|
value={config.aimlApiKey}
|
||||||
|
isSaving={savingStates['aimlApiKey']}
|
||||||
|
onChange={(e) => {
|
||||||
|
setConfig((prev) => ({
|
||||||
|
...prev!,
|
||||||
|
aimlApiKey: e.target.value,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
onSave={(value) => saveConfig('aimlApiKey', 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">
|
||||||
LM Studio API URL
|
LM Studio API URL
|
||||||
|
@@ -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,512 +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;
|
|
||||||
|
|
||||||
chatModelProvider =
|
|
||||||
chatModelProvider || Object.keys(chatModelProviders)[0];
|
|
||||||
|
|
||||||
chatModel = Object.keys(chatModelProviders[chatModelProvider])[0];
|
|
||||||
|
|
||||||
if (!chatModelProviders || Object.keys(chatModelProviders).length === 0)
|
|
||||||
return toast.error('No chat models available');
|
|
||||||
}
|
|
||||||
|
|
||||||
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]
|
|
||||||
) {
|
|
||||||
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]
|
|
||||||
) {
|
|
||||||
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">
|
||||||
@@ -559,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>
|
||||||
)
|
)
|
||||||
|
@@ -1,32 +1,11 @@
|
|||||||
import { Settings } from 'lucide-react';
|
import { Settings } from 'lucide-react';
|
||||||
import EmptyChatMessageInput from './EmptyChatMessageInput';
|
import EmptyChatMessageInput from './EmptyChatMessageInput';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { File } from './ChatWindow';
|
import { File } from './ChatWindow';
|
||||||
import Link from 'next/link';
|
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">
|
||||||
@@ -34,26 +13,18 @@ const EmptyChat = ({
|
|||||||
<Settings className="cursor-pointer lg:hidden" />
|
<Settings className="cursor-pointer lg:hidden" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen max-w-screen-sm mx-auto p-2 space-y-8">
|
<div className="flex flex-col items-center justify-center min-h-screen max-w-screen-sm mx-auto p-2 space-y-4">
|
||||||
|
<div className="flex flex-col items-center justify-center w-full space-y-8">
|
||||||
<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}
|
</div>
|
||||||
focusMode={focusMode}
|
|
||||||
setFocusMode={setFocusMode}
|
|
||||||
optimizationMode={optimizationMode}
|
|
||||||
setOptimizationMode={setOptimizationMode}
|
|
||||||
fileIds={fileIds}
|
|
||||||
setFileIds={setFileIds}
|
|
||||||
files={files}
|
|
||||||
setFiles={setFiles}
|
|
||||||
/>
|
|
||||||
<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 max-w-xs">
|
<div className="flex-1 w-full">
|
||||||
<WeatherWidget />
|
<WeatherWidget />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 max-w-xs">
|
<div className="flex-1 w-full">
|
||||||
<NewsArticleWidget />
|
<NewsArticleWidget />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -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>();
|
||||||
|
|
||||||
|
@@ -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(() => {
|
||||||
@@ -31,30 +34,40 @@ const WeatherWidget = () => {
|
|||||||
city: string;
|
city: string;
|
||||||
}) => void,
|
}) => void,
|
||||||
) => {
|
) => {
|
||||||
/*
|
|
||||||
// Geolocation doesn't give city so we'll country using ipapi for now
|
|
||||||
if (navigator.geolocation) {
|
if (navigator.geolocation) {
|
||||||
const result = await navigator.permissions.query({
|
const result = await navigator.permissions.query({
|
||||||
name: 'geolocation',
|
name: 'geolocation',
|
||||||
})
|
});
|
||||||
|
|
||||||
if (result.state === 'granted') {
|
if (result.state === 'granted') {
|
||||||
navigator.geolocation.getCurrentPosition(position => {
|
navigator.geolocation.getCurrentPosition(async (position) => {
|
||||||
|
const res = await fetch(
|
||||||
|
`https://api-bdc.io/data/reverse-geocode-client?latitude=${position.coords.latitude}&longitude=${position.coords.longitude}&localityLanguage=en`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
latitude: position.coords.latitude,
|
latitude: position.coords.latitude,
|
||||||
longitude: position.coords.longitude,
|
longitude: position.coords.longitude,
|
||||||
})
|
city: data.locality,
|
||||||
})
|
});
|
||||||
|
});
|
||||||
} else if (result.state === 'prompt') {
|
} else if (result.state === 'prompt') {
|
||||||
callback(await getApproxLocation())
|
callback(await getApproxLocation());
|
||||||
navigator.geolocation.getCurrentPosition(position => {})
|
navigator.geolocation.getCurrentPosition((position) => {});
|
||||||
} else if (result.state === 'denied') {
|
} else if (result.state === 'denied') {
|
||||||
callback(await getApproxLocation())
|
callback(await getApproxLocation());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
callback(await getApproxLocation())
|
|
||||||
} */
|
|
||||||
callback(await getApproxLocation());
|
callback(await getApproxLocation());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getLocation(async (location) => {
|
getLocation(async (location) => {
|
||||||
@@ -63,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',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -81,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);
|
||||||
});
|
});
|
||||||
@@ -115,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">
|
||||||
@@ -125,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,10 +31,14 @@ interface Config {
|
|||||||
};
|
};
|
||||||
OLLAMA: {
|
OLLAMA: {
|
||||||
API_URL: string;
|
API_URL: string;
|
||||||
|
API_KEY: string;
|
||||||
};
|
};
|
||||||
DEEPSEEK: {
|
DEEPSEEK: {
|
||||||
API_KEY: string;
|
API_KEY: string;
|
||||||
};
|
};
|
||||||
|
AIMLAPI: {
|
||||||
|
API_KEY: string;
|
||||||
|
};
|
||||||
LM_STUDIO: {
|
LM_STUDIO: {
|
||||||
API_URL: string;
|
API_URL: string;
|
||||||
};
|
};
|
||||||
@@ -83,8 +87,12 @@ 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 getCustomOpenaiApiKey = () =>
|
export const getCustomOpenaiApiKey = () =>
|
||||||
loadConfig().MODELS.CUSTOM_OPENAI.API_KEY;
|
loadConfig().MODELS.CUSTOM_OPENAI.API_KEY;
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
94
src/lib/providers/aimlapi.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
|
||||||
|
import { getAimlApiKey } from '../config';
|
||||||
|
import { ChatModel, EmbeddingModel } from '.';
|
||||||
|
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
import { Embeddings } from '@langchain/core/embeddings';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export const PROVIDER_INFO = {
|
||||||
|
key: 'aimlapi',
|
||||||
|
displayName: 'AI/ML API',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AimlApiModel {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const API_URL = 'https://api.aimlapi.com';
|
||||||
|
|
||||||
|
export const loadAimlApiChatModels = async () => {
|
||||||
|
const apiKey = getAimlApiKey();
|
||||||
|
|
||||||
|
if (!apiKey) return {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_URL}/models`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const chatModels: Record<string, ChatModel> = {};
|
||||||
|
|
||||||
|
response.data.data.forEach((model: AimlApiModel) => {
|
||||||
|
if (model.type === 'chat-completion') {
|
||||||
|
chatModels[model.id] = {
|
||||||
|
displayName: model.name || model.id,
|
||||||
|
model: new ChatOpenAI({
|
||||||
|
apiKey: apiKey,
|
||||||
|
modelName: model.id,
|
||||||
|
temperature: 0.7,
|
||||||
|
configuration: {
|
||||||
|
baseURL: API_URL,
|
||||||
|
},
|
||||||
|
}) as unknown as BaseChatModel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return chatModels;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error loading AI/ML API models: ${err}`);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadAimlApiEmbeddingModels = async () => {
|
||||||
|
const apiKey = getAimlApiKey();
|
||||||
|
|
||||||
|
if (!apiKey) return {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_URL}/models`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const embeddingModels: Record<string, EmbeddingModel> = {};
|
||||||
|
|
||||||
|
response.data.data.forEach((model: AimlApiModel) => {
|
||||||
|
if (model.type === 'embedding') {
|
||||||
|
embeddingModels[model.id] = {
|
||||||
|
displayName: model.name || model.id,
|
||||||
|
model: new OpenAIEmbeddings({
|
||||||
|
apiKey: apiKey,
|
||||||
|
modelName: model.id,
|
||||||
|
configuration: {
|
||||||
|
baseURL: API_URL,
|
||||||
|
},
|
||||||
|
}) as unknown as Embeddings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return embeddingModels;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error loading AI/ML API embeddings models: ${err}`);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
@@ -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,8 +14,16 @@ import { Embeddings } from '@langchain/core/embeddings';
|
|||||||
|
|
||||||
const geminiChatModels: Record<string, string>[] = [
|
const geminiChatModels: Record<string, string>[] = [
|
||||||
{
|
{
|
||||||
displayName: 'Gemini 2.5 Pro Experimental',
|
displayName: 'Gemini 2.5 Flash',
|
||||||
key: 'gemini-2.5-pro-exp-03-25',
|
key: 'gemini-2.5-flash',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Gemini 2.5 Flash-Lite',
|
||||||
|
key: 'gemini-2.5-flash-lite',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Gemini 2.5 Pro',
|
||||||
|
key: 'gemini-2.5-pro',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Gemini 2.0 Flash',
|
displayName: 'Gemini 2.0 Flash',
|
||||||
@@ -67,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,
|
||||||
};
|
};
|
||||||
@@ -100,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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@@ -35,6 +35,11 @@ import {
|
|||||||
loadDeepseekChatModels,
|
loadDeepseekChatModels,
|
||||||
PROVIDER_INFO as DeepseekInfo,
|
PROVIDER_INFO as DeepseekInfo,
|
||||||
} from './deepseek';
|
} from './deepseek';
|
||||||
|
import {
|
||||||
|
loadAimlApiChatModels,
|
||||||
|
loadAimlApiEmbeddingModels,
|
||||||
|
PROVIDER_INFO as AimlApiInfo,
|
||||||
|
} from './aimlapi';
|
||||||
import {
|
import {
|
||||||
loadLMStudioChatModels,
|
loadLMStudioChatModels,
|
||||||
loadLMStudioEmbeddingsModels,
|
loadLMStudioEmbeddingsModels,
|
||||||
@@ -49,6 +54,7 @@ export const PROVIDER_METADATA = {
|
|||||||
gemini: GeminiInfo,
|
gemini: GeminiInfo,
|
||||||
transformers: TransformersInfo,
|
transformers: TransformersInfo,
|
||||||
deepseek: DeepseekInfo,
|
deepseek: DeepseekInfo,
|
||||||
|
aimlapi: AimlApiInfo,
|
||||||
lmstudio: LMStudioInfo,
|
lmstudio: LMStudioInfo,
|
||||||
custom_openai: {
|
custom_openai: {
|
||||||
key: 'custom_openai',
|
key: 'custom_openai',
|
||||||
@@ -76,6 +82,7 @@ export const chatModelProviders: Record<
|
|||||||
anthropic: loadAnthropicChatModels,
|
anthropic: loadAnthropicChatModels,
|
||||||
gemini: loadGeminiChatModels,
|
gemini: loadGeminiChatModels,
|
||||||
deepseek: loadDeepseekChatModels,
|
deepseek: loadDeepseekChatModels,
|
||||||
|
aimlapi: loadAimlApiChatModels,
|
||||||
lmstudio: loadLMStudioChatModels,
|
lmstudio: loadLMStudioChatModels,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -87,6 +94,7 @@ export const embeddingModelProviders: Record<
|
|||||||
ollama: loadOllamaEmbeddingModels,
|
ollama: loadOllamaEmbeddingModels,
|
||||||
gemini: loadGeminiEmbeddingModels,
|
gemini: loadGeminiEmbeddingModels,
|
||||||
transformers: loadTransformersEmbeddingsModels,
|
transformers: loadTransformersEmbeddingsModels,
|
||||||
|
aimlapi: loadAimlApiEmbeddingModels,
|
||||||
lmstudio: loadLMStudioEmbeddingsModels,
|
lmstudio: loadLMStudioEmbeddingsModels,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -110,7 +118,7 @@ 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,
|
temperature: 0.7,
|
||||||
configuration: {
|
configuration: {
|
||||||
|
@@ -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}` } }
|
||||||
|
: {}),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@@ -42,6 +42,18 @@ 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 mini',
|
||||||
|
key: 'gpt-5-mini',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'GPT 5',
|
||||||
|
key: 'gpt-5',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const openaiEmbeddingModels: Record<string, string>[] = [
|
const openaiEmbeddingModels: Record<string, string>[] = [
|
||||||
@@ -67,9 +79,9 @@ export const loadOpenAIChatModels = async () => {
|
|||||||
chatModels[model.key] = {
|
chatModels[model.key] = {
|
||||||
displayName: model.displayName,
|
displayName: model.displayName,
|
||||||
model: new ChatOpenAI({
|
model: new ChatOpenAI({
|
||||||
openAIApiKey: openaiApiKey,
|
apiKey: openaiApiKey,
|
||||||
modelName: model.key,
|
modelName: model.key,
|
||||||
temperature: 0.7,
|
temperature: model.key.includes('gpt-5') ? 1 : 0.7,
|
||||||
}) as unknown as BaseChatModel,
|
}) as unknown as BaseChatModel,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -93,7 +105,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');
|
||||||
};
|
};
|
||||||
|
|
||||||
|