This commit is contained in:
wei 2025-08-22 06:01:40 +00:00 committed by GitHub
commit 9c23882029
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 3928 additions and 590 deletions

View file

@ -34,6 +34,7 @@ The API accepts a JSON object in the request body, where you define the focus mo
["assistant", "I am doing well, how can I help you today?"] ["assistant", "I am doing well, how can I help you today?"]
], ],
"systemInstructions": "Focus on providing technical details about Perplexica's architecture.", "systemInstructions": "Focus on providing technical details about Perplexica's architecture.",
"locale": "en-US",
"stream": false "stream": false
} }
``` ```
@ -66,6 +67,8 @@ The API accepts a JSON object in the request body, where you define the focus mo
- **`systemInstructions`** (string, optional): Custom instructions provided by the user to guide the AI's response. These instructions are treated as user preferences and have lower priority than the system's core instructions. For example, you can specify a particular writing style, format, or focus area. - **`systemInstructions`** (string, optional): Custom instructions provided by the user to guide the AI's response. These instructions are treated as user preferences and have lower priority than the system's core instructions. For example, you can specify a particular writing style, format, or focus area.
- **`locale`** (string, optional): Specifies a custom locale for the search operation. If not provided, the default locale (en-US) will be used. This can be useful for tailoring search results to a specific language or region. Format: IETF BCP 47 codes ({ISO 639-1}-{ISO 3166-1 alpha-2}), see https://www.rfc-editor.org/rfc/bcp/bcp47.txt.
- **`history`** (array, optional): An array of message pairs representing the conversation history. Each pair consists of a role (either 'human' or 'assistant') and the message content. This allows the system to use the context of the conversation to refine results. Example: - **`history`** (array, optional): An array of message pairs representing the conversation history. Each pair consists of a role (either 'human' or 'assistant') and the message content. This allows the system to use the context of the conversation to refine results. Example:
```json ```json

View file

@ -1,3 +1,5 @@
import createNextIntlPlugin from 'next-intl/plugin';
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
output: 'standalone', output: 'standalone',
@ -11,4 +13,5 @@ const nextConfig = {
serverExternalPackages: ['pdf-parse'], serverExternalPackages: ['pdf-parse'],
}; };
export default nextConfig; const withNextIntl = createNextIntlPlugin();
export default withNextIntl(nextConfig);

View file

@ -38,6 +38,7 @@
"mammoth": "^1.9.1", "mammoth": "^1.9.1",
"markdown-to-jsx": "^7.7.2", "markdown-to-jsx": "^7.7.2",
"next": "^15.2.2", "next": "^15.2.2",
"next-intl": "^4.3.4",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"pdf-parse": "^1.1.1", "pdf-parse": "^1.1.1",
"react": "^18", "react": "^18",

36
public/fonts/LICENSE Normal file
View file

@ -0,0 +1,36 @@
Noto Sans Traditional Chinese
License
Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'
This Font Software is licensed under the SIL Open Font License, Version 1.1 . This license is copied below, and is also available with a FAQ at: https://openfontlicense.org
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the copyright statement(s).
"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

13
public/fonts/README.md Normal file
View file

@ -0,0 +1,13 @@
Place font files here for client-side PDF generation.
Recommended (Noto Sans TC - SIL Open Font License 1.1):
- NotoSansTC-Regular.ttf
- NotoSansTC-Bold.ttf
You can download from:
- https://fonts.google.com/noto/specimen/Noto+Sans+TC
- https://github.com/googlefonts/noto-cjk (Sans/TTF)
These fonts will be fetched via relative URLs at runtime by jsPDF.

View file

