├── .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 |
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 |
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 |
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 | {/* {
22 | router.push(getSearchUrl(encodeURIComponent(query), nanoid()))
23 | }}
24 | type="button"
25 | className="flex items-center gap-2 rounded bg-transparent px-2 py-1 text-xs font-semibold text-[#696963] hover:bg-zinc-100"
26 | >
27 | Share
28 | */}
29 | {
31 | router.push(getSearchUrl(encodeURIComponent(query), nanoid()))
32 | }}
33 | type="button"
34 | className="flex items-center gap-2 rounded bg-transparent px-2 py-1 text-xs font-semibold text-[#696963] hover:bg-zinc-100"
35 | >
36 | Regenerate
37 |
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 |
32 |
46 |
47 |
48 |
49 |
50 | {/* 背景图片 */}
51 |
52 |
57 |
58 |
59 | {children}
60 |
61 |
62 | )
63 | }
64 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React from 'react'
4 |
5 | import { Logo } from '@/app/components/logo'
6 | import { Search } from '@/app/components/search'
7 |
8 | export default function Home() {
9 | return (
10 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/search/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useSearchParams } from 'next/navigation'
4 |
5 | import { Result } from '@/app/components/result'
6 | import { SearchForm } from '@/app/components/search-form'
7 | import { Title } from '@/app/components/title'
8 |
9 | export default function SearchPage() {
10 | const searchParams = useSearchParams()
11 | const query = decodeURIComponent(searchParams.get('q') || '')
12 | const rid = decodeURIComponent(searchParams.get('rid') || '')
13 | console.log('query--', query)
14 | return (
15 |
16 |
21 |
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/utils/cn.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/utils/fetch-stream.ts:
--------------------------------------------------------------------------------
1 | async function pump(
2 | reader: ReadableStreamDefaultReader,
3 | controller: ReadableStreamDefaultController,
4 | onChunk?: (chunk: Uint8Array) => void,
5 | onDone?: () => void,
6 | ): Promise | undefined> {
7 | const { done, value } = await reader.read();
8 | if (done) {
9 | onDone && onDone();
10 | controller.close();
11 | return;
12 | }
13 | onChunk && onChunk(value);
14 | controller.enqueue(value);
15 | return pump(reader, controller, onChunk, onDone);
16 | }
17 | export const fetchStream = (
18 | response: Response,
19 | onChunk?: (chunk: Uint8Array) => void,
20 | onDone?: () => void,
21 | ): ReadableStream => {
22 | const reader = response.body!.getReader();
23 | return new ReadableStream({
24 | start: (controller) => pump(reader, controller, onChunk, onDone),
25 | });
26 | };
27 |
--------------------------------------------------------------------------------
/src/app/utils/get-search-url.ts:
--------------------------------------------------------------------------------
1 | export const getSearchUrl = (query: string, search_uuid: string) => {
2 | const prefix = "/search";
3 | return `${prefix}?q=${encodeURIComponent(query)}&rid=${search_uuid}`;
4 | };
5 |
--------------------------------------------------------------------------------
/src/app/utils/parse-streaming.ts:
--------------------------------------------------------------------------------
1 | import { Relate } from '@/app/interfaces/relate'
2 | import { Source } from '@/app/interfaces/source'
3 | import { fetchStream } from '@/app/utils/fetch-stream'
4 |
5 | const LLM_SPLIT = '__LLM_RESPONSE__'
6 | const RELATED_SPLIT = '__RELATED_QUESTIONS__'
7 |
8 | export const parseStreaming = async (
9 | controller: AbortController,
10 | query: string,
11 | search_uuid: string,
12 | onSources: (value: Source[]) => void,
13 | onMarkdown: (value: string) => void,
14 | onRelates: (value: Relate[]) => void,
15 | onError?: (status: number) => void,
16 | ) => {
17 | const decoder = new TextDecoder()
18 | let uint8Array = new Uint8Array()
19 | let chunks = ''
20 | let sourcesEmitted = false
21 | const response = await fetch(`/query`, {
22 | method: 'POST',
23 | headers: {
24 | 'Content-Type': 'application/json',
25 | Accept: '*./*',
26 | Authorization: `Bearer ${process.env.NEXT_PUBLIC_LEPTON_API_TOKEN}`,
27 | },
28 | signal: controller.signal,
29 | body: JSON.stringify({
30 | query,
31 | search_uuid,
32 | }),
33 | })
34 | if (response.status !== 200) {
35 | onError?.(response.status)
36 | return
37 | }
38 | const markdownParse = (text: string) => {
39 | // console.log('::text::', text)
40 | onMarkdown(
41 | text
42 | .replace(/\[\[([cC])itation/g, '[citation')
43 | .replace(/[cC]itation:(\d+)]]/g, 'citation:$1]')
44 | .replace(/\[\[([cC]itation:\d+)]](?!])/g, `[$1]`)
45 | .replace(/\[[cC]itation:(\d+)]/g, '[citation]($1)'),
46 | )
47 | }
48 | fetchStream(
49 | response,
50 | (chunk) => {
51 | uint8Array = new Uint8Array([...uint8Array, ...chunk])
52 | chunks = decoder.decode(uint8Array, { stream: true })
53 | if (chunks.includes(LLM_SPLIT)) {
54 | const [sources, rest] = chunks.split(LLM_SPLIT)
55 | if (!sourcesEmitted) {
56 | try {
57 | onSources(JSON.parse(sources))
58 | } catch (e) {
59 | onSources([])
60 | }
61 | }
62 | sourcesEmitted = true
63 | if (rest.includes(RELATED_SPLIT)) {
64 | const [md] = rest.split(RELATED_SPLIT)
65 | markdownParse(md)
66 | } else {
67 | markdownParse(rest)
68 | }
69 | }
70 | },
71 | () => {
72 | const [_, relates] = chunks.split(RELATED_SPLIT)
73 | try {
74 | onRelates(JSON.parse(relates))
75 | } catch (e) {
76 | onRelates([])
77 | }
78 | },
79 | )
80 | }
81 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | content: [
5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
13 | "gradient-conic":
14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
15 | },
16 | colors: {
17 | blue: {
18 | 500: "#2F80ED",
19 | },
20 | },
21 | },
22 | },
23 | plugins: [require("@tailwindcss/typography")],
24 | };
25 | export default config;
26 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2015",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "noEmit": true,
13 | "esModuleInterop": true,
14 | "module": "esnext",
15 | "moduleResolution": "bundler",
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "jsx": "preserve",
19 | "incremental": true,
20 | "plugins": [
21 | {
22 | "name": "next"
23 | }
24 | ],
25 | "paths": {
26 | "@/*": [
27 | "./src/*"
28 | ]
29 | }
30 | },
31 | "include": [
32 | "next-env.d.ts",
33 | "**/*.ts",
34 | "**/*.tsx",
35 | ".next/types/**/*.ts",
36 | "../ui/types/**/*.ts"
37 | ],
38 | "exclude": [
39 | "node_modules"
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------