Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# .env.local
OPENAI_API_KEY="sk-..."
GROQ_API_KEY="gsk_..."
GOOGLE_API_KEY="AIza..."
FIRECRAWL_API_KEY="..."
24 changes: 24 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Usa una imagen base de Node.js 20
FROM node:20-alpine AS base

# Establece el directorio de trabajo
WORKDIR /app

# Instala las dependencias necesarias para la compilación
RUN apk add --no-cache libc6-compat

# Copia los archivos de manifiesto del paquete e instala las dependencias
COPY package*.json ./
RUN npm install

# Copia el resto de los archivos de la aplicación
COPY . .

# Construye la aplicación
RUN npm run build

# Expone el puerto en el que se ejecuta la aplicación
EXPOSE 3000

# Define el comando para iniciar la aplicación
CMD ["npm", "start"]
50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,61 @@ npm run dev

Visit http://localhost:3000

---

## Docker

You can also run Fireplexity in a Docker container:

```bash
docker build -t fireplexity .
docker run --env-file .env.local -p 3000:3000 fireplexity
```

Make sure to set your API keys in `.env.local` before building the image.

---

## LLM Providers

Fireplexity supports multiple LLM providers. By default, it uses OpenAI, but you can switch to other providers (such as Fireworks, Together, or Groq) by setting the following environment variable in `.env.local`:

```
LLM_PROVIDER=openai # or fireworks, together, groq
```

You may need to provide additional API keys depending on the provider you choose. See the documentation for each provider for details.

---

## Example Queries & Scraping Tips

Fireplexity can scrape and answer questions about a wide range of topics using real-time web data. Here are some example queries you can try:

- `Summarize the latest news about OpenAI.`
- `What are the top 3 competitors of Firecrawl?`
- `Show me the current stock price and chart for Tesla.`
- `Find recent reviews for the iPhone 15.`
- `Get the weather forecast for Madrid this week.`
- `What are the main findings from the latest Nature article on AI safety?`
- `Compare the pricing of AWS and Google Cloud in 2024.`

**Tips for Optimal Results:**
- Be specific: Include names, dates, or sources if possible (e.g., "Summarize the latest Wired article on quantum computing").
- Ask for summaries or comparisons to get concise, actionable answers.
- For financial data, mention the company name or ticker (e.g., "AAPL stock chart").
- For academic or technical topics, reference the publication or author for more accurate scraping.
- If you want sources, add "with sources" or "with citations" to your query.

## Tech Stack

- **Firecrawl** - Web scraping API
- **Next.js 15** - React framework
- **OpenAI** - GPT-4o-mini
- **OpenAI** - GPT-4o-mini (default, configurable)
- **Vercel AI SDK** - Streaming
- **TradingView** - Stock charts
- **Docker** - Containerized deployment
- **Multiple LLM Providers** - OpenAI, Fireworks, Together, Groq

## Deploy

Expand Down
28 changes: 11 additions & 17 deletions app/api/fire-cache/search/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextResponse } from 'next/server'
import { createOpenAI } from '@ai-sdk/openai'
import { getLLMProvider, Provider } from '@/lib/llm-provider'
import { streamText, generateText, createDataStreamResponse } from 'ai'
import { detectCompanyTicker } from '@/lib/company-ticker-map'