@ -46,6 +46,7 @@ type Body = {
chatModel: ChatModel; chatModel: ChatModel;
embeddingModel: EmbeddingModel; embeddingModel: EmbeddingModel;
systemInstructions: string; systemInstructions: string;
locale: string;
}; };
const handleEmitterEvents = async ( const handleEmitterEvents = async (
@ -284,6 +285,7 @@ export const POST = async (req: Request) => {
body.optimizationMode, body.optimizationMode,
body.files, body.files,
body.systemInstructions, body.systemInstructions,
body.locale,
); );
const responseStream = new TransformStream(); const responseStream = new TransformStream();

View file

@ -1,4 +1,5 @@
import { searchSearxng } from '@/lib/searxng'; import { searchSearxng } from '@/lib/searxng';
import { DEFAULT_LOCALE } from '@/i18n/locales';
const websitesForTopic = { const websitesForTopic = {
tech: { tech: {
@ -46,9 +47,9 @@ export const GET = async (req: Request) => {
selectedTopic.query.map(async (query) => { selectedTopic.query.map(async (query) => {
return ( return (
await searchSearxng(`site:${link} ${query}`, { await searchSearxng(`site:${link} ${query}`, {
engines: ['bing news'], engines: ['google news', 'bing news'],
pageno: 1, pageno: 1,
language: 'en', language: DEFAULT_LOCALE,
}) })
).results; ).results;
}), }),
@ -68,9 +69,9 @@ export const GET = async (req: Request) => {
await searchSearxng( await searchSearxng(
`site:${selectedTopic.links[Math.floor(Math.random() * selectedTopic.links.length)]} ${selectedTopic.query[Math.floor(Math.random() * selectedTopic.query.length)]}`, `site:${selectedTopic.links[Math.floor(Math.random() * selectedTopic.links.length)]} ${selectedTopic.query[Math.floor(Math.random() * selectedTopic.query.length)]}`,
{ {
engines: ['bing news'], engines: ['google news', 'bing news'],
pageno: 1, pageno: 1,
language: 'en', language: DEFAULT_LOCALE,
}, },
) )
).results; ).results;

View file

@ -13,6 +13,7 @@ import {
getCustomOpenaiModelName, getCustomOpenaiModelName,
} from '@/lib/config'; } from '@/lib/config';
import { searchHandlers } from '@/lib/search'; import { searchHandlers } from '@/lib/search';
import { DEFAULT_LOCALE } from '@/i18n/locales';
interface chatModel { interface chatModel {
provider: string; provider: string;
@ -35,6 +36,7 @@ interface ChatRequestBody {
history: Array<[string, string]>; history: Array<[string, string]>;
stream?: boolean; stream?: boolean;
systemInstructions?: string; systemInstructions?: string;
locale?: string;
} }
export const POST = async (req: Request) => { export const POST = async (req: Request) => {
@ -126,6 +128,7 @@ export const POST = async (req: Request) => {
body.optimizationMode, body.optimizationMode,
[], [],
body.systemInstructions || '', body.systemInstructions || '',
body.locale || DEFAULT_LOCALE,
); );
if (!body.stream) { if (!body.stream) {

View file

@ -8,6 +8,7 @@ import { getAvailableChatModelProviders } from '@/lib/providers';
import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
import { ChatOpenAI } from '@langchain/openai'; import { ChatOpenAI } from '@langchain/openai';
import { DEFAULT_LOCALE } from '@/i18n/locales';
interface ChatModel { interface ChatModel {
provider: string; provider: string;
@ -17,6 +18,7 @@ interface ChatModel {
interface SuggestionsGenerationBody { interface SuggestionsGenerationBody {
chatHistory: any[]; chatHistory: any[];
chatModel?: ChatModel; chatModel?: ChatModel;
locale?: string;
} }
export const POST = async (req: Request) => { export const POST = async (req: Request) => {
@ -66,6 +68,7 @@ export const POST = async (req: Request) => {
const suggestions = await generateSuggestions( const suggestions = await generateSuggestions(
{ {
chat_history: chatHistory, chat_history: chatHistory,
locale: body.locale || DEFAULT_LOCALE,
}, },
llm, llm,
); );

View file

@ -1,5 +1,8 @@
import { getTranslations } from 'next-intl/server';
export const POST = async (req: Request) => { export const POST = async (req: Request) => {
try { try {
const t = await getTranslations('weather.conditions');
const body: { const body: {
lat: number; lat: number;
lng: number; lng: number;
@ -58,104 +61,104 @@ export const POST = async (req: Request) => {
switch (code) { switch (code) {
case 0: case 0:
weather.icon = `clear-${dayOrNight}`; weather.icon = `clear-${dayOrNight}`;
weather.condition = 'Clear'; weather.condition = t('clear');
break; break;
case 1: case 1:
weather.condition = 'Mainly Clear'; weather.condition = t('mainlyClear');
case 2: case 2:
weather.condition = 'Partly Cloudy'; weather.condition = t('partlyCloudy');
case 3: case 3:
weather.icon = `cloudy-1-${dayOrNight}`; weather.icon = `cloudy-1-${dayOrNight}`;
weather.condition = 'Cloudy'; weather.condition = t('cloudy');
break; break;
case 45: case 45:
weather.condition = 'Fog'; weather.condition = t('fog');
case 48: case 48:
weather.icon = `fog-${dayOrNight}`; weather.icon = `fog-${dayOrNight}`;
weather.condition = 'Fog'; weather.condition = t('fog');
break; break;
case 51: case 51:
weather.condition = 'Light Drizzle'; weather.condition = t('lightDrizzle');
case 53: case 53:
weather.condition = 'Moderate Drizzle'; weather.condition = t('moderateDrizzle');
case 55: case 55:
weather.icon = `rainy-1-${dayOrNight}`; weather.icon = `rainy-1-${dayOrNight}`;
weather.condition = 'Dense Drizzle'; weather.condition = t('denseDrizzle');
break; break;
case 56: case 56:
weather.condition = 'Light Freezing Drizzle'; weather.condition = t('lightFreezingDrizzle');
case 57: case 57:
weather.icon = `frost-${dayOrNight}`; weather.icon = `frost-${dayOrNight}`;
weather.condition = 'Dense Freezing Drizzle'; weather.condition = t('denseFreezingDrizzle');
break; break;
case 61: case 61:
weather.condition = 'Slight Rain'; weather.condition = t('slightRain');
case 63: case 63:
weather.condition = 'Moderate Rain'; weather.condition = t('moderateRain');
case 65: case 65:
weather.condition = 'Heavy Rain'; weather.condition = t('heavyRain');
weather.icon = `rainy-2-${dayOrNight}`; weather.icon = `rainy-2-${dayOrNight}`;
break; break;
case 66: case 66:
weather.condition = 'Light Freezing Rain'; weather.condition = t('lightFreezingRain');
case 67: case 67:
weather.condition = 'Heavy Freezing Rain'; weather.condition = t('heavyFreezingRain');
weather.icon = 'rain-and-sleet-mix'; weather.icon = 'rain-and-sleet-mix';
break; break;
case 71: case 71:
weather.condition = 'Slight Snow Fall'; weather.condition = t('slightSnowFall');
case 73: case 73:
weather.condition = 'Moderate Snow Fall'; weather.condition = t('moderateSnowFall');
case 75: case 75:
weather.condition = 'Heavy Snow Fall'; weather.condition = t('heavySnowFall');
weather.icon = `snowy-2-${dayOrNight}`; weather.icon = `snowy-2-${dayOrNight}`;
break; break;
case 77: case 77:
weather.condition = 'Snow'; weather.condition = t('snow');
weather.icon = `snowy-1-${dayOrNight}`; weather.icon = `snowy-1-${dayOrNight}`;
break; break;
case 80: case 80:
weather.condition = 'Slight Rain Showers'; weather.condition = t('slightRainShowers');
case 81: case 81:
weather.condition = 'Moderate Rain Showers'; weather.condition = t('moderateRainShowers');
case 82: case 82:
weather.condition = 'Heavy Rain Showers'; weather.condition = t('heavyRainShowers');
weather.icon = `rainy-3-${dayOrNight}`; weather.icon = `rainy-3-${dayOrNight}`;
break; break;
case 85: case 85:
weather.condition = 'Slight Snow Showers'; weather.condition = t('slightSnowShowers');
case 86: case 86:
weather.condition = 'Moderate Snow Showers'; weather.condition = t('moderateSnowShowers');
case 87: case 87:
weather.condition = 'Heavy Snow Showers'; weather.condition = t('heavySnowShowers');
weather.icon = `snowy-3-${dayOrNight}`; weather.icon = `snowy-3-${dayOrNight}`;
break; break;
case 95: case 95:
weather.condition = 'Thunderstorm'; weather.condition = t('thunderstorm');
weather.icon = `scattered-thunderstorms-${dayOrNight}`; weather.icon = `scattered-thunderstorms-${dayOrNight}`;
break; break;
case 96: case 96:
weather.condition = 'Thunderstorm with Slight Hail'; weather.condition = t('thunderstormSlightHail');
case 99: case 99:
weather.condition = 'Thunderstorm with Heavy Hail'; weather.condition = t('thunderstormHeavyHail');
weather.icon = 'severe-thunderstorm'; weather.icon = 'severe-thunderstorm';
break; break;
default: default:
weather.icon = `clear-${dayOrNight}`; weather.icon = `clear-${dayOrNight}`;
weather.condition = 'Clear'; weather.condition = t('clear');
break; break;
} }

View file

@ -1,8 +1,10 @@
'use client'; 'use client';
import { Search } from 'lucide-react'; import { Search } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
import Link from 'next/link'; import Link from 'next/link';
import Image from 'next/image';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@ -13,35 +15,24 @@ interface Discover {
thumbnail: string; thumbnail: string;
} }
const topics: { key: string; display: string }[] = [ const topics: {
{ key: 'tech' | 'finance' | 'art' | 'sports' | 'entertainment';
display: 'Tech & Science', }[] = [
key: 'tech', { key: 'tech' },
}, { key: 'finance' },
{ { key: 'art' },
display: 'Finance', { key: 'sports' },
key: 'finance', { key: 'entertainment' },
},
{
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); const [activeTopic, setActiveTopic] = useState<string>(topics[0].key);
const t = useTranslations('pages.discover');
const fetchArticles = async (topic: string) => { const fetchArticles = useCallback(
async (topic: string) => {
setLoading(true); setLoading(true);
try { try {
const res = await fetch(`/api/discover?topic=${topic}`, { const res = await fetch(`/api/discover?topic=${topic}`, {
@ -62,15 +53,17 @@ const Page = () => {
setDiscover(data.blogs); setDiscover(data.blogs);
} catch (err: any) { } catch (err: any) {
console.error('Error fetching data:', err.message); console.error('Error fetching data:', err.message);
toast.error('Error fetching data'); toast.error(t('errorFetchingData'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; },
[t],
);
useEffect(() => { useEffect(() => {
fetchArticles(activeTopic); fetchArticles(activeTopic);
}, [activeTopic]); }, [activeTopic, fetchArticles]);
return ( return (
<> <>
@ -78,24 +71,24 @@ const Page = () => {
<div className="flex flex-col pt-4"> <div className="flex flex-col pt-4">
<div className="flex items-center"> <div className="flex items-center">
<Search /> <Search />
<h1 className="text-3xl font-medium p-2">Discover</h1> <h1 className="text-3xl font-medium p-2">{t('title')}</h1>
</div> </div>
<hr className="border-t border-[#2B2C2C] my-4 w-full" /> <hr className="border-t border-[#2B2C2C] my-4 w-full" />
</div> </div>
<div className="flex flex-row items-center space-x-2 overflow-x-auto"> <div className="flex flex-row items-center space-x-2 overflow-x-auto">
{topics.map((t, i) => ( {topics.map((topic, i) => (
<div <div
key={i} key={i}
className={cn( className={cn(
'border-[0.1px] rounded-full text-sm px-3 py-1 text-nowrap transition duration-200 cursor-pointer', 'border-[0.1px] rounded-full text-sm px-3 py-1 text-nowrap transition duration-200 cursor-pointer',
activeTopic === t.key activeTopic === topic.key
? 'text-cyan-300 bg-cyan-300/30 border-cyan-300/60' ? 'text-cyan-600 bg-cyan-100 border-cyan-300 dark:text-cyan-300 dark:bg-cyan-300/30 dark:border-cyan-300/60'
: 'border-white/30 text-white/70 hover:text-white hover:border-white/40 hover:bg-white/5', : 'text-gray-700 border-gray-300 hover:text-gray-900 hover:bg-gray-100 hover:border-gray-400 dark:text-white/70 dark:border-white/30 dark:hover:text-white dark:hover:bg-white/5 dark:hover:border-white/40',
)} )}
onClick={() => setActiveTopic(t.key)} onClick={() => setActiveTopic(topic.key)}
> >
<span>{t.display}</span> <span>{t(`topics.${topic.key}`)}</span>
</div> </div>
))} ))}
</div> </div>
@ -129,15 +122,20 @@ const Page = () => {
className="max-w-sm rounded-lg overflow-hidden bg-light-secondary dark:bg-dark-secondary hover:-translate-y-[1px] transition duration-200" className="max-w-sm rounded-lg overflow-hidden bg-light-secondary dark:bg-dark-secondary hover:-translate-y-[1px] transition duration-200"
target="_blank" target="_blank"
> >
<img <div className="relative w-full aspect-video">
className="object-cover w-full aspect-video" <Image
fill
unoptimized
className="object-cover"
src={ src={
new URL(item.thumbnail).origin + new URL(item.thumbnail).origin +
new URL(item.thumbnail).pathname + new URL(item.thumbnail).pathname +
`?id=${new URL(item.thumbnail).searchParams.get('id')}` `?id=${new URL(item.thumbnail).searchParams.get('id')}`
} }
alt={item.title} alt={item.title}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
/> />
</div>
<div className="px-6 py-4"> <div className="px-6 py-4">
<div className="font-bold text-lg mb-2"> <div className="font-bold text-lg mb-2">
{item.title.slice(0, 100)}... {item.title.slice(0, 100)}...

View file

@ -5,6 +5,10 @@ import { cn } from '@/lib/utils';
import Sidebar from '@/components/Sidebar'; import Sidebar from '@/components/Sidebar';
import { Toaster } from 'sonner'; import { Toaster } from 'sonner';
import ThemeProvider from '@/components/theme/Provider'; import ThemeProvider from '@/components/theme/Provider';
import { NextIntlClientProvider } from 'next-intl';
import { getLocale, getTranslations } from 'next-intl/server';
import LocaleBootstrap from '@/components/LocaleBootstrap';
import { LOCALES, DEFAULT_LOCALE, type AppLocale } from '@/i18n/locales';
const montserrat = Montserrat({ const montserrat = Montserrat({
weight: ['300', '400', '500', '700'], weight: ['300', '400', '500', '700'],
@ -13,20 +17,30 @@ const montserrat = Montserrat({
fallback: ['Arial', 'sans-serif'], fallback: ['Arial', 'sans-serif'],
}); });
export const metadata: Metadata = { export async function generateMetadata(): Promise<Metadata> {
title: 'Perplexica - Chat with the internet', const t = await getTranslations('metadata');
description: return {
'Perplexica is an AI powered chatbot that is connected to the internet.', title: t('title'),
}; description: t('description'),
};
}
export default function RootLayout({ export default async function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
const locale = await getLocale();
const appLocale: AppLocale = (LOCALES as readonly string[]).includes(
locale as string,
)
? (locale as AppLocale)
: DEFAULT_LOCALE;
return ( return (
<html className="h-full" lang="en" suppressHydrationWarning> <html className="h-full" lang={locale} suppressHydrationWarning>
<body className={cn('h-full', montserrat.className)}> <body className={cn('h-full', montserrat.className)}>
<LocaleBootstrap initialLocale={appLocale} />
<NextIntlClientProvider>
<ThemeProvider> <ThemeProvider>
<Sidebar>{children}</Sidebar> <Sidebar>{children}</Sidebar>
<Toaster <Toaster
@ -39,6 +53,7 @@ export default function RootLayout({
}} }}
/> />
</ThemeProvider> </ThemeProvider>
</NextIntlClientProvider>
</body> </body>
</html> </html>
); );

View file

@ -1,9 +1,13 @@
import { Metadata } from 'next'; import { Metadata } from 'next';
import React from 'react'; import React from 'react';
import { getTranslations } from 'next-intl/server';
export const metadata: Metadata = { export async function generateMetadata(): Promise<Metadata> {
title: 'Library - Perplexica', const t = await getTranslations('pages.library');
}; return {
title: `${t('title')} - Perplexica`,
};
}
const Layout = ({ children }: { children: React.ReactNode }) => { const Layout = ({ children }: { children: React.ReactNode }) => {
return <div>{children}</div>; return <div>{children}</div>;

View file

@ -1,10 +1,11 @@
'use client'; 'use client';
import DeleteChat from '@/components/DeleteChat'; import DeleteChat from '@/components/DeleteChat';
import { cn, formatTimeDifference } from '@/lib/utils'; import { cn, formatRelativeTime } from '@/lib/utils';
import { BookOpenText, ClockIcon, Delete, ScanEye } from 'lucide-react'; import { BookOpenText, ClockIcon, Delete, ScanEye } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useLocale, useTranslations } from 'next-intl';
export interface Chat { export interface Chat {
id: string; id: string;
@ -16,6 +17,8 @@ export interface Chat {
const Page = () => { const Page = () => {
const [chats, setChats] = useState<Chat[]>([]); const [chats, setChats] = useState<Chat[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const t = useTranslations('pages.library');
const locale = useLocale();
useEffect(() => { useEffect(() => {
const fetchChats = async () => { const fetchChats = async () => {
@ -61,14 +64,14 @@ const Page = () => {
<div className="flex flex-col pt-4"> <div className="flex flex-col pt-4">
<div className="flex items-center"> <div className="flex items-center">
<BookOpenText /> <BookOpenText />
<h1 className="text-3xl font-medium p-2">Library</h1> <h1 className="text-3xl font-medium p-2">{t('title')}</h1>
</div> </div>
<hr className="border-t border-[#2B2C2C] my-4 w-full" /> <hr className="border-t border-[#2B2C2C] my-4 w-full" />
</div> </div>
{chats.length === 0 && ( {chats.length === 0 && (
<div className="flex flex-row items-center justify-center min-h-screen"> <div className="flex flex-row items-center justify-center min-h-screen">
<p className="text-black/70 dark:text-white/70 text-sm"> <p className="text-black/70 dark:text-white/70 text-sm">
No chats found. {t('empty')}
</p> </p>
</div> </div>
)} )}
@ -94,7 +97,7 @@ const Page = () => {
<div className="flex flex-row items-center space-x-1 lg:space-x-1.5 text-black/70 dark:text-white/70"> <div className="flex flex-row items-center space-x-1 lg:space-x-1.5 text-black/70 dark:text-white/70">
<ClockIcon size={15} /> <ClockIcon size={15} />
<p className="text-xs"> <p className="text-xs">
{formatTimeDifference(new Date(), chat.createdAt)} Ago {formatRelativeTime(new Date(), chat.createdAt, locale)}
</p> </p>
</div> </div>
<DeleteChat <DeleteChat

View file

@ -1,11 +1,12 @@
import type { MetadataRoute } from 'next'; import type { MetadataRoute } from 'next';
import { getTranslations } from 'next-intl/server';
export default function manifest(): MetadataRoute.Manifest { export default async function manifest(): Promise<MetadataRoute.Manifest> {
const t = await getTranslations('manifest');
return { return {
name: 'Perplexica - Chat with the internet', name: t('name'),
short_name: 'Perplexica', short_name: t('shortName'),
description: description: t('description'),
'Perplexica is an AI powered chatbot that is connected to the internet.',
start_url: '/', start_url: '/',
display: 'standalone', display: 'standalone',
background_color: '#0a0a0a', background_color: '#0a0a0a',

View file

@ -2,11 +2,15 @@ import ChatWindow from '@/components/ChatWindow';
import { ChatProvider } from '@/lib/hooks/useChat'; import { ChatProvider } from '@/lib/hooks/useChat';
import { Metadata } from 'next'; import { Metadata } from 'next';
import { Suspense } from 'react'; import { Suspense } from 'react';
import { getTranslations } from 'next-intl/server';
export const metadata: Metadata = { export async function generateMetadata(): Promise<Metadata> {
title: 'Chat - Perplexica', const t = await getTranslations('pages.home');
description: 'Chat with the internet, chat with Perplexica.', return {
}; title: t('title'),
description: t('description'),
};
}
const Home = () => { const Home = () => {
return ( return (

View file

@ -5,9 +5,11 @@ import { useEffect, useState } from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Switch } from '@headlessui/react'; import { Switch } from '@headlessui/react';
import ThemeSwitcher from '@/components/theme/Switcher'; import ThemeSwitcher from '@/components/theme/Switcher';
import LocaleSwitcher from '@/components/LocaleSwitcher';
import { ImagesIcon, VideoIcon } from 'lucide-react'; import { ImagesIcon, VideoIcon } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { PROVIDER_METADATA } from '@/lib/providers'; import { PROVIDER_METADATA } from '@/lib/providers';
import { useTranslations } from 'next-intl';
interface SettingsType { interface SettingsType {
chatModelProviders: { chatModelProviders: {
@ -129,6 +131,7 @@ const SettingsSection = ({
); );
const Page = () => { const Page = () => {
const t = useTranslations('pages.settings');
const [config, setConfig] = useState<SettingsType | null>(null); const [config, setConfig] = useState<SettingsType | null>(null);
const [chatModels, setChatModels] = useState<Record<string, any>>({}); const [chatModels, setChatModels] = useState<Record<string, any>>({});
const [embeddingModels, setEmbeddingModels] = useState<Record<string, any>>( const [embeddingModels, setEmbeddingModels] = useState<Record<string, any>>(
@ -398,7 +401,7 @@ const Page = () => {
</Link> </Link>
<div className="flex flex-row space-x-0.5 items-center"> <div className="flex flex-row space-x-0.5 items-center">
<SettingsIcon size={23} /> <SettingsIcon size={23} />
<h1 className="text-3xl font-medium p-2">Settings</h1> <h1 className="text-3xl font-medium p-2">{t('title')}</h1>
</div> </div>
</div> </div>
<hr className="border-t border-[#2B2C2C] my-4 w-full" /> <hr className="border-t border-[#2B2C2C] my-4 w-full" />
@ -426,16 +429,16 @@ 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="Preferences"> <SettingsSection title={t('sections.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 {t('preferences.theme')}
</p> </p>
<ThemeSwitcher /> <ThemeSwitcher />
</div> </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">
Measurement Units {t('preferences.measurementUnits')}
</p> </p>
<Select <Select
value={measureUnit ?? undefined} value={measureUnit ?? undefined}
@ -445,19 +448,25 @@ const Page = () => {
}} }}
options={[ options={[
{ {
label: 'Metric', label: t('preferences.metric'),
value: 'Metric', value: 'Metric',
}, },
{ {
label: 'Imperial', label: t('preferences.imperial'),
value: 'Imperial', value: 'Imperial',
}, },
]} ]}
/> />
</div> </div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
{t('preferences.language')}
</p>
<LocaleSwitcher />
</div>
</SettingsSection> </SettingsSection>
<SettingsSection title="Automatic Search"> <SettingsSection title={t('sections.automaticSearch')}>
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
<div className="flex items-center justify-between p-3 bg-light-secondary dark:bg-dark-secondary rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition-colors"> <div className="flex items-center justify-between p-3 bg-light-secondary dark:bg-dark-secondary rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition-colors">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
@ -469,11 +478,10 @@ const Page = () => {
</div> </div>
<div> <div>
<p className="text-sm text-black/90 dark:text-white/90 font-medium"> <p className="text-sm text-black/90 dark:text-white/90 font-medium">
Automatic Image Search {t('automaticSearch.image.title')}
</p> </p>
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5"> <p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
Automatically search for relevant images in chat {t('automaticSearch.image.desc')}
responses
</p> </p>
</div> </div>
</div> </div>
@ -511,11 +519,10 @@ const Page = () => {
</div> </div>
<div> <div>
<p className="text-sm text-black/90 dark:text-white/90 font-medium"> <p className="text-sm text-black/90 dark:text-white/90 font-medium">
Automatic Video Search {t('automaticSearch.video.title')}
</p> </p>
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5"> <p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
Automatically search for relevant videos in chat {t('automaticSearch.video.desc')}
responses
</p> </p>
</div> </div>
</div> </div>
@ -545,7 +552,7 @@ const Page = () => {
</div> </div>
</SettingsSection> </SettingsSection>
<SettingsSection title="System Instructions"> <SettingsSection title={t('sections.systemInstructions')}>
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
<Textarea <Textarea
value={systemInstructions ?? undefined} value={systemInstructions ?? undefined}
@ -554,16 +561,17 @@ const Page = () => {
setSystemInstructions(e.target.value); setSystemInstructions(e.target.value);
}} }}
onSave={(value) => saveConfig('systemInstructions', value)} onSave={(value) => saveConfig('systemInstructions', value)}
placeholder={t('systemInstructions.placeholder')}
/> />
</div> </div>
</SettingsSection> </SettingsSection>
<SettingsSection title="Model Settings"> <SettingsSection title={t('sections.modelSettings')}>
{config.chatModelProviders && ( {config.chatModelProviders && (
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
<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">
Chat Model Provider {t('model.chatProvider')}
</p> </p>
<Select <Select
value={selectedChatModelProvider ?? undefined} value={selectedChatModelProvider ?? undefined}
@ -594,7 +602,7 @@ const Page = () => {
selectedChatModelProvider != 'custom_openai' && ( selectedChatModelProvider != 'custom_openai' && (
<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">
Chat Model {t('model.chat')}
</p> </p>
<Select <Select
value={selectedChatModel ?? undefined} value={selectedChatModel ?? undefined}
@ -617,15 +625,14 @@ const Page = () => {
: [ : [
{ {
value: '', value: '',
label: 'No models available', label: t('model.noModels'),
disabled: true, disabled: true,
}, },
] ]
: [ : [
{ {
value: '', value: '',
label: label: t('model.invalidProvider'),
'Invalid provider, please check backend logs',
disabled: true, disabled: true,
}, },
]; ];
@ -641,11 +648,11 @@ const Page = () => {
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
<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">
Model Name {t('model.custom.modelName')}
</p> </p>
<Input <Input
type="text" type="text"
placeholder="Model name" placeholder={t('model.custom.modelName')}
value={config.customOpenaiModelName} value={config.customOpenaiModelName}
isSaving={savingStates['customOpenaiModelName']} isSaving={savingStates['customOpenaiModelName']}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
@ -661,11 +668,10 @@ const Page = () => {
</div> </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">
Custom OpenAI API Key {t('model.custom.apiKey')}
</p> </p>
<Input <Input
type="text" type="password"
placeholder="Custom OpenAI API Key"
value={config.customOpenaiApiKey} value={config.customOpenaiApiKey}
isSaving={savingStates['customOpenaiApiKey']} isSaving={savingStates['customOpenaiApiKey']}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
@ -681,11 +687,11 @@ const Page = () => {
</div> </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">
Custom OpenAI Base URL {t('model.custom.baseUrl')}
</p> </p>
<Input <Input
type="text" type="text"
placeholder="Custom OpenAI Base URL" placeholder={t('model.custom.baseUrl')}
value={config.customOpenaiApiUrl} value={config.customOpenaiApiUrl}
isSaving={savingStates['customOpenaiApiUrl']} isSaving={savingStates['customOpenaiApiUrl']}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
@ -706,7 +712,7 @@ const Page = () => {
<div className="flex flex-col space-y-4 mt-4 pt-4 border-t border-light-200 dark:border-dark-200"> <div className="flex flex-col space-y-4 mt-4 pt-4 border-t border-light-200 dark:border-dark-200">
<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">
Embedding Model Provider {t('embedding.provider')}
</p> </p>
<Select <Select
value={selectedEmbeddingModelProvider ?? undefined} value={selectedEmbeddingModelProvider ?? undefined}
@ -736,7 +742,7 @@ const Page = () => {
{selectedEmbeddingModelProvider && ( {selectedEmbeddingModelProvider && (
<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">
Embedding Model {t('embedding.model')}
</p> </p>
<Select <Select
value={selectedEmbeddingModel ?? undefined} value={selectedEmbeddingModel ?? undefined}
@ -759,15 +765,14 @@ const Page = () => {
: [ : [
{ {
value: '', value: '',
label: 'No models available', label: t('model.noModels'),
disabled: true, disabled: true,
}, },
] ]
: [ : [
{ {
value: '', value: '',
label: label: t('model.invalidProvider'),
'Invalid provider, please check backend logs',
disabled: true, disabled: true,
}, },
]; ];
@ -779,15 +784,14 @@ const Page = () => {
)} )}
</SettingsSection> </SettingsSection>
<SettingsSection title="API Keys"> <SettingsSection title={t('sections.apiKeys')}>
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
<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">
OpenAI API Key {t('api.openaiApiKey')}
</p> </p>
<Input <Input
type="text" type="password"
placeholder="OpenAI API Key"
value={config.openaiApiKey} value={config.openaiApiKey}
isSaving={savingStates['openaiApiKey']} isSaving={savingStates['openaiApiKey']}
onChange={(e) => { onChange={(e) => {
@ -802,11 +806,10 @@ const Page = () => {
<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">
Ollama API URL {t('api.ollamaApiUrl')}
</p> </p>
<Input <Input
type="text" type="text"
placeholder="Ollama API URL"
value={config.ollamaApiUrl} value={config.ollamaApiUrl}
isSaving={savingStates['ollamaApiUrl']} isSaving={savingStates['ollamaApiUrl']}
onChange={(e) => { onChange={(e) => {
@ -821,11 +824,10 @@ const Page = () => {
<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">
Ollama API Key (Can be left blank) {t('api.ollamaApiKey')}
</p> </p>
<Input <Input
type="text" type="text"
placeholder="Ollama API Key"
value={config.ollamaApiKey} value={config.ollamaApiKey}
isSaving={savingStates['ollamaApiKey']} isSaving={savingStates['ollamaApiKey']}
onChange={(e) => { onChange={(e) => {
@ -840,11 +842,10 @@ const Page = () => {
<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 {t('api.groqApiKey')}
</p> </p>
<Input <Input
type="text" type="password"
placeholder="GROQ API Key"
value={config.groqApiKey} value={config.groqApiKey}
isSaving={savingStates['groqApiKey']} isSaving={savingStates['groqApiKey']}
onChange={(e) => { onChange={(e) => {
@ -859,11 +860,10 @@ const Page = () => {
<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">
Anthropic API Key {t('api.anthropicApiKey')}
</p> </p>
<Input <Input
type="text" type="password"
placeholder="Anthropic API key"
value={config.anthropicApiKey} value={config.anthropicApiKey}
isSaving={savingStates['anthropicApiKey']} isSaving={savingStates['anthropicApiKey']}
onChange={(e) => { onChange={(e) => {
@ -878,11 +878,10 @@ const Page = () => {
<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">
Gemini API Key {t('api.geminiApiKey')}
</p> </p>
<Input <Input
type="text" type="password"
placeholder="Gemini API key"
value={config.geminiApiKey} value={config.geminiApiKey}
isSaving={savingStates['geminiApiKey']} isSaving={savingStates['geminiApiKey']}
onChange={(e) => { onChange={(e) => {
@ -897,11 +896,10 @@ const Page = () => {
<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">
Deepseek API Key {t('api.deepseekApiKey')}
</p> </p>
<Input <Input
type="text" type="password"
placeholder="Deepseek API Key"
value={config.deepseekApiKey} value={config.deepseekApiKey}
isSaving={savingStates['deepseekApiKey']} isSaving={savingStates['deepseekApiKey']}
onChange={(e) => { onChange={(e) => {
@ -916,11 +914,10 @@ const Page = () => {
<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">
AI/ML API Key {t('api.aimlApiKey')}
</p> </p>
<Input <Input
type="text" type="password"
placeholder="AI/ML API Key"
value={config.aimlApiKey} value={config.aimlApiKey}
isSaving={savingStates['aimlApiKey']} isSaving={savingStates['aimlApiKey']}
onChange={(e) => { onChange={(e) => {
@ -935,11 +932,10 @@ const Page = () => {
<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 {t('api.lmStudioApiUrl')}
</p> </p>
<Input <Input
type="text" type="text"
placeholder="LM Studio API URL"
value={config.lmStudioApiUrl} value={config.lmStudioApiUrl}
isSaving={savingStates['lmStudioApiUrl']} isSaving={savingStates['lmStudioApiUrl']}
onChange={(e) => { onChange={(e) => {

View file

@ -11,6 +11,7 @@ import {
import { Fragment, useState } from 'react'; import { Fragment, useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Chat } from '@/app/library/page'; import { Chat } from '@/app/library/page';
import { useTranslations } from 'next-intl';
const DeleteChat = ({ const DeleteChat = ({
chatId, chatId,
@ -25,6 +26,7 @@ const DeleteChat = ({
}) => { }) => {
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false); const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const t = useTranslations();
const handleDelete = async () => { const handleDelete = async () => {
setLoading(true); setLoading(true);
@ -48,7 +50,8 @@ const DeleteChat = ({
window.location.href = '/'; window.location.href = '/';
} }
} catch (err: any) { } catch (err: any) {
toast.error(err.message); console.error(err);
toast.error(t('common.errors.failedToDeleteChat'));
} finally { } finally {
setConfirmationDialogOpen(false); setConfirmationDialogOpen(false);
setLoading(false); setLoading(false);
@ -89,10 +92,10 @@ const DeleteChat = ({
> >
<DialogPanel className="w-full max-w-md transform rounded-2xl bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 p-6 text-left align-middle shadow-xl transition-all"> <DialogPanel className="w-full max-w-md transform rounded-2xl bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 p-6 text-left align-middle shadow-xl transition-all">
<DialogTitle className="text-lg font-medium leading-6 dark:text-white"> <DialogTitle className="text-lg font-medium leading-6 dark:text-white">
Delete Confirmation {t('components.deleteChat.title')}
</DialogTitle> </DialogTitle>
<Description className="text-sm dark:text-white/70 text-black/70"> <Description className="text-sm dark:text-white/70 text-black/70">
Are you sure you want to delete this chat? {t('components.deleteChat.description')}
</Description> </Description>
<div className="flex flex-row items-end justify-end space-x-4 mt-6"> <div className="flex flex-row items-end justify-end space-x-4 mt-6">
<button <button
@ -103,13 +106,13 @@ const DeleteChat = ({
}} }}
className="text-black/50 dark:text-white/50 text-sm hover:text-black/70 hover:dark:text-white/70 transition duration-200" className="text-black/50 dark:text-white/50 text-sm hover:text-black/70 hover:dark:text-white/70 transition duration-200"
> >
Cancel {t('components.deleteChat.cancel')}
</button> </button>
<button <button
onClick={handleDelete} onClick={handleDelete}
className="text-red-400 text-sm hover:text-red-500 transition duration200" className="text-red-400 text-sm hover:text-red-500 transition duration200"
> >
Delete {t('components.deleteChat.delete')}
</button> </button>
</div> </div>
</DialogPanel> </DialogPanel>

View file

@ -1,11 +1,12 @@
import { Settings } from 'lucide-react'; import { Settings } from 'lucide-react';
import EmptyChatMessageInput from './EmptyChatMessageInput'; import EmptyChatMessageInput from './EmptyChatMessageInput';
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';
import { useTranslations } from 'next-intl';
const EmptyChat = () => { const EmptyChat = () => {
const t = useTranslations('components');
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">
@ -16,7 +17,7 @@ const EmptyChat = () => {
<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 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"> <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. {t('emptyChat.title')}
</h2> </h2>
<EmptyChatMessageInput /> <EmptyChatMessageInput />
</div> </div>

View file

@ -4,6 +4,7 @@ import TextareaAutosize from 'react-textarea-autosize';
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 { useTranslations } from 'next-intl';
import { useChat } from '@/lib/hooks/useChat'; import { useChat } from '@/lib/hooks/useChat';
const EmptyChatMessageInput = () => { const EmptyChatMessageInput = () => {
@ -11,6 +12,7 @@ const EmptyChatMessageInput = () => {
/* const [copilotEnabled, setCopilotEnabled] = useState(false); */ /* const [copilotEnabled, setCopilotEnabled] = useState(false); */
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const t = useTranslations('components');
const inputRef = useRef<HTMLTextAreaElement | null>(null); const inputRef = useRef<HTMLTextAreaElement | null>(null);
@ -61,7 +63,7 @@ const EmptyChatMessageInput = () => {
onChange={(e) => setMessage(e.target.value)} onChange={(e) => setMessage(e.target.value)}
minRows={2} minRows={2}
className="bg-transparent placeholder:text-black/50 dark:placeholder:text-white/50 text-sm text-black dark:text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48" className="bg-transparent placeholder:text-black/50 dark:placeholder:text-white/50 text-sm text-black dark:text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48"
placeholder="Ask anything..." placeholder={t('emptyChatMessageInput.placeholder')}
/> />
<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">

View file

@ -0,0 +1,24 @@
'use client';
import { useEffect } from 'react';
import { LOCALES, DEFAULT_LOCALE, type AppLocale } from '@/i18n/locales';
export default function LocaleBootstrap({
initialLocale,
}: {
initialLocale: AppLocale;
}) {
useEffect(() => {
const hasCookie = /(?:^|; )locale=/.test(document.cookie);
if (hasCookie) return;
const supported = new Set<string>(LOCALES as readonly string[]);
const loc = (initialLocale || DEFAULT_LOCALE) as string;
const chosen = Array.from(supported).find(
(s) => s.toLowerCase() === loc.toLowerCase(),
) as AppLocale | undefined;
const finalLocale: AppLocale = chosen || DEFAULT_LOCALE;
document.cookie = `locale=${finalLocale}; Path=/; Max-Age=31536000; SameSite=Lax`;
}, [initialLocale]);
return null;
}

View file

@ -0,0 +1,61 @@
'use client';
import { useEffect, useMemo, useState } from 'react';
import { useLocale } from 'next-intl';
import { useRouter } from 'next/navigation';
import {
LOCALES,
DEFAULT_LOCALE,
type AppLocale,
LOCALE_LABELS,
} from '@/i18n/locales';
function setLocaleCookie(value: AppLocale) {
const oneYear = 60 * 60 * 24 * 365;
const isSecure =
typeof window !== 'undefined' && window.location.protocol === 'https:';
document.cookie = `locale=${value}; path=/; max-age=${oneYear}; samesite=lax${isSecure ? '; secure' : ''}`;
}
export default function LocaleSwitcher({
onChange,
}: {
onChange?: (next: AppLocale) => void;
}) {
const router = useRouter();
const current = useLocale();
const currentLocale: AppLocale = useMemo(() => {
return (LOCALES as readonly string[]).includes(current)
? (current as AppLocale)
: DEFAULT_LOCALE;
}, [current]);
const [value, setValue] = useState<AppLocale>(currentLocale);
useEffect(() => {
setValue(currentLocale);
}, [currentLocale]);
return (
<select
value={value}
onChange={(e) => {
const next = e.target.value as AppLocale;
setValue(next);
setLocaleCookie(next);
onChange?.(next);
router.refresh();
}}
className={
'bg-light-secondary dark:bg-dark-secondary px-3 py-2 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 dark:text-white rounded-lg text-sm'
}
aria-label="Language"
>
{LOCALES.map((loc) => (
<option key={loc} value={loc}>
{LOCALE_LABELS[loc]}
</option>
))}
</select>
);
}

View file

@ -1,6 +1,7 @@
import { Check, ClipboardList } from 'lucide-react'; import { Check, ClipboardList } from 'lucide-react';
import { Message } from '../ChatWindow'; import { Message } from '../ChatWindow';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslations } from 'next-intl';
const Copy = ({ const Copy = ({
message, message,
@ -10,11 +11,12 @@ const Copy = ({
initialMessage: string; initialMessage: string;
}) => { }) => {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const t = useTranslations();
return ( return (
<button <button
onClick={() => { onClick={() => {
const contentToCopy = `${initialMessage}${message.sources && message.sources.length > 0 && `\n\nCitations:\n${message.sources?.map((source: any, i: any) => `[${i + 1}] ${source.metadata.url}`).join(`\n`)}`}`; const contentToCopy = `${initialMessage}${message.sources && message.sources.length > 0 && `\n\n${t('common.citations')}\n${message.sources?.map((source: any, i: any) => `[${i + 1}] ${source.metadata.url}`).join(`\n`)}`}`;
navigator.clipboard.writeText(contentToCopy); navigator.clipboard.writeText(contentToCopy);
setCopied(true); setCopied(true);
setTimeout(() => setCopied(false), 1000); setTimeout(() => setCopied(false), 1000);

View file

@ -1,4 +1,5 @@
import { ArrowLeftRight } from 'lucide-react'; import { ArrowLeftRight } from 'lucide-react';
import { useTranslations } from 'next-intl';
const Rewrite = ({ const Rewrite = ({
rewrite, rewrite,
@ -7,13 +8,14 @@ const Rewrite = ({
rewrite: (messageId: string) => void; rewrite: (messageId: string) => void;
messageId: string; messageId: string;
}) => { }) => {
const t = useTranslations('components');
return ( return (
<button <button
onClick={() => rewrite(messageId)} onClick={() => rewrite(messageId)}
className="py-2 px-3 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white flex flex-row items-center space-x-1" className="py-2 px-3 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white flex flex-row items-center space-x-1"
> >
<ArrowLeftRight size={18} /> <ArrowLeftRight size={18} />
<p className="text-xs font-medium">Rewrite</p> <p className="text-xs font-medium">{t('messageActions.rewrite')}</p>
</button> </button>
); );
}; };

View file

@ -20,6 +20,7 @@ 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 { useTranslations } from 'next-intl';
import { useChat } from '@/lib/hooks/useChat'; import { useChat } from '@/lib/hooks/useChat';
const ThinkTagProcessor = ({ const ThinkTagProcessor = ({
@ -46,6 +47,7 @@ const MessageBox = ({
isLast: boolean; isLast: boolean;
}) => { }) => {
const { loading, messages: history, sendMessage, rewrite } = useChat(); const { loading, messages: history, sendMessage, rewrite } = useChat();
const t = useTranslations('components');
const [parsedMessage, setParsedMessage] = useState(message.content); const [parsedMessage, setParsedMessage] = useState(message.content);
const [speechMessage, setSpeechMessage] = useState(message.content); const [speechMessage, setSpeechMessage] = useState(message.content);
@ -161,7 +163,7 @@ const MessageBox = ({
<div className="flex flex-row items-center space-x-2"> <div className="flex flex-row items-center space-x-2">
<BookCopy className="text-black dark:text-white" size={20} /> <BookCopy className="text-black dark:text-white" size={20} />
<h3 className="text-black dark:text-white font-medium text-xl"> <h3 className="text-black dark:text-white font-medium text-xl">
Sources {t('messageBox.sources')}
</h3> </h3>
</div> </div>
<MessageSources sources={message.sources} /> <MessageSources sources={message.sources} />
@ -177,7 +179,7 @@ const MessageBox = ({
size={20} size={20}
/> />
<h3 className="text-black dark:text-white font-medium text-xl"> <h3 className="text-black dark:text-white font-medium text-xl">
Answer {t('messageBox.answer')}
</h3> </h3>
</div> </div>
@ -229,7 +231,9 @@ const MessageBox = ({
<div className="flex flex-col space-y-3 text-black dark:text-white"> <div className="flex flex-col space-y-3 text-black dark:text-white">
<div className="flex flex-row items-center space-x-2 mt-4"> <div className="flex flex-row items-center space-x-2 mt-4">
<Layers3 /> <Layers3 />
<h3 className="text-xl font-medium">Related</h3> <h3 className="text-xl font-medium">
{t('messageBox.related')}
</h3>
</div> </div>
<div className="flex flex-col space-y-3"> <div className="flex flex-col space-y-3">
{message.suggestions.map((suggestion, i) => ( {message.suggestions.map((suggestion, i) => (

View file

@ -2,15 +2,14 @@ import { cn } from '@/lib/utils';
import { ArrowUp } from 'lucide-react'; import { ArrowUp } 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 Attach from './MessageInputActions/Attach';
import CopilotToggle from './MessageInputActions/Copilot'; import CopilotToggle from './MessageInputActions/Copilot';
import { File } from './ChatWindow';
import AttachSmall from './MessageInputActions/AttachSmall'; import AttachSmall from './MessageInputActions/AttachSmall';
import { useTranslations } from 'next-intl';
import { useChat } from '@/lib/hooks/useChat'; import { useChat } from '@/lib/hooks/useChat';
const MessageInput = () => { const MessageInput = () => {
const { loading, sendMessage } = useChat(); const { loading, sendMessage } = useChat();
const t = useTranslations('components.messageInput');
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);
@ -77,7 +76,7 @@ const MessageInput = () => {
setTextareaRows(Math.ceil(height / props.rowHeight)); setTextareaRows(Math.ceil(height / props.rowHeight));
}} }}
className="transition bg-transparent dark:placeholder:text-white/50 placeholder:text-sm text-sm dark:text-white resize-none focus:outline-none w-full px-2 max-h-24 lg:max-h-36 xl:max-h-48 flex-grow flex-shrink" className="transition bg-transparent dark:placeholder:text-white/50 placeholder:text-sm text-sm dark:text-white resize-none focus:outline-none w-full px-2 max-h-24 lg:max-h-36 xl:max-h-48 flex-grow flex-shrink"
placeholder="Ask a follow-up" placeholder={t('placeholder')}
/> />
{mode === 'single' && ( {mode === 'single' && (
<div className="flex flex-row items-center space-x-4"> <div className="flex flex-row items-center space-x-4">

View file

@ -7,12 +7,12 @@ 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 { useTranslations } from 'next-intl';
import { useChat } from '@/lib/hooks/useChat'; import { useChat } from '@/lib/hooks/useChat';
const Attach = ({ showText }: { showText?: boolean }) => { const Attach = ({ showText }: { showText?: boolean }) => {
const { files, setFiles, setFileIds, fileIds } = useChat(); const { files, setFiles, setFileIds, fileIds } = useChat();
const t = useTranslations('components.attach');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const fileInputRef = useRef<any>(); const fileInputRef = useRef<any>();
@ -48,7 +48,7 @@ const Attach = ({ showText }: { showText?: boolean }) => {
<div className="flex flex-row items-center justify-between space-x-1"> <div className="flex flex-row items-center justify-between space-x-1">
<LoaderCircle size={18} className="text-sky-400 animate-spin" /> <LoaderCircle size={18} className="text-sky-400 animate-spin" />
<p className="text-sky-400 inline whitespace-nowrap text-xs font-medium"> <p className="text-sky-400 inline whitespace-nowrap text-xs font-medium">
Uploading.. {t('uploading')}
</p> </p>
</div> </div>
) : files.length > 0 ? ( ) : files.length > 0 ? (
@ -95,7 +95,7 @@ const Attach = ({ showText }: { showText?: boolean }) => {
<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"> <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">
<div className="flex flex-row items-center justify-between px-3 py-2"> <div className="flex flex-row items-center justify-between px-3 py-2">
<h4 className="text-black dark:text-white font-medium text-sm"> <h4 className="text-black dark:text-white font-medium text-sm">
Attached files {t('attachedFiles')}
</h4> </h4>
<div className="flex flex-row items-center space-x-4"> <div className="flex flex-row items-center space-x-4">
<button <button
@ -112,7 +112,7 @@ const Attach = ({ showText }: { showText?: boolean }) => {
hidden hidden
/> />
<Plus size={18} /> <Plus size={18} />
<p className="text-xs">Add</p> <p className="text-xs">{t('add')}</p>
</button> </button>
<button <button
onClick={() => { onClick={() => {
@ -122,7 +122,7 @@ const Attach = ({ showText }: { showText?: boolean }) => {
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" 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"
> >
<Trash size={14} /> <Trash size={14} />
<p className="text-xs">Clear</p> <p className="text-xs">{t('clear')}</p>
</button> </button>
</div> </div>
</div> </div>
@ -168,7 +168,9 @@ const Attach = ({ showText }: { showText?: boolean }) => {
hidden hidden
/> />
<CopyPlus size={showText ? 18 : undefined} /> <CopyPlus size={showText ? 18 : undefined} />
{showText && <p className="text-xs font-medium pl-[1px]">Attach</p>} {showText && (
<p className="text-xs font-medium pl-[1px] w-max">{t('attach')}</p>
)}
</button> </button>
); );
}; };

View file

@ -1,4 +1,3 @@
import { cn } from '@/lib/utils';
import { import {
Popover, Popover,
PopoverButton, PopoverButton,
@ -7,12 +6,12 @@ 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 { useTranslations } from 'next-intl';
import { useChat } from '@/lib/hooks/useChat'; import { useChat } from '@/lib/hooks/useChat';
const AttachSmall = () => { const AttachSmall = () => {
const { files, setFiles, setFileIds, fileIds } = useChat(); const { files, setFiles, setFileIds, fileIds } = useChat();
const t = useTranslations('components.attach');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const fileInputRef = useRef<any>(); const fileInputRef = useRef<any>();
@ -69,7 +68,7 @@ const AttachSmall = () => {
<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"> <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">
<div className="flex flex-row items-center justify-between px-3 py-2"> <div className="flex flex-row items-center justify-between px-3 py-2">
<h4 className="text-black dark:text-white font-medium text-sm"> <h4 className="text-black dark:text-white font-medium text-sm">
Attached files {t('attachedFiles')}
</h4> </h4>
<div className="flex flex-row items-center space-x-4"> <div className="flex flex-row items-center space-x-4">
<button <button
@ -86,7 +85,7 @@ const AttachSmall = () => {
hidden hidden
/> />
<Plus size={18} /> <Plus size={18} />
<p className="text-xs">Add</p> <p className="text-xs">{t('add')}</p>
</button> </button>
<button <button
onClick={() => { onClick={() => {
@ -96,7 +95,7 @@ const AttachSmall = () => {
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" 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"
> >
<Trash size={14} /> <Trash size={14} />
<p className="text-xs">Clear</p> <p className="text-xs">{t('clear')}</p>
</button> </button>
</div> </div>
</div> </div>

View file

@ -1,5 +1,6 @@
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Switch } from '@headlessui/react'; import { Switch } from '@headlessui/react';
import { useTranslations } from 'next-intl';
const CopilotToggle = ({ const CopilotToggle = ({
copilotEnabled, copilotEnabled,
@ -8,6 +9,7 @@ const CopilotToggle = ({
copilotEnabled: boolean; copilotEnabled: boolean;
setCopilotEnabled: (enabled: boolean) => void; setCopilotEnabled: (enabled: boolean) => void;
}) => { }) => {
const t = useTranslations('components.copilot');
return ( return (
<div className="group flex flex-row items-center space-x-1 active:scale-95 duration-200 transition cursor-pointer"> <div className="group flex flex-row items-center space-x-1 active:scale-95 duration-200 transition cursor-pointer">
<Switch <Switch
@ -15,7 +17,7 @@ const CopilotToggle = ({
onChange={setCopilotEnabled} onChange={setCopilotEnabled}
className="bg-light-secondary dark:bg-dark-secondary border border-light-200/70 dark:border-dark-200 relative inline-flex h-5 w-10 sm:h-6 sm:w-11 items-center rounded-full" className="bg-light-secondary dark:bg-dark-secondary border border-light-200/70 dark:border-dark-200 relative inline-flex h-5 w-10 sm:h-6 sm:w-11 items-center rounded-full"
> >
<span className="sr-only">Copilot</span> <span className="sr-only">{t('label')}</span>
<span <span
className={cn( className={cn(
copilotEnabled copilotEnabled
@ -34,7 +36,7 @@ const CopilotToggle = ({
: 'text-black/50 dark:text-white/50 group-hover:text-black dark:group-hover:text-white', : 'text-black/50 dark:text-white/50 group-hover:text-black dark:group-hover:text-white',
)} )}
> >
Copilot {t('label')}
</p> </p>
</div> </div>
); );

View file

@ -15,50 +15,29 @@ 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 { useTranslations } from 'next-intl';
import { useChat } from '@/lib/hooks/useChat'; import { useChat } from '@/lib/hooks/useChat';
const focusModes = [ const focusModeIcons: Record<string, JSX.Element> = {
{ webSearch: <Globe size={20} />,
key: 'webSearch', academicSearch: <SwatchBook size={20} />,
title: 'All', writingAssistant: <Pencil size={16} />,
description: 'Searches across all of the internet', wolframAlphaSearch: <BadgePercent size={20} />,
icon: <Globe size={20} />, youtubeSearch: <SiYoutube className="h-5 w-auto mr-0.5" />,
}, redditSearch: <SiReddit className="h-5 w-auto mr-0.5" />,
{ };
key: 'academicSearch',
title: 'Academic',
description: 'Search in published academic papers',
icon: <SwatchBook size={20} />,
},
{
key: 'writingAssistant',
title: 'Writing',
description: 'Chat without searching the web',
icon: <Pencil size={16} />,
},
{
key: 'wolframAlphaSearch',
title: 'Wolfram Alpha',
description: 'Computational knowledge engine',
icon: <BadgePercent size={20} />,
},
{
key: 'youtubeSearch',
title: 'Youtube',
description: 'Search and watch videos',
icon: <SiYoutube className="h-5 w-auto mr-0.5" />,
},
{
key: 'redditSearch',
title: 'Reddit',
description: 'Search for discussions and opinions',
icon: <SiReddit className="h-5 w-auto mr-0.5" />,
},
];
const Focus = () => { const Focus = () => {
const { focusMode, setFocusMode } = useChat(); const { focusMode, setFocusMode } = useChat();
const t = useTranslations('components.focus');
const modes = [
'webSearch',
'academicSearch',
'writingAssistant',
'wolframAlphaSearch',
'youtubeSearch',
'redditSearch',
];
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
@ -67,16 +46,16 @@ const Focus = () => {
> >
{focusMode !== 'webSearch' ? ( {focusMode !== 'webSearch' ? (
<div className="flex flex-row items-center space-x-1"> <div className="flex flex-row items-center space-x-1">
{focusModes.find((mode) => mode.key === focusMode)?.icon} {focusModeIcons[focusMode]}
<p className="text-xs font-medium hidden lg:block"> <p className="text-xs font-medium hidden lg:block">
{focusModes.find((mode) => mode.key === focusMode)?.title} {t(`modes.${focusMode}.title`)}
</p> </p>
<ChevronDown size={20} className="-translate-x-1" /> <ChevronDown size={20} className="-translate-x-1" />
</div> </div>
) : ( ) : (
<div className="flex flex-row items-center space-x-1"> <div className="flex flex-row items-center space-x-1">
<ScanEye size={20} /> <ScanEye size={20} />
<p className="text-xs font-medium hidden lg:block">Focus</p> <p className="text-xs font-medium hidden lg:block">{t('button')}</p>
</div> </div>
)} )}
</PopoverButton> </PopoverButton>
@ -91,13 +70,13 @@ const Focus = () => {
> >
<PopoverPanel className="absolute z-10 w-64 md:w-[500px] left-0"> <PopoverPanel className="absolute z-10 w-64 md:w-[500px] left-0">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto">
{focusModes.map((mode, i) => ( {modes.map((key, i) => (
<PopoverButton <PopoverButton
onClick={() => setFocusMode(mode.key)} onClick={() => setFocusMode(key)}
key={i} key={i}
className={cn( className={cn(
'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-2 duration-200 cursor-pointer transition', 'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-2 duration-200 cursor-pointer transition',
focusMode === mode.key focusMode === 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',
)} )}
@ -105,16 +84,18 @@ const Focus = () => {
<div <div
className={cn( className={cn(
'flex flex-row items-center space-x-1', 'flex flex-row items-center space-x-1',
focusMode === mode.key focusMode === key
? 'text-[#24A0ED]' ? 'text-[#24A0ED]'
: 'text-black dark:text-white', : 'text-black dark:text-white',
)} )}
> >
{mode.icon} {focusModeIcons[key]}
<p className="text-sm font-medium">{mode.title}</p> <p className="text-sm font-medium">
{t(`modes.${key}.title`)}
</p>
</div> </div>
<p className="text-black/70 dark:text-white/70 text-xs"> <p className="text-black/70 dark:text-white/70 text-xs">
{mode.description} {t(`modes.${key}.description`)}
</p> </p>
</PopoverButton> </PopoverButton>
))} ))}

View file

@ -7,25 +7,20 @@ import {
Transition, Transition,
} from '@headlessui/react'; } from '@headlessui/react';
import { Fragment } from 'react'; import { Fragment } from 'react';
import { useTranslations } from 'next-intl';
import { useChat } from '@/lib/hooks/useChat'; import { useChat } from '@/lib/hooks/useChat';
const OptimizationModes = [ const OptimizationModes = [
{ {
key: 'speed', key: 'speed',
title: 'Speed',
description: 'Prioritize speed and get the quickest possible answer.',
icon: <Zap size={20} className="text-[#FF9800]" />, icon: <Zap size={20} className="text-[#FF9800]" />,
}, },
{ {
key: 'balanced', key: 'balanced',
title: 'Balanced',
description: 'Find the right balance between speed and accuracy',
icon: <Sliders size={20} className="text-[#4CAF50]" />, icon: <Sliders size={20} className="text-[#4CAF50]" />,
}, },
{ {
key: 'quality', key: 'quality',
title: 'Quality (Soon)',
description: 'Get the most thorough and accurate answer',
icon: ( icon: (
<Star <Star
size={16} size={16}
@ -37,7 +32,7 @@ const OptimizationModes = [
const Optimization = () => { const Optimization = () => {
const { optimizationMode, setOptimizationMode } = useChat(); const { optimizationMode, setOptimizationMode } = useChat();
const t = useTranslations('components.optimization');
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
@ -50,10 +45,7 @@ const Optimization = () => {
?.icon ?.icon
} }
<p className="text-xs font-medium"> <p className="text-xs font-medium">
{ {t(`modes.${optimizationMode}.title`)}
OptimizationModes.find((mode) => mode.key === optimizationMode)
?.title
}
</p> </p>
<ChevronDown size={20} /> <ChevronDown size={20} />
</div> </div>
@ -84,10 +76,12 @@ const Optimization = () => {
> >
<div className="flex flex-row items-center space-x-1 text-black dark:text-white"> <div className="flex flex-row items-center space-x-1 text-black dark:text-white">
{mode.icon} {mode.icon}
<p className="text-sm font-medium">{mode.title}</p> <p className="text-sm font-medium">
{t(`modes.${mode.key}.title`)}
</p>
</div> </div>
<p className="text-black/70 dark:text-white/70 text-xs"> <p className="text-black/70 dark:text-white/70 text-xs">
{mode.description} {t(`modes.${mode.key}.description`)}
</p> </p>
</PopoverButton> </PopoverButton>
))} ))}

View file

@ -9,9 +9,11 @@ import {
import { Document } from '@langchain/core/documents'; import { Document } from '@langchain/core/documents';
import { File } from 'lucide-react'; import { File } from 'lucide-react';
import { Fragment, useState } from 'react'; import { Fragment, useState } from 'react';
import { useTranslations } from 'next-intl';
const MessageSources = ({ sources }: { sources: Document[] }) => { const MessageSources = ({ sources }: { sources: Document[] }) => {
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const t = useTranslations('components');
const closeModal = () => { const closeModal = () => {
setIsDialogOpen(false); setIsDialogOpen(false);
@ -88,7 +90,7 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
})} })}
</div> </div>
<p className="text-xs text-black/50 dark:text-white/50"> <p className="text-xs text-black/50 dark:text-white/50">
View {sources.length - 3} more {t('common.viewMore', { count: sources.length - 3 })}
</p> </p>
</button> </button>
)} )}
@ -107,7 +109,7 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
> >
<DialogPanel className="w-full max-w-md transform rounded-2xl bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 p-6 text-left align-middle shadow-xl transition-all"> <DialogPanel className="w-full max-w-md transform rounded-2xl bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 p-6 text-left align-middle shadow-xl transition-all">
<DialogTitle className="text-lg font-medium leading-6 dark:text-white"> <DialogTitle className="text-lg font-medium leading-6 dark:text-white">
Sources {t('messageBox.sources')}
</DialogTitle> </DialogTitle>
<div className="grid grid-cols-2 gap-2 overflow-auto max-h-[300px] mt-2 pr-2"> <div className="grid grid-cols-2 gap-2 overflow-auto max-h-[300px] mt-2 pr-2">
{sources.map((source, i) => ( {sources.map((source, i) => (

View file

@ -1,7 +1,8 @@
import { Clock, Edit, Share, Trash, FileText, FileDown } from 'lucide-react'; import { Clock, Edit, Share, FileText, FileDown } from 'lucide-react';
import { Message } from './ChatWindow'; import { Message } from './ChatWindow';
import { useEffect, useState, Fragment } from 'react'; import { useEffect, useState, Fragment } from 'react';
import { formatTimeDifference } from '@/lib/utils'; import { formatRelativeTime, formatDate } from '@/lib/utils';
import { useLocale, useTranslations } from 'next-intl';
import DeleteChat from './DeleteChat'; import DeleteChat from './DeleteChat';
import { import {
Popover, Popover,
@ -9,8 +10,17 @@ import {
PopoverPanel, PopoverPanel,
Transition, Transition,
} from '@headlessui/react'; } from '@headlessui/react';
import jsPDF from 'jspdf';
import { useChat } from '@/lib/hooks/useChat'; import { useChat } from '@/lib/hooks/useChat';
import jsPDF from 'jspdf';
import { ensureNotoSansTC } from '@/lib/pdfFont';
type ExportLabels = {
chatExportTitle: (p: { title: string }) => string;
exportedOn: string;
user: string;
assistant: string;
citations: string;
};
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 });
@ -26,18 +36,22 @@ const downloadFile = (filename: string, content: string, type: string) => {
}, 0); }, 0);
}; };
const exportAsMarkdown = (messages: Message[], title: string) => { const exportAsMarkdown = (
const date = new Date(messages[0]?.createdAt || Date.now()).toLocaleString(); messages: Message[],
let md = `# 💬 Chat Export: ${title}\n\n`; title: string,
md += `*Exported on: ${date}*\n\n---\n`; labels: ExportLabels,
messages.forEach((msg, idx) => { locale: string,
) => {
const date = formatDate(messages[0]?.createdAt || Date.now(), locale);
let md = `# 💬 ${labels.chatExportTitle({ title })}\n\n`;
md += `*${labels.exportedOn} ${date}*\n\n---\n`;
messages.forEach((msg) => {
md += `\n---\n`; md += `\n---\n`;
md += `**${msg.role === 'user' ? '🧑 User' : '🤖 Assistant'}** md += `**${msg.role === 'user' ? `🧑 ${labels.user}` : `🤖 ${labels.assistant}`}** \n`;
`; md += `*${formatDate(msg.createdAt, locale)}*\n\n`;
md += `*${new Date(msg.createdAt).toLocaleString()}*\n\n`;
md += `> ${msg.content.replace(/\n/g, '\n> ')}\n`; md += `> ${msg.content.replace(/\n/g, '\n> ')}\n`;
if (msg.sources && msg.sources.length > 0) { if (msg.sources && msg.sources.length > 0) {
md += `\n**Citations:**\n`; md += `\n**${labels.citations}**\n`;
msg.sources.forEach((src: any, i: number) => { msg.sources.forEach((src: any, i: number) => {
const url = src.metadata?.url || ''; const url = src.metadata?.url || '';
md += `- [${i + 1}] [${url}](${url})\n`; md += `- [${i + 1}] [${url}](${url})\n`;
@ -48,33 +62,45 @@ const exportAsMarkdown = (messages: Message[], title: string) => {
downloadFile(`${title || 'chat'}.md`, md, 'text/markdown'); downloadFile(`${title || 'chat'}.md`, md, 'text/markdown');
}; };
const exportAsPDF = (messages: Message[], title: string) => { const exportAsPDF = async (
messages: Message[],
title: string,
labels: ExportLabels,
locale: string,
) => {
const doc = new jsPDF(); const doc = new jsPDF();
const date = new Date(messages[0]?.createdAt || Date.now()).toLocaleString(); // Ensure CJK-capable font is available, then set fonts
try {
await ensureNotoSansTC(doc);
doc.setFont('NotoSansTC', 'normal');
} catch (e) {
// If network fails, fallback to default font (may garble CJK)
}
const date = formatDate(messages[0]?.createdAt || Date.now(), locale);
let y = 15; let y = 15;
const pageHeight = doc.internal.pageSize.height; const pageHeight = doc.internal.pageSize.height;
doc.setFontSize(18); doc.setFontSize(18);
doc.text(`Chat Export: ${title}`, 10, y); doc.text(labels.chatExportTitle({ title }), 10, y);
y += 8; y += 8;
doc.setFontSize(11); doc.setFontSize(11);
doc.setTextColor(100); doc.setTextColor(100);
doc.text(`Exported on: ${date}`, 10, y); doc.text(`${labels.exportedOn} ${date}`, 10, y);
y += 8; y += 8;
doc.setDrawColor(200); doc.setDrawColor(200);
doc.line(10, y, 200, y); doc.line(10, y, 200, y);
y += 6; y += 6;
doc.setTextColor(30); doc.setTextColor(30);
messages.forEach((msg, idx) => { messages.forEach((msg) => {
if (y > pageHeight - 30) { if (y > pageHeight - 30) {
doc.addPage(); doc.addPage();
y = 15; y = 15;
} }
doc.setFont('helvetica', 'bold'); doc.setFont('NotoSansTC', 'bold');
doc.text(`${msg.role === 'user' ? 'User' : 'Assistant'}`, 10, y); doc.text(`${msg.role === 'user' ? labels.user : labels.assistant}`, 10, y);
doc.setFont('helvetica', 'normal'); doc.setFont('NotoSansTC', 'normal');
doc.setFontSize(10); doc.setFontSize(10);
doc.setTextColor(120); doc.setTextColor(120);
doc.text(`${new Date(msg.createdAt).toLocaleString()}`, 40, y); doc.text(`${formatDate(msg.createdAt, locale)}`, 40, y);
y += 6; y += 6;
doc.setTextColor(30); doc.setTextColor(30);
doc.setFontSize(12); doc.setFontSize(12);
@ -94,7 +120,7 @@ const exportAsPDF = (messages: Message[], title: string) => {
doc.addPage(); doc.addPage();
y = 15; y = 15;
} }
doc.text('Citations:', 12, y); doc.text(labels.citations, 12, y);
y += 5; y += 5;
msg.sources.forEach((src: any, i: number) => { msg.sources.forEach((src: any, i: number) => {
const url = src.metadata?.url || ''; const url = src.metadata?.url || '';
@ -121,7 +147,10 @@ const exportAsPDF = (messages: Message[], title: string) => {
const Navbar = () => { const Navbar = () => {
const [title, setTitle] = useState<string>(''); const [title, setTitle] = useState<string>('');
const [timeAgo, setTimeAgo] = useState<string>(''); const tCommon = useTranslations('common');
const tNavbar = useTranslations('navbar');
const tExport = useTranslations('export');
const locale = useLocale();
const { messages, chatId } = useChat(); const { messages, chatId } = useChat();
@ -132,29 +161,9 @@ const Navbar = () => {
? `${messages[0].content.substring(0, 20).trim()}...` ? `${messages[0].content.substring(0, 20).trim()}...`
: messages[0].content; : messages[0].content;
setTitle(newTitle); setTitle(newTitle);
const newTimeAgo = formatTimeDifference(
new Date(),
messages[0].createdAt,
);
setTimeAgo(newTimeAgo);
} }
}, [messages]); }, [messages]);
useEffect(() => {
const intervalId = setInterval(() => {
if (messages.length > 0) {
const newTimeAgo = formatTimeDifference(
new Date(),
messages[0].createdAt,
);
setTimeAgo(newTimeAgo);
}
}, 1000);
return () => clearInterval(intervalId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return ( return (
<div className="fixed z-40 top-0 left-0 right-0 px-4 lg:pl-[104px] lg:pr-6 lg:px-8 flex flex-row items-center justify-between w-full py-4 text-sm text-black dark:text-white/70 border-b bg-light-primary dark:bg-dark-primary border-light-100 dark:border-dark-200"> <div className="fixed z-40 top-0 left-0 right-0 px-4 lg:pl-[104px] lg:pr-6 lg:px-8 flex flex-row items-center justify-between w-full py-4 text-sm text-black dark:text-white/70 border-b bg-light-primary dark:bg-dark-primary border-light-100 dark:border-dark-200">
<a <a
@ -165,7 +174,13 @@ const Navbar = () => {
</a> </a>
<div className="hidden lg:flex flex-row items-center justify-center space-x-2"> <div className="hidden lg:flex flex-row items-center justify-center space-x-2">
<Clock size={17} /> <Clock size={17} />
<p className="text-xs">{timeAgo} ago</p> <p className="text-xs">
{formatRelativeTime(
new Date(),
messages[0]?.createdAt || new Date(),
locale,
)}
</p>
</div> </div>
<p className="hidden lg:flex">{title}</p> <p className="hidden lg:flex">{title}</p>
@ -187,17 +202,43 @@ const Navbar = () => {
<div className="flex flex-col py-3 px-3 gap-2"> <div className="flex flex-col py-3 px-3 gap-2">
<button <button
className="flex items-center gap-2 px-4 py-2 text-left hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors text-black dark:text-white rounded-lg font-medium" className="flex items-center gap-2 px-4 py-2 text-left hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors text-black dark:text-white rounded-lg font-medium"
onClick={() => exportAsMarkdown(messages, title || '')} onClick={async () => {
exportAsMarkdown(
messages,
title || '',
{
chatExportTitle: (p) => tExport('chatExportTitle', p),
exportedOn: tCommon('exportedOn'),
user: tCommon('user'),
assistant: tCommon('assistant'),
citations: tCommon('citations'),
},
locale,
);
}}
> >
<FileText size={17} className="text-[#24A0ED]" /> <FileText size={17} className="text-[#24A0ED]" />
Export as Markdown {tNavbar('exportAsMarkdown')}
</button> </button>
<button <button
className="flex items-center gap-2 px-4 py-2 text-left hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors text-black dark:text-white rounded-lg font-medium" className="flex items-center gap-2 px-4 py-2 text-left hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors text-black dark:text-white rounded-lg font-medium"
onClick={() => exportAsPDF(messages, title || '')} onClick={async () => {
await exportAsPDF(
messages,
title || '',
{
chatExportTitle: (p) => tExport('chatExportTitle', p),
exportedOn: tCommon('exportedOn'),
user: tCommon('user'),
assistant: tCommon('assistant'),
citations: tCommon('citations'),
},
locale,
);
}}
> >
<FileDown size={17} className="text-[#24A0ED]" /> <FileDown size={17} className="text-[#24A0ED]" />
Export as PDF {tNavbar('exportAsPDF')}
</button> </button>
</div> </div>
</PopoverPanel> </PopoverPanel>

View file

@ -1,4 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Image from 'next/image';
import { useTranslations } from 'next-intl';
interface Article { interface Article {
title: string; title: string;
@ -8,6 +10,7 @@ interface Article {
} }
const NewsArticleWidget = () => { const NewsArticleWidget = () => {
const t = useTranslations('components');
const [article, setArticle] = useState<Article | null>(null); const [article, setArticle] = useState<Article | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(false); const [error, setError] = useState(false);
@ -39,13 +42,15 @@ const NewsArticleWidget = () => {
</div> </div>
</> </>
) : error ? ( ) : error ? (
<div className="w-full text-xs text-red-400">Could not load news.</div> <div className="w-full text-xs text-red-400">
{t('newsArticleWidget.error')}
</div>
) : article ? ( ) : article ? (
<a <a
href={`/?q=Summary: ${article.url}`} href={`/?q=Summary: ${article.url}`}
className="flex flex-row items-center w-full h-full group" className="flex flex-row items-center w-full h-full group"
> >
<img <Image
className="object-cover rounded-lg w-16 min-w-16 max-w-16 h-16 min-h-16 max-h-16 border border-light-200 dark:border-dark-200 bg-light-200 dark:bg-dark-200 group-hover:opacity-90 transition" className="object-cover rounded-lg w-16 min-w-16 max-w-16 h-16 min-h-16 max-h-16 border border-light-200 dark:border-dark-200 bg-light-200 dark:bg-dark-200 group-hover:opacity-90 transition"
src={ src={
new URL(article.thumbnail).origin + new URL(article.thumbnail).origin +
@ -53,6 +58,9 @@ const NewsArticleWidget = () => {
`?id=${new URL(article.thumbnail).searchParams.get('id')}` `?id=${new URL(article.thumbnail).searchParams.get('id')}`
} }
alt={article.title} alt={article.title}
width={64}
height={64}
unoptimized
/> />
<div className="flex flex-col justify-center flex-1 h-full pl-3 w-0"> <div className="flex flex-col justify-center flex-1 h-full pl-3 w-0">
<div className="font-bold text-xs text-black dark:text-white leading-tight truncate overflow-hidden whitespace-nowrap"> <div className="font-bold text-xs text-black dark:text-white leading-tight truncate overflow-hidden whitespace-nowrap">

View file

@ -1,6 +1,7 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import { ImagesIcon, PlusIcon } from 'lucide-react'; import { ImagesIcon, PlusIcon } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslations } from 'next-intl';
import Lightbox from 'yet-another-react-lightbox'; import Lightbox from 'yet-another-react-lightbox';
import 'yet-another-react-lightbox/styles.css'; import 'yet-another-react-lightbox/styles.css';
import { Message } from './ChatWindow'; import { Message } from './ChatWindow';
@ -20,6 +21,7 @@ const SearchImages = ({
chatHistory: Message[]; chatHistory: Message[];
messageId: string; messageId: string;
}) => { }) => {
const t = useTranslations('components');
const [images, setImages] = useState<Image[] | null>(null); const [images, setImages] = useState<Image[] | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -75,7 +77,7 @@ const SearchImages = ({
> >
<div className="flex flex-row items-center space-x-2"> <div className="flex flex-row items-center space-x-2">
<ImagesIcon size={17} /> <ImagesIcon size={17} />
<p>Search images</p> <p>{t('searchImages.searchButton')}</p>
</div> </div>
<PlusIcon className="text-[#24A0ED]" size={17} /> <PlusIcon className="text-[#24A0ED]" size={17} />
</button> </button>
@ -142,7 +144,7 @@ const SearchImages = ({
))} ))}
</div> </div>
<p className="text-black/70 dark:text-white/70 text-xs"> <p className="text-black/70 dark:text-white/70 text-xs">
View {images.length - 3} more {t('common.viewMore', { count: images.length - 3 })}
</p> </p>
</button> </button>
)} )}

View file

@ -4,6 +4,7 @@ import { useRef, useState } from 'react';
import Lightbox, { GenericSlide, VideoSlide } from 'yet-another-react-lightbox'; import Lightbox, { GenericSlide, VideoSlide } from 'yet-another-react-lightbox';
import 'yet-another-react-lightbox/styles.css'; import 'yet-another-react-lightbox/styles.css';
import { Message } from './ChatWindow'; import { Message } from './ChatWindow';
import { useTranslations } from 'next-intl';
type Video = { type Video = {
url: string; url: string;
@ -33,6 +34,7 @@ const Searchvideos = ({
chatHistory: Message[]; chatHistory: Message[];
messageId: string; messageId: string;
}) => { }) => {
const t = useTranslations('components');
const [videos, setVideos] = useState<Video[] | null>(null); const [videos, setVideos] = useState<Video[] | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -92,7 +94,7 @@ const Searchvideos = ({
> >
<div className="flex flex-row items-center space-x-2"> <div className="flex flex-row items-center space-x-2">
<VideoIcon size={17} /> <VideoIcon size={17} />
<p>Search videos</p> <p>{t('searchVideos.searchButton')}</p>
</div> </div>
<PlusIcon className="text-[#24A0ED]" size={17} /> <PlusIcon className="text-[#24A0ED]" size={17} />
</button> </button>
@ -131,7 +133,7 @@ const Searchvideos = ({
/> />
<div className="absolute bg-white/70 dark:bg-black/70 text-black/70 dark:text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md"> <div className="absolute bg-white/70 dark:bg-black/70 text-black/70 dark:text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
<PlayCircle size={15} /> <PlayCircle size={15} />
<p className="text-xs">Video</p> <p className="text-xs">{t('searchVideos.badge')}</p>
</div> </div>
</div> </div>
)) ))
@ -155,7 +157,7 @@ const Searchvideos = ({
/> />
<div className="absolute bg-white/70 dark:bg-black/70 text-black/70 dark:text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md"> <div className="absolute bg-white/70 dark:bg-black/70 text-black/70 dark:text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
<PlayCircle size={15} /> <PlayCircle size={15} />
<p className="text-xs">Video</p> <p className="text-xs">{t('searchVideos.badge')}</p>
</div> </div>
</div> </div>
))} ))}
@ -175,7 +177,7 @@ const Searchvideos = ({
))} ))}
</div> </div>
<p className="text-black/70 dark:text-white/70 text-xs"> <p className="text-black/70 dark:text-white/70 text-xs">
View {videos.length - 3} more {t('common.viewMore', { count: videos.length - 3 })}
</p> </p>
</button> </button>
)} )}

View file

@ -6,6 +6,7 @@ import Link from 'next/link';
import { useSelectedLayoutSegments } from 'next/navigation'; import { useSelectedLayoutSegments } from 'next/navigation';
import React, { useState, type ReactNode } from 'react'; import React, { useState, type ReactNode } from 'react';
import Layout from './Layout'; import Layout from './Layout';
import { useTranslations } from 'next-intl';
const VerticalIconContainer = ({ children }: { children: ReactNode }) => { const VerticalIconContainer = ({ children }: { children: ReactNode }) => {
return ( return (
@ -15,25 +16,26 @@ const VerticalIconContainer = ({ children }: { children: ReactNode }) => {
const Sidebar = ({ children }: { children: React.ReactNode }) => { const Sidebar = ({ children }: { children: React.ReactNode }) => {
const segments = useSelectedLayoutSegments(); const segments = useSelectedLayoutSegments();
const t = useTranslations('navigation');
const navLinks = [ const navLinks = [
{ {
icon: Home, icon: Home,
href: '/', href: '/',
active: segments.length === 0 || segments.includes('c'), active: segments.length === 0 || segments.includes('c'),
label: 'Home', label: t('home'),
}, },
{ {
icon: Search, icon: Search,
href: '/discover', href: '/discover',
active: segments.includes('discover'), active: segments.includes('discover'),
label: 'Discover', label: t('discover'),
}, },
{ {
icon: BookOpenText, icon: BookOpenText,
href: '/library', href: '/library',
active: segments.includes('library'), active: segments.includes('library'),
label: 'Library', label: t('library'),
}, },
]; ];

View file

@ -1,7 +1,11 @@
import { Cloud, Sun, CloudRain, CloudSnow, Wind } from 'lucide-react'; import { Cloud, Sun, CloudRain, CloudSnow, Wind } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Image from 'next/image';
import { useLocale, useTranslations } from 'next-intl';
const WeatherWidget = () => { const WeatherWidget = () => {
const t = useTranslations('components');
const locale = useLocale();
const [data, setData] = useState({ const [data, setData] = useState({
temperature: 0, temperature: 0,
condition: '', condition: '',
@ -42,7 +46,7 @@ const WeatherWidget = () => {
if (result.state === 'granted') { if (result.state === 'granted') {
navigator.geolocation.getCurrentPosition(async (position) => { navigator.geolocation.getCurrentPosition(async (position) => {
const res = await fetch( const res = await fetch(
`https://api-bdc.io/data/reverse-geocode-client?latitude=${position.coords.latitude}&longitude=${position.coords.longitude}&localityLanguage=en`, `https://api-bdc.io/data/reverse-geocode-client?latitude=${position.coords.latitude}&longitude=${position.coords.longitude}&localityLanguage=${locale}`,
{ {
method: 'GET', method: 'GET',
headers: { headers: {
@ -100,7 +104,7 @@ const WeatherWidget = () => {
}); });
setLoading(false); setLoading(false);
}); });
}, []); }, [locale]);
return ( return (
<div className="bg-light-secondary dark:bg-dark-secondary rounded-xl border border-light-200 dark:border-dark-200 shadow-sm flex flex-row items-center w-full h-24 min-h-[96px] max-h-[96px] px-3 py-2 gap-3"> <div className="bg-light-secondary dark:bg-dark-secondary rounded-xl border border-light-200 dark:border-dark-200 shadow-sm flex flex-row items-center w-full h-24 min-h-[96px] max-h-[96px] px-3 py-2 gap-3">
@ -125,9 +129,11 @@ const WeatherWidget = () => {
) : ( ) : (
<> <>
<div className="flex flex-col items-center justify-center w-16 min-w-16 max-w-16 h-full"> <div className="flex flex-col items-center justify-center w-16 min-w-16 max-w-16 h-full">
<img <Image
src={`/weather-ico/${data.icon}.svg`} src={`/weather-ico/${data.icon}.svg`}
alt={data.condition} alt={data.condition}
width={40}
height={40}
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">
@ -148,8 +154,10 @@ const WeatherWidget = () => {
{data.condition} {data.condition}
</span> </span>
<div className="flex flex-row justify-between w-full mt-auto pt-1 border-t border-light-200 dark:border-dark-200 text-xs text-black/60 dark:text-white/60"> <div className="flex flex-row justify-between w-full mt-auto pt-1 border-t border-light-200 dark:border-dark-200 text-xs text-black/60 dark:text-white/60">
<span>Humidity: {data.humidity}%</span> <span>
<span>Now</span> {t('weather.humidity')}: {data.humidity}%
</span>
<span>{t('weather.now')}</span>
</div> </div>
</div> </div>
</> </>

View file

@ -2,11 +2,13 @@
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import Select from '../ui/Select'; import Select from '../ui/Select';
import { useTranslations } from 'next-intl';
type Theme = 'dark' | 'light' | 'system'; type Theme = 'dark' | 'light' | 'system';
const ThemeSwitcher = ({ className }: { className?: string }) => { const ThemeSwitcher = ({ className }: { className?: string }) => {
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const t = useTranslations('components');
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
@ -50,8 +52,8 @@ const ThemeSwitcher = ({ className }: { className?: string }) => {
value={theme} value={theme}
onChange={(e) => handleThemeSwitch(e.target.value as Theme)} onChange={(e) => handleThemeSwitch(e.target.value as Theme)}
options={[ options={[
{ value: 'light', label: 'Light' }, { value: 'light', label: t('themeSwitcher.options.light') },
{ value: 'dark', label: 'Dark' }, { value: 'dark', label: t('themeSwitcher.options.dark') },
]} ]}
/> />
); );

252
src/i18n/de.json Normal file
View file

@ -0,0 +1,252 @@
{
"metadata": {
"title": "Perplexica - Mit dem Internet chatten",
"description": "Perplexica ist ein KI-Chatbot, der mit dem Internet verbunden ist."
},
"manifest": {
"name": "Perplexica - Mit dem Internet chatten",
"shortName": "Perplexica",
"description": "Perplexica ist ein KI-Chatbot, der mit dem Internet verbunden ist."
},
"navigation": {
"home": "Startseite",
"discover": "Entdecken",
"library": "Bibliothek",
"settings": "Einstellungen"
},
"common": {
"appName": "Perplexica",
"exportedOn": "Exportiert am:",
"citations": "Quellen:",
"user": "Benutzer",
"assistant": "Assistent",
"errors": {
"noChatModelsAvailable": "Keine Chat-Modelle verfügbar",
"chatProviderNotConfigured": "Es scheint, dass kein Chat-Model-Anbieter konfiguriert ist. Bitte konfigurieren Sie diesen auf der Einstellungsseite oder in der Konfigurationsdatei.",
"noEmbeddingModelsAvailable": "Keine Embedding-Modelle verfügbar",
"cannotSendBeforeConfigReady": "Nachricht kann nicht gesendet werden, bevor die Konfiguration abgeschlossen ist",
"failedToDeleteChat": "Chat konnte nicht gelöscht werden"
}
},
"navbar": {
"exportAsMarkdown": "Als Markdown exportieren",
"exportAsPDF": "Als PDF exportieren"
},
"export": {
"chatExportTitle": "Chat-Export: {title}"
},
"weather": {
"conditions": {
"clear": "Klar",
"mainlyClear": "Überwiegend klar",
"partlyCloudy": "Teilweise bewölkt",
"cloudy": "Bewölkt",
"fog": "Nebel",
"lightDrizzle": "Leichter Nieselregen",
"moderateDrizzle": "Mäßiger Nieselregen",
"denseDrizzle": "Starker Nieselregen",
"lightFreezingDrizzle": "Leichter gefrierender Nieselregen",
"denseFreezingDrizzle": "Starker gefrierender Nieselregen",
"slightRain": "Leichter Regen",
"moderateRain": "Mäßiger Regen",
"heavyRain": "Starker Regen",
"lightFreezingRain": "Leichter gefrierender Regen",
"heavyFreezingRain": "Starker gefrierender Regen",
"slightSnowFall": "Leichter Schneefall",
"moderateSnowFall": "Mäßiger Schneefall",
"heavySnowFall": "Starker Schneefall",
"snow": "Schneefall",
"slightRainShowers": "Leichte Regenschauer",
"moderateRainShowers": "Regenschauer",
"heavyRainShowers": "Starke Regenschauer",
"slightSnowShowers": "Leichte Schneeschauer",
"moderateSnowShowers": "Schneeschauer",
"heavySnowShowers": "Starke Schneeschauer",
"thunderstorm": "Gewitter",
"thunderstormSlightHail": "Gewitter mit kleinem Hagel",
"thunderstormHeavyHail": "Gewitter mit starkem Hagel"
}
},
"pages": {
"home": {
"title": "Chat - Perplexica",
"description": "Chatten Sie mit dem Internet, chatten Sie mit Perplexica."
},
"discover": {
"title": "Entdecken",
"topics": {
"tech": "Technik & Wissenschaft",
"finance": "Finanzen",
"art": "Kunst & Kultur",
"sports": "Sport",
"entertainment": "Unterhaltung"
},
"errorFetchingData": "Fehler beim Abrufen der Daten"
},
"library": {
"title": "Bibliothek",
"empty": "Keine Chats gefunden.",
"ago": "vor {time}"
},
"settings": {
"title": "Einstellungen",
"sections": {
"preferences": "Voreinstellungen",
"automaticSearch": "Automatische Suche",
"systemInstructions": "Systemanweisungen",
"modelSettings": "Modelleinstellungen",
"apiKeys": "API-Schlüssel"
},
"preferences": {
"theme": "Theme",
"measurementUnits": "Maßeinheiten",
"language": "Sprache",
"metric": "Metrisch",
"imperial": "Imperial"
},
"automaticSearch": {
"image": {
"title": "Automatische Bildsuche",
"desc": "Automatisch relevante Bilder in Chat-Antworten suchen"
},
"video": {
"title": "Automatische Videosuche",
"desc": "Automatisch relevante Videos in Chat-Antworten suchen"
}
},
"model": {
"chatProvider": "Anbieter des Chat-Modells",
"chat": "Chat-Modell",
"noModels": "Keine Modelle verfügbar",
"invalidProvider": "Ungültiger Anbieter, bitte prüfen Sie die Backend-Logs",
"custom": {
"modelName": "Modellname",
"apiKey": "Benutzerdefinierter OpenAI API-Schlüssel",
"baseUrl": "Benutzerdefinierte OpenAI-Basis-URL"
}
},
"embedding": {
"provider": "Anbieter des Embedding-Modells",
"model": "Embedding-Modell"
},
"api": {
"openaiApiKey": "OpenAI API-Schlüssel",
"ollamaApiUrl": "Ollama API-URL",
"ollamaApiKey": "Ollama API-Schlüssel (Kann leer gelassen werden)",
"groqApiKey": "GROQ API-Schlüssel",
"anthropicApiKey": "Anthropic API-Schlüssel",
"geminiApiKey": "Gemini API-Schlüssel",
"deepseekApiKey": "Deepseek API-Schlüssel",
"aimlApiKey": "AI/ML API-Schlüssel",
"lmStudioApiUrl": "LM Studio API-URL"
},
"systemInstructions": {
"placeholder": "Besondere Anweisungen für das LLM"
}
}
},
"components": {
"common": {
"viewMore": "Weitere {count} anzeigen"
},
"messageInput": {
"placeholder": "Rückfrage stellen"
},
"messageBox": {
"sources": "Quellen",
"answer": "Antwort",
"related": "Ähnlich"
},
"copilot": {
"label": "Copilot"
},
"attach": {
"attachedFiles": "Angehängte Dateien",
"add": "Hinzufügen",
"clear": "Leeren",
"attach": "Anhängen",
"uploading": "Hochladen...",
"files": "{count} Dateien"
},
"focus": {
"button": "Fokus",
"modes": {
"webSearch": {
"title": "Alle",
"description": "Im gesamten Internet suchen"
},
"academicSearch": {
"title": "Wissenschaftlich",
"description": "In veröffentlichten wissenschaftlichen Arbeiten suchen"
},
"writingAssistant": {
"title": "Schreiben",
"description": "Ohne Websuche chatten"
},
"wolframAlphaSearch": {
"title": "Wolfram Alpha",
"description": "Rechenwissen-Engine"
},
"youtubeSearch": {
"title": "YouTube",
"description": "Videos suchen und ansehen"
},
"redditSearch": {
"title": "Reddit",
"description": "Diskussionen und Meinungen suchen"
}
}
},
"optimization": {
"modes": {
"speed": {
"title": "Geschwindigkeit",
"description": "Geschwindigkeit priorisieren und schnellstmögliche Antwort erhalten"
},
"balanced": {
"title": "Ausgewogen",
"description": "Das richtige Gleichgewicht zwischen Geschwindigkeit und Genauigkeit finden"
},
"quality": {
"title": "Qualität (bald)",
"description": "Die gründlichste und genaueste Antwort erhalten"
}
}
},
"messageActions": {
"rewrite": "Neu formulieren"
},
"searchImages": {
"searchButton": "Bilder suchen"
},
"searchVideos": {
"searchButton": "Videos suchen",
"badge": "Video"
},
"weather": {
"humidity": "Luftfeuchtigkeit",
"now": "Jetzt"
},
"newsArticleWidget": {
"error": "News konnten nicht geladen werden."
},
"emptyChat": {
"title": "Forschung beginnt hier."
},
"emptyChatMessageInput": {
"placeholder": "Frag mich alles..."
},
"deleteChat": {
"title": "Löschbestätigung",
"description": "Möchten Sie diesen Chat wirklich löschen?",
"cancel": "Abbrechen",
"delete": "Löschen"
},
"themeSwitcher": {
"options": {
"light": "Hell",
"dark": "Dunkel"
}
}
}
}

252
src/i18n/en-GB.json Normal file
View file

@ -0,0 +1,252 @@
{
"metadata": {
"title": "Perplexica - Chat with the internet",
"description": "Perplexica is an AI powered chatbot that is connected to the internet."
},
"manifest": {
"name": "Perplexica - Chat with the internet",
"shortName": "Perplexica",
"description": "Perplexica is an AI powered chatbot that is connected to the internet."
},
"navigation": {
"home": "Home",
"discover": "Discover",
"library": "Library",
"settings": "Settings"
},
"common": {
"appName": "Perplexica",
"exportedOn": "Exported on:",
"citations": "Citations:",
"user": "User",
"assistant": "Assistant",
"errors": {
"noChatModelsAvailable": "No chat models available",
"chatProviderNotConfigured": "Looks like you haven't configured any chat model providers. Please configure them from the settings page or the config file.",
"noEmbeddingModelsAvailable": "No embedding models available",
"cannotSendBeforeConfigReady": "Cannot send message before the configuration is ready",
"failedToDeleteChat": "Failed to delete chat"
}
},
"navbar": {
"exportAsMarkdown": "Export as Markdown",
"exportAsPDF": "Export as PDF"
},
"export": {
"chatExportTitle": "Chat Export: {title}"
},
"weather": {
"conditions": {
"clear": "Clear",
"mainlyClear": "Mainly Clear",
"partlyCloudy": "Partly Cloudy",
"cloudy": "Cloudy",
"fog": "Fog",
"lightDrizzle": "Light Drizzle",
"moderateDrizzle": "Moderate Drizzle",
"denseDrizzle": "Dense Drizzle",
"lightFreezingDrizzle": "Light Freezing Drizzle",
"denseFreezingDrizzle": "Dense Freezing Drizzle",
"slightRain": "Slight Rain",
"moderateRain": "Moderate Rain",
"heavyRain": "Heavy Rain",
"lightFreezingRain": "Light Freezing Rain",
"heavyFreezingRain": "Heavy Freezing Rain",
"slightSnowFall": "Slight Snow Fall",
"moderateSnowFall": "Moderate Snow Fall",
"heavySnowFall": "Heavy Snow Fall",
"snow": "Snow",
"slightRainShowers": "Slight Rain Showers",
"moderateRainShowers": "Moderate Rain Showers",
"heavyRainShowers": "Heavy Rain Showers",
"slightSnowShowers": "Slight Snow Showers",
"moderateSnowShowers": "Moderate Snow Showers",
"heavySnowShowers": "Heavy Snow Showers",
"thunderstorm": "Thunderstorm",
"thunderstormSlightHail": "Thunderstorm with Slight Hail",
"thunderstormHeavyHail": "Thunderstorm with Heavy Hail"
}
},
"pages": {
"home": {
"title": "Chat - Perplexica",
"description": "Chat with the internet, chat with Perplexica."
},
"discover": {
"title": "Discover",
"topics": {
"tech": "Tech & Science",
"finance": "Finance",
"art": "Art & Culture",
"sports": "Sports",
"entertainment": "Entertainment"
},
"errorFetchingData": "Error fetching data"
},
"library": {
"title": "Library",
"empty": "No chats found.",
"ago": "{time} Ago"
},
"settings": {
"title": "Settings",
"sections": {
"preferences": "Preferences",
"automaticSearch": "Automatic Search",
"systemInstructions": "System Instructions",
"modelSettings": "Model Settings",
"apiKeys": "API Keys"
},
"preferences": {
"theme": "Theme",
"measurementUnits": "Measurement Units",
"language": "Language",
"metric": "Metric",
"imperial": "Imperial"
},
"automaticSearch": {
"image": {
"title": "Automatic Image Search",
"desc": "Automatically search for relevant images in chat responses"
},
"video": {
"title": "Automatic Video Search",
"desc": "Automatically search for relevant videos in chat responses"
}
},
"model": {
"chatProvider": "Chat Model Provider",
"chat": "Chat Model",
"noModels": "No models available",
"invalidProvider": "Invalid provider, please check backend logs",
"custom": {
"modelName": "Model Name",
"apiKey": "Custom OpenAI API Key",
"baseUrl": "Custom OpenAI Base URL"
}
},
"embedding": {
"provider": "Embedding Model Provider",
"model": "Embedding Model"
},
"api": {
"openaiApiKey": "OpenAI API Key",
"ollamaApiUrl": "Ollama API URL",
"ollamaApiKey": "Ollama API Key (Can be left blank)",
"groqApiKey": "GROQ API Key",
"anthropicApiKey": "Anthropic API Key",
"geminiApiKey": "Gemini API Key",
"deepseekApiKey": "Deepseek API Key",
"aimlApiKey": "AI/ML API Key",
"lmStudioApiUrl": "LM Studio API URL"
},
"systemInstructions": {
"placeholder": "Any special instructions for the LLM"
}
}
},
"components": {
"common": {
"viewMore": "View {count} more"
},
"messageInput": {
"placeholder": "Ask a follow-up"
},
"messageBox": {
"sources": "Sources",
"answer": "Answer",
"related": "Related"
},
"copilot": {
"label": "Copilot"
},
"attach": {
"attachedFiles": "Attached files",
"add": "Add",
"clear": "Clear",
"attach": "Attach",
"uploading": "Uploading...",
"files": "{count} files"
},
"focus": {
"button": "Focus",
"modes": {
"webSearch": {
"title": "All",
"description": "Searches across all of the internet"
},
"academicSearch": {
"title": "Academic",
"description": "Search in published academic papers"
},
"writingAssistant": {
"title": "Writing",
"description": "Chat without searching the web"
},
"wolframAlphaSearch": {
"title": "Wolfram Alpha",
"description": "Computational knowledge engine"
},
"youtubeSearch": {
"title": "YouTube",
"description": "Search and watch videos"
},
"redditSearch": {
"title": "Reddit",
"description": "Search for discussions and opinions"
}
}
},
"optimization": {
"modes": {
"speed": {
"title": "Speed",
"description": "Prioritize speed and get the quickest possible answer."
},
"balanced": {
"title": "Balanced",
"description": "Find the right balance between speed and accuracy"
},
"quality": {
"title": "Quality (Soon)",
"description": "Get the most thorough and accurate answer"
}
}
},
"messageActions": {
"rewrite": "Rewrite"
},
"searchImages": {
"searchButton": "Search images"
},
"searchVideos": {
"searchButton": "Search videos",
"badge": "Video"
},
"weather": {
"humidity": "Humidity",
"now": "Now"
},
"newsArticleWidget": {
"error": "Could not load news."
},
"emptyChat": {
"title": "Research begins here."
},
"emptyChatMessageInput": {
"placeholder": "Ask anything..."
},
"deleteChat": {
"title": "Delete Confirmation",
"description": "Are you sure you want to delete this chat?",
"cancel": "Cancel",
"delete": "Delete"
},
"themeSwitcher": {
"options": {
"light": "Light",
"dark": "Dark"
}
}
}
}

252
src/i18n/en-US.json Normal file
View file

@ -0,0 +1,252 @@
{
"metadata": {
"title": "Perplexica - Chat with the internet",
"description": "Perplexica is an AI powered chatbot that is connected to the internet."
},
"manifest": {
"name": "Perplexica - Chat with the internet",
"shortName": "Perplexica",
"description": "Perplexica is an AI powered chatbot that is connected to the internet."
},
"navigation": {
"home": "Home",
"discover": "Discover",
"library": "Library",
"settings": "Settings"
},
"common": {
"appName": "Perplexica",
"exportedOn": "Exported on:",
"citations": "Citations:",
"user": "User",
"assistant": "Assistant",
"errors": {
"noChatModelsAvailable": "No chat models available",
"chatProviderNotConfigured": "Looks like you haven't configured any chat model providers. Please configure them from the settings page or the config file.",
"noEmbeddingModelsAvailable": "No embedding models available",
"cannotSendBeforeConfigReady": "Cannot send message before the configuration is ready",
"failedToDeleteChat": "Failed to delete chat"
}
},
"navbar": {
"exportAsMarkdown": "Export as Markdown",
"exportAsPDF": "Export as PDF"
},
"export": {
"chatExportTitle": "Chat Export: {title}"
},
"weather": {
"conditions": {
"clear": "Clear",
"mainlyClear": "Mainly Clear",
"partlyCloudy": "Partly Cloudy",
"cloudy": "Cloudy",
"fog": "Fog",
"lightDrizzle": "Light Drizzle",
"moderateDrizzle": "Moderate Drizzle",
"denseDrizzle": "Dense Drizzle",
"lightFreezingDrizzle": "Light Freezing Drizzle",
"denseFreezingDrizzle": "Dense Freezing Drizzle",
"slightRain": "Slight Rain",
"moderateRain": "Moderate Rain",
"heavyRain": "Heavy Rain",
"lightFreezingRain": "Light Freezing Rain",
"heavyFreezingRain": "Heavy Freezing Rain",
"slightSnowFall": "Slight Snow Fall",
"moderateSnowFall": "Moderate Snow Fall",
"heavySnowFall": "Heavy Snow Fall",
"snow": "Snow",
"slightRainShowers": "Slight Rain Showers",
"moderateRainShowers": "Moderate Rain Showers",
"heavyRainShowers": "Heavy Rain Showers",
"slightSnowShowers": "Slight Snow Showers",
"moderateSnowShowers": "Moderate Snow Showers",
"heavySnowShowers": "Heavy Snow Showers",
"thunderstorm": "Thunderstorm",
"thunderstormSlightHail": "Thunderstorm with Slight Hail",
"thunderstormHeavyHail": "Thunderstorm with Heavy Hail"
}
},
"pages": {
"home": {
"title": "Chat - Perplexica",
"description": "Chat with the internet, chat with Perplexica."
},
"discover": {
"title": "Discover",
"topics": {
"tech": "Tech & Science",
"finance": "Finance",
"art": "Art & Culture",
"sports": "Sports",
"entertainment": "Entertainment"
},
"errorFetchingData": "Error fetching data"
},
"library": {
"title": "Library",
"empty": "No chats found.",
"ago": "{time} Ago"
},
"settings": {
"title": "Settings",
"sections": {
"preferences": "Preferences",
"automaticSearch": "Automatic Search",
"systemInstructions": "System Instructions",
"modelSettings": "Model Settings",
"apiKeys": "API Keys"
},
"preferences": {
"theme": "Theme",
"measurementUnits": "Measurement Units",
"language": "Language",
"metric": "Metric",
"imperial": "Imperial"
},
"automaticSearch": {
"image": {
"title": "Automatic Image Search",
"desc": "Automatically search for relevant images in chat responses"
},
"video": {
"title": "Automatic Video Search",
"desc": "Automatically search for relevant videos in chat responses"
}
},
"model": {
"chatProvider": "Chat Model Provider",
"chat": "Chat Model",
"noModels": "No models available",
"invalidProvider": "Invalid provider, please check backend logs",
"custom": {
"modelName": "Model Name",
"apiKey": "Custom OpenAI API Key",
"baseUrl": "Custom OpenAI Base URL"
}
},
"embedding": {
"provider": "Embedding Model Provider",
"model": "Embedding Model"
},
"api": {
"openaiApiKey": "OpenAI API Key",
"ollamaApiUrl": "Ollama API URL",
"ollamaApiKey": "Ollama API Key (Can be left blank)",
"groqApiKey": "GROQ API Key",
"anthropicApiKey": "Anthropic API Key",
"geminiApiKey": "Gemini API Key",
"deepseekApiKey": "Deepseek API Key",
"aimlApiKey": "AI/ML API Key",
"lmStudioApiUrl": "LM Studio API URL"
},
"systemInstructions": {
"placeholder": "Any special instructions for the LLM"
}
}
},
"components": {
"common": {
"viewMore": "View {count} more"
},
"messageInput": {
"placeholder": "Ask a follow-up"
},
"messageBox": {
"sources": "Sources",
"answer": "Answer",
"related": "Related"
},
"copilot": {
"label": "Copilot"
},
"attach": {
"attachedFiles": "Attached files",
"add": "Add",
"clear": "Clear",
"attach": "Attach",
"uploading": "Uploading...",
"files": "{count} files"
},
"focus": {
"button": "Focus",
"modes": {
"webSearch": {
"title": "All",
"description": "Searches across all of the internet"
},
"academicSearch": {
"title": "Academic",
"description": "Search in published academic papers"
},
"writingAssistant": {
"title": "Writing",
"description": "Chat without searching the web"
},
"wolframAlphaSearch": {
"title": "Wolfram Alpha",
"description": "Computational knowledge engine"
},
"youtubeSearch": {
"title": "YouTube",
"description": "Search and watch videos"
},
"redditSearch": {
"title": "Reddit",
"description": "Search for discussions and opinions"
}
}
},
"optimization": {
"modes": {
"speed": {
"title": "Speed",
"description": "Prioritize speed and get the quickest possible answer."
},
"balanced": {
"title": "Balanced",
"description": "Find the right balance between speed and accuracy"
},
"quality": {
"title": "Quality (Soon)",
"description": "Get the most thorough and accurate answer"
}
}
},
"messageActions": {
"rewrite": "Rewrite"
},
"searchImages": {
"searchButton": "Search images"
},
"searchVideos": {
"searchButton": "Search videos",
"badge": "Video"
},
"weather": {
"humidity": "Humidity",
"now": "Now"
},
"newsArticleWidget": {
"error": "Could not load news."
},
"emptyChat": {
"title": "Research begins here."
},
"emptyChatMessageInput": {
"placeholder": "Ask anything..."
},
"deleteChat": {
"title": "Delete Confirmation",
"description": "Are you sure you want to delete this chat?",
"cancel": "Cancel",
"delete": "Delete"
},
"themeSwitcher": {
"options": {
"light": "Light",
"dark": "Dark"
}
}
}
}

252
src/i18n/fr-CA.json Normal file
View file

@ -0,0 +1,252 @@
{
"metadata": {
"title": "Perplexica - Jaser avec Internet",
"description": "Perplexica est un chatbot IA branché sur Internet."
},
"manifest": {
"name": "Perplexica - Jaser avec Internet",
"shortName": "Perplexica",
"description": "Perplexica est un chatbot IA branché sur Internet."
},
"navigation": {
"home": "Accueil",
"discover": "Découvrir",
"library": "Bibliothèque",
"settings": "Paramètres"
},
"common": {
"appName": "Perplexica",
"exportedOn": "Exporté le :",
"citations": "Citations :",
"user": "Utilisateur",
"assistant": "Assistant",
"errors": {
"noChatModelsAvailable": "Aucun modèle de chat disponible",
"chatProviderNotConfigured": "Aucun fournisseur de modèle de chat n'est configuré. Veuillez le configurer à partir de la page des paramètres ou du fichier de configuration.",
"noEmbeddingModelsAvailable": "Aucun modèle d'embedding disponible",
"cannotSendBeforeConfigReady": "Impossible d'envoyer un message avant la fin de la configuration",
"failedToDeleteChat": "Échec de la suppression du chat"
}
},
"navbar": {
"exportAsMarkdown": "Exporter en Markdown",
"exportAsPDF": "Exporter en PDF"
},
"export": {
"chatExportTitle": "Export de conversation : {title}"
},
"weather": {
"conditions": {
"clear": "Dégagé",
"mainlyClear": "Plutôt dégagé",
"partlyCloudy": "Partiellement nuageux",
"cloudy": "Nuageux",
"fog": "Brouillard",
"lightDrizzle": "Bruine faible",
"moderateDrizzle": "Bruine modérée",
"denseDrizzle": "Bruine forte",
"lightFreezingDrizzle": "Bruine verglaçante faible",
"denseFreezingDrizzle": "Bruine verglaçante forte",
"slightRain": "Pluie faible",
"moderateRain": "Pluie modérée",
"heavyRain": "Pluie forte",
"lightFreezingRain": "Pluie verglaçante faible",
"heavyFreezingRain": "Pluie verglaçante forte",
"slightSnowFall": "Neige faible",
"moderateSnowFall": "Neige modérée",
"heavySnowFall": "Fortes chutes de neige",
"snow": "Chute de neige",
"slightRainShowers": "Averses faibles",
"moderateRainShowers": "Averses",
"heavyRainShowers": "Fortes averses",
"slightSnowShowers": "Averses de neige faibles",
"moderateSnowShowers": "Averses de neige",
"heavySnowShowers": "Fortes averses de neige",
"thunderstorm": "Orage",
"thunderstormSlightHail": "Orage avec petites grêles",
"thunderstormHeavyHail": "Orage avec fortes grêles"
}
},
"pages": {
"home": {
"title": "Chat - Perplexica",
"description": "Jasez avec Internet, jasez avec Perplexica."
},
"discover": {
"title": "Découvrir",
"topics": {
"tech": "Tech et science",
"finance": "Finance",
"art": "Art et culture",
"sports": "Sports",
"entertainment": "Divertissement"
},
"errorFetchingData": "Erreur lors de la récupération des données"
},
"library": {
"title": "Bibliothèque",
"empty": "Aucune conversation trouvée.",
"ago": "Il y a {time}"
},
"settings": {
"title": "Paramètres",
"sections": {
"preferences": "Préférences",
"automaticSearch": "Recherche automatique",
"systemInstructions": "Instructions système",
"modelSettings": "Paramètres du modèle",
"apiKeys": "Clés API"
},
"preferences": {
"theme": "Thème",
"measurementUnits": "Unités de mesure",
"language": "Langue",
"metric": "Métrique",
"imperial": "Impérial"
},
"automaticSearch": {
"image": {
"title": "Recherche d'images automatique",
"desc": "Rechercher automatiquement des images pertinentes dans les réponses du chat"
},
"video": {
"title": "Recherche de vidéos automatique",
"desc": "Rechercher automatiquement des vidéos pertinentes dans les réponses du chat"
}
},
"model": {
"chatProvider": "Fournisseur du modèle de chat",
"chat": "Modèle de chat",
"noModels": "Aucun modèle disponible",
"invalidProvider": "Fournisseur invalide, veuillez vérifier les journaux du backend",
"custom": {
"modelName": "Nom du modèle",
"apiKey": "Clé API OpenAI personnalisée",
"baseUrl": "URL de base OpenAI personnalisée"
}
},
"embedding": {
"provider": "Fournisseur du modèle d'embedding",
"model": "Modèle d'embedding"
},
"api": {
"openaiApiKey": "Clé API OpenAI",
"ollamaApiUrl": "URL de l'API Ollama",
"ollamaApiKey": "Clé API Ollama (peut être laissée vide)",
"groqApiKey": "Clé API GROQ",
"anthropicApiKey": "Clé API Anthropic",
"geminiApiKey": "Clé API Gemini",
"deepseekApiKey": "Clé API Deepseek",
"aimlApiKey": "Clé API AI/ML",
"lmStudioApiUrl": "URL de l'API LM Studio"
},
"systemInstructions": {
"placeholder": "Toute instruction spécifique pour le LLM"
}
}
},
"components": {
"common": {
"viewMore": "Voir {count} de plus"
},
"messageInput": {
"placeholder": "Poser une question de suivi"
},
"messageBox": {
"sources": "Sources",
"answer": "Réponse",
"related": "Connexe"
},
"copilot": {
"label": "Copilot"
},
"attach": {
"attachedFiles": "Fichiers joints",
"add": "Ajouter",
"clear": "Effacer",
"attach": "Joindre",
"uploading": "Téléversement...",
"files": "{count} fichiers"
},
"focus": {
"button": "Focus",
"modes": {
"webSearch": {
"title": "Tous",
"description": "Recherche sur tout Internet"
},
"academicSearch": {
"title": "Académique",
"description": "Recherche dans des articles académiques publiés"
},
"writingAssistant": {
"title": "Rédaction",
"description": "Discuter sans rechercher sur le web"
},
"wolframAlphaSearch": {
"title": "Wolfram Alpha",
"description": "Moteur de connaissances calculatoires"
},
"youtubeSearch": {
"title": "YouTube",
"description": "Rechercher et regarder des vidéos"
},
"redditSearch": {
"title": "Reddit",
"description": "Rechercher des discussions et des opinions"
}
}
},
"optimization": {
"modes": {
"speed": {
"title": "Vitesse",
"description": "Prioriser la vitesse pour obtenir la réponse la plus rapide"
},
"balanced": {
"title": "Équilibré",
"description": "Trouver un équilibre entre vitesse et précision"
},
"quality": {
"title": "Qualité (bientôt)",
"description": "Obtenir la réponse la plus complète et la plus précise"
}
}
},
"messageActions": {
"rewrite": "Réécrire"
},
"searchImages": {
"searchButton": "Rechercher des images"
},
"searchVideos": {
"searchButton": "Rechercher des vidéos",
"badge": "Vidéo"
},
"weather": {
"humidity": "Humidité",
"now": "Maintenant"
},
"newsArticleWidget": {
"error": "Impossible de charger les nouvelles."
},
"emptyChat": {
"title": "La recherche commence ici."
},
"emptyChatMessageInput": {
"placeholder": "Demandez-moi n'importe quoi..."
},
"deleteChat": {
"title": "Confirmation de suppression",
"description": "Voulez-vous vraiment supprimer cette conversation?",
"cancel": "Annuler",
"delete": "Supprimer"
},
"themeSwitcher": {
"options": {
"light": "Clair",
"dark": "Sombre"
}
}
}
}

252
src/i18n/fr-FR.json Normal file
View file

@ -0,0 +1,252 @@
{
"metadata": {
"title": "Perplexica - Discuter avec Internet",
"description": "Perplexica est un chatbot IA connecté à Internet."
},
"manifest": {
"name": "Perplexica - Discuter avec Internet",
"shortName": "Perplexica",
"description": "Perplexica est un chatbot IA connecté à Internet."
},
"navigation": {
"home": "Accueil",
"discover": "Découvrir",
"library": "Bibliothèque",
"settings": "Paramètres"
},
"common": {
"appName": "Perplexica",
"exportedOn": "Exporté le :",
"citations": "Citations :",
"user": "Utilisateur",
"assistant": "Assistant",
"errors": {
"noChatModelsAvailable": "Aucun modèle de chat disponible",
"chatProviderNotConfigured": "Aucun fournisseur de modèle de chat n'est configuré. Merci de le configurer depuis la page des paramètres ou le fichier de configuration.",
"noEmbeddingModelsAvailable": "Aucun modèle d'embedding disponible",
"cannotSendBeforeConfigReady": "Impossible d'envoyer un message avant la fin de la configuration",
"failedToDeleteChat": "Échec de la suppression du chat"
}
},
"navbar": {
"exportAsMarkdown": "Exporter en Markdown",
"exportAsPDF": "Exporter en PDF"
},
"export": {
"chatExportTitle": "Export de conversation : {title}"
},
"weather": {
"conditions": {
"clear": "Dégagé",
"mainlyClear": "Plutôt dégagé",
"partlyCloudy": "Partiellement nuageux",
"cloudy": "Nuageux",
"fog": "Brouillard",
"lightDrizzle": "Bruine faible",
"moderateDrizzle": "Bruine modérée",
"denseDrizzle": "Bruine forte",
"lightFreezingDrizzle": "Bruine verglaçante faible",
"denseFreezingDrizzle": "Bruine verglaçante forte",
"slightRain": "Pluie faible",
"moderateRain": "Pluie modérée",
"heavyRain": "Pluie forte",
"lightFreezingRain": "Pluie verglaçante faible",
"heavyFreezingRain": "Pluie verglaçante forte",
"slightSnowFall": "Neige faible",
"moderateSnowFall": "Neige modérée",
"heavySnowFall": "Fortes chutes de neige",
"snow": "Chute de neige",
"slightRainShowers": "Averses faibles",
"moderateRainShowers": "Averses",
"heavyRainShowers": "Fortes averses",
"slightSnowShowers": "Averses de neige faibles",
"moderateSnowShowers": "Averses de neige",
"heavySnowShowers": "Fortes averses de neige",
"thunderstorm": "Orage",
"thunderstormSlightHail": "Orage avec petites grêles",
"thunderstormHeavyHail": "Orage avec fortes grêles"
}
},
"pages": {
"home": {
"title": "Chat - Perplexica",
"description": "Discutez avec Internet, discutez avec Perplexica."
},
"discover": {
"title": "Découvrir",
"topics": {
"tech": "Tech et science",
"finance": "Finance",
"art": "Art et culture",
"sports": "Sports",
"entertainment": "Divertissement"
},
"errorFetchingData": "Erreur lors de la récupération des données"
},
"library": {
"title": "Bibliothèque",
"empty": "Aucune conversation trouvée.",
"ago": "Il y a {time}"
},
"settings": {
"title": "Paramètres",
"sections": {
"preferences": "Préférences",
"automaticSearch": "Recherche automatique",
"systemInstructions": "Instructions système",
"modelSettings": "Paramètres du modèle",
"apiKeys": "Clés API"
},
"preferences": {
"theme": "Thème",
"measurementUnits": "Unités de mesure",
"language": "Langue",
"metric": "Métrique",
"imperial": "Impérial"
},
"automaticSearch": {
"image": {
"title": "Recherche d'images automatique",
"desc": "Rechercher automatiquement des images pertinentes dans les réponses du chat"
},
"video": {
"title": "Recherche de vidéos automatique",
"desc": "Rechercher automatiquement des vidéos pertinentes dans les réponses du chat"
}
},
"model": {
"chatProvider": "Fournisseur du modèle de chat",
"chat": "Modèle de chat",
"noModels": "Aucun modèle disponible",
"invalidProvider": "Fournisseur invalide, veuillez vérifier les logs backend",
"custom": {
"modelName": "Nom du modèle",
"apiKey": "Clé API OpenAI personnalisée",
"baseUrl": "URL de base OpenAI personnalisée"
}
},
"embedding": {
"provider": "Fournisseur du modèle d'embedding",
"model": "Modèle d'embedding"
},
"api": {
"openaiApiKey": "Clé API OpenAI",
"ollamaApiUrl": "URL de l'API Ollama",
"ollamaApiKey": "Clé API Ollama (peut être laissée vide)",
"groqApiKey": "Clé API GROQ",
"anthropicApiKey": "Clé API Anthropic",
"geminiApiKey": "Clé API Gemini",
"deepseekApiKey": "Clé API Deepseek",
"aimlApiKey": "Clé API AI/ML",
"lmStudioApiUrl": "URL de l'API LM Studio"
},
"systemInstructions": {
"placeholder": "Toute instruction spécifique pour le LLM"
}
}
},
"components": {
"common": {
"viewMore": "Voir {count} de plus"
},
"messageInput": {
"placeholder": "Poser une question de suivi"
},
"messageBox": {
"sources": "Sources",
"answer": "Réponse",
"related": "Connexe"
},
"copilot": {
"label": "Copilot"
},
"attach": {
"attachedFiles": "Fichiers joints",
"add": "Ajouter",
"clear": "Effacer",
"attach": "Joindre",
"uploading": "Téléversement...",
"files": "{count} fichiers"
},
"focus": {
"button": "Focus",
"modes": {
"webSearch": {
"title": "Tous",
"description": "Recherche sur tout Internet"
},
"academicSearch": {
"title": "Académique",
"description": "Recherche dans des articles académiques publiés"
},
"writingAssistant": {
"title": "Rédaction",
"description": "Discuter sans rechercher sur le web"
},
"wolframAlphaSearch": {
"title": "Wolfram Alpha",
"description": "Moteur de connaissance computationnelle"
},
"youtubeSearch": {
"title": "YouTube",
"description": "Rechercher et regarder des vidéos"
},
"redditSearch": {
"title": "Reddit",
"description": "Rechercher des discussions et des opinions"
}
}
},
"optimization": {
"modes": {
"speed": {
"title": "Vitesse",
"description": "Prioriser la vitesse pour obtenir la réponse la plus rapide"
},
"balanced": {
"title": "Équilibré",
"description": "Trouver un équilibre entre vitesse et précision"
},
"quality": {
"title": "Qualité (bientôt)",
"description": "Obtenir la réponse la plus complète et la plus précise"
}
}
},
"messageActions": {
"rewrite": "Réécrire"
},
"searchImages": {
"searchButton": "Rechercher des images"
},
"searchVideos": {
"searchButton": "Rechercher des vidéos",
"badge": "Vidéo"
},
"weather": {
"humidity": "Humidité",
"now": "Maintenant"
},
"newsArticleWidget": {
"error": "Impossible de charger les actualités."
},
"emptyChat": {
"title": "La recherche commence ici."
},
"emptyChatMessageInput": {
"placeholder": "Demandez-moi n'importe quoi..."
},
"deleteChat": {
"title": "Confirmation de suppression",
"description": "Êtes-vous sûr de vouloir supprimer cette conversation ?",
"cancel": "Annuler",
"delete": "Supprimer"
},
"themeSwitcher": {
"options": {
"light": "Clair",
"dark": "Sombre"
}
}
}
}

252
src/i18n/ja.json Normal file
View file

@ -0,0 +1,252 @@
{
"metadata": {
"title": "Perplexica - インターネットと会話",
"description": "Perplexica はインターネットに接続された AI チャットボットです。"
},
"manifest": {
"name": "Perplexica - インターネットと会話",
"shortName": "Perplexica",
"description": "Perplexica はインターネットに接続された AI チャットボットです。"
},
"navigation": {
"home": "ホーム",
"discover": "ディスカバー",
"library": "ライブラリ",
"settings": "設定"
},
"common": {
"appName": "Perplexica",
"exportedOn": "書き出し日:",
"citations": "参考文献:",
"user": "ユーザー",
"assistant": "アシスタント",
"errors": {
"noChatModelsAvailable": "利用可能なチャットモデルがありません",
"chatProviderNotConfigured": "チャットモデルのプロバイダーが設定されていません。設定ページまたは設定ファイルで設定してください。",
"noEmbeddingModelsAvailable": "利用可能な埋め込みモデルがありません",
"cannotSendBeforeConfigReady": "設定が完了するまでメッセージを送信できません",
"failedToDeleteChat": "チャットの削除に失敗しました"
}
},
"navbar": {
"exportAsMarkdown": "Markdown として書き出し",
"exportAsPDF": "PDF として書き出し"
},
"export": {
"chatExportTitle": "チャット書き出し:{title}"
},
"weather": {
"conditions": {
"clear": "快晴",
"mainlyClear": "晴れ時々薄曇り",
"partlyCloudy": "所により曇り",
"cloudy": "曇り",
"fog": "霧",
"lightDrizzle": "弱い霧雨",
"moderateDrizzle": "やや強い霧雨",
"denseDrizzle": "強い霧雨",
"lightFreezingDrizzle": "弱い着氷性霧雨",
"denseFreezingDrizzle": "強い着氷性霧雨",
"slightRain": "小雨",
"moderateRain": "雨",
"heavyRain": "大雨",
"lightFreezingRain": "弱い凍雨",
"heavyFreezingRain": "強い凍雨",
"slightSnowFall": "小雪",
"moderateSnowFall": "雪",
"heavySnowFall": "大雪",
"snow": "降雪",
"slightRainShowers": "にわか小雨",
"moderateRainShowers": "にわか雨",
"heavyRainShowers": "激しいにわか雨",
"slightSnowShowers": "にわか小雪",
"moderateSnowShowers": "にわか雪",
"heavySnowShowers": "激しいにわか雪",
"thunderstorm": "雷雨",
"thunderstormSlightHail": "雷雨(小さな雹)",
"thunderstormHeavyHail": "雷雨(大きな雹)"
}
},
"pages": {
"home": {
"title": "チャット - Perplexica",
"description": "インターネットと会話し、Perplexica と対話しましょう。"
},
"discover": {
"title": "ディスカバー",
"topics": {
"tech": "テック・サイエンス",
"finance": "ファイナンス",
"art": "アート・文化",
"sports": "スポーツ",
"entertainment": "エンタメ"
},
"errorFetchingData": "データの取得中にエラーが発生しました"
},
"library": {
"title": "ライブラリ",
"empty": "チャットが見つかりません。",
"ago": "{time} 前"
},
"settings": {
"title": "設定",
"sections": {
"preferences": "環境設定",
"automaticSearch": "自動検索",
"systemInstructions": "システム指示",
"modelSettings": "モデル設定",
"apiKeys": "API キー"
},
"preferences": {
"theme": "テーマ",
"measurementUnits": "単位",
"language": "言語",
"metric": "メートル法",
"imperial": "ヤード・ポンド法"
},
"automaticSearch": {
"image": {
"title": "自動画像検索",
"desc": "チャットの回答で関連画像を自動検索"
},
"video": {
"title": "自動画像検索",
"desc": "チャットの回答で関連動画を自動検索"
}
},
"model": {
"chatProvider": "チャットモデルのプロバイダー",
"chat": "チャットモデル",
"noModels": "利用可能なモデルはありません",
"invalidProvider": "無効なプロバイダーです。バックエンドのログを確認してください",
"custom": {
"modelName": "モデル名",
"apiKey": "カスタム OpenAI API キー",
"baseUrl": "カスタム OpenAI Base URL"
}
},
"embedding": {
"provider": "埋め込みモデルのプロバイダー",
"model": "埋め込みモデル"
},
"api": {
"openaiApiKey": "OpenAI API キー",
"ollamaApiUrl": "Ollama API URL",
"ollamaApiKey": "Ollama API キー(空白のままにすることもできます)",
"groqApiKey": "GROQ API キー",
"anthropicApiKey": "Anthropic API キー",
"geminiApiKey": "Gemini API キー",
"deepseekApiKey": "Deepseek API キー",
"aimlApiKey": "AI/ML API キー",
"lmStudioApiUrl": "LM Studio API URL"
},
"systemInstructions": {
"placeholder": "LLM への特別な指示があれば入力してください"
}
}
},
"components": {
"common": {
"viewMore": "さらに {count} 件を表示"
},
"messageInput": {
"placeholder": "追質問する"
},
"messageBox": {
"sources": "出典",
"answer": "回答",
"related": "関連"
},
"copilot": {
"label": "Copilot"
},
"attach": {
"attachedFiles": "添付ファイル",
"add": "追加",
"clear": "クリア",
"attach": "添付",
"uploading": "アップロード中...",
"files": "{count} 件のファイル"
},
"focus": {
"button": "フォーカス",
"modes": {
"webSearch": {
"title": "すべて",
"description": "インターネット全体を検索"
},
"academicSearch": {
"title": "学術",
"description": "公開済みの学術論文を検索"
},
"writingAssistant": {
"title": "ライティング",
"description": "ウェブ検索をせずにチャット"
},
"wolframAlphaSearch": {
"title": "Wolfram Alpha",
"description": "計算知識エンジン"
},
"youtubeSearch": {
"title": "YouTube",
"description": "動画を検索・視聴"
},
"redditSearch": {
"title": "Reddit",
"description": "議論と意見を検索"
}
}
},
"optimization": {
"modes": {
"speed": {
"title": "スピード",
"description": "速度を優先し、最速で回答を取得"
},
"balanced": {
"title": "バランス",
"description": "速度と正確性のバランスを取る"
},
"quality": {
"title": "クオリティ(近日対応)",
"description": "最も丁寧で正確な回答を取得"
}
}
},
"messageActions": {
"rewrite": "書き直す"
},
"searchImages": {
"searchButton": "画像を検索"
},
"searchVideos": {
"searchButton": "動画を検索",
"badge": "動画"
},
"weather": {
"humidity": "湿度",
"now": "現在"
},
"newsArticleWidget": {
"error": "ニュースを読み込めませんでした。"
},
"emptyChat": {
"title": "ここからリサーチが始まる。"
},
"emptyChatMessageInput": {
"placeholder": "何でも聞いてください..."
},
"deleteChat": {
"title": "削除の確認",
"description": "このチャットを削除してよろしいですか?",
"cancel": "キャンセル",
"delete": "削除"
},
"themeSwitcher": {
"options": {
"light": "ライト",
"dark": "ダーク"
}
}
}
}

252
src/i18n/ko.json Normal file
View file

@ -0,0 +1,252 @@
{
"metadata": {
"title": "Perplexica - 인터넷과 대화",
"description": "Perplexica는 인터넷에 연결된 AI 챗봇입니다."
},
"manifest": {
"name": "Perplexica - 인터넷과 대화",
"shortName": "Perplexica",
"description": "Perplexica는 인터넷에 연결된 AI 챗봇입니다."
},
"navigation": {
"home": "홈",
"discover": "디스커버",
"library": "라이브러리",
"settings": "설정"
},
"common": {
"appName": "Perplexica",
"exportedOn": "내보낸 날짜:",
"citations": "참고 출처:",
"user": "사용자",
"assistant": "도우미",
"errors": {
"noChatModelsAvailable": "사용 가능한 채팅 모델이 없습니다",
"chatProviderNotConfigured": "채팅 모델 공급자가 구성되지 않은 것 같습니다. 설정 페이지 또는 구성 파일에서 설정하세요.",
"noEmbeddingModelsAvailable": "사용 가능한 임베딩 모델이 없습니다",
"cannotSendBeforeConfigReady": "구성이 완료되기 전에는 메시지를 보낼 수 없습니다",
"failedToDeleteChat": "채팅 삭제에 실패했습니다"
}
},
"navbar": {
"exportAsMarkdown": "Markdown으로 내보내기",
"exportAsPDF": "PDF로 내보내기"
},
"export": {
"chatExportTitle": "채팅 내보내기: {title}"
},
"weather": {
"conditions": {
"clear": "맑음",
"mainlyClear": "대체로 맑음",
"partlyCloudy": "부분적으로 구름",
"cloudy": "흐림",
"fog": "안개",
"lightDrizzle": "약한 이슬비",
"moderateDrizzle": "보통 이슬비",
"denseDrizzle": "강한 이슬비",
"lightFreezingDrizzle": "약한 착빙성 이슬비",
"denseFreezingDrizzle": "강한 착빙성 이슬비",
"slightRain": "약한 비",
"moderateRain": "보통 비",
"heavyRain": "강한 비",
"lightFreezingRain": "약한 어는 비",
"heavyFreezingRain": "강한 어는 비",
"slightSnowFall": "약한 눈",
"moderateSnowFall": "눈",
"heavySnowFall": "폭설",
"snow": "강설",
"slightRainShowers": "약한 소나기",
"moderateRainShowers": "소나기",
"heavyRainShowers": "강한 소나기",
"slightSnowShowers": "약한 눈보라",
"moderateSnowShowers": "눈보라",
"heavySnowShowers": "강한 눈보라",
"thunderstorm": "뇌우",
"thunderstormSlightHail": "약한 우박을 동반한 뇌우",
"thunderstormHeavyHail": "강한 우박을 동반한 뇌우"
}
},
"pages": {
"home": {
"title": "채팅 - Perplexica",
"description": "인터넷과 대화하고 Perplexica와 대화하세요."
},
"discover": {
"title": "디스커버",
"topics": {
"tech": "기술과 과학",
"finance": "금융",
"art": "예술과 문화",
"sports": "스포츠",
"entertainment": "엔터테인먼트"
},
"errorFetchingData": "데이터를 가져오는 중 오류가 발생했습니다"
},
"library": {
"title": "라이브러리",
"empty": "채팅을 찾을 수 없습니다.",
"ago": "{time} 전"
},
"settings": {
"title": "설정",
"sections": {
"preferences": "기본 설정",
"automaticSearch": "자동 검색",
"systemInstructions": "시스템 지시",
"modelSettings": "모델 설정",
"apiKeys": "API 키"
},
"preferences": {
"theme": "테마",
"measurementUnits": "단위",
"language": "언어",
"metric": "미터법",
"imperial": "야드파운드법"
},
"automaticSearch": {
"image": {
"title": "자동 이미지 검색",
"desc": "채팅 응답에서 관련 이미지를 자동으로 검색"
},
"video": {
"title": "자동 비디오 검색",
"desc": "채팅 응답에서 관련 비디오를 자동으로 검색"
}
},
"model": {
"chatProvider": "채팅 모델 공급자",
"chat": "채팅 모델",
"noModels": "사용 가능한 모델이 없습니다",
"invalidProvider": "잘못된 공급자입니다. 백엔드 로그를 확인하세요",
"custom": {
"modelName": "모델 이름",
"apiKey": "사용자 지정 OpenAI API 키",
"baseUrl": "사용자 지정 OpenAI Base URL"
}
},
"embedding": {
"provider": "임베딩 모델 공급자",
"model": "임베딩 모델"
},
"api": {
"openaiApiKey": "OpenAI API 키",
"ollamaApiUrl": "Ollama API URL",
"ollamaApiKey": "Ollama API 키 (비워둘 수 있음)",
"groqApiKey": "GROQ API 키",
"anthropicApiKey": "Anthropic API 키",
"geminiApiKey": "Gemini API 키",
"deepseekApiKey": "Deepseek API 키",
"aimlApiKey": "AI/ML API 키",
"lmStudioApiUrl": "LM Studio API URL"
},
"systemInstructions": {
"placeholder": "LLM에 대한 특별한 지시사항"
}
}
},
"components": {
"common": {
"viewMore": "추가 {count}개 보기"
},
"messageInput": {
"placeholder": "후속 질문하기"
},
"messageBox": {
"sources": "출처",
"answer": "답변",
"related": "관련"
},
"copilot": {
"label": "Copilot"
},
"attach": {
"attachedFiles": "첨부된 파일",
"add": "추가",
"clear": "지우기",
"attach": "첨부",
"uploading": "업로드 중...",
"files": "{count}개 파일"
},
"focus": {
"button": "포커스",
"modes": {
"webSearch": {
"title": "전체",
"description": "인터넷 전체 검색"
},
"academicSearch": {
"title": "학술",
"description": "발표된 학술 논문 검색"
},
"writingAssistant": {
"title": "글쓰기",
"description": "웹을 검색하지 않고 채팅"
},
"wolframAlphaSearch": {
"title": "Wolfram Alpha",
"description": "계산 지식 엔진"
},
"youtubeSearch": {
"title": "YouTube",
"description": "동영상 검색 및 시청"
},
"redditSearch": {
"title": "Reddit",
"description": "토론과 의견 검색"
}
}
},
"optimization": {
"modes": {
"speed": {
"title": "속도",
"description": "속도를 우선하여 가장 빠른 답변 얻기"
},
"balanced": {
"title": "균형",
"description": "속도와 정확성의 균형 맞추기"
},
"quality": {
"title": "품질(곧 제공)",
"description": "가장 철저하고 정확한 답변 얻기"
}
}
},
"messageActions": {
"rewrite": "다시 쓰기"
},
"searchImages": {
"searchButton": "이미지 검색"
},
"searchVideos": {
"searchButton": "동영상 검색",
"badge": "동영상"
},
"weather": {
"humidity": "습도",
"now": "지금"
},
"newsArticleWidget": {
"error": "뉴스를 불러올 수 없습니다."
},
"emptyChat": {
"title": "연구는 여기에서 시작됩니다."
},
"emptyChatMessageInput": {
"placeholder": "아무거나 물어보세요..."
},
"deleteChat": {
"title": "삭제 확인",
"description": "이 채팅을 삭제하시겠습니까?",
"cancel": "취소",
"delete": "삭제"
},
"themeSwitcher": {
"options": {
"light": "라이트",
"dark": "다크"
}
}
}
}

42
src/i18n/locales.ts Normal file
View file

@ -0,0 +1,42 @@
// IETF BCP 47 codes, see https://www.rfc-editor.org/rfc/bcp/bcp47.txt. {ISO 639-1}-{ISO 3166-1 alpha-2}
export const LOCALES = [
'en-US',
'en-GB',
'zh-TW',
'zh-HK',
'zh-CN',
'ja',
'ko',
'fr-FR',
'fr-CA',
'de',
] as const;
export type AppLocale = (typeof LOCALES)[number];
// Default locale for fallbacks
export const DEFAULT_LOCALE: AppLocale = 'en-US';
// UI labels for language options
export const LOCALE_LABELS: Record<AppLocale, string> = {
'en-US': 'English (US)',
'en-GB': 'English (UK)',
'zh-TW': '繁體中文(台灣)',
'zh-HK': '繁體中文(香港)',
'zh-CN': '简体中文',
ja: '日本語',
ko: '한국어',
'fr-FR': 'Français (France)',
'fr-CA': 'Français (Canada)',
de: 'Deutsch',
};
// Human-readable language name for prompt
export function getPromptLanguageName(loc: string): string {
const l = (loc || '').toLowerCase();
const match = (
Object.keys(LOCALE_LABELS) as Array<keyof typeof LOCALE_LABELS>
).find((k) => k.toLowerCase() === l);
if (match) return LOCALE_LABELS[match];
return LOCALE_LABELS[DEFAULT_LOCALE];
}

115
src/i18n/request.ts Normal file
View file

@ -0,0 +1,115 @@
import { cookies, headers } from 'next/headers';
import { getRequestConfig } from 'next-intl/server';
import { LOCALES, DEFAULT_LOCALE, type AppLocale } from './locales';
export default getRequestConfig(async () => {
const cookieStore = await cookies();
const rawCookieLocale = cookieStore.get('locale')?.value;
// Helper: parse Accept-Language and pick best supported locale
function resolveFromAcceptLanguage(al: string | null | undefined): AppLocale {
const supported = new Set<string>(LOCALES as readonly string[]);
const raw = (al || '').toLowerCase();
if (!raw) return DEFAULT_LOCALE;
type Candidate = { tag: string; q: number };
const candidates: Candidate[] = raw
.split(',')
.map((part) => part.trim())
.filter(Boolean)
.map((part) => {
const [tagPart, ...params] = part.split(';');
const tag = tagPart.trim();
let q = 1;
for (const p of params) {
const m = p.trim().match(/^q=([0-9.]+)$/);
if (m) {
const v = parseFloat(m[1]);
if (!Number.isNaN(v)) q = v;
}
}
return { tag, q } as Candidate;
})
.sort((a, b) => b.q - a.q);
// Try in order: exact match -> base language match -> custom mapping
for (const { tag } of candidates) {
// exact match against supported
const exact = Array.from(supported).find((s) => s.toLowerCase() === tag);
if (exact) return exact as AppLocale;
// base language match (e.g., en-US -> en-GB/en-US: prefer en-US if available)
const base = tag.split('-')[0];
const englishVariants = Array.from(supported).filter((s) =>
s.toLowerCase().startsWith('en-'),
) as AppLocale[];
if (base === 'en' && englishVariants.length > 0) {
// prefer en-US as default English
const enUS = englishVariants.find((e) => e.toLowerCase() === 'en-us');
return (enUS || englishVariants[0]) as AppLocale;
}
const baseMatch = Array.from(supported).find(
(s) => s.split('-')[0].toLowerCase() === base,
);
if (baseMatch) return baseMatch as AppLocale;
// custom mapping for Chinese:
// - zh-HK -> zh-HK
// - zh-TW -> zh-TW
// - zh-CN, zh-SG -> zh-CN
if (tag.startsWith('zh')) {
if (/^zh-(hk)/i.test(tag)) return 'zh-HK';
if (/^zh-(tw)/i.test(tag)) return 'zh-TW';
if (/^zh-(cn|sg)/i.test(tag)) return 'zh-CN';
// default Chinese fallback: zh-TW
return 'zh-TW';
}
}
return DEFAULT_LOCALE;
}
// Normalize any incoming locale (including legacy cookies like 'en' or 'fr')
function normalizeToSupported(loc?: string | null): AppLocale | null {
const val = (loc || '').trim();
if (!val) return null;
const lower = val.toLowerCase();
// exact case-insensitive match against supported
const exact = (LOCALES as readonly string[]).find(
(s) => s.toLowerCase() === lower,
);
if (exact) return exact as AppLocale;
// map base tags to preferred regional variants
const base = lower.split('-')[0];
if (base === 'en') return 'en-US';
if (base === 'fr') return 'fr-FR';
if (base === 'zh') {
if (/^zh-(hk)/i.test(lower)) return 'zh-HK';
if (/^zh-(tw)/i.test(lower)) return 'zh-TW';
if (/^zh-(cn|sg)/i.test(lower)) return 'zh-CN';
// default Chinese fallback
return 'zh-TW';
}
// try base language match generically
const baseMatch = (LOCALES as readonly string[]).find(
(s) => s.split('-')[0].toLowerCase() === base,
);
return (baseMatch as AppLocale) || null;
}
// Prefer normalized cookie if present and valid; otherwise use Accept-Language
let locale: AppLocale;
const normalizedCookie = normalizeToSupported(rawCookieLocale);
if (normalizedCookie) {
locale = normalizedCookie;
} else {
const hdrs = await headers();
const acceptLanguage = hdrs.get('accept-language');
locale = resolveFromAcceptLanguage(acceptLanguage);
}
return {
locale,
messages: (await import(`./${locale}.json`)).default,
};
});

252
src/i18n/zh-CN.json Normal file
View file

@ -0,0 +1,252 @@
{
"metadata": {
"title": "Perplexica - 与网络对话",
"description": "Perplexica 是一个能连接互联网的 AI 聊天机器人。"
},
"manifest": {
"name": "Perplexica - 与网络对话",
"shortName": "Perplexica",
"description": "Perplexica 是一个能连接互联网的 AI 聊天机器人。"
},
"navigation": {
"home": "首页",
"discover": "探索",
"library": "资料库",
"settings": "设置"
},
"common": {
"appName": "Perplexica",
"exportedOn": "导出日期:",
"citations": "参考来源:",
"user": "使用者",
"assistant": "助理",
"errors": {
"noChatModelsAvailable": "没有可用的聊天模型",
"chatProviderNotConfigured": "看起来你还没有配置任何聊天模型供应商,请到设置页或配置文件进行设置。",
"noEmbeddingModelsAvailable": "没有可用的向量嵌入模型",
"cannotSendBeforeConfigReady": "在设置就绪前无法发送消息",
"failedToDeleteChat": "删除聊天失败"
}
},
"navbar": {
"exportAsMarkdown": "导出为 Markdown",
"exportAsPDF": "导出为 PDF"
},
"export": {
"chatExportTitle": "聊天导出:{title}"
},
"weather": {
"conditions": {
"clear": "晴朗",
"mainlyClear": "大致晴朗",
"partlyCloudy": "局部多云",
"cloudy": "多云",
"fog": "雾",
"lightDrizzle": "小毛毛雨",
"moderateDrizzle": "中等毛毛雨",
"denseDrizzle": "大毛毛雨",
"lightFreezingDrizzle": "轻微冰雾雨",
"denseFreezingDrizzle": "强冰雾雨",
"slightRain": "小雨",
"moderateRain": "中雨",
"heavyRain": "大雨",
"lightFreezingRain": "轻微冻雨",
"heavyFreezingRain": "强冻雨",
"slightSnowFall": "小雪",
"moderateSnowFall": "中雪",
"heavySnowFall": "大雪",
"snow": "降雪",
"slightRainShowers": "短暂小雨",
"moderateRainShowers": "短暂中雨",
"heavyRainShowers": "短暂大雨",
"slightSnowShowers": "短暂小雪",
"moderateSnowShowers": "短暂中雪",
"heavySnowShowers": "短暂大雪",
"thunderstorm": "雷雨",
"thunderstormSlightHail": "雷雨伴随小冰雹",
"thunderstormHeavyHail": "雷雨伴随大冰雹"
}
},
"pages": {
"home": {
"title": "聊天 - Perplexica",
"description": "连上互联网聊聊天,与 Perplexica 对话。"
},
"discover": {
"title": "探索",
"topics": {
"tech": "科技与科学",
"finance": "财经",
"art": "艺术与文化",
"sports": "运动",
"entertainment": "娱乐"
},
"errorFetchingData": "获取数据时发生错误"
},
"library": {
"title": "资料库",
"empty": "没有找到任何聊天记录。",
"ago": "{time} 前"
},
"settings": {
"title": "设置",
"sections": {
"preferences": "偏好设置",
"automaticSearch": "自动搜索",
"systemInstructions": "系统指示",
"modelSettings": "模型设置",
"apiKeys": "API 密钥"
},
"preferences": {
"theme": "主题",
"measurementUnits": "计量单位",
"language": "语言",
"metric": "公制",
"imperial": "英制"
},
"automaticSearch": {
"image": {
"title": "自动图片搜索",
"desc": "在聊天回复中自动搜索相关图片"
},
"video": {
"title": "自动视频搜索",
"desc": "在聊天回复中自动搜索相关视频"
}
},
"model": {
"chatProvider": "聊天模型供应商",
"chat": "聊天模型",
"noModels": "没有可用的模型",
"invalidProvider": "供应商无效,请检查后端日志",
"custom": {
"modelName": "模型名称",
"apiKey": "自定义 OpenAI API 密钥",
"baseUrl": "自定义 OpenAI Base URL"
}
},
"embedding": {
"provider": "向量嵌入供应商",
"model": "向量嵌入模型"
},
"api": {
"openaiApiKey": "OpenAI API 密钥",
"ollamaApiUrl": "Ollama API 地址",
"ollamaApiKey": "Ollama API 密钥(可留空)",
"groqApiKey": "GROQ API 密钥",
"anthropicApiKey": "Anthropic API 密钥",
"geminiApiKey": "Gemini API 密钥",
"deepseekApiKey": "Deepseek API 密钥",
"aimlApiKey": "AI/ML 密钥",
"lmStudioApiUrl": "LM Studio API 地址"
},
"systemInstructions": {
"placeholder": "任何要给 LLM 的特别指示"
}
}
},
"components": {
"common": {
"viewMore": "查看另外 {count} 项"
},
"messageInput": {
"placeholder": "提出追问"
},
"messageBox": {
"sources": "参考来源",
"answer": "回答",
"related": "相关内容"
},
"copilot": {
"label": "Copilot"
},
"attach": {
"attachedFiles": "已附加的文件",
"add": "新增",
"clear": "清除",
"attach": "附加",
"uploading": "上传中...",
"files": "{count} 个文件"
},
"focus": {
"button": "焦点",
"modes": {
"webSearch": {
"title": "全部",
"description": "在整个互联网搜索"
},
"academicSearch": {
"title": "学术",
"description": "搜索已发表的学术论文"
},
"writingAssistant": {
"title": "写作",
"description": "不搜索网络,直接聊天"
},
"wolframAlphaSearch": {
"title": "Wolfram Alpha",
"description": "计算型知识引擎"
},
"youtubeSearch": {
"title": "YouTube",
"description": "搜索和观看视频"
},
"redditSearch": {
"title": "Reddit",
"description": "搜索讨论与观点"
}
}
},
"optimization": {
"modes": {
"speed": {
"title": "速度",
"description": "优先速度,以最快的方式得到答案。"
},
"balanced": {
"title": "平衡",
"description": "在速度与准确度之间取得平衡"
},
"quality": {
"title": "品质(即将推出)",
"description": "取得最完整与最精确的回答"
}
}
},
"messageActions": {
"rewrite": "重写"
},
"searchImages": {
"searchButton": "搜索图片"
},
"searchVideos": {
"searchButton": "搜索视频",
"badge": "视频"
},
"weather": {
"humidity": "湿度",
"now": "现在"
},
"newsArticleWidget": {
"error": "无法载入新闻。"
},
"emptyChat": {
"title": "研究从这里开始。"
},
"emptyChatMessageInput": {
"placeholder": "问我任何事..."
},
"deleteChat": {
"title": "删除确认",
"description": "确定要删除此聊天吗?",
"cancel": "取消",
"delete": "删除"
},
"themeSwitcher": {
"options": {
"light": "浅色",
"dark": "深色"
}
}
}
}

252
src/i18n/zh-HK.json Normal file
View file

@ -0,0 +1,252 @@
{
"metadata": {
"title": "Perplexica - 與網絡對話",
"description": "Perplexica 是一個能連接互聯網的 AI 聊天機械人。"
},
"manifest": {
"name": "Perplexica - 與網絡對話",
"shortName": "Perplexica",
"description": "Perplexica 是一個能連接互聯網的 AI 聊天機械人。"
},
"navigation": {
"home": "主頁",
"discover": "探索",
"library": "資料庫",
"settings": "設定"
},
"common": {
"appName": "Perplexica",
"exportedOn": "匯出日期:",
"citations": "參考來源:",
"user": "用戶",
"assistant": "助理",
"errors": {
"noChatModelsAvailable": "沒有可用的聊天模型",
"chatProviderNotConfigured": "你似乎未有設定任何聊天模型供應商,請到設定頁或設定檔進行設定。",
"noEmbeddingModelsAvailable": "沒有可用的向量嵌入模型",
"cannotSendBeforeConfigReady": "在設定完成前無法發送訊息",
"failedToDeleteChat": "刪除聊天失敗"
}
},
"navbar": {
"exportAsMarkdown": "匯出為 Markdown",
"exportAsPDF": "匯出為 PDF"
},
"export": {
"chatExportTitle": "聊天匯出:{title}"
},
"weather": {
"conditions": {
"clear": "天晴",
"mainlyClear": "大致天晴",
"partlyCloudy": "局部多雲",
"cloudy": "多雲",
"fog": "霧",
"lightDrizzle": "微毛毛雨",
"moderateDrizzle": "毛毛雨",
"denseDrizzle": "密集毛毛雨",
"lightFreezingDrizzle": "輕微凍霧雨",
"denseFreezingDrizzle": "強凍霧雨",
"slightRain": "微雨",
"moderateRain": "雨",
"heavyRain": "大雨",
"lightFreezingRain": "輕微凍雨",
"heavyFreezingRain": "強凍雨",
"slightSnowFall": "微雪",
"moderateSnowFall": "下雪",
"heavySnowFall": "大雪",
"snow": "降雪",
"slightRainShowers": "驟雨",
"moderateRainShowers": "驟雨",
"heavyRainShowers": "大驟雨",
"slightSnowShowers": "驟雪",
"moderateSnowShowers": "驟雪",
"heavySnowShowers": "大驟雪",
"thunderstorm": "雷暴",
"thunderstormSlightHail": "雷暴(小冰雹)",
"thunderstormHeavyHail": "雷暴(大冰雹)"
}
},
"pages": {
"home": {
"title": "聊天 - Perplexica",
"description": "連接網絡傾下計,與 Perplexica 對話。"
},
"discover": {
"title": "探索",
"topics": {
"tech": "科技與科學",
"finance": "財經",
"art": "藝術與文化",
"sports": "體育",
"entertainment": "娛樂"
},
"errorFetchingData": "取得資料時發生錯誤"
},
"library": {
"title": "資料庫",
"empty": "未找到任何聊天紀錄。",
"ago": "{time} 前"
},
"settings": {
"title": "設定",
"sections": {
"preferences": "偏好設定",
"automaticSearch": "自動搜尋",
"systemInstructions": "系統指示",
"modelSettings": "模型設定",
"apiKeys": "API 金鑰"
},
"preferences": {
"theme": "主題",
"measurementUnits": "度量單位",
"language": "語言",
"metric": "公制",
"imperial": "英制"
},
"automaticSearch": {
"image": {
"title": "自動圖片搜尋",
"desc": "在聊天回應中自動搜尋相關圖片"
},
"video": {
"title": "自動影片搜尋",
"desc": "在聊天回應中自動搜尋相關影片"
}
},
"model": {
"chatProvider": "聊天模型供應商",
"chat": "聊天模型",
"noModels": "沒有可用的模型",
"invalidProvider": "供應商無效,請檢查後端日誌",
"custom": {
"modelName": "模型名稱",
"apiKey": "自訂 OpenAI API 金鑰",
"baseUrl": "自訂 OpenAI Base URL"
}
},
"embedding": {
"provider": "向量嵌入供應商",
"model": "向量嵌入模型"
},
"api": {
"openaiApiKey": "OpenAI API 金鑰",
"ollamaApiUrl": "Ollama API 位址",
"ollamaApiKey": "Ollama API 金鑰(可留空)",
"groqApiKey": "GROQ API 金鑰",
"anthropicApiKey": "Anthropic API 金鑰",
"geminiApiKey": "Gemini API 金鑰",
"deepseekApiKey": "Deepseek API 金鑰",
"aimlApiKey": "AI/ML 金鑰",
"lmStudioApiUrl": "LM Studio API 位址"
},
"systemInstructions": {
"placeholder": "任何要給 LLM 的特別指示"
}
}
},
"components": {
"common": {
"viewMore": "查看另外 {count} 項"
},
"messageInput": {
"placeholder": "提出追問"
},
"messageBox": {
"sources": "參考來源",
"answer": "回答",
"related": "相關內容"
},
"copilot": {
"label": "Copilot"
},
"attach": {
"attachedFiles": "已附加的檔案",
"add": "新增",
"clear": "清除",
"attach": "附加",
"uploading": "上載中...",
"files": "{count} 個檔案"
},
"focus": {
"button": "焦點",
"modes": {
"webSearch": {
"title": "全部",
"description": "在整個網絡上搜尋"
},
"academicSearch": {
"title": "學術",
"description": "搜尋已發表的學術論文"
},
"writingAssistant": {
"title": "寫作",
"description": "不搜尋網絡,直接聊天"
},
"wolframAlphaSearch": {
"title": "Wolfram Alpha",
"description": "計算型知識引擎"
},
"youtubeSearch": {
"title": "YouTube",
"description": "搜尋及觀看影片"
},
"redditSearch": {
"title": "Reddit",
"description": "搜尋討論與觀點"
}
}
},
"optimization": {
"modes": {
"speed": {
"title": "速度",
"description": "優先速度,以最快的方式得到答案。"
},
"balanced": {
"title": "平衡",
"description": "在速度與準確度之間取得平衡"
},
"quality": {
"title": "品質(即將推出)",
"description": "取得最完整與最精確的回答"
}
}
},
"messageActions": {
"rewrite": "重寫"
},
"searchImages": {
"searchButton": "搜尋圖片"
},
"searchVideos": {
"searchButton": "搜尋影片",
"badge": "影片"
},
"weather": {
"humidity": "濕度",
"now": "現在"
},
"newsArticleWidget": {
"error": "無法載入新聞。"
},
"emptyChat": {
"title": "研究由此開始。"
},
"emptyChatMessageInput": {
"placeholder": "問我任何事..."
},
"deleteChat": {
"title": "刪除確認",
"description": "確定要刪除此聊天嗎?",
"cancel": "取消",
"delete": "刪除"
},
"themeSwitcher": {
"options": {
"light": "淺色",
"dark": "深色"
}
}
}
}

252
src/i18n/zh-TW.json Normal file
View file

@ -0,0 +1,252 @@
{
"metadata": {
"title": "Perplexica - 與網路對話",
"description": "Perplexica 是一個能連上網路的 AI 聊天機器人。"
},
"manifest": {
"name": "Perplexica - 與網路對話",
"shortName": "Perplexica",
"description": "Perplexica 是一個能連上網路的 AI 聊天機器人。"
},
"navigation": {
"home": "首頁",
"discover": "探索",
"library": "資料庫",
"settings": "設定"
},
"common": {
"appName": "Perplexica",
"exportedOn": "匯出日期:",
"citations": "參考來源:",
"user": "使用者",
"assistant": "助理",
"errors": {
"noChatModelsAvailable": "沒有可用的聊天模型",
"chatProviderNotConfigured": "看起來你還沒有設定任何聊天模型供應商,請至設定頁或設定檔進行設定。",
"noEmbeddingModelsAvailable": "沒有可用的向量嵌入模型",
"cannotSendBeforeConfigReady": "在設定就緒前無法送出訊息",
"failedToDeleteChat": "刪除聊天失敗"
}
},
"navbar": {
"exportAsMarkdown": "匯出為 Markdown",
"exportAsPDF": "匯出為 PDF"
},
"export": {
"chatExportTitle": "聊天匯出:{title}"
},
"weather": {
"conditions": {
"clear": "晴朗",
"mainlyClear": "大致晴朗",
"partlyCloudy": "局部多雲",
"cloudy": "多雲",
"fog": "霧",
"lightDrizzle": "小毛毛雨",
"moderateDrizzle": "中等毛毛雨",
"denseDrizzle": "大毛毛雨",
"lightFreezingDrizzle": "輕微冰霧雨",
"denseFreezingDrizzle": "強冰霧雨",
"slightRain": "小雨",
"moderateRain": "中雨",
"heavyRain": "大雨",
"lightFreezingRain": "輕微凍雨",
"heavyFreezingRain": "強凍雨",
"slightSnowFall": "小雪",
"moderateSnowFall": "中雪",
"heavySnowFall": "大雪",
"snow": "降雪",
"slightRainShowers": "短暫小雨",
"moderateRainShowers": "短暫中雨",
"heavyRainShowers": "短暫大雨",
"slightSnowShowers": "短暫小雪",
"moderateSnowShowers": "短暫中雪",
"heavySnowShowers": "短暫大雪",
"thunderstorm": "雷雨",
"thunderstormSlightHail": "雷雨伴隨小冰雹",
"thunderstormHeavyHail": "雷雨伴隨大冰雹"
}
},
"pages": {
"home": {
"title": "聊天 - Perplexica",
"description": "連上網路聊聊天,和 Perplexica 對話。"
},
"discover": {
"title": "探索",
"topics": {
"tech": "科技與科學",
"finance": "財經",
"art": "藝術與文化",
"sports": "運動",
"entertainment": "娛樂"
},
"errorFetchingData": "取得資料時發生錯誤"
},
"library": {
"title": "資料庫",
"empty": "沒有找到任何聊天紀錄。",
"ago": "{time} 前"
},
"settings": {
"title": "設定",
"sections": {
"preferences": "偏好設定",
"automaticSearch": "自動搜尋",
"systemInstructions": "系統指示",
"modelSettings": "模型設定",
"apiKeys": "API 金鑰"
},
"preferences": {
"theme": "主題",
"measurementUnits": "度量單位",
"language": "語言",
"metric": "公制",
"imperial": "英制"
},
"automaticSearch": {
"image": {
"title": "自動圖片搜尋",
"desc": "在聊天回應中自動搜尋相關圖片"
},
"video": {
"title": "自動影片搜尋",
"desc": "在聊天回應中自動搜尋相關影片"
}
},
"model": {
"chatProvider": "聊天模型供應商",
"chat": "聊天模型",
"noModels": "沒有可用的模型",
"invalidProvider": "供應商無效,請檢查後端日誌",
"custom": {
"modelName": "模型名稱",
"apiKey": "自訂 OpenAI API 金鑰",
"baseUrl": "自訂 OpenAI Base URL"
}
},
"embedding": {
"provider": "向量嵌入供應商",
"model": "向量嵌入模型"
},
"api": {
"openaiApiKey": "OpenAI API 金鑰",
"ollamaApiUrl": "Ollama API 位址",
"ollamaApiKey": "Ollama API 金鑰(可留空)",
"groqApiKey": "GROQ API 金鑰",
"anthropicApiKey": "Anthropic API 金鑰",
"geminiApiKey": "Gemini API 金鑰",
"deepseekApiKey": "Deepseek API 金鑰",
"aimlApiKey": "AI/ML API 金鑰",
"lmStudioApiUrl": "LM Studio API 位址"
},
"systemInstructions": {
"placeholder": "任何要給 LLM 的特別指示"
}
}
},
"components": {
"common": {
"viewMore": "查看另外 {count} 項"
},
"messageInput": {
"placeholder": "提出追問"
},
"messageBox": {
"sources": "參考來源",
"answer": "回答",
"related": "相關內容"
},
"copilot": {
"label": "Copilot"
},
"attach": {
"attachedFiles": "已附加的檔案",
"add": "新增",
"clear": "清除",
"attach": "附加",
"uploading": "上傳中...",
"files": "{count} 個檔案"
},
"focus": {
"button": "焦點",
"modes": {
"webSearch": {
"title": "全部",
"description": "在整個網路上搜尋"
},
"academicSearch": {
"title": "學術",
"description": "搜尋已發表的學術論文"
},
"writingAssistant": {
"title": "寫作",
"description": "不搜尋網路,直接聊天"
},
"wolframAlphaSearch": {
"title": "Wolfram Alpha",
"description": "計算型知識引擎"
},
"youtubeSearch": {
"title": "YouTube",
"description": "搜尋與觀看影片"
},
"redditSearch": {
"title": "Reddit",
"description": "搜尋討論與觀點"
}
}
},
"optimization": {
"modes": {
"speed": {
"title": "速度",
"description": "優先速度,以最快的方式得到答案。"
},
"balanced": {
"title": "平衡",
"description": "在速度與準確度之間取得平衡"
},
"quality": {
"title": "品質(即將推出)",
"description": "取得最完整與最精確的回答"
}
}
},
"messageActions": {
"rewrite": "重寫"
},
"searchImages": {
"searchButton": "搜尋圖片"
},
"searchVideos": {
"searchButton": "搜尋影片",
"badge": "影片"
},
"weather": {
"humidity": "濕度",
"now": "現在"
},
"newsArticleWidget": {
"error": "無法載入新聞。"
},
"emptyChat": {
"title": "研究從這裡開始。"
},
"emptyChatMessageInput": {
"placeholder": "問我任何事..."
},
"deleteChat": {
"title": "刪除確認",
"description": "確定要刪除此聊天嗎?",
"cancel": "取消",
"delete": "刪除"
},
"themeSwitcher": {
"options": {
"light": "淺色",
"dark": "深色"
}
}
}
}

View file

@ -1,6 +1,9 @@
import { Message } from '@/components/ChatWindow'; import { Message } from '@/components/ChatWindow';
export const getSuggestions = async (chatHisory: Message[]) => { export const getSuggestions = async (
chatHistory: Message[],
locale?: string,
) => {
const chatModel = localStorage.getItem('chatModel'); const chatModel = localStorage.getItem('chatModel');
const chatModelProvider = localStorage.getItem('chatModelProvider'); const chatModelProvider = localStorage.getItem('chatModelProvider');
@ -13,7 +16,7 @@ export const getSuggestions = async (chatHisory: Message[]) => {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
chatHistory: chatHisory, chatHistory: chatHistory,
chatModel: { chatModel: {
provider: chatModelProvider, provider: chatModelProvider,
model: chatModel, model: chatModel,
@ -22,6 +25,7 @@ export const getSuggestions = async (chatHisory: Message[]) => {
customOpenAIBaseURL, customOpenAIBaseURL,
}), }),
}, },
locale,
}), }),
}); });

View file

@ -5,14 +5,31 @@ import formatChatHistoryAsString from '../utils/formatHistory';
import { BaseMessage } from '@langchain/core/messages'; import { BaseMessage } from '@langchain/core/messages';
import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { ChatOpenAI } from '@langchain/openai'; import { ChatOpenAI } from '@langchain/openai';
import { getPromptLanguageName } from '@/i18n/locales';
const suggestionGeneratorPrompt = ` const suggestionGeneratorPrompt = `
You are an AI suggestion generator for an AI powered search engine. You will be given a conversation below. You need to generate 4-5 suggestions based on the conversation. The suggestion should be relevant to the conversation that can be used by the user to ask the chat model for more information. You are an AI suggestion generator for an AI powered search engine.
You need to make sure the suggestions are relevant to the conversation and are helpful to the user. Keep a note that the user might use these suggestions to ask a chat model for more information.
Make sure the suggestions are medium in length and are informative and relevant to the conversation.
Provide these suggestions separated by newlines between the XML tags <suggestions> and </suggestions>. For example: Your need to meet these requirements:
- You will be given a conversation below. You need to generate 4-5 suggestions based on the conversation.
- The suggestion should be relevant to the conversation that can be used by the user to ask the chat model for more information.
- You need to make sure the suggestions are relevant to the conversation and are helpful to the user. Keep a note that the user might use these suggestions to ask a chat model for more information.
### Language Instructions
- **Language Definition**: Interpret "{language}" as a combination of language and optional region.
- Format: "language (region)" or "languageregion" (e.g., "English (US)", "繁體中文(台灣)").
- The main language indicates the linguistic system (e.g., English, , ).
- The region in parentheses indicates the regional variant or locale style (e.g., US, UK, , , France).
- **Primary Language**: Use "{language}" for all non-code content, including explanations, descriptions, and examples.
- **Regional Variants**: Adjust word choice, spelling, and style according to the region specified in "{language}" (e.g., 使, 使; English (US) uses "color", English (UK) uses "colour").
- **Code and Comments**: All code blocks and code comments must be entirely in "English (US)".
- **Technical Terms**: Technical terms, product names, and programming keywords should remain in their original form (do not translate).
- **Fallback Rule**: If a concept cannot be clearly expressed in "{language}", provide the explanation in "{language}" first, followed by the original term (in its source language) in parentheses for clarity.
- **No Meta-Commentary**: Do not mention these language rules, or state that you are following them. Simply apply them in your response without explanation.
### Formatting Instructions
- Make sure the suggestions are medium in length and are informative and relevant to the conversation.
- Provide these suggestions separated by newlines between the XML tags <suggestions> and </suggestions>. For example:
<suggestions> <suggestions>
Tell me more about SpaceX and their recent projects Tell me more about SpaceX and their recent projects
What is the latest news on SpaceX? What is the latest news on SpaceX?
@ -25,6 +42,7 @@ Conversation:
type SuggestionGeneratorInput = { type SuggestionGeneratorInput = {
chat_history: BaseMessage[]; chat_history: BaseMessage[];
locale: string;
}; };
const outputParser = new ListLineOutputParser({ const outputParser = new ListLineOutputParser({
@ -36,6 +54,8 @@ const createSuggestionGeneratorChain = (llm: BaseChatModel) => {
RunnableMap.from({ RunnableMap.from({
chat_history: (input: SuggestionGeneratorInput) => chat_history: (input: SuggestionGeneratorInput) =>
formatChatHistoryAsString(input.chat_history), formatChatHistoryAsString(input.chat_history),
language: (input: SuggestionGeneratorInput) =>
getPromptLanguageName(input.locale),
}), }),
PromptTemplate.fromTemplate(suggestionGeneratorPrompt), PromptTemplate.fromTemplate(suggestionGeneratorPrompt),
llm, llm,

View file

@ -6,6 +6,7 @@ import crypto from 'crypto';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Document } from '@langchain/core/documents'; import { Document } from '@langchain/core/documents';
import { useLocale, useTranslations } from 'next-intl';
import { getSuggestions } from '../actions'; import { getSuggestions } from '../actions';
type ChatContext = { type ChatContext = {
@ -55,6 +56,7 @@ const checkConfig = async (
setEmbeddingModelProvider: (provider: EmbeddingModelProvider) => void, setEmbeddingModelProvider: (provider: EmbeddingModelProvider) => void,
setIsConfigReady: (ready: boolean) => void, setIsConfigReady: (ready: boolean) => void,
setHasError: (hasError: boolean) => void, setHasError: (hasError: boolean) => void,
t: (key: string) => string,
) => { ) => {
try { try {
let chatModel = localStorage.getItem('chatModel'); let chatModel = localStorage.getItem('chatModel');
@ -96,7 +98,7 @@ const checkConfig = async (
const chatModelProvidersKeys = Object.keys(chatModelProviders); const chatModelProvidersKeys = Object.keys(chatModelProviders);
if (!chatModelProviders || chatModelProvidersKeys.length === 0) { if (!chatModelProviders || chatModelProvidersKeys.length === 0) {
return toast.error('No chat models available'); return toast.error(t('common.errors.noChatModelsAvailable'));
} else { } else {
chatModelProvider = chatModelProvider =
chatModelProvidersKeys.find( chatModelProvidersKeys.find(
@ -109,9 +111,7 @@ const checkConfig = async (
chatModelProvider === 'custom_openai' && chatModelProvider === 'custom_openai' &&
Object.keys(chatModelProviders[chatModelProvider]).length === 0 Object.keys(chatModelProviders[chatModelProvider]).length === 0
) { ) {
toast.error( toast.error(t('common.errors.chatProviderNotConfigured'));
"Looks like you haven't configured any chat model providers. Please configure them from the settings page or the config file.",
);
return setHasError(true); return setHasError(true);
} }
@ -125,7 +125,7 @@ const checkConfig = async (
!embeddingModelProviders || !embeddingModelProviders ||
Object.keys(embeddingModelProviders).length === 0 Object.keys(embeddingModelProviders).length === 0
) )
return toast.error('No embedding models available'); return toast.error(t('common.errors.noEmbeddingModelsAvailable'));
embeddingModelProvider = Object.keys(embeddingModelProviders)[0]; embeddingModelProvider = Object.keys(embeddingModelProviders)[0];
embeddingModel = Object.keys( embeddingModel = Object.keys(
@ -163,9 +163,7 @@ const checkConfig = async (
chatModelProvider === 'custom_openai' && chatModelProvider === 'custom_openai' &&
Object.keys(chatModelProviders[chatModelProvider]).length === 0 Object.keys(chatModelProviders[chatModelProvider]).length === 0
) { ) {
toast.error( toast.error(t('common.errors.chatProviderNotConfigured'));
"Looks like you haven't configured any chat model providers. Please configure them from the settings page or the config file.",
);
return setHasError(true); return setHasError(true);
} }
@ -345,12 +343,16 @@ export const ChatProvider = ({
const messagesRef = useRef<Message[]>([]); const messagesRef = useRef<Message[]>([]);
const t = useTranslations();
const locale = useLocale();
useEffect(() => { useEffect(() => {
checkConfig( checkConfig(
setChatModelProvider, setChatModelProvider,
setEmbeddingModelProvider, setEmbeddingModelProvider,
setIsConfigReady, setIsConfigReady,
setHasError, setHasError,
t,
); );
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
@ -413,7 +415,7 @@ export const ChatProvider = ({
useEffect(() => { useEffect(() => {
if (isReady && initialMessage && isConfigReady) { if (isReady && initialMessage && isConfigReady) {
if (!isConfigReady) { if (!isConfigReady) {
toast.error('Cannot send message before the configuration is ready'); toast.error(t('common.errors.cannotSendBeforeConfigReady'));
return; return;
} }
sendMessage(initialMessage); sendMessage(initialMessage);
@ -535,7 +537,7 @@ export const ChatProvider = ({
lastMsg.sources.length > 0 && lastMsg.sources.length > 0 &&
!lastMsg.suggestions !lastMsg.suggestions
) { ) {
const suggestions = await getSuggestions(messagesRef.current); const suggestions = await getSuggestions(messagesRef.current, locale);
setMessages((prev) => setMessages((prev) =>
prev.map((msg) => { prev.map((msg) => {
if (msg.messageId === lastMsg.messageId) { if (msg.messageId === lastMsg.messageId) {
@ -578,6 +580,7 @@ export const ChatProvider = ({
provider: embeddingModelProvider.provider, provider: embeddingModelProvider.provider,
}, },
systemInstructions: localStorage.getItem('systemInstructions'), systemInstructions: localStorage.getItem('systemInstructions'),
locale,
}), }),
}); });

45
src/lib/pdfFont.ts Normal file
View file

@ -0,0 +1,45 @@
// Runtime loader for a Unicode-capable font (Noto Sans TC) for jsPDF, to fix CJK garbled text
// Note: Load fonts from local /public/fonts to avoid any external CDN dependency.
// License: Noto fonts are under SIL Open Font License 1.1
import jsPDF from 'jspdf';
let fontsLoaded = false;
// Files should be placed at public/fonts (see public/fonts/README.md)
const NOTO_TC_REGULAR = '/fonts/NotoSansTC-Regular.ttf';
const NOTO_TC_BOLD = '/fonts/NotoSansTC-Bold.ttf';
function arrayBufferToBinaryString(buffer: ArrayBuffer): string {
let binary = '';
const bytes = new Uint8Array(buffer);
const chunkSize = 0x8000; // avoid call stack overflow
for (let i = 0; i < bytes.length; i += chunkSize) {
const chunk = bytes.subarray(i, i + chunkSize);
binary += String.fromCharCode.apply(
null,
Array.from(chunk) as unknown as number[],
);
}
return binary;
}
export async function ensureNotoSansTC(doc: jsPDF): Promise<void> {
if (fontsLoaded) return;
const [regBuf, boldBuf] = await Promise.all([
fetch(NOTO_TC_REGULAR).then((r) => r.arrayBuffer()),
fetch(NOTO_TC_BOLD).then((r) => r.arrayBuffer()),
]);
const regBin = arrayBufferToBinaryString(regBuf);
const boldBin = arrayBufferToBinaryString(boldBuf);
// Register into VFS and add fonts
doc.addFileToVFS('NotoSansTC-Regular.ttf', regBin);
doc.addFont('NotoSansTC-Regular.ttf', 'NotoSansTC', 'normal');
doc.addFileToVFS('NotoSansTC-Bold.ttf', boldBin);
doc.addFont('NotoSansTC-Bold.ttf', 'NotoSansTC', 'bold');
fontsLoaded = true;
}

View file

@ -20,50 +20,62 @@ Rephrased question:
`; `;
export const academicSearchResponsePrompt = ` export const academicSearchResponsePrompt = `
You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses. You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses.
Your task is to provide answers that are: Your task is to provide answers that are:
- **Informative and relevant**: Thoroughly address the user's query using the given context. - **Informative and relevant**: Thoroughly address the user's query using the given context.
- **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically. - **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically.
- **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights. - **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights.
- **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included. - **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included.
- **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable. - **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable.
### Formatting Instructions ### Formatting Instructions
- **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate. - **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate.
- **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience. - **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience.
- **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability. - **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability.
- **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition. Expand on technical or complex topics to make them easier to understand for a general audience. - **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition. Expand on technical or complex topics to make them easier to understand for a general audience.
- **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title. - **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title.
- **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate. - **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate.
### Citation Requirements ### Citation Requirements
- Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`. - Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`.
- Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]." - Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]."
- Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context. - Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context.
- Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]." - Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]."
- Always prioritize credibility and accuracy by linking all statements back to their respective context sources. - Always prioritize credibility and accuracy by linking all statements back to their respective context sources.
- Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation. - Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation.
### Special Instructions ### Special Instructions
- If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity. - If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity.
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search. - If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
- If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query. - If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query.
- You are set on focus mode 'Academic', this means you will be searching for academic papers and articles on the web. - You are set on focus mode 'Academic', this means you will be searching for academic papers and articles on the web.
### User instructions ### User instructions
These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines. These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines.
{systemInstructions} {systemInstructions}
### Example Output ### Language Instructions
- Begin with a brief introduction summarizing the event or query topic. - **Language Definition**: Interpret "{language}" as a combination of language and optional region.
- Follow with detailed sections under clear headings, covering all aspects of the query if possible. - Format: "language (region)" or "languageregion" (e.g., "English (US)", "繁體中文(台灣)").
- Provide explanations or historical context as needed to enhance understanding. - The main language indicates the linguistic system (e.g., English, , ).
- End with a conclusion or overall perspective if relevant. - The region in parentheses indicates the regional variant or locale style (e.g., US, UK, , , France).
- **Primary Language**: Use "{language}" for all non-code content, including explanations, descriptions, and examples.
- **Regional Variants**: Adjust word choice, spelling, and style according to the region specified in "{language}" (e.g., 使, 使; English (US) uses "color", English (UK) uses "colour").
- **Code and Comments**: All code blocks and code comments must be entirely in "English (US)".
- **Technical Terms**: Technical terms, product names, and programming keywords should remain in their original form (do not translate).
- **Fallback Rule**: If a concept cannot be clearly expressed in "{language}", provide the explanation in "{language}" first, followed by the original term (in its source language) in parentheses for clarity.
- **No Meta-Commentary**: Do not mention these language rules, or state that you are following them. Simply apply them in your response without explanation.
<context> ### Example Output
{context} - Begin with a brief introduction summarizing the event or query topic.
</context> - Follow with detailed sections under clear headings, covering all aspects of the query if possible.
- Provide explanations or historical context as needed to enhance understanding.
- End with a conclusion or overall perspective if relevant.
Current date & time in ISO format (UTC timezone) is: {date}. <context>
{context}
</context>
Current date & time in ISO format (UTC timezone) is: {date}.
`; `;

View file

@ -17,7 +17,7 @@ import {
youtubeSearchRetrieverPrompt, youtubeSearchRetrieverPrompt,
} from './youtubeSearch'; } from './youtubeSearch';
export default { const prompts = {
webSearchResponsePrompt, webSearchResponsePrompt,
webSearchRetrieverPrompt, webSearchRetrieverPrompt,
academicSearchResponsePrompt, academicSearchResponsePrompt,
@ -30,3 +30,5 @@ export default {
youtubeSearchResponsePrompt, youtubeSearchResponsePrompt,
youtubeSearchRetrieverPrompt, youtubeSearchRetrieverPrompt,
}; };
export default prompts;

View file

@ -20,50 +20,62 @@ Rephrased question:
`; `;
export const redditSearchResponsePrompt = ` export const redditSearchResponsePrompt = `
You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses. You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses.
Your task is to provide answers that are: Your task is to provide answers that are:
- **Informative and relevant**: Thoroughly address the user's query using the given context. - **Informative and relevant**: Thoroughly address the user's query using the given context.
- **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically. - **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically.
- **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights. - **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights.
- **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included. - **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included.
- **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable. - **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable.
### Formatting Instructions ### Formatting Instructions
- **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate. - **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate.
- **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience. - **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience.
- **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability. - **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability.
- **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition. Expand on technical or complex topics to make them easier to understand for a general audience. - **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition. Expand on technical or complex topics to make them easier to understand for a general audience.
- **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title. - **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title.
- **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate. - **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate.
### Citation Requirements ### Citation Requirements
- Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`. - Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`.
- Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]." - Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]."
- Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context. - Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context.
- Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]." - Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]."
- Always prioritize credibility and accuracy by linking all statements back to their respective context sources. - Always prioritize credibility and accuracy by linking all statements back to their respective context sources.
- Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation. - Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation.
### Special Instructions ### Special Instructions
- If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity. - If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity.
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search. - If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
- If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query. - If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query.
- You are set on focus mode 'Reddit', this means you will be searching for information, opinions and discussions on the web using Reddit. - You are set on focus mode 'Reddit', this means you will be searching for information, opinions and discussions on the web using Reddit.
### User instructions ### User instructions
These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines. These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines.
{systemInstructions} {systemInstructions}
### Example Output ### Language Instructions
- Begin with a brief introduction summarizing the event or query topic. - **Language Definition**: Interpret "{language}" as a combination of language and optional region.
- Follow with detailed sections under clear headings, covering all aspects of the query if possible. - Format: "language (region)" or "languageregion" (e.g., "English (US)", "繁體中文(台灣)").
- Provide explanations or historical context as needed to enhance understanding. - The main language indicates the linguistic system (e.g., English, , ).
- End with a conclusion or overall perspective if relevant. - The region in parentheses indicates the regional variant or locale style (e.g., US, UK, , , France).
- **Primary Language**: Use "{language}" for all non-code content, including explanations, descriptions, and examples.
- **Regional Variants**: Adjust word choice, spelling, and style according to the region specified in "{language}" (e.g., 使, 使; English (US) uses "color", English (UK) uses "colour").
- **Code and Comments**: All code blocks and code comments must be entirely in "English (US)".
- **Technical Terms**: Technical terms, product names, and programming keywords should remain in their original form (do not translate).
- **Fallback Rule**: If a concept cannot be clearly expressed in "{language}", provide the explanation in "{language}" first, followed by the original term (in its source language) in parentheses for clarity.
- **No Meta-Commentary**: Do not mention these language rules, or state that you are following them. Simply apply them in your response without explanation.
<context> ### Example Output
{context} - Begin with a brief introduction summarizing the event or query topic.
</context> - Follow with detailed sections under clear headings, covering all aspects of the query if possible.
- Provide explanations or historical context as needed to enhance understanding.
- End with a conclusion or overall perspective if relevant.
Current date & time in ISO format (UTC timezone) is: {date}. <context>
{context}
</context>
Current date & time in ISO format (UTC timezone) is: {date}.
`; `;

View file

@ -62,49 +62,61 @@ Rephrased question:
`; `;
export const webSearchResponsePrompt = ` export const webSearchResponsePrompt = `
You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses. You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses.
Your task is to provide answers that are: Your task is to provide answers that are:
- **Informative and relevant**: Thoroughly address the user's query using the given context. - **Informative and relevant**: Thoroughly address the user's query using the given context.
- **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically. - **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically.
- **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights. - **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights.
- **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included. - **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included.
- **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable. - **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable.
### Formatting Instructions ### Formatting Instructions
- **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate. - **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate.
- **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience. - **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience.
- **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability. - **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability.
- **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition. Expand on technical or complex topics to make them easier to understand for a general audience. - **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition. Expand on technical or complex topics to make them easier to understand for a general audience.
- **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title. - **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title.
- **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate. - **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate.
### Citation Requirements ### Citation Requirements
- Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`. - Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`.
- Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]." - Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]."
- Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context. - Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context.
- Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]." - Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]."
- Always prioritize credibility and accuracy by linking all statements back to their respective context sources. - Always prioritize credibility and accuracy by linking all statements back to their respective context sources.
- Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation. - Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation.
### Special Instructions ### Special Instructions
- If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity. - If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity.
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search. - If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
- If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query. - If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query.
### User instructions ### User instructions
These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines. These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines.
{systemInstructions} {systemInstructions}
### Example Output ### Language Instructions
- Begin with a brief introduction summarizing the event or query topic. - **Language Definition**: Interpret "{language}" as a combination of language and optional region.
- Follow with detailed sections under clear headings, covering all aspects of the query if possible. - Format: "language (region)" or "languageregion" (e.g., "English (US)", "繁體中文(台灣)").
- Provide explanations or historical context as needed to enhance understanding. - The main language indicates the linguistic system (e.g., English, , ).
- End with a conclusion or overall perspective if relevant. - The region in parentheses indicates the regional variant or locale style (e.g., US, UK, , , France).
- **Primary Language**: Use "{language}" for all non-code content, including explanations, descriptions, and examples.
- **Regional Variants**: Adjust word choice, spelling, and style according to the region specified in "{language}" (e.g., 使, 使; English (US) uses "color", English (UK) uses "colour").
- **Code and Comments**: All code blocks and code comments must be entirely in "English (US)".
- **Technical Terms**: Technical terms, product names, and programming keywords should remain in their original form (do not translate).
- **Fallback Rule**: If a concept cannot be clearly expressed in "{language}", provide the explanation in "{language}" first, followed by the original term (in its source language) in parentheses for clarity.
- **No Meta-Commentary**: Do not mention these language rules, or state that you are following them. Simply apply them in your response without explanation.
<context> ### Example Output
{context} - Begin with a brief introduction summarizing the event or query topic.
</context> - Follow with detailed sections under clear headings, covering all aspects of the query if possible.
- Provide explanations or historical context as needed to enhance understanding.
- End with a conclusion or overall perspective if relevant.
Current date & time in ISO format (UTC timezone) is: {date}. <context>
{context}
</context>
Current date & time in ISO format (UTC timezone) is: {date}.
`; `;

View file

@ -20,50 +20,62 @@ Rephrased question:
`; `;
export const wolframAlphaSearchResponsePrompt = ` export const wolframAlphaSearchResponsePrompt = `
You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses. You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses.
Your task is to provide answers that are: Your task is to provide answers that are:
- **Informative and relevant**: Thoroughly address the user's query using the given context. - **Informative and relevant**: Thoroughly address the user's query using the given context.
- **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically. - **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically.
- **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights. - **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights.
- **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included. - **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included.
- **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable. - **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable.
### Formatting Instructions ### Formatting Instructions
- **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate. - **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate.
- **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience. - **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience.
- **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability. - **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability.
- **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition. Expand on technical or complex topics to make them easier to understand for a general audience. - **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition. Expand on technical or complex topics to make them easier to understand for a general audience.
- **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title. - **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title.
- **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate. - **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate.
### Citation Requirements ### Citation Requirements
- Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`. - Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`.
- Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]." - Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]."
- Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context. - Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context.
- Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]." - Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]."
- Always prioritize credibility and accuracy by linking all statements back to their respective context sources. - Always prioritize credibility and accuracy by linking all statements back to their respective context sources.
- Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation. - Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation.
### Special Instructions ### Special Instructions
- If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity. - If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity.
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search. - If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
- If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query. - If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query.
- You are set on focus mode 'Wolfram Alpha', this means you will be searching for information on the web using Wolfram Alpha. It is a computational knowledge engine that can answer factual queries and perform computations. - You are set on focus mode 'Wolfram Alpha', this means you will be searching for information on the web using Wolfram Alpha. It is a computational knowledge engine that can answer factual queries and perform computations.
### User instructions ### User instructions
These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines. These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines.
{systemInstructions} {systemInstructions}
### Example Output ### Language Instructions
- Begin with a brief introduction summarizing the event or query topic. - **Language Definition**: Interpret "{language}" as a combination of language and optional region.
- Follow with detailed sections under clear headings, covering all aspects of the query if possible. - Format: "language (region)" or "languageregion" (e.g., "English (US)", "繁體中文(台灣)").
- Provide explanations or historical context as needed to enhance understanding. - The main language indicates the linguistic system (e.g., English, , ).
- End with a conclusion or overall perspective if relevant. - The region in parentheses indicates the regional variant or locale style (e.g., US, UK, , , France).
- **Primary Language**: Use "{language}" for all non-code content, including explanations, descriptions, and examples.
- **Regional Variants**: Adjust word choice, spelling, and style according to the region specified in "{language}" (e.g., 使, 使; English (US) uses "color", English (UK) uses "colour").
- **Code and Comments**: All code blocks and code comments must be entirely in "English (US)".
- **Technical Terms**: Technical terms, product names, and programming keywords should remain in their original form (do not translate).
- **Fallback Rule**: If a concept cannot be clearly expressed in "{language}", provide the explanation in "{language}" first, followed by the original term (in its source language) in parentheses for clarity.
- **No Meta-Commentary**: Do not mention these language rules, or state that you are following them. Simply apply them in your response without explanation.
<context> ### Example Output
{context} - Begin with a brief introduction summarizing the event or query topic.
</context> - Follow with detailed sections under clear headings, covering all aspects of the query if possible.
- Provide explanations or historical context as needed to enhance understanding.
- End with a conclusion or overall perspective if relevant.
Current date & time in ISO format (UTC timezone) is: {date}. <context>
{context}
</context>
Current date & time in ISO format (UTC timezone) is: {date}.
`; `;

View file

@ -11,6 +11,18 @@ However you do not need to cite it using the same number. You can use different
These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines. These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines.
{systemInstructions} {systemInstructions}
### Language Instructions
- **Language Definition**: Interpret "{language}" as a combination of language and optional region.
- Format: "language (region)" or "languageregion" (e.g., "English (US)", "繁體中文(台灣)").
- The main language indicates the linguistic system (e.g., English, , ).
- The region in parentheses indicates the regional variant or locale style (e.g., US, UK, , , France).
- **Primary Language**: Use "{language}" for all non-code content, including explanations, descriptions, and examples.
- **Regional Variants**: Adjust word choice, spelling, and style according to the region specified in "{language}" (e.g., 使, 使; English (US) uses "color", English (UK) uses "colour").
- **Code and Comments**: All code blocks and code comments must be entirely in "English (US)".
- **Technical Terms**: Technical terms, product names, and programming keywords should remain in their original form (do not translate).
- **Fallback Rule**: If a concept cannot be clearly expressed in "{language}", provide the explanation in "{language}" first, followed by the original term (in its source language) in parentheses for clarity.
- **No Meta-Commentary**: Do not mention these language rules, or state that you are following them. Simply apply them in your response without explanation.
<context> <context>
{context} {context}
</context> </context>

View file

@ -20,50 +20,62 @@ Rephrased question:
`; `;
export const youtubeSearchResponsePrompt = ` export const youtubeSearchResponsePrompt = `
You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses. You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses.
Your task is to provide answers that are: Your task is to provide answers that are:
- **Informative and relevant**: Thoroughly address the user's query using the given context. - **Informative and relevant**: Thoroughly address the user's query using the given context.
- **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically. - **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically.
- **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights. - **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights.
- **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included. - **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included.
- **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable. - **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable.
### Formatting Instructions ### Formatting Instructions
- **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate. - **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate.
- **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience. - **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience.
- **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability. - **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability.
- **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition. Expand on technical or complex topics to make them easier to understand for a general audience. - **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition. Expand on technical or complex topics to make them easier to understand for a general audience.
- **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title. - **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title.
- **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate. - **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate.
### Citation Requirements ### Citation Requirements
- Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`. - Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`.
- Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]." - Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]."
- Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context. - Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context.
- Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]." - Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]."
- Always prioritize credibility and accuracy by linking all statements back to their respective context sources. - Always prioritize credibility and accuracy by linking all statements back to their respective context sources.
- Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation. - Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation.
### Special Instructions ### Special Instructions
- If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity. - If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity.
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search. - If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
- If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query. - If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query.
- You are set on focus mode 'Youtube', this means you will be searching for videos on the web using Youtube and providing information based on the video's transcrip - You are set on focus mode 'Youtube', this means you will be searching for videos on the web using Youtube and providing information based on the video's transcrip
### User instructions ### User Instructions
These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines. These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines.
{systemInstructions} {systemInstructions}
### Example Output ### Language Instructions
- Begin with a brief introduction summarizing the event or query topic. - **Language Definition**: Interpret "{language}" as a combination of language and optional region.
- Follow with detailed sections under clear headings, covering all aspects of the query if possible. - Format: "language (region)" or "languageregion" (e.g., "English (US)", "繁體中文(台灣)").
- Provide explanations or historical context as needed to enhance understanding. - The main language indicates the linguistic system (e.g., English, , ).
- End with a conclusion or overall perspective if relevant. - The region in parentheses indicates the regional variant or locale style (e.g., US, UK, , , France).
- **Primary Language**: Use "{language}" for all non-code content, including explanations, descriptions, and examples.
- **Regional Variants**: Adjust word choice, spelling, and style according to the region specified in "{language}" (e.g., 使, 使; English (US) uses "color", English (UK) uses "colour").
- **Code and Comments**: All code blocks and code comments must be entirely in "English (US)".
- **Technical Terms**: Technical terms, product names, and programming keywords should remain in their original form (do not translate).
- **Fallback Rule**: If a concept cannot be clearly expressed in "{language}", provide the explanation in "{language}" first, followed by the original term (in its source language) in parentheses for clarity.
- **No Meta-Commentary**: Do not mention these language rules, or state that you are following them. Simply apply them in your response without explanation.
<context> ### Example Output
{context} - Begin with a brief introduction summarizing the event or query topic.
</context> - Follow with detailed sections under clear headings, covering all aspects of the query if possible.
- Provide explanations or historical context as needed to enhance understanding.
- End with a conclusion or overall perspective if relevant.
Current date & time in ISO format (UTC timezone) is: {date}. <context>
{context}
</context>
Current date & time in ISO format (UTC timezone) is: {date}.
`; `;

View file

@ -18,12 +18,14 @@ import LineOutputParser from '../outputParsers/lineOutputParser';
import { getDocumentsFromLinks } from '../utils/documents'; import { getDocumentsFromLinks } from '../utils/documents';
import { Document } from 'langchain/document'; import { Document } from 'langchain/document';
import { searchSearxng } from '../searxng'; import { searchSearxng } from '../searxng';
import { getLocale } from 'next-intl/server';
import path from 'node:path'; import path from 'node:path';
import fs from 'node:fs'; import fs from 'node:fs';
import computeSimilarity from '../utils/computeSimilarity'; import computeSimilarity from '../utils/computeSimilarity';
import formatChatHistoryAsString from '../utils/formatHistory'; import formatChatHistoryAsString from '../utils/formatHistory';
import eventEmitter from 'events'; import eventEmitter from 'events';
import { StreamEvent } from '@langchain/core/tracers/log_stream'; import { StreamEvent } from '@langchain/core/tracers/log_stream';
import { getPromptLanguageName } from '@/i18n/locales';
export interface MetaSearchAgentType { export interface MetaSearchAgentType {
searchAndAnswer: ( searchAndAnswer: (
@ -34,6 +36,7 @@ export interface MetaSearchAgentType {
optimizationMode: 'speed' | 'balanced' | 'quality', optimizationMode: 'speed' | 'balanced' | 'quality',
fileIds: string[], fileIds: string[],
systemInstructions: string, systemInstructions: string,
locale: string,
) => Promise<eventEmitter>; ) => Promise<eventEmitter>;
} }
@ -205,8 +208,10 @@ class MetaSearchAgent implements MetaSearchAgentType {
} else { } else {
question = question.replace(/<think>.*?<\/think>/g, ''); question = question.replace(/<think>.*?<\/think>/g, '');
const currentLocale = await getLocale();
const baseLang = (currentLocale?.split('-')[0] || 'en') as string;
const res = await searchSearxng(question, { const res = await searchSearxng(question, {
language: 'en', language: baseLang,
engines: this.config.activeEngines, engines: this.config.activeEngines,
}); });
@ -238,10 +243,12 @@ class MetaSearchAgent implements MetaSearchAgentType {
embeddings: Embeddings, embeddings: Embeddings,
optimizationMode: 'speed' | 'balanced' | 'quality', optimizationMode: 'speed' | 'balanced' | 'quality',
systemInstructions: string, systemInstructions: string,
language: string,
) { ) {
return RunnableSequence.from([ return RunnableSequence.from([
RunnableMap.from({ RunnableMap.from({
systemInstructions: () => systemInstructions, systemInstructions: () => systemInstructions,
language: () => language,
query: (input: BasicChainInput) => input.query, query: (input: BasicChainInput) => input.query,
chat_history: (input: BasicChainInput) => input.chat_history, chat_history: (input: BasicChainInput) => input.chat_history,
date: () => new Date().toISOString(), date: () => new Date().toISOString(),
@ -472,6 +479,7 @@ class MetaSearchAgent implements MetaSearchAgentType {
optimizationMode: 'speed' | 'balanced' | 'quality', optimizationMode: 'speed' | 'balanced' | 'quality',
fileIds: string[], fileIds: string[],
systemInstructions: string, systemInstructions: string,
locale: string,
) { ) {
const emitter = new eventEmitter(); const emitter = new eventEmitter();
@ -481,6 +489,7 @@ class MetaSearchAgent implements MetaSearchAgentType {
embeddings, embeddings,
optimizationMode, optimizationMode,
systemInstructions, systemInstructions,
getPromptLanguageName(locale),
); );
const stream = answeringChain.streamEvents( const stream = answeringChain.streamEvents(

View file

@ -3,25 +3,60 @@ import { twMerge } from 'tailwind-merge';
export const cn = (...classes: ClassValue[]) => twMerge(clsx(...classes)); export const cn = (...classes: ClassValue[]) => twMerge(clsx(...classes));
export const formatTimeDifference = ( // Locale-aware absolute date formatting
export const formatDate = (
date: Date | string,
locale: string,
options: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
},
) => {
const d = new Date(date);
return new Intl.DateTimeFormat(locale || undefined, options).format(d);
};
// Locale-aware relative time using Intl.RelativeTimeFormat
export const formatRelativeTime = (
date1: Date | string, date1: Date | string,
date2: Date | string, date2: Date | string,
locale: string,
): string => { ): string => {
date1 = new Date(date1); const d1 = new Date(date1);
date2 = new Date(date2); const d2 = new Date(date2);
const diffSeconds = Math.floor((d2.getTime() - d1.getTime()) / 1000); // positive if d2 > d1
const diffInSeconds = Math.floor( const abs = Math.abs(diffSeconds);
Math.abs(date2.getTime() - date1.getTime()) / 1000, let value: number;
); let unit: Intl.RelativeTimeFormatUnit;
if (diffInSeconds < 60) if (abs < 60) {
return `${diffInSeconds} second${diffInSeconds !== 1 ? 's' : ''}`; value = Math.round(diffSeconds);
else if (diffInSeconds < 3600) unit = 'second';
return `${Math.floor(diffInSeconds / 60)} minute${Math.floor(diffInSeconds / 60) !== 1 ? 's' : ''}`; } else if (abs < 3600) {
else if (diffInSeconds < 86400) value = Math.round(diffSeconds / 60);
return `${Math.floor(diffInSeconds / 3600)} hour${Math.floor(diffInSeconds / 3600) !== 1 ? 's' : ''}`; unit = 'minute';
else if (diffInSeconds < 31536000) } else if (abs < 86400) {
return `${Math.floor(diffInSeconds / 86400)} day${Math.floor(diffInSeconds / 86400) !== 1 ? 's' : ''}`; value = Math.round(diffSeconds / 3600);
else unit = 'hour';
return `${Math.floor(diffInSeconds / 31536000)} year${Math.floor(diffInSeconds / 31536000) !== 1 ? 's' : ''}`; } else if (abs < 2629800) {
// ~1 month (30.4 days)
value = Math.round(diffSeconds / 86400);
unit = 'day';
} else if (abs < 31557600) {
// ~1 year
value = Math.round(diffSeconds / 2629800);
unit = 'month';
} else {
value = Math.round(diffSeconds / 31557600);
unit = 'year';
}
const rtf = new Intl.RelativeTimeFormat(locale || undefined, {
numeric: 'auto',
});
return rtf.format(value, unit);
}; };

263
yarn.lock
View file

@ -369,6 +369,54 @@
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62" resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62"
integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig== integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==
"@formatjs/ecma402-abstract@2.3.4":
version "2.3.4"
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz#e90c5a846ba2b33d92bc400fdd709da588280fbc"
integrity sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==
dependencies:
"@formatjs/fast-memoize" "2.2.7"
"@formatjs/intl-localematcher" "0.6.1"
decimal.js "^10.4.3"
tslib "^2.8.0"
"@formatjs/fast-memoize@2.2.7", "@formatjs/fast-memoize@^2.2.0":
version "2.2.7"
resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz#707f9ddaeb522a32f6715bb7950b0831f4cc7b15"
integrity sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==
dependencies:
tslib "^2.8.0"
"@formatjs/icu-messageformat-parser@2.11.2":
version "2.11.2"
resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz#85aea211bea40aa81ee1d44ac7accc3cf5500a73"
integrity sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==
dependencies:
"@formatjs/ecma402-abstract" "2.3.4"
"@formatjs/icu-skeleton-parser" "1.8.14"
tslib "^2.8.0"
"@formatjs/icu-skeleton-parser@1.8.14":
version "1.8.14"
resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz#b9581d00363908efb29817fdffc32b79f41dabe5"
integrity sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==
dependencies:
"@formatjs/ecma402-abstract" "2.3.4"
tslib "^2.8.0"
"@formatjs/intl-localematcher@0.6.1":
version "0.6.1"
resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz#25dc30675320bf65a9d7f73876fc1e4064c0e299"
integrity sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==
dependencies:
tslib "^2.8.0"
"@formatjs/intl-localematcher@^0.5.4":
version "0.5.10"
resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz#1e0bd3fc1332c1fe4540cfa28f07e9227b659a58"
integrity sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==
dependencies:
tslib "2"
"@google/generative-ai@^0.24.0": "@google/generative-ai@^0.24.0":
version "0.24.1" version "0.24.1"
resolved "https://registry.yarnpkg.com/@google/generative-ai/-/generative-ai-0.24.1.tgz#634a3c06f8ea7a6125c1b0d6c1e66bb11afb52c9" resolved "https://registry.yarnpkg.com/@google/generative-ai/-/generative-ai-0.24.1.tgz#634a3c06f8ea7a6125c1b0d6c1e66bb11afb52c9"
@ -885,6 +933,11 @@
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.1.tgz#7ca168b6937818e9a74b47ac4e2112b2e1a024cf" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.1.tgz#7ca168b6937818e9a74b47ac4e2112b2e1a024cf"
integrity sha512-S3Kq8e7LqxkA9s7HKLqXGTGck1uwis5vAXan3FnU5yw1Ec5hsSGnq4s/UCaSqABPOnOTg7zASLyst7+ohgWexg== integrity sha512-S3Kq8e7LqxkA9s7HKLqXGTGck1uwis5vAXan3FnU5yw1Ec5hsSGnq4s/UCaSqABPOnOTg7zASLyst7+ohgWexg==
"@schummar/icu-type-parser@1.21.5":
version "1.21.5"
resolved "https://registry.yarnpkg.com/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz#75989085bbbf80ee325874a0137437bde77e9baf"
integrity sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==
"@selderee/plugin-htmlparser2@^0.11.0": "@selderee/plugin-htmlparser2@^0.11.0":
version "0.11.0" version "0.11.0"
resolved "https://registry.yarnpkg.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz#d5b5e29a7ba6d3958a1972c7be16f4b2c188c517" resolved "https://registry.yarnpkg.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz#d5b5e29a7ba6d3958a1972c7be16f4b2c188c517"
@ -956,6 +1009,14 @@
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==
"@types/node-fetch@^2.6.4":
version "2.6.13"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.13.tgz#e0c9b7b5edbdb1b50ce32c127e85e880872d56ee"
integrity sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==
dependencies:
"@types/node" "*"
form-data "^4.0.4"
"@types/node@*", "@types/node@^20": "@types/node@*", "@types/node@^20":
version "20.12.5" version "20.12.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.5.tgz#74c4f31ab17955d0b5808cdc8fd2839526ad00b3" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.5.tgz#74c4f31ab17955d0b5808cdc8fd2839526ad00b3"
@ -970,6 +1031,13 @@
dependencies: dependencies:
undici-types "~6.20.0" undici-types "~6.20.0"
"@types/node@^18.11.18":
version "18.19.123"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.123.tgz#08a3e4f5e0c73b8840c677b7635ce59d5dc1f76d"
integrity sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg==
dependencies:
undici-types "~5.26.4"
"@types/pdf-parse@^1.1.4": "@types/pdf-parse@^1.1.4":
version "1.1.4" version "1.1.4"
resolved "https://registry.yarnpkg.com/@types/pdf-parse/-/pdf-parse-1.1.4.tgz#21a539efd2f16009d08aeed3350133948b5d7ed1" resolved "https://registry.yarnpkg.com/@types/pdf-parse/-/pdf-parse-1.1.4.tgz#21a539efd2f16009d08aeed3350133948b5d7ed1"
@ -1092,6 +1160,13 @@ abort-controller-x@^0.4.0, abort-controller-x@^0.4.3:
resolved "https://registry.yarnpkg.com/abort-controller-x/-/abort-controller-x-0.4.3.tgz#ff269788386fabd58a7b6eeaafcb6cf55c2958e0" resolved "https://registry.yarnpkg.com/abort-controller-x/-/abort-controller-x-0.4.3.tgz#ff269788386fabd58a7b6eeaafcb6cf55c2958e0"
integrity sha512-VtUwTNU8fpMwvWGn4xE93ywbogTYsuT+AUxAXOeelbXuQVIwNmC5YLeho9sH4vZ4ITW8414TTAOG1nW6uIVHCA== integrity sha512-VtUwTNU8fpMwvWGn4xE93ywbogTYsuT+AUxAXOeelbXuQVIwNmC5YLeho9sH4vZ4ITW8414TTAOG1nW6uIVHCA==
abort-controller@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
dependencies:
event-target-shim "^5.0.0"
acorn-jsx@^5.3.2: acorn-jsx@^5.3.2:
version "5.3.2" version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@ -1102,6 +1177,13 @@ acorn@^8.9.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
agentkeepalive@^4.2.1:
version "4.6.0"
resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz#35f73e94b3f40bf65f105219c623ad19c136ea6a"
integrity sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==
dependencies:
humanize-ms "^1.2.1"
ajv@^6.12.4: ajv@^6.12.4:
version "6.12.6" version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@ -1484,6 +1566,14 @@ busboy@1.6.0:
dependencies: dependencies:
streamsearch "^1.1.0" streamsearch "^1.1.0"
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
dependencies:
es-errors "^1.3.0"
function-bind "^1.1.2"
call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
version "1.0.7" version "1.0.7"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
@ -1510,15 +1600,10 @@ camelcase@6:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
caniuse-lite@^1.0.30001579: caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599:
version "1.0.30001705" version "1.0.30001735"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001705.tgz#dc3510bcdef261444ca944b7be9c8d0bb7fafeef" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz"
integrity sha512-S0uyMMiYvA7CxNgomYBwwwPUnWzFD83f3B1ce5jHUfHTH//QL6hHsreI8RVC5606R4ssqravelYO5TU6t8sEyg== integrity sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==
caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599:
version "1.0.30001606"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001606.tgz#b4d5f67ab0746a3b8b5b6d1f06e39c51beb39a9e"
integrity sha512-LPbwnW4vfpJId225pwjZJOgX1m9sGfbw/RKJvw/t0QhYOOaTXHvkjVGFGPpvwEzufrjvTlsULnVTxdy4/6cqkg==
canvg@^3.0.11: canvg@^3.0.11:
version "3.0.11" version "3.0.11"
@ -1786,6 +1871,11 @@ decamelize@1.2.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
decimal.js@^10.4.3:
version "10.6.0"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a"
integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==
decompress-response@^6.0.0: decompress-response@^6.0.0:
version "6.0.0" version "6.0.0"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
@ -1937,6 +2027,15 @@ duck@^0.1.12:
dependencies: dependencies:
underscore "^1.13.1" underscore "^1.13.1"
dunder-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
dependencies:
call-bind-apply-helpers "^1.0.1"
es-errors "^1.3.0"
gopd "^1.2.0"
eastasianwidth@^0.2.0: eastasianwidth@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
@ -2046,6 +2145,11 @@ es-define-property@^1.0.0:
dependencies: dependencies:
get-intrinsic "^1.2.4" get-intrinsic "^1.2.4"
es-define-property@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0: es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
@ -2078,6 +2182,13 @@ es-object-atoms@^1.0.0:
dependencies: dependencies:
es-errors "^1.3.0" es-errors "^1.3.0"
es-object-atoms@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
dependencies:
es-errors "^1.3.0"
es-set-tostringtag@^2.0.3: es-set-tostringtag@^2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777"
@ -2087,6 +2198,16 @@ es-set-tostringtag@^2.0.3:
has-tostringtag "^1.0.2" has-tostringtag "^1.0.2"
hasown "^2.0.1" hasown "^2.0.1"
es-set-tostringtag@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
dependencies:
es-errors "^1.3.0"
get-intrinsic "^1.2.6"
has-tostringtag "^1.0.2"
hasown "^2.0.2"
es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763"
@ -2385,6 +2506,11 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
event-target-shim@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
eventemitter3@^4.0.4: eventemitter3@^4.0.4:
version "4.0.7" version "4.0.7"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
@ -2531,6 +2657,11 @@ foreground-child@^3.1.0:
cross-spawn "^7.0.0" cross-spawn "^7.0.0"
signal-exit "^4.0.1" signal-exit "^4.0.1"
form-data-encoder@1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz#1f1ae3dccf58ed4690b86d87e4f57c654fbab040"
integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==
form-data@^4.0.0: form-data@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
@ -2540,6 +2671,25 @@ form-data@^4.0.0:
combined-stream "^1.0.8" combined-stream "^1.0.8"
mime-types "^2.1.12" mime-types "^2.1.12"
form-data@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
es-set-tostringtag "^2.1.0"
hasown "^2.0.2"
mime-types "^2.1.12"
formdata-node@^4.3.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/formdata-node/-/formdata-node-4.4.1.tgz#23f6a5cb9cb55315912cbec4ff7b0f59bbd191e2"
integrity sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==
dependencies:
node-domexception "1.0.0"
web-streams-polyfill "4.0.0-beta.3"
fraction.js@^4.3.7: fraction.js@^4.3.7:
version "4.3.7" version "4.3.7"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
@ -2608,6 +2758,30 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@
has-symbols "^1.0.3" has-symbols "^1.0.3"
hasown "^2.0.0" hasown "^2.0.0"
get-intrinsic@^1.2.6:
version "1.3.0"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
dependencies:
call-bind-apply-helpers "^1.0.2"
es-define-property "^1.0.1"
es-errors "^1.3.0"
es-object-atoms "^1.1.1"
function-bind "^1.1.2"
get-proto "^1.0.1"
gopd "^1.2.0"
has-symbols "^1.1.0"
hasown "^2.0.2"
math-intrinsics "^1.1.0"
get-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
dependencies:
dunder-proto "^1.0.1"
es-object-atoms "^1.0.0"
get-symbol-description@^1.0.2: get-symbol-description@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5"
@ -2717,6 +2891,11 @@ gopd@^1.0.1:
dependencies: dependencies:
get-intrinsic "^1.1.3" get-intrinsic "^1.1.3"
gopd@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
graceful-fs@^4.2.4: graceful-fs@^4.2.4:
version "4.2.11" version "4.2.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
@ -2785,6 +2964,11 @@ has-symbols@^1.0.2, has-symbols@^1.0.3:
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
has-symbols@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: has-tostringtag@^1.0.0, has-tostringtag@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
@ -2828,6 +3012,13 @@ htmlparser2@^8.0.2:
domutils "^3.0.1" domutils "^3.0.1"
entities "^4.4.0" entities "^4.4.0"
humanize-ms@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==
dependencies:
ms "^2.0.0"
ieee754@^1.1.13: ieee754@^1.1.13:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@ -2883,6 +3074,16 @@ internal-slot@^1.0.7:
hasown "^2.0.0" hasown "^2.0.0"
side-channel "^1.0.4" side-channel "^1.0.4"
intl-messageformat@^10.5.14:
version "10.7.16"
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.7.16.tgz#d909f9f9f4ab857fbe681d559b958dd4dd9f665a"
integrity sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==
dependencies:
"@formatjs/ecma402-abstract" "2.3.4"
"@formatjs/fast-memoize" "2.2.7"
"@formatjs/icu-messageformat-parser" "2.11.2"
tslib "^2.8.0"
is-array-buffer@^3.0.4: is-array-buffer@^3.0.4:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98"
@ -3393,6 +3594,11 @@ markdown-to-jsx@^7.7.2:
resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.7.2.tgz#59c1dd64f48b53719311ab140be3cd51cdabccd3" resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.7.2.tgz#59c1dd64f48b53719311ab140be3cd51cdabccd3"
integrity sha512-N3AKfYRvxNscvcIH6HDnDKILp4S8UWbebp+s92Y8SwIq0CuSbLW4Jgmrbjku3CWKjTQO0OyIMS6AhzqrwjEa3g== integrity sha512-N3AKfYRvxNscvcIH6HDnDKILp4S8UWbebp+s92Y8SwIq0CuSbLW4Jgmrbjku3CWKjTQO0OyIMS6AhzqrwjEa3g==
math-intrinsics@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
merge2@^1.3.0, merge2@^1.4.1: merge2@^1.3.0, merge2@^1.4.1:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
@ -3464,7 +3670,7 @@ ms@2.1.2:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
ms@^2.1.1: ms@^2.0.0, ms@^2.1.1:
version "2.1.3" version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@ -3503,6 +3709,20 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
negotiator@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a"
integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==
next-intl@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/next-intl/-/next-intl-4.3.4.tgz#247ae789f1490b6518e8b18fe13d44cce703098b"
integrity sha512-VWLIDlGbnL/o4LnveJTJD1NOYN8lh3ZAGTWw2krhfgg53as3VsS4jzUVnArJdqvwtlpU/2BIDbWTZ7V4o1jFEw==
dependencies:
"@formatjs/intl-localematcher" "^0.5.4"
negotiator "^1.0.0"
use-intl "^4.3.4"
next-themes@^0.3.0: next-themes@^0.3.0:
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.3.0.tgz#b4d2a866137a67d42564b07f3a3e720e2ff3871a" resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.3.0.tgz#b4d2a866137a67d42564b07f3a3e720e2ff3871a"
@ -3567,12 +3787,17 @@ node-addon-api@^6.1.0:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76"
integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA== integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==
node-domexception@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
node-ensure@^0.0.0: node-ensure@^0.0.0:
version "0.0.0" version "0.0.0"
resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7" resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7"
integrity sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw== integrity sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==
node-fetch@^2.7.0: node-fetch@^2.6.7, node-fetch@^2.7.0:
version "2.7.0" version "2.7.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
@ -4833,7 +5058,7 @@ tsconfig-paths@^3.15.0:
minimist "^1.2.6" minimist "^1.2.6"
strip-bom "^3.0.0" strip-bom "^3.0.0"
tslib@^2.4.0, tslib@^2.8.0: tslib@2, tslib@^2.4.0, tslib@^2.8.0:
version "2.8.1" version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
@ -4951,6 +5176,15 @@ use-composed-ref@^1.3.0:
resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda" resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda"
integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ== integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==
use-intl@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/use-intl/-/use-intl-4.3.4.tgz#42bb2141480032623ffe412799c62cbdf306ed0f"
integrity sha512-sHfiU0QeJ1rirNWRxvCyvlSh9+NczcOzRnPyMeo2rtHXhVnBsvMRjE+UG4eh3lRhCxrvcqei/I0lBxsc59on1w==
dependencies:
"@formatjs/fast-memoize" "^2.2.0"
"@schummar/icu-type-parser" "1.21.5"
intl-messageformat "^10.5.14"
use-isomorphic-layout-effect@^1.1.1: use-isomorphic-layout-effect@^1.1.1:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"
@ -5014,6 +5248,11 @@ weaviate-client@^3.5.2:
nice-grpc-common "^2.0.2" nice-grpc-common "^2.0.2"
uuid "^9.0.1" uuid "^9.0.1"
web-streams-polyfill@4.0.0-beta.3:
version "4.0.0-beta.3"
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz#2898486b74f5156095e473efe989dcf185047a38"
integrity sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==
webidl-conversions@^3.0.0: webidl-conversions@^3.0.0:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"