@@ -76,35 +83,73 @@ const Page = () => {
-
- {discover &&
- discover?.map((item, i) => (
-
-

-
-
- {item.title.slice(0, 100)}...
-
-
- {item.content.slice(0, 100)}...
-
-
-
- ))}
+
+ {topics.map((t, i) => (
+
setActiveTopic(t.key)}
+ >
+ {t.display}
+
+ ))}
+
+ {loading ? (
+
+ ) : (
+
+ {discover &&
+ discover?.map((item, i) => (
+
+

+
+
+ {item.title.slice(0, 100)}...
+
+
+ {item.content.slice(0, 100)}...
+
+
+
+ ))}
+
+ )}
>
);
diff --git a/src/app/globals.css b/src/app/globals.css
index f75daca..6bdc1a8 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -11,3 +11,11 @@
display: none;
}
}
+
+@media screen and (-webkit-min-device-pixel-ratio: 0) {
+ select,
+ textarea,
+ input {
+ font-size: 16px !important;
+ }
+}
diff --git a/src/app/manifest.ts b/src/app/manifest.ts
new file mode 100644
index 0000000..792e752
--- /dev/null
+++ b/src/app/manifest.ts
@@ -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',
+ },
+ ],
+ };
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index e18aca9..25981b5 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,4 +1,5 @@
import ChatWindow from '@/components/ChatWindow';
+import { ChatProvider } from '@/lib/hooks/useChat';
import { Metadata } from 'next';
import { Suspense } from 'react';
@@ -11,7 +12,9 @@ const Home = () => {
return (
-
+
+
+
);
diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx
index 8eee9a4..6fb8255 100644
--- a/src/app/settings/page.tsx
+++ b/src/app/settings/page.tsx
@@ -7,6 +7,7 @@ import { Switch } from '@headlessui/react';
import ThemeSwitcher from '@/components/theme/Switcher';
import { ImagesIcon, VideoIcon } from 'lucide-react';
import Link from 'next/link';
+import { PROVIDER_METADATA } from '@/lib/providers';
interface SettingsType {
chatModelProviders: {
@@ -20,7 +21,10 @@ interface SettingsType {
anthropicApiKey: string;
geminiApiKey: string;
ollamaApiUrl: string;
+ ollamaApiKey: string;
+ lmStudioApiUrl: string;
deepseekApiKey: string;
+ aimlApiKey: string;
customOpenaiApiKey: string;
customOpenaiApiUrl: string;
customOpenaiModelName: string;
@@ -141,15 +145,17 @@ const Page = () => {
const [selectedEmbeddingModel, setSelectedEmbeddingModel] = useState<
string | null
>(null);
- const [isLoading, setIsLoading] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
const [automaticImageSearch, setAutomaticImageSearch] = useState(false);
const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false);
const [systemInstructions, setSystemInstructions] = useState
('');
+ const [measureUnit, setMeasureUnit] = useState<'Imperial' | 'Metric'>(
+ 'Metric',
+ );
const [savingStates, setSavingStates] = useState>({});
useEffect(() => {
const fetchConfig = async () => {
- setIsLoading(true);
const res = await fetch(`/api/config`, {
headers: {
'Content-Type': 'application/json',
@@ -208,6 +214,10 @@ const Page = () => {
setSystemInstructions(localStorage.getItem('systemInstructions')!);
+ setMeasureUnit(
+ localStorage.getItem('measureUnit')! as 'Imperial' | 'Metric',
+ );
+
setIsLoading(false);
};
@@ -366,6 +376,8 @@ const Page = () => {
localStorage.setItem('embeddingModel', value);
} else if (key === 'systemInstructions') {
localStorage.setItem('systemInstructions', value);
+ } else if (key === 'measureUnit') {
+ localStorage.setItem('measureUnit', value.toString());
}
} catch (err) {
console.error('Failed to save:', err);
@@ -414,13 +426,35 @@ const Page = () => {
) : (
config && (
-
+
+
+
+ Measurement Units
+
+
@@ -514,7 +548,7 @@ const Page = () => {
+
+
+ Ollama API Key (Can be left blank)
+
+
{
+ setConfig((prev) => ({
+ ...prev!,
+ ollamaApiKey: e.target.value,
+ }));
+ }}
+ onSave={(value) => saveConfig('ollamaApiKey', value)}
+ />
+
+
GROQ API Key
@@ -858,6 +913,44 @@ const Page = () => {
onSave={(value) => saveConfig('deepseekApiKey', value)}
/>
+
+
+
+ AI/ML API Key
+
+
{
+ setConfig((prev) => ({
+ ...prev!,
+ aimlApiKey: e.target.value,
+ }));
+ }}
+ onSave={(value) => saveConfig('aimlApiKey', value)}
+ />
+
+
+
+
+ LM Studio API URL
+
+
{
+ setConfig((prev) => ({
+ ...prev!,
+ lmStudioApiUrl: e.target.value,
+ }));
+ }}
+ onSave={(value) => saveConfig('lmStudioApiUrl', value)}
+ />
+
diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx
index 0cf125b..a5d8cf9 100644
--- a/src/components/Chat.tsx
+++ b/src/components/Chat.tsx
@@ -5,28 +5,11 @@ import MessageInput from './MessageInput';
import { File, Message } from './ChatWindow';
import MessageBox from './MessageBox';
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 dividerRef = useRef