Compare commits

...

6 Commits

Author SHA1 Message Date
ItzCrazyKns
cf95ea0af7 feat(app): lint & beautify 2025-12-23 18:54:01 +05:30
ItzCrazyKns
24c32ed881 feat(app): enhance attach transition 2025-12-23 18:53:40 +05:30
ItzCrazyKns
b47f522bf2 feat(app): update guide for run command 2025-12-23 18:40:30 +05:30
ItzCrazyKns
ea18c13326 feat(app): remove uploads 2025-12-23 18:38:25 +05:30
ItzCrazyKns
b706434bac feat(chat-window): display only when ready 2025-12-23 17:56:15 +05:30
ItzCrazyKns
2c65bd916b feat(chat-hook): set ready before reconnecting 2025-12-23 17:29:14 +05:30
10 changed files with 209 additions and 225 deletions

View File

@@ -81,7 +81,7 @@ 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 -d -p 3000:3000 -v perplexica-data:/home/perplexica/data -v perplexica-uploads:/home/perplexica/uploads --name perplexica itzcrazykns1337/perplexica:latest docker run -d -p 3000:3000 -v perplexica-data:/home/perplexica/data --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.
@@ -93,7 +93,7 @@ This will pull and start the Perplexica container with the bundled SearxNG searc
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 -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 docker run -d -p 3000:3000 -e SEARXNG_API_URL=http://your-searxng-url:8080 -v perplexica-data:/home/perplexica/data --name perplexica itzcrazykns1337/perplexica:slim-latest
``` ```
**Important**: Make sure your SearxNG instance has: **Important**: Make sure your SearxNG instance has:
@@ -120,7 +120,7 @@ If you prefer to build from source or need more control:
```bash ```bash
docker build -t perplexica . docker build -t perplexica .
docker run -d -p 3000:3000 -v perplexica-data:/home/perplexica/data -v perplexica-uploads:/home/perplexica/uploads --name perplexica perplexica docker run -d -p 3000:3000 -v perplexica-data:/home/perplexica/data --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.

View File

@@ -1,6 +1,8 @@
services: services:
perplexica: perplexica:
image: itzcrazykns1337/perplexica:latest image: itzcrazykns1337/perplexica:latest
build:
context: .
ports: ports:
- '3000:3000' - '3000:3000'
volumes: volumes:

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 -d -p 3000:3000 -v perplexica-data:/home/perplexica/data -v perplexica-uploads:/home/perplexica/uploads --name perplexica itzcrazykns1337/perplexica:latest docker run -d -p 3000:3000 -v perplexica-data:/home/perplexica/data --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 -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 docker run -d -p 3000:3000 -e SEARXNG_API_URL=http://your-searxng-url:8080 -v perplexica-data:/home/perplexica/data --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,10 +1,5 @@
'use client'; 'use client';
import ChatWindow from '@/components/ChatWindow'; import ChatWindow from '@/components/ChatWindow';
import React from 'react';
const Page = () => { export default ChatWindow;
return <ChatWindow />;
};
export default Page;

View File

