introducing dark mode, we are so back!
This commit is contained in:
parent
59b0da7710
commit
40de2e9415
154
app/actions.ts
154
app/actions.ts
@ -1,13 +1,9 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { OpenAI } from 'openai';
|
import { OpenAI } from 'openai';
|
||||||
import { Redis } from '@upstash/redis';
|
import { generateObject } from 'ai';
|
||||||
import { Ratelimit } from '@upstash/ratelimit';
|
|
||||||
import { generateObject, generateText } from 'ai';
|
|
||||||
import { createOpenAI } from '@ai-sdk/openai'
|
|
||||||
import { createOpenAI as createGroq } from '@ai-sdk/openai';
|
import { createOpenAI as createGroq } from '@ai-sdk/openai';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { headers } from 'next/headers';
|
|
||||||
import { load } from 'cheerio';
|
import { load } from 'cheerio';
|
||||||
|
|
||||||
const groq = createGroq({
|
const groq = createGroq({
|
||||||
@ -19,7 +15,7 @@ export async function suggestQuestions(history: any[]) {
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
const { object } = await generateObject({
|
const { object } = await generateObject({
|
||||||
model: groq('llama-3.1-70b-versatile'),
|
model: groq('llama-3.2-90b-text-preview'),
|
||||||
temperature: 0,
|
temperature: 0,
|
||||||
system:
|
system:
|
||||||
`You are a search engine query generator. You 'have' to create only '3' questions for the search engine based on the message history which has been provided to you.
|
`You are a search engine query generator. You 'have' to create only '3' questions for the search engine based on the message history which has been provided to you.
|
||||||
@ -42,123 +38,49 @@ Never use pronouns in the questions as they blur the context.`,
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ELEVENLABS_API_KEY = process.env.ELEVENLABS_API_KEY;
|
||||||
|
|
||||||
export async function generateSpeech(text: string, voice: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer' = "alloy") {
|
export async function generateSpeech(text: string, voice: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer' = "alloy") {
|
||||||
if (process.env.OPENAI_PROVIDER === 'azure') {
|
|
||||||
if (!process.env.AZURE_OPENAI_API_KEY || !process.env.AZURE_OPENAI_API_URL) {
|
|
||||||
throw new Error('Azure OpenAI API key and URL are required.');
|
|
||||||
}
|
|
||||||
const url = process.env.AZURE_OPENAI_API_URL!;
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const VOICE_ID = 'JBFqnCBsd6RMkjVDRZzb' // This is the ID for the "George" voice. Replace with your preferred voice ID.
|
||||||
method: 'POST',
|
const url = `https://api.elevenlabs.io/v1/text-to-speech/${VOICE_ID}`
|
||||||
headers: {
|
const method = 'POST'
|
||||||
'api-key': process.env.AZURE_OPENAI_API_KEY!,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: "tts",
|
|
||||||
input: text,
|
|
||||||
voice: voice
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!ELEVENLABS_API_KEY) {
|
||||||
throw new Error(`Failed to generate speech: ${response.statusText}`);
|
throw new Error('ELEVENLABS_API_KEY is not defined');
|
||||||
}
|
|
||||||
|
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
|
||||||
const base64Audio = Buffer.from(arrayBuffer).toString('base64');
|
|
||||||
|
|
||||||
return {
|
|
||||||
audio: `data:audio/mp3;base64,${base64Audio}`,
|
|
||||||
};
|
|
||||||
} else if (process.env.OPENAI_PROVIDER === 'openai') {
|
|
||||||
const openai = new OpenAI();
|
|
||||||
|
|
||||||
const response = await openai.audio.speech.create({
|
|
||||||
model: "tts-1",
|
|
||||||
voice: voice,
|
|
||||||
input: text,
|
|
||||||
});
|
|
||||||
|
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
|
||||||
const base64Audio = Buffer.from(arrayBuffer).toString('base64');
|
|
||||||
|
|
||||||
return {
|
|
||||||
audio: `data:audio/mp3;base64,${base64Audio}`,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const openai = new OpenAI();
|
|
||||||
|
|
||||||
const response = await openai.audio.speech.create({
|
|
||||||
model: "tts-1",
|
|
||||||
voice: voice,
|
|
||||||
input: text,
|
|
||||||
});
|
|
||||||
|
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
|
||||||
|
|
||||||
const base64Audio = Buffer.from(arrayBuffer).toString('base64');
|
|
||||||
|
|
||||||
return {
|
|
||||||
audio: `data:audio/mp3;base64,${base64Audio}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const openai = createOpenAI({
|
|
||||||
baseURL: "https://openrouter.ai/api/v1/",
|
|
||||||
apiKey: process.env.OPENROUTER_API_KEY,
|
|
||||||
headers: {
|
|
||||||
"HTTP-Referer": "https://mplx.run/search",
|
|
||||||
"X-Title": "MiniPerplx"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const redis = new Redis({
|
|
||||||
url: process.env.UPSTASH_REDIS_REST_URL!,
|
|
||||||
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
|
|
||||||
});
|
|
||||||
|
|
||||||
const ratelimit = new Ratelimit({
|
|
||||||
redis: redis,
|
|
||||||
limiter: Ratelimit.fixedWindow(10, '4 h'),
|
|
||||||
analytics: true,
|
|
||||||
prefix: 'miniperplx',
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface Message {
|
|
||||||
role: 'user' | 'assistant';
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function continueConversation(history: Message[]) {
|
|
||||||
'use server';
|
|
||||||
|
|
||||||
const headersList = headers();
|
|
||||||
const ip = headersList.get('x-forwarded-for') ?? 'unknown';
|
|
||||||
const { success, limit, reset, remaining } = await ratelimit.limit(ip);
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
const resetDate = new Date(reset * 1000); // Convert seconds to milliseconds
|
|
||||||
throw new Error(`4-hour rate limit exceeded. Try again after ${resetDate.toLocaleTimeString()}.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { text } = await generateText({
|
const headers = {
|
||||||
model: openai('openai/o1-mini'),
|
Accept: 'audio/mpeg',
|
||||||
messages: history,
|
'xi-api-key': ELEVENLABS_API_KEY,
|
||||||
});
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
text,
|
||||||
|
model_id: 'eleven_turbo_v2_5',
|
||||||
|
voice_settings: {
|
||||||
|
stability: 0.5,
|
||||||
|
similarity_boost: 0.5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = JSON.stringify(data)
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, input)
|
||||||
|
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
|
||||||
|
const base64Audio = Buffer.from(arrayBuffer).toString('base64');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
messages: [
|
audio: `data:audio/mp3;base64,${base64Audio}`,
|
||||||
...history,
|
|
||||||
{
|
|
||||||
role: 'assistant' as const,
|
|
||||||
content: text,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
remaining,
|
|
||||||
reset: reset * 1000, // Convert seconds to milliseconds
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,12 +4,13 @@ import { Metadata, Viewport } from "next";
|
|||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
import { Inter, Instrument_Serif, IBM_Plex_Mono } from 'next/font/google';
|
import { Inter, Instrument_Serif, IBM_Plex_Mono } from 'next/font/google';
|
||||||
import { Analytics } from "@vercel/analytics/react";
|
import { Analytics } from "@vercel/analytics/react";
|
||||||
|
import { Providers } from './providers'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
metadataBase: new URL("https://mplx.run"),
|
metadataBase: new URL("https://mplx.run"),
|
||||||
title: "MiniPerplx",
|
title: "MiniPerplx",
|
||||||
description: "MiniPerplx is a minimalistic AI-powered search engine that helps you find information on the internet.",
|
description: "MiniPerplx is a minimalistic AI-powered search engine that helps you find information on the internet.",
|
||||||
openGraph : {
|
openGraph: {
|
||||||
url: "https://mplx.run",
|
url: "https://mplx.run",
|
||||||
siteName: "MiniPerplx",
|
siteName: "MiniPerplx",
|
||||||
}
|
}
|
||||||
@ -48,8 +49,10 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={`${inter.className} ${instrumentSerif.className} ${plexMono.className}`}>
|
<body className={`${inter.className} ${instrumentSerif.className} ${plexMono.className}`}>
|
||||||
<Toaster position="top-center" richColors />
|
<Providers>
|
||||||
{children}
|
<Toaster position="top-center" richColors />
|
||||||
|
{children}
|
||||||
|
</Providers>
|
||||||
<Analytics />
|
<Analytics />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
16
app/page.tsx
16
app/page.tsx
@ -122,7 +122,7 @@ function GetStarted() {
|
|||||||
title="Get Started"
|
title="Get Started"
|
||||||
icon={BarChart}
|
icon={BarChart}
|
||||||
description={"Experience the power of minimalistic AI search with MiniPerplx."}
|
description={"Experience the power of minimalistic AI search with MiniPerplx."}
|
||||||
className="col-span-full sm:col-span-1 sm:row-span-2"
|
className="col-span-full sm:col-span-1 sm:row-span-2 dark:text-neutral-950"
|
||||||
gradient="from-blue-700 via-60% via-blue-600 to-cyan-600"
|
gradient="from-blue-700 via-60% via-blue-600 to-cyan-600"
|
||||||
>
|
>
|
||||||
<div className="group relative flex cursor-pointer flex-col justify-end rounded-md bg-zinc-900 p-2 text-xl sm:text-2xl md:text-4xl tracking-tight text-gray-100">
|
<div className="group relative flex cursor-pointer flex-col justify-end rounded-md bg-zinc-900 p-2 text-xl sm:text-2xl md:text-4xl tracking-tight text-gray-100">
|
||||||
@ -146,7 +146,7 @@ function MinimalisticSearch() {
|
|||||||
icon={Search}
|
icon={Search}
|
||||||
description="We strip away the clutter to focus on what matters most - delivering accurate and relevant results."
|
description="We strip away the clutter to focus on what matters most - delivering accurate and relevant results."
|
||||||
gradient="from-red-700 via-60% via-red-600 to-rose-600"
|
gradient="from-red-700 via-60% via-red-600 to-rose-600"
|
||||||
className="group col-span-full sm:col-span-1"
|
className="group col-span-full sm:col-span-1 dark:text-neutral-950"
|
||||||
>
|
>
|
||||||
<div className="mt-2 sm:mt-4 space-y-1 sm:space-y-2">
|
<div className="mt-2 sm:mt-4 space-y-1 sm:space-y-2">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@ -173,7 +173,7 @@ function AIPowered() {
|
|||||||
icon={Code}
|
icon={Code}
|
||||||
description="Leveraging cutting-edge AI technology to understand and respond to your queries with precision."
|
description="Leveraging cutting-edge AI technology to understand and respond to your queries with precision."
|
||||||
gradient="from-emerald-700 via-60% via-emerald-600 to-green-600"
|
gradient="from-emerald-700 via-60% via-emerald-600 to-green-600"
|
||||||
className="group col-span-full sm:col-span-1"
|
className="group col-span-full sm:col-span-1 dark:text-neutral-950"
|
||||||
>
|
>
|
||||||
<div className="mt-2 sm:mt-4 space-y-1 sm:space-y-2">
|
<div className="mt-2 sm:mt-4 space-y-1 sm:space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@ -206,7 +206,7 @@ function LightningFast() {
|
|||||||
icon={Zap}
|
icon={Zap}
|
||||||
description="Designed for speed, MiniPerplx provides instant answers to keep up with your pace of work."
|
description="Designed for speed, MiniPerplx provides instant answers to keep up with your pace of work."
|
||||||
gradient="from-purple-700 via-60% via-purple-600 to-fuchsia-600"
|
gradient="from-purple-700 via-60% via-purple-600 to-fuchsia-600"
|
||||||
className="col-span-full sm:col-span-2"
|
className="col-span-full sm:col-span-2 dark:text-neutral-950"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -247,7 +247,7 @@ const MarqueeTestimonials: React.FC = () => {
|
|||||||
transition={{ repeat: Infinity, duration: 20, ease: "linear" }}
|
transition={{ repeat: Infinity, duration: 20, ease: "linear" }}
|
||||||
>
|
>
|
||||||
{testimonials.concat(testimonials).map((text, index) => (
|
{testimonials.concat(testimonials).map((text, index) => (
|
||||||
<span key={index} className="text-white text-xl font-bold mx-8">
|
<span key={index} className="text-white dark:text-black text-xl font-bold mx-8">
|
||||||
{text}
|
{text}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@ -697,12 +697,12 @@ const LandingPage: React.FC = () => {
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
<main className="flex-1">
|
<main className="flex-1">
|
||||||
<section className="w-full py-48 bg-gradient-to-b from-background to-muted relative overflow-hidden">
|
<section className="w-full py-48 bg-gradient-to-b from-background f to-muted relative overflow-hidden">
|
||||||
<FloatingIcons />
|
<FloatingIcons />
|
||||||
<div className="container px-4 md:px-6 relative z-10">
|
<div className="container px-4 md:px-6 relative z-10">
|
||||||
<div className="text-center space-y-4">
|
<div className="text-center space-y-4">
|
||||||
<motion.h1
|
<motion.h1
|
||||||
className="font-serif font-bold text-6xl md:text-7xl lg:text-8xl bg-clip-text text-transparent bg-black leading-[1.1] tracking-tight pb-2"
|
className="font-serif font-bold text-6xl md:text-7xl lg:text-8xl bg-clip-text text-transparent bg-black dark:bg-white leading-[1.1] tracking-tight pb-2"
|
||||||
variants={itemVariants}
|
variants={itemVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="visible"
|
animate="visible"
|
||||||
@ -710,7 +710,7 @@ const LandingPage: React.FC = () => {
|
|||||||
Introducing MiniPerplx
|
Introducing MiniPerplx
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
<motion.p
|
<motion.p
|
||||||
className="mx-auto max-w-[700px] text-muted-foreground text-xl md:text-2xl text-balance font-serif tracking-normal"
|
className="mx-auto max-w-[700px] text-muted-foreground dark:text-neutral-200 text-xl md:text-2xl text-balance font-serif tracking-normal"
|
||||||
variants={itemVariants}
|
variants={itemVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="visible"
|
animate="visible"
|
||||||
|
|||||||
15
app/providers.tsx
Normal file
15
app/providers.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { ThemeProvider } from "next-themes"
|
||||||
|
import { ReactNode } from "react"
|
||||||
|
|
||||||
|
export function Providers({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="system"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
29
components/ui/switch.tsx
Normal file
29
components/ui/switch.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Switch = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SwitchPrimitives.Root
|
||||||
|
className={cn(
|
||||||
|
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<SwitchPrimitives.Thumb
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitives.Root>
|
||||||
|
))
|
||||||
|
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||||
|
|
||||||
|
export { Switch }
|
||||||
25
components/ui/textarea.tsx
Normal file
25
components/ui/textarea.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export interface TextareaProps
|
||||||
|
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||||
|
|
||||||
|
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
dir="ltr"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Textarea.displayName = "Textarea"
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
@ -26,6 +26,7 @@
|
|||||||
"@radix-ui/react-popover": "^1.1.1",
|
"@radix-ui/react-popover": "^1.1.1",
|
||||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-switch": "^1.1.1",
|
||||||
"@radix-ui/react-tabs": "^1.1.0",
|
"@radix-ui/react-tabs": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
"@tailwindcss/typography": "^0.5.13",
|
"@tailwindcss/typography": "^0.5.13",
|
||||||
@ -51,6 +52,7 @@
|
|||||||
"lucide-react": "^0.424.0",
|
"lucide-react": "^0.424.0",
|
||||||
"marked-react": "^2.0.0",
|
"marked-react": "^2.0.0",
|
||||||
"next": "^14.2.10",
|
"next": "^14.2.10",
|
||||||
|
"next-themes": "^0.3.0",
|
||||||
"openai": "^4.56.0",
|
"openai": "^4.56.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
|||||||
1427
pnpm-lock.yaml
1427
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
|||||||
import type { Config } from "tailwindcss"
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user