├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── app ├── [page] │ └── page.tsx ├── api │ ├── og-image │ │ └── route.ts │ ├── query │ │ └── route.ts │ ├── rsearch │ │ └── route.ts │ └── search │ │ └── route.ts ├── globals.css ├── layout.tsx ├── page.tsx ├── rsearch │ └── page.tsx └── settings │ └── page.tsx ├── components.json ├── components ├── rSearch │ ├── query.tsx │ ├── results.tsx │ ├── sources-sidebar.tsx │ ├── sources.tsx │ ├── sources │ │ ├── sources-academic.tsx │ │ ├── sources-images.tsx │ │ ├── sources-knowledge.tsx │ │ ├── sources-news.tsx │ │ ├── sources-patents.tsx │ │ ├── sources-places.tsx │ │ ├── sources-scholar.tsx │ │ ├── sources-search.tsx │ │ ├── sources-shopping.tsx │ │ ├── sources-videos.tsx │ │ └── sources-web.tsx │ └── thinking.tsx └── ui │ ├── button.tsx │ ├── card.tsx │ ├── carousel.tsx │ ├── dialog.tsx │ ├── drawer.tsx │ ├── dropdown-menu.tsx │ ├── input.tsx │ ├── label.tsx │ ├── logo.tsx │ ├── meteors.tsx │ ├── mobile-header.tsx │ ├── radio-group.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── sidebar.tsx │ ├── skeleton.tsx │ ├── switch.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ ├── toggle.tsx │ └── tooltip.tsx ├── data ├── serper_samples │ ├── apple inc-us-en-true-1-10-images--.json │ ├── apple inc-us-en-true-1-10-maps-----.json │ ├── apple inc-us-en-true-1-10-news---------.json │ ├── apple inc-us-en-true-1-10-places--.json │ ├── apple inc-us-en-true-1-10-search--.json │ ├── apple inc-us-en-true-1-10-videos--.json │ ├── apple inc-us-en-true-1-20-patents---------.json │ ├── apple inc-us-en-true-1-20-scholar---------.json │ └── apple inc-us-en-true-1-20-shopping---------.json └── static.json ├── demo.png ├── eslint.config.mjs ├── hooks ├── use-media-query.ts └── use-mobile.tsx ├── lib ├── imageLoader.ts ├── prompts.ts └── utils.ts ├── next.config.js ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── apple-touch-icon-114x114.png ├── apple-touch-icon-120x120.png ├── apple-touch-icon-144x144.png ├── apple-touch-icon-152x152.png ├── apple-touch-icon-180x180.png ├── apple-touch-icon-57x57.png ├── apple-touch-icon-72x72.png ├── apple-touch-icon-76x76.png ├── apple-touch-icon.png ├── favicon.ico ├── file.svg ├── globe.svg ├── logo.png ├── next.svg ├── og.png ├── og.svg ├── robots.txt ├── sitemap.xml ├── vercel.svg └── window.svg ├── tailwind.config.ts ├── tsconfig.json └── types └── search.ts /.env.example: -------------------------------------------------------------------------------- 1 | # Serper API Key (Google Search API) - https://serper.dev/api-key 2 | SERPER_API_KEY= 3 | 4 | # You can either use OpenAI, OpenRouter or DeepSeek as your AI provider. 5 | # OpenAI API Key (AI Provider) - https://platform.openai.com/api-keys 6 | # OpenRouter API Key (AI Provider) - https://openrouter.ai/settings/keys 7 | # DeepSeek API Key (AI Provider) - https://platform.deepseek.com/api_keys 8 | NEXT_PUBLIC_AI_PROVIDER_API_KEY= 9 | 10 | # AI Provider Base URL (AI Provider) 11 | # DeepSeek Base URL (AI Provider) - https://api.deepseek.com 12 | # OpenAI Base URL (AI Provider) - https://api.openai.com 13 | # OpenRouter Base URL (AI Provider) - https://openrouter.ai/api/v1 14 | NEXT_PUBLIC_AI_PROVIDER_BASE_URL=https://api.deepseek.com 15 | 16 | # AI Query Refiner Model (AI Provider) 17 | # DeepSeek Refiner Model (AI Provider) - deepseek-chat 18 | # OpenAI Refiner Model (AI Provider) - gpt-4o-mini 19 | # OpenRouter Refiner Model (AI Provider) - openai/gpt-4o-mini 20 | NEXT_PUBLIC_AI_REFINER_MODEL=gpt-4o-mini 21 | 22 | # AI Reasoning Model (AI Provider) 23 | # DeepSeek Reasoning Model (AI Provider) - deepseek-reasoner 24 | # OpenAI Reasoning Model (AI Provider) - gpt-4o 25 | # OpenRouter Reasoning Model (AI Provider) - openai/gpt-4o 26 | NEXT_PUBLIC_AI_REASONING_MODEL=gpt-4o 27 | 28 | # Landing Page Copy Text 29 | NEXT_PUBLIC_LANDING_PAGE_COPY_TEXT="AI-powered search with advanced reasoning capabilities" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env.local 35 | .env 36 | 37 | # vercel 38 | .vercel 39 | 40 | # typescript 41 | *.tsbuildinfo 42 | next-env.d.ts 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Malhar Ujawane 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rSearch: AI-Powered Reasoning Engine 2 | 3 | ![rSearch](https://rsearch.app/og.png) 4 | 5 | 6 | A cutting-edge research assistant powered by artificial intelligence that harnesses the advanced reasoning capabilities of [DeepSeek R1](https://huggingface.co/deepseek-ai/DeepSeek-R1) combined with comprehensive internet search functionality. **rSearch** delivers precise, well-reasoned responses to complex research queries by: 7 | 8 | - Leveraging state-of-the-art language models for refining queries 9 | - Performing intelligent web searches across multiple data sources 10 | - Synthesizing information through sophisticated chain-of-thought reasoning processes 11 | - Providing clear, actionable insights backed by reliable sources 12 | - Customizable AI models from [OpenAI](https://openai.com), [OpenRouter](https://openrouter.ai) or [DeepSeek](https://deepseek.com) 13 | - Self hosted alternative to [Perplexity](https://perplexity.ai) 14 | 15 | ## Demo 16 | 17 | 18 | ![Demo](https://github.com/Justmalhar/rsearch/raw/main/demo.png) 19 | 20 | 🔍 **Try rSearch for free** → [rsearch.app](https://rsearch.app) (Points to Beta branch) 21 | 22 | 🚀 **Get the latest features**: Clone the **beta** branch from github: 23 | 24 | ```bash 25 | git clone -b beta https://github.com/Justmalhar/rsearch.git 26 | ``` 27 | 28 | ## Overview 29 | 30 | **rSearch** leverages [DeepSeek's Reasoner - R1 model](https://huggingface.co/deepseek-ai/DeepSeek-R1) to perform Chain-of-Thought reasoning on search results. The platform first refines your query, searches the internet using [Serper.dev](https://serper.dev) API, and then applies advanced reasoning to synthesize a comprehensive, well-thought-out response. Built with [Next.js](https://nextjs.org/) and [TypeScript](https://www.typescriptlang.org/), it offers a seamless research experience while maintaining high performance and code quality. 31 | 32 | ### AI Models and Reasoning 33 | 34 | - **DeepSeek-R1**: State-of-the-art reasoning model that powers rSearch's analytical capabilities 35 | - Trained via large-scale reinforcement learning 36 | - Exceptional performance in math, code, and reasoning tasks 37 | - Comparable performance to [OpenAI-o1](https://openai.com) 38 | - Open-source availability for research and development 39 | 40 | - **Customizable AI Models**: Added support to use any AI models from [OpenAI](https://openai.com), [OpenRouter](https://openrouter.ai) or [DeepSeek](https://deepseek.com) as your AI provider. 41 | 42 | - **Chain-of-Thought Process**: 43 | 1. Query Refinement: Intelligent processing of user input 44 | 2. Internet Search: Comprehensive data gathering via [Serper.dev](https://serper.dev) API 45 | 3. Reasoning Analysis: Deep analysis using DeepSeek's reasoning capabilities 46 | 4. Response Synthesis: Well-structured, logical conclusions 47 | 48 | ## Key Features 49 | 50 | - **Multiple Search Modes**: Search across diverse content categories: 51 | - **Web Search**: Broad and efficient web search capability 52 | - **Images**: Visual content search 53 | - **Videos**: Video content discovery 54 | - **News**: Latest headlines and articles 55 | - **Places**: Local information and geographical insights 56 | - **Shopping**: Product information and price comparisons 57 | - **Scholar**: Academic content and research papers 58 | - **Patents**: Patent database search 59 | 60 | - **Modern UI Components**: 61 | - Responsive sidebar navigation 62 | - Mobile-optimized header 63 | - Dynamic search results display 64 | - Interactive loading states 65 | - Customizable themes 66 | 67 | ## Deploy 68 | 69 | Deploying **rSearch** is simple and fast with Vercel's one-click deployment option. Vercel provides a powerful and scalable environment for your project. 70 | 71 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/justmalhar/rsearch&env=SERPER_API_KEY&env=NEXT_PUBLIC_AI_PROVIDER_API_KEY&env=NEXT_PUBLIC_AI_PROVIDER_BASE_URL&env=NEXT_PUBLIC_AI_REFINER_MODEL&env=NEXT_PUBLIC_AI_REASONING_MODEL&env=NEXT_PUBLIC_LANDING_PAGE_COPY_TEXT) 72 | 73 | 74 | ## Tech Stack 75 | 76 | - **Framework**: Next.js with App Router 77 | - **Language**: TypeScript 78 | - **Styling**: TailwindCSS 79 | - **Components**: ShadCN UI components 80 | - **State Management**: React Hooks 81 | - **Code Quality**: ESLint 82 | 83 | ## Getting Started 84 | 85 | ### Prerequisites 86 | 87 | - Node.js 18.x or later 88 | - npm or yarn package manager 89 | 90 | ### Installation 91 | 92 | 1. Clone the repository: 93 | ```bash 94 | git clone https://github.com/justmalhar/rsearch.git 95 | cd rsearch 96 | ``` 97 | 98 | 2. Install dependencies: 99 | ```bash 100 | npm install 101 | # or 102 | yarn install 103 | ``` 104 | 105 | 3. Set up environment variables: 106 | ```bash 107 | cp .env.example .env.local 108 | ``` 109 | 110 | Create a `.env.local` file in the root directory with the following variables: 111 | 112 | ```bash 113 | # Serper API Key (Google Search API) - https://serper.dev/api-key 114 | SERPER_API_KEY= 115 | 116 | # You can either use OpenAI, OpenRouter or DeepSeek as your AI provider. 117 | # OpenAI API Key (AI Provider) - https://platform.openai.com/api-keys 118 | # OpenRouter API Key (AI Provider) - https://openrouter.ai/settings/keys 119 | # DeepSeek API Key (AI Provider) - https://platform.deepseek.com/api_keys 120 | NEXT_PUBLIC_AI_PROVIDER_API_KEY= 121 | 122 | # AI Provider Base URL (AI Provider) 123 | # DeepSeek Base URL (AI Provider) - https://api.deepseek.com 124 | # OpenAI Base URL (AI Provider) - https://api.openai.com 125 | # OpenRouter Base URL (AI Provider) - https://openrouter.ai/api/v1 126 | NEXT_PUBLIC_AI_PROVIDER_BASE_URL=https://api.deepseek.com 127 | 128 | # AI Query Refiner Model (AI Provider) 129 | # DeepSeek Refiner Model (AI Provider) - deepseek-chat 130 | # OpenAI Refiner Model (AI Provider) - gpt-4o-mini 131 | # OpenRouter Refiner Model (AI Provider) - openai/gpt-4o-mini 132 | NEXT_PUBLIC_AI_REFINER_MODEL=gpt-4o-mini 133 | 134 | # AI Reasoning Model (AI Provider) 135 | # DeepSeek Reasoning Model (AI Provider) - deepseek-reasoner 136 | # OpenAI Reasoning Model (AI Provider) - gpt-4o 137 | # OpenRouter Reasoning Model (AI Provider) - openai/gpt-4o 138 | NEXT_PUBLIC_AI_REASONING_MODEL=gpt-4o 139 | 140 | # Landing Page Copy Text 141 | NEXT_PUBLIC_LANDING_PAGE_COPY_TEXT="AI-powered search with advanced reasoning capabilities" 142 | ``` 143 | 144 | 5. Start the development server: 145 | ```bash 146 | npm run dev 147 | # or 148 | yarn dev 149 | ``` 150 | 151 | The application will be available at `http://localhost:3000` 152 | 153 | ## Development Commands 154 | 155 | - `npm run dev`: Start development server 156 | - `npm run build`: Create production build 157 | - `npm run start`: Start production server 158 | - `npm run lint`: Run ESLint for code quality checks 159 | 160 | ## Project Structure 161 | 162 | ``` 163 | rsearch/ 164 | ├── app/ # Next.js app directory 165 | │ ├── api/ # API routes 166 | │ ├── layout.tsx # Root layout 167 | │ ├── page.tsx # Home page 168 | │ └── rSearch/ # Search page 169 | ├── components/ # React components 170 | │ ├── rSearch/ # Search-specific components 171 | │ └── ui/ # Reusable ShadCN UI components 172 | ├── hooks/ # Custom React hooks 173 | ├── lib/ # Utility functions 174 | ├── public/ # Static assets 175 | └── types/ # TypeScript type definitions 176 | ``` 177 | 178 | ## Contributing 179 | 180 | 1. Fork the repository 181 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 182 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 183 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 184 | 5. Open a Pull Request 185 | 186 | ## License 187 | 188 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 189 | 190 | ## Acknowledgments 191 | 192 | - Built with [Next.js](https://nextjs.org/) 193 | - Powered by [DeepSeek-R1](https://huggingface.co/deepseek-ai/DeepSeek-R1) 194 | - Search capabilities by [Serper.dev](https://serper.dev) 195 | - Styled with [TailwindCSS](https://tailwindcss.com/) 196 | - Icons from various sources including custom SVGs 197 | 198 | 199 | ## Stay Connected 200 | - **Twitter/X**: [@justmalhar](https://twitter.com/justmalhar) 🛠 201 | - **LinkedIn**: [Malhar Ujawane](https://linkedin.com/in/justmalhar) 💻 202 | - **GitHub**: [justmalhar](https://github.com/justmalhar) 💻 203 | -------------------------------------------------------------------------------- /app/[page]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation"; 2 | import staticData from "@/data/static.json"; 3 | import { Logo } from "@/components/ui/logo"; 4 | 5 | type StaticPage = "terms" | "privacy" | "about"; 6 | 7 | export async function generateStaticParams() { 8 | return [ 9 | { page: "terms" }, 10 | { page: "privacy" }, 11 | { page: "about" }, 12 | ]; 13 | } 14 | 15 | export default async function StaticPage({ 16 | params 17 | }: { 18 | params: Promise<{ page: StaticPage }> 19 | }) { 20 | const resolvedParams = await params; 21 | const pageData = staticData[resolvedParams.page]; 22 | 23 | if (!pageData) { 24 | notFound(); 25 | } 26 | 27 | return ( 28 |
29 |
30 |
31 | 32 |
33 |
34 | 35 |
36 |