@@ -6,7 +6,8 @@ import EmptyChat from './EmptyChat';
import NextError from 'next/error'; import NextError from 'next/error';
import { useChat } from '@/lib/hooks/useChat'; import { useChat } from '@/lib/hooks/useChat';
import SettingsButtonMobile from './Settings/SettingsButtonMobile'; import SettingsButtonMobile from './Settings/SettingsButtonMobile';
import { Block, Chunk } from '@/lib/types'; import { Block } from '@/lib/types';
import Loader from './ui/Loader';
export interface BaseMessage { export interface BaseMessage {
chatId: string; chatId: string;
@@ -21,35 +22,6 @@ export interface Message extends BaseMessage {
status: 'answering' | 'completed' | 'error'; status: 'answering' | 'completed' | 'error';
} }
export interface UserMessage extends BaseMessage {
role: 'user';
content: string;
}
export interface AssistantMessage extends BaseMessage {
role: 'assistant';
content: string;
suggestions?: string[];
}
export interface SourceMessage extends BaseMessage {
role: 'source';
sources: Chunk[];
}
export interface SuggestionMessage extends BaseMessage {
role: 'suggestion';
suggestions: string[];
}
export type LegacyMessage =
| AssistantMessage
| UserMessage
| SourceMessage
| SuggestionMessage;
export type ChatTurn = UserMessage | AssistantMessage;
export interface File { export interface File {
fileName: string; fileName: string;
fileExtension: string; fileExtension: string;
@@ -62,7 +34,8 @@ export interface Widget {
} }
const ChatWindow = () => { const ChatWindow = () => {
const { hasError, notFound, messages } = useChat(); const { hasError, notFound, messages, isReady } = useChat();
if (hasError) { if (hasError) {
return ( return (
<div className="relative"> <div className="relative">
@@ -78,18 +51,24 @@ const ChatWindow = () => {
); );
} }
return notFound ? ( return isReady ? (
<NextError statusCode={404} /> notFound ? (
<NextError statusCode={404} />
) : (
<div>
{messages.length > 0 ? (
<>
<Navbar />
<Chat />
</>
) : (
<EmptyChat />
)}
</div>
)
) : ( ) : (
<div> <div className="flex items-center justify-center min-h-screen w-full">
{messages.length > 0 ? ( <Loader />
<>
<Navbar />
<Chat />
</>
) : (
<EmptyChat />
)}
</div> </div>
); );
}; };

View File

@@ -16,6 +16,8 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { Fragment, useRef, useState } from 'react'; import { Fragment, useRef, useState } from 'react';
import { useChat } from '@/lib/hooks/useChat'; import { useChat } from '@/lib/hooks/useChat';
import { AnimatePresence } from 'motion/react';
import { motion } from 'framer-motion';
const Attach = () => { const Attach = () => {
const { files, setFiles, setFileIds, fileIds } = useChat(); const { files, setFiles, setFileIds, fileIds } = useChat();
@@ -53,86 +55,95 @@ const Attach = () => {
return loading ? ( return loading ? (
<div className="active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none text-black/50 dark:text-white/50 transition duration-200"> <div className="active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none text-black/50 dark:text-white/50 transition duration-200">
<LoaderCircle size={16} className="text-sky-400 animate-spin" /> <LoaderCircle size={16} className="text-sky-500 animate-spin" />
</div> </div>
) : files.length > 0 ? ( ) : files.length > 0 ? (
<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 {({ open }) => (
type="button" <>
className="active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none headless-open:text-black dark:headless-open:text-white text-black/50 dark:text-white/50 active:scale-95 transition duration-200 hover:text-black dark:hover:text-white" <PopoverButton
> type="button"
<File size={16} className="text-sky-400" /> className="active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none headless-open:text-black dark:headless-open:text-white text-black/50 dark:text-white/50 active:scale-95 transition duration-200 hover:text-black dark:hover:text-white"
</PopoverButton> >
<Transition <File size={16} className="text-sky-500" />
as={Fragment} </PopoverButton>
enter="transition ease-out duration-150" <AnimatePresence>
enterFrom="opacity-0 translate-y-1" {open && (
enterTo="opacity-100 translate-y-0" <PopoverPanel
leave="transition ease-in duration-150" className="absolute z-10 w-64 md:w-[350px] right-0"
leaveFrom="opacity-100 translate-y-0" static
leaveTo="opacity-0 translate-y-1" >
> <motion.div
<PopoverPanel className="absolute z-10 w-64 md:w-[350px] right-0"> initial={{ opacity: 0, scale: 0.9 }}
<div className="bg-light-primary dark:bg-dark-primary border rounded-md border-light-200 dark:border-dark-200 w-full max-h-[200px] md:max-h-none overflow-y-auto flex flex-col"> animate={{ opacity: 1, scale: 1 }}
<div className="flex flex-row items-center justify-between px-3 py-2"> exit={{ opacity: 0, scale: 0.9 }}
<h4 className="text-black dark:text-white font-medium text-sm"> transition={{ duration: 0.1, ease: 'easeOut' }}
Attached files className="origin-top-right bg-light-primary dark:bg-dark-primary border rounded-md border-light-200 dark:border-dark-200 w-full max-h-[200px] md:max-h-none overflow-y-auto flex flex-col"
</h4>
<div className="flex flex-row items-center space-x-4">
<button
type="button"
onClick={() => fileInputRef.current.click()}
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200 focus:outline-none"
> >
<input <div className="flex flex-row items-center justify-between px-3 py-2">
type="file" <h4 className="text-black/70 dark:text-white/70 text-sm">
onChange={handleChange} Attached files
ref={fileInputRef} </h4>
accept=".pdf,.docx,.txt" <div className="flex flex-row items-center space-x-4">
multiple <button
hidden type="button"
/> onClick={() => fileInputRef.current.click()}
<Plus size={16} /> className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200 focus:outline-none"
<p className="text-xs">Add</p> >
</button> <input
<button type="file"
onClick={() => { onChange={handleChange}
setFiles([]); ref={fileInputRef}
setFileIds([]); accept=".pdf,.docx,.txt"
}} multiple
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200 focus:outline-none" hidden
> />
<Trash size={14} /> <Plus size={16} />
<p className="text-xs">Clear</p> <p className="text-xs">Add</p>
</button> </button>
</div> <button
</div> onClick={() => {
<div className="h-[0.5px] mx-2 bg-white/10" /> setFiles([]);
<div className="flex flex-col items-center"> setFileIds([]);
{files.map((file, i) => ( }}
<div className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200 focus:outline-none"
key={i} >
className="flex flex-row items-center justify-start w-full space-x-3 p-3" <Trash size={13} />
> <p className="text-xs">Clear</p>
<div className="bg-light-100 dark:bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md"> </button>
<File </div>
size={16}
className="text-black/70 dark:text-white/70"
/>
</div> </div>
<p className="text-black/70 dark:text-white/70 text-sm"> <div className="h-[0.5px] mx-2 bg-white/10" />
{file.fileName.length > 25 <div className="flex flex-col items-center">
? file.fileName.replace(/\.\w+$/, '').substring(0, 25) + {files.map((file, i) => (
'...' + <div
file.fileExtension key={i}
: file.fileName} className="flex flex-row items-center justify-start w-full space-x-3 p-3"
</p> >
</div> <div className="bg-light-100 dark:bg-dark-100 flex items-center justify-center w-9 h-9 rounded-md">
))} <File
</div> size={16}
</div> className="text-black/70 dark:text-white/70"
</PopoverPanel> />
</Transition> </div>
<p className="text-black/70 dark:text-white/70 text-xs">
{file.fileName.length > 25
? file.fileName
.replace(/\.\w+$/, '')
.substring(0, 25) +
'...' +
file.fileExtension
: file.fileName}
</p>
</div>
))}
</div>
</motion.div>
</PopoverPanel>
)}
</AnimatePresence>
</>
)}
</Popover> </Popover>
) : ( ) : (
<button <button

View File

@@ -1,21 +1,14 @@
import { cn } from '@/lib/utils';
import { import {
Popover, Popover,
PopoverButton, PopoverButton,
PopoverPanel, PopoverPanel,
Transition, Transition,
} from '@headlessui/react'; } from '@headlessui/react';
import { import { File, LoaderCircle, Paperclip, Plus, Trash } from 'lucide-react';
CopyPlus,
File,
LoaderCircle,
Paperclip,
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'; import { useChat } from '@/lib/hooks/useChat';
import { AnimatePresence } from 'motion/react';
import { motion } from 'framer-motion';
const AttachSmall = () => { const AttachSmall = () => {
const { files, setFiles, setFileIds, fileIds } = useChat(); const { files, setFiles, setFileIds, fileIds } = useChat();
@@ -53,86 +46,95 @@ const AttachSmall = () => {
return loading ? ( return loading ? (
<div className="flex flex-row items-center justify-between space-x-1 p-1 "> <div className="flex flex-row items-center justify-between space-x-1 p-1 ">
<LoaderCircle size={20} className="text-sky-400 animate-spin" /> <LoaderCircle size={20} className="text-sky-500 animate-spin" />
</div> </div>
) : files.length > 0 ? ( ) : files.length > 0 ? (
<Popover className="max-w-[15rem] md:max-w-md lg:max-w-lg"> <Popover className="max-w-[15rem] md:max-w-md lg:max-w-lg">
<PopoverButton {({ open }) => (
type="button" <>
className="flex flex-row items-center justify-between space-x-1 p-1 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white" <PopoverButton
> type="button"
<File size={20} className="text-sky-400" /> className="flex flex-row items-center justify-between space-x-1 p-1 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white"
</PopoverButton> >
<Transition <File size={20} className="text-sky-500" />
as={Fragment} </PopoverButton>
enter="transition ease-out duration-150" <AnimatePresence>
enterFrom="opacity-0 translate-y-1" {open && (
enterTo="opacity-100 translate-y-0" <PopoverPanel
leave="transition ease-in duration-150" className="absolute z-10 w-64 md:w-[350px] bottom-14"
leaveFrom="opacity-100 translate-y-0" static
leaveTo="opacity-0 translate-y-1" >
> <motion.div
<PopoverPanel className="absolute z-10 w-64 md:w-[350px] bottom-14 -ml-3"> initial={{ opacity: 0, scale: 0.9 }}
<div className="bg-light-primary dark:bg-dark-primary border rounded-md border-light-200 dark:border-dark-200 w-full max-h-[200px] md:max-h-none overflow-y-auto flex flex-col"> animate={{ opacity: 1, scale: 1 }}
<div className="flex flex-row items-center justify-between px-3 py-2"> exit={{ opacity: 0, scale: 0.9 }}
<h4 className="text-black dark:text-white font-medium text-sm"> transition={{ duration: 0.1, ease: 'easeOut' }}
Attached files className="origin-bottom-left bg-light-primary dark:bg-dark-primary border rounded-md border-light-200 dark:border-dark-200 w-full max-h-[200px] md:max-h-none overflow-y-auto flex flex-col"
</h4>
<div className="flex flex-row items-center space-x-4">
<button
type="button"
onClick={() => fileInputRef.current.click()}
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
> >
<input <div className="flex flex-row items-center justify-between px-3 py-2">
type="file" <h4 className="text-black/70 dark:text-white/70 font-medium text-sm">
onChange={handleChange} Attached files
ref={fileInputRef} </h4>
accept=".pdf,.docx,.txt" <div className="flex flex-row items-center space-x-4">
multiple <button
hidden type="button"
/> onClick={() => fileInputRef.current.click()}
<Plus size={18} /> className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
<p className="text-xs">Add</p> >
</button> <input
<button type="file"
onClick={() => { onChange={handleChange}
setFiles([]); ref={fileInputRef}
setFileIds([]); accept=".pdf,.docx,.txt"
}} multiple
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200" hidden
> />
<Trash size={14} /> <Plus size={16} />
<p className="text-xs">Clear</p> <p className="text-xs">Add</p>
</button> </button>
</div> <button
</div> onClick={() => {
<div className="h-[0.5px] mx-2 bg-white/10" /> setFiles([]);
<div className="flex flex-col items-center"> setFileIds([]);
{files.map((file, i) => ( }}
<div className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
key={i} >
className="flex flex-row items-center justify-start w-full space-x-3 p-3" <Trash size={13} />
> <p className="text-xs">Clear</p>
<div className="bg-light-100 dark:bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md"> </button>
<File </div>
size={16}
className="text-black/70 dark:text-white/70"
/>
</div> </div>
<p className="text-black/70 dark:text-white/70 text-sm"> <div className="h-[0.5px] mx-2 bg-white/10" />
{file.fileName.length > 25 <div className="flex flex-col items-center">
? file.fileName.replace(/\.\w+$/, '').substring(0, 25) + {files.map((file, i) => (
'...' + <div
file.fileExtension key={i}
: file.fileName} className="flex flex-row items-center justify-start w-full space-x-3 p-3"
</p> >
</div> <div className="bg-light-100 dark:bg-dark-100 flex items-center justify-center w-9 h-9 rounded-md">
))} <File
</div> size={16}
</div> className="text-black/70 dark:text-white/70"
</PopoverPanel> />
</Transition> </div>
<p className="text-black/70 dark:text-white/70 text-xs">
{file.fileName.length > 25
? file.fileName
.replace(/\.\w+$/, '')
.substring(0, 25) +
'...' +
file.fileExtension
: file.fileName}
</p>
</div>
))}
</div>
</motion.div>
</PopoverPanel>
)}
</AnimatePresence>
</>
)}
</Popover> </Popover>
) : ( ) : (
<button <button

View File

@@ -86,12 +86,12 @@ const Optimization = () => {
)} )}
> >
<div className="flex flex-row justify-between w-full text-black dark:text-white"> <div className="flex flex-row justify-between w-full text-black dark:text-white">
<div className='flex flex-row space-x-1'> <div className="flex flex-row space-x-1">
{mode.icon} {mode.icon}
<p className="text-xs font-medium">{mode.title}</p> <p className="text-xs font-medium">{mode.title}</p>
</div> </div>
{mode.key === 'quality' && ( {mode.key === 'quality' && (
<span className='bg-sky-500/70 dark:bg-sky-500/40 border border-sky-600 px-1 rounded-full text-[10px] text-white'> <span className="bg-sky-500/70 dark:bg-sky-500/40 border border-sky-600 px-1 rounded-full text-[10px] text-white">
Beta Beta
</span> </span>
)} )}

View File

@@ -269,6 +269,7 @@ export const chatContext = createContext<ChatContext>({
export const ChatProvider = ({ children }: { children: React.ReactNode }) => { export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
const params: { chatId: string } = useParams(); const params: { chatId: string } = useParams();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const initialMessage = searchParams.get('q'); const initialMessage = searchParams.get('q');
@@ -402,6 +403,9 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
}, [messages]); }, [messages]);
const checkReconnect = async () => { const checkReconnect = async () => {
setIsReady(true);
console.debug(new Date(), 'app:ready');
if (messages.length > 0) { if (messages.length > 0) {
const lastMsg = messages[messages.length - 1]; const lastMsg = messages[messages.length - 1];
@@ -502,14 +506,7 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
setIsReady(true); setIsReady(true);
console.debug(new Date(), 'app:ready'); console.debug(new Date(), 'app:ready');
} else if (isMessagesLoaded && isConfigReady && !newChatCreated) { } else if (isMessagesLoaded && isConfigReady && !newChatCreated) {
checkReconnect() checkReconnect();
.then(() => {
setIsReady(true);
console.debug(new Date(), 'app:ready');
})
.catch((err) => {
console.error('Error during reconnect:', err);
});
} else { } else {
setIsReady(false); setIsReady(false);
} }

2
uploads/.gitignore vendored
View File

@@ -1,2 +0,0 @@
*
!.gitignore