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: | ||||
|     branches: | ||||
|       - master | ||||
|       - canary | ||||
|   release: | ||||
|     types: [published] | ||||
|  | ||||
| @@ -43,6 +44,19 @@ jobs: | ||||
|             -t itzcrazykns1337/${IMAGE_NAME}:amd64 \ | ||||
|             --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 | ||||
|         if: github.event_name == 'release' | ||||
|         run: | | ||||
| @@ -91,6 +105,19 @@ jobs: | ||||
|             -t itzcrazykns1337/${IMAGE_NAME}:arm64 \ | ||||
|             --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 | ||||
|         if: github.event_name == 'release' | ||||
|         run: | | ||||
| @@ -128,6 +155,15 @@ jobs: | ||||
|             --amend itzcrazykns1337/${IMAGE_NAME}:arm64 | ||||
|           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 | ||||
|         if: github.event_name == 'release' | ||||
|         run: | | ||||
|   | ||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -37,3 +37,5 @@ Thumbs.db | ||||
| # Db | ||||
| db.sqlite | ||||
| /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. | ||||
| 2. Rename it to `config.toml` and fill in the necessary configuration fields. | ||||
| 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. | ||||
|  | ||||
| **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/> | ||||
|  | ||||
| [](https://discord.gg/26aArMy8tT) | ||||
| [](https://discord.gg/26aArMy8tT) | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -29,6 +29,7 @@ | ||||
|   - [Getting Started with Docker (Recommended)](#getting-started-with-docker-recommended) | ||||
|   - [Non-Docker Installation](#non-docker-installation) | ||||
|   - [Ollama Connection Errors](#ollama-connection-errors) | ||||
|   - [Lemonade Connection Errors](#lemonade-connection-errors) | ||||
| - [Using as a Search Engine](#using-as-a-search-engine) | ||||
| - [Using Perplexica's API](#using-perplexicas-api) | ||||
| - [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 | ||||
|  | ||||
| - **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:** | ||||
|   - **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. | ||||
| @@ -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: | ||||
|  | ||||
|    - `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**. | ||||
|    - `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**. | ||||
|    - `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. | ||||
|  | ||||
| @@ -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. | ||||
| 3. After populating the configuration run `npm i`. | ||||
| 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. | ||||
|  | ||||
| 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: | ||||
|  | ||||
| @@ -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:** | ||||
|  | ||||
|    - 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. | ||||
|  | ||||
| #### 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 | ||||
|  | ||||
| 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://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 | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
| @@ -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 src ./src | ||||
| COPY public ./public | ||||
| COPY drizzle ./drizzle | ||||
|  | ||||
| RUN mkdir -p /home/perplexica/data | ||||
| 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 | ||||
|  | ||||
| @@ -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/data ./data | ||||
| COPY drizzle ./drizzle | ||||
|  | ||||
| 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: . | ||||
|       dockerfile: app.dockerfile | ||||
|     environment: | ||||
|       - DOCKER=true | ||||
|       - SEARXNG_API_URL=http://searxng:8080 | ||||
|       - DATA_DIR=/home/perplexica | ||||
|     ports: | ||||
|       - 3000:3000 | ||||
|     networks: | ||||
| @@ -23,7 +25,6 @@ services: | ||||
|     volumes: | ||||
|       - backend-dbstore:/home/perplexica/data | ||||
|       - uploads:/home/perplexica/uploads | ||||
|       - ./config.toml:/home/perplexica/config.toml | ||||
|     restart: unless-stopped | ||||
|  | ||||
| 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. | ||||
| 4. After populating the configuration run `npm i`. | ||||
| 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 path from 'path'; | ||||
|  | ||||
| export default defineConfig({ | ||||
|   dialect: 'sqlite', | ||||
|   schema: './src/lib/db/schema.ts', | ||||
|   out: './drizzle', | ||||
|   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", | ||||
|   "version": "1.10.2", | ||||
|   "version": "1.11.0-rc3", | ||||
|   "license": "MIT", | ||||
|   "author": "ItzCrazyKns", | ||||
|   "scripts": { | ||||
|     "dev": "next dev", | ||||
|     "build": "npm run db:push && next build", | ||||
|     "build": "next build", | ||||
|     "start": "next start", | ||||
|     "lint": "next lint", | ||||
|     "format:write": "prettier . --write", | ||||
|     "db:push": "drizzle-kit push" | ||||
|     "format:write": "prettier . --write" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@headlessui/react": "^2.2.0", | ||||
|     "@headlessui/tailwindcss": "^0.2.2", | ||||
|     "@iarna/toml": "^2.2.5", | ||||
|     "@icons-pack/react-simple-icons": "^12.3.0", | ||||
|     "@langchain/anthropic": "^0.3.15", | ||||
|     "@langchain/community": "^0.3.36", | ||||
|     "@langchain/core": "^0.3.42", | ||||
|     "@langchain/google-genai": "^0.1.12", | ||||
|     "@langchain/openai": "^0.0.25", | ||||
|     "@langchain/anthropic": "^0.3.24", | ||||
|     "@langchain/community": "^0.3.49", | ||||
|     "@langchain/core": "^0.3.66", | ||||
|     "@langchain/google-genai": "^0.2.15", | ||||
|     "@langchain/groq": "^0.2.3", | ||||
|     "@langchain/ollama": "^0.2.3", | ||||
|     "@langchain/openai": "^0.6.2", | ||||
|     "@langchain/textsplitters": "^0.1.0", | ||||
|     "@tailwindcss/typography": "^0.5.12", | ||||
|     "@xenova/transformers": "^2.17.2", | ||||
| @@ -27,11 +29,13 @@ | ||||
|     "better-sqlite3": "^11.9.1", | ||||
|     "clsx": "^2.1.0", | ||||
|     "compute-cosine-similarity": "^1.1.0", | ||||
|     "compute-dot": "^1.1.0", | ||||
|     "drizzle-orm": "^0.40.1", | ||||
|     "framer-motion": "^12.23.24", | ||||
|     "html-to-text": "^9.0.5", | ||||
|     "langchain": "^0.1.30", | ||||
|     "jspdf": "^3.0.1", | ||||
|     "langchain": "^0.3.30", | ||||
|     "lucide-react": "^0.363.0", | ||||
|     "mammoth": "^1.9.1", | ||||
|     "markdown-to-jsx": "^7.7.2", | ||||
|     "next": "^15.2.2", | ||||
|     "next-themes": "^0.3.0", | ||||
| @@ -49,6 +53,7 @@ | ||||
|   "devDependencies": { | ||||
|     "@types/better-sqlite3": "^7.6.12", | ||||
|     "@types/html-to-text": "^9.0.4", | ||||
|     "@types/jspdf": "^2.0.0", | ||||
|     "@types/node": "^20", | ||||
|     "@types/pdf-parse": "^1.1.4", | ||||
|     "@types/react": "^18", | ||||
| @@ -60,6 +65,7 @@ | ||||
|     "postcss": "^8", | ||||
|     "prettier": "^3.2.5", | ||||
|     "tailwindcss": "^3.3.0", | ||||
|     "ts-node": "^10.9.2", | ||||
|     "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 { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; | ||||
| import { EventEmitter } from 'stream'; | ||||
| import { | ||||
|   chatModelProviders, | ||||
|   embeddingModelProviders, | ||||
|   getAvailableChatModelProviders, | ||||
|   getAvailableEmbeddingModelProviders, | ||||
| } from '@/lib/providers'; | ||||
| import db from '@/lib/db'; | ||||
| import { chats, messages as messagesSchema } from '@/lib/db/schema'; | ||||
| import { and, eq, gt } from 'drizzle-orm'; | ||||
| 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 { z } from 'zod'; | ||||
| import ModelRegistry from '@/lib/models/registry'; | ||||
| import { ModelWithProvider } from '@/lib/models/types'; | ||||
|  | ||||
| export const runtime = 'nodejs'; | ||||
| export const dynamic = 'force-dynamic'; | ||||
|  | ||||
| type Message = { | ||||
|   messageId: string; | ||||
|   chatId: string; | ||||
|   content: string; | ||||
| }; | ||||
| const messageSchema = z.object({ | ||||
|   messageId: z.string().min(1, 'Message ID is required'), | ||||
|   chatId: z.string().min(1, 'Chat ID is required'), | ||||
|   content: z.string().min(1, 'Message content is required'), | ||||
| }); | ||||
|  | ||||
| type ChatModel = { | ||||
|   provider: string; | ||||
|   name: string; | ||||
| }; | ||||
| const chatModelSchema: z.ZodType<ModelWithProvider> = z.object({ | ||||
|   providerId: z.string({ | ||||
|     errorMap: () => ({ | ||||
|       message: 'Chat model provider id must be provided', | ||||
|     }), | ||||
|   }), | ||||
|   key: z.string({ | ||||
|     errorMap: () => ({ | ||||
|       message: 'Chat model key must be provided', | ||||
|     }), | ||||
|   }), | ||||
| }); | ||||
|  | ||||
| type EmbeddingModel = { | ||||
|   provider: string; | ||||
|   name: string; | ||||
| }; | ||||
| const embeddingModelSchema: z.ZodType<ModelWithProvider> = z.object({ | ||||
|   providerId: z.string({ | ||||
|     errorMap: () => ({ | ||||
|       message: 'Embedding model provider id must be provided', | ||||
|     }), | ||||
|   }), | ||||
|   key: z.string({ | ||||
|     errorMap: () => ({ | ||||
|       message: 'Embedding model key must be provided', | ||||
|     }), | ||||
|   }), | ||||
| }); | ||||
|  | ||||
| type Body = { | ||||
|   message: Message; | ||||
|   optimizationMode: 'speed' | 'balanced' | 'quality'; | ||||
|   focusMode: string; | ||||
|   history: Array<[string, string]>; | ||||
|   files: Array<string>; | ||||
|   chatModel: ChatModel; | ||||
|   embeddingModel: EmbeddingModel; | ||||
|   systemInstructions: string; | ||||
| const bodySchema = z.object({ | ||||
|   message: messageSchema, | ||||
|   optimizationMode: z.enum(['speed', 'balanced', 'quality'], { | ||||
|     errorMap: () => ({ | ||||
|       message: 'Optimization mode must be one of: speed, balanced, quality', | ||||
|     }), | ||||
|   }), | ||||
|   focusMode: z.string().min(1, 'Focus mode is required'), | ||||
|   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 ( | ||||
|   stream: EventEmitter, | ||||
|   writer: WritableStreamDefaultWriter, | ||||
|   encoder: TextEncoder, | ||||
|   aiMessageId: string, | ||||
|   chatId: string, | ||||
| ) => { | ||||
|   let recievedMessage = ''; | ||||
|   let sources: any[] = []; | ||||
|   const aiMessageId = crypto.randomBytes(7).toString('hex'); | ||||
|  | ||||
|   stream.on('data', (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', () => { | ||||
| @@ -95,7 +143,6 @@ const handleEmitterEvents = async ( | ||||
|       encoder.encode( | ||||
|         JSON.stringify({ | ||||
|           type: 'messageEnd', | ||||
|           messageId: aiMessageId, | ||||
|         }) + '\n', | ||||
|       ), | ||||
|     ); | ||||
| @@ -107,10 +154,7 @@ const handleEmitterEvents = async ( | ||||
|         chatId: chatId, | ||||
|         messageId: aiMessageId, | ||||
|         role: 'assistant', | ||||
|         metadata: JSON.stringify({ | ||||
|           createdAt: new Date(), | ||||
|           ...(sources && sources.length > 0 && { sources }), | ||||
|         }), | ||||
|         createdAt: new Date().toString(), | ||||
|       }) | ||||
|       .execute(); | ||||
|   }); | ||||
| @@ -138,6 +182,8 @@ const handleHistorySave = async ( | ||||
|     where: eq(chats.id, message.chatId), | ||||
|   }); | ||||
|  | ||||
|   const fileData = files.map(getFileDetails); | ||||
|  | ||||
|   if (!chat) { | ||||
|     await db | ||||
|       .insert(chats) | ||||
| @@ -146,9 +192,15 @@ const handleHistorySave = async ( | ||||
|         title: message.content, | ||||
|         createdAt: new Date().toString(), | ||||
|         focusMode: focusMode, | ||||
|         files: files.map(getFileDetails), | ||||
|         files: fileData, | ||||
|       }) | ||||
|       .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({ | ||||
| @@ -163,9 +215,7 @@ const handleHistorySave = async ( | ||||
|         chatId: message.chatId, | ||||
|         messageId: humanMessageId, | ||||
|         role: 'user', | ||||
|         metadata: JSON.stringify({ | ||||
|           createdAt: new Date(), | ||||
|         }), | ||||
|         createdAt: new Date().toString(), | ||||
|       }) | ||||
|       .execute(); | ||||
|   } else { | ||||
| @@ -183,7 +233,17 @@ const handleHistorySave = async ( | ||||
|  | ||||
| export const POST = async (req: Request) => { | ||||
|   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; | ||||
|  | ||||
|     if (message.content === '') { | ||||
| @@ -195,59 +255,18 @@ export const POST = async (req: Request) => { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     const [chatModelProviders, embeddingModelProviders] = await Promise.all([ | ||||
|       getAvailableChatModelProviders(), | ||||
|       getAvailableEmbeddingModelProviders(), | ||||
|     const registry = new ModelRegistry(); | ||||
|  | ||||
|     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 = | ||||
|       message.messageId ?? crypto.randomBytes(7).toString('hex'); | ||||
|     const aiMessageId = crypto.randomBytes(7).toString('hex'); | ||||
|  | ||||
|     const history: BaseMessage[] = body.history.map((msg) => { | ||||
|       if (msg[0] === 'human') { | ||||
| @@ -279,14 +298,14 @@ export const POST = async (req: Request) => { | ||||
|       embedding, | ||||
|       body.optimizationMode, | ||||
|       body.files, | ||||
|       body.systemInstructions, | ||||
|       body.systemInstructions as string, | ||||
|     ); | ||||
|  | ||||
|     const responseStream = new TransformStream(); | ||||
|     const writer = responseStream.writable.getWriter(); | ||||
|     const encoder = new TextEncoder(); | ||||
|  | ||||
|     handleEmitterEvents(stream, writer, encoder, aiMessageId, message.chatId); | ||||
|     handleEmitterEvents(stream, writer, encoder, message.chatId); | ||||
|     handleHistorySave(message, humanMessageId, body.focusMode, body.files); | ||||
|  | ||||
|     return new Response(responseStream.readable, { | ||||
|   | ||||
| @@ -1,113 +1,76 @@ | ||||
| import { | ||||
|   getAnthropicApiKey, | ||||
|   getCustomOpenaiApiKey, | ||||
|   getCustomOpenaiApiUrl, | ||||
|   getCustomOpenaiModelName, | ||||
|   getGeminiApiKey, | ||||
|   getGroqApiKey, | ||||
|   getOllamaApiEndpoint, | ||||
|   getOpenaiApiKey, | ||||
|   getDeepseekApiKey, | ||||
|   updateConfig, | ||||
| } from '@/lib/config'; | ||||
| import { | ||||
|   getAvailableChatModelProviders, | ||||
|   getAvailableEmbeddingModelProviders, | ||||
| } from '@/lib/providers'; | ||||
| import configManager from '@/lib/config'; | ||||
| import ModelRegistry from '@/lib/models/registry'; | ||||
| import { NextRequest, NextResponse } from 'next/server'; | ||||
| import { ConfigModelProvider } from '@/lib/config/types'; | ||||
|  | ||||
| export const GET = async (req: Request) => { | ||||
| type SaveConfigBody = { | ||||
|   key: string; | ||||
|   value: string; | ||||
| }; | ||||
|  | ||||
| export const GET = async (req: NextRequest) => { | ||||
|   try { | ||||
|     const config: Record<string, any> = {}; | ||||
|     const values = configManager.getCurrentConfig(); | ||||
|     const fields = configManager.getUIConfigSections(); | ||||
|  | ||||
|     const [chatModelProviders, embeddingModelProviders] = await Promise.all([ | ||||
|       getAvailableChatModelProviders(), | ||||
|       getAvailableEmbeddingModelProviders(), | ||||
|     ]); | ||||
|     const modelRegistry = new ModelRegistry(); | ||||
|     const modelProviders = await modelRegistry.getActiveProviders(); | ||||
|  | ||||
|     config['chatModelProviders'] = {}; | ||||
|     config['embeddingModelProviders'] = {}; | ||||
|     values.modelProviders = values.modelProviders.map( | ||||
|       (mp: ConfigModelProvider) => { | ||||
|         const activeProvider = modelProviders.find((p) => p.id === mp.id); | ||||
|  | ||||
|     for (const provider in chatModelProviders) { | ||||
|       config['chatModelProviders'][provider] = Object.keys( | ||||
|         chatModelProviders[provider], | ||||
|       ).map((model) => { | ||||
|         return { | ||||
|           name: model, | ||||
|           displayName: chatModelProviders[provider][model].displayName, | ||||
|           ...mp, | ||||
|           chatModels: activeProvider?.chatModels ?? mp.chatModels, | ||||
|           embeddingModels: | ||||
|             activeProvider?.embeddingModels ?? mp.embeddingModels, | ||||
|         }; | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
|     return NextResponse.json({ | ||||
|       values, | ||||
|       fields, | ||||
|     }); | ||||
|     } | ||||
|  | ||||
|     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); | ||||
|     console.error('Error in getting config: ', err); | ||||
|     return Response.json( | ||||
|       { message: 'An error occurred while getting config' }, | ||||
|       { message: 'An error has occurred.' }, | ||||
|       { status: 500 }, | ||||
|     ); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export const POST = async (req: Request) => { | ||||
| export const POST = async (req: NextRequest) => { | ||||
|   try { | ||||
|     const config = await req.json(); | ||||
|     const body: SaveConfigBody = await req.json(); | ||||
|  | ||||
|     const updatedConfig = { | ||||
|       MODELS: { | ||||
|         OPENAI: { | ||||
|           API_KEY: config.openaiApiKey, | ||||
|         }, | ||||
|         GROQ: { | ||||
|           API_KEY: config.groqApiKey, | ||||
|         }, | ||||
|         ANTHROPIC: { | ||||
|           API_KEY: config.anthropicApiKey, | ||||
|         }, | ||||
|         GEMINI: { | ||||
|           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 Response.json({ message: 'Config updated' }, { status: 200 }); | ||||
|   } catch (err) { | ||||
|     console.error('An error occurred while updating config:', err); | ||||
|     if (!body.key || !body.value) { | ||||
|       return Response.json( | ||||
|       { message: 'An error occurred while updating config' }, | ||||
|         { | ||||
|           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 }, | ||||
|     ); | ||||
|   } | ||||
|   | ||||
							
								
								
									
										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'; | ||||
|  | ||||
| const articleWebsites = [ | ||||
|   'yahoo.com', | ||||
|   'www.exchangewire.com', | ||||
|   'businessinsider.com', | ||||
|   /* 'wired.com', | ||||
|   'mashable.com', | ||||
|   'theverge.com', | ||||
|   'gizmodo.com', | ||||
|   'cnet.com', | ||||
|   'venturebeat.com', */ | ||||
| ]; | ||||
| const websitesForTopic = { | ||||
|   tech: { | ||||
|     query: ['technology news', 'latest tech', 'AI', 'science and innovation'], | ||||
|     links: ['techcrunch.com', 'wired.com', 'theverge.com'], | ||||
|   }, | ||||
|   finance: { | ||||
|     query: ['finance news', 'economy', 'stock market', 'investing'], | ||||
|     links: ['bloomberg.com', 'cnbc.com', 'marketwatch.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) => { | ||||
|   try { | ||||
|     const data = ( | ||||
|       await Promise.all([ | ||||
|         ...new Array(articleWebsites.length * topics.length) | ||||
|           .fill(0) | ||||
|           .map(async (_, i) => { | ||||
|     const params = new URL(req.url).searchParams; | ||||
|  | ||||
|     const mode: 'normal' | 'preview' = | ||||
|       (params.get('mode') as 'normal' | 'preview') || 'normal'; | ||||
|     const topic: Topic = (params.get('topic') as Topic) || 'tech'; | ||||
|  | ||||
|     const selectedTopic = websitesForTopic[topic]; | ||||
|  | ||||
|     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'], | ||||
|                   pageno: 1, | ||||
|                   language: 'en', | ||||
|                 }) | ||||
|               ).results; | ||||
|             }), | ||||
|           ), | ||||
|         ) | ||||
|       ) | ||||
|         .flat() | ||||
|         .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:${articleWebsites[i % articleWebsites.length]} ${ | ||||
|                   topics[i % topics.length] | ||||
|                 }`, | ||||
|           `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; | ||||
|           }), | ||||
|       ]) | ||||
|     ) | ||||
|       .map((result) => result) | ||||
|       .flat() | ||||
|       .sort(() => Math.random() - 0.5); | ||||
|     } | ||||
|  | ||||
|     return Response.json( | ||||
|       { | ||||
|   | ||||
| @@ -1,23 +1,12 @@ | ||||
| import handleImageSearch from '@/lib/chains/imageSearchAgent'; | ||||
| import { | ||||
|   getCustomOpenaiApiKey, | ||||
|   getCustomOpenaiApiUrl, | ||||
|   getCustomOpenaiModelName, | ||||
| } from '@/lib/config'; | ||||
| import { getAvailableChatModelProviders } from '@/lib/providers'; | ||||
| import { BaseChatModel } from '@langchain/core/language_models/chat_models'; | ||||
| import ModelRegistry from '@/lib/models/registry'; | ||||
| import { ModelWithProvider } from '@/lib/models/types'; | ||||
| import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; | ||||
| import { ChatOpenAI } from '@langchain/openai'; | ||||
|  | ||||
| interface ChatModel { | ||||
|   provider: string; | ||||
|   model: string; | ||||
| } | ||||
|  | ||||
| interface ImageSearchBody { | ||||
|   query: string; | ||||
|   chatHistory: any[]; | ||||
|   chatModel?: ChatModel; | ||||
|   chatModel: ModelWithProvider; | ||||
| } | ||||
|  | ||||
| export const POST = async (req: Request) => { | ||||
| @@ -34,35 +23,12 @@ export const POST = async (req: Request) => { | ||||
|       }) | ||||
|       .filter((msg) => msg !== undefined) as BaseMessage[]; | ||||
|  | ||||
|     const chatModelProviders = await getAvailableChatModelProviders(); | ||||
|     const registry = new ModelRegistry(); | ||||
|  | ||||
|     const chatModelProvider = | ||||
|       chatModelProviders[ | ||||
|         body.chatModel?.provider || Object.keys(chatModelProviders)[0] | ||||
|       ]; | ||||
|     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 llm = await registry.loadChatModel( | ||||
|       body.chatModel.providerId, | ||||
|       body.chatModel.key, | ||||
|     ); | ||||
|  | ||||
|     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 { MetaSearchAgentType } from '@/lib/search/metaSearchAgent'; | ||||
| import { | ||||
|   getCustomOpenaiApiKey, | ||||
|   getCustomOpenaiApiUrl, | ||||
|   getCustomOpenaiModelName, | ||||
| } from '@/lib/config'; | ||||
| import { searchHandlers } from '@/lib/search'; | ||||
|  | ||||
| interface chatModel { | ||||
|   provider: string; | ||||
|   name: string; | ||||
|   customOpenAIKey?: string; | ||||
|   customOpenAIBaseURL?: string; | ||||
| } | ||||
|  | ||||
| interface embeddingModel { | ||||
|   provider: string; | ||||
|   name: string; | ||||
| } | ||||
| import ModelRegistry from '@/lib/models/registry'; | ||||
| import { ModelWithProvider } from '@/lib/models/types'; | ||||
|  | ||||
| interface ChatRequestBody { | ||||
|   optimizationMode: 'speed' | 'balanced'; | ||||
|   focusMode: string; | ||||
|   chatModel?: chatModel; | ||||
|   embeddingModel?: embeddingModel; | ||||
|   chatModel: ModelWithProvider; | ||||
|   embeddingModel: ModelWithProvider; | ||||
|   query: string; | ||||
|   history: Array<[string, string]>; | ||||
|   stream?: boolean; | ||||
| @@ -58,61 +36,16 @@ export const POST = async (req: Request) => { | ||||
|         : new AIMessage({ content: msg[1] }); | ||||
|     }); | ||||
|  | ||||
|     const [chatModelProviders, embeddingModelProviders] = await Promise.all([ | ||||
|       getAvailableChatModelProviders(), | ||||
|       getAvailableEmbeddingModelProviders(), | ||||
|     const registry = new ModelRegistry(); | ||||
|  | ||||
|     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]; | ||||
|  | ||||
|     if (!searchHandler) { | ||||
|   | ||||
| @@ -1,22 +1,12 @@ | ||||
| import generateSuggestions from '@/lib/chains/suggestionGeneratorAgent'; | ||||
| import { | ||||
|   getCustomOpenaiApiKey, | ||||
|   getCustomOpenaiApiUrl, | ||||
|   getCustomOpenaiModelName, | ||||
| } from '@/lib/config'; | ||||
| import { getAvailableChatModelProviders } from '@/lib/providers'; | ||||
| import ModelRegistry from '@/lib/models/registry'; | ||||
| import { ModelWithProvider } from '@/lib/models/types'; | ||||
| import { BaseChatModel } from '@langchain/core/language_models/chat_models'; | ||||
| import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; | ||||
| import { ChatOpenAI } from '@langchain/openai'; | ||||
|  | ||||
| interface ChatModel { | ||||
|   provider: string; | ||||
|   model: string; | ||||
| } | ||||
|  | ||||
| interface SuggestionsGenerationBody { | ||||
|   chatHistory: any[]; | ||||
|   chatModel?: ChatModel; | ||||
|   chatModel: ModelWithProvider; | ||||
| } | ||||
|  | ||||
| export const POST = async (req: Request) => { | ||||
| @@ -33,35 +23,12 @@ export const POST = async (req: Request) => { | ||||
|       }) | ||||
|       .filter((msg) => msg !== undefined) as BaseMessage[]; | ||||
|  | ||||
|     const chatModelProviders = await getAvailableChatModelProviders(); | ||||
|     const registry = new ModelRegistry(); | ||||
|  | ||||
|     const chatModelProvider = | ||||
|       chatModelProviders[ | ||||
|         body.chatModel?.provider || Object.keys(chatModelProviders)[0] | ||||
|       ]; | ||||
|     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 llm = await registry.loadChatModel( | ||||
|       body.chatModel.providerId, | ||||
|       body.chatModel.key, | ||||
|     ); | ||||
|  | ||||
|     const suggestions = await generateSuggestions( | ||||
|       { | ||||
|   | ||||
| @@ -2,11 +2,11 @@ import { NextResponse } from 'next/server'; | ||||
| import fs from 'fs'; | ||||
| import path from 'path'; | ||||
| import crypto from 'crypto'; | ||||
| import { getAvailableEmbeddingModelProviders } from '@/lib/providers'; | ||||
| import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'; | ||||
| import { DocxLoader } from '@langchain/community/document_loaders/fs/docx'; | ||||
| import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'; | ||||
| import { Document } from 'langchain/document'; | ||||
| import ModelRegistry from '@/lib/models/registry'; | ||||
|  | ||||
| interface FileRes { | ||||
|   fileName: string; | ||||
| @@ -30,8 +30,8 @@ export async function POST(req: Request) { | ||||
|     const formData = await req.formData(); | ||||
|  | ||||
|     const files = formData.getAll('files') as File[]; | ||||
|     const embedding_model = formData.get('embedding_model'); | ||||
|     const embedding_model_provider = formData.get('embedding_model_provider'); | ||||
|     const embedding_model = formData.get('embedding_model_key') as string; | ||||
|     const embedding_model_provider = formData.get('embedding_model_provider_id') as string; | ||||
|  | ||||
|     if (!embedding_model || !embedding_model_provider) { | ||||
|       return NextResponse.json( | ||||
| @@ -40,20 +40,9 @@ export async function POST(req: Request) { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     const embeddingModels = await getAvailableEmbeddingModelProviders(); | ||||
|     const provider = | ||||
|       embedding_model_provider ?? Object.keys(embeddingModels)[0]; | ||||
|     const embeddingModel = | ||||
|       embedding_model ?? Object.keys(embeddingModels[provider as string])[0]; | ||||
|     const registry = new ModelRegistry(); | ||||
|  | ||||
|     let embeddingsModel = | ||||
|       embeddingModels[provider as string]?.[embeddingModel as string]?.model; | ||||
|     if (!embeddingsModel) { | ||||
|       return NextResponse.json( | ||||
|         { message: 'Invalid embedding model selected' }, | ||||
|         { status: 400 }, | ||||
|       ); | ||||
|     } | ||||
|     const model = await registry.loadEmbeddingModel(embedding_model_provider, embedding_model); | ||||
|  | ||||
|     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), | ||||
|         ); | ||||
|         const embeddingsDataPath = filePath.replace( | ||||
|   | ||||
| @@ -1,23 +1,12 @@ | ||||
| import handleVideoSearch from '@/lib/chains/videoSearchAgent'; | ||||
| import { | ||||
|   getCustomOpenaiApiKey, | ||||
|   getCustomOpenaiApiUrl, | ||||
|   getCustomOpenaiModelName, | ||||
| } from '@/lib/config'; | ||||
| import { getAvailableChatModelProviders } from '@/lib/providers'; | ||||
| import { BaseChatModel } from '@langchain/core/language_models/chat_models'; | ||||
| import ModelRegistry from '@/lib/models/registry'; | ||||
| import { ModelWithProvider } from '@/lib/models/types'; | ||||
| import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; | ||||
| import { ChatOpenAI } from '@langchain/openai'; | ||||
|  | ||||
| interface ChatModel { | ||||
|   provider: string; | ||||
|   model: string; | ||||
| } | ||||
|  | ||||
| interface VideoSearchBody { | ||||
|   query: string; | ||||
|   chatHistory: any[]; | ||||
|   chatModel?: ChatModel; | ||||
|   chatModel: ModelWithProvider; | ||||
| } | ||||
|  | ||||
| export const POST = async (req: Request) => { | ||||
| @@ -34,35 +23,12 @@ export const POST = async (req: Request) => { | ||||
|       }) | ||||
|       .filter((msg) => msg !== undefined) as BaseMessage[]; | ||||
|  | ||||
|     const chatModelProviders = await getAvailableChatModelProviders(); | ||||
|     const registry = new ModelRegistry(); | ||||
|  | ||||
|     const chatModelProvider = | ||||
|       chatModelProviders[ | ||||
|         body.chatModel?.provider || Object.keys(chatModelProviders)[0] | ||||
|       ]; | ||||
|     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 llm = await registry.loadChatModel( | ||||
|       body.chatModel.providerId, | ||||
|       body.chatModel.key, | ||||
|     ); | ||||
|  | ||||
|     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'; | ||||
| import React from 'react'; | ||||
| 'use client'; | ||||
|  | ||||
| const Page = ({ params }: { params: Promise<{ chatId: string }> }) => { | ||||
|   const { chatId } = React.use(params); | ||||
|   return <ChatWindow id={chatId} />; | ||||
| import ChatWindow from '@/components/ChatWindow'; | ||||
| import { useParams } from 'next/navigation'; | ||||
| 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; | ||||
|   | ||||
| @@ -1,25 +1,51 @@ | ||||
| 'use client'; | ||||
|  | ||||
| import { Search } from 'lucide-react'; | ||||
| import { Globe2Icon } from 'lucide-react'; | ||||
| import { useEffect, useState } from 'react'; | ||||
| import Link from 'next/link'; | ||||
| 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; | ||||
|   content: string; | ||||
|   url: 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 [discover, setDiscover] = useState<Discover[] | null>(null); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const [activeTopic, setActiveTopic] = useState<string>(topics[0].key); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const fetchData = async () => { | ||||
|   const fetchArticles = async (topic: string) => { | ||||
|     setLoading(true); | ||||
|     try { | ||||
|         const res = await fetch(`/api/discover`, { | ||||
|       const res = await fetch(`/api/discover?topic=${topic}`, { | ||||
|         method: 'GET', | ||||
|         headers: { | ||||
|           'Content-Type': 'application/json', | ||||
| @@ -43,10 +69,44 @@ const Page = () => { | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|     fetchData(); | ||||
|   }, []); | ||||
|   useEffect(() => { | ||||
|     fetchArticles(activeTopic); | ||||
|   }, [activeTopic]); | ||||
|  | ||||
|   return loading ? ( | ||||
|   return ( | ||||
|     <> | ||||
|       <div> | ||||
|         <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 flex-col lg:flex-row lg:items-center lg:justify-between gap-4"> | ||||
|             <div className="flex items-center justify-center"> | ||||
|               <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> | ||||
|  | ||||
|         {loading ? ( | ||||
|           <div className="flex flex-row items-center justify-center min-h-screen"> | ||||
|             <svg | ||||
|               aria-hidden="true" | ||||
| @@ -66,46 +126,144 @@ const Page = () => { | ||||
|             </svg> | ||||
|           </div> | ||||
|         ) : ( | ||||
|     <> | ||||
|       <div> | ||||
|         <div className="flex flex-col pt-4"> | ||||
|           <div className="flex items-center"> | ||||
|             <Search /> | ||||
|             <h1 className="text-3xl font-medium p-2">Discover</h1> | ||||
|           </div> | ||||
|           <hr className="border-t border-[#2B2C2C] my-4 w-full" /> | ||||
|         </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"> | ||||
|           {discover && | ||||
|             discover?.map((item, i) => ( | ||||
|               <Link | ||||
|                 href={`/?q=Summary: ${item.url}`} | ||||
|                 key={i} | ||||
|                 className="max-w-sm rounded-lg overflow-hidden bg-light-secondary dark:bg-dark-secondary hover:-translate-y-[1px] transition duration-200" | ||||
|                 target="_blank" | ||||
|               > | ||||
|                 <img | ||||
|                   className="object-cover w-full aspect-video" | ||||
|                   src={ | ||||
|                     new URL(item.thumbnail).origin + | ||||
|                     new URL(item.thumbnail).pathname + | ||||
|                     `?id=${new URL(item.thumbnail).searchParams.get('id')}` | ||||
|                   } | ||||
|                   alt={item.title} | ||||
|                 /> | ||||
|                 <div className="px-6 py-4"> | ||||
|                   <div className="font-bold text-lg mb-2"> | ||||
|                     {item.title.slice(0, 100)}... | ||||
|                   </div> | ||||
|                   <p className="text-black-70 dark:text-white/70 text-sm"> | ||||
|                     {item.content.slice(0, 100)}... | ||||
|                   </p> | ||||
|                 </div> | ||||
|               </Link> | ||||
|           <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; | ||||
|                     } | ||||
|                   } | ||||
|  | ||||
|                   return sections; | ||||
|                 })()} | ||||
|             </div> | ||||
|           </div> | ||||
|         )} | ||||
|       </div> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -2,6 +2,14 @@ | ||||
| @tailwind components; | ||||
| @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 { | ||||
|   .overflow-hidden-scrollable { | ||||
|     -ms-overflow-style: none; | ||||
| @@ -10,4 +18,82 @@ | ||||
|   .overflow-hidden-scrollable::-webkit-scrollbar { | ||||
|     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 { Montserrat } from 'next/font/google'; | ||||
| import './globals.css'; | ||||
| @@ -5,6 +7,8 @@ import { cn } from '@/lib/utils'; | ||||
| import Sidebar from '@/components/Sidebar'; | ||||
| import { Toaster } from 'sonner'; | ||||
| import ThemeProvider from '@/components/theme/Provider'; | ||||
| import configManager from '@/lib/config'; | ||||
| import SetupWizard from '@/components/Setup/SetupWizard'; | ||||
|  | ||||
| const montserrat = Montserrat({ | ||||
|   weight: ['300', '400', '500', '700'], | ||||
| @@ -24,20 +28,29 @@ export default function RootLayout({ | ||||
| }: Readonly<{ | ||||
|   children: React.ReactNode; | ||||
| }>) { | ||||
|   const setupComplete = configManager.isSetupComplete(); | ||||
|   const configSections = configManager.getUIConfigSections(); | ||||
|  | ||||
|   return ( | ||||
|     <html className="h-full" lang="en" suppressHydrationWarning> | ||||
|       <body className={cn('h-full', montserrat.className)}> | ||||
|         <ThemeProvider> | ||||
|           {setupComplete ? ( | ||||
|             <> | ||||
|               <Sidebar>{children}</Sidebar> | ||||
|               <Toaster | ||||
|                 toastOptions={{ | ||||
|                   unstyled: true, | ||||
|                   classNames: { | ||||
|                     toast: | ||||
|                   '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', | ||||
|                       '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> | ||||
|       </body> | ||||
|     </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 { ChatProvider } from '@/lib/hooks/useChat'; | ||||
| import { Metadata } from 'next'; | ||||
| import { Suspense } from 'react'; | ||||
|  | ||||
| @@ -11,7 +12,9 @@ const Home = () => { | ||||
|   return ( | ||||
|     <div> | ||||
|       <Suspense> | ||||
|         <ChatProvider> | ||||
|           <ChatWindow /> | ||||
|         </ChatProvider> | ||||
|       </Suspense> | ||||
|     </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 MessageInput from './MessageInput'; | ||||
| import { File, Message } from './ChatWindow'; | ||||
| import MessageBox from './MessageBox'; | ||||
| 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 dividerRef = useRef<HTMLDivElement | null>(null); | ||||
|   const messageEnd = useRef<HTMLDivElement | null>(null); | ||||
| @@ -45,41 +27,36 @@ const Chat = ({ | ||||
|     return () => { | ||||
|       window.removeEventListener('resize', updateDividerWidth); | ||||
|     }; | ||||
|   }); | ||||
|   }, []); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const scroll = () => { | ||||
|       messageEnd.current?.scrollIntoView({ behavior: 'smooth' }); | ||||
|     }; | ||||
|  | ||||
|     if (messages.length === 1) { | ||||
|       document.title = `${messages[0].content.substring(0, 30)} - Perplexica`; | ||||
|     if (chatTurns.length === 1) { | ||||
|       document.title = `${chatTurns[0].content.substring(0, 30)} - Perplexica`; | ||||
|     } | ||||
|  | ||||
|     if (messages[messages.length - 1]?.role == 'user') { | ||||
|     if (chatTurns[chatTurns.length - 1]?.role === 'user') { | ||||
|       scroll(); | ||||
|     } | ||||
|   }, [messages]); | ||||
|   }, [chatTurns]); | ||||
|  | ||||
|   return ( | ||||
|     <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) => { | ||||
|         const isLast = i === messages.length - 1; | ||||
|       {sections.map((section, i) => { | ||||
|         const isLast = i === sections.length - 1; | ||||
|  | ||||
|         return ( | ||||
|           <Fragment key={msg.messageId}> | ||||
|           <Fragment key={section.userMessage.messageId}> | ||||
|             <MessageBox | ||||
|               key={i} | ||||
|               message={msg} | ||||
|               messageIndex={i} | ||||
|               history={messages} | ||||
|               loading={loading} | ||||
|               section={section} | ||||
|               sectionIndex={i} | ||||
|               dividerRef={isLast ? dividerRef : undefined} | ||||
|               isLast={isLast} | ||||
|               rewrite={rewrite} | ||||
|               sendMessage={sendMessage} | ||||
|             /> | ||||
|             {!isLast && msg.role === 'assistant' && ( | ||||
|             {!isLast && ( | ||||
|               <div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" /> | ||||
|             )} | ||||
|           </Fragment> | ||||
| @@ -92,14 +69,7 @@ const Chat = ({ | ||||
|           className="bottom-24 lg:bottom-10 fixed z-40" | ||||
|           style={{ width: dividerWidth }} | ||||
|         > | ||||
|           <MessageInput | ||||
|             loading={loading} | ||||
|             sendMessage={sendMessage} | ||||
|             fileIds={fileIds} | ||||
|             setFileIds={setFileIds} | ||||
|             files={files} | ||||
|             setFiles={setFiles} | ||||
|           /> | ||||
|           <MessageInput /> | ||||
|         </div> | ||||
|       )} | ||||
|     </div> | ||||
|   | ||||
| @@ -1,27 +1,48 @@ | ||||
| 'use client'; | ||||
|  | ||||
| import { useEffect, useRef, useState } from 'react'; | ||||
| import { Document } from '@langchain/core/documents'; | ||||
| import Navbar from './Navbar'; | ||||
| import Chat from './Chat'; | ||||
| 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 Link from 'next/link'; | ||||
| import NextError from 'next/error'; | ||||
| import { useChat } from '@/lib/hooks/useChat'; | ||||
| import Loader from './ui/Loader'; | ||||
|  | ||||
| export type Message = { | ||||
|   messageId: string; | ||||
| export interface BaseMessage { | ||||
|   chatId: string; | ||||
|   messageId: string; | ||||
|   createdAt: Date; | ||||
| } | ||||
|  | ||||
| export interface AssistantMessage extends BaseMessage { | ||||
|   role: 'assistant'; | ||||
|   content: string; | ||||
|   role: 'user' | 'assistant'; | ||||
|   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 { | ||||
|   fileName: string; | ||||
| @@ -29,512 +50,8 @@ export interface File { | ||||
|   fileId: string; | ||||
| } | ||||
|  | ||||
| interface ChatModelProvider { | ||||
|   name: string; | ||||
|   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]); | ||||
|  | ||||
| const ChatWindow = () => { | ||||
|   const { hasError, isReady, notFound, messages } = useChat(); | ||||
|   if (hasError) { | ||||
|     return ( | ||||
|       <div className="relative"> | ||||
| @@ -559,52 +76,17 @@ const ChatWindow = ({ id }: { id?: string }) => { | ||||
|       <div> | ||||
|         {messages.length > 0 ? ( | ||||
|           <> | ||||
|             <Navbar chatId={chatId!} messages={messages} /> | ||||
|             <Chat | ||||
|               loading={loading} | ||||
|               messages={messages} | ||||
|               sendMessage={sendMessage} | ||||
|               messageAppeared={messageAppeared} | ||||
|               rewrite={rewrite} | ||||
|               fileIds={fileIds} | ||||
|               setFileIds={setFileIds} | ||||
|               files={files} | ||||
|               setFiles={setFiles} | ||||
|             /> | ||||
|             <Navbar /> | ||||
|             <Chat /> | ||||
|           </> | ||||
|         ) : ( | ||||
|           <EmptyChat | ||||
|             sendMessage={sendMessage} | ||||
|             focusMode={focusMode} | ||||
|             setFocusMode={setFocusMode} | ||||
|             optimizationMode={optimizationMode} | ||||
|             setOptimizationMode={setOptimizationMode} | ||||
|             fileIds={fileIds} | ||||
|             setFileIds={setFileIds} | ||||
|             files={files} | ||||
|             setFiles={setFiles} | ||||
|           /> | ||||
|           <EmptyChat /> | ||||
|         )} | ||||
|       </div> | ||||
|     ) | ||||
|   ) : ( | ||||
|     <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> | ||||
|       <Loader /> | ||||
|     </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 EmptyChatMessageInput from './EmptyChatMessageInput'; | ||||
| import { useState } from 'react'; | ||||
| import { File } from './ChatWindow'; | ||||
| import Link from 'next/link'; | ||||
| import WeatherWidget from './WeatherWidget'; | ||||
| import NewsArticleWidget from './NewsArticleWidget'; | ||||
|  | ||||
| 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); | ||||
|  | ||||
| const EmptyChat = () => { | ||||
|   return ( | ||||
|     <div className="relative"> | ||||
|       <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" /> | ||||
|         </Link> | ||||
|       </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"> | ||||
|         <div className="flex flex-col items-center justify-center w-full space-y-8"> | ||||
|           <h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8"> | ||||
|             Research begins here. | ||||
|           </h2> | ||||
|         <EmptyChatMessageInput | ||||
|           sendMessage={sendMessage} | ||||
|           focusMode={focusMode} | ||||
|           setFocusMode={setFocusMode} | ||||
|           optimizationMode={optimizationMode} | ||||
|           setOptimizationMode={setOptimizationMode} | ||||
|           fileIds={fileIds} | ||||
|           setFileIds={setFileIds} | ||||
|           files={files} | ||||
|           setFiles={setFiles} | ||||
|         /> | ||||
|           <EmptyChatMessageInput /> | ||||
|         </div> | ||||
|         <div className="flex flex-col w-full gap-4 mt-2 sm:flex-row sm:justify-center"> | ||||
|           <div className="flex-1 w-full"> | ||||
|             <WeatherWidget /> | ||||
|           </div> | ||||
|           <div className="flex-1 w-full"> | ||||
|             <NewsArticleWidget /> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
|   | ||||
| @@ -1,34 +1,17 @@ | ||||
| import { ArrowRight } from 'lucide-react'; | ||||
| import { useEffect, useRef, useState } from 'react'; | ||||
| import TextareaAutosize from 'react-textarea-autosize'; | ||||
| import CopilotToggle from './MessageInputActions/Copilot'; | ||||
| import Focus from './MessageInputActions/Focus'; | ||||
| import Optimization from './MessageInputActions/Optimization'; | ||||
| 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 = ({ | ||||
|   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 [copilotEnabled, setCopilotEnabled] = useState(false); | ||||
| const EmptyChatMessageInput = () => { | ||||
|   const { sendMessage } = useChat(); | ||||
|  | ||||
|   /* const [copilotEnabled, setCopilotEnabled] = useState(false); */ | ||||
|   const [message, setMessage] = useState(''); | ||||
|  | ||||
|   const inputRef = useRef<HTMLTextAreaElement | null>(null); | ||||
| @@ -73,34 +56,26 @@ const EmptyChatMessageInput = ({ | ||||
|       }} | ||||
|       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 | ||||
|           ref={inputRef} | ||||
|           value={message} | ||||
|           onChange={(e) => setMessage(e.target.value)} | ||||
|           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..." | ||||
|         /> | ||||
|         <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"> | ||||
|             <Focus focusMode={focusMode} setFocusMode={setFocusMode} /> | ||||
|             <Attach | ||||
|               fileIds={fileIds} | ||||
|               setFileIds={setFileIds} | ||||
|               files={files} | ||||
|               setFiles={setFiles} | ||||
|               showText | ||||
|             /> | ||||
|           <Optimization /> | ||||
|           <div className="flex flex-row items-center space-x-2"> | ||||
|             <div className="flex flex-row items-center space-x-1"> | ||||
|               <ModelSelector /> | ||||
|               <Focus /> | ||||
|               <Attach /> | ||||
|             </div> | ||||
|           <div className="flex flex-row items-center space-x-1 sm:space-x-4"> | ||||
|             <Optimization | ||||
|               optimizationMode={optimizationMode} | ||||
|               setOptimizationMode={setOptimizationMode} | ||||
|             /> | ||||
|             <button | ||||
|               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} /> | ||||
|             </button> | ||||
|   | ||||
| @@ -1,12 +1,13 @@ | ||||
| import { Check, ClipboardList } from 'lucide-react'; | ||||
| import { Message } from '../ChatWindow'; | ||||
| import { useState } from 'react'; | ||||
| import { Section } from '@/lib/hooks/useChat'; | ||||
|  | ||||
| const Copy = ({ | ||||
|   message, | ||||
|   section, | ||||
|   initialMessage, | ||||
| }: { | ||||
|   message: Message; | ||||
|   section: Section; | ||||
|   initialMessage: string; | ||||
| }) => { | ||||
|   const [copied, setCopied] = useState(false); | ||||
| @@ -14,7 +15,7 @@ const Copy = ({ | ||||
|   return ( | ||||
|     <button | ||||
|       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); | ||||
|         setCopied(true); | ||||
|         setTimeout(() => setCopied(false), 1000); | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| 'use client'; | ||||
|  | ||||
| /* eslint-disable @next/next/no-img-element */ | ||||
| import React, { MutableRefObject, useEffect, useState } from 'react'; | ||||
| import { Message } from './ChatWindow'; | ||||
| import React, { MutableRefObject } from 'react'; | ||||
| import { cn } from '@/lib/utils'; | ||||
| import { | ||||
|   BookCopy, | ||||
| @@ -20,89 +19,37 @@ import SearchImages from './SearchImages'; | ||||
| import SearchVideos from './SearchVideos'; | ||||
| import { useSpeech } from 'react-text-to-speech'; | ||||
| import ThinkBox from './ThinkBox'; | ||||
| import { useChat, Section } from '@/lib/hooks/useChat'; | ||||
| import Citation from './Citation'; | ||||
|  | ||||
| const ThinkTagProcessor = ({ children }: { children: React.ReactNode }) => { | ||||
|   return <ThinkBox content={children as string} />; | ||||
| const ThinkTagProcessor = ({ | ||||
|   children, | ||||
|   thinkingEnded, | ||||
| }: { | ||||
|   children: React.ReactNode; | ||||
|   thinkingEnded: boolean; | ||||
| }) => { | ||||
|   return ( | ||||
|     <ThinkBox content={children as string} thinkingEnded={thinkingEnded} /> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| const MessageBox = ({ | ||||
|   message, | ||||
|   messageIndex, | ||||
|   history, | ||||
|   loading, | ||||
|   section, | ||||
|   sectionIndex, | ||||
|   dividerRef, | ||||
|   isLast, | ||||
|   rewrite, | ||||
|   sendMessage, | ||||
| }: { | ||||
|   message: Message; | ||||
|   messageIndex: number; | ||||
|   history: Message[]; | ||||
|   loading: boolean; | ||||
|   section: Section; | ||||
|   sectionIndex: number; | ||||
|   dividerRef?: MutableRefObject<HTMLDivElement | null>; | ||||
|   isLast: boolean; | ||||
|   rewrite: (messageId: string) => void; | ||||
|   sendMessage: (message: string) => void; | ||||
| }) => { | ||||
|   const [parsedMessage, setParsedMessage] = useState(message.content); | ||||
|   const [speechMessage, setSpeechMessage] = useState(message.content); | ||||
|   const { loading, chatTurns, sendMessage, rewrite } = useChat(); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const citationRegex = /\[([^\]]+)\]/g; | ||||
|     const regex = /\[(\d+)\]/g; | ||||
|     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 parsedMessage = section.parsedAssistantMessage || ''; | ||||
|   const speechMessage = section.speechMessage || ''; | ||||
|   const thinkingEnded = section.thinkingEnded; | ||||
|  | ||||
|   const { speechStatus, start, stop } = useSpeech({ text: speechMessage }); | ||||
|  | ||||
| @@ -110,33 +57,31 @@ const MessageBox = ({ | ||||
|     overrides: { | ||||
|       think: { | ||||
|         component: ThinkTagProcessor, | ||||
|         props: { | ||||
|           thinkingEnded: thinkingEnded, | ||||
|         }, | ||||
|       }, | ||||
|       citation: { | ||||
|         component: Citation, | ||||
|       }, | ||||
|     }, | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <div> | ||||
|       {message.role === 'user' && ( | ||||
|         <div | ||||
|           className={cn( | ||||
|             'w-full', | ||||
|             messageIndex === 0 ? 'pt-16' : 'pt-8', | ||||
|             'break-words', | ||||
|           )} | ||||
|         > | ||||
|     <div className="space-y-6"> | ||||
|       <div className={'w-full pt-8 break-words'}> | ||||
|         <h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12"> | ||||
|             {message.content} | ||||
|           {section.userMessage.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 | ||||
|           ref={dividerRef} | ||||
|           className="flex flex-col space-y-6 w-full lg:w-9/12" | ||||
|         > | ||||
|             {message.sources && message.sources.length > 0 && ( | ||||
|           {section.sourceMessage && | ||||
|             section.sourceMessage.sources.length > 0 && ( | ||||
|               <div className="flex flex-col space-y-2"> | ||||
|                 <div className="flex flex-row items-center space-x-2"> | ||||
|                   <BookCopy className="text-black dark:text-white" size={20} /> | ||||
| @@ -144,10 +89,12 @@ const MessageBox = ({ | ||||
|                     Sources | ||||
|                   </h3> | ||||
|                 </div> | ||||
|                 <MessageSources sources={message.sources} /> | ||||
|                 <MessageSources sources={section.sourceMessage.sources} /> | ||||
|               </div> | ||||
|             )} | ||||
|  | ||||
|           <div className="flex flex-col space-y-2"> | ||||
|             {section.sourceMessage && ( | ||||
|               <div className="flex flex-row items-center space-x-2"> | ||||
|                 <Disc3 | ||||
|                   className={cn( | ||||
| @@ -160,7 +107,10 @@ const MessageBox = ({ | ||||
|                   Answer | ||||
|                 </h3> | ||||
|               </div> | ||||
|             )} | ||||
|  | ||||
|             {section.assistantMessage && ( | ||||
|               <> | ||||
|                 <Markdown | ||||
|                   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]', | ||||
| @@ -170,16 +120,20 @@ const MessageBox = ({ | ||||
|                 > | ||||
|                   {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"> | ||||
|                     {/*  <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"> | ||||
|                       <Share size={18} /> | ||||
|                     </button> */} | ||||
|                     <Rewrite rewrite={rewrite} messageId={message.messageId} /> | ||||
|                       <Rewrite | ||||
|                         rewrite={rewrite} | ||||
|                         messageId={section.assistantMessage.messageId} | ||||
|                       /> | ||||
|                     </div> | ||||
|                     <div className="flex flex-row items-center space-x-1"> | ||||
|                     <Copy initialMessage={message.content} message={message} /> | ||||
|                       <Copy | ||||
|                         initialMessage={section.assistantMessage.content} | ||||
|                         section={section} | ||||
|                       /> | ||||
|                       <button | ||||
|                         onClick={() => { | ||||
|                           if (speechStatus === 'started') { | ||||
| @@ -199,62 +153,70 @@ const MessageBox = ({ | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 )} | ||||
|  | ||||
|                 {isLast && | ||||
|                 message.suggestions && | ||||
|                 message.suggestions.length > 0 && | ||||
|                 message.role === 'assistant' && | ||||
|                   section.suggestions && | ||||
|                   section.suggestions.length > 0 && | ||||
|                   section.assistantMessage && | ||||
|                   !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 className="mt-8 pt-6 border-t border-light-200/50 dark:border-dark-200/50"> | ||||
|                       <div className="flex flex-row items-center space-x-2 mb-4"> | ||||
|                         <Layers3 | ||||
|                           className="text-black dark:text-white" | ||||
|                           size={20} | ||||
|                         /> | ||||
|                         <h3 className="text-black dark:text-white font-medium text-xl"> | ||||
|                           Related | ||||
|                         </h3> | ||||
|                       </div> | ||||
|                       <div className="flex flex-col space-y-3"> | ||||
|                         {message.suggestions.map((suggestion, i) => ( | ||||
|                           <div | ||||
|                             className="flex flex-col space-y-3 text-sm" | ||||
|                             key={i} | ||||
|                       <div className="space-y-0"> | ||||
|                         {section.suggestions.map( | ||||
|                           (suggestion: string, i: number) => ( | ||||
|                             <div key={i}> | ||||
|                               {i > 0 && ( | ||||
|                                 <div className="h-px bg-light-200/40 dark:bg-dark-200/40 mx-3" /> | ||||
|                               )} | ||||
|                               <button | ||||
|                                 onClick={() => sendMessage(suggestion)} | ||||
|                                 className="group w-full px-3 py-4 text-left transition-colors duration-200" | ||||
|                               > | ||||
|                             <div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" /> | ||||
|                             <div | ||||
|                               onClick={() => { | ||||
|                                 sendMessage(suggestion); | ||||
|                               }} | ||||
|                               className="cursor-pointer flex flex-row justify-between font-medium space-x-2 items-center" | ||||
|                             > | ||||
|                               <p className="transition duration-200 hover:text-[#24A0ED]"> | ||||
|                                 <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"> | ||||
|                                     {suggestion} | ||||
|                                   </p> | ||||
|                                   <Plus | ||||
|                                 size={20} | ||||
|                                 className="text-[#24A0ED] flex-shrink-0" | ||||
|                                     size={16} | ||||
|                                     className="text-black/40 dark:text-white/40 group-hover:text-[#24A0ED] transition-colors duration-200 flex-shrink-0" | ||||
|                                   /> | ||||
|                                 </div> | ||||
|                               </button> | ||||
|                             </div> | ||||
|                         ))} | ||||
|                           ), | ||||
|                         )} | ||||
|                       </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={history[messageIndex - 1].content} | ||||
|               chatHistory={history.slice(0, messageIndex - 1)} | ||||
|               messageId={message.messageId} | ||||
|               query={section.userMessage.content} | ||||
|               chatHistory={chatTurns.slice(0, sectionIndex * 2)} | ||||
|               messageId={section.assistantMessage.messageId} | ||||
|             /> | ||||
|             <SearchVideos | ||||
|               chatHistory={history.slice(0, messageIndex - 1)} | ||||
|               query={history[messageIndex - 1].content} | ||||
|               messageId={message.messageId} | ||||
|               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 { File } from './ChatWindow'; | ||||
| 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 [message, setMessage] = useState(''); | ||||
|   const [textareaRows, setTextareaRows] = useState(1); | ||||
| @@ -75,18 +64,11 @@ const MessageInput = ({ | ||||
|         } | ||||
|       }} | ||||
|       className={cn( | ||||
|         'bg-light-secondary dark:bg-dark-secondary p-4 flex items-center overflow-hidden border border-light-200 dark:border-dark-200', | ||||
|         mode === 'multi' ? 'flex-col rounded-lg' : 'flex-row rounded-full', | ||||
|         '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-2xl' : 'flex-row rounded-full', | ||||
|       )} | ||||
|     > | ||||
|       {mode === 'single' && ( | ||||
|         <AttachSmall | ||||
|           fileIds={fileIds} | ||||
|           setFileIds={setFileIds} | ||||
|           files={files} | ||||
|           setFiles={setFiles} | ||||
|         /> | ||||
|       )} | ||||
|       {mode === 'single' && <AttachSmall />} | ||||
|       <TextareaAutosize | ||||
|         ref={inputRef} | ||||
|         value={message} | ||||
| @@ -113,12 +95,7 @@ const MessageInput = ({ | ||||
|       )} | ||||
|       {mode === 'multi' && ( | ||||
|         <div className="flex flex-row items-center justify-between w-full pt-2"> | ||||
|           <AttachSmall | ||||
|             fileIds={fileIds} | ||||
|             setFileIds={setFileIds} | ||||
|             files={files} | ||||
|             setFiles={setFiles} | ||||
|           /> | ||||
|           <AttachSmall /> | ||||
|           <div className="flex flex-row items-center space-x-4"> | ||||
|             <CopilotToggle | ||||
|               copilotEnabled={copilotEnabled} | ||||
|   | ||||
| @@ -5,23 +5,21 @@ import { | ||||
|   PopoverPanel, | ||||
|   Transition, | ||||
| } 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 { 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 fileInputRef = useRef<any>(); | ||||
|  | ||||
| @@ -34,12 +32,12 @@ const Attach = ({ | ||||
|     } | ||||
|  | ||||
|     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', embeddingModel!); | ||||
|     data.append('embedding_model_provider_id', embeddingModelProvider!); | ||||
|     data.append('embedding_model_key', embeddingModel!); | ||||
|  | ||||
|     const res = await fetch(`/api/uploads`, { | ||||
|       method: 'POST', | ||||
| @@ -54,42 +52,16 @@ const Attach = ({ | ||||
|   }; | ||||
|  | ||||
|   return loading ? ( | ||||
|     <div className="flex flex-row items-center justify-between space-x-1"> | ||||
|       <LoaderCircle size={18} className="text-sky-400 animate-spin" /> | ||||
|       <p className="text-sky-400 inline whitespace-nowrap text-xs font-medium"> | ||||
|         Uploading.. | ||||
|       </p> | ||||
|     <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={16} className="text-sky-400 animate-spin" /> | ||||
|     </div> | ||||
|   ) : files.length > 0 ? ( | ||||
|     <Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg"> | ||||
|       <PopoverButton | ||||
|         type="button" | ||||
|         className={cn( | ||||
|           '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' : '', | ||||
|         )} | ||||
|         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" | ||||
|       > | ||||
|         {files.length > 1 && ( | ||||
|           <> | ||||
|             <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> | ||||
|           </> | ||||
|         )} | ||||
|         <File size={16} className="text-sky-400" /> | ||||
|       </PopoverButton> | ||||
|       <Transition | ||||
|         as={Fragment} | ||||
| @@ -110,7 +82,7 @@ const Attach = ({ | ||||
|                 <button | ||||
|                   type="button" | ||||
|                   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 | ||||
|                     type="file" | ||||
| @@ -120,7 +92,7 @@ const Attach = ({ | ||||
|                     multiple | ||||
|                     hidden | ||||
|                   /> | ||||
|                   <Plus size={18} /> | ||||
|                   <Plus size={16} /> | ||||
|                   <p className="text-xs">Add</p> | ||||
|                 </button> | ||||
|                 <button | ||||
| @@ -128,7 +100,7 @@ const Attach = ({ | ||||
|                     setFiles([]); | ||||
|                     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} /> | ||||
|                   <p className="text-xs">Clear</p> | ||||
| @@ -142,8 +114,11 @@ const Attach = ({ | ||||
|                   key={i} | ||||
|                   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"> | ||||
|                     <File size={16} className="text-white/70" /> | ||||
|                   <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-black/70 dark:text-white/70" | ||||
|                     /> | ||||
|                   </div> | ||||
|                   <p className="text-black/70 dark:text-white/70 text-sm"> | ||||
|                     {file.fileName.length > 25 | ||||
| @@ -164,8 +139,7 @@ const Attach = ({ | ||||
|       type="button" | ||||
|       onClick={() => fileInputRef.current.click()} | ||||
|       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', | ||||
|         showText ? '' : 'p-2', | ||||
|         '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', | ||||
|       )} | ||||
|     > | ||||
|       <input | ||||
| @@ -176,8 +150,7 @@ const Attach = ({ | ||||
|         multiple | ||||
|         hidden | ||||
|       /> | ||||
|       <CopyPlus size={showText ? 18 : undefined} /> | ||||
|       {showText && <p className="text-xs font-medium pl-[1px]">Attach</p>} | ||||
|       <Paperclip size={16} /> | ||||
|     </button> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -5,21 +5,21 @@ import { | ||||
|   PopoverPanel, | ||||
|   Transition, | ||||
| } 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 { 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 fileInputRef = useRef<any>(); | ||||
|  | ||||
| @@ -32,12 +32,12 @@ const AttachSmall = ({ | ||||
|     } | ||||
|  | ||||
|     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', embeddingModel!); | ||||
|     data.append('embedding_model_provider_id', embeddingModelProvider!); | ||||
|     data.append('embedding_model_key', embeddingModel!); | ||||
|  | ||||
|     const res = await fetch(`/api/uploads`, { | ||||
|       method: 'POST', | ||||
| @@ -114,8 +114,11 @@ const AttachSmall = ({ | ||||
|                   key={i} | ||||
|                   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"> | ||||
|                     <File size={16} className="text-white/70" /> | ||||
|                   <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-black/70 dark:text-white/70" | ||||
|                     /> | ||||
|                   </div> | ||||
|                   <p className="text-black/70 dark:text-white/70 text-sm"> | ||||
|                     {file.fileName.length > 25 | ||||
| @@ -145,7 +148,7 @@ const AttachSmall = ({ | ||||
|         multiple | ||||
|         hidden | ||||
|       /> | ||||
|       <CopyPlus size={20} /> | ||||
|       <Paperclip size={16} /> | ||||
|     </button> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -15,19 +15,20 @@ import { | ||||
| } from '@headlessui/react'; | ||||
| import { SiReddit, SiYoutube } from '@icons-pack/react-simple-icons'; | ||||
| import { Fragment } from 'react'; | ||||
| import { useChat } from '@/lib/hooks/useChat'; | ||||
|  | ||||
| const focusModes = [ | ||||
|   { | ||||
|     key: 'webSearch', | ||||
|     title: 'All', | ||||
|     description: 'Searches across all of the internet', | ||||
|     icon: <Globe size={20} />, | ||||
|     icon: <Globe size={16} />, | ||||
|   }, | ||||
|   { | ||||
|     key: 'academicSearch', | ||||
|     title: 'Academic', | ||||
|     description: 'Search in published academic papers', | ||||
|     icon: <SwatchBook size={20} />, | ||||
|     icon: <SwatchBook size={16} />, | ||||
|   }, | ||||
|   { | ||||
|     key: 'writingAssistant', | ||||
| @@ -39,47 +40,38 @@ const focusModes = [ | ||||
|     key: 'wolframAlphaSearch', | ||||
|     title: 'Wolfram Alpha', | ||||
|     description: 'Computational knowledge engine', | ||||
|     icon: <BadgePercent size={20} />, | ||||
|     icon: <BadgePercent size={16} />, | ||||
|   }, | ||||
|   { | ||||
|     key: 'youtubeSearch', | ||||
|     title: 'Youtube', | ||||
|     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', | ||||
|     title: 'Reddit', | ||||
|     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 = ({ | ||||
|   focusMode, | ||||
|   setFocusMode, | ||||
| }: { | ||||
|   focusMode: string; | ||||
|   setFocusMode: (mode: string) => void; | ||||
| }) => { | ||||
| const Focus = () => { | ||||
|   const { focusMode, setFocusMode } = useChat(); | ||||
|  | ||||
|   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 | ||||
|         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' ? ( | ||||
|           <div className="flex flex-row items-center space-x-1"> | ||||
|             {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 className="flex flex-row items-center space-x-1"> | ||||
|             <ScanEye size={20} /> | ||||
|             <p className="text-xs font-medium hidden lg:block">Focus</p> | ||||
|             <Globe size={16} /> | ||||
|           </div> | ||||
|         )} | ||||
|       </PopoverButton> | ||||
| @@ -92,14 +84,14 @@ const Focus = ({ | ||||
|         leaveFrom="opacity-100 translate-y-0" | ||||
|         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"> | ||||
|             {focusModes.map((mode, i) => ( | ||||
|               <PopoverButton | ||||
|                 onClick={() => setFocusMode(mode.key)} | ||||
|                 key={i} | ||||
|                 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 | ||||
|                     ? 'bg-light-secondary dark: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, | ||||
| } from '@headlessui/react'; | ||||
| import { Fragment } from 'react'; | ||||
| import { useChat } from '@/lib/hooks/useChat'; | ||||
|  | ||||
| const OptimizationModes = [ | ||||
|   { | ||||
|     key: 'speed', | ||||
|     title: 'Speed', | ||||
|     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', | ||||
|     title: 'Balanced', | ||||
|     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', | ||||
| @@ -34,31 +35,29 @@ const OptimizationModes = [ | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| const Optimization = ({ | ||||
|   optimizationMode, | ||||
|   setOptimizationMode, | ||||
| }: { | ||||
|   optimizationMode: string; | ||||
|   setOptimizationMode: (mode: string) => void; | ||||
| }) => { | ||||
| const Optimization = () => { | ||||
|   const { optimizationMode, setOptimizationMode } = useChat(); | ||||
|  | ||||
|   return ( | ||||
|     <Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg"> | ||||
|       {({ open }) => ( | ||||
|         <> | ||||
|           <PopoverButton | ||||
|             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" | ||||
|             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" | ||||
|           > | ||||
|             <div className="flex flex-row items-center space-x-1"> | ||||
|               { | ||||
|                 OptimizationModes.find((mode) => mode.key === optimizationMode) | ||||
|                   ?.icon | ||||
|               } | ||||
|           <p className="text-xs font-medium"> | ||||
|             { | ||||
|               OptimizationModes.find((mode) => mode.key === optimizationMode) | ||||
|                 ?.title | ||||
|             } | ||||
|           </p> | ||||
|           <ChevronDown size={20} /> | ||||
|               <ChevronDown | ||||
|                 size={16} | ||||
|                 className={cn( | ||||
|                   open ? 'rotate-180' : 'rotate-0', | ||||
|                   'transition duration:200', | ||||
|                 )} | ||||
|               /> | ||||
|             </div> | ||||
|           </PopoverButton> | ||||
|           <Transition | ||||
| @@ -70,7 +69,7 @@ const Optimization = ({ | ||||
|             leaveFrom="opacity-100 translate-y-0" | ||||
|             leaveTo="opacity-0 translate-y-1" | ||||
|           > | ||||
|         <PopoverPanel className="absolute z-10 w-64 md:w-[250px] right-0"> | ||||
|             <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 | ||||
| @@ -78,7 +77,7 @@ const Optimization = ({ | ||||
|                     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', | ||||
|                       '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', | ||||
| @@ -97,6 +96,8 @@ const Optimization = ({ | ||||
|               </div> | ||||
|             </PopoverPanel> | ||||
|           </Transition> | ||||
|         </> | ||||
|       )} | ||||
|     </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 { useEffect, useState } from 'react'; | ||||
| import { useEffect, useState, Fragment } from 'react'; | ||||
| import { formatTimeDifference } from '@/lib/utils'; | ||||
| 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 = ({ | ||||
|   chatId, | ||||
|   messages, | ||||
| }: { | ||||
|   messages: Message[]; | ||||
|   chatId: string; | ||||
| }) => { | ||||
| const downloadFile = (filename: string, content: string, type: string) => { | ||||
|   const blob = new Blob([content], { type }); | ||||
|   const url = URL.createObjectURL(blob); | ||||
|   const a = document.createElement('a'); | ||||
|   a.href = url; | ||||
|   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 [timeAgo, setTimeAgo] = useState<string>(''); | ||||
|  | ||||
|   const { sections, chatId } = useChat(); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (messages.length > 0) { | ||||
|     if (sections.length > 0 && sections[0].userMessage) { | ||||
|       const newTitle = | ||||
|         messages[0].content.length > 20 | ||||
|           ? `${messages[0].content.substring(0, 20).trim()}...` | ||||
|           : messages[0].content; | ||||
|         sections[0].userMessage.content.length > 20 | ||||
|           ? `${sections[0].userMessage.content.substring(0, 20).trim()}...` | ||||
|           : sections[0].userMessage.content; | ||||
|       setTitle(newTitle); | ||||
|       const newTimeAgo = formatTimeDifference( | ||||
|         new Date(), | ||||
|         messages[0].createdAt, | ||||
|         sections[0].userMessage.createdAt, | ||||
|       ); | ||||
|       setTimeAgo(newTimeAgo); | ||||
|     } | ||||
|   }, [messages]); | ||||
|   }, [sections]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const intervalId = setInterval(() => { | ||||
|       if (messages.length > 0) { | ||||
|       if (sections.length > 0 && sections[0].userMessage) { | ||||
|         const newTimeAgo = formatTimeDifference( | ||||
|           new Date(), | ||||
|           messages[0].createdAt, | ||||
|           sections[0].userMessage.createdAt, | ||||
|         ); | ||||
|         setTimeAgo(newTimeAgo); | ||||
|       } | ||||
| @@ -45,25 +228,91 @@ const Navbar = ({ | ||||
|   }, []); | ||||
|  | ||||
|   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"> | ||||
|       <div className="px-4 lg:px-6 py-4"> | ||||
|         <div className="flex items-center justify-between"> | ||||
|           <div className="flex items-center min-w-0"> | ||||
|             <a | ||||
|               href="/" | ||||
|         className="active:scale-95 transition duration-100 cursor-pointer lg:hidden" | ||||
|               className="lg:hidden mr-3 p-2 -ml-2 rounded-lg hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors duration-200" | ||||
|             > | ||||
|         <Edit size={17} /> | ||||
|               <Edit size={18} className="text-black/70 dark:text-white/70" /> | ||||
|             </a> | ||||
|       <div className="hidden lg:flex flex-row items-center justify-center space-x-2"> | ||||
|         <Clock size={17} /> | ||||
|         <p className="text-xs">{timeAgo} ago</p> | ||||
|             <div className="hidden lg:flex items-center gap-2 text-black/50 dark:text-white/50 min-w-0"> | ||||
|               <Clock size={14} /> | ||||
|               <span className="text-xs whitespace-nowrap">{timeAgo} ago</span> | ||||
|             </div> | ||||
|           </div> | ||||
|       <p className="hidden lg:flex">{title}</p> | ||||
|  | ||||
|       <div className="flex flex-row items-center space-x-4"> | ||||
|         <Share | ||||
|           size={17} | ||||
|           className="active:scale-95 transition duration-100 cursor-pointer" | ||||
|           <div className="flex-1 mx-4 min-w-0"> | ||||
|             <h1 className="text-center text-sm font-medium text-black/80 dark:text-white/90 truncate"> | ||||
|               {title || 'New Conversation'} | ||||
|             </h1> | ||||
|           </div> | ||||
|  | ||||
|           <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={() => {}} | ||||
|             /> | ||||
|         <DeleteChat redirect chatId={chatId} chats={[]} setChats={() => {}} /> | ||||
|           </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 () => { | ||||
|             setLoading(true); | ||||
|  | ||||
|             const chatModelProvider = localStorage.getItem('chatModelProvider'); | ||||
|             const chatModel = localStorage.getItem('chatModel'); | ||||
|  | ||||
|             const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL'); | ||||
|             const customOpenAIKey = localStorage.getItem('openAIApiKey'); | ||||
|             const chatModelProvider = localStorage.getItem( | ||||
|               'chatModelProviderId', | ||||
|             ); | ||||
|             const chatModel = localStorage.getItem('chatModelKey'); | ||||
|  | ||||
|             const res = await fetch(`/api/images`, { | ||||
|               method: 'POST', | ||||
| @@ -48,12 +47,8 @@ const SearchImages = ({ | ||||
|                 query: query, | ||||
|                 chatHistory: chatHistory, | ||||
|                 chatModel: { | ||||
|                   provider: chatModelProvider, | ||||
|                   model: chatModel, | ||||
|                   ...(chatModelProvider === 'custom_openai' && { | ||||
|                     customOpenAIBaseURL: customOpenAIBaseURL, | ||||
|                     customOpenAIKey: customOpenAIKey, | ||||
|                   }), | ||||
|                   providerId: chatModelProvider, | ||||
|                   key: chatModel, | ||||
|                 }, | ||||
|               }), | ||||
|             }); | ||||
|   | ||||
| @@ -48,11 +48,10 @@ const Searchvideos = ({ | ||||
|           onClick={async () => { | ||||
|             setLoading(true); | ||||
|  | ||||
|             const chatModelProvider = localStorage.getItem('chatModelProvider'); | ||||
|             const chatModel = localStorage.getItem('chatModel'); | ||||
|  | ||||
|             const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL'); | ||||
|             const customOpenAIKey = localStorage.getItem('openAIApiKey'); | ||||
|             const chatModelProvider = localStorage.getItem( | ||||
|               'chatModelProviderId', | ||||
|             ); | ||||
|             const chatModel = localStorage.getItem('chatModelKey'); | ||||
|  | ||||
|             const res = await fetch(`/api/videos`, { | ||||
|               method: 'POST', | ||||
| @@ -63,12 +62,8 @@ const Searchvideos = ({ | ||||
|                 query: query, | ||||
|                 chatHistory: chatHistory, | ||||
|                 chatModel: { | ||||
|                   provider: chatModelProvider, | ||||
|                   model: chatModel, | ||||
|                   ...(chatModelProvider === 'custom_openai' && { | ||||
|                     customOpenAIBaseURL: customOpenAIBaseURL, | ||||
|                     customOpenAIKey: customOpenAIKey, | ||||
|                   }), | ||||
|                   providerId: chatModelProvider, | ||||
|                   key: chatModel, | ||||
|                 }, | ||||
|               }), | ||||
|             }); | ||||
|   | ||||
							
								
								
									
										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; | ||||