Compare commits
	
		
			211 Commits
		
	
	
		
			da1123d84b
			...
			feat/confi
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 8f22d9f626 | ||
|  | 3564bcc48c | ||
|  | e471cb5da1 | ||
|  | 836cfb80c8 | ||
|  | 9d0e2e7f7c | ||
|  | df1ed5f0f9 | ||
|  | f55c2371fe | ||
|  | 3e03947b1b | ||
|  | 86143c83d8 | ||
|  | 2c9012e99c | ||
|  | d43ef9e43d | ||
|  | 53a1b3bc56 | ||
|  | f28ea8cee2 | ||
|  | 716629f6fe | ||
|  | 190b6aa79a | ||
|  | 97e542acf8 | ||
|  | 97bee75e39 | ||
|  | 16b31fe34f | ||
|  | 8a1052e82b | ||
|  | 43f23e21a3 | ||
|  | 5a7f45cace | ||
|  | e02b9a5efc | ||
|  | 09dd8dba5a | ||
|  | ca8b74b695 | ||
|  | ac7cfac784 | ||
|  | 861d50674a | ||
|  | d97fa708f1 | ||
|  | 0c7566bb87 | ||
|  | 0ff1be47bf | ||
|  | 768578951c | ||
|  | 9706079ed4 | ||
|  | 9219593ee1 | ||
|  | 36fdb6491d | ||
|  | 0d2cd4bb1e | ||
|  | b67ca79e2a | ||
|  | 626cb646e2 | ||
|  | 410201b117 | ||
|  | 30fb1e312b | ||
|  | cc5eea17e4 | ||
|  | 4ee3173368 | ||
|  | 6d61528347 | ||
|  | c02e535f4c | ||
|  | a375de73cc | ||
|  | 87226957f1 | ||
|  | 77743949c7 | ||
|  | 64c4514cad | ||
|  | 999553877d | ||
|  | e45a9af9ff | ||
|  | e7fbab12ed | ||
|  | 387da5dbdd | ||
|  | 3003d44544 | ||
|  | f1e6aa9c1a | ||
|  | f39638fe02 | ||
|  | 535c0b9897 | ||
|  | 47350b34ec | ||
|  | 7c97df98c7 | ||
|  | b084c42aca | ||
|  | fdfa2f3ea6 | ||
|  | 3323e7a0ed | ||
|  | d4f9da34c6 | ||
|  | 10ed67c753 | ||
|  | cf3cc4e638 | ||
|  | 46b9e41100 | ||
|  | 02adafbd4b | ||
|  | f141d4719c | ||
|  | f18e132d1b | ||
|  | 37317992b4 | ||
|  | 8b588824f2 | ||
|  | a19cf00873 | ||
|  | 5cc11ac0bf | ||
|  | d611ddaab9 | ||
|  | 3b41905abb | ||
|  | b9071ceed7 | ||
|  | bb5002de85 | ||
|  | fc99653441 | ||
|  | 23dde9fa59 | ||
|  | dde6c8d719 | ||
|  | 650c69e04f | ||
|  | 984163bbbc | ||
|  | 5f18fc1d22 | ||
|  | b1426e8638 | ||
|  | 7337f3423d | ||
|  | 8f728a2518 | ||
|  | bd8e3dfa2e | ||
|  | 9c6e42f7d8 | ||
|  | fabb48cc2f | ||
|  | c46b421219 | ||
|  | 8dc24c2d1a | ||
|  | 8afcdd044c | ||
|  | 5b12e99335 | ||
|  | 5b5e83a3a0 | ||
|  | 6dd33aa33c | ||
|  | e705952503 | ||
|  | b8e4152e77 | ||
|  | c8ac9279bd | ||
|  | 6f367c34a8 | ||
|  | 328b12ffbe | ||
|  | 536ec24093 | ||
|  | bb9eab7aa7 | ||
|  | b0e8a33f1d | ||
|  | a268ce345c | ||
|  | 7b46b815c1 | ||
|  | d6b02db37a | ||
|  | 34fa52ad12 | ||
|  | 266c333b29 | ||
|  | a6f3d98aef | ||
|  | 705ae464ad | ||
|  | d8486e90bb | ||
|  | 238bcaff2b | ||
|  | 6f7c55b783 | ||
|  | 83a0cffe1b | ||
|  | 829ae59944 | ||
|  | a546eb18a1 | ||
|  | ff1ca56157 | ||
|  | 30725b5d6d | ||
|  | 8dc54efbdd | ||
|  | 72f26b4370 | ||
|  | f680188905 | ||
|  | 0b15bfbe32 | ||
|  | 8fc7808654 | ||
|  | 0dc17286b9 | ||
|  | 3edd7d44dd | ||
|  | 1132997108 | ||
|  | eadbedb713 | ||
|  | 37cd6d3ab5 | ||
|  | 88be3a045b | ||
|  | 45b51ab156 | ||
|  | 3bee01cfa7 | ||
|  | 567c6a8758 | ||
|  | 81a91da743 | ||
|  | 70a61ee1eb | ||
|  | 9d89a4413b | ||
|  | 6ea17d54c6 | ||
|  | 11a828b073 | ||
|  | 37022fb11e | ||
|  | dd50d4927b | ||
|  | fdaf3af3af | ||
|  | 3f2a8f862c | ||
|  | 58c7be6e95 | ||
|  | 829b4e7134 | ||
|  | 77870b39cc | ||
|  | 8e0ae9b867 | ||
|  | 543f1df5ce | ||
|  | 341aae4587 | ||
|  | 7f62907385 | ||
|  | 7c4aa683a2 | ||
|  | b48b0eeb0e | ||
|  | cddc793915 | ||
|  | 94e6db10bb | ||
|  | 65fc881356 | ||
|  | 26e1d5fec3 | ||
|  | 66be87b688 | ||
|  | f7b4e32218 | ||
|  | 57407112fb | ||
|  | b280cc2e01 | ||
|  | e6ebf892c5 | ||
|  | b754641058 | ||
|  | 722f4f760e | ||
|  | 01e04a209f | ||
|  | 0299fd1ea0 | ||
|  | cf8dec53ca | ||
|  | d5c012d748 | ||
|  | 2ccbd9a44c | ||
|  | ccd89d48d9 | ||
|  | 87d788ddef | ||
|  | 809b625a34 | ||
|  | 95c753a549 | ||
|  | 0bb8b7ec5c | ||
|  | c6d084f5dc | ||
|  | 0024ce36c8 | ||
|  | c44e746807 | ||
|  | b1826066f4 | ||
|  | b0b8acc45b | ||
|  | e2b9ffc072 | ||
|  | 68c43ea372 | ||
|  | 3b46baca4f | ||
|  | 772e461c08 | ||
|  | 5c6018a0f9 | ||
|  | 0b7989c3d3 | ||
|  | 8cfcc3e39c | ||
|  | 3a57261590 | ||
|  | a86a1a461c | ||
|  | 2257e1df0c | ||
|  | ccb72c8970 | ||
|  | 740ff941a5 | ||
|  | 117a683d9a | ||
|  | 9eba4b7373 | ||
|  | 91306dc0c7 | ||
|  | 1716dd5a65 | ||
|  | 66f9a674f1 | ||
|  | 41fc5274ff | ||
|  | bcebdb5fd9 | ||
|  | 876487ad11 | ||
|  | 18da75ad97 | ||
|  | c80ac1415d | ||
|  | bb21184ea2 | ||
|  | 0c3740fdf2 | ||
|  | 701819d018 | ||
|  | 68e151b2bd | ||
|  | 06ff272541 | ||
|  | 4154d5e4b1 | ||
|  | 1862491496 | ||
|  | 073b5e897c | ||
|  | 7e1d6ebd19 | ||
|  | 9a332e79e4 | ||
|  | 72450b9217 | ||
|  | 7e1dc33a08 | ||
|  | aa240009ab | ||
|  | 41b258e4d8 | ||
|  | 28b9cca413 | ||
|  | 8aaee2c40c | 
							
								
								
									
										0
									
								
								.assets/manifest.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| Before Width: | Height: | Size: 641 KiB After Width: | Height: | Size: 2.1 MiB | 
							
								
								
									
										36
									
								
								.github/workflows/docker-build.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -4,6 +4,7 @@ on: | |||||||
|   push: |   push: | ||||||
|     branches: |     branches: | ||||||
|       - master |       - master | ||||||
|  |       - canary | ||||||
|   release: |   release: | ||||||
|     types: [published] |     types: [published] | ||||||
|  |  | ||||||
| @@ -43,6 +44,19 @@ jobs: | |||||||
|             -t itzcrazykns1337/${IMAGE_NAME}:amd64 \ |             -t itzcrazykns1337/${IMAGE_NAME}:amd64 \ | ||||||
|             --push . |             --push . | ||||||
|  |  | ||||||
|  |       - name: Build and push AMD64 Canary Docker image | ||||||
|  |         if: github.ref == 'refs/heads/canary' && github.event_name == 'push' | ||||||
|  |         run: | | ||||||
|  |           DOCKERFILE=app.dockerfile | ||||||
|  |           IMAGE_NAME=perplexica | ||||||
|  |           docker buildx build --platform linux/amd64 \ | ||||||
|  |             --cache-from=type=registry,ref=itzcrazykns1337/${IMAGE_NAME}:canary-amd64 \ | ||||||
|  |             --cache-to=type=inline \ | ||||||
|  |             --provenance false \ | ||||||
|  |             -f $DOCKERFILE \ | ||||||
|  |             -t itzcrazykns1337/${IMAGE_NAME}:canary-amd64 \ | ||||||
|  |             --push . | ||||||
|  |  | ||||||
|       - name: Build and push AMD64 release Docker image |       - name: Build and push AMD64 release Docker image | ||||||
|         if: github.event_name == 'release' |         if: github.event_name == 'release' | ||||||
|         run: | |         run: | | ||||||
| @@ -91,6 +105,19 @@ jobs: | |||||||
|             -t itzcrazykns1337/${IMAGE_NAME}:arm64 \ |             -t itzcrazykns1337/${IMAGE_NAME}:arm64 \ | ||||||
|             --push . |             --push . | ||||||
|  |  | ||||||
|  |       - name: Build and push ARM64 Canary Docker image | ||||||
|  |         if: github.ref == 'refs/heads/canary' && github.event_name == 'push' | ||||||
|  |         run: | | ||||||
|  |           DOCKERFILE=app.dockerfile | ||||||
|  |           IMAGE_NAME=perplexica | ||||||
|  |           docker buildx build --platform linux/arm64 \ | ||||||
|  |             --cache-from=type=registry,ref=itzcrazykns1337/${IMAGE_NAME}:canary-arm64 \ | ||||||
|  |             --cache-to=type=inline \ | ||||||
|  |             --provenance false \ | ||||||
|  |             -f $DOCKERFILE \ | ||||||
|  |             -t itzcrazykns1337/${IMAGE_NAME}:canary-arm64 \ | ||||||
|  |             --push . | ||||||
|  |  | ||||||
|       - name: Build and push ARM64 release Docker image |       - name: Build and push ARM64 release Docker image | ||||||
|         if: github.event_name == 'release' |         if: github.event_name == 'release' | ||||||
|         run: | |         run: | | ||||||
| @@ -128,6 +155,15 @@ jobs: | |||||||
|             --amend itzcrazykns1337/${IMAGE_NAME}:arm64 |             --amend itzcrazykns1337/${IMAGE_NAME}:arm64 | ||||||
|           docker manifest push itzcrazykns1337/${IMAGE_NAME}:main |           docker manifest push itzcrazykns1337/${IMAGE_NAME}:main | ||||||
|  |  | ||||||
|  |       - name: Create and push multi-arch manifest for canary | ||||||
|  |         if: github.ref == 'refs/heads/canary' && github.event_name == 'push' | ||||||
|  |         run: | | ||||||
|  |           IMAGE_NAME=perplexica | ||||||
|  |           docker manifest create itzcrazykns1337/${IMAGE_NAME}:canary \ | ||||||
|  |             --amend itzcrazykns1337/${IMAGE_NAME}:canary-amd64 \ | ||||||
|  |             --amend itzcrazykns1337/${IMAGE_NAME}:canary-arm64 | ||||||
|  |           docker manifest push itzcrazykns1337/${IMAGE_NAME}:canary | ||||||
|  |  | ||||||
|       - name: Create and push multi-arch manifest for releases |       - name: Create and push multi-arch manifest for releases | ||||||
|         if: github.event_name == 'release' |         if: github.event_name == 'release' | ||||||
|         run: | |         run: | | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -37,3 +37,5 @@ Thumbs.db | |||||||
| # Db | # Db | ||||||
| db.sqlite | db.sqlite | ||||||
| /searxng | /searxng | ||||||
|  |  | ||||||
|  | certificates | ||||||
| @@ -36,7 +36,7 @@ Before diving into coding, setting up your local environment is key. Here's what | |||||||
| 1. In the root directory, locate the `sample.config.toml` file. | 1. In the root directory, locate the `sample.config.toml` file. | ||||||
| 2. Rename it to `config.toml` and fill in the necessary configuration fields. | 2. Rename it to `config.toml` and fill in the necessary configuration fields. | ||||||
| 3. Run `npm install` to install all dependencies. | 3. Run `npm install` to install all dependencies. | ||||||
| 4. Run `npm run db:push` to set up the local sqlite database. | 4. Run `npm run db:migrate` to set up the local sqlite database. | ||||||
| 5. Use `npm run dev` to start the application in development mode. | 5. Use `npm run dev` to start the application in development mode. | ||||||
|  |  | ||||||
| **Please note**: Docker configurations are present for setting up production environments, whereas `npm run dev` is used for development purposes. | **Please note**: Docker configurations are present for setting up production environments, whereas `npm run dev` is used for development purposes. | ||||||
|   | |||||||
							
								
								
									
										49
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -16,7 +16,7 @@ | |||||||
|  |  | ||||||
| <hr/> | <hr/> | ||||||
|  |  | ||||||
| [](https://discord.gg/26aArMy8tT) | [](https://discord.gg/26aArMy8tT) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -29,6 +29,7 @@ | |||||||
|   - [Getting Started with Docker (Recommended)](#getting-started-with-docker-recommended) |   - [Getting Started with Docker (Recommended)](#getting-started-with-docker-recommended) | ||||||
|   - [Non-Docker Installation](#non-docker-installation) |   - [Non-Docker Installation](#non-docker-installation) | ||||||
|   - [Ollama Connection Errors](#ollama-connection-errors) |   - [Ollama Connection Errors](#ollama-connection-errors) | ||||||
|  |   - [Lemonade Connection Errors](#lemonade-connection-errors) | ||||||
| - [Using as a Search Engine](#using-as-a-search-engine) | - [Using as a Search Engine](#using-as-a-search-engine) | ||||||
| - [Using Perplexica's API](#using-perplexicas-api) | - [Using Perplexica's API](#using-perplexicas-api) | ||||||
| - [Expose Perplexica to a network](#expose-perplexica-to-network) | - [Expose Perplexica to a network](#expose-perplexica-to-network) | ||||||
| @@ -53,7 +54,7 @@ Want to know more about its architecture and how it works? You can read it [here | |||||||
|  |  | ||||||
| ## Features | ## Features | ||||||
|  |  | ||||||
| - **Local LLMs**: You can make use local LLMs such as Llama3 and Mixtral using Ollama. | - **Local LLMs**: You can utilize local LLMs such as Qwen, DeepSeek, Llama, and Mistral. | ||||||
| - **Two Main Modes:** | - **Two Main Modes:** | ||||||
|   - **Copilot Mode:** (In development) Boosts search by generating different queries to find more relevant internet sources. Like normal search instead of just using the context by SearxNG, it visits the top matches and tries to find relevant sources to the user's query directly from the page. |   - **Copilot Mode:** (In development) Boosts search by generating different queries to find more relevant internet sources. Like normal search instead of just using the context by SearxNG, it visits the top matches and tries to find relevant sources to the user's query directly from the page. | ||||||
|   - **Normal Mode:** Processes your query and performs a web search. |   - **Normal Mode:** Processes your query and performs a web search. | ||||||
| @@ -87,9 +88,14 @@ There are mainly 2 ways of installing Perplexica - With Docker, Without Docker. | |||||||
| 4. Rename the `sample.config.toml` file to `config.toml`. For Docker setups, you need only fill in the following fields: | 4. Rename the `sample.config.toml` file to `config.toml`. For Docker setups, you need only fill in the following fields: | ||||||
|  |  | ||||||
|    - `OPENAI`: Your OpenAI API key. **You only need to fill this if you wish to use OpenAI's models**. |    - `OPENAI`: Your OpenAI API key. **You only need to fill this if you wish to use OpenAI's models**. | ||||||
|  |    - `CUSTOM_OPENAI`: Your OpenAI-API-compliant local server URL, model name, and API key. You should run your local server with host set to `0.0.0.0`, take note of which port number it is running on, and then use that port number to set `API_URL = http://host.docker.internal:PORT_NUMBER`. You must specify the model name, such as `MODEL_NAME = "unsloth/DeepSeek-R1-0528-Qwen3-8B-GGUF:Q4_K_XL"`. Finally, set `API_KEY` to the appropriate value. If you have not defined an API key, just put anything you want in-between the quotation marks: `API_KEY = "whatever-you-want-but-not-blank"` **You only need to configure these settings if you want to use a local OpenAI-compliant server, such as Llama.cpp's [`llama-server`](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md)**. | ||||||
|    - `OLLAMA`: Your Ollama API URL. You should enter it as `http://host.docker.internal:PORT_NUMBER`. If you installed Ollama on port 11434, use `http://host.docker.internal:11434`. For other ports, adjust accordingly. **You need to fill this if you wish to use Ollama's models instead of OpenAI's**. |    - `OLLAMA`: Your Ollama API URL. You should enter it as `http://host.docker.internal:PORT_NUMBER`. If you installed Ollama on port 11434, use `http://host.docker.internal:11434`. For other ports, adjust accordingly. **You need to fill this if you wish to use Ollama's models instead of OpenAI's**. | ||||||
|    - `GROQ`: Your Groq API key. **You only need to fill this if you wish to use Groq's hosted models**. |    - `LEMONADE`: Your Lemonade API URL. Since Lemonade runs directly on your local machine (not in Docker), you should enter it as `http://host.docker.internal:PORT_NUMBER`. If you installed Lemonade on port 8000, use `http://host.docker.internal:8000`. For other ports, adjust accordingly. **You need to fill this if you wish to use Lemonade's models**. | ||||||
|  |    - `GROQ`: Your Groq API key. **You only need to fill this if you wish to use Groq's hosted models**.` | ||||||
|    - `ANTHROPIC`: Your Anthropic API key. **You only need to fill this if you wish to use Anthropic models**. |    - `ANTHROPIC`: Your Anthropic API key. **You only need to fill this if you wish to use Anthropic models**. | ||||||
|  |    - `Gemini`: Your Gemini API key. **You only need to fill this if you wish to use Google's models**. | ||||||
|  |    - `DEEPSEEK`: Your Deepseek API key. **Only needed if you want Deepseek models.** | ||||||
|  |    - `AIMLAPI`: Your AI/ML API key. **Only needed if you want to use AI/ML API models and embeddings.** | ||||||
|  |  | ||||||
|      **Note**: You can change these after starting Perplexica from the settings dialog. |      **Note**: You can change these after starting Perplexica from the settings dialog. | ||||||
|  |  | ||||||
| @@ -111,13 +117,23 @@ There are mainly 2 ways of installing Perplexica - With Docker, Without Docker. | |||||||
| 2. Clone the repository and rename the `sample.config.toml` file to `config.toml` in the root directory. Ensure you complete all required fields in this file. | 2. Clone the repository and rename the `sample.config.toml` file to `config.toml` in the root directory. Ensure you complete all required fields in this file. | ||||||
| 3. After populating the configuration run `npm i`. | 3. After populating the configuration run `npm i`. | ||||||
| 4. Install the dependencies and then execute `npm run build`. | 4. Install the dependencies and then execute `npm run build`. | ||||||
| 5. Finally, start the app by running `npm rum start` | 5. Finally, start the app by running `npm run start` | ||||||
|  |  | ||||||
| **Note**: Using Docker is recommended as it simplifies the setup process, especially for managing environment variables and dependencies. | **Note**: Using Docker is recommended as it simplifies the setup process, especially for managing environment variables and dependencies. | ||||||
|  |  | ||||||
| See the [installation documentation](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/installation) for more information like updating, etc. | See the [installation documentation](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/installation) for more information like updating, etc. | ||||||
|  |  | ||||||
| ### Ollama Connection Errors | ### Troubleshooting | ||||||
|  |  | ||||||
|  | #### Local OpenAI-API-Compliant Servers | ||||||
|  |  | ||||||
|  | If Perplexica tells you that you haven't configured any chat model providers, ensure that: | ||||||
|  |  | ||||||
|  | 1. Your server is running on `0.0.0.0` (not `127.0.0.1`) and on the same port you put in the API URL. | ||||||
|  | 2. You have specified the correct model name loaded by your local LLM server. | ||||||
|  | 3. You have specified the correct API key, or if one is not defined, you have put _something_ in the API key field and not left it empty. | ||||||
|  |  | ||||||
|  | #### Ollama Connection Errors | ||||||
|  |  | ||||||
| If you're encountering an Ollama connection error, it is likely due to the backend being unable to connect to Ollama's API. To fix this issue you can: | If you're encountering an Ollama connection error, it is likely due to the backend being unable to connect to Ollama's API. To fix this issue you can: | ||||||
|  |  | ||||||
| @@ -132,10 +148,29 @@ If you're encountering an Ollama connection error, it is likely due to the backe | |||||||
|  |  | ||||||
| 3. **Linux Users - Expose Ollama to Network:** | 3. **Linux Users - Expose Ollama to Network:** | ||||||
|  |  | ||||||
|    - Inside `/etc/systemd/system/ollama.service`, you need to add `Environment="OLLAMA_HOST=0.0.0.0"`. Then restart Ollama by `systemctl restart ollama`. For more information see [Ollama docs](https://github.com/ollama/ollama/blob/main/docs/faq.md#setting-environment-variables-on-linux) |    - Inside `/etc/systemd/system/ollama.service`, you need to add `Environment="OLLAMA_HOST=0.0.0.0:11434"`. (Change the port number if you are using a different one.) Then reload the systemd manager configuration with `systemctl daemon-reload`, and restart Ollama by `systemctl restart ollama`. For more information see [Ollama docs](https://github.com/ollama/ollama/blob/main/docs/faq.md#setting-environment-variables-on-linux) | ||||||
|  |  | ||||||
|    - Ensure that the port (default is 11434) is not blocked by your firewall. |    - Ensure that the port (default is 11434) is not blocked by your firewall. | ||||||
|  |  | ||||||
|  | #### Lemonade Connection Errors | ||||||
|  |  | ||||||
|  | If you're encountering a Lemonade connection error, it is likely due to the backend being unable to connect to Lemonade's API. To fix this issue you can: | ||||||
|  |  | ||||||
|  | 1. **Check your Lemonade API URL:** Ensure that the API URL is correctly set in the settings menu. | ||||||
|  | 2. **Update API URL Based on OS:** | ||||||
|  |  | ||||||
|  |    - **Windows:** Use `http://host.docker.internal:8000` | ||||||
|  |    - **Mac:** Use `http://host.docker.internal:8000` | ||||||
|  |    - **Linux:** Use `http://<private_ip_of_host>:8000` | ||||||
|  |  | ||||||
|  |    Adjust the port number if you're using a different one. | ||||||
|  |  | ||||||
|  | 3. **Ensure Lemonade Server is Running:** | ||||||
|  |  | ||||||
|  |    - Make sure your Lemonade server is running and accessible on the configured port (default is 8000). | ||||||
|  |    - Verify that Lemonade is configured to accept connections from all interfaces (`0.0.0.0`), not just localhost (`127.0.0.1`). | ||||||
|  |    - Ensure that the port (default is 8000) is not blocked by your firewall. | ||||||
|  |  | ||||||
| ## Using as a Search Engine | ## Using as a Search Engine | ||||||
|  |  | ||||||
| If you wish to use Perplexica as an alternative to traditional search engines like Google or Bing, or if you want to add a shortcut for quick access from your browser's search bar, follow these steps: | If you wish to use Perplexica as an alternative to traditional search engines like Google or Bing, or if you want to add a shortcut for quick access from your browser's search bar, follow these steps: | ||||||
| @@ -159,6 +194,8 @@ Perplexica runs on Next.js and handles all API requests. It works right away on | |||||||
|  |  | ||||||
| [](https://usw.sealos.io/?openapp=system-template%3FtemplateName%3Dperplexica) | [](https://usw.sealos.io/?openapp=system-template%3FtemplateName%3Dperplexica) | ||||||
| [](https://repocloud.io/details/?app_id=267) | [](https://repocloud.io/details/?app_id=267) | ||||||
|  | [](https://template.run.claw.cloud/?referralCode=U11MRQ8U9RM4&openapp=system-fastdeploy%3FtemplateName%3Dperplexica) | ||||||
|  | [](https://www.hostinger.com/vps/docker-hosting?compose_url=https://raw.githubusercontent.com/ItzCrazyKns/Perplexica/refs/heads/master/docker-compose.yaml) | ||||||
|  |  | ||||||
| ## Upcoming Features | ## Upcoming Features | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,6 @@ | |||||||
| FROM node:20.18.0-slim AS builder | FROM node:24.5.0-slim AS builder | ||||||
|  |  | ||||||
|  | RUN apt-get update && apt-get install -y python3 python3-pip sqlite3 && rm -rf /var/lib/apt/lists/* | ||||||
|  |  | ||||||
| WORKDIR /home/perplexica | WORKDIR /home/perplexica | ||||||
|  |  | ||||||
| @@ -8,11 +10,14 @@ RUN yarn install --frozen-lockfile --network-timeout 600000 | |||||||
| COPY tsconfig.json next.config.mjs next-env.d.ts postcss.config.js drizzle.config.ts tailwind.config.ts ./ | COPY tsconfig.json next.config.mjs next-env.d.ts postcss.config.js drizzle.config.ts tailwind.config.ts ./ | ||||||
| COPY src ./src | COPY src ./src | ||||||
| COPY public ./public | COPY public ./public | ||||||
|  | COPY drizzle ./drizzle | ||||||
|  |  | ||||||
| RUN mkdir -p /home/perplexica/data | RUN mkdir -p /home/perplexica/data | ||||||
| RUN yarn build | RUN yarn build | ||||||
|  |  | ||||||
| FROM node:20.18.0-slim | FROM node:24.5.0-slim | ||||||
|  |  | ||||||
|  | RUN apt-get update && apt-get install -y python3 python3-pip sqlite3 && rm -rf /var/lib/apt/lists/* | ||||||
|  |  | ||||||
| WORKDIR /home/perplexica | WORKDIR /home/perplexica | ||||||
|  |  | ||||||
| @@ -21,7 +26,12 @@ COPY --from=builder /home/perplexica/.next/static ./public/_next/static | |||||||
|  |  | ||||||
| COPY --from=builder /home/perplexica/.next/standalone ./ | COPY --from=builder /home/perplexica/.next/standalone ./ | ||||||
| COPY --from=builder /home/perplexica/data ./data | COPY --from=builder /home/perplexica/data ./data | ||||||
|  | COPY drizzle ./drizzle | ||||||
|  |  | ||||||
| RUN mkdir /home/perplexica/uploads | RUN mkdir /home/perplexica/uploads | ||||||
|  |  | ||||||
| CMD ["node", "server.js"] | COPY entrypoint.sh ./entrypoint.sh | ||||||
|  | RUN chmod +x ./entrypoint.sh | ||||||
|  | RUN sed -i 's/\r$//' ./entrypoint.sh || true | ||||||
|  |  | ||||||
|  | CMD ["/home/perplexica/entrypoint.sh"] | ||||||
| @@ -15,7 +15,9 @@ services: | |||||||
|       context: . |       context: . | ||||||
|       dockerfile: app.dockerfile |       dockerfile: app.dockerfile | ||||||
|     environment: |     environment: | ||||||
|  |       - DOCKER=true | ||||||
|       - SEARXNG_API_URL=http://searxng:8080 |       - SEARXNG_API_URL=http://searxng:8080 | ||||||
|  |       - DATA_DIR=/home/perplexica | ||||||
|     ports: |     ports: | ||||||
|       - 3000:3000 |       - 3000:3000 | ||||||
|     networks: |     networks: | ||||||
| @@ -23,7 +25,6 @@ services: | |||||||
|     volumes: |     volumes: | ||||||
|       - backend-dbstore:/home/perplexica/data |       - backend-dbstore:/home/perplexica/data | ||||||
|       - uploads:/home/perplexica/uploads |       - uploads:/home/perplexica/uploads | ||||||
|       - ./config.toml:/home/perplexica/config.toml |  | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|  |  | ||||||
| networks: | networks: | ||||||
|   | |||||||
| @@ -41,6 +41,6 @@ To update Perplexica to the latest version, follow these steps: | |||||||
| 3. Check for changes in the configuration files. If the `sample.config.toml` file contains new fields, delete your existing `config.toml` file, rename `sample.config.toml` to `config.toml`, and update the configuration accordingly. | 3. Check for changes in the configuration files. If the `sample.config.toml` file contains new fields, delete your existing `config.toml` file, rename `sample.config.toml` to `config.toml`, and update the configuration accordingly. | ||||||
| 4. After populating the configuration run `npm i`. | 4. After populating the configuration run `npm i`. | ||||||
| 5. Install the dependencies and then execute `npm run build`. | 5. Install the dependencies and then execute `npm run build`. | ||||||
| 6. Finally, start the app by running `npm rum start` | 6. Finally, start the app by running `npm run start` | ||||||
|  |  | ||||||
| --- | --- | ||||||
|   | |||||||
| @@ -1,10 +1,11 @@ | |||||||
| import { defineConfig } from 'drizzle-kit'; | import { defineConfig } from 'drizzle-kit'; | ||||||
|  | import path from 'path'; | ||||||
|  |  | ||||||
| export default defineConfig({ | export default defineConfig({ | ||||||
|   dialect: 'sqlite', |   dialect: 'sqlite', | ||||||
|   schema: './src/lib/db/schema.ts', |   schema: './src/lib/db/schema.ts', | ||||||
|   out: './drizzle', |   out: './drizzle', | ||||||
|   dbCredentials: { |   dbCredentials: { | ||||||
|     url: './data/db.sqlite', |     url: path.join(process.cwd(), 'data', 'db.sqlite'), | ||||||
|   }, |   }, | ||||||
| }); | }); | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								drizzle/0000_fuzzy_randall.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | |||||||
|  | CREATE TABLE IF NOT EXISTS `chats` ( | ||||||
|  | 	`id` text PRIMARY KEY NOT NULL, | ||||||
|  | 	`title` text NOT NULL, | ||||||
|  | 	`createdAt` text NOT NULL, | ||||||
|  | 	`focusMode` text NOT NULL, | ||||||
|  | 	`files` text DEFAULT '[]' | ||||||
|  | ); | ||||||
|  | --> statement-breakpoint | ||||||
|  | CREATE TABLE IF NOT EXISTS `messages` ( | ||||||
|  | 	`id` integer PRIMARY KEY NOT NULL, | ||||||
|  | 	`content` text NOT NULL, | ||||||
|  | 	`chatId` text NOT NULL, | ||||||
|  | 	`messageId` text NOT NULL, | ||||||
|  | 	`type` text, | ||||||
|  | 	`metadata` text | ||||||
|  | ); | ||||||
							
								
								
									
										1
									
								
								drizzle/0001_wise_rockslide.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | /* Do nothing */ | ||||||
							
								
								
									
										116
									
								
								drizzle/meta/0000_snapshot.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,116 @@ | |||||||
|  | { | ||||||
|  |   "version": "6", | ||||||
|  |   "dialect": "sqlite", | ||||||
|  |   "id": "ef3a044b-0f34-40b5-babb-2bb3a909ba27", | ||||||
|  |   "prevId": "00000000-0000-0000-0000-000000000000", | ||||||
|  |   "tables": { | ||||||
|  |     "chats": { | ||||||
|  |       "name": "chats", | ||||||
|  |       "columns": { | ||||||
|  |         "id": { | ||||||
|  |           "name": "id", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": true, | ||||||
|  |           "notNull": true, | ||||||
|  |           "autoincrement": false | ||||||
|  |         }, | ||||||
|  |         "title": { | ||||||
|  |           "name": "title", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": false, | ||||||
|  |           "notNull": true, | ||||||
|  |           "autoincrement": false | ||||||
|  |         }, | ||||||
|  |         "createdAt": { | ||||||
|  |           "name": "createdAt", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": false, | ||||||
|  |           "notNull": true, | ||||||
|  |           "autoincrement": false | ||||||
|  |         }, | ||||||
|  |         "focusMode": { | ||||||
|  |           "name": "focusMode", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": false, | ||||||
|  |           "notNull": true, | ||||||
|  |           "autoincrement": false | ||||||
|  |         }, | ||||||
|  |         "files": { | ||||||
|  |           "name": "files", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": false, | ||||||
|  |           "notNull": false, | ||||||
|  |           "autoincrement": false, | ||||||
|  |           "default": "'[]'" | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       "indexes": {}, | ||||||
|  |       "foreignKeys": {}, | ||||||
|  |       "compositePrimaryKeys": {}, | ||||||
|  |       "uniqueConstraints": {}, | ||||||
|  |       "checkConstraints": {} | ||||||
|  |     }, | ||||||
|  |     "messages": { | ||||||
|  |       "name": "messages", | ||||||
|  |       "columns": { | ||||||
|  |         "id": { | ||||||
|  |           "name": "id", | ||||||
|  |           "type": "integer", | ||||||
|  |           "primaryKey": true, | ||||||
|  |           "notNull": true, | ||||||
|  |           "autoincrement": false | ||||||
|  |         }, | ||||||
|  |         "content": { | ||||||
|  |           "name": "content", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": false, | ||||||
|  |           "notNull": true, | ||||||
|  |           "autoincrement": false | ||||||
|  |         }, | ||||||
|  |         "chatId": { | ||||||
|  |           "name": "chatId", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": false, | ||||||
|  |           "notNull": true, | ||||||
|  |           "autoincrement": false | ||||||
|  |         }, | ||||||
|  |         "messageId": { | ||||||
|  |           "name": "messageId", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": false, | ||||||
|  |           "notNull": true, | ||||||
|  |           "autoincrement": false | ||||||
|  |         }, | ||||||
|  |         "type": { | ||||||
|  |           "name": "type", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": false, | ||||||
|  |           "notNull": false, | ||||||
|  |           "autoincrement": false | ||||||
|  |         }, | ||||||
|  |         "metadata": { | ||||||
|  |           "name": "metadata", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": false, | ||||||
|  |           "notNull": false, | ||||||
|  |           "autoincrement": false | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       "indexes": {}, | ||||||
|  |       "foreignKeys": {}, | ||||||
|  |       "compositePrimaryKeys": {}, | ||||||
|  |       "uniqueConstraints": {}, | ||||||
|  |       "checkConstraints": {} | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "views": {}, | ||||||
|  |   "enums": {}, | ||||||
|  |   "_meta": { | ||||||
|  |     "schemas": {}, | ||||||
|  |     "tables": {}, | ||||||
|  |     "columns": {} | ||||||
|  |   }, | ||||||
|  |   "internal": { | ||||||
|  |     "indexes": {} | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										125
									
								
								drizzle/meta/0001_snapshot.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,125 @@ | |||||||
|  | { | ||||||
|  |   "version": "6", | ||||||
|  |   "dialect": "sqlite", | ||||||
|  |   "id": "6dedf55f-0e44-478f-82cf-14a21ac686f8", | ||||||
|  |   "prevId": "ef3a044b-0f34-40b5-babb-2bb3a909ba27", | ||||||
|  |   "tables": { | ||||||
|  |     "chats": { | ||||||
|  |       "name": "chats", | ||||||
|  |       "columns": { | ||||||
|  |         "id": { | ||||||
|  |           "name": "id", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": true, | ||||||
|  |           "notNull": true, | ||||||
|  |           "autoincrement": false | ||||||
|  |         }, | ||||||
|  |         "title": { | ||||||
|  |           "name": "title", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": false, | ||||||
|  |           "notNull": true, | ||||||
|  |           "autoincrement": false | ||||||
|  |         }, | ||||||
|  |         "createdAt": { | ||||||
|  |           "name": "createdAt", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": false, | ||||||
|  |           "notNull": true, | ||||||
|  |           "autoincrement": false | ||||||
|  |         }, | ||||||
|  |         "focusMode": { | ||||||
|  |           "name": "focusMode", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": false, | ||||||
|  |           "notNull": true, | ||||||
|  |           "autoincrement": false | ||||||
|  |         }, | ||||||
|  |         "files": { | ||||||
|  |           "name": "files", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": false, | ||||||
|  |           "notNull": false, | ||||||
|  |           "autoincrement": false, | ||||||
|  |           "default": "'[]'" | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       "indexes": {}, | ||||||
|  |       "foreignKeys": {}, | ||||||
|  |       "compositePrimaryKeys": {}, | ||||||
|  |       "uniqueConstraints": {}, | ||||||
|  |       "checkConstraints": {} | ||||||
|  |     }, | ||||||
|  |     "messages": { | ||||||
|  |       "name": "messages", | ||||||
|  |       "columns": { | ||||||
|  |         "id": { | ||||||
|  |           "name": "id", | ||||||
|  |           "type": "integer", | ||||||
|  |           "primaryKey": true, | ||||||
|  |           "notNull": true, | ||||||
|  |           "autoincrement": false | ||||||
|  |         }, | ||||||
|  |         "type": { | ||||||
|  |           "name": "type", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": false, | ||||||
|  |           "notNull": true, | ||||||
|  |           "autoincrement": false | ||||||
|  |         }, | ||||||
|  |         "chatId": { | ||||||
|  |           "name": "chatId", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": false, | ||||||
|  |           "notNull": true, | ||||||
|  |           "autoincrement": false | ||||||
|  |         }, | ||||||
|  |         "createdAt": { | ||||||
|  |           "name": "createdAt", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": false, | ||||||
|  |           "notNull": true, | ||||||
|  |           "autoincrement": false, | ||||||
|  |           "default": "CURRENT_TIMESTAMP" | ||||||
|  |         }, | ||||||
|  |         "messageId": { | ||||||
|  |           "name": "messageId", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": false, | ||||||
|  |           "notNull": true, | ||||||
|  |           "autoincrement": false | ||||||
|  |         }, | ||||||
|  |         "content": { | ||||||
|  |           "name": "content", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": false, | ||||||
|  |           "notNull": false, | ||||||
|  |           "autoincrement": false | ||||||
|  |         }, | ||||||
|  |         "sources": { | ||||||
|  |           "name": "sources", | ||||||
|  |           "type": "text", | ||||||
|  |           "primaryKey": false, | ||||||
|  |           "notNull": false, | ||||||
|  |           "autoincrement": false, | ||||||
|  |           "default": "'[]'" | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       "indexes": {}, | ||||||
|  |       "foreignKeys": {}, | ||||||
|  |       "compositePrimaryKeys": {}, | ||||||
|  |       "uniqueConstraints": {}, | ||||||
|  |       "checkConstraints": {} | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "views": {}, | ||||||
|  |   "enums": {}, | ||||||
|  |   "_meta": { | ||||||
|  |     "schemas": {}, | ||||||
|  |     "tables": {}, | ||||||
|  |     "columns": {} | ||||||
|  |   }, | ||||||
|  |   "internal": { | ||||||
|  |     "indexes": {} | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										20
									
								
								drizzle/meta/_journal.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,20 @@ | |||||||
|  | { | ||||||
|  |   "version": "7", | ||||||
|  |   "dialect": "sqlite", | ||||||
|  |   "entries": [ | ||||||
|  |     { | ||||||
|  |       "idx": 0, | ||||||
|  |       "version": "6", | ||||||
|  |       "when": 1748405503809, | ||||||
|  |       "tag": "0000_fuzzy_randall", | ||||||
|  |       "breakpoints": true | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "idx": 1, | ||||||
|  |       "version": "6", | ||||||
|  |       "when": 1758863991284, | ||||||
|  |       "tag": "0001_wise_rockslide", | ||||||
|  |       "breakpoints": true | ||||||
|  |     } | ||||||
|  |   ] | ||||||
|  | } | ||||||
							
								
								
									
										4
									
								
								entrypoint.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | |||||||
|  | #!/bin/sh | ||||||
|  | set -e | ||||||
|  |  | ||||||
|  | exec node server.js | ||||||
							
								
								
									
										28
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @@ -1,25 +1,27 @@ | |||||||
| { | { | ||||||
|   "name": "perplexica-frontend", |   "name": "perplexica-frontend", | ||||||
|   "version": "1.10.2", |   "version": "1.11.0-rc3", | ||||||
|   "license": "MIT", |   "license": "MIT", | ||||||
|   "author": "ItzCrazyKns", |   "author": "ItzCrazyKns", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "dev": "next dev", |     "dev": "next dev", | ||||||
|     "build": "npm run db:push && next build", |     "build": "next build", | ||||||
|     "start": "next start", |     "start": "next start", | ||||||
|     "lint": "next lint", |     "lint": "next lint", | ||||||
|     "format:write": "prettier . --write", |     "format:write": "prettier . --write" | ||||||
|     "db:push": "drizzle-kit push" |  | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@headlessui/react": "^2.2.0", |     "@headlessui/react": "^2.2.0", | ||||||
|  |     "@headlessui/tailwindcss": "^0.2.2", | ||||||
|     "@iarna/toml": "^2.2.5", |     "@iarna/toml": "^2.2.5", | ||||||
|     "@icons-pack/react-simple-icons": "^12.3.0", |     "@icons-pack/react-simple-icons": "^12.3.0", | ||||||
|     "@langchain/anthropic": "^0.3.15", |     "@langchain/anthropic": "^0.3.24", | ||||||
|     "@langchain/community": "^0.3.36", |     "@langchain/community": "^0.3.49", | ||||||
|     "@langchain/core": "^0.3.42", |     "@langchain/core": "^0.3.66", | ||||||
|     "@langchain/google-genai": "^0.1.12", |     "@langchain/google-genai": "^0.2.15", | ||||||
|     "@langchain/openai": "^0.0.25", |     "@langchain/groq": "^0.2.3", | ||||||
|  |     "@langchain/ollama": "^0.2.3", | ||||||
|  |     "@langchain/openai": "^0.6.2", | ||||||
|     "@langchain/textsplitters": "^0.1.0", |     "@langchain/textsplitters": "^0.1.0", | ||||||
|     "@tailwindcss/typography": "^0.5.12", |     "@tailwindcss/typography": "^0.5.12", | ||||||
|     "@xenova/transformers": "^2.17.2", |     "@xenova/transformers": "^2.17.2", | ||||||
| @@ -27,11 +29,13 @@ | |||||||
|     "better-sqlite3": "^11.9.1", |     "better-sqlite3": "^11.9.1", | ||||||
|     "clsx": "^2.1.0", |     "clsx": "^2.1.0", | ||||||
|     "compute-cosine-similarity": "^1.1.0", |     "compute-cosine-similarity": "^1.1.0", | ||||||
|     "compute-dot": "^1.1.0", |  | ||||||
|     "drizzle-orm": "^0.40.1", |     "drizzle-orm": "^0.40.1", | ||||||
|  |     "framer-motion": "^12.23.24", | ||||||
|     "html-to-text": "^9.0.5", |     "html-to-text": "^9.0.5", | ||||||
|     "langchain": "^0.1.30", |     "jspdf": "^3.0.1", | ||||||
|  |     "langchain": "^0.3.30", | ||||||
|     "lucide-react": "^0.363.0", |     "lucide-react": "^0.363.0", | ||||||
|  |     "mammoth": "^1.9.1", | ||||||
|     "markdown-to-jsx": "^7.7.2", |     "markdown-to-jsx": "^7.7.2", | ||||||
|     "next": "^15.2.2", |     "next": "^15.2.2", | ||||||
|     "next-themes": "^0.3.0", |     "next-themes": "^0.3.0", | ||||||
| @@ -49,6 +53,7 @@ | |||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@types/better-sqlite3": "^7.6.12", |     "@types/better-sqlite3": "^7.6.12", | ||||||
|     "@types/html-to-text": "^9.0.4", |     "@types/html-to-text": "^9.0.4", | ||||||
|  |     "@types/jspdf": "^2.0.0", | ||||||
|     "@types/node": "^20", |     "@types/node": "^20", | ||||||
|     "@types/pdf-parse": "^1.1.4", |     "@types/pdf-parse": "^1.1.4", | ||||||
|     "@types/react": "^18", |     "@types/react": "^18", | ||||||
| @@ -60,6 +65,7 @@ | |||||||
|     "postcss": "^8", |     "postcss": "^8", | ||||||
|     "prettier": "^3.2.5", |     "prettier": "^3.2.5", | ||||||
|     "tailwindcss": "^3.3.0", |     "tailwindcss": "^3.3.0", | ||||||
|  |     "ts-node": "^10.9.2", | ||||||
|     "typescript": "^5" |     "typescript": "^5" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								public/fonts/pp-ed-ul.otf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/icon-100.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 916 B | 
							
								
								
									
										
											BIN
										
									
								
								public/icon-50.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 515 B | 
							
								
								
									
										
											BIN
										
									
								
								public/icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 30 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/screenshots/p1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 183 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/screenshots/p1_small.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 130 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/screenshots/p2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 627 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/screenshots/p2_small.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 202 KiB | 
							
								
								
									
										131
									
								
								public/weather-ico/clear-day.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,131 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <filter id="blur" x="-.34167" y="-.34167" width="1.6833" height="1.85"> | ||||||
|  |       <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |       <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |       <feComponentTransfer> | ||||||
|  |         <feFuncA slope="0.05" type="linear" /> | ||||||
|  |       </feComponentTransfer> | ||||||
|  |       <feMerge> | ||||||
|  |         <feMergeNode /> | ||||||
|  |         <feMergeNode in="SourceGraphic" /> | ||||||
|  |       </feMerge> | ||||||
|  |     </filter> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** SUN | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-sun { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: rotate(360deg); | ||||||
|  |           -moz-transform: rotate(360deg); | ||||||
|  |           -ms-transform: rotate(360deg); | ||||||
|  |           transform: rotate(360deg); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-sun { | ||||||
|  |         -webkit-animation-name: am-weather-sun; | ||||||
|  |         -moz-animation-name: am-weather-sun; | ||||||
|  |         -ms-animation-name: am-weather-sun; | ||||||
|  |         animation-name: am-weather-sun; | ||||||
|  |         -webkit-animation-duration: 9s; | ||||||
|  |         -moz-animation-duration: 9s; | ||||||
|  |         -ms-animation-duration: 9s; | ||||||
|  |         animation-duration: 9s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-sun-shiny { | ||||||
|  |         0% { | ||||||
|  |           stroke-dasharray: 3px 10px; | ||||||
|  |           stroke-dashoffset: 0px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           stroke-dasharray: 0.1px 10px; | ||||||
|  |           stroke-dashoffset: -1px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           stroke-dasharray: 3px 10px; | ||||||
|  |           stroke-dashoffset: 0px; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-sun-shiny line { | ||||||
|  |         -webkit-animation-name: am-weather-sun-shiny; | ||||||
|  |         -moz-animation-name: am-weather-sun-shiny; | ||||||
|  |         -ms-animation-name: am-weather-sun-shiny; | ||||||
|  |         animation-name: am-weather-sun-shiny; | ||||||
|  |         -webkit-animation-duration: 2s; | ||||||
|  |         -moz-animation-duration: 2s; | ||||||
|  |         -ms-animation-duration: 2s; | ||||||
|  |         animation-duration: 2s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |     <g transform="translate(0,16)"> | ||||||
|  |       <g class="am-weather-sun" | ||||||
|  |         style="-moz-animation-duration:9s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-sun;-moz-animation-timing-function:linear;-ms-animation-duration:9s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-sun;-ms-animation-timing-function:linear;-webkit-animation-duration:9s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-sun;-webkit-animation-timing-function:linear"> | ||||||
|  |         <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" stroke-width="2" /> | ||||||
|  |         <g transform="rotate(45)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(90)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(135)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="scale(-1)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(225)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(-90)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(-45)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <circle r="5" fill="#ffa500" stroke="#ffa500" stroke-width="2" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 4.9 KiB | 
							
								
								
									
										159
									
								
								public/weather-ico/clear-night.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,159 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <filter id="blur" x="-.3038" y="-.3318" width="1.6076" height="1.894"> | ||||||
|  |       <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |       <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |       <feComponentTransfer> | ||||||
|  |         <feFuncA slope="0.05" type="linear" /> | ||||||
|  |       </feComponentTransfer> | ||||||
|  |       <feMerge> | ||||||
|  |         <feMergeNode /> | ||||||
|  |         <feMergeNode in="SourceGraphic" /> | ||||||
|  |       </feMerge> | ||||||
|  |     </filter> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** MOON | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-moon { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: rotate(15deg); | ||||||
|  |           -moz-transform: rotate(15deg); | ||||||
|  |           -ms-transform: rotate(15deg); | ||||||
|  |           transform: rotate(15deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon { | ||||||
|  |         -webkit-animation-name: am-weather-moon; | ||||||
|  |         -moz-animation-name: am-weather-moon; | ||||||
|  |         -ms-animation-name: am-weather-moon; | ||||||
|  |         animation-name: am-weather-moon; | ||||||
|  |         -webkit-animation-duration: 6s; | ||||||
|  |         -moz-animation-duration: 6s; | ||||||
|  |         -ms-animation-duration: 6s; | ||||||
|  |         animation-duration: 6s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |         -webkit-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         -moz-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         -ms-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-moon-star-1 { | ||||||
|  |         0% { | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon-star-1 { | ||||||
|  |         -webkit-animation-name: am-weather-moon-star-1; | ||||||
|  |         -moz-animation-name: am-weather-moon-star-1; | ||||||
|  |         -ms-animation-name: am-weather-moon-star-1; | ||||||
|  |         animation-name: am-weather-moon-star-1; | ||||||
|  |         -webkit-animation-delay: 3s; | ||||||
|  |         -moz-animation-delay: 3s; | ||||||
|  |         -ms-animation-delay: 3s; | ||||||
|  |         animation-delay: 3s; | ||||||
|  |         -webkit-animation-duration: 5s; | ||||||
|  |         -moz-animation-duration: 5s; | ||||||
|  |         -ms-animation-duration: 5s; | ||||||
|  |         animation-duration: 5s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: 1; | ||||||
|  |         -moz-animation-iteration-count: 1; | ||||||
|  |         -ms-animation-iteration-count: 1; | ||||||
|  |         animation-iteration-count: 1; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-moon-star-2 { | ||||||
|  |         0% { | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon-star-2 { | ||||||
|  |         -webkit-animation-name: am-weather-moon-star-2; | ||||||
|  |         -moz-animation-name: am-weather-moon-star-2; | ||||||
|  |         -ms-animation-name: am-weather-moon-star-2; | ||||||
|  |         animation-name: am-weather-moon-star-2; | ||||||
|  |         -webkit-animation-delay: 5s; | ||||||
|  |         -moz-animation-delay: 5s; | ||||||
|  |         -ms-animation-delay: 5s; | ||||||
|  |         animation-delay: 5s; | ||||||
|  |         -webkit-animation-duration: 4s; | ||||||
|  |         -moz-animation-duration: 4s; | ||||||
|  |         -ms-animation-duration: 4s; | ||||||
|  |         animation-duration: 4s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: 1; | ||||||
|  |         -moz-animation-iteration-count: 1; | ||||||
|  |         -ms-animation-iteration-count: 1; | ||||||
|  |         animation-iteration-count: 1; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g id="night" transform="translate(-4,-18)" filter="url(#blur)"> | ||||||
|  |     <g transform="matrix(.8 0 0 .78534 36 20.022)" stroke-width="1.2616"> | ||||||
|  |       <g class="am-weather-moon-star-1" | ||||||
|  |         style="-moz-animation-delay:3s;-moz-animation-duration:5s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-1;-moz-animation-timing-function:linear;-ms-animation-delay:3s;-ms-animation-duration:5s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-1;-ms-animation-timing-function:linear;-webkit-animation-delay:3s;-webkit-animation-duration:5s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-1;-webkit-animation-timing-function:linear"> | ||||||
|  |         <polygon points="4 2.7 5.2 3.3 4 4 3.3 5.2 2.7 4 1.5 3.3 2.7 2.7 3.3 1.5" fill="#ffa500" stroke-miterlimit="10" | ||||||
|  |           stroke-width="1.4105" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-moon-star-2" | ||||||
|  |         style="-moz-animation-delay:5s;-moz-animation-duration:4s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-2;-moz-animation-timing-function:linear;-ms-animation-delay:5s;-ms-animation-duration:4s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-2;-ms-animation-timing-function:linear;-webkit-animation-delay:5s;-webkit-animation-duration:4s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-2;-webkit-animation-timing-function:linear"> | ||||||
|  |         <polygon transform="translate(20,10)" points="4 2.7 5.2 3.3 4 4 3.3 5.2 2.7 4 1.5 3.3 2.7 2.7 3.3 1.5" | ||||||
|  |           fill="#ffa500" stroke-miterlimit="10" stroke-width="1.4105" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-moon" | ||||||
|  |         style="-moz-animation-duration:6s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-moon;-moz-animation-timing-function:linear;-moz-transform-origin:12.5px 15.15px 0;-ms-animation-duration:6s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-moon;-ms-animation-timing-function:linear;-ms-transform-origin:12.5px 15.15px 0;-webkit-animation-duration:6s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-moon;-webkit-animation-timing-function:linear;-webkit-transform-origin:12.5px 15.15px 0"> | ||||||
|  |         <path | ||||||
|  |           d="m14.5 13.2c0-3.7 2-6.9 5-8.7-1.5-0.9-3.2-1.3-5-1.3-5.5 0-10 4.5-10 10s4.5 10 10 10c1.8 0 3.5-0.5 5-1.3-3-1.7-5-5-5-8.7z" | ||||||
|  |           fill="#ffa500" stroke="#ffa500" stroke-linejoin="round" stroke-width="2.5232" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 6.7 KiB | 
							
								
								
									
										178
									
								
								public/weather-ico/cloudy-1-day.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,178 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <filter id="blur" x="-.20655" y="-.28472" width="1.403" height="1.6944"> | ||||||
|  |       <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |       <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |       <feComponentTransfer> | ||||||
|  |         <feFuncA slope="0.05" type="linear" /> | ||||||
|  |       </feComponentTransfer> | ||||||
|  |       <feMerge> | ||||||
|  |         <feMergeNode /> | ||||||
|  |         <feMergeNode in="SourceGraphic" /> | ||||||
|  |       </feMerge> | ||||||
|  |     </filter> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** CLOUDS | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-cloud-2 { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: translate(2px, 0px); | ||||||
|  |           -moz-transform: translate(2px, 0px); | ||||||
|  |           -ms-transform: translate(2px, 0px); | ||||||
|  |           transform: translate(2px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-cloud-2 { | ||||||
|  |         -webkit-animation-name: am-weather-cloud-2; | ||||||
|  |         -moz-animation-name: am-weather-cloud-2; | ||||||
|  |         animation-name: am-weather-cloud-2; | ||||||
|  |         -webkit-animation-duration: 3s; | ||||||
|  |         -moz-animation-duration: 3s; | ||||||
|  |         animation-duration: 3s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** SUN | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-sun { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: rotate(360deg); | ||||||
|  |           -moz-transform: rotate(360deg); | ||||||
|  |           -ms-transform: rotate(360deg); | ||||||
|  |           transform: rotate(360deg); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-sun { | ||||||
|  |         -webkit-animation-name: am-weather-sun; | ||||||
|  |         -moz-animation-name: am-weather-sun; | ||||||
|  |         -ms-animation-name: am-weather-sun; | ||||||
|  |         animation-name: am-weather-sun; | ||||||
|  |         -webkit-animation-duration: 9s; | ||||||
|  |         -moz-animation-duration: 9s; | ||||||
|  |         -ms-animation-duration: 9s; | ||||||
|  |         animation-duration: 9s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-sun-shiny { | ||||||
|  |         0% { | ||||||
|  |           stroke-dasharray: 3px 10px; | ||||||
|  |           stroke-dashoffset: 0px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           stroke-dasharray: 0.1px 10px; | ||||||
|  |           stroke-dashoffset: -1px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           stroke-dasharray: 3px 10px; | ||||||
|  |           stroke-dashoffset: 0px; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-sun-shiny line { | ||||||
|  |         -webkit-animation-name: am-weather-sun-shiny; | ||||||
|  |         -moz-animation-name: am-weather-sun-shiny; | ||||||
|  |         -ms-animation-name: am-weather-sun-shiny; | ||||||
|  |         animation-name: am-weather-sun-shiny; | ||||||
|  |         -webkit-animation-duration: 2s; | ||||||
|  |         -moz-animation-duration: 2s; | ||||||
|  |         -ms-animation-duration: 2s; | ||||||
|  |         animation-duration: 2s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |     <g transform="translate(0,16)"> | ||||||
|  |       <g class="am-weather-sun" | ||||||
|  |         style="-moz-animation-duration:9s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-sun;-moz-animation-timing-function:linear;-ms-animation-duration:9s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-sun;-ms-animation-timing-function:linear;-webkit-animation-duration:9s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-sun;-webkit-animation-timing-function:linear"> | ||||||
|  |         <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" stroke-width="2" /> | ||||||
|  |         <g transform="rotate(45)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(90)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(135)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="scale(-1)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(225)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(-90)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(-45)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <circle r="5" fill="#ffa500" stroke="#ffa500" stroke-width="2" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-cloud-2" | ||||||
|  |       style="-moz-animation-duration:3s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-cloud-2;-moz-animation-timing-function:linear;-webkit-animation-duration:3s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-cloud-2;-webkit-animation-timing-function:linear"> | ||||||
|  |       <path transform="translate(-20,-11)" | ||||||
|  |         d="m47.7 35.4c0-4.6-3.7-8.2-8.2-8.2-1 0-1.9 0.2-2.8 0.5-0.3-3.4-3.1-6.2-6.6-6.2-3.7 0-6.7 3-6.7 6.7 0 0.8 0.2 1.6 0.4 2.3-0.3-0.1-0.7-0.1-1-0.1-3.7 0-6.7 3-6.7 6.7 0 3.6 2.9 6.6 6.5 6.7h17.2c4.4-0.5 7.9-4 7.9-8.4z" | ||||||
|  |         fill="#c6deff" stroke="#fff" stroke-linejoin="round" stroke-width="1.2" /> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 6.8 KiB | 
							
								
								
									
										206
									
								
								public/weather-ico/cloudy-1-night.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,206 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <filter id="blur" x="-.19471" y="-.26087" width="1.3744" height="1.6884"> | ||||||
|  |       <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |       <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |       <feComponentTransfer> | ||||||
|  |         <feFuncA slope="0.05" type="linear" /> | ||||||
|  |       </feComponentTransfer> | ||||||
|  |       <feMerge> | ||||||
|  |         <feMergeNode /> | ||||||
|  |         <feMergeNode in="SourceGraphic" /> | ||||||
|  |       </feMerge> | ||||||
|  |     </filter> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** CLOUDS | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-cloud-2 { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: translate(2px, 0px); | ||||||
|  |           -moz-transform: translate(2px, 0px); | ||||||
|  |           -ms-transform: translate(2px, 0px); | ||||||
|  |           transform: translate(2px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-cloud-2 { | ||||||
|  |         -webkit-animation-name: am-weather-cloud-2; | ||||||
|  |         -moz-animation-name: am-weather-cloud-2; | ||||||
|  |         animation-name: am-weather-cloud-2; | ||||||
|  |         -webkit-animation-duration: 3s; | ||||||
|  |         -moz-animation-duration: 3s; | ||||||
|  |         animation-duration: 3s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** MOON | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-moon { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: rotate(15deg); | ||||||
|  |           -moz-transform: rotate(15deg); | ||||||
|  |           -ms-transform: rotate(15deg); | ||||||
|  |           transform: rotate(15deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon { | ||||||
|  |         -webkit-animation-name: am-weather-moon; | ||||||
|  |         -moz-animation-name: am-weather-moon; | ||||||
|  |         -ms-animation-name: am-weather-moon; | ||||||
|  |         animation-name: am-weather-moon; | ||||||
|  |         -webkit-animation-duration: 6s; | ||||||
|  |         -moz-animation-duration: 6s; | ||||||
|  |         -ms-animation-duration: 6s; | ||||||
|  |         animation-duration: 6s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |         -webkit-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         -moz-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         -ms-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-moon-star-1 { | ||||||
|  |         0% { | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon-star-1 { | ||||||
|  |         -webkit-animation-name: am-weather-moon-star-1; | ||||||
|  |         -moz-animation-name: am-weather-moon-star-1; | ||||||
|  |         -ms-animation-name: am-weather-moon-star-1; | ||||||
|  |         animation-name: am-weather-moon-star-1; | ||||||
|  |         -webkit-animation-delay: 3s; | ||||||
|  |         -moz-animation-delay: 3s; | ||||||
|  |         -ms-animation-delay: 3s; | ||||||
|  |         animation-delay: 3s; | ||||||
|  |         -webkit-animation-duration: 5s; | ||||||
|  |         -moz-animation-duration: 5s; | ||||||
|  |         -ms-animation-duration: 5s; | ||||||
|  |         animation-duration: 5s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: 1; | ||||||
|  |         -moz-animation-iteration-count: 1; | ||||||
|  |         -ms-animation-iteration-count: 1; | ||||||
|  |         animation-iteration-count: 1; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-moon-star-2 { | ||||||
|  |         0% { | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon-star-2 { | ||||||
|  |         -webkit-animation-name: am-weather-moon-star-2; | ||||||
|  |         -moz-animation-name: am-weather-moon-star-2; | ||||||
|  |         -ms-animation-name: am-weather-moon-star-2; | ||||||
|  |         animation-name: am-weather-moon-star-2; | ||||||
|  |         -webkit-animation-delay: 5s; | ||||||
|  |         -moz-animation-delay: 5s; | ||||||
|  |         -ms-animation-delay: 5s; | ||||||
|  |         animation-delay: 5s; | ||||||
|  |         -webkit-animation-duration: 4s; | ||||||
|  |         -moz-animation-duration: 4s; | ||||||
|  |         -ms-animation-duration: 4s; | ||||||
|  |         animation-duration: 4s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: 1; | ||||||
|  |         -moz-animation-iteration-count: 1; | ||||||
|  |         -ms-animation-iteration-count: 1; | ||||||
|  |         animation-iteration-count: 1; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |     <g transform="matrix(.8 0 0 .8 16 4)"> | ||||||
|  |       <g class="am-weather-moon-star-1" | ||||||
|  |         style="-moz-animation-delay:3s;-moz-animation-duration:5s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-1;-moz-animation-timing-function:linear;-ms-animation-delay:3s;-ms-animation-duration:5s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-1;-ms-animation-timing-function:linear;-webkit-animation-delay:3s;-webkit-animation-duration:5s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-1;-webkit-animation-timing-function:linear"> | ||||||
|  |         <polygon points="1.5 3.3 2.7 2.7 3.3 1.5 4 2.7 5.2 3.3 4 4 3.3 5.2 2.7 4" fill="#ffa500" | ||||||
|  |           stroke-miterlimit="10" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-moon-star-2" | ||||||
|  |         style="-moz-animation-delay:5s;-moz-animation-duration:4s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-2;-moz-animation-timing-function:linear;-ms-animation-delay:5s;-ms-animation-duration:4s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-2;-ms-animation-timing-function:linear;-webkit-animation-delay:5s;-webkit-animation-duration:4s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-2;-webkit-animation-timing-function:linear"> | ||||||
|  |         <polygon transform="translate(20,10)" points="1.5 3.3 2.7 2.7 3.3 1.5 4 2.7 5.2 3.3 4 4 3.3 5.2 2.7 4" | ||||||
|  |           fill="#ffa500" stroke-miterlimit="10" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-moon" | ||||||
|  |         style="-moz-animation-duration:6s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-moon;-moz-animation-timing-function:linear;-moz-transform-origin:12.5px 15.15px 0;-ms-animation-duration:6s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-moon;-ms-animation-timing-function:linear;-ms-transform-origin:12.5px 15.15px 0;-webkit-animation-duration:6s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-moon;-webkit-animation-timing-function:linear;-webkit-transform-origin:12.5px 15.15px 0"> | ||||||
|  |         <path | ||||||
|  |           d="m14.5 13.2c0-3.7 2-6.9 5-8.7-1.5-0.9-3.2-1.3-5-1.3-5.5 0-10 4.5-10 10s4.5 10 10 10c1.8 0 3.5-0.5 5-1.3-3-1.7-5-5-5-8.7z" | ||||||
|  |           fill="#ffa500" stroke="#ffa500" stroke-linejoin="round" stroke-width="2" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-cloud-2" | ||||||
|  |       style="-moz-animation-duration:3s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-cloud-2;-moz-animation-timing-function:linear;-webkit-animation-duration:3s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-cloud-2;-webkit-animation-timing-function:linear"> | ||||||
|  |       <path transform="translate(-20,-11)" | ||||||
|  |         d="m47.7 35.4c0-4.6-3.7-8.2-8.2-8.2-1 0-1.9 0.2-2.8 0.5-0.3-3.4-3.1-6.2-6.6-6.2-3.7 0-6.7 3-6.7 6.7 0 0.8 0.2 1.6 0.4 2.3-0.3-0.1-0.7-0.1-1-0.1-3.7 0-6.7 3-6.7 6.7 0 3.6 2.9 6.6 6.5 6.7h17.2c4.4-0.5 7.9-4 7.9-8.4z" | ||||||
|  |         fill="#c6deff" stroke="#fff" stroke-linejoin="round" stroke-width="1.2" /> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 8.6 KiB | 
							
								
								
									
										244
									
								
								public/weather-ico/fog-day.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,244 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <filter id="blur" x="-.20655" y="-.21122" width="1.403" height="1.4997"> | ||||||
|  |       <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |       <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |       <feComponentTransfer> | ||||||
|  |         <feFuncA slope="0.05" type="linear" /> | ||||||
|  |       </feComponentTransfer> | ||||||
|  |       <feMerge> | ||||||
|  |         <feMergeNode /> | ||||||
|  |         <feMergeNode in="SourceGraphic" /> | ||||||
|  |       </feMerge> | ||||||
|  |     </filter> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** SUN | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-sun { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: rotate(360deg); | ||||||
|  |           -moz-transform: rotate(360deg); | ||||||
|  |           -ms-transform: rotate(360deg); | ||||||
|  |           transform: rotate(360deg); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-sun { | ||||||
|  |         -webkit-animation-name: am-weather-sun; | ||||||
|  |         -moz-animation-name: am-weather-sun; | ||||||
|  |         -ms-animation-name: am-weather-sun; | ||||||
|  |         animation-name: am-weather-sun; | ||||||
|  |         -webkit-animation-duration: 9s; | ||||||
|  |         -moz-animation-duration: 9s; | ||||||
|  |         -ms-animation-duration: 9s; | ||||||
|  |         animation-duration: 9s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** FOG | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-fog-1 { | ||||||
|  |         0% { | ||||||
|  |           transform: translate(0px, 0px) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           transform: translate(7px, 0px) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           transform: translate(0px, 0px) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-fog-1 { | ||||||
|  |         -webkit-animation-name: am-weather-fog-1; | ||||||
|  |         -moz-animation-name: am-weather-fog-1; | ||||||
|  |         -ms-animation-name: am-weather-fog-1; | ||||||
|  |         animation-name: am-weather-fog-1; | ||||||
|  |         -webkit-animation-duration: 8s; | ||||||
|  |         -moz-animation-duration: 8s; | ||||||
|  |         -ms-animation-duration: 8s; | ||||||
|  |         animation-duration: 8s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-fog-2 { | ||||||
|  |         0% { | ||||||
|  |           transform: translate(0px, 0px) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         21.05% { | ||||||
|  |           transform: translate(-6px, 0px) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         78.95% { | ||||||
|  |           transform: translate(9px, 0px) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           transform: translate(0px, 0px) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-fog-2 { | ||||||
|  |         -webkit-animation-name: am-weather-fog-2; | ||||||
|  |         -moz-animation-name: am-weather-fog-2; | ||||||
|  |         -ms-animation-name: am-weather-fog-2; | ||||||
|  |         animation-name: am-weather-fog-2; | ||||||
|  |         -webkit-animation-duration: 20s; | ||||||
|  |         -moz-animation-duration: 20s; | ||||||
|  |         -ms-animation-duration: 20s; | ||||||
|  |         animation-duration: 20s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-fog-3 { | ||||||
|  |         0% { | ||||||
|  |           transform: translate(0px, 0px) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         25% { | ||||||
|  |           transform: translate(4px, 0px) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         75% { | ||||||
|  |           transform: translate(-4px, 0px) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           transform: translate(0px, 0px) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-fog-3 { | ||||||
|  |         -webkit-animation-name: am-weather-fog-3; | ||||||
|  |         -moz-animation-name: am-weather-fog-3; | ||||||
|  |         -ms-animation-name: am-weather-fog-3; | ||||||
|  |         animation-name: am-weather-fog-3; | ||||||
|  |         -webkit-animation-duration: 6s; | ||||||
|  |         -moz-animation-duration: 6s; | ||||||
|  |         -ms-animation-duration: 6s; | ||||||
|  |         animation-duration: 6s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-fog-4 { | ||||||
|  |         0% { | ||||||
|  |           transform: translate(0px, 0px) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           transform: translate(-4px, 0px) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           transform: translate(0px, 0px) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-fog-4 { | ||||||
|  |         -webkit-animation-name: am-weather-fog-4; | ||||||
|  |         -moz-animation-name: am-weather-fog-4; | ||||||
|  |         -ms-animation-name: am-weather-fog-4; | ||||||
|  |         animation-name: am-weather-fog-4; | ||||||
|  |         -webkit-animation-duration: 6s; | ||||||
|  |         -moz-animation-duration: 6s; | ||||||
|  |         -ms-animation-duration: 6s; | ||||||
|  |         animation-duration: 6s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |     <g transform="translate(0,16)"> | ||||||
|  |       <g class="am-weather-sun" transform="translate(0,16)"> | ||||||
|  |         <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffc04a" stroke-linecap="round" stroke-width="2" /> | ||||||
|  |         <g transform="rotate(45)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffc04a" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(90)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffc04a" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(135)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffc04a" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="scale(-1)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffc04a" stroke-linecap="round" | ||||||
|  |             stroke-width="2" />F | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(225)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffc04a" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(-90)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffc04a" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(-45)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffc04a" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <circle r="5" fill="#ffc04a" stroke="#ffc04a" stroke-width="2" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-fog" transform="translate(-10,20)" fill="none" stroke="#c6deff" stroke-linecap="round" | ||||||
|  |       stroke-width="2"> | ||||||
|  |       <line class="am-weather-fog-1" y1="0" y2="0" x1="1" x2="37" stroke-dasharray="3, 5, 17, 5, 7" /> | ||||||
|  |       <line class="am-weather-fog-2" y1="5" y2="5" x1="9" x2="33" stroke-dasharray="11, 7, 15" /> | ||||||
|  |       <line class="am-weather-fog-3" y1="10" y2="10" x1="5" x2="40" stroke-dasharray="11, 7, 3, 5, 9" /> | ||||||
|  |       <line class="am-weather-fog-4" y1="15" y2="15" x1="7" x2="42" stroke-dasharray="13, 5, 9, 5, 3" /> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 8.0 KiB | 
							
								
								
									
										309
									
								
								public/weather-ico/fog-night.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,309 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <filter id="blur" x="-.20655" y="-.21122" width="1.403" height="1.4997"> | ||||||
|  |       <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |       <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |       <feComponentTransfer> | ||||||
|  |         <feFuncA slope="0.05" type="linear" /> | ||||||
|  |       </feComponentTransfer> | ||||||
|  |       <feMerge> | ||||||
|  |         <feMergeNode /> | ||||||
|  |         <feMergeNode in="SourceGraphic" /> | ||||||
|  |       </feMerge> | ||||||
|  |     </filter> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** MOON | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-moon { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: rotate(15deg); | ||||||
|  |           -moz-transform: rotate(15deg); | ||||||
|  |           -ms-transform: rotate(15deg); | ||||||
|  |           transform: rotate(15deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon { | ||||||
|  |         -webkit-animation-name: am-weather-moon; | ||||||
|  |         -moz-animation-name: am-weather-moon; | ||||||
|  |         -ms-animation-name: am-weather-moon; | ||||||
|  |         animation-name: am-weather-moon; | ||||||
|  |         -webkit-animation-duration: 6s; | ||||||
|  |         -moz-animation-duration: 6s; | ||||||
|  |         -ms-animation-duration: 6s; | ||||||
|  |         animation-duration: 6s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |         -webkit-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         -moz-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         -ms-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-moon-star-1 { | ||||||
|  |         0% { | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon-star-1 { | ||||||
|  |         -webkit-animation-name: am-weather-moon-star-1; | ||||||
|  |         -moz-animation-name: am-weather-moon-star-1; | ||||||
|  |         -ms-animation-name: am-weather-moon-star-1; | ||||||
|  |         animation-name: am-weather-moon-star-1; | ||||||
|  |         -webkit-animation-delay: 3s; | ||||||
|  |         -moz-animation-delay: 3s; | ||||||
|  |         -ms-animation-delay: 3s; | ||||||
|  |         animation-delay: 3s; | ||||||
|  |         -webkit-animation-duration: 5s; | ||||||
|  |         -moz-animation-duration: 5s; | ||||||
|  |         -ms-animation-duration: 5s; | ||||||
|  |         animation-duration: 5s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: 1; | ||||||
|  |         -moz-animation-iteration-count: 1; | ||||||
|  |         -ms-animation-iteration-count: 1; | ||||||
|  |         animation-iteration-count: 1; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-moon-star-2 { | ||||||
|  |         0% { | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon-star-2 { | ||||||
|  |         -webkit-animation-name: am-weather-moon-star-2; | ||||||
|  |         -moz-animation-name: am-weather-moon-star-2; | ||||||
|  |         -ms-animation-name: am-weather-moon-star-2; | ||||||
|  |         animation-name: am-weather-moon-star-2; | ||||||
|  |         -webkit-animation-delay: 5s; | ||||||
|  |         -moz-animation-delay: 5s; | ||||||
|  |         -ms-animation-delay: 5s; | ||||||
|  |         animation-delay: 5s; | ||||||
|  |         -webkit-animation-duration: 4s; | ||||||
|  |         -moz-animation-duration: 4s; | ||||||
|  |         -ms-animation-duration: 4s; | ||||||
|  |         animation-duration: 4s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: 1; | ||||||
|  |         -moz-animation-iteration-count: 1; | ||||||
|  |         -ms-animation-iteration-count: 1; | ||||||
|  |         animation-iteration-count: 1; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** FOG | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-fog-1 { | ||||||
|  |         0% { | ||||||
|  |           transform: translate(0px, 0px) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           transform: translate(7px, 0px) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           transform: translate(0px, 0px) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-fog-1 { | ||||||
|  |         -webkit-animation-name: am-weather-fog-1; | ||||||
|  |         -moz-animation-name: am-weather-fog-1; | ||||||
|  |         -ms-animation-name: am-weather-fog-1; | ||||||
|  |         animation-name: am-weather-fog-1; | ||||||
|  |         -webkit-animation-duration: 8s; | ||||||
|  |         -moz-animation-duration: 8s; | ||||||
|  |         -ms-animation-duration: 8s; | ||||||
|  |         animation-duration: 8s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-fog-2 { | ||||||
|  |         0% { | ||||||
|  |           transform: translate(0px, 0px) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         21.05% { | ||||||
|  |           transform: translate(-6px, 0px) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         78.95% { | ||||||
|  |           transform: translate(9px, 0px) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           transform: translate(0px, 0px) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-fog-2 { | ||||||
|  |         -webkit-animation-name: am-weather-fog-2; | ||||||
|  |         -moz-animation-name: am-weather-fog-2; | ||||||
|  |         -ms-animation-name: am-weather-fog-2; | ||||||
|  |         animation-name: am-weather-fog-2; | ||||||
|  |         -webkit-animation-duration: 20s; | ||||||
|  |         -moz-animation-duration: 20s; | ||||||
|  |         -ms-animation-duration: 20s; | ||||||
|  |         animation-duration: 20s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-fog-3 { | ||||||
|  |         0% { | ||||||
|  |           transform: translate(0px, 0px) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         25% { | ||||||
|  |           transform: translate(4px, 0px) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         75% { | ||||||
|  |           transform: translate(-4px, 0px) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           transform: translate(0px, 0px) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-fog-3 { | ||||||
|  |         -webkit-animation-name: am-weather-fog-3; | ||||||
|  |         -moz-animation-name: am-weather-fog-3; | ||||||
|  |         -ms-animation-name: am-weather-fog-3; | ||||||
|  |         animation-name: am-weather-fog-3; | ||||||
|  |         -webkit-animation-duration: 6s; | ||||||
|  |         -moz-animation-duration: 6s; | ||||||
|  |         -ms-animation-duration: 6s; | ||||||
|  |         animation-duration: 6s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-fog-4 { | ||||||
|  |         0% { | ||||||
|  |           transform: translate(0px, 0px) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           transform: translate(-4px, 0px) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           transform: translate(0px, 0px) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-fog-4 { | ||||||
|  |         -webkit-animation-name: am-weather-fog-4; | ||||||
|  |         -moz-animation-name: am-weather-fog-4; | ||||||
|  |         -ms-animation-name: am-weather-fog-4; | ||||||
|  |         animation-name: am-weather-fog-4; | ||||||
|  |         -webkit-animation-duration: 6s; | ||||||
|  |         -moz-animation-duration: 6s; | ||||||
|  |         -ms-animation-duration: 6s; | ||||||
|  |         animation-duration: 6s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |     <g transform="matrix(.8 0 0 .8 16 4)"> | ||||||
|  |       <g class="am-weather-moon-star-1" | ||||||
|  |         style="-moz-animation-delay:3s;-moz-animation-duration:5s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-1;-moz-animation-timing-function:linear;-ms-animation-delay:3s;-ms-animation-duration:5s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-1;-ms-animation-timing-function:linear;-webkit-animation-delay:3s;-webkit-animation-duration:5s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-1;-webkit-animation-timing-function:linear"> | ||||||
|  |         <polygon points="4 4 3.3 5.2 2.7 4 1.5 3.3 2.7 2.7 3.3 1.5 4 2.7 5.2 3.3" fill="#ffc04a" | ||||||
|  |           stroke-miterlimit="10" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-moon-star-2" | ||||||
|  |         style="-moz-animation-delay:5s;-moz-animation-duration:4s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-2;-moz-animation-timing-function:linear;-ms-animation-delay:5s;-ms-animation-duration:4s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-2;-ms-animation-timing-function:linear;-webkit-animation-delay:5s;-webkit-animation-duration:4s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-2;-webkit-animation-timing-function:linear"> | ||||||
|  |         <polygon transform="translate(20,10)" points="4 4 3.3 5.2 2.7 4 1.5 3.3 2.7 2.7 3.3 1.5 4 2.7 5.2 3.3" | ||||||
|  |           fill="#ffc04a" stroke-miterlimit="10" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-moon" | ||||||
|  |         style="-moz-animation-duration:6s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-moon;-moz-animation-timing-function:linear;-moz-transform-origin:12.5px 15.15px 0;-ms-animation-duration:6s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-moon;-ms-animation-timing-function:linear;-ms-transform-origin:12.5px 15.15px 0;-webkit-animation-duration:6s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-moon;-webkit-animation-timing-function:linear;-webkit-transform-origin:12.5px 15.15px 0"> | ||||||
|  |         <path | ||||||
|  |           d="m14.5 13.2c0-3.7 2-6.9 5-8.7-1.5-0.9-3.2-1.3-5-1.3-5.5 0-10 4.5-10 10s4.5 10 10 10c1.8 0 3.5-0.5 5-1.3-3-1.7-5-5-5-8.7z" | ||||||
|  |           fill="#ffc04a" stroke="#ffc04a" stroke-linejoin="round" stroke-width="2" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-fog" transform="translate(-10,20)" fill="none" stroke="#c6deff" stroke-linecap="round" | ||||||
|  |       stroke-width="2"> | ||||||
|  |       <line class="am-weather-fog-1" y1="0" y2="0" x1="1" x2="37" stroke-dasharray="3, 5, 17, 5, 7" /> | ||||||
|  |       <line class="am-weather-fog-2" y1="5" y2="5" x1="9" x2="33" stroke-dasharray="11, 7, 15" /> | ||||||
|  |       <line class="am-weather-fog-3" y1="10" y2="10" x1="5" x2="40" stroke-dasharray="11, 7, 3, 5, 9" /> | ||||||
|  |       <line class="am-weather-fog-4" y1="15" y2="15" x1="7" x2="42" stroke-dasharray="13, 5, 9, 5, 3" /> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 11 KiB | 
							
								
								
									
										204
									
								
								public/weather-ico/frost-day.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,204 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <filter id="blur" x="-.20655" y="-.21122" width="1.403" height="1.4997"> | ||||||
|  |       <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |       <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |       <feComponentTransfer> | ||||||
|  |         <feFuncA slope="0.05" type="linear" /> | ||||||
|  |       </feComponentTransfer> | ||||||
|  |       <feMerge> | ||||||
|  |         <feMergeNode /> | ||||||
|  |         <feMergeNode in="SourceGraphic" /> | ||||||
|  |       </feMerge> | ||||||
|  |     </filter> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** SUN | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-sun { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: rotate(360deg); | ||||||
|  |           -moz-transform: rotate(360deg); | ||||||
|  |           -ms-transform: rotate(360deg); | ||||||
|  |           transform: rotate(360deg); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-sun { | ||||||
|  |         -webkit-animation-name: am-weather-sun; | ||||||
|  |         -moz-animation-name: am-weather-sun; | ||||||
|  |         -ms-animation-name: am-weather-sun; | ||||||
|  |         animation-name: am-weather-sun; | ||||||
|  |         -webkit-animation-duration: 9s; | ||||||
|  |         -moz-animation-duration: 9s; | ||||||
|  |         -ms-animation-duration: 9s; | ||||||
|  |         animation-duration: 9s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** FROST | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-frost { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         1% { | ||||||
|  |           -webkit-transform: translate(0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.3px, 0.0px); | ||||||
|  |           transform: translate(0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         3% { | ||||||
|  |           -webkit-transform: translate(-0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(-0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(-0.3px, 0.0px); | ||||||
|  |           transform: translate(-0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         5% { | ||||||
|  |           -webkit-transform: translate(0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.3px, 0.0px); | ||||||
|  |           transform: translate(0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         7% { | ||||||
|  |           -webkit-transform: translate(-0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(-0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(-0.3px, 0.0px); | ||||||
|  |           transform: translate(-0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         9% { | ||||||
|  |           -webkit-transform: translate(0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.3px, 0.0px); | ||||||
|  |           transform: translate(0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         11% { | ||||||
|  |           -webkit-transform: translate(-0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(-0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(-0.3px, 0.0px); | ||||||
|  |           transform: translate(-0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         13% { | ||||||
|  |           -webkit-transform: translate(0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.3px, 0.0px); | ||||||
|  |           transform: translate(0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         15% { | ||||||
|  |           -webkit-transform: translate(-0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(-0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(-0.3px, 0.0px); | ||||||
|  |           transform: translate(-0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         16% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-frost { | ||||||
|  |         -webkit-animation-name: am-weather-frost; | ||||||
|  |         -moz-animation-name: am-weather-frost; | ||||||
|  |         animation-name: am-weather-frost; | ||||||
|  |         -webkit-animation-duration: 1.11s; | ||||||
|  |         -moz-animation-duration: 1.11s; | ||||||
|  |         animation-duration: 1.11s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |     <g transform="translate(0,16)"> | ||||||
|  |       <g class="am-weather-sun" transform="translate(0,16)"> | ||||||
|  |         <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffc04a" stroke-linecap="round" stroke-width="2" /> | ||||||
|  |         <g transform="rotate(45)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffc04a" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(90)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffc04a" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(135)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffc04a" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="scale(-1)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffc04a" stroke-linecap="round" | ||||||
|  |             stroke-width="2" />F | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(225)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffc04a" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(-90)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffc04a" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(-45)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffc04a" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <circle r="5" fill="#ffc04a" stroke="#ffc04a" stroke-width="2" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |     <g transform="translate(-16,4)"> | ||||||
|  |       <g class="am-weather-frost" stroke="#57a0ee" transform="translate(0,2)" fill="none" stroke-width="2" | ||||||
|  |         stroke-linecap="round" | ||||||
|  |         style="-moz-animation-duration:1.11s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-frost;-moz-animation-timing-function:linear;-webkit-animation-duration:1.11s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-frost;-webkit-animation-timing-function:linear"> | ||||||
|  |         <path d="M11,32H45" /> | ||||||
|  |         <path d="M15.5,37H40.5" /> | ||||||
|  |         <path d="M22.5,42H33.5" /> | ||||||
|  |       </g> | ||||||
|  |       <g> | ||||||
|  |         <path stroke="#57a0ee" transform="translate(0,0)" fill="none" stroke-width="2" stroke-linecap="round" | ||||||
|  |           d="M28,31V9M28,22l11,-3.67M34,20l2,-4M34,20l4,2M28,22l-11,-3.67M22,20l-2,-4M22,20l-4,2M28,14.27l3.01,-3.02M28,14.27l-3.01,-3.02" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 7.3 KiB | 
							
								
								
									
										269
									
								
								public/weather-ico/frost-night.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,269 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <filter id="blur" x="-.20655" y="-.21122" width="1.403" height="1.4997"> | ||||||
|  |       <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |       <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |       <feComponentTransfer> | ||||||
|  |         <feFuncA slope="0.05" type="linear" /> | ||||||
|  |       </feComponentTransfer> | ||||||
|  |       <feMerge> | ||||||
|  |         <feMergeNode /> | ||||||
|  |         <feMergeNode in="SourceGraphic" /> | ||||||
|  |       </feMerge> | ||||||
|  |     </filter> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** MOON | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-moon { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: rotate(15deg); | ||||||
|  |           -moz-transform: rotate(15deg); | ||||||
|  |           -ms-transform: rotate(15deg); | ||||||
|  |           transform: rotate(15deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon { | ||||||
|  |         -webkit-animation-name: am-weather-moon; | ||||||
|  |         -moz-animation-name: am-weather-moon; | ||||||
|  |         -ms-animation-name: am-weather-moon; | ||||||
|  |         animation-name: am-weather-moon; | ||||||
|  |         -webkit-animation-duration: 6s; | ||||||
|  |         -moz-animation-duration: 6s; | ||||||
|  |         -ms-animation-duration: 6s; | ||||||
|  |         animation-duration: 6s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |         -webkit-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         -moz-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         -ms-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-moon-star-1 { | ||||||
|  |         0% { | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon-star-1 { | ||||||
|  |         -webkit-animation-name: am-weather-moon-star-1; | ||||||
|  |         -moz-animation-name: am-weather-moon-star-1; | ||||||
|  |         -ms-animation-name: am-weather-moon-star-1; | ||||||
|  |         animation-name: am-weather-moon-star-1; | ||||||
|  |         -webkit-animation-delay: 3s; | ||||||
|  |         -moz-animation-delay: 3s; | ||||||
|  |         -ms-animation-delay: 3s; | ||||||
|  |         animation-delay: 3s; | ||||||
|  |         -webkit-animation-duration: 5s; | ||||||
|  |         -moz-animation-duration: 5s; | ||||||
|  |         -ms-animation-duration: 5s; | ||||||
|  |         animation-duration: 5s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: 1; | ||||||
|  |         -moz-animation-iteration-count: 1; | ||||||
|  |         -ms-animation-iteration-count: 1; | ||||||
|  |         animation-iteration-count: 1; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-moon-star-2 { | ||||||
|  |         0% { | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon-star-2 { | ||||||
|  |         -webkit-animation-name: am-weather-moon-star-2; | ||||||
|  |         -moz-animation-name: am-weather-moon-star-2; | ||||||
|  |         -ms-animation-name: am-weather-moon-star-2; | ||||||
|  |         animation-name: am-weather-moon-star-2; | ||||||
|  |         -webkit-animation-delay: 5s; | ||||||
|  |         -moz-animation-delay: 5s; | ||||||
|  |         -ms-animation-delay: 5s; | ||||||
|  |         animation-delay: 5s; | ||||||
|  |         -webkit-animation-duration: 4s; | ||||||
|  |         -moz-animation-duration: 4s; | ||||||
|  |         -ms-animation-duration: 4s; | ||||||
|  |         animation-duration: 4s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: 1; | ||||||
|  |         -moz-animation-iteration-count: 1; | ||||||
|  |         -ms-animation-iteration-count: 1; | ||||||
|  |         animation-iteration-count: 1; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** FROST | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-frost { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         1% { | ||||||
|  |           -webkit-transform: translate(0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.3px, 0.0px); | ||||||
|  |           transform: translate(0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         3% { | ||||||
|  |           -webkit-transform: translate(-0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(-0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(-0.3px, 0.0px); | ||||||
|  |           transform: translate(-0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         5% { | ||||||
|  |           -webkit-transform: translate(0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.3px, 0.0px); | ||||||
|  |           transform: translate(0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         7% { | ||||||
|  |           -webkit-transform: translate(-0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(-0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(-0.3px, 0.0px); | ||||||
|  |           transform: translate(-0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         9% { | ||||||
|  |           -webkit-transform: translate(0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.3px, 0.0px); | ||||||
|  |           transform: translate(0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         11% { | ||||||
|  |           -webkit-transform: translate(-0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(-0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(-0.3px, 0.0px); | ||||||
|  |           transform: translate(-0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         13% { | ||||||
|  |           -webkit-transform: translate(0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.3px, 0.0px); | ||||||
|  |           transform: translate(0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         15% { | ||||||
|  |           -webkit-transform: translate(-0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(-0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(-0.3px, 0.0px); | ||||||
|  |           transform: translate(-0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         16% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-frost { | ||||||
|  |         -webkit-animation-name: am-weather-frost; | ||||||
|  |         -moz-animation-name: am-weather-frost; | ||||||
|  |         animation-name: am-weather-frost; | ||||||
|  |         -webkit-animation-duration: 1.11s; | ||||||
|  |         -moz-animation-duration: 1.11s; | ||||||
|  |         animation-duration: 1.11s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |     <g transform="matrix(.8 0 0 .8 16 4)"> | ||||||
|  |       <g class="am-weather-moon-star-1" | ||||||
|  |         style="-moz-animation-delay:3s;-moz-animation-duration:5s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-1;-moz-animation-timing-function:linear;-ms-animation-delay:3s;-ms-animation-duration:5s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-1;-ms-animation-timing-function:linear;-webkit-animation-delay:3s;-webkit-animation-duration:5s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-1;-webkit-animation-timing-function:linear"> | ||||||
|  |         <polygon points="4 4 3.3 5.2 2.7 4 1.5 3.3 2.7 2.7 3.3 1.5 4 2.7 5.2 3.3" fill="#ffc04a" | ||||||
|  |           stroke-miterlimit="10" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-moon-star-2" | ||||||
|  |         style="-moz-animation-delay:5s;-moz-animation-duration:4s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-2;-moz-animation-timing-function:linear;-ms-animation-delay:5s;-ms-animation-duration:4s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-2;-ms-animation-timing-function:linear;-webkit-animation-delay:5s;-webkit-animation-duration:4s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-2;-webkit-animation-timing-function:linear"> | ||||||
|  |         <polygon transform="translate(20,10)" points="4 4 3.3 5.2 2.7 4 1.5 3.3 2.7 2.7 3.3 1.5 4 2.7 5.2 3.3" | ||||||
|  |           fill="#ffc04a" stroke-miterlimit="10" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-moon" | ||||||
|  |         style="-moz-animation-duration:6s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-moon;-moz-animation-timing-function:linear;-moz-transform-origin:12.5px 15.15px 0;-ms-animation-duration:6s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-moon;-ms-animation-timing-function:linear;-ms-transform-origin:12.5px 15.15px 0;-webkit-animation-duration:6s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-moon;-webkit-animation-timing-function:linear;-webkit-transform-origin:12.5px 15.15px 0"> | ||||||
|  |         <path | ||||||
|  |           d="m14.5 13.2c0-3.7 2-6.9 5-8.7-1.5-0.9-3.2-1.3-5-1.3-5.5 0-10 4.5-10 10s4.5 10 10 10c1.8 0 3.5-0.5 5-1.3-3-1.7-5-5-5-8.7z" | ||||||
|  |           fill="#ffc04a" stroke="#ffc04a" stroke-linejoin="round" stroke-width="2" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |     <g transform="translate(-16,4)"> | ||||||
|  |       <g class="am-weather-frost" stroke="#57a0ee" transform="translate(0,2)" fill="none" stroke-width="2" | ||||||
|  |         stroke-linecap="round" | ||||||
|  |         style="-moz-animation-duration:1.11s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-frost;-moz-animation-timing-function:linear;-webkit-animation-duration:1.11s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-frost;-webkit-animation-timing-function:linear"> | ||||||
|  |         <path d="M11,32H45" /> | ||||||
|  |         <path d="M15.5,37H40.5" /> | ||||||
|  |         <path d="M22.5,42H33.5" /> | ||||||
|  |       </g> | ||||||
|  |       <g> | ||||||
|  |         <path stroke="#57a0ee" transform="translate(0,0)" fill="none" stroke-width="2" stroke-linecap="round" | ||||||
|  |           d="M28,31V9M28,22l11,-3.67M34,20l2,-4M34,20l4,2M28,22l-11,-3.67M22,20l-2,-4M22,20l-4,2M28,14.27l3.01,-3.02M28,14.27l-3.01,-3.02" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 11 KiB | 
							
								
								
									
										141
									
								
								public/weather-ico/rain-and-sleet-mix.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,141 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <!-- Mix of Rain and Sleet | Contributed by hsoJ95 on GitHub: https://github.com/hsoj95 --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <filter id="blur" x="-.24684" y="-.22776" width="1.4937" height="1.5756"> | ||||||
|  |       <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |       <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |       <feComponentTransfer> | ||||||
|  |         <feFuncA slope="0.05" type="linear" /> | ||||||
|  |       </feComponentTransfer> | ||||||
|  |       <feMerge> | ||||||
|  |         <feMergeNode /> | ||||||
|  |         <feMergeNode in="SourceGraphic" /> | ||||||
|  |       </feMerge> | ||||||
|  |     </filter> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** RAIN | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-rain { | ||||||
|  |         0% { | ||||||
|  |           stroke-dashoffset: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           stroke-dashoffset: -100; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-rain-1 { | ||||||
|  |         -webkit-animation-name: am-weather-rain; | ||||||
|  |         -moz-animation-name: am-weather-rain; | ||||||
|  |         -ms-animation-name: am-weather-rain; | ||||||
|  |         animation-name: am-weather-rain; | ||||||
|  |         -webkit-animation-duration: 8s; | ||||||
|  |         -moz-animation-duration: 8s; | ||||||
|  |         -ms-animation-duration: 8s; | ||||||
|  |         animation-duration: 8s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-rain-2 { | ||||||
|  |         -webkit-animation-name: am-weather-rain; | ||||||
|  |         -moz-animation-name: am-weather-rain; | ||||||
|  |         -ms-animation-name: am-weather-rain; | ||||||
|  |         animation-name: am-weather-rain; | ||||||
|  |         -webkit-animation-delay: 0.25s; | ||||||
|  |         -moz-animation-delay: 0.25s; | ||||||
|  |         -ms-animation-delay: 0.25s; | ||||||
|  |         animation-delay: 0.25s; | ||||||
|  |         -webkit-animation-duration: 8s; | ||||||
|  |         -moz-animation-duration: 8s; | ||||||
|  |         -ms-animation-duration: 8s; | ||||||
|  |         animation-duration: 8s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** CLOUDS | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-cloud-3 { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: translate(2px, 0px); | ||||||
|  |           -moz-transform: translate(2px, 0px); | ||||||
|  |           -ms-transform: translate(2px, 0px); | ||||||
|  |           transform: translate(2px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-cloud-3 { | ||||||
|  |         -webkit-animation-name: am-weather-cloud-3; | ||||||
|  |         -moz-animation-name: am-weather-cloud-3; | ||||||
|  |         animation-name: am-weather-cloud-3; | ||||||
|  |         -webkit-animation-duration: 3s; | ||||||
|  |         -moz-animation-duration: 3s; | ||||||
|  |         animation-duration: 3s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |     <g class="am-weather-cloud-3" | ||||||
|  |       style="-moz-animation-duration:3s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-cloud-3;-moz-animation-timing-function:linear;-webkit-animation-duration:3s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-cloud-3;-webkit-animation-timing-function:linear"> | ||||||
|  |       <path transform="translate(-20,-11)" | ||||||
|  |         d="m47.7 35.4c0-4.6-3.7-8.2-8.2-8.2-1 0-1.9 0.2-2.8 0.5-0.3-3.4-3.1-6.2-6.6-6.2-3.7 0-6.7 3-6.7 6.7 0 0.8 0.2 1.6 0.4 2.3-0.3-0.1-0.7-0.1-1-0.1-3.7 0-6.7 3-6.7 6.7 0 3.6 2.9 6.6 6.5 6.7h17.2c4.4-0.5 7.9-4 7.9-8.4z" | ||||||
|  |         fill="#57a0ee" stroke="#fff" stroke-linejoin="round" stroke-width="1.2" /> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-sleet-2" transform="translate(-20,-10) rotate(10,-247.39,200.17)" fill="none" stroke="#91c0f8" | ||||||
|  |       stroke-linecap="round"> | ||||||
|  |       <line class="am-weather-rain-1" transform="translate(-5,1)" y2="8" stroke-dasharray="0.1, 7" stroke-width="2" | ||||||
|  |         style="-moz-animation-duration:8s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-rain;-moz-animation-timing-function:linear;-ms-animation-duration:8s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-rain;-ms-animation-timing-function:linear;-webkit-animation-duration:8s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-rain;-webkit-animation-timing-function:linear" /> | ||||||
|  |       <line class="am-weather-rain-1" transform="translate(5)" y2="8" stroke-dasharray="0.1, 7" stroke-width="2" | ||||||
|  |         style="-moz-animation-duration:8s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-rain;-moz-animation-timing-function:linear;-ms-animation-duration:8s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-rain;-ms-animation-timing-function:linear;-webkit-animation-duration:8s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-rain;-webkit-animation-timing-function:linear" /> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-rain-3" transform="translate(-20,-10) rotate(10,-245.89,217.31)" fill="none" stroke="#91c0f8" | ||||||
|  |       stroke-dasharray="4, 7" stroke-linecap="round" stroke-width="2"> | ||||||
|  |       <line class="am-weather-rain-1" transform="translate(-13,1)" y2="8" | ||||||
|  |         style="-moz-animation-duration:8s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-rain;-moz-animation-timing-function:linear;-ms-animation-duration:8s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-rain;-ms-animation-timing-function:linear;-webkit-animation-duration:8s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-rain;-webkit-animation-timing-function:linear" /> | ||||||
|  |       <line class="am-weather-rain-1" transform="translate(-3,2)" y2="8" | ||||||
|  |         style="-moz-animation-duration:8s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-rain;-moz-animation-timing-function:linear;-ms-animation-duration:8s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-rain;-ms-animation-timing-function:linear;-webkit-animation-duration:8s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-rain;-webkit-animation-timing-function:linear" /> | ||||||
|  |       <line class="am-weather-rain-2" transform="translate(7,-1)" y2="8" | ||||||
|  |         style="-moz-animation-delay:0.25s;-moz-animation-duration:8s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-rain;-moz-animation-timing-function:linear;-ms-animation-delay:0.25s;-ms-animation-duration:8s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-rain;-ms-animation-timing-function:linear;-webkit-animation-delay:0.25s;-webkit-animation-duration:8s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-rain;-webkit-animation-timing-function:linear" /> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 7.6 KiB | 
							
								
								
									
										179
									
								
								public/weather-ico/rainy-1-day.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,179 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <filter id="blur" x="-.20655" y="-.21122" width="1.403" height="1.4997"> | ||||||
|  |       <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |       <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |       <feComponentTransfer> | ||||||
|  |         <feFuncA slope="0.05" type="linear" /> | ||||||
|  |       </feComponentTransfer> | ||||||
|  |       <feMerge> | ||||||
|  |         <feMergeNode /> | ||||||
|  |         <feMergeNode in="SourceGraphic" /> | ||||||
|  |       </feMerge> | ||||||
|  |     </filter> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** SUN | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-sun { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: rotate(360deg); | ||||||
|  |           -moz-transform: rotate(360deg); | ||||||
|  |           -ms-transform: rotate(360deg); | ||||||
|  |           transform: rotate(360deg); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-sun { | ||||||
|  |         -webkit-animation-name: am-weather-sun; | ||||||
|  |         -moz-animation-name: am-weather-sun; | ||||||
|  |         -ms-animation-name: am-weather-sun; | ||||||
|  |         animation-name: am-weather-sun; | ||||||
|  |         -webkit-animation-duration: 9s; | ||||||
|  |         -moz-animation-duration: 9s; | ||||||
|  |         -ms-animation-duration: 9s; | ||||||
|  |         animation-duration: 9s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** CLOUDS | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-cloud-2 { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: translate(2px, 0px); | ||||||
|  |           -moz-transform: translate(2px, 0px); | ||||||
|  |           -ms-transform: translate(2px, 0px); | ||||||
|  |           transform: translate(2px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-cloud-2 { | ||||||
|  |         -webkit-animation-name: am-weather-cloud-2; | ||||||
|  |         -moz-animation-name: am-weather-cloud-2; | ||||||
|  |         animation-name: am-weather-cloud-2; | ||||||
|  |         -webkit-animation-duration: 3s; | ||||||
|  |         -moz-animation-duration: 3s; | ||||||
|  |         animation-duration: 3s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** RAIN | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-rain { | ||||||
|  |         0% { | ||||||
|  |           stroke-dashoffset: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           stroke-dashoffset: -100; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-rain-1 { | ||||||
|  |         -webkit-animation-name: am-weather-rain; | ||||||
|  |         -moz-animation-name: am-weather-rain; | ||||||
|  |         -ms-animation-name: am-weather-rain; | ||||||
|  |         animation-name: am-weather-rain; | ||||||
|  |         -webkit-animation-duration: 8s; | ||||||
|  |         -moz-animation-duration: 8s; | ||||||
|  |         -ms-animation-duration: 8s; | ||||||
|  |         animation-duration: 8s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |     <g transform="translate(0,16)"> | ||||||
|  |       <g class="am-weather-sun" | ||||||
|  |         style="-moz-animation-duration:9s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-sun;-moz-animation-timing-function:linear;-ms-animation-duration:9s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-sun;-ms-animation-timing-function:linear;-webkit-animation-duration:9s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-sun;-webkit-animation-timing-function:linear"> | ||||||
|  |         <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" stroke-width="2" /> | ||||||
|  |         <g transform="rotate(45)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(90)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(135)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="scale(-1)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(225)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(-90)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(-45)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <circle r="5" fill="#ffa500" stroke="#ffa500" stroke-width="2" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-cloud-3" | ||||||
|  |       style="-moz-animation-duration:3s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-cloud-2;-moz-animation-timing-function:linear;-webkit-animation-duration:3s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-cloud-2;-webkit-animation-timing-function:linear"> | ||||||
|  |       <path transform="translate(-20,-11)" | ||||||
|  |         d="m47.7 35.4c0-4.6-3.7-8.2-8.2-8.2-1 0-1.9 0.2-2.8 0.5-0.3-3.4-3.1-6.2-6.6-6.2-3.7 0-6.7 3-6.7 6.7 0 0.8 0.2 1.6 0.4 2.3-0.3-0.1-0.7-0.1-1-0.1-3.7 0-6.7 3-6.7 6.7 0 3.6 2.9 6.6 6.5 6.7h17.2c4.4-0.5 7.9-4 7.9-8.4z" | ||||||
|  |         fill="#57a0ee" stroke="#fff" stroke-linejoin="round" stroke-width="1.2" /> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-rain-1" transform="translate(-20,-10) rotate(10,-238.68,233.96)"> | ||||||
|  |       <line class="am-weather-rain-1" transform="translate(-6,1)" y2="8" fill="none" stroke="#91c0f8" | ||||||
|  |         stroke-dasharray="4, 7" stroke-linecap="round" stroke-width="2" | ||||||
|  |         style="-moz-animation-duration:8s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-rain;-moz-animation-timing-function:linear;-ms-animation-duration:8s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-rain;-ms-animation-timing-function:linear;-webkit-animation-duration:8s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-rain;-webkit-animation-timing-function:linear" /> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 7.4 KiB | 
							
								
								
									
										243
									
								
								public/weather-ico/rainy-1-night.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,243 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <filter id="blur" x="-.20655" y="-.21122" width="1.403" height="1.4997"> | ||||||
|  |       <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |       <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |       <feComponentTransfer> | ||||||
|  |         <feFuncA slope="0.05" type="linear" /> | ||||||
|  |       </feComponentTransfer> | ||||||
|  |       <feMerge> | ||||||
|  |         <feMergeNode /> | ||||||
|  |         <feMergeNode in="SourceGraphic" /> | ||||||
|  |       </feMerge> | ||||||
|  |     </filter> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** MOON | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-moon { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: rotate(15deg); | ||||||
|  |           -moz-transform: rotate(15deg); | ||||||
|  |           -ms-transform: rotate(15deg); | ||||||
|  |           transform: rotate(15deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon { | ||||||
|  |         -webkit-animation-name: am-weather-moon; | ||||||
|  |         -moz-animation-name: am-weather-moon; | ||||||
|  |         -ms-animation-name: am-weather-moon; | ||||||
|  |         animation-name: am-weather-moon; | ||||||
|  |         -webkit-animation-duration: 6s; | ||||||
|  |         -moz-animation-duration: 6s; | ||||||
|  |         -ms-animation-duration: 6s; | ||||||
|  |         animation-duration: 6s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |         -webkit-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         -moz-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         -ms-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-moon-star-1 { | ||||||
|  |         0% { | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon-star-1 { | ||||||
|  |         -webkit-animation-name: am-weather-moon-star-1; | ||||||
|  |         -moz-animation-name: am-weather-moon-star-1; | ||||||
|  |         -ms-animation-name: am-weather-moon-star-1; | ||||||
|  |         animation-name: am-weather-moon-star-1; | ||||||
|  |         -webkit-animation-delay: 3s; | ||||||
|  |         -moz-animation-delay: 3s; | ||||||
|  |         -ms-animation-delay: 3s; | ||||||
|  |         animation-delay: 3s; | ||||||
|  |         -webkit-animation-duration: 5s; | ||||||
|  |         -moz-animation-duration: 5s; | ||||||
|  |         -ms-animation-duration: 5s; | ||||||
|  |         animation-duration: 5s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: 1; | ||||||
|  |         -moz-animation-iteration-count: 1; | ||||||
|  |         -ms-animation-iteration-count: 1; | ||||||
|  |         animation-iteration-count: 1; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-moon-star-2 { | ||||||
|  |         0% { | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon-star-2 { | ||||||
|  |         -webkit-animation-name: am-weather-moon-star-2; | ||||||
|  |         -moz-animation-name: am-weather-moon-star-2; | ||||||
|  |         -ms-animation-name: am-weather-moon-star-2; | ||||||
|  |         animation-name: am-weather-moon-star-2; | ||||||
|  |         -webkit-animation-delay: 5s; | ||||||
|  |         -moz-animation-delay: 5s; | ||||||
|  |         -ms-animation-delay: 5s; | ||||||
|  |         animation-delay: 5s; | ||||||
|  |         -webkit-animation-duration: 4s; | ||||||
|  |         -moz-animation-duration: 4s; | ||||||
|  |         -ms-animation-duration: 4s; | ||||||
|  |         animation-duration: 4s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: 1; | ||||||
|  |         -moz-animation-iteration-count: 1; | ||||||
|  |         -ms-animation-iteration-count: 1; | ||||||
|  |         animation-iteration-count: 1; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** CLOUDS | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-cloud-2 { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: translate(2px, 0px); | ||||||
|  |           -moz-transform: translate(2px, 0px); | ||||||
|  |           -ms-transform: translate(2px, 0px); | ||||||
|  |           transform: translate(2px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-cloud-2 { | ||||||
|  |         -webkit-animation-name: am-weather-cloud-2; | ||||||
|  |         -moz-animation-name: am-weather-cloud-2; | ||||||
|  |         animation-name: am-weather-cloud-2; | ||||||
|  |         -webkit-animation-duration: 3s; | ||||||
|  |         -moz-animation-duration: 3s; | ||||||
|  |         animation-duration: 3s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** RAIN | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-rain { | ||||||
|  |         0% { | ||||||
|  |           stroke-dashoffset: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           stroke-dashoffset: -100; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-rain-1 { | ||||||
|  |         -webkit-animation-name: am-weather-rain; | ||||||
|  |         -moz-animation-name: am-weather-rain; | ||||||
|  |         -ms-animation-name: am-weather-rain; | ||||||
|  |         animation-name: am-weather-rain; | ||||||
|  |         -webkit-animation-duration: 8s; | ||||||
|  |         -moz-animation-duration: 8s; | ||||||
|  |         -ms-animation-duration: 8s; | ||||||
|  |         animation-duration: 8s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |     <g transform="matrix(.8 0 0 .8 16 4)"> | ||||||
|  |       <g class="am-weather-moon-star-1" | ||||||
|  |         style="-moz-animation-delay:3s;-moz-animation-duration:5s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-1;-moz-animation-timing-function:linear;-ms-animation-delay:3s;-ms-animation-duration:5s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-1;-ms-animation-timing-function:linear;-webkit-animation-delay:3s;-webkit-animation-duration:5s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-1;-webkit-animation-timing-function:linear"> | ||||||
|  |         <polygon points="4 4 3.3 5.2 2.7 4 1.5 3.3 2.7 2.7 3.3 1.5 4 2.7 5.2 3.3" fill="#ffa500" | ||||||
|  |           stroke-miterlimit="10" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-moon-star-2" | ||||||
|  |         style="-moz-animation-delay:5s;-moz-animation-duration:4s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-2;-moz-animation-timing-function:linear;-ms-animation-delay:5s;-ms-animation-duration:4s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-2;-ms-animation-timing-function:linear;-webkit-animation-delay:5s;-webkit-animation-duration:4s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-2;-webkit-animation-timing-function:linear"> | ||||||
|  |         <polygon transform="translate(20,10)" points="4 4 3.3 5.2 2.7 4 1.5 3.3 2.7 2.7 3.3 1.5 4 2.7 5.2 3.3" | ||||||
|  |           fill="#ffa500" stroke-miterlimit="10" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-moon" | ||||||
|  |         style="-moz-animation-duration:6s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-moon;-moz-animation-timing-function:linear;-moz-transform-origin:12.5px 15.15px 0;-ms-animation-duration:6s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-moon;-ms-animation-timing-function:linear;-ms-transform-origin:12.5px 15.15px 0;-webkit-animation-duration:6s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-moon;-webkit-animation-timing-function:linear;-webkit-transform-origin:12.5px 15.15px 0"> | ||||||
|  |         <path | ||||||
|  |           d="m14.5 13.2c0-3.7 2-6.9 5-8.7-1.5-0.9-3.2-1.3-5-1.3-5.5 0-10 4.5-10 10s4.5 10 10 10c1.8 0 3.5-0.5 5-1.3-3-1.7-5-5-5-8.7z" | ||||||
|  |           fill="#ffa500" stroke="#ffa500" stroke-linejoin="round" stroke-width="2" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-cloud-3" | ||||||
|  |       style="-moz-animation-duration:3s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-cloud-2;-moz-animation-timing-function:linear;-webkit-animation-duration:3s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-cloud-2;-webkit-animation-timing-function:linear"> | ||||||
|  |       <path transform="translate(-20,-11)" | ||||||
|  |         d="m47.7 35.4c0-4.6-3.7-8.2-8.2-8.2-1 0-1.9 0.2-2.8 0.5-0.3-3.4-3.1-6.2-6.6-6.2-3.7 0-6.7 3-6.7 6.7 0 0.8 0.2 1.6 0.4 2.3-0.3-0.1-0.7-0.1-1-0.1-3.7 0-6.7 3-6.7 6.7 0 3.6 2.9 6.6 6.5 6.7h17.2c4.4-0.5 7.9-4 7.9-8.4z" | ||||||
|  |         fill="#57a0ee" stroke="#fff" stroke-linejoin="round" stroke-width="1.2" /> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weaher-rain-1" transform="translate(-20,-10) rotate(10,-238.68,233.96)"> | ||||||
|  |       <line class="am-weather-rain-1" transform="translate(-6,1)" y2="8" fill="none" stroke="#91c0f8" | ||||||
|  |         stroke-dasharray="4, 7" stroke-linecap="round" stroke-width="2" | ||||||
|  |         style="-moz-animation-duration:8s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-rain;-moz-animation-timing-function:linear;-ms-animation-duration:8s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-rain;-ms-animation-timing-function:linear;-webkit-animation-duration:8s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-rain;-webkit-animation-timing-function:linear" /> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 10 KiB | 
							
								
								
									
										204
									
								
								public/weather-ico/rainy-2-day.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,204 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <filter id="blur" x="-.20655" y="-.20592" width="1.403" height="1.4872"> | ||||||
|  |       <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |       <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |       <feComponentTransfer> | ||||||
|  |         <feFuncA slope="0.05" type="linear" /> | ||||||
|  |       </feComponentTransfer> | ||||||
|  |       <feMerge> | ||||||
|  |         <feMergeNode /> | ||||||
|  |         <feMergeNode in="SourceGraphic" /> | ||||||
|  |       </feMerge> | ||||||
|  |     </filter> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** SUN | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-sun { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: rotate(360deg); | ||||||
|  |           -moz-transform: rotate(360deg); | ||||||
|  |           -ms-transform: rotate(360deg); | ||||||
|  |           transform: rotate(360deg); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-sun { | ||||||
|  |         -webkit-animation-name: am-weather-sun; | ||||||
|  |         -moz-animation-name: am-weather-sun; | ||||||
|  |         -ms-animation-name: am-weather-sun; | ||||||
|  |         animation-name: am-weather-sun; | ||||||
|  |         -webkit-animation-duration: 9s; | ||||||
|  |         -moz-animation-duration: 9s; | ||||||
|  |         -ms-animation-duration: 9s; | ||||||
|  |         animation-duration: 9s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** CLOUDS | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-cloud-2 { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: translate(2px, 0px); | ||||||
|  |           -moz-transform: translate(2px, 0px); | ||||||
|  |           -ms-transform: translate(2px, 0px); | ||||||
|  |           transform: translate(2px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-cloud-2 { | ||||||
|  |         -webkit-animation-name: am-weather-cloud-2; | ||||||
|  |         -moz-animation-name: am-weather-cloud-2; | ||||||
|  |         animation-name: am-weather-cloud-2; | ||||||
|  |         -webkit-animation-duration: 3s; | ||||||
|  |         -moz-animation-duration: 3s; | ||||||
|  |         animation-duration: 3s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** RAIN | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-rain { | ||||||
|  |         0% { | ||||||
|  |           stroke-dashoffset: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           stroke-dashoffset: -100; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-rain-1 { | ||||||
|  |         -webkit-animation-name: am-weather-rain; | ||||||
|  |         -moz-animation-name: am-weather-rain; | ||||||
|  |         -ms-animation-name: am-weather-rain; | ||||||
|  |         animation-name: am-weather-rain; | ||||||
|  |         -webkit-animation-duration: 8s; | ||||||
|  |         -moz-animation-duration: 8s; | ||||||
|  |         -ms-animation-duration: 8s; | ||||||
|  |         animation-duration: 8s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-rain-2 { | ||||||
|  |         -webkit-animation-name: am-weather-rain; | ||||||
|  |         -moz-animation-name: am-weather-rain; | ||||||
|  |         -ms-animation-name: am-weather-rain; | ||||||
|  |         animation-name: am-weather-rain; | ||||||
|  |         -webkit-animation-delay: 0.25s; | ||||||
|  |         -moz-animation-delay: 0.25s; | ||||||
|  |         -ms-animation-delay: 0.25s; | ||||||
|  |         animation-delay: 0.25s; | ||||||
|  |         -webkit-animation-duration: 8s; | ||||||
|  |         -moz-animation-duration: 8s; | ||||||
|  |         -ms-animation-duration: 8s; | ||||||
|  |         animation-duration: 8s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |     <g transform="translate(0,16)"> | ||||||
|  |       <g class="am-weather-sun" | ||||||
|  |         style="-moz-animation-duration:9s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-sun;-moz-animation-timing-function:linear;-ms-animation-duration:9s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-sun;-ms-animation-timing-function:linear;-webkit-animation-duration:9s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-sun;-webkit-animation-timing-function:linear"> | ||||||
|  |         <line transform="translate(0,9)" y2="3" stroke="#ffa500" stroke-linecap="round" stroke-width="2" fifll="none" /> | ||||||
|  |         <g transform="rotate(45)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(90)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(135)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="scale(-1)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(225)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(-90)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(-45)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <circle r="5" fill="#ffa500" stroke="#ffa500" stroke-width="2" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-cloud-3" | ||||||
|  |       style="-moz-animation-duration:3s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-cloud-2;-moz-animation-timing-function:linear;-webkit-animation-duration:3s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-cloud-2;-webkit-animation-timing-function:linear"> | ||||||
|  |       <path transform="translate(-20,-11)" | ||||||
|  |         d="m47.7 35.4c0-4.6-3.7-8.2-8.2-8.2-1 0-1.9 0.2-2.8 0.5-0.3-3.4-3.1-6.2-6.6-6.2-3.7 0-6.7 3-6.7 6.7 0 0.8 0.2 1.6 0.4 2.3-0.3-0.1-0.7-0.1-1-0.1-3.7 0-6.7 3-6.7 6.7 0 3.6 2.9 6.6 6.5 6.7h17.2c4.4-0.5 7.9-4 7.9-8.4z" | ||||||
|  |         fill="#57a0ee" stroke="#fff" stroke-linejoin="round" stroke-width="1.2" /> | ||||||
|  |     </g> | ||||||
|  |     <g transform="translate(-20,-10) rotate(10,-245.89,217.31)" fill="none" stroke="#91c0f8" stroke-dasharray="4, 7" stroke-linecap="round" | ||||||
|  |       stroke-width="2"> | ||||||
|  |       <line class="am-weather-rain-1" transform="translate(-6,1)" y2="8" | ||||||
|  |         style="-moz-animation-duration:8s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-rain;-moz-animation-timing-function:linear;-ms-animation-duration:8s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-rain;-ms-animation-timing-function:linear;-webkit-animation-duration:8s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-rain;-webkit-animation-timing-function:linear" /> | ||||||
|  |       <line class="am-weather-rain-2" transform="translate(0,-1)" y2="8" | ||||||
|  |         style="-moz-animation-delay:0.25s;-moz-animation-duration:8s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-rain;-moz-animation-timing-function:linear;-ms-animation-delay:0.25s;-ms-animation-duration:8s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-rain;-ms-animation-timing-function:linear;-webkit-animation-delay:0.25s;-webkit-animation-duration:8s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-rain;-webkit-animation-timing-function:linear" /> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 8.8 KiB | 
							
								
								
									
										256
									
								
								public/weather-ico/rainy-2-night.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,256 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** CLOUDS | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-cloud-2 { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: translate(2px, 0px); | ||||||
|  |           -moz-transform: translate(2px, 0px); | ||||||
|  |           -ms-transform: translate(2px, 0px); | ||||||
|  |           transform: translate(2px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-cloud-2 { | ||||||
|  |         -webkit-animation-name: am-weather-cloud-2; | ||||||
|  |         -moz-animation-name: am-weather-cloud-2; | ||||||
|  |         animation-name: am-weather-cloud-2; | ||||||
|  |         -webkit-animation-duration: 3s; | ||||||
|  |         -moz-animation-duration: 3s; | ||||||
|  |         animation-duration: 3s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** MOON | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-moon { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: rotate(15deg); | ||||||
|  |           -moz-transform: rotate(15deg); | ||||||
|  |           -ms-transform: rotate(15deg); | ||||||
|  |           transform: rotate(15deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon { | ||||||
|  |         -webkit-animation-name: am-weather-moon; | ||||||
|  |         -moz-animation-name: am-weather-moon; | ||||||
|  |         -ms-animation-name: am-weather-moon; | ||||||
|  |         animation-name: am-weather-moon; | ||||||
|  |         -webkit-animation-duration: 6s; | ||||||
|  |         -moz-animation-duration: 6s; | ||||||
|  |         -ms-animation-duration: 6s; | ||||||
|  |         animation-duration: 6s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |         -webkit-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         -moz-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         -ms-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-moon-star-1 { | ||||||
|  |         0% { | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon-star-1 { | ||||||
|  |         -webkit-animation-name: am-weather-moon-star-1; | ||||||
|  |         -moz-animation-name: am-weather-moon-star-1; | ||||||
|  |         -ms-animation-name: am-weather-moon-star-1; | ||||||
|  |         animation-name: am-weather-moon-star-1; | ||||||
|  |         -webkit-animation-delay: 3s; | ||||||
|  |         -moz-animation-delay: 3s; | ||||||
|  |         -ms-animation-delay: 3s; | ||||||
|  |         animation-delay: 3s; | ||||||
|  |         -webkit-animation-duration: 5s; | ||||||
|  |         -moz-animation-duration: 5s; | ||||||
|  |         -ms-animation-duration: 5s; | ||||||
|  |         animation-duration: 5s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: 1; | ||||||
|  |         -moz-animation-iteration-count: 1; | ||||||
|  |         -ms-animation-iteration-count: 1; | ||||||
|  |         animation-iteration-count: 1; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-moon-star-2 { | ||||||
|  |         0% { | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon-star-2 { | ||||||
|  |         -webkit-animation-name: am-weather-moon-star-2; | ||||||
|  |         -moz-animation-name: am-weather-moon-star-2; | ||||||
|  |         -ms-animation-name: am-weather-moon-star-2; | ||||||
|  |         animation-name: am-weather-moon-star-2; | ||||||
|  |         -webkit-animation-delay: 5s; | ||||||
|  |         -moz-animation-delay: 5s; | ||||||
|  |         -ms-animation-delay: 5s; | ||||||
|  |         animation-delay: 5s; | ||||||
|  |         -webkit-animation-duration: 4s; | ||||||
|  |         -moz-animation-duration: 4s; | ||||||
|  |         -ms-animation-duration: 4s; | ||||||
|  |         animation-duration: 4s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: 1; | ||||||
|  |         -moz-animation-iteration-count: 1; | ||||||
|  |         -ms-animation-iteration-count: 1; | ||||||
|  |         animation-iteration-count: 1; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** RAIN | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-rain { | ||||||
|  |         0% { | ||||||
|  |           stroke-dashoffset: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           stroke-dashoffset: -100; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-rain-1 { | ||||||
|  |         -webkit-animation-name: am-weather-rain; | ||||||
|  |         -moz-animation-name: am-weather-rain; | ||||||
|  |         -ms-animation-name: am-weather-rain; | ||||||
|  |         animation-name: am-weather-rain; | ||||||
|  |         -webkit-animation-duration: 8s; | ||||||
|  |         -moz-animation-duration: 8s; | ||||||
|  |         -ms-animation-duration: 8s; | ||||||
|  |         animation-duration: 8s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-rain-2 { | ||||||
|  |         -webkit-animation-name: am-weather-rain; | ||||||
|  |         -moz-animation-name: am-weather-rain; | ||||||
|  |         -ms-animation-name: am-weather-rain; | ||||||
|  |         animation-name: am-weather-rain; | ||||||
|  |         -webkit-animation-delay: 0.25s; | ||||||
|  |         -moz-animation-delay: 0.25s; | ||||||
|  |         -ms-animation-delay: 0.25s; | ||||||
|  |         animation-delay: 0.25s; | ||||||
|  |         -webkit-animation-duration: 8s; | ||||||
|  |         -moz-animation-duration: 8s; | ||||||
|  |         -ms-animation-duration: 8s; | ||||||
|  |         animation-duration: 8s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g class="layer" transform="translate(16,-2)"> | ||||||
|  |     <g transform="matrix(.8 0 0 .8 16 4)"> | ||||||
|  |       <g class="am-weather-moon-star-1" | ||||||
|  |         style="-moz-animation-delay:3s;-moz-animation-duration:5s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-1;-moz-animation-timing-function:linear;-ms-animation-delay:3s;-ms-animation-duration:5s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-1;-ms-animation-timing-function:linear;-webkit-animation-delay:3s;-webkit-animation-duration:5s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-1;-webkit-animation-timing-function:linear"> | ||||||
|  |         <polygon points="4 4 3.3 5.2 2.7 4 1.5 3.3 2.7 2.7 3.3 1.5 4 2.7 5.2 3.3" fill="#ffa500" | ||||||
|  |           stroke-miterlimit="10" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-moon-star-2" | ||||||
|  |         style="-moz-animation-delay:5s;-moz-animation-duration:4s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-2;-moz-animation-timing-function:linear;-ms-animation-delay:5s;-ms-animation-duration:4s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-2;-ms-animation-timing-function:linear;-webkit-animation-delay:5s;-webkit-animation-duration:4s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-2;-webkit-animation-timing-function:linear"> | ||||||
|  |         <polygon transform="translate(20,10)" points="4 4 3.3 5.2 2.7 4 1.5 3.3 2.7 2.7 3.3 1.5 4 2.7 5.2 3.3" | ||||||
|  |           fill="#ffa500" stroke-miterlimit="10" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-moon" | ||||||
|  |         style="-moz-animation-duration:6s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-moon;-moz-animation-timing-function:linear;-moz-transform-origin:12.5px 15.15px 0;-ms-animation-duration:6s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-moon;-ms-animation-timing-function:linear;-ms-transform-origin:12.5px 15.15px 0;-webkit-animation-duration:6s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-moon;-webkit-animation-timing-function:linear;-webkit-transform-origin:12.5px 15.15px 0"> | ||||||
|  |         <path | ||||||
|  |           d="m14.5 13.2c0-3.7 2-6.9 5-8.7-1.5-0.9-3.2-1.3-5-1.3-5.5 0-10 4.5-10 10s4.5 10 10 10c1.8 0 3.5-0.5 5-1.3-3-1.7-5-5-5-8.7z" | ||||||
|  |           fill="#ffa500" stroke="#ffa500" stroke-linejoin="round" stroke-width="2" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-cloud-3" | ||||||
|  |       style="-moz-animation-duration:3s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-cloud-2;-moz-animation-timing-function:linear;-webkit-animation-duration:3s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-cloud-2;-webkit-animation-timing-function:linear"> | ||||||
|  |       <path transform="translate(-20,-11)" | ||||||
|  |         d="m47.7 35.4c0-4.6-3.7-8.2-8.2-8.2-1 0-1.9 0.2-2.8 0.5-0.3-3.4-3.1-6.2-6.6-6.2-3.7 0-6.7 3-6.7 6.7 0 0.8 0.2 1.6 0.4 2.3-0.3-0.1-0.7-0.1-1-0.1-3.7 0-6.7 3-6.7 6.7 0 3.6 2.9 6.6 6.5 6.7h17.2c4.4-0.5 7.9-4 7.9-8.4z" | ||||||
|  |         fill="#57a0ee" stroke="#fff" stroke-linejoin="round" stroke-width="1.2" /> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-rain-2" transform="translate(-20,-10) rotate(10,34,46)" fill="none" stroke="#91c0f8" | ||||||
|  |       stroke-dasharray="4, 7" stroke-linecap="round" stroke-width="2"> | ||||||
|  |       <line class="am-weather-rain-1" transform="translate(-6,1)" x1="34" x2="34" y1="46" y2="54" | ||||||
|  |         style="-moz-animation-duration:8s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-rain;-moz-animation-timing-function:linear;-ms-animation-duration:8s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-rain;-ms-animation-timing-function:linear;-webkit-animation-duration:8s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-rain;-webkit-animation-timing-function:linear" /> | ||||||
|  |       <line class="am-weather-rain-2" transform="translate(0,-1)" x1="34" x2="34" y1="46" y2="54" | ||||||
|  |         style="-moz-animation-delay:0.25s;-moz-animation-duration:8s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-rain;-moz-animation-timing-function:linear;-ms-animation-delay:0.25s;-ms-animation-duration:8s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-rain;-ms-animation-timing-function:linear;-webkit-animation-delay:0.25s;-webkit-animation-duration:8s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-rain;-webkit-animation-timing-function:linear" /> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 11 KiB | 
							
								
								
									
										206
									
								
								public/weather-ico/rainy-3-day.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,206 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <filter id="blur" x="-.24684" y="-.22892" width="1.4937" height="1.5576"> | ||||||
|  |       <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |       <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |       <feComponentTransfer> | ||||||
|  |         <feFuncA slope="0.05" type="linear" /> | ||||||
|  |       </feComponentTransfer> | ||||||
|  |       <feMerge> | ||||||
|  |         <feMergeNode /> | ||||||
|  |         <feMergeNode in="SourceGraphic" /> | ||||||
|  |       </feMerge> | ||||||
|  |     </filter> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** SUN | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-sun { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: rotate(360deg); | ||||||
|  |           -moz-transform: rotate(360deg); | ||||||
|  |           -ms-transform: rotate(360deg); | ||||||
|  |           transform: rotate(360deg); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-sun { | ||||||
|  |         -webkit-animation-name: am-weather-sun; | ||||||
|  |         -moz-animation-name: am-weather-sun; | ||||||
|  |         -ms-animation-name: am-weather-sun; | ||||||
|  |         animation-name: am-weather-sun; | ||||||
|  |         -webkit-animation-duration: 9s; | ||||||
|  |         -moz-animation-duration: 9s; | ||||||
|  |         -ms-animation-duration: 9s; | ||||||
|  |         animation-duration: 9s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** RAIN | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-rain { | ||||||
|  |         0% { | ||||||
|  |           stroke-dashoffset: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           stroke-dashoffset: -100; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-rain-1 { | ||||||
|  |         -webkit-animation-name: am-weather-rain; | ||||||
|  |         -moz-animation-name: am-weather-rain; | ||||||
|  |         -ms-animation-name: am-weather-rain; | ||||||
|  |         animation-name: am-weather-rain; | ||||||
|  |         -webkit-animation-duration: 8s; | ||||||
|  |         -moz-animation-duration: 8s; | ||||||
|  |         -ms-animation-duration: 8s; | ||||||
|  |         animation-duration: 8s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-rain-2 { | ||||||
|  |         -webkit-animation-name: am-weather-rain; | ||||||
|  |         -moz-animation-name: am-weather-rain; | ||||||
|  |         -ms-animation-name: am-weather-rain; | ||||||
|  |         animation-name: am-weather-rain; | ||||||
|  |         -webkit-animation-delay: 0.25s; | ||||||
|  |         -moz-animation-delay: 0.25s; | ||||||
|  |         -ms-animation-delay: 0.25s; | ||||||
|  |         animation-delay: 0.25s; | ||||||
|  |         -webkit-animation-duration: 8s; | ||||||
|  |         -moz-animation-duration: 8s; | ||||||
|  |         -ms-animation-duration: 8s; | ||||||
|  |         animation-duration: 8s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** CLOUDS | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-cloud-2 { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: translate(2px, 0px); | ||||||
|  |           -moz-transform: translate(2px, 0px); | ||||||
|  |           -ms-transform: translate(2px, 0px); | ||||||
|  |           transform: translate(2px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-cloud-2 { | ||||||
|  |         -webkit-animation-name: am-weather-cloud-2; | ||||||
|  |         -moz-animation-name: am-weather-cloud-2; | ||||||
|  |         animation-name: am-weather-cloud-2; | ||||||
|  |         -webkit-animation-duration: 3s; | ||||||
|  |         -moz-animation-duration: 3s; | ||||||
|  |         animation-duration: 3s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |     <g transform="translate(0,16)"> | ||||||
|  |       <g class="am-weather-sun" | ||||||
|  |         style="-moz-animation-duration:9s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-sun;-moz-animation-timing-function:linear;-ms-animation-duration:9s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-sun;-ms-animation-timing-function:linear;-webkit-animation-duration:9s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-sun;-webkit-animation-timing-function:linear"> | ||||||
|  |         <line transform="translate(0,9)" y2="3" stroke="#ffa500" stroke-linecap="round" stroke-width="2" fifll="none" /> | ||||||
|  |         <g transform="rotate(45)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(90)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(135)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="scale(-1)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(225)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(-90)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(-45)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <circle r="5" fill="#ffa500" stroke="#ffa500" stroke-width="2" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-cloud-3" | ||||||
|  |       style="-moz-animation-duration:3s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-cloud-2;-moz-animation-timing-function:linear;-webkit-animation-duration:3s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-cloud-2;-webkit-animation-timing-function:linear"> | ||||||
|  |       <path transform="translate(-20,-11)" | ||||||
|  |         d="m47.7 35.4c0-4.6-3.7-8.2-8.2-8.2-1 0-1.9 0.2-2.8 0.5-0.3-3.4-3.1-6.2-6.6-6.2-3.7 0-6.7 3-6.7 6.7 0 0.8 0.2 1.6 0.4 2.3-0.3-0.1-0.7-0.1-1-0.1-3.7 0-6.7 3-6.7 6.7 0 3.6 2.9 6.6 6.5 6.7h17.2c4.4-0.5 7.9-4 7.9-8.4z" | ||||||
|  |         fill="#57a0ee" stroke="#fff" stroke-linejoin="round" stroke-width="1.2" /> | ||||||
|  |     </g> | ||||||
|  |     <g transform="translate(-20,-10) rotate(10,-247.39,200.17)" fill="none" stroke="#91c0f8" stroke-dasharray="4, 4" | ||||||
|  |       stroke-linecap="round" stroke-width="2"> | ||||||
|  |       <line class="am-weather-rain-1" transform="translate(-4,1)" y2="8" | ||||||
|  |         style="-moz-animation-duration:8s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-rain;-moz-animation-timing-function:linear;-ms-animation-duration:8s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-rain;-ms-animation-timing-function:linear;-webkit-animation-duration:8s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-rain;-webkit-animation-timing-function:linear" /> | ||||||
|  |       <line class="am-weather-rain-2" transform="translate(0,-1)" y2="8" | ||||||
|  |         style="-moz-animation-delay:0.25s;-moz-animation-duration:8s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-rain;-moz-animation-timing-function:linear;-ms-animation-delay:0.25s;-ms-animation-duration:8s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-rain;-ms-animation-timing-function:linear;-webkit-animation-delay:0.25s;-webkit-animation-duration:8s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-rain;-webkit-animation-timing-function:linear" /> | ||||||
|  |       <line class="am-weather-rain-1" transform="translate(4)" y2="8" | ||||||
|  |         style="-moz-animation-duration:8s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-rain;-moz-animation-timing-function:linear;-ms-animation-duration:8s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-rain;-ms-animation-timing-function:linear;-webkit-animation-duration:8s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-rain;-webkit-animation-timing-function:linear" /> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 9.3 KiB | 
							
								
								
									
										270
									
								
								public/weather-ico/rainy-3-night.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,270 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <filter id="blur" x="-.24684" y="-.22892" width="1.4937" height="1.5576"> | ||||||
|  |       <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |       <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |       <feComponentTransfer> | ||||||
|  |         <feFuncA slope="0.05" type="linear" /> | ||||||
|  |       </feComponentTransfer> | ||||||
|  |       <feMerge> | ||||||
|  |         <feMergeNode /> | ||||||
|  |         <feMergeNode in="SourceGraphic" /> | ||||||
|  |       </feMerge> | ||||||
|  |     </filter> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** RAIN | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-rain { | ||||||
|  |         0% { | ||||||
|  |           stroke-dashoffset: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           stroke-dashoffset: -100; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-rain-1 { | ||||||
|  |         -webkit-animation-name: am-weather-rain; | ||||||
|  |         -moz-animation-name: am-weather-rain; | ||||||
|  |         -ms-animation-name: am-weather-rain; | ||||||
|  |         animation-name: am-weather-rain; | ||||||
|  |         -webkit-animation-duration: 8s; | ||||||
|  |         -moz-animation-duration: 8s; | ||||||
|  |         -ms-animation-duration: 8s; | ||||||
|  |         animation-duration: 8s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-rain-2 { | ||||||
|  |         -webkit-animation-name: am-weather-rain; | ||||||
|  |         -moz-animation-name: am-weather-rain; | ||||||
|  |         -ms-animation-name: am-weather-rain; | ||||||
|  |         animation-name: am-weather-rain; | ||||||
|  |         -webkit-animation-delay: 0.25s; | ||||||
|  |         -moz-animation-delay: 0.25s; | ||||||
|  |         -ms-animation-delay: 0.25s; | ||||||
|  |         animation-delay: 0.25s; | ||||||
|  |         -webkit-animation-duration: 8s; | ||||||
|  |         -moz-animation-duration: 8s; | ||||||
|  |         -ms-animation-duration: 8s; | ||||||
|  |         animation-duration: 8s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** MOON | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-moon { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: rotate(15deg); | ||||||
|  |           -moz-transform: rotate(15deg); | ||||||
|  |           -ms-transform: rotate(15deg); | ||||||
|  |           transform: rotate(15deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon { | ||||||
|  |         -webkit-animation-name: am-weather-moon; | ||||||
|  |         -moz-animation-name: am-weather-moon; | ||||||
|  |         -ms-animation-name: am-weather-moon; | ||||||
|  |         animation-name: am-weather-moon; | ||||||
|  |         -webkit-animation-duration: 6s; | ||||||
|  |         -moz-animation-duration: 6s; | ||||||
|  |         -ms-animation-duration: 6s; | ||||||
|  |         animation-duration: 6s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |         -webkit-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         -moz-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         -ms-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-moon-star-1 { | ||||||
|  |         0% { | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon-star-1 { | ||||||
|  |         -webkit-animation-name: am-weather-moon-star-1; | ||||||
|  |         -moz-animation-name: am-weather-moon-star-1; | ||||||
|  |         -ms-animation-name: am-weather-moon-star-1; | ||||||
|  |         animation-name: am-weather-moon-star-1; | ||||||
|  |         -webkit-animation-delay: 3s; | ||||||
|  |         -moz-animation-delay: 3s; | ||||||
|  |         -ms-animation-delay: 3s; | ||||||
|  |         animation-delay: 3s; | ||||||
|  |         -webkit-animation-duration: 5s; | ||||||
|  |         -moz-animation-duration: 5s; | ||||||
|  |         -ms-animation-duration: 5s; | ||||||
|  |         animation-duration: 5s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: 1; | ||||||
|  |         -moz-animation-iteration-count: 1; | ||||||
|  |         -ms-animation-iteration-count: 1; | ||||||
|  |         animation-iteration-count: 1; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-moon-star-2 { | ||||||
|  |         0% { | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon-star-2 { | ||||||
|  |         -webkit-animation-name: am-weather-moon-star-2; | ||||||
|  |         -moz-animation-name: am-weather-moon-star-2; | ||||||
|  |         -ms-animation-name: am-weather-moon-star-2; | ||||||
|  |         animation-name: am-weather-moon-star-2; | ||||||
|  |         -webkit-animation-delay: 5s; | ||||||
|  |         -moz-animation-delay: 5s; | ||||||
|  |         -ms-animation-delay: 5s; | ||||||
|  |         animation-delay: 5s; | ||||||
|  |         -webkit-animation-duration: 4s; | ||||||
|  |         -moz-animation-duration: 4s; | ||||||
|  |         -ms-animation-duration: 4s; | ||||||
|  |         animation-duration: 4s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: 1; | ||||||
|  |         -moz-animation-iteration-count: 1; | ||||||
|  |         -ms-animation-iteration-count: 1; | ||||||
|  |         animation-iteration-count: 1; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** CLOUDS | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-cloud-2 { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: translate(2px, 0px); | ||||||
|  |           -moz-transform: translate(2px, 0px); | ||||||
|  |           -ms-transform: translate(2px, 0px); | ||||||
|  |           transform: translate(2px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-cloud-2 { | ||||||
|  |         -webkit-animation-name: am-weather-cloud-2; | ||||||
|  |         -moz-animation-name: am-weather-cloud-2; | ||||||
|  |         animation-name: am-weather-cloud-2; | ||||||
|  |         -webkit-animation-duration: 3s; | ||||||
|  |         -moz-animation-duration: 3s; | ||||||
|  |         animation-duration: 3s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |     <g transform="matrix(.8 0 0 .8 16 4)"> | ||||||
|  |       <g class="am-weather-moon-star-1" | ||||||
|  |         style="-moz-animation-delay:3s;-moz-animation-duration:5s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-1;-moz-animation-timing-function:linear;-ms-animation-delay:3s;-ms-animation-duration:5s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-1;-ms-animation-timing-function:linear;-webkit-animation-delay:3s;-webkit-animation-duration:5s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-1;-webkit-animation-timing-function:linear"> | ||||||
|  |         <polygon points="4 4 3.3 5.2 2.7 4 1.5 3.3 2.7 2.7 3.3 1.5 4 2.7 5.2 3.3" fill="#ffa500" | ||||||
|  |           stroke-miterlimit="10" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-moon-star-2" | ||||||
|  |         style="-moz-animation-delay:5s;-moz-animation-duration:4s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-2;-moz-animation-timing-function:linear;-ms-animation-delay:5s;-ms-animation-duration:4s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-2;-ms-animation-timing-function:linear;-webkit-animation-delay:5s;-webkit-animation-duration:4s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-2;-webkit-animation-timing-function:linear"> | ||||||
|  |         <polygon transform="translate(20,10)" points="4 4 3.3 5.2 2.7 4 1.5 3.3 2.7 2.7 3.3 1.5 4 2.7 5.2 3.3" | ||||||
|  |           fill="#ffa500" stroke-miterlimit="10" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-moon" | ||||||
|  |         style="-moz-animation-duration:6s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-moon;-moz-animation-timing-function:linear;-moz-transform-origin:12.5px 15.15px 0;-ms-animation-duration:6s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-moon;-ms-animation-timing-function:linear;-ms-transform-origin:12.5px 15.15px 0;-webkit-animation-duration:6s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-moon;-webkit-animation-timing-function:linear;-webkit-transform-origin:12.5px 15.15px 0"> | ||||||
|  |         <path | ||||||
|  |           d="m14.5 13.2c0-3.7 2-6.9 5-8.7-1.5-0.9-3.2-1.3-5-1.3-5.5 0-10 4.5-10 10s4.5 10 10 10c1.8 0 3.5-0.5 5-1.3-3-1.7-5-5-5-8.7z" | ||||||
|  |           fill="#ffa500" stroke="#ffa500" stroke-linejoin="round" stroke-width="2" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-cloud-3" | ||||||
|  |       style="-moz-animation-duration:3s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-cloud-2;-moz-animation-timing-function:linear;-webkit-animation-duration:3s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-cloud-2;-webkit-animation-timing-function:linear"> | ||||||
|  |       <path transform="translate(-20,-11)" | ||||||
|  |         d="m47.7 35.4c0-4.6-3.7-8.2-8.2-8.2-1 0-1.9 0.2-2.8 0.5-0.3-3.4-3.1-6.2-6.6-6.2-3.7 0-6.7 3-6.7 6.7 0 0.8 0.2 1.6 0.4 2.3-0.3-0.1-0.7-0.1-1-0.1-3.7 0-6.7 3-6.7 6.7 0 3.6 2.9 6.6 6.5 6.7h17.2c4.4-0.5 7.9-4 7.9-8.4z" | ||||||
|  |         fill="#57a0ee" stroke="#fff" stroke-linejoin="round" stroke-width="1.2" /> | ||||||
|  |     </g> | ||||||
|  |     <g transform="translate(-20,-10) rotate(10,-247.39,200.17)" fill="none" stroke="#91c0f8" stroke-dasharray="4, 4" | ||||||
|  |       stroke-linecap="round" stroke-width="2"> | ||||||
|  |       <line class="am-weather-rain-1" transform="translate(-4,1)" y2="8" | ||||||
|  |         style="-moz-animation-duration:8s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-rain;-moz-animation-timing-function:linear;-ms-animation-duration:8s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-rain;-ms-animation-timing-function:linear;-webkit-animation-duration:8s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-rain;-webkit-animation-timing-function:linear" /> | ||||||
|  |       <line class="am-weather-rain-2" transform="translate(0,-1)" y2="8" | ||||||
|  |         style="-moz-animation-delay:0.25s;-moz-animation-duration:8s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-rain;-moz-animation-timing-function:linear;-ms-animation-delay:0.25s;-ms-animation-duration:8s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-rain;-ms-animation-timing-function:linear;-webkit-animation-delay:0.25s;-webkit-animation-duration:8s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-rain;-webkit-animation-timing-function:linear" /> | ||||||
|  |       <line class="am-weather-rain-1" transform="translate(4)" y2="8" | ||||||
|  |         style="-moz-animation-duration:8s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-rain;-moz-animation-timing-function:linear;-ms-animation-duration:8s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-rain;-ms-animation-timing-function:linear;-webkit-animation-duration:8s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-rain;-webkit-animation-timing-function:linear" /> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 12 KiB | 
							
								
								
									
										374
									
								
								public/weather-ico/scattered-thunderstorms-day.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,374 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <!-- Scattered Thunderstorms | Contributed by hsoJ95 on GitHub: https://github.com/hsoj95 --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <filter id="blur" x="-.20655" y="-.1975" width="1.403" height="1.4766"> | ||||||
|  |       <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |       <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |       <feComponentTransfer> | ||||||
|  |         <feFuncA slope="0.05" type="linear" /> | ||||||
|  |       </feComponentTransfer> | ||||||
|  |       <feMerge> | ||||||
|  |         <feMergeNode /> | ||||||
|  |         <feMergeNode in="SourceGraphic" /> | ||||||
|  |       </feMerge> | ||||||
|  |     </filter> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** CLOUDS | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-cloud-3 { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(-5px, 0px); | ||||||
|  |           -moz-transform: translate(-5px, 0px); | ||||||
|  |           -ms-transform: translate(-5px, 0px); | ||||||
|  |           transform: translate(-5px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: translate(10px, 0px); | ||||||
|  |           -moz-transform: translate(10px, 0px); | ||||||
|  |           -ms-transform: translate(10px, 0px); | ||||||
|  |           transform: translate(10px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(-5px, 0px); | ||||||
|  |           -moz-transform: translate(-5px, 0px); | ||||||
|  |           -ms-transform: translate(-5px, 0px); | ||||||
|  |           transform: translate(-5px, 0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-cloud-3 { | ||||||
|  |         -webkit-animation-name: am-weather-cloud-3; | ||||||
|  |         -moz-animation-name: am-weather-cloud-3; | ||||||
|  |         animation-name: am-weather-cloud-3; | ||||||
|  |         -webkit-animation-duration: 7s; | ||||||
|  |         -moz-animation-duration: 7s; | ||||||
|  |         animation-duration: 7s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-cloud-2 { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: translate(2px, 0px); | ||||||
|  |           -moz-transform: translate(2px, 0px); | ||||||
|  |           -ms-transform: translate(2px, 0px); | ||||||
|  |           transform: translate(2px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-cloud-2 { | ||||||
|  |         -webkit-animation-name: am-weather-cloud-2; | ||||||
|  |         -moz-animation-name: am-weather-cloud-2; | ||||||
|  |         animation-name: am-weather-cloud-2; | ||||||
|  |         -webkit-animation-duration: 3s; | ||||||
|  |         -moz-animation-duration: 3s; | ||||||
|  |         animation-duration: 3s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** SUN | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-sun { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: rotate(360deg); | ||||||
|  |           -moz-transform: rotate(360deg); | ||||||
|  |           -ms-transform: rotate(360deg); | ||||||
|  |           transform: rotate(360deg); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-sun { | ||||||
|  |         -webkit-animation-name: am-weather-sun; | ||||||
|  |         -moz-animation-name: am-weather-sun; | ||||||
|  |         -ms-animation-name: am-weather-sun; | ||||||
|  |         animation-name: am-weather-sun; | ||||||
|  |         -webkit-animation-duration: 9s; | ||||||
|  |         -moz-animation-duration: 9s; | ||||||
|  |         -ms-animation-duration: 9s; | ||||||
|  |         animation-duration: 9s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-sun-shiny { | ||||||
|  |         0% { | ||||||
|  |           stroke-dasharray: 3px 10px; | ||||||
|  |           stroke-dashoffset: 0px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           stroke-dasharray: 0.1px 10px; | ||||||
|  |           stroke-dashoffset: -1px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           stroke-dasharray: 3px 10px; | ||||||
|  |           stroke-dashoffset: 0px; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-sun-shiny line { | ||||||
|  |         -webkit-animation-name: am-weather-sun-shiny; | ||||||
|  |         -moz-animation-name: am-weather-sun-shiny; | ||||||
|  |         -ms-animation-name: am-weather-sun-shiny; | ||||||
|  |         animation-name: am-weather-sun-shiny; | ||||||
|  |         -webkit-animation-duration: 2s; | ||||||
|  |         -moz-animation-duration: 2s; | ||||||
|  |         -ms-animation-duration: 2s; | ||||||
|  |         animation-duration: 2s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** STROKE | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-stroke { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         2% { | ||||||
|  |           -webkit-transform: translate(0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.3px, 0.0px); | ||||||
|  |           transform: translate(0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         4% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         6% { | ||||||
|  |           -webkit-transform: translate(0.5px, 0.4px); | ||||||
|  |           -moz-transform: translate(0.5px, 0.4px); | ||||||
|  |           -ms-transform: translate(0.5px, 0.4px); | ||||||
|  |           transform: translate(0.5px, 0.4px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         8% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         10% { | ||||||
|  |           -webkit-transform: translate(0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.3px, 0.0px); | ||||||
|  |           transform: translate(0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         12% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         14% { | ||||||
|  |           -webkit-transform: translate(0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.3px, 0.0px); | ||||||
|  |           transform: translate(0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         16% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         18% { | ||||||
|  |           -webkit-transform: translate(0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.3px, 0.0px); | ||||||
|  |           transform: translate(0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         20% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         22% { | ||||||
|  |           -webkit-transform: translate(1px, 0.0px); | ||||||
|  |           -moz-transform: translate(1px, 0.0px); | ||||||
|  |           -ms-transform: translate(1px, 0.0px); | ||||||
|  |           transform: translate(1px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         24% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         26% { | ||||||
|  |           -webkit-transform: translate(-1px, 0.0px); | ||||||
|  |           -moz-transform: translate(-1px, 0.0px); | ||||||
|  |           -ms-transform: translate(-1px, 0.0px); | ||||||
|  |           transform: translate(-1px, 0.0px); | ||||||
|  |  | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         28% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         40% { | ||||||
|  |           fill: orange; | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         65% { | ||||||
|  |           fill: white; | ||||||
|  |           -webkit-transform: translate(-1px, 5.0px); | ||||||
|  |           -moz-transform: translate(-1px, 5.0px); | ||||||
|  |           -ms-transform: translate(-1px, 5.0px); | ||||||
|  |           transform: translate(-1px, 5.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         61% { | ||||||
|  |           fill: orange; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-stroke { | ||||||
|  |         -webkit-animation-name: am-weather-stroke; | ||||||
|  |         -moz-animation-name: am-weather-stroke; | ||||||
|  |         animation-name: am-weather-stroke; | ||||||
|  |         -webkit-animation-duration: 1.11s; | ||||||
|  |         -moz-animation-duration: 1.11s; | ||||||
|  |         animation-duration: 1.11s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g id="thunder" transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |     <g transform="translate(0,16)"> | ||||||
|  |       <g class="am-weather-sun" | ||||||
|  |         style="-moz-animation-duration:9s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-sun;-moz-animation-timing-function:linear;-ms-animation-duration:9s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-sun;-ms-animation-timing-function:linear;-webkit-animation-duration:9s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-sun;-webkit-animation-timing-function:linear"> | ||||||
|  |         <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" stroke-width="2" /> | ||||||
|  |         <g transform="rotate(45)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(90)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(135)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="scale(-1)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(225)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(-90)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(-45)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <circle r="5" fill="#ffa500" stroke="#ffa500" stroke-width="2" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-cloud-3"> | ||||||
|  |       <path transform="translate(-20,-11)" | ||||||
|  |         d="m47.7 35.4c0-4.6-3.7-8.2-8.2-8.2-1 0-1.9 0.2-2.8 0.5-0.3-3.4-3.1-6.2-6.6-6.2-3.7 0-6.7 3-6.7 6.7 0 0.8 0.2 1.6 0.4 2.3-0.3-0.1-0.7-0.1-1-0.1-3.7 0-6.7 3-6.7 6.7 0 3.6 2.9 6.6 6.5 6.7h17.2c4.4-0.5 7.9-4 7.9-8.4z" | ||||||
|  |         fill="#57a0ee" stroke="#fff" stroke-linejoin="round" stroke-width="1.2" /> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-lightning" transform="matrix(1.2,0,0,1.2,-4,28)"> | ||||||
|  |       <polygon class="am-weather-stroke" points="11.1 6.9 14.3 -2.9 20.5 -2.9 16.4 4.3 20.3 4.3 11.5 14.6 14.9 6.9" | ||||||
|  |         fill="#ffa500" stroke="#fff" | ||||||
|  |         style="-moz-animation-duration:1.11s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-stroke;-moz-animation-timing-function:linear;-webkit-animation-duration:1.11s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-stroke;-webkit-animation-timing-function:linear" /> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 13 KiB | 
							
								
								
									
										283
									
								
								public/weather-ico/scattered-thunderstorms-night.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,283 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <!-- Scattered Thunderstorms | Contributed by hsoJ95 on GitHub: https://github.com/hsoj95 --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <filter id="blur" x="-.20655" y="-.1975" width="1.403" height="1.4766"> | ||||||
|  |       <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |       <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |       <feComponentTransfer> | ||||||
|  |         <feFuncA slope="0.05" type="linear" /> | ||||||
|  |       </feComponentTransfer> | ||||||
|  |       <feMerge> | ||||||
|  |         <feMergeNode /> | ||||||
|  |         <feMergeNode in="SourceGraphic" /> | ||||||
|  |       </feMerge> | ||||||
|  |     </filter> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** CLOUDS | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-cloud-3 { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(-5px, 0px); | ||||||
|  |           -moz-transform: translate(-5px, 0px); | ||||||
|  |           -ms-transform: translate(-5px, 0px); | ||||||
|  |           transform: translate(-5px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: translate(10px, 0px); | ||||||
|  |           -moz-transform: translate(10px, 0px); | ||||||
|  |           -ms-transform: translate(10px, 0px); | ||||||
|  |           transform: translate(10px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(-5px, 0px); | ||||||
|  |           -moz-transform: translate(-5px, 0px); | ||||||
|  |           -ms-transform: translate(-5px, 0px); | ||||||
|  |           transform: translate(-5px, 0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-cloud-3 { | ||||||
|  |         -webkit-animation-name: am-weather-cloud-3; | ||||||
|  |         -moz-animation-name: am-weather-cloud-3; | ||||||
|  |         animation-name: am-weather-cloud-3; | ||||||
|  |         -webkit-animation-duration: 7s; | ||||||
|  |         -moz-animation-duration: 7s; | ||||||
|  |         animation-duration: 7s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-cloud-2 { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: translate(2px, 0px); | ||||||
|  |           -moz-transform: translate(2px, 0px); | ||||||
|  |           -ms-transform: translate(2px, 0px); | ||||||
|  |           transform: translate(2px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-cloud-2 { | ||||||
|  |         -webkit-animation-name: am-weather-cloud-2; | ||||||
|  |         -moz-animation-name: am-weather-cloud-2; | ||||||
|  |         animation-name: am-weather-cloud-2; | ||||||
|  |         -webkit-animation-duration: 3s; | ||||||
|  |         -moz-animation-duration: 3s; | ||||||
|  |         animation-duration: 3s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** STROKE | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-stroke { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         2% { | ||||||
|  |           -webkit-transform: translate(0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.3px, 0.0px); | ||||||
|  |           transform: translate(0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         4% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         6% { | ||||||
|  |           -webkit-transform: translate(0.5px, 0.4px); | ||||||
|  |           -moz-transform: translate(0.5px, 0.4px); | ||||||
|  |           -ms-transform: translate(0.5px, 0.4px); | ||||||
|  |           transform: translate(0.5px, 0.4px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         8% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         10% { | ||||||
|  |           -webkit-transform: translate(0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.3px, 0.0px); | ||||||
|  |           transform: translate(0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         12% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         14% { | ||||||
|  |           -webkit-transform: translate(0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.3px, 0.0px); | ||||||
|  |           transform: translate(0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         16% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         18% { | ||||||
|  |           -webkit-transform: translate(0.3px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.3px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.3px, 0.0px); | ||||||
|  |           transform: translate(0.3px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         20% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         22% { | ||||||
|  |           -webkit-transform: translate(1px, 0.0px); | ||||||
|  |           -moz-transform: translate(1px, 0.0px); | ||||||
|  |           -ms-transform: translate(1px, 0.0px); | ||||||
|  |           transform: translate(1px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         24% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         26% { | ||||||
|  |           -webkit-transform: translate(-1px, 0.0px); | ||||||
|  |           -moz-transform: translate(-1px, 0.0px); | ||||||
|  |           -ms-transform: translate(-1px, 0.0px); | ||||||
|  |           transform: translate(-1px, 0.0px); | ||||||
|  |  | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         28% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         40% { | ||||||
|  |           fill: orange; | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         65% { | ||||||
|  |           fill: white; | ||||||
|  |           -webkit-transform: translate(-1px, 5.0px); | ||||||
|  |           -moz-transform: translate(-1px, 5.0px); | ||||||
|  |           -ms-transform: translate(-1px, 5.0px); | ||||||
|  |           transform: translate(-1px, 5.0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         61% { | ||||||
|  |           fill: orange; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |           -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |           -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |           transform: translate(0.0px, 0.0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-stroke { | ||||||
|  |         -webkit-animation-name: am-weather-stroke; | ||||||
|  |         -moz-animation-name: am-weather-stroke; | ||||||
|  |         animation-name: am-weather-stroke; | ||||||
|  |         -webkit-animation-duration: 1.11s; | ||||||
|  |         -moz-animation-duration: 1.11s; | ||||||
|  |         animation-duration: 1.11s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g id="thunder" transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |     <g transform="matrix(.8 0 0 .8 16 4)"> | ||||||
|  |       <g class="am-weather-moon-star-1" | ||||||
|  |         style="-moz-animation-delay:3s;-moz-animation-duration:5s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-1;-moz-animation-timing-function:linear;-ms-animation-delay:3s;-ms-animation-duration:5s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-1;-ms-animation-timing-function:linear;-webkit-animation-delay:3s;-webkit-animation-duration:5s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-1;-webkit-animation-timing-function:linear"> | ||||||
|  |         <polygon points="3.3 1.5 4 2.7 5.2 3.3 4 4 3.3 5.2 2.7 4 1.5 3.3 2.7 2.7" fill="#ffa500" | ||||||
|  |           stroke-miterlimit="10" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-moon-star-2" | ||||||
|  |         style="-moz-animation-delay:5s;-moz-animation-duration:4s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-2;-moz-animation-timing-function:linear;-ms-animation-delay:5s;-ms-animation-duration:4s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-2;-ms-animation-timing-function:linear;-webkit-animation-delay:5s;-webkit-animation-duration:4s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-2;-webkit-animation-timing-function:linear"> | ||||||
|  |         <polygon transform="translate(20,10)" points="3.3 1.5 4 2.7 5.2 3.3 4 4 3.3 5.2 2.7 4 1.5 3.3 2.7 2.7" | ||||||
|  |           fill="#ffa500" stroke-miterlimit="10" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-moon" | ||||||
|  |         style="-moz-animation-duration:6s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-moon;-moz-animation-timing-function:linear;-moz-transform-origin:12.5px 15.15px 0;-ms-animation-duration:6s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-moon;-ms-animation-timing-function:linear;-ms-transform-origin:12.5px 15.15px 0;-webkit-animation-duration:6s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-moon;-webkit-animation-timing-function:linear;-webkit-transform-origin:12.5px 15.15px 0"> | ||||||
|  |         <path | ||||||
|  |           d="m14.5 13.2c0-3.7 2-6.9 5-8.7-1.5-0.9-3.2-1.3-5-1.3-5.5 0-10 4.5-10 10s4.5 10 10 10c1.8 0 3.5-0.5 5-1.3-3-1.7-5-5-5-8.7z" | ||||||
|  |           fill="#ffa500" stroke="#ffa500" stroke-linejoin="round" stroke-width="2" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-cloud-3"> | ||||||
|  |       <path transform="translate(-20,-11)" | ||||||
|  |         d="m47.7 35.4c0-4.6-3.7-8.2-8.2-8.2-1 0-1.9 0.2-2.8 0.5-0.3-3.4-3.1-6.2-6.6-6.2-3.7 0-6.7 3-6.7 6.7 0 0.8 0.2 1.6 0.4 2.3-0.3-0.1-0.7-0.1-1-0.1-3.7 0-6.7 3-6.7 6.7 0 3.6 2.9 6.6 6.5 6.7h17.2c4.4-0.5 7.9-4 7.9-8.4z" | ||||||
|  |         fill="#57a0ee" stroke="#fff" stroke-linejoin="round" stroke-width="1.2" /> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-lightning" transform="matrix(1.2,0,0,1.2,-4,28)"> | ||||||
|  |       <polygon class="am-weather-stroke" points="11.1 6.9 14.3 -2.9 20.5 -2.9 16.4 4.3 20.3 4.3 11.5 14.6 14.9 6.9" | ||||||
|  |         fill="#ffa500" stroke="#fff" | ||||||
|  |         style="-moz-animation-duration:1.11s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-stroke;-moz-animation-timing-function:linear;-webkit-animation-duration:1.11s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-stroke;-webkit-animation-timing-function:linear" /> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 11 KiB | 
							
								
								
									
										307
									
								
								public/weather-ico/severe-thunderstorm.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,307 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <!-- Severe Thunderstorm | Contributed by hsoJ95 on GitHub: https://github.com/hsoj95 --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |     <defs> | ||||||
|  |         <filter id="blur" x="-.17571" y="-.19575" width="1.3379" height="1.4959"> | ||||||
|  |             <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |             <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |             <feComponentTransfer> | ||||||
|  |                 <feFuncA slope="0.05" type="linear" /> | ||||||
|  |             </feComponentTransfer> | ||||||
|  |             <feMerge> | ||||||
|  |                 <feMergeNode /> | ||||||
|  |                 <feMergeNode in="SourceGraphic" /> | ||||||
|  |             </feMerge> | ||||||
|  |         </filter> | ||||||
|  |         <style type="text/css"> | ||||||
|  |             <![CDATA[ | ||||||
|  |             /* | ||||||
|  | ** CLOUDS | ||||||
|  | */ | ||||||
|  |             @keyframes am-weather-cloud-3 { | ||||||
|  |                 0% { | ||||||
|  |                     -webkit-transform: translate(-5px, 0px); | ||||||
|  |                     -moz-transform: translate(-5px, 0px); | ||||||
|  |                     -ms-transform: translate(-5px, 0px); | ||||||
|  |                     transform: translate(-5px, 0px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 50% { | ||||||
|  |                     -webkit-transform: translate(10px, 0px); | ||||||
|  |                     -moz-transform: translate(10px, 0px); | ||||||
|  |                     -ms-transform: translate(10px, 0px); | ||||||
|  |                     transform: translate(10px, 0px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 100% { | ||||||
|  |                     -webkit-transform: translate(-5px, 0px); | ||||||
|  |                     -moz-transform: translate(-5px, 0px); | ||||||
|  |                     -ms-transform: translate(-5px, 0px); | ||||||
|  |                     transform: translate(-5px, 0px); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             .am-weather-cloud-3 { | ||||||
|  |                 -webkit-animation-name: am-weather-cloud-3; | ||||||
|  |                 -moz-animation-name: am-weather-cloud-3; | ||||||
|  |                 animation-name: am-weather-cloud-3; | ||||||
|  |                 -webkit-animation-duration: 7s; | ||||||
|  |                 -moz-animation-duration: 7s; | ||||||
|  |                 animation-duration: 7s; | ||||||
|  |                 -webkit-animation-timing-function: linear; | ||||||
|  |                 -moz-animation-timing-function: linear; | ||||||
|  |                 animation-timing-function: linear; | ||||||
|  |                 -webkit-animation-iteration-count: infinite; | ||||||
|  |                 -moz-animation-iteration-count: infinite; | ||||||
|  |                 animation-iteration-count: infinite; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @keyframes am-weather-cloud-1 { | ||||||
|  |                 0% { | ||||||
|  |                     -webkit-transform: translate(0px, 0px); | ||||||
|  |                     -moz-transform: translate(0px, 0px); | ||||||
|  |                     -ms-transform: translate(0px, 0px); | ||||||
|  |                     transform: translate(0px, 0px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 50% { | ||||||
|  |                     -webkit-transform: translate(2px, 0px); | ||||||
|  |                     -moz-transform: translate(2px, 0px); | ||||||
|  |                     -ms-transform: translate(2px, 0px); | ||||||
|  |                     transform: translate(2px, 0px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 100% { | ||||||
|  |                     -webkit-transform: translate(0px, 0px); | ||||||
|  |                     -moz-transform: translate(0px, 0px); | ||||||
|  |                     -ms-transform: translate(0px, 0px); | ||||||
|  |                     transform: translate(0px, 0px); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             .am-weather-cloud-1 { | ||||||
|  |                 -webkit-animation-name: am-weather-cloud-1; | ||||||
|  |                 -moz-animation-name: am-weather-cloud-1; | ||||||
|  |                 animation-name: am-weather-cloud-1; | ||||||
|  |                 -webkit-animation-duration: 3s; | ||||||
|  |                 -moz-animation-duration: 3s; | ||||||
|  |                 animation-duration: 3s; | ||||||
|  |                 -webkit-animation-timing-function: linear; | ||||||
|  |                 -moz-animation-timing-function: linear; | ||||||
|  |                 animation-timing-function: linear; | ||||||
|  |                 -webkit-animation-iteration-count: infinite; | ||||||
|  |                 -moz-animation-iteration-count: infinite; | ||||||
|  |                 animation-iteration-count: infinite; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             /* | ||||||
|  | ** STROKE | ||||||
|  | */ | ||||||
|  |  | ||||||
|  |             @keyframes am-weather-stroke { | ||||||
|  |                 0% { | ||||||
|  |                     -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |                     -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |                     -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |                     transform: translate(0.0px, 0.0px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 2% { | ||||||
|  |                     -webkit-transform: translate(0.3px, 0.0px); | ||||||
|  |                     -moz-transform: translate(0.3px, 0.0px); | ||||||
|  |                     -ms-transform: translate(0.3px, 0.0px); | ||||||
|  |                     transform: translate(0.3px, 0.0px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 4% { | ||||||
|  |                     -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |                     -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |                     -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |                     transform: translate(0.0px, 0.0px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 6% { | ||||||
|  |                     -webkit-transform: translate(0.5px, 0.4px); | ||||||
|  |                     -moz-transform: translate(0.5px, 0.4px); | ||||||
|  |                     -ms-transform: translate(0.5px, 0.4px); | ||||||
|  |                     transform: translate(0.5px, 0.4px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 8% { | ||||||
|  |                     -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |                     -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |                     -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |                     transform: translate(0.0px, 0.0px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 10% { | ||||||
|  |                     -webkit-transform: translate(0.3px, 0.0px); | ||||||
|  |                     -moz-transform: translate(0.3px, 0.0px); | ||||||
|  |                     -ms-transform: translate(0.3px, 0.0px); | ||||||
|  |                     transform: translate(0.3px, 0.0px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 12% { | ||||||
|  |                     -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |                     -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |                     -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |                     transform: translate(0.0px, 0.0px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 14% { | ||||||
|  |                     -webkit-transform: translate(0.3px, 0.0px); | ||||||
|  |                     -moz-transform: translate(0.3px, 0.0px); | ||||||
|  |                     -ms-transform: translate(0.3px, 0.0px); | ||||||
|  |                     transform: translate(0.3px, 0.0px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 16% { | ||||||
|  |                     -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |                     -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |                     -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |                     transform: translate(0.0px, 0.0px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 18% { | ||||||
|  |                     -webkit-transform: translate(0.3px, 0.0px); | ||||||
|  |                     -moz-transform: translate(0.3px, 0.0px); | ||||||
|  |                     -ms-transform: translate(0.3px, 0.0px); | ||||||
|  |                     transform: translate(0.3px, 0.0px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 20% { | ||||||
|  |                     -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |                     -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |                     -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |                     transform: translate(0.0px, 0.0px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 22% { | ||||||
|  |                     -webkit-transform: translate(1px, 0.0px); | ||||||
|  |                     -moz-transform: translate(1px, 0.0px); | ||||||
|  |                     -ms-transform: translate(1px, 0.0px); | ||||||
|  |                     transform: translate(1px, 0.0px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 24% { | ||||||
|  |                     -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |                     -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |                     -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |                     transform: translate(0.0px, 0.0px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 26% { | ||||||
|  |                     -webkit-transform: translate(-1px, 0.0px); | ||||||
|  |                     -moz-transform: translate(-1px, 0.0px); | ||||||
|  |                     -ms-transform: translate(-1px, 0.0px); | ||||||
|  |                     transform: translate(-1px, 0.0px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 28% { | ||||||
|  |                     -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |                     -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |                     -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |                     transform: translate(0.0px, 0.0px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 40% { | ||||||
|  |                     fill: orange; | ||||||
|  |                     -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |                     -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |                     -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |                     transform: translate(0.0px, 0.0px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 65% { | ||||||
|  |                     fill: white; | ||||||
|  |                     -webkit-transform: translate(-1px, 5.0px); | ||||||
|  |                     -moz-transform: translate(-1px, 5.0px); | ||||||
|  |                     -ms-transform: translate(-1px, 5.0px); | ||||||
|  |                     transform: translate(-1px, 5.0px); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 61% { | ||||||
|  |                     fill: orange; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 100% { | ||||||
|  |                     -webkit-transform: translate(0.0px, 0.0px); | ||||||
|  |                     -moz-transform: translate(0.0px, 0.0px); | ||||||
|  |                     -ms-transform: translate(0.0px, 0.0px); | ||||||
|  |                     transform: translate(0.0px, 0.0px); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             .am-weather-stroke { | ||||||
|  |                 -webkit-animation-name: am-weather-stroke; | ||||||
|  |                 -moz-animation-name: am-weather-stroke; | ||||||
|  |                 animation-name: am-weather-stroke; | ||||||
|  |                 -webkit-animation-duration: 1.11s; | ||||||
|  |                 -moz-animation-duration: 1.11s; | ||||||
|  |                 animation-duration: 1.11s; | ||||||
|  |                 -webkit-animation-timing-function: linear; | ||||||
|  |                 -moz-animation-timing-function: linear; | ||||||
|  |                 animation-timing-function: linear; | ||||||
|  |                 -webkit-animation-iteration-count: infinite; | ||||||
|  |                 -moz-animation-iteration-count: infinite; | ||||||
|  |                 animation-iteration-count: infinite; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @keyframes error { | ||||||
|  |                 0% { | ||||||
|  |                     fill: #cc0000; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 50% { | ||||||
|  |                     fill: #ff0000; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 100% { | ||||||
|  |                     fill: #cc0000; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             #Shape { | ||||||
|  |                 -webkit-animation-name: error; | ||||||
|  |                 -moz-animation-name: error; | ||||||
|  |                 animation-name: error; | ||||||
|  |                 -webkit-animation-duration: 1s; | ||||||
|  |                 -moz-animation-duration: 1s; | ||||||
|  |                 animation-duration: 1s; | ||||||
|  |                 -webkit-animation-timing-function: linear; | ||||||
|  |                 -moz-animation-timing-function: linear; | ||||||
|  |                 animation-timing-function: linear; | ||||||
|  |                 -webkit-animation-iteration-count: infinite; | ||||||
|  |                 -moz-animation-iteration-count: infinite; | ||||||
|  |                 animation-iteration-count: infinite; | ||||||
|  |             } | ||||||
|  |             ]]> | ||||||
|  |         </style> | ||||||
|  |     </defs> | ||||||
|  |     <g transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |         <g class="am-weather-cloud-1" | ||||||
|  |             style="-moz-animation-duration:7s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-cloud-1;-moz-animation-timing-function:linear;-webkit-animation-duration:7s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-cloud-1;-webkit-animation-timing-function:linear"> | ||||||
|  |             <path transform="matrix(.6 0 0 .6 -10 -6)" | ||||||
|  |                 d="m47.7 35.4c0-4.6-3.7-8.2-8.2-8.2-1 0-1.9 0.2-2.8 0.5-0.3-3.4-3.1-6.2-6.6-6.2-3.7 0-6.7 3-6.7 6.7 0 0.8 0.2 1.6 0.4 2.3-0.3-0.1-0.7-0.1-1-0.1-3.7 0-6.7 3-6.7 6.7 0 3.6 2.9 6.6 6.5 6.7h17.2c4.4-0.5 7.9-4 7.9-8.4z" | ||||||
|  |                 fill="#666" stroke="#fff" stroke-linejoin="round" stroke-width="1.2" /> | ||||||
|  |         </g> | ||||||
|  |         <g class="am-weather-cloud-3"> | ||||||
|  |             <path transform="translate(-20,-11)" | ||||||
|  |                 d="m47.7 35.4c0-4.6-3.7-8.2-8.2-8.2-1 0-1.9 0.2-2.8 0.5-0.3-3.4-3.1-6.2-6.6-6.2-3.7 0-6.7 3-6.7 6.7 0 0.8 0.2 1.6 0.4 2.3-0.3-0.1-0.7-0.1-1-0.1-3.7 0-6.7 3-6.7 6.7 0 3.6 2.9 6.6 6.5 6.7h17.2c4.4-0.5 7.9-4 7.9-8.4z" | ||||||
|  |                 fill="#333" stroke="#fff" stroke-linejoin="round" stroke-width="1.2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="matrix(1.2,0,0,1.2,-4,28)"> | ||||||
|  |             <polygon class="am-weather-stroke" | ||||||
|  |                 points="11.1 6.9 14.3 -2.9 20.5 -2.9 16.4 4.3 20.3 4.3 11.5 14.6 14.9 6.9" fill="#ffa500" stroke="#fff" | ||||||
|  |                 style="-moz-animation-duration:1.11s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-stroke;-moz-animation-timing-function:linear;-webkit-animation-duration:1.11s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-stroke;-webkit-animation-timing-function:linear" /> | ||||||
|  |         </g> | ||||||
|  |         <g class="warning" transform="translate(20,30)"> | ||||||
|  |             <path | ||||||
|  |                 d="m7.7791 2.906-5.9912 10.117c-0.56283 0.95042-0.24862 2.1772 0.7018 2.74 0.30853 0.18271 0.66051 0.27911 1.0191 0.27911h11.982c1.1046 0 2-0.89543 2-2 0-0.35857-0.0964-0.71056-0.27911-1.0191l-5.9912-10.117c-0.56283-0.95042-1.7896-1.2646-2.74-0.7018-0.28918 0.17125-0.53055 0.41262-0.7018 0.7018z" | ||||||
|  |                 fill="#c00" /> | ||||||
|  |             <path d="m9.5 10.5v-5" stroke="#fff" stroke-linecap="round" stroke-width="1.5" /> | ||||||
|  |             <circle cx="9.5" cy="13" r="1" fill="#fff" /> | ||||||
|  |         </g> | ||||||
|  |     </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 13 KiB | 
							
								
								
									
										241
									
								
								public/weather-ico/snowy-1-day.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,241 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <filter id="blur" x="-.20655" y="-.23099" width="1.403" height="1.5634"> | ||||||
|  |       <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |       <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |       <feComponentTransfer> | ||||||
|  |         <feFuncA slope="0.05" type="linear" /> | ||||||
|  |       </feComponentTransfer> | ||||||
|  |       <feMerge> | ||||||
|  |         <feMergeNode /> | ||||||
|  |         <feMergeNode in="SourceGraphic" /> | ||||||
|  |       </feMerge> | ||||||
|  |     </filter> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** CLOUDS | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-cloud-2 { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: translate(2px, 0px); | ||||||
|  |           -moz-transform: translate(2px, 0px); | ||||||
|  |           -ms-transform: translate(2px, 0px); | ||||||
|  |           transform: translate(2px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-cloud-2 { | ||||||
|  |         -webkit-animation-name: am-weather-cloud-2; | ||||||
|  |         -moz-animation-name: am-weather-cloud-2; | ||||||
|  |         animation-name: am-weather-cloud-2; | ||||||
|  |         -webkit-animation-duration: 3s; | ||||||
|  |         -moz-animation-duration: 3s; | ||||||
|  |         animation-duration: 3s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** SUN | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-sun { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: rotate(360deg); | ||||||
|  |           -moz-transform: rotate(360deg); | ||||||
|  |           -ms-transform: rotate(360deg); | ||||||
|  |           transform: rotate(360deg); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-sun { | ||||||
|  |         -webkit-animation-name: am-weather-sun; | ||||||
|  |         -moz-animation-name: am-weather-sun; | ||||||
|  |         -ms-animation-name: am-weather-sun; | ||||||
|  |         animation-name: am-weather-sun; | ||||||
|  |         -webkit-animation-duration: 9s; | ||||||
|  |         -moz-animation-duration: 9s; | ||||||
|  |         -ms-animation-duration: 9s; | ||||||
|  |         animation-duration: 9s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-sun-shiny { | ||||||
|  |         0% { | ||||||
|  |           stroke-dasharray: 3px 10px; | ||||||
|  |           stroke-dashoffset: 0px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           stroke-dasharray: 0.1px 10px; | ||||||
|  |           stroke-dashoffset: -1px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           stroke-dasharray: 3px 10px; | ||||||
|  |           stroke-dashoffset: 0px; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-sun-shiny line { | ||||||
|  |         -webkit-animation-name: am-weather-sun-shiny; | ||||||
|  |         -moz-animation-name: am-weather-sun-shiny; | ||||||
|  |         -ms-animation-name: am-weather-sun-shiny; | ||||||
|  |         animation-name: am-weather-sun-shiny; | ||||||
|  |         -webkit-animation-duration: 2s; | ||||||
|  |         -moz-animation-duration: 2s; | ||||||
|  |         -ms-animation-duration: 2s; | ||||||
|  |         animation-duration: 2s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** SNOW | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-snow { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translateX(0) translateY(0); | ||||||
|  |           -moz-transform: translateX(0) translateY(0); | ||||||
|  |           -ms-transform: translateX(0) translateY(0); | ||||||
|  |           transform: translateX(0) translateY(0); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         33.33% { | ||||||
|  |           -webkit-transform: translateX(-1.2px) translateY(2px); | ||||||
|  |           -moz-transform: translateX(-1.2px) translateY(2px); | ||||||
|  |           -ms-transform: translateX(-1.2px) translateY(2px); | ||||||
|  |           transform: translateX(-1.2px) translateY(2px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         66.66% { | ||||||
|  |           -webkit-transform: translateX(1.4px) translateY(4px); | ||||||
|  |           -moz-transform: translateX(1.4px) translateY(4px); | ||||||
|  |           -ms-transform: translateX(1.4px) translateY(4px); | ||||||
|  |           transform: translateX(1.4px) translateY(4px); | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translateX(-1.6px) translateY(6px); | ||||||
|  |           -moz-transform: translateX(-1.6px) translateY(6px); | ||||||
|  |           -ms-transform: translateX(-1.6px) translateY(6px); | ||||||
|  |           transform: translateX(-1.6px) translateY(6px); | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-snow-1 { | ||||||
|  |         -webkit-animation-name: am-weather-snow; | ||||||
|  |         -moz-animation-name: am-weather-snow; | ||||||
|  |         -ms-animation-name: am-weather-snow; | ||||||
|  |         animation-name: am-weather-snow; | ||||||
|  |         -webkit-animation-duration: 2s; | ||||||
|  |         -moz-animation-duration: 2s; | ||||||
|  |         -ms-animation-duration: 2s; | ||||||
|  |         animation-duration: 2s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |     <g transform="translate(0,16)"> | ||||||
|  |       <g class="am-weather-sun" transform="translate(0,16)" | ||||||
|  |         style="-moz-animation-duration:9s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-sun;-moz-animation-timing-function:linear;-ms-animation-duration:9s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-sun;-ms-animation-timing-function:linear;-webkit-animation-duration:9s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-sun;-webkit-animation-timing-function:linear"> | ||||||
|  |         <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" stroke-width="2" /> | ||||||
|  |         <g transform="rotate(45)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(90)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(135)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="scale(-1)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(225)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(-90)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(-45)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <circle r="5" fill="#ffa500" stroke="#ffa500" stroke-width="2" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-cloud-3" | ||||||
|  |       style="-moz-animation-duration:3s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-cloud-2;-moz-animation-timing-function:linear;-webkit-animation-duration:3s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-cloud-2;-webkit-animation-timing-function:linear"> | ||||||
|  |       <path transform="translate(-20,-11)" | ||||||
|  |         d="m47.7 35.4c0-4.6-3.7-8.2-8.2-8.2-1 0-1.9 0.2-2.8 0.5-0.3-3.4-3.1-6.2-6.6-6.2-3.7 0-6.7 3-6.7 6.7 0 0.8 0.2 1.6 0.4 2.3-0.3-0.1-0.7-0.1-1-0.1-3.7 0-6.7 3-6.7 6.7 0 3.6 2.9 6.6 6.5 6.7h17.2c4.4-0.5 7.9-4 7.9-8.4z" | ||||||
|  |         fill="#57a0ee" stroke="#fff" stroke-linejoin="round" stroke-width="1.2" /> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-snow-1" | ||||||
|  |       style="-moz-animation-duration:2s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-snow;-moz-animation-timing-function:linear;-ms-animation-duration:2s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-snow;-ms-animation-timing-function:linear;-webkit-animation-duration:2s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-snow;-webkit-animation-timing-function:linear"> | ||||||
|  |       <g transform="translate(12,28)" fill="none" stroke="#57a0ee" stroke-linecap="round"> | ||||||
|  |         <line transform="translate(0,9)" y1="-2.5" y2="2.5" stroke-width="1.2" /> | ||||||
|  |         <line transform="rotate(45,-10.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |         <line transform="rotate(90,-4.5,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |         <line transform="rotate(135,-1.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 9.6 KiB | 
							
								
								
									
										269
									
								
								public/weather-ico/snowy-1-night.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,269 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <filter id="blur" x="-.20655" y="-.23099" width="1.403" height="1.5634"> | ||||||
|  |       <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |       <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |       <feComponentTransfer> | ||||||
|  |         <feFuncA slope="0.05" type="linear" /> | ||||||
|  |       </feComponentTransfer> | ||||||
|  |       <feMerge> | ||||||
|  |         <feMergeNode /> | ||||||
|  |         <feMergeNode in="SourceGraphic" /> | ||||||
|  |       </feMerge> | ||||||
|  |     </filter> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** CLOUDS | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-cloud-2 { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: translate(2px, 0px); | ||||||
|  |           -moz-transform: translate(2px, 0px); | ||||||
|  |           -ms-transform: translate(2px, 0px); | ||||||
|  |           transform: translate(2px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-cloud-2 { | ||||||
|  |         -webkit-animation-name: am-weather-cloud-2; | ||||||
|  |         -moz-animation-name: am-weather-cloud-2; | ||||||
|  |         animation-name: am-weather-cloud-2; | ||||||
|  |         -webkit-animation-duration: 3s; | ||||||
|  |         -moz-animation-duration: 3s; | ||||||
|  |         animation-duration: 3s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** MOON | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-moon { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: rotate(15deg); | ||||||
|  |           -moz-transform: rotate(15deg); | ||||||
|  |           -ms-transform: rotate(15deg); | ||||||
|  |           transform: rotate(15deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon { | ||||||
|  |         -webkit-animation-name: am-weather-moon; | ||||||
|  |         -moz-animation-name: am-weather-moon; | ||||||
|  |         -ms-animation-name: am-weather-moon; | ||||||
|  |         animation-name: am-weather-moon; | ||||||
|  |         -webkit-animation-duration: 6s; | ||||||
|  |         -moz-animation-duration: 6s; | ||||||
|  |         -ms-animation-duration: 6s; | ||||||
|  |         animation-duration: 6s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |         -webkit-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         -moz-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         -ms-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-moon-star-1 { | ||||||
|  |         0% { | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon-star-1 { | ||||||
|  |         -webkit-animation-name: am-weather-moon-star-1; | ||||||
|  |         -moz-animation-name: am-weather-moon-star-1; | ||||||
|  |         -ms-animation-name: am-weather-moon-star-1; | ||||||
|  |         animation-name: am-weather-moon-star-1; | ||||||
|  |         -webkit-animation-delay: 3s; | ||||||
|  |         -moz-animation-delay: 3s; | ||||||
|  |         -ms-animation-delay: 3s; | ||||||
|  |         animation-delay: 3s; | ||||||
|  |         -webkit-animation-duration: 5s; | ||||||
|  |         -moz-animation-duration: 5s; | ||||||
|  |         -ms-animation-duration: 5s; | ||||||
|  |         animation-duration: 5s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: 1; | ||||||
|  |         -moz-animation-iteration-count: 1; | ||||||
|  |         -ms-animation-iteration-count: 1; | ||||||
|  |         animation-iteration-count: 1; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-moon-star-2 { | ||||||
|  |         0% { | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon-star-2 { | ||||||
|  |         -webkit-animation-name: am-weather-moon-star-2; | ||||||
|  |         -moz-animation-name: am-weather-moon-star-2; | ||||||
|  |         -ms-animation-name: am-weather-moon-star-2; | ||||||
|  |         animation-name: am-weather-moon-star-2; | ||||||
|  |         -webkit-animation-delay: 5s; | ||||||
|  |         -moz-animation-delay: 5s; | ||||||
|  |         -ms-animation-delay: 5s; | ||||||
|  |         animation-delay: 5s; | ||||||
|  |         -webkit-animation-duration: 4s; | ||||||
|  |         -moz-animation-duration: 4s; | ||||||
|  |         -ms-animation-duration: 4s; | ||||||
|  |         animation-duration: 4s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: 1; | ||||||
|  |         -moz-animation-iteration-count: 1; | ||||||
|  |         -ms-animation-iteration-count: 1; | ||||||
|  |         animation-iteration-count: 1; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** SNOW | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-snow { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translateX(0) translateY(0); | ||||||
|  |           -moz-transform: translateX(0) translateY(0); | ||||||
|  |           -ms-transform: translateX(0) translateY(0); | ||||||
|  |           transform: translateX(0) translateY(0); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         33.33% { | ||||||
|  |           -webkit-transform: translateX(-1.2px) translateY(2px); | ||||||
|  |           -moz-transform: translateX(-1.2px) translateY(2px); | ||||||
|  |           -ms-transform: translateX(-1.2px) translateY(2px); | ||||||
|  |           transform: translateX(-1.2px) translateY(2px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         66.66% { | ||||||
|  |           -webkit-transform: translateX(1.4px) translateY(4px); | ||||||
|  |           -moz-transform: translateX(1.4px) translateY(4px); | ||||||
|  |           -ms-transform: translateX(1.4px) translateY(4px); | ||||||
|  |           transform: translateX(1.4px) translateY(4px); | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translateX(-1.6px) translateY(6px); | ||||||
|  |           -moz-transform: translateX(-1.6px) translateY(6px); | ||||||
|  |           -ms-transform: translateX(-1.6px) translateY(6px); | ||||||
|  |           transform: translateX(-1.6px) translateY(6px); | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-snow-1 { | ||||||
|  |         -webkit-animation-name: am-weather-snow; | ||||||
|  |         -moz-animation-name: am-weather-snow; | ||||||
|  |         -ms-animation-name: am-weather-snow; | ||||||
|  |         animation-name: am-weather-snow; | ||||||
|  |         -webkit-animation-duration: 2s; | ||||||
|  |         -moz-animation-duration: 2s; | ||||||
|  |         -ms-animation-duration: 2s; | ||||||
|  |         animation-duration: 2s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |     <g transform="matrix(.8 0 0 .8 16 4)"> | ||||||
|  |       <g class="am-weather-moon-star-1" | ||||||
|  |         style="-moz-animation-delay:3s;-moz-animation-duration:5s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-1;-moz-animation-timing-function:linear;-ms-animation-delay:3s;-ms-animation-duration:5s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-1;-ms-animation-timing-function:linear;-webkit-animation-delay:3s;-webkit-animation-duration:5s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-1;-webkit-animation-timing-function:linear"> | ||||||
|  |         <polygon points="4 4 3.3 5.2 2.7 4 1.5 3.3 2.7 2.7 3.3 1.5 4 2.7 5.2 3.3" fill="#ffa500" | ||||||
|  |           stroke-miterlimit="10" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-moon-star-2" | ||||||
|  |         style="-moz-animation-delay:5s;-moz-animation-duration:4s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-2;-moz-animation-timing-function:linear;-ms-animation-delay:5s;-ms-animation-duration:4s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-2;-ms-animation-timing-function:linear;-webkit-animation-delay:5s;-webkit-animation-duration:4s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-2;-webkit-animation-timing-function:linear"> | ||||||
|  |         <polygon transform="translate(20,10)" points="4 4 3.3 5.2 2.7 4 1.5 3.3 2.7 2.7 3.3 1.5 4 2.7 5.2 3.3" | ||||||
|  |           fill="#ffa500" stroke-miterlimit="10" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-moon" | ||||||
|  |         style="-moz-animation-duration:6s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-moon;-moz-animation-timing-function:linear;-moz-transform-origin:12.5px 15.15px 0;-ms-animation-duration:6s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-moon;-ms-animation-timing-function:linear;-ms-transform-origin:12.5px 15.15px 0;-webkit-animation-duration:6s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-moon;-webkit-animation-timing-function:linear;-webkit-transform-origin:12.5px 15.15px 0"> | ||||||
|  |         <path | ||||||
|  |           d="m14.5 13.2c0-3.7 2-6.9 5-8.7-1.5-0.9-3.2-1.3-5-1.3-5.5 0-10 4.5-10 10s4.5 10 10 10c1.8 0 3.5-0.5 5-1.3-3-1.7-5-5-5-8.7z" | ||||||
|  |           fill="#ffa500" stroke="#ffa500" stroke-linejoin="round" stroke-width="2" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-cloud-3" | ||||||
|  |       style="-moz-animation-duration:3s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-cloud-2;-moz-animation-timing-function:linear;-webkit-animation-duration:3s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-cloud-2;-webkit-animation-timing-function:linear"> | ||||||
|  |       <path transform="translate(-20,-11)" | ||||||
|  |         d="m47.7 35.4c0-4.6-3.7-8.2-8.2-8.2-1 0-1.9 0.2-2.8 0.5-0.3-3.4-3.1-6.2-6.6-6.2-3.7 0-6.7 3-6.7 6.7 0 0.8 0.2 1.6 0.4 2.3-0.3-0.1-0.7-0.1-1-0.1-3.7 0-6.7 3-6.7 6.7 0 3.6 2.9 6.6 6.5 6.7h17.2c4.4-0.5 7.9-4 7.9-8.4z" | ||||||
|  |         fill="#57a0ee" stroke="#fff" stroke-linejoin="round" stroke-width="1.2" /> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-snow-1" | ||||||
|  |       style="-moz-animation-duration:2s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-snow;-moz-animation-timing-function:linear;-ms-animation-duration:2s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-snow;-ms-animation-timing-function:linear;-webkit-animation-duration:2s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-snow;-webkit-animation-timing-function:linear"> | ||||||
|  |       <g transform="translate(12,28)" fill="none" stroke="#57a0ee" stroke-linecap="round"> | ||||||
|  |         <line transform="translate(0,9)" y1="-2.5" y2="2.5" stroke-width="1.2" /> | ||||||
|  |         <line transform="rotate(45,-10.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |         <line transform="rotate(90,-4.5,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |         <line transform="rotate(135,-1.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 11 KiB | 
							
								
								
									
										273
									
								
								public/weather-ico/snowy-2-day.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,273 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <filter id="blur" x="-.20655" y="-.23099" width="1.403" height="1.5634"> | ||||||
|  |       <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |       <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |       <feComponentTransfer> | ||||||
|  |         <feFuncA slope="0.05" type="linear" /> | ||||||
|  |       </feComponentTransfer> | ||||||
|  |       <feMerge> | ||||||
|  |         <feMergeNode /> | ||||||
|  |         <feMergeNode in="SourceGraphic" /> | ||||||
|  |       </feMerge> | ||||||
|  |     </filter> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** CLOUDS | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-cloud-2 { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: translate(2px, 0px); | ||||||
|  |           -moz-transform: translate(2px, 0px); | ||||||
|  |           -ms-transform: translate(2px, 0px); | ||||||
|  |           transform: translate(2px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-cloud-2 { | ||||||
|  |         -webkit-animation-name: am-weather-cloud-2; | ||||||
|  |         -moz-animation-name: am-weather-cloud-2; | ||||||
|  |         animation-name: am-weather-cloud-2; | ||||||
|  |         -webkit-animation-duration: 3s; | ||||||
|  |         -moz-animation-duration: 3s; | ||||||
|  |         animation-duration: 3s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** SUN | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-sun { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: rotate(360deg); | ||||||
|  |           -moz-transform: rotate(360deg); | ||||||
|  |           -ms-transform: rotate(360deg); | ||||||
|  |           transform: rotate(360deg); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-sun { | ||||||
|  |         -webkit-animation-name: am-weather-sun; | ||||||
|  |         -moz-animation-name: am-weather-sun; | ||||||
|  |         -ms-animation-name: am-weather-sun; | ||||||
|  |         animation-name: am-weather-sun; | ||||||
|  |         -webkit-animation-duration: 9s; | ||||||
|  |         -moz-animation-duration: 9s; | ||||||
|  |         -ms-animation-duration: 9s; | ||||||
|  |         animation-duration: 9s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-sun-shiny { | ||||||
|  |         0% { | ||||||
|  |           stroke-dasharray: 3px 10px; | ||||||
|  |           stroke-dashoffset: 0px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           stroke-dasharray: 0.1px 10px; | ||||||
|  |           stroke-dashoffset: -1px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           stroke-dasharray: 3px 10px; | ||||||
|  |           stroke-dashoffset: 0px; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-sun-shiny line { | ||||||
|  |         -webkit-animation-name: am-weather-sun-shiny; | ||||||
|  |         -moz-animation-name: am-weather-sun-shiny; | ||||||
|  |         -ms-animation-name: am-weather-sun-shiny; | ||||||
|  |         animation-name: am-weather-sun-shiny; | ||||||
|  |         -webkit-animation-duration: 2s; | ||||||
|  |         -moz-animation-duration: 2s; | ||||||
|  |         -ms-animation-duration: 2s; | ||||||
|  |         animation-duration: 2s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** SNOW | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-snow { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translateX(0) translateY(0); | ||||||
|  |           -moz-transform: translateX(0) translateY(0); | ||||||
|  |           -ms-transform: translateX(0) translateY(0); | ||||||
|  |           transform: translateX(0) translateY(0); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         33.33% { | ||||||
|  |           -webkit-transform: translateX(-1.2px) translateY(2px); | ||||||
|  |           -moz-transform: translateX(-1.2px) translateY(2px); | ||||||
|  |           -ms-transform: translateX(-1.2px) translateY(2px); | ||||||
|  |           transform: translateX(-1.2px) translateY(2px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         66.66% { | ||||||
|  |           -webkit-transform: translateX(1.4px) translateY(4px); | ||||||
|  |           -moz-transform: translateX(1.4px) translateY(4px); | ||||||
|  |           -ms-transform: translateX(1.4px) translateY(4px); | ||||||
|  |           transform: translateX(1.4px) translateY(4px); | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translateX(-1.6px) translateY(6px); | ||||||
|  |           -moz-transform: translateX(-1.6px) translateY(6px); | ||||||
|  |           -ms-transform: translateX(-1.6px) translateY(6px); | ||||||
|  |           transform: translateX(-1.6px) translateY(6px); | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-snow-1 { | ||||||
|  |         -webkit-animation-name: am-weather-snow; | ||||||
|  |         -moz-animation-name: am-weather-snow; | ||||||
|  |         -ms-animation-name: am-weather-snow; | ||||||
|  |         animation-name: am-weather-snow; | ||||||
|  |         -webkit-animation-duration: 2s; | ||||||
|  |         -moz-animation-duration: 2s; | ||||||
|  |         -ms-animation-duration: 2s; | ||||||
|  |         animation-duration: 2s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-snow-2 { | ||||||
|  |         -webkit-animation-name: am-weather-snow; | ||||||
|  |         -moz-animation-name: am-weather-snow; | ||||||
|  |         -ms-animation-name: am-weather-snow; | ||||||
|  |         animation-name: am-weather-snow; | ||||||
|  |         -webkit-animation-delay: 1.2s; | ||||||
|  |         -moz-animation-delay: 1.2s; | ||||||
|  |         -ms-animation-delay: 1.2s; | ||||||
|  |         animation-delay: 1.2s; | ||||||
|  |         -webkit-animation-duration: 2s; | ||||||
|  |         -moz-animation-duration: 2s; | ||||||
|  |         -ms-animation-duration: 2s; | ||||||
|  |         animation-duration: 2s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |     <g transform="translate(0,16)"> | ||||||
|  |       <g class="am-weather-sun" | ||||||
|  |         style="-moz-animation-duration:9s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-sun;-moz-animation-timing-function:linear;-ms-animation-duration:9s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-sun;-ms-animation-timing-function:linear;-webkit-animation-duration:9s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-sun;-webkit-animation-timing-function:linear"> | ||||||
|  |         <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" stroke-width="2" /> | ||||||
|  |         <g transform="rotate(45)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(90)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(135)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="scale(-1)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(225)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(-90)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |         <g transform="rotate(-45)"> | ||||||
|  |           <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |             stroke-width="2" /> | ||||||
|  |         </g> | ||||||
|  |       </g> | ||||||
|  |       <circle r="5" fill="#ffa500" stroke="#ffa500" stroke-width="2" /> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-cloud-3" | ||||||
|  |       style="-moz-animation-duration:3s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-cloud-2;-moz-animation-timing-function:linear;-webkit-animation-duration:3s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-cloud-2;-webkit-animation-timing-function:linear"> | ||||||
|  |       <path transform="translate(-20,-11)" | ||||||
|  |         d="m47.7 35.4c0-4.6-3.7-8.2-8.2-8.2-1 0-1.9 0.2-2.8 0.5-0.3-3.4-3.1-6.2-6.6-6.2-3.7 0-6.7 3-6.7 6.7 0 0.8 0.2 1.6 0.4 2.3-0.3-0.1-0.7-0.1-1-0.1-3.7 0-6.7 3-6.7 6.7 0 3.6 2.9 6.6 6.5 6.7h17.2c4.4-0.5 7.9-4 7.9-8.4z" | ||||||
|  |         fill="#57a0ee" stroke="#fff" stroke-linejoin="round" stroke-width="1.2" /> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-snow-1" | ||||||
|  |       style="-moz-animation-duration:2s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-snow;-moz-animation-timing-function:linear;-ms-animation-duration:2s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-snow;-ms-animation-timing-function:linear;-webkit-animation-duration:2s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-snow;-webkit-animation-timing-function:linear"> | ||||||
|  |       <g transform="translate(7,28)" fill="none" stroke="#57a0ee" stroke-linecap="round"> | ||||||
|  |         <line transform="translate(0,9)" y1="-2.5" y2="2.5" stroke-width="1.2" /> | ||||||
|  |         <line transform="rotate(45,-10.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |         <line transform="rotate(90,-4.5,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |         <line transform="rotate(135,-1.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-snow-2" | ||||||
|  |       style="-moz-animation-delay:1.2s;-moz-animation-duration:2s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-snow;-moz-animation-timing-function:linear;-ms-animation-delay:1.2s;-ms-animation-duration:2s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-snow;-ms-animation-timing-function:linear;-webkit-animation-delay:1.2s;-webkit-animation-duration:2s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-snow;-webkit-animation-timing-function:linear"> | ||||||
|  |       <g transform="translate(16,28)" fill="none" stroke="#57a0ee" stroke-linecap="round"> | ||||||
|  |         <line transform="translate(0,9)" y1="-2.5" y2="2.5" stroke-width="1.2" /> | ||||||
|  |         <line transform="rotate(45,-10.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |         <line transform="rotate(90,-4.5,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |         <line transform="rotate(135,-1.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 11 KiB | 
							
								
								
									
										301
									
								
								public/weather-ico/snowy-2-night.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,301 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs> | ||||||
|  |     <filter id="blur" x="-.20655" y="-.23099" width="1.403" height="1.5634"> | ||||||
|  |       <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |       <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |       <feComponentTransfer> | ||||||
|  |         <feFuncA slope="0.05" type="linear" /> | ||||||
|  |       </feComponentTransfer> | ||||||
|  |       <feMerge> | ||||||
|  |         <feMergeNode /> | ||||||
|  |         <feMergeNode in="SourceGraphic" /> | ||||||
|  |       </feMerge> | ||||||
|  |     </filter> | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <![CDATA[ | ||||||
|  |       /* | ||||||
|  | ** CLOUDS | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-cloud-2 { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: translate(2px, 0px); | ||||||
|  |           -moz-transform: translate(2px, 0px); | ||||||
|  |           -ms-transform: translate(2px, 0px); | ||||||
|  |           transform: translate(2px, 0px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translate(0px, 0px); | ||||||
|  |           -moz-transform: translate(0px, 0px); | ||||||
|  |           -ms-transform: translate(0px, 0px); | ||||||
|  |           transform: translate(0px, 0px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-cloud-2 { | ||||||
|  |         -webkit-animation-name: am-weather-cloud-2; | ||||||
|  |         -moz-animation-name: am-weather-cloud-2; | ||||||
|  |         animation-name: am-weather-cloud-2; | ||||||
|  |         -webkit-animation-duration: 3s; | ||||||
|  |         -moz-animation-duration: 3s; | ||||||
|  |         animation-duration: 3s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** MOON | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-moon { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         50% { | ||||||
|  |           -webkit-transform: rotate(15deg); | ||||||
|  |           -moz-transform: rotate(15deg); | ||||||
|  |           -ms-transform: rotate(15deg); | ||||||
|  |           transform: rotate(15deg); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: rotate(0deg); | ||||||
|  |           -moz-transform: rotate(0deg); | ||||||
|  |           -ms-transform: rotate(0deg); | ||||||
|  |           transform: rotate(0deg); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon { | ||||||
|  |         -webkit-animation-name: am-weather-moon; | ||||||
|  |         -moz-animation-name: am-weather-moon; | ||||||
|  |         -ms-animation-name: am-weather-moon; | ||||||
|  |         animation-name: am-weather-moon; | ||||||
|  |         -webkit-animation-duration: 6s; | ||||||
|  |         -moz-animation-duration: 6s; | ||||||
|  |         -ms-animation-duration: 6s; | ||||||
|  |         animation-duration: 6s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |         -webkit-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         -moz-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         -ms-transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |         transform-origin: 12.5px 15.15px 0; | ||||||
|  |         /* TODO FF CENTER ISSUE */ | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-moon-star-1 { | ||||||
|  |         0% { | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon-star-1 { | ||||||
|  |         -webkit-animation-name: am-weather-moon-star-1; | ||||||
|  |         -moz-animation-name: am-weather-moon-star-1; | ||||||
|  |         -ms-animation-name: am-weather-moon-star-1; | ||||||
|  |         animation-name: am-weather-moon-star-1; | ||||||
|  |         -webkit-animation-delay: 3s; | ||||||
|  |         -moz-animation-delay: 3s; | ||||||
|  |         -ms-animation-delay: 3s; | ||||||
|  |         animation-delay: 3s; | ||||||
|  |         -webkit-animation-duration: 5s; | ||||||
|  |         -moz-animation-duration: 5s; | ||||||
|  |         -ms-animation-duration: 5s; | ||||||
|  |         animation-duration: 5s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: 1; | ||||||
|  |         -moz-animation-iteration-count: 1; | ||||||
|  |         -ms-animation-iteration-count: 1; | ||||||
|  |         animation-iteration-count: 1; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @keyframes am-weather-moon-star-2 { | ||||||
|  |         0% { | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-moon-star-2 { | ||||||
|  |         -webkit-animation-name: am-weather-moon-star-2; | ||||||
|  |         -moz-animation-name: am-weather-moon-star-2; | ||||||
|  |         -ms-animation-name: am-weather-moon-star-2; | ||||||
|  |         animation-name: am-weather-moon-star-2; | ||||||
|  |         -webkit-animation-delay: 5s; | ||||||
|  |         -moz-animation-delay: 5s; | ||||||
|  |         -ms-animation-delay: 5s; | ||||||
|  |         animation-delay: 5s; | ||||||
|  |         -webkit-animation-duration: 4s; | ||||||
|  |         -moz-animation-duration: 4s; | ||||||
|  |         -ms-animation-duration: 4s; | ||||||
|  |         animation-duration: 4s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: 1; | ||||||
|  |         -moz-animation-iteration-count: 1; | ||||||
|  |         -ms-animation-iteration-count: 1; | ||||||
|  |         animation-iteration-count: 1; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* | ||||||
|  | ** SNOW | ||||||
|  | */ | ||||||
|  |       @keyframes am-weather-snow { | ||||||
|  |         0% { | ||||||
|  |           -webkit-transform: translateX(0) translateY(0); | ||||||
|  |           -moz-transform: translateX(0) translateY(0); | ||||||
|  |           -ms-transform: translateX(0) translateY(0); | ||||||
|  |           transform: translateX(0) translateY(0); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         33.33% { | ||||||
|  |           -webkit-transform: translateX(-1.2px) translateY(2px); | ||||||
|  |           -moz-transform: translateX(-1.2px) translateY(2px); | ||||||
|  |           -ms-transform: translateX(-1.2px) translateY(2px); | ||||||
|  |           transform: translateX(-1.2px) translateY(2px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         66.66% { | ||||||
|  |           -webkit-transform: translateX(1.4px) translateY(4px); | ||||||
|  |           -moz-transform: translateX(1.4px) translateY(4px); | ||||||
|  |           -ms-transform: translateX(1.4px) translateY(4px); | ||||||
|  |           transform: translateX(1.4px) translateY(4px); | ||||||
|  |           opacity: 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         100% { | ||||||
|  |           -webkit-transform: translateX(-1.6px) translateY(6px); | ||||||
|  |           -moz-transform: translateX(-1.6px) translateY(6px); | ||||||
|  |           -ms-transform: translateX(-1.6px) translateY(6px); | ||||||
|  |           transform: translateX(-1.6px) translateY(6px); | ||||||
|  |           opacity: 0; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-snow-1 { | ||||||
|  |         -webkit-animation-name: am-weather-snow; | ||||||
|  |         -moz-animation-name: am-weather-snow; | ||||||
|  |         -ms-animation-name: am-weather-snow; | ||||||
|  |         animation-name: am-weather-snow; | ||||||
|  |         -webkit-animation-duration: 2s; | ||||||
|  |         -moz-animation-duration: 2s; | ||||||
|  |         -ms-animation-duration: 2s; | ||||||
|  |         animation-duration: 2s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .am-weather-snow-2 { | ||||||
|  |         -webkit-animation-name: am-weather-snow; | ||||||
|  |         -moz-animation-name: am-weather-snow; | ||||||
|  |         -ms-animation-name: am-weather-snow; | ||||||
|  |         animation-name: am-weather-snow; | ||||||
|  |         -webkit-animation-delay: 1.2s; | ||||||
|  |         -moz-animation-delay: 1.2s; | ||||||
|  |         -ms-animation-delay: 1.2s; | ||||||
|  |         animation-delay: 1.2s; | ||||||
|  |         -webkit-animation-duration: 2s; | ||||||
|  |         -moz-animation-duration: 2s; | ||||||
|  |         -ms-animation-duration: 2s; | ||||||
|  |         animation-duration: 2s; | ||||||
|  |         -webkit-animation-timing-function: linear; | ||||||
|  |         -moz-animation-timing-function: linear; | ||||||
|  |         -ms-animation-timing-function: linear; | ||||||
|  |         animation-timing-function: linear; | ||||||
|  |         -webkit-animation-iteration-count: infinite; | ||||||
|  |         -moz-animation-iteration-count: infinite; | ||||||
|  |         -ms-animation-iteration-count: infinite; | ||||||
|  |         animation-iteration-count: infinite; | ||||||
|  |       } | ||||||
|  |       ]]> | ||||||
|  |     </style> | ||||||
|  |   </defs> | ||||||
|  |   <g transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |     <g transform="matrix(.8 0 0 .8 16 4)"> | ||||||
|  |       <g class="am-weather-moon-star-1" | ||||||
|  |         style="-moz-animation-delay:3s;-moz-animation-duration:5s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-1;-moz-animation-timing-function:linear;-ms-animation-delay:3s;-ms-animation-duration:5s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-1;-ms-animation-timing-function:linear;-webkit-animation-delay:3s;-webkit-animation-duration:5s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-1;-webkit-animation-timing-function:linear"> | ||||||
|  |         <polygon points="4 4 3.3 5.2 2.7 4 1.5 3.3 2.7 2.7 3.3 1.5 4 2.7 5.2 3.3" fill="#ffa500" | ||||||
|  |           stroke-miterlimit="10" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-moon-star-2" | ||||||
|  |         style="-moz-animation-delay:5s;-moz-animation-duration:4s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-2;-moz-animation-timing-function:linear;-ms-animation-delay:5s;-ms-animation-duration:4s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-2;-ms-animation-timing-function:linear;-webkit-animation-delay:5s;-webkit-animation-duration:4s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-2;-webkit-animation-timing-function:linear"> | ||||||
|  |         <polygon transform="translate(20,10)" points="4 4 3.3 5.2 2.7 4 1.5 3.3 2.7 2.7 3.3 1.5 4 2.7 5.2 3.3" | ||||||
|  |           fill="#ffa500" stroke-miterlimit="10" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-moon" | ||||||
|  |         style="-moz-animation-duration:6s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-moon;-moz-animation-timing-function:linear;-moz-transform-origin:12.5px 15.15px 0;-ms-animation-duration:6s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-moon;-ms-animation-timing-function:linear;-ms-transform-origin:12.5px 15.15px 0;-webkit-animation-duration:6s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-moon;-webkit-animation-timing-function:linear;-webkit-transform-origin:12.5px 15.15px 0"> | ||||||
|  |         <path | ||||||
|  |           d="m14.5 13.2c0-3.7 2-6.9 5-8.7-1.5-0.9-3.2-1.3-5-1.3-5.5 0-10 4.5-10 10s4.5 10 10 10c1.8 0 3.5-0.5 5-1.3-3-1.7-5-5-5-8.7z" | ||||||
|  |           fill="#ffa500" stroke="#ffa500" stroke-linejoin="round" stroke-width="2" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-cloud-3" | ||||||
|  |       style="-moz-animation-duration:3s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-cloud-2;-moz-animation-timing-function:linear;-webkit-animation-duration:3s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-cloud-2;-webkit-animation-timing-function:linear"> | ||||||
|  |       <path transform="translate(-20,-11)" | ||||||
|  |         d="m47.7 35.4c0-4.6-3.7-8.2-8.2-8.2-1 0-1.9 0.2-2.8 0.5-0.3-3.4-3.1-6.2-6.6-6.2-3.7 0-6.7 3-6.7 6.7 0 0.8 0.2 1.6 0.4 2.3-0.3-0.1-0.7-0.1-1-0.1-3.7 0-6.7 3-6.7 6.7 0 3.6 2.9 6.6 6.5 6.7h17.2c4.4-0.5 7.9-4 7.9-8.4z" | ||||||
|  |         fill="#57a0ee" stroke="#fff" stroke-linejoin="round" stroke-width="1.2" /> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-snow-1" | ||||||
|  |       style="-moz-animation-duration:2s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-snow;-moz-animation-timing-function:linear;-ms-animation-duration:2s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-snow;-ms-animation-timing-function:linear;-webkit-animation-duration:2s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-snow;-webkit-animation-timing-function:linear"> | ||||||
|  |       <g transform="translate(7,28)" fill="none" stroke="#57a0ee" stroke-linecap="round"> | ||||||
|  |         <line transform="translate(0,9)" y1="-2.5" y2="2.5" stroke-width="1.2" /> | ||||||
|  |         <line transform="rotate(45,-10.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |         <line transform="rotate(90,-4.5,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |         <line transform="rotate(135,-1.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |     <g class="am-weather-snow-2" | ||||||
|  |       style="-moz-animation-delay:1.2s;-moz-animation-duration:2s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-snow;-moz-animation-timing-function:linear;-ms-animation-delay:1.2s;-ms-animation-duration:2s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-snow;-ms-animation-timing-function:linear;-webkit-animation-delay:1.2s;-webkit-animation-duration:2s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-snow;-webkit-animation-timing-function:linear"> | ||||||
|  |       <g transform="translate(16,28)" fill="none" stroke="#57a0ee" stroke-linecap="round"> | ||||||
|  |         <line transform="translate(0,9)" y1="-2.5" y2="2.5" stroke-width="1.2" /> | ||||||
|  |         <line transform="rotate(45,-10.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |         <line transform="rotate(90,-4.5,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |         <line transform="rotate(135,-1.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 13 KiB | 
							
								
								
									
										334
									
								
								public/weather-ico/snowy-3-day.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,334 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |    <defs> | ||||||
|  |       <filter id="blur" x="-.24684" y="-.26897" width="1.4937" height="1.6759"> | ||||||
|  |          <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |          <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |          <feComponentTransfer> | ||||||
|  |             <feFuncA slope="0.05" type="linear" /> | ||||||
|  |          </feComponentTransfer> | ||||||
|  |          <feMerge> | ||||||
|  |             <feMergeNode /> | ||||||
|  |             <feMergeNode in="SourceGraphic" /> | ||||||
|  |          </feMerge> | ||||||
|  |       </filter> | ||||||
|  |       <style type="text/css"> | ||||||
|  |          <![CDATA[ | ||||||
|  |          /* | ||||||
|  | ** CLOUDS | ||||||
|  | */ | ||||||
|  |          @keyframes am-weather-cloud-2 { | ||||||
|  |             0% { | ||||||
|  |                -webkit-transform: translate(0px, 0px); | ||||||
|  |                -moz-transform: translate(0px, 0px); | ||||||
|  |                -ms-transform: translate(0px, 0px); | ||||||
|  |                transform: translate(0px, 0px); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             50% { | ||||||
|  |                -webkit-transform: translate(2px, 0px); | ||||||
|  |                -moz-transform: translate(2px, 0px); | ||||||
|  |                -ms-transform: translate(2px, 0px); | ||||||
|  |                transform: translate(2px, 0px); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             100% { | ||||||
|  |                -webkit-transform: translate(0px, 0px); | ||||||
|  |                -moz-transform: translate(0px, 0px); | ||||||
|  |                -ms-transform: translate(0px, 0px); | ||||||
|  |                transform: translate(0px, 0px); | ||||||
|  |             } | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          .am-weather-cloud-2 { | ||||||
|  |             -webkit-animation-name: am-weather-cloud-2; | ||||||
|  |             -moz-animation-name: am-weather-cloud-2; | ||||||
|  |             animation-name: am-weather-cloud-2; | ||||||
|  |             -webkit-animation-duration: 3s; | ||||||
|  |             -moz-animation-duration: 3s; | ||||||
|  |             animation-duration: 3s; | ||||||
|  |             -webkit-animation-timing-function: linear; | ||||||
|  |             -moz-animation-timing-function: linear; | ||||||
|  |             animation-timing-function: linear; | ||||||
|  |             -webkit-animation-iteration-count: infinite; | ||||||
|  |             -moz-animation-iteration-count: infinite; | ||||||
|  |             animation-iteration-count: infinite; | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          /* | ||||||
|  | ** SUN | ||||||
|  | */ | ||||||
|  |          @keyframes am-weather-sun { | ||||||
|  |             0% { | ||||||
|  |                -webkit-transform: rotate(0deg); | ||||||
|  |                -moz-transform: rotate(0deg); | ||||||
|  |                -ms-transform: rotate(0deg); | ||||||
|  |                transform: rotate(0deg); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             100% { | ||||||
|  |                -webkit-transform: rotate(360deg); | ||||||
|  |                -moz-transform: rotate(360deg); | ||||||
|  |                -ms-transform: rotate(360deg); | ||||||
|  |                transform: rotate(360deg); | ||||||
|  |             } | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          .am-weather-sun { | ||||||
|  |             -webkit-animation-name: am-weather-sun; | ||||||
|  |             -moz-animation-name: am-weather-sun; | ||||||
|  |             -ms-animation-name: am-weather-sun; | ||||||
|  |             animation-name: am-weather-sun; | ||||||
|  |             -webkit-animation-duration: 9s; | ||||||
|  |             -moz-animation-duration: 9s; | ||||||
|  |             -ms-animation-duration: 9s; | ||||||
|  |             animation-duration: 9s; | ||||||
|  |             -webkit-animation-timing-function: linear; | ||||||
|  |             -moz-animation-timing-function: linear; | ||||||
|  |             -ms-animation-timing-function: linear; | ||||||
|  |             animation-timing-function: linear; | ||||||
|  |             -webkit-animation-iteration-count: infinite; | ||||||
|  |             -moz-animation-iteration-count: infinite; | ||||||
|  |             -ms-animation-iteration-count: infinite; | ||||||
|  |             animation-iteration-count: infinite; | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          @keyframes am-weather-sun-shiny { | ||||||
|  |             0% { | ||||||
|  |                stroke-dasharray: 3px 10px; | ||||||
|  |                stroke-dashoffset: 0px; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             50% { | ||||||
|  |                stroke-dasharray: 0.1px 10px; | ||||||
|  |                stroke-dashoffset: -1px; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             100% { | ||||||
|  |                stroke-dasharray: 3px 10px; | ||||||
|  |                stroke-dashoffset: 0px; | ||||||
|  |             } | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          .am-weather-sun-shiny line { | ||||||
|  |             -webkit-animation-name: am-weather-sun-shiny; | ||||||
|  |             -moz-animation-name: am-weather-sun-shiny; | ||||||
|  |             -ms-animation-name: am-weather-sun-shiny; | ||||||
|  |             animation-name: am-weather-sun-shiny; | ||||||
|  |             -webkit-animation-duration: 2s; | ||||||
|  |             -moz-animation-duration: 2s; | ||||||
|  |             -ms-animation-duration: 2s; | ||||||
|  |             animation-duration: 2s; | ||||||
|  |             -webkit-animation-timing-function: linear; | ||||||
|  |             -moz-animation-timing-function: linear; | ||||||
|  |             -ms-animation-timing-function: linear; | ||||||
|  |             animation-timing-function: linear; | ||||||
|  |             -webkit-animation-iteration-count: infinite; | ||||||
|  |             -moz-animation-iteration-count: infinite; | ||||||
|  |             -ms-animation-iteration-count: infinite; | ||||||
|  |             animation-iteration-count: infinite; | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          /* | ||||||
|  | ** SNOW | ||||||
|  | */ | ||||||
|  |          @keyframes am-weather-snow { | ||||||
|  |             0% { | ||||||
|  |                -webkit-transform: translateX(0) translateY(0); | ||||||
|  |                -moz-transform: translateX(0) translateY(0); | ||||||
|  |                -ms-transform: translateX(0) translateY(0); | ||||||
|  |                transform: translateX(0) translateY(0); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             33.33% { | ||||||
|  |                -webkit-transform: translateX(-1.2px) translateY(2px); | ||||||
|  |                -moz-transform: translateX(-1.2px) translateY(2px); | ||||||
|  |                -ms-transform: translateX(-1.2px) translateY(2px); | ||||||
|  |                transform: translateX(-1.2px) translateY(2px); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             66.66% { | ||||||
|  |                -webkit-transform: translateX(1.4px) translateY(4px); | ||||||
|  |                -moz-transform: translateX(1.4px) translateY(4px); | ||||||
|  |                -ms-transform: translateX(1.4px) translateY(4px); | ||||||
|  |                transform: translateX(1.4px) translateY(4px); | ||||||
|  |                opacity: 1; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             100% { | ||||||
|  |                -webkit-transform: translateX(-1.6px) translateY(6px); | ||||||
|  |                -moz-transform: translateX(-1.6px) translateY(6px); | ||||||
|  |                -ms-transform: translateX(-1.6px) translateY(6px); | ||||||
|  |                transform: translateX(-1.6px) translateY(6px); | ||||||
|  |                opacity: 0; | ||||||
|  |             } | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          @keyframes am-weather-snow-reverse { | ||||||
|  |             0% { | ||||||
|  |                -webkit-transform: translateX(0) translateY(0); | ||||||
|  |                -moz-transform: translateX(0) translateY(0); | ||||||
|  |                -ms-transform: translateX(0) translateY(0); | ||||||
|  |                transform: translateX(0) translateY(0); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             33.33% { | ||||||
|  |                -webkit-transform: translateX(1.2px) translateY(2px); | ||||||
|  |                -moz-transform: translateX(1.2px) translateY(2px); | ||||||
|  |                -ms-transform: translateX(1.2px) translateY(2px); | ||||||
|  |                transform: translateX(1.2px) translateY(2px); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             66.66% { | ||||||
|  |                -webkit-transform: translateX(-1.4px) translateY(4px); | ||||||
|  |                -moz-transform: translateX(-1.4px) translateY(4px); | ||||||
|  |                -ms-transform: translateX(-1.4px) translateY(4px); | ||||||
|  |                transform: translateX(-1.4px) translateY(4px); | ||||||
|  |                opacity: 1; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             100% { | ||||||
|  |                -webkit-transform: translateX(1.6px) translateY(6px); | ||||||
|  |                -moz-transform: translateX(1.6px) translateY(6px); | ||||||
|  |                -ms-transform: translateX(1.6px) translateY(6px); | ||||||
|  |                transform: translateX(1.6px) translateY(6px); | ||||||
|  |                opacity: 0; | ||||||
|  |             } | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          .am-weather-snow-1 { | ||||||
|  |             -webkit-animation-name: am-weather-snow; | ||||||
|  |             -moz-animation-name: am-weather-snow; | ||||||
|  |             -ms-animation-name: am-weather-snow; | ||||||
|  |             animation-name: am-weather-snow; | ||||||
|  |             -webkit-animation-duration: 2s; | ||||||
|  |             -moz-animation-duration: 2s; | ||||||
|  |             -ms-animation-duration: 2s; | ||||||
|  |             animation-duration: 2s; | ||||||
|  |             -webkit-animation-timing-function: linear; | ||||||
|  |             -moz-animation-timing-function: linear; | ||||||
|  |             -ms-animation-timing-function: linear; | ||||||
|  |             animation-timing-function: linear; | ||||||
|  |             -webkit-animation-iteration-count: infinite; | ||||||
|  |             -moz-animation-iteration-count: infinite; | ||||||
|  |             -ms-animation-iteration-count: infinite; | ||||||
|  |             animation-iteration-count: infinite; | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          .am-weather-snow-2 { | ||||||
|  |             -webkit-animation-name: am-weather-snow; | ||||||
|  |             -moz-animation-name: am-weather-snow; | ||||||
|  |             -ms-animation-name: am-weather-snow; | ||||||
|  |             animation-name: am-weather-snow; | ||||||
|  |             -webkit-animation-delay: 1.2s; | ||||||
|  |             -moz-animation-delay: 1.2s; | ||||||
|  |             -ms-animation-delay: 1.2s; | ||||||
|  |             animation-delay: 1.2s; | ||||||
|  |             -webkit-animation-duration: 2s; | ||||||
|  |             -moz-animation-duration: 2s; | ||||||
|  |             -ms-animation-duration: 2s; | ||||||
|  |             animation-duration: 2s; | ||||||
|  |             -webkit-animation-timing-function: linear; | ||||||
|  |             -moz-animation-timing-function: linear; | ||||||
|  |             -ms-animation-timing-function: linear; | ||||||
|  |             animation-timing-function: linear; | ||||||
|  |             -webkit-animation-iteration-count: infinite; | ||||||
|  |             -moz-animation-iteration-count: infinite; | ||||||
|  |             -ms-animation-iteration-count: infinite; | ||||||
|  |             animation-iteration-count: infinite; | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          .am-weather-snow-3 { | ||||||
|  |             -webkit-animation-name: am-weather-snow-reverse; | ||||||
|  |             -moz-animation-name: am-weather-snow-reverse; | ||||||
|  |             -ms-animation-name: am-weather-snow-reverse; | ||||||
|  |             animation-name: am-weather-snow-reverse; | ||||||
|  |             -webkit-animation-duration: 2s; | ||||||
|  |             -moz-animation-duration: 2s; | ||||||
|  |             -ms-animation-duration: 2s; | ||||||
|  |             animation-duration: 2s; | ||||||
|  |             -webkit-animation-timing-function: linear; | ||||||
|  |             -moz-animation-timing-function: linear; | ||||||
|  |             -ms-animation-timing-function: linear; | ||||||
|  |             animation-timing-function: linear; | ||||||
|  |             -webkit-animation-iteration-count: infinite; | ||||||
|  |             -moz-animation-iteration-count: infinite; | ||||||
|  |             -ms-animation-iteration-count: infinite; | ||||||
|  |             animation-iteration-count: infinite; | ||||||
|  |          } | ||||||
|  |          ]]> | ||||||
|  |       </style> | ||||||
|  |    </defs> | ||||||
|  |    <g transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |       <g transform="translate(0,16)"> | ||||||
|  |          <g class="am-weather-sun" | ||||||
|  |             style="-moz-animation-duration:9s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-sun;-moz-animation-timing-function:linear;-ms-animation-duration:9s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-sun;-ms-animation-timing-function:linear;-webkit-animation-duration:9s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-sun;-webkit-animation-timing-function:linear"> | ||||||
|  |             <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |                stroke-width="2" /> | ||||||
|  |             <g transform="rotate(45)"> | ||||||
|  |                <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |                   stroke-width="2" /> | ||||||
|  |             </g> | ||||||
|  |             <g transform="rotate(90)"> | ||||||
|  |                <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |                   stroke-width="2" /> | ||||||
|  |             </g> | ||||||
|  |             <g transform="rotate(135)"> | ||||||
|  |                <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |                   stroke-width="2" /> | ||||||
|  |             </g> | ||||||
|  |             <g transform="scale(-1)"> | ||||||
|  |                <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |                   stroke-width="2" /> | ||||||
|  |             </g> | ||||||
|  |             <g transform="rotate(225)"> | ||||||
|  |                <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |                   stroke-width="2" /> | ||||||
|  |             </g> | ||||||
|  |             <g transform="rotate(-90)"> | ||||||
|  |                <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |                   stroke-width="2" /> | ||||||
|  |             </g> | ||||||
|  |             <g transform="rotate(-45)"> | ||||||
|  |                <line transform="translate(0,9)" y2="3" fill="none" stroke="#ffa500" stroke-linecap="round" | ||||||
|  |                   stroke-width="2" /> | ||||||
|  |             </g> | ||||||
|  |          </g> | ||||||
|  |          <circle r="5" fill="#ffa500" stroke="#ffa500" stroke-width="2" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-cloud-2" | ||||||
|  |          style="-moz-animation-duration:3s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-cloud-2;-moz-animation-timing-function:linear;-webkit-animation-duration:3s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-cloud-2;-webkit-animation-timing-function:linear"> | ||||||
|  |          <path transform="translate(-20,-11)" | ||||||
|  |             d="m47.7 35.4c0-4.6-3.7-8.2-8.2-8.2-1 0-1.9 0.2-2.8 0.5-0.3-3.4-3.1-6.2-6.6-6.2-3.7 0-6.7 3-6.7 6.7 0 0.8 0.2 1.6 0.4 2.3-0.3-0.1-0.7-0.1-1-0.1-3.7 0-6.7 3-6.7 6.7 0 3.6 2.9 6.6 6.5 6.7h17.2c4.4-0.5 7.9-4 7.9-8.4z" | ||||||
|  |             fill="#57a0ee" stroke="#fff" stroke-linejoin="round" stroke-width="1.2" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-snow-1" | ||||||
|  |          style="-moz-animation-duration:2s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-snow;-moz-animation-timing-function:linear;-ms-animation-duration:2s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-snow;-ms-animation-timing-function:linear;-webkit-animation-duration:2s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-snow;-webkit-animation-timing-function:linear"> | ||||||
|  |          <g transform="translate(3,28)" fill="none" stroke="#57a0ee" stroke-linecap="round"> | ||||||
|  |             <line transform="translate(0,9)" y1="-2.5" y2="2.5" stroke-width="1.2" /> | ||||||
|  |             <line transform="rotate(45,-10.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |             <line transform="rotate(90,-4.5,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |             <line transform="rotate(135,-1.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |          </g> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-snow-2" | ||||||
|  |          style="-moz-animation-delay:1.2s;-moz-animation-duration:2s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-snow;-moz-animation-timing-function:linear;-ms-animation-delay:1.2s;-ms-animation-duration:2s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-snow;-ms-animation-timing-function:linear;-webkit-animation-delay:1.2s;-webkit-animation-duration:2s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-snow;-webkit-animation-timing-function:linear"> | ||||||
|  |          <g transform="translate(11,28)" fill="none" stroke="#57a0ee" stroke-linecap="round"> | ||||||
|  |             <line transform="translate(0,9)" y1="-2.5" y2="2.5" stroke-width="1.2" /> | ||||||
|  |             <line transform="rotate(45,-10.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |             <line transform="rotate(90,-4.5,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |             <line transform="rotate(135,-1.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |          </g> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-snow-3" | ||||||
|  |          style="-moz-animation-duration:2s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-snow-reverse;-moz-animation-timing-function:linear;-ms-animation-duration:2s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-snow-reverse;-ms-animation-timing-function:linear;-webkit-animation-duration:2s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-snow-reverse;-webkit-animation-timing-function:linear"> | ||||||
|  |          <g transform="translate(20,28)" fill="none" stroke="#57a0ee" stroke-linecap="round"> | ||||||
|  |             <line transform="translate(0,9)" y1="-2.5" y2="2.5" stroke-width="1.2" /> | ||||||
|  |             <line transform="rotate(45,-10.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |             <line transform="rotate(90,-4.5,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |             <line transform="rotate(135,-1.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |          </g> | ||||||
|  |       </g> | ||||||
|  |    </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 15 KiB | 
							
								
								
									
										361
									
								
								public/weather-ico/snowy-3-night.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,361 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- (c) ammap.com | SVG weather icons --> | ||||||
|  | <svg width="56" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |    <defs> | ||||||
|  |       <filter id="blur" x="-.24684" y="-.26897" width="1.4937" height="1.6759"> | ||||||
|  |          <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | ||||||
|  |          <feOffset dx="0" dy="4" result="offsetblur" /> | ||||||
|  |          <feComponentTransfer> | ||||||
|  |             <feFuncA slope="0.05" type="linear" /> | ||||||
|  |          </feComponentTransfer> | ||||||
|  |          <feMerge> | ||||||
|  |             <feMergeNode /> | ||||||
|  |             <feMergeNode in="SourceGraphic" /> | ||||||
|  |          </feMerge> | ||||||
|  |       </filter> | ||||||
|  |       <style type="text/css"> | ||||||
|  |          <![CDATA[ | ||||||
|  |          /* | ||||||
|  | ** CLOUDS | ||||||
|  | */ | ||||||
|  |          @keyframes am-weather-cloud-2 { | ||||||
|  |             0% { | ||||||
|  |                -webkit-transform: translate(0px, 0px); | ||||||
|  |                -moz-transform: translate(0px, 0px); | ||||||
|  |                -ms-transform: translate(0px, 0px); | ||||||
|  |                transform: translate(0px, 0px); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             50% { | ||||||
|  |                -webkit-transform: translate(2px, 0px); | ||||||
|  |                -moz-transform: translate(2px, 0px); | ||||||
|  |                -ms-transform: translate(2px, 0px); | ||||||
|  |                transform: translate(2px, 0px); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             100% { | ||||||
|  |                -webkit-transform: translate(0px, 0px); | ||||||
|  |                -moz-transform: translate(0px, 0px); | ||||||
|  |                -ms-transform: translate(0px, 0px); | ||||||
|  |                transform: translate(0px, 0px); | ||||||
|  |             } | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          .am-weather-cloud-2 { | ||||||
|  |             -webkit-animation-name: am-weather-cloud-2; | ||||||
|  |             -moz-animation-name: am-weather-cloud-2; | ||||||
|  |             animation-name: am-weather-cloud-2; | ||||||
|  |             -webkit-animation-duration: 3s; | ||||||
|  |             -moz-animation-duration: 3s; | ||||||
|  |             animation-duration: 3s; | ||||||
|  |             -webkit-animation-timing-function: linear; | ||||||
|  |             -moz-animation-timing-function: linear; | ||||||
|  |             animation-timing-function: linear; | ||||||
|  |             -webkit-animation-iteration-count: infinite; | ||||||
|  |             -moz-animation-iteration-count: infinite; | ||||||
|  |             animation-iteration-count: infinite; | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          /* | ||||||
|  | ** MOON | ||||||
|  | */ | ||||||
|  |          @keyframes am-weather-moon { | ||||||
|  |             0% { | ||||||
|  |                -webkit-transform: rotate(0deg); | ||||||
|  |                -moz-transform: rotate(0deg); | ||||||
|  |                -ms-transform: rotate(0deg); | ||||||
|  |                transform: rotate(0deg); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             50% { | ||||||
|  |                -webkit-transform: rotate(15deg); | ||||||
|  |                -moz-transform: rotate(15deg); | ||||||
|  |                -ms-transform: rotate(15deg); | ||||||
|  |                transform: rotate(15deg); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             100% { | ||||||
|  |                -webkit-transform: rotate(0deg); | ||||||
|  |                -moz-transform: rotate(0deg); | ||||||
|  |                -ms-transform: rotate(0deg); | ||||||
|  |                transform: rotate(0deg); | ||||||
|  |             } | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          .am-weather-moon { | ||||||
|  |             -webkit-animation-name: am-weather-moon; | ||||||
|  |             -moz-animation-name: am-weather-moon; | ||||||
|  |             -ms-animation-name: am-weather-moon; | ||||||
|  |             animation-name: am-weather-moon; | ||||||
|  |             -webkit-animation-duration: 6s; | ||||||
|  |             -moz-animation-duration: 6s; | ||||||
|  |             -ms-animation-duration: 6s; | ||||||
|  |             animation-duration: 6s; | ||||||
|  |             -webkit-animation-timing-function: linear; | ||||||
|  |             -moz-animation-timing-function: linear; | ||||||
|  |             -ms-animation-timing-function: linear; | ||||||
|  |             animation-timing-function: linear; | ||||||
|  |             -webkit-animation-iteration-count: infinite; | ||||||
|  |             -moz-animation-iteration-count: infinite; | ||||||
|  |             -ms-animation-iteration-count: infinite; | ||||||
|  |             animation-iteration-count: infinite; | ||||||
|  |             -webkit-transform-origin: 12.5px 15.15px 0; | ||||||
|  |             /* TODO FF CENTER ISSUE */ | ||||||
|  |             -moz-transform-origin: 12.5px 15.15px 0; | ||||||
|  |             /* TODO FF CENTER ISSUE */ | ||||||
|  |             -ms-transform-origin: 12.5px 15.15px 0; | ||||||
|  |             /* TODO FF CENTER ISSUE */ | ||||||
|  |             transform-origin: 12.5px 15.15px 0; | ||||||
|  |             /* TODO FF CENTER ISSUE */ | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          @keyframes am-weather-moon-star-1 { | ||||||
|  |             0% { | ||||||
|  |                opacity: 0; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             100% { | ||||||
|  |                opacity: 1; | ||||||
|  |             } | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          .am-weather-moon-star-1 { | ||||||
|  |             -webkit-animation-name: am-weather-moon-star-1; | ||||||
|  |             -moz-animation-name: am-weather-moon-star-1; | ||||||
|  |             -ms-animation-name: am-weather-moon-star-1; | ||||||
|  |             animation-name: am-weather-moon-star-1; | ||||||
|  |             -webkit-animation-delay: 3s; | ||||||
|  |             -moz-animation-delay: 3s; | ||||||
|  |             -ms-animation-delay: 3s; | ||||||
|  |             animation-delay: 3s; | ||||||
|  |             -webkit-animation-duration: 5s; | ||||||
|  |             -moz-animation-duration: 5s; | ||||||
|  |             -ms-animation-duration: 5s; | ||||||
|  |             animation-duration: 5s; | ||||||
|  |             -webkit-animation-timing-function: linear; | ||||||
|  |             -moz-animation-timing-function: linear; | ||||||
|  |             -ms-animation-timing-function: linear; | ||||||
|  |             animation-timing-function: linear; | ||||||
|  |             -webkit-animation-iteration-count: 1; | ||||||
|  |             -moz-animation-iteration-count: 1; | ||||||
|  |             -ms-animation-iteration-count: 1; | ||||||
|  |             animation-iteration-count: 1; | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          @keyframes am-weather-moon-star-2 { | ||||||
|  |             0% { | ||||||
|  |                opacity: 0; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             100% { | ||||||
|  |                opacity: 1; | ||||||
|  |             } | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          .am-weather-moon-star-2 { | ||||||
|  |             -webkit-animation-name: am-weather-moon-star-2; | ||||||
|  |             -moz-animation-name: am-weather-moon-star-2; | ||||||
|  |             -ms-animation-name: am-weather-moon-star-2; | ||||||
|  |             animation-name: am-weather-moon-star-2; | ||||||
|  |             -webkit-animation-delay: 5s; | ||||||
|  |             -moz-animation-delay: 5s; | ||||||
|  |             -ms-animation-delay: 5s; | ||||||
|  |             animation-delay: 5s; | ||||||
|  |             -webkit-animation-duration: 4s; | ||||||
|  |             -moz-animation-duration: 4s; | ||||||
|  |             -ms-animation-duration: 4s; | ||||||
|  |             animation-duration: 4s; | ||||||
|  |             -webkit-animation-timing-function: linear; | ||||||
|  |             -moz-animation-timing-function: linear; | ||||||
|  |             -ms-animation-timing-function: linear; | ||||||
|  |             animation-timing-function: linear; | ||||||
|  |             -webkit-animation-iteration-count: 1; | ||||||
|  |             -moz-animation-iteration-count: 1; | ||||||
|  |             -ms-animation-iteration-count: 1; | ||||||
|  |             animation-iteration-count: 1; | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          /* | ||||||
|  | ** SNOW | ||||||
|  | */ | ||||||
|  |          @keyframes am-weather-snow { | ||||||
|  |             0% { | ||||||
|  |                -webkit-transform: translateX(0) translateY(0); | ||||||
|  |                -moz-transform: translateX(0) translateY(0); | ||||||
|  |                -ms-transform: translateX(0) translateY(0); | ||||||
|  |                transform: translateX(0) translateY(0); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             33.33% { | ||||||
|  |                -webkit-transform: translateX(-1.2px) translateY(2px); | ||||||
|  |                -moz-transform: translateX(-1.2px) translateY(2px); | ||||||
|  |                -ms-transform: translateX(-1.2px) translateY(2px); | ||||||
|  |                transform: translateX(-1.2px) translateY(2px); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             66.66% { | ||||||
|  |                -webkit-transform: translateX(1.4px) translateY(4px); | ||||||
|  |                -moz-transform: translateX(1.4px) translateY(4px); | ||||||
|  |                -ms-transform: translateX(1.4px) translateY(4px); | ||||||
|  |                transform: translateX(1.4px) translateY(4px); | ||||||
|  |                opacity: 1; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             100% { | ||||||
|  |                -webkit-transform: translateX(-1.6px) translateY(6px); | ||||||
|  |                -moz-transform: translateX(-1.6px) translateY(6px); | ||||||
|  |                -ms-transform: translateX(-1.6px) translateY(6px); | ||||||
|  |                transform: translateX(-1.6px) translateY(6px); | ||||||
|  |                opacity: 0; | ||||||
|  |             } | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          @keyframes am-weather-snow-reverse { | ||||||
|  |             0% { | ||||||
|  |                -webkit-transform: translateX(0) translateY(0); | ||||||
|  |                -moz-transform: translateX(0) translateY(0); | ||||||
|  |                -ms-transform: translateX(0) translateY(0); | ||||||
|  |                transform: translateX(0) translateY(0); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             33.33% { | ||||||
|  |                -webkit-transform: translateX(1.2px) translateY(2px); | ||||||
|  |                -moz-transform: translateX(1.2px) translateY(2px); | ||||||
|  |                -ms-transform: translateX(1.2px) translateY(2px); | ||||||
|  |                transform: translateX(1.2px) translateY(2px); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             66.66% { | ||||||
|  |                -webkit-transform: translateX(-1.4px) translateY(4px); | ||||||
|  |                -moz-transform: translateX(-1.4px) translateY(4px); | ||||||
|  |                -ms-transform: translateX(-1.4px) translateY(4px); | ||||||
|  |                transform: translateX(-1.4px) translateY(4px); | ||||||
|  |                opacity: 1; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             100% { | ||||||
|  |                -webkit-transform: translateX(1.6px) translateY(6px); | ||||||
|  |                -moz-transform: translateX(1.6px) translateY(6px); | ||||||
|  |                -ms-transform: translateX(1.6px) translateY(6px); | ||||||
|  |                transform: translateX(1.6px) translateY(6px); | ||||||
|  |                opacity: 0; | ||||||
|  |             } | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          .am-weather-snow-1 { | ||||||
|  |             -webkit-animation-name: am-weather-snow; | ||||||
|  |             -moz-animation-name: am-weather-snow; | ||||||
|  |             -ms-animation-name: am-weather-snow; | ||||||
|  |             animation-name: am-weather-snow; | ||||||
|  |             -webkit-animation-duration: 2s; | ||||||
|  |             -moz-animation-duration: 2s; | ||||||
|  |             -ms-animation-duration: 2s; | ||||||
|  |             animation-duration: 2s; | ||||||
|  |             -webkit-animation-timing-function: linear; | ||||||
|  |             -moz-animation-timing-function: linear; | ||||||
|  |             -ms-animation-timing-function: linear; | ||||||
|  |             animation-timing-function: linear; | ||||||
|  |             -webkit-animation-iteration-count: infinite; | ||||||
|  |             -moz-animation-iteration-count: infinite; | ||||||
|  |             -ms-animation-iteration-count: infinite; | ||||||
|  |             animation-iteration-count: infinite; | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          .am-weather-snow-2 { | ||||||
|  |             -webkit-animation-name: am-weather-snow; | ||||||
|  |             -moz-animation-name: am-weather-snow; | ||||||
|  |             -ms-animation-name: am-weather-snow; | ||||||
|  |             animation-name: am-weather-snow; | ||||||
|  |             -webkit-animation-delay: 1.2s; | ||||||
|  |             -moz-animation-delay: 1.2s; | ||||||
|  |             -ms-animation-delay: 1.2s; | ||||||
|  |             animation-delay: 1.2s; | ||||||
|  |             -webkit-animation-duration: 2s; | ||||||
|  |             -moz-animation-duration: 2s; | ||||||
|  |             -ms-animation-duration: 2s; | ||||||
|  |             animation-duration: 2s; | ||||||
|  |             -webkit-animation-timing-function: linear; | ||||||
|  |             -moz-animation-timing-function: linear; | ||||||
|  |             -ms-animation-timing-function: linear; | ||||||
|  |             animation-timing-function: linear; | ||||||
|  |             -webkit-animation-iteration-count: infinite; | ||||||
|  |             -moz-animation-iteration-count: infinite; | ||||||
|  |             -ms-animation-iteration-count: infinite; | ||||||
|  |             animation-iteration-count: infinite; | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |          .am-weather-snow-3 { | ||||||
|  |             -webkit-animation-name: am-weather-snow-reverse; | ||||||
|  |             -moz-animation-name: am-weather-snow-reverse; | ||||||
|  |             -ms-animation-name: am-weather-snow-reverse; | ||||||
|  |             animation-name: am-weather-snow-reverse; | ||||||
|  |             -webkit-animation-duration: 2s; | ||||||
|  |             -moz-animation-duration: 2s; | ||||||
|  |             -ms-animation-duration: 2s; | ||||||
|  |             animation-duration: 2s; | ||||||
|  |             -webkit-animation-timing-function: linear; | ||||||
|  |             -moz-animation-timing-function: linear; | ||||||
|  |             -ms-animation-timing-function: linear; | ||||||
|  |             animation-timing-function: linear; | ||||||
|  |             -webkit-animation-iteration-count: infinite; | ||||||
|  |             -moz-animation-iteration-count: infinite; | ||||||
|  |             -ms-animation-iteration-count: infinite; | ||||||
|  |             animation-iteration-count: infinite; | ||||||
|  |          } | ||||||
|  |          ]]> | ||||||
|  |       </style> | ||||||
|  |    </defs> | ||||||
|  |    <g transform="translate(16,-2)" filter="url(#blur)"> | ||||||
|  |       <g transform="matrix(.8 0 0 .8 16 4)"> | ||||||
|  |          <g class="am-weather-moon-star-1" | ||||||
|  |             style="-moz-animation-delay:3s;-moz-animation-duration:5s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-1;-moz-animation-timing-function:linear;-ms-animation-delay:3s;-ms-animation-duration:5s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-1;-ms-animation-timing-function:linear;-webkit-animation-delay:3s;-webkit-animation-duration:5s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-1;-webkit-animation-timing-function:linear"> | ||||||
|  |             <polygon points="4 4 3.3 5.2 2.7 4 1.5 3.3 2.7 2.7 3.3 1.5 4 2.7 5.2 3.3" fill="#ffa500" | ||||||
|  |                stroke-miterlimit="10" /> | ||||||
|  |          </g> | ||||||
|  |          <g class="am-weather-moon-star-2" | ||||||
|  |             style="-moz-animation-delay:5s;-moz-animation-duration:4s;-moz-animation-iteration-count:1;-moz-animation-name:am-weather-moon-star-2;-moz-animation-timing-function:linear;-ms-animation-delay:5s;-ms-animation-duration:4s;-ms-animation-iteration-count:1;-ms-animation-name:am-weather-moon-star-2;-ms-animation-timing-function:linear;-webkit-animation-delay:5s;-webkit-animation-duration:4s;-webkit-animation-iteration-count:1;-webkit-animation-name:am-weather-moon-star-2;-webkit-animation-timing-function:linear"> | ||||||
|  |             <polygon transform="translate(20,10)" points="4 4 3.3 5.2 2.7 4 1.5 3.3 2.7 2.7 3.3 1.5 4 2.7 5.2 3.3" | ||||||
|  |                fill="#ffa500" stroke-miterlimit="10" /> | ||||||
|  |          </g> | ||||||
|  |          <g class="am-weather-moon" | ||||||
|  |             style="-moz-animation-duration:6s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-moon;-moz-animation-timing-function:linear;-moz-transform-origin:12.5px 15.15px 0;-ms-animation-duration:6s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-moon;-ms-animation-timing-function:linear;-ms-transform-origin:12.5px 15.15px 0;-webkit-animation-duration:6s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-moon;-webkit-animation-timing-function:linear;-webkit-transform-origin:12.5px 15.15px 0"> | ||||||
|  |             <path | ||||||
|  |                d="m14.5 13.2c0-3.7 2-6.9 5-8.7-1.5-0.9-3.2-1.3-5-1.3-5.5 0-10 4.5-10 10s4.5 10 10 10c1.8 0 3.5-0.5 5-1.3-3-1.7-5-5-5-8.7z" | ||||||
|  |                fill="#ffa500" stroke="#ffa500" stroke-linejoin="round" stroke-width="2" /> | ||||||
|  |          </g> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-cloud-2" | ||||||
|  |          style="-moz-animation-duration:3s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-cloud-2;-moz-animation-timing-function:linear;-webkit-animation-duration:3s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-cloud-2;-webkit-animation-timing-function:linear"> | ||||||
|  |          <path transform="translate(-20,-11)" | ||||||
|  |             d="m47.7 35.4c0-4.6-3.7-8.2-8.2-8.2-1 0-1.9 0.2-2.8 0.5-0.3-3.4-3.1-6.2-6.6-6.2-3.7 0-6.7 3-6.7 6.7 0 0.8 0.2 1.6 0.4 2.3-0.3-0.1-0.7-0.1-1-0.1-3.7 0-6.7 3-6.7 6.7 0 3.6 2.9 6.6 6.5 6.7h17.2c4.4-0.5 7.9-4 7.9-8.4z" | ||||||
|  |             fill="#57a0ee" stroke="#fff" stroke-linejoin="round" stroke-width="1.2" /> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-snow-1" | ||||||
|  |          style="-moz-animation-duration:2s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-snow;-moz-animation-timing-function:linear;-ms-animation-duration:2s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-snow;-ms-animation-timing-function:linear;-webkit-animation-duration:2s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-snow;-webkit-animation-timing-function:linear"> | ||||||
|  |          <g transform="translate(3,28)" fill="none" stroke="#57a0ee" stroke-linecap="round"> | ||||||
|  |             <line transform="translate(0,9)" y1="-2.5" y2="2.5" stroke-width="1.2" /> | ||||||
|  |             <line transform="rotate(45,-10.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |             <line transform="rotate(90,-4.5,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |             <line transform="rotate(135,-1.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |          </g> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-snow-2" | ||||||
|  |          style="-moz-animation-delay:1.2s;-moz-animation-duration:2s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-snow;-moz-animation-timing-function:linear;-ms-animation-delay:1.2s;-ms-animation-duration:2s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-snow;-ms-animation-timing-function:linear;-webkit-animation-delay:1.2s;-webkit-animation-duration:2s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-snow;-webkit-animation-timing-function:linear"> | ||||||
|  |          <g transform="translate(11,28)" fill="none" stroke="#57a0ee" stroke-linecap="round"> | ||||||
|  |             <line transform="translate(0,9)" y1="-2.5" y2="2.5" stroke-width="1.2" /> | ||||||
|  |             <line transform="rotate(45,-10.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |             <line transform="rotate(90,-4.5,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |             <line transform="rotate(135,-1.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |          </g> | ||||||
|  |       </g> | ||||||
|  |       <g class="am-weather-snow-3" | ||||||
|  |          style="-moz-animation-duration:2s;-moz-animation-iteration-count:infinite;-moz-animation-name:am-weather-snow-reverse;-moz-animation-timing-function:linear;-ms-animation-duration:2s;-ms-animation-iteration-count:infinite;-ms-animation-name:am-weather-snow-reverse;-ms-animation-timing-function:linear;-webkit-animation-duration:2s;-webkit-animation-iteration-count:infinite;-webkit-animation-name:am-weather-snow-reverse;-webkit-animation-timing-function:linear"> | ||||||
|  |          <g transform="translate(20,28)" fill="none" stroke="#57a0ee" stroke-linecap="round"> | ||||||
|  |             <line transform="translate(0,9)" y1="-2.5" y2="2.5" stroke-width="1.2" /> | ||||||
|  |             <line transform="rotate(45,-10.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |             <line transform="rotate(90,-4.5,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |             <line transform="rotate(135,-1.864,4.5)" y1="-2.5" y2="2.5" /> | ||||||
|  |          </g> | ||||||
|  |       </g> | ||||||
|  |    </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 17 KiB | 
| @@ -1,29 +0,0 @@ | |||||||
| [GENERAL] |  | ||||||
| SIMILARITY_MEASURE = "cosine" # "cosine" or "dot" |  | ||||||
| KEEP_ALIVE = "5m" # How long to keep Ollama models loaded into memory. (Instead of using -1 use "-1m") |  | ||||||
|  |  | ||||||
| [MODELS.OPENAI] |  | ||||||
| API_KEY = "" |  | ||||||
|  |  | ||||||
| [MODELS.GROQ] |  | ||||||
| API_KEY = "" |  | ||||||
|  |  | ||||||
| [MODELS.ANTHROPIC] |  | ||||||
| API_KEY = "" |  | ||||||
|  |  | ||||||
| [MODELS.GEMINI] |  | ||||||
| API_KEY = "" |  | ||||||
|  |  | ||||||
| [MODELS.CUSTOM_OPENAI] |  | ||||||
| API_KEY = "" |  | ||||||
| API_URL = "" |  | ||||||
| MODEL_NAME = "" |  | ||||||
|  |  | ||||||
| [MODELS.OLLAMA] |  | ||||||
| API_URL = "" # Ollama API URL - http://host.docker.internal:11434 |  | ||||||
|  |  | ||||||
| [MODELS.DEEPSEEK] |  | ||||||
| API_KEY = "" |  | ||||||
|  |  | ||||||
| [API_ENDPOINTS] |  | ||||||
| SEARXNG = "" # SearxNG API URL - http://localhost:32768 |  | ||||||
| @@ -1,66 +1,104 @@ | |||||||
| import prompts from '@/lib/prompts'; |  | ||||||
| import MetaSearchAgent from '@/lib/search/metaSearchAgent'; |  | ||||||
| import crypto from 'crypto'; | import crypto from 'crypto'; | ||||||
| import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; | import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; | ||||||
| import { EventEmitter } from 'stream'; | import { EventEmitter } from 'stream'; | ||||||
| import { |  | ||||||
|   chatModelProviders, |  | ||||||
|   embeddingModelProviders, |  | ||||||
|   getAvailableChatModelProviders, |  | ||||||
|   getAvailableEmbeddingModelProviders, |  | ||||||
| } from '@/lib/providers'; |  | ||||||
| import db from '@/lib/db'; | import db from '@/lib/db'; | ||||||
| import { chats, messages as messagesSchema } from '@/lib/db/schema'; | import { chats, messages as messagesSchema } from '@/lib/db/schema'; | ||||||
| import { and, eq, gt } from 'drizzle-orm'; | import { and, eq, gt } from 'drizzle-orm'; | ||||||
| import { getFileDetails } from '@/lib/utils/files'; | import { getFileDetails } from '@/lib/utils/files'; | ||||||
| import { BaseChatModel } from '@langchain/core/language_models/chat_models'; |  | ||||||
| import { ChatOpenAI } from '@langchain/openai'; |  | ||||||
| import { |  | ||||||
|   getCustomOpenaiApiKey, |  | ||||||
|   getCustomOpenaiApiUrl, |  | ||||||
|   getCustomOpenaiModelName, |  | ||||||
| } from '@/lib/config'; |  | ||||||
| import { searchHandlers } from '@/lib/search'; | import { searchHandlers } from '@/lib/search'; | ||||||
|  | import { z } from 'zod'; | ||||||
|  | import ModelRegistry from '@/lib/models/registry'; | ||||||
|  | import { ModelWithProvider } from '@/lib/models/types'; | ||||||
|  |  | ||||||
| export const runtime = 'nodejs'; | export const runtime = 'nodejs'; | ||||||
| export const dynamic = 'force-dynamic'; | export const dynamic = 'force-dynamic'; | ||||||
|  |  | ||||||
| type Message = { | const messageSchema = z.object({ | ||||||
|   messageId: string; |   messageId: z.string().min(1, 'Message ID is required'), | ||||||
|   chatId: string; |   chatId: z.string().min(1, 'Chat ID is required'), | ||||||
|   content: string; |   content: z.string().min(1, 'Message content is required'), | ||||||
| }; | }); | ||||||
|  |  | ||||||
| type ChatModel = { | const chatModelSchema: z.ZodType<ModelWithProvider> = z.object({ | ||||||
|   provider: string; |   providerId: z.string({ | ||||||
|   name: string; |     errorMap: () => ({ | ||||||
| }; |       message: 'Chat model provider id must be provided', | ||||||
|  |     }), | ||||||
|  |   }), | ||||||
|  |   key: z.string({ | ||||||
|  |     errorMap: () => ({ | ||||||
|  |       message: 'Chat model key must be provided', | ||||||
|  |     }), | ||||||
|  |   }), | ||||||
|  | }); | ||||||
|  |  | ||||||
| type EmbeddingModel = { | const embeddingModelSchema: z.ZodType<ModelWithProvider> = z.object({ | ||||||
|   provider: string; |   providerId: z.string({ | ||||||
|   name: string; |     errorMap: () => ({ | ||||||
| }; |       message: 'Embedding model provider id must be provided', | ||||||
|  |     }), | ||||||
|  |   }), | ||||||
|  |   key: z.string({ | ||||||
|  |     errorMap: () => ({ | ||||||
|  |       message: 'Embedding model key must be provided', | ||||||
|  |     }), | ||||||
|  |   }), | ||||||
|  | }); | ||||||
|  |  | ||||||
| type Body = { | const bodySchema = z.object({ | ||||||
|   message: Message; |   message: messageSchema, | ||||||
|   optimizationMode: 'speed' | 'balanced' | 'quality'; |   optimizationMode: z.enum(['speed', 'balanced', 'quality'], { | ||||||
|   focusMode: string; |     errorMap: () => ({ | ||||||
|   history: Array<[string, string]>; |       message: 'Optimization mode must be one of: speed, balanced, quality', | ||||||
|   files: Array<string>; |     }), | ||||||
|   chatModel: ChatModel; |   }), | ||||||
|   embeddingModel: EmbeddingModel; |   focusMode: z.string().min(1, 'Focus mode is required'), | ||||||
|   systemInstructions: string; |   history: z | ||||||
|  |     .array( | ||||||
|  |       z.tuple([z.string(), z.string()], { | ||||||
|  |         errorMap: () => ({ | ||||||
|  |           message: 'History items must be tuples of two strings', | ||||||
|  |         }), | ||||||
|  |       }), | ||||||
|  |     ) | ||||||
|  |     .optional() | ||||||
|  |     .default([]), | ||||||
|  |   files: z.array(z.string()).optional().default([]), | ||||||
|  |   chatModel: chatModelSchema, | ||||||
|  |   embeddingModel: embeddingModelSchema, | ||||||
|  |   systemInstructions: z.string().nullable().optional().default(''), | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | type Message = z.infer<typeof messageSchema>; | ||||||
|  | type Body = z.infer<typeof bodySchema>; | ||||||
|  |  | ||||||
|  | const safeValidateBody = (data: unknown) => { | ||||||
|  |   const result = bodySchema.safeParse(data); | ||||||
|  |  | ||||||
|  |   if (!result.success) { | ||||||
|  |     return { | ||||||
|  |       success: false, | ||||||
|  |       error: result.error.errors.map((e) => ({ | ||||||
|  |         path: e.path.join('.'), | ||||||
|  |         message: e.message, | ||||||
|  |       })), | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return { | ||||||
|  |     success: true, | ||||||
|  |     data: result.data, | ||||||
|  |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const handleEmitterEvents = async ( | const handleEmitterEvents = async ( | ||||||
|   stream: EventEmitter, |   stream: EventEmitter, | ||||||
|   writer: WritableStreamDefaultWriter, |   writer: WritableStreamDefaultWriter, | ||||||
|   encoder: TextEncoder, |   encoder: TextEncoder, | ||||||
|   aiMessageId: string, |  | ||||||
|   chatId: string, |   chatId: string, | ||||||
| ) => { | ) => { | ||||||
|   let recievedMessage = ''; |   let recievedMessage = ''; | ||||||
|   let sources: any[] = []; |   const aiMessageId = crypto.randomBytes(7).toString('hex'); | ||||||
|  |  | ||||||
|   stream.on('data', (data) => { |   stream.on('data', (data) => { | ||||||
|     const parsedData = JSON.parse(data); |     const parsedData = JSON.parse(data); | ||||||
| @@ -87,7 +125,17 @@ const handleEmitterEvents = async ( | |||||||
|         ), |         ), | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|       sources = parsedData.data; |       const sourceMessageId = crypto.randomBytes(7).toString('hex'); | ||||||
|  |  | ||||||
|  |       db.insert(messagesSchema) | ||||||
|  |         .values({ | ||||||
|  |           chatId: chatId, | ||||||
|  |           messageId: sourceMessageId, | ||||||
|  |           role: 'source', | ||||||
|  |           sources: parsedData.data, | ||||||
|  |           createdAt: new Date().toString(), | ||||||
|  |         }) | ||||||
|  |         .execute(); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
|   stream.on('end', () => { |   stream.on('end', () => { | ||||||
| @@ -95,7 +143,6 @@ const handleEmitterEvents = async ( | |||||||
|       encoder.encode( |       encoder.encode( | ||||||
|         JSON.stringify({ |         JSON.stringify({ | ||||||
|           type: 'messageEnd', |           type: 'messageEnd', | ||||||
|           messageId: aiMessageId, |  | ||||||
|         }) + '\n', |         }) + '\n', | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
| @@ -107,10 +154,7 @@ const handleEmitterEvents = async ( | |||||||
|         chatId: chatId, |         chatId: chatId, | ||||||
|         messageId: aiMessageId, |         messageId: aiMessageId, | ||||||
|         role: 'assistant', |         role: 'assistant', | ||||||
|         metadata: JSON.stringify({ |         createdAt: new Date().toString(), | ||||||
|           createdAt: new Date(), |  | ||||||
|           ...(sources && sources.length > 0 && { sources }), |  | ||||||
|         }), |  | ||||||
|       }) |       }) | ||||||
|       .execute(); |       .execute(); | ||||||
|   }); |   }); | ||||||
| @@ -138,6 +182,8 @@ const handleHistorySave = async ( | |||||||
|     where: eq(chats.id, message.chatId), |     where: eq(chats.id, message.chatId), | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  |   const fileData = files.map(getFileDetails); | ||||||
|  |  | ||||||
|   if (!chat) { |   if (!chat) { | ||||||
|     await db |     await db | ||||||
|       .insert(chats) |       .insert(chats) | ||||||
| @@ -146,9 +192,15 @@ const handleHistorySave = async ( | |||||||
|         title: message.content, |         title: message.content, | ||||||
|         createdAt: new Date().toString(), |         createdAt: new Date().toString(), | ||||||
|         focusMode: focusMode, |         focusMode: focusMode, | ||||||
|         files: files.map(getFileDetails), |         files: fileData, | ||||||
|       }) |       }) | ||||||
|       .execute(); |       .execute(); | ||||||
|  |   } else if (JSON.stringify(chat.files ?? []) != JSON.stringify(fileData)) { | ||||||
|  |     db.update(chats) | ||||||
|  |       .set({ | ||||||
|  |         files: files.map(getFileDetails), | ||||||
|  |       }) | ||||||
|  |       .where(eq(chats.id, message.chatId)); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const messageExists = await db.query.messages.findFirst({ |   const messageExists = await db.query.messages.findFirst({ | ||||||
| @@ -163,9 +215,7 @@ const handleHistorySave = async ( | |||||||
|         chatId: message.chatId, |         chatId: message.chatId, | ||||||
|         messageId: humanMessageId, |         messageId: humanMessageId, | ||||||
|         role: 'user', |         role: 'user', | ||||||
|         metadata: JSON.stringify({ |         createdAt: new Date().toString(), | ||||||
|           createdAt: new Date(), |  | ||||||
|         }), |  | ||||||
|       }) |       }) | ||||||
|       .execute(); |       .execute(); | ||||||
|   } else { |   } else { | ||||||
| @@ -183,7 +233,17 @@ const handleHistorySave = async ( | |||||||
|  |  | ||||||
| export const POST = async (req: Request) => { | export const POST = async (req: Request) => { | ||||||
|   try { |   try { | ||||||
|     const body = (await req.json()) as Body; |     const reqBody = (await req.json()) as Body; | ||||||
|  |  | ||||||
|  |     const parseBody = safeValidateBody(reqBody); | ||||||
|  |     if (!parseBody.success) { | ||||||
|  |       return Response.json( | ||||||
|  |         { message: 'Invalid request body', error: parseBody.error }, | ||||||
|  |         { status: 400 }, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const body = parseBody.data as Body; | ||||||
|     const { message } = body; |     const { message } = body; | ||||||
|  |  | ||||||
|     if (message.content === '') { |     if (message.content === '') { | ||||||
| @@ -195,59 +255,18 @@ export const POST = async (req: Request) => { | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const [chatModelProviders, embeddingModelProviders] = await Promise.all([ |     const registry = new ModelRegistry(); | ||||||
|       getAvailableChatModelProviders(), |  | ||||||
|       getAvailableEmbeddingModelProviders(), |     const [llm, embedding] = await Promise.all([ | ||||||
|  |       registry.loadChatModel(body.chatModel.providerId, body.chatModel.key), | ||||||
|  |       registry.loadEmbeddingModel( | ||||||
|  |         body.embeddingModel.providerId, | ||||||
|  |         body.embeddingModel.key, | ||||||
|  |       ), | ||||||
|     ]); |     ]); | ||||||
|  |  | ||||||
|     const chatModelProvider = |  | ||||||
|       chatModelProviders[ |  | ||||||
|         body.chatModel?.provider || Object.keys(chatModelProviders)[0] |  | ||||||
|       ]; |  | ||||||
|     const chatModel = |  | ||||||
|       chatModelProvider[ |  | ||||||
|         body.chatModel?.name || Object.keys(chatModelProvider)[0] |  | ||||||
|       ]; |  | ||||||
|  |  | ||||||
|     const embeddingProvider = |  | ||||||
|       embeddingModelProviders[ |  | ||||||
|         body.embeddingModel?.provider || Object.keys(embeddingModelProviders)[0] |  | ||||||
|       ]; |  | ||||||
|     const embeddingModel = |  | ||||||
|       embeddingProvider[ |  | ||||||
|         body.embeddingModel?.name || Object.keys(embeddingProvider)[0] |  | ||||||
|       ]; |  | ||||||
|  |  | ||||||
|     let llm: BaseChatModel | undefined; |  | ||||||
|     let embedding = embeddingModel.model; |  | ||||||
|  |  | ||||||
|     if (body.chatModel?.provider === 'custom_openai') { |  | ||||||
|       llm = new ChatOpenAI({ |  | ||||||
|         openAIApiKey: getCustomOpenaiApiKey(), |  | ||||||
|         modelName: getCustomOpenaiModelName(), |  | ||||||
|         temperature: 0.7, |  | ||||||
|         configuration: { |  | ||||||
|           baseURL: getCustomOpenaiApiUrl(), |  | ||||||
|         }, |  | ||||||
|       }) as unknown as BaseChatModel; |  | ||||||
|     } else if (chatModelProvider && chatModel) { |  | ||||||
|       llm = chatModel.model; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!llm) { |  | ||||||
|       return Response.json({ error: 'Invalid chat model' }, { status: 400 }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!embedding) { |  | ||||||
|       return Response.json( |  | ||||||
|         { error: 'Invalid embedding model' }, |  | ||||||
|         { status: 400 }, |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const humanMessageId = |     const humanMessageId = | ||||||
|       message.messageId ?? crypto.randomBytes(7).toString('hex'); |       message.messageId ?? crypto.randomBytes(7).toString('hex'); | ||||||
|     const aiMessageId = crypto.randomBytes(7).toString('hex'); |  | ||||||
|  |  | ||||||
|     const history: BaseMessage[] = body.history.map((msg) => { |     const history: BaseMessage[] = body.history.map((msg) => { | ||||||
|       if (msg[0] === 'human') { |       if (msg[0] === 'human') { | ||||||
| @@ -279,14 +298,14 @@ export const POST = async (req: Request) => { | |||||||
|       embedding, |       embedding, | ||||||
|       body.optimizationMode, |       body.optimizationMode, | ||||||
|       body.files, |       body.files, | ||||||
|       body.systemInstructions, |       body.systemInstructions as string, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const responseStream = new TransformStream(); |     const responseStream = new TransformStream(); | ||||||
|     const writer = responseStream.writable.getWriter(); |     const writer = responseStream.writable.getWriter(); | ||||||
|     const encoder = new TextEncoder(); |     const encoder = new TextEncoder(); | ||||||
|  |  | ||||||
|     handleEmitterEvents(stream, writer, encoder, aiMessageId, message.chatId); |     handleEmitterEvents(stream, writer, encoder, message.chatId); | ||||||
|     handleHistorySave(message, humanMessageId, body.focusMode, body.files); |     handleHistorySave(message, humanMessageId, body.focusMode, body.files); | ||||||
|  |  | ||||||
|     return new Response(responseStream.readable, { |     return new Response(responseStream.readable, { | ||||||
|   | |||||||
| @@ -1,113 +1,76 @@ | |||||||
| import { | import configManager from '@/lib/config'; | ||||||
|   getAnthropicApiKey, | import ModelRegistry from '@/lib/models/registry'; | ||||||
|   getCustomOpenaiApiKey, | import { NextRequest, NextResponse } from 'next/server'; | ||||||
|   getCustomOpenaiApiUrl, | import { ConfigModelProvider } from '@/lib/config/types'; | ||||||
|   getCustomOpenaiModelName, |  | ||||||
|   getGeminiApiKey, |  | ||||||
|   getGroqApiKey, |  | ||||||
|   getOllamaApiEndpoint, |  | ||||||
|   getOpenaiApiKey, |  | ||||||
|   getDeepseekApiKey, |  | ||||||
|   updateConfig, |  | ||||||
| } from '@/lib/config'; |  | ||||||
| import { |  | ||||||
|   getAvailableChatModelProviders, |  | ||||||
|   getAvailableEmbeddingModelProviders, |  | ||||||
| } from '@/lib/providers'; |  | ||||||
|  |  | ||||||
| export const GET = async (req: Request) => { | type SaveConfigBody = { | ||||||
|   try { |   key: string; | ||||||
|     const config: Record<string, any> = {}; |   value: string; | ||||||
|  |  | ||||||
|     const [chatModelProviders, embeddingModelProviders] = await Promise.all([ |  | ||||||
|       getAvailableChatModelProviders(), |  | ||||||
|       getAvailableEmbeddingModelProviders(), |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     config['chatModelProviders'] = {}; |  | ||||||
|     config['embeddingModelProviders'] = {}; |  | ||||||
|  |  | ||||||
|     for (const provider in chatModelProviders) { |  | ||||||
|       config['chatModelProviders'][provider] = Object.keys( |  | ||||||
|         chatModelProviders[provider], |  | ||||||
|       ).map((model) => { |  | ||||||
|         return { |  | ||||||
|           name: model, |  | ||||||
|           displayName: chatModelProviders[provider][model].displayName, |  | ||||||
|         }; |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     for (const provider in embeddingModelProviders) { |  | ||||||
|       config['embeddingModelProviders'][provider] = Object.keys( |  | ||||||
|         embeddingModelProviders[provider], |  | ||||||
|       ).map((model) => { |  | ||||||
|         return { |  | ||||||
|           name: model, |  | ||||||
|           displayName: embeddingModelProviders[provider][model].displayName, |  | ||||||
|         }; |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     config['openaiApiKey'] = getOpenaiApiKey(); |  | ||||||
|     config['ollamaApiUrl'] = getOllamaApiEndpoint(); |  | ||||||
|     config['anthropicApiKey'] = getAnthropicApiKey(); |  | ||||||
|     config['groqApiKey'] = getGroqApiKey(); |  | ||||||
|     config['geminiApiKey'] = getGeminiApiKey(); |  | ||||||
|     config['deepseekApiKey'] = getDeepseekApiKey(); |  | ||||||
|     config['customOpenaiApiUrl'] = getCustomOpenaiApiUrl(); |  | ||||||
|     config['customOpenaiApiKey'] = getCustomOpenaiApiKey(); |  | ||||||
|     config['customOpenaiModelName'] = getCustomOpenaiModelName(); |  | ||||||
|  |  | ||||||
|     return Response.json({ ...config }, { status: 200 }); |  | ||||||
|   } catch (err) { |  | ||||||
|     console.error('An error occurred while getting config:', err); |  | ||||||
|     return Response.json( |  | ||||||
|       { message: 'An error occurred while getting config' }, |  | ||||||
|       { status: 500 }, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const POST = async (req: Request) => { | export const GET = async (req: NextRequest) => { | ||||||
|   try { |   try { | ||||||
|     const config = await req.json(); |     const values = configManager.getCurrentConfig(); | ||||||
|  |     const fields = configManager.getUIConfigSections(); | ||||||
|  |  | ||||||
|     const updatedConfig = { |     const modelRegistry = new ModelRegistry(); | ||||||
|       MODELS: { |     const modelProviders = await modelRegistry.getActiveProviders(); | ||||||
|         OPENAI: { |  | ||||||
|           API_KEY: config.openaiApiKey, |     values.modelProviders = values.modelProviders.map( | ||||||
|         }, |       (mp: ConfigModelProvider) => { | ||||||
|         GROQ: { |         const activeProvider = modelProviders.find((p) => p.id === mp.id); | ||||||
|           API_KEY: config.groqApiKey, |  | ||||||
|         }, |         return { | ||||||
|         ANTHROPIC: { |           ...mp, | ||||||
|           API_KEY: config.anthropicApiKey, |           chatModels: activeProvider?.chatModels ?? mp.chatModels, | ||||||
|         }, |           embeddingModels: | ||||||
|         GEMINI: { |             activeProvider?.embeddingModels ?? mp.embeddingModels, | ||||||
|           API_KEY: config.geminiApiKey, |         }; | ||||||
|         }, |  | ||||||
|         OLLAMA: { |  | ||||||
|           API_URL: config.ollamaApiUrl, |  | ||||||
|         }, |  | ||||||
|         DEEPSEEK: { |  | ||||||
|           API_KEY: config.deepseekApiKey, |  | ||||||
|         }, |  | ||||||
|         CUSTOM_OPENAI: { |  | ||||||
|           API_URL: config.customOpenaiApiUrl, |  | ||||||
|           API_KEY: config.customOpenaiApiKey, |  | ||||||
|           MODEL_NAME: config.customOpenaiModelName, |  | ||||||
|         }, |  | ||||||
|       }, |       }, | ||||||
|     }; |     ); | ||||||
|  |  | ||||||
|     updateConfig(updatedConfig); |     return NextResponse.json({ | ||||||
|  |       values, | ||||||
|     return Response.json({ message: 'Config updated' }, { status: 200 }); |       fields, | ||||||
|  |     }); | ||||||
|   } catch (err) { |   } catch (err) { | ||||||
|     console.error('An error occurred while updating config:', err); |     console.error('Error in getting config: ', err); | ||||||
|     return Response.json( |     return Response.json( | ||||||
|       { message: 'An error occurred while updating config' }, |       { message: 'An error has occurred.' }, | ||||||
|  |       { status: 500 }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const POST = async (req: NextRequest) => { | ||||||
|  |   try { | ||||||
|  |     const body: SaveConfigBody = await req.json(); | ||||||
|  |  | ||||||
|  |     if (!body.key || !body.value) { | ||||||
|  |       return Response.json( | ||||||
|  |         { | ||||||
|  |           message: 'Key and value are required.', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           status: 400, | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     configManager.updateConfig(body.key, body.value); | ||||||
|  |  | ||||||
|  |     return Response.json( | ||||||
|  |       { | ||||||
|  |         message: 'Config updated successfully.', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         status: 200, | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } catch (err) { | ||||||
|  |     console.error('Error in getting config: ', err); | ||||||
|  |     return Response.json( | ||||||
|  |       { message: 'An error has occurred.' }, | ||||||
|       { status: 500 }, |       { status: 500 }, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								src/app/api/config/setup-complete/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | |||||||
|  | import configManager from '@/lib/config'; | ||||||
|  | import { NextRequest } from 'next/server'; | ||||||
|  |  | ||||||
|  | export const POST = async (req: NextRequest) => { | ||||||
|  |   try { | ||||||
|  |     configManager.markSetupComplete(); | ||||||
|  |  | ||||||
|  |     return Response.json( | ||||||
|  |       { | ||||||
|  |         message: 'Setup marked as complete.', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         status: 200, | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } catch (err) { | ||||||
|  |     console.error('Error marking setup as complete: ', err); | ||||||
|  |     return Response.json( | ||||||
|  |       { message: 'An error has occurred.' }, | ||||||
|  |       { status: 500 }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | }; | ||||||
| @@ -1,43 +1,80 @@ | |||||||
| import { searchSearxng } from '@/lib/searxng'; | import { searchSearxng } from '@/lib/searxng'; | ||||||
|  |  | ||||||
| const articleWebsites = [ | const websitesForTopic = { | ||||||
|   'yahoo.com', |   tech: { | ||||||
|   'www.exchangewire.com', |     query: ['technology news', 'latest tech', 'AI', 'science and innovation'], | ||||||
|   'businessinsider.com', |     links: ['techcrunch.com', 'wired.com', 'theverge.com'], | ||||||
|   /* 'wired.com', |   }, | ||||||
|   'mashable.com', |   finance: { | ||||||
|   'theverge.com', |     query: ['finance news', 'economy', 'stock market', 'investing'], | ||||||
|   'gizmodo.com', |     links: ['bloomberg.com', 'cnbc.com', 'marketwatch.com'], | ||||||
|   'cnet.com', |   }, | ||||||
|   'venturebeat.com', */ |   art: { | ||||||
| ]; |     query: ['art news', 'culture', 'modern art', 'cultural events'], | ||||||
|  |     links: ['artnews.com', 'hyperallergic.com', 'theartnewspaper.com'], | ||||||
|  |   }, | ||||||
|  |   sports: { | ||||||
|  |     query: ['sports news', 'latest sports', 'cricket football tennis'], | ||||||
|  |     links: ['espn.com', 'bbc.com/sport', 'skysports.com'], | ||||||
|  |   }, | ||||||
|  |   entertainment: { | ||||||
|  |     query: ['entertainment news', 'movies', 'TV shows', 'celebrities'], | ||||||
|  |     links: ['hollywoodreporter.com', 'variety.com', 'deadline.com'], | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  |  | ||||||
| const topics = ['AI', 'tech']; /* TODO: Add UI to customize this */ | type Topic = keyof typeof websitesForTopic; | ||||||
|  |  | ||||||
| export const GET = async (req: Request) => { | export const GET = async (req: Request) => { | ||||||
|   try { |   try { | ||||||
|     const data = ( |     const params = new URL(req.url).searchParams; | ||||||
|       await Promise.all([ |  | ||||||
|         ...new Array(articleWebsites.length * topics.length) |     const mode: 'normal' | 'preview' = | ||||||
|           .fill(0) |       (params.get('mode') as 'normal' | 'preview') || 'normal'; | ||||||
|           .map(async (_, i) => { |     const topic: Topic = (params.get('topic') as Topic) || 'tech'; | ||||||
|             return ( |  | ||||||
|               await searchSearxng( |     const selectedTopic = websitesForTopic[topic]; | ||||||
|                 `site:${articleWebsites[i % articleWebsites.length]} ${ |  | ||||||
|                   topics[i % topics.length] |     let data = []; | ||||||
|                 }`, |  | ||||||
|                 { |     if (mode === 'normal') { | ||||||
|  |       const seenUrls = new Set(); | ||||||
|  |  | ||||||
|  |       data = ( | ||||||
|  |         await Promise.all( | ||||||
|  |           selectedTopic.links.flatMap((link) => | ||||||
|  |             selectedTopic.query.map(async (query) => { | ||||||
|  |               return ( | ||||||
|  |                 await searchSearxng(`site:${link} ${query}`, { | ||||||
|                   engines: ['bing news'], |                   engines: ['bing news'], | ||||||
|                   pageno: 1, |                   pageno: 1, | ||||||
|                 }, |                   language: 'en', | ||||||
|               ) |                 }) | ||||||
|             ).results; |               ).results; | ||||||
|           }), |             }), | ||||||
|       ]) |           ), | ||||||
|     ) |         ) | ||||||
|       .map((result) => result) |       ) | ||||||
|       .flat() |         .flat() | ||||||
|       .sort(() => Math.random() - 0.5); |         .filter((item) => { | ||||||
|  |           const url = item.url?.toLowerCase().trim(); | ||||||
|  |           if (seenUrls.has(url)) return false; | ||||||
|  |           seenUrls.add(url); | ||||||
|  |           return true; | ||||||
|  |         }) | ||||||
|  |         .sort(() => Math.random() - 0.5); | ||||||
|  |     } else { | ||||||
|  |       data = ( | ||||||
|  |         await searchSearxng( | ||||||
|  |           `site:${selectedTopic.links[Math.floor(Math.random() * selectedTopic.links.length)]} ${selectedTopic.query[Math.floor(Math.random() * selectedTopic.query.length)]}`, | ||||||
|  |           { | ||||||
|  |             engines: ['bing news'], | ||||||
|  |             pageno: 1, | ||||||
|  |             language: 'en', | ||||||
|  |           }, | ||||||
|  |         ) | ||||||
|  |       ).results; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return Response.json( |     return Response.json( | ||||||
|       { |       { | ||||||
|   | |||||||
| @@ -1,23 +1,12 @@ | |||||||
| import handleImageSearch from '@/lib/chains/imageSearchAgent'; | import handleImageSearch from '@/lib/chains/imageSearchAgent'; | ||||||
| import { | import ModelRegistry from '@/lib/models/registry'; | ||||||
|   getCustomOpenaiApiKey, | import { ModelWithProvider } from '@/lib/models/types'; | ||||||
|   getCustomOpenaiApiUrl, |  | ||||||
|   getCustomOpenaiModelName, |  | ||||||
| } from '@/lib/config'; |  | ||||||
| import { getAvailableChatModelProviders } from '@/lib/providers'; |  | ||||||
| import { BaseChatModel } from '@langchain/core/language_models/chat_models'; |  | ||||||
| import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; | import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; | ||||||
| import { ChatOpenAI } from '@langchain/openai'; |  | ||||||
|  |  | ||||||
| interface ChatModel { |  | ||||||
|   provider: string; |  | ||||||
|   model: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| interface ImageSearchBody { | interface ImageSearchBody { | ||||||
|   query: string; |   query: string; | ||||||
|   chatHistory: any[]; |   chatHistory: any[]; | ||||||
|   chatModel?: ChatModel; |   chatModel: ModelWithProvider; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const POST = async (req: Request) => { | export const POST = async (req: Request) => { | ||||||
| @@ -34,35 +23,12 @@ export const POST = async (req: Request) => { | |||||||
|       }) |       }) | ||||||
|       .filter((msg) => msg !== undefined) as BaseMessage[]; |       .filter((msg) => msg !== undefined) as BaseMessage[]; | ||||||
|  |  | ||||||
|     const chatModelProviders = await getAvailableChatModelProviders(); |     const registry = new ModelRegistry(); | ||||||
|  |  | ||||||
|     const chatModelProvider = |     const llm = await registry.loadChatModel( | ||||||
|       chatModelProviders[ |       body.chatModel.providerId, | ||||||
|         body.chatModel?.provider || Object.keys(chatModelProviders)[0] |       body.chatModel.key, | ||||||
|       ]; |     ); | ||||||
|     const chatModel = |  | ||||||
|       chatModelProvider[ |  | ||||||
|         body.chatModel?.model || Object.keys(chatModelProvider)[0] |  | ||||||
|       ]; |  | ||||||
|  |  | ||||||
|     let llm: BaseChatModel | undefined; |  | ||||||
|  |  | ||||||
|     if (body.chatModel?.provider === 'custom_openai') { |  | ||||||
|       llm = new ChatOpenAI({ |  | ||||||
|         openAIApiKey: getCustomOpenaiApiKey(), |  | ||||||
|         modelName: getCustomOpenaiModelName(), |  | ||||||
|         temperature: 0.7, |  | ||||||
|         configuration: { |  | ||||||
|           baseURL: getCustomOpenaiApiUrl(), |  | ||||||
|         }, |  | ||||||
|       }) as unknown as BaseChatModel; |  | ||||||
|     } else if (chatModelProvider && chatModel) { |  | ||||||
|       llm = chatModel.model; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!llm) { |  | ||||||
|       return Response.json({ error: 'Invalid chat model' }, { status: 400 }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const images = await handleImageSearch( |     const images = await handleImageSearch( | ||||||
|       { |       { | ||||||
|   | |||||||
| @@ -1,47 +0,0 @@ | |||||||
| import { |  | ||||||
|   getAvailableChatModelProviders, |  | ||||||
|   getAvailableEmbeddingModelProviders, |  | ||||||
| } from '@/lib/providers'; |  | ||||||
|  |  | ||||||
| export const GET = async (req: Request) => { |  | ||||||
|   try { |  | ||||||
|     const [chatModelProviders, embeddingModelProviders] = await Promise.all([ |  | ||||||
|       getAvailableChatModelProviders(), |  | ||||||
|       getAvailableEmbeddingModelProviders(), |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     Object.keys(chatModelProviders).forEach((provider) => { |  | ||||||
|       Object.keys(chatModelProviders[provider]).forEach((model) => { |  | ||||||
|         delete (chatModelProviders[provider][model] as { model?: unknown }) |  | ||||||
|           .model; |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     Object.keys(embeddingModelProviders).forEach((provider) => { |  | ||||||
|       Object.keys(embeddingModelProviders[provider]).forEach((model) => { |  | ||||||
|         delete (embeddingModelProviders[provider][model] as { model?: unknown }) |  | ||||||
|           .model; |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     return Response.json( |  | ||||||
|       { |  | ||||||
|         chatModelProviders, |  | ||||||
|         embeddingModelProviders, |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         status: 200, |  | ||||||
|       }, |  | ||||||
|     ); |  | ||||||
|   } catch (err) { |  | ||||||
|     console.error('An error occurred while fetching models', err); |  | ||||||
|     return Response.json( |  | ||||||
|       { |  | ||||||
|         message: 'An error has occurred.', |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         status: 500, |  | ||||||
|       }, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
							
								
								
									
										94
									
								
								src/app/api/providers/[id]/models/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,94 @@ | |||||||
|  | import ModelRegistry from '@/lib/models/registry'; | ||||||
|  | import { Model } from '@/lib/models/types'; | ||||||
|  | import { NextRequest } from 'next/server'; | ||||||
|  |  | ||||||
|  | export const POST = async ( | ||||||
|  |   req: NextRequest, | ||||||
|  |   { params }: { params: Promise<{ id: string }> }, | ||||||
|  | ) => { | ||||||
|  |   try { | ||||||
|  |     const { id } = await params; | ||||||
|  |  | ||||||
|  |     const body: Partial<Model> & { type: 'embedding' | 'chat' } = | ||||||
|  |       await req.json(); | ||||||
|  |  | ||||||
|  |     if (!body.key || !body.name) { | ||||||
|  |       return Response.json( | ||||||
|  |         { | ||||||
|  |           message: 'Key and name must be provided', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           status: 400, | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const registry = new ModelRegistry(); | ||||||
|  |  | ||||||
|  |     await registry.addProviderModel(id, body.type, body); | ||||||
|  |  | ||||||
|  |     return Response.json( | ||||||
|  |       { | ||||||
|  |         message: 'Model added successfully', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         status: 200, | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } catch (err) { | ||||||
|  |     console.error('An error occurred while adding provider model', err); | ||||||
|  |     return Response.json( | ||||||
|  |       { | ||||||
|  |         message: 'An error has occurred.', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         status: 500, | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const DELETE = async ( | ||||||
|  |   req: NextRequest, | ||||||
|  |   { params }: { params: Promise<{ id: string }> }, | ||||||
|  | ) => { | ||||||
|  |   try { | ||||||
|  |     const { id } = await params; | ||||||
|  |  | ||||||
|  |     const body: { key: string; type: 'embedding' | 'chat' } = await req.json(); | ||||||
|  |  | ||||||
|  |     if (!body.key) { | ||||||
|  |       return Response.json( | ||||||
|  |         { | ||||||
|  |           message: 'Key and name must be provided', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           status: 400, | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const registry = new ModelRegistry(); | ||||||
|  |  | ||||||
|  |     await registry.removeProviderModel(id, body.type, body.key); | ||||||
|  |  | ||||||
|  |     return Response.json( | ||||||
|  |       { | ||||||
|  |         message: 'Model added successfully', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         status: 200, | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } catch (err) { | ||||||
|  |     console.error('An error occurred while deleting provider model', err); | ||||||
|  |     return Response.json( | ||||||
|  |       { | ||||||
|  |         message: 'An error has occurred.', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         status: 500, | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | }; | ||||||
							
								
								
									
										89
									
								
								src/app/api/providers/[id]/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,89 @@ | |||||||
|  | import ModelRegistry from '@/lib/models/registry'; | ||||||
|  | import { NextRequest } from 'next/server'; | ||||||
|  |  | ||||||
|  | export const DELETE = async ( | ||||||
|  |   req: NextRequest, | ||||||
|  |   { params }: { params: Promise<{ id: string }> }, | ||||||
|  | ) => { | ||||||
|  |   try { | ||||||
|  |     const { id } = await params; | ||||||
|  |  | ||||||
|  |     if (!id) { | ||||||
|  |       return Response.json( | ||||||
|  |         { | ||||||
|  |           message: 'Provider ID is required.', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           status: 400, | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const registry = new ModelRegistry(); | ||||||
|  |     await registry.removeProvider(id); | ||||||
|  |  | ||||||
|  |     return Response.json( | ||||||
|  |       { | ||||||
|  |         message: 'Provider deleted successfully.', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         status: 200, | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } catch (err: any) { | ||||||
|  |     console.error('An error occurred while deleting provider', err.message); | ||||||
|  |     return Response.json( | ||||||
|  |       { | ||||||
|  |         message: 'An error has occurred.', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         status: 500, | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const PATCH = async ( | ||||||
|  |   req: NextRequest, | ||||||
|  |   { params }: { params: Promise<{ id: string }> }, | ||||||
|  | ) => { | ||||||
|  |   try { | ||||||
|  |     const body = await req.json(); | ||||||
|  |     const { name, config } = body; | ||||||
|  |     const { id } = await params; | ||||||
|  |  | ||||||
|  |     if (!id || !name || !config) { | ||||||
|  |       return Response.json( | ||||||
|  |         { | ||||||
|  |           message: 'Missing required fields.', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           status: 400, | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const registry = new ModelRegistry(); | ||||||
|  |  | ||||||
|  |     const updatedProvider = await registry.updateProvider(id, name, config); | ||||||
|  |  | ||||||
|  |     return Response.json( | ||||||
|  |       { | ||||||
|  |         provider: updatedProvider, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         status: 200, | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } catch (err: any) { | ||||||
|  |     console.error('An error occurred while updating provider', err.message); | ||||||
|  |     return Response.json( | ||||||
|  |       { | ||||||
|  |         message: 'An error has occurred.', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         status: 500, | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | }; | ||||||
							
								
								
									
										74
									
								
								src/app/api/providers/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,74 @@ | |||||||
|  | import ModelRegistry from '@/lib/models/registry'; | ||||||
|  | import { NextRequest } from 'next/server'; | ||||||
|  |  | ||||||
|  | export const GET = async (req: Request) => { | ||||||
|  |   try { | ||||||
|  |     const registry = new ModelRegistry(); | ||||||
|  |  | ||||||
|  |     const activeProviders = await registry.getActiveProviders(); | ||||||
|  |  | ||||||
|  |     const filteredProviders = activeProviders.filter((p) => { | ||||||
|  |       return !p.chatModels.some((m) => m.key === 'error'); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return Response.json( | ||||||
|  |       { | ||||||
|  |         providers: filteredProviders, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         status: 200, | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } catch (err) { | ||||||
|  |     console.error('An error occurred while fetching providers', err); | ||||||
|  |     return Response.json( | ||||||
|  |       { | ||||||
|  |         message: 'An error has occurred.', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         status: 500, | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const POST = async (req: NextRequest) => { | ||||||
|  |   try { | ||||||
|  |     const body = await req.json(); | ||||||
|  |     const { type, name, config } = body; | ||||||
|  |  | ||||||
|  |     if (!type || !name || !config) { | ||||||
|  |       return Response.json( | ||||||
|  |         { | ||||||
|  |           message: 'Missing required fields.', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           status: 400, | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const registry = new ModelRegistry(); | ||||||
|  |  | ||||||
|  |     const newProvider = await registry.addProvider(type, name, config); | ||||||
|  |  | ||||||
|  |     return Response.json( | ||||||
|  |       { | ||||||
|  |         provider: newProvider, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         status: 200, | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } catch (err) { | ||||||
|  |     console.error('An error occurred while creating provider', err); | ||||||
|  |     return Response.json( | ||||||
|  |       { | ||||||
|  |         message: 'An error has occurred.', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         status: 500, | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | }; | ||||||
| @@ -1,36 +1,14 @@ | |||||||
| import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; |  | ||||||
| import type { Embeddings } from '@langchain/core/embeddings'; |  | ||||||
| import { ChatOpenAI } from '@langchain/openai'; |  | ||||||
| import { |  | ||||||
|   getAvailableChatModelProviders, |  | ||||||
|   getAvailableEmbeddingModelProviders, |  | ||||||
| } from '@/lib/providers'; |  | ||||||
| import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; | import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; | ||||||
| import { MetaSearchAgentType } from '@/lib/search/metaSearchAgent'; | import { MetaSearchAgentType } from '@/lib/search/metaSearchAgent'; | ||||||
| import { |  | ||||||
|   getCustomOpenaiApiKey, |  | ||||||
|   getCustomOpenaiApiUrl, |  | ||||||
|   getCustomOpenaiModelName, |  | ||||||
| } from '@/lib/config'; |  | ||||||
| import { searchHandlers } from '@/lib/search'; | import { searchHandlers } from '@/lib/search'; | ||||||
|  | import ModelRegistry from '@/lib/models/registry'; | ||||||
| interface chatModel { | import { ModelWithProvider } from '@/lib/models/types'; | ||||||
|   provider: string; |  | ||||||
|   name: string; |  | ||||||
|   customOpenAIKey?: string; |  | ||||||
|   customOpenAIBaseURL?: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| interface embeddingModel { |  | ||||||
|   provider: string; |  | ||||||
|   name: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| interface ChatRequestBody { | interface ChatRequestBody { | ||||||
|   optimizationMode: 'speed' | 'balanced'; |   optimizationMode: 'speed' | 'balanced'; | ||||||
|   focusMode: string; |   focusMode: string; | ||||||
|   chatModel?: chatModel; |   chatModel: ModelWithProvider; | ||||||
|   embeddingModel?: embeddingModel; |   embeddingModel: ModelWithProvider; | ||||||
|   query: string; |   query: string; | ||||||
|   history: Array<[string, string]>; |   history: Array<[string, string]>; | ||||||
|   stream?: boolean; |   stream?: boolean; | ||||||
| @@ -58,61 +36,16 @@ export const POST = async (req: Request) => { | |||||||
|         : new AIMessage({ content: msg[1] }); |         : new AIMessage({ content: msg[1] }); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     const [chatModelProviders, embeddingModelProviders] = await Promise.all([ |     const registry = new ModelRegistry(); | ||||||
|       getAvailableChatModelProviders(), |  | ||||||
|       getAvailableEmbeddingModelProviders(), |     const [llm, embeddings] = await Promise.all([ | ||||||
|  |       registry.loadChatModel(body.chatModel.providerId, body.chatModel.key), | ||||||
|  |       registry.loadEmbeddingModel( | ||||||
|  |         body.embeddingModel.providerId, | ||||||
|  |         body.embeddingModel.key, | ||||||
|  |       ), | ||||||
|     ]); |     ]); | ||||||
|  |  | ||||||
|     const chatModelProvider = |  | ||||||
|       body.chatModel?.provider || Object.keys(chatModelProviders)[0]; |  | ||||||
|     const chatModel = |  | ||||||
|       body.chatModel?.name || |  | ||||||
|       Object.keys(chatModelProviders[chatModelProvider])[0]; |  | ||||||
|  |  | ||||||
|     const embeddingModelProvider = |  | ||||||
|       body.embeddingModel?.provider || Object.keys(embeddingModelProviders)[0]; |  | ||||||
|     const embeddingModel = |  | ||||||
|       body.embeddingModel?.name || |  | ||||||
|       Object.keys(embeddingModelProviders[embeddingModelProvider])[0]; |  | ||||||
|  |  | ||||||
|     let llm: BaseChatModel | undefined; |  | ||||||
|     let embeddings: Embeddings | undefined; |  | ||||||
|  |  | ||||||
|     if (body.chatModel?.provider === 'custom_openai') { |  | ||||||
|       llm = new ChatOpenAI({ |  | ||||||
|         modelName: body.chatModel?.name || getCustomOpenaiModelName(), |  | ||||||
|         openAIApiKey: |  | ||||||
|           body.chatModel?.customOpenAIKey || getCustomOpenaiApiKey(), |  | ||||||
|         temperature: 0.7, |  | ||||||
|         configuration: { |  | ||||||
|           baseURL: |  | ||||||
|             body.chatModel?.customOpenAIBaseURL || getCustomOpenaiApiUrl(), |  | ||||||
|         }, |  | ||||||
|       }) as unknown as BaseChatModel; |  | ||||||
|     } else if ( |  | ||||||
|       chatModelProviders[chatModelProvider] && |  | ||||||
|       chatModelProviders[chatModelProvider][chatModel] |  | ||||||
|     ) { |  | ||||||
|       llm = chatModelProviders[chatModelProvider][chatModel] |  | ||||||
|         .model as unknown as BaseChatModel | undefined; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if ( |  | ||||||
|       embeddingModelProviders[embeddingModelProvider] && |  | ||||||
|       embeddingModelProviders[embeddingModelProvider][embeddingModel] |  | ||||||
|     ) { |  | ||||||
|       embeddings = embeddingModelProviders[embeddingModelProvider][ |  | ||||||
|         embeddingModel |  | ||||||
|       ].model as Embeddings | undefined; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!llm || !embeddings) { |  | ||||||
|       return Response.json( |  | ||||||
|         { message: 'Invalid model selected' }, |  | ||||||
|         { status: 400 }, |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const searchHandler: MetaSearchAgentType = searchHandlers[body.focusMode]; |     const searchHandler: MetaSearchAgentType = searchHandlers[body.focusMode]; | ||||||
|  |  | ||||||
|     if (!searchHandler) { |     if (!searchHandler) { | ||||||
|   | |||||||
| @@ -1,22 +1,12 @@ | |||||||
| import generateSuggestions from '@/lib/chains/suggestionGeneratorAgent'; | import generateSuggestions from '@/lib/chains/suggestionGeneratorAgent'; | ||||||
| import { | import ModelRegistry from '@/lib/models/registry'; | ||||||
|   getCustomOpenaiApiKey, | import { ModelWithProvider } from '@/lib/models/types'; | ||||||
|   getCustomOpenaiApiUrl, |  | ||||||
|   getCustomOpenaiModelName, |  | ||||||
| } from '@/lib/config'; |  | ||||||
| import { getAvailableChatModelProviders } from '@/lib/providers'; |  | ||||||
| import { BaseChatModel } from '@langchain/core/language_models/chat_models'; | import { BaseChatModel } from '@langchain/core/language_models/chat_models'; | ||||||
| import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; | import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; | ||||||
| import { ChatOpenAI } from '@langchain/openai'; |  | ||||||
|  |  | ||||||
| interface ChatModel { |  | ||||||
|   provider: string; |  | ||||||
|   model: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| interface SuggestionsGenerationBody { | interface SuggestionsGenerationBody { | ||||||
|   chatHistory: any[]; |   chatHistory: any[]; | ||||||
|   chatModel?: ChatModel; |   chatModel: ModelWithProvider; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const POST = async (req: Request) => { | export const POST = async (req: Request) => { | ||||||
| @@ -33,35 +23,12 @@ export const POST = async (req: Request) => { | |||||||
|       }) |       }) | ||||||
|       .filter((msg) => msg !== undefined) as BaseMessage[]; |       .filter((msg) => msg !== undefined) as BaseMessage[]; | ||||||
|  |  | ||||||
|     const chatModelProviders = await getAvailableChatModelProviders(); |     const registry = new ModelRegistry(); | ||||||
|  |  | ||||||
|     const chatModelProvider = |     const llm = await registry.loadChatModel( | ||||||
|       chatModelProviders[ |       body.chatModel.providerId, | ||||||
|         body.chatModel?.provider || Object.keys(chatModelProviders)[0] |       body.chatModel.key, | ||||||
|       ]; |     ); | ||||||
|     const chatModel = |  | ||||||
|       chatModelProvider[ |  | ||||||
|         body.chatModel?.model || Object.keys(chatModelProvider)[0] |  | ||||||
|       ]; |  | ||||||
|  |  | ||||||
|     let llm: BaseChatModel | undefined; |  | ||||||
|  |  | ||||||
|     if (body.chatModel?.provider === 'custom_openai') { |  | ||||||
|       llm = new ChatOpenAI({ |  | ||||||
|         openAIApiKey: getCustomOpenaiApiKey(), |  | ||||||
|         modelName: getCustomOpenaiModelName(), |  | ||||||
|         temperature: 0.7, |  | ||||||
|         configuration: { |  | ||||||
|           baseURL: getCustomOpenaiApiUrl(), |  | ||||||
|         }, |  | ||||||
|       }) as unknown as BaseChatModel; |  | ||||||
|     } else if (chatModelProvider && chatModel) { |  | ||||||
|       llm = chatModel.model; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!llm) { |  | ||||||
|       return Response.json({ error: 'Invalid chat model' }, { status: 400 }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const suggestions = await generateSuggestions( |     const suggestions = await generateSuggestions( | ||||||
|       { |       { | ||||||
|   | |||||||
| @@ -2,11 +2,11 @@ import { NextResponse } from 'next/server'; | |||||||
| import fs from 'fs'; | import fs from 'fs'; | ||||||
| import path from 'path'; | import path from 'path'; | ||||||
| import crypto from 'crypto'; | import crypto from 'crypto'; | ||||||
| import { getAvailableEmbeddingModelProviders } from '@/lib/providers'; |  | ||||||
| import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'; | import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'; | ||||||
| import { DocxLoader } from '@langchain/community/document_loaders/fs/docx'; | import { DocxLoader } from '@langchain/community/document_loaders/fs/docx'; | ||||||
| import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'; | import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'; | ||||||
| import { Document } from 'langchain/document'; | import { Document } from 'langchain/document'; | ||||||
|  | import ModelRegistry from '@/lib/models/registry'; | ||||||
|  |  | ||||||
| interface FileRes { | interface FileRes { | ||||||
|   fileName: string; |   fileName: string; | ||||||
| @@ -30,8 +30,8 @@ export async function POST(req: Request) { | |||||||
|     const formData = await req.formData(); |     const formData = await req.formData(); | ||||||
|  |  | ||||||
|     const files = formData.getAll('files') as File[]; |     const files = formData.getAll('files') as File[]; | ||||||
|     const embedding_model = formData.get('embedding_model'); |     const embedding_model = formData.get('embedding_model_key') as string; | ||||||
|     const embedding_model_provider = formData.get('embedding_model_provider'); |     const embedding_model_provider = formData.get('embedding_model_provider_id') as string; | ||||||
|  |  | ||||||
|     if (!embedding_model || !embedding_model_provider) { |     if (!embedding_model || !embedding_model_provider) { | ||||||
|       return NextResponse.json( |       return NextResponse.json( | ||||||
| @@ -40,20 +40,9 @@ export async function POST(req: Request) { | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const embeddingModels = await getAvailableEmbeddingModelProviders(); |     const registry = new ModelRegistry(); | ||||||
|     const provider = |  | ||||||
|       embedding_model_provider ?? Object.keys(embeddingModels)[0]; |  | ||||||
|     const embeddingModel = |  | ||||||
|       embedding_model ?? Object.keys(embeddingModels[provider as string])[0]; |  | ||||||
|  |  | ||||||
|     let embeddingsModel = |     const model = await registry.loadEmbeddingModel(embedding_model_provider, embedding_model); | ||||||
|       embeddingModels[provider as string]?.[embeddingModel as string]?.model; |  | ||||||
|     if (!embeddingsModel) { |  | ||||||
|       return NextResponse.json( |  | ||||||
|         { message: 'Invalid embedding model selected' }, |  | ||||||
|         { status: 400 }, |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const processedFiles: FileRes[] = []; |     const processedFiles: FileRes[] = []; | ||||||
|  |  | ||||||
| @@ -98,7 +87,7 @@ export async function POST(req: Request) { | |||||||
|           }), |           }), | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         const embeddings = await embeddingsModel.embedDocuments( |         const embeddings = await model.embedDocuments( | ||||||
|           splitted.map((doc) => doc.pageContent), |           splitted.map((doc) => doc.pageContent), | ||||||
|         ); |         ); | ||||||
|         const embeddingsDataPath = filePath.replace( |         const embeddingsDataPath = filePath.replace( | ||||||
|   | |||||||
| @@ -1,23 +1,12 @@ | |||||||
| import handleVideoSearch from '@/lib/chains/videoSearchAgent'; | import handleVideoSearch from '@/lib/chains/videoSearchAgent'; | ||||||
| import { | import ModelRegistry from '@/lib/models/registry'; | ||||||
|   getCustomOpenaiApiKey, | import { ModelWithProvider } from '@/lib/models/types'; | ||||||
|   getCustomOpenaiApiUrl, |  | ||||||
|   getCustomOpenaiModelName, |  | ||||||
| } from '@/lib/config'; |  | ||||||
| import { getAvailableChatModelProviders } from '@/lib/providers'; |  | ||||||
| import { BaseChatModel } from '@langchain/core/language_models/chat_models'; |  | ||||||
| import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; | import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; | ||||||
| import { ChatOpenAI } from '@langchain/openai'; |  | ||||||
|  |  | ||||||
| interface ChatModel { |  | ||||||
|   provider: string; |  | ||||||
|   model: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| interface VideoSearchBody { | interface VideoSearchBody { | ||||||
|   query: string; |   query: string; | ||||||
|   chatHistory: any[]; |   chatHistory: any[]; | ||||||
|   chatModel?: ChatModel; |   chatModel: ModelWithProvider; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const POST = async (req: Request) => { | export const POST = async (req: Request) => { | ||||||
| @@ -34,35 +23,12 @@ export const POST = async (req: Request) => { | |||||||
|       }) |       }) | ||||||
|       .filter((msg) => msg !== undefined) as BaseMessage[]; |       .filter((msg) => msg !== undefined) as BaseMessage[]; | ||||||
|  |  | ||||||
|     const chatModelProviders = await getAvailableChatModelProviders(); |     const registry = new ModelRegistry(); | ||||||
|  |  | ||||||
|     const chatModelProvider = |     const llm = await registry.loadChatModel( | ||||||
|       chatModelProviders[ |       body.chatModel.providerId, | ||||||
|         body.chatModel?.provider || Object.keys(chatModelProviders)[0] |       body.chatModel.key, | ||||||
|       ]; |     ); | ||||||
|     const chatModel = |  | ||||||
|       chatModelProvider[ |  | ||||||
|         body.chatModel?.model || Object.keys(chatModelProvider)[0] |  | ||||||
|       ]; |  | ||||||
|  |  | ||||||
|     let llm: BaseChatModel | undefined; |  | ||||||
|  |  | ||||||
|     if (body.chatModel?.provider === 'custom_openai') { |  | ||||||
|       llm = new ChatOpenAI({ |  | ||||||
|         openAIApiKey: getCustomOpenaiApiKey(), |  | ||||||
|         modelName: getCustomOpenaiModelName(), |  | ||||||
|         temperature: 0.7, |  | ||||||
|         configuration: { |  | ||||||
|           baseURL: getCustomOpenaiApiUrl(), |  | ||||||
|         }, |  | ||||||
|       }) as unknown as BaseChatModel; |  | ||||||
|     } else if (chatModelProvider && chatModel) { |  | ||||||
|       llm = chatModel.model; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!llm) { |  | ||||||
|       return Response.json({ error: 'Invalid chat model' }, { status: 400 }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const videos = await handleVideoSearch( |     const videos = await handleVideoSearch( | ||||||
|       { |       { | ||||||
|   | |||||||
							
								
								
									
										174
									
								
								src/app/api/weather/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,174 @@ | |||||||
|  | export const POST = async (req: Request) => { | ||||||
|  |   try { | ||||||
|  |     const body: { | ||||||
|  |       lat: number; | ||||||
|  |       lng: number; | ||||||
|  |       measureUnit: 'Imperial' | 'Metric'; | ||||||
|  |     } = await req.json(); | ||||||
|  |  | ||||||
|  |     if (!body.lat || !body.lng) { | ||||||
|  |       return Response.json( | ||||||
|  |         { | ||||||
|  |           message: 'Invalid request.', | ||||||
|  |         }, | ||||||
|  |         { status: 400 }, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const res = await fetch( | ||||||
|  |       `https://api.open-meteo.com/v1/forecast?latitude=${body.lat}&longitude=${body.lng}¤t=weather_code,temperature_2m,is_day,relative_humidity_2m,wind_speed_10m&timezone=auto${ | ||||||
|  |         body.measureUnit === 'Metric' ? '' : '&temperature_unit=fahrenheit' | ||||||
|  |       }${body.measureUnit === 'Metric' ? '' : '&wind_speed_unit=mph'}`, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const data = await res.json(); | ||||||
|  |  | ||||||
|  |     if (data.error) { | ||||||
|  |       console.error(`Error fetching weather data: ${data.reason}`); | ||||||
|  |       return Response.json( | ||||||
|  |         { | ||||||
|  |           message: 'An error has occurred.', | ||||||
|  |         }, | ||||||
|  |         { status: 500 }, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const weather: { | ||||||
|  |       temperature: number; | ||||||
|  |       condition: string; | ||||||
|  |       humidity: number; | ||||||
|  |       windSpeed: number; | ||||||
|  |       icon: string; | ||||||
|  |       temperatureUnit: 'C' | 'F'; | ||||||
|  |       windSpeedUnit: 'm/s' | 'mph'; | ||||||
|  |     } = { | ||||||
|  |       temperature: data.current.temperature_2m, | ||||||
|  |       condition: '', | ||||||
|  |       humidity: data.current.relative_humidity_2m, | ||||||
|  |       windSpeed: data.current.wind_speed_10m, | ||||||
|  |       icon: '', | ||||||
|  |       temperatureUnit: body.measureUnit === 'Metric' ? 'C' : 'F', | ||||||
|  |       windSpeedUnit: body.measureUnit === 'Metric' ? 'm/s' : 'mph', | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const code = data.current.weather_code; | ||||||
|  |     const isDay = data.current.is_day === 1; | ||||||
|  |     const dayOrNight = isDay ? 'day' : 'night'; | ||||||
|  |  | ||||||
|  |     switch (code) { | ||||||
|  |       case 0: | ||||||
|  |         weather.icon = `clear-${dayOrNight}`; | ||||||
|  |         weather.condition = 'Clear'; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case 1: | ||||||
|  |         weather.condition = 'Mainly Clear'; | ||||||
|  |       case 2: | ||||||
|  |         weather.condition = 'Partly Cloudy'; | ||||||
|  |       case 3: | ||||||
|  |         weather.icon = `cloudy-1-${dayOrNight}`; | ||||||
|  |         weather.condition = 'Cloudy'; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case 45: | ||||||
|  |         weather.condition = 'Fog'; | ||||||
|  |       case 48: | ||||||
|  |         weather.icon = `fog-${dayOrNight}`; | ||||||
|  |         weather.condition = 'Fog'; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case 51: | ||||||
|  |         weather.condition = 'Light Drizzle'; | ||||||
|  |       case 53: | ||||||
|  |         weather.condition = 'Moderate Drizzle'; | ||||||
|  |       case 55: | ||||||
|  |         weather.icon = `rainy-1-${dayOrNight}`; | ||||||
|  |         weather.condition = 'Dense Drizzle'; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case 56: | ||||||
|  |         weather.condition = 'Light Freezing Drizzle'; | ||||||
|  |       case 57: | ||||||
|  |         weather.icon = `frost-${dayOrNight}`; | ||||||
|  |         weather.condition = 'Dense Freezing Drizzle'; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case 61: | ||||||
|  |         weather.condition = 'Slight Rain'; | ||||||
|  |       case 63: | ||||||
|  |         weather.condition = 'Moderate Rain'; | ||||||
|  |       case 65: | ||||||
|  |         weather.condition = 'Heavy Rain'; | ||||||
|  |         weather.icon = `rainy-2-${dayOrNight}`; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case 66: | ||||||
|  |         weather.condition = 'Light Freezing Rain'; | ||||||
|  |       case 67: | ||||||
|  |         weather.condition = 'Heavy Freezing Rain'; | ||||||
|  |         weather.icon = 'rain-and-sleet-mix'; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case 71: | ||||||
|  |         weather.condition = 'Slight Snow Fall'; | ||||||
|  |       case 73: | ||||||
|  |         weather.condition = 'Moderate Snow Fall'; | ||||||
|  |       case 75: | ||||||
|  |         weather.condition = 'Heavy Snow Fall'; | ||||||
|  |         weather.icon = `snowy-2-${dayOrNight}`; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case 77: | ||||||
|  |         weather.condition = 'Snow'; | ||||||
|  |         weather.icon = `snowy-1-${dayOrNight}`; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case 80: | ||||||
|  |         weather.condition = 'Slight Rain Showers'; | ||||||
|  |       case 81: | ||||||
|  |         weather.condition = 'Moderate Rain Showers'; | ||||||
|  |       case 82: | ||||||
|  |         weather.condition = 'Heavy Rain Showers'; | ||||||
|  |         weather.icon = `rainy-3-${dayOrNight}`; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case 85: | ||||||
|  |         weather.condition = 'Slight Snow Showers'; | ||||||
|  |       case 86: | ||||||
|  |         weather.condition = 'Moderate Snow Showers'; | ||||||
|  |       case 87: | ||||||
|  |         weather.condition = 'Heavy Snow Showers'; | ||||||
|  |         weather.icon = `snowy-3-${dayOrNight}`; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case 95: | ||||||
|  |         weather.condition = 'Thunderstorm'; | ||||||
|  |         weather.icon = `scattered-thunderstorms-${dayOrNight}`; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case 96: | ||||||
|  |         weather.condition = 'Thunderstorm with Slight Hail'; | ||||||
|  |       case 99: | ||||||
|  |         weather.condition = 'Thunderstorm with Heavy Hail'; | ||||||
|  |         weather.icon = 'severe-thunderstorm'; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       default: | ||||||
|  |         weather.icon = `clear-${dayOrNight}`; | ||||||
|  |         weather.condition = 'Clear'; | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return Response.json(weather); | ||||||
|  |   } catch (err) { | ||||||
|  |     console.error('An error occurred while getting home widgets', err); | ||||||
|  |     return Response.json( | ||||||
|  |       { | ||||||
|  |         message: 'An error has occurred.', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         status: 500, | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | }; | ||||||
| @@ -1,9 +1,17 @@ | |||||||
| import ChatWindow from '@/components/ChatWindow'; | 'use client'; | ||||||
| import React from 'react'; |  | ||||||
|  |  | ||||||
| const Page = ({ params }: { params: Promise<{ chatId: string }> }) => { | import ChatWindow from '@/components/ChatWindow'; | ||||||
|   const { chatId } = React.use(params); | import { useParams } from 'next/navigation'; | ||||||
|   return <ChatWindow id={chatId} />; | import React from 'react'; | ||||||
|  | import { ChatProvider } from '@/lib/hooks/useChat'; | ||||||
|  |  | ||||||
|  | const Page = () => { | ||||||
|  |   const { chatId }: { chatId: string } = useParams(); | ||||||
|  |   return ( | ||||||
|  |     <ChatProvider id={chatId}> | ||||||
|  |       <ChatWindow /> | ||||||
|  |     </ChatProvider> | ||||||
|  |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default Page; | export default Page; | ||||||
|   | |||||||
| @@ -1,110 +1,268 @@ | |||||||
| 'use client'; | 'use client'; | ||||||
|  |  | ||||||
| import { Search } from 'lucide-react'; | import { Globe2Icon } from 'lucide-react'; | ||||||
| import { useEffect, useState } from 'react'; | import { useEffect, useState } from 'react'; | ||||||
| import Link from 'next/link'; |  | ||||||
| import { toast } from 'sonner'; | import { toast } from 'sonner'; | ||||||
|  | import { cn } from '@/lib/utils'; | ||||||
|  | import SmallNewsCard from '@/components/Discover/SmallNewsCard'; | ||||||
|  | import MajorNewsCard from '@/components/Discover/MajorNewsCard'; | ||||||
|  |  | ||||||
| interface Discover { | export interface Discover { | ||||||
|   title: string; |   title: string; | ||||||
|   content: string; |   content: string; | ||||||
|   url: string; |   url: string; | ||||||
|   thumbnail: string; |   thumbnail: string; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | const topics: { key: string; display: string }[] = [ | ||||||
|  |   { | ||||||
|  |     display: 'Tech & Science', | ||||||
|  |     key: 'tech', | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     display: 'Finance', | ||||||
|  |     key: 'finance', | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     display: 'Art & Culture', | ||||||
|  |     key: 'art', | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     display: 'Sports', | ||||||
|  |     key: 'sports', | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     display: 'Entertainment', | ||||||
|  |     key: 'entertainment', | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
| const Page = () => { | const Page = () => { | ||||||
|   const [discover, setDiscover] = useState<Discover[] | null>(null); |   const [discover, setDiscover] = useState<Discover[] | null>(null); | ||||||
|   const [loading, setLoading] = useState(true); |   const [loading, setLoading] = useState(true); | ||||||
|  |   const [activeTopic, setActiveTopic] = useState<string>(topics[0].key); | ||||||
|  |  | ||||||
|  |   const fetchArticles = async (topic: string) => { | ||||||
|  |     setLoading(true); | ||||||
|  |     try { | ||||||
|  |       const res = await fetch(`/api/discover?topic=${topic}`, { | ||||||
|  |         method: 'GET', | ||||||
|  |         headers: { | ||||||
|  |           'Content-Type': 'application/json', | ||||||
|  |         }, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       const data = await res.json(); | ||||||
|  |  | ||||||
|  |       if (!res.ok) { | ||||||
|  |         throw new Error(data.message); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       data.blogs = data.blogs.filter((blog: Discover) => blog.thumbnail); | ||||||
|  |  | ||||||
|  |       setDiscover(data.blogs); | ||||||
|  |     } catch (err: any) { | ||||||
|  |       console.error('Error fetching data:', err.message); | ||||||
|  |       toast.error('Error fetching data'); | ||||||
|  |     } finally { | ||||||
|  |       setLoading(false); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     const fetchData = async () => { |     fetchArticles(activeTopic); | ||||||
|       try { |   }, [activeTopic]); | ||||||
|         const res = await fetch(`/api/discover`, { |  | ||||||
|           method: 'GET', |  | ||||||
|           headers: { |  | ||||||
|             'Content-Type': 'application/json', |  | ||||||
|           }, |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         const data = await res.json(); |   return ( | ||||||
|  |  | ||||||
|         if (!res.ok) { |  | ||||||
|           throw new Error(data.message); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         data.blogs = data.blogs.filter((blog: Discover) => blog.thumbnail); |  | ||||||
|  |  | ||||||
|         setDiscover(data.blogs); |  | ||||||
|       } catch (err: any) { |  | ||||||
|         console.error('Error fetching data:', err.message); |  | ||||||
|         toast.error('Error fetching data'); |  | ||||||
|       } finally { |  | ||||||
|         setLoading(false); |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     fetchData(); |  | ||||||
|   }, []); |  | ||||||
|  |  | ||||||
|   return loading ? ( |  | ||||||
|     <div className="flex flex-row items-center justify-center min-h-screen"> |  | ||||||
|       <svg |  | ||||||
|         aria-hidden="true" |  | ||||||
|         className="w-8 h-8 text-light-200 fill-light-secondary dark:text-[#202020] animate-spin dark:fill-[#ffffff3b]" |  | ||||||
|         viewBox="0 0 100 101" |  | ||||||
|         fill="none" |  | ||||||
|         xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       > |  | ||||||
|         <path |  | ||||||
|           d="M100 50.5908C100.003 78.2051 78.1951 100.003 50.5908 100C22.9765 99.9972 0.997224 78.018 1 50.4037C1.00281 22.7993 22.8108 0.997224 50.4251 1C78.0395 1.00281 100.018 22.8108 100 50.4251ZM9.08164 50.594C9.06312 73.3997 27.7909 92.1272 50.5966 92.1457C73.4023 92.1642 92.1298 73.4365 92.1483 50.6308C92.1669 27.8251 73.4392 9.0973 50.6335 9.07878C27.8278 9.06026 9.10003 27.787 9.08164 50.594Z" |  | ||||||
|           fill="currentColor" |  | ||||||
|         /> |  | ||||||
|         <path |  | ||||||
|           d="M93.9676 39.0409C96.393 38.4037 97.8624 35.9116 96.9801 33.5533C95.1945 28.8227 92.871 24.3692 90.0681 20.348C85.6237 14.1775 79.4473 9.36872 72.0454 6.45794C64.6435 3.54717 56.3134 2.65431 48.3133 3.89319C45.869 4.27179 44.3768 6.77534 45.014 9.20079C45.6512 11.6262 48.1343 13.0956 50.5786 12.717C56.5073 11.8281 62.5542 12.5399 68.0406 14.7911C73.527 17.0422 78.2187 20.7487 81.5841 25.4923C83.7976 28.5886 85.4467 32.059 86.4416 35.7474C87.1273 38.1189 89.5423 39.6781 91.9676 39.0409Z" |  | ||||||
|           fill="currentFill" |  | ||||||
|         /> |  | ||||||
|       </svg> |  | ||||||
|     </div> |  | ||||||
|   ) : ( |  | ||||||
|     <> |     <> | ||||||
|       <div> |       <div> | ||||||
|         <div className="flex flex-col pt-4"> |         <div className="flex flex-col pt-10 border-b border-light-200/20 dark:border-dark-200/20 pb-6 px-2"> | ||||||
|           <div className="flex items-center"> |           <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4"> | ||||||
|             <Search /> |             <div className="flex items-center justify-center"> | ||||||
|             <h1 className="text-3xl font-medium p-2">Discover</h1> |               <Globe2Icon size={45} className="mb-2.5" /> | ||||||
|  |               <h1 | ||||||
|  |                 className="text-5xl font-normal p-2" | ||||||
|  |                 style={{ fontFamily: 'PP Editorial, serif' }} | ||||||
|  |               > | ||||||
|  |                 Discover | ||||||
|  |               </h1> | ||||||
|  |             </div> | ||||||
|  |             <div className="flex flex-row items-center space-x-2 overflow-x-auto"> | ||||||
|  |               {topics.map((t, i) => ( | ||||||
|  |                 <div | ||||||
|  |                   key={i} | ||||||
|  |                   className={cn( | ||||||
|  |                     'border-[0.1px] rounded-full text-sm px-3 py-1 text-nowrap transition duration-200 cursor-pointer', | ||||||
|  |                     activeTopic === t.key | ||||||
|  |                       ? 'text-cyan-700 dark:text-cyan-300 bg-cyan-300/20 border-cyan-700/60 dar:bg-cyan-300/30 dark:border-cyan-300/40' | ||||||
|  |                       : 'border-black/30 dark:border-white/30 text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white hover:border-black/40 dark:hover:border-white/40 hover:bg-black/5 dark:hover:bg-white/5', | ||||||
|  |                   )} | ||||||
|  |                   onClick={() => setActiveTopic(t.key)} | ||||||
|  |                 > | ||||||
|  |                   <span>{t.display}</span> | ||||||
|  |                 </div> | ||||||
|  |               ))} | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|           <hr className="border-t border-[#2B2C2C] my-4 w-full" /> |  | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-4 pb-28 lg:pb-8 w-full justify-items-center lg:justify-items-start"> |         {loading ? ( | ||||||
|           {discover && |           <div className="flex flex-row items-center justify-center min-h-screen"> | ||||||
|             discover?.map((item, i) => ( |             <svg | ||||||
|               <Link |               aria-hidden="true" | ||||||
|                 href={`/?q=Summary: ${item.url}`} |               className="w-8 h-8 text-light-200 fill-light-secondary dark:text-[#202020] animate-spin dark:fill-[#ffffff3b]" | ||||||
|                 key={i} |               viewBox="0 0 100 101" | ||||||
|                 className="max-w-sm rounded-lg overflow-hidden bg-light-secondary dark:bg-dark-secondary hover:-translate-y-[1px] transition duration-200" |               fill="none" | ||||||
|                 target="_blank" |               xmlns="http://www.w3.org/2000/svg" | ||||||
|               > |             > | ||||||
|                 <img |               <path | ||||||
|                   className="object-cover w-full aspect-video" |                 d="M100 50.5908C100.003 78.2051 78.1951 100.003 50.5908 100C22.9765 99.9972 0.997224 78.018 1 50.4037C1.00281 22.7993 22.8108 0.997224 50.4251 1C78.0395 1.00281 100.018 22.8108 100 50.4251ZM9.08164 50.594C9.06312 73.3997 27.7909 92.1272 50.5966 92.1457C73.4023 92.1642 92.1298 73.4365 92.1483 50.6308C92.1669 27.8251 73.4392 9.0973 50.6335 9.07878C27.8278 9.06026 9.10003 27.787 9.08164 50.594Z" | ||||||
|                   src={ |                 fill="currentColor" | ||||||
|                     new URL(item.thumbnail).origin + |               /> | ||||||
|                     new URL(item.thumbnail).pathname + |               <path | ||||||
|                     `?id=${new URL(item.thumbnail).searchParams.get('id')}` |                 d="M93.9676 39.0409C96.393 38.4037 97.8624 35.9116 96.9801 33.5533C95.1945 28.8227 92.871 24.3692 90.0681 20.348C85.6237 14.1775 79.4473 9.36872 72.0454 6.45794C64.6435 3.54717 56.3134 2.65431 48.3133 3.89319C45.869 4.27179 44.3768 6.77534 45.014 9.20079C45.6512 11.6262 48.1343 13.0956 50.5786 12.717C56.5073 11.8281 62.5542 12.5399 68.0406 14.7911C73.527 17.0422 78.2187 20.7487 81.5841 25.4923C83.7976 28.5886 85.4467 32.059 86.4416 35.7474C87.1273 38.1189 89.5423 39.6781 91.9676 39.0409Z" | ||||||
|  |                 fill="currentFill" | ||||||
|  |               /> | ||||||
|  |             </svg> | ||||||
|  |           </div> | ||||||
|  |         ) : ( | ||||||
|  |           <div className="flex flex-col gap-4 pb-28 pt-5 lg:pb-8 w-full"> | ||||||
|  |             <div className="block lg:hidden"> | ||||||
|  |               <div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> | ||||||
|  |                 {discover?.map((item, i) => ( | ||||||
|  |                   <SmallNewsCard key={`mobile-${i}`} item={item} /> | ||||||
|  |                 ))} | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |  | ||||||
|  |             <div className="hidden lg:block"> | ||||||
|  |               {discover && | ||||||
|  |                 discover.length > 0 && | ||||||
|  |                 (() => { | ||||||
|  |                   const sections = []; | ||||||
|  |                   let index = 0; | ||||||
|  |  | ||||||
|  |                   while (index < discover.length) { | ||||||
|  |                     if (sections.length > 0) { | ||||||
|  |                       sections.push( | ||||||
|  |                         <hr | ||||||
|  |                           key={`sep-${index}`} | ||||||
|  |                           className="border-t border-light-200/20 dark:border-dark-200/20 my-3 w-full" | ||||||
|  |                         />, | ||||||
|  |                       ); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if (index < discover.length) { | ||||||
|  |                       sections.push( | ||||||
|  |                         <MajorNewsCard | ||||||
|  |                           key={`major-${index}`} | ||||||
|  |                           item={discover[index]} | ||||||
|  |                           isLeft={false} | ||||||
|  |                         />, | ||||||
|  |                       ); | ||||||
|  |                       index++; | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if (index < discover.length) { | ||||||
|  |                       sections.push( | ||||||
|  |                         <hr | ||||||
|  |                           key={`sep-${index}-after`} | ||||||
|  |                           className="border-t border-light-200/20 dark:border-dark-200/20 my-3 w-full" | ||||||
|  |                         />, | ||||||
|  |                       ); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if (index < discover.length) { | ||||||
|  |                       const smallCards = discover.slice(index, index + 3); | ||||||
|  |                       sections.push( | ||||||
|  |                         <div | ||||||
|  |                           key={`small-group-${index}`} | ||||||
|  |                           className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-4" | ||||||
|  |                         > | ||||||
|  |                           {smallCards.map((item, i) => ( | ||||||
|  |                             <SmallNewsCard | ||||||
|  |                               key={`small-${index + i}`} | ||||||
|  |                               item={item} | ||||||
|  |                             /> | ||||||
|  |                           ))} | ||||||
|  |                         </div>, | ||||||
|  |                       ); | ||||||
|  |                       index += 3; | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if (index < discover.length) { | ||||||
|  |                       sections.push( | ||||||
|  |                         <hr | ||||||
|  |                           key={`sep-${index}-after-small`} | ||||||
|  |                           className="border-t border-light-200/20 dark:border-dark-200/20 my-3 w-full" | ||||||
|  |                         />, | ||||||
|  |                       ); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if (index < discover.length - 1) { | ||||||
|  |                       const twoMajorCards = discover.slice(index, index + 2); | ||||||
|  |                       twoMajorCards.forEach((item, i) => { | ||||||
|  |                         sections.push( | ||||||
|  |                           <MajorNewsCard | ||||||
|  |                             key={`double-${index + i}`} | ||||||
|  |                             item={item} | ||||||
|  |                             isLeft={i === 0} | ||||||
|  |                           />, | ||||||
|  |                         ); | ||||||
|  |                         if (i === 0) { | ||||||
|  |                           sections.push( | ||||||
|  |                             <hr | ||||||
|  |                               key={`sep-double-${index + i}`} | ||||||
|  |                               className="border-t border-light-200/20 dark:border-dark-200/20 my-3 w-full" | ||||||
|  |                             />, | ||||||
|  |                           ); | ||||||
|  |                         } | ||||||
|  |                       }); | ||||||
|  |                       index += 2; | ||||||
|  |                     } else if (index < discover.length) { | ||||||
|  |                       sections.push( | ||||||
|  |                         <MajorNewsCard | ||||||
|  |                           key={`final-major-${index}`} | ||||||
|  |                           item={discover[index]} | ||||||
|  |                           isLeft={true} | ||||||
|  |                         />, | ||||||
|  |                       ); | ||||||
|  |                       index++; | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if (index < discover.length) { | ||||||
|  |                       sections.push( | ||||||
|  |                         <hr | ||||||
|  |                           key={`sep-${index}-after-major`} | ||||||
|  |                           className="border-t border-light-200/20 dark:border-dark-200/20 my-3 w-full" | ||||||
|  |                         />, | ||||||
|  |                       ); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if (index < discover.length) { | ||||||
|  |                       const smallCards = discover.slice(index, index + 3); | ||||||
|  |                       sections.push( | ||||||
|  |                         <div | ||||||
|  |                           key={`small-group-2-${index}`} | ||||||
|  |                           className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-4" | ||||||
|  |                         > | ||||||
|  |                           {smallCards.map((item, i) => ( | ||||||
|  |                             <SmallNewsCard | ||||||
|  |                               key={`small-2-${index + i}`} | ||||||
|  |                               item={item} | ||||||
|  |                             /> | ||||||
|  |                           ))} | ||||||
|  |                         </div>, | ||||||
|  |                       ); | ||||||
|  |                       index += 3; | ||||||
|  |                     } | ||||||
|                   } |                   } | ||||||
|                   alt={item.title} |  | ||||||
|                 /> |                   return sections; | ||||||
|                 <div className="px-6 py-4"> |                 })()} | ||||||
|                   <div className="font-bold text-lg mb-2"> |             </div> | ||||||
|                     {item.title.slice(0, 100)}... |           </div> | ||||||
|                   </div> |         )} | ||||||
|                   <p className="text-black-70 dark:text-white/70 text-sm"> |  | ||||||
|                     {item.content.slice(0, 100)}... |  | ||||||
|                   </p> |  | ||||||
|                 </div> |  | ||||||
|               </Link> |  | ||||||
|             ))} |  | ||||||
|         </div> |  | ||||||
|       </div> |       </div> | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
|   | |||||||
| @@ -2,6 +2,14 @@ | |||||||
| @tailwind components; | @tailwind components; | ||||||
| @tailwind utilities; | @tailwind utilities; | ||||||
|  |  | ||||||
|  | @font-face { | ||||||
|  |   font-family: 'PP Editorial'; | ||||||
|  |   src: url('/fonts/pp-ed-ul.otf') format('opentype'); | ||||||
|  |   font-weight: 300; | ||||||
|  |   font-style: normal; | ||||||
|  |   font-display: swap; | ||||||
|  | } | ||||||
|  |  | ||||||
| @layer base { | @layer base { | ||||||
|   .overflow-hidden-scrollable { |   .overflow-hidden-scrollable { | ||||||
|     -ms-overflow-style: none; |     -ms-overflow-style: none; | ||||||
| @@ -10,4 +18,82 @@ | |||||||
|   .overflow-hidden-scrollable::-webkit-scrollbar { |   .overflow-hidden-scrollable::-webkit-scrollbar { | ||||||
|     display: none; |     display: none; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   * { | ||||||
|  |     scrollbar-width: thin; | ||||||
|  |     scrollbar-color: #e8edf1 transparent; /* light-200 */ | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   *::-webkit-scrollbar { | ||||||
|  |     width: 6px; | ||||||
|  |     height: 6px; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   *::-webkit-scrollbar-track { | ||||||
|  |     background: transparent; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   *::-webkit-scrollbar-thumb { | ||||||
|  |     background: #e8edf1; /* light-200 */ | ||||||
|  |     border-radius: 3px; | ||||||
|  |     transition: background 0.2s ease; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   *::-webkit-scrollbar-thumb:hover { | ||||||
|  |     background: #d0d7de; /* light-300 */ | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @media (prefers-color-scheme: dark) { | ||||||
|  |     * { | ||||||
|  |       scrollbar-color: #21262d transparent; /* dark-200 */ | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     *::-webkit-scrollbar-thumb { | ||||||
|  |       background: #21262d; /* dark-200 */ | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     *::-webkit-scrollbar-thumb:hover { | ||||||
|  |       background: #30363d; /* dark-300 */ | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   :root.dark *, | ||||||
|  |   html.dark *, | ||||||
|  |   body.dark * { | ||||||
|  |     scrollbar-color: #21262d transparent; /* dark-200 */ | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   :root.dark *::-webkit-scrollbar-thumb, | ||||||
|  |   html.dark *::-webkit-scrollbar-thumb, | ||||||
|  |   body.dark *::-webkit-scrollbar-thumb { | ||||||
|  |     background: #21262d; /* dark-200 */ | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   :root.dark *::-webkit-scrollbar-thumb:hover, | ||||||
|  |   html.dark *::-webkit-scrollbar-thumb:hover, | ||||||
|  |   body.dark *::-webkit-scrollbar-thumb:hover { | ||||||
|  |     background: #30363d; /* dark-300 */ | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   html { | ||||||
|  |     scroll-behavior: smooth; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @layer utilities { | ||||||
|  |   .line-clamp-2 { | ||||||
|  |     display: -webkit-box; | ||||||
|  |     -webkit-box-orient: vertical; | ||||||
|  |     -webkit-line-clamp: 2; | ||||||
|  |     line-clamp: 2; | ||||||
|  |     overflow: hidden; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media screen and (-webkit-min-device-pixel-ratio: 0) { | ||||||
|  |   select, | ||||||
|  |   textarea, | ||||||
|  |   input { | ||||||
|  |     font-size: 16px !important; | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | export const dynamic = 'force-dynamic'; | ||||||
|  |  | ||||||
| import type { Metadata } from 'next'; | import type { Metadata } from 'next'; | ||||||
| import { Montserrat } from 'next/font/google'; | import { Montserrat } from 'next/font/google'; | ||||||
| import './globals.css'; | import './globals.css'; | ||||||
| @@ -5,6 +7,8 @@ import { cn } from '@/lib/utils'; | |||||||
| import Sidebar from '@/components/Sidebar'; | import Sidebar from '@/components/Sidebar'; | ||||||
| import { Toaster } from 'sonner'; | import { Toaster } from 'sonner'; | ||||||
| import ThemeProvider from '@/components/theme/Provider'; | import ThemeProvider from '@/components/theme/Provider'; | ||||||
|  | import configManager from '@/lib/config'; | ||||||
|  | import SetupWizard from '@/components/Setup/SetupWizard'; | ||||||
|  |  | ||||||
| const montserrat = Montserrat({ | const montserrat = Montserrat({ | ||||||
|   weight: ['300', '400', '500', '700'], |   weight: ['300', '400', '500', '700'], | ||||||
| @@ -24,20 +28,29 @@ export default function RootLayout({ | |||||||
| }: Readonly<{ | }: Readonly<{ | ||||||
|   children: React.ReactNode; |   children: React.ReactNode; | ||||||
| }>) { | }>) { | ||||||
|  |   const setupComplete = configManager.isSetupComplete(); | ||||||
|  |   const configSections = configManager.getUIConfigSections(); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <html className="h-full" lang="en" suppressHydrationWarning> |     <html className="h-full" lang="en" suppressHydrationWarning> | ||||||
|       <body className={cn('h-full', montserrat.className)}> |       <body className={cn('h-full', montserrat.className)}> | ||||||
|         <ThemeProvider> |         <ThemeProvider> | ||||||
|           <Sidebar>{children}</Sidebar> |           {setupComplete ? ( | ||||||
|           <Toaster |             <> | ||||||
|             toastOptions={{ |               <Sidebar>{children}</Sidebar> | ||||||
|               unstyled: true, |               <Toaster | ||||||
|               classNames: { |                 toastOptions={{ | ||||||
|                 toast: |                   unstyled: true, | ||||||
|                   'bg-light-primary dark:bg-dark-secondary dark:text-white/70 text-black-70 rounded-lg p-4 flex flex-row items-center space-x-2', |                   classNames: { | ||||||
|               }, |                     toast: | ||||||
|             }} |                       'bg-light-secondary dark:bg-dark-secondary dark:text-white/70 text-black-70 rounded-lg p-4 flex flex-row items-center space-x-2', | ||||||
|           /> |                   }, | ||||||
|  |                 }} | ||||||
|  |               /> | ||||||
|  |             </> | ||||||
|  |           ) : ( | ||||||
|  |             <SetupWizard configSections={configSections} /> | ||||||
|  |           )} | ||||||
|         </ThemeProvider> |         </ThemeProvider> | ||||||
|       </body> |       </body> | ||||||
|     </html> |     </html> | ||||||
|   | |||||||
							
								
								
									
										54
									
								
								src/app/manifest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,54 @@ | |||||||
|  | import type { MetadataRoute } from 'next'; | ||||||
|  |  | ||||||
|  | export default function manifest(): MetadataRoute.Manifest { | ||||||
|  |   return { | ||||||
|  |     name: 'Perplexica - Chat with the internet', | ||||||
|  |     short_name: 'Perplexica', | ||||||
|  |     description: | ||||||
|  |       'Perplexica is an AI powered chatbot that is connected to the internet.', | ||||||
|  |     start_url: '/', | ||||||
|  |     display: 'standalone', | ||||||
|  |     background_color: '#0a0a0a', | ||||||
|  |     theme_color: '#0a0a0a', | ||||||
|  |     screenshots: [ | ||||||
|  |       { | ||||||
|  |         src: '/screenshots/p1.png', | ||||||
|  |         form_factor: 'wide', | ||||||
|  |         sizes: '2560x1600', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         src: '/screenshots/p2.png', | ||||||
|  |         form_factor: 'wide', | ||||||
|  |         sizes: '2560x1600', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         src: '/screenshots/p1_small.png', | ||||||
|  |         form_factor: 'narrow', | ||||||
|  |         sizes: '828x1792', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         src: '/screenshots/p2_small.png', | ||||||
|  |         form_factor: 'narrow', | ||||||
|  |         sizes: '828x1792', | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |     icons: [ | ||||||
|  |       { | ||||||
|  |         src: '/icon-50.png', | ||||||
|  |         sizes: '50x50', | ||||||
|  |         type: 'image/png' as const, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         src: '/icon-100.png', | ||||||
|  |         sizes: '100x100', | ||||||
|  |         type: 'image/png', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         src: '/icon.png', | ||||||
|  |         sizes: '440x440', | ||||||
|  |         type: 'image/png', | ||||||
|  |         purpose: 'any', | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |   }; | ||||||
|  | } | ||||||
| @@ -1,4 +1,5 @@ | |||||||
| import ChatWindow from '@/components/ChatWindow'; | import ChatWindow from '@/components/ChatWindow'; | ||||||
|  | import { ChatProvider } from '@/lib/hooks/useChat'; | ||||||
| import { Metadata } from 'next'; | import { Metadata } from 'next'; | ||||||
| import { Suspense } from 'react'; | import { Suspense } from 'react'; | ||||||
|  |  | ||||||
| @@ -11,7 +12,9 @@ const Home = () => { | |||||||
|   return ( |   return ( | ||||||
|     <div> |     <div> | ||||||
|       <Suspense> |       <Suspense> | ||||||
|         <ChatWindow /> |         <ChatProvider> | ||||||
|  |           <ChatWindow /> | ||||||
|  |         </ChatProvider> | ||||||
|       </Suspense> |       </Suspense> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
|   | |||||||
| @@ -1,870 +0,0 @@ | |||||||
| 'use client'; |  | ||||||
|  |  | ||||||
| import { Settings as SettingsIcon, ArrowLeft, Loader2 } from 'lucide-react'; |  | ||||||
| import { useEffect, useState } from 'react'; |  | ||||||
| import { cn } from '@/lib/utils'; |  | ||||||
| import { Switch } from '@headlessui/react'; |  | ||||||
| import ThemeSwitcher from '@/components/theme/Switcher'; |  | ||||||
| import { ImagesIcon, VideoIcon } from 'lucide-react'; |  | ||||||
| import Link from 'next/link'; |  | ||||||
|  |  | ||||||
| interface SettingsType { |  | ||||||
|   chatModelProviders: { |  | ||||||
|     [key: string]: [Record<string, any>]; |  | ||||||
|   }; |  | ||||||
|   embeddingModelProviders: { |  | ||||||
|     [key: string]: [Record<string, any>]; |  | ||||||
|   }; |  | ||||||
|   openaiApiKey: string; |  | ||||||
|   groqApiKey: string; |  | ||||||
|   anthropicApiKey: string; |  | ||||||
|   geminiApiKey: string; |  | ||||||
|   ollamaApiUrl: string; |  | ||||||
|   deepseekApiKey: string; |  | ||||||
|   customOpenaiApiKey: string; |  | ||||||
|   customOpenaiApiUrl: string; |  | ||||||
|   customOpenaiModelName: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { |  | ||||||
|   isSaving?: boolean; |  | ||||||
|   onSave?: (value: string) => void; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const Input = ({ className, isSaving, onSave, ...restProps }: InputProps) => { |  | ||||||
|   return ( |  | ||||||
|     <div className="relative"> |  | ||||||
|       <input |  | ||||||
|         {...restProps} |  | ||||||
|         className={cn( |  | ||||||
|           'bg-light-secondary dark:bg-dark-secondary w-full px-3 py-2 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 dark:text-white rounded-lg text-sm', |  | ||||||
|           isSaving && 'pr-10', |  | ||||||
|           className, |  | ||||||
|         )} |  | ||||||
|         onBlur={(e) => onSave?.(e.target.value)} |  | ||||||
|       /> |  | ||||||
|       {isSaving && ( |  | ||||||
|         <div className="absolute right-3 top-1/2 -translate-y-1/2"> |  | ||||||
|           <Loader2 |  | ||||||
|             size={16} |  | ||||||
|             className="animate-spin text-black/70 dark:text-white/70" |  | ||||||
|           /> |  | ||||||
|         </div> |  | ||||||
|       )} |  | ||||||
|     </div> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| interface TextareaProps extends React.InputHTMLAttributes<HTMLTextAreaElement> { |  | ||||||
|   isSaving?: boolean; |  | ||||||
|   onSave?: (value: string) => void; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const Textarea = ({ |  | ||||||
|   className, |  | ||||||
|   isSaving, |  | ||||||
|   onSave, |  | ||||||
|   ...restProps |  | ||||||
| }: TextareaProps) => { |  | ||||||
|   return ( |  | ||||||
|     <div className="relative"> |  | ||||||
|       <textarea |  | ||||||
|         placeholder="Any special instructions for the LLM" |  | ||||||
|         className="placeholder:text-sm text-sm w-full flex items-center justify-between p-3 bg-light-secondary dark:bg-dark-secondary rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition-colors" |  | ||||||
|         rows={4} |  | ||||||
|         onBlur={(e) => onSave?.(e.target.value)} |  | ||||||
|         {...restProps} |  | ||||||
|       /> |  | ||||||
|       {isSaving && ( |  | ||||||
|         <div className="absolute right-3 top-3"> |  | ||||||
|           <Loader2 |  | ||||||
|             size={16} |  | ||||||
|             className="animate-spin text-black/70 dark:text-white/70" |  | ||||||
|           /> |  | ||||||
|         </div> |  | ||||||
|       )} |  | ||||||
|     </div> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const Select = ({ |  | ||||||
|   className, |  | ||||||
|   options, |  | ||||||
|   ...restProps |  | ||||||
| }: React.SelectHTMLAttributes<HTMLSelectElement> & { |  | ||||||
|   options: { value: string; label: string; disabled?: boolean }[]; |  | ||||||
| }) => { |  | ||||||
|   return ( |  | ||||||
|     <select |  | ||||||
|       {...restProps} |  | ||||||
|       className={cn( |  | ||||||
|         'bg-light-secondary dark:bg-dark-secondary px-3 py-2 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 dark:text-white rounded-lg text-sm', |  | ||||||
|         className, |  | ||||||
|       )} |  | ||||||
|     > |  | ||||||
|       {options.map(({ label, value, disabled }) => ( |  | ||||||
|         <option key={value} value={value} disabled={disabled}> |  | ||||||
|           {label} |  | ||||||
|         </option> |  | ||||||
|       ))} |  | ||||||
|     </select> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const SettingsSection = ({ |  | ||||||
|   title, |  | ||||||
|   children, |  | ||||||
| }: { |  | ||||||
|   title: string; |  | ||||||
|   children: React.ReactNode; |  | ||||||
| }) => ( |  | ||||||
|   <div className="flex flex-col space-y-4 p-4 bg-light-secondary/50 dark:bg-dark-secondary/50 rounded-xl border border-light-200 dark:border-dark-200"> |  | ||||||
|     <h2 className="text-black/90 dark:text-white/90 font-medium">{title}</h2> |  | ||||||
|     {children} |  | ||||||
|   </div> |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| const Page = () => { |  | ||||||
|   const [config, setConfig] = useState<SettingsType | null>(null); |  | ||||||
|   const [chatModels, setChatModels] = useState<Record<string, any>>({}); |  | ||||||
|   const [embeddingModels, setEmbeddingModels] = useState<Record<string, any>>( |  | ||||||
|     {}, |  | ||||||
|   ); |  | ||||||
|   const [selectedChatModelProvider, setSelectedChatModelProvider] = useState< |  | ||||||
|     string | null |  | ||||||
|   >(null); |  | ||||||
|   const [selectedChatModel, setSelectedChatModel] = useState<string | null>( |  | ||||||
|     null, |  | ||||||
|   ); |  | ||||||
|   const [selectedEmbeddingModelProvider, setSelectedEmbeddingModelProvider] = |  | ||||||
|     useState<string | null>(null); |  | ||||||
|   const [selectedEmbeddingModel, setSelectedEmbeddingModel] = useState< |  | ||||||
|     string | null |  | ||||||
|   >(null); |  | ||||||
|   const [isLoading, setIsLoading] = useState(false); |  | ||||||
|   const [automaticImageSearch, setAutomaticImageSearch] = useState(false); |  | ||||||
|   const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false); |  | ||||||
|   const [systemInstructions, setSystemInstructions] = useState<string>(''); |  | ||||||
|   const [savingStates, setSavingStates] = useState<Record<string, boolean>>({}); |  | ||||||
|  |  | ||||||
|   useEffect(() => { |  | ||||||
|     const fetchConfig = async () => { |  | ||||||
|       setIsLoading(true); |  | ||||||
|       const res = await fetch(`/api/config`, { |  | ||||||
|         headers: { |  | ||||||
|           'Content-Type': 'application/json', |  | ||||||
|         }, |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       const data = (await res.json()) as SettingsType; |  | ||||||
|       setConfig(data); |  | ||||||
|  |  | ||||||
|       const chatModelProvidersKeys = Object.keys(data.chatModelProviders || {}); |  | ||||||
|       const embeddingModelProvidersKeys = Object.keys( |  | ||||||
|         data.embeddingModelProviders || {}, |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       const defaultChatModelProvider = |  | ||||||
|         chatModelProvidersKeys.length > 0 ? chatModelProvidersKeys[0] : ''; |  | ||||||
|       const defaultEmbeddingModelProvider = |  | ||||||
|         embeddingModelProvidersKeys.length > 0 |  | ||||||
|           ? embeddingModelProvidersKeys[0] |  | ||||||
|           : ''; |  | ||||||
|  |  | ||||||
|       const chatModelProvider = |  | ||||||
|         localStorage.getItem('chatModelProvider') || |  | ||||||
|         defaultChatModelProvider || |  | ||||||
|         ''; |  | ||||||
|       const chatModel = |  | ||||||
|         localStorage.getItem('chatModel') || |  | ||||||
|         (data.chatModelProviders && |  | ||||||
|         data.chatModelProviders[chatModelProvider]?.length > 0 |  | ||||||
|           ? data.chatModelProviders[chatModelProvider][0].name |  | ||||||
|           : undefined) || |  | ||||||
|         ''; |  | ||||||
|       const embeddingModelProvider = |  | ||||||
|         localStorage.getItem('embeddingModelProvider') || |  | ||||||
|         defaultEmbeddingModelProvider || |  | ||||||
|         ''; |  | ||||||
|       const embeddingModel = |  | ||||||
|         localStorage.getItem('embeddingModel') || |  | ||||||
|         (data.embeddingModelProviders && |  | ||||||
|           data.embeddingModelProviders[embeddingModelProvider]?.[0].name) || |  | ||||||
|         ''; |  | ||||||
|  |  | ||||||
|       setSelectedChatModelProvider(chatModelProvider); |  | ||||||
|       setSelectedChatModel(chatModel); |  | ||||||
|       setSelectedEmbeddingModelProvider(embeddingModelProvider); |  | ||||||
|       setSelectedEmbeddingModel(embeddingModel); |  | ||||||
|       setChatModels(data.chatModelProviders || {}); |  | ||||||
|       setEmbeddingModels(data.embeddingModelProviders || {}); |  | ||||||
|  |  | ||||||
|       setAutomaticImageSearch( |  | ||||||
|         localStorage.getItem('autoImageSearch') === 'true', |  | ||||||
|       ); |  | ||||||
|       setAutomaticVideoSearch( |  | ||||||
|         localStorage.getItem('autoVideoSearch') === 'true', |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       setSystemInstructions(localStorage.getItem('systemInstructions')!); |  | ||||||
|  |  | ||||||
|       setIsLoading(false); |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     fetchConfig(); |  | ||||||
|   }, []); |  | ||||||
|  |  | ||||||
|   const saveConfig = async (key: string, value: any) => { |  | ||||||
|     setSavingStates((prev) => ({ ...prev, [key]: true })); |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|       const updatedConfig = { |  | ||||||
|         ...config, |  | ||||||
|         [key]: value, |  | ||||||
|       } as SettingsType; |  | ||||||
|  |  | ||||||
|       const response = await fetch(`/api/config`, { |  | ||||||
|         method: 'POST', |  | ||||||
|         headers: { |  | ||||||
|           'Content-Type': 'application/json', |  | ||||||
|         }, |  | ||||||
|         body: JSON.stringify(updatedConfig), |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       if (!response.ok) { |  | ||||||
|         throw new Error('Failed to update config'); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       setConfig(updatedConfig); |  | ||||||
|  |  | ||||||
|       if ( |  | ||||||
|         key.toLowerCase().includes('api') || |  | ||||||
|         key.toLowerCase().includes('url') |  | ||||||
|       ) { |  | ||||||
|         const res = await fetch(`/api/config`, { |  | ||||||
|           headers: { |  | ||||||
|             'Content-Type': 'application/json', |  | ||||||
|           }, |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         if (!res.ok) { |  | ||||||
|           throw new Error('Failed to fetch updated config'); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const data = await res.json(); |  | ||||||
|  |  | ||||||
|         setChatModels(data.chatModelProviders || {}); |  | ||||||
|         setEmbeddingModels(data.embeddingModelProviders || {}); |  | ||||||
|  |  | ||||||
|         const currentChatProvider = selectedChatModelProvider; |  | ||||||
|         const newChatProviders = Object.keys(data.chatModelProviders || {}); |  | ||||||
|  |  | ||||||
|         if (!currentChatProvider && newChatProviders.length > 0) { |  | ||||||
|           const firstProvider = newChatProviders[0]; |  | ||||||
|           const firstModel = data.chatModelProviders[firstProvider]?.[0]?.name; |  | ||||||
|  |  | ||||||
|           if (firstModel) { |  | ||||||
|             setSelectedChatModelProvider(firstProvider); |  | ||||||
|             setSelectedChatModel(firstModel); |  | ||||||
|             localStorage.setItem('chatModelProvider', firstProvider); |  | ||||||
|             localStorage.setItem('chatModel', firstModel); |  | ||||||
|           } |  | ||||||
|         } else if ( |  | ||||||
|           currentChatProvider && |  | ||||||
|           (!data.chatModelProviders || |  | ||||||
|             !data.chatModelProviders[currentChatProvider] || |  | ||||||
|             !Array.isArray(data.chatModelProviders[currentChatProvider]) || |  | ||||||
|             data.chatModelProviders[currentChatProvider].length === 0) |  | ||||||
|         ) { |  | ||||||
|           const firstValidProvider = Object.entries( |  | ||||||
|             data.chatModelProviders || {}, |  | ||||||
|           ).find( |  | ||||||
|             ([_, models]) => Array.isArray(models) && models.length > 0, |  | ||||||
|           )?.[0]; |  | ||||||
|  |  | ||||||
|           if (firstValidProvider) { |  | ||||||
|             setSelectedChatModelProvider(firstValidProvider); |  | ||||||
|             setSelectedChatModel( |  | ||||||
|               data.chatModelProviders[firstValidProvider][0].name, |  | ||||||
|             ); |  | ||||||
|             localStorage.setItem('chatModelProvider', firstValidProvider); |  | ||||||
|             localStorage.setItem( |  | ||||||
|               'chatModel', |  | ||||||
|               data.chatModelProviders[firstValidProvider][0].name, |  | ||||||
|             ); |  | ||||||
|           } else { |  | ||||||
|             setSelectedChatModelProvider(null); |  | ||||||
|             setSelectedChatModel(null); |  | ||||||
|             localStorage.removeItem('chatModelProvider'); |  | ||||||
|             localStorage.removeItem('chatModel'); |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const currentEmbeddingProvider = selectedEmbeddingModelProvider; |  | ||||||
|         const newEmbeddingProviders = Object.keys( |  | ||||||
|           data.embeddingModelProviders || {}, |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         if (!currentEmbeddingProvider && newEmbeddingProviders.length > 0) { |  | ||||||
|           const firstProvider = newEmbeddingProviders[0]; |  | ||||||
|           const firstModel = |  | ||||||
|             data.embeddingModelProviders[firstProvider]?.[0]?.name; |  | ||||||
|  |  | ||||||
|           if (firstModel) { |  | ||||||
|             setSelectedEmbeddingModelProvider(firstProvider); |  | ||||||
|             setSelectedEmbeddingModel(firstModel); |  | ||||||
|             localStorage.setItem('embeddingModelProvider', firstProvider); |  | ||||||
|             localStorage.setItem('embeddingModel', firstModel); |  | ||||||
|           } |  | ||||||
|         } else if ( |  | ||||||
|           currentEmbeddingProvider && |  | ||||||
|           (!data.embeddingModelProviders || |  | ||||||
|             !data.embeddingModelProviders[currentEmbeddingProvider] || |  | ||||||
|             !Array.isArray( |  | ||||||
|               data.embeddingModelProviders[currentEmbeddingProvider], |  | ||||||
|             ) || |  | ||||||
|             data.embeddingModelProviders[currentEmbeddingProvider].length === 0) |  | ||||||
|         ) { |  | ||||||
|           const firstValidProvider = Object.entries( |  | ||||||
|             data.embeddingModelProviders || {}, |  | ||||||
|           ).find( |  | ||||||
|             ([_, models]) => Array.isArray(models) && models.length > 0, |  | ||||||
|           )?.[0]; |  | ||||||
|  |  | ||||||
|           if (firstValidProvider) { |  | ||||||
|             setSelectedEmbeddingModelProvider(firstValidProvider); |  | ||||||
|             setSelectedEmbeddingModel( |  | ||||||
|               data.embeddingModelProviders[firstValidProvider][0].name, |  | ||||||
|             ); |  | ||||||
|             localStorage.setItem('embeddingModelProvider', firstValidProvider); |  | ||||||
|             localStorage.setItem( |  | ||||||
|               'embeddingModel', |  | ||||||
|               data.embeddingModelProviders[firstValidProvider][0].name, |  | ||||||
|             ); |  | ||||||
|           } else { |  | ||||||
|             setSelectedEmbeddingModelProvider(null); |  | ||||||
|             setSelectedEmbeddingModel(null); |  | ||||||
|             localStorage.removeItem('embeddingModelProvider'); |  | ||||||
|             localStorage.removeItem('embeddingModel'); |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         setConfig(data); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (key === 'automaticImageSearch') { |  | ||||||
|         localStorage.setItem('autoImageSearch', value.toString()); |  | ||||||
|       } else if (key === 'automaticVideoSearch') { |  | ||||||
|         localStorage.setItem('autoVideoSearch', value.toString()); |  | ||||||
|       } else if (key === 'chatModelProvider') { |  | ||||||
|         localStorage.setItem('chatModelProvider', value); |  | ||||||
|       } else if (key === 'chatModel') { |  | ||||||
|         localStorage.setItem('chatModel', value); |  | ||||||
|       } else if (key === 'embeddingModelProvider') { |  | ||||||
|         localStorage.setItem('embeddingModelProvider', value); |  | ||||||
|       } else if (key === 'embeddingModel') { |  | ||||||
|         localStorage.setItem('embeddingModel', value); |  | ||||||
|       } else if (key === 'systemInstructions') { |  | ||||||
|         localStorage.setItem('systemInstructions', value); |  | ||||||
|       } |  | ||||||
|     } catch (err) { |  | ||||||
|       console.error('Failed to save:', err); |  | ||||||
|       setConfig((prev) => ({ ...prev! })); |  | ||||||
|     } finally { |  | ||||||
|       setTimeout(() => { |  | ||||||
|         setSavingStates((prev) => ({ ...prev, [key]: false })); |  | ||||||
|       }, 500); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <div className="max-w-3xl mx-auto"> |  | ||||||
|       <div className="flex flex-col pt-4"> |  | ||||||
|         <div className="flex items-center space-x-2"> |  | ||||||
|           <Link href="/" className="lg:hidden"> |  | ||||||
|             <ArrowLeft className="text-black/70 dark:text-white/70" /> |  | ||||||
|           </Link> |  | ||||||
|           <div className="flex flex-row space-x-0.5 items-center"> |  | ||||||
|             <SettingsIcon size={23} /> |  | ||||||
|             <h1 className="text-3xl font-medium p-2">Settings</h1> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|         <hr className="border-t border-[#2B2C2C] my-4 w-full" /> |  | ||||||
|       </div> |  | ||||||
|  |  | ||||||
|       {isLoading ? ( |  | ||||||
|         <div className="flex flex-row items-center justify-center min-h-[50vh]"> |  | ||||||
|           <svg |  | ||||||
|             aria-hidden="true" |  | ||||||
|             className="w-8 h-8 text-light-200 fill-light-secondary dark:text-[#202020] animate-spin dark:fill-[#ffffff3b]" |  | ||||||
|             viewBox="0 0 100 101" |  | ||||||
|             fill="none" |  | ||||||
|             xmlns="http://www.w3.org/2000/svg" |  | ||||||
|           > |  | ||||||
|             <path |  | ||||||
|               d="M100 50.5908C100.003 78.2051 78.1951 100.003 50.5908 100C22.9765 99.9972 0.997224 78.018 1 50.4037C1.00281 22.7993 22.8108 0.997224 50.4251 1C78.0395 1.00281 100.018 22.8108 100 50.4251ZM9.08164 50.594C9.06312 73.3997 27.7909 92.1272 50.5966 92.1457C73.4023 92.1642 92.1298 73.4365 92.1483 50.6308C92.1669 27.8251 73.4392 9.0973 50.6335 9.07878C27.8278 9.06026 9.10003 27.787 9.08164 50.594Z" |  | ||||||
|               fill="currentColor" |  | ||||||
|             /> |  | ||||||
|             <path |  | ||||||
|               d="M93.9676 39.0409C96.393 38.4037 97.8624 35.9116 96.9801 33.5533C95.1945 28.8227 92.871 24.3692 90.0681 20.348C85.6237 14.1775 79.4473 9.36872 72.0454 6.45794C64.6435 3.54717 56.3134 2.65431 48.3133 3.89319C45.869 4.27179 44.3768 6.77534 45.014 9.20079C45.6512 11.6262 48.1343 13.0956 50.5786 12.717C56.5073 11.8281 62.5542 12.5399 68.0406 14.7911C73.527 17.0422 78.2187 20.7487 81.5841 25.4923C83.7976 28.5886 85.4467 32.059 86.4416 35.7474C87.1273 38.1189 89.5423 39.6781 91.9676 39.0409Z" |  | ||||||
|               fill="currentFill" |  | ||||||
|             /> |  | ||||||
|           </svg> |  | ||||||
|         </div> |  | ||||||
|       ) : ( |  | ||||||
|         config && ( |  | ||||||
|           <div className="flex flex-col space-y-6 pb-28 lg:pb-8"> |  | ||||||
|             <SettingsSection title="Appearance"> |  | ||||||
|               <div className="flex flex-col space-y-1"> |  | ||||||
|                 <p className="text-black/70 dark:text-white/70 text-sm"> |  | ||||||
|                   Theme |  | ||||||
|                 </p> |  | ||||||
|                 <ThemeSwitcher /> |  | ||||||
|               </div> |  | ||||||
|             </SettingsSection> |  | ||||||
|  |  | ||||||
|             <SettingsSection title="Automatic Search"> |  | ||||||
|               <div className="flex flex-col space-y-4"> |  | ||||||
|                 <div className="flex items-center justify-between p-3 bg-light-secondary dark:bg-dark-secondary rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition-colors"> |  | ||||||
|                   <div className="flex items-center space-x-3"> |  | ||||||
|                     <div className="p-2 bg-light-200 dark:bg-dark-200 rounded-lg"> |  | ||||||
|                       <ImagesIcon |  | ||||||
|                         size={18} |  | ||||||
|                         className="text-black/70 dark:text-white/70" |  | ||||||
|                       /> |  | ||||||
|                     </div> |  | ||||||
|                     <div> |  | ||||||
|                       <p className="text-sm text-black/90 dark:text-white/90 font-medium"> |  | ||||||
|                         Automatic Image Search |  | ||||||
|                       </p> |  | ||||||
|                       <p className="text-xs text-black/60 dark:text-white/60 mt-0.5"> |  | ||||||
|                         Automatically search for relevant images in chat |  | ||||||
|                         responses |  | ||||||
|                       </p> |  | ||||||
|                     </div> |  | ||||||
|                   </div> |  | ||||||
|                   <Switch |  | ||||||
|                     checked={automaticImageSearch} |  | ||||||
|                     onChange={(checked) => { |  | ||||||
|                       setAutomaticImageSearch(checked); |  | ||||||
|                       saveConfig('automaticImageSearch', checked); |  | ||||||
|                     }} |  | ||||||
|                     className={cn( |  | ||||||
|                       automaticImageSearch |  | ||||||
|                         ? 'bg-[#24A0ED]' |  | ||||||
|                         : 'bg-light-200 dark:bg-dark-200', |  | ||||||
|                       'relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none', |  | ||||||
|                     )} |  | ||||||
|                   > |  | ||||||
|                     <span |  | ||||||
|                       className={cn( |  | ||||||
|                         automaticImageSearch |  | ||||||
|                           ? 'translate-x-6' |  | ||||||
|                           : 'translate-x-1', |  | ||||||
|                         'inline-block h-4 w-4 transform rounded-full bg-white transition-transform', |  | ||||||
|                       )} |  | ||||||
|                     /> |  | ||||||
|                   </Switch> |  | ||||||
|                 </div> |  | ||||||
|  |  | ||||||
|                 <div className="flex items-center justify-between p-3 bg-light-secondary dark:bg-dark-secondary rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition-colors"> |  | ||||||
|                   <div className="flex items-center space-x-3"> |  | ||||||
|                     <div className="p-2 bg-light-200 dark:bg-dark-200 rounded-lg"> |  | ||||||
|                       <VideoIcon |  | ||||||
|                         size={18} |  | ||||||
|                         className="text-black/70 dark:text-white/70" |  | ||||||
|                       /> |  | ||||||
|                     </div> |  | ||||||
|                     <div> |  | ||||||
|                       <p className="text-sm text-black/90 dark:text-white/90 font-medium"> |  | ||||||
|                         Automatic Video Search |  | ||||||
|                       </p> |  | ||||||
|                       <p className="text-xs text-black/60 dark:text-white/60 mt-0.5"> |  | ||||||
|                         Automatically search for relevant videos in chat |  | ||||||
|                         responses |  | ||||||
|                       </p> |  | ||||||
|                     </div> |  | ||||||
|                   </div> |  | ||||||
|                   <Switch |  | ||||||
|                     checked={automaticVideoSearch} |  | ||||||
|                     onChange={(checked) => { |  | ||||||
|                       setAutomaticVideoSearch(checked); |  | ||||||
|                       saveConfig('automaticVideoSearch', checked); |  | ||||||
|                     }} |  | ||||||
|                     className={cn( |  | ||||||
|                       automaticVideoSearch |  | ||||||
|                         ? 'bg-[#24A0ED]' |  | ||||||
|                         : 'bg-light-200 dark:bg-dark-200', |  | ||||||
|                       'relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none', |  | ||||||
|                     )} |  | ||||||
|                   > |  | ||||||
|                     <span |  | ||||||
|                       className={cn( |  | ||||||
|                         automaticVideoSearch |  | ||||||
|                           ? 'translate-x-6' |  | ||||||
|                           : 'translate-x-1', |  | ||||||
|                         'inline-block h-4 w-4 transform rounded-full bg-white transition-transform', |  | ||||||
|                       )} |  | ||||||
|                     /> |  | ||||||
|                   </Switch> |  | ||||||
|                 </div> |  | ||||||
|               </div> |  | ||||||
|             </SettingsSection> |  | ||||||
|  |  | ||||||
|             <SettingsSection title="System Instructions"> |  | ||||||
|               <div className="flex flex-col space-y-4"> |  | ||||||
|                 <Textarea |  | ||||||
|                   value={systemInstructions} |  | ||||||
|                   isSaving={savingStates['systemInstructions']} |  | ||||||
|                   onChange={(e) => { |  | ||||||
|                     setSystemInstructions(e.target.value); |  | ||||||
|                   }} |  | ||||||
|                   onSave={(value) => saveConfig('systemInstructions', value)} |  | ||||||
|                 /> |  | ||||||
|               </div> |  | ||||||
|             </SettingsSection> |  | ||||||
|  |  | ||||||
|             <SettingsSection title="Model Settings"> |  | ||||||
|               {config.chatModelProviders && ( |  | ||||||
|                 <div className="flex flex-col space-y-4"> |  | ||||||
|                   <div className="flex flex-col space-y-1"> |  | ||||||
|                     <p className="text-black/70 dark:text-white/70 text-sm"> |  | ||||||
|                       Chat Model Provider |  | ||||||
|                     </p> |  | ||||||
|                     <Select |  | ||||||
|                       value={selectedChatModelProvider ?? undefined} |  | ||||||
|                       onChange={(e) => { |  | ||||||
|                         const value = e.target.value; |  | ||||||
|                         setSelectedChatModelProvider(value); |  | ||||||
|                         saveConfig('chatModelProvider', value); |  | ||||||
|                         const firstModel = |  | ||||||
|                           config.chatModelProviders[value]?.[0]?.name; |  | ||||||
|                         if (firstModel) { |  | ||||||
|                           setSelectedChatModel(firstModel); |  | ||||||
|                           saveConfig('chatModel', firstModel); |  | ||||||
|                         } |  | ||||||
|                       }} |  | ||||||
|                       options={Object.keys(config.chatModelProviders).map( |  | ||||||
|                         (provider) => ({ |  | ||||||
|                           value: provider, |  | ||||||
|                           label: |  | ||||||
|                             provider.charAt(0).toUpperCase() + |  | ||||||
|                             provider.slice(1), |  | ||||||
|                         }), |  | ||||||
|                       )} |  | ||||||
|                     /> |  | ||||||
|                   </div> |  | ||||||
|  |  | ||||||
|                   {selectedChatModelProvider && |  | ||||||
|                     selectedChatModelProvider != 'custom_openai' && ( |  | ||||||
|                       <div className="flex flex-col space-y-1"> |  | ||||||
|                         <p className="text-black/70 dark:text-white/70 text-sm"> |  | ||||||
|                           Chat Model |  | ||||||
|                         </p> |  | ||||||
|                         <Select |  | ||||||
|                           value={selectedChatModel ?? undefined} |  | ||||||
|                           onChange={(e) => { |  | ||||||
|                             const value = e.target.value; |  | ||||||
|                             setSelectedChatModel(value); |  | ||||||
|                             saveConfig('chatModel', value); |  | ||||||
|                           }} |  | ||||||
|                           options={(() => { |  | ||||||
|                             const chatModelProvider = |  | ||||||
|                               config.chatModelProviders[ |  | ||||||
|                                 selectedChatModelProvider |  | ||||||
|                               ]; |  | ||||||
|                             return chatModelProvider |  | ||||||
|                               ? chatModelProvider.length > 0 |  | ||||||
|                                 ? chatModelProvider.map((model) => ({ |  | ||||||
|                                     value: model.name, |  | ||||||
|                                     label: model.displayName, |  | ||||||
|                                   })) |  | ||||||
|                                 : [ |  | ||||||
|                                     { |  | ||||||
|                                       value: '', |  | ||||||
|                                       label: 'No models available', |  | ||||||
|                                       disabled: true, |  | ||||||
|                                     }, |  | ||||||
|                                   ] |  | ||||||
|                               : [ |  | ||||||
|                                   { |  | ||||||
|                                     value: '', |  | ||||||
|                                     label: |  | ||||||
|                                       'Invalid provider, please check backend logs', |  | ||||||
|                                     disabled: true, |  | ||||||
|                                   }, |  | ||||||
|                                 ]; |  | ||||||
|                           })()} |  | ||||||
|                         /> |  | ||||||
|                       </div> |  | ||||||
|                     )} |  | ||||||
|                 </div> |  | ||||||
|               )} |  | ||||||
|  |  | ||||||
|               {selectedChatModelProvider && |  | ||||||
|                 selectedChatModelProvider === 'custom_openai' && ( |  | ||||||
|                   <div className="flex flex-col space-y-4"> |  | ||||||
|                     <div className="flex flex-col space-y-1"> |  | ||||||
|                       <p className="text-black/70 dark:text-white/70 text-sm"> |  | ||||||
|                         Model Name |  | ||||||
|                       </p> |  | ||||||
|                       <Input |  | ||||||
|                         type="text" |  | ||||||
|                         placeholder="Model name" |  | ||||||
|                         value={config.customOpenaiModelName} |  | ||||||
|                         isSaving={savingStates['customOpenaiModelName']} |  | ||||||
|                         onChange={(e: React.ChangeEvent<HTMLInputElement>) => { |  | ||||||
|                           setConfig((prev) => ({ |  | ||||||
|                             ...prev!, |  | ||||||
|                             customOpenaiModelName: e.target.value, |  | ||||||
|                           })); |  | ||||||
|                         }} |  | ||||||
|                         onSave={(value) => |  | ||||||
|                           saveConfig('customOpenaiModelName', value) |  | ||||||
|                         } |  | ||||||
|                       /> |  | ||||||
|                     </div> |  | ||||||
|                     <div className="flex flex-col space-y-1"> |  | ||||||
|                       <p className="text-black/70 dark:text-white/70 text-sm"> |  | ||||||
|                         Custom OpenAI API Key |  | ||||||
|                       </p> |  | ||||||
|                       <Input |  | ||||||
|                         type="text" |  | ||||||
|                         placeholder="Custom OpenAI API Key" |  | ||||||
|                         value={config.customOpenaiApiKey} |  | ||||||
|                         isSaving={savingStates['customOpenaiApiKey']} |  | ||||||
|                         onChange={(e: React.ChangeEvent<HTMLInputElement>) => { |  | ||||||
|                           setConfig((prev) => ({ |  | ||||||
|                             ...prev!, |  | ||||||
|                             customOpenaiApiKey: e.target.value, |  | ||||||
|                           })); |  | ||||||
|                         }} |  | ||||||
|                         onSave={(value) => |  | ||||||
|                           saveConfig('customOpenaiApiKey', value) |  | ||||||
|                         } |  | ||||||
|                       /> |  | ||||||
|                     </div> |  | ||||||
|                     <div className="flex flex-col space-y-1"> |  | ||||||
|                       <p className="text-black/70 dark:text-white/70 text-sm"> |  | ||||||
|                         Custom OpenAI Base URL |  | ||||||
|                       </p> |  | ||||||
|                       <Input |  | ||||||
|                         type="text" |  | ||||||
|                         placeholder="Custom OpenAI Base URL" |  | ||||||
|                         value={config.customOpenaiApiUrl} |  | ||||||
|                         isSaving={savingStates['customOpenaiApiUrl']} |  | ||||||
|                         onChange={(e: React.ChangeEvent<HTMLInputElement>) => { |  | ||||||
|                           setConfig((prev) => ({ |  | ||||||
|                             ...prev!, |  | ||||||
|                             customOpenaiApiUrl: e.target.value, |  | ||||||
|                           })); |  | ||||||
|                         }} |  | ||||||
|                         onSave={(value) => |  | ||||||
|                           saveConfig('customOpenaiApiUrl', value) |  | ||||||
|                         } |  | ||||||
|                       /> |  | ||||||
|                     </div> |  | ||||||
|                   </div> |  | ||||||
|                 )} |  | ||||||
|  |  | ||||||
|               {config.embeddingModelProviders && ( |  | ||||||
|                 <div className="flex flex-col space-y-4 mt-4 pt-4 border-t border-light-200 dark:border-dark-200"> |  | ||||||
|                   <div className="flex flex-col space-y-1"> |  | ||||||
|                     <p className="text-black/70 dark:text-white/70 text-sm"> |  | ||||||
|                       Embedding Model Provider |  | ||||||
|                     </p> |  | ||||||
|                     <Select |  | ||||||
|                       value={selectedEmbeddingModelProvider ?? undefined} |  | ||||||
|                       onChange={(e) => { |  | ||||||
|                         const value = e.target.value; |  | ||||||
|                         setSelectedEmbeddingModelProvider(value); |  | ||||||
|                         saveConfig('embeddingModelProvider', value); |  | ||||||
|                         const firstModel = |  | ||||||
|                           config.embeddingModelProviders[value]?.[0]?.name; |  | ||||||
|                         if (firstModel) { |  | ||||||
|                           setSelectedEmbeddingModel(firstModel); |  | ||||||
|                           saveConfig('embeddingModel', firstModel); |  | ||||||
|                         } |  | ||||||
|                       }} |  | ||||||
|                       options={Object.keys(config.embeddingModelProviders).map( |  | ||||||
|                         (provider) => ({ |  | ||||||
|                           value: provider, |  | ||||||
|                           label: |  | ||||||
|                             provider.charAt(0).toUpperCase() + |  | ||||||
|                             provider.slice(1), |  | ||||||
|                         }), |  | ||||||
|                       )} |  | ||||||
|                     /> |  | ||||||
|                   </div> |  | ||||||
|  |  | ||||||
|                   {selectedEmbeddingModelProvider && ( |  | ||||||
|                     <div className="flex flex-col space-y-1"> |  | ||||||
|                       <p className="text-black/70 dark:text-white/70 text-sm"> |  | ||||||
|                         Embedding Model |  | ||||||
|                       </p> |  | ||||||
|                       <Select |  | ||||||
|                         value={selectedEmbeddingModel ?? undefined} |  | ||||||
|                         onChange={(e) => { |  | ||||||
|                           const value = e.target.value; |  | ||||||
|                           setSelectedEmbeddingModel(value); |  | ||||||
|                           saveConfig('embeddingModel', value); |  | ||||||
|                         }} |  | ||||||
|                         options={(() => { |  | ||||||
|                           const embeddingModelProvider = |  | ||||||
|                             config.embeddingModelProviders[ |  | ||||||
|                               selectedEmbeddingModelProvider |  | ||||||
|                             ]; |  | ||||||
|                           return embeddingModelProvider |  | ||||||
|                             ? embeddingModelProvider.length > 0 |  | ||||||
|                               ? embeddingModelProvider.map((model) => ({ |  | ||||||
|                                   value: model.name, |  | ||||||
|                                   label: model.displayName, |  | ||||||
|                                 })) |  | ||||||
|                               : [ |  | ||||||
|                                   { |  | ||||||
|                                     value: '', |  | ||||||
|                                     label: 'No models available', |  | ||||||
|                                     disabled: true, |  | ||||||
|                                   }, |  | ||||||
|                                 ] |  | ||||||
|                             : [ |  | ||||||
|                                 { |  | ||||||
|                                   value: '', |  | ||||||
|                                   label: |  | ||||||
|                                     'Invalid provider, please check backend logs', |  | ||||||
|                                   disabled: true, |  | ||||||
|                                 }, |  | ||||||
|                               ]; |  | ||||||
|                         })()} |  | ||||||
|                       /> |  | ||||||
|                     </div> |  | ||||||
|                   )} |  | ||||||
|                 </div> |  | ||||||
|               )} |  | ||||||
|             </SettingsSection> |  | ||||||
|  |  | ||||||
|             <SettingsSection title="API Keys"> |  | ||||||
|               <div className="flex flex-col space-y-4"> |  | ||||||
|                 <div className="flex flex-col space-y-1"> |  | ||||||
|                   <p className="text-black/70 dark:text-white/70 text-sm"> |  | ||||||
|                     OpenAI API Key |  | ||||||
|                   </p> |  | ||||||
|                   <Input |  | ||||||
|                     type="text" |  | ||||||
|                     placeholder="OpenAI API Key" |  | ||||||
|                     value={config.openaiApiKey} |  | ||||||
|                     isSaving={savingStates['openaiApiKey']} |  | ||||||
|                     onChange={(e) => { |  | ||||||
|                       setConfig((prev) => ({ |  | ||||||
|                         ...prev!, |  | ||||||
|                         openaiApiKey: e.target.value, |  | ||||||
|                       })); |  | ||||||
|                     }} |  | ||||||
|                     onSave={(value) => saveConfig('openaiApiKey', value)} |  | ||||||
|                   /> |  | ||||||
|                 </div> |  | ||||||
|  |  | ||||||
|                 <div className="flex flex-col space-y-1"> |  | ||||||
|                   <p className="text-black/70 dark:text-white/70 text-sm"> |  | ||||||
|                     Ollama API URL |  | ||||||
|                   </p> |  | ||||||
|                   <Input |  | ||||||
|                     type="text" |  | ||||||
|                     placeholder="Ollama API URL" |  | ||||||
|                     value={config.ollamaApiUrl} |  | ||||||
|                     isSaving={savingStates['ollamaApiUrl']} |  | ||||||
|                     onChange={(e) => { |  | ||||||
|                       setConfig((prev) => ({ |  | ||||||
|                         ...prev!, |  | ||||||
|                         ollamaApiUrl: e.target.value, |  | ||||||
|                       })); |  | ||||||
|                     }} |  | ||||||
|                     onSave={(value) => saveConfig('ollamaApiUrl', value)} |  | ||||||
|                   /> |  | ||||||
|                 </div> |  | ||||||
|  |  | ||||||
|                 <div className="flex flex-col space-y-1"> |  | ||||||
|                   <p className="text-black/70 dark:text-white/70 text-sm"> |  | ||||||
|                     GROQ API Key |  | ||||||
|                   </p> |  | ||||||
|                   <Input |  | ||||||
|                     type="text" |  | ||||||
|                     placeholder="GROQ API Key" |  | ||||||
|                     value={config.groqApiKey} |  | ||||||
|                     isSaving={savingStates['groqApiKey']} |  | ||||||
|                     onChange={(e) => { |  | ||||||
|                       setConfig((prev) => ({ |  | ||||||
|                         ...prev!, |  | ||||||
|                         groqApiKey: e.target.value, |  | ||||||
|                       })); |  | ||||||
|                     }} |  | ||||||
|                     onSave={(value) => saveConfig('groqApiKey', value)} |  | ||||||
|                   /> |  | ||||||
|                 </div> |  | ||||||
|  |  | ||||||
|                 <div className="flex flex-col space-y-1"> |  | ||||||
|                   <p className="text-black/70 dark:text-white/70 text-sm"> |  | ||||||
|                     Anthropic API Key |  | ||||||
|                   </p> |  | ||||||
|                   <Input |  | ||||||
|                     type="text" |  | ||||||
|                     placeholder="Anthropic API key" |  | ||||||
|                     value={config.anthropicApiKey} |  | ||||||
|                     isSaving={savingStates['anthropicApiKey']} |  | ||||||
|                     onChange={(e) => { |  | ||||||
|                       setConfig((prev) => ({ |  | ||||||
|                         ...prev!, |  | ||||||
|                         anthropicApiKey: e.target.value, |  | ||||||
|                       })); |  | ||||||
|                     }} |  | ||||||
|                     onSave={(value) => saveConfig('anthropicApiKey', value)} |  | ||||||
|                   /> |  | ||||||
|                 </div> |  | ||||||
|  |  | ||||||
|                 <div className="flex flex-col space-y-1"> |  | ||||||
|                   <p className="text-black/70 dark:text-white/70 text-sm"> |  | ||||||
|                     Gemini API Key |  | ||||||
|                   </p> |  | ||||||
|                   <Input |  | ||||||
|                     type="text" |  | ||||||
|                     placeholder="Gemini API key" |  | ||||||
|                     value={config.geminiApiKey} |  | ||||||
|                     isSaving={savingStates['geminiApiKey']} |  | ||||||
|                     onChange={(e) => { |  | ||||||
|                       setConfig((prev) => ({ |  | ||||||
|                         ...prev!, |  | ||||||
|                         geminiApiKey: e.target.value, |  | ||||||
|                       })); |  | ||||||
|                     }} |  | ||||||
|                     onSave={(value) => saveConfig('geminiApiKey', value)} |  | ||||||
|                   /> |  | ||||||
|                 </div> |  | ||||||
|  |  | ||||||
|                 <div className="flex flex-col space-y-1"> |  | ||||||
|                   <p className="text-black/70 dark:text-white/70 text-sm"> |  | ||||||
|                     Deepseek API Key |  | ||||||
|                   </p> |  | ||||||
|                   <Input |  | ||||||
|                     type="text" |  | ||||||
|                     placeholder="Deepseek API Key" |  | ||||||
|                     value={config.deepseekApiKey} |  | ||||||
|                     isSaving={savingStates['deepseekApiKey']} |  | ||||||
|                     onChange={(e) => { |  | ||||||
|                       setConfig((prev) => ({ |  | ||||||
|                         ...prev!, |  | ||||||
|                         deepseekApiKey: e.target.value, |  | ||||||
|                       })); |  | ||||||
|                     }} |  | ||||||
|                     onSave={(value) => saveConfig('deepseekApiKey', value)} |  | ||||||
|                   /> |  | ||||||
|                 </div> |  | ||||||
|               </div> |  | ||||||
|             </SettingsSection> |  | ||||||
|           </div> |  | ||||||
|         ) |  | ||||||
|       )} |  | ||||||
|     </div> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default Page; |  | ||||||
| @@ -2,31 +2,13 @@ | |||||||
|  |  | ||||||
| import { Fragment, useEffect, useRef, useState } from 'react'; | import { Fragment, useEffect, useRef, useState } from 'react'; | ||||||
| import MessageInput from './MessageInput'; | import MessageInput from './MessageInput'; | ||||||
| import { File, Message } from './ChatWindow'; |  | ||||||
| import MessageBox from './MessageBox'; | import MessageBox from './MessageBox'; | ||||||
| import MessageBoxLoading from './MessageBoxLoading'; | import MessageBoxLoading from './MessageBoxLoading'; | ||||||
|  | import { useChat } from '@/lib/hooks/useChat'; | ||||||
|  |  | ||||||
|  | const Chat = () => { | ||||||
|  |   const { sections, chatTurns, loading, messageAppeared } = useChat(); | ||||||
|  |  | ||||||
| const Chat = ({ |  | ||||||
|   loading, |  | ||||||
|   messages, |  | ||||||
|   sendMessage, |  | ||||||
|   messageAppeared, |  | ||||||
|   rewrite, |  | ||||||
|   fileIds, |  | ||||||
|   setFileIds, |  | ||||||
|   files, |  | ||||||
|   setFiles, |  | ||||||
| }: { |  | ||||||
|   messages: Message[]; |  | ||||||
|   sendMessage: (message: string) => void; |  | ||||||
|   loading: boolean; |  | ||||||
|   messageAppeared: boolean; |  | ||||||
|   rewrite: (messageId: string) => void; |  | ||||||
|   fileIds: string[]; |  | ||||||
|   setFileIds: (fileIds: string[]) => void; |  | ||||||
|   files: File[]; |  | ||||||
|   setFiles: (files: File[]) => void; |  | ||||||
| }) => { |  | ||||||
|   const [dividerWidth, setDividerWidth] = useState(0); |   const [dividerWidth, setDividerWidth] = useState(0); | ||||||
|   const dividerRef = useRef<HTMLDivElement | null>(null); |   const dividerRef = useRef<HTMLDivElement | null>(null); | ||||||
|   const messageEnd = useRef<HTMLDivElement | null>(null); |   const messageEnd = useRef<HTMLDivElement | null>(null); | ||||||
| @@ -45,41 +27,36 @@ const Chat = ({ | |||||||
|     return () => { |     return () => { | ||||||
|       window.removeEventListener('resize', updateDividerWidth); |       window.removeEventListener('resize', updateDividerWidth); | ||||||
|     }; |     }; | ||||||
|   }); |   }, []); | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     const scroll = () => { |     const scroll = () => { | ||||||
|       messageEnd.current?.scrollIntoView({ behavior: 'smooth' }); |       messageEnd.current?.scrollIntoView({ behavior: 'smooth' }); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     if (messages.length === 1) { |     if (chatTurns.length === 1) { | ||||||
|       document.title = `${messages[0].content.substring(0, 30)} - Perplexica`; |       document.title = `${chatTurns[0].content.substring(0, 30)} - Perplexica`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (messages[messages.length - 1]?.role == 'user') { |     if (chatTurns[chatTurns.length - 1]?.role === 'user') { | ||||||
|       scroll(); |       scroll(); | ||||||
|     } |     } | ||||||
|   }, [messages]); |   }, [chatTurns]); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <div className="flex flex-col space-y-6 pt-8 pb-44 lg:pb-32 sm:mx-4 md:mx-8"> |     <div className="flex flex-col space-y-6 pt-8 pb-44 lg:pb-32 sm:mx-4 md:mx-8"> | ||||||
|       {messages.map((msg, i) => { |       {sections.map((section, i) => { | ||||||
|         const isLast = i === messages.length - 1; |         const isLast = i === sections.length - 1; | ||||||
|  |  | ||||||
|         return ( |         return ( | ||||||
|           <Fragment key={msg.messageId}> |           <Fragment key={section.userMessage.messageId}> | ||||||
|             <MessageBox |             <MessageBox | ||||||
|               key={i} |               section={section} | ||||||
|               message={msg} |               sectionIndex={i} | ||||||
|               messageIndex={i} |  | ||||||
|               history={messages} |  | ||||||
|               loading={loading} |  | ||||||
|               dividerRef={isLast ? dividerRef : undefined} |               dividerRef={isLast ? dividerRef : undefined} | ||||||
|               isLast={isLast} |               isLast={isLast} | ||||||
|               rewrite={rewrite} |  | ||||||
|               sendMessage={sendMessage} |  | ||||||
|             /> |             /> | ||||||
|             {!isLast && msg.role === 'assistant' && ( |             {!isLast && ( | ||||||
|               <div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" /> |               <div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" /> | ||||||
|             )} |             )} | ||||||
|           </Fragment> |           </Fragment> | ||||||
| @@ -92,14 +69,7 @@ const Chat = ({ | |||||||
|           className="bottom-24 lg:bottom-10 fixed z-40" |           className="bottom-24 lg:bottom-10 fixed z-40" | ||||||
|           style={{ width: dividerWidth }} |           style={{ width: dividerWidth }} | ||||||
|         > |         > | ||||||
|           <MessageInput |           <MessageInput /> | ||||||
|             loading={loading} |  | ||||||
|             sendMessage={sendMessage} |  | ||||||
|             fileIds={fileIds} |  | ||||||
|             setFileIds={setFileIds} |  | ||||||
|             files={files} |  | ||||||
|             setFiles={setFiles} |  | ||||||
|           /> |  | ||||||
|         </div> |         </div> | ||||||
|       )} |       )} | ||||||
|     </div> |     </div> | ||||||
|   | |||||||
| @@ -1,27 +1,48 @@ | |||||||
| 'use client'; | 'use client'; | ||||||
|  |  | ||||||
| import { useEffect, useRef, useState } from 'react'; |  | ||||||
| import { Document } from '@langchain/core/documents'; | import { Document } from '@langchain/core/documents'; | ||||||
| import Navbar from './Navbar'; | import Navbar from './Navbar'; | ||||||
| import Chat from './Chat'; | import Chat from './Chat'; | ||||||
| import EmptyChat from './EmptyChat'; | import EmptyChat from './EmptyChat'; | ||||||
| import crypto from 'crypto'; |  | ||||||
| import { toast } from 'sonner'; |  | ||||||
| import { useSearchParams } from 'next/navigation'; |  | ||||||
| import { getSuggestions } from '@/lib/actions'; |  | ||||||
| import { Settings } from 'lucide-react'; | import { Settings } from 'lucide-react'; | ||||||
| import Link from 'next/link'; | import Link from 'next/link'; | ||||||
| import NextError from 'next/error'; | import NextError from 'next/error'; | ||||||
|  | import { useChat } from '@/lib/hooks/useChat'; | ||||||
|  | import Loader from './ui/Loader'; | ||||||
|  |  | ||||||
| export type Message = { | export interface BaseMessage { | ||||||
|   messageId: string; |  | ||||||
|   chatId: string; |   chatId: string; | ||||||
|  |   messageId: string; | ||||||
|   createdAt: Date; |   createdAt: Date; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface AssistantMessage extends BaseMessage { | ||||||
|  |   role: 'assistant'; | ||||||
|   content: string; |   content: string; | ||||||
|   role: 'user' | 'assistant'; |  | ||||||
|   suggestions?: string[]; |   suggestions?: string[]; | ||||||
|   sources?: Document[]; | } | ||||||
| }; |  | ||||||
|  | export interface UserMessage extends BaseMessage { | ||||||
|  |   role: 'user'; | ||||||
|  |   content: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface SourceMessage extends BaseMessage { | ||||||
|  |   role: 'source'; | ||||||
|  |   sources: Document[]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface SuggestionMessage extends BaseMessage { | ||||||
|  |   role: 'suggestion'; | ||||||
|  |   suggestions: string[]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export type Message = | ||||||
|  |   | AssistantMessage | ||||||
|  |   | UserMessage | ||||||
|  |   | SourceMessage | ||||||
|  |   | SuggestionMessage; | ||||||
|  | export type ChatTurn = UserMessage | AssistantMessage; | ||||||
|  |  | ||||||
| export interface File { | export interface File { | ||||||
|   fileName: string; |   fileName: string; | ||||||
| @@ -29,512 +50,8 @@ export interface File { | |||||||
|   fileId: string; |   fileId: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| interface ChatModelProvider { | const ChatWindow = () => { | ||||||
|   name: string; |   const { hasError, isReady, notFound, messages } = useChat(); | ||||||
|   provider: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| interface EmbeddingModelProvider { |  | ||||||
|   name: string; |  | ||||||
|   provider: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const checkConfig = async ( |  | ||||||
|   setChatModelProvider: (provider: ChatModelProvider) => void, |  | ||||||
|   setEmbeddingModelProvider: (provider: EmbeddingModelProvider) => void, |  | ||||||
|   setIsConfigReady: (ready: boolean) => void, |  | ||||||
|   setHasError: (hasError: boolean) => void, |  | ||||||
| ) => { |  | ||||||
|   try { |  | ||||||
|     let chatModel = localStorage.getItem('chatModel'); |  | ||||||
|     let chatModelProvider = localStorage.getItem('chatModelProvider'); |  | ||||||
|     let embeddingModel = localStorage.getItem('embeddingModel'); |  | ||||||
|     let embeddingModelProvider = localStorage.getItem('embeddingModelProvider'); |  | ||||||
|  |  | ||||||
|     const autoImageSearch = localStorage.getItem('autoImageSearch'); |  | ||||||
|     const autoVideoSearch = localStorage.getItem('autoVideoSearch'); |  | ||||||
|  |  | ||||||
|     if (!autoImageSearch) { |  | ||||||
|       localStorage.setItem('autoImageSearch', 'true'); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!autoVideoSearch) { |  | ||||||
|       localStorage.setItem('autoVideoSearch', 'false'); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const providers = await fetch(`/api/models`, { |  | ||||||
|       headers: { |  | ||||||
|         'Content-Type': 'application/json', |  | ||||||
|       }, |  | ||||||
|     }).then(async (res) => { |  | ||||||
|       if (!res.ok) |  | ||||||
|         throw new Error( |  | ||||||
|           `Failed to fetch models: ${res.status} ${res.statusText}`, |  | ||||||
|         ); |  | ||||||
|       return res.json(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     if ( |  | ||||||
|       !chatModel || |  | ||||||
|       !chatModelProvider || |  | ||||||
|       !embeddingModel || |  | ||||||
|       !embeddingModelProvider |  | ||||||
|     ) { |  | ||||||
|       if (!chatModel || !chatModelProvider) { |  | ||||||
|         const chatModelProviders = providers.chatModelProviders; |  | ||||||
|  |  | ||||||
|         chatModelProvider = |  | ||||||
|           chatModelProvider || Object.keys(chatModelProviders)[0]; |  | ||||||
|  |  | ||||||
|         chatModel = Object.keys(chatModelProviders[chatModelProvider])[0]; |  | ||||||
|  |  | ||||||
|         if (!chatModelProviders || Object.keys(chatModelProviders).length === 0) |  | ||||||
|           return toast.error('No chat models available'); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (!embeddingModel || !embeddingModelProvider) { |  | ||||||
|         const embeddingModelProviders = providers.embeddingModelProviders; |  | ||||||
|  |  | ||||||
|         if ( |  | ||||||
|           !embeddingModelProviders || |  | ||||||
|           Object.keys(embeddingModelProviders).length === 0 |  | ||||||
|         ) |  | ||||||
|           return toast.error('No embedding models available'); |  | ||||||
|  |  | ||||||
|         embeddingModelProvider = Object.keys(embeddingModelProviders)[0]; |  | ||||||
|         embeddingModel = Object.keys( |  | ||||||
|           embeddingModelProviders[embeddingModelProvider], |  | ||||||
|         )[0]; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       localStorage.setItem('chatModel', chatModel!); |  | ||||||
|       localStorage.setItem('chatModelProvider', chatModelProvider); |  | ||||||
|       localStorage.setItem('embeddingModel', embeddingModel!); |  | ||||||
|       localStorage.setItem('embeddingModelProvider', embeddingModelProvider); |  | ||||||
|     } else { |  | ||||||
|       const chatModelProviders = providers.chatModelProviders; |  | ||||||
|       const embeddingModelProviders = providers.embeddingModelProviders; |  | ||||||
|  |  | ||||||
|       if ( |  | ||||||
|         Object.keys(chatModelProviders).length > 0 && |  | ||||||
|         !chatModelProviders[chatModelProvider] |  | ||||||
|       ) { |  | ||||||
|         const chatModelProvidersKeys = Object.keys(chatModelProviders); |  | ||||||
|         chatModelProvider = |  | ||||||
|           chatModelProvidersKeys.find( |  | ||||||
|             (key) => Object.keys(chatModelProviders[key]).length > 0, |  | ||||||
|           ) || chatModelProvidersKeys[0]; |  | ||||||
|  |  | ||||||
|         localStorage.setItem('chatModelProvider', chatModelProvider); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if ( |  | ||||||
|         chatModelProvider && |  | ||||||
|         !chatModelProviders[chatModelProvider][chatModel] |  | ||||||
|       ) { |  | ||||||
|         chatModel = Object.keys( |  | ||||||
|           chatModelProviders[ |  | ||||||
|             Object.keys(chatModelProviders[chatModelProvider]).length > 0 |  | ||||||
|               ? chatModelProvider |  | ||||||
|               : Object.keys(chatModelProviders)[0] |  | ||||||
|           ], |  | ||||||
|         )[0]; |  | ||||||
|         localStorage.setItem('chatModel', chatModel); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if ( |  | ||||||
|         Object.keys(embeddingModelProviders).length > 0 && |  | ||||||
|         !embeddingModelProviders[embeddingModelProvider] |  | ||||||
|       ) { |  | ||||||
|         embeddingModelProvider = Object.keys(embeddingModelProviders)[0]; |  | ||||||
|         localStorage.setItem('embeddingModelProvider', embeddingModelProvider); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if ( |  | ||||||
|         embeddingModelProvider && |  | ||||||
|         !embeddingModelProviders[embeddingModelProvider][embeddingModel] |  | ||||||
|       ) { |  | ||||||
|         embeddingModel = Object.keys( |  | ||||||
|           embeddingModelProviders[embeddingModelProvider], |  | ||||||
|         )[0]; |  | ||||||
|         localStorage.setItem('embeddingModel', embeddingModel); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     setChatModelProvider({ |  | ||||||
|       name: chatModel!, |  | ||||||
|       provider: chatModelProvider, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     setEmbeddingModelProvider({ |  | ||||||
|       name: embeddingModel!, |  | ||||||
|       provider: embeddingModelProvider, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     setIsConfigReady(true); |  | ||||||
|   } catch (err) { |  | ||||||
|     console.error('An error occurred while checking the configuration:', err); |  | ||||||
|     setIsConfigReady(false); |  | ||||||
|     setHasError(true); |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const loadMessages = async ( |  | ||||||
|   chatId: string, |  | ||||||
|   setMessages: (messages: Message[]) => void, |  | ||||||
|   setIsMessagesLoaded: (loaded: boolean) => void, |  | ||||||
|   setChatHistory: (history: [string, string][]) => void, |  | ||||||
|   setFocusMode: (mode: string) => void, |  | ||||||
|   setNotFound: (notFound: boolean) => void, |  | ||||||
|   setFiles: (files: File[]) => void, |  | ||||||
|   setFileIds: (fileIds: string[]) => void, |  | ||||||
| ) => { |  | ||||||
|   const res = await fetch(`/api/chats/${chatId}`, { |  | ||||||
|     method: 'GET', |  | ||||||
|     headers: { |  | ||||||
|       'Content-Type': 'application/json', |  | ||||||
|     }, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   if (res.status === 404) { |  | ||||||
|     setNotFound(true); |  | ||||||
|     setIsMessagesLoaded(true); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const data = await res.json(); |  | ||||||
|  |  | ||||||
|   const messages = data.messages.map((msg: any) => { |  | ||||||
|     return { |  | ||||||
|       ...msg, |  | ||||||
|       ...JSON.parse(msg.metadata), |  | ||||||
|     }; |  | ||||||
|   }) as Message[]; |  | ||||||
|  |  | ||||||
|   setMessages(messages); |  | ||||||
|  |  | ||||||
|   const history = messages.map((msg) => { |  | ||||||
|     return [msg.role, msg.content]; |  | ||||||
|   }) as [string, string][]; |  | ||||||
|  |  | ||||||
|   console.debug(new Date(), 'app:messages_loaded'); |  | ||||||
|  |  | ||||||
|   document.title = messages[0].content; |  | ||||||
|  |  | ||||||
|   const files = data.chat.files.map((file: any) => { |  | ||||||
|     return { |  | ||||||
|       fileName: file.name, |  | ||||||
|       fileExtension: file.name.split('.').pop(), |  | ||||||
|       fileId: file.fileId, |  | ||||||
|     }; |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   setFiles(files); |  | ||||||
|   setFileIds(files.map((file: File) => file.fileId)); |  | ||||||
|  |  | ||||||
|   setChatHistory(history); |  | ||||||
|   setFocusMode(data.chat.focusMode); |  | ||||||
|   setIsMessagesLoaded(true); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const ChatWindow = ({ id }: { id?: string }) => { |  | ||||||
|   const searchParams = useSearchParams(); |  | ||||||
|   const initialMessage = searchParams.get('q'); |  | ||||||
|  |  | ||||||
|   const [chatId, setChatId] = useState<string | undefined>(id); |  | ||||||
|   const [newChatCreated, setNewChatCreated] = useState(false); |  | ||||||
|  |  | ||||||
|   const [chatModelProvider, setChatModelProvider] = useState<ChatModelProvider>( |  | ||||||
|     { |  | ||||||
|       name: '', |  | ||||||
|       provider: '', |  | ||||||
|     }, |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   const [embeddingModelProvider, setEmbeddingModelProvider] = |  | ||||||
|     useState<EmbeddingModelProvider>({ |  | ||||||
|       name: '', |  | ||||||
|       provider: '', |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|   const [isConfigReady, setIsConfigReady] = useState(false); |  | ||||||
|   const [hasError, setHasError] = useState(false); |  | ||||||
|   const [isReady, setIsReady] = useState(false); |  | ||||||
|  |  | ||||||
|   useEffect(() => { |  | ||||||
|     checkConfig( |  | ||||||
|       setChatModelProvider, |  | ||||||
|       setEmbeddingModelProvider, |  | ||||||
|       setIsConfigReady, |  | ||||||
|       setHasError, |  | ||||||
|     ); |  | ||||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps |  | ||||||
|   }, []); |  | ||||||
|  |  | ||||||
|   const [loading, setLoading] = useState(false); |  | ||||||
|   const [messageAppeared, setMessageAppeared] = useState(false); |  | ||||||
|  |  | ||||||
|   const [chatHistory, setChatHistory] = useState<[string, string][]>([]); |  | ||||||
|   const [messages, setMessages] = useState<Message[]>([]); |  | ||||||
|  |  | ||||||
|   const [files, setFiles] = useState<File[]>([]); |  | ||||||
|   const [fileIds, setFileIds] = useState<string[]>([]); |  | ||||||
|  |  | ||||||
|   const [focusMode, setFocusMode] = useState('webSearch'); |  | ||||||
|   const [optimizationMode, setOptimizationMode] = useState('speed'); |  | ||||||
|  |  | ||||||
|   const [isMessagesLoaded, setIsMessagesLoaded] = useState(false); |  | ||||||
|  |  | ||||||
|   const [notFound, setNotFound] = useState(false); |  | ||||||
|  |  | ||||||
|   useEffect(() => { |  | ||||||
|     if ( |  | ||||||
|       chatId && |  | ||||||
|       !newChatCreated && |  | ||||||
|       !isMessagesLoaded && |  | ||||||
|       messages.length === 0 |  | ||||||
|     ) { |  | ||||||
|       loadMessages( |  | ||||||
|         chatId, |  | ||||||
|         setMessages, |  | ||||||
|         setIsMessagesLoaded, |  | ||||||
|         setChatHistory, |  | ||||||
|         setFocusMode, |  | ||||||
|         setNotFound, |  | ||||||
|         setFiles, |  | ||||||
|         setFileIds, |  | ||||||
|       ); |  | ||||||
|     } else if (!chatId) { |  | ||||||
|       setNewChatCreated(true); |  | ||||||
|       setIsMessagesLoaded(true); |  | ||||||
|       setChatId(crypto.randomBytes(20).toString('hex')); |  | ||||||
|     } |  | ||||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps |  | ||||||
|   }, []); |  | ||||||
|  |  | ||||||
|   const messagesRef = useRef<Message[]>([]); |  | ||||||
|  |  | ||||||
|   useEffect(() => { |  | ||||||
|     messagesRef.current = messages; |  | ||||||
|   }, [messages]); |  | ||||||
|  |  | ||||||
|   useEffect(() => { |  | ||||||
|     if (isMessagesLoaded && isConfigReady) { |  | ||||||
|       setIsReady(true); |  | ||||||
|       console.debug(new Date(), 'app:ready'); |  | ||||||
|     } else { |  | ||||||
|       setIsReady(false); |  | ||||||
|     } |  | ||||||
|   }, [isMessagesLoaded, isConfigReady]); |  | ||||||
|  |  | ||||||
|   const sendMessage = async (message: string, messageId?: string) => { |  | ||||||
|     if (loading) return; |  | ||||||
|     if (!isConfigReady) { |  | ||||||
|       toast.error('Cannot send message before the configuration is ready'); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     setLoading(true); |  | ||||||
|     setMessageAppeared(false); |  | ||||||
|  |  | ||||||
|     let sources: Document[] | undefined = undefined; |  | ||||||
|     let recievedMessage = ''; |  | ||||||
|     let added = false; |  | ||||||
|  |  | ||||||
|     messageId = messageId ?? crypto.randomBytes(7).toString('hex'); |  | ||||||
|  |  | ||||||
|     setMessages((prevMessages) => [ |  | ||||||
|       ...prevMessages, |  | ||||||
|       { |  | ||||||
|         content: message, |  | ||||||
|         messageId: messageId, |  | ||||||
|         chatId: chatId!, |  | ||||||
|         role: 'user', |  | ||||||
|         createdAt: new Date(), |  | ||||||
|       }, |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     const messageHandler = async (data: any) => { |  | ||||||
|       if (data.type === 'error') { |  | ||||||
|         toast.error(data.data); |  | ||||||
|         setLoading(false); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (data.type === 'sources') { |  | ||||||
|         sources = data.data; |  | ||||||
|         if (!added) { |  | ||||||
|           setMessages((prevMessages) => [ |  | ||||||
|             ...prevMessages, |  | ||||||
|             { |  | ||||||
|               content: '', |  | ||||||
|               messageId: data.messageId, |  | ||||||
|               chatId: chatId!, |  | ||||||
|               role: 'assistant', |  | ||||||
|               sources: sources, |  | ||||||
|               createdAt: new Date(), |  | ||||||
|             }, |  | ||||||
|           ]); |  | ||||||
|           added = true; |  | ||||||
|         } |  | ||||||
|         setMessageAppeared(true); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (data.type === 'message') { |  | ||||||
|         if (!added) { |  | ||||||
|           setMessages((prevMessages) => [ |  | ||||||
|             ...prevMessages, |  | ||||||
|             { |  | ||||||
|               content: data.data, |  | ||||||
|               messageId: data.messageId, |  | ||||||
|               chatId: chatId!, |  | ||||||
|               role: 'assistant', |  | ||||||
|               sources: sources, |  | ||||||
|               createdAt: new Date(), |  | ||||||
|             }, |  | ||||||
|           ]); |  | ||||||
|           added = true; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         setMessages((prev) => |  | ||||||
|           prev.map((message) => { |  | ||||||
|             if (message.messageId === data.messageId) { |  | ||||||
|               return { ...message, content: message.content + data.data }; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             return message; |  | ||||||
|           }), |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         recievedMessage += data.data; |  | ||||||
|         setMessageAppeared(true); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (data.type === 'messageEnd') { |  | ||||||
|         setChatHistory((prevHistory) => [ |  | ||||||
|           ...prevHistory, |  | ||||||
|           ['human', message], |  | ||||||
|           ['assistant', recievedMessage], |  | ||||||
|         ]); |  | ||||||
|  |  | ||||||
|         setLoading(false); |  | ||||||
|  |  | ||||||
|         const lastMsg = messagesRef.current[messagesRef.current.length - 1]; |  | ||||||
|  |  | ||||||
|         const autoImageSearch = localStorage.getItem('autoImageSearch'); |  | ||||||
|         const autoVideoSearch = localStorage.getItem('autoVideoSearch'); |  | ||||||
|  |  | ||||||
|         if (autoImageSearch === 'true') { |  | ||||||
|           document |  | ||||||
|             .getElementById(`search-images-${lastMsg.messageId}`) |  | ||||||
|             ?.click(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (autoVideoSearch === 'true') { |  | ||||||
|           document |  | ||||||
|             .getElementById(`search-videos-${lastMsg.messageId}`) |  | ||||||
|             ?.click(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if ( |  | ||||||
|           lastMsg.role === 'assistant' && |  | ||||||
|           lastMsg.sources && |  | ||||||
|           lastMsg.sources.length > 0 && |  | ||||||
|           !lastMsg.suggestions |  | ||||||
|         ) { |  | ||||||
|           const suggestions = await getSuggestions(messagesRef.current); |  | ||||||
|           setMessages((prev) => |  | ||||||
|             prev.map((msg) => { |  | ||||||
|               if (msg.messageId === lastMsg.messageId) { |  | ||||||
|                 return { ...msg, suggestions: suggestions }; |  | ||||||
|               } |  | ||||||
|               return msg; |  | ||||||
|             }), |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     const res = await fetch('/api/chat', { |  | ||||||
|       method: 'POST', |  | ||||||
|       headers: { |  | ||||||
|         'Content-Type': 'application/json', |  | ||||||
|       }, |  | ||||||
|       body: JSON.stringify({ |  | ||||||
|         content: message, |  | ||||||
|         message: { |  | ||||||
|           messageId: messageId, |  | ||||||
|           chatId: chatId!, |  | ||||||
|           content: message, |  | ||||||
|         }, |  | ||||||
|         chatId: chatId!, |  | ||||||
|         files: fileIds, |  | ||||||
|         focusMode: focusMode, |  | ||||||
|         optimizationMode: optimizationMode, |  | ||||||
|         history: chatHistory, |  | ||||||
|         chatModel: { |  | ||||||
|           name: chatModelProvider.name, |  | ||||||
|           provider: chatModelProvider.provider, |  | ||||||
|         }, |  | ||||||
|         embeddingModel: { |  | ||||||
|           name: embeddingModelProvider.name, |  | ||||||
|           provider: embeddingModelProvider.provider, |  | ||||||
|         }, |  | ||||||
|         systemInstructions: localStorage.getItem('systemInstructions'), |  | ||||||
|       }), |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     if (!res.body) throw new Error('No response body'); |  | ||||||
|  |  | ||||||
|     const reader = res.body?.getReader(); |  | ||||||
|     const decoder = new TextDecoder('utf-8'); |  | ||||||
|  |  | ||||||
|     let partialChunk = ''; |  | ||||||
|  |  | ||||||
|     while (true) { |  | ||||||
|       const { value, done } = await reader.read(); |  | ||||||
|       if (done) break; |  | ||||||
|  |  | ||||||
|       partialChunk += decoder.decode(value, { stream: true }); |  | ||||||
|  |  | ||||||
|       try { |  | ||||||
|         const messages = partialChunk.split('\n'); |  | ||||||
|         for (const msg of messages) { |  | ||||||
|           if (!msg.trim()) continue; |  | ||||||
|           const json = JSON.parse(msg); |  | ||||||
|           messageHandler(json); |  | ||||||
|         } |  | ||||||
|         partialChunk = ''; |  | ||||||
|       } catch (error) { |  | ||||||
|         console.warn('Incomplete JSON, waiting for next chunk...'); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   const rewrite = (messageId: string) => { |  | ||||||
|     const index = messages.findIndex((msg) => msg.messageId === messageId); |  | ||||||
|  |  | ||||||
|     if (index === -1) return; |  | ||||||
|  |  | ||||||
|     const message = messages[index - 1]; |  | ||||||
|  |  | ||||||
|     setMessages((prev) => { |  | ||||||
|       return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)]; |  | ||||||
|     }); |  | ||||||
|     setChatHistory((prev) => { |  | ||||||
|       return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)]; |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     sendMessage(message.content, message.messageId); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   useEffect(() => { |  | ||||||
|     if (isReady && initialMessage && isConfigReady) { |  | ||||||
|       sendMessage(initialMessage); |  | ||||||
|     } |  | ||||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps |  | ||||||
|   }, [isConfigReady, isReady, initialMessage]); |  | ||||||
|  |  | ||||||
|   if (hasError) { |   if (hasError) { | ||||||
|     return ( |     return ( | ||||||
|       <div className="relative"> |       <div className="relative"> | ||||||
| @@ -559,52 +76,17 @@ const ChatWindow = ({ id }: { id?: string }) => { | |||||||
|       <div> |       <div> | ||||||
|         {messages.length > 0 ? ( |         {messages.length > 0 ? ( | ||||||
|           <> |           <> | ||||||
|             <Navbar chatId={chatId!} messages={messages} /> |             <Navbar /> | ||||||
|             <Chat |             <Chat /> | ||||||
|               loading={loading} |  | ||||||
|               messages={messages} |  | ||||||
|               sendMessage={sendMessage} |  | ||||||
|               messageAppeared={messageAppeared} |  | ||||||
|               rewrite={rewrite} |  | ||||||
|               fileIds={fileIds} |  | ||||||
|               setFileIds={setFileIds} |  | ||||||
|               files={files} |  | ||||||
|               setFiles={setFiles} |  | ||||||
|             /> |  | ||||||
|           </> |           </> | ||||||
|         ) : ( |         ) : ( | ||||||
|           <EmptyChat |           <EmptyChat /> | ||||||
|             sendMessage={sendMessage} |  | ||||||
|             focusMode={focusMode} |  | ||||||
|             setFocusMode={setFocusMode} |  | ||||||
|             optimizationMode={optimizationMode} |  | ||||||
|             setOptimizationMode={setOptimizationMode} |  | ||||||
|             fileIds={fileIds} |  | ||||||
|             setFileIds={setFileIds} |  | ||||||
|             files={files} |  | ||||||
|             setFiles={setFiles} |  | ||||||
|           /> |  | ||||||
|         )} |         )} | ||||||
|       </div> |       </div> | ||||||
|     ) |     ) | ||||||
|   ) : ( |   ) : ( | ||||||
|     <div className="flex flex-row items-center justify-center min-h-screen"> |     <div className="flex flex-row items-center justify-center min-h-screen"> | ||||||
|       <svg |       <Loader /> | ||||||
|         aria-hidden="true" |  | ||||||
|         className="w-8 h-8 text-light-200 fill-light-secondary dark:text-[#202020] animate-spin dark:fill-[#ffffff3b]" |  | ||||||
|         viewBox="0 0 100 101" |  | ||||||
|         fill="none" |  | ||||||
|         xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       > |  | ||||||
|         <path |  | ||||||
|           d="M100 50.5908C100.003 78.2051 78.1951 100.003 50.5908 100C22.9765 99.9972 0.997224 78.018 1 50.4037C1.00281 22.7993 22.8108 0.997224 50.4251 1C78.0395 1.00281 100.018 22.8108 100 50.4251ZM9.08164 50.594C9.06312 73.3997 27.7909 92.1272 50.5966 92.1457C73.4023 92.1642 92.1298 73.4365 92.1483 50.6308C92.1669 27.8251 73.4392 9.0973 50.6335 9.07878C27.8278 9.06026 9.10003 27.787 9.08164 50.594Z" |  | ||||||
|           fill="currentColor" |  | ||||||
|         /> |  | ||||||
|         <path |  | ||||||
|           d="M93.9676 39.0409C96.393 38.4037 97.8624 35.9116 96.9801 33.5533C95.1945 28.8227 92.871 24.3692 90.0681 20.348C85.6237 14.1775 79.4473 9.36872 72.0454 6.45794C64.6435 3.54717 56.3134 2.65431 48.3133 3.89319C45.869 4.27179 44.3768 6.77534 45.014 9.20079C45.6512 11.6262 48.1343 13.0956 50.5786 12.717C56.5073 11.8281 62.5542 12.5399 68.0406 14.7911C73.527 17.0422 78.2187 20.7487 81.5841 25.4923C83.7976 28.5886 85.4467 32.059 86.4416 35.7474C87.1273 38.1189 89.5423 39.6781 91.9676 39.0409Z" |  | ||||||
|           fill="currentFill" |  | ||||||
|         /> |  | ||||||
|       </svg> |  | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								src/components/Citation.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | |||||||
|  | const Citation = ({ | ||||||
|  |   href, | ||||||
|  |   children, | ||||||
|  | }: { | ||||||
|  |   href: string; | ||||||
|  |   children: React.ReactNode; | ||||||
|  | }) => { | ||||||
|  |   return ( | ||||||
|  |     <a | ||||||
|  |       href={href} | ||||||
|  |       target="_blank" | ||||||
|  |       className="bg-light-secondary dark:bg-dark-secondary px-1 rounded ml-1 no-underline text-xs text-black/70 dark:text-white/70 relative" | ||||||
|  |     > | ||||||
|  |       {children} | ||||||
|  |     </a> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default Citation; | ||||||
							
								
								
									
										70
									
								
								src/components/Discover/MajorNewsCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,70 @@ | |||||||
|  | import { Discover } from '@/app/discover/page'; | ||||||
|  | import Link from 'next/link'; | ||||||
|  |  | ||||||
|  | const MajorNewsCard = ({ | ||||||
|  |   item, | ||||||
|  |   isLeft = true, | ||||||
|  | }: { | ||||||
|  |   item: Discover; | ||||||
|  |   isLeft?: boolean; | ||||||
|  | }) => ( | ||||||
|  |   <Link | ||||||
|  |     href={`/?q=Summary: ${item.url}`} | ||||||
|  |     className="w-full group flex flex-row items-stretch gap-6 h-60 py-3" | ||||||
|  |     target="_blank" | ||||||
|  |   > | ||||||
|  |     {isLeft ? ( | ||||||
|  |       <> | ||||||
|  |         <div className="relative w-80 h-full overflow-hidden rounded-2xl flex-shrink-0"> | ||||||
|  |           <img | ||||||
|  |             className="object-cover w-full h-full group-hover:scale-105 transition-transform duration-500" | ||||||
|  |             src={ | ||||||
|  |               new URL(item.thumbnail).origin + | ||||||
|  |               new URL(item.thumbnail).pathname + | ||||||
|  |               `?id=${new URL(item.thumbnail).searchParams.get('id')}` | ||||||
|  |             } | ||||||
|  |             alt={item.title} | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  |         <div className="flex flex-col justify-center flex-1 py-4"> | ||||||
|  |           <h2 | ||||||
|  |             className="text-3xl font-light mb-3 leading-tight line-clamp-3 group-hover:text-cyan-500 dark:group-hover:text-cyan-300 transition duration-200" | ||||||
|  |             style={{ fontFamily: 'PP Editorial, serif' }} | ||||||
|  |           > | ||||||
|  |             {item.title} | ||||||
|  |           </h2> | ||||||
|  |           <p className="text-black/60 dark:text-white/60 text-base leading-relaxed line-clamp-4"> | ||||||
|  |             {item.content} | ||||||
|  |           </p> | ||||||
|  |         </div> | ||||||
|  |       </> | ||||||
|  |     ) : ( | ||||||
|  |       <> | ||||||
|  |         <div className="flex flex-col justify-center flex-1 py-4"> | ||||||
|  |           <h2 | ||||||
|  |             className="text-3xl font-light mb-3 leading-tight line-clamp-3 group-hover:text-cyan-500 dark:group-hover:text-cyan-300 transition duration-200" | ||||||
|  |             style={{ fontFamily: 'PP Editorial, serif' }} | ||||||
|  |           > | ||||||
|  |             {item.title} | ||||||
|  |           </h2> | ||||||
|  |           <p className="text-black/60 dark:text-white/60 text-base leading-relaxed line-clamp-4"> | ||||||
|  |             {item.content} | ||||||
|  |           </p> | ||||||
|  |         </div> | ||||||
|  |         <div className="relative w-80 h-full overflow-hidden rounded-2xl flex-shrink-0"> | ||||||
|  |           <img | ||||||
|  |             className="object-cover w-full h-full group-hover:scale-105 transition-transform duration-500" | ||||||
|  |             src={ | ||||||
|  |               new URL(item.thumbnail).origin + | ||||||
|  |               new URL(item.thumbnail).pathname + | ||||||
|  |               `?id=${new URL(item.thumbnail).searchParams.get('id')}` | ||||||
|  |             } | ||||||
|  |             alt={item.title} | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  |       </> | ||||||
|  |     )} | ||||||
|  |   </Link> | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | export default MajorNewsCard; | ||||||
							
								
								
									
										32
									
								
								src/components/Discover/SmallNewsCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,32 @@ | |||||||
|  | import { Discover } from '@/app/discover/page'; | ||||||
|  | import Link from 'next/link'; | ||||||
|  |  | ||||||
|  | const SmallNewsCard = ({ item }: { item: Discover }) => ( | ||||||
|  |   <Link | ||||||
|  |     href={`/?q=Summary: ${item.url}`} | ||||||
|  |     className="rounded-3xl overflow-hidden bg-light-secondary dark:bg-dark-secondary shadow-sm shadow-light-200/10 dark:shadow-black/25 group flex flex-col" | ||||||
|  |     target="_blank" | ||||||
|  |   > | ||||||
|  |     <div className="relative aspect-video overflow-hidden"> | ||||||
|  |       <img | ||||||
|  |         className="object-cover w-full h-full group-hover:scale-105 transition-transform duration-300" | ||||||
|  |         src={ | ||||||
|  |           new URL(item.thumbnail).origin + | ||||||
|  |           new URL(item.thumbnail).pathname + | ||||||
|  |           `?id=${new URL(item.thumbnail).searchParams.get('id')}` | ||||||
|  |         } | ||||||
|  |         alt={item.title} | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |     <div className="p-4"> | ||||||
|  |       <h3 className="font-semibold text-sm mb-2 leading-tight line-clamp-2 group-hover:text-cyan-500 dark:group-hover:text-cyan-300 transition duration-200"> | ||||||
|  |         {item.title} | ||||||
|  |       </h3> | ||||||
|  |       <p className="text-black/60 dark:text-white/60 text-xs leading-relaxed line-clamp-2"> | ||||||
|  |         {item.content} | ||||||
|  |       </p> | ||||||
|  |     </div> | ||||||
|  |   </Link> | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | export default SmallNewsCard; | ||||||
| @@ -1,32 +1,11 @@ | |||||||
| import { Settings } from 'lucide-react'; | import { Settings } from 'lucide-react'; | ||||||
| import EmptyChatMessageInput from './EmptyChatMessageInput'; | import EmptyChatMessageInput from './EmptyChatMessageInput'; | ||||||
| import { useState } from 'react'; |  | ||||||
| import { File } from './ChatWindow'; | import { File } from './ChatWindow'; | ||||||
| import Link from 'next/link'; | import Link from 'next/link'; | ||||||
|  | import WeatherWidget from './WeatherWidget'; | ||||||
|  | import NewsArticleWidget from './NewsArticleWidget'; | ||||||
|  |  | ||||||
| const EmptyChat = ({ | const EmptyChat = () => { | ||||||
|   sendMessage, |  | ||||||
|   focusMode, |  | ||||||
|   setFocusMode, |  | ||||||
|   optimizationMode, |  | ||||||
|   setOptimizationMode, |  | ||||||
|   fileIds, |  | ||||||
|   setFileIds, |  | ||||||
|   files, |  | ||||||
|   setFiles, |  | ||||||
| }: { |  | ||||||
|   sendMessage: (message: string) => void; |  | ||||||
|   focusMode: string; |  | ||||||
|   setFocusMode: (mode: string) => void; |  | ||||||
|   optimizationMode: string; |  | ||||||
|   setOptimizationMode: (mode: string) => void; |  | ||||||
|   fileIds: string[]; |  | ||||||
|   setFileIds: (fileIds: string[]) => void; |  | ||||||
|   files: File[]; |  | ||||||
|   setFiles: (files: File[]) => void; |  | ||||||
| }) => { |  | ||||||
|   const [isSettingsOpen, setIsSettingsOpen] = useState(false); |  | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <div className="relative"> |     <div className="relative"> | ||||||
|       <div className="absolute w-full flex flex-row items-center justify-end mr-5 mt-5"> |       <div className="absolute w-full flex flex-row items-center justify-end mr-5 mt-5"> | ||||||
| @@ -34,21 +13,21 @@ const EmptyChat = ({ | |||||||
|           <Settings className="cursor-pointer lg:hidden" /> |           <Settings className="cursor-pointer lg:hidden" /> | ||||||
|         </Link> |         </Link> | ||||||
|       </div> |       </div> | ||||||
|       <div className="flex flex-col items-center justify-center min-h-screen max-w-screen-sm mx-auto p-2 space-y-8"> |       <div className="flex flex-col items-center justify-center min-h-screen max-w-screen-sm mx-auto p-2 space-y-4"> | ||||||
|         <h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8"> |         <div className="flex flex-col items-center justify-center w-full space-y-8"> | ||||||
|           Research begins here. |           <h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8"> | ||||||
|         </h2> |             Research begins here. | ||||||
|         <EmptyChatMessageInput |           </h2> | ||||||
|           sendMessage={sendMessage} |           <EmptyChatMessageInput /> | ||||||
|           focusMode={focusMode} |         </div> | ||||||
|           setFocusMode={setFocusMode} |         <div className="flex flex-col w-full gap-4 mt-2 sm:flex-row sm:justify-center"> | ||||||
|           optimizationMode={optimizationMode} |           <div className="flex-1 w-full"> | ||||||
|           setOptimizationMode={setOptimizationMode} |             <WeatherWidget /> | ||||||
|           fileIds={fileIds} |           </div> | ||||||
|           setFileIds={setFileIds} |           <div className="flex-1 w-full"> | ||||||
|           files={files} |             <NewsArticleWidget /> | ||||||
|           setFiles={setFiles} |           </div> | ||||||
|         /> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
|   | |||||||
| @@ -1,34 +1,17 @@ | |||||||
| import { ArrowRight } from 'lucide-react'; | import { ArrowRight } from 'lucide-react'; | ||||||
| import { useEffect, useRef, useState } from 'react'; | import { useEffect, useRef, useState } from 'react'; | ||||||
| import TextareaAutosize from 'react-textarea-autosize'; | import TextareaAutosize from 'react-textarea-autosize'; | ||||||
| import CopilotToggle from './MessageInputActions/Copilot'; |  | ||||||
| import Focus from './MessageInputActions/Focus'; | import Focus from './MessageInputActions/Focus'; | ||||||
| import Optimization from './MessageInputActions/Optimization'; | import Optimization from './MessageInputActions/Optimization'; | ||||||
| import Attach from './MessageInputActions/Attach'; | import Attach from './MessageInputActions/Attach'; | ||||||
| import { File } from './ChatWindow'; | import { useChat } from '@/lib/hooks/useChat'; | ||||||
|  | import AttachSmall from './MessageInputActions/AttachSmall'; | ||||||
|  | import ModelSelector from './MessageInputActions/ModelSelector'; | ||||||
|  |  | ||||||
| const EmptyChatMessageInput = ({ | const EmptyChatMessageInput = () => { | ||||||
|   sendMessage, |   const { sendMessage } = useChat(); | ||||||
|   focusMode, |  | ||||||
|   setFocusMode, |   /* const [copilotEnabled, setCopilotEnabled] = useState(false); */ | ||||||
|   optimizationMode, |  | ||||||
|   setOptimizationMode, |  | ||||||
|   fileIds, |  | ||||||
|   setFileIds, |  | ||||||
|   files, |  | ||||||
|   setFiles, |  | ||||||
| }: { |  | ||||||
|   sendMessage: (message: string) => void; |  | ||||||
|   focusMode: string; |  | ||||||
|   setFocusMode: (mode: string) => void; |  | ||||||
|   optimizationMode: string; |  | ||||||
|   setOptimizationMode: (mode: string) => void; |  | ||||||
|   fileIds: string[]; |  | ||||||
|   setFileIds: (fileIds: string[]) => void; |  | ||||||
|   files: File[]; |  | ||||||
|   setFiles: (files: File[]) => void; |  | ||||||
| }) => { |  | ||||||
|   const [copilotEnabled, setCopilotEnabled] = useState(false); |  | ||||||
|   const [message, setMessage] = useState(''); |   const [message, setMessage] = useState(''); | ||||||
|  |  | ||||||
|   const inputRef = useRef<HTMLTextAreaElement | null>(null); |   const inputRef = useRef<HTMLTextAreaElement | null>(null); | ||||||
| @@ -73,34 +56,26 @@ const EmptyChatMessageInput = ({ | |||||||
|       }} |       }} | ||||||
|       className="w-full" |       className="w-full" | ||||||
|     > |     > | ||||||
|       <div className="flex flex-col bg-light-secondary dark:bg-dark-secondary px-5 pt-5 pb-2 rounded-lg w-full border border-light-200 dark:border-dark-200"> |       <div className="flex flex-col bg-light-secondary dark:bg-dark-secondary px-3 pt-5 pb-3 rounded-2xl w-full border border-light-200 dark:border-dark-200 shadow-sm shadow-light-200/10 dark:shadow-black/20 transition-all duration-200 focus-within:border-light-300 dark:focus-within:border-dark-300"> | ||||||
|         <TextareaAutosize |         <TextareaAutosize | ||||||
|           ref={inputRef} |           ref={inputRef} | ||||||
|           value={message} |           value={message} | ||||||
|           onChange={(e) => setMessage(e.target.value)} |           onChange={(e) => setMessage(e.target.value)} | ||||||
|           minRows={2} |           minRows={2} | ||||||
|           className="bg-transparent placeholder:text-black/50 dark:placeholder:text-white/50 text-sm text-black dark:text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48" |           className="px-2 bg-transparent placeholder:text-[15px] placeholder:text-black/50 dark:placeholder:text-white/50 text-sm text-black dark:text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48" | ||||||
|           placeholder="Ask anything..." |           placeholder="Ask anything..." | ||||||
|         /> |         /> | ||||||
|         <div className="flex flex-row items-center justify-between mt-4"> |         <div className="flex flex-row items-center justify-between mt-4"> | ||||||
|           <div className="flex flex-row items-center space-x-2 lg:space-x-4"> |           <Optimization /> | ||||||
|             <Focus focusMode={focusMode} setFocusMode={setFocusMode} /> |           <div className="flex flex-row items-center space-x-2"> | ||||||
|             <Attach |             <div className="flex flex-row items-center space-x-1"> | ||||||
|               fileIds={fileIds} |               <ModelSelector /> | ||||||
|               setFileIds={setFileIds} |               <Focus /> | ||||||
|               files={files} |               <Attach /> | ||||||
|               setFiles={setFiles} |             </div> | ||||||
|               showText |  | ||||||
|             /> |  | ||||||
|           </div> |  | ||||||
|           <div className="flex flex-row items-center space-x-1 sm:space-x-4"> |  | ||||||
|             <Optimization |  | ||||||
|               optimizationMode={optimizationMode} |  | ||||||
|               setOptimizationMode={setOptimizationMode} |  | ||||||
|             /> |  | ||||||
|             <button |             <button | ||||||
|               disabled={message.trim().length === 0} |               disabled={message.trim().length === 0} | ||||||
|               className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 disabled:bg-[#e0e0dc] dark:disabled:bg-[#ececec21] hover:bg-opacity-85 transition duration-100 rounded-full p-2" |               className="bg-sky-500 text-white disabled:text-black/50 dark:disabled:text-white/50 disabled:bg-[#e0e0dc] dark:disabled:bg-[#ececec21] hover:bg-opacity-85 transition duration-100 rounded-full p-2" | ||||||
|             > |             > | ||||||
|               <ArrowRight className="bg-background" size={17} /> |               <ArrowRight className="bg-background" size={17} /> | ||||||
|             </button> |             </button> | ||||||
|   | |||||||
| @@ -1,12 +1,13 @@ | |||||||
| import { Check, ClipboardList } from 'lucide-react'; | import { Check, ClipboardList } from 'lucide-react'; | ||||||
| import { Message } from '../ChatWindow'; | import { Message } from '../ChatWindow'; | ||||||
| import { useState } from 'react'; | import { useState } from 'react'; | ||||||
|  | import { Section } from '@/lib/hooks/useChat'; | ||||||
|  |  | ||||||
| const Copy = ({ | const Copy = ({ | ||||||
|   message, |   section, | ||||||
|   initialMessage, |   initialMessage, | ||||||
| }: { | }: { | ||||||
|   message: Message; |   section: Section; | ||||||
|   initialMessage: string; |   initialMessage: string; | ||||||
| }) => { | }) => { | ||||||
|   const [copied, setCopied] = useState(false); |   const [copied, setCopied] = useState(false); | ||||||
| @@ -14,7 +15,7 @@ const Copy = ({ | |||||||
|   return ( |   return ( | ||||||
|     <button |     <button | ||||||
|       onClick={() => { |       onClick={() => { | ||||||
|         const contentToCopy = `${initialMessage}${message.sources && message.sources.length > 0 && `\n\nCitations:\n${message.sources?.map((source: any, i: any) => `[${i + 1}] ${source.metadata.url}`).join(`\n`)}`}`; |         const contentToCopy = `${initialMessage}${section?.sourceMessage?.sources && section.sourceMessage.sources.length > 0 && `\n\nCitations:\n${section.sourceMessage.sources?.map((source: any, i: any) => `[${i + 1}] ${source.metadata.url}`).join(`\n`)}`}`; | ||||||
|         navigator.clipboard.writeText(contentToCopy); |         navigator.clipboard.writeText(contentToCopy); | ||||||
|         setCopied(true); |         setCopied(true); | ||||||
|         setTimeout(() => setCopied(false), 1000); |         setTimeout(() => setCopied(false), 1000); | ||||||
|   | |||||||
| @@ -1,8 +1,7 @@ | |||||||
| 'use client'; | 'use client'; | ||||||
|  |  | ||||||
| /* eslint-disable @next/next/no-img-element */ | /* eslint-disable @next/next/no-img-element */ | ||||||
| import React, { MutableRefObject, useEffect, useState } from 'react'; | import React, { MutableRefObject } from 'react'; | ||||||
| import { Message } from './ChatWindow'; |  | ||||||
| import { cn } from '@/lib/utils'; | import { cn } from '@/lib/utils'; | ||||||
| import { | import { | ||||||
|   BookCopy, |   BookCopy, | ||||||
| @@ -20,89 +19,37 @@ import SearchImages from './SearchImages'; | |||||||
| import SearchVideos from './SearchVideos'; | import SearchVideos from './SearchVideos'; | ||||||
| import { useSpeech } from 'react-text-to-speech'; | import { useSpeech } from 'react-text-to-speech'; | ||||||
| import ThinkBox from './ThinkBox'; | import ThinkBox from './ThinkBox'; | ||||||
|  | import { useChat, Section } from '@/lib/hooks/useChat'; | ||||||
|  | import Citation from './Citation'; | ||||||
|  |  | ||||||
| const ThinkTagProcessor = ({ children }: { children: React.ReactNode }) => { | const ThinkTagProcessor = ({ | ||||||
|   return <ThinkBox content={children as string} />; |   children, | ||||||
|  |   thinkingEnded, | ||||||
|  | }: { | ||||||
|  |   children: React.ReactNode; | ||||||
|  |   thinkingEnded: boolean; | ||||||
|  | }) => { | ||||||
|  |   return ( | ||||||
|  |     <ThinkBox content={children as string} thinkingEnded={thinkingEnded} /> | ||||||
|  |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const MessageBox = ({ | const MessageBox = ({ | ||||||
|   message, |   section, | ||||||
|   messageIndex, |   sectionIndex, | ||||||
|   history, |  | ||||||
|   loading, |  | ||||||
|   dividerRef, |   dividerRef, | ||||||
|   isLast, |   isLast, | ||||||
|   rewrite, |  | ||||||
|   sendMessage, |  | ||||||
| }: { | }: { | ||||||
|   message: Message; |   section: Section; | ||||||
|   messageIndex: number; |   sectionIndex: number; | ||||||
|   history: Message[]; |  | ||||||
|   loading: boolean; |  | ||||||
|   dividerRef?: MutableRefObject<HTMLDivElement | null>; |   dividerRef?: MutableRefObject<HTMLDivElement | null>; | ||||||
|   isLast: boolean; |   isLast: boolean; | ||||||
|   rewrite: (messageId: string) => void; |  | ||||||
|   sendMessage: (message: string) => void; |  | ||||||
| }) => { | }) => { | ||||||
|   const [parsedMessage, setParsedMessage] = useState(message.content); |   const { loading, chatTurns, sendMessage, rewrite } = useChat(); | ||||||
|   const [speechMessage, setSpeechMessage] = useState(message.content); |  | ||||||
|  |  | ||||||
|   useEffect(() => { |   const parsedMessage = section.parsedAssistantMessage || ''; | ||||||
|     const citationRegex = /\[([^\]]+)\]/g; |   const speechMessage = section.speechMessage || ''; | ||||||
|     const regex = /\[(\d+)\]/g; |   const thinkingEnded = section.thinkingEnded; | ||||||
|     let processedMessage = message.content; |  | ||||||
|  |  | ||||||
|     if (message.role === 'assistant' && message.content.includes('<think>')) { |  | ||||||
|       const openThinkTag = processedMessage.match(/<think>/g)?.length || 0; |  | ||||||
|       const closeThinkTag = processedMessage.match(/<\/think>/g)?.length || 0; |  | ||||||
|  |  | ||||||
|       if (openThinkTag > closeThinkTag) { |  | ||||||
|         processedMessage += '</think> <a> </a>'; // The extra <a> </a> is to prevent the the think component from looking bad |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if ( |  | ||||||
|       message.role === 'assistant' && |  | ||||||
|       message?.sources && |  | ||||||
|       message.sources.length > 0 |  | ||||||
|     ) { |  | ||||||
|       setParsedMessage( |  | ||||||
|         processedMessage.replace( |  | ||||||
|           citationRegex, |  | ||||||
|           (_, capturedContent: string) => { |  | ||||||
|             const numbers = capturedContent |  | ||||||
|               .split(',') |  | ||||||
|               .map((numStr) => numStr.trim()); |  | ||||||
|  |  | ||||||
|             const linksHtml = numbers |  | ||||||
|               .map((numStr) => { |  | ||||||
|                 const number = parseInt(numStr); |  | ||||||
|  |  | ||||||
|                 if (isNaN(number) || number <= 0) { |  | ||||||
|                   return `[${numStr}]`; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 const source = message.sources?.[number - 1]; |  | ||||||
|                 const url = source?.metadata?.url; |  | ||||||
|  |  | ||||||
|                 if (url) { |  | ||||||
|                   return `<a href="${url}" target="_blank" className="bg-light-secondary dark:bg-dark-secondary px-1 rounded ml-1 no-underline text-xs text-black/70 dark:text-white/70 relative">${numStr}</a>`; |  | ||||||
|                 } else { |  | ||||||
|                   return `[${numStr}]`; |  | ||||||
|                 } |  | ||||||
|               }) |  | ||||||
|               .join(''); |  | ||||||
|  |  | ||||||
|             return linksHtml; |  | ||||||
|           }, |  | ||||||
|         ), |  | ||||||
|       ); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     setSpeechMessage(message.content.replace(regex, '')); |  | ||||||
|     setParsedMessage(processedMessage); |  | ||||||
|   }, [message.content, message.sources, message.role]); |  | ||||||
|  |  | ||||||
|   const { speechStatus, start, stop } = useSpeech({ text: speechMessage }); |   const { speechStatus, start, stop } = useSpeech({ text: speechMessage }); | ||||||
|  |  | ||||||
| @@ -110,33 +57,31 @@ const MessageBox = ({ | |||||||
|     overrides: { |     overrides: { | ||||||
|       think: { |       think: { | ||||||
|         component: ThinkTagProcessor, |         component: ThinkTagProcessor, | ||||||
|  |         props: { | ||||||
|  |           thinkingEnded: thinkingEnded, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       citation: { | ||||||
|  |         component: Citation, | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <div> |     <div className="space-y-6"> | ||||||
|       {message.role === 'user' && ( |       <div className={'w-full pt-8 break-words'}> | ||||||
|         <div |         <h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12"> | ||||||
|           className={cn( |           {section.userMessage.content} | ||||||
|             'w-full', |         </h2> | ||||||
|             messageIndex === 0 ? 'pt-16' : 'pt-8', |       </div> | ||||||
|             'break-words', |  | ||||||
|           )} |  | ||||||
|         > |  | ||||||
|           <h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12"> |  | ||||||
|             {message.content} |  | ||||||
|           </h2> |  | ||||||
|         </div> |  | ||||||
|       )} |  | ||||||
|  |  | ||||||
|       {message.role === 'assistant' && ( |       <div className="flex flex-col space-y-9 lg:space-y-0 lg:flex-row lg:justify-between lg:space-x-9"> | ||||||
|         <div className="flex flex-col space-y-9 lg:space-y-0 lg:flex-row lg:justify-between lg:space-x-9"> |         <div | ||||||
|           <div |           ref={dividerRef} | ||||||
|             ref={dividerRef} |           className="flex flex-col space-y-6 w-full lg:w-9/12" | ||||||
|             className="flex flex-col space-y-6 w-full lg:w-9/12" |         > | ||||||
|           > |           {section.sourceMessage && | ||||||
|             {message.sources && message.sources.length > 0 && ( |             section.sourceMessage.sources.length > 0 && ( | ||||||
|               <div className="flex flex-col space-y-2"> |               <div className="flex flex-col space-y-2"> | ||||||
|                 <div className="flex flex-row items-center space-x-2"> |                 <div className="flex flex-row items-center space-x-2"> | ||||||
|                   <BookCopy className="text-black dark:text-white" size={20} /> |                   <BookCopy className="text-black dark:text-white" size={20} /> | ||||||
| @@ -144,10 +89,12 @@ const MessageBox = ({ | |||||||
|                     Sources |                     Sources | ||||||
|                   </h3> |                   </h3> | ||||||
|                 </div> |                 </div> | ||||||
|                 <MessageSources sources={message.sources} /> |                 <MessageSources sources={section.sourceMessage.sources} /> | ||||||
|               </div> |               </div> | ||||||
|             )} |             )} | ||||||
|             <div className="flex flex-col space-y-2"> |  | ||||||
|  |           <div className="flex flex-col space-y-2"> | ||||||
|  |             {section.sourceMessage && ( | ||||||
|               <div className="flex flex-row items-center space-x-2"> |               <div className="flex flex-row items-center space-x-2"> | ||||||
|                 <Disc3 |                 <Disc3 | ||||||
|                   className={cn( |                   className={cn( | ||||||
| @@ -160,100 +107,115 @@ const MessageBox = ({ | |||||||
|                   Answer |                   Answer | ||||||
|                 </h3> |                 </h3> | ||||||
|               </div> |               </div> | ||||||
|  |             )} | ||||||
|  |  | ||||||
|               <Markdown |             {section.assistantMessage && ( | ||||||
|                 className={cn( |               <> | ||||||
|                   'prose prose-h1:mb-3 prose-h2:mb-2 prose-h2:mt-6 prose-h2:font-[800] prose-h3:mt-4 prose-h3:mb-1.5 prose-h3:font-[600] dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 font-[400]', |                 <Markdown | ||||||
|                   'max-w-none break-words text-black dark:text-white', |                   className={cn( | ||||||
|  |                     'prose prose-h1:mb-3 prose-h2:mb-2 prose-h2:mt-6 prose-h2:font-[800] prose-h3:mt-4 prose-h3:mb-1.5 prose-h3:font-[600] dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 font-[400]', | ||||||
|  |                     'max-w-none break-words text-black dark:text-white', | ||||||
|  |                   )} | ||||||
|  |                   options={markdownOverrides} | ||||||
|  |                 > | ||||||
|  |                   {parsedMessage} | ||||||
|  |                 </Markdown> | ||||||
|  |  | ||||||
|  |                 {loading && isLast ? null : ( | ||||||
|  |                   <div className="flex flex-row items-center justify-between w-full text-black dark:text-white py-4 -mx-2"> | ||||||
|  |                     <div className="flex flex-row items-center space-x-1"> | ||||||
|  |                       <Rewrite | ||||||
|  |                         rewrite={rewrite} | ||||||
|  |                         messageId={section.assistantMessage.messageId} | ||||||
|  |                       /> | ||||||
|  |                     </div> | ||||||
|  |                     <div className="flex flex-row items-center space-x-1"> | ||||||
|  |                       <Copy | ||||||
|  |                         initialMessage={section.assistantMessage.content} | ||||||
|  |                         section={section} | ||||||
|  |                       /> | ||||||
|  |                       <button | ||||||
|  |                         onClick={() => { | ||||||
|  |                           if (speechStatus === 'started') { | ||||||
|  |                             stop(); | ||||||
|  |                           } else { | ||||||
|  |                             start(); | ||||||
|  |                           } | ||||||
|  |                         }} | ||||||
|  |                         className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white" | ||||||
|  |                       > | ||||||
|  |                         {speechStatus === 'started' ? ( | ||||||
|  |                           <StopCircle size={18} /> | ||||||
|  |                         ) : ( | ||||||
|  |                           <Volume2 size={18} /> | ||||||
|  |                         )} | ||||||
|  |                       </button> | ||||||
|  |                     </div> | ||||||
|  |                   </div> | ||||||
|                 )} |                 )} | ||||||
|                 options={markdownOverrides} |  | ||||||
|               > |                 {isLast && | ||||||
|                 {parsedMessage} |                   section.suggestions && | ||||||
|               </Markdown> |                   section.suggestions.length > 0 && | ||||||
|               {loading && isLast ? null : ( |                   section.assistantMessage && | ||||||
|                 <div className="flex flex-row items-center justify-between w-full text-black dark:text-white py-4 -mx-2"> |                   !loading && ( | ||||||
|                   <div className="flex flex-row items-center space-x-1"> |                     <div className="mt-8 pt-6 border-t border-light-200/50 dark:border-dark-200/50"> | ||||||
|                     {/*  <button className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black text-black dark:hover:text-white"> |                       <div className="flex flex-row items-center space-x-2 mb-4"> | ||||||
|                       <Share size={18} /> |                         <Layers3 | ||||||
|                     </button> */} |                           className="text-black dark:text-white" | ||||||
|                     <Rewrite rewrite={rewrite} messageId={message.messageId} /> |                           size={20} | ||||||
|                   </div> |                         /> | ||||||
|                   <div className="flex flex-row items-center space-x-1"> |                         <h3 className="text-black dark:text-white font-medium text-xl"> | ||||||
|                     <Copy initialMessage={message.content} message={message} /> |                           Related | ||||||
|                     <button |                         </h3> | ||||||
|                       onClick={() => { |  | ||||||
|                         if (speechStatus === 'started') { |  | ||||||
|                           stop(); |  | ||||||
|                         } else { |  | ||||||
|                           start(); |  | ||||||
|                         } |  | ||||||
|                       }} |  | ||||||
|                       className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white" |  | ||||||
|                     > |  | ||||||
|                       {speechStatus === 'started' ? ( |  | ||||||
|                         <StopCircle size={18} /> |  | ||||||
|                       ) : ( |  | ||||||
|                         <Volume2 size={18} /> |  | ||||||
|                       )} |  | ||||||
|                     </button> |  | ||||||
|                   </div> |  | ||||||
|                 </div> |  | ||||||
|               )} |  | ||||||
|               {isLast && |  | ||||||
|                 message.suggestions && |  | ||||||
|                 message.suggestions.length > 0 && |  | ||||||
|                 message.role === 'assistant' && |  | ||||||
|                 !loading && ( |  | ||||||
|                   <> |  | ||||||
|                     <div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" /> |  | ||||||
|                     <div className="flex flex-col space-y-3 text-black dark:text-white"> |  | ||||||
|                       <div className="flex flex-row items-center space-x-2 mt-4"> |  | ||||||
|                         <Layers3 /> |  | ||||||
|                         <h3 className="text-xl font-medium">Related</h3> |  | ||||||
|                       </div> |                       </div> | ||||||
|                       <div className="flex flex-col space-y-3"> |                       <div className="space-y-0"> | ||||||
|                         {message.suggestions.map((suggestion, i) => ( |                         {section.suggestions.map( | ||||||
|                           <div |                           (suggestion: string, i: number) => ( | ||||||
|                             className="flex flex-col space-y-3 text-sm" |                             <div key={i}> | ||||||
|                             key={i} |                               {i > 0 && ( | ||||||
|                           > |                                 <div className="h-px bg-light-200/40 dark:bg-dark-200/40 mx-3" /> | ||||||
|                             <div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" /> |                               )} | ||||||
|                             <div |                               <button | ||||||
|                               onClick={() => { |                                 onClick={() => sendMessage(suggestion)} | ||||||
|                                 sendMessage(suggestion); |                                 className="group w-full px-3 py-4 text-left transition-colors duration-200" | ||||||
|                               }} |                               > | ||||||
|                               className="cursor-pointer flex flex-row justify-between font-medium space-x-2 items-center" |                                 <div className="flex items-center justify-between gap-3"> | ||||||
|                             > |                                   <p className="text-sm text-black/70 dark:text-white/70 group-hover:text-[#24A0ED] transition-colors duration-200 leading-relaxed"> | ||||||
|                               <p className="transition duration-200 hover:text-[#24A0ED]"> |                                     {suggestion} | ||||||
|                                 {suggestion} |                                   </p> | ||||||
|                               </p> |                                   <Plus | ||||||
|                               <Plus |                                     size={16} | ||||||
|                                 size={20} |                                     className="text-black/40 dark:text-white/40 group-hover:text-[#24A0ED] transition-colors duration-200 flex-shrink-0" | ||||||
|                                 className="text-[#24A0ED] flex-shrink-0" |                                   /> | ||||||
|                               /> |                                 </div> | ||||||
|  |                               </button> | ||||||
|                             </div> |                             </div> | ||||||
|                           </div> |                           ), | ||||||
|                         ))} |                         )} | ||||||
|                       </div> |                       </div> | ||||||
|                     </div> |                     </div> | ||||||
|                   </> |                   )} | ||||||
|                 )} |               </> | ||||||
|             </div> |             )} | ||||||
|           </div> |  | ||||||
|           <div className="lg:sticky lg:top-20 flex flex-col items-center space-y-3 w-full lg:w-3/12 z-30 h-full pb-4"> |  | ||||||
|             <SearchImages |  | ||||||
|               query={history[messageIndex - 1].content} |  | ||||||
|               chatHistory={history.slice(0, messageIndex - 1)} |  | ||||||
|               messageId={message.messageId} |  | ||||||
|             /> |  | ||||||
|             <SearchVideos |  | ||||||
|               chatHistory={history.slice(0, messageIndex - 1)} |  | ||||||
|               query={history[messageIndex - 1].content} |  | ||||||
|               messageId={message.messageId} |  | ||||||
|             /> |  | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       )} |  | ||||||
|  |         {section.assistantMessage && ( | ||||||
|  |           <div className="lg:sticky lg:top-20 flex flex-col items-center space-y-3 w-full lg:w-3/12 z-30 h-full pb-4"> | ||||||
|  |             <SearchImages | ||||||
|  |               query={section.userMessage.content} | ||||||
|  |               chatHistory={chatTurns.slice(0, sectionIndex * 2)} | ||||||
|  |               messageId={section.assistantMessage.messageId} | ||||||
|  |             /> | ||||||
|  |             <SearchVideos | ||||||
|  |               chatHistory={chatTurns.slice(0, sectionIndex * 2)} | ||||||
|  |               query={section.userMessage.content} | ||||||
|  |               messageId={section.assistantMessage.messageId} | ||||||
|  |             /> | ||||||
|  |           </div> | ||||||
|  |         )} | ||||||
|  |       </div> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -6,22 +6,11 @@ import Attach from './MessageInputActions/Attach'; | |||||||
| import CopilotToggle from './MessageInputActions/Copilot'; | import CopilotToggle from './MessageInputActions/Copilot'; | ||||||
| import { File } from './ChatWindow'; | import { File } from './ChatWindow'; | ||||||
| import AttachSmall from './MessageInputActions/AttachSmall'; | import AttachSmall from './MessageInputActions/AttachSmall'; | ||||||
|  | import { useChat } from '@/lib/hooks/useChat'; | ||||||
|  |  | ||||||
|  | const MessageInput = () => { | ||||||
|  |   const { loading, sendMessage } = useChat(); | ||||||
|  |  | ||||||
| const MessageInput = ({ |  | ||||||
|   sendMessage, |  | ||||||
|   loading, |  | ||||||
|   fileIds, |  | ||||||
|   setFileIds, |  | ||||||
|   files, |  | ||||||
|   setFiles, |  | ||||||
| }: { |  | ||||||
|   sendMessage: (message: string) => void; |  | ||||||
|   loading: boolean; |  | ||||||
|   fileIds: string[]; |  | ||||||
|   setFileIds: (fileIds: string[]) => void; |  | ||||||
|   files: File[]; |  | ||||||
|   setFiles: (files: File[]) => void; |  | ||||||
| }) => { |  | ||||||
|   const [copilotEnabled, setCopilotEnabled] = useState(false); |   const [copilotEnabled, setCopilotEnabled] = useState(false); | ||||||
|   const [message, setMessage] = useState(''); |   const [message, setMessage] = useState(''); | ||||||
|   const [textareaRows, setTextareaRows] = useState(1); |   const [textareaRows, setTextareaRows] = useState(1); | ||||||
| @@ -75,18 +64,11 @@ const MessageInput = ({ | |||||||
|         } |         } | ||||||
|       }} |       }} | ||||||
|       className={cn( |       className={cn( | ||||||
|         'bg-light-secondary dark:bg-dark-secondary p-4 flex items-center overflow-hidden border border-light-200 dark:border-dark-200', |         'bg-light-secondary dark:bg-dark-secondary p-4 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 shadow-sm shadow-light-200/10 dark:shadow-black/20 transition-all duration-200 focus-within:border-light-300 dark:focus-within:border-dark-300', | ||||||
|         mode === 'multi' ? 'flex-col rounded-lg' : 'flex-row rounded-full', |         mode === 'multi' ? 'flex-col rounded-2xl' : 'flex-row rounded-full', | ||||||
|       )} |       )} | ||||||
|     > |     > | ||||||
|       {mode === 'single' && ( |       {mode === 'single' && <AttachSmall />} | ||||||
|         <AttachSmall |  | ||||||
|           fileIds={fileIds} |  | ||||||
|           setFileIds={setFileIds} |  | ||||||
|           files={files} |  | ||||||
|           setFiles={setFiles} |  | ||||||
|         /> |  | ||||||
|       )} |  | ||||||
|       <TextareaAutosize |       <TextareaAutosize | ||||||
|         ref={inputRef} |         ref={inputRef} | ||||||
|         value={message} |         value={message} | ||||||
| @@ -113,12 +95,7 @@ const MessageInput = ({ | |||||||
|       )} |       )} | ||||||
|       {mode === 'multi' && ( |       {mode === 'multi' && ( | ||||||
|         <div className="flex flex-row items-center justify-between w-full pt-2"> |         <div className="flex flex-row items-center justify-between w-full pt-2"> | ||||||
|           <AttachSmall |           <AttachSmall /> | ||||||
|             fileIds={fileIds} |  | ||||||
|             setFileIds={setFileIds} |  | ||||||
|             files={files} |  | ||||||
|             setFiles={setFiles} |  | ||||||
|           /> |  | ||||||
|           <div className="flex flex-row items-center space-x-4"> |           <div className="flex flex-row items-center space-x-4"> | ||||||
|             <CopilotToggle |             <CopilotToggle | ||||||
|               copilotEnabled={copilotEnabled} |               copilotEnabled={copilotEnabled} | ||||||
|   | |||||||
| @@ -5,23 +5,21 @@ import { | |||||||
|   PopoverPanel, |   PopoverPanel, | ||||||
|   Transition, |   Transition, | ||||||
| } from '@headlessui/react'; | } from '@headlessui/react'; | ||||||
| import { CopyPlus, File, LoaderCircle, Plus, Trash } from 'lucide-react'; | import { | ||||||
|  |   CopyPlus, | ||||||
|  |   File, | ||||||
|  |   Link, | ||||||
|  |   LoaderCircle, | ||||||
|  |   Paperclip, | ||||||
|  |   Plus, | ||||||
|  |   Trash, | ||||||
|  | } from 'lucide-react'; | ||||||
| import { Fragment, useRef, useState } from 'react'; | import { Fragment, useRef, useState } from 'react'; | ||||||
| import { File as FileType } from '../ChatWindow'; | import { useChat } from '@/lib/hooks/useChat'; | ||||||
|  |  | ||||||
|  | const Attach = () => { | ||||||
|  |   const { files, setFiles, setFileIds, fileIds } = useChat(); | ||||||
|  |  | ||||||
| const Attach = ({ |  | ||||||
|   fileIds, |  | ||||||
|   setFileIds, |  | ||||||
|   showText, |  | ||||||
|   files, |  | ||||||
|   setFiles, |  | ||||||
| }: { |  | ||||||
|   fileIds: string[]; |  | ||||||
|   setFileIds: (fileIds: string[]) => void; |  | ||||||
|   showText?: boolean; |  | ||||||
|   files: FileType[]; |  | ||||||
|   setFiles: (files: FileType[]) => void; |  | ||||||
| }) => { |  | ||||||
|   const [loading, setLoading] = useState(false); |   const [loading, setLoading] = useState(false); | ||||||
|   const fileInputRef = useRef<any>(); |   const fileInputRef = useRef<any>(); | ||||||
|  |  | ||||||
| @@ -34,12 +32,12 @@ const Attach = ({ | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const embeddingModelProvider = localStorage.getItem( |     const embeddingModelProvider = localStorage.getItem( | ||||||
|       'embeddingModelProvider', |       'embeddingModelProviderId', | ||||||
|     ); |     ); | ||||||
|     const embeddingModel = localStorage.getItem('embeddingModel'); |     const embeddingModel = localStorage.getItem('embeddingModelKey'); | ||||||
|  |  | ||||||
|     data.append('embedding_model_provider', embeddingModelProvider!); |     data.append('embedding_model_provider_id', embeddingModelProvider!); | ||||||
|     data.append('embedding_model', embeddingModel!); |     data.append('embedding_model_key', embeddingModel!); | ||||||
|  |  | ||||||
|     const res = await fetch(`/api/uploads`, { |     const res = await fetch(`/api/uploads`, { | ||||||
|       method: 'POST', |       method: 'POST', | ||||||
| @@ -54,42 +52,16 @@ const Attach = ({ | |||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   return loading ? ( |   return loading ? ( | ||||||
|     <div className="flex flex-row items-center justify-between space-x-1"> |     <div className="active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none text-black/50 dark:text-white/50 transition duration-200"> | ||||||
|       <LoaderCircle size={18} className="text-sky-400 animate-spin" /> |       <LoaderCircle size={16} className="text-sky-400 animate-spin" /> | ||||||
|       <p className="text-sky-400 inline whitespace-nowrap text-xs font-medium"> |  | ||||||
|         Uploading.. |  | ||||||
|       </p> |  | ||||||
|     </div> |     </div> | ||||||
|   ) : files.length > 0 ? ( |   ) : files.length > 0 ? ( | ||||||
|     <Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg"> |     <Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg"> | ||||||
|       <PopoverButton |       <PopoverButton | ||||||
|         type="button" |         type="button" | ||||||
|         className={cn( |         className="active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none headless-open:text-black dark:headless-open:text-white text-black/50 dark:text-white/50 active:scale-95 transition duration-200 hover:text-black dark:hover:text-white" | ||||||
|           'flex flex-row items-center justify-between space-x-1 p-2 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white', |  | ||||||
|           files.length > 0 ? '-ml-2 lg:-ml-3' : '', |  | ||||||
|         )} |  | ||||||
|       > |       > | ||||||
|         {files.length > 1 && ( |         <File size={16} className="text-sky-400" /> | ||||||
|           <> |  | ||||||
|             <File size={19} className="text-sky-400" /> |  | ||||||
|             <p className="text-sky-400 inline whitespace-nowrap text-xs font-medium"> |  | ||||||
|               {files.length} files |  | ||||||
|             </p> |  | ||||||
|           </> |  | ||||||
|         )} |  | ||||||
|  |  | ||||||
|         {files.length === 1 && ( |  | ||||||
|           <> |  | ||||||
|             <File size={18} className="text-sky-400" /> |  | ||||||
|             <p className="text-sky-400 text-xs font-medium"> |  | ||||||
|               {files[0].fileName.length > 10 |  | ||||||
|                 ? files[0].fileName.replace(/\.\w+$/, '').substring(0, 3) + |  | ||||||
|                   '...' + |  | ||||||
|                   files[0].fileExtension |  | ||||||
|                 : files[0].fileName} |  | ||||||
|             </p> |  | ||||||
|           </> |  | ||||||
|         )} |  | ||||||
|       </PopoverButton> |       </PopoverButton> | ||||||
|       <Transition |       <Transition | ||||||
|         as={Fragment} |         as={Fragment} | ||||||
| @@ -110,7 +82,7 @@ const Attach = ({ | |||||||
|                 <button |                 <button | ||||||
|                   type="button" |                   type="button" | ||||||
|                   onClick={() => fileInputRef.current.click()} |                   onClick={() => fileInputRef.current.click()} | ||||||
|                   className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200" |                   className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200 focus:outline-none" | ||||||
|                 > |                 > | ||||||
|                   <input |                   <input | ||||||
|                     type="file" |                     type="file" | ||||||
| @@ -120,7 +92,7 @@ const Attach = ({ | |||||||
|                     multiple |                     multiple | ||||||
|                     hidden |                     hidden | ||||||
|                   /> |                   /> | ||||||
|                   <Plus size={18} /> |                   <Plus size={16} /> | ||||||
|                   <p className="text-xs">Add</p> |                   <p className="text-xs">Add</p> | ||||||
|                 </button> |                 </button> | ||||||
|                 <button |                 <button | ||||||
| @@ -128,7 +100,7 @@ const Attach = ({ | |||||||
|                     setFiles([]); |                     setFiles([]); | ||||||
|                     setFileIds([]); |                     setFileIds([]); | ||||||
|                   }} |                   }} | ||||||
|                   className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200" |                   className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200 focus:outline-none" | ||||||
|                 > |                 > | ||||||
|                   <Trash size={14} /> |                   <Trash size={14} /> | ||||||
|                   <p className="text-xs">Clear</p> |                   <p className="text-xs">Clear</p> | ||||||
| @@ -142,8 +114,11 @@ const Attach = ({ | |||||||
|                   key={i} |                   key={i} | ||||||
|                   className="flex flex-row items-center justify-start w-full space-x-3 p-3" |                   className="flex flex-row items-center justify-start w-full space-x-3 p-3" | ||||||
|                 > |                 > | ||||||
|                   <div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md"> |                   <div className="bg-light-100 dark:bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md"> | ||||||
|                     <File size={16} className="text-white/70" /> |                     <File | ||||||
|  |                       size={16} | ||||||
|  |                       className="text-black/70 dark:text-white/70" | ||||||
|  |                     /> | ||||||
|                   </div> |                   </div> | ||||||
|                   <p className="text-black/70 dark:text-white/70 text-sm"> |                   <p className="text-black/70 dark:text-white/70 text-sm"> | ||||||
|                     {file.fileName.length > 25 |                     {file.fileName.length > 25 | ||||||
| @@ -164,8 +139,7 @@ const Attach = ({ | |||||||
|       type="button" |       type="button" | ||||||
|       onClick={() => fileInputRef.current.click()} |       onClick={() => fileInputRef.current.click()} | ||||||
|       className={cn( |       className={cn( | ||||||
|         'flex flex-row items-center space-x-1 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white', |         'flex items-center justify-center active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none headless-open:text-black dark:headless-open:text-white text-black/50 dark:text-white/50 active:scale-95 transition duration-200 hover:text-black dark:hover:text-white', | ||||||
|         showText ? '' : 'p-2', |  | ||||||
|       )} |       )} | ||||||
|     > |     > | ||||||
|       <input |       <input | ||||||
| @@ -176,8 +150,7 @@ const Attach = ({ | |||||||
|         multiple |         multiple | ||||||
|         hidden |         hidden | ||||||
|       /> |       /> | ||||||
|       <CopyPlus size={showText ? 18 : undefined} /> |       <Paperclip size={16} /> | ||||||
|       {showText && <p className="text-xs font-medium pl-[1px]">Attach</p>} |  | ||||||
|     </button> |     </button> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -5,21 +5,21 @@ import { | |||||||
|   PopoverPanel, |   PopoverPanel, | ||||||
|   Transition, |   Transition, | ||||||
| } from '@headlessui/react'; | } from '@headlessui/react'; | ||||||
| import { CopyPlus, File, LoaderCircle, Plus, Trash } from 'lucide-react'; | import { | ||||||
|  |   CopyPlus, | ||||||
|  |   File, | ||||||
|  |   LoaderCircle, | ||||||
|  |   Paperclip, | ||||||
|  |   Plus, | ||||||
|  |   Trash, | ||||||
|  | } from 'lucide-react'; | ||||||
| import { Fragment, useRef, useState } from 'react'; | import { Fragment, useRef, useState } from 'react'; | ||||||
| import { File as FileType } from '../ChatWindow'; | import { File as FileType } from '../ChatWindow'; | ||||||
|  | import { useChat } from '@/lib/hooks/useChat'; | ||||||
|  |  | ||||||
|  | const AttachSmall = () => { | ||||||
|  |   const { files, setFiles, setFileIds, fileIds } = useChat(); | ||||||
|  |  | ||||||
| const AttachSmall = ({ |  | ||||||
|   fileIds, |  | ||||||
|   setFileIds, |  | ||||||
|   files, |  | ||||||
|   setFiles, |  | ||||||
| }: { |  | ||||||
|   fileIds: string[]; |  | ||||||
|   setFileIds: (fileIds: string[]) => void; |  | ||||||
|   files: FileType[]; |  | ||||||
|   setFiles: (files: FileType[]) => void; |  | ||||||
| }) => { |  | ||||||
|   const [loading, setLoading] = useState(false); |   const [loading, setLoading] = useState(false); | ||||||
|   const fileInputRef = useRef<any>(); |   const fileInputRef = useRef<any>(); | ||||||
|  |  | ||||||
| @@ -32,12 +32,12 @@ const AttachSmall = ({ | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const embeddingModelProvider = localStorage.getItem( |     const embeddingModelProvider = localStorage.getItem( | ||||||
|       'embeddingModelProvider', |       'embeddingModelProviderId', | ||||||
|     ); |     ); | ||||||
|     const embeddingModel = localStorage.getItem('embeddingModel'); |     const embeddingModel = localStorage.getItem('embeddingModelKey'); | ||||||
|  |  | ||||||
|     data.append('embedding_model_provider', embeddingModelProvider!); |     data.append('embedding_model_provider_id', embeddingModelProvider!); | ||||||
|     data.append('embedding_model', embeddingModel!); |     data.append('embedding_model_key', embeddingModel!); | ||||||
|  |  | ||||||
|     const res = await fetch(`/api/uploads`, { |     const res = await fetch(`/api/uploads`, { | ||||||
|       method: 'POST', |       method: 'POST', | ||||||
| @@ -52,7 +52,7 @@ const AttachSmall = ({ | |||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   return loading ? ( |   return loading ? ( | ||||||
|     <div className="flex flex-row items-center justify-between space-x-1 p-1"> |     <div className="flex flex-row items-center justify-between space-x-1 p-1 "> | ||||||
|       <LoaderCircle size={20} className="text-sky-400 animate-spin" /> |       <LoaderCircle size={20} className="text-sky-400 animate-spin" /> | ||||||
|     </div> |     </div> | ||||||
|   ) : files.length > 0 ? ( |   ) : files.length > 0 ? ( | ||||||
| @@ -114,8 +114,11 @@ const AttachSmall = ({ | |||||||
|                   key={i} |                   key={i} | ||||||
|                   className="flex flex-row items-center justify-start w-full space-x-3 p-3" |                   className="flex flex-row items-center justify-start w-full space-x-3 p-3" | ||||||
|                 > |                 > | ||||||
|                   <div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md"> |                   <div className="bg-light-100 dark:bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md"> | ||||||
|                     <File size={16} className="text-white/70" /> |                     <File | ||||||
|  |                       size={16} | ||||||
|  |                       className="text-black/70 dark:text-white/70" | ||||||
|  |                     /> | ||||||
|                   </div> |                   </div> | ||||||
|                   <p className="text-black/70 dark:text-white/70 text-sm"> |                   <p className="text-black/70 dark:text-white/70 text-sm"> | ||||||
|                     {file.fileName.length > 25 |                     {file.fileName.length > 25 | ||||||
| @@ -145,7 +148,7 @@ const AttachSmall = ({ | |||||||
|         multiple |         multiple | ||||||
|         hidden |         hidden | ||||||
|       /> |       /> | ||||||
|       <CopyPlus size={20} /> |       <Paperclip size={16} /> | ||||||
|     </button> |     </button> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -15,19 +15,20 @@ import { | |||||||
| } from '@headlessui/react'; | } from '@headlessui/react'; | ||||||
| import { SiReddit, SiYoutube } from '@icons-pack/react-simple-icons'; | import { SiReddit, SiYoutube } from '@icons-pack/react-simple-icons'; | ||||||
| import { Fragment } from 'react'; | import { Fragment } from 'react'; | ||||||
|  | import { useChat } from '@/lib/hooks/useChat'; | ||||||
|  |  | ||||||
| const focusModes = [ | const focusModes = [ | ||||||
|   { |   { | ||||||
|     key: 'webSearch', |     key: 'webSearch', | ||||||
|     title: 'All', |     title: 'All', | ||||||
|     description: 'Searches across all of the internet', |     description: 'Searches across all of the internet', | ||||||
|     icon: <Globe size={20} />, |     icon: <Globe size={16} />, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     key: 'academicSearch', |     key: 'academicSearch', | ||||||
|     title: 'Academic', |     title: 'Academic', | ||||||
|     description: 'Search in published academic papers', |     description: 'Search in published academic papers', | ||||||
|     icon: <SwatchBook size={20} />, |     icon: <SwatchBook size={16} />, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     key: 'writingAssistant', |     key: 'writingAssistant', | ||||||
| @@ -39,47 +40,38 @@ const focusModes = [ | |||||||
|     key: 'wolframAlphaSearch', |     key: 'wolframAlphaSearch', | ||||||
|     title: 'Wolfram Alpha', |     title: 'Wolfram Alpha', | ||||||
|     description: 'Computational knowledge engine', |     description: 'Computational knowledge engine', | ||||||
|     icon: <BadgePercent size={20} />, |     icon: <BadgePercent size={16} />, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     key: 'youtubeSearch', |     key: 'youtubeSearch', | ||||||
|     title: 'Youtube', |     title: 'Youtube', | ||||||
|     description: 'Search and watch videos', |     description: 'Search and watch videos', | ||||||
|     icon: <SiYoutube className="h-5 w-auto mr-0.5" />, |     icon: <SiYoutube className="h-[16px] w-auto mr-0.5" />, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     key: 'redditSearch', |     key: 'redditSearch', | ||||||
|     title: 'Reddit', |     title: 'Reddit', | ||||||
|     description: 'Search for discussions and opinions', |     description: 'Search for discussions and opinions', | ||||||
|     icon: <SiReddit className="h-5 w-auto mr-0.5" />, |     icon: <SiReddit className="h-[16px] w-auto mr-0.5" />, | ||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| const Focus = ({ | const Focus = () => { | ||||||
|   focusMode, |   const { focusMode, setFocusMode } = useChat(); | ||||||
|   setFocusMode, |  | ||||||
| }: { |  | ||||||
|   focusMode: string; |  | ||||||
|   setFocusMode: (mode: string) => void; |  | ||||||
| }) => { |  | ||||||
|   return ( |   return ( | ||||||
|     <Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg mt-[6.5px]"> |     <Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg"> | ||||||
|       <PopoverButton |       <PopoverButton | ||||||
|         type="button" |         type="button" | ||||||
|         className=" text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white" |         className="active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none headless-open:text-black dark:headless-open:text-white text-black/50 dark:text-white/50 active:scale-95 transition duration-200 hover:text-black dark:hover:text-white" | ||||||
|       > |       > | ||||||
|         {focusMode !== 'webSearch' ? ( |         {focusMode !== 'webSearch' ? ( | ||||||
|           <div className="flex flex-row items-center space-x-1"> |           <div className="flex flex-row items-center space-x-1"> | ||||||
|             {focusModes.find((mode) => mode.key === focusMode)?.icon} |             {focusModes.find((mode) => mode.key === focusMode)?.icon} | ||||||
|             <p className="text-xs font-medium hidden lg:block"> |  | ||||||
|               {focusModes.find((mode) => mode.key === focusMode)?.title} |  | ||||||
|             </p> |  | ||||||
|             <ChevronDown size={20} className="-translate-x-1" /> |  | ||||||
|           </div> |           </div> | ||||||
|         ) : ( |         ) : ( | ||||||
|           <div className="flex flex-row items-center space-x-1"> |           <div className="flex flex-row items-center space-x-1"> | ||||||
|             <ScanEye size={20} /> |             <Globe size={16} /> | ||||||
|             <p className="text-xs font-medium hidden lg:block">Focus</p> |  | ||||||
|           </div> |           </div> | ||||||
|         )} |         )} | ||||||
|       </PopoverButton> |       </PopoverButton> | ||||||
| @@ -92,14 +84,14 @@ const Focus = ({ | |||||||
|         leaveFrom="opacity-100 translate-y-0" |         leaveFrom="opacity-100 translate-y-0" | ||||||
|         leaveTo="opacity-0 translate-y-1" |         leaveTo="opacity-0 translate-y-1" | ||||||
|       > |       > | ||||||
|         <PopoverPanel className="absolute z-10 w-64 md:w-[500px] left-0"> |         <PopoverPanel className="absolute z-10 w-64 md:w-[500px] -right-4"> | ||||||
|           <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto"> |           <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto"> | ||||||
|             {focusModes.map((mode, i) => ( |             {focusModes.map((mode, i) => ( | ||||||
|               <PopoverButton |               <PopoverButton | ||||||
|                 onClick={() => setFocusMode(mode.key)} |                 onClick={() => setFocusMode(mode.key)} | ||||||
|                 key={i} |                 key={i} | ||||||
|                 className={cn( |                 className={cn( | ||||||
|                   'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-2 duration-200 cursor-pointer transition', |                   'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-2 duration-200 cursor-pointer transition focus:outline-none', | ||||||
|                   focusMode === mode.key |                   focusMode === mode.key | ||||||
|                     ? 'bg-light-secondary dark:bg-dark-secondary' |                     ? 'bg-light-secondary dark:bg-dark-secondary' | ||||||
|                     : 'hover:bg-light-secondary dark:hover:bg-dark-secondary', |                     : 'hover:bg-light-secondary dark:hover:bg-dark-secondary', | ||||||
|   | |||||||
							
								
								
									
										198
									
								
								src/components/MessageInputActions/ModelSelector.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,198 @@ | |||||||
|  | 'use client'; | ||||||
|  |  | ||||||
|  | import { Cpu, Loader2, Search } from 'lucide-react'; | ||||||
|  | import { cn } from '@/lib/utils'; | ||||||
|  | import { | ||||||
|  |   Popover, | ||||||
|  |   PopoverButton, | ||||||
|  |   PopoverPanel, | ||||||
|  |   Transition, | ||||||
|  | } from '@headlessui/react'; | ||||||
|  | import { Fragment, useEffect, useState } from 'react'; | ||||||
|  | import { MinimalProvider } from '@/lib/models/types'; | ||||||
|  |  | ||||||
|  | const ModelSelector = () => { | ||||||
|  |   const [providers, setProviders] = useState<MinimalProvider[]>([]); | ||||||
|  |   const [isLoading, setIsLoading] = useState(true); | ||||||
|  |   const [searchQuery, setSearchQuery] = useState(''); | ||||||
|  |   const [selectedModel, setSelectedModel] = useState<{ | ||||||
|  |     providerId: string; | ||||||
|  |     modelKey: string; | ||||||
|  |   } | null>(null); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     const loadProviders = async () => { | ||||||
|  |       try { | ||||||
|  |         setIsLoading(true); | ||||||
|  |         const res = await fetch('/api/providers'); | ||||||
|  |  | ||||||
|  |         if (!res.ok) { | ||||||
|  |           throw new Error('Failed to fetch providers'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const data = await res.json(); | ||||||
|  |         setProviders(data.providers || []); | ||||||
|  |  | ||||||
|  |         const savedProviderId = localStorage.getItem('chatModelProviderId'); | ||||||
|  |         const savedModelKey = localStorage.getItem('chatModelKey'); | ||||||
|  |  | ||||||
|  |         if (savedProviderId && savedModelKey) { | ||||||
|  |           setSelectedModel({ | ||||||
|  |             providerId: savedProviderId, | ||||||
|  |             modelKey: savedModelKey, | ||||||
|  |           }); | ||||||
|  |         } else if (data.providers && data.providers.length > 0) { | ||||||
|  |           const firstProvider = data.providers.find( | ||||||
|  |             (p: MinimalProvider) => p.chatModels.length > 0, | ||||||
|  |           ); | ||||||
|  |           if (firstProvider && firstProvider.chatModels[0]) { | ||||||
|  |             setSelectedModel({ | ||||||
|  |               providerId: firstProvider.id, | ||||||
|  |               modelKey: firstProvider.chatModels[0].key, | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } catch (error) { | ||||||
|  |         console.error('Error loading providers:', error); | ||||||
|  |       } finally { | ||||||
|  |         setIsLoading(false); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     loadProviders(); | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   const handleModelSelect = (providerId: string, modelKey: string) => { | ||||||
|  |     setSelectedModel({ providerId, modelKey }); | ||||||
|  |     localStorage.setItem('chatModelProviderId', providerId); | ||||||
|  |     localStorage.setItem('chatModelKey', modelKey); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const filteredProviders = providers | ||||||
|  |     .map((provider) => ({ | ||||||
|  |       ...provider, | ||||||
|  |       chatModels: provider.chatModels.filter( | ||||||
|  |         (model) => | ||||||
|  |           model.name.toLowerCase().includes(searchQuery.toLowerCase()) || | ||||||
|  |           provider.name.toLowerCase().includes(searchQuery.toLowerCase()), | ||||||
|  |       ), | ||||||
|  |     })) | ||||||
|  |     .filter((provider) => provider.chatModels.length > 0); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg"> | ||||||
|  |       <PopoverButton | ||||||
|  |         type="button" | ||||||
|  |         className="active:border-none hover:bg-light-200  hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none headless-open:text-black dark:headless-open:text-white text-black/50 dark:text-white/50 active:scale-95 transition duration-200 hover:text-black dark:hover:text-white" | ||||||
|  |       > | ||||||
|  |         <Cpu size={16} className="text-sky-500" /> | ||||||
|  |       </PopoverButton> | ||||||
|  |       <Transition | ||||||
|  |         as={Fragment} | ||||||
|  |         enter="transition ease-out duration-100" | ||||||
|  |         enterFrom="opacity-0 translate-y-1" | ||||||
|  |         enterTo="opacity-100 translate-y-0" | ||||||
|  |         leave="transition ease-in duration-100" | ||||||
|  |         leaveFrom="opacity-100 translate-y-0" | ||||||
|  |         leaveTo="opacity-0 translate-y-1" | ||||||
|  |       > | ||||||
|  |         <PopoverPanel className="absolute z-10 w-[230px] sm:w-[270px] md:w-[300px] -right-4"> | ||||||
|  |           <div className="bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full flex flex-col shadow-lg overflow-hidden"> | ||||||
|  |             <div className="p-4 border-b border-light-200 dark:border-dark-200"> | ||||||
|  |               <div className="relative"> | ||||||
|  |                 <Search | ||||||
|  |                   size={16} | ||||||
|  |                   className="absolute left-3 top-1/2 -translate-y-1/2 text-black/40 dark:text-white/40" | ||||||
|  |                 /> | ||||||
|  |                 <input | ||||||
|  |                   type="text" | ||||||
|  |                   placeholder="Search models..." | ||||||
|  |                   value={searchQuery} | ||||||
|  |                   onChange={(e) => setSearchQuery(e.target.value)} | ||||||
|  |                   className="w-full pl-9 pr-3 py-2 bg-light-secondary dark:bg-dark-secondary rounded-lg text-xs text-black dark:text-white placeholder:text-black/40 dark:placeholder:text-white/40 focus:outline-none focus:ring-2 focus:ring-sky-500/20 border border-transparent focus:border-sky-500/30 transition duration-200" | ||||||
|  |                 /> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |  | ||||||
|  |             <div className="max-h-[320px] overflow-y-auto"> | ||||||
|  |               {isLoading ? ( | ||||||
|  |                 <div className="flex items-center justify-center py-16"> | ||||||
|  |                   <Loader2 | ||||||
|  |                     className="animate-spin text-black/40 dark:text-white/40" | ||||||
|  |                     size={24} | ||||||
|  |                   /> | ||||||
|  |                 </div> | ||||||
|  |               ) : filteredProviders.length === 0 ? ( | ||||||
|  |                 <div className="text-center py-16 px-4 text-black/60 dark:text-white/60 text-sm"> | ||||||
|  |                   {searchQuery | ||||||
|  |                     ? 'No models found' | ||||||
|  |                     : 'No chat models configured'} | ||||||
|  |                 </div> | ||||||
|  |               ) : ( | ||||||
|  |                 <div className="flex flex-col"> | ||||||
|  |                   {filteredProviders.map((provider, providerIndex) => ( | ||||||
|  |                     <div key={provider.id}> | ||||||
|  |                       <div className="px-4 py-2.5 sticky top-0 bg-light-primary dark:bg-dark-primary border-b border-light-200/50 dark:border-dark-200/50"> | ||||||
|  |                         <p className="text-xs text-black/50 dark:text-white/50 uppercase tracking-wider"> | ||||||
|  |                           {provider.name} | ||||||
|  |                         </p> | ||||||
|  |                       </div> | ||||||
|  |  | ||||||
|  |                       <div className="flex flex-col px-2 py-2 space-y-0.5"> | ||||||
|  |                         {provider.chatModels.map((model) => ( | ||||||
|  |                           <PopoverButton | ||||||
|  |                             key={model.key} | ||||||
|  |                             onClick={() => | ||||||
|  |                               handleModelSelect(provider.id, model.key) | ||||||
|  |                             } | ||||||
|  |                             className={cn( | ||||||
|  |                               'px-3 py-2 flex items-center justify-between text-start duration-200 cursor-pointer transition rounded-lg group', | ||||||
|  |                               selectedModel?.providerId === provider.id && | ||||||
|  |                                 selectedModel?.modelKey === model.key | ||||||
|  |                                 ? 'bg-light-secondary dark:bg-dark-secondary' | ||||||
|  |                                 : 'hover:bg-light-secondary dark:hover:bg-dark-secondary', | ||||||
|  |                             )} | ||||||
|  |                           > | ||||||
|  |                             <div className="flex items-center space-x-2.5 min-w-0 flex-1"> | ||||||
|  |                               <Cpu | ||||||
|  |                                 size={15} | ||||||
|  |                                 className={cn( | ||||||
|  |                                   'shrink-0', | ||||||
|  |                                   selectedModel?.providerId === provider.id && | ||||||
|  |                                     selectedModel?.modelKey === model.key | ||||||
|  |                                     ? 'text-sky-500' | ||||||
|  |                                     : 'text-black/50 dark:text-white/50 group-hover:text-black/70 group-hover:dark:text-white/70', | ||||||
|  |                                 )} | ||||||
|  |                               /> | ||||||
|  |                               <p | ||||||
|  |                                 className={cn( | ||||||
|  |                                   'text-sm truncate', | ||||||
|  |                                   selectedModel?.providerId === provider.id && | ||||||
|  |                                     selectedModel?.modelKey === model.key | ||||||
|  |                                     ? 'text-sky-500 font-medium' | ||||||
|  |                                     : 'text-black/70 dark:text-white/70 group-hover:text-black dark:group-hover:text-white', | ||||||
|  |                                 )} | ||||||
|  |                               > | ||||||
|  |                                 {model.name} | ||||||
|  |                               </p> | ||||||
|  |                             </div> | ||||||
|  |                           </PopoverButton> | ||||||
|  |                         ))} | ||||||
|  |                       </div> | ||||||
|  |  | ||||||
|  |                       {providerIndex < filteredProviders.length - 1 && ( | ||||||
|  |                         <div className="h-px bg-light-200 dark:bg-dark-200" /> | ||||||
|  |                       )} | ||||||
|  |                     </div> | ||||||
|  |                   ))} | ||||||
|  |                 </div> | ||||||
|  |               )} | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </PopoverPanel> | ||||||
|  |       </Transition> | ||||||
|  |     </Popover> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default ModelSelector; | ||||||
| @@ -7,19 +7,20 @@ import { | |||||||
|   Transition, |   Transition, | ||||||
| } from '@headlessui/react'; | } from '@headlessui/react'; | ||||||
| import { Fragment } from 'react'; | import { Fragment } from 'react'; | ||||||
|  | import { useChat } from '@/lib/hooks/useChat'; | ||||||
|  |  | ||||||
| const OptimizationModes = [ | const OptimizationModes = [ | ||||||
|   { |   { | ||||||
|     key: 'speed', |     key: 'speed', | ||||||
|     title: 'Speed', |     title: 'Speed', | ||||||
|     description: 'Prioritize speed and get the quickest possible answer.', |     description: 'Prioritize speed and get the quickest possible answer.', | ||||||
|     icon: <Zap size={20} className="text-[#FF9800]" />, |     icon: <Zap size={16} className="text-[#FF9800]" />, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     key: 'balanced', |     key: 'balanced', | ||||||
|     title: 'Balanced', |     title: 'Balanced', | ||||||
|     description: 'Find the right balance between speed and accuracy', |     description: 'Find the right balance between speed and accuracy', | ||||||
|     icon: <Sliders size={20} className="text-[#4CAF50]" />, |     icon: <Sliders size={16} className="text-[#4CAF50]" />, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     key: 'quality', |     key: 'quality', | ||||||
| @@ -34,69 +35,69 @@ const OptimizationModes = [ | |||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| const Optimization = ({ | const Optimization = () => { | ||||||
|   optimizationMode, |   const { optimizationMode, setOptimizationMode } = useChat(); | ||||||
|   setOptimizationMode, |  | ||||||
| }: { |  | ||||||
|   optimizationMode: string; |  | ||||||
|   setOptimizationMode: (mode: string) => void; |  | ||||||
| }) => { |  | ||||||
|   return ( |   return ( | ||||||
|     <Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg"> |     <Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg"> | ||||||
|       <PopoverButton |       {({ open }) => ( | ||||||
|         type="button" |         <> | ||||||
|         className="p-2 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white" |           <PopoverButton | ||||||
|       > |             type="button" | ||||||
|         <div className="flex flex-row items-center space-x-1"> |             className="p-2 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white focus:outline-none" | ||||||
|           { |           > | ||||||
|             OptimizationModes.find((mode) => mode.key === optimizationMode) |             <div className="flex flex-row items-center space-x-1"> | ||||||
|               ?.icon |               { | ||||||
|           } |                 OptimizationModes.find((mode) => mode.key === optimizationMode) | ||||||
|           <p className="text-xs font-medium"> |                   ?.icon | ||||||
|             { |               } | ||||||
|               OptimizationModes.find((mode) => mode.key === optimizationMode) |               <ChevronDown | ||||||
|                 ?.title |                 size={16} | ||||||
|             } |  | ||||||
|           </p> |  | ||||||
|           <ChevronDown size={20} /> |  | ||||||
|         </div> |  | ||||||
|       </PopoverButton> |  | ||||||
|       <Transition |  | ||||||
|         as={Fragment} |  | ||||||
|         enter="transition ease-out duration-150" |  | ||||||
|         enterFrom="opacity-0 translate-y-1" |  | ||||||
|         enterTo="opacity-100 translate-y-0" |  | ||||||
|         leave="transition ease-in duration-150" |  | ||||||
|         leaveFrom="opacity-100 translate-y-0" |  | ||||||
|         leaveTo="opacity-0 translate-y-1" |  | ||||||
|       > |  | ||||||
|         <PopoverPanel className="absolute z-10 w-64 md:w-[250px] right-0"> |  | ||||||
|           <div className="flex flex-col gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto"> |  | ||||||
|             {OptimizationModes.map((mode, i) => ( |  | ||||||
|               <PopoverButton |  | ||||||
|                 onClick={() => setOptimizationMode(mode.key)} |  | ||||||
|                 key={i} |  | ||||||
|                 disabled={mode.key === 'quality'} |  | ||||||
|                 className={cn( |                 className={cn( | ||||||
|                   'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-1 duration-200 cursor-pointer transition', |                   open ? 'rotate-180' : 'rotate-0', | ||||||
|                   optimizationMode === mode.key |                   'transition duration:200', | ||||||
|                     ? 'bg-light-secondary dark:bg-dark-secondary' |  | ||||||
|                     : 'hover:bg-light-secondary dark:hover:bg-dark-secondary', |  | ||||||
|                   mode.key === 'quality' && 'opacity-50 cursor-not-allowed', |  | ||||||
|                 )} |                 )} | ||||||
|               > |               /> | ||||||
|                 <div className="flex flex-row items-center space-x-1 text-black dark:text-white"> |             </div> | ||||||
|                   {mode.icon} |           </PopoverButton> | ||||||
|                   <p className="text-sm font-medium">{mode.title}</p> |           <Transition | ||||||
|                 </div> |             as={Fragment} | ||||||
|                 <p className="text-black/70 dark:text-white/70 text-xs"> |             enter="transition ease-out duration-150" | ||||||
|                   {mode.description} |             enterFrom="opacity-0 translate-y-1" | ||||||
|                 </p> |             enterTo="opacity-100 translate-y-0" | ||||||
|               </PopoverButton> |             leave="transition ease-in duration-150" | ||||||
|             ))} |             leaveFrom="opacity-100 translate-y-0" | ||||||
|           </div> |             leaveTo="opacity-0 translate-y-1" | ||||||
|         </PopoverPanel> |           > | ||||||
|       </Transition> |             <PopoverPanel className="absolute z-10 w-64 md:w-[250px] left-0"> | ||||||
|  |               <div className="flex flex-col gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto"> | ||||||
|  |                 {OptimizationModes.map((mode, i) => ( | ||||||
|  |                   <PopoverButton | ||||||
|  |                     onClick={() => setOptimizationMode(mode.key)} | ||||||
|  |                     key={i} | ||||||
|  |                     disabled={mode.key === 'quality'} | ||||||
|  |                     className={cn( | ||||||
|  |                       'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-1 duration-200 cursor-pointer transition focus:outline-none', | ||||||
|  |                       optimizationMode === mode.key | ||||||
|  |                         ? 'bg-light-secondary dark:bg-dark-secondary' | ||||||
|  |                         : 'hover:bg-light-secondary dark:hover:bg-dark-secondary', | ||||||
|  |                       mode.key === 'quality' && 'opacity-50 cursor-not-allowed', | ||||||
|  |                     )} | ||||||
|  |                   > | ||||||
|  |                     <div className="flex flex-row items-center space-x-1 text-black dark:text-white"> | ||||||
|  |                       {mode.icon} | ||||||
|  |                       <p className="text-sm font-medium">{mode.title}</p> | ||||||
|  |                     </div> | ||||||
|  |                     <p className="text-black/70 dark:text-white/70 text-xs"> | ||||||
|  |                       {mode.description} | ||||||
|  |                     </p> | ||||||
|  |                   </PopoverButton> | ||||||
|  |                 ))} | ||||||
|  |               </div> | ||||||
|  |             </PopoverPanel> | ||||||
|  |           </Transition> | ||||||
|  |         </> | ||||||
|  |       )} | ||||||
|     </Popover> |     </Popover> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,40 +1,223 @@ | |||||||
| import { Clock, Edit, Share, Trash } from 'lucide-react'; | import { Clock, Edit, Share, Trash, FileText, FileDown } from 'lucide-react'; | ||||||
| import { Message } from './ChatWindow'; | import { Message } from './ChatWindow'; | ||||||
| import { useEffect, useState } from 'react'; | import { useEffect, useState, Fragment } from 'react'; | ||||||
| import { formatTimeDifference } from '@/lib/utils'; | import { formatTimeDifference } from '@/lib/utils'; | ||||||
| import DeleteChat from './DeleteChat'; | import DeleteChat from './DeleteChat'; | ||||||
|  | import { | ||||||
|  |   Popover, | ||||||
|  |   PopoverButton, | ||||||
|  |   PopoverPanel, | ||||||
|  |   Transition, | ||||||
|  | } from '@headlessui/react'; | ||||||
|  | import jsPDF from 'jspdf'; | ||||||
|  | import { useChat, Section } from '@/lib/hooks/useChat'; | ||||||
|  |  | ||||||
| const Navbar = ({ | const downloadFile = (filename: string, content: string, type: string) => { | ||||||
|   chatId, |   const blob = new Blob([content], { type }); | ||||||
|   messages, |   const url = URL.createObjectURL(blob); | ||||||
| }: { |   const a = document.createElement('a'); | ||||||
|   messages: Message[]; |   a.href = url; | ||||||
|   chatId: string; |   a.download = filename; | ||||||
| }) => { |   document.body.appendChild(a); | ||||||
|  |   a.click(); | ||||||
|  |   setTimeout(() => { | ||||||
|  |     document.body.removeChild(a); | ||||||
|  |     URL.revokeObjectURL(url); | ||||||
|  |   }, 0); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const exportAsMarkdown = (sections: Section[], title: string) => { | ||||||
|  |   const date = new Date( | ||||||
|  |     sections[0]?.userMessage?.createdAt || Date.now(), | ||||||
|  |   ).toLocaleString(); | ||||||
|  |   let md = `# 💬 Chat Export: ${title}\n\n`; | ||||||
|  |   md += `*Exported on: ${date}*\n\n---\n`; | ||||||
|  |  | ||||||
|  |   sections.forEach((section, idx) => { | ||||||
|  |     if (section.userMessage) { | ||||||
|  |       md += `\n---\n`; | ||||||
|  |       md += `**🧑 User**   | ||||||
|  | `; | ||||||
|  |       md += `*${new Date(section.userMessage.createdAt).toLocaleString()}*\n\n`; | ||||||
|  |       md += `> ${section.userMessage.content.replace(/\n/g, '\n> ')}\n`; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (section.assistantMessage) { | ||||||
|  |       md += `\n---\n`; | ||||||
|  |       md += `**🤖 Assistant**   | ||||||
|  | `; | ||||||
|  |       md += `*${new Date(section.assistantMessage.createdAt).toLocaleString()}*\n\n`; | ||||||
|  |       md += `> ${section.assistantMessage.content.replace(/\n/g, '\n> ')}\n`; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if ( | ||||||
|  |       section.sourceMessage && | ||||||
|  |       section.sourceMessage.sources && | ||||||
|  |       section.sourceMessage.sources.length > 0 | ||||||
|  |     ) { | ||||||
|  |       md += `\n**Citations:**\n`; | ||||||
|  |       section.sourceMessage.sources.forEach((src: any, i: number) => { | ||||||
|  |         const url = src.metadata?.url || ''; | ||||||
|  |         md += `- [${i + 1}] [${url}](${url})\n`; | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |   md += '\n---\n'; | ||||||
|  |   downloadFile(`${title || 'chat'}.md`, md, 'text/markdown'); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const exportAsPDF = (sections: Section[], title: string) => { | ||||||
|  |   const doc = new jsPDF(); | ||||||
|  |   const date = new Date( | ||||||
|  |     sections[0]?.userMessage?.createdAt || Date.now(), | ||||||
|  |   ).toLocaleString(); | ||||||
|  |   let y = 15; | ||||||
|  |   const pageHeight = doc.internal.pageSize.height; | ||||||
|  |   doc.setFontSize(18); | ||||||
|  |   doc.text(`Chat Export: ${title}`, 10, y); | ||||||
|  |   y += 8; | ||||||
|  |   doc.setFontSize(11); | ||||||
|  |   doc.setTextColor(100); | ||||||
|  |   doc.text(`Exported on: ${date}`, 10, y); | ||||||
|  |   y += 8; | ||||||
|  |   doc.setDrawColor(200); | ||||||
|  |   doc.line(10, y, 200, y); | ||||||
|  |   y += 6; | ||||||
|  |   doc.setTextColor(30); | ||||||
|  |  | ||||||
|  |   sections.forEach((section, idx) => { | ||||||
|  |     if (section.userMessage) { | ||||||
|  |       if (y > pageHeight - 30) { | ||||||
|  |         doc.addPage(); | ||||||
|  |         y = 15; | ||||||
|  |       } | ||||||
|  |       doc.setFont('helvetica', 'bold'); | ||||||
|  |       doc.text('User', 10, y); | ||||||
|  |       doc.setFont('helvetica', 'normal'); | ||||||
|  |       doc.setFontSize(10); | ||||||
|  |       doc.setTextColor(120); | ||||||
|  |       doc.text( | ||||||
|  |         `${new Date(section.userMessage.createdAt).toLocaleString()}`, | ||||||
|  |         40, | ||||||
|  |         y, | ||||||
|  |       ); | ||||||
|  |       y += 6; | ||||||
|  |       doc.setTextColor(30); | ||||||
|  |       doc.setFontSize(12); | ||||||
|  |       const userLines = doc.splitTextToSize(section.userMessage.content, 180); | ||||||
|  |       for (let i = 0; i < userLines.length; i++) { | ||||||
|  |         if (y > pageHeight - 20) { | ||||||
|  |           doc.addPage(); | ||||||
|  |           y = 15; | ||||||
|  |         } | ||||||
|  |         doc.text(userLines[i], 12, y); | ||||||
|  |         y += 6; | ||||||
|  |       } | ||||||
|  |       y += 6; | ||||||
|  |       doc.setDrawColor(230); | ||||||
|  |       if (y > pageHeight - 10) { | ||||||
|  |         doc.addPage(); | ||||||
|  |         y = 15; | ||||||
|  |       } | ||||||
|  |       doc.line(10, y, 200, y); | ||||||
|  |       y += 4; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (section.assistantMessage) { | ||||||
|  |       if (y > pageHeight - 30) { | ||||||
|  |         doc.addPage(); | ||||||
|  |         y = 15; | ||||||
|  |       } | ||||||
|  |       doc.setFont('helvetica', 'bold'); | ||||||
|  |       doc.text('Assistant', 10, y); | ||||||
|  |       doc.setFont('helvetica', 'normal'); | ||||||
|  |       doc.setFontSize(10); | ||||||
|  |       doc.setTextColor(120); | ||||||
|  |       doc.text( | ||||||
|  |         `${new Date(section.assistantMessage.createdAt).toLocaleString()}`, | ||||||
|  |         40, | ||||||
|  |         y, | ||||||
|  |       ); | ||||||
|  |       y += 6; | ||||||
|  |       doc.setTextColor(30); | ||||||
|  |       doc.setFontSize(12); | ||||||
|  |       const assistantLines = doc.splitTextToSize( | ||||||
|  |         section.assistantMessage.content, | ||||||
|  |         180, | ||||||
|  |       ); | ||||||
|  |       for (let i = 0; i < assistantLines.length; i++) { | ||||||
|  |         if (y > pageHeight - 20) { | ||||||
|  |           doc.addPage(); | ||||||
|  |           y = 15; | ||||||
|  |         } | ||||||
|  |         doc.text(assistantLines[i], 12, y); | ||||||
|  |         y += 6; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if ( | ||||||
|  |         section.sourceMessage && | ||||||
|  |         section.sourceMessage.sources && | ||||||
|  |         section.sourceMessage.sources.length > 0 | ||||||
|  |       ) { | ||||||
|  |         doc.setFontSize(11); | ||||||
|  |         doc.setTextColor(80); | ||||||
|  |         if (y > pageHeight - 20) { | ||||||
|  |           doc.addPage(); | ||||||
|  |           y = 15; | ||||||
|  |         } | ||||||
|  |         doc.text('Citations:', 12, y); | ||||||
|  |         y += 5; | ||||||
|  |         section.sourceMessage.sources.forEach((src: any, i: number) => { | ||||||
|  |           const url = src.metadata?.url || ''; | ||||||
|  |           if (y > pageHeight - 15) { | ||||||
|  |             doc.addPage(); | ||||||
|  |             y = 15; | ||||||
|  |           } | ||||||
|  |           doc.text(`- [${i + 1}] ${url}`, 15, y); | ||||||
|  |           y += 5; | ||||||
|  |         }); | ||||||
|  |         doc.setTextColor(30); | ||||||
|  |       } | ||||||
|  |       y += 6; | ||||||
|  |       doc.setDrawColor(230); | ||||||
|  |       if (y > pageHeight - 10) { | ||||||
|  |         doc.addPage(); | ||||||
|  |         y = 15; | ||||||
|  |       } | ||||||
|  |       doc.line(10, y, 200, y); | ||||||
|  |       y += 4; | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |   doc.save(`${title || 'chat'}.pdf`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const Navbar = () => { | ||||||
|   const [title, setTitle] = useState<string>(''); |   const [title, setTitle] = useState<string>(''); | ||||||
|   const [timeAgo, setTimeAgo] = useState<string>(''); |   const [timeAgo, setTimeAgo] = useState<string>(''); | ||||||
|  |  | ||||||
|  |   const { sections, chatId } = useChat(); | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (messages.length > 0) { |     if (sections.length > 0 && sections[0].userMessage) { | ||||||
|       const newTitle = |       const newTitle = | ||||||
|         messages[0].content.length > 20 |         sections[0].userMessage.content.length > 20 | ||||||
|           ? `${messages[0].content.substring(0, 20).trim()}...` |           ? `${sections[0].userMessage.content.substring(0, 20).trim()}...` | ||||||
|           : messages[0].content; |           : sections[0].userMessage.content; | ||||||
|       setTitle(newTitle); |       setTitle(newTitle); | ||||||
|       const newTimeAgo = formatTimeDifference( |       const newTimeAgo = formatTimeDifference( | ||||||
|         new Date(), |         new Date(), | ||||||
|         messages[0].createdAt, |         sections[0].userMessage.createdAt, | ||||||
|       ); |       ); | ||||||
|       setTimeAgo(newTimeAgo); |       setTimeAgo(newTimeAgo); | ||||||
|     } |     } | ||||||
|   }, [messages]); |   }, [sections]); | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     const intervalId = setInterval(() => { |     const intervalId = setInterval(() => { | ||||||
|       if (messages.length > 0) { |       if (sections.length > 0 && sections[0].userMessage) { | ||||||
|         const newTimeAgo = formatTimeDifference( |         const newTimeAgo = formatTimeDifference( | ||||||
|           new Date(), |           new Date(), | ||||||
|           messages[0].createdAt, |           sections[0].userMessage.createdAt, | ||||||
|         ); |         ); | ||||||
|         setTimeAgo(newTimeAgo); |         setTimeAgo(newTimeAgo); | ||||||
|       } |       } | ||||||
| @@ -45,25 +228,91 @@ const Navbar = ({ | |||||||
|   }, []); |   }, []); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <div className="fixed z-40 top-0 left-0 right-0 px-4 lg:pl-[104px] lg:pr-6 lg:px-8 flex flex-row items-center justify-between w-full py-4 text-sm text-black dark:text-white/70 border-b bg-light-primary dark:bg-dark-primary border-light-100 dark:border-dark-200"> |     <div className="sticky -mx-4 lg:mx-0 top-0 z-40 bg-light-primary/95 dark:bg-dark-primary/95 backdrop-blur-sm border-b border-light-200/50 dark:border-dark-200/30"> | ||||||
|       <a |       <div className="px-4 lg:px-6 py-4"> | ||||||
|         href="/" |         <div className="flex items-center justify-between"> | ||||||
|         className="active:scale-95 transition duration-100 cursor-pointer lg:hidden" |           <div className="flex items-center min-w-0"> | ||||||
|       > |             <a | ||||||
|         <Edit size={17} /> |               href="/" | ||||||
|       </a> |               className="lg:hidden mr-3 p-2 -ml-2 rounded-lg hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors duration-200" | ||||||
|       <div className="hidden lg:flex flex-row items-center justify-center space-x-2"> |             > | ||||||
|         <Clock size={17} /> |               <Edit size={18} className="text-black/70 dark:text-white/70" /> | ||||||
|         <p className="text-xs">{timeAgo} ago</p> |             </a> | ||||||
|       </div> |             <div className="hidden lg:flex items-center gap-2 text-black/50 dark:text-white/50 min-w-0"> | ||||||
|       <p className="hidden lg:flex">{title}</p> |               <Clock size={14} /> | ||||||
|  |               <span className="text-xs whitespace-nowrap">{timeAgo} ago</span> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |  | ||||||
|       <div className="flex flex-row items-center space-x-4"> |           <div className="flex-1 mx-4 min-w-0"> | ||||||
|         <Share |             <h1 className="text-center text-sm font-medium text-black/80 dark:text-white/90 truncate"> | ||||||
|           size={17} |               {title || 'New Conversation'} | ||||||
|           className="active:scale-95 transition duration-100 cursor-pointer" |             </h1> | ||||||
|         /> |           </div> | ||||||
|         <DeleteChat redirect chatId={chatId} chats={[]} setChats={() => {}} /> |  | ||||||
|  |           <div className="flex items-center gap-1 min-w-0"> | ||||||
|  |             <Popover className="relative"> | ||||||
|  |               <PopoverButton className="p-2 rounded-lg hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors duration-200"> | ||||||
|  |                 <Share size={16} className="text-black/60 dark:text-white/60" /> | ||||||
|  |               </PopoverButton> | ||||||
|  |               <Transition | ||||||
|  |                 as={Fragment} | ||||||
|  |                 enter="transition ease-out duration-200" | ||||||
|  |                 enterFrom="opacity-0 translate-y-1" | ||||||
|  |                 enterTo="opacity-100 translate-y-0" | ||||||
|  |                 leave="transition ease-in duration-150" | ||||||
|  |                 leaveFrom="opacity-100 translate-y-0" | ||||||
|  |                 leaveTo="opacity-0 translate-y-1" | ||||||
|  |               > | ||||||
|  |                 <PopoverPanel className="absolute right-0 mt-2 w-64 origin-top-right rounded-2xl bg-light-primary dark:bg-dark-primary border border-light-200 dark:border-dark-200 shadow-xl shadow-black/10 dark:shadow-black/30 z-50"> | ||||||
|  |                   <div className="p-3"> | ||||||
|  |                     <div className="mb-2"> | ||||||
|  |                       <p className="text-xs font-medium text-black/40 dark:text-white/40 uppercase tracking-wide"> | ||||||
|  |                         Export Chat | ||||||
|  |                       </p> | ||||||
|  |                     </div> | ||||||
|  |                     <div className="space-y-1"> | ||||||
|  |                       <button | ||||||
|  |                         className="w-full flex items-center gap-3 px-3 py-2 text-left rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors duration-200" | ||||||
|  |                         onClick={() => exportAsMarkdown(sections, title || '')} | ||||||
|  |                       > | ||||||
|  |                         <FileText size={16} className="text-[#24A0ED]" /> | ||||||
|  |                         <div> | ||||||
|  |                           <p className="text-sm font-medium text-black dark:text-white"> | ||||||
|  |                             Markdown | ||||||
|  |                           </p> | ||||||
|  |                           <p className="text-xs text-black/50 dark:text-white/50"> | ||||||
|  |                             .md format | ||||||
|  |                           </p> | ||||||
|  |                         </div> | ||||||
|  |                       </button> | ||||||
|  |                       <button | ||||||
|  |                         className="w-full flex items-center gap-3 px-3 py-2 text-left rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors duration-200" | ||||||
|  |                         onClick={() => exportAsPDF(sections, title || '')} | ||||||
|  |                       > | ||||||
|  |                         <FileDown size={16} className="text-[#24A0ED]" /> | ||||||
|  |                         <div> | ||||||
|  |                           <p className="text-sm font-medium text-black dark:text-white"> | ||||||
|  |                             PDF | ||||||
|  |                           </p> | ||||||
|  |                           <p className="text-xs text-black/50 dark:text-white/50"> | ||||||
|  |                             Document format | ||||||
|  |                           </p> | ||||||
|  |                         </div> | ||||||
|  |                       </button> | ||||||
|  |                     </div> | ||||||
|  |                   </div> | ||||||
|  |                 </PopoverPanel> | ||||||
|  |               </Transition> | ||||||
|  |             </Popover> | ||||||
|  |             <DeleteChat | ||||||
|  |               redirect | ||||||
|  |               chatId={chatId!} | ||||||
|  |               chats={[]} | ||||||
|  |               setChats={() => {}} | ||||||
|  |             /> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
|   | |||||||
							
								
								
									
										71
									
								
								src/components/NewsArticleWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,71 @@ | |||||||
|  | import { useEffect, useState } from 'react'; | ||||||
|  |  | ||||||
|  | interface Article { | ||||||
|  |   title: string; | ||||||
|  |   content: string; | ||||||
|  |   url: string; | ||||||
|  |   thumbnail: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const NewsArticleWidget = () => { | ||||||
|  |   const [article, setArticle] = useState<Article | null>(null); | ||||||
|  |   const [loading, setLoading] = useState(true); | ||||||
|  |   const [error, setError] = useState(false); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     fetch('/api/discover?mode=preview') | ||||||
|  |       .then((res) => res.json()) | ||||||
|  |       .then((data) => { | ||||||
|  |         const articles = (data.blogs || []).filter((a: Article) => a.thumbnail); | ||||||
|  |         setArticle(articles[Math.floor(Math.random() * articles.length)]); | ||||||
|  |         setLoading(false); | ||||||
|  |       }) | ||||||
|  |       .catch(() => { | ||||||
|  |         setError(true); | ||||||
|  |         setLoading(false); | ||||||
|  |       }); | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <div className="bg-light-secondary dark:bg-dark-secondary rounded-2xl border border-light-200 dark:border-dark-200 shadow-sm shadow-light-200/10 dark:shadow-black/25 flex flex-row items-stretch w-full h-24 min-h-[96px] max-h-[96px] p-0 overflow-hidden"> | ||||||
|  |       {loading ? ( | ||||||
|  |         <div className="animate-pulse flex flex-row items-stretch w-full h-full"> | ||||||
|  |           <div className="w-24 min-w-24 max-w-24 h-full bg-light-200 dark:bg-dark-200" /> | ||||||
|  |           <div className="flex flex-col justify-center flex-1 px-3 py-2 gap-2"> | ||||||
|  |             <div className="h-4 w-3/4 rounded bg-light-200 dark:bg-dark-200" /> | ||||||
|  |             <div className="h-3 w-1/2 rounded bg-light-200 dark:bg-dark-200" /> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       ) : error ? ( | ||||||
|  |         <div className="w-full text-xs text-red-400">Could not load news.</div> | ||||||
|  |       ) : article ? ( | ||||||
|  |         <a | ||||||
|  |           href={`/?q=Summary: ${article.url}`} | ||||||
|  |           className="flex flex-row items-stretch w-full h-full relative overflow-hidden group" | ||||||
|  |         > | ||||||
|  |           <div className="relative w-24 min-w-24 max-w-24 h-full overflow-hidden"> | ||||||
|  |             <img | ||||||
|  |               className="object-cover w-full h-full bg-light-200 dark:bg-dark-200 group-hover:scale-110 transition-transform duration-300" | ||||||
|  |               src={ | ||||||
|  |                 new URL(article.thumbnail).origin + | ||||||
|  |                 new URL(article.thumbnail).pathname + | ||||||
|  |                 `?id=${new URL(article.thumbnail).searchParams.get('id')}` | ||||||
|  |               } | ||||||
|  |               alt={article.title} | ||||||
|  |             /> | ||||||
|  |           </div> | ||||||
|  |           <div className="flex flex-col justify-center flex-1 px-3 py-2"> | ||||||
|  |             <div className="font-semibold text-xs text-black dark:text-white leading-tight line-clamp-2 mb-1"> | ||||||
|  |               {article.title} | ||||||
|  |             </div> | ||||||
|  |             <p className="text-black/60 dark:text-white/60 text-[10px] leading-relaxed line-clamp-2"> | ||||||
|  |               {article.content} | ||||||
|  |             </p> | ||||||
|  |           </div> | ||||||
|  |         </a> | ||||||
|  |       ) : null} | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default NewsArticleWidget; | ||||||
| @@ -33,11 +33,10 @@ const SearchImages = ({ | |||||||
|           onClick={async () => { |           onClick={async () => { | ||||||
|             setLoading(true); |             setLoading(true); | ||||||
|  |  | ||||||
|             const chatModelProvider = localStorage.getItem('chatModelProvider'); |             const chatModelProvider = localStorage.getItem( | ||||||
|             const chatModel = localStorage.getItem('chatModel'); |               'chatModelProviderId', | ||||||
|  |             ); | ||||||
|             const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL'); |             const chatModel = localStorage.getItem('chatModelKey'); | ||||||
|             const customOpenAIKey = localStorage.getItem('openAIApiKey'); |  | ||||||
|  |  | ||||||
|             const res = await fetch(`/api/images`, { |             const res = await fetch(`/api/images`, { | ||||||
|               method: 'POST', |               method: 'POST', | ||||||
| @@ -48,12 +47,8 @@ const SearchImages = ({ | |||||||
|                 query: query, |                 query: query, | ||||||
|                 chatHistory: chatHistory, |                 chatHistory: chatHistory, | ||||||
|                 chatModel: { |                 chatModel: { | ||||||
|                   provider: chatModelProvider, |                   providerId: chatModelProvider, | ||||||
|                   model: chatModel, |                   key: chatModel, | ||||||
|                   ...(chatModelProvider === 'custom_openai' && { |  | ||||||
|                     customOpenAIBaseURL: customOpenAIBaseURL, |  | ||||||
|                     customOpenAIKey: customOpenAIKey, |  | ||||||
|                   }), |  | ||||||
|                 }, |                 }, | ||||||
|               }), |               }), | ||||||
|             }); |             }); | ||||||
|   | |||||||
| @@ -48,11 +48,10 @@ const Searchvideos = ({ | |||||||
|           onClick={async () => { |           onClick={async () => { | ||||||
|             setLoading(true); |             setLoading(true); | ||||||
|  |  | ||||||
|             const chatModelProvider = localStorage.getItem('chatModelProvider'); |             const chatModelProvider = localStorage.getItem( | ||||||
|             const chatModel = localStorage.getItem('chatModel'); |               'chatModelProviderId', | ||||||
|  |             ); | ||||||
|             const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL'); |             const chatModel = localStorage.getItem('chatModelKey'); | ||||||
|             const customOpenAIKey = localStorage.getItem('openAIApiKey'); |  | ||||||
|  |  | ||||||
|             const res = await fetch(`/api/videos`, { |             const res = await fetch(`/api/videos`, { | ||||||
|               method: 'POST', |               method: 'POST', | ||||||
| @@ -63,12 +62,8 @@ const Searchvideos = ({ | |||||||
|                 query: query, |                 query: query, | ||||||
|                 chatHistory: chatHistory, |                 chatHistory: chatHistory, | ||||||
|                 chatModel: { |                 chatModel: { | ||||||
|                   provider: chatModelProvider, |                   providerId: chatModelProvider, | ||||||
|                   model: chatModel, |                   key: chatModel, | ||||||
|                   ...(chatModelProvider === 'custom_openai' && { |  | ||||||
|                     customOpenAIBaseURL: customOpenAIBaseURL, |  | ||||||
|                     customOpenAIKey: customOpenAIKey, |  | ||||||
|                   }), |  | ||||||
|                 }, |                 }, | ||||||
|               }), |               }), | ||||||
|             }); |             }); | ||||||
|   | |||||||
							
								
								
									
										29
									
								
								src/components/Settings/Sections/General.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,29 @@ | |||||||
|  | import { UIConfigField } from '@/lib/config/types'; | ||||||
|  | import SettingsField from '../SettingsField'; | ||||||
|  |  | ||||||
|  | const General = ({ | ||||||
|  |   fields, | ||||||
|  |   values, | ||||||
|  | }: { | ||||||
|  |   fields: UIConfigField[]; | ||||||
|  |   values: Record<string, any>; | ||||||
|  | }) => { | ||||||
|  |   return ( | ||||||
|  |     <div className="flex-1 space-y-6 overflow-y-auto px-6 py-6"> | ||||||
|  |       {fields.map((field) => ( | ||||||
|  |         <SettingsField | ||||||
|  |           key={field.key} | ||||||
|  |           field={field} | ||||||
|  |           value={ | ||||||
|  |             (field.scope === 'client' | ||||||
|  |               ? localStorage.getItem(field.key) | ||||||
|  |               : values[field.key]) ?? field.default | ||||||
|  |           } | ||||||
|  |           dataAdd="general" | ||||||
|  |         /> | ||||||
|  |       ))} | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default General; | ||||||
							
								
								
									
										163
									
								
								src/components/Settings/Sections/Models/AddModelDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,163 @@ | |||||||
|  | import { Dialog, DialogPanel } from '@headlessui/react'; | ||||||
|  | import { Loader2, Plus } from 'lucide-react'; | ||||||
|  | import { useState } from 'react'; | ||||||
|  | import { AnimatePresence, motion } from 'framer-motion'; | ||||||
|  | import { ConfigModelProvider } from '@/lib/config/types'; | ||||||
|  | import { toast } from 'sonner'; | ||||||
|  |  | ||||||
|  | const AddModel = ({ | ||||||
|  |   providerId, | ||||||
|  |   setProviders, | ||||||
|  |   type, | ||||||
|  | }: { | ||||||
|  |   providerId: string; | ||||||
|  |   setProviders: React.Dispatch<React.SetStateAction<ConfigModelProvider[]>>; | ||||||
|  |   type: 'chat' | 'embedding'; | ||||||
|  | }) => { | ||||||
|  |   const [open, setOpen] = useState(false); | ||||||
|  |   const [modelName, setModelName] = useState(''); | ||||||
|  |   const [modelKey, setModelKey] = useState(''); | ||||||
|  |   const [loading, setLoading] = useState(false); | ||||||
|  |  | ||||||
|  |   const handleSubmit = async (e: React.FormEvent) => { | ||||||
|  |     e.preventDefault(); | ||||||
|  |     setLoading(true); | ||||||
|  |     try { | ||||||
|  |       const res = await fetch(`/api/providers/${providerId}/models`, { | ||||||
|  |         method: 'POST', | ||||||
|  |         headers: { | ||||||
|  |           'Content-Type': 'application/json', | ||||||
|  |         }, | ||||||
|  |         body: JSON.stringify({ | ||||||
|  |           name: modelName, | ||||||
|  |           key: modelKey, | ||||||
|  |           type: type, | ||||||
|  |         }), | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (!res.ok) { | ||||||
|  |         throw new Error('Failed to add model'); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       setProviders((prev) => | ||||||
|  |         prev.map((provider) => { | ||||||
|  |           if (provider.id === providerId) { | ||||||
|  |             const newModel = { name: modelName, key: modelKey }; | ||||||
|  |             return { | ||||||
|  |               ...provider, | ||||||
|  |               chatModels: | ||||||
|  |                 type === 'chat' | ||||||
|  |                   ? [...provider.chatModels, newModel] | ||||||
|  |                   : provider.chatModels, | ||||||
|  |               embeddingModels: | ||||||
|  |                 type === 'embedding' | ||||||
|  |                   ? [...provider.embeddingModels, newModel] | ||||||
|  |                   : provider.embeddingModels, | ||||||
|  |             }; | ||||||
|  |           } | ||||||
|  |           return provider; | ||||||
|  |         }), | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       toast.success('Model added successfully.'); | ||||||
|  |       setModelName(''); | ||||||
|  |       setModelKey(''); | ||||||
|  |       setOpen(false); | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Error adding model:', error); | ||||||
|  |       toast.error('Failed to add model.'); | ||||||
|  |     } finally { | ||||||
|  |       setLoading(false); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <button | ||||||
|  |         onClick={() => setOpen(true)} | ||||||
|  |         className="text-xs text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white flex flex-row items-center space-x-1 active:scale-95 transition duration-200" | ||||||
|  |       > | ||||||
|  |         <Plus size={12} /> | ||||||
|  |         <span>Add</span> | ||||||
|  |       </button> | ||||||
|  |       <AnimatePresence> | ||||||
|  |         {open && ( | ||||||
|  |           <Dialog | ||||||
|  |             static | ||||||
|  |             open={open} | ||||||
|  |             onClose={() => setOpen(false)} | ||||||
|  |             className="relative z-[60]" | ||||||
|  |           > | ||||||
|  |             <motion.div | ||||||
|  |               initial={{ opacity: 0 }} | ||||||
|  |               animate={{ opacity: 1 }} | ||||||
|  |               exit={{ opacity: 0 }} | ||||||
|  |               transition={{ duration: 0.1 }} | ||||||
|  |               className="fixed inset-0 flex w-screen items-center justify-center p-4 bg-black/30 backdrop-blur-sm" | ||||||
|  |             > | ||||||
|  |               <DialogPanel className="w-full mx-4 lg:w-[600px] max-h-[85vh] flex flex-col border bg-light-primary dark:bg-dark-primary border-light-secondary dark:border-dark-secondary rounded-lg"> | ||||||
|  |                 <div className="px-6 pt-6 pb-4"> | ||||||
|  |                   <h3 className="text-black/90 dark:text-white/90 font-medium"> | ||||||
|  |                     Add new {type === 'chat' ? 'chat' : 'embedding'} model | ||||||
|  |                   </h3> | ||||||
|  |                 </div> | ||||||
|  |                 <div className="border-t border-light-200 dark:border-dark-200" /> | ||||||
|  |                 <div className="flex-1 overflow-y-auto px-6 py-4"> | ||||||
|  |                   <form | ||||||
|  |                     onSubmit={handleSubmit} | ||||||
|  |                     className="flex flex-col h-full" | ||||||
|  |                   > | ||||||
|  |                     <div className="flex flex-col space-y-4 flex-1"> | ||||||
|  |                       <div className="flex flex-col items-start space-y-2"> | ||||||
|  |                         <label className="text-xs text-black/70 dark:text-white/70"> | ||||||
|  |                           Model name* | ||||||
|  |                         </label> | ||||||
|  |                         <input | ||||||
|  |                           value={modelName} | ||||||
|  |                           onChange={(e) => setModelName(e.target.value)} | ||||||
|  |                           className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60" | ||||||
|  |                           placeholder="e.g., GPT-4" | ||||||
|  |                           type="text" | ||||||
|  |                           required | ||||||
|  |                         /> | ||||||
|  |                       </div> | ||||||
|  |                       <div className="flex flex-col items-start space-y-2"> | ||||||
|  |                         <label className="text-xs text-black/70 dark:text-white/70"> | ||||||
|  |                           Model key* | ||||||
|  |                         </label> | ||||||
|  |                         <input | ||||||
|  |                           value={modelKey} | ||||||
|  |                           onChange={(e) => setModelKey(e.target.value)} | ||||||
|  |                           className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60" | ||||||
|  |                           placeholder="e.g., gpt-4" | ||||||
|  |                           type="text" | ||||||
|  |                           required | ||||||
|  |                         /> | ||||||
|  |                       </div> | ||||||
|  |                     </div> | ||||||
|  |                     <div className="border-t border-light-200 dark:border-dark-200 -mx-6 my-4" /> | ||||||
|  |                     <div className="flex justify-end"> | ||||||
|  |                       <button | ||||||
|  |                         type="submit" | ||||||
|  |                         disabled={loading} | ||||||
|  |                         className="px-4 py-2 rounded-lg text-sm bg-sky-500 text-white font-medium disabled:opacity-85 hover:opacity-85 active:scale-95 transition duration-200" | ||||||
|  |                       > | ||||||
|  |                         {loading ? ( | ||||||
|  |                           <Loader2 className="animate-spin" size={16} /> | ||||||
|  |                         ) : ( | ||||||
|  |                           'Add Model' | ||||||
|  |                         )} | ||||||
|  |                       </button> | ||||||
|  |                     </div> | ||||||
|  |                   </form> | ||||||
|  |                 </div> | ||||||
|  |               </DialogPanel> | ||||||
|  |             </motion.div> | ||||||
|  |           </Dialog> | ||||||
|  |         )} | ||||||
|  |       </AnimatePresence> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default AddModel; | ||||||
							
								
								
									
										216
									
								
								src/components/Settings/Sections/Models/AddProviderDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,216 @@ | |||||||
|  | import { | ||||||
|  |   Description, | ||||||
|  |   Dialog, | ||||||
|  |   DialogPanel, | ||||||
|  |   DialogTitle, | ||||||
|  | } from '@headlessui/react'; | ||||||
|  | import { Loader2, Plus } from 'lucide-react'; | ||||||
|  | import { useMemo, useState } from 'react'; | ||||||
|  | import { AnimatePresence, motion } from 'framer-motion'; | ||||||
|  | import { | ||||||
|  |   ConfigModelProvider, | ||||||
|  |   ModelProviderUISection, | ||||||
|  |   StringUIConfigField, | ||||||
|  |   UIConfigField, | ||||||
|  | } from '@/lib/config/types'; | ||||||
|  | import Select from '@/components/ui/Select'; | ||||||
|  | import { toast } from 'sonner'; | ||||||
|  |  | ||||||
|  | const AddProvider = ({ | ||||||
|  |   modelProviders, | ||||||
|  |   setProviders, | ||||||
|  | }: { | ||||||
|  |   modelProviders: ModelProviderUISection[]; | ||||||
|  |   setProviders: React.Dispatch<React.SetStateAction<ConfigModelProvider[]>>; | ||||||
|  | }) => { | ||||||
|  |   const [open, setOpen] = useState(false); | ||||||
|  |   const [selectedProvider, setSelectedProvider] = useState<null | string>( | ||||||
|  |     modelProviders[0]?.key || null, | ||||||
|  |   ); | ||||||
|  |   const [config, setConfig] = useState<Record<string, any>>({}); | ||||||
|  |   const [name, setName] = useState(''); | ||||||
|  |   const [loading, setLoading] = useState(false); | ||||||
|  |  | ||||||
|  |   const providerConfigMap = useMemo(() => { | ||||||
|  |     const map: Record<string, { name: string; fields: UIConfigField[] }> = {}; | ||||||
|  |  | ||||||
|  |     modelProviders.forEach((p) => { | ||||||
|  |       map[p.key] = { | ||||||
|  |         name: p.name, | ||||||
|  |         fields: p.fields, | ||||||
|  |       }; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return map; | ||||||
|  |   }, [modelProviders]); | ||||||
|  |  | ||||||
|  |   const selectedProviderFields = useMemo(() => { | ||||||
|  |     if (!selectedProvider) return []; | ||||||
|  |     const providerFields = providerConfigMap[selectedProvider]?.fields || []; | ||||||
|  |     const config: Record<string, any> = {}; | ||||||
|  |  | ||||||
|  |     providerFields.forEach((field) => { | ||||||
|  |       config[field.key] = field.default || ''; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     setConfig(config); | ||||||
|  |  | ||||||
|  |     return providerFields; | ||||||
|  |   }, [selectedProvider, providerConfigMap]); | ||||||
|  |  | ||||||
|  |   const handleSubmit = async (e: React.FormEvent) => { | ||||||
|  |     e.preventDefault(); | ||||||
|  |     setLoading(true); | ||||||
|  |     try { | ||||||
|  |       const res = await fetch('/api/providers', { | ||||||
|  |         method: 'POST', | ||||||
|  |         headers: { | ||||||
|  |           'Content-Type': 'application/json', | ||||||
|  |         }, | ||||||
|  |         body: JSON.stringify({ | ||||||
|  |           type: selectedProvider, | ||||||
|  |           name: name, | ||||||
|  |           config: config, | ||||||
|  |         }), | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (!res.ok) { | ||||||
|  |         throw new Error('Failed to add provider'); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const data: ConfigModelProvider = (await res.json()).provider; | ||||||
|  |  | ||||||
|  |       setProviders((prev) => [...prev, data]); | ||||||
|  |  | ||||||
|  |       toast.success('Provider added successfully.'); | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Error adding provider:', error); | ||||||
|  |       toast.error('Failed to add provider.'); | ||||||
|  |     } finally { | ||||||
|  |       setLoading(false); | ||||||
|  |       setOpen(false); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <button | ||||||
|  |         onClick={() => setOpen(true)} | ||||||
|  |         className="px-3 md:px-4 py-1.5 md:py-2 rounded-lg text-xs sm:text-sm border border-light-200 dark:border-dark-200 text-black dark:text-white bg-light-secondary/50 dark:bg-dark-secondary/50 hover:bg-light-secondary hover:dark:bg-dark-secondary hover:border-light-300 hover:dark:border-dark-300 flex flex-row items-center space-x-1 active:scale-95 transition duration-200" | ||||||
|  |       > | ||||||
|  |         <Plus className="w-3.5 h-3.5 md:w-4 md:h-4" /> | ||||||
|  |         <span>Add Provider</span> | ||||||
|  |       </button> | ||||||
|  |       <AnimatePresence> | ||||||
|  |         {open && ( | ||||||
|  |           <Dialog | ||||||
|  |             static | ||||||
|  |             open={open} | ||||||
|  |             onClose={() => setOpen(false)} | ||||||
|  |             className="relative z-[60]" | ||||||
|  |           > | ||||||
|  |             <motion.div | ||||||
|  |               initial={{ opacity: 0 }} | ||||||
|  |               animate={{ opacity: 1 }} | ||||||
|  |               exit={{ opacity: 0 }} | ||||||
|  |               transition={{ duration: 0.1 }} | ||||||
|  |               className="fixed inset-0 flex w-screen items-center justify-center p-4 bg-black/30 backdrop-blur-sm" | ||||||
|  |             > | ||||||
|  |               <DialogPanel className="w-full mx-4 lg:w-[600px] max-h-[85vh] flex flex-col border bg-light-primary dark:bg-dark-primary border-light-secondary dark:border-dark-secondary rounded-lg"> | ||||||
|  |                 <form onSubmit={handleSubmit} className="flex flex-col flex-1"> | ||||||
|  |                   <div className="px-6 pt-6 pb-4"> | ||||||
|  |                     <h3 className="text-black/90 dark:text-white/90 font-medium"> | ||||||
|  |                       Add new provider | ||||||
|  |                     </h3> | ||||||
|  |                   </div> | ||||||
|  |                   <div className="border-t border-light-200 dark:border-dark-200" /> | ||||||
|  |                   <div className="flex-1 overflow-y-auto px-6 py-4"> | ||||||
|  |                     <div className="flex flex-col space-y-4"> | ||||||
|  |                       <div className="flex flex-col items-start space-y-2"> | ||||||
|  |                         <label className="text-xs text-black/70 dark:text-white/70"> | ||||||
|  |                           Select provider type | ||||||
|  |                         </label> | ||||||
|  |                         <Select | ||||||
|  |                           value={selectedProvider ?? ''} | ||||||
|  |                           onChange={(e) => setSelectedProvider(e.target.value)} | ||||||
|  |                           options={Object.entries(providerConfigMap).map( | ||||||
|  |                             ([key, val]) => { | ||||||
|  |                               return { | ||||||
|  |                                 label: val.name, | ||||||
|  |                                 value: key, | ||||||
|  |                               }; | ||||||
|  |                             }, | ||||||
|  |                           )} | ||||||
|  |                         /> | ||||||
|  |                       </div> | ||||||
|  |  | ||||||
|  |                       <div | ||||||
|  |                         key="name" | ||||||
|  |                         className="flex flex-col items-start space-y-2" | ||||||
|  |                       > | ||||||
|  |                         <label className="text-xs text-black/70 dark:text-white/70"> | ||||||
|  |                           Name* | ||||||
|  |                         </label> | ||||||
|  |                         <input | ||||||
|  |                           value={name} | ||||||
|  |                           onChange={(e) => setName(e.target.value)} | ||||||
|  |                           className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 pr-10 text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60" | ||||||
|  |                           placeholder={'Provider Name'} | ||||||
|  |                           type="text" | ||||||
|  |                           required={true} | ||||||
|  |                         /> | ||||||
|  |                       </div> | ||||||
|  |  | ||||||
|  |                       {selectedProviderFields.map((field: UIConfigField) => ( | ||||||
|  |                         <div | ||||||
|  |                           key={field.key} | ||||||
|  |                           className="flex flex-col items-start space-y-2" | ||||||
|  |                         > | ||||||
|  |                           <label className="text-xs text-black/70 dark:text-white/70"> | ||||||
|  |                             {field.name} | ||||||
|  |                             {field.required && '*'} | ||||||
|  |                           </label> | ||||||
|  |                           <input | ||||||
|  |                             value={config[field.key] ?? field.default ?? ''} | ||||||
|  |                             onChange={(event) => | ||||||
|  |                               setConfig((prev) => ({ | ||||||
|  |                                 ...prev, | ||||||
|  |                                 [field.key]: event.target.value, | ||||||
|  |                               })) | ||||||
|  |                             } | ||||||
|  |                             className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 pr-10 text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60" | ||||||
|  |                             placeholder={ | ||||||
|  |                               (field as StringUIConfigField).placeholder | ||||||
|  |                             } | ||||||
|  |                             type="text" | ||||||
|  |                             required={field.required} | ||||||
|  |                           /> | ||||||
|  |                         </div> | ||||||
|  |                       ))} | ||||||
|  |                     </div> | ||||||
|  |                   </div> | ||||||
|  |                   <div className="border-t border-light-200 dark:border-dark-200" /> | ||||||
|  |                   <div className="px-6 py-4 flex justify-end"> | ||||||
|  |                     <button | ||||||
|  |                       type="submit" | ||||||
|  |                       disabled={loading} | ||||||
|  |                       className="px-4 py-2 rounded-lg text-sm bg-sky-500 text-white font-medium disabled:opacity-85 hover:opacity-85 active:scale-95 transition duration-200" | ||||||
|  |                     > | ||||||
|  |                       {loading ? ( | ||||||
|  |                         <Loader2 className="animate-spin" size={16} /> | ||||||
|  |                       ) : ( | ||||||
|  |                         'Add Provider' | ||||||
|  |                       )} | ||||||
|  |                     </button> | ||||||
|  |                   </div> | ||||||
|  |                 </form> | ||||||
|  |               </DialogPanel> | ||||||
|  |             </motion.div> | ||||||
|  |           </Dialog> | ||||||
|  |         )} | ||||||
|  |       </AnimatePresence> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default AddProvider; | ||||||
							
								
								
									
										118
									
								
								src/components/Settings/Sections/Models/DeleteProviderDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,118 @@ | |||||||
|  | import { Dialog, DialogPanel } from '@headlessui/react'; | ||||||
|  | import { Loader2, Trash2 } from 'lucide-react'; | ||||||
|  | import { useState } from 'react'; | ||||||
|  | import { AnimatePresence, motion } from 'framer-motion'; | ||||||
|  | import { ConfigModelProvider } from '@/lib/config/types'; | ||||||
|  | import { toast } from 'sonner'; | ||||||
|  |  | ||||||
|  | const DeleteProvider = ({ | ||||||
|  |   modelProvider, | ||||||
|  |   setProviders, | ||||||
|  | }: { | ||||||
|  |   modelProvider: ConfigModelProvider; | ||||||
|  |   setProviders: React.Dispatch<React.SetStateAction<ConfigModelProvider[]>>; | ||||||
|  | }) => { | ||||||
|  |   const [open, setOpen] = useState(false); | ||||||
|  |   const [loading, setLoading] = useState(false); | ||||||
|  |  | ||||||
|  |   const handleDelete = async (e: React.FormEvent) => { | ||||||
|  |     e.preventDefault(); | ||||||
|  |     setLoading(true); | ||||||
|  |     try { | ||||||
|  |       const res = await fetch(`/api/providers/${modelProvider.id}`, { | ||||||
|  |         method: 'DELETE', | ||||||
|  |         headers: { | ||||||
|  |           'Content-Type': 'application/json', | ||||||
|  |         }, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (!res.ok) { | ||||||
|  |         throw new Error('Failed to delete provider'); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       setProviders((prev) => { | ||||||
|  |         return prev.filter((p) => p.id !== modelProvider.id); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       toast.success('Provider deleted successfully.'); | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Error deleting provider:', error); | ||||||
|  |       toast.error('Failed to delete provider.'); | ||||||
|  |     } finally { | ||||||
|  |       setLoading(false); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <button | ||||||
|  |         onClick={(e) => { | ||||||
|  |           e.stopPropagation(); | ||||||
|  |           setOpen(true); | ||||||
|  |         }} | ||||||
|  |         className="group p-1.5 rounded-md hover:bg-light-200 hover:dark:bg-dark-200 transition-colors group" | ||||||
|  |         title="Delete provider" | ||||||
|  |       > | ||||||
|  |         <Trash2 | ||||||
|  |           size={14} | ||||||
|  |           className="text-black/60 dark:text-white/60 group-hover:text-red-500 group-hover:dark:text-red-400" | ||||||
|  |         /> | ||||||
|  |       </button> | ||||||
|  |       <AnimatePresence> | ||||||
|  |         {open && ( | ||||||
|  |           <Dialog | ||||||
|  |             static | ||||||
|  |             open={open} | ||||||
|  |             onClose={() => setOpen(false)} | ||||||
|  |             className="relative z-[60]" | ||||||
|  |           > | ||||||
|  |             <motion.div | ||||||
|  |               initial={{ opacity: 0 }} | ||||||
|  |               animate={{ opacity: 1 }} | ||||||
|  |               exit={{ opacity: 0 }} | ||||||
|  |               transition={{ duration: 0.1 }} | ||||||
|  |               className="fixed inset-0 flex w-screen items-center justify-center p-4 bg-black/30 backdrop-blur-sm" | ||||||
|  |             > | ||||||
|  |               <DialogPanel className="w-full mx-4 lg:w-[600px] max-h-[85vh] flex flex-col border bg-light-primary dark:bg-dark-primary border-light-secondary dark:border-dark-secondary rounded-lg"> | ||||||
|  |                 <div className="px-6 pt-6 pb-4"> | ||||||
|  |                   <h3 className="text-black/90 dark:text-white/90 font-medium"> | ||||||
|  |                     Delete provider | ||||||
|  |                   </h3> | ||||||
|  |                 </div> | ||||||
|  |                 <div className="border-t border-light-200 dark:border-dark-200" /> | ||||||
|  |                 <div className="flex-1 overflow-y-auto px-6 py-4"> | ||||||
|  |                   <p className="text-SM text-black/60 dark:text-white/60"> | ||||||
|  |                     Are you sure you want to delete the provider " | ||||||
|  |                     {modelProvider.name}"? This action cannot be undone. | ||||||
|  |                   </p> | ||||||
|  |                 </div> | ||||||
|  |                 <div className="px-6 py-6 flex justify-end space-x-2"> | ||||||
|  |                   <button | ||||||
|  |                     disabled={loading} | ||||||
|  |                     onClick={() => setOpen(false)} | ||||||
|  |                     className="px-4 py-2 rounded-lg text-sm border border-light-200 dark:border-dark-200 text-black dark:text-white bg-light-secondary/50 dark:bg-dark-secondary/50 hover:bg-light-secondary hover:dark:bg-dark-secondary hover:border-light-300 hover:dark:border-dark-300 flex flex-row items-center space-x-1 active:scale-95 transition duration-200" | ||||||
|  |                   > | ||||||
|  |                     Cancel | ||||||
|  |                   </button> | ||||||
|  |                   <button | ||||||
|  |                     disabled={loading} | ||||||
|  |                     onClick={handleDelete} | ||||||
|  |                     className="px-4 py-2 rounded-lg text-sm bg-red-500 text-white font-medium disabled:opacity-85 hover:opacity-85 active:scale-95 transition duration-200" | ||||||
|  |                   > | ||||||
|  |                     {loading ? ( | ||||||
|  |                       <Loader2 className="animate-spin" size={16} /> | ||||||
|  |                     ) : ( | ||||||
|  |                       'Delete' | ||||||
|  |                     )} | ||||||
|  |                   </button> | ||||||
|  |                 </div> | ||||||
|  |               </DialogPanel> | ||||||
|  |             </motion.div> | ||||||
|  |           </Dialog> | ||||||
|  |         )} | ||||||
|  |       </AnimatePresence> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default DeleteProvider; | ||||||
							
								
								
									
										217
									
								
								src/components/Settings/Sections/Models/ModelProvider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,217 @@ | |||||||
|  | import { UIConfigField, ConfigModelProvider } from '@/lib/config/types'; | ||||||
|  | import { cn } from '@/lib/utils'; | ||||||
|  | import { AnimatePresence, motion } from 'framer-motion'; | ||||||
|  | import { AlertCircle, ChevronDown, Pencil, Trash2, X } from 'lucide-react'; | ||||||
|  | import { useState } from 'react'; | ||||||
|  | import { toast } from 'sonner'; | ||||||
|  | import AddModel from './AddModelDialog'; | ||||||
|  | import UpdateProvider from './UpdateProviderDialog'; | ||||||
|  | import DeleteProvider from './DeleteProviderDialog'; | ||||||
|  |  | ||||||
|  | const ModelProvider = ({ | ||||||
|  |   modelProvider, | ||||||
|  |   setProviders, | ||||||
|  |   fields, | ||||||
|  | }: { | ||||||
|  |   modelProvider: ConfigModelProvider; | ||||||
|  |   fields: UIConfigField[]; | ||||||
|  |   setProviders: React.Dispatch<React.SetStateAction<ConfigModelProvider[]>>; | ||||||
|  | }) => { | ||||||
|  |   const [open, setOpen] = useState(false); | ||||||
|  |  | ||||||
|  |   const handleModelDelete = async ( | ||||||
|  |     type: 'chat' | 'embedding', | ||||||
|  |     modelKey: string, | ||||||
|  |   ) => { | ||||||
|  |     try { | ||||||
|  |       const res = await fetch(`/api/providers/${modelProvider.id}/models`, { | ||||||
|  |         method: 'DELETE', | ||||||
|  |         headers: { | ||||||
|  |           'Content-Type': 'application/json', | ||||||
|  |         }, | ||||||
|  |         body: JSON.stringify({ key: modelKey, type: type }), | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (!res.ok) { | ||||||
|  |         throw new Error('Failed to delete model: ' + (await res.text())); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       setProviders( | ||||||
|  |         (prev) => | ||||||
|  |           prev.map((provider) => { | ||||||
|  |             if (provider.id === modelProvider.id) { | ||||||
|  |               return { | ||||||
|  |                 ...provider, | ||||||
|  |                 ...(type === 'chat' | ||||||
|  |                   ? { | ||||||
|  |                       chatModels: provider.chatModels.filter( | ||||||
|  |                         (m) => m.key !== modelKey, | ||||||
|  |                       ), | ||||||
|  |                     } | ||||||
|  |                   : { | ||||||
|  |                       embeddingModels: provider.embeddingModels.filter( | ||||||
|  |                         (m) => m.key !== modelKey, | ||||||
|  |                       ), | ||||||
|  |                     }), | ||||||
|  |               }; | ||||||
|  |             } | ||||||
|  |             return provider; | ||||||
|  |           }) as ConfigModelProvider[], | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       toast.success('Model deleted successfully.'); | ||||||
|  |     } catch (err) { | ||||||
|  |       console.error('Failed to delete model', err); | ||||||
|  |       toast.error('Failed to delete model.'); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <div | ||||||
|  |       key={modelProvider.id} | ||||||
|  |       className="border border-light-200 dark:border-dark-200 rounded-lg overflow-hidden" | ||||||
|  |     > | ||||||
|  |       <div | ||||||
|  |         className={cn( | ||||||
|  |           'group px-5 py-4 flex flex-row justify-between w-full cursor-pointer hover:bg-light-secondary hover:dark:bg-dark-secondary transition duration-200 items-center', | ||||||
|  |           !open && 'rounded-lg', | ||||||
|  |         )} | ||||||
|  |         onClick={() => setOpen(!open)} | ||||||
|  |       > | ||||||
|  |         <p className="text-black dark:text-white font-medium"> | ||||||
|  |           {modelProvider.name} | ||||||
|  |         </p> | ||||||
|  |         <div className="flex items-center gap-4"> | ||||||
|  |           <div className="flex flex-row items-center"> | ||||||
|  |             <UpdateProvider | ||||||
|  |               fields={fields} | ||||||
|  |               modelProvider={modelProvider} | ||||||
|  |               setProviders={setProviders} | ||||||
|  |             /> | ||||||
|  |             <DeleteProvider | ||||||
|  |               modelProvider={modelProvider} | ||||||
|  |               setProviders={setProviders} | ||||||
|  |             /> | ||||||
|  |           </div> | ||||||
|  |           <ChevronDown | ||||||
|  |             size={16} | ||||||
|  |             className={cn( | ||||||
|  |               open ? 'rotate-180' : '', | ||||||
|  |               'transition duration-200 text-black/70 dark:text-white/70 group-hover:text-sky-500', | ||||||
|  |             )} | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |       <AnimatePresence> | ||||||
|  |         {open && ( | ||||||
|  |           <motion.div | ||||||
|  |             initial={{ height: 0, opacity: 0 }} | ||||||
|  |             animate={{ height: 'auto', opacity: 1 }} | ||||||
|  |             exit={{ height: 0, opacity: 0 }} | ||||||
|  |             transition={{ duration: 0.1 }} | ||||||
|  |           > | ||||||
|  |             <div className="border-t border-light-200 dark:border-dark-200" /> | ||||||
|  |             <div className="flex flex-col gap-y-4 px-5 py-4"> | ||||||
|  |               {modelProvider.chatModels.length > 0 && ( | ||||||
|  |                 <div className="flex flex-col gap-y-2"> | ||||||
|  |                   <div className="flex flex-row w-full justify-between items-center"> | ||||||
|  |                     <p className="text-xs text-black/70 dark:text-white/70"> | ||||||
|  |                       Chat models | ||||||
|  |                     </p> | ||||||
|  |                     <AddModel | ||||||
|  |                       providerId={modelProvider.id} | ||||||
|  |                       setProviders={setProviders} | ||||||
|  |                       type="chat" | ||||||
|  |                     /> | ||||||
|  |                   </div> | ||||||
|  |                   <div className="flex flex-col gap-2"> | ||||||
|  |                     {modelProvider.chatModels.some((m) => m.key === 'error') ? ( | ||||||
|  |                       <div className="flex flex-row items-center gap-2 text-sm text-red-500 dark:text-red-400 rounded-lg bg-red-50 dark:bg-red-950/20 px-3 py-2 border border-red-200 dark:border-red-900/30"> | ||||||
|  |                         <AlertCircle size={16} className="shrink-0" /> | ||||||
|  |                         <span className="break-words"> | ||||||
|  |                           { | ||||||
|  |                             modelProvider.chatModels.find( | ||||||
|  |                               (m) => m.key === 'error', | ||||||
|  |                             )?.name | ||||||
|  |                           } | ||||||
|  |                         </span> | ||||||
|  |                       </div> | ||||||
|  |                     ) : ( | ||||||
|  |                       <div className="flex flex-row flex-wrap gap-2"> | ||||||
|  |                         {modelProvider.chatModels.map((model, index) => ( | ||||||
|  |                           <div | ||||||
|  |                             key={`${modelProvider.id}-chat-${model.key}-${index}`} | ||||||
|  |                             className="flex flex-row items-center space-x-1 text-sm text-black/70 dark:text-white/70 rounded-lg bg-light-secondary dark:bg-dark-secondary px-3 py-1.5" | ||||||
|  |                           > | ||||||
|  |                             <span>{model.name}</span> | ||||||
|  |                             <button | ||||||
|  |                               onClick={() => { | ||||||
|  |                                 handleModelDelete('chat', model.key); | ||||||
|  |                               }} | ||||||
|  |                             > | ||||||
|  |                               <X size={12} /> | ||||||
|  |                             </button> | ||||||
|  |                           </div> | ||||||
|  |                         ))} | ||||||
|  |                       </div> | ||||||
|  |                     )} | ||||||
|  |                   </div> | ||||||
|  |                 </div> | ||||||
|  |               )} | ||||||
|  |               {modelProvider.embeddingModels.length > 0 && ( | ||||||
|  |                 <div className="flex flex-col gap-y-2"> | ||||||
|  |                   <div className="flex flex-row w-full justify-between items-center"> | ||||||
|  |                     <p className="text-xs text-black/70 dark:text-white/70"> | ||||||
|  |                       Embedding models | ||||||
|  |                     </p> | ||||||
|  |                     <AddModel | ||||||
|  |                       providerId={modelProvider.id} | ||||||
|  |                       setProviders={setProviders} | ||||||
|  |                       type="embedding" | ||||||
|  |                     /> | ||||||
|  |                   </div> | ||||||
|  |                   <div className="flex flex-col gap-2"> | ||||||
|  |                     {modelProvider.embeddingModels.some( | ||||||
|  |                       (m) => m.key === 'error', | ||||||
|  |                     ) ? ( | ||||||
|  |                       <div className="flex flex-row items-center gap-2 text-sm text-red-500 dark:text-red-400 rounded-lg bg-red-50 dark:bg-red-950/20 px-3 py-2 border border-red-200 dark:border-red-900/30"> | ||||||
|  |                         <AlertCircle size={16} className="shrink-0" /> | ||||||
|  |                         <span className="break-words"> | ||||||
|  |                           { | ||||||
|  |                             modelProvider.embeddingModels.find( | ||||||
|  |                               (m) => m.key === 'error', | ||||||
|  |                             )?.name | ||||||
|  |                           } | ||||||
|  |                         </span> | ||||||
|  |                       </div> | ||||||
|  |                     ) : ( | ||||||
|  |                       <div className="flex flex-row flex-wrap gap-2"> | ||||||
|  |                         {modelProvider.embeddingModels.map((model, index) => ( | ||||||
|  |                           <div | ||||||
|  |                             key={`${modelProvider.id}-embedding-${model.key}-${index}`} | ||||||
|  |                             className="flex flex-row items-center space-x-1 text-sm text-black/70 dark:text-white/70 rounded-lg bg-light-secondary dark:bg-dark-secondary px-3 py-1.5" | ||||||
|  |                           > | ||||||
|  |                             <span>{model.name}</span> | ||||||
|  |                             <button | ||||||
|  |                               onClick={() => { | ||||||
|  |                                 handleModelDelete('embedding', model.key); | ||||||
|  |                               }} | ||||||
|  |                             > | ||||||
|  |                               <X size={12} /> | ||||||
|  |                             </button> | ||||||
|  |                           </div> | ||||||
|  |                         ))} | ||||||
|  |                       </div> | ||||||
|  |                     )} | ||||||
|  |                   </div> | ||||||
|  |                 </div> | ||||||
|  |               )} | ||||||
|  |             </div> | ||||||
|  |           </motion.div> | ||||||
|  |         )} | ||||||
|  |       </AnimatePresence> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default ModelProvider; | ||||||
							
								
								
									
										44
									
								
								src/components/Settings/Sections/Models/Section.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,44 @@ | |||||||
|  | import React, { useState } from 'react'; | ||||||
|  | import AddProvider from './AddProviderDialog'; | ||||||
|  | import { | ||||||
|  |   ConfigModelProvider, | ||||||
|  |   ModelProviderUISection, | ||||||
|  |   UIConfigField, | ||||||
|  | } from '@/lib/config/types'; | ||||||
|  | import ModelProvider from './ModelProvider'; | ||||||
|  |  | ||||||
|  | const Models = ({ | ||||||
|  |   fields, | ||||||
|  |   values, | ||||||
|  | }: { | ||||||
|  |   fields: ModelProviderUISection[]; | ||||||
|  |   values: ConfigModelProvider[]; | ||||||
|  | }) => { | ||||||
|  |   const [providers, setProviders] = useState<ConfigModelProvider[]>(values); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <div className="flex-1 space-y-6 overflow-y-auto px-6 py-6"> | ||||||
|  |       <div className="flex flex-row justify-between items-center"> | ||||||
|  |         <p className="text-sm text-black/70 dark:text-white/70"> | ||||||
|  |           Manage model provider | ||||||
|  |         </p> | ||||||
|  |         <AddProvider modelProviders={fields} setProviders={setProviders} /> | ||||||
|  |       </div> | ||||||
|  |       <div className="flex flex-col gap-y-4"> | ||||||
|  |         {providers.map((provider) => ( | ||||||
|  |           <ModelProvider | ||||||
|  |             key={`provider-${provider.id}`} | ||||||
|  |             fields={ | ||||||
|  |               (fields.find((f) => f.key === provider.type)?.fields ?? | ||||||
|  |                 []) as UIConfigField[] | ||||||
|  |             } | ||||||
|  |             modelProvider={provider} | ||||||
|  |             setProviders={setProviders} | ||||||
|  |           /> | ||||||
|  |         ))} | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default Models; | ||||||
							
								
								
									
										188
									
								
								src/components/Settings/Sections/Models/UpdateProviderDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,188 @@ | |||||||
|  | import { Dialog, DialogPanel } from '@headlessui/react'; | ||||||
|  | import { Loader2, Pencil } from 'lucide-react'; | ||||||
|  | import { useEffect, useState } from 'react'; | ||||||
|  | import { AnimatePresence, motion } from 'framer-motion'; | ||||||
|  | import { | ||||||
|  |   ConfigModelProvider, | ||||||
|  |   StringUIConfigField, | ||||||
|  |   UIConfigField, | ||||||
|  | } from '@/lib/config/types'; | ||||||
|  | import { toast } from 'sonner'; | ||||||
|  |  | ||||||
|  | const UpdateProvider = ({ | ||||||
|  |   modelProvider, | ||||||
|  |   fields, | ||||||
|  |   setProviders, | ||||||
|  | }: { | ||||||
|  |   fields: UIConfigField[]; | ||||||
|  |   modelProvider: ConfigModelProvider; | ||||||
|  |   setProviders: React.Dispatch<React.SetStateAction<ConfigModelProvider[]>>; | ||||||
|  | }) => { | ||||||
|  |   const [open, setOpen] = useState(false); | ||||||
|  |   const [config, setConfig] = useState<Record<string, any>>({}); | ||||||
|  |   const [name, setName] = useState(modelProvider.name); | ||||||
|  |   const [loading, setLoading] = useState(false); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     const config: Record<string, any> = { | ||||||
|  |       name: modelProvider.name, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     fields.forEach((field) => { | ||||||
|  |       config[field.key] = | ||||||
|  |         modelProvider.config[field.key] || field.default || ''; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     setConfig(config); | ||||||
|  |   }, [fields]); | ||||||
|  |  | ||||||
|  |   const handleSubmit = async (e: React.FormEvent) => { | ||||||
|  |     e.preventDefault(); | ||||||
|  |     setLoading(true); | ||||||
|  |     try { | ||||||
|  |       const res = await fetch(`/api/providers/${modelProvider.id}`, { | ||||||
|  |         method: 'PATCH', | ||||||
|  |         headers: { | ||||||
|  |           'Content-Type': 'application/json', | ||||||
|  |         }, | ||||||
|  |         body: JSON.stringify({ | ||||||
|  |           name: name, | ||||||
|  |           config: config, | ||||||
|  |         }), | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (!res.ok) { | ||||||
|  |         throw new Error('Failed to update provider'); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const data: ConfigModelProvider = (await res.json()).provider; | ||||||
|  |  | ||||||
|  |       setProviders((prev) => { | ||||||
|  |         return prev.map((p) => { | ||||||
|  |           if (p.id === modelProvider.id) { | ||||||
|  |             return data; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           return p; | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       toast.success('Provider updated successfully.'); | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Error updating provider:', error); | ||||||
|  |       toast.error('Failed to update provider.'); | ||||||
|  |     } finally { | ||||||
|  |       setLoading(false); | ||||||
|  |       setOpen(false); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <button | ||||||
|  |         onClick={(e) => { | ||||||
|  |           e.stopPropagation(); | ||||||
|  |           setOpen(true); | ||||||
|  |         }} | ||||||
|  |         className="group p-1.5 rounded-md hover:bg-light-200 hover:dark:bg-dark-200 transition-colors group" | ||||||
|  |       > | ||||||
|  |         <Pencil | ||||||
|  |           size={14} | ||||||
|  |           className="text-black/60 dark:text-white/60 group-hover:text-black group-hover:dark:text-white" | ||||||
|  |         /> | ||||||
|  |       </button> | ||||||
|  |       <AnimatePresence> | ||||||
|  |         {open && ( | ||||||
|  |           <Dialog | ||||||
|  |             static | ||||||
|  |             open={open} | ||||||
|  |             onClose={() => setOpen(false)} | ||||||
|  |             className="relative z-[60]" | ||||||
|  |           > | ||||||
|  |             <motion.div | ||||||
|  |               initial={{ opacity: 0 }} | ||||||
|  |               animate={{ opacity: 1 }} | ||||||
|  |               exit={{ opacity: 0 }} | ||||||
|  |               transition={{ duration: 0.1 }} | ||||||
|  |               className="fixed inset-0 flex w-screen items-center justify-center p-4 bg-black/30 backdrop-blur-sm" | ||||||
|  |             > | ||||||
|  |               <DialogPanel className="w-full mx-4 lg:w-[600px] max-h-[85vh] flex flex-col border bg-light-primary dark:bg-dark-primary border-light-secondary dark:border-dark-secondary rounded-lg"> | ||||||
|  |                 <form onSubmit={handleSubmit} className="flex flex-col flex-1"> | ||||||
|  |                   <div className="px-6 pt-6 pb-4"> | ||||||
|  |                     <h3 className="text-black/90 dark:text-white/90 font-medium"> | ||||||
|  |                       Update provider | ||||||
|  |                     </h3> | ||||||
|  |                   </div> | ||||||
|  |                   <div className="border-t border-light-200 dark:border-dark-200" /> | ||||||
|  |                   <div className="flex-1 overflow-y-auto px-6 py-4"> | ||||||
|  |                     <div className="flex flex-col space-y-4"> | ||||||
|  |                       <div | ||||||
|  |                         key="name" | ||||||
|  |                         className="flex flex-col items-start space-y-2" | ||||||
|  |                       > | ||||||
|  |                         <label className="text-xs text-black/70 dark:text-white/70"> | ||||||
|  |                           Name* | ||||||
|  |                         </label> | ||||||
|  |                         <input | ||||||
|  |                           value={name} | ||||||
|  |                           onChange={(event) => setName(event.target.value)} | ||||||
|  |                           className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 pr-10 text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60" | ||||||
|  |                           placeholder={'Provider Name'} | ||||||
|  |                           type="text" | ||||||
|  |                           required={true} | ||||||
|  |                         /> | ||||||
|  |                       </div> | ||||||
|  |  | ||||||
|  |                       {fields.map((field: UIConfigField) => ( | ||||||
|  |                         <div | ||||||
|  |                           key={field.key} | ||||||
|  |                           className="flex flex-col items-start space-y-2" | ||||||
|  |                         > | ||||||
|  |                           <label className="text-xs text-black/70 dark:text-white/70"> | ||||||
|  |                             {field.name} | ||||||
|  |                             {field.required && '*'} | ||||||
|  |                           </label> | ||||||
|  |                           <input | ||||||
|  |                             value={config[field.key] ?? field.default ?? ''} | ||||||
|  |                             onChange={(event) => | ||||||
|  |                               setConfig((prev) => ({ | ||||||
|  |                                 ...prev, | ||||||
|  |                                 [field.key]: event.target.value, | ||||||
|  |                               })) | ||||||
|  |                             } | ||||||
|  |                             className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 pr-10 text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60" | ||||||
|  |                             placeholder={ | ||||||
|  |                               (field as StringUIConfigField).placeholder | ||||||
|  |                             } | ||||||
|  |                             type="text" | ||||||
|  |                             required={field.required} | ||||||
|  |                           /> | ||||||
|  |                         </div> | ||||||
|  |                       ))} | ||||||
|  |                     </div> | ||||||
|  |                   </div> | ||||||
|  |                   <div className="border-t border-light-200 dark:border-dark-200" /> | ||||||
|  |                   <div className="px-6 py-4 flex justify-end"> | ||||||
|  |                     <button | ||||||
|  |                       type="submit" | ||||||
|  |                       disabled={loading} | ||||||
|  |                       className="px-4 py-2 rounded-lg text-sm bg-sky-500 text-white font-medium disabled:opacity-85 hover:opacity-85 active:scale-95 transition duration-200" | ||||||
|  |                     > | ||||||
|  |                       {loading ? ( | ||||||
|  |                         <Loader2 className="animate-spin" size={16} /> | ||||||
|  |                       ) : ( | ||||||
|  |                         'Update Provider' | ||||||
|  |                       )} | ||||||
|  |                     </button> | ||||||
|  |                   </div> | ||||||
|  |                 </form> | ||||||
|  |               </DialogPanel> | ||||||
|  |             </motion.div> | ||||||
|  |           </Dialog> | ||||||
|  |         )} | ||||||
|  |       </AnimatePresence> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default UpdateProvider; | ||||||
							
								
								
									
										29
									
								
								src/components/Settings/Sections/Search.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,29 @@ | |||||||
|  | import { UIConfigField } from '@/lib/config/types'; | ||||||
|  | import SettingsField from '../SettingsField'; | ||||||
|  |  | ||||||
|  | const Search = ({ | ||||||
|  |   fields, | ||||||
|  |   values, | ||||||
|  | }: { | ||||||
|  |   fields: UIConfigField[]; | ||||||
|  |   values: Record<string, any>; | ||||||
|  | }) => { | ||||||
|  |   return ( | ||||||
|  |     <div className="flex-1 space-y-6 overflow-y-auto px-6 py-6"> | ||||||
|  |       {fields.map((field) => ( | ||||||
|  |         <SettingsField | ||||||
|  |           key={field.key} | ||||||
|  |           field={field} | ||||||
|  |           value={ | ||||||
|  |             (field.scope === 'client' | ||||||
|  |               ? localStorage.getItem(field.key) | ||||||
|  |               : values[field.key]) ?? field.default | ||||||
|  |           } | ||||||
|  |           dataAdd="search" | ||||||
|  |         /> | ||||||
|  |       ))} | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default Search; | ||||||
							
								
								
									
										24
									
								
								src/components/Settings/SettingsButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,24 @@ | |||||||
|  | import { Settings } from 'lucide-react'; | ||||||
|  | import { useState } from 'react'; | ||||||
|  | import SettingsDialogue from './SettingsDialogue'; | ||||||
|  | import { AnimatePresence } from 'framer-motion'; | ||||||
|  |  | ||||||
|  | const SettingsButton = () => { | ||||||
|  |   const [isOpen, setIsOpen] = useState<boolean>(false); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <div | ||||||
|  |         className="p-2.5 rounded-full bg-light-200 text-black/70 dark:bg-dark-200 dark:text-white/70 hover:opacity-70 hover:scale-105 transition duration-200 cursor-pointer active:scale-95" | ||||||
|  |         onClick={() => setIsOpen(true)} | ||||||
|  |       > | ||||||
|  |         <Settings size={19} className="cursor-pointer" /> | ||||||
|  |       </div> | ||||||
|  |       <AnimatePresence> | ||||||
|  |         {isOpen && <SettingsDialogue isOpen={isOpen} setIsOpen={setIsOpen} />} | ||||||
|  |       </AnimatePresence> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default SettingsButton; | ||||||
							
								
								
									
										152
									
								
								src/components/Settings/SettingsDialogue.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,152 @@ | |||||||
|  | import { Dialog, DialogPanel } from '@headlessui/react'; | ||||||
|  | import { BrainCog, ChevronLeft, Search, Settings } from 'lucide-react'; | ||||||
|  | import General from './Sections/General'; | ||||||
|  | import { motion } from 'framer-motion'; | ||||||
|  | import { useEffect, useState } from 'react'; | ||||||
|  | import { toast } from 'sonner'; | ||||||
|  | import Loader from '../ui/Loader'; | ||||||
|  | import { cn } from '@/lib/utils'; | ||||||
|  | import Models from './Sections/Models/Section'; | ||||||
|  | import SearchSection from './Sections/Search'; | ||||||
|  |  | ||||||
|  | const sections = [ | ||||||
|  |   { | ||||||
|  |     name: 'General', | ||||||
|  |     description: 'Adjust common settings.', | ||||||
|  |     icon: Settings, | ||||||
|  |     component: General, | ||||||
|  |     dataAdd: 'general', | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     name: 'Models', | ||||||
|  |     description: 'Configure model settings.', | ||||||
|  |     icon: BrainCog, | ||||||
|  |     component: Models, | ||||||
|  |     dataAdd: 'modelProviders', | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     name: 'Search', | ||||||
|  |     description: 'Manage search settings.', | ||||||
|  |     icon: Search, | ||||||
|  |     component: SearchSection, | ||||||
|  |     dataAdd: 'search', | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | const SettingsDialogue = ({ | ||||||
|  |   isOpen, | ||||||
|  |   setIsOpen, | ||||||
|  | }: { | ||||||
|  |   isOpen: boolean; | ||||||
|  |   setIsOpen: (active: boolean) => void; | ||||||
|  | }) => { | ||||||
|  |   const [isLoading, setIsLoading] = useState(true); | ||||||
|  |   const [config, setConfig] = useState<any>(null); | ||||||
|  |   const [activeSection, setActiveSection] = useState(sections[0]); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (isOpen) { | ||||||
|  |       const fetchConfig = async () => { | ||||||
|  |         try { | ||||||
|  |           const res = await fetch('/api/config', { | ||||||
|  |             method: 'GET', | ||||||
|  |             headers: { | ||||||
|  |               'Content-Type': 'application/json', | ||||||
|  |             }, | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           const data = await res.json(); | ||||||
|  |  | ||||||
|  |           setConfig(data); | ||||||
|  |         } catch (error) { | ||||||
|  |           console.error('Error fetching config:', error); | ||||||
|  |           toast.error('Failed to load configuration.'); | ||||||
|  |         } finally { | ||||||
|  |           setIsLoading(false); | ||||||
|  |         } | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       fetchConfig(); | ||||||
|  |     } | ||||||
|  |   }, [isOpen]); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Dialog | ||||||
|  |       open={isOpen} | ||||||
|  |       onClose={() => setIsOpen(false)} | ||||||
|  |       className="relative z-50" | ||||||
|  |     > | ||||||
|  |       <motion.div | ||||||
|  |         initial={{ opacity: 0 }} | ||||||
|  |         animate={{ opacity: 1 }} | ||||||
|  |         exit={{ opacity: 0 }} | ||||||
|  |         transition={{ duration: 0.1 }} | ||||||
|  |         className="fixed inset-0 flex w-screen items-center justify-center p-4 bg-black/30 backdrop-blur-sm h-screen" | ||||||
|  |       > | ||||||
|  |         <DialogPanel className="space-y-4 border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary backdrop-blur-lg rounded-xl h-[calc(100vh-2%)] w-[calc(100vw-2%)] md:h-[calc(100vh-7%)] md:w-[calc(100vw-7%)] lg:h-[calc(100vh-20%)] lg:w-[calc(100vw-30%)]"> | ||||||
|  |           {isLoading ? ( | ||||||
|  |             <div className="flex items-center justify-center h-full w-full"> | ||||||
|  |               <Loader /> | ||||||
|  |             </div> | ||||||
|  |           ) : ( | ||||||
|  |             <div className="flex flex-1 inset-0 h-full"> | ||||||
|  |               <div className="w-[240px] border-r border-white-200 dark:border-dark-200 h-full px-3 pt-3 flex flex-col"> | ||||||
|  |                 <button | ||||||
|  |                   onClick={() => setIsOpen(false)} | ||||||
|  |                   className="group flex flex-row items-center hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg" | ||||||
|  |                 > | ||||||
|  |                   <ChevronLeft | ||||||
|  |                     size={18} | ||||||
|  |                     className="text-black/50 dark:text-white/50 group-hover:text-black/70 group-hover:dark:text-white/70" | ||||||
|  |                   /> | ||||||
|  |                   <p className="text-black/50 dark:text-white/50 group-hover:text-black/70 group-hover:dark:text-white/70 text-[14px]"> | ||||||
|  |                     Back | ||||||
|  |                   </p> | ||||||
|  |                 </button> | ||||||
|  |                 <div className="flex flex-col items-start space-y-1 mt-8"> | ||||||
|  |                   {sections.map((section) => ( | ||||||
|  |                     <button | ||||||
|  |                       key={section.dataAdd} | ||||||
|  |                       className={cn( | ||||||
|  |                         `flex flex-row items-center space-x-2 px-2 py-1.5 rounded-lg w-full text-sm hover:bg-light-200 hover:dark:bg-dark-200 transition duration-200 active:scale-95`, | ||||||
|  |                         activeSection.name === section.name | ||||||
|  |                           ? 'bg-light-200 dark:bg-dark-200 text-black/90 dark:text-white/90' | ||||||
|  |                           : ' text-black/70 dark:text-white/70', | ||||||
|  |                       )} | ||||||
|  |                       onClick={() => setActiveSection(section)} | ||||||
|  |                     > | ||||||
|  |                       <section.icon size={17} /> | ||||||
|  |                       <p>{section.name}</p> | ||||||
|  |                     </button> | ||||||
|  |                   ))} | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |               <div className="w-full"> | ||||||
|  |                 {activeSection.component && ( | ||||||
|  |                   <div className="flex h-full flex-col"> | ||||||
|  |                     <div className="border-b border-light-200/60 px-6 pb-6 pt-8 dark:border-dark-200/60"> | ||||||
|  |                       <div className="flex flex-col"> | ||||||
|  |                         <h4 className="font-medium text-black dark:text-white"> | ||||||
|  |                           {activeSection.name} | ||||||
|  |                         </h4> | ||||||
|  |                         <p className="text-xs text-black/50 dark:text-white/50"> | ||||||
|  |                           {activeSection.description} | ||||||
|  |                         </p> | ||||||
|  |                       </div> | ||||||
|  |                     </div> | ||||||
|  |                     <activeSection.component | ||||||
|  |                       fields={config.fields[activeSection.dataAdd]} | ||||||
|  |                       values={config.values[activeSection.dataAdd]} | ||||||
|  |                     /> | ||||||
|  |                   </div> | ||||||
|  |                 )} | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |           )} | ||||||
|  |         </DialogPanel> | ||||||
|  |       </motion.div> | ||||||
|  |     </Dialog> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default SettingsDialogue; | ||||||