├── .gitignore ├── .prettierrc.mjs ├── LICENSE ├── README.md ├── next.config.mjs ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── bg.jpg ├── bg.svg ├── bg2.svg ├── logo.svg └── search-logo.svg ├── src └── app │ ├── components │ ├── answer.tsx │ ├── footer.tsx │ ├── header.tsx │ ├── logo.tsx │ ├── new-preset-query.tsx │ ├── popover.tsx │ ├── preset-query.tsx │ ├── relates.tsx │ ├── result.tsx │ ├── search-form.tsx │ ├── search.tsx │ ├── skeleton.tsx │ ├── sources.tsx │ ├── title.tsx │ └── wrapper.tsx │ ├── favicon.ico │ ├── globals.css │ ├── icon.png │ ├── interfaces │ ├── relate.ts │ └── source.ts │ ├── layout.tsx │ ├── page.tsx │ ├── search │ └── page.tsx │ └── utils │ ├── cn.ts │ ├── fetch-stream.ts │ ├── get-search-url.ts │ └── parse-streaming.ts ├── tailwind.config.ts └── tsconfig.json /.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.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env* 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.prettierrc.mjs: -------------------------------------------------------------------------------- 1 | import { factory } from '@innei/prettier' 2 | 3 | export default factory({ importSort: true, tailwindcss: true }) 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Heurist AI (team@heurist.xyz) 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 | # Heurist Search 2 | 3 | Think https://www.perplexity.ai/ but open-source and the compute is powered by a DePIN of GPUs! 4 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | export default (phase, { defaultConfig }) => { 2 | const env = process.env.NODE_ENV; 3 | /** 4 | * @type {import("next").NextConfig} 5 | */ 6 | 7 | return { 8 | async rewrites() { 9 | return [ 10 | { 11 | source: "/query", 12 | destination: "https://n2bifuff-search-with-lepton-v2.tin.lepton.run/query" // Proxy to Backend 13 | } 14 | ]; 15 | } 16 | }; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "search", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@next/third-parties": "^14.0.4", 13 | "@radix-ui/react-popover": "^1.0.7", 14 | "@tailwindcss/forms": "^0.5.7", 15 | "@upstash/ratelimit": "^1.0.0", 16 | "@vercel/kv": "^1.0.1", 17 | "clsx": "^2.1.0", 18 | "headlessui": "^0.0.0", 19 | "lucide-react": "^0.309.0", 20 | "mdast-util-from-markdown": "^2.0.0", 21 | "nanoid": "^5.0.4", 22 | "next": "14.0.4", 23 | "react": "^18", 24 | "react-dom": "^18", 25 | "react-markdown": "^9.0.1", 26 | "tailwind-merge": "^2.2.0", 27 | "unist-builder": "^4.0.0" 28 | }, 29 | "devDependencies": { 30 | "@innei/prettier": "^0.12.2", 31 | "@tailwindcss/typography": "^0.5.10", 32 | "@types/node": "^20", 33 | "@types/react": "^18", 34 | "@types/react-dom": "^18", 35 | "autoprefixer": "^10.0.1", 36 | "eslint": "^8", 37 | "eslint-config-next": "14.0.4", 38 | "eslint-config-prettier": "^9.0.0", 39 | "eslint-plugin-prettier": "^5.0.1", 40 | "eslint-plugin-unused-imports": "^3.0.0", 41 | "postcss": "^8", 42 | "prettier": "^3.1.0", 43 | "tailwindcss": "^3.3.0", 44 | "typescript": "^5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heurist-network/gpt-search-web/e9d68c47d167dc8cf0717e8dce14c7e2f8bd402e/public/bg.jpg -------------------------------------------------------------------------------- /public/bg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/search-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/app/components/answer.tsx: -------------------------------------------------------------------------------- 1 | import { BookOpenText } from 'lucide-react' 2 | import { FC } from 'react' 3 | import Markdown from 'react-markdown' 4 | 5 | import { 6 | Popover, 7 | PopoverContent, 8 | PopoverTrigger, 9 | } from '@/app/components/popover' 10 | import { Skeleton } from '@/app/components/skeleton' 11 | import { Wrapper } from '@/app/components/wrapper' 12 | import { Source } from '@/app/interfaces/source' 13 | 14 | export const Answer: FC<{ markdown: string; sources: Source[] }> = ({ 15 | markdown, 16 | sources, 17 | }) => { 18 | return ( 19 | Answer} 21 | content={ 22 | markdown ? ( 23 |
24 | { 27 | if (!props.href) return <> 28 | const source = sources[+props.href - 1] 29 | if (!source) return <> 30 | return ( 31 | 32 | 33 | 34 | 38 | {props.href} 39 | 40 | 41 | 45 |
46 | {source.name} 47 |
48 |
49 | {source.primaryImageOfPage?.thumbnailUrl && ( 50 |
51 | 57 |
58 | )} 59 |
60 |
61 | {source.snippet} 62 |
63 |
64 |
65 | 66 |
67 | 112 |
113 |
114 |
115 | ) 116 | }, 117 | }} 118 | > 119 | {markdown} 120 |
121 |
122 | ) : ( 123 |
124 | 125 | 126 | 127 | 128 | 129 |
130 | ) 131 | } 132 | >
133 | ) 134 | } 135 | -------------------------------------------------------------------------------- /src/app/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import { Mails } from "lucide-react"; 2 | import { FC } from "react"; 3 | import Link from "next/link"; 4 | export const Footer: FC = () => { 5 | return ( 6 |
7 |
Heurist 🤝 Lepton
8 |
9 |
10 | 14 | 15 | Talk to us 16 | 17 |
18 |
If you need a low-cost and performant LLM API!
19 |
20 |
21 | 25 | 37 | 38 | 39 | 40 | 41 | Heurist Protocol 42 | 43 | {/* 47 | API Playground 48 | */} 49 | 50 | 55 | 67 | 68 | 69 | 70 | Github 71 | 72 | 77 | 89 | 90 | 91 | Twitter 92 | 93 | {/* 94 | Blog 95 | */} 96 |
97 |
98 | ); 99 | }; 100 | -------------------------------------------------------------------------------- /src/app/components/header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Image from 'next/image' 3 | 4 | // import SearchLogo from '@/public/search.svg' 5 | const Header = () => { 6 | return ( 7 |
8 | 128 |
129 | ) 130 | } 131 | 132 | export default Header 133 | -------------------------------------------------------------------------------- /src/app/components/logo.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import Image from 'next/image' 3 | 4 | export const Logo: FC = () => { 5 | return ( 6 |
7 | {/*
8 | Logo 9 |
*/} 10 |
11 | Redefine Internet Search with AI 12 |
13 | {/*
14 | beta 15 |
*/} 16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/app/components/new-preset-query.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useMemo } from 'react' 2 | import { nanoid } from 'nanoid' 3 | import Link from 'next/link' 4 | 5 | import { getSearchUrl } from '@/app/utils/get-search-url' 6 | 7 | export const PresetQuery: FC<{ query: string }> = ({ query }) => { 8 | const rid = useMemo(() => nanoid(), [query]) 9 | 10 | return ( 11 | 17 | {query} 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/app/components/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 5 | 6 | import { cn } from "@/app/utils/cn"; 7 | 8 | const Popover = PopoverPrimitive.Root; 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger; 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )); 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 30 | 31 | export { Popover, PopoverTrigger, PopoverContent }; 32 | -------------------------------------------------------------------------------- /src/app/components/preset-query.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useMemo } from 'react' 2 | import { nanoid } from 'nanoid' 3 | import Link from 'next/link' 4 | 5 | import { getSearchUrl } from '@/app/utils/get-search-url' 6 | 7 | export const PresetQuery: FC<{ query: string }> = ({ query }) => { 8 | const rid = useMemo(() => nanoid(), [query]) 9 | 10 | return ( 11 | 17 | {query} 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/app/components/relates.tsx: -------------------------------------------------------------------------------- 1 | import { MessageSquareQuote } from 'lucide-react' 2 | import React, { FC } from 'react' 3 | 4 | import { PresetQuery } from '@/app/components/preset-query' 5 | import { Skeleton } from '@/app/components/skeleton' 6 | import { Wrapper } from '@/app/components/wrapper' 7 | import { Relate } from '@/app/interfaces/relate' 8 | 9 | export const Relates: FC<{ relates: Relate[] | null }> = ({ relates }) => { 10 | return ( 11 | Related · {relates && relates.length}} 13 | content={ 14 |
15 | {relates !== null ? ( 16 | relates.length > 0 ? ( 17 | relates.map(({ question }) => ( 18 | 19 | )) 20 | ) : ( 21 |
No related questions.
22 | ) 23 | ) : ( 24 | <> 25 | 26 | 27 | 28 | 29 | )} 30 |
31 | } 32 | >
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/app/components/result.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Annoyed } from 'lucide-react' 4 | import { FC, useEffect, useState } from 'react' 5 | 6 | import { Answer } from '@/app/components/answer' 7 | import { Relates } from '@/app/components/relates' 8 | import { Sources } from '@/app/components/sources' 9 | import { Relate } from '@/app/interfaces/relate' 10 | import { Source } from '@/app/interfaces/source' 11 | import { parseStreaming } from '@/app/utils/parse-streaming' 12 | 13 | export const Result: FC<{ query: string; rid: string }> = ({ query, rid }) => { 14 | const [sources, setSources] = useState([]) 15 | const [markdown, setMarkdown] = useState('') 16 | const [relates, setRelates] = useState(null) 17 | const [error, setError] = useState(null) 18 | useEffect(() => { 19 | const controller = new AbortController() 20 | void parseStreaming( 21 | controller, 22 | query, 23 | rid, 24 | setSources, 25 | setMarkdown, 26 | setRelates, 27 | setError, 28 | ) 29 | return () => { 30 | controller && controller.abort() 31 | } 32 | }, [query]) 33 | return ( 34 |
35 | 36 | 37 |
38 | 39 | {error && ( 40 |
41 |
42 | 43 | {error === 429 44 | ? 'Sorry, you have made too many requests recently, try again later.' 45 | : 'Sorry, we might be overloaded, try again later.'} 46 |
47 |
48 | )} 49 |
50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/app/components/search-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Search as SearchIcon } from 'lucide-react' 4 | import React, { FC, useState } from 'react' 5 | import { nanoid } from 'nanoid' 6 | import { useRouter } from 'next/navigation' 7 | 8 | import { getSearchUrl } from '@/app/utils/get-search-url' 9 | 10 | interface SearchFormProps { 11 | hideBorder?: boolean 12 | query?: string 13 | } 14 | export const SearchForm: FC = ({ 15 | hideBorder = false, 16 | query, 17 | }) => { 18 | const router = useRouter() 19 | const [value, setValue] = useState(query || '') 20 | const [isFocused, setIsFocused] = useState(true) 21 | 22 | return ( 23 |
{ 25 | e.preventDefault() 26 | if (value) { 27 | setValue('') 28 | router.push(getSearchUrl(encodeURIComponent(value), nanoid())) 29 | } 30 | }} 31 | className="relative flex w-full items-center rounded-2xl bg-white" 32 | > 33 |
34 | setValue(e.target.value)} 38 | onFocus={() => setIsFocused(true)} 39 | onBlur={() => setIsFocused(false)} 40 | onKeyDown={(e) => { 41 | if (e.key === 'Enter') { 42 | e.preventDefault() 43 | router.push(getSearchUrl(encodeURIComponent(value), nanoid())) 44 | } 45 | }} 46 | autoFocus 47 | placeholder="Ask anything..." 48 | maxLength={100} 49 | className={`mx-6 w-5/6 flex-1 ${hideBorder ? '' : 'border-b'} overflow-hidden text-ellipsis whitespace-nowrap bg-[transparent] py-4 pr-10 text-2xl outline-none focus:border-[#1d1d1b]`} 50 | /> 51 | {value && ( 52 | 71 | )} 72 |
73 | 83 |
84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /src/app/components/search.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React, { FC, useState } from 'react' 4 | 5 | import { PresetQuery } from '@/app/components/new-preset-query' 6 | import { SearchForm } from '@/app/components/search-form' 7 | 8 | export const Search: FC = () => { 9 | return ( 10 |
11 | {' '} 12 |
13 | 14 |
15 |
16 | {' '} 17 | 18 | 19 |
20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/app/components/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/app/utils/cn"; 2 | import { HTMLAttributes } from "react"; 3 | 4 | function Skeleton({ className, ...props }: HTMLAttributes) { 5 | return ( 6 |
10 | ); 11 | } 12 | 13 | export { Skeleton }; 14 | -------------------------------------------------------------------------------- /src/app/components/sources.tsx: -------------------------------------------------------------------------------- 1 | import { BookText } from 'lucide-react' 2 | import { FC } from 'react' 3 | 4 | import { Skeleton } from '@/app/components/skeleton' 5 | import { Wrapper } from '@/app/components/wrapper' 6 | import { Source } from '@/app/interfaces/source' 7 | 8 | const SourceItem: FC<{ source: Source; index: number }> = ({ 9 | source, 10 | index, 11 | }) => { 12 | const { id, name, url } = source 13 | const domain = new URL(url).hostname 14 | return ( 15 | 63 | ) 64 | } 65 | 66 | export const Sources: FC<{ sources: Source[] }> = ({ sources }) => { 67 | return ( 68 | Sources · {sources.length}} 70 | content={ 71 |
72 | {sources.length > 0 ? ( 73 | sources.map((item, index) => ( 74 | 79 | )) 80 | ) : ( 81 | <> 82 | 83 | 84 | 85 | 86 | 87 | )} 88 |
89 | } 90 | >
91 | ) 92 | } 93 | -------------------------------------------------------------------------------- /src/app/components/title.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { RefreshCcw } from 'lucide-react' 4 | import { nanoid } from 'nanoid' 5 | import { useRouter } from 'next/navigation' 6 | 7 | import { getSearchUrl } from '@/app/utils/get-search-url' 8 | 9 | export const Title = ({ query }: { query: string }) => { 10 | const router = useRouter() 11 | return ( 12 |
13 |
17 | {/* {query} */} 18 |
19 |
20 | {/* */} 29 | 38 |
39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/app/components/wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react' 2 | 3 | export const Wrapper: FC<{ 4 | title: ReactNode 5 | content: ReactNode 6 | }> = ({ title, content }) => { 7 | return ( 8 |
9 |
{title}
10 | {content} 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heurist-network/gpt-search-web/e9d68c47d167dc8cf0717e8dce14c7e2f8bd402e/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | input:-webkit-autofill, 6 | input:-webkit-autofill:hover, 7 | input:-webkit-autofill:focus, 8 | textarea:-webkit-autofill, 9 | textarea:-webkit-autofill:hover, 10 | textarea:-webkit-autofill:focus, 11 | select:-webkit-autofill, 12 | select:-webkit-autofill:hover, 13 | select:-webkit-autofill:focus { 14 | -webkit-background-clip: text; 15 | } 16 | 17 | body { 18 | position: relative; 19 | margin: 0; 20 | padding: 0; 21 | } 22 | 23 | body::before { 24 | /* content: ''; */ 25 | position: absolute; 26 | top: 0; 27 | left: 0; 28 | width: 100%; 29 | height: 100%; 30 | background-image: url('/bg.jpg'); 31 | background-size: cover; 32 | background-position: center; 33 | background-repeat: no-repeat; 34 | filter: blur(8px); /* 初始虚化效果 */ 35 | z-index: -1; /* 确保背景图在所有内容的后面 */ 36 | animation: breatheBlur 10s ease-in-out infinite; /* 添加呼吸效果动画 */ 37 | } 38 | 39 | @keyframes breatheBlur { 40 | 0%, 100% { 41 | filter: blur(8px); /* 初始虚化效果 */ 42 | } 43 | 50% { 44 | filter: blur(20px); /* 呼吸效果的最大虚化 */ 45 | } 46 | } 47 | 48 | @layer base { 49 | .prose { 50 | @apply text-gray-800; 51 | } 52 | .prose p { 53 | margin:0px; 54 | } 55 | .prose ol { 56 | margin:0px; 57 | } 58 | .prose ul,.prose li, .prose li span { 59 | margin:0px; 60 | } 61 | .prose li span span { 62 | line-height: 24px; 63 | } 64 | .prose li { 65 | line-height: 2; 66 | } 67 | 68 | /* 添加更多自定义样式 */ 69 | } -------------------------------------------------------------------------------- /src/app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heurist-network/gpt-search-web/e9d68c47d167dc8cf0717e8dce14c7e2f8bd402e/src/app/icon.png -------------------------------------------------------------------------------- /src/app/interfaces/relate.ts: -------------------------------------------------------------------------------- 1 | export interface Relate { 2 | question: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/interfaces/source.ts: -------------------------------------------------------------------------------- 1 | export interface Source { 2 | id: string; 3 | name: string; 4 | url: string; 5 | isFamilyFriendly: boolean; 6 | displayUrl: string; 7 | snippet: string; 8 | deepLinks: { snippet: string; name: string; url: string }[]; 9 | dateLastCrawled: string; 10 | cachedPageUrl: string; 11 | language: string; 12 | primaryImageOfPage?: { 13 | thumbnailUrl: string; 14 | width: number; 15 | height: number; 16 | imageId: string; 17 | }; 18 | isNavigational: boolean; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Inter } from 'next/font/google' 2 | import type { Metadata } from 'next' 3 | 4 | import './globals.css' 5 | 6 | import { ReactNode } from 'react' 7 | import Script from 'next/script' 8 | 9 | // 删除这行 10 | // import Bg from './bg.jpg' 11 | 12 | import Header from './components/header' 13 | 14 | const inter = Inter({ subsets: ['latin'] }) 15 | 16 | export const metadata: Metadata = { 17 | title: 'Heurist Search', 18 | 19 | description: 20 | 'AI-based search engine powered by Heurist LLM API\n Deployed on Lepton', 21 | } 22 | 23 | export default function RootLayout({ children }: { children: ReactNode }) { 24 | const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GA_ID 25 | return ( 26 | 27 | 28 |