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';
|
||||
|
||||
import { OpenAI } from 'openai';
|
||||
import { Redis } from '@upstash/redis';
|
||||
import { Ratelimit } from '@upstash/ratelimit';
|
||||
import { generateObject, generateText } from 'ai';
|
||||
import { createOpenAI } from '@ai-sdk/openai'
|
||||
import { generateObject } from 'ai';
|
||||
import { createOpenAI as createGroq } from '@ai-sdk/openai';
|
||||
import { z } from 'zod';
|
||||
import { headers } from 'next/headers';
|
||||
import { load } from 'cheerio';
|
||||
|
||||
const groq = createGroq({
|
||||
@ -19,7 +15,7 @@ export async function suggestQuestions(history: any[]) {
|
||||
'use server';
|
||||
|
||||
const { object } = await generateObject({
|
||||
model: groq('llama-3.1-70b-versatile'),
|
||||
model: groq('llama-3.2-90b-text-preview'),
|
||||
temperature: 0,
|
||||
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.
|
||||
@ -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") {
|
||||
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, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'api-key': process.env.AZURE_OPENAI_API_KEY!,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "tts",
|
||||
input: text,
|
||||
voice: voice
|
||||
})
|
||||
});
|
||||
const VOICE_ID = 'JBFqnCBsd6RMkjVDRZzb' // This is the ID for the "George" voice. Replace with your preferred voice ID.
|
||||
const url = `https://api.elevenlabs.io/v1/text-to-speech/${VOICE_ID}`
|
||||
const method = 'POST'
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to generate speech: ${response.statusText}`);
|
||||
}
|
||||
|
||||
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()}.`);
|
||||
if (!ELEVENLABS_API_KEY) {
|
||||
throw new Error('ELEVENLABS_API_KEY is not defined');
|
||||
}
|
||||
|
||||
const { text } = await generateText({
|
||||
model: openai('openai/o1-mini'),
|
||||
messages: history,
|
||||
});
|
||||
const headers = {
|
||||
Accept: 'audio/mpeg',
|
||||
'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 {
|
||||
messages: [
|
||||
...history,
|
||||
{
|
||||
role: 'assistant' as const,
|
||||
content: text,
|
||||
},
|
||||
],
|
||||
remaining,
|
||||
reset: reset * 1000, // Convert seconds to milliseconds
|
||||
audio: `data:audio/mp3;base64,${base64Audio}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -4,12 +4,13 @@ import { Metadata, Viewport } from "next";
|
||||
import { Toaster } from "sonner";
|
||||
import { Inter, Instrument_Serif, IBM_Plex_Mono } from 'next/font/google';
|
||||
import { Analytics } from "@vercel/analytics/react";
|
||||
import { Providers } from './providers'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://mplx.run"),
|
||||
title: "MiniPerplx",
|
||||
description: "MiniPerplx is a minimalistic AI-powered search engine that helps you find information on the internet.",
|
||||
openGraph : {
|
||||
openGraph: {
|
||||
url: "https://mplx.run",
|
||||
siteName: "MiniPerplx",
|
||||
}
|
||||
@ -48,8 +49,10 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${inter.className} ${instrumentSerif.className} ${plexMono.className}`}>
|
||||
<Toaster position="top-center" richColors />
|
||||
{children}
|
||||
<Providers>
|
||||
<Toaster position="top-center" richColors />
|
||||
{children}
|
||||
</Providers>
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
16
app/page.tsx
16
app/page.tsx
@ -122,7 +122,7 @@ function GetStarted() {
|
||||
title="Get Started"
|
||||
icon={BarChart}
|
||||
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"
|
||||
>
|
||||
<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}
|
||||
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"
|
||||
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="flex items-center">
|
||||
@ -173,7 +173,7 @@ function AIPowered() {
|
||||
icon={Code}
|
||||
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"
|
||||
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="flex items-center justify-between">
|
||||
@ -206,7 +206,7 @@ function LightningFast() {
|
||||
icon={Zap}
|
||||
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"
|
||||
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" }}
|
||||
>
|
||||
{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}
|
||||
</span>
|
||||
))}
|
||||
@ -697,12 +697,12 @@ const LandingPage: React.FC = () => {
|
||||
</AnimatePresence>
|
||||
|
||||
<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 />
|
||||
<div className="container px-4 md:px-6 relative z-10">
|
||||
<div className="text-center space-y-4">
|
||||
<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}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
@ -710,7 +710,7 @@ const LandingPage: React.FC = () => {
|
||||
Introducing MiniPerplx
|
||||
</motion.h1>
|
||||
<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}
|
||||
initial="hidden"
|
||||
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-scroll-area": "^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-tooltip": "^1.1.2",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
@ -51,6 +52,7 @@
|
||||
"lucide-react": "^0.424.0",
|
||||
"marked-react": "^2.0.0",
|
||||
"next": "^14.2.10",
|
||||
"next-themes": "^0.3.0",
|
||||
"openai": "^4.56.0",
|
||||
"react": "^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 = {
|
||||
darkMode: ["class"],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user