37 | {pageData.title} 38 |

39 | 40 | {"lastUpdated" in pageData && ( 41 |

42 | Last updated: {pageData.lastUpdated} 43 |

44 | )} 45 | 46 |
47 | {pageData.sections.map((section: { heading: string; content: string }) => ( 48 |
49 |

50 | {section.heading} 51 |

52 |

53 | {section.content} 54 |

55 |
56 | ))} 57 |
58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /app/api/og-image/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | async function fetchOGImage(url: string): Promise { 4 | try { 5 | const response = await fetch(url); 6 | const html = await response.text(); 7 | 8 | // Extract og:image meta tag 9 | const ogImageMatch = html.match(/]*property=["']og:image["'][^>]*content=["']([^"']+)["'][^>]*>/i) 10 | ?? html.match(/]*content=["']([^"']+)["'][^>]*property=["']og:image["'][^>]*>/i); 11 | 12 | if (ogImageMatch?.[1]) { 13 | return ogImageMatch[1]; 14 | } 15 | 16 | // Fallback to twitter:image 17 | const twitterImageMatch = html.match(/]*name=["']twitter:image["'][^>]*content=["']([^"']+)["'][^>]*>/i) 18 | ?? html.match(/]*content=["']([^"']+)["'][^>]*name=["']twitter:image["'][^>]*>/i); 19 | 20 | if (twitterImageMatch?.[1]) { 21 | return twitterImageMatch[1]; 22 | } 23 | 24 | return null; 25 | } catch (error) { 26 | console.error('Error fetching OG image:', error); 27 | return null; 28 | } 29 | } 30 | 31 | export async function GET(request: Request) { 32 | const { searchParams } = new URL(request.url); 33 | const url = searchParams.get('url'); 34 | 35 | if (!url) { 36 | return NextResponse.json({ error: 'URL parameter is required' }, { status: 400 }); 37 | } 38 | 39 | try { 40 | const ogImage = await fetchOGImage(url); 41 | 42 | if (!ogImage) { 43 | return NextResponse.json({ error: 'No OG image found' }, { status: 404 }); 44 | } 45 | 46 | return NextResponse.json({ url: ogImage }); 47 | } catch { 48 | return NextResponse.json({ error: 'Failed to fetch OG image' }, { status: 500 }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/api/query/route.ts: -------------------------------------------------------------------------------- 1 | import { refineSearchQueryPrompt } from '@/lib/prompts'; 2 | import OpenAI from 'openai'; 3 | import { z } from 'zod'; 4 | 5 | const openai = new OpenAI({ 6 | apiKey: process.env.NEXT_PUBLIC_AI_PROVIDER_API_KEY, 7 | baseURL: process.env.NEXT_PUBLIC_AI_PROVIDER_BASE_URL, 8 | }); 9 | 10 | export const runtime = 'edge'; 11 | export const dynamic = 'force-dynamic'; 12 | export const revalidate = 0; 13 | 14 | const RefinedSearchSchema = z.object({ 15 | refined_query: z.string(), 16 | explanation: z.string(), 17 | }); 18 | 19 | export async function POST(req: Request) { 20 | try { 21 | const { searchTerm, mode } = await req.json(); 22 | 23 | const currentDate = new Date().toISOString().split('T')[0]; 24 | 25 | const prompt = refineSearchQueryPrompt(searchTerm, mode, currentDate); 26 | 27 | const model = process.env.NEXT_PUBLIC_AI_REFINER_MODEL; 28 | 29 | const response = await openai.chat.completions.create({ 30 | model: model as string, 31 | messages: [ 32 | { role: 'system', content: prompt }, 33 | { role: 'user', content: searchTerm } 34 | ], 35 | response_format: { type: "json_object" }, 36 | temperature: 0.6, 37 | max_tokens: 500, 38 | }); 39 | 40 | const content = response.choices[0].message.content; 41 | if (!content) { 42 | throw new Error('No content received from OpenAI'); 43 | } 44 | 45 | const refinedSearch = JSON.parse(content); 46 | 47 | // Validate the response against our schema 48 | const validatedResponse = RefinedSearchSchema.parse(refinedSearch); 49 | 50 | return new Response(JSON.stringify(validatedResponse), { 51 | headers: { 'Content-Type': 'application/json' }, 52 | }); 53 | 54 | } catch (error) { 55 | console.error('Search refinement error:', error); 56 | return new Response( 57 | JSON.stringify({ 58 | error: 'Failed to refine search query', 59 | details: error instanceof Error ? error.message : 'Unknown error' 60 | }), 61 | { 62 | status: 500, 63 | headers: { 'Content-Type': 'application/json' }, 64 | } 65 | ); 66 | } 67 | } -------------------------------------------------------------------------------- /app/api/rsearch/route.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | import type { 3 | SerperResponse, 4 | WebSearchResult, 5 | ImageSearchResult, 6 | VideoSearchResult, 7 | NewsSearchResult, 8 | ShoppingSearchResult 9 | } from '@/types/search'; 10 | import { rSearchPrompt } from '@/lib/prompts'; 11 | 12 | const openai = new OpenAI({ 13 | apiKey: process.env.NEXT_PUBLIC_AI_PROVIDER_API_KEY, 14 | baseURL: process.env.NEXT_PUBLIC_AI_PROVIDER_BASE_URL, 15 | }); 16 | 17 | 18 | // Make sure to export these properly for Next.js API routes 19 | export const runtime = 'edge'; 20 | export const dynamic = 'force-dynamic'; 21 | export const revalidate = 0; 22 | 23 | export async function POST(req: Request) { 24 | try { 25 | const { 26 | searchTerm, 27 | searchResults, 28 | mode, 29 | refinedQuery 30 | }: { 31 | searchTerm: string; 32 | searchResults: SerperResponse; 33 | mode: string; 34 | refinedQuery?: { 35 | query: string; 36 | explanation: string; 37 | }; 38 | } = await req.json(); 39 | 40 | // Create a comprehensive context from search results 41 | let context = ''; 42 | 43 | // Add knowledge graph info if available 44 | if (searchResults.knowledgeGraph) { 45 | const kg = searchResults.knowledgeGraph; 46 | context += `### Knowledge Graph\nTitle: ${kg.title}\nType: ${kg.type}${kg.description ? `\nDescription: ${kg.description}` : ''}\n`; 47 | if (kg.attributes) { 48 | context += 'Attributes:\n'; 49 | for (const [key, value] of Object.entries(kg.attributes)) { 50 | context += `- ${key}: ${value}\n`; 51 | } 52 | } 53 | // Add any images from knowledge graph 54 | if (kg.images?.length) { 55 | context += 'Images:\n'; 56 | for (const image of kg.images) { 57 | context += `- ${image.title || 'Image'}: ${image.imageUrl}\n`; 58 | } 59 | } 60 | context += '\n'; 61 | } 62 | 63 | // Add organic search results 64 | if (searchResults.organic?.length) { 65 | context += '### Organic Results\n'; 66 | context += searchResults.organic.map((result: WebSearchResult, index: number) => { 67 | return `[${index + 1}] ${result.title} 68 | Source: ${result.link} 69 | ${result.snippet} 70 | ${result.date ? `Date: ${result.date}\n` : ''}${result.attributes ? `Attributes: 71 | ${Object.entries(result.attributes).map(([key, value]) => `- ${key}: ${value}`).join('\n')}\n` : ''}${result.imageUrl ? `Image: ${result.imageUrl}\n` : ''}${result.thumbnailUrl ? `Thumbnail: ${result.thumbnailUrl}\n` : ''}\n`; 72 | }).join(''); 73 | } 74 | 75 | // Add top stories if available 76 | if (searchResults.news?.length) { 77 | context += '### Top Stories\n'; 78 | context += searchResults.news.map((story: NewsSearchResult, index: number) => { 79 | return `[${index + 1}] ${story.title} 80 | Source: ${story.source} 81 | Link: ${story.link} 82 | ${story.date ? `Date: ${story.date}\n` : ''}${story.imageUrl ? `Image: ${story.imageUrl}\n` : ''}${story.snippet ? `Summary: ${story.snippet}\n` : ''}\n`; 83 | }).join(''); 84 | } 85 | 86 | // Add people also ask if available 87 | if (searchResults.peopleAlsoAsk?.length) { 88 | context += '### People Also Ask\n'; 89 | context += searchResults.peopleAlsoAsk.map((item: { question: string; snippet: string; link: string; title?: string }, index: number) => { 90 | return `[${index + 1}] Q: ${item.question} 91 | A: ${item.snippet} 92 | Source: ${item.link} 93 | ${item.title ? `Title: ${item.title}\n` : ''}\n`; 94 | }).join(''); 95 | } 96 | 97 | // Add related searches if available 98 | if (searchResults.relatedSearches?.length) { 99 | context += '### Related Searches\n'; 100 | context += searchResults.relatedSearches.map((item: { query: string }, index: number) => 101 | `[${index + 1}] ${item.query}\n` 102 | ).join(''); 103 | context += '\n'; 104 | } 105 | 106 | // Add images section if available 107 | if (searchResults.images?.length) { 108 | context += '### Images\n'; 109 | context += searchResults.images.map((image: ImageSearchResult, index: number) => { 110 | return `[${index + 1}] ${image.title || 'Image'} 111 | URL: ${image.imageUrl} 112 | ${image.source ? `Source: ${image.source}\n` : ''}\n`; 113 | }).join(''); 114 | } 115 | 116 | // Add shopping results if available 117 | if (searchResults.shopping?.length) { 118 | context += '### Shopping Results\n'; 119 | context += searchResults.shopping.map((item: ShoppingSearchResult, index: number) => { 120 | return `[${index + 1}] ${item.title} 121 | Price: ${item.price || 'N/A'} 122 | ${item.rating ? `Rating: ${item.rating}\n` : ''}${item.source ? `Source: ${item.source}\n` : ''}${item.link ? `Link: ${item.link}\n` : ''}${item.imageUrl ? `Image: ${item.imageUrl}\n` : ''}\n`; 123 | }).join(''); 124 | } 125 | 126 | // Add videos section if available 127 | if (searchResults.videos?.length) { 128 | context += '### Videos\n'; 129 | context += searchResults.videos.map((video: VideoSearchResult, index: number) => { 130 | return `[${index + 1}] ${video.title} 131 | Link: ${video.link} 132 | ${video.date ? `Date: ${video.date}\n` : ''}${video.duration ? `Duration: ${video.duration}\n` : ''}${video.imageUrl ? `Image: ${video.imageUrl}\n` : ''}\n`; 133 | }).join(''); 134 | } 135 | 136 | const currentDate = new Date().toISOString().split('T')[0]; 137 | 138 | // Include mode and refined query in context 139 | let searchContext = ''; 140 | if (refinedQuery) { 141 | searchContext += `### Search Context\nOriginal Query: ${searchTerm}\nRefined Query: ${refinedQuery.query}\nRefinement Explanation: ${refinedQuery.explanation}\nSearch Mode: ${mode}\n\n`; 142 | } else { 143 | searchContext += `### Search Context\nQuery: ${searchTerm}\nSearch Mode: ${mode}\n\n`; 144 | } 145 | 146 | const prompt = rSearchPrompt(searchTerm, searchContext + context, currentDate); 147 | 148 | const model = process.env.NEXT_PUBLIC_AI_REASONING_MODEL; 149 | 150 | 151 | const response = await openai.chat.completions.create({ 152 | model: model as string, // Make sure to use the correct model name 153 | messages: [ 154 | { role: "system", content: prompt }, 155 | { role: 'user', content: searchTerm } 156 | ], 157 | stream: true, 158 | }); 159 | 160 | // Create a streaming response 161 | const stream = new ReadableStream({ 162 | async start(controller) { 163 | try { 164 | for await (const chunk of response) { 165 | const newChunk = chunk.choices[0]?.delta; 166 | 167 | // Send the entire chunk object as JSON 168 | controller.enqueue(new TextEncoder().encode(`${JSON.stringify(newChunk)}\n`)); 169 | } 170 | controller.close(); 171 | } catch (error) { 172 | controller.error(error); 173 | } 174 | }, 175 | }); 176 | 177 | return new Response(stream, { 178 | headers: { 179 | 'Content-Type': 'text/plain; charset=utf-8', 180 | 'Cache-Control': 'no-cache', 181 | }, 182 | }); 183 | } catch (error) { 184 | console.error('OpenAI API error:', error); 185 | return new Response( 186 | JSON.stringify({ 187 | error: 'Failed to generate AI response', 188 | details: error instanceof Error ? error.message : 'Unknown error' 189 | }), 190 | { 191 | status: 500, 192 | headers: { 'Content-Type': 'application/json' }, 193 | } 194 | ); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer base { 10 | :root { 11 | --background: 0 0% 100%; 12 | --foreground: 0 0% 3.9%; 13 | --card: 0 0% 100%; 14 | --card-foreground: 0 0% 3.9%; 15 | --popover: 0 0% 100%; 16 | --popover-foreground: 0 0% 3.9%; 17 | --primary: 0 0% 9%; 18 | --primary-foreground: 0 0% 98%; 19 | --secondary: 0 0% 96.1%; 20 | --secondary-foreground: 0 0% 9%; 21 | --muted: 0 0% 96.1%; 22 | --muted-foreground: 0 0% 45.1%; 23 | --accent: 0 0% 96.1%; 24 | --accent-foreground: 0 0% 9%; 25 | --destructive: 0 84.2% 60.2%; 26 | --destructive-foreground: 0 0% 98%; 27 | --border: 0 0% 89.8%; 28 | --input: 0 0% 89.8%; 29 | --ring: 0 0% 3.9%; 30 | --chart-1: 12 76% 61%; 31 | --chart-2: 173 58% 39%; 32 | --chart-3: 197 37% 24%; 33 | --chart-4: 43 74% 66%; 34 | --chart-5: 27 87% 67%; 35 | --radius: 0.5rem; 36 | --sidebar-background: 0 0% 98%; 37 | --sidebar-foreground: 240 5.3% 26.1%; 38 | --sidebar-primary: 240 5.9% 10%; 39 | --sidebar-primary-foreground: 0 0% 98%; 40 | --sidebar-accent: 240 4.8% 95.9%; 41 | --sidebar-accent-foreground: 240 5.9% 10%; 42 | --sidebar-border: 220 13% 91%; 43 | --sidebar-ring: 217.2 91.2% 59.8%; 44 | } 45 | .dark { 46 | --background: 0 0% 3.9%; 47 | --foreground: 0 0% 98%; 48 | --card: 0 0% 3.9%; 49 | --card-foreground: 0 0% 98%; 50 | --popover: 0 0% 3.9%; 51 | --popover-foreground: 0 0% 98%; 52 | --primary: 0 0% 98%; 53 | --primary-foreground: 0 0% 9%; 54 | --secondary: 0 0% 14.9%; 55 | --secondary-foreground: 0 0% 98%; 56 | --muted: 0 0% 14.9%; 57 | --muted-foreground: 0 0% 63.9%; 58 | --accent: 0 0% 14.9%; 59 | --accent-foreground: 0 0% 98%; 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 0 0% 98%; 62 | --border: 0 0% 14.9%; 63 | --input: 0 0% 14.9%; 64 | --ring: 0 0% 83.1%; 65 | --chart-1: 220 70% 50%; 66 | --chart-2: 160 60% 45%; 67 | --chart-3: 30 80% 55%; 68 | --chart-4: 280 65% 60%; 69 | --chart-5: 340 75% 55%; 70 | --sidebar-background: 240 5.9% 10%; 71 | --sidebar-foreground: 240 4.8% 95.9%; 72 | --sidebar-primary: 224.3 76.3% 48%; 73 | --sidebar-primary-foreground: 0 0% 100%; 74 | --sidebar-accent: 240 3.7% 15.9%; 75 | --sidebar-accent-foreground: 240 4.8% 95.9%; 76 | --sidebar-border: 240 3.7% 15.9%; 77 | --sidebar-ring: 217.2 91.2% 59.8%; 78 | } 79 | } 80 | 81 | @layer base { 82 | * { 83 | @apply border-border; 84 | } 85 | body { 86 | @apply bg-background text-foreground; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import { Sidebar } from "@/components/ui/sidebar"; 5 | import { MobileHeader } from "@/components/ui/mobile-header"; 6 | import { Analytics } from '@vercel/analytics/next'; 7 | 8 | const inter = Inter({ subsets: ["latin"] }); 9 | export const metadata: Metadata = { 10 | title: "rSearch: AI-Powered Reasoning Engine", 11 | description: "A cutting-edge reasoning engine powered by artificial intelligence that harnesses advanced reasoning capabilities combined with comprehensive internet search functionality.", 12 | applicationName: "rSearch", 13 | authors: [{ name: "Malhar Ujawane", url: "https://twitter.com/justmalhar" }], 14 | keywords: ["rsearch", "deepseek", "reasoning engine", "AI search", "semantic search", "research assistant", "AI powered search", "deepseek coder", "deepseek chat", "deepseek coder 2", "deepseek chat 2"], 15 | creator: "Malhar Ujawane", 16 | publisher: "rSearch", 17 | metadataBase: new URL('https://rsearch.app'), 18 | alternates: { 19 | canonical: 'https://rsearch.app', 20 | }, 21 | robots: { 22 | index: true, 23 | follow: true, 24 | googleBot: { 25 | index: true, 26 | follow: true, 27 | 'max-video-preview': -1, 28 | 'max-image-preview': 'large', 29 | 'max-snippet': -1, 30 | }, 31 | }, 32 | openGraph: { 33 | title: "rSearch: AI-Powered Reasoning Engine", 34 | description: "Discover Insights, Not Just Results. AI-powered reasoning engine that thinks just like you do.", 35 | url: 'https://rsearch.app', 36 | siteName: 'rSearch', 37 | images: [ 38 | { 39 | url: '/og.png', 40 | width: 1200, 41 | height: 630, 42 | alt: 'rSearch - AI-Powered Research Assistant' 43 | } 44 | ], 45 | locale: 'en_US', 46 | type: 'website', 47 | }, 48 | twitter: { 49 | card: 'summary_large_image', 50 | title: 'rSearch: AI-Powered Research Assistant', 51 | description: 'Discover Insights, Not Just Results. AI-powered reasoning engine that thinks just like you do.', 52 | creator: '@justmalhar', 53 | site: '@justmalhar', 54 | images: ['/og.png'], 55 | }, 56 | icons: { 57 | icon: { 58 | url: `data:image/svg+xml,${encodeURIComponent(``)}`, 59 | type: "image/svg+xml" 60 | }, 61 | shortcut: '/favicon.ico', 62 | apple: '/apple-touch-icon.png', 63 | }, 64 | category: 'technology', 65 | }; 66 | 67 | export default function RootLayout({ 68 | children, 69 | }: { 70 | children: React.ReactNode; 71 | }) { 72 | return ( 73 | 74 | 75 | {/* Twitter Meta Tags */} 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | {/* Open Graph Meta Tags */} 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 |
97 | 98 |
99 | 100 |
101 | {children} 102 |
103 |
104 |
105 | 106 | 107 | 108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /app/settings/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 4 | import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; 5 | import { Label } from "@/components/ui/label"; 6 | import { Settings, Search, ChevronDown } from "lucide-react"; 7 | import { Button } from "@/components/ui/button"; 8 | import { useEffect, useState } from "react"; 9 | import { Switch } from "@/components/ui/switch"; 10 | 11 | const aiProviders = [ 12 | { 13 | id: "deepseek", 14 | name: "DeepSeek", 15 | description: "Use DeepSeek's language models deepseek-chat and deepseek-reasoner with reasoning", 16 | disabled: false 17 | }, 18 | { 19 | id: "openai", 20 | name: "OpenAI", 21 | description: "Coming soon - o1 and o1-mini reasoning mode not available", 22 | disabled: true 23 | }, 24 | { 25 | id: "openrouter", 26 | name: "OpenRouter", 27 | description: "Coming soon - Access multiple AI models through OpenRouter", 28 | disabled: true 29 | } 30 | ]; 31 | 32 | const searchProviders = [ 33 | { 34 | id: "serper", 35 | name: "Serper.dev", 36 | description: "Google Search API via Serper.dev", 37 | disabled: false 38 | }, 39 | { 40 | id: "tavily", 41 | name: "Tavily", 42 | description: "Coming soon", 43 | disabled: true 44 | }, 45 | { 46 | id: "searxng", 47 | name: "SearXNG", 48 | description: "Coming soon", 49 | disabled: true 50 | } 51 | ]; 52 | 53 | export default function SettingsPage() { 54 | const [selectedAIProvider, setSelectedAIProvider] = useState("deepseek"); 55 | const [selectedSearchProvider, setSelectedSearchProvider] = useState("serper"); 56 | const [autoExpandSections, setAutoExpandSections] = useState(true); 57 | const [isSaved, setIsSaved] = useState(false); 58 | 59 | useEffect(() => { 60 | // Load saved settings on mount 61 | const savedSettings = localStorage.getItem("rSearch_settings"); 62 | if (savedSettings) { 63 | const settings = JSON.parse(savedSettings); 64 | setSelectedAIProvider(settings.aiProvider); 65 | setSelectedSearchProvider(settings.searchProvider); 66 | setAutoExpandSections(settings.autoExpandSections ?? true); 67 | } 68 | }, []); 69 | 70 | const handleSave = () => { 71 | const settings = { 72 | aiProvider: selectedAIProvider, 73 | searchProvider: selectedSearchProvider, 74 | autoExpandSections 75 | }; 76 | localStorage.setItem("rSearch_settings", JSON.stringify(settings)); 77 | setIsSaved(true); 78 | setTimeout(() => setIsSaved(false), 2000); 79 | }; 80 | 81 | return ( 82 |
83 |
84 |
85 |

Settings

86 |

Configure your search preferences

87 |
88 | 89 |
90 | 91 | 92 |
93 | 94 | Display Settings 95 |
96 |
97 | 98 |
99 | 105 | 111 |
112 |
113 |
114 | 115 | 116 | 117 |
118 | 119 | AI Provider 120 |
121 |
122 | 123 | 128 | {aiProviders.map((provider) => ( 129 |
130 | 136 | 147 |
148 | ))} 149 |
150 |
151 |
152 | 153 | 154 | 155 |
156 | 157 | Search Provider 158 |
159 |
160 | 161 | 166 | {searchProviders.map((provider) => ( 167 |
168 | 174 | 185 |
186 | ))} 187 |
188 |
189 |
190 | 191 |
192 | 198 |
199 |
200 |
201 |
202 | ); 203 | } 204 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /components/rSearch/query.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | Globe, BookText, Video, ShoppingBag, MapPin, Newspaper, GraduationCap, Lightbulb 4 | } from "lucide-react"; 5 | 6 | 7 | 8 | interface QueryProps { 9 | searchTerm: string; 10 | mode?: string; 11 | } 12 | 13 | export default function Query({ searchTerm, mode = 'web' }: QueryProps) { 14 | const searchModes = { 15 | web: Globe, 16 | images: BookText, 17 | videos: Video, 18 | news: Newspaper, 19 | places: MapPin, 20 | shopping: ShoppingBag, 21 | scholar: GraduationCap, 22 | patents: Lightbulb 23 | }; 24 | 25 | const Icon = searchModes[mode as keyof typeof searchModes] || Globe; 26 | 27 | return ( 28 |
29 |
30 | 31 |

32 | {searchTerm} 33 |

34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /components/rSearch/sources-sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { SearchResult, PlaceSearchResult } from '@/types/search'; 2 | 3 | interface SourcesSidebarProps { 4 | showSidebar: boolean; 5 | sources: SearchResult[]; 6 | getWebsiteName: (url: string) => string; 7 | } 8 | 9 | const isPlaceResult = (source: SearchResult): source is PlaceSearchResult => 10 | 'address' in source; 11 | 12 | export default function SourcesSidebar({ showSidebar, sources, getWebsiteName }: SourcesSidebarProps) { 13 | const getSourceUrl = (source: SearchResult): string => { 14 | if (isPlaceResult(source)) { 15 | return source.website || ''; 16 | } 17 | return source.link; 18 | }; 19 | return ( 20 |
21 |
22 |
23 |
24 |

25 | Sources 26 |

27 | 28 | {sources.length} 29 | 30 |
31 |
32 | 33 | 101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /components/rSearch/sources/sources-academic.tsx: -------------------------------------------------------------------------------- 1 | import { AcademicSearchResult } from '@/types/search'; 2 | 3 | interface AcademicSourcesProps { 4 | sources: AcademicSearchResult[]; 5 | displayCount: number; 6 | } 7 | 8 | export default function AcademicSources({ sources, displayCount }: AcademicSourcesProps) { 9 | return ( 10 |
11 | {sources.slice(0, displayCount).map((source) => ( 12 |
13 |

{source.title}

14 |

{source.snippet}

15 | {source.authors && ( 16 |

Authors: {source.authors.join(', ')}

17 | )} 18 |
19 | ))} 20 |
21 | ); 22 | } -------------------------------------------------------------------------------- /components/rSearch/sources/sources-images.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { ImageSearchResult } from '@/types/search'; 4 | import Image from 'next/image'; 5 | import { 6 | Carousel, 7 | CarouselContent, 8 | CarouselItem, 9 | CarouselNext, 10 | CarouselPrevious, 11 | } from "@/components/ui/carousel"; 12 | 13 | interface ImageSourcesProps { 14 | sources: ImageSearchResult[]; 15 | displayCount: number; 16 | } 17 | 18 | export default function ImageSources({ sources, displayCount }: ImageSourcesProps) { 19 | return ( 20 |
21 | 26 | 27 | {sources.slice(0, displayCount).map((source) => ( 28 | 29 | 35 |
36 | {source.title} 43 |
44 |
45 |

{source.title}

46 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /components/rSearch/sources/sources-knowledge.tsx: -------------------------------------------------------------------------------- 1 | import type { SerperResponse } from '@/types/search'; 2 | 3 | interface KnowledgeGraphProps { 4 | data: SerperResponse['knowledgeGraph']; 5 | } 6 | 7 | export default function KnowledgeGraph({ data }: KnowledgeGraphProps) { 8 | if (!data) return null; 9 | 10 | return ( 11 |
12 |
13 |
14 |
15 |
16 | {/* Image Column */} 17 | {data.imageUrl && ( 18 |
19 | {data.title} 24 |
25 | )} 26 | 27 | {/* Content Column */} 28 |
29 | {/* Header */} 30 |
31 |

32 | {data.title} 33 |

34 |

35 | {data.type} 36 |

37 |
38 | 39 | {/* Attributes Grid */} 40 | {data.attributes && Object.entries(data.attributes).length > 0 && ( 41 |
42 | {Object.entries(data.attributes).map(([key, value]) => ( 43 |
47 | 48 | {key} 49 | 50 | 51 | {value} 52 | 53 |
54 | ))} 55 |
56 | )} 57 |
58 |
59 |
60 |
61 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /components/rSearch/sources/sources-news.tsx: -------------------------------------------------------------------------------- 1 | import type { NewsSearchResult } from '@/types/search'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | interface NewsSourcesProps { 5 | sources: NewsSearchResult[]; 6 | displayCount: number; 7 | } 8 | 9 | export default function NewsSources({ sources, displayCount }: NewsSourcesProps) { 10 | const [imageUrls, setImageUrls] = useState>({}); 11 | 12 | useEffect(() => { 13 | const fetchOGImages = async () => { 14 | const newImageUrls: Record = {}; 15 | 16 | for (const source of sources) { 17 | if (!source.imageUrl || source.imageUrl.includes('favicon')) { 18 | try { 19 | const response = await fetch(`/api/og-image?url=${encodeURIComponent(source.link)}`); 20 | if (response.ok) { 21 | const data = await response.json(); 22 | if (data.url) { 23 | newImageUrls[source.link] = data.url; 24 | } 25 | } 26 | } catch (error) { 27 | console.error('Error fetching OG image:', error); 28 | } 29 | } 30 | } 31 | 32 | setImageUrls(newImageUrls); 33 | }; 34 | 35 | fetchOGImages(); 36 | }, [sources]); 37 | 38 | return ( 39 |
40 | {sources.slice(0, displayCount).map((source, index) => ( 41 |
42 | 48 | 112 | ); 113 | } 114 | -------------------------------------------------------------------------------- /components/rSearch/sources/sources-patents.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { AcademicSearchResult } from '@/types/search'; 3 | 4 | interface PatentSourcesProps { 5 | sources: (AcademicSearchResult & { 6 | priorityDate?: string; 7 | filingDate?: string; 8 | grantDate?: string; 9 | publicationDate?: string; 10 | inventor?: string; 11 | assignee?: string; 12 | publicationNumber?: string; 13 | figures?: Array<{ 14 | imageUrl: string; 15 | thumbnailUrl: string; 16 | }>; 17 | })[]; 18 | displayCount?: number; 19 | } 20 | 21 | export default function PatentSources({ sources, displayCount = 10 }: PatentSourcesProps) { 22 | 23 | return ( 24 |
25 |
26 | {sources.slice(0, displayCount).map((source) => ( 27 |
28 | {/* Title and Link */} 29 |
30 |
31 |
32 |
33 | {`Favicon 40 |
41 |
42 |
43 |
44 | 50 | {source.title} 51 | 52 | 53 | {/* Patent Info */} 54 |
55 | {source.publicationNumber && ( 56 | {source.publicationNumber} 57 | )} 58 | {source.inventor && ( 59 | <> 60 | 61 | Inventor: {source.inventor} 62 | 63 | )} 64 | {source.assignee && ( 65 | <> 66 | 67 | Assignee: {source.assignee} 68 | 69 | )} 70 |
71 | 72 | {/* Dates */} 73 |
74 | {source.priorityDate && ( 75 | Priority: {new Date(source.priorityDate).toLocaleDateString()} 76 | )} 77 | {source.filingDate && ( 78 | <> 79 | 80 | Filed: {new Date(source.filingDate).toLocaleDateString()} 81 | 82 | )} 83 | {source.grantDate && ( 84 | <> 85 | 86 | Granted: {new Date(source.grantDate).toLocaleDateString()} 87 | 88 | )} 89 |
90 | 91 | {/* Snippet/Abstract */} 92 | {source.snippet && ( 93 |

94 | {source.snippet} 95 |

96 | )} 97 | 98 | {/* Patent Figures */} 99 | {source.figures && source.figures.length > 0 && ( 100 |
101 | {source.figures.slice(0, 5).map((figure, index) => ( 102 | 109 | {`Patent 114 | 115 | ))} 116 | {source.figures.length > 5 && ( 117 |
118 | +{source.figures.length - 5} more 119 |
120 | )} 121 |
122 | )} 123 | 124 | {/* PDF Link if available */} 125 | {'pdfUrl' in source && source.pdfUrl && ( 126 | 153 | )} 154 |
155 |
156 |
157 | ))} 158 |
159 |
160 | ); 161 | } 162 | -------------------------------------------------------------------------------- /components/rSearch/sources/sources-places.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { PlaceSearchResult } from '@/types/search'; 4 | import { MapPin, Phone, Star } from 'lucide-react'; 5 | import { 6 | Carousel, 7 | CarouselContent, 8 | CarouselItem, 9 | CarouselNext, 10 | CarouselPrevious, 11 | } from "@/components/ui/carousel"; 12 | 13 | interface PlaceSourcesProps { 14 | sources: PlaceSearchResult[]; 15 | displayCount: number; 16 | } 17 | 18 | export default function PlaceSources({ sources = [], displayCount }: PlaceSourcesProps) { 19 | const validSources = sources?.filter(source => 20 | source?.title && 21 | source?.address 22 | ) ?? []; 23 | 24 | return ( 25 |
26 | 33 | 34 | {!validSources.length ? ( 35 | 36 |
37 |

No place results found

38 |
39 |
40 | ) : validSources.slice(0, displayCount).map((source) => ( 41 | 42 |
43 |
44 |

{source.title}

45 |

{source.category}

46 |
47 | 48 |
49 | 50 | {source.rating} 51 | ({source.ratingCount} reviews) 52 |
53 | 54 |
55 |
56 | 57 |

{source.address}

58 |
59 | 60 | {source.phoneNumber && ( 61 | 70 | )} 71 | 72 | {source.website && ( 73 |
74 |
75 | 96 | )} 97 |
98 |
99 | 100 | ))} 101 | 102 | {validSources.length > 1 && ( 103 | <> 104 | 105 | 106 | 107 | )} 108 | 109 |
110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /components/rSearch/sources/sources-scholar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { AcademicSearchResult } from '@/types/search'; 3 | 4 | interface ScholarSourcesProps { 5 | sources: (AcademicSearchResult & { 6 | pdfUrl?: string; 7 | citations?: number; 8 | publicationInfo?: string; 9 | })[]; 10 | displayCount?: number; 11 | } 12 | 13 | export default function ScholarSources({ sources, displayCount = 10 }: ScholarSourcesProps) { 14 | 15 | return ( 16 |
17 |
18 | {sources.slice(0, displayCount).map((source) => ( 19 |
20 | {/* Title and Link */} 21 |
22 |
23 |
24 |
25 | {`Favicon 32 |
33 |
34 |
35 |
36 | 42 | {source.title} 43 | 44 | 45 | {/* Publication Info */} 46 |
47 | {source.publicationInfo ? ( 48 | {source.publicationInfo} 49 | ) : ( 50 | <> 51 | {source.authors && source.authors.length > 0 && ( 52 | <> 53 | Authors: 54 | {source.authors.join(', ')} 55 | 56 | )} 57 | {source.publisher && ( 58 | <> 59 | 60 | Publisher: 61 | {source.publisher} 62 | 63 | )} 64 | 65 | )} 66 | {source.year && ( 67 | <> 68 | 69 | Year: 70 | {source.year} 71 | 72 | )} 73 | {source.citations && ( 74 | <> 75 | 76 | Citations: 77 | {source.citations} 78 | 79 | )} 80 |
81 | 82 | {/* Snippet/Abstract */} 83 | {source.snippet && ( 84 |

85 | {source.snippet} 86 |

87 | )} 88 | 89 | {/* PDF Link if available */} 90 | {'pdfUrl' in source && source.pdfUrl && ( 91 | 118 | )} 119 |
120 |
121 |
122 | ))} 123 |
124 |
125 | ); 126 | } 127 | -------------------------------------------------------------------------------- /components/rSearch/sources/sources-search.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Justmalhar/rsearch/0515631cceadba32cc610959c3e819bf9b86e2ba/components/rSearch/sources/sources-search.tsx -------------------------------------------------------------------------------- /components/rSearch/sources/sources-shopping.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { ShoppingSearchResult } from '@/types/search'; 4 | import Image from 'next/image'; 5 | import Link from 'next/link'; 6 | import { 7 | Carousel, 8 | CarouselContent, 9 | CarouselItem, 10 | CarouselNext, 11 | CarouselPrevious, 12 | } from "@/components/ui/carousel"; 13 | 14 | interface ShoppingSourcesProps { 15 | sources: ShoppingSearchResult[]; 16 | displayCount: number; 17 | } 18 | 19 | export default function ShoppingSources({ sources, displayCount }: ShoppingSourcesProps) { 20 | const displayedSources = sources.slice(0, displayCount); 21 | 22 | return ( 23 |
24 | 31 | 32 | {!displayedSources.length ? ( 33 | 34 |
35 |

No shopping results found

36 |
37 |
38 | ) : displayedSources.map((source) => ( 39 | 40 | 45 |
46 |
47 |
48 | {source.imageUrl && ( 49 | {source.title} 56 | )} 57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | 71 |
72 |
73 |
74 | {source.source} 75 |
76 | 77 |

78 | {source.title} 79 |

80 |
81 | 82 |
83 |
84 | 85 | {source.price} 86 | 87 | {source.delivery && ( 88 | 89 | {source.delivery} 90 | 91 | )} 92 |
93 | 94 |
95 | {source.rating && ( 96 |
97 | 98 | 99 | {source.rating} 100 | {source.ratingCount && ( 101 | 102 | ({source.ratingCount}) 103 | 104 | )} 105 | 106 |
107 | )} 108 | {source.offers && ( 109 | 110 | {source.offers} offers 111 | 112 | )} 113 |
114 |
115 |
116 |
117 |
118 | 119 | 120 | ))} 121 | 122 | {displayedSources.length > 1 && ( 123 | <> 124 | 125 | 126 | 127 | )} 128 | 129 |
130 | ); 131 | } 132 | -------------------------------------------------------------------------------- /components/rSearch/sources/sources-videos.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { VideoSearchResult } from '@/types/search'; 4 | import Image from 'next/image'; 5 | import { 6 | Carousel, 7 | CarouselContent, 8 | CarouselItem, 9 | CarouselNext, 10 | CarouselPrevious, 11 | } from "@/components/ui/carousel"; 12 | 13 | interface VideoSourcesProps { 14 | sources: VideoSearchResult[]; 15 | displayCount: number; 16 | } 17 | 18 | export default function VideoSources({ sources = [], displayCount }: VideoSourcesProps) { 19 | const validSources = sources?.filter(source => 20 | source?.link && 21 | source?.title 22 | ) ?? []; 23 | 24 | return ( 25 |
26 | 33 | 34 | {!validSources.length ? ( 35 | 36 |
37 |

No video results found

38 |
39 |
40 | ) : validSources.slice(0, displayCount).map((source) => ( 41 | 42 | 48 |
49 |
50 | {source.imageUrl && ( 51 | {source.title} { 58 | img.classList.remove('opacity-0'); 59 | }} 60 | onLoad={(e) => { 61 | const target = e.target as HTMLImageElement; 62 | target.classList.remove('opacity-0'); 63 | }} 64 | loading="eager" 65 | priority 66 | /> 67 | )} 68 | {source.duration && ( 69 | 70 | {source.duration} 71 | 72 | )} 73 |
74 |
75 |

{source.title}

76 |
110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /components/rSearch/sources/sources-web.tsx: -------------------------------------------------------------------------------- 1 | import type { WebSearchResult } from '@/types/search'; 2 | 3 | interface WebSourcesProps { 4 | sources: WebSearchResult[]; 5 | displayCount: number; 6 | getWebsiteName: (url: string) => string; 7 | } 8 | 9 | export default function WebSources({ sources, displayCount, getWebsiteName }: WebSourcesProps) { 10 | return ( 11 |
12 | {sources.slice(0, displayCount).map((source, index) => ( 13 |
14 | 20 | 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /components/rSearch/thinking.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Markdown from 'react-markdown'; 4 | 5 | interface ThinkingProps { 6 | reasoningContent?: string | null; 7 | } 8 | 9 | export default function Thinking({ reasoningContent }: ThinkingProps) { 10 | 11 | return ( 12 | <> 13 | {/* Thinking Section */} 14 | {reasoningContent && ( 15 |
16 |
17 | ( 20 |

21 | ), 22 | h2: (props) => ( 23 |

24 | ), 25 | h3: (props) => ( 26 |

27 | ), 28 | h4: (props) => ( 29 |

30 | ), 31 | h5: (props) => ( 32 |

33 | ), 34 | h6: (props) => ( 35 |
36 | ), 37 | table: (props) => ( 38 | 39 | ), 40 | thead: (props) => ( 41 | 42 | ), 43 | tbody: (props) => ( 44 | 45 | ), 46 | tr: (props) => ( 47 | 48 | ), 49 | th: (props) => ( 50 |
51 | ), 52 | td: (props) => ( 53 | 54 | ), 55 | img: (props) => ( 56 | {props.alt 57 | ), 58 | p: (props) => ( 59 |

60 | ), 61 | ul: (props) => ( 62 |