Compare commits

...

10 Commits

Author SHA1 Message Date
ItzCrazyKns
295334b195 feat(app): fix empty message being sent 2025-10-24 23:40:01 +05:30
ItzCrazyKns
b106abd77f feat(package): bump version 2025-10-24 23:00:07 +05:30
ItzCrazyKns
2d80fc400d feat(app): lint & beautify 2025-10-24 22:58:10 +05:30
ItzCrazyKns
097a5c55c6 feat(layout): add everything inside chat provider 2025-10-24 22:57:56 +05:30
ItzCrazyKns
d0719429b4 feat(app): fix issues with model selection 2025-10-24 22:56:23 +05:30
ItzCrazyKns
600d4ceb29 feat(hf-transformer): use langchain's inbuilt transformer class 2025-10-23 23:06:05 +05:30
ItzCrazyKns
4f50462f1d feat(package): bump version 2025-10-23 21:04:33 +05:30
ItzCrazyKns
231bc22a36 feat(docker): update searxng build script 2025-10-23 19:07:22 +05:30
ItzCrazyKns
cb1d85e458 feat(readme): add volumes 2025-10-21 16:57:57 +05:30
ItzCrazyKns
ce78b4ff62 feat(app): show "add model" button 2025-10-21 16:32:40 +05:30
19 changed files with 256 additions and 301 deletions

View File

@@ -17,22 +17,11 @@ RUN yarn build
FROM node:24.5.0-slim FROM node:24.5.0-slim
RUN apt-get update && \ RUN apt-get update && apt-get install -y \
apt-get install -y \ python3-dev python3-babel python3-venv python-is-python3 \
python3 \ uwsgi uwsgi-plugin-python3 \
python3-pip \ git build-essential libxslt-dev zlib1g-dev libffi-dev libssl-dev \
python3-venv \ curl sudo \
python3-dev \
sqlite3 \
git \
build-essential \
libxslt-dev \
zlib1g-dev \
libffi-dev \
libssl-dev \
uwsgi \
uwsgi-plugin-python3 \
curl \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
WORKDIR /home/perplexica WORKDIR /home/perplexica
@@ -45,28 +34,39 @@ COPY drizzle ./drizzle
RUN mkdir /home/perplexica/uploads RUN mkdir /home/perplexica/uploads
RUN useradd --system --home-dir /usr/local/searxng --shell /bin/sh searxng RUN useradd --shell /bin/bash --system \
--home-dir "/usr/local/searxng" \
WORKDIR /usr/local/searxng --comment 'Privacy-respecting metasearch engine' \
RUN git clone https://github.com/searxng/searxng.git . && \ searxng
python3 -m venv venv && \
. venv/bin/activate && \
pip install --upgrade pip setuptools wheel pyyaml && \
pip install -r requirements.txt && \
pip install uwsgi
RUN mkdir "/usr/local/searxng"
RUN mkdir -p /etc/searxng RUN mkdir -p /etc/searxng
RUN chown -R "searxng:searxng" "/usr/local/searxng"
COPY searxng/settings.yml /etc/searxng/settings.yml COPY searxng/settings.yml /etc/searxng/settings.yml
COPY searxng/limiter.toml /etc/searxng/limiter.toml COPY searxng/limiter.toml /etc/searxng/limiter.toml
COPY searxng/uwsgi.ini /etc/searxng/uwsgi.ini COPY searxng/uwsgi.ini /etc/searxng/uwsgi.ini
RUN chown -R searxng:searxng /etc/searxng
RUN chown -R searxng:searxng /usr/local/searxng /etc/searxng USER searxng
RUN git clone "https://github.com/searxng/searxng" \
"/usr/local/searxng/searxng-src"
RUN python3 -m venv "/usr/local/searxng/searx-pyenv"
RUN "/usr/local/searxng/searx-pyenv/bin/pip" install --upgrade pip setuptools wheel pyyaml msgspec
RUN cd "/usr/local/searxng/searxng-src" && \
"/usr/local/searxng/searx-pyenv/bin/pip" install --use-pep517 --no-build-isolation -e .
USER root
WORKDIR /home/perplexica WORKDIR /home/perplexica
COPY entrypoint.sh ./entrypoint.sh COPY entrypoint.sh ./entrypoint.sh
RUN chmod +x ./entrypoint.sh RUN chmod +x ./entrypoint.sh
RUN sed -i 's/\r$//' ./entrypoint.sh || true RUN sed -i 's/\r$//' ./entrypoint.sh || true
RUN echo "searxng ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
EXPOSE 3000 8080 EXPOSE 3000 8080
ENV SEARXNG_API_URL=http://localhost:8080 ENV SEARXNG_API_URL=http://localhost:8080

