first commit
This commit is contained in:
commit
0b86be3426
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
*.txt
|
||||
.env
|
||||
data
|
||||
ai-search
|
7
Dockerfile
Normal file
7
Dockerfile
Normal file
@ -0,0 +1,7 @@
|
||||
FROM debian
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ai-search .
|
||||
|
||||
CMD [ "./ai-search" ]
|
36
README.md
Normal file
36
README.md
Normal file
@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
4
build.sh
Normal file
4
build.sh
Normal file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
pnpm build
|
||||
GOOS=linux GOARCH=amd64 go build -o ai-search *.go
|
||||
docker buildx build -f ./Dockerfile -t git.vaala.cloud/vaalacat/ai-search --platform=linux/amd64 . --push
|
28
docker-compose.yaml
Normal file
28
docker-compose.yaml
Normal file
@ -0,0 +1,28 @@
|
||||
version: "3"
|
||||
services:
|
||||
search:
|
||||
image: git.vaala.cloud/vaalacat/ddgs:latest
|
||||
environment:
|
||||
HTTP_PROXY: http://xxxxxxxx
|
||||
HTTPS_PROXY: http://xxxxxxxx
|
||||
restart: always
|
||||
ai:
|
||||
image: git.vaala.cloud/vaalacat/ai-search
|
||||
ports:
|
||||
- 8080:8080
|
||||
restart: always
|
||||
volumes:
|
||||
- ./data:/data
|
||||
environment:
|
||||
RPC_SEARCH_URL: http://search:8000/search
|
||||
# 下面两个都填一样的
|
||||
OPENAI_ENDPOINT: https://xxxxxxxx/v1
|
||||
OPENAI_API_KEY: sk-ssssssssss
|
||||
OPENAI_CHAT_ENDPOINT: https://xxxxxxxx/v1
|
||||
OPENAI_CHAT_API_KEY: sk-ssssssssss
|
||||
RAG_MAX_TOKENS: 4096
|
||||
RAG_MORE_QUESTIONS_MAX_TOKENS: 4096
|
||||
RAG_TEMPERATURE: 0.9
|
||||
RAG_MORE_QUESTIONS_TEMPERATURE: 0.7
|
||||
PROMPT_RAG_PATH: /data/rag.txt
|
||||
PROMPT_MORE_QUESTIONS_PATH: /data/more.txt
|
61
go.mod
Normal file
61
go.mod
Normal file
@ -0,0 +1,61 @@
|
||||
module ai-search
|
||||
|
||||
go 1.21.4
|
||||
|
||||
require (
|
||||
github.com/coocood/freecache v1.2.4
|
||||
github.com/gin-contrib/static v0.0.1
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0
|
||||
github.com/imroc/req/v3 v3.42.3
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/sashabaranov/go-openai v1.19.2
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.13.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||
github.com/quic-go/quic-go v0.40.1 // indirect
|
||||
github.com/refraction-networking/utls v1.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.16.1 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
|
||||
)
|
174
go.sum
Normal file
174
go.sum
Normal file
@ -0,0 +1,174 @@
|
||||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||
github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M=
|
||||
github.com/coocood/freecache v1.2.4/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U=
|
||||
github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
||||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 h1:dHLYa5D8/Ta0aLR2XcPsrkpAgGeFs6thhMcQK0oQ0n8=
|
||||
github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
|
||||
github.com/imroc/req/v3 v3.42.3 h1:ryPG2AiwouutAopwPxKpWKyxgvO8fB3hts4JXlh3PaE=
|
||||
github.com/imroc/req/v3 v3.42.3/go.mod h1:Axz9Y/a2b++w5/Jht3IhQsdBzrG1ftJd1OJhu21bB2Q=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs=
|
||||
github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM=
|
||||
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
|
||||
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||
github.com/quic-go/quic-go v0.40.1 h1:X3AGzUNFs0jVuO3esAGnTfvdgvL4fq655WaOi1snv1Q=
|
||||
github.com/quic-go/quic-go v0.40.1/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c=
|
||||
github.com/refraction-networking/utls v1.6.0 h1:X5vQMqVx7dY7ehxxqkFER/W6DSjy8TMqSItXm8hRDYQ=
|
||||
github.com/refraction-networking/utls v1.6.0/go.mod h1:kHJ6R9DFFA0WsRgBM35iiDku4O7AqPR6y79iuzW7b10=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sashabaranov/go-openai v1.19.2 h1:+dkuCADSnwXV02YVJkdphY8XD9AyHLUWwk6V7LB6EL8=
|
||||
github.com/sashabaranov/go-openai v1.19.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM=
|
||||
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
|
||||
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
22
main.go
Normal file
22
main.go
Normal file
@ -0,0 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"ai-search/service"
|
||||
"embed"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
//go:embed all:out/*
|
||||
var fs embed.FS
|
||||
|
||||
var (
|
||||
router *gin.Engine
|
||||
)
|
||||
|
||||
func main() {
|
||||
router = gin.Default()
|
||||
service.HandleStaticFile(router, fs)
|
||||
router.POST("/v1/chat/search", service.SearchHandler)
|
||||
router.Run(service.GetSettings().ListenAddr)
|
||||
}
|
16
next.config.mjs
Normal file
16
next.config.mjs
Normal file
@ -0,0 +1,16 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'export',
|
||||
async rewrites() {
|
||||
return {
|
||||
fallback: [
|
||||
{
|
||||
source: '/v1/:path*',
|
||||
destination: `http://localhost:8080/v1/:path*`,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
31
package.json
Normal file
31
package.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "my-app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"clsx": "^2.0.0",
|
||||
"lucide-react": "^0.301.0",
|
||||
"nanoid": "^5.0.5",
|
||||
"next": "14.1.0",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-markdown": "^8.0.7",
|
||||
"tailwind-merge": "^2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
2117
pnpm-lock.yaml
generated
Normal file
2117
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
1
public/next.svg
Normal file
1
public/next.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
public/vercel.svg
Normal file
1
public/vercel.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
After Width: | Height: | Size: 629 B |
18
service/cache.go
Normal file
18
service/cache.go
Normal file
@ -0,0 +1,18 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/coocood/freecache"
|
||||
)
|
||||
|
||||
var (
|
||||
searchCache *freecache.Cache
|
||||
)
|
||||
|
||||
func InitCache() {
|
||||
cacheSize := GetSettings().CacheSize * 1024 * 1024 // 100 MB
|
||||
searchCache = freecache.NewCache(cacheSize)
|
||||
}
|
||||
|
||||
func GetSearchCache() *freecache.Cache {
|
||||
return searchCache
|
||||
}
|
27
service/const.go
Normal file
27
service/const.go
Normal file
@ -0,0 +1,27 @@
|
||||
package service
|
||||
|
||||
const (
|
||||
HeaderKey = "header"
|
||||
AppEntryKey = "entry"
|
||||
UIDKey = "uid"
|
||||
EndpointKey = "endpoint"
|
||||
UserInfoKey = "userinfo"
|
||||
TokenKey = "token"
|
||||
AuthorizationKey = "authorization"
|
||||
SetAuthorizationKey = "x-authorization-token"
|
||||
TraceIDKey = "x-vaala-trace-id"
|
||||
ClientRequestIDKey = "x-client-request-id"
|
||||
SessionKey = "x-vaala-session"
|
||||
RegistrationSessionKey = "x-vaala-registration-session"
|
||||
LoginSessionKey = "x-vaala-login-session"
|
||||
)
|
||||
|
||||
const (
|
||||
RespSuccess = "success"
|
||||
ValueNone = "none"
|
||||
)
|
||||
|
||||
const (
|
||||
LangEN = "en"
|
||||
LangZH = "zh"
|
||||
)
|
129
service/helper.go
Normal file
129
service/helper.go
Normal file
@ -0,0 +1,129 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"ai-search/service/logger"
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func getOpenAIConfig(c context.Context, mode string) (token, endpoint string) {
|
||||
token = GetSettings().OpenAIAPIKey
|
||||
endpoint = GetSettings().OpenAIEndpint
|
||||
if mode == "chat" {
|
||||
token = GetSettings().OpenAIChatAPIKey
|
||||
endpoint = GetSettings().OpenAIChatEndpoint
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type Queue struct {
|
||||
data []time.Duration
|
||||
length int
|
||||
}
|
||||
|
||||
func NewQueue(length int, defaultValue int) *Queue {
|
||||
data := make([]time.Duration, 0, length)
|
||||
for i := 0; i < length; i++ {
|
||||
data = append(data, time.Duration(defaultValue)*time.Millisecond)
|
||||
}
|
||||
return &Queue{
|
||||
data: data,
|
||||
length: length,
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Queue) Add(value time.Duration) {
|
||||
if len(q.data) >= q.length {
|
||||
q.data = q.data[1:]
|
||||
}
|
||||
q.data = append(q.data, value)
|
||||
}
|
||||
|
||||
func (q *Queue) Avg(k ...int) time.Duration {
|
||||
param := 1
|
||||
if len(k) > 0 {
|
||||
param = k[0]
|
||||
}
|
||||
total := time.Duration(0)
|
||||
count := 0
|
||||
for _, value := range q.data {
|
||||
if value != 0 {
|
||||
total += value
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count == 0 {
|
||||
return time.Duration(0)
|
||||
}
|
||||
ans := total * time.Duration(param) / time.Duration(count)
|
||||
if ans > time.Duration(20)*time.Millisecond {
|
||||
return time.Duration(20) * time.Millisecond
|
||||
}
|
||||
return ans
|
||||
}
|
||||
|
||||
func streamResp(c *gin.Context, resp *openai.ChatCompletionStream) string {
|
||||
result := ""
|
||||
|
||||
if resp == nil {
|
||||
logger.Logger(c).Error("stream resp is nil")
|
||||
return result
|
||||
}
|
||||
|
||||
_, ok := c.Writer.(http.Flusher)
|
||||
if !ok {
|
||||
logger.Logger(c).Panic("server not support")
|
||||
}
|
||||
defer func() {
|
||||
c.Writer.Flush()
|
||||
}()
|
||||
|
||||
queue := NewQueue(GetSettings().OpenAIChatQueueLen, GetSettings().OpenAIChatNetworkDelay)
|
||||
ch := make(chan rune, 1024)
|
||||
|
||||
go func(c *gin.Context, msgChan chan rune) {
|
||||
lastTime := time.Now()
|
||||
for {
|
||||
line, err := resp.Recv()
|
||||
if err != nil {
|
||||
close(msgChan)
|
||||
logger.Logger(c).WithError(err).Error("read openai completion line error")
|
||||
return
|
||||
}
|
||||
if len(line.Choices[0].Delta.Content) == 0 {
|
||||
continue
|
||||
}
|
||||
nowTime := time.Now()
|
||||
|
||||
division := strings.Count(line.Choices[0].Delta.Content, "")
|
||||
for _, v := range line.Choices[0].Delta.Content {
|
||||
msgChan <- v
|
||||
}
|
||||
|
||||
during := (nowTime.Sub(lastTime) + (time.Duration(GetSettings().OpenAIChatNetworkDelay) *
|
||||
time.Millisecond)) / time.Duration(division)
|
||||
queue.Add(during)
|
||||
lastTime = nowTime
|
||||
}
|
||||
}(c, ch)
|
||||
|
||||
for char := range ch {
|
||||
str := string(char)
|
||||
_, err := c.Writer.WriteString(str)
|
||||
result += str
|
||||
if err != nil {
|
||||
logger.Logger(c).WithError(err).Error("write string to client error")
|
||||
return ""
|
||||
}
|
||||
c.Writer.Flush()
|
||||
time.Sleep(queue.Avg(len(str) * 2)) // 英文平均长度为6个字符,一个UTF8字符是3个长度,试图让一个单词等于一个汉字
|
||||
}
|
||||
logger.Logger(c).Info("finish stream text to client")
|
||||
return result
|
||||
}
|
11
service/logger/logger.go
Normal file
11
service/logger/logger.go
Normal file
@ -0,0 +1,11 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func Logger(c context.Context) *logrus.Entry {
|
||||
return logrus.WithContext(c)
|
||||
}
|
51
service/response.go
Normal file
51
service/response.go
Normal file
@ -0,0 +1,51 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type CommonResp interface {
|
||||
gin.H
|
||||
}
|
||||
|
||||
func OKResp[T CommonResp](c *gin.Context, origin *T) {
|
||||
if c.ContentType() == "application/x-protobuf" {
|
||||
c.ProtoBuf(http.StatusOK, origin)
|
||||
} else {
|
||||
c.JSON(http.StatusOK, OK(RespSuccess).WithBody(origin))
|
||||
}
|
||||
}
|
||||
|
||||
func OKRespWithJsonMarshal[T CommonResp](c *gin.Context, origin *T) {
|
||||
c.JSON(http.StatusOK, OK(RespSuccess).WithBody(origin))
|
||||
}
|
||||
|
||||
func ErrResp[T CommonResp](c *gin.Context, origin *T, err string, errCode ...int) {
|
||||
if c.ContentType() == "application/x-protobuf" {
|
||||
c.ProtoBuf(http.StatusInternalServerError, origin)
|
||||
} else {
|
||||
if len(errCode) > 0 {
|
||||
c.JSON(errCode[0], Err(err).WithBody(origin))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, Err(err).WithBody(origin))
|
||||
}
|
||||
}
|
||||
|
||||
func ErrUnAuthorized(c *gin.Context, err string) {
|
||||
if c.ContentType() == "application/x-protobuf" {
|
||||
c.ProtoBuf(http.StatusUnauthorized, nil)
|
||||
} else {
|
||||
c.JSON(http.StatusUnauthorized, Err(err).WithBody(nil))
|
||||
}
|
||||
}
|
||||
|
||||
func ErrNotFound(c *gin.Context, err string) {
|
||||
if c.ContentType() == "application/x-protobuf" {
|
||||
c.ProtoBuf(http.StatusNotFound, nil)
|
||||
} else {
|
||||
c.JSON(http.StatusNotFound, Err(err).WithBody(nil))
|
||||
}
|
||||
}
|
55
service/result.go
Normal file
55
service/result.go
Normal file
@ -0,0 +1,55 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
Code int `json:"code,omitempty"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Data gin.H `json:"data,omitempty"`
|
||||
Body interface{} `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
func (r *Result) WithMsg(message string) *Result {
|
||||
r.Msg = message
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Result) WithData(data gin.H) *Result {
|
||||
r.Data = data
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Result) WithKeyValue(key string, value interface{}) *Result {
|
||||
if r.Data == nil {
|
||||
r.Data = gin.H{}
|
||||
}
|
||||
r.Data[key] = value
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Result) WithBody(body interface{}) *Result {
|
||||
r.Body = body
|
||||
return r
|
||||
}
|
||||
|
||||
func newResult(code int, msg string) *Result {
|
||||
return &Result{
|
||||
Code: code,
|
||||
Msg: msg,
|
||||
Data: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func OK(msg string) *Result {
|
||||
return newResult(200, msg)
|
||||
}
|
||||
|
||||
func Err(msg string) *Result {
|
||||
return newResult(500, msg)
|
||||
}
|
||||
|
||||
func UnAuth(msg string) *Result {
|
||||
return newResult(401, msg)
|
||||
}
|
51
service/rpc.go
Normal file
51
service/rpc.go
Normal file
@ -0,0 +1,51 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"ai-search/service/logger"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
)
|
||||
|
||||
type SearchClient interface {
|
||||
Search(c context.Context, query string, nums int) (SearchResp, error)
|
||||
}
|
||||
|
||||
type searchClient struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
type SearchResp struct {
|
||||
Results []SearchResult `json:"results"`
|
||||
}
|
||||
|
||||
type SearchResult struct {
|
||||
Body string `json:"body"`
|
||||
Href string `json:"href"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
func NewSearchClient() SearchClient {
|
||||
return &searchClient{
|
||||
URL: GetSettings().RPCEndpoints.SearchURL,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *searchClient) Search(c context.Context, query string, nums int) (SearchResp, error) {
|
||||
resp := SearchResp{}
|
||||
_, err := req.C().R().
|
||||
SetFormData(map[string]string{
|
||||
"q": query,
|
||||
"max_results": fmt.Sprintf("%d", nums),
|
||||
}).
|
||||
SetContentType("application/x-www-form-urlencoded").
|
||||
SetSuccessResult(&resp).
|
||||
Post(s.URL)
|
||||
|
||||
if err != nil {
|
||||
logger.Logger(c).Error(err)
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
214
service/search_handler.go
Normal file
214
service/search_handler.go
Normal file
@ -0,0 +1,214 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"ai-search/service/logger"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func SearchHandler(c *gin.Context) {
|
||||
searchReq := &SearchReq{}
|
||||
if err := c.Copy().ShouldBindJSON(searchReq); err != nil {
|
||||
ErrResp[gin.H](c, nil, "error", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
cachedResult, err := getCacheResp(c, searchReq.SearchUUID)
|
||||
if err == nil {
|
||||
logger.Logger(c).Infof("cache key hit [%s], query: [%s]", searchReq.SearchUUID, searchReq.Query)
|
||||
c.String(http.StatusOK, cachedResult)
|
||||
return
|
||||
}
|
||||
|
||||
if searchReq.Query == "" && searchReq.SearchUUID == "" {
|
||||
ErrResp[gin.H](c, nil, "param is invalid", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if searchReq.Query == "" && searchReq.SearchUUID != "" {
|
||||
ErrResp[gin.H](c, nil, "content is gone", http.StatusGone)
|
||||
return
|
||||
}
|
||||
|
||||
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
||||
c.Writer.Header().Set("Cache-Control", "no-cache")
|
||||
c.Writer.Header().Set("Connection", "keep-alive")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
|
||||
cli := NewSearchClient()
|
||||
searchResp, err := cli.Search(c, searchReq.Query, GetSettings().RAGSearchCount)
|
||||
if err != nil {
|
||||
logger.Logger(c).WithError(err).Errorf("client.Search error")
|
||||
return
|
||||
}
|
||||
ss := &Sources{}
|
||||
ss.FromSearchResp(&searchResp, searchReq.Query, searchReq.SearchUUID)
|
||||
|
||||
originReq := &openai.ChatCompletionRequest{
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: openai.ChatMessageRoleSystem,
|
||||
Content: fmt.Sprintf(RagPrompt(), getSearchContext(ss)),
|
||||
},
|
||||
{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: searchReq.Query,
|
||||
},
|
||||
},
|
||||
Stream: true,
|
||||
}
|
||||
|
||||
apiKey, endpoint := getOpenAIConfig(c, "chat")
|
||||
conf := openai.DefaultConfig(apiKey)
|
||||
conf.BaseURL = endpoint
|
||||
client := openai.NewClientWithConfig(conf)
|
||||
request := openai.ChatCompletionRequest{
|
||||
Model: openai.GPT3Dot5Turbo,
|
||||
Messages: originReq.Messages,
|
||||
Temperature: GetSettings().RAGParams.Temperature,
|
||||
MaxTokens: GetSettings().RAGParams.MaxTokens,
|
||||
Stream: true,
|
||||
}
|
||||
|
||||
resp, err := client.CreateChatCompletionStream(
|
||||
context.Background(),
|
||||
request,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Logger(c).WithError(err).Errorf("client.CreateChatCompletionStream error")
|
||||
}
|
||||
|
||||
relatedStrChan := make(chan string)
|
||||
defer close(relatedStrChan)
|
||||
go func() {
|
||||
relatedStrChan <- getRelatedQuestionsResp(c, searchReq.Query, ss)
|
||||
}()
|
||||
|
||||
finalResult := streamSearchItemResp(c, []string{
|
||||
ss.ToString(),
|
||||
"\n\n__LLM_RESPONSE__\n\n",
|
||||
})
|
||||
finalResult = finalResult + streamResp(c, resp)
|
||||
finalResult = finalResult + streamSearchItemResp(c, []string{
|
||||
"\n\n__RELATED_QUESTIONS__\n\n",
|
||||
// `[{"question": "What is the formal way to say hello in Chinese?"}, {"question": "How do you say 'How are you' in Chinese?"}]`,
|
||||
<-relatedStrChan,
|
||||
})
|
||||
|
||||
GetSearchCache().Set([]byte(searchReq.SearchUUID), newCachedResult(searchReq.SearchUUID, searchReq.Query, finalResult).ToBytes(), GetSettings().RAGSearchCacheTime)
|
||||
logger.Logger(c).Infof("cache key miss [%s], query: [%s], set result to cache", searchReq.SearchUUID, searchReq.Query)
|
||||
}
|
||||
|
||||
func getCacheResp(c *gin.Context, searchUUID string) (string, error) {
|
||||
ans, err := GetSearchCache().Get([]byte(searchUUID))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if ans != nil {
|
||||
cachedResult := &cachedResult{}
|
||||
cachedResult.FromBytes(ans)
|
||||
return cachedResult.Result, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("cache not found")
|
||||
}
|
||||
|
||||
func streamSearchItemResp(c *gin.Context, t []string) string {
|
||||
result := ""
|
||||
|
||||
_, ok := c.Writer.(http.Flusher)
|
||||
if !ok {
|
||||
logger.Logger(c).Panic("server not support")
|
||||
}
|
||||
defer func() {
|
||||
c.Writer.Flush()
|
||||
}()
|
||||
|
||||
for _, line := range t {
|
||||
_, err := c.Writer.WriteString(line)
|
||||
result += line
|
||||
if err != nil {
|
||||
logger.Logger(c).WithError(err).Error("write string to client error")
|
||||
return ""
|
||||
}
|
||||
}
|
||||
logger.Logger(c).Info("finish stream text to client")
|
||||
return result
|
||||
}
|
||||
|
||||
func getRelatedQuestionsResp(c context.Context, query string, ss *Sources) string {
|
||||
apiKey, endpoint := getOpenAIConfig(c, "chat")
|
||||
conf := openai.DefaultConfig(apiKey)
|
||||
conf.BaseURL = endpoint
|
||||
client := openai.NewClientWithConfig(conf)
|
||||
request := openai.ChatCompletionRequest{
|
||||
Model: openai.GPT3Dot5Turbo,
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: fmt.Sprintf(MoreQuestionsPrompt(), getSearchContext(ss)) + query,
|
||||
},
|
||||
},
|
||||
Temperature: GetSettings().RAGParams.MoreQuestionsTemperature,
|
||||
MaxTokens: GetSettings().RAGParams.MoreQuestionsMaxTokens,
|
||||
}
|
||||
|
||||
resp, err := client.CreateChatCompletion(
|
||||
context.Background(),
|
||||
request,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Logger(c).WithError(err).Errorf("client.CreateChatCompletion error")
|
||||
}
|
||||
|
||||
mode := 1
|
||||
|
||||
cs := strings.Split(resp.Choices[0].Message.Content, ". ")
|
||||
if len(cs) == 1 {
|
||||
cs = strings.Split(resp.Choices[0].Message.Content, "- ")
|
||||
mode = 2
|
||||
}
|
||||
rq := []string{}
|
||||
for i, line := range cs {
|
||||
if len(line) <= 2 {
|
||||
continue
|
||||
}
|
||||
if i != len(cs)-1 && mode == 1 {
|
||||
line = line[:len(line)-1]
|
||||
}
|
||||
rq = append(rq, line)
|
||||
}
|
||||
|
||||
return parseRelatedQuestionsResp(rq)
|
||||
}
|
||||
|
||||
func parseRelatedQuestionsResp(qs []string) string {
|
||||
q := []struct {
|
||||
Question string `json:"question"`
|
||||
}{}
|
||||
for _, line := range qs {
|
||||
if len(strings.Trim(line, " ")) <= 2 {
|
||||
continue
|
||||
}
|
||||
q = append(q, struct {
|
||||
Question string `json:"question"`
|
||||
}{line})
|
||||
}
|
||||
rawBytes, _ := json.Marshal(q)
|
||||
return string(rawBytes)
|
||||
}
|
||||
|
||||
func getSearchContext(ss *Sources) string {
|
||||
ans := ""
|
||||
for i, ctx := range ss.Contexts {
|
||||
ans = ans + fmt.Sprintf("[[citation:%d]] ", i+1) + ctx.Snippet + "\n\n"
|
||||
}
|
||||
return ans
|
||||
}
|
69
service/settings.go
Normal file
69
service/settings.go
Normal file
@ -0,0 +1,69 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"ai-search/utils"
|
||||
|
||||
"github.com/ilyakaznacheev/cleanenv"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
type AppSettings struct {
|
||||
ListenAddr string `env:"LISTEN_ADDR" env-default:":8080"`
|
||||
DBPath string `env:"DB_PATH" env-default:"/litefs/db"`
|
||||
OpenAIEndpint string `env:"OPENAI_ENDPOINT" env-required:"true"`
|
||||
OpenAIAPIKey string `env:"OPENAI_API_KEY" env-required:"true"`
|
||||
OpenAIWriterPrompt string `env:"OPENAI_WRITER_PROMPT" env-default:"您是一名人工智能写作助手,可以根据先前文本的上下文继续现有文本。给予后面的字符比开始的字符更多的权重/优先级。而且绝对一定要尽量多使用中文!将您的回答限制在 200 个字符以内,但请确保构建完整的句子。"`
|
||||
HttpProxy string `env:"HTTP_PROXY"`
|
||||
IsDebug bool `env:"DEBUG" env-default:"false"`
|
||||
ChatMaxTokens int `env:"CHAT_MAX_TOKENS" env-default:"400"`
|
||||
OpenAIChatEndpoint string `env:"OPENAI_CHAT_ENDPOINT" env-required:"true"`
|
||||
OpenAIChatAPIKey string `env:"OPENAI_CHAT_API_KEY" env-required:"true"`
|
||||
OpenAIChatNetworkDelay int `env:"OPENAI_CHAT_NETWORK_DELAY" env-default:"5"`
|
||||
OpenAIChatQueueLen int `env:"OPENAI_CHAT_QUEUE_LEN" env-default:"10"`
|
||||
CacheSize int `env:"CACHE_SIZE" env-default:"100"` // in MB
|
||||
RAGSearchCount int `env:"RAG_SEARCH_COUNT" env-default:"8"`
|
||||
RAGSearchCacheTime int `env:"RAG_SEARCH_CACHE_TIME" env-default:"1200"` // sec
|
||||
RPCEndpoints RPCEndpoints `env-prefix:"RPC_"`
|
||||
Prompts Prompts `env-prefix:"PROMPT_"`
|
||||
RAGParams RAGParams `env-prefix:"RAG_"`
|
||||
}
|
||||
|
||||
type RPCEndpoints struct {
|
||||
SearchURL string `env:"SEARCH_URL" env-required:"true"`
|
||||
}
|
||||
|
||||
type Prompts struct {
|
||||
RAGPath string `env:"RAG_PATH" env-default:""`
|
||||
MoreQuestionsPath string `env:"MORE_QUESTIONS_PATH" env-default:""`
|
||||
}
|
||||
|
||||
type RAGParams struct {
|
||||
MaxTokens int `env:"MAX_TOKENS" env-default:"2048"`
|
||||
MoreQuestionsMaxTokens int `env:"MORE_QUESTIONS_MAX_TOKENS" env-default:"1024"`
|
||||
Temperature float32 `env:"TEMPERATURE" env-default:"0.9"`
|
||||
MoreQuestionsTemperature float32 `env:"MORE_QUESTIONS_TEMPERATURE" env-default:"0.7"`
|
||||
}
|
||||
|
||||
var (
|
||||
appSetting *AppSettings
|
||||
)
|
||||
|
||||
func init() {
|
||||
godotenv.Load()
|
||||
conf := &AppSettings{}
|
||||
if err := cleanenv.ReadEnv(conf); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
appSetting = conf
|
||||
InitCache()
|
||||
if len(appSetting.Prompts.MoreQuestionsPath) >= 0 {
|
||||
moreQuestionsPrompt = utils.GetFileContent(appSetting.Prompts.MoreQuestionsPath)
|
||||
}
|
||||
if len(appSetting.Prompts.RAGPath) >= 0 {
|
||||
ragPrompt = utils.GetFileContent(appSetting.Prompts.RAGPath)
|
||||
}
|
||||
}
|
||||
|
||||
func GetSettings() *AppSettings {
|
||||
return appSetting
|
||||
}
|
51
service/static.go
Normal file
51
service/static.go
Normal file
@ -0,0 +1,51 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-contrib/static"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type embedFileSystem struct {
|
||||
http.FileSystem
|
||||
}
|
||||
|
||||
func (e embedFileSystem) Exists(prefix string, path string) bool {
|
||||
_, err := e.Open(path)
|
||||
return err == nil
|
||||
}
|
||||
func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem {
|
||||
fsys, err := fs.Sub(fsEmbed, targetPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return embedFileSystem{
|
||||
FileSystem: http.FS(fsys),
|
||||
}
|
||||
}
|
||||
|
||||
func HandleStaticFile(router *gin.Engine, f embed.FS) {
|
||||
root := EmbedFolder(f, "out")
|
||||
router.Use(static.Serve("/", root))
|
||||
staticServer := static.Serve("/", root)
|
||||
router.NoRoute(func(c *gin.Context) {
|
||||
if c.Request.Method == http.MethodGet &&
|
||||
!strings.ContainsRune(c.Request.URL.Path, '.') &&
|
||||
!strings.HasPrefix(c.Request.URL.Path, "/v1/") {
|
||||
if strings.HasSuffix(c.Request.URL.Path, "/") {
|
||||
c.Request.URL.Path += "index.html"
|
||||
staticServer(c)
|
||||
return
|
||||
}
|
||||
if !strings.HasSuffix(c.Request.URL.Path, ".html") {
|
||||
c.Request.URL.Path += ".html"
|
||||
staticServer(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
126
service/types.go
Normal file
126
service/types.go
Normal file
@ -0,0 +1,126 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ragPrompt = `You are a large language AI assistant built by VaalaCat. You are given a user question, and please write clean, concise and accurate answer to the question. You will be given a set of related contexts to the question, each starting with a reference number like [[citation:x]], where x is a number. Please use the context and cite the context at the end of each sentence if applicable.
|
||||
|
||||
Your answer must be correct, accurate and written by an expert using an unbiased and professional tone. Please limit to 1024 tokens. Do not give any information that is not related to the question, and do not repeat. Say "information is missing on" followed by the related topic, if the given context do not provide sufficient information.
|
||||
|
||||
Please cite the contexts with the reference numbers, in the format [citation:x]. If a sentence comes from multiple contexts, please list all applicable citations, like [citation:3][citation:5]. Other than code and specific names and citations, your answer must be written in the same language as the question.
|
||||
|
||||
Here are the set of contexts:
|
||||
|
||||
%s
|
||||
|
||||
Remember, use Chinese more, don't blindly repeat the contexts verbatim. And here is the user question:
|
||||
`
|
||||
moreQuestionsPrompt = `You are a helpful assistant that helps the user to ask related questions, based on user's original question and the related contexts. Please identify worthwhile topics that can be follow-ups, and write questions no longer than 20 words each. Please make sure that specifics, like events, names, locations, are included in follow up questions so they can be asked standalone. For example, if the original question asks about "the Manhattan project", in the follow up question, do not just say "the project", but use the full name "the Manhattan project". Your related questions must be in the same language as the original question.
|
||||
|
||||
Here are the contexts of the question:
|
||||
|
||||
%s
|
||||
|
||||
Remember, use Chinese more, based on the original question and related contexts, suggest three such further questions. Do NOT repeat the original question. Each related question should be no longer than 20 words. Here is the original question:
|
||||
`
|
||||
)
|
||||
|
||||
func RagPrompt() string {
|
||||
return ragPrompt
|
||||
}
|
||||
|
||||
func MoreQuestionsPrompt() string {
|
||||
return moreQuestionsPrompt
|
||||
}
|
||||
|
||||
type SearchReq struct {
|
||||
Query string `json:"query"`
|
||||
SearchUUID string `json:"search_uuid"`
|
||||
}
|
||||
|
||||
type Sources struct {
|
||||
Query string `json:"query"`
|
||||
RID string `json:"rid"`
|
||||
Contexts []Source `json:"contexts"`
|
||||
}
|
||||
|
||||
func (ss *Sources) FromSearchResp(resp *SearchResp, query, rid string) {
|
||||
ss.Query = query
|
||||
ss.RID = rid
|
||||
ctxs := make([]Source, 0)
|
||||
for _, ctx := range resp.Results {
|
||||
ctxs = append(ctxs, Source{
|
||||
ID: ctx.Href,
|
||||
Name: ctx.Title,
|
||||
URL: ctx.Href,
|
||||
Snippet: ctx.Body,
|
||||
})
|
||||
}
|
||||
ss.Contexts = ctxs
|
||||
}
|
||||
|
||||
func (ss *Sources) ToString() string {
|
||||
rawBytes, _ := json.Marshal(ss)
|
||||
return string(rawBytes)
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
IsFamilyFriendly bool `json:"isFamilyFriendly"`
|
||||
DisplayURL string `json:"displayUrl"`
|
||||
Snippet string `json:"snippet"`
|
||||
DeepLinks []DeepLink `json:"deepLinks"`
|
||||
DateLastCrawled time.Time `json:"dateLastCrawled"`
|
||||
CachedPageURL string `json:"cachedPageUrl"`
|
||||
Language string `json:"language"`
|
||||
PrimaryImageOfPage *PrimaryImage `json:"primaryImageOfPage,omitempty"`
|
||||
IsNavigational bool `json:"isNavigational"`
|
||||
}
|
||||
|
||||
type DeepLink struct {
|
||||
Snippet string `json:"snippet"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type PrimaryImage struct {
|
||||
ThumbnailURL string `json:"thumbnailUrl"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
ImageID string `json:"imageId"`
|
||||
}
|
||||
|
||||
func (s *Source) FromSearchResp(resp *SearchResult) {
|
||||
s.ID = resp.Href
|
||||
s.Name = resp.Title
|
||||
s.URL = resp.Href
|
||||
s.Snippet = resp.Body
|
||||
}
|
||||
|
||||
type cachedResult struct {
|
||||
SearchUUID string `json:"search_uuid"`
|
||||
Query string `json:"query"`
|
||||
Result string `json:"result"`
|
||||
}
|
||||
|
||||
func (cs *cachedResult) FromBytes(rawBytes []byte) {
|
||||
json.Unmarshal(rawBytes, cs)
|
||||
}
|
||||
|
||||
func (cs *cachedResult) ToBytes() []byte {
|
||||
rawBytes, _ := json.Marshal(cs)
|
||||
return rawBytes
|
||||
}
|
||||
|
||||
func newCachedResult(searchUUID, query, result string) *cachedResult {
|
||||
return &cachedResult{
|
||||
SearchUUID: searchUUID,
|
||||
Query: query,
|
||||
Result: result,
|
||||
}
|
||||
}
|
BIN
src/app/favicon.ico
Normal file
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
30
src/app/globals.css
Normal file
30
src/app/globals.css
Normal file
@ -0,0 +1,30 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* :root {
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
} */
|
||||
|
||||
/* @media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
}
|
||||
} */
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: linear-gradient(to bottom,
|
||||
transparent,
|
||||
rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb));
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
22
src/app/layout.tsx
Normal file
22
src/app/layout.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "AI Search",
|
||||
description: "AI 搜索",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
26
src/app/page.tsx
Normal file
26
src/app/page.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
import { Footer } from "@/components/search/footer";
|
||||
import { Logo } from "@/components/search/logo";
|
||||
import { PresetQuery } from "@/components/search/preset-query";
|
||||
import { Search } from "@/components/search/search";
|
||||
|
||||
import React from "react";
|
||||
|
||||
export default function SearchHome() {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="absolute inset-0 min-h-[500px] flex items-center justify-center">
|
||||
|
||||
<div className="relative flex flex-col gap-8 px-4 -mt-24">
|
||||
<Logo></Logo>
|
||||
<Search autofucs={true}></Search>
|
||||
<div className="flex gap-2 flex-wrap justify-center">
|
||||
<PresetQuery query="“我思故我在”是谁的名言?"></PresetQuery>
|
||||
<PresetQuery query="太阳和地球相距多远?"></PresetQuery>
|
||||
</div>
|
||||
<Footer></Footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
39
src/app/search/page.tsx
Normal file
39
src/app/search/page.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
import { Result } from "@/components/search/result";
|
||||
import { Search } from "@/components/search/search";
|
||||
import { Title } from "@/components/search/title";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import React, { Suspense, useState } from "react";
|
||||
|
||||
function SearchPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const query = decodeURIComponent(searchParams.get("q") || "");
|
||||
const rid = decodeURIComponent(searchParams.get("rid") || "");
|
||||
const [serverQuery, setServerQuery] = useState("");
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="absolute inset-0 bg-[url('/bg.svg')] mt-20">
|
||||
<div className="mx-auto max-w-3xl absolute inset-4 md:inset-8 bg-white">
|
||||
<div className="h-20 pointer-events-none rounded-t-2xl w-full backdrop-filter absolute top-0 bg-gradient-to-t from-transparent to-white [mask-image:linear-gradient(to_bottom,white,transparent)]"></div>
|
||||
<div className="px-4 md:px-8 pt-6 pb-24 rounded-2xl ring-8 ring-zinc-300/20 border border-zinc-200 h-full overflow-auto">
|
||||
<Title key={query} query={query == undefined || query == null || query == "" ? serverQuery : query}></Title>
|
||||
<Result key={rid} query={query} rid={rid} updateQuery={setServerQuery}></Result>
|
||||
</div>
|
||||
<div className="h-80 pointer-events-none w-full rounded-b-2xl backdrop-filter absolute bottom-0 bg-gradient-to-b from-transparent to-white [mask-image:linear-gradient(to_top,white,transparent)]"></div>
|
||||
<div className="absolute z-10 flex items-center justify-center bottom-6 px-4 md:px-8 w-full">
|
||||
<div className="w-full">
|
||||
<Search autofucs={false} ></Search>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return <Suspense>
|
||||
<SearchPage />
|
||||
</Suspense>
|
||||
}
|
111
src/components/search/answer.tsx
Normal file
111
src/components/search/answer.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/search/popover";
|
||||
import { Skeleton } from "@/components/search/skeleton";
|
||||
import { Wrapper } from "@/components/search/wrapper";
|
||||
import { Source } from "@/types/source";
|
||||
import { BookOpenText } from "lucide-react";
|
||||
import { FC } from "react";
|
||||
import Markdown from "react-markdown";
|
||||
|
||||
export const Answer: FC<{ markdown: string; sources: Source[] }> = ({
|
||||
markdown,
|
||||
sources,
|
||||
}) => {
|
||||
return (
|
||||
<Wrapper
|
||||
title={
|
||||
<>
|
||||
<BookOpenText></BookOpenText> 回答
|
||||
</>
|
||||
}
|
||||
content={
|
||||
markdown && markdown.trim().length > 0 ? (
|
||||
<div className="prose prose-sm max-w-full">
|
||||
<Markdown
|
||||
components={{
|
||||
a: ({ node: _, ...props }) => {
|
||||
if (!props.href) return <></>;
|
||||
const source = sources[+props.href - 1];
|
||||
if (!source) return <></>;
|
||||
return (
|
||||
<span className="inline-block w-4">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<span
|
||||
title={source.name}
|
||||
className="inline-block cursor-pointer transform scale-[60%] no-underline font-medium bg-zinc-300 hover:bg-zinc-400 w-6 text-center h-6 rounded-full origin-top-left"
|
||||
>
|
||||
{props.href}
|
||||
</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align={"start"}
|
||||
className="max-w-screen-md flex flex-col gap-2 bg-white shadow-transparent ring-zinc-50 ring-4 text-xs"
|
||||
>
|
||||
<div className="text-ellipsis overflow-hidden whitespace-nowrap font-medium">
|
||||
{source.name}
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
{source.primaryImageOfPage?.thumbnailUrl && (
|
||||
<div className="flex-none">
|
||||
<img
|
||||
className="rounded h-16 w-16"
|
||||
width={source.primaryImageOfPage?.width}
|
||||
height={source.primaryImageOfPage?.height}
|
||||
src={source.primaryImageOfPage?.thumbnailUrl}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<div className="line-clamp-4 text-zinc-500 break-words">
|
||||
{source.snippet}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="text-ellipsis text-blue-500 overflow-hidden whitespace-nowrap">
|
||||
<a
|
||||
title={source.name}
|
||||
href={source.url}
|
||||
target="_blank"
|
||||
>
|
||||
{source.url}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none flex items-center relative">
|
||||
<img
|
||||
className="h-3 w-3"
|
||||
alt={source.url}
|
||||
src={`https://www.google.com/s2/favicons?domain=${source.url}&sz=${16}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{markdown}
|
||||
</Markdown>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="max-w-sm h-4 bg-zinc-200"></Skeleton>
|
||||
<Skeleton className="max-w-lg h-4 bg-zinc-200"></Skeleton>
|
||||
<Skeleton className="max-w-2xl h-4 bg-zinc-200"></Skeleton>
|
||||
<Skeleton className="max-w-lg h-4 bg-zinc-200"></Skeleton>
|
||||
<Skeleton className="max-w-xl h-4 bg-zinc-200"></Skeleton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
></Wrapper>
|
||||
);
|
||||
};
|
37
src/components/search/footer.tsx
Normal file
37
src/components/search/footer.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { Mails } from "lucide-react";
|
||||
import { FC } from "react";
|
||||
|
||||
export const Footer: FC = () => {
|
||||
return (
|
||||
<div className="text-center flex flex-col items-center text-xs text-zinc-700 gap-1">
|
||||
<div className="text-zinc-400">
|
||||
注意⚠️,回答来自大模型,VaalaAI不为您提供任何保证
|
||||
</div>
|
||||
<div className="text-zinc-400">
|
||||
Vaala☁️/VaalaAI 提供技术支持, 参考
|
||||
<a className="text-blue-500" href="https://github.com/leptonai/search_with_lepton">Lepton AI</a> 开发
|
||||
</div>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<div>如果您需要技术支持,请与我们联系</div>
|
||||
<div>
|
||||
<a
|
||||
className="text-blue-500 font-medium inline-flex gap-1 items-center flex-nowrap text-nowrap"
|
||||
href="mailto:me@vaala.cat"
|
||||
>
|
||||
<Mails size={8} />
|
||||
发送邮件
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center flex-wrap gap-x-4 gap-y-2 mt-2 text-zinc-400">
|
||||
<a className="hover:text-zinc-950" href="https://api.vaa.la/">
|
||||
VaalaAI
|
||||
</a>
|
||||
<a className="hover:text-zinc-950" href="https://vaala.cat/">
|
||||
Blog
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
15
src/components/search/logo.tsx
Normal file
15
src/components/search/logo.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { FC } from "react";
|
||||
|
||||
export const Logo: FC = () => {
|
||||
return (
|
||||
<div className="flex gap-4 items-center justify-center cursor-default select-none relative">
|
||||
<img src="https://vaala.cat/favicon.ico" alt="vaala logo" className="h-10 w-10"></img>
|
||||
<div className="text-center font-medium text-2xl md:text-3xl text-zinc-950 relative text-nowrap">
|
||||
VaalaAI Search
|
||||
</div>
|
||||
<div className="transform scale-75 origin-left border items-center rounded-lg bg-gray-100 px-2 py-1 text-xs font-medium text-zinc-600">
|
||||
beta
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
31
src/components/search/popover.tsx
Normal file
31
src/components/search/popover.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent };
|
17
src/components/search/preset-query.tsx
Normal file
17
src/components/search/preset-query.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { getSearchUrl } from "@/utils/get-search-url";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { FC } from "react";
|
||||
|
||||
export const PresetQuery: FC<{ query: string }> = ({ query }) => {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<div
|
||||
title={query}
|
||||
className="border border-zinc-200/50 text-ellipsis overflow-hidden text-nowrap items-center rounded-lg bg-zinc-100 hover:bg-zinc-200/80 hover:text-zinc-950 px-2 py-1 text-xs font-medium text-zinc-600"
|
||||
onClick={() => { router.push(getSearchUrl(encodeURIComponent(query), nanoid())) }}
|
||||
>
|
||||
{query}
|
||||
</div>
|
||||
);
|
||||
};
|
41
src/components/search/relates.tsx
Normal file
41
src/components/search/relates.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { PresetQuery } from "@/components/search/preset-query";
|
||||
import { Skeleton } from "@/components/search/skeleton";
|
||||
import { Wrapper } from "@/components/search/wrapper";
|
||||
import { Relate } from "@/types/relate";
|
||||
import { MessageSquareQuote } from "lucide-react";
|
||||
import { FC } from "react";
|
||||
|
||||
export const Relates: FC<{ relates: Relate[] | null }> = ({ relates }) => {
|
||||
return (
|
||||
<Wrapper
|
||||
title={
|
||||
<>
|
||||
<MessageSquareQuote></MessageSquareQuote> 相关
|
||||
</>
|
||||
}
|
||||
content={
|
||||
<div className="flex gap-2 flex-col">
|
||||
{relates !== null ? (
|
||||
relates.length > 0 ? (
|
||||
relates.map(({ question }) => (
|
||||
<PresetQuery key={question} query={question}></PresetQuery>
|
||||
))
|
||||
) : (
|
||||
<div className="text-sm">没有相关问题.</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<Skeleton className="w-full h-5 bg-zinc-200/80"></Skeleton>
|
||||
<Skeleton className="w-full h-5 bg-zinc-200/80"></Skeleton>
|
||||
<Skeleton className="w-full h-5 bg-zinc-200/80"></Skeleton>
|
||||
</>
|
||||
)}
|
||||
<div className="text-xs text-zinc-500 text-center mt-4">
|
||||
<div>VaalaAI回答完成后,您可以通过分享链接将回答分享给朋友!</div>
|
||||
<div>有效期20分钟</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
></Wrapper>
|
||||
);
|
||||
};
|
74
src/components/search/result.tsx
Normal file
74
src/components/search/result.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
import { Answer } from "@/components/search/answer";
|
||||
import { Relates } from "@/components/search/relates";
|
||||
import { Sources } from "@/components/search/sources";
|
||||
import { Relate } from "@/types/relate";
|
||||
import { Source } from "@/types/source";
|
||||
import { parseStreaming } from "@/utils/parse-streaming";
|
||||
import { Annoyed, Mails } from "lucide-react";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
|
||||
export const Result: FC<{ query: string; rid: string, updateQuery: (query: string) => void }> = ({ query, rid, updateQuery }) => {
|
||||
const [sources, setSources] = useState<Source[]>([]);
|
||||
const [markdown, setMarkdown] = useState<string>("");
|
||||
const [relates, setRelates] = useState<Relate[] | null>(null);
|
||||
const [error, setError] = useState<number | null>(null);
|
||||
const [serverQuery, setServerQuery] = useState("");
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
void parseStreaming(
|
||||
controller,
|
||||
query,
|
||||
rid,
|
||||
"query",
|
||||
setSources,
|
||||
setMarkdown,
|
||||
setRelates,
|
||||
setServerQuery,
|
||||
setError,
|
||||
);
|
||||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, [query, rid]);
|
||||
|
||||
useEffect(() => {
|
||||
updateQuery(serverQuery);
|
||||
}, [serverQuery, updateQuery]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8" >
|
||||
<Answer markdown={markdown} sources={sources}></Answer>
|
||||
<Sources sources={sources}></Sources>
|
||||
<Relates relates={relates}></Relates>
|
||||
{error && (
|
||||
<div className="absolute inset-4 flex items-center justify-center bg-white/40 backdrop-blur-sm">
|
||||
<div className="p-4 bg-white shadow-2xl rounded text-blue-500 font-medium flex gap-4">
|
||||
<Annoyed></Annoyed>
|
||||
{error === 429
|
||||
? "抱歉,您的请求过于频繁,Vaala被累死了QwQ,请稍后再试吧。"
|
||||
: <div className="text-center">{
|
||||
error === 400 ?
|
||||
<div>请求不合法,换个关键词试试吧!</div>
|
||||
: (error === 401 ?
|
||||
<div>要不试试登录呢?</div>
|
||||
: (error === 410 ?
|
||||
(true ? <div>内容过期啦,换个关键词再搜一下呢~</div> : <div>对方分享的内容已过期,请登录后换个关键词搜索~</div>)
|
||||
: (<div>服务器出错,Vaala紧急修复中!
|
||||
<div>
|
||||
<a
|
||||
className="text-blue-500 font-medium inline-flex gap-1 items-center flex-nowrap text-nowrap"
|
||||
href="mailto:me@vaala.cat"
|
||||
>
|
||||
<Mails size={8} />
|
||||
点我提交问题
|
||||
</a>
|
||||
</div>
|
||||
</div>)))
|
||||
}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
43
src/components/search/search.tsx
Normal file
43
src/components/search/search.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
import { getSearchUrl } from "@/utils/get-search-url";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { FC, useState } from "react";
|
||||
|
||||
export const Search: FC<{ autofucs?: boolean }> = ({ autofucs }) => {
|
||||
const [value, setValue] = useState("");
|
||||
const router = useRouter()
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (value) {
|
||||
setValue("");
|
||||
router.push(getSearchUrl(encodeURIComponent(value), nanoid()));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<label
|
||||
className="relative bg-white flex items-center justify-center border ring-8 ring-zinc-300/20 py-2 px-2 rounded-lg gap-2 focus-within:border-zinc-300"
|
||||
htmlFor="search-bar"
|
||||
>
|
||||
<input
|
||||
id="search-bar"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
autoFocus={autofucs}
|
||||
placeholder="询问Vaala任何问题 ..."
|
||||
className="px-2 pr-6 text-sm w-full rounded-md flex-1 outline-none bg-white"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
key={Math.random()}
|
||||
className="w-auto py-1 px-2 bg-black border-black text-white fill-white active:scale-95 border overflow-hidden relative rounded-xl"
|
||||
>
|
||||
<ArrowRight size={16} />
|
||||
</button>
|
||||
</label>
|
||||
</form>
|
||||
);
|
||||
};
|
13
src/components/search/skeleton.tsx
Normal file
13
src/components/search/skeleton.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { cn } from "@/utils/cn";
|
||||
import { HTMLAttributes } from "react";
|
||||
|
||||
function Skeleton({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton };
|
70
src/components/search/sources.tsx
Normal file
70
src/components/search/sources.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { Skeleton } from "@/components/search/skeleton";
|
||||
import { Wrapper } from "@/components/search/wrapper";
|
||||
import { Source } from "@/types/source";
|
||||
import { BookText } from "lucide-react";
|
||||
import { FC } from "react";
|
||||
|
||||
const SourceItem: FC<{ source: Source; index: number }> = ({
|
||||
source,
|
||||
index,
|
||||
}) => {
|
||||
const { id, name, url } = source;
|
||||
const domain = new URL(url).hostname;
|
||||
return (
|
||||
<div
|
||||
className="relative text-xs py-3 px-3 bg-zinc-100 hover:bg-zinc-200 rounded-lg flex flex-col gap-2"
|
||||
key={id}
|
||||
>
|
||||
<a href={url} target="_blank" className="absolute inset-0"></a>
|
||||
<div className="font-medium text-zinc-950 text-ellipsis overflow-hidden whitespace-nowrap break-words">
|
||||
{name}
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="text-ellipsis whitespace-nowrap break-all text-zinc-400 overflow-hidden w-full">
|
||||
{index + 1} - {domain}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none flex items-center">
|
||||
<img
|
||||
className="h-3 w-3"
|
||||
alt={domain}
|
||||
src={`https://www.google.com/s2/favicons?domain=${domain}&sz=${16}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Sources: FC<{ sources: Source[] }> = ({ sources }) => {
|
||||
return (
|
||||
<Wrapper
|
||||
title={
|
||||
<>
|
||||
<BookText></BookText> 参考
|
||||
</>
|
||||
}
|
||||
content={
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
{sources.length > 0 ? (
|
||||
sources.map((item, index) => (
|
||||
<SourceItem
|
||||
key={item.id}
|
||||
index={index}
|
||||
source={item}
|
||||
></SourceItem>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<Skeleton className="max-w-sm h-16 bg-zinc-200/80"></Skeleton>
|
||||
<Skeleton className="max-w-sm h-16 bg-zinc-200/80"></Skeleton>
|
||||
<Skeleton className="max-w-sm h-16 bg-zinc-200/80"></Skeleton>
|
||||
<Skeleton className="max-w-sm h-16 bg-zinc-200/80"></Skeleton>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
></Wrapper>
|
||||
);
|
||||
};
|
30
src/components/search/title.tsx
Normal file
30
src/components/search/title.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
import { getSearchUrl } from "@/utils/get-search-url";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export const Title = ({ query }: { query: string }) => {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<div className="flex items-center pb-4 mb-6 border-b gap-4">
|
||||
<div
|
||||
className="flex-1 text-lg sm:text-xl text-black text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
title={query}
|
||||
>
|
||||
{query}
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<button
|
||||
onClick={() => {
|
||||
router.push(getSearchUrl(encodeURIComponent(query), nanoid()));
|
||||
}}
|
||||
type="button"
|
||||
className="rounded flex gap-2 items-center bg-transparent px-2 py-1 text-xs font-semibold text-blue-500 hover:bg-zinc-100"
|
||||
>
|
||||
<RefreshCcw size={12}></RefreshCcw>重新生成
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
13
src/components/search/wrapper.tsx
Normal file
13
src/components/search/wrapper.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { FC, ReactNode } from "react";
|
||||
|
||||
export const Wrapper: FC<{
|
||||
title: ReactNode;
|
||||
content: ReactNode;
|
||||
}> = ({ title, content }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<div className="flex gap-2 text-blue-500">{title}</div>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
};
|
3
src/types/relate.ts
Normal file
3
src/types/relate.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface Relate {
|
||||
question: string;
|
||||
}
|
19
src/types/source.ts
Normal file
19
src/types/source.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export interface Source {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
isFamilyFriendly: boolean;
|
||||
displayUrl: string;
|
||||
snippet: string;
|
||||
deepLinks: { snippet: string; name: string; url: string }[];
|
||||
dateLastCrawled: string;
|
||||
cachedPageUrl: string;
|
||||
language: string;
|
||||
primaryImageOfPage?: {
|
||||
thumbnailUrl: string;
|
||||
width: number;
|
||||
height: number;
|
||||
imageId: string;
|
||||
};
|
||||
isNavigational: boolean;
|
||||
}
|
217
src/utils/base64.ts
Normal file
217
src/utils/base64.ts
Normal file
@ -0,0 +1,217 @@
|
||||
/*
|
||||
This file is a work taken from the following location: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
|
||||
|
||||
It has been modified using the below algorithms to make it work for the standard web encoding.
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Egor Nepomnyaschih
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
/*
|
||||
// This constant can also be computed with the following algorithm:
|
||||
const base64Chars = [],
|
||||
A = "A".charCodeAt(0),
|
||||
a = "a".charCodeAt(0),
|
||||
n = "0".charCodeAt(0);
|
||||
for (let i = 0; i < 26; ++i) {
|
||||
base64Chars.push(String.fromCharCode(A + i));
|
||||
}
|
||||
for (let i = 0; i < 26; ++i) {
|
||||
base64Chars.push(String.fromCharCode(a + i));
|
||||
}
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
base64Chars.push(String.fromCharCode(n + i));
|
||||
}
|
||||
base64Chars.push("+");
|
||||
base64Chars.push("/");
|
||||
*/
|
||||
|
||||
const base64Chars = [
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"D",
|
||||
"E",
|
||||
"F",
|
||||
"G",
|
||||
"H",
|
||||
"I",
|
||||
"J",
|
||||
"K",
|
||||
"L",
|
||||
"M",
|
||||
"N",
|
||||
"O",
|
||||
"P",
|
||||
"Q",
|
||||
"R",
|
||||
"S",
|
||||
"T",
|
||||
"U",
|
||||
"V",
|
||||
"W",
|
||||
"X",
|
||||
"Y",
|
||||
"Z",
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
"e",
|
||||
"f",
|
||||
"g",
|
||||
"h",
|
||||
"i",
|
||||
"j",
|
||||
"k",
|
||||
"l",
|
||||
"m",
|
||||
"n",
|
||||
"o",
|
||||
"p",
|
||||
"q",
|
||||
"r",
|
||||
"s",
|
||||
"t",
|
||||
"u",
|
||||
"v",
|
||||
"w",
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"+",
|
||||
"/",
|
||||
];
|
||||
|
||||
/*
|
||||
// This constant can also be computed with the following algorithm:
|
||||
const l = 256, base64codes = new Uint8Array(l);
|
||||
for (let i = 0; i < l; ++i) {
|
||||
base64codes[i] = 255; // invalid character
|
||||
}
|
||||
base64Chars.forEach((char, index) => {
|
||||
base64codes[char.charCodeAt(0)] = index;
|
||||
});
|
||||
base64codes["=".charCodeAt(0)] = 0; // ignored anyway, so we just need to prevent an error
|
||||
*/
|
||||
|
||||
const base64Codes = [
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 62, 255, 255, 255, 63,
|
||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, 255, 0, 255, 255,
|
||||
255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
|
||||
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255,
|
||||
255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
|
||||
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
|
||||
];
|
||||
|
||||
function getBase64Code(charCode: number) {
|
||||
if (charCode >= base64Codes.length) {
|
||||
throw new Error("Unable to parse base64 string.");
|
||||
}
|
||||
|
||||
const code = base64Codes[charCode];
|
||||
if (code === 255) {
|
||||
throw new Error("Unable to parse base64 string.");
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
export function getBase64FromBytes(bytes: number[] | Uint8Array): string {
|
||||
let result = "",
|
||||
i,
|
||||
l = bytes.length;
|
||||
|
||||
for (i = 2; i < l; i += 3) {
|
||||
result += base64Chars[bytes[i - 2] >> 2];
|
||||
result += base64Chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
|
||||
result += base64Chars[((bytes[i - 1] & 0x0F) << 2) | (bytes[i] >> 6)];
|
||||
result += base64Chars[bytes[i] & 0x3F];
|
||||
}
|
||||
|
||||
if (i === l + 1) { // 1 octet yet to write
|
||||
result += base64Chars[bytes[i - 2] >> 2];
|
||||
result += base64Chars[(bytes[i - 2] & 0x03) << 4];
|
||||
result += "==";
|
||||
}
|
||||
|
||||
if (i === l) { // 2 octets yet to write
|
||||
result += base64Chars[bytes[i - 2] >> 2];
|
||||
result += base64Chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
|
||||
result += base64Chars[(bytes[i - 1] & 0x0F) << 2];
|
||||
result += "=";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getBase64WebEncodingFromBytes(bytes: number[] | Uint8Array): string {
|
||||
return getBase64FromBytes(bytes)
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
}
|
||||
|
||||
export function getBytesFromBase64(str: string): Uint8Array {
|
||||
str = str.replace(/\-/g, "+")
|
||||
.replace(/\_/g, "/");
|
||||
|
||||
// if (str.length % 4 !== 0) {
|
||||
// throw new Error("Unable to parse base64 string.");
|
||||
// }
|
||||
|
||||
const index = str.indexOf("=");
|
||||
|
||||
if (index !== -1 && index < str.length - 2) {
|
||||
throw new Error("Unable to parse base64 string.");
|
||||
}
|
||||
|
||||
let missingOctets = str.endsWith("==") ? 2 : str.endsWith("=") ? 1 : 0,
|
||||
n = str.length,
|
||||
result = new Uint8Array(3 * (n / 4)),
|
||||
buffer;
|
||||
|
||||
for (let i = 0, j = 0; i < n; i += 4, j += 3) {
|
||||
buffer =
|
||||
getBase64Code(str.charCodeAt(i)) << 18 |
|
||||
getBase64Code(str.charCodeAt(i + 1)) << 12 |
|
||||
getBase64Code(str.charCodeAt(i + 2)) << 6 |
|
||||
getBase64Code(str.charCodeAt(i + 3));
|
||||
result[j] = buffer >> 16;
|
||||
result[j + 1] = (buffer >> 8) & 0xFF;
|
||||
result[j + 2] = buffer & 0xFF;
|
||||
}
|
||||
|
||||
return result.subarray(0, result.length - missingOctets);
|
||||
}
|
6
src/utils/cn.ts
Normal file
6
src/utils/cn.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
26
src/utils/fetch-stream.ts
Normal file
26
src/utils/fetch-stream.ts
Normal file
@ -0,0 +1,26 @@
|
||||
async function pump(
|
||||
reader: ReadableStreamDefaultReader<Uint8Array>,
|
||||
controller: ReadableStreamDefaultController,
|
||||
onChunk?: (chunk: Uint8Array) => void,
|
||||
onDone?: () => void,
|
||||
): Promise<ReadableStreamReadResult<Uint8Array> | undefined> {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
onDone && onDone();
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
onChunk && onChunk(value);
|
||||
controller.enqueue(value);
|
||||
return pump(reader, controller, onChunk, onDone);
|
||||
}
|
||||
export const fetchStream = (
|
||||
response: Response,
|
||||
onChunk?: (chunk: Uint8Array) => void,
|
||||
onDone?: () => void,
|
||||
): ReadableStream<string> => {
|
||||
const reader = response.body!.getReader();
|
||||
return new ReadableStream({
|
||||
start: (controller) => pump(reader, controller, onChunk, onDone),
|
||||
});
|
||||
};
|
4
src/utils/get-search-url.ts
Normal file
4
src/utils/get-search-url.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const getSearchUrl = (query: string, search_uuid: string) => {
|
||||
const prefix = "/search";
|
||||
return `${prefix}?q=${encodeURIComponent(query)}&rid=${search_uuid}`;
|
||||
};
|
85
src/utils/parse-streaming.ts
Normal file
85
src/utils/parse-streaming.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { Source } from "@/types/source";
|
||||
import { Relate } from "@/types/relate";
|
||||
import { fetchStream } from "@/utils/fetch-stream";
|
||||
|
||||
const LLM_SPLIT = "__LLM_RESPONSE__";
|
||||
const RELATED_SPLIT = "__RELATED_QUESTIONS__";
|
||||
|
||||
export const parseStreaming = async (
|
||||
controller: AbortController,
|
||||
query: string,
|
||||
search_uuid: string,
|
||||
mode: "query" | "share",
|
||||
onSources: (value: Source[]) => void,
|
||||
onMarkdown: (value: string) => void,
|
||||
onRelates: (value: Relate[]) => void,
|
||||
onServerQuery: (value: string) => void,
|
||||
onError?: (status: number) => void,
|
||||
) => {
|
||||
const decoder = new TextDecoder();
|
||||
let uint8Array = new Uint8Array();
|
||||
let chunks = "";
|
||||
let sourcesEmitted = false;
|
||||
const response = await fetch(mode === "query" ? `/v1/chat/search` : `/v1/chat/share`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "*./*",
|
||||
},
|
||||
signal: controller.signal,
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
search_uuid,
|
||||
}),
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
onError?.(response.status);
|
||||
return;
|
||||
}
|
||||
const markdownParse = (text: string) => {
|
||||
onMarkdown(
|
||||
text
|
||||
.replace(/\[\[([cC])itation/g, "[citation")
|
||||
.replace(/[cC]itation:(\d+)]]/g, "citation:$1]")
|
||||
.replace(/\[\[([cC]itation:\d+)]](?!])/g, `[$1]`)
|
||||
.replace(/\[[cC]itation:(\d+)]/g, "[citation]($1)")
|
||||
.replace(/\[(\d+)\]/g, "[citation]($1)")
|
||||
.replace(/\[引用:(\d+)\]/g, "[citation]($1)")
|
||||
.replace(/\[引用(\d+)\]/g, "[citation]($1)"),
|
||||
);
|
||||
};
|
||||
fetchStream(
|
||||
response,
|
||||
(chunk) => {
|
||||
uint8Array = new Uint8Array([...uint8Array, ...chunk]);
|
||||
chunks = decoder.decode(uint8Array, { stream: true });
|
||||
if (chunks.includes(LLM_SPLIT)) {
|
||||
const [sources, rest] = chunks.split(LLM_SPLIT);
|
||||
if (!sourcesEmitted) {
|
||||
try {
|
||||
onServerQuery((JSON.parse(sources)).query);
|
||||
onSources((JSON.parse(sources)).contexts);
|
||||
} catch (e) {
|
||||
onSources([]);
|
||||
}
|
||||
}
|
||||
sourcesEmitted = true;
|
||||
if (rest.includes(RELATED_SPLIT)) {
|
||||
const [md] = rest.split(RELATED_SPLIT);
|
||||
markdownParse(md);
|
||||
} else {
|
||||
markdownParse(rest);
|
||||
}
|
||||
}
|
||||
},
|
||||
() => {
|
||||
const [_, relates] = chunks.split(RELATED_SPLIT);
|
||||
try {
|
||||
onRelates(JSON.parse(relates));
|
||||
window.history.replaceState(null, "", "/search?rid=" + search_uuid);
|
||||
} catch (e) {
|
||||
onRelates([]);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
20
tailwind.config.ts
Normal file
20
tailwind.config.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
backgroundImage: {
|
||||
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
||||
"gradient-conic":
|
||||
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
export default config;
|
40
tsconfig.json
Normal file
40
tsconfig.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"target": "ES2020",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
11
utils/file.go
Normal file
11
utils/file.go
Normal file
@ -0,0 +1,11 @@
|
||||
package utils
|
||||
|
||||
import "os"
|
||||
|
||||
func GetFileContent(path string) string {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(content)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user