Expand All @@ -8,7 +8,7 @@ export async function POST(request: Request) {
console.log(`[${requestId}] Fire Cache Search API called`)
try {
const body = await request.json()
const messages = body.messages || []
const { messages = [], provider = 'groq', model = 'llama3-8b-8192' } = body
const query = messages[messages.length - 1]?.content || body.query
console.log(`[${requestId}] Query received:`, query)

Expand All @@ -17,20 +17,14 @@ export async function POST(request: Request) {
}

const firecrawlApiKey = process.env.FIRECRAWL_API_KEY
const openaiApiKey = process.env.OPENAI_API_KEY

const llmProvider = getLLMProvider(provider as Provider)
const llmModel = llmProvider(model)
console.log(`[${requestId}] Using provider: ${provider}, model: ${model}`)
console.log(`[${requestId}] LLM Model Object:`, llmModel)

if (!firecrawlApiKey) {
return NextResponse.json({ error: 'Firecrawl API key not configured' }, { status: 500 })
}

if (!openaiApiKey) {
return NextResponse.json({ error: 'OpenAI API key not configured' }, { status: 500 })
}

// Configure OpenAI with API key
const openai = createOpenAI({
apiKey: openaiApiKey
})

// Always perform a fresh search for each query to ensure relevant results
const isFollowUp = messages.length > 2
Expand Down Expand Up @@ -200,7 +194,7 @@ export async function POST(request: Request) {
: `user: ${query}`

const followUpPromise = generateText({
model: openai('gpt-4o'),
model: llmModel,
messages: [
{
role: 'system',
Expand All @@ -213,15 +207,15 @@ export async function POST(request: Request) {
content: `Query: ${query}\n\nConversation context:\n${conversationPreview}\n\n${sources.length > 0 ? `Available sources about: ${sources.map((s: { title: string }) => s.title).join(', ')}\n\n` : ''}Generate 5 diverse follow-up questions that would help the user learn more about this topic from different angles.`
}
],
temperature: 0.7,
temperature: 0.5,
maxTokens: 150,
})

// Stream the text generation
const result = streamText({
model: openai('gpt-4o'),
model: llmModel,
messages: aiMessages,
temperature: 0.7,
temperature: 0.5,
maxTokens: 2000
})

Expand Down
18 changes: 8 additions & 10 deletions app/api/fireplexity/search/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextResponse } from 'next/server'
import { createOpenAI } from '@ai-sdk/openai'
import { getLLMProvider, Provider } from '@/lib/llm-provider'
import { streamText, generateText, createDataStreamResponse } from 'ai'
import { detectCompanyTicker } from '@/lib/company-ticker-map'
import { selectRelevantContent } from '@/lib/content-selection'
Expand All @@ -10,8 +10,11 @@ export async function POST(request: Request) {
console.log(`[${requestId}] Fireplexity Search API called`)
try {
const body = await request.json()
const messages = body.messages || []
const { messages = [], provider = 'groq', model = 'llama3-8b-8192' } = body
// pick the requested LLM
const llmModel = getLLMProvider(provider as Provider)(model)
const query = messages[messages.length - 1]?.content || body.query

console.log(`[${requestId}] Query received:`, query)

if (!query) {
Expand All @@ -30,11 +33,6 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'OpenAI API key not configured' }, { status: 500 })
}

// Configure OpenAI with API key
const openai = createOpenAI({
apiKey: openaiApiKey
})

// Initialize Firecrawl
const firecrawl = new FirecrawlApp({ apiKey: firecrawlApiKey })

Expand Down Expand Up @@ -170,13 +168,13 @@ export async function POST(request: Request) {
]
}

// Start generating follow-up questions in parallel (before streaming answer)
// Start generating follow-up questions in parallel
const conversationPreview = isFollowUp
? messages.map((m: { role: string; content: string }) => `${m.role}: ${m.content}`).join('\n\n')
: `user: ${query}`

const followUpPromise = generateText({
model: openai('gpt-4o-mini'),
model: llmModel,
messages: [
{
role: 'system',
Expand All @@ -195,7 +193,7 @@ export async function POST(request: Request) {

// Stream the text generation
const result = streamText({
model: openai('gpt-4o-mini'),
model: llmModel,
messages: aiMessages,
temperature: 0.7,
maxTokens: 2000
Expand Down
7 changes: 4 additions & 3 deletions app/chat-interface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export function ChatInterface({ messages, sources, followUpQuestions, searchStat
</svg>
)}
</div>
))}
))}
</div>
</div>
)}
Expand Down Expand Up @@ -614,7 +614,7 @@ export function ChatInterface({ messages, sources, followUpQuestions, searchStat
}
}}
placeholder="Ask a follow-up question..."
className="resize-none border-0 focus:ring-0 focus:outline-none bg-transparent placeholder:text-gray-400 dark:placeholder:text-gray-500 px-4 py-2 pr-2 shadow-none focus-visible:ring-0 focus-visible:border-0"
className="resize-none border-0 focus:ring-0 focus:outline-none bg-transparent placeholder:text-gray-500 dark:placeholder:text-gray-400 text-gray-900 dark:text-gray-100 px-4 py-2 pr-2 shadow-none focus-visible:ring-0 focus-visible:border-0"
rows={1}
style={{
minHeight: '36px',
Expand All @@ -626,7 +626,8 @@ export function ChatInterface({ messages, sources, followUpQuestions, searchStat
<button
type="submit"
disabled={!input.trim() || isLoading}
className="bg-orange-500 hover:bg-orange-600 disabled:bg-gray-300 disabled:cursor-not-allowed text-white rounded-full h-8 w-8 min-h-[32px] min-w-[32px] flex items-center justify-center flex-shrink-0 transition-colors"
className={`bg-orange-500 hover:bg-orange-600 disabled:bg-gray-300 disabled:text-gray-400 disabled:cursor-not-allowed text-white rounded-full h-8 w-8 min-h-[32px] min-w-[32px] flex items-center justify-center flex-shrink-0 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-orange-400/50`}
aria-label="Send"
>
{isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
Expand Down
12 changes: 9 additions & 3 deletions app/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ export function SearchComponent({ handleSubmit, input, handleInputChange, isLoad
value={input}
onChange={handleInputChange}
placeholder="Ask anything..."
className="pr-24 h-14 text-lg rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-zinc-800 transition-colors"
className="flex w-full min-w-0 px-3 py-1 h-14 rounded-xl border border-gray-200 dark:border-gray-700
bg-white dark:bg-zinc-800
text-gray-900 dark:text-gray-100
placeholder:text-gray-500 dark:placeholder:text-gray-400
selection:bg-primary selection:text-primary-foreground
focus-visible:border-orange-400 focus-visible:ring-orange-400/20 focus-visible:ring-2
transition-colors"
disabled={isLoading}
/>
<Button
Expand All @@ -30,9 +36,9 @@ export function SearchComponent({ handleSubmit, input, handleInputChange, isLoad
className="absolute right-2 rounded-lg"
>
{isLoading ? (
<Loader2 className="h-5 w-5 animate-spin" />
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Search className="h-5 w-5" />
<Search className="h-4 w-4" />
)}
</Button>
</div>
Expand Down
2 changes: 1 addition & 1 deletion components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const buttonVariants = cva(
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
code: "h-9 px-4 rounded-[10px] text-sm font-medium items-center transition-all duration-200 disabled:cursor-not-allowed disabled:opacity-50 bg-[#36322F] text-[#fff] hover:bg-[#4a4542] disabled:bg-[#8c8885] disabled:hover:bg-[#8c8885] [box-shadow:inset_0px_-2.108433723449707px_0px_0px_#171310,_0px_1.2048193216323853px_6.325301647186279px_0px_rgba(58,_33,_8,_58%)] hover:translate-y-[1px] hover:scale-[0.98] hover:[box-shadow:inset_0px_-1px_0px_0px_#171310,_0px_1px_3px_0px_rgba(58,_33,_8,_40%)] active:translate-y-[2px] active:scale-[0.97] active:[box-shadow:inset_0px_1px_1px_0px_#171310,_0px_1px_2px_0px_rgba(58,_33,_8,_30%)] disabled:shadow-none disabled:hover:translate-y-0 disabled:hover:scale-100",
orange: "h-9 px-4 rounded-[10px] text-sm font-medium items-center transition-all duration-200 disabled:cursor-not-allowed disabled:opacity-50 bg-orange-500 text-white hover:bg-orange-300 dark:bg-orange-500 dark:hover:bg-orange-300 dark:text-white [box-shadow:inset_0px_-2.108433723449707px_0px_0px_#c2410c,_0px_1.2048193216323853px_6.325301647186279px_0px_rgba(234,_88,_12,_58%)] hover:translate-y-[1px] hover:scale-[0.98] hover:[box-shadow:inset_0px_-1px_0px_0px_#c2410c,_0px_1px_3px_0px_rgba(234,_88,_12,_40%)] active:translate-y-[2px] active:scale-[0.97] active:[box-shadow:inset_0px_1px_1px_0px_#c2410c,_0px_1px_2px_0px_rgba(234,_88,_12,_30%)] disabled:shadow-none disabled:hover:translate-y-0 disabled:hover:scale-100",
orange: "h-9 px-4 rounded-[10px] text-sm font-medium items-center transition-all duration-200 disabled:cursor-not-allowed disabled:opacity-50 bg-orange-500 text-white hover:bg-orange-600 dark:bg-orange-500 dark:hover:bg-orange-400 dark:text-white disabled:bg-gray-300 disabled:text-gray-400 [box-shadow:inset_0px_-2.108433723449707px_0px_0px_#c2410c,_0px_1.2048193216323853px_6.325301647186279px_0px_rgba(234,_88,_12,_58%)] hover:translate-y-[1px] hover:scale-[0.98] hover:[box-shadow:inset_0px_-1px_0px_0px_#c2410c,_0px_1px_3px_0px_rgba(234,_88,_12,_40%)] active:translate-y-[2px] active:scale-[0.97] active:[box-shadow:inset_0px_1px_1px_0px_#c2410c,_0px_1px_2px_0px_rgba(234,_88,_12,_40%)]",
},
size: {
default: "h-10 px-4 py-2",
Expand Down
2 changes: 1 addition & 1 deletion components/ui/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"file:text-foreground placeholder:text-gray-500 dark:placeholder:text-gray-400 selection:bg-primary selection:text-primary-foreground dark:bg-zinc-800 bg-white text-gray-900 dark:text-gray-100",
"focus-visible:border-orange-400 focus-visible:ring-orange-400/20 focus-visible:ring-2",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
Expand Down
2 changes: 1 addition & 1 deletion components/ui/textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"border-input placeholder:text-gray-500 dark:placeholder:text-gray-400 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-zinc-800 bg-white text-gray-900 dark:text-gray-100 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
Expand Down
57 changes: 57 additions & 0 deletions lib/company-ticker-map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# company-ticker-map.ts

Este módulo contiene un mapa de nombres comunes de empresas, índices bursátiles y criptomonedas a sus respectivos símbolos de ticker, así como una función para detectar el ticker relevante en una consulta de texto.

## Uso óptimo

### 1. Detección de ticker en texto libre

Utiliza la función:

```typescript
import { detectCompanyTicker } from './company-ticker-map'

const ticker = detectCompanyTicker('¿Cómo va apple en la bolsa?')
// ticker === 'NASDAQ:AAPL'
```

- Devuelve el ticker en formato estándar (`NASDAQ:AAPL`, `CRYPTO:BTCUSD`, `INDEX:SPX`) o `null` si no detecta ninguno.

### 2. Búsqueda directa en el mapa

Puedes buscar directamente por nombre común, alias o símbolo:

```typescript
import { companyTickerMap } from './company-ticker-map'

companyTickerMap['apple'] // 'NASDAQ:AAPL'
companyTickerMap['btc'] // 'CRYPTO:BTCUSD'
companyTickerMap['sp500'] // 'INDEX:SPX'
```

### 3. Consultas optimizadas

- Normaliza el texto a minúsculas y elimina tildes/acentos si es necesario.
- Busca primero por ticker directo (ej: `$AAPL`, `NASDAQ:AAPL`, `BTCUSD`).
- Si no hay coincidencia directa, busca por nombre común usando el mapa.
- Usa la función `detectCompanyTicker` para manejar patrones y alias automáticamente.

### 4. Ejemplos de consultas válidas

| Consulta | Resultado esperado |
|-------------------------------------------|---------------------------|
| ¿Cómo va apple en la bolsa? | NASDAQ:AAPL |
| Precio de BTC hoy | CRYPTO:BTCUSD |
| sp500 chart | INDEX:SPX |
| Dame el gráfico de ethereum | CRYPTO:ETHUSD |
| Qué tal el dow jones? | INDEX:DJI |
| AAPL stock | NASDAQ:AAPL |
| $TSLA | NASDAQ:TSLA |

### 5. Ampliar soporte

Para ampliar el soporte, agrega nuevos alias o tickers al objeto `companyTickerMap` en el archivo TypeScript.

> **Nota:** El sistema es sensible a nombres y alias definidos en el mapa. Para mejores resultados, mantén actualizado el mapa con los nombres y símbolos más usados.

---
Loading