diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000..ae221f64a13 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +Dockerfile +.vscode/ +.idea +.gitignore +LICENSE +README.md +node_modules/ +.svelte-kit/ diff --git a/.env b/.env index a070571b50a..33f72ae6210 100644 --- a/.env +++ b/.env @@ -3,14 +3,24 @@ MONGODB_URL=#your mongodb URL here MONGODB_DB_NAME=chat-ui +MONGODB_DIRECT_CONNECTION=false + COOKIE_NAME=hf-chat HF_ACCESS_TOKEN=#hf_ from from https://huggingface.co/settings/token +HF_API_ROOT=https://api-inference.huggingface.co/models + +# used to activate search with web functionality. disabled if none are defined. choose one of the following: +SERPER_API_KEY=#your serper.dev api key here +SERPAPI_KEY=#your serpapi key here # Parameters to enable "Sign in with HF" -HF_CLIENT_ID= -HF_CLIENT_SECRET= +OPENID_CLIENT_ID= +OPENID_CLIENT_SECRET= +OPENID_SCOPES="openid profile" # Add "email" for some providers like Google that do not provide preferred_username +OPENID_PROVIDER_URL=https://huggingface.co # for Google, use https://accounts.google.com -# 'name', 'userMessageToken', 'assistantMessageToken', 'parameters' are required + +# 'name', 'userMessageToken', 'assistantMessageToken' are required MODELS=`[ { "name": "OpenAssistant/oasst-sft-4-pythia-12b-epoch-3.5", @@ -45,10 +55,33 @@ MODELS=`[ ]` OLD_MODELS=`[]`# any removed models, `{ name: string, displayName?: string, id?: string }` -PUBLIC_ORIGIN=#https://hf.co +PUBLIC_ORIGIN=#https://huggingface.co +PUBLIC_SHARE_PREFIX=#https://hf.co/chat PUBLIC_GOOGLE_ANALYTICS_ID=#G-XXXXXXXX / Leave empty to disable PUBLIC_DEPRECATED_GOOGLE_ANALYTICS_ID=#UA-XXXXXXXX-X / Leave empty to disable +PUBLIC_ANNOUNCEMENT_BANNERS=`[ + { + "title": "Llama v2 is live on HuggingChat! 🦙", + "linkTitle": "Announcement", + "linkHref": "https://huggingface.co/blog/llama2" + } +]` PARQUET_EXPORT_DATASET= PARQUET_EXPORT_HF_TOKEN= PARQUET_EXPORT_SECRET= + +RATE_LIMIT= # requests per minute +MESSAGES_BEFORE_LOGIN=# how many messages a user can send in a conversation before having to login. set to 0 to force login right away + +PUBLIC_APP_NAME=ChatUI # name used as title throughout the app +PUBLIC_APP_ASSETS=chatui # used to find logos & favicons in static/$PUBLIC_APP_ASSETS +PUBLIC_APP_COLOR=blue # can be any of tailwind colors: https://tailwindcss.com/docs/customizing-colors#default-color-palette +PUBLIC_APP_DATA_SHARING=#set to 1 to enable options & text regarding data sharing +PUBLIC_APP_DISCLAIMER=#set to 1 to show a disclaimer on login page + +# PUBLIC_APP_NAME=HuggingChat +# PUBLIC_APP_ASSETS=huggingchat +# PUBLIC_APP_COLOR=yellow +# PUBLIC_APP_DATA_SHARING=1 +# PUBLIC_APP_DISCLAIMER=1 \ No newline at end of file diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index a3c0aa45707..66117b3e0d0 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -25,3 +25,25 @@ jobs: - name: "Checking type errors" run: | npm run check + test: + runs-on: ubuntu-latest + timeout-minutes: 10 + + services: + mongodb: + image: mongo:6.0.5 + ports: + - 27017:27017 + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: "18" + cache: "npm" + - run: | + npm ci + - name: "Tests" + run: | + npm run test diff --git a/Dockerfile b/Dockerfile index 8276e4aaacb..9d5791e55ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,31 @@ # read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker # you will also find guides on how best to write your Dockerfile +FROM node:19 as builder-production -FROM node:19 +WORKDIR /app -RUN npm install -g pm2 +COPY --link --chown=1000 package-lock.json package.json ./ +RUN --mount=type=cache,target=/app/.npm \ + npm set cache /app/.npm && \ + npm ci --omit=dev -WORKDIR /app +FROM builder-production as builder + +RUN --mount=type=cache,target=/app/.npm \ + npm set cache /app/.npm && \ + npm ci COPY --link --chown=1000 . . -RUN npm i +RUN --mount=type=secret,id=DOTENV_LOCAL,dst=.env.local \ + npm run build + +FROM node:19-slim + +RUN npm install -g pm2 -RUN --mount=type=secret,id=DOTENV_LOCAL,dst=.env.local npm run build +COPY --from=builder-production /app/node_modules /app/node_modules +COPY --link --chown=1000 package.json /app/package.json +COPY --from=builder /app/build /app/build -CMD pm2 start build/index.js -i $CPU_CORES --no-daemon +CMD pm2 start /app/build/index.js -i $CPU_CORES --no-daemon diff --git a/PRIVACY.md b/PRIVACY.md index dcdcaa13b94..ccd7fb2c252 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,10 +1,10 @@ ## Privacy -> Last updated: May 11th, 2023 +> Last updated: July 23, 2023 -In this `v0.1` of HuggingChat, users are not authenticated in any way, i.e. this app doesn't have access to your HF user account even if you're logged in to huggingface.co. The app is only using an anonymous session cookie. ❗️ Warning ❗️ this means if you switch browsers or clear cookies, you will currently lose your conversations. +Users of HuggingChat are authenticated through their HF user account. -By default, your conversations are shared with the model's authors (for the `v0.1` model, to Open Assistant) to improve their training data and model over time. Model authors are the custodians of the data collected by their model, even if it's hosted on our platform. +By default, your conversations may be shared with the respective models' authors (e.g. if you're chatting with the Open Assistant model, to Open Assistant) to improve their training data and model over time. Model authors are the custodians of the data collected by their model, even if it's hosted on our platform. If you disable data sharing in your settings, your conversations will not be used for any downstream usage (including for research or model training purposes), and they will only be stored to let you access past conversations. You can click on the Delete icon to delete any past conversation at any moment. @@ -14,9 +14,9 @@ If you disable data sharing in your settings, your conversations will not be use The goal of this app is to showcase that it is now (May 2023) possible to build an open source alternative to ChatGPT. 💪 -For now, it's running OpenAssistant's [latest LLaMA based model](https://huggingface.co/OpenAssistant/oasst-sft-6-llama-30b-xor) (which is one of the current best open source chat models), but the plan in the longer-term is to expose all good-quality chat models from the Hub. +For now, it's running both OpenAssistant's [latest LLaMA based model](https://huggingface.co/OpenAssistant/oasst-sft-6-llama-30b-xor) (which is one of the current best open source chat models) as well as [Meta's newer Llama 2](https://huggingface.co/meta-llama/Llama-2-70b-chat-hf), but the plan in the longer-term is to expose all good-quality chat models from the Hub. -We are not affiliated with Open Assistant, but if you want to contribute to the training data for the next generation of open models, please consider contributing to https://open-assistant.io/ ❤️ +We are not affiliated with Open Assistant nor Meta AI, but if you want to contribute to the training data for the next generation of open models, please consider contributing to https://open-assistant.io/ or https://ai.meta.com/llama/ ❤️ ## Technical details @@ -26,7 +26,7 @@ This app is running in a [Space](https://huggingface.co/docs/hub/spaces-overview The inference backend is running the optimized [text-generation-inference](https://github.com/huggingface/text-generation-inference) on HuggingFace's Inference API infrastructure. -It is therefore possible to deploy a copy of this app to a Space and customize it (swap model, add some UI elements, or store user messages according to your own Terms and conditions) +It is therefore possible to deploy a copy of this app to a Space and customize it (swap model, add some UI elements, or store user messages according to your own Terms and conditions). You can also 1-click deploy your own instance using the [Chat UI Spaces Docker template](https://huggingface.co/new-space?template=huggingchat/chat-ui-template). We welcome any feedback on this app: please participate to the public discussion at https://huggingface.co/spaces/huggingchat/chat-ui/discussions diff --git a/README.md b/README.md index 46acbf846a6..77536eb1384 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ +# Test New LLMs (CodeLlama, Llama2, etc.) + +Notice you forked chat-ui. if you're trying to test other LLMs (codellama, wizardcoder, etc.) with it, I just wrote a [1-click proxy](https://github.com/BerriAI/litellm#openai-proxy-server) to translate openai calls to huggingface, anthropic, togetherai, etc. api calls. + +**code** +``` +$ pip install litellm +$ litellm --model huggingface/bigcode/starcoder +#INFO: Uvicorn running on http://0.0.0.0:8000 +$ aider --openai-api-base http://0.0.0.0:8000 +``` + +I'd love to know if this solves a problem for you + --- title: chat-ui emoji: 🔥 @@ -14,51 +28,212 @@ app_port: 3000 ![Chat UI repository thumbnail](https://huggingface.co/datasets/huggingface/documentation-images/raw/f038917dd40d711a72d654ab1abfc03ae9f177e6/chat-ui-repo-thumbnail.svg) -A chat interface using open source models, eg OpenAssistant. It is a SvelteKit app and it powers the [HuggingChat app on hf.co/chat](https://huggingface.co/chat). +A chat interface using open source models, eg OpenAssistant or Llama. It is a SvelteKit app and it powers the [HuggingChat app on hf.co/chat](https://huggingface.co/chat). + +0. [No Setup Deploy](#no-setup-deploy) +1. [Setup](#setup) +2. [Launch](#launch) +3. [Extra parameters](#extra-parameters) +4. [Deploying to a HF Space](#deploying-to-a-hf-space) +5. [Building](#building) + +##  No Setup Deploy + +If you don't want to configure, setup, and launch your own Chat UI yourself, you can use this option as a fast deploy alternative. + +You can deploy your own customized Chat UI instance with any supported LLM of your choice with only a few clicks to Hugging Face Spaces thanks to the Chat UI Spaces Docker template. Get started [here](https://huggingface.co/new-space?template=huggingchat/chat-ui-template). +If you'd like to deploy a model with gated access or a model in a private repository, you can simply provide `HUGGING_FACE_HUB_TOKEN` in [Space secrets](https://huggingface.co/docs/hub/spaces-overview#managing-secrets-and-environment-variables). You need to set its value to an access token you can get from [here](https://huggingface.co/settings/tokens). + +Read the full tutorial [here](https://huggingface.co/docs/hub/spaces-sdks-docker-chatui#chatui-on-spaces). + +## Setup + +The default config for Chat UI is stored in the `.env` file. You will need to override some values to get Chat UI to run locally. This is done in `.env.local`. + +Start by creating a `.env.local` file in the root of the repository. The bare minimum config you need to get Chat UI to run locally is the following: + +```bash +MONGODB_URL= +HF_ACCESS_TOKEN= +``` + +### Database + +The chat history is stored in a MongoDB instance, and having a DB instance available is needed for Chat UI to work. + +You can use a local MongoDB instance. The easiest way is to spin one up using docker: + +```bash +docker run -d -p 27017:27017 --name mongo-chatui mongo:latest +``` + +In which case the url of your DB will be `MONGODB_URL=mongodb://localhost:27017`. + +Alternatively, you can use a [free MongoDB Atlas](https://www.mongodb.com/pricing) instance for this, Chat UI should fit comfortably within their free tier. After which you can set the `MONGODB_URL` variable in `.env.local` to match your instance. + +### Hugging Face Access Token + +You will need a Hugging Face access token to run Chat UI locally, if you use a remote inference endpoint. You can get one from [your Hugging Face profile](https://huggingface.co/settings/tokens). ## Launch +After you're done with the `.env.local` file you can run Chat UI locally with: + ```bash npm install npm run dev ``` -## Environment +## Extra parameters + +### OpenID connect + +The login feature is disabled by default and users are attributed a unique ID based on their browser. But if you want to use OpenID to authenticate your users, you can add the following to your `.env.local` file: + +```bash +OPENID_PROVIDER_URL= +OPENID_CLIENT_ID= +OPENID_CLIENT_SECRET= +``` + +These variables will enable the openID sign-in modal for users. + +### Theming + +You can use a few environment variables to customize the look and feel of chat-ui. These are by default: + +``` +PUBLIC_APP_NAME=ChatUI +PUBLIC_APP_ASSETS=chatui +PUBLIC_APP_COLOR=blue +PUBLIC_APP_DATA_SHARING= +PUBLIC_APP_DISCLAIMER= +``` + +- `PUBLIC_APP_NAME` The name used as a title throughout the app. +- `PUBLIC_APP_ASSETS` Is used to find logos & favicons in `static/$PUBLIC_APP_ASSETS`, current options are `chatui` and `huggingchat`. +- `PUBLIC_APP_COLOR` Can be any of the [tailwind colors](https://tailwindcss.com/docs/customizing-colors#default-color-palette). +- `PUBLIC_APP_DATA_SHARING` Can be set to 1 to add a toggle in the user settings that lets your users opt-in to data sharing with models creator. +- `PUBLIC_APP_DISCLAIMER` If set to 1, we show a disclaimer about generated outputs on login. + +### Web Search -Default configuration is in `.env`. Put custom config and secrets in `.env.local`, it will override the values in `.env`. +You can enable the web search by adding either `SERPER_API_KEY` ([serper.dev](https://serper.dev/)) or `SERPAPI_KEY` ([serpapi.com](https://serpapi.com/)) to your `.env.local`. -Check out [.env](./.env) to see what needs to be set. +### Custom models -Basically you need to create a `.env.local` with the following contents: +You can customize the parameters passed to the model or even use a new model by updating the `MODELS` variable in your `.env.local`. The default one can be found in `.env` and looks like this : ``` -MONGODB_URL= -HF_ACCESS_TOKEN= + +MODELS=`[ + { + "name": "OpenAssistant/oasst-sft-4-pythia-12b-epoch-3.5", + "datasetName": "OpenAssistant/oasst1", + "description": "A good alternative to ChatGPT", + "websiteUrl": "https://open-assistant.io", + "userMessageToken": "<|prompter|>", # This does not need to be a token, can be any string + "assistantMessageToken": "<|assistant|>", # This does not need to be a token, can be any string + "messageEndToken": "<|endoftext|>", # This does not need to be a token, can be any string + # "userMessageEndToken": "", # Applies only to user messages, messageEndToken has no effect if specified. Can be any string. + # "assistantMessageEndToken": "", # Applies only to assistant messages, messageEndToken has no effect if specified. Can be any string. + "preprompt": "Below are a series of dialogues between various people and an AI assistant. The AI tries to be helpful, polite, honest, sophisticated, emotionally aware, and humble-but-knowledgeable. The assistant is happy to help with almost anything, and will do its best to understand exactly what is needed. It also tries to avoid giving false or misleading information, and it caveats when it isn't entirely sure about the right answer. That said, the assistant is practical and really does its best, and doesn't let caution get too much in the way of being useful.\n-----\n", + "promptExamples": [ + { + "title": "Write an email from bullet list", + "prompt": "As a restaurant owner, write a professional email to the supplier to get these products every week: \n\n- Wine (x10)\n- Eggs (x24)\n- Bread (x12)" + }, { + "title": "Code a snake game", + "prompt": "Code a basic snake game in python, give explanations for each step." + }, { + "title": "Assist in a task", + "prompt": "How do I make a delicious lemon cheesecake?" + } + ], + "parameters": { + "temperature": 0.9, + "top_p": 0.95, + "repetition_penalty": 1.2, + "top_k": 50, + "truncate": 1000, + "max_new_tokens": 1024, + "stop": ["<|endoftext|>"] # This does not need to be tokens, can be any list of strings + } + } +]` + ``` -## Duplicating to a Space +You can change things like the parameters, or customize the preprompt to better suit your needs. You can also add more models by adding more objects to the array, with different preprompts for example. -Create a `DOTENV_LOCAL` secret to your space with the following contents: +#### Running your own models using a custom endpoint + +If you want to, instead of hitting models on the Hugging Face Inference API, you can run your own models locally. + +A good option is to hit a [text-generation-inference](https://github.com/huggingface/text-generation-inference) endpoint. This is what is done in the official [Chat UI Spaces Docker template](https://huggingface.co/new-space?template=huggingchat/chat-ui-template) for instance: both this app and a text-generation-inference server run inside the same container. + +To do this, you can add your own endpoints to the `MODELS` variable in `.env.local`, by adding an `"endpoints"` key for each model in `MODELS`. ``` -MONGODB_URL= -HF_ACCESS_TOKEN= + +{ +// rest of the model config here +"endpoints": [{"url": "https://HOST:PORT/generate_stream"}] +} + ``` -Where the contents in `<...>` are replaced by the MongoDB URL and your [HF Access Token](https://huggingface.co/settings/tokens). +If `endpoints` is left unspecified, ChatUI will look for the model on the hosted Hugging Face inference API using the model name. + +#### Custom endpoint authorization + +Custom endpoints may require authorization, depending on how you configure them. Authentication will usually be set either with `Basic` or `Bearer`. + +For `Basic` we will need to generate a base64 encoding of the username and password. -## Running Local Inference +`echo -n "USER:PASS" | base64` -Both the example above use the HF Inference API or HF Endpoints API. +> VVNFUjpQQVNT -If you want to run the model locally, you need to run this inference server locally: https://github.com/huggingface/text-generation-inference +For `Bearer` you can use a token, which can be grabbed from [here](https://huggingface.co/settings/tokens). -And add this to your `.env.local`: +You can then add the generated information and the `authorization` parameter to your `.env.local`. ``` -MODELS=`[{"name": "...", "endpoints": [{"url": "127.0.0.1:8080/generate_stream"}]}]` + +"endpoints": [ +{ +"url": "https://HOST:PORT/generate_stream", +"authorization": "Basic VVNFUjpQQVNT", +} +] + ``` +#### Models hosted on multiple custom endpoints + +If the model being hosted will be available on multiple servers/instances add the `weight` parameter to your `.env.local`. The `weight` will be used to determine the probability of requesting a particular endpoint. + +``` + +"endpoints": [ +{ +"url": "https://HOST:PORT/generate_stream", +"weight": 1 +} +{ +"url": "https://HOST:PORT/generate_stream", +"weight": 2 +} +... +] + +``` + +## Deploying to a HF Space + +Create a `DOTENV_LOCAL` secret to your HF space with the content of your .env.local, and they will be picked up automatically when you run. + ## Building To create a production version of your app: diff --git a/package-lock.json b/package-lock.json index 5ad497735b6..986edd266ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "chat-ui", - "version": "0.1.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chat-ui", - "version": "0.1.0", + "version": "0.4.0", "dependencies": { "@huggingface/hub": "^0.5.1", "@huggingface/inference": "^2.2.0", @@ -14,20 +14,25 @@ "date-fns": "^2.29.3", "dotenv": "^16.0.3", "highlight.js": "^11.7.0", + "jsdom": "^22.0.0", "marked": "^4.3.0", "mongodb": "^5.3.0", "nanoid": "^4.0.2", + "openid-client": "^5.4.2", "parquetjs": "^0.11.2", "postcss": "^8.4.21", + "serpapi": "^1.1.1", "tailwind-scrollbar": "^3.0.0", "tailwindcss": "^3.3.1", "zod": "^3.21.4" }, "devDependencies": { "@iconify-json/carbon": "^1.1.16", + "@iconify-json/eos-icons": "^1.1.6", "@sveltejs/adapter-node": "^1.2.4", "@sveltejs/kit": "^1.15.10", "@tailwindcss/typography": "^0.5.9", + "@types/jsdom": "^21.1.1", "@types/marked": "^4.0.8", "@types/parquetjs": "^0.10.3", "@typescript-eslint/eslint-plugin": "^5.45.0", @@ -43,7 +48,8 @@ "tslib": "^2.4.1", "typescript": "^4.9.3", "unplugin-icons": "^0.16.1", - "vite": "^4.0.0" + "vite": "^4.0.0", + "vitest": "^0.31.0" } }, "node_modules/@antfu/install-pkg": { @@ -537,6 +543,15 @@ "@iconify/types": "*" } }, + "node_modules/@iconify-json/eos-icons": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@iconify-json/eos-icons/-/eos-icons-1.1.6.tgz", + "integrity": "sha512-A1kUcVbgrdlBBacFcs+srwnfSH9htQvlgbi0u6Jf38lp4PZAK3InXVbVySrJKx//FJtSMdnpZh0b89yjcAIIBg==", + "dev": true, + "dependencies": { + "@iconify/types": "*" + } + }, "node_modules/@iconify/types": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", @@ -878,6 +893,29 @@ "node": ">=4" } }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/chai": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", + "dev": true + }, + "node_modules/@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, "node_modules/@types/cookie": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz", @@ -890,6 +928,17 @@ "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", "dev": true }, + "node_modules/@types/jsdom": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.1.tgz", + "integrity": "sha512-cZFuoVLtzKP3gmq9eNosUL1R50U+USkbLtUQ1bYVgl/lKp0FZM7Cq4aIHAL8oIvQ17uSHi7jXPtfDOdjPwBE7A==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -943,6 +992,12 @@ "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", "dev": true }, + "node_modules/@types/tough-cookie": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", + "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", + "dev": true + }, "node_modules/@types/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -1145,6 +1200,107 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@vitest/expect": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.31.0.tgz", + "integrity": "sha512-Jlm8ZTyp6vMY9iz9Ny9a0BHnCG4fqBa8neCF6Pk/c/6vkUk49Ls6UBlgGAU82QnzzoaUs9E/mUhq/eq9uMOv/g==", + "dev": true, + "dependencies": { + "@vitest/spy": "0.31.0", + "@vitest/utils": "0.31.0", + "chai": "^4.3.7" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.31.0.tgz", + "integrity": "sha512-H1OE+Ly7JFeBwnpHTrKyCNm/oZgr+16N4qIlzzqSG/YRQDATBYmJb/KUn3GrZaiQQyL7GwpNHVZxSQd6juLCgw==", + "dev": true, + "dependencies": { + "@vitest/utils": "0.31.0", + "concordance": "^5.0.4", + "p-limit": "^4.0.0", + "pathe": "^1.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.31.0.tgz", + "integrity": "sha512-5dTXhbHnyUMTMOujZPB0wjFjQ6q5x9c8TvAsSPUNKjp1tVU7i9pbqcKPqntyu2oXtmVxKbuHCqrOd+Ft60r4tg==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.0", + "pathe": "^1.1.0", + "pretty-format": "^27.5.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.31.0.tgz", + "integrity": "sha512-IzCEQ85RN26GqjQNkYahgVLLkULOxOm5H/t364LG0JYb3Apg0PsYCHLBYGA006+SVRMWhQvHlBBCyuByAMFmkg==", + "dev": true, + "dependencies": { + "tinyspy": "^2.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.31.0.tgz", + "integrity": "sha512-kahaRyLX7GS1urekRXN2752X4gIgOGVX4Wo8eDUGUkTWlGpXzf5ZS6N9RUUS+Re3XEE8nVGqNyxkSxF5HXlGhQ==", + "dev": true, + "dependencies": { + "concordance": "^5.0.4", + "loupe": "^2.3.6", + "pretty-format": "^27.5.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" + }, "node_modules/acorn": { "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", @@ -1166,6 +1322,26 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1243,6 +1419,20 @@ "node": ">=8" } }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/autoprefixer": { "version": "10.4.14", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", @@ -1313,6 +1503,12 @@ "integrity": "sha512-u4cBQNepWxYA55FunZSM7wMi55yQaN0otnhhilNoWHq0MfOfJeQx0v0mRRpolGOExPjZcl6FtB0BB8Xkb88F0g==", "optional": true }, + "node_modules/blueimp-md5": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", + "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", + "dev": true + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1401,7 +1597,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dev": true, "dependencies": { "streamsearch": "^1.1.0" }, @@ -1409,6 +1604,15 @@ "node": ">=10.16.0" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1445,6 +1649,24 @@ } ] }, + "node_modules/chai": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1461,6 +1683,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -1515,6 +1746,17 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -1534,6 +1776,25 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concordance": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/concordance/-/concordance-5.0.4.tgz", + "integrity": "sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==", + "dev": true, + "dependencies": { + "date-time": "^3.1.0", + "esutils": "^2.0.3", + "fast-diff": "^1.2.0", + "js-string-escape": "^1.0.1", + "lodash": "^4.17.15", + "md5-hex": "^3.0.1", + "semver": "^7.3.2", + "well-known-symbols": "^2.0.0" + }, + "engines": { + "node": ">=10.18.0 <11 || >=12.14.0 <13 || >=14" + } + }, "node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -1568,6 +1829,53 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/data-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/date-fns": { "version": "2.29.3", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", @@ -1580,11 +1888,22 @@ "url": "https://opencollective.com/date-fns" } }, + "node_modules/date-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz", + "integrity": "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==", + "dev": true, + "dependencies": { + "time-zone": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -1597,6 +1916,23 @@ } } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1612,6 +1948,14 @@ "node": ">=0.10.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -1661,6 +2005,17 @@ "node": ">=6.0.0" } }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/dotenv": { "version": "16.0.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", @@ -1674,6 +2029,17 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.359.tgz", "integrity": "sha512-OoVcngKCIuNXtZnsYoqlCvr0Cf3NIPzDIgwUfI9bdTFjXCrr79lI0kwQstLPZ7WhCezLlGksZk/BFAzoXC7GDw==" }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es6-promise": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", @@ -2030,6 +2396,12 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, "node_modules/fast-glob": { "version": "3.2.12", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", @@ -2134,6 +2506,19 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", @@ -2169,6 +2554,15 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -2304,6 +2698,42 @@ "node": ">=12.0.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -2313,6 +2743,17 @@ "node": ">=10.17.0" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -2450,6 +2891,11 @@ "node": ">=8" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -2485,6 +2931,14 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jose": { + "version": "4.14.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", + "integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-sdsl": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", @@ -2495,6 +2949,15 @@ "url": "https://opencollective.com/js-sdsl" } }, + "node_modules/js-string-escape": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", + "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2507,10 +2970,74 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "node_modules/jsdom": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.0.0.tgz", + "integrity": "sha512-p5ZTEb5h+O+iU02t0GfEjAnkdYPrQSkfuTSMkMYyIoMvUNEHsbG0bHHbfXIcfTqD2UfvjQX7mmgiFsyRwGscVw==", + "dependencies": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, "node_modules/json-stable-stringify-without-jsonify": { @@ -2519,6 +3046,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -2593,6 +3126,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "node_modules/lodash.castarray": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", @@ -2611,11 +3150,19 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -2656,6 +3203,18 @@ "node": ">= 12" } }, + "node_modules/md5-hex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz", + "integrity": "sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==", + "dev": true, + "dependencies": { + "blueimp-md5": "^2.10.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/memory-pager": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", @@ -2700,6 +3259,25 @@ "node": ">=10.0.0" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -2750,6 +3328,18 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mlly": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.2.1.tgz", + "integrity": "sha512-1aMEByaWgBPEbWV2BOPEMySRrzl7rIHXmQxam4DM8jVjalTQDjpN2ZKOLUrwyhfZQO7IXHml2StcHMhooDeEEQ==", + "dev": true, + "dependencies": { + "acorn": "^8.8.2", + "pathe": "^1.1.0", + "pkg-types": "^1.0.3", + "ufo": "^1.1.2" + } + }, "node_modules/mongodb": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.3.0.tgz", @@ -2812,8 +3402,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mz": { "version": "2.7.0", @@ -2892,6 +3481,11 @@ "node": ">=8" } }, + "node_modules/nwsapi": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.4.tgz", + "integrity": "sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g==" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2916,6 +3510,14 @@ "node": ">=0.10" } }, + "node_modules/oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2939,6 +3541,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openid-client": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.4.2.tgz", + "integrity": "sha512-lIhsdPvJ2RneBm3nGBBhQchpe3Uka//xf7WPHTIglery8gnckvW7Bd9IaQzekzXJvWthCMyi/xVEyGW0RFPytw==", + "dependencies": { + "jose": "^4.14.1", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -3026,6 +3650,17 @@ "node": ">=0.6.19" } }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3066,6 +3701,21 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.0.tgz", + "integrity": "sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -3098,6 +3748,17 @@ "node": ">= 6" } }, + "node_modules/pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, "node_modules/postcss": { "version": "8.4.23", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", @@ -3363,6 +4024,37 @@ } } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -3380,6 +4072,11 @@ "teleport": ">=0.2.0" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -3410,6 +4107,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -3441,6 +4144,11 @@ "url": "https://github.com/sponsors/mysticatea" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -3506,6 +4214,11 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -3540,6 +4253,11 @@ "node": ">=6" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/sander": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", @@ -3576,6 +4294,17 @@ "node": ">=6" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/semver": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", @@ -3591,6 +4320,14 @@ "node": ">=10" } }, + "node_modules/serpapi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/serpapi/-/serpapi-1.1.1.tgz", + "integrity": "sha512-t5Bqu/6VMJ9naX8K+qCgUStpZOaNQFvIM4AudhMJLS6sqQT/EHaYrhGidDZHVx8QvcEdY6y1wNlxizOCtvJtUQ==", + "dependencies": { + "undici": "^5.12.0" + } + }, "node_modules/set-cookie-parser": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", @@ -3618,6 +4355,12 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -3706,11 +4449,22 @@ "memory-pager": "^1.0.2" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.3.tgz", + "integrity": "sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==", + "dev": true + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "dev": true, "engines": { "node": ">=10.0.0" } @@ -3760,6 +4514,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.0.1.tgz", + "integrity": "sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==", + "dev": true, + "dependencies": { + "acorn": "^8.8.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/sucrase": { "version": "3.32.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz", @@ -4003,6 +4769,11 @@ "node": ">=12" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, "node_modules/tailwind-scrollbar": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-3.0.0.tgz", @@ -4093,6 +4864,15 @@ "node": ">= 4.1.0" } }, + "node_modules/time-zone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz", + "integrity": "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/tiny-glob": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", @@ -4103,6 +4883,30 @@ "globrex": "^0.1.2" } }, + "node_modules/tinybench": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.0.tgz", + "integrity": "sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.5.0.tgz", + "integrity": "sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.0.tgz", + "integrity": "sha512-7eORpyqImoOvkQJCSkL0d0mB4NHHIFAy4b1u8PHdDa7SjGS2njzl6/lyGoZLm+eyYEtlUmFGE0rFj66SWxZgQQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4123,6 +4927,20 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", + "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tr46": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", @@ -4178,6 +4996,15 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -4203,11 +5030,16 @@ "node": ">=4.2.0" } }, + "node_modules/ufo": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.2.tgz", + "integrity": "sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==", + "dev": true + }, "node_modules/undici": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.0.tgz", "integrity": "sha512-fR9RXCc+6Dxav4P9VV/sp5w3eFiSdOjJYsbtWfd4s5L5C4ogyuVpdKIVHeW0vV1MloM65/f7W45nR9ZxwVdyiA==", - "dev": true, "dependencies": { "busboy": "^1.6.0" }, @@ -4215,6 +5047,14 @@ "node": ">=14.0" } }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/unplugin": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.3.1.tgz", @@ -4299,6 +5139,15 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -4357,6 +5206,29 @@ } } }, + "node_modules/vite-node": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.31.0.tgz", + "integrity": "sha512-8x1x1LNuPvE2vIvkSB7c1mApX5oqlgsxzHQesYF7l5n1gKrEmrClIiZuOFbFDQcjLsmcWSwwmrWrcGWm9Fxc/g==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "mlly": "^1.2.0", + "pathe": "^1.1.0", + "picocolors": "^1.0.0", + "vite": "^3.0.0 || ^4.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vitefu": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.4.tgz", @@ -4371,6 +5243,95 @@ } } }, + "node_modules/vitest": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.31.0.tgz", + "integrity": "sha512-JwWJS9p3GU9GxkG7eBSmr4Q4x4bvVBSswaCFf1PBNHiPx00obfhHRJfgHcnI0ffn+NMlIh9QGvG75FlaIBdKGA==", + "dev": true, + "dependencies": { + "@types/chai": "^4.3.4", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "@vitest/expect": "0.31.0", + "@vitest/runner": "0.31.0", + "@vitest/snapshot": "0.31.0", + "@vitest/spy": "0.31.0", + "@vitest/utils": "0.31.0", + "acorn": "^8.8.2", + "acorn-walk": "^8.2.0", + "cac": "^6.7.14", + "chai": "^4.3.7", + "concordance": "^5.0.4", + "debug": "^4.3.4", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.0", + "pathe": "^1.1.0", + "picocolors": "^1.0.0", + "std-env": "^3.3.2", + "strip-literal": "^1.0.1", + "tinybench": "^2.4.0", + "tinypool": "^0.5.0", + "vite": "^3.0.0 || ^4.0.0", + "vite-node": "0.31.0", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@vitest/browser": "*", + "@vitest/ui": "*", + "happy-dom": "*", + "jsdom": "*", + "playwright": "*", + "safaridriver": "*", + "webdriverio": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "playwright": { + "optional": true + }, + "safaridriver": { + "optional": true + }, + "webdriverio": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -4394,6 +5355,34 @@ "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==", "dev": true }, + "node_modules/well-known-symbols": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz", + "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "engines": { + "node": ">=12" + } + }, "node_modules/whatwg-url": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", @@ -4421,6 +5410,22 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -4455,11 +5460,23 @@ } } }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { "version": "1.10.2", diff --git a/package.json b/package.json index 0862d294bd2..258ba0265c3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "name": "chat-ui", - "version": "0.1.0", + "version": "0.4.0", "private": true, + "packageManager": "npm@9.5.0", "scripts": { "dev": "vite dev", "build": "vite build", @@ -9,13 +10,16 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --plugin-search-dir . --check . && eslint .", - "format": "prettier --plugin-search-dir . --write ." + "format": "prettier --plugin-search-dir . --write .", + "test": "MONGODB_URL=mongodb://127.0.0.1:27017/ vitest" }, "devDependencies": { "@iconify-json/carbon": "^1.1.16", + "@iconify-json/eos-icons": "^1.1.6", "@sveltejs/adapter-node": "^1.2.4", "@sveltejs/kit": "^1.15.10", "@tailwindcss/typography": "^0.5.9", + "@types/jsdom": "^21.1.1", "@types/marked": "^4.0.8", "@types/parquetjs": "^0.10.3", "@typescript-eslint/eslint-plugin": "^5.45.0", @@ -31,21 +35,25 @@ "tslib": "^2.4.1", "typescript": "^4.9.3", "unplugin-icons": "^0.16.1", - "vite": "^4.0.0" + "vite": "^4.0.0", + "vitest": "^0.31.0" }, "type": "module", "dependencies": { - "@huggingface/inference": "^2.2.0", "@huggingface/hub": "^0.5.1", + "@huggingface/inference": "^2.2.0", "autoprefixer": "^10.4.14", "date-fns": "^2.29.3", "dotenv": "^16.0.3", "highlight.js": "^11.7.0", + "jsdom": "^22.0.0", "marked": "^4.3.0", "mongodb": "^5.3.0", "nanoid": "^4.0.2", + "openid-client": "^5.4.2", "parquetjs": "^0.11.2", "postcss": "^8.4.21", + "serpapi": "^1.1.1", "tailwind-scrollbar": "^3.0.0", "tailwindcss": "^3.3.1", "zod": "^3.21.4" diff --git a/src/app.d.ts b/src/app.d.ts index a4ea1d124d2..dfd942be68d 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,7 +1,7 @@ /// /// -import type { ObjectId } from "mongodb"; +import type { User } from "$lib/types/User"; // See https://kit.svelte.dev/docs/types#app // for information about these interfaces @@ -10,7 +10,7 @@ declare global { // interface Error {} interface Locals { sessionId: string; - userId?: ObjectId; + user?: User; } // interface PageData {} // interface Platform {} diff --git a/src/app.html b/src/app.html index b9badddc131..f17eea3e26d 100644 --- a/src/app.html +++ b/src/app.html @@ -2,9 +2,7 @@ - - HuggingChat - - -
-

