Compare commits

...

32 Commits

Author SHA1 Message Date
ItzCrazyKns
0dc17286b9 Merge branch 'pr/843' 2025-08-12 21:39:25 +05:30
ItzCrazyKns
3edd7d44dd feat(openai): conditionally set temperature 2025-08-12 21:39:14 +05:30
Samuel Dockery
1132997108 feat: Add support for latest AI models from Anthropic, Google, & OpenAI 2025-08-10 07:50:31 -07:00
ItzCrazyKns
eadbedb713 feat(groq): switch to @langchain/groq for better handling 2025-08-02 17:14:34 +05:30
ItzCrazyKns
37cd6d3ab5 feat(discover): prevent duplicate articles 2025-08-01 20:41:07 +05:30
ItzCrazyKns
88be3a045b feat(discover): randomly sort results 2025-07-29 13:18:36 +05:30
ItzCrazyKns
45b51ab156 feat(discover-api): handle topics 2025-07-29 13:17:07 +05:30
ItzCrazyKns
3bee01cfa7 feat(discover): add topic selection 2025-07-28 20:39:50 +05:30
ItzCrazyKns
567c6a8758 Merge branch 'pr/831' 2025-07-24 22:36:19 +05:30
ItzCrazyKns
81a91da743 feat(gemini): use model instead of modelName 2025-07-23 12:22:26 +05:30
ItzCrazyKns
70a61ee1eb feat(think-box): handle thinkingEnded 2025-07-23 12:21:07 +05:30
ItzCrazyKns
9d89a4413b feat(format-history): remove extra : 2025-07-23 12:20:49 +05:30
ItzCrazyKns
6ea17d54c6 feat(chat-window): fix wrong history while rewriting 2025-07-22 21:21:49 +05:30
ItzCrazyKns
11a828b073 feat(message-box): close think box after thinking process ends 2025-07-22 21:21:09 +05:30
ItzCrazyKns
37022fb11e feat(format-history): update role determination 2025-07-22 21:20:16 +05:30
ItzCrazyKns
dd50d4927b Merge branch 'master' of https://github.com/ItzCrazyKns/Perplexica 2025-07-22 12:27:11 +05:30
ItzCrazyKns
fdaf3af3af Merge pull request #832 from tuxthepenguin84/patch-2
Fix name of provider in embeddings models error message
2025-07-21 20:56:24 +05:30
Samuel Dockery
3f2a8f862c Fix name of provider in embeddings models error message 2025-07-20 09:20:39 -07:00
Samuel Dockery
58c7be6e95 Update Gemini 2.5 Models 2025-07-20 09:17:20 -07:00
ItzCrazyKns
829b4e7134 feat(custom-openai): use apiKey instead of openAIApiKey 2025-07-19 21:37:34 +05:30
ItzCrazyKns
77870b39cc feat(ollama): use @langchain/ollama library 2025-07-19 21:37:34 +05:30
ItzCrazyKns
8e0ae9b867 feat(providers): switch to apiKey key 2025-07-19 21:37:34 +05:30
ItzCrazyKns
543f1df5ce feat(modules): update langchain packages 2025-07-19 21:37:34 +05:30
ItzCrazyKns
341aae4587 Merge branch 'pr/830' 2025-07-19 21:36:23 +05:30
Willie Zutz
7f62907385 feat(weather): update measurement units to Imperial/Metric 2025-07-19 08:53:11 -06:00
ItzCrazyKns
7c4aa683a2 feat(chains): remove unused imports 2025-07-19 17:57:32 +05:30
ItzCrazyKns
b48b0eeb0e feat(imageSearch): use XML parsing, implement few shot prompting 2025-07-19 17:52:30 +05:30
ItzCrazyKns
cddc793915 feat(videoSearch): use XML parsing, use few shot prompting 2025-07-19 17:52:14 +05:30
ItzCrazyKns
94e6db10bb feat(weather): add other measurement units, closes #821 #790 2025-07-18 21:09:32 +05:30
ItzCrazyKns
26e1d5fec3 feat(routes): lint & beautify 2025-07-17 22:23:11 +05:30
ItzCrazyKns
66be87b688 Merge branch 'pr/827' 2025-07-17 22:22:50 +05:30
amoshydra
f7b4e32218 fix(discover): provide language when fetching
some engines provide empty response when no language is provided.

fix #618
2025-07-17 02:14:49 +08:00
27 changed files with 719 additions and 597 deletions

View File

@@ -15,11 +15,13 @@
"@headlessui/react": "^2.2.0", "@headlessui/react": "^2.2.0",
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@icons-pack/react-simple-icons": "^12.3.0", "@icons-pack/react-simple-icons": "^12.3.0",
"@langchain/anthropic": "^0.3.15", "@langchain/anthropic": "^0.3.24",
"@langchain/community": "^0.3.36", "@langchain/community": "^0.3.49",
"@langchain/core": "^0.3.42", "@langchain/core": "^0.3.66",
"@langchain/google-genai": "^0.1.12", "@langchain/google-genai": "^0.2.15",
"@langchain/openai": "^0.0.25", "@langchain/groq": "^0.2.3",
"@langchain/ollama": "^0.2.3",
"@langchain/openai": "^0.6.2",
"@langchain/textsplitters": "^0.1.0", "@langchain/textsplitters": "^0.1.0",
"@tailwindcss/typography": "^0.5.12", "@tailwindcss/typography": "^0.5.12",
"@xenova/transformers": "^2.17.2", "@xenova/transformers": "^2.17.2",
@@ -31,7 +33,7 @@
"drizzle-orm": "^0.40.1", "drizzle-orm": "^0.40.1",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"jspdf": "^3.0.1", "jspdf": "^3.0.1",
"langchain": "^0.1.30", "langchain": "^0.3.30",
"lucide-react": "^0.363.0", "lucide-react": "^0.363.0",
"mammoth": "^1.9.1", "mammoth": "^1.9.1",
"markdown-to-jsx": "^7.7.2", "markdown-to-jsx": "^7.7.2",