View File

@@ -79,19 +79,19 @@ There are mainly 2 ways of installing Perplexica - With Docker, Without Docker.
Perplexica can be easily run using Docker. Simply run the following command: Perplexica can be easily run using Docker. Simply run the following command:
```bash ```bash
docker run -p 3000:3000 --name perplexica itzcrazykns1337/perplexica:latest docker run -d -p 3000:3000 -v perplexica-data:/home/perplexica/data -v perplexica-uploads:/home/perplexica/uploads --name perplexica itzcrazykns1337/perplexica:latest
``` ```
This will pull and start the Perplexica container with the bundled SearxNG search engine. Once running, open your browser and navigate to http://localhost:3000. You can then configure your settings (API keys, models, etc.) directly in the setup screen. This will pull and start the Perplexica container with the bundled SearxNG search engine. Once running, open your browser and navigate to http://localhost:3000. You can then configure your settings (API keys, models, etc.) directly in the setup screen.
**Note**: The image includes both Perplexica and SearxNG, so no additional setup is required. **Note**: The image includes both Perplexica and SearxNG, so no additional setup is required. The `-v` flags create persistent volumes for your data and uploaded files.
#### Using Perplexica with Your Own SearxNG Instance #### Using Perplexica with Your Own SearxNG Instance
If you already have SearxNG running, you can use the slim version of Perplexica: If you already have SearxNG running, you can use the slim version of Perplexica:
```bash ```bash
docker run -p 3000:3000 -e SEARXNG_API_URL=http://your-searxng-url:8080 --name perplexica itzcrazykns1337/perplexica:slim-latest docker run -d -p 3000:3000 -e SEARXNG_API_URL=http://your-searxng-url:8080 -v perplexica-data:/home/perplexica/data -v perplexica-uploads:/home/perplexica/uploads --name perplexica itzcrazykns1337/perplexica:slim-latest
``` ```
**Important**: Make sure your SearxNG instance has: **Important**: Make sure your SearxNG instance has:
@@ -118,7 +118,7 @@ If you prefer to build from source or need more control:
```bash ```bash
docker build -t perplexica . docker build -t perplexica .
docker run -p 3000:3000 --name perplexica perplexica docker run -d -p 3000:3000 -v perplexica-data:/home/perplexica/data -v perplexica-uploads:/home/perplexica/uploads --name perplexica perplexica
``` ```
5. Access Perplexica at http://localhost:3000 and configure your settings in the setup screen. 5. Access Perplexica at http://localhost:3000 and configure your settings in the setup screen.

15
docker-compose.yaml Normal file
View File

@@ -0,0 +1,15 @@
services:
perplexica:
image: itzcrazykns1337/perplexica:latest
ports:
- '3000:3000'
volumes:
- data:/home/perplexica/data
- uploads:/home/perplexica/uploads
restart: unless-stopped
volumes:
data:
name: 'perplexica-data'
uploads:
name: 'perplexica-uploads'

View File

