mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-11-21 20:48:14 +00:00
Compare commits
7 Commits
3e03947b1b
...
8f22d9f626
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f22d9f626 | ||
|
|
3564bcc48c | ||
|
|
e471cb5da1 | ||
|
|
836cfb80c8 | ||
|
|
9d0e2e7f7c | ||
|
|
df1ed5f0f9 | ||
|
|
f55c2371fe |
@@ -15,6 +15,7 @@ services:
|
||||
context: .
|
||||
dockerfile: app.dockerfile
|
||||
environment:
|
||||
- DOCKER=true
|
||||
- SEARXNG_API_URL=http://searxng:8080
|
||||
- DATA_DIR=/home/perplexica
|
||||
ports:
|
||||
@@ -24,7 +25,6 @@ services:
|
||||
volumes:
|
||||
- backend-dbstore:/home/perplexica/data
|
||||
- uploads:/home/perplexica/uploads
|
||||
- ./config.toml:/home/perplexica/config.toml
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
[GENERAL]
|
||||
SIMILARITY_MEASURE = "cosine" # "cosine" or "dot"
|
||||
KEEP_ALIVE = "5m" # How long to keep Ollama models loaded into memory. (Instead of using -1 use "-1m")
|
||||
|
||||
[MODELS.OPENAI]
|
||||
API_KEY = ""
|
||||
|
||||
[MODELS.GROQ]
|
||||
API_KEY = ""
|
||||
|
||||
[MODELS.ANTHROPIC]
|
||||
API_KEY = ""
|
||||
|
||||
[MODELS.GEMINI]
|
||||
API_KEY = ""
|
||||
|
||||
[MODELS.CUSTOM_OPENAI]
|
||||
API_KEY = ""
|
||||
API_URL = ""
|
||||
MODEL_NAME = ""
|
||||
|
||||
[MODELS.OLLAMA]
|
||||
API_URL = "" # Ollama API URL - http://host.docker.internal:11434
|
||||
|
||||
[MODELS.DEEPSEEK]
|
||||
API_KEY = ""
|
||||
|
||||
[MODELS.AIMLAPI]
|
||||
API_KEY = "" # Required to use AI/ML API chat and embedding models
|
||||
|
||||
[MODELS.LM_STUDIO]
|
||||
API_URL = "" # LM Studio API URL - http://host.docker.internal:1234
|
||||
|
||||
[MODELS.LEMONADE]
|
||||
API_URL = "" # Lemonade API URL - http://host.docker.internal:8000
|
||||
API_KEY = "" # Optional API key for Lemonade
|
||||
|
||||
[API_ENDPOINTS]
|
||||
SEARXNG = "" # SearxNG API URL - http://localhost:32768
|
||||
23
src/app/api/config/setup-complete/route.ts
Normal file
23
src/app/api/config/setup-complete/route.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import configManager from '@/lib/config';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
try {
|
||||
configManager.markSetupComplete();
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
message: 'Setup marked as complete.',
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Error marking setup as complete: ', err);
|
||||
return Response.json(
|
||||
{ message: 'An error has occurred.' },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -5,7 +5,7 @@
|
||||
@font-face {
|
||||
font-family: 'PP Editorial';
|
||||
src: url('/fonts/pp-ed-ul.otf') format('opentype');
|
||||
font-weight: 200;
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@@ -18,6 +18,66 @@
|
||||
.overflow-hidden-scrollable::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #e8edf1 transparent; /* light-200 */
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: #e8edf1; /* light-200 */
|
||||
border-radius: 3px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: #d0d7de; /* light-300 */
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
* {
|
||||
scrollbar-color: #21262d transparent; /* dark-200 */
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: #21262d; /* dark-200 */
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: #30363d; /* dark-300 */
|
||||
}
|
||||
}
|
||||
|
||||
:root.dark *,
|
||||
html.dark *,
|
||||
body.dark * {
|
||||
scrollbar-color: #21262d transparent; /* dark-200 */
|
||||
}
|
||||
|
||||
:root.dark *::-webkit-scrollbar-thumb,
|
||||
html.dark *::-webkit-scrollbar-thumb,
|
||||
body.dark *::-webkit-scrollbar-thumb {
|
||||
background: #21262d; /* dark-200 */
|
||||
}
|
||||
|
||||
:root.dark *::-webkit-scrollbar-thumb:hover,
|
||||
html.dark *::-webkit-scrollbar-thumb:hover,
|
||||
body.dark *::-webkit-scrollbar-thumb:hover {
|
||||
background: #30363d; /* dark-300 */
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
@@ -25,6 +85,7 @@
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import type { Metadata } from 'next';
|
||||
import { Montserrat } from 'next/font/google';
|
||||
import './globals.css';
|
||||
@@ -5,6 +7,8 @@ import { cn } from '@/lib/utils';
|
||||
import Sidebar from '@/components/Sidebar';
|
||||
import { Toaster } from 'sonner';
|
||||
import ThemeProvider from '@/components/theme/Provider';
|
||||
import configManager from '@/lib/config';
|
||||
import SetupWizard from '@/components/Setup/SetupWizard';
|
||||
|
||||
const montserrat = Montserrat({
|
||||
weight: ['300', '400', '500', '700'],
|
||||
@@ -24,20 +28,29 @@ export default function RootLayout({
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const setupComplete = configManager.isSetupComplete();
|
||||
const configSections = configManager.getUIConfigSections();
|
||||
|
||||
return (
|
||||
<html className="h-full" lang="en" suppressHydrationWarning>
|
||||
<body className={cn('h-full', montserrat.className)}>
|
||||
<ThemeProvider>
|
||||
<Sidebar>{children}</Sidebar>
|
||||
<Toaster
|
||||
toastOptions={{
|
||||
unstyled: true,
|
||||
classNames: {
|
||||
toast:
|
||||
'bg-light-primary dark:bg-dark-secondary dark:text-white/70 text-black-70 rounded-lg p-4 flex flex-row items-center space-x-2',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{setupComplete ? (
|
||||
<>
|
||||
<Sidebar>{children}</Sidebar>
|
||||
<Toaster
|
||||
toastOptions={{
|
||||
unstyled: true,
|
||||
classNames: {
|
||||
toast:
|
||||
'bg-light-secondary dark:bg-dark-secondary dark:text-white/70 text-black-70 rounded-lg p-4 flex flex-row items-center space-x-2',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<SetupWizard configSections={configSections} />
|
||||
)}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -109,7 +109,7 @@ const ModelSelector = () => {
|
||||
placeholder="Search models..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-9 pr-3 py-2 bg-light-secondary dark:bg-dark-secondary rounded-lg text-sm text-black dark:text-white placeholder:text-black/40 dark:placeholder:text-white/40 focus:outline-none focus:ring-2 focus:ring-sky-500/20 border border-transparent focus:border-sky-500/30 transition duration-200"
|
||||
className="w-full pl-9 pr-3 py-2 bg-light-secondary dark:bg-dark-secondary rounded-lg text-xs text-black dark:text-white placeholder:text-black/40 dark:placeholder:text-white/40 focus:outline-none focus:ring-2 focus:ring-sky-500/20 border border-transparent focus:border-sky-500/30 transition duration-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -96,9 +96,9 @@ const AddProvider = ({
|
||||
<>
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="px-4 py-2 rounded-lg text-sm border border-light-200 dark:border-dark-200 text-black dark:text-white bg-light-secondary/50 dark:bg-dark-secondary/50 hover:bg-light-secondary hover:dark:bg-dark-secondary hover:border-light-300 hover:dark:border-dark-300 flex flex-row items-center space-x-1 active:scale-95 transition duration-200"
|
||||
className="px-3 md:px-4 py-1.5 md:py-2 rounded-lg text-xs sm:text-sm border border-light-200 dark:border-dark-200 text-black dark:text-white bg-light-secondary/50 dark:bg-dark-secondary/50 hover:bg-light-secondary hover:dark:bg-dark-secondary hover:border-light-300 hover:dark:border-dark-300 flex flex-row items-center space-x-1 active:scale-95 transition duration-200"
|
||||
>
|
||||
<Plus size={16} />
|
||||
<Plus className="w-3.5 h-3.5 md:w-4 md:h-4" />
|
||||
<span>Add Provider</span>
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
|
||||
149
src/components/Setup/SetupConfig.tsx
Normal file
149
src/components/Setup/SetupConfig.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import {
|
||||
ConfigModelProvider,
|
||||
UIConfigField,
|
||||
UIConfigSections,
|
||||
} from '@/lib/config/types';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ArrowLeft, ArrowRight, Check } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import AddProvider from '../Settings/Sections/Models/AddProviderDialog';
|
||||
import ModelProvider from '../Settings/Sections/Models/ModelProvider';
|
||||
|
||||
const SetupConfig = ({
|
||||
configSections,
|
||||
setupState,
|
||||
setSetupState,
|
||||
}: {
|
||||
configSections: UIConfigSections;
|
||||
setupState: number;
|
||||
setSetupState: (state: number) => void;
|
||||
}) => {
|
||||
const [providers, setProviders] = useState<ConfigModelProvider[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isFinishing, setIsFinishing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProviders = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await fetch('/api/providers');
|
||||
if (!res.ok) throw new Error('Failed to fetch providers');
|
||||
|
||||
const data = await res.json();
|
||||
setProviders(data.providers || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching providers:', error);
|
||||
toast.error('Failed to load providers');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (setupState === 2) {
|
||||
fetchProviders();
|
||||
}
|
||||
}, [setupState]);
|
||||
|
||||
const handleFinish = async () => {
|
||||
try {
|
||||
setIsFinishing(true);
|
||||
const res = await fetch('/api/config/setup-complete', {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Failed to complete setup');
|
||||
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error('Error completing setup:', error);
|
||||
toast.error('Failed to complete setup');
|
||||
setIsFinishing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const hasProviders = providers.length > 0;
|
||||
|
||||
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">
|
||||
{setupState === 2 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.5, delay: 0.1 },
|
||||
}}
|
||||
className="w-full h-[calc(95vh-80px)] bg-light-primary dark:bg-dark-primary border border-light-200 dark:border-dark-200 rounded-xl shadow-sm flex flex-col overflow-hidden"
|
||||
>
|
||||
<div className="flex-1 overflow-y-auto px-3 sm:px-4 md:px-6 py-4 md:py-6">
|
||||
<div className="flex flex-row justify-between items-center mb-4 md:mb-6 pb-3 md:pb-4 border-b border-light-200 dark:border-dark-200">
|
||||
<div>
|
||||
<p className="text-xs sm:text-sm font-medium text-black dark:text-white">
|
||||
Manage Providers
|
||||
</p>
|
||||
<p className="text-[10px] sm:text-xs text-black/50 dark:text-white/50 mt-0.5">
|
||||
Add and configure your model providers
|
||||
</p>
|
||||
</div>
|
||||
<AddProvider
|
||||
modelProviders={configSections.modelProviders}
|
||||
setProviders={setProviders}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 md:space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8 md:py-12">
|
||||
<p className="text-xs sm:text-sm text-black/50 dark:text-white/50">
|
||||
Loading providers...
|
||||
</p>
|
||||
</div>
|
||||
) : providers.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 md:py-12 text-center">
|
||||
<p className="text-xs sm:text-sm font-medium text-black/70 dark:text-white/70">
|
||||
No providers configured
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
providers.map((provider) => (
|
||||
<ModelProvider
|
||||
key={`provider-${provider.id}`}
|
||||
fields={
|
||||
(configSections.modelProviders.find(
|
||||
(f) => f.key === provider.type,
|
||||
)?.fields ?? []) as UIConfigField[]
|
||||
}
|
||||
modelProvider={provider}
|
||||
setProviders={setProviders}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-row items-center justify-between pt-2">
|
||||
{setupState === 2 && (
|
||||
<motion.button
|
||||
initial={{ opacity: 0, x: 10 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: { duration: 0.5 },
|
||||
}}
|
||||
onClick={handleFinish}
|
||||
disabled={!hasProviders || isLoading || isFinishing}
|
||||
className="flex flex-row items-center gap-1.5 md:gap-2 px-3 md:px-5 py-2 md:py-2.5 rounded-lg bg-[#24A0ED] text-white hover:bg-[#1e8fd1] active:scale-95 transition-all duration-200 font-medium text-xs sm:text-sm disabled:bg-light-200 dark:disabled:bg-dark-200 disabled:text-black/40 dark:disabled:text-white/40 disabled:cursor-not-allowed disabled:active:scale-100"
|
||||
>
|
||||
<span>{isFinishing ? 'Finishing...' : 'Finish'}</span>
|
||||
<Check className="w-4 h-4 md:w-[18px] md:h-[18px]" />
|
||||
</motion.button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupConfig;
|
||||
126
src/components/Setup/SetupWizard.tsx
Normal file
126
src/components/Setup/SetupWizard.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { UIConfigSections } from '@/lib/config/types';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import SetupConfig from './SetupConfig';
|
||||
|
||||
const SetupWizard = ({
|
||||
configSections,
|
||||
}: {
|
||||
configSections: UIConfigSections;
|
||||
}) => {
|
||||
const [showWelcome, setShowWelcome] = useState(true);
|
||||
const [showSetup, setShowSetup] = useState(false);
|
||||
const [setupState, setSetupState] = useState(1);
|
||||
|
||||
const delay = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await delay(2500);
|
||||
setShowWelcome(false);
|
||||
await delay(600);
|
||||
setShowSetup(true);
|
||||
setSetupState(1);
|
||||
await delay(1500);
|
||||
setSetupState(2);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-light-primary dark:bg-dark-primary h-screen w-screen fixed inset-0 overflow-hidden">
|
||||
<AnimatePresence>
|
||||
{showWelcome && (
|
||||
<div className="absolute inset-0 flex items-center justify-center overflow-hidden">
|
||||
<motion.div
|
||||
className="absolute flex flex-col items-center justify-center h-full"
|
||||
initial={{ opacity: 1 }}
|
||||
exit={{ opacity: 0, scale: 1.1 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<motion.h2
|
||||
transition={{ duration: 0.6 }}
|
||||
initial={{ opacity: 0, translateY: '30px' }}
|
||||
animate={{ opacity: 1, translateY: '0px' }}
|
||||
className="text-4xl md:text-6xl xl:text-8xl font-normal font-['Instrument_Serif'] tracking-tight"
|
||||
>
|
||||
Welcome to{' '}
|
||||
<span className="text-[#24A0ED] italic font-['PP_Editorial']">
|
||||
Perplexica
|
||||
</span>
|
||||
</motion.h2>
|
||||
<motion.p
|
||||
transition={{ delay: 0.8, duration: 0.7 }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="text-black/70 dark:text-white/70 text-sm md:text-lg xl:text-2xl mt-2"
|
||||
>
|
||||
<span className="font-light">Web search,</span>{' '}
|
||||
<span className="font-light font-['PP_Editorial'] italic">
|
||||
reimagined
|
||||
</span>
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.5 }}
|
||||
animate={{
|
||||
opacity: 0.2,
|
||||
scale: 1,
|
||||
transition: { delay: 0.8, duration: 0.7 },
|
||||
}}
|
||||
exit={{ opacity: 0, scale: 1.1, transition: { duration: 0.6 } }}
|
||||
className="bg-[#24A0ED] left-50 translate-x-[-50%] h-[250px] w-[250px] rounded-full relative z-40 blur-[100px]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showSetup && (
|
||||
<div className="absolute inset-0 flex items-center justify-center overflow-hidden">
|
||||
<AnimatePresence mode="wait">
|
||||
{setupState === 1 && (
|
||||
<motion.p
|
||||
key="setup-text"
|
||||
transition={{ duration: 0.6 }}
|
||||
initial={{ opacity: 0, translateY: '30px' }}
|
||||
animate={{ opacity: 1, translateY: '0px' }}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
translateY: '-30px',
|
||||
transition: { duration: 0.6 },
|
||||
}}
|
||||
className="text-2xl md:text-4xl xl:text-6xl font-normal font-['Instrument_Serif'] tracking-tight"
|
||||
>
|
||||
Let us get{' '}
|
||||
<span className="text-[#24A0ED] italic font-['PP_Editorial']">
|
||||
Perplexica
|
||||
</span>{' '}
|
||||
set up for you
|
||||
</motion.p>
|
||||
)}
|
||||
{setupState > 1 && (
|
||||
<motion.div
|
||||
key="setup-config"
|
||||
initial={{ opacity: 0, translateY: '30px' }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
translateY: '0px',
|
||||
transition: { duration: 0.6 },
|
||||
}}
|
||||
>
|
||||
<SetupConfig
|
||||
configSections={configSections}
|
||||
setupState={setupState}
|
||||
setSetupState={setSetupState}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupWizard;
|
||||
@@ -104,7 +104,7 @@ const WeatherWidget = () => {
|
||||
|
||||
useEffect(() => {
|
||||
updateWeather();
|
||||
const intervalId = setInterval(updateWeather, 2 * 60 * 1000);
|
||||
const intervalId = setInterval(updateWeather, 30 * 1000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -17,8 +17,7 @@ const providerConfigFields: UIConfigField[] = [
|
||||
key: 'baseURL',
|
||||
description: 'The base URL for the Ollama',
|
||||
required: true,
|
||||
placeholder: 'Ollama Base URL',
|
||||
default: process.env.DOCKER
|
||||
placeholder: process.env.DOCKER
|
||||
? 'http://host.docker.internal:11434'
|
||||
: 'http://localhost:11434',
|
||||
env: 'OLLAMA_BASE_URL',
|
||||
|
||||
Reference in New Issue
Block a user