- HuggingChat -
- v{PUBLIC_VERSION} -
-

-

- This application is for demonstration purposes only. -

-

- AI is an area of active research with known problems such as biased generation and - misinformation. Do not use this application for high-stakes decisions or advice. -

-

- Your conversations will be shared with model authors unless you disable it from your settings. -

-
- - {#each Object.entries(settings) as [key, val]} - - {/each} - -
-
-
diff --git a/src/lib/components/LoginModal.svelte b/src/lib/components/LoginModal.svelte new file mode 100644 index 00000000000..ba94d916d3f --- /dev/null +++ b/src/lib/components/LoginModal.svelte @@ -0,0 +1,76 @@ + + + +
+

+ + {PUBLIC_APP_NAME} +
+ v{PUBLIC_VERSION} +
+

+ {#if $page.data.requiresLogin} +

+ Please Sign in with Hugging Face to continue +

+ {/if} +

+ Disclaimer: AI is an area of active research with known problems such as biased generation and + misinformation. Do not use this application for high-stakes decisions or advice. +

+ {#if PUBLIC_APP_DATA_SHARING} +

+ Your conversations will be shared with model authors unless you disable it from your + settings. +

+ {/if} +
+ {#if $page.data.requiresLogin} + + {:else} + + {#each Object.entries(settings) as [key, val]} + + {/each} + + {/if} +
+
+
diff --git a/src/lib/components/MobileNav.svelte b/src/lib/components/MobileNav.svelte index cb9c2ec1fbd..7225e83dcba 100644 --- a/src/lib/components/MobileNav.svelte +++ b/src/lib/components/MobileNav.svelte @@ -40,7 +40,7 @@ bind:this={openEl}> {title} - diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte index 90559b5eae0..c161ad33088 100644 --- a/src/lib/components/Modal.svelte +++ b/src/lib/components/Modal.svelte @@ -33,7 +33,10 @@ onDestroy(() => { if (!browser) return; - document.getElementById("app")?.removeAttribute("inert"); + // remove inert attribute if this is the last modal + if (document.querySelectorAll('[role="dialog"]:not(#app *)').length === 1) { + document.getElementById("app")?.removeAttribute("inert"); + } }); diff --git a/src/lib/components/ModelCardMetadata.svelte b/src/lib/components/ModelCardMetadata.svelte index 2576a95f596..97f55d30f61 100644 --- a/src/lib/components/ModelCardMetadata.svelte +++ b/src/lib/components/ModelCardMetadata.svelte @@ -3,7 +3,7 @@ import CarbonArrowUpRight from "~icons/carbon/arrow-up-right"; import type { Model } from "$lib/types/Model"; - export let model: Pick; + export let model: Pick; export let variant: "light" | "dark" = "light"; @@ -15,7 +15,7 @@ : 'text-gray-800 dark:bg-gray-100 dark:text-gray-600'}" >  page - {#if model.datasetName} + {#if model.datasetName || model.datasetUrl} diff --git a/src/lib/components/NavMenu.svelte b/src/lib/components/NavMenu.svelte index c63fc8f5cad..38eab499381 100644 --- a/src/lib/components/NavMenu.svelte +++ b/src/lib/components/NavMenu.svelte @@ -4,27 +4,34 @@ import Logo from "$lib/components/icons/Logo.svelte"; import { switchTheme } from "$lib/switchTheme"; - import { PUBLIC_ORIGIN } from "$env/static/public"; + import { PUBLIC_APP_NAME, PUBLIC_ORIGIN } from "$env/static/public"; import NavConversationItem from "./NavConversationItem.svelte"; + import type { LayoutData } from "../../routes/$types"; const dispatch = createEventDispatcher<{ shareConversation: { id: string; title: string }; clickSettings: void; + clickLogout: void; }>(); export let conversations: Array<{ id: string; title: string; }> = []; + + export let canLogin: boolean; + export let user: LayoutData["user"]; + + export let loginModalVisible;
- - HuggingChat + + {PUBLIC_APP_NAME} New Chat @@ -40,32 +47,61 @@ diff --git a/src/lib/components/OpenWebSearchResults.svelte b/src/lib/components/OpenWebSearchResults.svelte new file mode 100644 index 00000000000..4b8fff3eb21 --- /dev/null +++ b/src/lib/components/OpenWebSearchResults.svelte @@ -0,0 +1,114 @@ + + +
+ + {#if error} + + {:else if loading} + + {:else} + + {/if} + Web search + +
+ +
+
+ +
+ {#if webSearchMessages.length === 0} +
+ +
+ {:else} +
    + {#each webSearchMessages as message} + {#if message.type === "update"} +
  1. +
    +
    +

    + {message.message} +

    +
    + {#if message.args} +

    + {message.args} +

    + {/if} +
  2. + {:else if message.type === "error"} +
  3. +
    + +

    + {message.message} +

    +
    + {#if message.args} +

    + {message.args} +

    + {/if} +
  4. + {/if} + {/each} +
+ {/if} +
+
+ + diff --git a/src/lib/components/SettingsModal.svelte b/src/lib/components/SettingsModal.svelte index 914ee15151c..5644499ad9d 100644 --- a/src/lib/components/SettingsModal.svelte +++ b/src/lib/components/SettingsModal.svelte @@ -7,59 +7,113 @@ import type { Settings } from "$lib/types/Settings"; import { enhance } from "$app/forms"; import { base } from "$app/paths"; + import { PUBLIC_APP_DATA_SHARING } from "$env/static/public"; + import type { Model } from "$lib/types/Model"; export let settings: Pick; + export let models: Array; + + let shareConversationsWithModelAuthors = settings.shareConversationsWithModelAuthors; + let isConfirmingDeletion = false; const dispatch = createEventDispatcher<{ close: void }>(); -
{ - dispatch("close"); - }} - method="post" - action="{base}/settings" - > +

Settings

+ { + dispatch("close"); + }} + method="post" + action="{base}/settings" + > + {#if PUBLIC_APP_DATA_SHARING} + - +

+ Sharing your data will help improve the training data and make open models better over + time. +

+

+ You can change this setting at any time, it applies to all your conversations. +

+
+

Read more about model authors:

+ +
+ {/if} + (isConfirmingDeletion = true)} + > + + + + -

- Sharing your data will help improve the training data and make open models better over time. -

-

- You can change this setting at any time, it applies to all your conversations. -

-

- Read more about this model's authors, - Open Assistant. -

- - + {#if isConfirmingDeletion} + (isConfirmingDeletion = false)}> +
{ + dispatch("close"); + }} + method="post" + action="{base}/conversations?/delete" + class="flex w-full flex-col gap-5 p-6" + > +
+

Are you sure?

+ +
+

+ This action will delete all your conversations. This cannot be undone. +

+ +
+
+ {/if} +
diff --git a/src/lib/components/StopGeneratingBtn.svelte b/src/lib/components/StopGeneratingBtn.svelte index 32bd1db6742..fd29e9c12e7 100644 --- a/src/lib/components/StopGeneratingBtn.svelte +++ b/src/lib/components/StopGeneratingBtn.svelte @@ -1,17 +1,13 @@ diff --git a/src/lib/components/Switch.svelte b/src/lib/components/Switch.svelte index bdae385bbe1..013bde8dff2 100644 --- a/src/lib/components/Switch.svelte +++ b/src/lib/components/Switch.svelte @@ -5,7 +5,9 @@
diff --git a/src/lib/components/WebSearchToggle.svelte b/src/lib/components/WebSearchToggle.svelte new file mode 100644 index 00000000000..66295e7637c --- /dev/null +++ b/src/lib/components/WebSearchToggle.svelte @@ -0,0 +1,27 @@ + + +
+ +
Search web
+
+ +
+

+ When enabled, the model will try to complement its answer with information queried from the + web. +

+
+
+
diff --git a/src/lib/components/chat/ChatInput.svelte b/src/lib/components/chat/ChatInput.svelte index a7e97b2f2e3..6ceb9f4a7e1 100644 --- a/src/lib/components/chat/ChatInput.svelte +++ b/src/lib/components/chat/ChatInput.svelte @@ -37,7 +37,7 @@
@@ -50,6 +50,7 @@ bind:this={textareaElement} {disabled} on:keydown={handleKeydown} + on:keypress {placeholder} />
diff --git a/src/lib/components/chat/ChatIntroduction.svelte b/src/lib/components/chat/ChatIntroduction.svelte index 3fb68c6bb74..d301e617054 100644 --- a/src/lib/components/chat/ChatIntroduction.svelte +++ b/src/lib/components/chat/ChatIntroduction.svelte @@ -1,5 +1,6 @@ @@ -26,8 +31,8 @@
- - HuggingChat + + {PUBLIC_APP_NAME}
@@ -40,14 +45,17 @@
- - GitHub repo - + {#each announcementBanners as banner} + + {banner.linkTitle} + + {/each} + {#if isModelsModalOpen} (isModelsModalOpen = false)} /> {/if} diff --git a/src/lib/components/chat/ChatMessage.svelte b/src/lib/components/chat/ChatMessage.svelte index fc0b55086ec..f9c8dc83577 100644 --- a/src/lib/components/chat/ChatMessage.svelte +++ b/src/lib/components/chat/ChatMessage.svelte @@ -9,8 +9,13 @@ import IconLoading from "../icons/IconLoading.svelte"; import CarbonRotate360 from "~icons/carbon/rotate-360"; import CarbonDownload from "~icons/carbon/download"; + import CarbonThumbsUp from "~icons/carbon/thumbs-up"; + import CarbonThumbsDown from "~icons/carbon/thumbs-down"; import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken"; import type { Model } from "$lib/types/Model"; + import type { WebSearchMessage } from "$lib/types/WebSearch"; + + import OpenWebSearchResults from "../OpenWebSearchResults.svelte"; function sanitizeMd(md: string) { let ret = md @@ -38,9 +43,16 @@ export let model: Model; export let message: Message; export let loading = false; + export let isAuthor = true; export let readOnly = false; + export let isTapped = false; + + export let webSearchMessages: WebSearchMessage[] = []; - const dispatch = createEventDispatcher<{ retry: void }>(); + const dispatch = createEventDispatcher<{ + retry: { content: string; id: Message["id"] }; + vote: { score: Message["score"]; id: Message["id"] }; + }>(); let contentEl: HTMLElement; let loadingEl: IconLoading; @@ -82,21 +94,39 @@ $: downloadLink = message.from === "user" ? `${$page.url.pathname}/message/${message.id}/prompt` : undefined; + + let webSearchIsDone = true; + + $: webSearchIsDone = + webSearchMessages.length > 0 && + webSearchMessages[webSearchMessages.length - 1].type === "result"; {#if message.from === "assistant"} -
+
(isTapped = !isTapped)} + on:keypress={() => (isTapped = !isTapped)} + >
- {#if !message.content} - + {#if webSearchMessages && webSearchMessages.length > 0} + + {/if} + {#if !message.content && (webSearchIsDone || (webSearchMessages && webSearchMessages.length === 0))} + {/if} +
+ {#if isAuthor && !loading && message.content} +
+ + +
+ {/if}
{/if} {#if message.from === "user"}
-
+
{message.content.trim()}
{#if !loading} @@ -137,7 +201,7 @@ class="cursor-pointer rounded-lg border border-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 dark:border-gray-800 dark:text-gray-400 dark:hover:text-gray-300 md:hidden lg:-right-2" title="Retry" type="button" - on:click={() => dispatch("retry")} + on:click={() => dispatch("retry", { content: message.content, id: message.id })} > diff --git a/src/lib/components/chat/ChatMessages.svelte b/src/lib/components/chat/ChatMessages.svelte index 6a0f97002c7..a8c89b1ca0e 100644 --- a/src/lib/components/chat/ChatMessages.svelte +++ b/src/lib/components/chat/ChatMessages.svelte @@ -2,26 +2,29 @@ import type { Message } from "$lib/types/Message"; import { snapScrollToBottom } from "$lib/actions/snapScrollToBottom"; import ScrollToBottomBtn from "$lib/components/ScrollToBottomBtn.svelte"; - import { createEventDispatcher, tick } from "svelte"; - - import ChatIntroduction from "./ChatIntroduction.svelte"; - import ChatMessage from "./ChatMessage.svelte"; + import { tick } from "svelte"; import { randomUUID } from "$lib/utils/randomUuid"; import type { Model } from "$lib/types/Model"; import type { LayoutData } from "../../../routes/$types"; - - const dispatch = createEventDispatcher<{ retry: { id: Message["id"]; content: string } }>(); + import ChatIntroduction from "./ChatIntroduction.svelte"; + import ChatMessage from "./ChatMessage.svelte"; + import type { WebSearchMessage } from "$lib/types/WebSearch"; export let messages: Message[]; export let loading: boolean; export let pending: boolean; + export let isAuthor: boolean; export let currentModel: Model; export let settings: LayoutData["settings"]; export let models: Model[]; export let readOnly: boolean; + export let searches: Record; + let webSearchArray: Array = []; let chatContainer: HTMLElement; + export let webSearchMessages: WebSearchMessage[] = []; + async function scrollToBottom() { await tick(); chatContainer.scrollTop = chatContainer.scrollHeight; @@ -31,21 +34,35 @@ $: if (messages[messages.length - 1]?.from === "user") { scrollToBottom(); } + + $: messages, + (webSearchArray = messages.map((message, idx) => { + if (message.webSearchId) { + return searches[message.webSearchId] ?? []; + } else if (idx === messages.length - 1) { + return webSearchMessages; + } else { + return []; + } + }));
-
+
{#each messages as message, i} dispatch("retry", { id: message.id, content: message.content })} + model={currentModel} + webSearchMessages={webSearchArray[i]} + on:retry + on:vote /> {:else} @@ -54,9 +71,10 @@ {/if} -
+
= {}; + export let loginRequired = false; $: isReadOnly = !models.some((model) => model.id === currentModel.id); + let loginModalOpen = false; let message: string; const dispatch = createEventDispatcher<{ @@ -37,6 +47,9 @@
+ {#if loginModalOpen} + (loginModalOpen = false)} /> + {/if} { if (!loading) dispatch("retry", ev.detail); }} @@ -53,11 +70,17 @@
- dispatch("stop")} - /> +
+ {#if settings?.searchEnabled} + + {/if} + {#if loading} + dispatch("stop")} + /> + {/if} +
{ + if (loginRequired) loginModalOpen = true; + }} maxRows={4} disabled={isReadOnly} /> - + + {#if loading} + + + {:else} + + {/if}

Model: {currentModel.displayName} dispatch("share")} > - +

Share this conversation
{/if} diff --git a/src/lib/components/icons/IconLoading.svelte b/src/lib/components/icons/IconLoading.svelte index e79f006f38f..878fd9d6c99 100644 --- a/src/lib/components/icons/IconLoading.svelte +++ b/src/lib/components/icons/IconLoading.svelte @@ -2,30 +2,17 @@ export let classNames = ""; - - {#each Array(3) as _, index} - - {index} - - - - - {/each} - +
+
+
+
+
diff --git a/src/lib/components/icons/Logo.svelte b/src/lib/components/icons/Logo.svelte index 51fa94e3fa6..ffd45f2b370 100644 --- a/src/lib/components/icons/Logo.svelte +++ b/src/lib/components/icons/Logo.svelte @@ -1,25 +1,28 @@ - - - - + + +{:else} + - +{/if} diff --git a/src/lib/components/icons/LogoHuggingFaceBorderless.svelte b/src/lib/components/icons/LogoHuggingFaceBorderless.svelte new file mode 100644 index 00000000000..89a92da09ec --- /dev/null +++ b/src/lib/components/icons/LogoHuggingFaceBorderless.svelte @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index ee021ed4d65..96793da575f 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -1,9 +1,118 @@ -import { HF_CLIENT_ID, HF_CLIENT_SECRET } from "$env/static/private"; +import { Issuer, BaseClient, type UserinfoResponse, TokenSet } from "openid-client"; +import { addHours, addYears } from "date-fns"; +import { + COOKIE_NAME, + OPENID_CLIENT_ID, + OPENID_CLIENT_SECRET, + OPENID_PROVIDER_URL, + OPENID_SCOPES, +} from "$env/static/private"; +import { sha256 } from "$lib/utils/sha256"; +import { z } from "zod"; +import { dev } from "$app/environment"; +import type { Cookies } from "@sveltejs/kit"; -export const requiresUser = !!HF_CLIENT_ID && !!HF_CLIENT_SECRET; +export interface OIDCSettings { + redirectURI: string; +} + +export interface OIDCUserInfo { + token: TokenSet; + userData: UserinfoResponse; +} + +export const requiresUser = !!OPENID_CLIENT_ID && !!OPENID_CLIENT_SECRET; + +export function refreshSessionCookie(cookies: Cookies, sessionId: string) { + cookies.set(COOKIE_NAME, sessionId, { + path: "/", + // So that it works inside the space's iframe + sameSite: dev ? "lax" : "none", + secure: !dev, + httpOnly: true, + expires: addYears(new Date(), 1), + }); +} export const authCondition = (locals: App.Locals) => { - return locals.userId - ? { userId: locals.userId } + return locals.user + ? { userId: locals.user._id } : { sessionId: locals.sessionId, userId: { $exists: false } }; }; + +/** + * Generates a CSRF token using the user sessionId. Note that we don't need a secret because sessionId is enough. + */ +export async function generateCsrfToken(sessionId: string, redirectUrl: string): Promise { + const data = { + expiration: addHours(new Date(), 1).getTime(), + redirectUrl, + }; + + return Buffer.from( + JSON.stringify({ + data, + signature: await sha256(JSON.stringify(data) + "##" + sessionId), + }) + ).toString("base64"); +} + +async function getOIDCClient(settings: OIDCSettings): Promise { + const issuer = await Issuer.discover(OPENID_PROVIDER_URL); + return new issuer.Client({ + client_id: OPENID_CLIENT_ID, + client_secret: OPENID_CLIENT_SECRET, + redirect_uris: [settings.redirectURI], + response_types: ["code"], + }); +} + +export async function getOIDCAuthorizationUrl( + settings: OIDCSettings, + params: { sessionId: string } +): Promise { + const client = await getOIDCClient(settings); + const csrfToken = await generateCsrfToken(params.sessionId, settings.redirectURI); + const url = client.authorizationUrl({ + scope: OPENID_SCOPES, + state: csrfToken, + }); + + return url; +} + +export async function getOIDCUserData(settings: OIDCSettings, code: string): Promise { + const client = await getOIDCClient(settings); + const token = await client.callback(settings.redirectURI, { code }); + const userData = await client.userinfo(token); + + return { token, userData }; +} + +export async function validateAndParseCsrfToken( + token: string, + sessionId: string +): Promise<{ + /** This is the redirect url that was passed to the OIDC provider */ + redirectUrl: string; +} | null> { + try { + const { data, signature } = z + .object({ + data: z.object({ + expiration: z.number().int(), + redirectUrl: z.string().url(), + }), + signature: z.string().length(64), + }) + .parse(JSON.parse(token)); + const reconstructSign = await sha256(JSON.stringify(data) + "##" + sessionId); + + if (data.expiration > Date.now() && signature === reconstructSign) { + return { redirectUrl: data.redirectUrl }; + } + } catch (e) { + console.error(e); + } + return null; +} diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts index 6bd7e464405..0925a8a6a3d 100644 --- a/src/lib/server/database.ts +++ b/src/lib/server/database.ts @@ -1,24 +1,34 @@ -import { MONGODB_URL, MONGODB_DB_NAME } from "$env/static/private"; +import { MONGODB_URL, MONGODB_DB_NAME, MONGODB_DIRECT_CONNECTION } from "$env/static/private"; import { MongoClient } from "mongodb"; import type { Conversation } from "$lib/types/Conversation"; import type { SharedConversation } from "$lib/types/SharedConversation"; +import type { WebSearch } from "$lib/types/WebSearch"; import type { AbortedGeneration } from "$lib/types/AbortedGeneration"; import type { Settings } from "$lib/types/Settings"; import type { User } from "$lib/types/User"; +import type { MessageEvent } from "$lib/types/MessageEvent"; + +if (!MONGODB_URL) { + throw new Error( + "Please specify the MONGODB_URL environment variable inside .env.local. Set it to mongodb://localhost:27017 if you are running MongoDB locally, or to a MongoDB Atlas free instance for example." + ); +} const client = new MongoClient(MONGODB_URL, { - // directConnection: true + directConnection: MONGODB_DIRECT_CONNECTION === "true", }); export const connectPromise = client.connect().catch(console.error); -const db = client.db(MONGODB_DB_NAME); +const db = client.db(MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "")); const conversations = db.collection("conversations"); const sharedConversations = db.collection("sharedConversations"); const abortedGenerations = db.collection("abortedGenerations"); const settings = db.collection("settings"); const users = db.collection("users"); +const webSearches = db.collection("webSearches"); +const messageEvents = db.collection("messageEvents"); export { client, db }; export const collections = { @@ -27,6 +37,8 @@ export const collections = { abortedGenerations, settings, users, + webSearches, + messageEvents, }; client.on("open", () => { @@ -42,6 +54,7 @@ client.on("open", () => { { partialFilterExpression: { userId: { $exists: true } } } ) .catch(console.error); + webSearches.createIndex({ sessionId: 1, updatedAt: -1 }).catch(console.error); abortedGenerations.createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 }).catch(console.error); abortedGenerations.createIndex({ conversationId: 1 }, { unique: true }).catch(console.error); sharedConversations.createIndex({ hash: 1 }, { unique: true }).catch(console.error); @@ -49,4 +62,5 @@ client.on("open", () => { settings.createIndex({ userId: 1 }, { unique: true, sparse: true }).catch(console.error); users.createIndex({ hfUserId: 1 }, { unique: true }).catch(console.error); users.createIndex({ sessionId: 1 }, { unique: true, sparse: true }).catch(console.error); + messageEvents.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }).catch(console.error); }); diff --git a/src/lib/server/generateFromDefaultEndpoint.ts b/src/lib/server/generateFromDefaultEndpoint.ts new file mode 100644 index 00000000000..f11a36fd14d --- /dev/null +++ b/src/lib/server/generateFromDefaultEndpoint.ts @@ -0,0 +1,52 @@ +import { defaultModel } from "$lib/server/models"; +import { modelEndpoint } from "./modelEndpoint"; +import { textGeneration } from "@huggingface/inference"; +import { trimSuffix } from "$lib/utils/trimSuffix"; +import { trimPrefix } from "$lib/utils/trimPrefix"; +import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken"; + +interface Parameters { + temperature: number; + truncate: number; + max_new_tokens: number; + stop: string[]; +} +export async function generateFromDefaultEndpoint( + prompt: string, + parameters?: Partial +) { + const newParameters = { + ...defaultModel.parameters, + ...parameters, + return_full_text: false, + }; + + const endpoint = modelEndpoint(defaultModel); + let { generated_text } = await textGeneration( + { + model: endpoint.url, + inputs: prompt, + parameters: newParameters, + }, + { + fetch: (url, options) => + fetch(url, { + ...options, + headers: { ...options?.headers, Authorization: endpoint.authorization }, + }), + } + ); + + generated_text = trimSuffix( + trimPrefix(generated_text, "<|startoftext|>"), + PUBLIC_SEP_TOKEN + ).trimEnd(); + + for (const stop of [...(newParameters?.stop ?? []), "<|endoftext|>"]) { + if (generated_text.endsWith(stop)) { + generated_text = generated_text.slice(0, -stop.length).trimEnd(); + } + } + + return generated_text; +} diff --git a/src/lib/server/modelEndpoint.ts b/src/lib/server/modelEndpoint.ts index 1edd45cf91e..8be152a811d 100644 --- a/src/lib/server/modelEndpoint.ts +++ b/src/lib/server/modelEndpoint.ts @@ -1,4 +1,4 @@ -import { HF_ACCESS_TOKEN } from "$env/static/private"; +import { HF_ACCESS_TOKEN, HF_API_ROOT } from "$env/static/private"; import { sum } from "$lib/utils/sum"; import type { BackendModel } from "./models"; @@ -12,7 +12,7 @@ export function modelEndpoint(model: BackendModel): { } { if (!model.endpoints) { return { - url: `https://api-inference.huggingface.co/models/${model.name}`, + url: `${HF_API_ROOT}/${model.name}`, authorization: `Bearer ${HF_ACCESS_TOKEN}`, weight: 1, }; diff --git a/src/lib/server/models.ts b/src/lib/server/models.ts index 6946f406230..7a6cd3ad5fe 100644 --- a/src/lib/server/models.ts +++ b/src/lib/server/models.ts @@ -11,10 +11,14 @@ const modelsRaw = z displayName: z.string().min(1).optional(), description: z.string().min(1).optional(), websiteUrl: z.string().url().optional(), + modelUrl: z.string().url().optional(), datasetName: z.string().min(1).optional(), - userMessageToken: z.string().min(1), - assistantMessageToken: z.string().min(1), - messageEndToken: z.string().min(1).optional(), + datasetUrl: z.string().url().optional(), + userMessageToken: z.string(), + userMessageEndToken: z.string().default(""), + assistantMessageToken: z.string(), + assistantMessageEndToken: z.string().default(""), + messageEndToken: z.string().default(""), preprompt: z.string().default(""), prepromptUrl: z.string().url().optional(), promptExamples: z @@ -50,6 +54,8 @@ const modelsRaw = z export const models = await Promise.all( modelsRaw.map(async (m) => ({ ...m, + userMessageEndToken: m?.userMessageEndToken || m?.messageEndToken, + assistantMessageEndToken: m?.assistantMessageEndToken || m?.messageEndToken, id: m.id || m.name, displayName: m.displayName || m.name, preprompt: m.prepromptUrl ? await fetch(m.prepromptUrl).then((r) => r.text()) : m.preprompt, diff --git a/src/lib/server/websearch/generateQuery.ts b/src/lib/server/websearch/generateQuery.ts new file mode 100644 index 00000000000..5ea93530b2c --- /dev/null +++ b/src/lib/server/websearch/generateQuery.ts @@ -0,0 +1,25 @@ +import type { Message } from "$lib/types/Message"; +import { generateFromDefaultEndpoint } from "../generateFromDefaultEndpoint"; +import type { BackendModel } from "../models"; + +export async function generateQuery(messages: Message[], model: BackendModel) { + const promptSearchQuery = + model.userMessageToken + + "The following messages were written by a user, trying to answer a question." + + model.userMessageEndToken + + messages + .filter((message) => message.from === "user") + .map((message) => model.userMessageToken + message.content + model.userMessageEndToken) + + model.userMessageToken + + "What plain-text english sentence would you input into Google to answer the last question? Answer with a short (10 words max) simple sentence." + + model.userMessageEndToken + + model.assistantMessageToken + + "Query: "; + + const searchQuery = await generateFromDefaultEndpoint(promptSearchQuery).then((query) => { + const arr = query.split(/\r?\n/); + return arr[0].length > 0 ? arr[0] : arr[1]; + }); + + return searchQuery; +} diff --git a/src/lib/server/websearch/parseWeb.ts b/src/lib/server/websearch/parseWeb.ts new file mode 100644 index 00000000000..fe3e567a6e2 --- /dev/null +++ b/src/lib/server/websearch/parseWeb.ts @@ -0,0 +1,56 @@ +import { JSDOM, VirtualConsole } from "jsdom"; + +function removeTags(node: Node) { + if (node.hasChildNodes()) { + node.childNodes.forEach((childNode) => { + if (node.nodeName === "SCRIPT" || node.nodeName === "STYLE") { + node.removeChild(childNode); + } else { + removeTags(childNode); + } + }); + } +} +function naiveInnerText(node: Node): string { + const Node = node; // We need Node(DOM's Node) for the constants, but Node doesn't exist in the nodejs global space, and any Node instance references the constants through the prototype chain + return [...node.childNodes] + .map((childNode) => { + switch (childNode.nodeType) { + case Node.TEXT_NODE: + return node.textContent; + case Node.ELEMENT_NODE: + return naiveInnerText(childNode); + default: + return ""; + } + }) + .join("\n"); +} + +export async function parseWeb(url: string) { + const abortController = new AbortController(); + setTimeout(() => abortController.abort(), 10000); + const htmlString = await fetch(url, { signal: abortController.signal }) + .then((response) => response.text()) + .catch((err) => console.log(err)); + + const virtualConsole = new VirtualConsole(); + virtualConsole.on("error", () => { + // No-op to skip console errors. + }); + + // put the html string into a DOM + const dom = new JSDOM(htmlString ?? "", { + virtualConsole, + }); + + const body = dom.window.document.querySelector("body"); + if (!body) throw new Error("body of the webpage is null"); + + removeTags(body); + + // recursively extract text content from the body and then remove newlines and multiple spaces + const text = (naiveInnerText(body) ?? "").replace(/ {2}|\r\n|\n|\r/gm, ""); + + return text; +} diff --git a/src/lib/server/websearch/searchWeb.ts b/src/lib/server/websearch/searchWeb.ts new file mode 100644 index 00000000000..42369689a10 --- /dev/null +++ b/src/lib/server/websearch/searchWeb.ts @@ -0,0 +1,63 @@ +import { SERPAPI_KEY, SERPER_API_KEY } from "$env/static/private"; + +import { getJson } from "serpapi"; +import type { GoogleParameters } from "serpapi"; + +// Show result as JSON +export async function searchWeb(query: string) { + if (SERPER_API_KEY) { + return await searchWebSerper(query); + } + if (SERPAPI_KEY) { + return await searchWebSerpApi(query); + } + throw new Error("No Serper.dev or SerpAPI key found"); +} + +export async function searchWebSerper(query: string) { + const params = { + q: query, + hl: "en", + gl: "us", + }; + + const response = await fetch("https://google.serper.dev/search", { + method: "POST", + body: JSON.stringify(params), + headers: { + "x-api-key": SERPER_API_KEY, + "Content-type": "application/json; charset=UTF-8", + }, + }); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + const data = (await response.json()) as Record; + + if (!response.ok) { + throw new Error( + data["message"] ?? + `Serper API returned error code ${response.status} - ${response.statusText}` + ); + } + + return { + organic_results: data["organic"] ?? [], + knowledge_graph: data["knowledgeGraph"] ?? null, + answer_box: data["answerBox"] ?? null, + }; +} + +export async function searchWebSerpApi(query: string) { + const params = { + q: query, + hl: "en", + gl: "us", + google_domain: "google.com", + api_key: SERPAPI_KEY, + } satisfies GoogleParameters; + + // Show result as JSON + const response = await getJson("google", params); + + return response; +} diff --git a/src/lib/server/websearch/summarizeWeb.ts b/src/lib/server/websearch/summarizeWeb.ts new file mode 100644 index 00000000000..8b793cdc093 --- /dev/null +++ b/src/lib/server/websearch/summarizeWeb.ts @@ -0,0 +1,44 @@ +import { HF_ACCESS_TOKEN } from "$env/static/private"; +import { HfInference } from "@huggingface/inference"; +import { generateFromDefaultEndpoint } from "../generateFromDefaultEndpoint"; +import type { BackendModel } from "../models"; + +export async function summarizeWeb(content: string, query: string, model: BackendModel) { + // if HF_ACCESS_TOKEN is set, we use a HF dedicated endpoint for summarization + try { + if (HF_ACCESS_TOKEN) { + const summary = ( + await new HfInference(HF_ACCESS_TOKEN).summarization({ + model: "facebook/bart-large-cnn", + inputs: content, + parameters: { + max_length: 512, + }, + }) + ).summary_text; + return summary; + } + } catch (e) { + console.log(e); + } + + // else we use the LLM to generate a summary + const summaryPrompt = + model.userMessageToken + + content + .split(" ") + .slice(0, model.parameters?.truncate ?? 0) + .join(" ") + + model.userMessageEndToken + + model.userMessageToken + + `The text above should be summarized to best answer the query: ${query}.` + + model.userMessageEndToken + + model.assistantMessageToken + + "Summary: "; + + const summary = await generateFromDefaultEndpoint(summaryPrompt).then((txt: string) => + txt.trim() + ); + + return summary; +} diff --git a/src/lib/stores/errors.ts b/src/lib/stores/errors.ts index c7dd124ff03..144b16faba7 100644 --- a/src/lib/stores/errors.ts +++ b/src/lib/stores/errors.ts @@ -2,6 +2,8 @@ import { writable } from "svelte/store"; export const ERROR_MESSAGES = { default: "Oops, something went wrong.", + authOnly: "You have to be logged in.", + rateLimited: "You are sending too many messages. Try again later.", }; export const error = writable(null); diff --git a/src/lib/stores/webSearchParameters.ts b/src/lib/stores/webSearchParameters.ts new file mode 100644 index 00000000000..fd088a60621 --- /dev/null +++ b/src/lib/stores/webSearchParameters.ts @@ -0,0 +1,9 @@ +import { writable } from "svelte/store"; +export interface WebSearchParameters { + useSearch: boolean; + nItems: number; +} +export const webSearchParameters = writable({ + useSearch: false, + nItems: 5, +}); diff --git a/src/lib/types/Message.ts b/src/lib/types/Message.ts index aee67c9b704..34908219c0d 100644 --- a/src/lib/types/Message.ts +++ b/src/lib/types/Message.ts @@ -1,5 +1,9 @@ -export interface Message { +import type { Timestamps } from "./Timestamps"; + +export type Message = Partial & { from: "user" | "assistant"; id: ReturnType; content: string; -} + webSearchId?: string; + score?: -1 | 0 | 1; +}; diff --git a/src/lib/types/MessageEvent.ts b/src/lib/types/MessageEvent.ts new file mode 100644 index 00000000000..7ec1a4b4303 --- /dev/null +++ b/src/lib/types/MessageEvent.ts @@ -0,0 +1,6 @@ +import type { Timestamps } from "./Timestamps"; +import type { User } from "./User"; + +export interface MessageEvent extends Pick { + userId: User["_id"] | User["sessionId"]; +} diff --git a/src/lib/types/Model.ts b/src/lib/types/Model.ts index 754210be0dd..6d739fdf4fc 100644 --- a/src/lib/types/Model.ts +++ b/src/lib/types/Model.ts @@ -10,4 +10,6 @@ export type Model = Pick< | "promptExamples" | "parameters" | "description" + | "modelUrl" + | "datasetUrl" >; diff --git a/src/lib/types/Settings.ts b/src/lib/types/Settings.ts index f2ca9f37407..6cf9e618a2a 100644 --- a/src/lib/types/Settings.ts +++ b/src/lib/types/Settings.ts @@ -1,3 +1,4 @@ +import { defaultModel } from "$lib/server/models"; import type { Timestamps } from "./Timestamps"; import type { User } from "./User"; @@ -6,7 +7,7 @@ export interface Settings extends Timestamps { sessionId?: string; /** - * Note: Only conversations with this settings explictly set to true should be shared. + * Note: Only conversations with this settings explicitly set to true should be shared. * * This setting is explicitly set to true when users accept the ethics modal. * */ @@ -14,3 +15,9 @@ export interface Settings extends Timestamps { ethicsModalAcceptedAt: Date | null; activeModel: string; } + +// TODO: move this to a constant file along with other constants +export const DEFAULT_SETTINGS = { + shareConversationsWithModelAuthors: true, + activeModel: defaultModel.id, +}; diff --git a/src/lib/types/UrlDependency.ts b/src/lib/types/UrlDependency.ts index 2b085888c79..dca26f87f49 100644 --- a/src/lib/types/UrlDependency.ts +++ b/src/lib/types/UrlDependency.ts @@ -1,4 +1,5 @@ /* eslint-disable no-shadow */ export enum UrlDependency { ConversationList = "conversation:list", + Conversation = "conversation", } diff --git a/src/lib/types/User.ts b/src/lib/types/User.ts index 5360eda3c30..7cd8d10377b 100644 --- a/src/lib/types/User.ts +++ b/src/lib/types/User.ts @@ -4,11 +4,12 @@ import type { Timestamps } from "./Timestamps"; export interface User extends Timestamps { _id: ObjectId; - username: string; + username?: string; name: string; + email?: string; avatarUrl: string; hfUserId: string; // Session identifier, stored in the cookie - sessionId?: string; + sessionId: string; } diff --git a/src/lib/types/WebSearch.ts b/src/lib/types/WebSearch.ts new file mode 100644 index 00000000000..66bb72145fc --- /dev/null +++ b/src/lib/types/WebSearch.ts @@ -0,0 +1,41 @@ +import type { ObjectId } from "mongodb"; +import type { Conversation } from "./Conversation"; +import type { Timestamps } from "./Timestamps"; + +export interface WebSearch extends Timestamps { + _id: ObjectId; + + convId: Conversation["_id"]; + + prompt: string; + + searchQuery: string; + results: string[]; + knowledgeGraph: string; + answerBox: string; + summary: string; + + messages: WebSearchMessage[]; +} + +export type WebSearchMessageUpdate = { + type: "update"; + message: string; + args?: string[]; +}; + +export type WebSearchMessageError = { + type: "error"; + message: string; + args?: string[]; +}; + +export type WebSearchMessageResult = { + type: "result"; + id: string; +}; + +export type WebSearchMessage = + | WebSearchMessageUpdate + | WebSearchMessageResult + | WebSearchMessageError; diff --git a/src/lib/utils/deepestChild.ts b/src/lib/utils/deepestChild.ts index 7177d64566b..ac6ed1d1dd6 100644 --- a/src/lib/utils/deepestChild.ts +++ b/src/lib/utils/deepestChild.ts @@ -1,7 +1,6 @@ -export function deepestChild(el: HTMLElement) { - let newEl = el; - while (newEl.hasChildNodes()) { - newEl = newEl.lastElementChild as HTMLElement; +export function deepestChild(el: HTMLElement): HTMLElement { + if (el.lastElementChild && el.lastElementChild.nodeType !== Node.TEXT_NODE) { + return deepestChild(el.lastElementChild as HTMLElement); } - return newEl; + return el; } diff --git a/src/lib/utils/hashConv.ts b/src/lib/utils/hashConv.ts new file mode 100644 index 00000000000..de014324f6f --- /dev/null +++ b/src/lib/utils/hashConv.ts @@ -0,0 +1,12 @@ +import type { Conversation } from "$lib/types/Conversation"; +import { sha256 } from "./sha256"; + +export async function hashConv(conv: Conversation) { + // messages contains the conversation message but only the immutable part + const messages = conv.messages.map((message) => { + return (({ from, id, content, webSearchId }) => ({ from, id, content, webSearchId }))(message); + }); + + const hash = await sha256(JSON.stringify(messages)); + return hash; +} diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte index fe9317f7f9e..6836376aa41 100644 --- a/src/routes/+error.svelte +++ b/src/routes/+error.svelte @@ -10,6 +10,6 @@ >

{$page.status}

-

{$page.error?.message}

+

{$page.error?.message}

diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index 8a800334033..46264b37a01 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -4,7 +4,9 @@ import { collections } from "$lib/server/database"; import type { Conversation } from "$lib/types/Conversation"; import { UrlDependency } from "$lib/types/UrlDependency"; import { defaultModel, models, oldModels, validateModel } from "$lib/server/models"; -import { authCondition } from "$lib/server/auth"; +import { authCondition, requiresUser } from "$lib/server/auth"; +import { DEFAULT_SETTINGS } from "$lib/types/Settings"; +import { SERPAPI_KEY, SERPER_API_KEY, MESSAGES_BEFORE_LOGIN } from "$env/static/private"; export const load: LayoutServerLoad = async ({ locals, depends, url }) => { const { conversations } = collections; @@ -54,20 +56,32 @@ export const load: LayoutServerLoad = async ({ locals, depends, url }) => { })) .toArray(), settings: { - shareConversationsWithModelAuthors: settings?.shareConversationsWithModelAuthors ?? true, + shareConversationsWithModelAuthors: + settings?.shareConversationsWithModelAuthors ?? + DEFAULT_SETTINGS.shareConversationsWithModelAuthors, ethicsModalAcceptedAt: settings?.ethicsModalAcceptedAt ?? null, - activeModel: settings?.activeModel ?? defaultModel.id, + activeModel: settings?.activeModel ?? DEFAULT_SETTINGS.activeModel, + searchEnabled: !!(SERPAPI_KEY || SERPER_API_KEY), }, models: models.map((model) => ({ id: model.id, name: model.name, websiteUrl: model.websiteUrl, + modelUrl: model.modelUrl, datasetName: model.datasetName, + datasetUrl: model.datasetUrl, displayName: model.displayName, description: model.description, promptExamples: model.promptExamples, parameters: model.parameters, })), oldModels, + user: locals.user && { + username: locals.user.username, + avatarUrl: locals.user.avatarUrl, + email: locals.user.email, + }, + requiresLogin: requiresUser, + messagesBeforeLogin: MESSAGES_BEFORE_LOGIN ? parseInt(MESSAGES_BEFORE_LOGIN) : 0, }; }; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index c0e12712ce0..a966b59953f 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -4,7 +4,7 @@ import { page } from "$app/stores"; import "../styles/main.css"; import { base } from "$app/paths"; - import { PUBLIC_ORIGIN } from "$env/static/public"; + import { PUBLIC_ORIGIN, PUBLIC_APP_DISCLAIMER } from "$env/static/public"; import { shareConversation } from "$lib/shareConversation"; import { UrlDependency } from "$lib/types/UrlDependency"; @@ -13,8 +13,9 @@ import MobileNav from "$lib/components/MobileNav.svelte"; import NavMenu from "$lib/components/NavMenu.svelte"; import Toast from "$lib/components/Toast.svelte"; - import EthicsModal from "$lib/components/EthicsModal.svelte"; import SettingsModal from "$lib/components/SettingsModal.svelte"; + import LoginModal from "$lib/components/LoginModal.svelte"; + import { PUBLIC_APP_ASSETS, PUBLIC_APP_NAME } from "$env/static/public"; export let data; @@ -56,7 +57,7 @@ if ($page.params.id !== id) { await invalidate(UrlDependency.ConversationList); } else { - await goto(base || "/", { invalidateAll: true }); + await goto(`${base}/`, { invalidateAll: true }); } } catch (err) { console.error(err); @@ -91,16 +92,59 @@ }); $: if ($error) onError(); + + const requiresLogin = + !$page.error && + !$page.route.id?.startsWith("/r/") && + (data.requiresLogin + ? !data.user + : !data.settings.ethicsModalAcceptedAt && !!PUBLIC_APP_DISCLAIMER); + + let loginModalVisible = false; + {PUBLIC_APP_NAME} - + - + + + + + + +
shareConversation(ev.detail.id, ev.detail.title)} on:deleteConversation={(ev) => deleteConversation(ev.detail)} on:clickSettings={() => (isSettingsOpen = true)} @@ -122,6 +169,9 @@
diff --git a/src/routes/admin/export/+server.ts b/src/routes/admin/export/+server.ts index e8806ab8b71..2cdae1f2aa7 100644 --- a/src/routes/admin/export/+server.ts +++ b/src/routes/admin/export/+server.ts @@ -34,7 +34,14 @@ export async function POST({ request }) { title: { type: "UTF8" }, created_at: { type: "TIMESTAMP_MILLIS" }, updated_at: { type: "TIMESTAMP_MILLIS" }, - messages: { repeated: true, fields: { from: { type: "UTF8" }, content: { type: "UTF8" } } }, + messages: { + repeated: true, + fields: { + from: { type: "UTF8" }, + content: { type: "UTF8" }, + score: { type: "INT_8", optional: true }, + }, + }, }); const fileName = `/tmp/conversations-${new Date().toJSON().slice(0, 10)}-${Date.now()}.parquet`; @@ -50,13 +57,64 @@ export async function POST({ request }) { updated_at: Date; messages: Message[]; }>([ - { $match: { shareConversationsWithModelAuthors: true } }, + { + $match: { + shareConversationsWithModelAuthors: true, + sessionId: { $exists: true }, + userId: { $exists: false }, + }, + }, { $lookup: { from: "conversations", localField: "sessionId", foreignField: "sessionId", as: "conversations", + pipeline: [{ $match: { model, userId: { $exists: false } } }], + }, + }, + { $unwind: "$conversations" }, + { + $project: { + title: "$conversations.title", + created_at: "$conversations.createdAt", + updated_at: "$conversations.updatedAt", + messages: "$conversations.messages", + }, + }, + ])) { + await writer.appendRow({ + title: conversation.title, + created_at: conversation.created_at, + updated_at: conversation.updated_at, + messages: conversation.messages.map((message: Message) => ({ + from: message.from, + content: message.content, + ...(message.score ? { score: message.score } : undefined), + })), + }); + ++count; + + if (count % 1_000 === 0) { + console.log("Exported", count, "conversations"); + } + } + + console.log("exporting convos with userId"); + + for await (const conversation of collections.settings.aggregate<{ + title: string; + created_at: Date; + updated_at: Date; + messages: Message[]; + }>([ + { $match: { shareConversationsWithModelAuthors: true, userId: { $exists: true } } }, + { + $lookup: { + from: "conversations", + localField: "userId", + foreignField: "userId", + as: "conversations", pipeline: [{ $match: { model } }], }, }, @@ -77,6 +135,7 @@ export async function POST({ request }) { messages: conversation.messages.map((message: Message) => ({ from: message.from, content: message.content, + ...(message.score ? { score: message.score } : undefined), })), }); ++count; diff --git a/src/routes/conversation/+server.ts b/src/routes/conversation/+server.ts index 0442cd884f2..fbf6034f90b 100644 --- a/src/routes/conversation/+server.ts +++ b/src/routes/conversation/+server.ts @@ -44,7 +44,7 @@ export const POST: RequestHandler = async ({ locals, request }) => { model: values.model, createdAt: new Date(), updatedAt: new Date(), - ...(locals.userId ? { userId: locals.userId } : { sessionId: locals.sessionId }), + ...(locals.user ? { userId: locals.user._id } : { sessionId: locals.sessionId }), ...(values.fromShare ? { meta: { fromShareId: values.fromShare } } : {}), }); @@ -57,5 +57,5 @@ export const POST: RequestHandler = async ({ locals, request }) => { }; export const GET: RequestHandler = async () => { - throw redirect(302, base || "/"); + throw redirect(302, `${base}/`); }; diff --git a/src/routes/conversation/[id]/+page.server.ts b/src/routes/conversation/[id]/+page.server.ts index 895c823c1c9..500b832a1f2 100644 --- a/src/routes/conversation/[id]/+page.server.ts +++ b/src/routes/conversation/[id]/+page.server.ts @@ -2,14 +2,18 @@ import { collections } from "$lib/server/database"; import { ObjectId } from "mongodb"; import { error } from "@sveltejs/kit"; import { authCondition } from "$lib/server/auth"; +import type { WebSearchMessageResult } from "$lib/types/WebSearch"; +import { UrlDependency } from "$lib/types/UrlDependency"; -export const load = async ({ params, locals }) => { +export const load = async ({ params, depends, locals }) => { // todo: add validation on params.id const conversation = await collections.conversations.findOne({ _id: new ObjectId(params.id), ...authCondition(locals), }); + depends(UrlDependency.Conversation); + if (!conversation) { const conversationExists = (await collections.conversations.countDocuments({ @@ -26,9 +30,23 @@ export const load = async ({ params, locals }) => { throw error(404, "Conversation not found."); } + const webSearchesId = conversation.messages + .filter((message) => message.webSearchId) + .map((message) => new ObjectId(message.webSearchId)); + + const results = await collections.webSearches.find({ _id: { $in: webSearchesId } }).toArray(); + + const searches = Object.fromEntries( + results.map((x) => [ + x._id.toString(), + [...x.messages, { type: "result", id: x._id.toString() } satisfies WebSearchMessageResult], + ]) + ); + return { messages: conversation.messages, title: conversation.title, model: conversation.model, + searches, }; }; diff --git a/src/routes/conversation/[id]/+page.svelte b/src/routes/conversation/[id]/+page.svelte index 431e20ae6b2..b9f9ba90c4f 100644 --- a/src/routes/conversation/[id]/+page.svelte +++ b/src/routes/conversation/[id]/+page.svelte @@ -12,6 +12,10 @@ import { ERROR_MESSAGES, error } from "$lib/stores/errors"; import { randomUUID } from "$lib/utils/randomUuid"; import { findCurrentModel } from "$lib/utils/models"; + import { webSearchParameters } from "$lib/stores/webSearchParameters"; + import type { WebSearchMessage } from "$lib/types/WebSearch"; + import type { Message } from "$lib/types/Message"; + import { PUBLIC_APP_DISCLAIMER } from "$env/static/public"; export let data; @@ -19,6 +23,8 @@ let lastLoadedMessages = data.messages; let isAborted = false; + let webSearchMessages: WebSearchMessage[] = []; + // Since we modify the messages array locally, we don't want to reset it if an old version is passed $: if (data.messages !== lastLoadedMessages) { messages = data.messages; @@ -27,9 +33,16 @@ let loading = false; let pending = false; + let loginRequired = false; - async function getTextGenerationStream(inputs: string, messageId: string, isRetry = false) { + async function getTextGenerationStream( + inputs: string, + messageId: string, + isRetry = false, + webSearchId?: string + ) { let conversationId = $page.params.id; + const responseId = randomUUID(); const response = textGenerationStream( { @@ -42,8 +55,10 @@ }, { id: messageId, + response_id: responseId, is_retry: isRetry, use_cache: false, + web_search_id: webSearchId, } as Options ); @@ -75,6 +90,7 @@ if (lastMessage) { lastMessage.content = output.generated_text; + lastMessage.webSearchId = webSearchId; messages = [...messages]; } break; @@ -88,7 +104,7 @@ messages = [ ...messages, // id doesn't match the backend id but it's not important for assistant messages - { from: "assistant", content: output.token.text.trimStart(), id: randomUUID() }, + { from: "assistant", content: output.token.text.trimStart(), id: responseId }, ]; } else { lastMessage.content += output.token.text; @@ -123,7 +139,63 @@ { from: "user", content: message, id: messageId }, ]; - await getTextGenerationStream(message, messageId, isRetry); + let searchResponseId: string | null = ""; + if ($webSearchParameters.useSearch) { + webSearchMessages = []; + + const res = await fetch( + `${base}/conversation/${$page.params.id}/web-search?` + + new URLSearchParams({ prompt: message }), + { + method: "GET", + } + ); + + // required bc linting doesn't see TextDecoderStream for some reason? + // eslint-disable-next-line no-undef + const encoder = new TextDecoderStream(); + const reader = res?.body?.pipeThrough(encoder).getReader(); + + while (searchResponseId === "") { + await new Promise((r) => setTimeout(r, 25)); + + if (isAborted) { + reader?.cancel(); + return; + } + + reader + ?.read() + .then(async ({ done, value }) => { + if (done) { + reader.cancel(); + return; + } + + try { + webSearchMessages = (JSON.parse(value) as { messages: WebSearchMessage[] }) + .messages; + } catch (parseError) { + // in case of parsing error we wait for the next message + return; + } + + const lastSearchMessage = webSearchMessages[webSearchMessages.length - 1]; + if (lastSearchMessage.type === "result") { + searchResponseId = lastSearchMessage.id; + reader.cancel(); + return; + } + }) + .catch(() => { + searchResponseId = null; + }); + } + } + + await getTextGenerationStream(message, messageId, isRetry, searchResponseId ?? undefined); + + webSearchMessages = []; if (messages.filter((m) => m.from === "user").length === 1) { summarizeTitle($page.params.id) @@ -135,6 +207,8 @@ } catch (err) { if (err instanceof Error && err.message.includes("overloaded")) { $error = "Too much traffic, please try again."; + } else if (err instanceof Error && err.message.includes("429")) { + $error = ERROR_MESSAGES.rateLimited; } else if (err instanceof Error) { $error = err.message; } else { @@ -147,6 +221,32 @@ } } + async function voteMessage(score: Message["score"], messageId: string) { + let conversationId = $page.params.id; + let oldScore: Message["score"] | undefined; + + // optimistic update to avoid waiting for the server + messages = messages.map((message) => { + if (message.id === messageId) { + oldScore = message.score; + return { ...message, score: score }; + } + return message; + }); + + try { + await fetch(`${base}/conversation/${conversationId}/message/${messageId}/vote`, { + method: "POST", + body: JSON.stringify({ score }), + }); + } catch { + // revert score on any error + messages = messages.map((message) => { + return message.id !== messageId ? message : { ...message, score: oldScore }; + }); + } + } + onMount(async () => { if ($pendingMessage) { const val = $pendingMessage; @@ -157,8 +257,14 @@ writeMessage(val, messageId); } }); - + $: $page.params.id, (isAborted = true); $: title = data.conversations.find((conv) => conv.id === $page.params.id)?.title ?? data.title; + + $: loginRequired = + (data.requiresLogin + ? !data.user + : !data.settings.ethicsModalAcceptedAt && !!PUBLIC_APP_DISCLAIMER) && + messages.length >= data.messagesBeforeLogin; @@ -169,11 +275,15 @@ {loading} {pending} {messages} - on:message={(message) => writeMessage(message.detail)} - on:retry={(message) => writeMessage(message.detail.content, message.detail.id)} + bind:webSearchMessages + searches={{ ...data.searches }} + on:message={(event) => writeMessage(event.detail)} + on:retry={(event) => writeMessage(event.detail.content, event.detail.id)} + on:vote={(event) => voteMessage(event.detail.score, event.detail.id)} on:share={() => shareConversation($page.params.id, data.title)} on:stop={() => (isAborted = true)} models={data.models} currentModel={findCurrentModel([...data.models, ...data.oldModels], data.model)} settings={data.settings} + {loginRequired} /> diff --git a/src/routes/conversation/[id]/+server.ts b/src/routes/conversation/[id]/+server.ts index e7e7bc0a674..a5effddb31a 100644 --- a/src/routes/conversation/[id]/+server.ts +++ b/src/routes/conversation/[id]/+server.ts @@ -1,10 +1,12 @@ +import { MESSAGES_BEFORE_LOGIN, RATE_LIMIT } from "$env/static/private"; import { buildPrompt } from "$lib/buildPrompt"; import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken"; import { abortedGenerations } from "$lib/server/abortedGenerations"; -import { authCondition } from "$lib/server/auth"; +import { authCondition, requiresUser } from "$lib/server/auth"; import { collections } from "$lib/server/database"; import { modelEndpoint } from "$lib/server/modelEndpoint"; import { models } from "$lib/server/models"; +import { ERROR_MESSAGES } from "$lib/stores/errors.js"; import type { Message } from "$lib/types/Message"; import { concatUint8Arrays } from "$lib/utils/concatUint8Arrays"; import { streamToAsyncIterable } from "$lib/utils/streamToAsyncIterable"; @@ -20,6 +22,12 @@ export async function POST({ request, fetch, locals, params }) { const convId = new ObjectId(id); const date = new Date(); + const userId = locals.user?._id ?? locals.sessionId; + + if (!userId) { + throw error(401, "Unauthorized"); + } + const conv = await collections.conversations.findOne({ _id: convId, ...authCondition(locals), @@ -29,6 +37,20 @@ export async function POST({ request, fetch, locals, params }) { throw error(404, "Conversation not found"); } + if ( + !locals.user?._id && + requiresUser && + conv.messages.length > (MESSAGES_BEFORE_LOGIN ? parseInt(MESSAGES_BEFORE_LOGIN) : 0) + ) { + throw error(429, "Exceeded number of messages before login"); + } + + const nEvents = await collections.messageEvents.countDocuments({ userId }); + + if (RATE_LIMIT != "" && nEvents > parseInt(RATE_LIMIT)) { + throw error(429, ERROR_MESSAGES.rateLimited); + } + const model = models.find((m) => m.id === conv.model); if (!model) { @@ -38,13 +60,15 @@ export async function POST({ request, fetch, locals, params }) { const json = await request.json(); const { inputs: newPrompt, - options: { id: messageId, is_retry }, + options: { id: messageId, is_retry, web_search_id, response_id: responseId }, } = z .object({ inputs: z.string().trim().min(1), options: z.object({ id: z.optional(z.string().uuid()), + response_id: z.optional(z.string().uuid()), is_retry: z.optional(z.boolean()), + web_search_id: z.ostring(), }), }) .parse(json); @@ -57,17 +81,22 @@ export async function POST({ request, fetch, locals, params }) { } return [ ...conv.messages.slice(0, retryMessageIdx), - { content: newPrompt, from: "user", id: messageId as Message["id"] }, + { content: newPrompt, from: "user", id: messageId as Message["id"], updatedAt: new Date() }, ]; } return [ ...conv.messages, - { content: newPrompt, from: "user", id: (messageId as Message["id"]) || crypto.randomUUID() }, + { + content: newPrompt, + from: "user", + id: (messageId as Message["id"]) || crypto.randomUUID(), + createdAt: new Date(), + updatedAt: new Date(), + }, ]; })() satisfies Message[]; - const prompt = buildPrompt(messages, model); - + const prompt = await buildPrompt(messages, model, web_search_id); const randomEndpoint = modelEndpoint(model); const abortController = new AbortController(); @@ -110,7 +139,19 @@ export async function POST({ request, fetch, locals, params }) { } } - messages.push({ from: "assistant", content: generated_text, id: crypto.randomUUID() }); + messages.push({ + from: "assistant", + content: generated_text, + webSearchId: web_search_id, + id: (responseId as Message["id"]) || crypto.randomUUID(), + createdAt: new Date(), + updatedAt: new Date(), + }); + + await collections.messageEvents.insertOne({ + userId: userId, + createdAt: new Date(), + }); await collections.conversations.updateOne( { @@ -126,7 +167,6 @@ export async function POST({ request, fetch, locals, params }) { } saveMessage().catch(console.error); - // Todo: maybe we should wait for the message to be saved before ending the response - in case of errors return new Response(stream1, { headers: Object.fromEntries(resp.headers.entries()), diff --git a/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts b/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts index 24c0067ede1..0a217aebf1c 100644 --- a/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts +++ b/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts @@ -31,7 +31,7 @@ export async function GET({ params, locals }) { throw error(404, "Conversation model not found"); } - const prompt = buildPrompt(conv.messages.slice(0, messageIndex + 1), model); + const prompt = await buildPrompt(conv.messages.slice(0, messageIndex + 1), model); return new Response( JSON.stringify( diff --git a/src/routes/conversation/[id]/message/[messageId]/vote/+server.ts b/src/routes/conversation/[id]/message/[messageId]/vote/+server.ts new file mode 100644 index 00000000000..8702a1346ae --- /dev/null +++ b/src/routes/conversation/[id]/message/[messageId]/vote/+server.ts @@ -0,0 +1,38 @@ +import { authCondition } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; +import { z } from "zod"; + +export async function POST({ params, request, locals }) { + const { score } = z + .object({ + score: z.number().int().min(-1).max(1), + }) + .parse(await request.json()); + const conversationId = new ObjectId(params.id); + const messageId = params.messageId; + + const document = await collections.conversations.updateOne( + { + _id: conversationId, + ...authCondition(locals), + "messages.id": messageId, + }, + { + ...(score !== 0 + ? { + $set: { + "messages.$.score": score, + }, + } + : { $unset: { "messages.$.score": "" } }), + } + ); + + if (!document.matchedCount) { + throw error(404, "Message not found"); + } + + return new Response(); +} diff --git a/src/routes/conversation/[id]/share/+server.ts b/src/routes/conversation/[id]/share/+server.ts index c5c181ab9b5..5fd0455d66d 100644 --- a/src/routes/conversation/[id]/share/+server.ts +++ b/src/routes/conversation/[id]/share/+server.ts @@ -1,9 +1,9 @@ import { base } from "$app/paths"; -import { PUBLIC_ORIGIN } from "$env/static/public"; +import { PUBLIC_ORIGIN, PUBLIC_SHARE_PREFIX } from "$env/static/public"; import { authCondition } from "$lib/server/auth"; import { collections } from "$lib/server/database"; import type { SharedConversation } from "$lib/types/SharedConversation"; -import { sha256 } from "$lib/utils/sha256"; +import { hashConv } from "$lib/utils/hashConv.js"; import { error } from "@sveltejs/kit"; import { ObjectId } from "mongodb"; import { nanoid } from "nanoid"; @@ -18,7 +18,7 @@ export async function POST({ params, url, locals }) { throw error(404, "Conversation not found"); } - const hash = await sha256(JSON.stringify(conversation.messages)); + const hash = await hashConv(conversation); const existingShare = await collections.sharedConversations.findOne({ hash }); @@ -52,5 +52,5 @@ export async function POST({ params, url, locals }) { } function getShareUrl(url: URL, shareId: string): string { - return `${PUBLIC_ORIGIN || url.origin}${base}/r/${shareId}`; + return `${PUBLIC_SHARE_PREFIX || `${PUBLIC_ORIGIN || url.origin}${base}`}/r/${shareId}`; } diff --git a/src/routes/conversation/[id]/summarize/+server.ts b/src/routes/conversation/[id]/summarize/+server.ts index bc33c490199..00ce45cf4f4 100644 --- a/src/routes/conversation/[id]/summarize/+server.ts +++ b/src/routes/conversation/[id]/summarize/+server.ts @@ -1,16 +1,12 @@ import { buildPrompt } from "$lib/buildPrompt"; -import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken"; import { authCondition } from "$lib/server/auth"; import { collections } from "$lib/server/database"; -import { modelEndpoint } from "$lib/server/modelEndpoint"; +import { generateFromDefaultEndpoint } from "$lib/server/generateFromDefaultEndpoint"; import { defaultModel } from "$lib/server/models"; -import { trimPrefix } from "$lib/utils/trimPrefix"; -import { trimSuffix } from "$lib/utils/trimSuffix"; -import { textGeneration } from "@huggingface/inference"; import { error } from "@sveltejs/kit"; import { ObjectId } from "mongodb"; -export async function POST({ params, locals, fetch }) { +export async function POST({ params, locals }) { const convId = new ObjectId(params.id); const conversation = await collections.conversations.findOne({ @@ -28,30 +24,8 @@ export async function POST({ params, locals, fetch }) { `Please summarize the following message as a single sentence of less than 5 words:\n` + firstMessage?.content; - const prompt = buildPrompt([{ from: "user", content: userPrompt }], defaultModel); - - const parameters = { - ...defaultModel.parameters, - return_full_text: false, - }; - - const endpoint = modelEndpoint(defaultModel); - let { generated_text } = await textGeneration( - { - model: endpoint.url, - inputs: prompt, - parameters, - }, - { - fetch: (url, options) => - fetch(url, { - ...options, - headers: { ...options?.headers, Authorization: endpoint.authorization }, - }), - } - ); - - generated_text = trimSuffix(trimPrefix(generated_text, "<|startoftext|>"), PUBLIC_SEP_TOKEN); + const prompt = await buildPrompt([{ from: "user", content: userPrompt }], defaultModel); + const generated_text = await generateFromDefaultEndpoint(prompt); if (generated_text) { await collections.conversations.updateOne( diff --git a/src/routes/conversation/[id]/web-search/+server.ts b/src/routes/conversation/[id]/web-search/+server.ts new file mode 100644 index 00000000000..542f8c9d63f --- /dev/null +++ b/src/routes/conversation/[id]/web-search/+server.ts @@ -0,0 +1,135 @@ +import { authCondition } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import { defaultModel } from "$lib/server/models"; +import { searchWeb } from "$lib/server/websearch/searchWeb"; +import type { Message } from "$lib/types/Message"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; +import { z } from "zod"; +import type { WebSearch } from "$lib/types/WebSearch"; +import { generateQuery } from "$lib/server/websearch/generateQuery"; +import { parseWeb } from "$lib/server/websearch/parseWeb"; +import { summarizeWeb } from "$lib/server/websearch/summarizeWeb"; + +interface GenericObject { + [key: string]: GenericObject | unknown; +} + +function removeLinks(obj: GenericObject) { + for (const prop in obj) { + if (prop.endsWith("link")) delete obj[prop]; + else if (typeof obj[prop] === "object") removeLinks(obj[prop] as GenericObject); + } + return obj; +} +export async function GET({ params, locals, url }) { + const model = defaultModel; + const convId = new ObjectId(params.id); + const searchId = new ObjectId(); + + const conv = await collections.conversations.findOne({ + _id: convId, + ...authCondition(locals), + }); + + if (!conv) { + throw error(404, "Conversation not found"); + } + + const prompt = z.string().trim().min(1).parse(url.searchParams.get("prompt")); + + const messages = (() => { + return [...conv.messages, { content: prompt, from: "user", id: crypto.randomUUID() }]; + })() satisfies Message[]; + + const stream = new ReadableStream({ + async start(controller) { + const webSearch: WebSearch = { + _id: searchId, + convId: convId, + prompt: prompt, + searchQuery: "", + knowledgeGraph: "", + answerBox: "", + results: [], + summary: "", + messages: [], + createdAt: new Date(), + updatedAt: new Date(), + }; + + function appendUpdate(message: string, args?: string[], type?: "error" | "update") { + webSearch.messages.push({ + type: type ?? "update", + message, + args, + }); + controller.enqueue(JSON.stringify({ messages: webSearch.messages })); + } + + try { + appendUpdate("Generating search query"); + webSearch.searchQuery = await generateQuery(messages, model); + + appendUpdate("Searching Google", [webSearch.searchQuery]); + const results = await searchWeb(webSearch.searchQuery); + + let text = ""; + webSearch.results = + (results.organic_results && + results.organic_results.map((el: { link: string }) => el.link)) ?? + []; + + if (results.answer_box) { + // if google returns an answer box, we use it + webSearch.answerBox = JSON.stringify(removeLinks(results.answer_box)); + text = webSearch.answerBox; + appendUpdate("Found a Google answer box"); + } else if (results.knowledge_graph) { + // if google returns a knowledge graph, we use it + webSearch.knowledgeGraph = JSON.stringify(removeLinks(results.knowledge_graph)); + text = webSearch.knowledgeGraph; + appendUpdate("Found a Google knowledge page"); + } else if (webSearch.results.length > 0) { + let tries = 0; + + while (!text && tries < 3) { + const searchUrl = webSearch.results[tries]; + appendUpdate("Browsing result", [JSON.stringify(searchUrl)]); + try { + text = await parseWeb(searchUrl); + if (!text) throw new Error("text of the webpage is null"); + } catch (e) { + appendUpdate("Error parsing webpage", [], "error"); + tries++; + } + } + if (!text) throw new Error("No text found on the first 3 results"); + } else { + throw new Error("No results found for this search query"); + } + + appendUpdate("Creating summary"); + webSearch.summary = await summarizeWeb(text, webSearch.searchQuery, model); + appendUpdate("Injecting summary", [JSON.stringify(webSearch.summary)]); + } catch (searchError) { + if (searchError instanceof Error) { + webSearch.messages.push({ + type: "error", + message: "An error occurred with the web search", + args: [JSON.stringify(searchError.message)], + }); + } + } + + const res = await collections.webSearches.insertOne(webSearch); + webSearch.messages.push({ + type: "result", + id: res.insertedId.toString(), + }); + controller.enqueue(JSON.stringify({ messages: webSearch.messages })); + }, + }); + + return new Response(stream, { headers: { "Content-Type": "application/json" } }); +} diff --git a/src/routes/conversations/+page.server.ts b/src/routes/conversations/+page.server.ts new file mode 100644 index 00000000000..e4f7daae5f7 --- /dev/null +++ b/src/routes/conversations/+page.server.ts @@ -0,0 +1,17 @@ +import { base } from "$app/paths"; +import { authCondition } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import { redirect } from "@sveltejs/kit"; + +export const actions = { + delete: async function ({ locals }) { + // double check we have a user to delete conversations for + if (locals.user?._id || locals.sessionId) { + await collections.conversations.deleteMany({ + ...authCondition(locals), + }); + } + + throw redirect(303, `${base}/`); + }, +}; diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts new file mode 100644 index 00000000000..28692b53046 --- /dev/null +++ b/src/routes/login/+page.server.ts @@ -0,0 +1,16 @@ +import { redirect } from "@sveltejs/kit"; +import { getOIDCAuthorizationUrl } from "$lib/server/auth"; +import { base } from "$app/paths"; + +export const actions = { + default: async function ({ url, locals, request }) { + // TODO: Handle errors if provider is not responding + const referer = request.headers.get("referer"); + const authorizationUrl = await getOIDCAuthorizationUrl( + { redirectURI: `${(referer ? new URL(referer) : url).origin}${base}/login/callback` }, + { sessionId: locals.sessionId } + ); + + throw redirect(303, authorizationUrl); + }, +}; diff --git a/src/routes/login/callback/+page.server.ts b/src/routes/login/callback/+page.server.ts new file mode 100644 index 00000000000..1ccbc1b48b6 --- /dev/null +++ b/src/routes/login/callback/+page.server.ts @@ -0,0 +1,39 @@ +import { redirect, error } from "@sveltejs/kit"; +import { getOIDCUserData, validateAndParseCsrfToken } from "$lib/server/auth"; +import { z } from "zod"; +import { base } from "$app/paths"; +import { updateUser } from "./updateUser"; + +export async function load({ url, locals, cookies }) { + const { error: errorName, error_description: errorDescription } = z + .object({ + error: z.string().optional(), + error_description: z.string().optional(), + }) + .parse(Object.fromEntries(url.searchParams.entries())); + + if (errorName) { + throw error(400, errorName + (errorDescription ? ": " + errorDescription : "")); + } + + const { code, state } = z + .object({ + code: z.string(), + state: z.string(), + }) + .parse(Object.fromEntries(url.searchParams.entries())); + + const csrfToken = Buffer.from(state, "base64").toString("utf-8"); + + const validatedToken = await validateAndParseCsrfToken(csrfToken, locals.sessionId); + + if (!validatedToken) { + throw error(403, "Invalid or expired CSRF token"); + } + + const { userData } = await getOIDCUserData({ redirectURI: validatedToken.redirectUrl }, code); + + await updateUser({ userData, locals, cookies }); + + throw redirect(302, `${base}/`); +} diff --git a/src/routes/login/callback/updateUser.spec.ts b/src/routes/login/callback/updateUser.spec.ts new file mode 100644 index 00000000000..24a7ff27575 --- /dev/null +++ b/src/routes/login/callback/updateUser.spec.ts @@ -0,0 +1,144 @@ +import { assert, it, describe, afterEach, vi, expect } from "vitest"; +import type { Cookies } from "@sveltejs/kit"; +import { collections } from "$lib/server/database"; +import { updateUser } from "./updateUser"; +import { ObjectId } from "mongodb"; +import { DEFAULT_SETTINGS } from "$lib/types/Settings"; +import { defaultModel } from "$lib/server/models"; + +const userData = { + preferred_username: "new-username", + name: "name", + picture: "https://example.com/avatar.png", + sub: "1234567890", +}; + +const locals = { + userId: "1234567890", + sessionId: "1234567890", +}; + +// @ts-expect-error SvelteKit cookies dumb mock +const cookiesMock: Cookies = { + set: vi.fn(), +}; + +const insertRandomUser = async () => { + const res = await collections.users.insertOne({ + _id: new ObjectId(), + createdAt: new Date(), + updatedAt: new Date(), + username: "base-username", + name: userData.name, + avatarUrl: userData.picture, + hfUserId: userData.sub, + sessionId: locals.sessionId, + }); + + return res.insertedId; +}; + +const insertRandomConversations = async (count: number) => { + const res = await collections.conversations.insertMany( + new Array(count).fill(0).map(() => ({ + _id: new ObjectId(), + title: "random title", + messages: [], + model: defaultModel.id, + createdAt: new Date(), + updatedAt: new Date(), + sessionId: locals.sessionId, + })) + ); + + return res.insertedIds; +}; + +describe("login", () => { + it("should update user if existing", async () => { + await insertRandomUser(); + + await updateUser({ userData, locals, cookies: cookiesMock }); + + const existingUser = await collections.users.findOne({ hfUserId: userData.sub }); + + assert.equal(existingUser?.name, userData.name); + + expect(cookiesMock.set).toBeCalledTimes(1); + }); + + it("should migrate pre-existing conversations for new user", async () => { + const insertedId = await insertRandomUser(); + + await insertRandomConversations(2); + + await updateUser({ userData, locals, cookies: cookiesMock }); + + const conversationCount = await collections.conversations.countDocuments({ + userId: insertedId, + sessionId: { $exists: false }, + }); + + assert.equal(conversationCount, 2); + + await collections.conversations.deleteMany({ userId: insertedId }); + }); + + it("should create default settings for new user", async () => { + await updateUser({ userData, locals, cookies: cookiesMock }); + + const user = await collections.users.findOne({ sessionId: locals.sessionId }); + + assert.exists(user); + + const settings = await collections.settings.findOne({ userId: user?._id }); + + expect(settings).toMatchObject({ + userId: user?._id, + updatedAt: expect.any(Date), + createdAt: expect.any(Date), + ethicsModalAcceptedAt: expect.any(Date), + ...DEFAULT_SETTINGS, + }); + + await collections.settings.deleteOne({ userId: user?._id }); + }); + + it("should migrate pre-existing settings for pre-existing user", async () => { + const { insertedId } = await collections.settings.insertOne({ + sessionId: locals.sessionId, + ethicsModalAcceptedAt: new Date(), + updatedAt: new Date(), + createdAt: new Date(), + ...DEFAULT_SETTINGS, + shareConversationsWithModelAuthors: false, + }); + + await updateUser({ userData, locals, cookies: cookiesMock }); + + const settings = await collections.settings.findOne({ + _id: insertedId, + sessionId: { $exists: false }, + }); + + assert.exists(settings); + + const user = await collections.users.findOne({ hfUserId: userData.sub }); + + expect(settings).toMatchObject({ + userId: user?._id, + updatedAt: expect.any(Date), + createdAt: expect.any(Date), + ethicsModalAcceptedAt: expect.any(Date), + ...DEFAULT_SETTINGS, + shareConversationsWithModelAuthors: false, + }); + + await collections.settings.deleteOne({ userId: user?._id }); + }); +}); + +afterEach(async () => { + await collections.users.deleteMany({ hfUserId: userData.sub }); + vi.clearAllMocks(); +}); diff --git a/src/routes/login/callback/updateUser.ts b/src/routes/login/callback/updateUser.ts new file mode 100644 index 00000000000..e7c8b5f643e --- /dev/null +++ b/src/routes/login/callback/updateUser.ts @@ -0,0 +1,84 @@ +import { authCondition, refreshSessionCookie } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { DEFAULT_SETTINGS } from "$lib/types/Settings"; +import { z } from "zod"; +import type { UserinfoResponse } from "openid-client"; +import type { Cookies } from "@sveltejs/kit"; + +export async function updateUser(params: { + userData: UserinfoResponse; + locals: App.Locals; + cookies: Cookies; +}) { + const { userData, locals, cookies } = params; + const { + preferred_username: username, + name, + email, + picture: avatarUrl, + sub: hfUserId, + } = z + .object({ + preferred_username: z.string().optional(), + name: z.string(), + picture: z.string(), + sub: z.string(), + email: z.string().email().optional(), + }) + .refine((data) => data.preferred_username || data.email, { + message: "Either preferred_username or email must be provided by the provider.", + }) + .parse(userData); + + const existingUser = await collections.users.findOne({ hfUserId }); + let userId = existingUser?._id; + + if (existingUser) { + // update existing user if any + await collections.users.updateOne( + { _id: existingUser._id }, + { $set: { username, name, avatarUrl } } + ); + // refresh session cookie + refreshSessionCookie(cookies, existingUser.sessionId); + } else { + // user doesn't exist yet, create a new one + const { insertedId } = await collections.users.insertOne({ + _id: new ObjectId(), + createdAt: new Date(), + updatedAt: new Date(), + username, + name, + email, + avatarUrl, + hfUserId, + sessionId: locals.sessionId, + }); + + userId = insertedId; + + // update pre-existing settings + const { matchedCount } = await collections.settings.updateOne(authCondition(locals), { + $set: { userId, updatedAt: new Date() }, + $unset: { sessionId: "" }, + }); + + if (!matchedCount) { + // create new default settings + await collections.settings.insertOne({ + userId, + ethicsModalAcceptedAt: new Date(), + updatedAt: new Date(), + createdAt: new Date(), + ...DEFAULT_SETTINGS, + }); + } + } + + // migrate pre-existing conversations + await collections.conversations.updateMany(authCondition(locals), { + $set: { userId }, + $unset: { sessionId: "" }, + }); +} diff --git a/src/routes/logout/+page.server.ts b/src/routes/logout/+page.server.ts new file mode 100644 index 00000000000..1d60b6c5d8d --- /dev/null +++ b/src/routes/logout/+page.server.ts @@ -0,0 +1,17 @@ +import { dev } from "$app/environment"; +import { base } from "$app/paths"; +import { COOKIE_NAME } from "$env/static/private"; +import { redirect } from "@sveltejs/kit"; + +export const actions = { + default: async function ({ cookies }) { + cookies.delete(COOKIE_NAME, { + path: "/", + // So that it works inside the space's iframe + sameSite: dev ? "lax" : "none", + secure: !dev, + httpOnly: true, + }); + throw redirect(303, `${base}/`); + }, +}; diff --git a/src/routes/r/[id]/+page.server.ts b/src/routes/r/[id]/+page.server.ts index f065f39a0c5..4bc892004c5 100644 --- a/src/routes/r/[id]/+page.server.ts +++ b/src/routes/r/[id]/+page.server.ts @@ -1,6 +1,8 @@ import type { PageServerLoad } from "./$types"; import { collections } from "$lib/server/database"; import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; +import type { WebSearchMessageResult } from "$lib/types/WebSearch"; export const load: PageServerLoad = async ({ params }) => { const conversation = await collections.sharedConversations.findOne({ @@ -11,9 +13,23 @@ export const load: PageServerLoad = async ({ params }) => { throw error(404, "Conversation not found"); } + const webSearchesId = conversation.messages + .filter((message) => message.webSearchId) + .map((message) => new ObjectId(message.webSearchId)); + + const results = await collections.webSearches.find({ _id: { $in: webSearchesId } }).toArray(); + + const searches = Object.fromEntries( + results.map((x) => [ + x._id.toString(), + [...x.messages, { type: "result", id: x._id.toString() } satisfies WebSearchMessageResult], + ]) + ); + return { messages: conversation.messages, title: conversation.title, model: conversation.model, + searches, }; }; diff --git a/src/routes/r/[id]/+page.svelte b/src/routes/r/[id]/+page.svelte index e962c77eb9c..d5894be27a6 100644 --- a/src/routes/r/[id]/+page.svelte +++ b/src/routes/r/[id]/+page.svelte @@ -2,6 +2,7 @@ import { goto } from "$app/navigation"; import { base } from "$app/paths"; import { page } from "$app/stores"; + import { PUBLIC_APP_DISCLAIMER } from "$env/static/public"; import ChatWindow from "$lib/components/chat/ChatWindow.svelte"; import { ERROR_MESSAGES, error } from "$lib/stores/errors"; import { pendingMessage } from "$lib/stores/pendingMessage"; @@ -24,6 +25,7 @@ }, body: JSON.stringify({ fromShare: $page.params.id, + model: data.model, }), }); @@ -55,6 +57,10 @@ createConversation() .then((convId) => { @@ -71,9 +77,11 @@ return goto(`${base}/conversation/${convId}`, { invalidateAll: true }); }) .finally(() => (loading = false))} - messages={data.messages} models={data.models} currentModel={findCurrentModel(data.models, data.model)} settings={data.settings} - {loading} + loginRequired={!$page.error && + (data.requiresLogin + ? !data.user + : !data.settings.ethicsModalAcceptedAt && !!PUBLIC_APP_DISCLAIMER)} /> diff --git a/src/routes/r/[id]/message/[messageId]/prompt/+server.ts b/src/routes/r/[id]/message/[messageId]/prompt/+server.ts index 47ccde935de..255fa79d83b 100644 --- a/src/routes/r/[id]/message/[messageId]/prompt/+server.ts +++ b/src/routes/r/[id]/message/[messageId]/prompt/+server.ts @@ -26,7 +26,7 @@ export async function GET({ params }) { throw error(404, "Conversation model not found"); } - const prompt = buildPrompt(conv.messages.slice(0, messageIndex + 1), model); + const prompt = await buildPrompt(conv.messages.slice(0, messageIndex + 1), model); return new Response( JSON.stringify( diff --git a/src/routes/search/[id]/+server.ts b/src/routes/search/[id]/+server.ts new file mode 100644 index 00000000000..3402bff6a14 --- /dev/null +++ b/src/routes/search/[id]/+server.ts @@ -0,0 +1,39 @@ +import { collections } from "$lib/server/database"; +import { hashConv } from "$lib/utils/hashConv.js"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; + +export async function GET({ params, locals }) { + const searchId = new ObjectId(params.id); + + const search = await collections.webSearches.findOne({ + _id: searchId, + }); + + if (!search) { + throw error(404, "Search query not found"); + } + + const conv = await collections.conversations.findOne({ + _id: search.convId, + }); + + if (!conv) { + throw error(404, "Conversation not found"); + } + + // there's no better way to see if a conversation has been shared, so we hash the messages and see if there's a shared conversation with the same hash + const hash = await hashConv(conv); + const sharedConv = await collections.sharedConversations.findOne({ + hash: hash, + }); + + const userShouldSeeConv = + (conv.userId && locals.user?._id.toString() === conv.userId.toString()) || sharedConv !== null; + + if (!userShouldSeeConv) { + throw error(403, "You don't have access to the conversation here."); + } + + return new Response(JSON.stringify(search), { headers: { "Content-Type": "application/json" } }); +} diff --git a/src/routes/settings/+page.server.ts b/src/routes/settings/+page.server.ts index 6e5f4b3d5d8..582c009559e 100644 --- a/src/routes/settings/+page.server.ts +++ b/src/routes/settings/+page.server.ts @@ -2,8 +2,9 @@ import { base } from "$app/paths"; import { collections } from "$lib/server/database"; import { redirect } from "@sveltejs/kit"; import { z } from "zod"; -import { defaultModel, models, validateModel } from "$lib/server/models"; +import { models, validateModel } from "$lib/server/models"; import { authCondition } from "$lib/server/auth"; +import { DEFAULT_SETTINGS } from "$lib/types/Settings"; export const actions = { default: async function ({ request, locals }) { @@ -11,14 +12,16 @@ export const actions = { const { ethicsModalAccepted, ...settings } = z .object({ - shareConversationsWithModelAuthors: z.boolean({ coerce: true }).default(true), + shareConversationsWithModelAuthors: z + .boolean({ coerce: true }) + .default(DEFAULT_SETTINGS.shareConversationsWithModelAuthors), ethicsModalAccepted: z.boolean({ coerce: true }).optional(), activeModel: validateModel(models), }) .parse({ shareConversationsWithModelAuthors: formData.get("shareConversationsWithModelAuthors"), ethicsModalAccepted: formData.get("ethicsModalAccepted"), - activeModel: formData.get("activeModel") ?? defaultModel.id, + activeModel: formData.get("activeModel") ?? DEFAULT_SETTINGS.activeModel, }); await collections.settings.updateOne( @@ -38,6 +41,6 @@ export const actions = { } ); - throw redirect(303, request.headers.get("referer") || base || "/"); + throw redirect(303, request.headers.get("referer") || `${base}/`); }, }; diff --git a/static/chatui/favicon.png b/static/chatui/favicon.png new file mode 100644 index 00000000000..5a3f7c66f7c Binary files /dev/null and b/static/chatui/favicon.png differ diff --git a/static/chatui/favicon.svg b/static/chatui/favicon.svg new file mode 100644 index 00000000000..cc55dcb9e60 --- /dev/null +++ b/static/chatui/favicon.svg @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/static/chatui/touch-icon-ipad-retina.png b/static/chatui/touch-icon-ipad-retina.png new file mode 100644 index 00000000000..38a6b8c0310 Binary files /dev/null and b/static/chatui/touch-icon-ipad-retina.png differ diff --git a/static/chatui/touch-icon-ipad.png b/static/chatui/touch-icon-ipad.png new file mode 100644 index 00000000000..66edeb65eb1 Binary files /dev/null and b/static/chatui/touch-icon-ipad.png differ diff --git a/static/chatui/touch-icon-iphone-retina.png b/static/chatui/touch-icon-iphone-retina.png new file mode 100644 index 00000000000..776c4d11855 Binary files /dev/null and b/static/chatui/touch-icon-iphone-retina.png differ diff --git a/static/favicon.png b/static/huggingchat/favicon.png similarity index 100% rename from static/favicon.png rename to static/huggingchat/favicon.png diff --git a/static/huggingchat/favicon.svg b/static/huggingchat/favicon.svg new file mode 100644 index 00000000000..baf19d86510 --- /dev/null +++ b/static/huggingchat/favicon.svg @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/static/thumbnail.png b/static/huggingchat/thumbnail.png similarity index 100% rename from static/thumbnail.png rename to static/huggingchat/thumbnail.png diff --git a/static/huggingchat/touch-icon-ipad-retina.png b/static/huggingchat/touch-icon-ipad-retina.png new file mode 100644 index 00000000000..61bdbba6b9f Binary files /dev/null and b/static/huggingchat/touch-icon-ipad-retina.png differ diff --git a/static/huggingchat/touch-icon-ipad.png b/static/huggingchat/touch-icon-ipad.png new file mode 100644 index 00000000000..895fc958003 Binary files /dev/null and b/static/huggingchat/touch-icon-ipad.png differ diff --git a/static/huggingchat/touch-icon-iphone-retina.png b/static/huggingchat/touch-icon-iphone-retina.png new file mode 100644 index 00000000000..cdf6d55032e Binary files /dev/null and b/static/huggingchat/touch-icon-iphone-retina.png differ diff --git a/svelte.config.js b/svelte.config.js index 3648883ae23..e93decaf872 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,12 +1,11 @@ import adapter from "@sveltejs/adapter-node"; import { vitePreprocess } from "@sveltejs/kit/vite"; import dotenv from "dotenv"; -import pkg from "./package.json" assert { type: "json" }; dotenv.config({ path: "./.env.local" }); dotenv.config({ path: "./.env" }); -process.env.PUBLIC_VERSION = pkg.version.replace(/\.0\b/g, ""); +process.env.PUBLIC_VERSION = process.env.npm_package_version; /** @type {import('@sveltejs/kit').Config} */ const config = { @@ -21,7 +20,7 @@ const config = { base: process.env.APP_BASE || "", }, csrf: { - // todo: fix + // handled in hooks.server.ts, because we can have multiple valid origins checkOrigin: false, }, }, diff --git a/tailwind.config.cjs b/tailwind.config.cjs index c9ecaf0cae5..773347ec02b 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -1,4 +1,8 @@ const defaultTheme = require("tailwindcss/defaultTheme"); +const colors = require("tailwindcss/colors"); + +import dotenv from "dotenv"; +dotenv.config({ path: "./.env" }); /** @type {import('tailwindcss').Config} */ export default { @@ -6,6 +10,9 @@ export default { content: ["./src/**/*.{html,js,svelte,ts}"], theme: { extend: { + colors: { + primary: colors[process.env.PUBLIC_APP_COLOR], + }, // fontFamily: { // sans: ['"Inter"', ...defaultTheme.fontFamily.sans] // },