@@ -17,6 +17,7 @@ Before making search requests, you'll need to get the available providers and th
Returns a list of all active providers with their available chat and embedding models. Returns a list of all active providers with their available chat and embedding models.
**Response Example:** **Response Example:**
```json ```json
{ {
"providers": [ "providers": [

View File

@@ -10,7 +10,7 @@ Simply pull the latest image and restart your container:
docker pull itzcrazykns1337/perplexica:latest docker pull itzcrazykns1337/perplexica:latest
docker stop perplexica docker stop perplexica
docker rm perplexica docker rm perplexica
docker run -p 3000:3000 --name perplexica itzcrazykns1337/perplexica:latest docker run -d -p 3000:3000 -v perplexica-data:/home/perplexica/data -v perplexica-uploads:/home/perplexica/uploads --name perplexica itzcrazykns1337/perplexica:latest
``` ```
For slim version: For slim version:
@@ -19,7 +19,7 @@ For slim version:
docker pull itzcrazykns1337/perplexica:slim-latest docker pull itzcrazykns1337/perplexica:slim-latest
docker stop perplexica docker stop perplexica
docker rm perplexica docker rm perplexica
docker run -p 3000:3000 -e SEARXNG_API_URL=http://your-searxng-url:8080 --name perplexica itzcrazykns1337/perplexica:slim-latest docker run -d -p 3000:3000 -e SEARXNG_API_URL=http://your-searxng-url:8080 -v perplexica-data:/home/perplexica/data -v perplexica-uploads:/home/perplexica/uploads --name perplexica itzcrazykns1337/perplexica:slim-latest
``` ```
Once updated, go to http://localhost:3000 and verify the latest changes. Your settings are preserved automatically. Once updated, go to http://localhost:3000 and verify the latest changes. Your settings are preserved automatically.

View File

@@ -1,24 +1,32 @@
#!/bin/sh #!/bin/sh
set -e set -e
cd /usr/local/searxng
export SEARXNG_SETTINGS_PATH=/etc/searxng/settings.yml
# Start SearXNG in background with all output redirected to /dev/null
/usr/local/searxng/venv/bin/uwsgi \
--http-socket 0.0.0.0:8080 \
--ini /etc/searxng/uwsgi.ini \
--virtualenv /usr/local/searxng/venv \
--disable-logging > /dev/null 2>&1 &
echo "Starting SearXNG..." echo "Starting SearXNG..."
sudo -H -u searxng bash -c "cd /usr/local/searxng/searxng-src && export SEARXNG_SETTINGS_PATH='/etc/searxng/settings.yml' && export FLASK_APP=searx/webapp.py && /usr/local/searxng/searx-pyenv/bin/python -m flask run --host=0.0.0.0 --port=8080" &
SEARXNG_PID=$!
echo "Waiting for SearXNG to be ready..."
sleep 5 sleep 5
COUNTER=0
MAX_TRIES=30
until curl -s http://localhost:8080 > /dev/null 2>&1; do until curl -s http://localhost:8080 > /dev/null 2>&1; do
COUNTER=$((COUNTER+1))
if [ $COUNTER -ge $MAX_TRIES ]; then
echo "Warning: SearXNG health check timeout, but continuing..."
break
fi
sleep 1 sleep 1
done done
echo "SearXNG started successfully"
if curl -s http://localhost:8080 > /dev/null 2>&1; then
echo "SearXNG started successfully (PID: $SEARXNG_PID)"
else
echo "SearXNG may not be fully ready, but continuing (PID: $SEARXNG_PID)"
fi
cd /home/perplexica cd /home/perplexica
echo "Starting Perplexica..." echo "Starting Perplexica..."
exec node server.js exec node server.js

View File

@@ -1,6 +1,6 @@
{ {
"name": "perplexica-frontend", "name": "perplexica-frontend",
"version": "1.11.0", "version": "1.11.2",
"license": "MIT", "license": "MIT",
"author": "ItzCrazyKns", "author": "ItzCrazyKns",
"scripts": { "scripts": {

View File

@@ -1,17 +1,10 @@
'use client'; 'use client';
import ChatWindow from '@/components/ChatWindow'; import ChatWindow from '@/components/ChatWindow';
import { useParams } from 'next/navigation';
import React from 'react'; import React from 'react';
import { ChatProvider } from '@/lib/hooks/useChat';
const Page = () => { const Page = () => {
const { chatId }: { chatId: string } = useParams(); return <ChatWindow />;
return (
<ChatProvider id={chatId}>
<ChatWindow />
</ChatProvider>
);
}; };
export default Page; export default Page;

View File

@@ -9,6 +9,7 @@ import { Toaster } from 'sonner';
import ThemeProvider from '@/components/theme/Provider'; import ThemeProvider from '@/components/theme/Provider';
import configManager from '@/lib/config'; import configManager from '@/lib/config';
import SetupWizard from '@/components/Setup/SetupWizard'; import SetupWizard from '@/components/Setup/SetupWizard';
import { ChatProvider } from '@/lib/hooks/useChat';
const montserrat = Montserrat({ const montserrat = Montserrat({
weight: ['300', '400', '500', '700'], weight: ['300', '400', '500', '700'],
@@ -36,7 +37,7 @@ export default function RootLayout({
<body className={cn('h-full', montserrat.className)}> <body className={cn('h-full', montserrat.className)}>
<ThemeProvider> <ThemeProvider>
{setupComplete ? ( {setupComplete ? (
<> <ChatProvider>
<Sidebar>{children}</Sidebar> <Sidebar>{children}</Sidebar>
<Toaster <Toaster
toastOptions={{ toastOptions={{
@@ -47,7 +48,7 @@ export default function RootLayout({
}, },
}} }}
/> />
</> </ChatProvider>
) : ( ) : (
<SetupWizard configSections={configSections} /> <SetupWizard configSections={configSections} />
)} )}

View File

@@ -1,7 +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';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Chat - Perplexica', title: 'Chat - Perplexica',
@@ -9,15 +7,7 @@ export const metadata: Metadata = {
}; };
const Home = () => { const Home = () => {
return ( return <ChatWindow />;
<div>
<Suspense>
<ChatProvider>
<ChatWindow />
</ChatProvider>
</Suspense>
</div>
);
}; };
export default Home; export default Home;

View File

@@ -9,6 +9,7 @@ import Link from 'next/link';
import NextError from 'next/error'; import NextError from 'next/error';
import { useChat } from '@/lib/hooks/useChat'; import { useChat } from '@/lib/hooks/useChat';
import Loader from './ui/Loader'; import Loader from './ui/Loader';
import SettingsButtonMobile from './Settings/SettingsButtonMobile';
export interface BaseMessage { export interface BaseMessage {
chatId: string; chatId: string;
@@ -56,9 +57,7 @@ const ChatWindow = () => {
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">
<Link href="/settings"> <SettingsButtonMobile />
<Settings className="cursor-pointer lg:hidden" />
</Link>
</div> </div>
<div className="flex flex-col items-center justify-center min-h-screen"> <div className="flex flex-col items-center justify-center min-h-screen">
<p className="dark:text-white/70 text-black/70 text-sm"> <p className="dark:text-white/70 text-black/70 text-sm">

View File

@@ -8,17 +8,16 @@ import {
PopoverPanel, PopoverPanel,
Transition, Transition,
} from '@headlessui/react'; } from '@headlessui/react';
import { Fragment, useEffect, useState } from 'react'; import { Fragment, useEffect, useMemo, useState } from 'react';
import { MinimalProvider } from '@/lib/models/types'; import { MinimalProvider } from '@/lib/models/types';
import { useChat } from '@/lib/hooks/useChat';
const ModelSelector = () => { const ModelSelector = () => {
const [providers, setProviders] = useState<MinimalProvider[]>([]); const [providers, setProviders] = useState<MinimalProvider[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [selectedModel, setSelectedModel] = useState<{
providerId: string; const { setChatModelProvider, chatModelProvider } = useChat();
modelKey: string;
} | null>(null);
useEffect(() => { useEffect(() => {
const loadProviders = async () => { const loadProviders = async () => {
@@ -30,28 +29,8 @@ const ModelSelector = () => {
throw new Error('Failed to fetch providers'); throw new Error('Failed to fetch providers');
} }
const data = await res.json(); const data: { providers: MinimalProvider[] } = await res.json();
setProviders(data.providers || []); setProviders(data.providers);
const savedProviderId = localStorage.getItem('chatModelProviderId');
const savedModelKey = localStorage.getItem('chatModelKey');
if (savedProviderId && savedModelKey) {
setSelectedModel({
providerId: savedProviderId,
modelKey: savedModelKey,
});
} else if (data.providers && data.providers.length > 0) {
const firstProvider = data.providers.find(
(p: MinimalProvider) => p.chatModels.length > 0,
);
if (firstProvider && firstProvider.chatModels[0]) {
setSelectedModel({
providerId: firstProvider.id,
modelKey: firstProvider.chatModels[0].key,
});
}
}
} catch (error) { } catch (error) {
console.error('Error loading providers:', error); console.error('Error loading providers:', error);
} finally { } finally {
@@ -62,13 +41,32 @@ const ModelSelector = () => {
loadProviders(); loadProviders();
}, []); }, []);
const orderedProviders = useMemo(() => {
if (!chatModelProvider?.providerId) return providers;
const currentProviderIndex = providers.findIndex(
(p) => p.id === chatModelProvider.providerId,
);
if (currentProviderIndex === -1) {
return providers;
}
const selectedProvider = providers[currentProviderIndex];
const remainingProviders = providers.filter(
(_, index) => index !== currentProviderIndex,
);
return [selectedProvider, ...remainingProviders];
}, [providers, chatModelProvider]);
const handleModelSelect = (providerId: string, modelKey: string) => { const handleModelSelect = (providerId: string, modelKey: string) => {
setSelectedModel({ providerId, modelKey }); setChatModelProvider({ providerId, key: modelKey });
localStorage.setItem('chatModelProviderId', providerId); localStorage.setItem('chatModelProviderId', providerId);
localStorage.setItem('chatModelKey', modelKey); localStorage.setItem('chatModelKey', modelKey);
}; };
const filteredProviders = providers const filteredProviders = orderedProviders
.map((provider) => ({ .map((provider) => ({
...provider, ...provider,
chatModels: provider.chatModels.filter( chatModels: provider.chatModels.filter(
@@ -140,15 +138,16 @@ const ModelSelector = () => {
<div className="flex flex-col px-2 py-2 space-y-0.5"> <div className="flex flex-col px-2 py-2 space-y-0.5">
{provider.chatModels.map((model) => ( {provider.chatModels.map((model) => (
<PopoverButton <button
key={model.key} key={model.key}
onClick={() => onClick={() =>
handleModelSelect(provider.id, model.key) handleModelSelect(provider.id, model.key)
} }
type="button"
className={cn( className={cn(
'px-3 py-2 flex items-center justify-between text-start duration-200 cursor-pointer transition rounded-lg group', 'px-3 py-2 flex items-center justify-between text-start duration-200 cursor-pointer transition rounded-lg group',
selectedModel?.providerId === provider.id && chatModelProvider?.providerId === provider.id &&
selectedModel?.modelKey === model.key chatModelProvider?.key === model.key
? 'bg-light-secondary dark:bg-dark-secondary' ? 'bg-light-secondary dark:bg-dark-secondary'
: 'hover:bg-light-secondary dark:hover:bg-dark-secondary', : 'hover:bg-light-secondary dark:hover:bg-dark-secondary',
)} )}
@@ -158,8 +157,9 @@ const ModelSelector = () => {
size={15} size={15}
className={cn( className={cn(
'shrink-0', 'shrink-0',
selectedModel?.providerId === provider.id && chatModelProvider?.providerId ===
selectedModel?.modelKey === model.key provider.id &&
chatModelProvider?.key === model.key
? 'text-sky-500' ? 'text-sky-500'
: 'text-black/50 dark:text-white/50 group-hover:text-black/70 group-hover:dark:text-white/70', : 'text-black/50 dark:text-white/50 group-hover:text-black/70 group-hover:dark:text-white/70',
)} )}
@@ -167,8 +167,9 @@ const ModelSelector = () => {
<p <p
className={cn( className={cn(
'text-sm truncate', 'text-sm truncate',
selectedModel?.providerId === provider.id && chatModelProvider?.providerId ===
selectedModel?.modelKey === model.key provider.id &&
chatModelProvider?.key === model.key
? 'text-sky-500 font-medium' ? 'text-sky-500 font-medium'
: 'text-black/70 dark:text-white/70 group-hover:text-black dark:group-hover:text-white', : 'text-black/70 dark:text-white/70 group-hover:text-black dark:group-hover:text-white',
)} )}
@@ -176,7 +177,7 @@ const ModelSelector = () => {
{model.name} {model.name}
</p> </p>
</div> </div>
</PopoverButton> </button>
))} ))}
</div> </div>

View File

@@ -112,100 +112,96 @@ const ModelProvider = ({
> >
<div className="border-t border-light-200 dark:border-dark-200" /> <div className="border-t border-light-200 dark:border-dark-200" />
<div className="flex flex-col gap-y-4 px-5 py-4"> <div className="flex flex-col gap-y-4 px-5 py-4">
{modelProvider.chatModels.length > 0 && ( <div className="flex flex-col gap-y-2">
<div className="flex flex-col gap-y-2"> <div className="flex flex-row w-full justify-between items-center">
<div className="flex flex-row w-full justify-between items-center"> <p className="text-[11px] lg:text-xs text-black/70 dark:text-white/70">
<p className="text-[11px] lg:text-xs text-black/70 dark:text-white/70"> Chat models
Chat models </p>
</p> <AddModel
<AddModel providerId={modelProvider.id}
providerId={modelProvider.id} setProviders={setProviders}
setProviders={setProviders} type="chat"
type="chat" />
/>
</div>
<div className="flex flex-col gap-2">
{modelProvider.chatModels.some((m) => m.key === 'error') ? (
<div className="flex flex-row items-center gap-2 text-xs lg:text-sm text-red-500 dark:text-red-400 rounded-lg bg-red-50 dark:bg-red-950/20 px-3 py-2 border border-red-200 dark:border-red-900/30">
<AlertCircle size={16} className="shrink-0" />
<span className="break-words">
{
modelProvider.chatModels.find(
(m) => m.key === 'error',
)?.name
}
</span>
</div>
) : (
<div className="flex flex-row flex-wrap gap-2">
{modelProvider.chatModels.map((model, index) => (
<div
key={`${modelProvider.id}-chat-${model.key}-${index}`}
className="flex flex-row items-center space-x-1 text-xs lg:text-sm text-black/70 dark:text-white/70 rounded-lg bg-light-secondary dark:bg-dark-secondary px-3 py-1.5"
>
<span>{model.name}</span>
<button
onClick={() => {
handleModelDelete('chat', model.key);
}}
>
<X size={12} />
</button>
</div>
))}
</div>
)}
</div>
</div> </div>
)} <div className="flex flex-col gap-2">
{modelProvider.embeddingModels.length > 0 && ( {modelProvider.chatModels.some((m) => m.key === 'error') ? (
<div className="flex flex-col gap-y-2"> <div className="flex flex-row items-center gap-2 text-xs lg:text-sm text-red-500 dark:text-red-400 rounded-lg bg-red-50 dark:bg-red-950/20 px-3 py-2 border border-red-200 dark:border-red-900/30">
<div className="flex flex-row w-full justify-between items-center"> <AlertCircle size={16} className="shrink-0" />
<p className="text-[11px] lg:text-xs text-black/70 dark:text-white/70"> <span className="break-words">
Embedding models {
</p> modelProvider.chatModels.find(
<AddModel (m) => m.key === 'error',
providerId={modelProvider.id} )?.name
setProviders={setProviders} }
type="embedding" </span>
/> </div>
</div> ) : (
<div className="flex flex-col gap-2"> <div className="flex flex-row flex-wrap gap-2">
{modelProvider.embeddingModels.some( {modelProvider.chatModels.map((model, index) => (
(m) => m.key === 'error', <div
) ? ( key={`${modelProvider.id}-chat-${model.key}-${index}`}
<div className="flex flex-row items-center gap-2 text-xs lg:text-sm text-red-500 dark:text-red-400 rounded-lg bg-red-50 dark:bg-red-950/20 px-3 py-2 border border-red-200 dark:border-red-900/30"> className="flex flex-row items-center space-x-1 text-xs lg:text-sm text-black/70 dark:text-white/70 rounded-lg bg-light-secondary dark:bg-dark-secondary px-3 py-1.5"
<AlertCircle size={16} className="shrink-0" /> >
<span className="break-words"> <span>{model.name}</span>
{ <button
modelProvider.embeddingModels.find( onClick={() => {
(m) => m.key === 'error', handleModelDelete('chat', model.key);
)?.name }}
}
</span>
</div>
) : (
<div className="flex flex-row flex-wrap gap-2">
{modelProvider.embeddingModels.map((model, index) => (
<div
key={`${modelProvider.id}-embedding-${model.key}-${index}`}
className="flex flex-row items-center space-x-1 text-xs lg:text-sm text-black/70 dark:text-white/70 rounded-lg bg-light-secondary dark:bg-dark-secondary px-3 py-1.5"
> >
<span>{model.name}</span> <X size={12} />
<button </button>
onClick={() => { </div>
handleModelDelete('embedding', model.key); ))}
}} </div>
> )}
<X size={12} />
</button>
</div>
))}
</div>
)}
</div>
</div> </div>
)} </div>
<div className="flex flex-col gap-y-2">
<div className="flex flex-row w-full justify-between items-center">
<p className="text-[11px] lg:text-xs text-black/70 dark:text-white/70">
Embedding models
</p>
<AddModel
providerId={modelProvider.id}
setProviders={setProviders}
type="embedding"
/>
</div>
<div className="flex flex-col gap-2">
{modelProvider.embeddingModels.some(
(m) => m.key === 'error',
) ? (
<div className="flex flex-row items-center gap-2 text-xs lg:text-sm text-red-500 dark:text-red-400 rounded-lg bg-red-50 dark:bg-red-950/20 px-3 py-2 border border-red-200 dark:border-red-900/30">
<AlertCircle size={16} className="shrink-0" />
<span className="break-words">
{
modelProvider.embeddingModels.find(
(m) => m.key === 'error',
)?.name
}
</span>
</div>
) : (
<div className="flex flex-row flex-wrap gap-2">
{modelProvider.embeddingModels.map((model, index) => (
<div
key={`${modelProvider.id}-embedding-${model.key}-${index}`}
className="flex flex-row items-center space-x-1 text-xs lg:text-sm text-black/70 dark:text-white/70 rounded-lg bg-light-secondary dark:bg-dark-secondary px-3 py-1.5"
>
<span>{model.name}</span>
<button
onClick={() => {
handleModelDelete('embedding', model.key);
}}
>
<X size={12} />
</button>
</div>
))}
</div>
)}
</div>
</div>
</div> </div>
</motion.div> </motion.div>
)} )}

View File

@@ -1,5 +1,6 @@
import Select from '@/components/ui/Select'; import Select from '@/components/ui/Select';
import { ConfigModelProvider } from '@/lib/config/types'; import { ConfigModelProvider } from '@/lib/config/types';
import { useChat } from '@/lib/hooks/useChat';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
@@ -16,6 +17,7 @@ const ModelSelect = ({
: `${localStorage.getItem('embeddingModelProviderId')}/${localStorage.getItem('embeddingModelKey')}`, : `${localStorage.getItem('embeddingModelProviderId')}/${localStorage.getItem('embeddingModelKey')}`,
); );
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { setChatModelProvider, setEmbeddingModelProvider } = useChat();
const handleSave = async (newValue: string) => { const handleSave = async (newValue: string) => {
setLoading(true); setLoading(true);
@@ -23,20 +25,27 @@ const ModelSelect = ({
try { try {
if (type === 'chat') { if (type === 'chat') {
localStorage.setItem('chatModelProviderId', newValue.split('/')[0]); const providerId = newValue.split('/')[0];
localStorage.setItem( const modelKey = newValue.split('/').slice(1).join('/');
'chatModelKey',
newValue.split('/').slice(1).join('/'), localStorage.setItem('chatModelProviderId', providerId);
); localStorage.setItem('chatModelKey', modelKey);
setChatModelProvider({
providerId: providerId,
key: modelKey,
});
} else { } else {
localStorage.setItem( const providerId = newValue.split('/')[0];
'embeddingModelProviderId', const modelKey = newValue.split('/').slice(1).join('/');
newValue.split('/')[0],
); localStorage.setItem('embeddingModelProviderId', providerId);
localStorage.setItem( localStorage.setItem('embeddingModelKey', modelKey);
'embeddingModelKey',
newValue.split('/').slice(1).join('/'), setEmbeddingModelProvider({
); providerId: providerId,
key: modelKey,
});
} }
} catch (error) { } catch (error) {
console.error('Error saving config:', error); console.error('Error saving config:', error);

View File

@@ -9,7 +9,7 @@ const SettingsButtonMobile = () => {
return ( return (
<> <>
<button className="lg:hidden" onClick={() => setIsOpen(true)}> <button className="lg:hidden" onClick={() => setIsOpen(true)}>
<Settings size={18}/> <Settings size={18} />
</button> </button>
<AnimatePresence> <AnimatePresence>
{isOpen && <SettingsDialogue isOpen={isOpen} setIsOpen={setIsOpen} />} {isOpen && <SettingsDialogue isOpen={isOpen} setIsOpen={setIsOpen} />}

View File

@@ -63,8 +63,7 @@ const SetupConfig = ({
} }
}; };
const hasProviders = const hasProviders = providers.length > 0;
providers.filter((p) => p.chatModels.length > 0).length > 0;
return ( return (
<div className="w-[95vw] md:w-[80vw] lg:w-[65vw] mx-auto px-2 sm:px-4 md:px-6 flex flex-col space-y-6"> <div className="w-[95vw] md:w-[80vw] lg:w-[65vw] mx-auto px-2 sm:px-4 md:px-6 flex flex-col space-y-6">

View File

@@ -17,7 +17,7 @@ import {
useState, useState,
} from 'react'; } from 'react';
import crypto from 'crypto'; import crypto from 'crypto';
import { useSearchParams } from 'next/navigation'; import { useParams, useSearchParams } from 'next/navigation';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { getSuggestions } from '../actions'; import { getSuggestions } from '../actions';
import { MinimalProvider } from '../models/types'; import { MinimalProvider } from '../models/types';
@@ -48,6 +48,8 @@ type ChatContext = {
messageAppeared: boolean; messageAppeared: boolean;
isReady: boolean; isReady: boolean;
hasError: boolean; hasError: boolean;
chatModelProvider: ChatModelProvider;
embeddingModelProvider: EmbeddingModelProvider;
setOptimizationMode: (mode: string) => void; setOptimizationMode: (mode: string) => void;
setFocusMode: (mode: string) => void; setFocusMode: (mode: string) => void;
setFiles: (files: File[]) => void; setFiles: (files: File[]) => void;
@@ -58,6 +60,8 @@ type ChatContext = {
rewrite?: boolean, rewrite?: boolean,
) => Promise<void>; ) => Promise<void>;
rewrite: (messageId: string) => void; rewrite: (messageId: string) => void;
setChatModelProvider: (provider: ChatModelProvider) => void;
setEmbeddingModelProvider: (provider: EmbeddingModelProvider) => void;
}; };
export interface File { export interface File {
@@ -256,25 +260,24 @@ export const chatContext = createContext<ChatContext>({
sections: [], sections: [],
notFound: false, notFound: false,
optimizationMode: '', optimizationMode: '',
chatModelProvider: { key: '', providerId: '' },
embeddingModelProvider: { key: '', providerId: '' },
rewrite: () => {}, rewrite: () => {},
sendMessage: async () => {}, sendMessage: async () => {},
setFileIds: () => {}, setFileIds: () => {},
setFiles: () => {}, setFiles: () => {},
setFocusMode: () => {}, setFocusMode: () => {},
setOptimizationMode: () => {}, setOptimizationMode: () => {},
setChatModelProvider: () => {},
setEmbeddingModelProvider: () => {},
}); });
export const ChatProvider = ({ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
children, const params: { chatId: string } = useParams();
id,
}: {
children: React.ReactNode;
id?: string;
}) => {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const initialMessage = searchParams.get('q'); const initialMessage = searchParams.get('q');
const [chatId, setChatId] = useState<string | undefined>(id); const [chatId, setChatId] = useState<string | undefined>(params.chatId);
const [newChatCreated, setNewChatCreated] = useState(false); const [newChatCreated, setNewChatCreated] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -443,6 +446,19 @@ export const ChatProvider = ({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
useEffect(() => {
if (params.chatId && params.chatId !== chatId) {
setChatId(params.chatId);
setMessages([]);
setChatHistory([]);
setFiles([]);
setFileIds([]);
setIsMessagesLoaded(false);
setNotFound(false);
setNewChatCreated(false);
}
}, [params.chatId, chatId]);
useEffect(() => { useEffect(() => {
if ( if (
chatId && chatId &&
@@ -466,7 +482,7 @@ export const ChatProvider = ({
setChatId(crypto.randomBytes(20).toString('hex')); setChatId(crypto.randomBytes(20).toString('hex'));
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [chatId, isMessagesLoaded, newChatCreated, messages.length]);
useEffect(() => { useEffect(() => {
messagesRef.current = messages; messagesRef.current = messages;
@@ -519,7 +535,7 @@ export const ChatProvider = ({
messageId, messageId,
rewrite = false, rewrite = false,
) => { ) => {
if (loading) return; if (loading || !message) return;
setLoading(true); setLoading(true);
setMessageAppeared(false); setMessageAppeared(false);
@@ -743,6 +759,10 @@ export const ChatProvider = ({
setOptimizationMode, setOptimizationMode,
rewrite, rewrite,
sendMessage, sendMessage,
setChatModelProvider,
chatModelProvider,
embeddingModelProvider,
setEmbeddingModelProvider,
}} }}
> >
{children} {children}

View File

@@ -1,76 +0,0 @@
import { Embeddings, type EmbeddingsParams } from '@langchain/core/embeddings';
import { chunkArray } from '@langchain/core/utils/chunk_array';
export interface HuggingFaceTransformersEmbeddingsParams
extends EmbeddingsParams {
modelName: string;
model: string;
timeout?: number;
batchSize?: number;
stripNewLines?: boolean;
}
export class HuggingFaceTransformersEmbeddings
extends Embeddings
implements HuggingFaceTransformersEmbeddingsParams
{
modelName = 'Xenova/all-MiniLM-L6-v2';
model = 'Xenova/all-MiniLM-L6-v2';
batchSize = 512;
stripNewLines = true;
timeout?: number;
constructor(fields?: Partial<HuggingFaceTransformersEmbeddingsParams>) {
super(fields ?? {});
this.modelName = fields?.model ?? fields?.modelName ?? this.model;
this.model = this.modelName;
this.stripNewLines = fields?.stripNewLines ?? this.stripNewLines;
this.timeout = fields?.timeout;
}
async embedDocuments(texts: string[]): Promise<number[][]> {
const batches = chunkArray(
this.stripNewLines ? texts.map((t) => t.replace(/\n/g, ' ')) : texts,
this.batchSize,
);
const batchRequests = batches.map((batch) => this.runEmbedding(batch));
const batchResponses = await Promise.all(batchRequests);
const embeddings: number[][] = [];
for (let i = 0; i < batchResponses.length; i += 1) {
const batchResponse = batchResponses[i];
for (let j = 0; j < batchResponse.length; j += 1) {
embeddings.push(batchResponse[j]);
}
}
return embeddings;
}
async embedQuery(text: string): Promise<number[]> {
const data = await this.runEmbedding([
this.stripNewLines ? text.replace(/\n/g, ' ') : text,
]);
return data[0];
}
private async runEmbedding(texts: string[]) {
const { pipeline } = await import('@huggingface/transformers');
const pipe = await pipeline('feature-extraction', this.model);
return this.caller.call(async () => {
const output = await pipe(texts, { pooling: 'mean', normalize: true });
return output.tolist();
});
}
}

View File

@@ -4,8 +4,7 @@ import BaseModelProvider from './baseProvider';
import { Embeddings } from '@langchain/core/embeddings'; import { Embeddings } from '@langchain/core/embeddings';
import { UIConfigField } from '@/lib/config/types'; import { UIConfigField } from '@/lib/config/types';
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry'; import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
import { HuggingFaceTransformersEmbeddings } from '@/lib/huggingfaceTransformer'; import { HuggingFaceTransformersEmbeddings } from '@langchain/community/embeddings/huggingface_transformers';
interface TransformersConfig {} interface TransformersConfig {}
const defaultEmbeddingModels: Model[] = [ const defaultEmbeddingModels: Model[] = [