View File

@@ -223,7 +223,7 @@ export const POST = async (req: Request) => {
if (body.chatModel?.provider === 'custom_openai') { if (body.chatModel?.provider === 'custom_openai') {
llm = new ChatOpenAI({ llm = new ChatOpenAI({
openAIApiKey: getCustomOpenaiApiKey(), apiKey: getCustomOpenaiApiKey(),
modelName: getCustomOpenaiModelName(), modelName: getCustomOpenaiModelName(),
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {

View File

@@ -1,55 +1,77 @@
import { searchSearxng } from '@/lib/searxng'; import { searchSearxng } from '@/lib/searxng';
const articleWebsites = [ const websitesForTopic = {
'yahoo.com', tech: {
'www.exchangewire.com', query: ['technology news', 'latest tech', 'AI', 'science and innovation'],
'businessinsider.com', links: ['techcrunch.com', 'wired.com', 'theverge.com'],
/* 'wired.com', },
'mashable.com', finance: {
'theverge.com', query: ['finance news', 'economy', 'stock market', 'investing'],
'gizmodo.com', links: ['bloomberg.com', 'cnbc.com', 'marketwatch.com'],
'cnet.com', },
'venturebeat.com', */ art: {
]; query: ['art news', 'culture', 'modern art', 'cultural events'],
links: ['artnews.com', 'hyperallergic.com', 'theartnewspaper.com'],
},
sports: {
query: ['sports news', 'latest sports', 'cricket football tennis'],
links: ['espn.com', 'bbc.com/sport', 'skysports.com'],
},
entertainment: {
query: ['entertainment news', 'movies', 'TV shows', 'celebrities'],
links: ['hollywoodreporter.com', 'variety.com', 'deadline.com'],
},
};
const topics = ['AI', 'tech']; /* TODO: Add UI to customize this */ type Topic = keyof typeof websitesForTopic;
export const GET = async (req: Request) => { export const GET = async (req: Request) => {
try { try {
const params = new URL(req.url).searchParams; const params = new URL(req.url).searchParams;
const mode: 'normal' | 'preview' = const mode: 'normal' | 'preview' =
(params.get('mode') as 'normal' | 'preview') || 'normal'; (params.get('mode') as 'normal' | 'preview') || 'normal';
const topic: Topic = (params.get('topic') as Topic) || 'tech';
const selectedTopic = websitesForTopic[topic];
let data = []; let data = [];
if (mode === 'normal') { if (mode === 'normal') {
const seenUrls = new Set();
data = ( data = (
await Promise.all([ await Promise.all(
...new Array(articleWebsites.length * topics.length) selectedTopic.links.flatMap((link) =>
.fill(0) selectedTopic.query.map(async (query) => {
.map(async (_, i) => {
return ( return (
await searchSearxng( await searchSearxng(`site:${link} ${query}`, {
`site:${articleWebsites[i % articleWebsites.length]} ${
topics[i % topics.length]
}`,
{
engines: ['bing news'], engines: ['bing news'],
pageno: 1, pageno: 1,
}, language: 'en',
) })
).results; ).results;
}), }),
]) ),
)
) )
.map((result) => result)
.flat() .flat()
.filter((item) => {
const url = item.url?.toLowerCase().trim();
if (seenUrls.has(url)) return false;
seenUrls.add(url);
return true;
})
.sort(() => Math.random() - 0.5); .sort(() => Math.random() - 0.5);
} else { } else {
data = ( data = (
await searchSearxng( await searchSearxng(
`site:${articleWebsites[Math.floor(Math.random() * articleWebsites.length)]} ${topics[Math.floor(Math.random() * topics.length)]}`, `site:${selectedTopic.links[Math.floor(Math.random() * selectedTopic.links.length)]} ${selectedTopic.query[Math.floor(Math.random() * selectedTopic.query.length)]}`,
{ engines: ['bing news'], pageno: 1 }, {
engines: ['bing news'],
pageno: 1,
language: 'en',
},
) )
).results; ).results;
} }

View File

@@ -49,7 +49,7 @@ export const POST = async (req: Request) => {
if (body.chatModel?.provider === 'custom_openai') { if (body.chatModel?.provider === 'custom_openai') {
llm = new ChatOpenAI({ llm = new ChatOpenAI({
openAIApiKey: getCustomOpenaiApiKey(), apiKey: getCustomOpenaiApiKey(),
modelName: getCustomOpenaiModelName(), modelName: getCustomOpenaiModelName(),
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {

View File

@@ -81,8 +81,7 @@ export const POST = async (req: Request) => {
if (body.chatModel?.provider === 'custom_openai') { if (body.chatModel?.provider === 'custom_openai') {
llm = new ChatOpenAI({ llm = new ChatOpenAI({
modelName: body.chatModel?.name || getCustomOpenaiModelName(), modelName: body.chatModel?.name || getCustomOpenaiModelName(),
openAIApiKey: apiKey: body.chatModel?.customOpenAIKey || getCustomOpenaiApiKey(),
body.chatModel?.customOpenAIKey || getCustomOpenaiApiKey(),
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {
baseURL: baseURL:

View File

@@ -48,7 +48,7 @@ export const POST = async (req: Request) => {
if (body.chatModel?.provider === 'custom_openai') { if (body.chatModel?.provider === 'custom_openai') {
llm = new ChatOpenAI({ llm = new ChatOpenAI({
openAIApiKey: getCustomOpenaiApiKey(), apiKey: getCustomOpenaiApiKey(),
modelName: getCustomOpenaiModelName(), modelName: getCustomOpenaiModelName(),
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {

View File

@@ -49,7 +49,7 @@ export const POST = async (req: Request) => {
if (body.chatModel?.provider === 'custom_openai') { if (body.chatModel?.provider === 'custom_openai') {
llm = new ChatOpenAI({ llm = new ChatOpenAI({
openAIApiKey: getCustomOpenaiApiKey(), apiKey: getCustomOpenaiApiKey(),
modelName: getCustomOpenaiModelName(), modelName: getCustomOpenaiModelName(),
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {

View File

@@ -1,6 +1,10 @@
export const POST = async (req: Request) => { export const POST = async (req: Request) => {
try { try {
const body: { lat: number; lng: number } = await req.json(); const body: {
lat: number;
lng: number;
measureUnit: 'Imperial' | 'Metric';
} = await req.json();
if (!body.lat || !body.lng) { if (!body.lat || !body.lng) {
return Response.json( return Response.json(
@@ -12,7 +16,9 @@ export const POST = async (req: Request) => {
} }
const res = await fetch( const res = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${body.lat}&longitude=${body.lng}&current=weather_code,temperature_2m,is_day,relative_humidity_2m,wind_speed_10m&timezone=auto`, `https://api.open-meteo.com/v1/forecast?latitude=${body.lat}&longitude=${body.lng}&current=weather_code,temperature_2m,is_day,relative_humidity_2m,wind_speed_10m&timezone=auto${
body.measureUnit === 'Metric' ? '' : '&temperature_unit=fahrenheit'
}${body.measureUnit === 'Metric' ? '' : '&wind_speed_unit=mph'}`,
); );
const data = await res.json(); const data = await res.json();
@@ -33,12 +39,16 @@ export const POST = async (req: Request) => {
humidity: number; humidity: number;
windSpeed: number; windSpeed: number;
icon: string; icon: string;
temperatureUnit: 'C' | 'F';
windSpeedUnit: 'm/s' | 'mph';
} = { } = {
temperature: data.current.temperature_2m, temperature: data.current.temperature_2m,
condition: '', condition: '',
humidity: data.current.relative_humidity_2m, humidity: data.current.relative_humidity_2m,
windSpeed: data.current.wind_speed_10m, windSpeed: data.current.wind_speed_10m,
icon: '', icon: '',
temperatureUnit: body.measureUnit === 'Metric' ? 'C' : 'F',
windSpeedUnit: body.measureUnit === 'Metric' ? 'm/s' : 'mph',
}; };
const code = data.current.weather_code; const code = data.current.weather_code;

View File

@@ -4,6 +4,7 @@ import { Search } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { cn } from '@/lib/utils';
interface Discover { interface Discover {
title: string; title: string;
@@ -12,14 +13,38 @@ interface Discover {
thumbnail: string; thumbnail: string;
} }
const topics: { key: string; display: string }[] = [
{
display: 'Tech & Science',
key: 'tech',
},
{
display: 'Finance',
key: 'finance',
},
{
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);
useEffect(() => { const fetchArticles = async (topic: string) => {
const fetchData = async () => { setLoading(true);
try { try {
const res = await fetch(`/api/discover`, { const res = await fetch(`/api/discover?topic=${topic}`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -43,10 +68,39 @@ const Page = () => {
} }
}; };
fetchData(); useEffect(() => {
}, []); fetchArticles(activeTopic);
}, [activeTopic]);
return loading ? ( return (
<>
<div>
<div className="flex flex-col pt-4">
<div className="flex items-center">
<Search />
<h1 className="text-3xl font-medium p-2">Discover</h1>
</div>
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
</div>
<div className="flex flex-row items-center space-x-2 overflow-x-auto">
{topics.map((t, i) => (
<div
key={i}
className={cn(
'border-[0.1px] rounded-full text-sm px-3 py-1 text-nowrap transition duration-200 cursor-pointer',
activeTopic === t.key
? 'text-cyan-300 bg-cyan-300/30 border-cyan-300/60'
: 'border-white/30 text-white/70 hover:text-white hover:border-white/40 hover:bg-white/5',
)}
onClick={() => setActiveTopic(t.key)}
>
<span>{t.display}</span>
</div>
))}
</div>
{loading ? (
<div className="flex flex-row items-center justify-center min-h-screen"> <div className="flex flex-row items-center justify-center min-h-screen">
<svg <svg
aria-hidden="true" aria-hidden="true"
@@ -66,17 +120,7 @@ const Page = () => {
</svg> </svg>
</div> </div>
) : ( ) : (
<> <div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-4 pb-28 pt-5 lg:pb-8 w-full justify-items-center lg:justify-items-start">
<div>
<div className="flex flex-col pt-4">
<div className="flex items-center">
<Search />
<h1 className="text-3xl font-medium p-2">Discover</h1>
</div>
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
</div>
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-4 pb-28 lg:pb-8 w-full justify-items-center lg:justify-items-start">
{discover && {discover &&
discover?.map((item, i) => ( discover?.map((item, i) => (
<Link <Link
@@ -105,6 +149,7 @@ const Page = () => {
</Link> </Link>
))} ))}
</div> </div>
)}
</div> </div>
</> </>
); );

View File

@@ -148,6 +148,9 @@ const Page = () => {
const [automaticImageSearch, setAutomaticImageSearch] = useState(false); const [automaticImageSearch, setAutomaticImageSearch] = useState(false);
const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false); const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false);
const [systemInstructions, setSystemInstructions] = useState<string>(''); const [systemInstructions, setSystemInstructions] = useState<string>('');
const [measureUnit, setMeasureUnit] = useState<'Imperial' | 'Metric'>(
'Metric',
);
const [savingStates, setSavingStates] = useState<Record<string, boolean>>({}); const [savingStates, setSavingStates] = useState<Record<string, boolean>>({});
useEffect(() => { useEffect(() => {
@@ -210,6 +213,10 @@ const Page = () => {
setSystemInstructions(localStorage.getItem('systemInstructions')!); setSystemInstructions(localStorage.getItem('systemInstructions')!);
setMeasureUnit(
localStorage.getItem('measureUnit')! as 'Imperial' | 'Metric',
);
setIsLoading(false); setIsLoading(false);
}; };
@@ -368,6 +375,8 @@ const Page = () => {
localStorage.setItem('embeddingModel', value); localStorage.setItem('embeddingModel', value);
} else if (key === 'systemInstructions') { } else if (key === 'systemInstructions') {
localStorage.setItem('systemInstructions', value); localStorage.setItem('systemInstructions', value);
} else if (key === 'measureUnit') {
localStorage.setItem('measureUnit', value.toString());
} }
} catch (err) { } catch (err) {
console.error('Failed to save:', err); console.error('Failed to save:', err);
@@ -416,13 +425,35 @@ 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="Appearance"> <SettingsSection title="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 Theme
</p> </p>
<ThemeSwitcher /> <ThemeSwitcher />
</div> </div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Measurement Units
</p>
<Select
value={measureUnit ?? undefined}
onChange={(e) => {
setMeasureUnit(e.target.value as 'Imperial' | 'Metric');
saveConfig('measureUnit', e.target.value);
}}
options={[
{
label: 'Metric',
value: 'Metric',
},
{
label: 'Imperial',
value: 'Imperial',
},
]}
/>
</div>
</SettingsSection> </SettingsSection>
<SettingsSection title="Automatic Search"> <SettingsSection title="Automatic Search">
@@ -516,7 +547,7 @@ const Page = () => {
<SettingsSection title="System Instructions"> <SettingsSection title="System Instructions">
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
<Textarea <Textarea
value={systemInstructions} value={systemInstructions ?? undefined}
isSaving={savingStates['systemInstructions']} isSaving={savingStates['systemInstructions']}
onChange={(e) => { onChange={(e) => {
setSystemInstructions(e.target.value); setSystemInstructions(e.target.value);

View File

@@ -354,7 +354,11 @@ const ChatWindow = ({ id }: { id?: string }) => {
} }
}, [isMessagesLoaded, isConfigReady]); }, [isMessagesLoaded, isConfigReady]);
const sendMessage = async (message: string, messageId?: string) => { const sendMessage = async (
message: string,
messageId?: string,
rewrite = false,
) => {
if (loading) return; if (loading) return;
if (!isConfigReady) { if (!isConfigReady) {
toast.error('Cannot send message before the configuration is ready'); toast.error('Cannot send message before the configuration is ready');
@@ -482,6 +486,8 @@ const ChatWindow = ({ id }: { id?: string }) => {
} }
}; };
const messageIndex = messages.findIndex((m) => m.messageId === messageId);
const res = await fetch('/api/chat', { const res = await fetch('/api/chat', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -498,7 +504,9 @@ const ChatWindow = ({ id }: { id?: string }) => {
files: fileIds, files: fileIds,
focusMode: focusMode, focusMode: focusMode,
optimizationMode: optimizationMode, optimizationMode: optimizationMode,
history: chatHistory, history: rewrite
? chatHistory.slice(0, messageIndex === -1 ? undefined : messageIndex)
: chatHistory,
chatModel: { chatModel: {
name: chatModelProvider.name, name: chatModelProvider.name,
provider: chatModelProvider.provider, provider: chatModelProvider.provider,
@@ -552,7 +560,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)]; return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)];
}); });
sendMessage(message.content, message.messageId); sendMessage(message.content, message.messageId, true);
}; };
useEffect(() => { useEffect(() => {

View File

@@ -21,8 +21,16 @@ 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';
const ThinkTagProcessor = ({ children }: { children: React.ReactNode }) => { const ThinkTagProcessor = ({
return <ThinkBox content={children as string} />; children,
thinkingEnded,
}: {
children: React.ReactNode;
thinkingEnded: boolean;
}) => {
return (
<ThinkBox content={children as string} thinkingEnded={thinkingEnded} />
);
}; };
const MessageBox = ({ const MessageBox = ({
@@ -46,6 +54,7 @@ const MessageBox = ({
}) => { }) => {
const [parsedMessage, setParsedMessage] = useState(message.content); const [parsedMessage, setParsedMessage] = useState(message.content);
const [speechMessage, setSpeechMessage] = useState(message.content); const [speechMessage, setSpeechMessage] = useState(message.content);
const [thinkingEnded, setThinkingEnded] = useState(false);
useEffect(() => { useEffect(() => {
const citationRegex = /\[([^\]]+)\]/g; const citationRegex = /\[([^\]]+)\]/g;
@@ -61,6 +70,10 @@ const MessageBox = ({
} }
} }
if (message.role === 'assistant' && message.content.includes('</think>')) {
setThinkingEnded(true);
}
if ( if (
message.role === 'assistant' && message.role === 'assistant' &&
message?.sources && message?.sources &&
@@ -88,7 +101,7 @@ const MessageBox = ({
if (url) { if (url) {
return `<a href="${url}" target="_blank" className="bg-light-secondary dark:bg-dark-secondary px-1 rounded ml-1 no-underline text-xs text-black/70 dark:text-white/70 relative">${numStr}</a>`; return `<a href="${url}" target="_blank" className="bg-light-secondary dark:bg-dark-secondary px-1 rounded ml-1 no-underline text-xs text-black/70 dark:text-white/70 relative">${numStr}</a>`;
} else { } else {
return `[${numStr}]`; return ``;
} }
}) })
.join(''); .join('');
@@ -99,6 +112,14 @@ const MessageBox = ({
); );
setSpeechMessage(message.content.replace(regex, '')); setSpeechMessage(message.content.replace(regex, ''));
return; return;
} else if (
message.role === 'assistant' &&
message?.sources &&
message.sources.length === 0
) {
setParsedMessage(processedMessage.replace(regex, ''));
setSpeechMessage(message.content.replace(regex, ''));
return;
} }
setSpeechMessage(message.content.replace(regex, '')); setSpeechMessage(message.content.replace(regex, ''));
@@ -111,6 +132,9 @@ const MessageBox = ({
overrides: { overrides: {
think: { think: {
component: ThinkTagProcessor, component: ThinkTagProcessor,
props: {
thinkingEnded: thinkingEnded,
},
}, },
}, },
}; };

View File

@@ -1,15 +1,23 @@
'use client'; 'use client';
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { cn } from '@/lib/utils';
import { ChevronDown, ChevronUp, BrainCircuit } from 'lucide-react'; import { ChevronDown, ChevronUp, BrainCircuit } from 'lucide-react';
interface ThinkBoxProps { interface ThinkBoxProps {
content: string; content: string;
thinkingEnded: boolean;
} }
const ThinkBox = ({ content }: ThinkBoxProps) => { const ThinkBox = ({ content, thinkingEnded }: ThinkBoxProps) => {
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(true);
useEffect(() => {
if (thinkingEnded) {
setIsExpanded(false);
} else {
setIsExpanded(true);
}
}, [thinkingEnded]);
return ( return (
<div className="my-4 bg-light-secondary/50 dark:bg-dark-secondary/50 rounded-xl border border-light-200 dark:border-dark-200 overflow-hidden"> <div className="my-4 bg-light-secondary/50 dark:bg-dark-secondary/50 rounded-xl border border-light-200 dark:border-dark-200 overflow-hidden">

View File

@@ -9,7 +9,10 @@ const WeatherWidget = () => {
humidity: 0, humidity: 0,
windSpeed: 0, windSpeed: 0,
icon: '', icon: '',
temperatureUnit: 'C',
windSpeedUnit: 'm/s',
}); });
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
@@ -73,6 +76,7 @@ const WeatherWidget = () => {
body: JSON.stringify({ body: JSON.stringify({
lat: location.latitude, lat: location.latitude,
lng: location.longitude, lng: location.longitude,
measureUnit: localStorage.getItem('measureUnit') ?? 'Metric',
}), }),
}); });
@@ -91,6 +95,8 @@ const WeatherWidget = () => {
humidity: data.humidity, humidity: data.humidity,
windSpeed: data.windSpeed, windSpeed: data.windSpeed,
icon: data.icon, icon: data.icon,
temperatureUnit: data.temperatureUnit,
windSpeedUnit: data.windSpeedUnit,
}); });
setLoading(false); setLoading(false);
}); });
@@ -125,7 +131,7 @@ const WeatherWidget = () => {
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">
{data.temperature}°C {data.temperature}°{data.temperatureUnit}
</span> </span>
</div> </div>
<div className="flex flex-col justify-between flex-1 h-full py-1"> <div className="flex flex-col justify-between flex-1 h-full py-1">
@@ -135,7 +141,7 @@ const WeatherWidget = () => {
</span> </span>
<span className="flex items-center text-xs text-black/60 dark:text-white/60"> <span className="flex items-center text-xs text-black/60 dark:text-white/60">
<Wind className="w-3 h-3 mr-1" /> <Wind className="w-3 h-3 mr-1" />
{data.windSpeed} km/h {data.windSpeed} {data.windSpeedUnit}
</span> </span>
</div> </div>
<span className="text-xs text-black/60 dark:text-white/60 mt-1"> <span className="text-xs text-black/60 dark:text-white/60 mt-1">

View File

@@ -3,32 +3,18 @@ import {
RunnableMap, RunnableMap,
RunnableLambda, RunnableLambda,
} from '@langchain/core/runnables'; } from '@langchain/core/runnables';
import { PromptTemplate } from '@langchain/core/prompts'; import { ChatPromptTemplate } from '@langchain/core/prompts';
import formatChatHistoryAsString from '../utils/formatHistory'; import formatChatHistoryAsString from '../utils/formatHistory';
import { BaseMessage } from '@langchain/core/messages'; import { BaseMessage } from '@langchain/core/messages';
import { StringOutputParser } from '@langchain/core/output_parsers'; import { StringOutputParser } from '@langchain/core/output_parsers';
import { searchSearxng } from '../searxng'; import { searchSearxng } from '../searxng';
import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import LineOutputParser from '../outputParsers/lineOutputParser';
const imageSearchChainPrompt = ` const imageSearchChainPrompt = `
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search the web for images. You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search the web for images.
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation. You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation.
Output only the rephrased query wrapped in an XML <query> element. Do not include any explanation or additional text.
Example:
1. Follow up question: What is a cat?
Rephrased: A cat
2. Follow up question: What is a car? How does it works?
Rephrased: Car working
3. Follow up question: How does an AC work?
Rephrased: AC working
Conversation:
{chat_history}
Follow up question: {query}
Rephrased question:
`; `;
type ImageSearchChainInput = { type ImageSearchChainInput = {
@@ -54,12 +40,39 @@ const createImageSearchChain = (llm: BaseChatModel) => {
return input.query; return input.query;
}, },
}), }),
PromptTemplate.fromTemplate(imageSearchChainPrompt), ChatPromptTemplate.fromMessages([
['system', imageSearchChainPrompt],
[
'user',
'<conversation>\n</conversation>\n<follow_up>\nWhat is a cat?\n</follow_up>',
],
['assistant', '<query>A cat</query>'],
[
'user',
'<conversation>\n</conversation>\n<follow_up>\nWhat is a car? How does it work?\n</follow_up>',
],
['assistant', '<query>Car working</query>'],
[
'user',
'<conversation>\n</conversation>\n<follow_up>\nHow does an AC work?\n</follow_up>',
],
['assistant', '<query>AC working</query>'],
[
'user',
'<conversation>{chat_history}</conversation>\n<follow_up>\n{query}\n</follow_up>',
],
]),
llm, llm,
strParser, strParser,
RunnableLambda.from(async (input: string) => { RunnableLambda.from(async (input: string) => {
input = input.replace(/<think>.*?<\/think>/g, ''); const queryParser = new LineOutputParser({
key: 'query',
});
return await queryParser.parse(input);
}),
RunnableLambda.from(async (input: string) => {
const res = await searchSearxng(input, { const res = await searchSearxng(input, {
engines: ['bing images', 'google images'], engines: ['bing images', 'google images'],
}); });

View File

@@ -3,33 +3,19 @@ import {
RunnableMap, RunnableMap,
RunnableLambda, RunnableLambda,
} from '@langchain/core/runnables'; } from '@langchain/core/runnables';
import { PromptTemplate } from '@langchain/core/prompts'; import { ChatPromptTemplate } from '@langchain/core/prompts';
import formatChatHistoryAsString from '../utils/formatHistory'; import formatChatHistoryAsString from '../utils/formatHistory';
import { BaseMessage } from '@langchain/core/messages'; import { BaseMessage } from '@langchain/core/messages';
import { StringOutputParser } from '@langchain/core/output_parsers'; import { StringOutputParser } from '@langchain/core/output_parsers';
import { searchSearxng } from '../searxng'; import { searchSearxng } from '../searxng';
import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import LineOutputParser from '../outputParsers/lineOutputParser';
const VideoSearchChainPrompt = ` const videoSearchChainPrompt = `
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search Youtube for videos. You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search Youtube for videos.
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation. You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation.
Output only the rephrased query wrapped in an XML <query> element. Do not include any explanation or additional text.
Example: `;
1. Follow up question: How does a car work?
Rephrased: How does a car work?
2. Follow up question: What is the theory of relativity?
Rephrased: What is theory of relativity
3. Follow up question: How does an AC work?
Rephrased: How does an AC work
Conversation:
{chat_history}
Follow up question: {query}
Rephrased question:
`;
type VideoSearchChainInput = { type VideoSearchChainInput = {
chat_history: BaseMessage[]; chat_history: BaseMessage[];
@@ -55,12 +41,37 @@ const createVideoSearchChain = (llm: BaseChatModel) => {
return input.query; return input.query;
}, },
}), }),
PromptTemplate.fromTemplate(VideoSearchChainPrompt), ChatPromptTemplate.fromMessages([
['system', videoSearchChainPrompt],
[
'user',
'<conversation>\n</conversation>\n<follow_up>\nHow does a car work?\n</follow_up>',
],
['assistant', '<query>How does a car work?</query>'],
[
'user',
'<conversation>\n</conversation>\n<follow_up>\nWhat is the theory of relativity?\n</follow_up>',
],
['assistant', '<query>Theory of relativity</query>'],
[
'user',
'<conversation>\n</conversation>\n<follow_up>\nHow does an AC work?\n</follow_up>',
],
['assistant', '<query>AC working</query>'],
[
'user',
'<conversation>{chat_history}</conversation>\n<follow_up>\n{query}\n</follow_up>',
],
]),
llm, llm,
strParser, strParser,
RunnableLambda.from(async (input: string) => { RunnableLambda.from(async (input: string) => {
input = input.replace(/<think>.*?<\/think>/g, ''); const queryParser = new LineOutputParser({
key: 'query',
});
return await queryParser.parse(input);
}),
RunnableLambda.from(async (input: string) => {
const res = await searchSearxng(input, { const res = await searchSearxng(input, {
engines: ['youtube'], engines: ['youtube'],
}); });
@@ -92,8 +103,8 @@ const handleVideoSearch = (
input: VideoSearchChainInput, input: VideoSearchChainInput,
llm: BaseChatModel, llm: BaseChatModel,
) => { ) => {
const VideoSearchChain = createVideoSearchChain(llm); const videoSearchChain = createVideoSearchChain(llm);
return VideoSearchChain.invoke(input); return videoSearchChain.invoke(input);
}; };
export default handleVideoSearch; export default handleVideoSearch;

View File

@@ -38,7 +38,7 @@ export const loadAimlApiChatModels = async () => {
chatModels[model.id] = { chatModels[model.id] = {
displayName: model.name || model.id, displayName: model.name || model.id,
model: new ChatOpenAI({ model: new ChatOpenAI({
openAIApiKey: apiKey, apiKey: apiKey,
modelName: model.id, modelName: model.id,
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {
@@ -76,7 +76,7 @@ export const loadAimlApiEmbeddingModels = async () => {
embeddingModels[model.id] = { embeddingModels[model.id] = {
displayName: model.name || model.id, displayName: model.name || model.id,
model: new OpenAIEmbeddings({ model: new OpenAIEmbeddings({
openAIApiKey: apiKey, apiKey: apiKey,
modelName: model.id, modelName: model.id,
configuration: { configuration: {
baseURL: API_URL, baseURL: API_URL,

View File

@@ -9,6 +9,18 @@ export const PROVIDER_INFO = {
import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { BaseChatModel } from '@langchain/core/language_models/chat_models';
const anthropicChatModels: Record<string, string>[] = [ const anthropicChatModels: Record<string, string>[] = [
{
displayName: 'Claude 4.1 Opus',
key: 'claude-opus-4-1-20250805',
},
{
displayName: 'Claude 4 Opus',
key: 'claude-opus-4-20250514',
},
{
displayName: 'Claude 4 Sonnet',
key: 'claude-sonnet-4-20250514',
},
{ {
displayName: 'Claude 3.7 Sonnet', displayName: 'Claude 3.7 Sonnet',
key: 'claude-3-7-sonnet-20250219', key: 'claude-3-7-sonnet-20250219',

View File

@@ -31,7 +31,7 @@ export const loadDeepseekChatModels = async () => {
chatModels[model.key] = { chatModels[model.key] = {
displayName: model.displayName, displayName: model.displayName,
model: new ChatOpenAI({ model: new ChatOpenAI({
openAIApiKey: deepseekApiKey, apiKey: deepseekApiKey,
modelName: model.key, modelName: model.key,
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {

View File

@@ -14,16 +14,16 @@ import { Embeddings } from '@langchain/core/embeddings';
const geminiChatModels: Record<string, string>[] = [ const geminiChatModels: Record<string, string>[] = [
{ {
displayName: 'Gemini 2.5 Flash Preview 05-20', displayName: 'Gemini 2.5 Flash',
key: 'gemini-2.5-flash-preview-05-20', key: 'gemini-2.5-flash',
}, },
{ {
displayName: 'Gemini 2.5 Pro Preview', displayName: 'Gemini 2.5 Flash-Lite',
key: 'gemini-2.5-pro-preview-05-06', key: 'gemini-2.5-flash-lite',
}, },
{ {
displayName: 'Gemini 2.5 Pro Experimental', displayName: 'Gemini 2.5 Pro',
key: 'gemini-2.5-pro-preview-05-06', key: 'gemini-2.5-pro',
}, },
{ {
displayName: 'Gemini 2.0 Flash', displayName: 'Gemini 2.0 Flash',
@@ -75,7 +75,7 @@ export const loadGeminiChatModels = async () => {
displayName: model.displayName, displayName: model.displayName,
model: new ChatGoogleGenerativeAI({ model: new ChatGoogleGenerativeAI({
apiKey: geminiApiKey, apiKey: geminiApiKey,
modelName: model.key, model: model.key,
temperature: 0.7, temperature: 0.7,
}) as unknown as BaseChatModel, }) as unknown as BaseChatModel,
}; };
@@ -108,7 +108,7 @@ export const loadGeminiEmbeddingModels = async () => {
return embeddingModels; return embeddingModels;
} catch (err) { } catch (err) {
console.error(`Error loading OpenAI embeddings models: ${err}`); console.error(`Error loading Gemini embeddings models: ${err}`);
return {}; return {};
} }
}; };

View File

@@ -1,4 +1,4 @@
import { ChatOpenAI } from '@langchain/openai'; import { ChatGroq } from '@langchain/groq';
import { getGroqApiKey } from '../config'; import { getGroqApiKey } from '../config';
import { ChatModel } from '.'; import { ChatModel } from '.';
@@ -28,13 +28,10 @@ export const loadGroqChatModels = async () => {
groqChatModels.forEach((model: any) => { groqChatModels.forEach((model: any) => {
chatModels[model.id] = { chatModels[model.id] = {
displayName: model.id, displayName: model.id,
model: new ChatOpenAI({ model: new ChatGroq({
openAIApiKey: groqApiKey, apiKey: groqApiKey,
modelName: model.id, model: model.id,
temperature: 0.7, temperature: 0.7,
configuration: {
baseURL: 'https://api.groq.com/openai/v1',
},
}) as unknown as BaseChatModel, }) as unknown as BaseChatModel,
}; };
}); });

View File

@@ -118,7 +118,7 @@ export const getAvailableChatModelProviders = async () => {
[customOpenAiModelName]: { [customOpenAiModelName]: {
displayName: customOpenAiModelName, displayName: customOpenAiModelName,
model: new ChatOpenAI({ model: new ChatOpenAI({
openAIApiKey: customOpenAiApiKey, apiKey: customOpenAiApiKey,
modelName: customOpenAiModelName, modelName: customOpenAiModelName,
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {

View File

@@ -47,7 +47,7 @@ export const loadLMStudioChatModels = async () => {
chatModels[model.id] = { chatModels[model.id] = {
displayName: model.name || model.id, displayName: model.name || model.id,
model: new ChatOpenAI({ model: new ChatOpenAI({
openAIApiKey: 'lm-studio', apiKey: 'lm-studio',
configuration: { configuration: {
baseURL: ensureV1Endpoint(endpoint), baseURL: ensureV1Endpoint(endpoint),
}, },
@@ -83,7 +83,7 @@ export const loadLMStudioEmbeddingsModels = async () => {
embeddingsModels[model.id] = { embeddingsModels[model.id] = {
displayName: model.name || model.id, displayName: model.name || model.id,
model: new OpenAIEmbeddings({ model: new OpenAIEmbeddings({
openAIApiKey: 'lm-studio', apiKey: 'lm-studio',
configuration: { configuration: {
baseURL: ensureV1Endpoint(endpoint), baseURL: ensureV1Endpoint(endpoint),
}, },

View File

@@ -6,8 +6,8 @@ export const PROVIDER_INFO = {
key: 'ollama', key: 'ollama',
displayName: 'Ollama', displayName: 'Ollama',
}; };
import { ChatOllama } from '@langchain/community/chat_models/ollama'; import { ChatOllama } from '@langchain/ollama';
import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama'; import { OllamaEmbeddings } from '@langchain/ollama';
export const loadOllamaChatModels = async () => { export const loadOllamaChatModels = async () => {
const ollamaApiEndpoint = getOllamaApiEndpoint(); const ollamaApiEndpoint = getOllamaApiEndpoint();

View File

@@ -42,6 +42,18 @@ const openaiChatModels: Record<string, string>[] = [
displayName: 'GPT 4.1', displayName: 'GPT 4.1',
key: 'gpt-4.1', key: 'gpt-4.1',
}, },
{
displayName: 'GPT 5 nano',
key: 'gpt-5-nano',
},
{
displayName: 'GPT 5 mini',
key: 'gpt-5-mini',
},
{
displayName: 'GPT 5',
key: 'gpt-5',
},
]; ];
const openaiEmbeddingModels: Record<string, string>[] = [ const openaiEmbeddingModels: Record<string, string>[] = [
@@ -67,9 +79,9 @@ export const loadOpenAIChatModels = async () => {
chatModels[model.key] = { chatModels[model.key] = {
displayName: model.displayName, displayName: model.displayName,
model: new ChatOpenAI({ model: new ChatOpenAI({
openAIApiKey: openaiApiKey, apiKey: openaiApiKey,
modelName: model.key, modelName: model.key,
temperature: 0.7, temperature: model.key.includes('gpt-5') ? 1 : 0.7,
}) as unknown as BaseChatModel, }) as unknown as BaseChatModel,
}; };
}); });
@@ -93,7 +105,7 @@ export const loadOpenAIEmbeddingModels = async () => {
embeddingModels[model.key] = { embeddingModels[model.key] = {
displayName: model.displayName, displayName: model.displayName,
model: new OpenAIEmbeddings({ model: new OpenAIEmbeddings({
openAIApiKey: openaiApiKey, apiKey: openaiApiKey,
modelName: model.key, modelName: model.key,
}) as unknown as Embeddings, }) as unknown as Embeddings,
}; };

View File

@@ -1,8 +1,11 @@
import { BaseMessage } from '@langchain/core/messages'; import { BaseMessage, isAIMessage } from '@langchain/core/messages';
const formatChatHistoryAsString = (history: BaseMessage[]) => { const formatChatHistoryAsString = (history: BaseMessage[]) => {
return history return history
.map((message) => `${message._getType()}: ${message.content}`) .map(
(message) =>
`${isAIMessage(message) ? 'AI' : 'User'}: ${message.content}`,
)
.join('\n'); .join('\n');
}; };

701
yarn.lock

File diff suppressed because it is too large Load Diff