Compare commits

..

8 Commits

Author SHA1 Message Date
haddadrm
07c1f3a647 Merge branch 'master' of https://github.com/haddadrm/Perplexica
chore: sync with upstream changes

Synced local repository with latest changes from upstream (ItzCrazyKns/Perplexica)
2025-04-10 09:13:17 +04:00
Rami
ddd0eb90c1 Merge branch 'ItzCrazyKns:master' into master 2025-04-10 09:09:51 +04:00
haddadrm
2589af828e update gitignore 2025-04-10 09:05:52 +04:00
haddadrm
6f69f6b539 fix: update batch delete URL format to match single delete format 2025-04-09 14:24:44 +04:00
haddadrm
e16ffdb392 Implement provider formatting improvements and fix client-side compatibility
- Add PROVIDER_INFO metadata to each provider file with proper display names
- Create centralized PROVIDER_METADATA in index.ts for consistent reference
- Update settings UI to use provider metadata for display names
- Fix client/server compatibility for Node.js modules in config.ts
2025-04-09 14:24:44 +04:00
haddadrm
70611cffc1 Feature: Add LM Studio provider integration - Added LM Studio provider to support OpenAI compatible API - Implemented chat and embeddings model loading - Updated config to include LM Studio API endpoint 2025-04-09 14:15:10 +04:00
haddadrm
51381827d0 Feature: Integrate Discover component with improved capabilities - Added category filters and personalization features - Implemented support for multiple languages and search engines - Added preferences saving - Fixed Chinese language search issue 2025-04-09 14:06:45 +04:00
haddadrm
1e87175b1b feat: Enhance Library component with search, selection, and batch deletion capabilities 2025-04-09 14:06:45 +04:00
121 changed files with 4357 additions and 10385 deletions

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 641 KiB

View File

@@ -4,7 +4,6 @@ on:
push: push:
branches: branches:
- master - master
- canary
release: release:
types: [published] types: [published]
@@ -44,19 +43,6 @@ jobs:
-t itzcrazykns1337/${IMAGE_NAME}:amd64 \ -t itzcrazykns1337/${IMAGE_NAME}:amd64 \
--push . --push .
- name: Build and push AMD64 Canary Docker image
if: github.ref == 'refs/heads/canary' && github.event_name == 'push'
run: |
DOCKERFILE=app.dockerfile
IMAGE_NAME=perplexica
docker buildx build --platform linux/amd64 \
--cache-from=type=registry,ref=itzcrazykns1337/${IMAGE_NAME}:canary-amd64 \
--cache-to=type=inline \
--provenance false \
-f $DOCKERFILE \
-t itzcrazykns1337/${IMAGE_NAME}:canary-amd64 \
--push .
- name: Build and push AMD64 release Docker image - name: Build and push AMD64 release Docker image
if: github.event_name == 'release' if: github.event_name == 'release'
run: | run: |
@@ -105,19 +91,6 @@ jobs:
-t itzcrazykns1337/${IMAGE_NAME}:arm64 \ -t itzcrazykns1337/${IMAGE_NAME}:arm64 \
--push . --push .
- name: Build and push ARM64 Canary Docker image
if: github.ref == 'refs/heads/canary' && github.event_name == 'push'
run: |
DOCKERFILE=app.dockerfile
IMAGE_NAME=perplexica
docker buildx build --platform linux/arm64 \
--cache-from=type=registry,ref=itzcrazykns1337/${IMAGE_NAME}:canary-arm64 \
--cache-to=type=inline \
--provenance false \
-f $DOCKERFILE \
-t itzcrazykns1337/${IMAGE_NAME}:canary-arm64 \
--push .
- name: Build and push ARM64 release Docker image - name: Build and push ARM64 release Docker image
if: github.event_name == 'release' if: github.event_name == 'release'
run: | run: |
@@ -155,15 +128,6 @@ jobs:
--amend itzcrazykns1337/${IMAGE_NAME}:arm64 --amend itzcrazykns1337/${IMAGE_NAME}:arm64
docker manifest push itzcrazykns1337/${IMAGE_NAME}:main docker manifest push itzcrazykns1337/${IMAGE_NAME}:main
- name: Create and push multi-arch manifest for canary
if: github.ref == 'refs/heads/canary' && github.event_name == 'push'
run: |
IMAGE_NAME=perplexica
docker manifest create itzcrazykns1337/${IMAGE_NAME}:canary \
--amend itzcrazykns1337/${IMAGE_NAME}:canary-amd64 \
--amend itzcrazykns1337/${IMAGE_NAME}:canary-arm64
docker manifest push itzcrazykns1337/${IMAGE_NAME}:canary
- name: Create and push multi-arch manifest for releases - name: Create and push multi-arch manifest for releases
if: github.event_name == 'release' if: github.event_name == 'release'
run: | run: |

3
.gitignore vendored
View File

@@ -37,5 +37,4 @@ Thumbs.db
# Db # Db
db.sqlite db.sqlite
/searxng /searxng
.qodo
certificates

View File

@@ -36,7 +36,7 @@ Before diving into coding, setting up your local environment is key. Here's what
1. In the root directory, locate the `sample.config.toml` file. 1. In the root directory, locate the `sample.config.toml` file.
2. Rename it to `config.toml` and fill in the necessary configuration fields. 2. Rename it to `config.toml` and fill in the necessary configuration fields.
3. Run `npm install` to install all dependencies. 3. Run `npm install` to install all dependencies.
4. Run `npm run db:migrate` to set up the local sqlite database. 4. Run `npm run db:push` to set up the local sqlite database.
5. Use `npm run dev` to start the application in development mode. 5. Use `npm run dev` to start the application in development mode.
**Please note**: Docker configurations are present for setting up production environments, whereas `npm run dev` is used for development purposes. **Please note**: Docker configurations are present for setting up production environments, whereas `npm run dev` is used for development purposes.

View File

@@ -16,7 +16,7 @@
<hr/> <hr/>
[![Discord](https://dcbadge.limes.pink/api/server/26aArMy8tT?style=flat)](https://discord.gg/26aArMy8tT) [![Discord](https://dcbadge.vercel.app/api/server/26aArMy8tT?style=flat&compact=true)](https://discord.gg/26aArMy8tT)
![preview](.assets/perplexica-screenshot.png?) ![preview](.assets/perplexica-screenshot.png?)
@@ -29,7 +29,6 @@
- [Getting Started with Docker (Recommended)](#getting-started-with-docker-recommended) - [Getting Started with Docker (Recommended)](#getting-started-with-docker-recommended)
- [Non-Docker Installation](#non-docker-installation) - [Non-Docker Installation](#non-docker-installation)
- [Ollama Connection Errors](#ollama-connection-errors) - [Ollama Connection Errors](#ollama-connection-errors)
- [Lemonade Connection Errors](#lemonade-connection-errors)
- [Using as a Search Engine](#using-as-a-search-engine) - [Using as a Search Engine](#using-as-a-search-engine)
- [Using Perplexica's API](#using-perplexicas-api) - [Using Perplexica's API](#using-perplexicas-api)
- [Expose Perplexica to a network](#expose-perplexica-to-network) - [Expose Perplexica to a network](#expose-perplexica-to-network)
@@ -54,7 +53,7 @@ Want to know more about its architecture and how it works? You can read it [here
## Features ## Features
- **Local LLMs**: You can utilize local LLMs such as Qwen, DeepSeek, Llama, and Mistral. - **Local LLMs**: You can make use local LLMs such as Llama3 and Mixtral using Ollama.
- **Two Main Modes:** - **Two Main Modes:**
- **Copilot Mode:** (In development) Boosts search by generating different queries to find more relevant internet sources. Like normal search instead of just using the context by SearxNG, it visits the top matches and tries to find relevant sources to the user's query directly from the page. - **Copilot Mode:** (In development) Boosts search by generating different queries to find more relevant internet sources. Like normal search instead of just using the context by SearxNG, it visits the top matches and tries to find relevant sources to the user's query directly from the page.
- **Normal Mode:** Processes your query and performs a web search. - **Normal Mode:** Processes your query and performs a web search.
@@ -88,14 +87,9 @@ There are mainly 2 ways of installing Perplexica - With Docker, Without Docker.
4. Rename the `sample.config.toml` file to `config.toml`. For Docker setups, you need only fill in the following fields: 4. Rename the `sample.config.toml` file to `config.toml`. For Docker setups, you need only fill in the following fields:
- `OPENAI`: Your OpenAI API key. **You only need to fill this if you wish to use OpenAI's models**. - `OPENAI`: Your OpenAI API key. **You only need to fill this if you wish to use OpenAI's models**.
- `CUSTOM_OPENAI`: Your OpenAI-API-compliant local server URL, model name, and API key. You should run your local server with host set to `0.0.0.0`, take note of which port number it is running on, and then use that port number to set `API_URL = http://host.docker.internal:PORT_NUMBER`. You must specify the model name, such as `MODEL_NAME = "unsloth/DeepSeek-R1-0528-Qwen3-8B-GGUF:Q4_K_XL"`. Finally, set `API_KEY` to the appropriate value. If you have not defined an API key, just put anything you want in-between the quotation marks: `API_KEY = "whatever-you-want-but-not-blank"` **You only need to configure these settings if you want to use a local OpenAI-compliant server, such as Llama.cpp's [`llama-server`](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md)**.
- `OLLAMA`: Your Ollama API URL. You should enter it as `http://host.docker.internal:PORT_NUMBER`. If you installed Ollama on port 11434, use `http://host.docker.internal:11434`. For other ports, adjust accordingly. **You need to fill this if you wish to use Ollama's models instead of OpenAI's**. - `OLLAMA`: Your Ollama API URL. You should enter it as `http://host.docker.internal:PORT_NUMBER`. If you installed Ollama on port 11434, use `http://host.docker.internal:11434`. For other ports, adjust accordingly. **You need to fill this if you wish to use Ollama's models instead of OpenAI's**.
- `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**.
- `GROQ`: Your Groq API key. **You only need to fill this if you wish to use Groq's hosted models**.`
- `ANTHROPIC`: Your Anthropic API key. **You only need to fill this if you wish to use Anthropic models**. - `ANTHROPIC`: Your Anthropic API key. **You only need to fill this if you wish to use Anthropic models**.
- `Gemini`: Your Gemini API key. **You only need to fill this if you wish to use Google's models**.
- `DEEPSEEK`: Your Deepseek API key. **Only needed if you want Deepseek models.**
- `AIMLAPI`: Your AI/ML API key. **Only needed if you want to use AI/ML API models and embeddings.**
**Note**: You can change these after starting Perplexica from the settings dialog. **Note**: You can change these after starting Perplexica from the settings dialog.
@@ -117,23 +111,13 @@ There are mainly 2 ways of installing Perplexica - With Docker, Without Docker.
2. Clone the repository and rename the `sample.config.toml` file to `config.toml` in the root directory. Ensure you complete all required fields in this file. 2. Clone the repository and rename the `sample.config.toml` file to `config.toml` in the root directory. Ensure you complete all required fields in this file.
3. After populating the configuration run `npm i`. 3. After populating the configuration run `npm i`.
4. Install the dependencies and then execute `npm run build`. 4. Install the dependencies and then execute `npm run build`.
5. Finally, start the app by running `npm run start` 5. Finally, start the app by running `npm rum start`
**Note**: Using Docker is recommended as it simplifies the setup process, especially for managing environment variables and dependencies. **Note**: Using Docker is recommended as it simplifies the setup process, especially for managing environment variables and dependencies.
See the [installation documentation](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/installation) for more information like updating, etc. See the [installation documentation](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/installation) for more information like updating, etc.
### Troubleshooting ### Ollama Connection Errors
#### Local OpenAI-API-Compliant Servers
If Perplexica tells you that you haven't configured any chat model providers, ensure that:
1. Your server is running on `0.0.0.0` (not `127.0.0.1`) and on the same port you put in the API URL.
2. You have specified the correct model name loaded by your local LLM server.
3. You have specified the correct API key, or if one is not defined, you have put _something_ in the API key field and not left it empty.
#### Ollama Connection Errors
If you're encountering an Ollama connection error, it is likely due to the backend being unable to connect to Ollama's API. To fix this issue you can: If you're encountering an Ollama connection error, it is likely due to the backend being unable to connect to Ollama's API. To fix this issue you can:
@@ -148,29 +132,10 @@ If you're encountering an Ollama connection error, it is likely due to the backe
3. **Linux Users - Expose Ollama to Network:** 3. **Linux Users - Expose Ollama to Network:**
- Inside `/etc/systemd/system/ollama.service`, you need to add `Environment="OLLAMA_HOST=0.0.0.0: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) - 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)
- Ensure that the port (default is 11434) is not blocked by your firewall. - Ensure that the port (default is 11434) is not blocked by your firewall.
#### Lemonade Connection Errors
If you're encountering a Lemonade connection error, it is likely due to the backend being unable to connect to Lemonade's API. To fix this issue you can:
1. **Check your Lemonade API URL:** Ensure that the API URL is correctly set in the settings menu.
2. **Update API URL Based on OS:**
- **Windows:** Use `http://host.docker.internal:8000`
- **Mac:** Use `http://host.docker.internal:8000`
- **Linux:** Use `http://<private_ip_of_host>:8000`
Adjust the port number if you're using a different one.
3. **Ensure Lemonade Server is Running:**
- Make sure your Lemonade server is running and accessible on the configured port (default is 8000).
- Verify that Lemonade is configured to accept connections from all interfaces (`0.0.0.0`), not just localhost (`127.0.0.1`).
- Ensure that the port (default is 8000) is not blocked by your firewall.
## Using as a Search Engine ## Using as a Search Engine
If you wish to use Perplexica as an alternative to traditional search engines like Google or Bing, or if you want to add a shortcut for quick access from your browser's search bar, follow these steps: If you wish to use Perplexica as an alternative to traditional search engines like Google or Bing, or if you want to add a shortcut for quick access from your browser's search bar, follow these steps:
@@ -194,8 +159,6 @@ Perplexica runs on Next.js and handles all API requests. It works right away on
[![Deploy to Sealos](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://usw.sealos.io/?openapp=system-template%3FtemplateName%3Dperplexica) [![Deploy to Sealos](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://usw.sealos.io/?openapp=system-template%3FtemplateName%3Dperplexica)
[![Deploy to RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploylobe.svg)](https://repocloud.io/details/?app_id=267) [![Deploy to RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploylobe.svg)](https://repocloud.io/details/?app_id=267)
[![Run on ClawCloud](https://raw.githubusercontent.com/ClawCloud/Run-Template/refs/heads/main/Run-on-ClawCloud.svg)](https://template.run.claw.cloud/?referralCode=U11MRQ8U9RM4&openapp=system-fastdeploy%3FtemplateName%3Dperplexica)
[![Deploy on Hostinger](https://assets.hostinger.com/vps/deploy.svg)](https://www.hostinger.com/vps/docker-hosting?compose_url=https://raw.githubusercontent.com/ItzCrazyKns/Perplexica/refs/heads/master/docker-compose.yaml)
## Upcoming Features ## Upcoming Features

View File

@@ -1,6 +1,4 @@
FROM node:24.5.0-slim AS builder FROM node:20.18.0-slim AS builder
RUN apt-get update && apt-get install -y python3 python3-pip sqlite3 && rm -rf /var/lib/apt/lists/*
WORKDIR /home/perplexica WORKDIR /home/perplexica
@@ -10,17 +8,11 @@ RUN yarn install --frozen-lockfile --network-timeout 600000
COPY tsconfig.json next.config.mjs next-env.d.ts postcss.config.js drizzle.config.ts tailwind.config.ts ./ COPY tsconfig.json next.config.mjs next-env.d.ts postcss.config.js drizzle.config.ts tailwind.config.ts ./
COPY src ./src COPY src ./src
COPY public ./public COPY public ./public
COPY drizzle ./drizzle
RUN mkdir -p /home/perplexica/data RUN mkdir -p /home/perplexica/data
RUN yarn build RUN yarn build
RUN yarn add --dev @vercel/ncc FROM node:20.18.0-slim
RUN yarn ncc build ./src/lib/db/migrate.ts -o migrator
FROM node:24.5.0-slim
RUN apt-get update && apt-get install -y python3 python3-pip sqlite3 && rm -rf /var/lib/apt/lists/*
WORKDIR /home/perplexica WORKDIR /home/perplexica
@@ -29,14 +21,7 @@ COPY --from=builder /home/perplexica/.next/static ./public/_next/static
COPY --from=builder /home/perplexica/.next/standalone ./ COPY --from=builder /home/perplexica/.next/standalone ./
COPY --from=builder /home/perplexica/data ./data COPY --from=builder /home/perplexica/data ./data
COPY drizzle ./drizzle
COPY --from=builder /home/perplexica/migrator/build ./build
COPY --from=builder /home/perplexica/migrator/index.js ./migrate.js
RUN mkdir /home/perplexica/uploads RUN mkdir /home/perplexica/uploads
COPY entrypoint.sh ./entrypoint.sh CMD ["node", "server.js"]
RUN chmod +x ./entrypoint.sh
RUN sed -i 's/\r$//' ./entrypoint.sh || true
CMD ["/home/perplexica/entrypoint.sh"]

View File

@@ -16,7 +16,6 @@ services:
dockerfile: app.dockerfile dockerfile: app.dockerfile
environment: environment:
- SEARXNG_API_URL=http://searxng:8080 - SEARXNG_API_URL=http://searxng:8080
- DATA_DIR=/home/perplexica
ports: ports:
- 3000:3000 - 3000:3000
networks: networks:

View File

@@ -41,6 +41,6 @@ To update Perplexica to the latest version, follow these steps:
3. Check for changes in the configuration files. If the `sample.config.toml` file contains new fields, delete your existing `config.toml` file, rename `sample.config.toml` to `config.toml`, and update the configuration accordingly. 3. Check for changes in the configuration files. If the `sample.config.toml` file contains new fields, delete your existing `config.toml` file, rename `sample.config.toml` to `config.toml`, and update the configuration accordingly.
4. After populating the configuration run `npm i`. 4. After populating the configuration run `npm i`.
5. Install the dependencies and then execute `npm run build`. 5. Install the dependencies and then execute `npm run build`.
6. Finally, start the app by running `npm run start` 6. Finally, start the app by running `npm rum start`
--- ---

View File

@@ -1,11 +1,10 @@
import { defineConfig } from 'drizzle-kit'; import { defineConfig } from 'drizzle-kit';
import path from 'path';
export default defineConfig({ export default defineConfig({
dialect: 'sqlite', dialect: 'sqlite',
schema: './src/lib/db/schema.ts', schema: './src/lib/db/schema.ts',
out: './drizzle', out: './drizzle',
dbCredentials: { dbCredentials: {
url: path.join(process.cwd(), 'data', 'db.sqlite'), url: './data/db.sqlite',
}, },
}); });

View File

@@ -1,16 +0,0 @@
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
);

View File

@@ -1 +0,0 @@
/* Do nothing */

View File

@@ -1,116 +0,0 @@
{
"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": {}
}
}

View File

@@ -1,125 +0,0 @@
{
"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": {}
}
}

View File

@@ -1,20 +0,0 @@
{
"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
}
]
}

View File

@@ -1,6 +0,0 @@
#!/bin/sh
set -e
node migrate.js
exec node server.js

View File

@@ -1,27 +1,25 @@
{ {
"name": "perplexica-frontend", "name": "perplexica-frontend",
"version": "1.11.0-rc3", "version": "1.10.2",
"license": "MIT", "license": "MIT",
"author": "ItzCrazyKns", "author": "ItzCrazyKns",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "npm run db:migrate && next build", "build": "npm run db:push && next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"format:write": "prettier . --write", "format:write": "prettier . --write",
"db:migrate": "node ./src/lib/db/migrate.ts" "db:push": "drizzle-kit push"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.0", "@headlessui/react": "^2.2.0",
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@icons-pack/react-simple-icons": "^12.3.0", "@icons-pack/react-simple-icons": "^12.3.0",
"@langchain/anthropic": "^0.3.24", "@langchain/anthropic": "^0.3.15",
"@langchain/community": "^0.3.49", "@langchain/community": "^0.3.36",
"@langchain/core": "^0.3.66", "@langchain/core": "^0.3.42",
"@langchain/google-genai": "^0.2.15", "@langchain/google-genai": "^0.1.12",
"@langchain/groq": "^0.2.3", "@langchain/openai": "^0.0.25",
"@langchain/ollama": "^0.2.3",
"@langchain/openai": "^0.6.2",
"@langchain/textsplitters": "^0.1.0", "@langchain/textsplitters": "^0.1.0",
"@tailwindcss/typography": "^0.5.12", "@tailwindcss/typography": "^0.5.12",
"@xenova/transformers": "^2.17.2", "@xenova/transformers": "^2.17.2",
@@ -32,10 +30,8 @@
"compute-dot": "^1.1.0", "compute-dot": "^1.1.0",
"drizzle-orm": "^0.40.1", "drizzle-orm": "^0.40.1",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"jspdf": "^3.0.1", "langchain": "^0.1.30",
"langchain": "^0.3.30",
"lucide-react": "^0.363.0", "lucide-react": "^0.363.0",
"mammoth": "^1.9.1",
"markdown-to-jsx": "^7.7.2", "markdown-to-jsx": "^7.7.2",
"next": "^15.2.2", "next": "^15.2.2",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
@@ -53,7 +49,6 @@
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "^7.6.12", "@types/better-sqlite3": "^7.6.12",
"@types/html-to-text": "^9.0.4", "@types/html-to-text": "^9.0.4",
"@types/jspdf": "^2.0.0",
"@types/node": "^20", "@types/node": "^20",
"@types/pdf-parse": "^1.1.4", "@types/pdf-parse": "^1.1.4",
"@types/react": "^18", "@types/react": "^18",

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 916 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 515 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 627 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

View File

@@ -1,131 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -1,159 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -1,178 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -1,206 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -1,244 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 8.0 KiB

View File

@@ -1,309 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,204 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -1,269 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,141 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -1,179 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -1,243 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,204 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -1,256 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,206 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -1,270 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,374 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,283 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,307 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,241 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 9.6 KiB

View File

@@ -1,269 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,273 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,301 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,334 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,361 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -25,15 +25,8 @@ API_URL = "" # Ollama API URL - http://host.docker.internal:11434
[MODELS.DEEPSEEK] [MODELS.DEEPSEEK]
API_KEY = "" API_KEY = ""
[MODELS.AIMLAPI]
API_KEY = "" # Required to use AI/ML API chat and embedding models
[MODELS.LM_STUDIO] [MODELS.LM_STUDIO]
API_URL = "" # LM Studio API URL - http://host.docker.internal:1234 API_URL = "" # LM Studio API URL - http://host.docker.internal:1234
[MODELS.LEMONADE]
API_URL = "" # Lemonade API URL - http://host.docker.internal:8000
API_KEY = "" # Optional API key for Lemonade
[API_ENDPOINTS] [API_ENDPOINTS]
SEARXNG = "" # SearxNG API URL - http://localhost:32768 SEARXNG = "" # SearxNG API URL - http://localhost:32768

View File

@@ -1,7 +1,11 @@
import prompts from '@/lib/prompts';
import MetaSearchAgent from '@/lib/search/metaSearchAgent';
import crypto from 'crypto'; import crypto from 'crypto';
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
import { EventEmitter } from 'stream'; import { EventEmitter } from 'stream';
import { import {
chatModelProviders,
embeddingModelProviders,
getAvailableChatModelProviders, getAvailableChatModelProviders,
getAvailableEmbeddingModelProviders, getAvailableEmbeddingModelProviders,
} from '@/lib/providers'; } from '@/lib/providers';
@@ -17,81 +21,46 @@ import {
getCustomOpenaiModelName, getCustomOpenaiModelName,
} from '@/lib/config'; } from '@/lib/config';
import { searchHandlers } from '@/lib/search'; import { searchHandlers } from '@/lib/search';
import { z } from 'zod';
export const runtime = 'nodejs'; export const runtime = 'nodejs';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
const messageSchema = z.object({ type Message = {
messageId: z.string().min(1, 'Message ID is required'), messageId: string;
chatId: z.string().min(1, 'Chat ID is required'), chatId: string;
content: z.string().min(1, 'Message content is required'), content: string;
}); };
const chatModelSchema = z.object({ type ChatModel = {
provider: z.string().optional(), provider: string;
name: z.string().optional(), name: string;
}); };
const embeddingModelSchema = z.object({ type EmbeddingModel = {
provider: z.string().optional(), provider: string;
name: z.string().optional(), name: string;
}); };
const bodySchema = z.object({ type Body = {
message: messageSchema, message: Message;
optimizationMode: z.enum(['speed', 'balanced', 'quality'], { optimizationMode: 'speed' | 'balanced' | 'quality';
errorMap: () => ({ focusMode: string;
message: 'Optimization mode must be one of: speed, balanced, quality', history: Array<[string, string]>;
}), files: Array<string>;
}), chatModel: ChatModel;
focusMode: z.string().min(1, 'Focus mode is required'), embeddingModel: EmbeddingModel;
history: z systemInstructions: string;
.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.optional().default({}),
embeddingModel: embeddingModelSchema.optional().default({}),
systemInstructions: z.string().nullable().optional().default(''),
});
type Message = z.infer<typeof messageSchema>;
type Body = z.infer<typeof bodySchema>;
const safeValidateBody = (data: unknown) => {
const result = bodySchema.safeParse(data);
if (!result.success) {
return {
success: false,
error: result.error.errors.map((e) => ({
path: e.path.join('.'),
message: e.message,
})),
};
}
return {
success: true,
data: result.data,
};
}; };
const handleEmitterEvents = async ( const handleEmitterEvents = async (
stream: EventEmitter, stream: EventEmitter,
writer: WritableStreamDefaultWriter, writer: WritableStreamDefaultWriter,
encoder: TextEncoder, encoder: TextEncoder,
aiMessageId: string,
chatId: string, chatId: string,
) => { ) => {
let recievedMessage = ''; let recievedMessage = '';
const aiMessageId = crypto.randomBytes(7).toString('hex'); let sources: any[] = [];
stream.on('data', (data) => { stream.on('data', (data) => {
const parsedData = JSON.parse(data); const parsedData = JSON.parse(data);
@@ -118,17 +87,7 @@ const handleEmitterEvents = async (
), ),
); );
const sourceMessageId = crypto.randomBytes(7).toString('hex'); sources = parsedData.data;
db.insert(messagesSchema)
.values({
chatId: chatId,
messageId: sourceMessageId,
role: 'source',
sources: parsedData.data,
createdAt: new Date().toString(),
})
.execute();
} }
}); });
stream.on('end', () => { stream.on('end', () => {
@@ -136,6 +95,7 @@ const handleEmitterEvents = async (
encoder.encode( encoder.encode(
JSON.stringify({ JSON.stringify({
type: 'messageEnd', type: 'messageEnd',
messageId: aiMessageId,
}) + '\n', }) + '\n',
), ),
); );
@@ -147,7 +107,10 @@ const handleEmitterEvents = async (
chatId: chatId, chatId: chatId,
messageId: aiMessageId, messageId: aiMessageId,
role: 'assistant', role: 'assistant',
createdAt: new Date().toString(), metadata: JSON.stringify({
createdAt: new Date(),
...(sources && sources.length > 0 && { sources }),
}),
}) })
.execute(); .execute();
}); });
@@ -175,8 +138,6 @@ const handleHistorySave = async (
where: eq(chats.id, message.chatId), where: eq(chats.id, message.chatId),
}); });
const fileData = files.map(getFileDetails);
if (!chat) { if (!chat) {
await db await db
.insert(chats) .insert(chats)
@@ -185,15 +146,9 @@ const handleHistorySave = async (
title: message.content, title: message.content,
createdAt: new Date().toString(), createdAt: new Date().toString(),
focusMode: focusMode, focusMode: focusMode,
files: fileData,
})
.execute();
} else if (JSON.stringify(chat.files ?? []) != JSON.stringify(fileData)) {
db.update(chats)
.set({
files: files.map(getFileDetails), files: files.map(getFileDetails),
}) })
.where(eq(chats.id, message.chatId)); .execute();
} }
const messageExists = await db.query.messages.findFirst({ const messageExists = await db.query.messages.findFirst({
@@ -208,7 +163,9 @@ const handleHistorySave = async (
chatId: message.chatId, chatId: message.chatId,
messageId: humanMessageId, messageId: humanMessageId,
role: 'user', role: 'user',
createdAt: new Date().toString(), metadata: JSON.stringify({
createdAt: new Date(),
}),
}) })
.execute(); .execute();
} else { } else {
@@ -226,17 +183,7 @@ const handleHistorySave = async (
export const POST = async (req: Request) => { export const POST = async (req: Request) => {
try { try {
const reqBody = (await req.json()) as Body; const body = (await req.json()) as Body;
const parseBody = safeValidateBody(reqBody);
if (!parseBody.success) {
return Response.json(
{ message: 'Invalid request body', error: parseBody.error },
{ status: 400 },
);
}
const body = parseBody.data as Body;
const { message } = body; const { message } = body;
if (message.content === '') { if (message.content === '') {
@@ -276,7 +223,7 @@ export const POST = async (req: Request) => {
if (body.chatModel?.provider === 'custom_openai') { if (body.chatModel?.provider === 'custom_openai') {
llm = new ChatOpenAI({ llm = new ChatOpenAI({
apiKey: getCustomOpenaiApiKey(), openAIApiKey: getCustomOpenaiApiKey(),
modelName: getCustomOpenaiModelName(), modelName: getCustomOpenaiModelName(),
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {
@@ -300,6 +247,7 @@ export const POST = async (req: Request) => {
const humanMessageId = const humanMessageId =
message.messageId ?? crypto.randomBytes(7).toString('hex'); message.messageId ?? crypto.randomBytes(7).toString('hex');
const aiMessageId = crypto.randomBytes(7).toString('hex');
const history: BaseMessage[] = body.history.map((msg) => { const history: BaseMessage[] = body.history.map((msg) => {
if (msg[0] === 'human') { if (msg[0] === 'human') {
@@ -331,14 +279,14 @@ export const POST = async (req: Request) => {
embedding, embedding,
body.optimizationMode, body.optimizationMode,
body.files, body.files,
body.systemInstructions as string, body.systemInstructions,
); );
const responseStream = new TransformStream(); const responseStream = new TransformStream();
const writer = responseStream.writable.getWriter(); const writer = responseStream.writable.getWriter();
const encoder = new TextEncoder(); const encoder = new TextEncoder();
handleEmitterEvents(stream, writer, encoder, message.chatId); handleEmitterEvents(stream, writer, encoder, aiMessageId, message.chatId);
handleHistorySave(message, humanMessageId, body.focusMode, body.files); handleHistorySave(message, humanMessageId, body.focusMode, body.files);
return new Response(responseStream.readable, { return new Response(responseStream.readable, {

View File

@@ -8,12 +8,7 @@ import {
getOllamaApiEndpoint, getOllamaApiEndpoint,
getOpenaiApiKey, getOpenaiApiKey,
getDeepseekApiKey, getDeepseekApiKey,
getAimlApiKey,
getLMStudioApiEndpoint,
getLemonadeApiEndpoint,
getLemonadeApiKey,
updateConfig, updateConfig,
getOllamaApiKey,
} from '@/lib/config'; } from '@/lib/config';
import { import {
getAvailableChatModelProviders, getAvailableChatModelProviders,
@@ -56,15 +51,10 @@ export const GET = async (req: Request) => {
config['openaiApiKey'] = getOpenaiApiKey(); config['openaiApiKey'] = getOpenaiApiKey();
config['ollamaApiUrl'] = getOllamaApiEndpoint(); config['ollamaApiUrl'] = getOllamaApiEndpoint();
config['ollamaApiKey'] = getOllamaApiKey();
config['lmStudioApiUrl'] = getLMStudioApiEndpoint();
config['lemonadeApiUrl'] = getLemonadeApiEndpoint();
config['lemonadeApiKey'] = getLemonadeApiKey();
config['anthropicApiKey'] = getAnthropicApiKey(); config['anthropicApiKey'] = getAnthropicApiKey();
config['groqApiKey'] = getGroqApiKey(); config['groqApiKey'] = getGroqApiKey();
config['geminiApiKey'] = getGeminiApiKey(); config['geminiApiKey'] = getGeminiApiKey();
config['deepseekApiKey'] = getDeepseekApiKey(); config['deepseekApiKey'] = getDeepseekApiKey();
config['aimlApiKey'] = getAimlApiKey();
config['customOpenaiApiUrl'] = getCustomOpenaiApiUrl(); config['customOpenaiApiUrl'] = getCustomOpenaiApiUrl();
config['customOpenaiApiKey'] = getCustomOpenaiApiKey(); config['customOpenaiApiKey'] = getCustomOpenaiApiKey();
config['customOpenaiModelName'] = getCustomOpenaiModelName(); config['customOpenaiModelName'] = getCustomOpenaiModelName();
@@ -99,21 +89,10 @@ export const POST = async (req: Request) => {
}, },
OLLAMA: { OLLAMA: {
API_URL: config.ollamaApiUrl, API_URL: config.ollamaApiUrl,
API_KEY: config.ollamaApiKey,
}, },
DEEPSEEK: { DEEPSEEK: {
API_KEY: config.deepseekApiKey, API_KEY: config.deepseekApiKey,
}, },
AIMLAPI: {
API_KEY: config.aimlApiKey,
},
LM_STUDIO: {
API_URL: config.lmStudioApiUrl,
},
LEMONADE: {
API_URL: config.lemonadeApiUrl,
API_KEY: config.lemonadeApiKey,
},
CUSTOM_OPENAI: { CUSTOM_OPENAI: {
API_URL: config.customOpenaiApiUrl, API_URL: config.customOpenaiApiUrl,
API_KEY: config.customOpenaiApiKey, API_KEY: config.customOpenaiApiKey,

View File

@@ -0,0 +1,124 @@
/**
* Default categories and functions for generating search queries
*/
import { LANGUAGE_SPECIFIC_SOURCES } from './languages';
/**
* Default English categories and their sources
*/
export const DEFAULT_CATEGORIES: Record<string, { site: string; keyword: string }[]> = {
'Technology': [
{ site: 'techcrunch.com', keyword: 'tech' },
{ site: 'wired.com', keyword: 'technology' },
{ site: 'theverge.com', keyword: 'tech' },
{ site: 'arstechnica.com', keyword: 'technology' },
{ site: 'thenextweb.com', keyword: 'tech' }
],
'AI': [
{ site: 'ai.googleblog.com', keyword: 'AI' },
{ site: 'openai.com/blog', keyword: 'AI' },
{ site: 'venturebeat.com', keyword: 'artificial intelligence' },
{ site: 'techcrunch.com', keyword: 'artificial intelligence' },
{ site: 'technologyreview.mit.edu', keyword: 'AI' }
],
'Sports': [
{ site: 'espn.com', keyword: 'sports' },
{ site: 'sports.yahoo.com', keyword: 'sports' },
{ site: 'cbssports.com', keyword: 'sports' },
{ site: 'si.com', keyword: 'sports' },
{ site: 'bleacherreport.com', keyword: 'sports' }
],
'Money': [
{ site: 'bloomberg.com', keyword: 'finance' },
{ site: 'cnbc.com', keyword: 'money' },
{ site: 'wsj.com', keyword: 'finance' },
{ site: 'ft.com', keyword: 'finance' },
{ site: 'economist.com', keyword: 'economy' }
],
'Gaming': [
{ site: 'ign.com', keyword: 'games' },
{ site: 'gamespot.com', keyword: 'gaming' },
{ site: 'polygon.com', keyword: 'games' },
{ site: 'kotaku.com', keyword: 'gaming' },
{ site: 'eurogamer.net', keyword: 'games' }
],
'Entertainment': [
{ site: 'variety.com', keyword: 'entertainment' },
{ site: 'hollywoodreporter.com', keyword: 'entertainment' },
{ site: 'ew.com', keyword: 'entertainment' },
{ site: 'deadline.com', keyword: 'entertainment' },
{ site: 'rollingstone.com', keyword: 'entertainment' }
],
'Art and Culture': [
{ site: 'artnews.com', keyword: 'art' },
{ site: 'artsy.net', keyword: 'art' },
{ site: 'theartnewspaper.com', keyword: 'art' },
{ site: 'nytimes.com/section/arts', keyword: 'culture' },
{ site: 'culturalweekly.com', keyword: 'culture' }
],
'Science': [
{ site: 'scientificamerican.com', keyword: 'science' },
{ site: 'nature.com', keyword: 'science' },
{ site: 'science.org', keyword: 'science' },
{ site: 'newscientist.com', keyword: 'science' },
{ site: 'popsci.com', keyword: 'science' }
],
'Health': [
{ site: 'webmd.com', keyword: 'health' },
{ site: 'health.harvard.edu', keyword: 'health' },
{ site: 'mayoclinic.org', keyword: 'health' },
{ site: 'nih.gov', keyword: 'health' },
{ site: 'medicalnewstoday.com', keyword: 'health' }
],
'Travel': [
{ site: 'travelandleisure.com', keyword: 'travel' },
{ site: 'lonelyplanet.com', keyword: 'travel' },
{ site: 'tripadvisor.com', keyword: 'travel' },
{ site: 'nationalgeographic.com', keyword: 'travel' },
{ site: 'cntraveler.com', keyword: 'travel' }
],
'Current News': [
{ site: 'reuters.com', keyword: 'news' },
{ site: 'apnews.com', keyword: 'news' },
{ site: 'bbc.com', keyword: 'news' },
{ site: 'npr.org', keyword: 'news' },
{ site: 'aljazeera.com', keyword: 'news' }
]
};
/**
* Helper function to get search queries for a category
* Prioritizes language-specific sources if available
*/
export function getSearchQueriesForCategory(category: string, language?: string): { site: string; keyword: string }[] {
// Check if we have language-specific sources for this language and category
if (language &&
LANGUAGE_SPECIFIC_SOURCES[language] &&
LANGUAGE_SPECIFIC_SOURCES[language][category]) {
return LANGUAGE_SPECIFIC_SOURCES[language][category];
}
// For Chinese variants, try the general zh sources
if (language &&
(language.startsWith('zh') || language.includes('Hans') || language.includes('Hant')) &&
LANGUAGE_SPECIFIC_SOURCES['zh'] &&
LANGUAGE_SPECIFIC_SOURCES['zh'][category]) {
return LANGUAGE_SPECIFIC_SOURCES['zh'][category];
}
// If no language-specific sources, use the default English sources
return DEFAULT_CATEGORIES[category] || DEFAULT_CATEGORIES['Technology'];
}
/**
* Default high-quality sources for the default view
*/
export const DEFAULT_SOURCES = [
{ site: 'techcrunch.com', keyword: 'tech' },
{ site: 'wired.com', keyword: 'technology' },
{ site: 'theverge.com', keyword: 'tech' },
{ site: 'venturebeat.com', keyword: 'artificial intelligence' },
{ site: 'technologyreview.mit.edu', keyword: 'AI' },
{ site: 'ai.googleblog.com', keyword: 'AI' }
];

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,128 @@
import db from "@/lib/db";
import { userPreferences } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
// GET handler to retrieve user preferences
export const GET = async (req: Request) => {
try {
console.log('[Preferences] Retrieving user preferences');
// In a production app, you would get user ID from an auth session
const url = new URL(req.url);
const userId = url.searchParams.get('userId') || "default-user";
console.log(`[Preferences] Fetching preferences for user: ${userId}`);
const userPrefs = await db.select().from(userPreferences).where(eq(userPreferences.userId, userId));
if (userPrefs.length === 0) {
console.log('[Preferences] No preferences found, returning defaults');
// Return default preferences if none exist
return Response.json({
categories: ['AI', 'Technology'],
languages: ['en'] // Default to English
});
}
// Handle backward compatibility for old schema versions
let languages = [];
if ('languages' in userPrefs[0] && userPrefs[0].languages) {
languages = userPrefs[0].languages;
} else if ('language' in userPrefs[0] && userPrefs[0].language) {
// Convert old single language to array for backward compatibility
languages = Array.isArray(userPrefs[0].language)
? userPrefs[0].language
: [userPrefs[0].language];
} else {
languages = ['en']; // Default to English if no language preference found
}
console.log(`[Preferences] Found user preferences: categories=${JSON.stringify(userPrefs[0].categories)}, languages=${JSON.stringify(languages)}`);
return Response.json({
categories: userPrefs[0].categories,
languages: languages
});
} catch (err: any) {
console.error(`[Preferences] Error getting user preferences: ${err instanceof Error ? err.message : String(err)}`);
console.error(`[Preferences] Error stack: ${err instanceof Error ? err.stack : 'No stack trace available'}`);
return Response.json(
{ message: 'An error has occurred' },
{ status: 500 }
);
}
};
// POST handler to save user preferences
export const POST = async (req: Request) => {
try {
console.log('[Preferences] Updating user preferences');
// In a production app, you would get user ID from an auth session
const url = new URL(req.url);
const userId = url.searchParams.get('userId') || "default-user";
const body = await req.json();
const { categories, languages } = body;
console.log(`[Preferences] Received update: userId=${userId}, categories=${JSON.stringify(categories)}, languages=${JSON.stringify(languages)}`);
if (!categories || !Array.isArray(categories)) {
console.error('[Preferences] Invalid categories format');
return Response.json(
{ message: 'Invalid categories format' },
{ status: 400 }
);
}
if (languages && !Array.isArray(languages)) {
console.error('[Preferences] Invalid languages format');
return Response.json(
{ message: 'Invalid languages format' },
{ status: 400 }
);
}
const userPrefs = await db.select().from(userPreferences).where(eq(userPreferences.userId, userId));
try {
if (userPrefs.length === 0) {
// Create new preferences
console.log(`[Preferences] Creating new preferences for user: ${userId}`);
await db.insert(userPreferences).values({
userId,
categories,
languages: languages || ['en'],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
} else {
// Update existing preferences
console.log(`[Preferences] Updating existing preferences for user: ${userId}`);
await db.update(userPreferences)
.set({
categories,
languages: languages || ['en'],
updatedAt: new Date().toISOString()
})
.where(eq(userPreferences.userId, userId));
}
console.log(`[Preferences] Successfully updated preferences for user: ${userId}`);
} catch (error: any) {
// If there's an error (likely due to schema mismatch), log it but don't fail
console.warn(`[Preferences] Error updating preferences with new schema: ${error instanceof Error ? error.message : String(error)}`);
console.warn('[Preferences] Continuing with request despite error');
// We'll just return success anyway since we can't fix the schema issue here
}
return Response.json({ message: 'Preferences updated successfully' });
} catch (err: any) {
console.error(`[Preferences] Error updating user preferences: ${err instanceof Error ? err.message : String(err)}`);
console.error(`[Preferences] Error stack: ${err instanceof Error ? err.stack : 'No stack trace available'}`);
return Response.json(
{ message: 'An error has occurred' },
{ status: 500 }
);
}
};

View File

@@ -1,91 +1,83 @@
import { searchSearxng } from '@/lib/searxng'; import { getSearchQueriesForCategory, DEFAULT_SOURCES } from './categories';
import { searchCategory, getDefaultResults, processResults } from './search';
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'],
},
};
type Topic = keyof typeof websitesForTopic;
export const GET = async (req: Request) => { export const GET = async (req: Request) => {
try { try {
const params = new URL(req.url).searchParams; const url = new URL(req.url);
const category = url.searchParams.get('category');
const preferencesParam = url.searchParams.get('preferences');
const languagesParam = url.searchParams.get('languages');
const mode: 'normal' | 'preview' = console.log(`[Discover] Request received: category=${category}, preferences=${preferencesParam}, languages=${languagesParam}`);
(params.get('mode') as 'normal' | 'preview') || 'normal';
const topic: Topic = (params.get('topic') as Topic) || 'tech';
const selectedTopic = websitesForTopic[topic]; let data: any[] = [];
let languages: string[] = [];
let data = []; // Parse languages parameter
if (languagesParam) {
if (mode === 'normal') { try {
const seenUrls = new Set(); const parsedLanguages = JSON.parse(languagesParam);
if (Array.isArray(parsedLanguages)) {
data = ( languages = parsedLanguages;
await Promise.all( }
selectedTopic.links.flatMap((link) => } catch (err) {
selectedTopic.query.map(async (query) => { console.error(`[Discover] Error parsing languages: ${err instanceof Error ? err.message : String(err)}`);
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:${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;
} }
console.log(`[Discover] Using languages: ${JSON.stringify(languages)}`);
// Handle category-specific searches
if (category && category !== 'For You') {
console.log(`[Discover] Searching for category: ${category}`);
data = await searchCategory(category, languages, getSearchQueriesForCategory);
}
// Handle preference-based searches
else if (preferencesParam) {
try {
const preferences = JSON.parse(preferencesParam);
if (Array.isArray(preferences) && preferences.length > 0) {
console.log(`[Discover] Searching for preferences: ${JSON.stringify(preferences)}`);
// Get content for each preferred category
const categoryPromises = preferences.map((pref: string) =>
searchCategory(pref, languages, getSearchQueriesForCategory)
);
const results = await Promise.all(categoryPromises);
data = results.flat();
} else {
console.log(`[Discover] No valid preferences found, using default search`);
// Fallback to default behavior
data = await getDefaultResults(languages, DEFAULT_SOURCES);
}
} catch (err) {
console.error(`[Discover] Error with preferences: ${err instanceof Error ? err.message : String(err)}`);
data = await getDefaultResults(languages, DEFAULT_SOURCES);
}
}
// Default search behavior
else {
console.log(`[Discover] Using default search`);
data = await getDefaultResults(languages, DEFAULT_SOURCES);
}
console.log(`[Discover] Found ${data.length} results before filtering`);
// Process and filter results for display
const finalData = processResults(data);
console.log(`[Discover] Found ${finalData.length} results after filtering`);
return Response.json( return Response.json(
{ {
blogs: data, blogs: finalData,
}, },
{ {
status: 200, status: 200,
}, },
); );
} catch (err) { } catch (err) {
console.error(`An error occurred in discover route: ${err}`); console.error(`[Discover] An error occurred in discover route: ${err instanceof Error ? err.message : String(err)}`);
console.error(`[Discover] Error stack: ${err instanceof Error ? err.stack : 'No stack trace available'}`);
return Response.json( return Response.json(
{ {
message: 'An error has occurred', message: 'An error has occurred',

View File

@@ -0,0 +1,173 @@
import { searchSearxng } from '@/lib/searxng';
import { LANGUAGE_SPECIFIC_ENGINES } from './languages';
// Define the search options interface to match the one in lib/searxng.ts
interface SearxngSearchOptions {
categories?: string[];
engines?: string[];
language?: string;
pageno?: number;
}
/**
* Default search engines to use, in priority order
*/
export const DEFAULT_ENGINES = ['bing news', 'brave news', 'duckduckgo news'];
/**
* Search with multiple engines as fallbacks
* Tries each engine in sequence until results are found or engines exhausted
*/
export async function searchWithMultipleEngines(
query: string,
language: string,
engines: string[] = DEFAULT_ENGINES
): Promise<any[]> {
let allResults: any[] = [];
let hasResults = false;
// Try each engine in sequence until we get results or run out of engines
for (const engine of engines) {
try {
console.log(`[Discover] Trying engine "${engine}" for query "${query}" in language "${language || 'default'}"`);
const searchOptions: SearxngSearchOptions = {
engines: [engine],
pageno: 1,
};
if (language) {
searchOptions.language = language;
}
const result = await searchSearxng(query, searchOptions);
if (result.results && result.results.length > 0) {
console.log(`[Discover] Found ${result.results.length} results from engine "${engine}"`);
allResults.push(...result.results);
hasResults = true;
// If we've found enough results, stop trying more engines
if (allResults.length >= 20) {
break;
}
} else {
console.log(`[Discover] No results from engine "${engine}", trying next engine if available`);
}
} catch (err) {
console.error(`[Discover] Error searching with engine "${engine}": ${err instanceof Error ? err.message : String(err)}`);
}
}
return allResults;
}
/**
* Search for a specific category across multiple languages and engines
*/
export async function searchCategory(
category: string,
languages: string[] = [],
getQueries: (cat: string, lang?: string) => { site: string; keyword: string }[]
): Promise<any[]> {
console.log(`[Discover] Searching category "${category}" in languages: ${JSON.stringify(languages)}`);
// If no languages specified or empty array, search in English
if (!languages || languages.length === 0) {
const queries = getQueries(category);
const searchPromises = queries.map(query =>
searchWithMultipleEngines(`site:${query.site} ${query.keyword}`, '')
);
const results = await Promise.all(searchPromises);
return results.flat();
}
// If languages specified, search each language and combine results
const allResults = [];
for (const language of languages) {
console.log(`[Discover] Searching in language: ${language}`);
// Get language-specific engines if available, otherwise use defaults
const engines = LANGUAGE_SPECIFIC_ENGINES[language] || DEFAULT_ENGINES;
// Get language-specific queries
const queries = getQueries(category, language);
const searchPromises = queries.map(query => {
// For Chinese languages, don't use the site: operator
const isChinese = language.startsWith('zh');
const queryString = isChinese ? query.keyword : `site:${query.site} ${query.keyword}`;
return searchWithMultipleEngines(queryString, language, engines);
});
const results = await Promise.all(searchPromises);
allResults.push(...results.flat());
}
return allResults;
}
/**
* Helper function for default search behavior that supports multiple languages
*/
export async function getDefaultResults(
languages: string[] = [],
defaultSources: { site: string; keyword: string }[]
): Promise<any[]> {
console.log(`[Discover] Getting default results for languages: ${JSON.stringify(languages)}`);
// If no languages specified, search with no language filter
if (languages.length === 0) {
const searchPromises = defaultSources.map(query =>
searchWithMultipleEngines(`site:${query.site} ${query.keyword}`, '')
);
const results = await Promise.all(searchPromises);
return results.flat();
}
// Otherwise, search each language separately and combine results
let allResults: any[] = [];
for (const language of languages) {
console.log(`[Discover] Default search in language: ${language}`);
// Get language-specific engines if available, otherwise use defaults
const engines = LANGUAGE_SPECIFIC_ENGINES[language] || DEFAULT_ENGINES;
const searchPromises = defaultSources.map(query => {
// For Chinese languages, don't use the site: operator
const isChinese = language.startsWith('zh');
const queryString = isChinese ? query.keyword : `site:${query.site} ${query.keyword}`;
return searchWithMultipleEngines(queryString, language, engines);
});
const results = await Promise.all(searchPromises);
allResults.push(...results.flat());
}
return allResults;
}
/**
* Process results to filter and prepare for display
*/
export function processResults(results: any[]): any[] {
// Filter out items without thumbnails
const resultsWithThumbnails = results.filter((item) => item.thumbnail);
// If there are no results with thumbnails but we have results without thumbnails,
// use some of the results without thumbnails rather than showing nothing
let finalResults = resultsWithThumbnails;
if (resultsWithThumbnails.length === 0 && results.length > 0) {
console.log(`[Discover] No results with thumbnails found, using up to 10 results without thumbnails`);
finalResults = results.slice(0, 10); // Limit to 10 results without thumbnails
} else {
finalResults = resultsWithThumbnails;
}
// Shuffle the results
return finalResults.sort(() => Math.random() - 0.5);
}

View File

@@ -49,7 +49,7 @@ export const POST = async (req: Request) => {
if (body.chatModel?.provider === 'custom_openai') { if (body.chatModel?.provider === 'custom_openai') {
llm = new ChatOpenAI({ llm = new ChatOpenAI({
apiKey: getCustomOpenaiApiKey(), openAIApiKey: getCustomOpenaiApiKey(),
modelName: getCustomOpenaiModelName(), modelName: getCustomOpenaiModelName(),
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {

View File

@@ -81,7 +81,8 @@ export const POST = async (req: Request) => {
if (body.chatModel?.provider === 'custom_openai') { if (body.chatModel?.provider === 'custom_openai') {
llm = new ChatOpenAI({ llm = new ChatOpenAI({
modelName: body.chatModel?.name || getCustomOpenaiModelName(), modelName: body.chatModel?.name || getCustomOpenaiModelName(),
apiKey: body.chatModel?.customOpenAIKey || getCustomOpenaiApiKey(), openAIApiKey:
body.chatModel?.customOpenAIKey || getCustomOpenaiApiKey(),
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {
baseURL: baseURL:

View File

@@ -48,7 +48,7 @@ export const POST = async (req: Request) => {
if (body.chatModel?.provider === 'custom_openai') { if (body.chatModel?.provider === 'custom_openai') {
llm = new ChatOpenAI({ llm = new ChatOpenAI({
apiKey: getCustomOpenaiApiKey(), openAIApiKey: getCustomOpenaiApiKey(),
modelName: getCustomOpenaiModelName(), modelName: getCustomOpenaiModelName(),
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {

View File

@@ -49,7 +49,7 @@ export const POST = async (req: Request) => {
if (body.chatModel?.provider === 'custom_openai') { if (body.chatModel?.provider === 'custom_openai') {
llm = new ChatOpenAI({ llm = new ChatOpenAI({
apiKey: getCustomOpenaiApiKey(), openAIApiKey: getCustomOpenaiApiKey(),
modelName: getCustomOpenaiModelName(), modelName: getCustomOpenaiModelName(),
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {

View File

@@ -1,174 +0,0 @@
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}&current=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,
},
);
}
};

View File

@@ -1,17 +1,9 @@
'use client';
import ChatWindow from '@/components/ChatWindow'; import ChatWindow from '@/components/ChatWindow';
import { useParams } from 'next/navigation';
import React from 'react'; import React from 'react';
import { ChatProvider } from '@/lib/hooks/useChat';
const Page = () => { const Page = ({ params }: { params: Promise<{ chatId: string }> }) => {
const { chatId }: { chatId: string } = useParams(); const { chatId } = React.use(params);
return ( return <ChatWindow id={chatId} />;
<ChatProvider id={chatId}>
<ChatWindow />
</ChatProvider>
);
}; };
export default Page; export default Page;

View File

@@ -1,270 +1,516 @@
'use client'; 'use client';
import { Globe2Icon } from 'lucide-react'; import { Search, Sliders, ChevronLeft, ChevronRight } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState, useRef, memo, useMemo } from 'react';
import Link from 'next/link';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { cn } from '@/lib/utils';
import SmallNewsCard from '@/components/Discover/SmallNewsCard';
import MajorNewsCard from '@/components/Discover/MajorNewsCard';
export interface Discover { interface Discover {
title: string; title: string;
content: string; content: string;
url: string; url: string;
thumbnail: string; thumbnail: string;
} }
const topics: { key: string; display: string }[] = [ const categories = [
{ 'For You', 'AI', 'Technology', 'Current News', 'Sports',
display: 'Tech & Science', 'Money', 'Gaming', 'Entertainment', 'Art and Culture',
key: 'tech', 'Science', 'Health', 'Travel'
},
{
display: 'Finance',
key: 'finance',
},
{
display: 'Art & Culture',
key: 'art',
},
{
display: 'Sports',
key: 'sports',
},
{
display: 'Entertainment',
key: 'entertainment',
},
]; ];
const Page = () => { // Header component with categories
const [discover, setDiscover] = useState<Discover[] | null>(null); const DiscoverHeader = memo(({
const [loading, setLoading] = useState(true); activeCategory,
const [activeTopic, setActiveTopic] = useState<string>(topics[0].key); setActiveCategory,
setShowPreferences,
userPreferences
}: {
activeCategory: string;
setActiveCategory: (category: string) => void;
setShowPreferences: (show: boolean) => void;
userPreferences: string[];
}) => {
const categoryContainerRef = useRef<HTMLDivElement>(null);
const fetchArticles = async (topic: string) => { // Filter categories to show only what the user has selected in preferences
setLoading(true); // Always include "For You" and the currently active category if it's not in preferences
try { const visibleCategories = useMemo(() => {
const res = await fetch(`/api/discover?topic=${topic}`, { // Always start with "For You"
method: 'GET', const filtered = ['For You'];
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json(); // Add user's preferred categories
userPreferences.forEach(category => {
if (!res.ok) { if (!filtered.includes(category)) {
throw new Error(data.message); filtered.push(category);
} }
});
data.blogs = data.blogs.filter((blog: Discover) => blog.thumbnail); // Add active category if it's not already included
if (activeCategory && !filtered.includes(activeCategory)) {
setDiscover(data.blogs); filtered.push(activeCategory);
} catch (err: any) {
console.error('Error fetching data:', err.message);
toast.error('Error fetching data');
} finally {
setLoading(false);
} }
// If user has no preferences, show a limited default set
if (filtered.length <= 1) {
return ['For You', 'AI', 'Technology', 'Current News'];
}
return filtered;
}, [userPreferences, activeCategory]);
const scrollCategories = (direction: 'left' | 'right') => {
const container = categoryContainerRef.current;
if (!container) return;
const scrollAmount = container.clientWidth * 0.8;
const currentScroll = container.scrollLeft;
container.scrollTo({
left: direction === 'left'
? Math.max(0, currentScroll - scrollAmount)
: currentScroll + scrollAmount,
behavior: 'smooth'
});
}; };
useEffect(() => {
fetchArticles(activeTopic);
}, [activeTopic]);
return ( return (
<> <div className="flex flex-col pt-4">
<div> <div className="flex items-center justify-between">
<div className="flex flex-col pt-10 border-b border-light-200/20 dark:border-dark-200/20 pb-6 px-2"> <div className="flex items-center">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4"> <Search />
<div className="flex items-center justify-center"> <h1 className="text-3xl font-medium p-2">Discover</h1>
<Globe2Icon size={45} className="mb-2.5" /> </div>
<h1 <button
className="text-5xl font-normal p-2" className="p-2 rounded-full bg-light-secondary dark:bg-dark-secondary hover:bg-light-primary hover:dark:bg-dark-primary transition-colors"
style={{ fontFamily: 'PP Editorial, serif' }} onClick={() => setShowPreferences(true)}
aria-label="Personalize"
>
<Sliders size={20} />
</button>
</div>
<div className="relative flex items-center py-4">
<button
className="absolute left-0 z-10 p-1 rounded-full bg-light-secondary dark:bg-dark-secondary hover:bg-light-primary/80 hover:dark:bg-dark-primary/80 transition-colors"
onClick={() => scrollCategories('left')}
aria-label="Scroll left"
>
<ChevronLeft size={20} />
</button>
<div
className="flex overflow-x-auto mx-8 no-scrollbar scroll-smooth"
ref={categoryContainerRef}
style={{ scrollbarWidth: 'none' }} // For Firefox
>
<div className="flex space-x-2">
{visibleCategories.map((category) => (
<button
key={category}
className={`px-4 py-2 rounded-full whitespace-nowrap transition-colors ${
activeCategory === category
? 'bg-light-primary dark:bg-dark-primary text-white'
: 'bg-light-secondary dark:bg-dark-secondary hover:bg-light-primary/80 hover:dark:bg-dark-primary/80'
}`}
onClick={() => setActiveCategory(category)}
> >
Discover {category}
</h1> </button>
</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>
</div> </div>
{loading ? ( <button
<div className="flex flex-row items-center justify-center min-h-screen"> className="absolute right-0 z-10 p-1 rounded-full bg-light-secondary dark:bg-dark-secondary hover:bg-light-primary/80 hover:dark:bg-dark-primary/80 transition-colors"
<svg onClick={() => scrollCategories('right')}
aria-hidden="true" aria-label="Scroll right"
className="w-8 h-8 text-light-200 fill-light-secondary dark:text-[#202020] animate-spin dark:fill-[#ffffff3b]" >
viewBox="0 0 100 101" <ChevronRight size={20} />
fill="none" </button>
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100.003 78.2051 78.1951 100.003 50.5908 100C22.9765 99.9972 0.997224 78.018 1 50.4037C1.00281 22.7993 22.8108 0.997224 50.4251 1C78.0395 1.00281 100.018 22.8108 100 50.4251ZM9.08164 50.594C9.06312 73.3997 27.7909 92.1272 50.5966 92.1457C73.4023 92.1642 92.1298 73.4365 92.1483 50.6308C92.1669 27.8251 73.4392 9.0973 50.6335 9.07878C27.8278 9.06026 9.10003 27.787 9.08164 50.594Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4037 97.8624 35.9116 96.9801 33.5533C95.1945 28.8227 92.871 24.3692 90.0681 20.348C85.6237 14.1775 79.4473 9.36872 72.0454 6.45794C64.6435 3.54717 56.3134 2.65431 48.3133 3.89319C45.869 4.27179 44.3768 6.77534 45.014 9.20079C45.6512 11.6262 48.1343 13.0956 50.5786 12.717C56.5073 11.8281 62.5542 12.5399 68.0406 14.7911C73.527 17.0422 78.2187 20.7487 81.5841 25.4923C83.7976 28.5886 85.4467 32.059 86.4416 35.7474C87.1273 38.1189 89.5423 39.6781 91.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
) : (
<div 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> </div>
</>
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
</div>
);
});
DiscoverHeader.displayName = 'DiscoverHeader';
// Content component that displays articles
const DiscoverContent = memo(({
activeCategory,
userPreferences,
preferredLanguages
}: {
activeCategory: string;
userPreferences: string[];
preferredLanguages: string[];
}) => {
const [discover, setDiscover] = useState<Discover[] | null>(null);
const [contentLoading, setContentLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
setContentLoading(true);
try {
let endpoint = `/api/discover`;
let params = [];
if (activeCategory !== 'For You') {
params.push(`category=${encodeURIComponent(activeCategory)}`);
} else if (userPreferences.length > 0) {
params.push(`preferences=${encodeURIComponent(JSON.stringify(userPreferences))}`);
}
if (preferredLanguages.length > 0) {
params.push(`languages=${encodeURIComponent(JSON.stringify(preferredLanguages))}`);
}
if (params.length > 0) {
endpoint += `?${params.join('&')}`;
}
const res = await fetch(endpoint, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.message);
}
// Filter out items without thumbnails (double-checking)
data.blogs = data.blogs.filter((blog: Discover) => blog.thumbnail);
setDiscover(data.blogs);
} catch (err: any) {
console.error('Error fetching data:', err.message);
toast.error('Error fetching data');
} finally {
setContentLoading(false);
}
};
fetchData();
}, [activeCategory, userPreferences, preferredLanguages]);
if (contentLoading) {
return (
<div className="flex flex-row items-center justify-center py-20">
<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>
);
}
if (!discover || discover.length === 0) {
return (
<div className="flex flex-row items-center justify-center min-h-[50vh]">
<p className="text-black/70 dark:text-white/70 text-sm">
No content found for this category.
</p>
</div>
);
}
return (
<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.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"
>
{/* Using img tag with URL processing for thumbnails */}
<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>
);
});
DiscoverContent.displayName = 'DiscoverContent';
// Preferences modal for personalization
const PreferencesModal = memo(({
showPreferences,
setShowPreferences,
userPreferences,
setUserPreferences,
preferredLanguages,
setPreferredLanguages,
setActiveCategory
}: {
showPreferences: boolean;
setShowPreferences: (show: boolean) => void;
userPreferences: string[];
setUserPreferences: (prefs: string[]) => void;
preferredLanguages: string[];
setPreferredLanguages: (langs: string[]) => void;
setActiveCategory: (category: string) => void;
}) => {
const [tempPreferences, setTempPreferences] = useState<string[]>([]);
const [tempLanguages, setTempLanguages] = useState<string[]>([]);
useEffect(() => {
if (showPreferences) {
setTempPreferences([...userPreferences]);
setTempLanguages([...preferredLanguages]);
}
}, [showPreferences, userPreferences, preferredLanguages]);
const saveUserPreferences = async (preferences: string[], languages: string[]) => {
try {
const res = await fetch(`/api/discover/preferences`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
categories: preferences,
languages
}),
});
if (res.ok) {
toast.success('Preferences saved successfully');
} else {
const data = await res.json();
throw new Error(data.message);
}
} catch (err: any) {
console.error('Error saving preferences:', err.message);
toast.error('Error saving preferences');
}
};
if (!showPreferences) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-[#1E1E1E] p-6 rounded-lg w-full max-w-md">
<h2 className="text-xl font-bold mb-4">Personalize Your Feed</h2>
<h3 className="font-medium mb-2">Select categories you&apos;re interested in:</h3>
<div className="grid grid-cols-2 gap-2 mb-6">
{categories.filter(c => c !== 'For You').map((category) => (
<button
key={category}
onClick={() => {
if (tempPreferences.includes(category)) {
setTempPreferences(tempPreferences.filter(p => p !== category));
} else {
setTempPreferences([...tempPreferences, category]);
}
}}
className={`px-3 py-2 rounded-md text-left transition-colors border ${
tempPreferences.includes(category)
? 'bg-blue-500 border-blue-500 text-white'
: 'bg-light-secondary dark:bg-dark-secondary border-gray-400 dark:border-gray-600 hover:border-blue-400 dark:hover:border-blue-400'
}`}
>
{category}
</button>
))}
</div>
<div className="mb-6">
<h3 className="font-medium mb-2">Preferred Languages</h3>
<div className="grid grid-cols-2 gap-2">
{[
{ code: 'en', name: 'English' },
{ code: 'ar', name: 'Arabic' },
{ code: 'zh', name: 'Chinese' },
{ code: 'fr', name: 'French' },
{ code: 'de', name: 'German' },
{ code: 'hi', name: 'Hindi' },
{ code: 'it', name: 'Italian' },
{ code: 'ja', name: 'Japanese' },
{ code: 'ko', name: 'Korean' },
{ code: 'pt', name: 'Portuguese' },
{ code: 'ru', name: 'Russian' },
{ code: 'es', name: 'Spanish' },
].map((language) => (
<button
key={language.code}
onClick={() => {
if (tempLanguages.includes(language.code)) {
setTempLanguages(tempLanguages.filter(l => l !== language.code));
} else {
setTempLanguages([...tempLanguages, language.code]);
}
}}
className={`px-3 py-2 rounded-md text-left transition-colors border ${
tempLanguages.includes(language.code)
? 'bg-blue-500 border-blue-500 text-white'
: 'bg-light-secondary dark:bg-dark-secondary border-gray-400 dark:border-gray-600 hover:border-blue-400 dark:hover:border-blue-400'
}`}
>
{language.name}
</button>
))}
</div>
<p className="text-sm text-gray-500 mt-2">
{tempLanguages.length === 0
? "No languages selected will show results in all languages"
: `Selected: ${tempLanguages.length} language(s)`}
</p>
</div>
<div className="flex justify-end space-x-2">
<button
className="px-4 py-2 rounded bg-gray-300 dark:bg-gray-700 hover:bg-gray-400 dark:hover:bg-gray-600 transition-colors"
onClick={() => {
setShowPreferences(false);
// Reset temp preferences
setTempPreferences([]);
setTempLanguages([]);
}}
>
Cancel
</button>
<button
className="px-4 py-2 rounded bg-light-primary dark:bg-dark-primary text-white hover:bg-light-primary/80 hover:dark:bg-dark-primary/80 transition-colors"
onClick={async () => {
await saveUserPreferences(tempPreferences, tempLanguages);
// Update the actual preferences after saving
setUserPreferences(tempPreferences);
setPreferredLanguages(tempLanguages);
setShowPreferences(false);
setActiveCategory('For You'); // Switch to For You view to show personalized content
// Reset temp preferences
setTempPreferences([]);
setTempLanguages([]);
}}
>
Save
</button>
</div>
</div>
</div>
);
});
PreferencesModal.displayName = 'PreferencesModal';
// Main page component
const Page = () => {
const [activeCategory, setActiveCategory] = useState('For You');
const [showPreferences, setShowPreferences] = useState(false);
const [userPreferences, setUserPreferences] = useState<string[]>(['AI', 'Technology']);
const [preferredLanguages, setPreferredLanguages] = useState<string[]>(['en']);
const [initialLoading, setInitialLoading] = useState(true);
// Load user preferences on initial render
useEffect(() => {
const loadUserPreferences = async () => {
try {
const res = await fetch(`/api/discover/preferences`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (res.ok) {
const data = await res.json();
setUserPreferences(data.categories || ['AI', 'Technology']);
setPreferredLanguages(data.languages || ['en']);
}
} catch (err: any) {
console.error('Error loading preferences:', err.message);
} finally {
setInitialLoading(false);
}
};
loadUserPreferences();
}, []);
if (initialLoading) {
return (
<div className="flex flex-row items-center justify-center min-h-screen">
<svg
aria-hidden="true"
className="w-8 h-8 text-light-200 fill-light-secondary dark:text-[#202020] animate-spin dark:fill-[#ffffff3b]"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100.003 78.2051 78.1951 100.003 50.5908 100C22.9765 99.9972 0.997224 78.018 1 50.4037C1.00281 22.7993 22.8108 0.997224 50.4251 1C78.0395 1.00281 100.018 22.8108 100 50.4251ZM9.08164 50.594C9.06312 73.3997 27.7909 92.1272 50.5966 92.1457C73.4023 92.1642 92.1298 73.4365 92.1483 50.6308C92.1669 27.8251 73.4392 9.0973 50.6335 9.07878C27.8278 9.06026 9.10003 27.787 9.08164 50.594Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4037 97.8624 35.9116 96.9801 33.5533C95.1945 28.8227 92.871 24.3692 90.0681 20.348C85.6237 14.1775 79.4473 9.36872 72.0454 6.45794C64.6435 3.54717 56.3134 2.65431 48.3133 3.89319C45.869 4.27179 44.3768 6.77534 45.014 9.20079C45.6512 11.6262 48.1343 13.0956 50.5786 12.717C56.5073 11.8281 62.5542 12.5399 68.0406 14.7911C73.527 17.0422 78.2187 20.7487 81.5841 25.4923C83.7976 28.5886 85.4467 32.059 86.4416 35.7474C87.1273 38.1189 89.5423 39.6781 91.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
);
}
return (
<div>
<DiscoverHeader
activeCategory={activeCategory}
setActiveCategory={setActiveCategory}
setShowPreferences={setShowPreferences}
userPreferences={userPreferences}
/>
<DiscoverContent
activeCategory={activeCategory}
userPreferences={userPreferences}
preferredLanguages={preferredLanguages}
/>
<PreferencesModal
showPreferences={showPreferences}
setShowPreferences={setShowPreferences}
userPreferences={userPreferences}
setUserPreferences={setUserPreferences}
preferredLanguages={preferredLanguages}
setPreferredLanguages={setPreferredLanguages}
setActiveCategory={setActiveCategory}
/>
</div>
); );
}; };

View File

@@ -2,14 +2,6 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@font-face {
font-family: 'PP Editorial';
src: url('/fonts/pp-ed-ul.otf') format('opentype');
font-weight: 200;
font-style: normal;
font-display: swap;
}
@layer base { @layer base {
.overflow-hidden-scrollable { .overflow-hidden-scrollable {
-ms-overflow-style: none; -ms-overflow-style: none;
@@ -19,20 +11,3 @@
display: none; display: none;
} }
} }
@layer utilities {
.line-clamp-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
}
@media screen and (-webkit-min-device-pixel-ratio: 0) {
select,
textarea,
input {
font-size: 16px !important;
}
}

View File

@@ -1,10 +1,12 @@
'use client'; 'use client';
import DeleteChat from '@/components/DeleteChat'; import DeleteChat from '@/components/DeleteChat';
import BatchDeleteChats from '@/components/BatchDeleteChats';
import { cn, formatTimeDifference } from '@/lib/utils'; import { cn, formatTimeDifference } from '@/lib/utils';
import { BookOpenText, ClockIcon, Delete, ScanEye } from 'lucide-react'; import { BookOpenText, Check, ClockIcon, Delete, ScanEye, Search, X } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'sonner';
export interface Chat { export interface Chat {
id: string; id: string;
@@ -15,7 +17,13 @@ export interface Chat {
const Page = () => { const Page = () => {
const [chats, setChats] = useState<Chat[]>([]); const [chats, setChats] = useState<Chat[]>([]);
const [filteredChats, setFilteredChats] = useState<Chat[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [selectionMode, setSelectionMode] = useState(false);
const [selectedChats, setSelectedChats] = useState<string[]>([]);
const [hoveredChatId, setHoveredChatId] = useState<string | null>(null);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
useEffect(() => { useEffect(() => {
const fetchChats = async () => { const fetchChats = async () => {
@@ -31,12 +39,71 @@ const Page = () => {
const data = await res.json(); const data = await res.json();
setChats(data.chats); setChats(data.chats);
setFilteredChats(data.chats);
setLoading(false); setLoading(false);
}; };
fetchChats(); fetchChats();
}, []); }, []);
useEffect(() => {
if (searchQuery.trim() === '') {
setFilteredChats(chats);
} else {
const filtered = chats.filter((chat) =>
chat.title.toLowerCase().includes(searchQuery.toLowerCase())
);
setFilteredChats(filtered);
}
}, [searchQuery, chats]);
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e.target.value);
};
const clearSearch = () => {
setSearchQuery('');
};
const toggleSelectionMode = () => {
setSelectionMode(!selectionMode);
setSelectedChats([]);
};
const toggleChatSelection = (chatId: string) => {
if (selectedChats.includes(chatId)) {
setSelectedChats(selectedChats.filter(id => id !== chatId));
} else {
setSelectedChats([...selectedChats, chatId]);
}
};
const selectAllChats = () => {
if (selectedChats.length === filteredChats.length) {
setSelectedChats([]);
} else {
setSelectedChats(filteredChats.map(chat => chat.id));
}
};
const deleteSelectedChats = () => {
if (selectedChats.length === 0) return;
setIsDeleteDialogOpen(true);
};
const handleBatchDeleteComplete = () => {
setSelectedChats([]);
setSelectionMode(false);
};
const updateChatsAfterDelete = (newChats: Chat[]) => {
setChats(newChats);
setFilteredChats(newChats.filter(chat =>
searchQuery.trim() === '' ||
chat.title.toLowerCase().includes(searchQuery.toLowerCase())
));
};
return loading ? ( return loading ? (
<div className="flex flex-row items-center justify-center min-h-screen"> <div className="flex flex-row items-center justify-center min-h-screen">
<svg <svg
@@ -64,32 +131,145 @@ const Page = () => {
<h1 className="text-3xl font-medium p-2">Library</h1> <h1 className="text-3xl font-medium p-2">Library</h1>
</div> </div>
<hr className="border-t border-[#2B2C2C] my-4 w-full" /> <hr className="border-t border-[#2B2C2C] my-4 w-full" />
{/* Search Box */}
<div className="relative mt-6 mb-6">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<Search className="w-5 h-5 text-black/50 dark:text-white/50" />
</div>
<input
type="text"
className="block w-full p-2 pl-10 pr-10 bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 rounded-md text-black dark:text-white focus:outline-none focus:ring-1 focus:ring-blue-500"
placeholder="Search your threads..."
value={searchQuery}
onChange={handleSearchChange}
/>
{searchQuery && (
<button
onClick={clearSearch}
className="absolute inset-y-0 right-0 flex items-center pr-3"
>
<X className="w-5 h-5 text-black/50 dark:text-white/50 hover:text-black dark:hover:text-white" />
</button>
)}
</div>
{/* Thread Count and Selection Controls */}
<div className="mb-4">
{!selectionMode ? (
<div className="flex items-center justify-between">
<div className="text-black/70 dark:text-white/70">
You have {chats.length} threads in Perplexica
</div>
<button
onClick={toggleSelectionMode}
className="text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white text-sm transition duration-200"
>
Select
</button>
</div>
) : (
<div className="flex items-center justify-between">
<div className="text-black/70 dark:text-white/70">
{selectedChats.length} selected thread{selectedChats.length !== 1 ? 's' : ''}
</div>
<div className="flex space-x-4">
<button
onClick={selectAllChats}
className="text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white text-sm transition duration-200"
>
{selectedChats.length === filteredChats.length ? 'Deselect all' : 'Select all'}
</button>
<button
onClick={toggleSelectionMode}
className="text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white text-sm transition duration-200"
>
Cancel
</button>
<button
onClick={deleteSelectedChats}
disabled={selectedChats.length === 0}
className={cn(
"text-sm transition duration-200",
selectedChats.length === 0
? "text-red-400/50 hover:text-red-500/50 cursor-not-allowed"
: "text-red-400 hover:text-red-500 cursor-pointer"
)}
>
Delete Selected
</button>
</div>
</div>
)}
</div>
</div> </div>
{chats.length === 0 && (
<div className="flex flex-row items-center justify-center min-h-screen"> {filteredChats.length === 0 && (
<div className="flex flex-row items-center justify-center min-h-[50vh]">
<p className="text-black/70 dark:text-white/70 text-sm"> <p className="text-black/70 dark:text-white/70 text-sm">
No chats found. {searchQuery ? 'No threads found matching your search.' : 'No threads found.'}
</p> </p>
</div> </div>
)} )}
{chats.length > 0 && (
{filteredChats.length > 0 && (
<div className="flex flex-col pb-20 lg:pb-2"> <div className="flex flex-col pb-20 lg:pb-2">
{chats.map((chat, i) => ( {filteredChats.map((chat, i) => (
<div <div
className={cn( className={cn(
'flex flex-col space-y-4 py-6', 'flex flex-col space-y-4 py-6',
i !== chats.length - 1 i !== filteredChats.length - 1
? 'border-b border-white-200 dark:border-dark-200' ? 'border-b border-white-200 dark:border-dark-200'
: '', : '',
)} )}
key={i} key={i}
onMouseEnter={() => setHoveredChatId(chat.id)}
onMouseLeave={() => setHoveredChatId(null)}
> >
<Link <div className="flex items-center">
href={`/c/${chat.id}`} {/* Checkbox - visible when in selection mode or when hovering */}
className="text-black dark:text-white lg:text-xl font-medium truncate transition duration-200 hover:text-[#24A0ED] dark:hover:text-[#24A0ED] cursor-pointer" {(selectionMode || hoveredChatId === chat.id) && (
> <div
{chat.title} className="mr-3 cursor-pointer"
</Link> onClick={(e) => {
e.preventDefault();
if (!selectionMode) setSelectionMode(true);
toggleChatSelection(chat.id);
}}
>
<div className={cn(
"w-5 h-5 border rounded flex items-center justify-center transition-colors",
selectedChats.includes(chat.id)
? "bg-blue-500 border-blue-500"
: "border-gray-400 dark:border-gray-600"
)}>
{selectedChats.includes(chat.id) && (
<Check className="w-4 h-4 text-white" />
)}
</div>
</div>
)}
{/* Chat Title */}
<Link
href={`/c/${chat.id}`}
className={cn(
"text-black dark:text-white lg:text-xl font-medium truncate transition duration-200 hover:text-[#24A0ED] dark:hover:text-[#24A0ED] cursor-pointer",
selectionMode && "pointer-events-none text-black dark:text-white hover:text-black dark:hover:text-white"
)}
onClick={(e) => {
if (selectionMode) {
e.preventDefault();
toggleChatSelection(chat.id);
}
}}
>
{chat.title}
</Link>
</div>
<div className="flex flex-row items-center justify-between w-full"> <div className="flex flex-row items-center justify-between w-full">
<div className="flex flex-row items-center space-x-1 lg:space-x-1.5 text-black/70 dark:text-white/70"> <div className="flex flex-row items-center space-x-1 lg:space-x-1.5 text-black/70 dark:text-white/70">
<ClockIcon size={15} /> <ClockIcon size={15} />
@@ -97,16 +277,30 @@ const Page = () => {
{formatTimeDifference(new Date(), chat.createdAt)} Ago {formatTimeDifference(new Date(), chat.createdAt)} Ago
</p> </p>
</div> </div>
<DeleteChat
chatId={chat.id} {/* Delete button - only visible when not in selection mode */}
chats={chats} {!selectionMode && (
setChats={setChats} <DeleteChat
/> chatId={chat.id}
chats={chats}
setChats={updateChatsAfterDelete}
/>
)}
</div> </div>
</div> </div>
))} ))}
</div> </div>
)} )}
{/* Batch Delete Confirmation Dialog */}
<BatchDeleteChats
chatIds={selectedChats}
chats={chats}
setChats={updateChatsAfterDelete}
onComplete={handleBatchDeleteComplete}
isOpen={isDeleteDialogOpen}
setIsOpen={setIsDeleteDialogOpen}
/>
</div> </div>
); );
}; };

View File

@@ -1,54 +0,0 @@
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',
},
],
};
}

View File

@@ -1,5 +1,4 @@
import ChatWindow from '@/components/ChatWindow'; import ChatWindow from '@/components/ChatWindow';
import { ChatProvider } from '@/lib/hooks/useChat';
import { Metadata } from 'next'; import { Metadata } from 'next';
import { Suspense } from 'react'; import { Suspense } from 'react';
@@ -12,9 +11,7 @@ const Home = () => {
return ( return (
<div> <div>
<Suspense> <Suspense>
<ChatProvider> <ChatWindow />
<ChatWindow />
</ChatProvider>
</Suspense> </Suspense>
</div> </div>
); );

View File

@@ -21,12 +21,7 @@ interface SettingsType {
anthropicApiKey: string; anthropicApiKey: string;
geminiApiKey: string; geminiApiKey: string;
ollamaApiUrl: string; ollamaApiUrl: string;
ollamaApiKey: string;
lmStudioApiUrl: string;
lemonadeApiUrl: string;
lemonadeApiKey: string;
deepseekApiKey: string; deepseekApiKey: string;
aimlApiKey: string;
customOpenaiApiKey: string; customOpenaiApiKey: string;
customOpenaiApiUrl: string; customOpenaiApiUrl: string;
customOpenaiModelName: string; customOpenaiModelName: string;
@@ -147,17 +142,15 @@ const Page = () => {
const [selectedEmbeddingModel, setSelectedEmbeddingModel] = useState< const [selectedEmbeddingModel, setSelectedEmbeddingModel] = useState<
string | null string | null
>(null); >(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(false);
const [automaticImageSearch, setAutomaticImageSearch] = useState(false); const [automaticImageSearch, setAutomaticImageSearch] = useState(false);
const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false); const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false);
const [systemInstructions, setSystemInstructions] = useState<string>(''); const [systemInstructions, setSystemInstructions] = useState<string>('');
const [measureUnit, setMeasureUnit] = useState<'Imperial' | 'Metric'>(
'Metric',
);
const [savingStates, setSavingStates] = useState<Record<string, boolean>>({}); const [savingStates, setSavingStates] = useState<Record<string, boolean>>({});
useEffect(() => { useEffect(() => {
const fetchConfig = async () => { const fetchConfig = async () => {
setIsLoading(true);
const res = await fetch(`/api/config`, { const res = await fetch(`/api/config`, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -216,10 +209,6 @@ const Page = () => {
setSystemInstructions(localStorage.getItem('systemInstructions')!); setSystemInstructions(localStorage.getItem('systemInstructions')!);
setMeasureUnit(
localStorage.getItem('measureUnit')! as 'Imperial' | 'Metric',
);
setIsLoading(false); setIsLoading(false);
}; };
@@ -378,8 +367,6 @@ const Page = () => {
localStorage.setItem('embeddingModel', value); localStorage.setItem('embeddingModel', value);
} else if (key === 'systemInstructions') { } else if (key === 'systemInstructions') {
localStorage.setItem('systemInstructions', value); localStorage.setItem('systemInstructions', value);
} else if (key === 'measureUnit') {
localStorage.setItem('measureUnit', value.toString());
} }
} catch (err) { } catch (err) {
console.error('Failed to save:', err); console.error('Failed to save:', err);
@@ -428,35 +415,13 @@ const Page = () => {
) : ( ) : (
config && ( config && (
<div className="flex flex-col space-y-6 pb-28 lg:pb-8"> <div className="flex flex-col space-y-6 pb-28 lg:pb-8">
<SettingsSection title="Preferences"> <SettingsSection title="Appearance">
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm"> <p className="text-black/70 dark:text-white/70 text-sm">
Theme Theme
</p> </p>
<ThemeSwitcher /> <ThemeSwitcher />
</div> </div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Measurement Units
</p>
<Select
value={measureUnit ?? undefined}
onChange={(e) => {
setMeasureUnit(e.target.value as 'Imperial' | 'Metric');
saveConfig('measureUnit', e.target.value);
}}
options={[
{
label: 'Metric',
value: 'Metric',
},
{
label: 'Imperial',
value: 'Imperial',
},
]}
/>
</div>
</SettingsSection> </SettingsSection>
<SettingsSection title="Automatic Search"> <SettingsSection title="Automatic Search">
@@ -550,7 +515,7 @@ const Page = () => {
<SettingsSection title="System Instructions"> <SettingsSection title="System Instructions">
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
<Textarea <Textarea
value={systemInstructions ?? undefined} value={systemInstructions}
isSaving={savingStates['systemInstructions']} isSaving={savingStates['systemInstructions']}
onChange={(e) => { onChange={(e) => {
setSystemInstructions(e.target.value); setSystemInstructions(e.target.value);
@@ -583,10 +548,8 @@ const Page = () => {
options={Object.keys(config.chatModelProviders).map( options={Object.keys(config.chatModelProviders).map(
(provider) => ({ (provider) => ({
value: provider, value: provider,
label: label: (PROVIDER_METADATA as any)[provider]?.displayName ||
(PROVIDER_METADATA as any)[provider]?.displayName || provider.charAt(0).toUpperCase() + provider.slice(1),
provider.charAt(0).toUpperCase() +
provider.slice(1),
}), }),
)} )}
/> />
@@ -726,10 +689,8 @@ const Page = () => {
options={Object.keys(config.embeddingModelProviders).map( options={Object.keys(config.embeddingModelProviders).map(
(provider) => ({ (provider) => ({
value: provider, value: provider,
label: label: (PROVIDER_METADATA as any)[provider]?.displayName ||
(PROVIDER_METADATA as any)[provider]?.displayName || provider.charAt(0).toUpperCase() + provider.slice(1),
provider.charAt(0).toUpperCase() +
provider.slice(1),
}), }),
)} )}
/> />
@@ -821,25 +782,6 @@ const Page = () => {
/> />
</div> </div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Ollama API Key (Can be left blank)
</p>
<Input
type="text"
placeholder="Ollama API Key"
value={config.ollamaApiKey}
isSaving={savingStates['ollamaApiKey']}
onChange={(e) => {
setConfig((prev) => ({
...prev!,
ollamaApiKey: e.target.value,
}));
}}
onSave={(value) => saveConfig('ollamaApiKey', value)}
/>
</div>
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm"> <p className="text-black/70 dark:text-white/70 text-sm">
GROQ API Key GROQ API Key
@@ -915,86 +857,6 @@ const Page = () => {
onSave={(value) => saveConfig('deepseekApiKey', value)} onSave={(value) => saveConfig('deepseekApiKey', value)}
/> />
</div> </div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
AI/ML API Key
</p>
<Input
type="text"
placeholder="AI/ML API Key"
value={config.aimlApiKey}
isSaving={savingStates['aimlApiKey']}
onChange={(e) => {
setConfig((prev) => ({
...prev!,
aimlApiKey: e.target.value,
}));
}}
onSave={(value) => saveConfig('aimlApiKey', value)}
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
LM Studio API URL
</p>
<Input
type="text"
placeholder="LM Studio API URL"
value={config.lmStudioApiUrl}
isSaving={savingStates['lmStudioApiUrl']}
onChange={(e) => {
setConfig((prev) => ({
...prev!,
lmStudioApiUrl: e.target.value,
}));
}}
onSave={(value) => saveConfig('lmStudioApiUrl', value)}
/>
</div>
</div>
</SettingsSection>
<SettingsSection title="Lemonade">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Lemonade API URL
</p>
<Input
type="text"
placeholder="Lemonade API URL"
value={config.lemonadeApiUrl}
isSaving={savingStates['lemonadeApiUrl']}
onChange={(e) => {
setConfig((prev) => ({
...prev!,
lemonadeApiUrl: e.target.value,
}));
}}
onSave={(value) => saveConfig('lemonadeApiUrl', value)}
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Lemonade API Key (Optional)
</p>
<Input
type="password"
placeholder="Lemonade API Key"
value={config.lemonadeApiKey}
isSaving={savingStates['lemonadeApiKey']}
onChange={(e) => {
setConfig((prev) => ({
...prev!,
lemonadeApiKey: e.target.value,
}));
}}
onSave={(value) => saveConfig('lemonadeApiKey', value)}
/>
</div>
</div> </div>
</SettingsSection> </SettingsSection>
</div> </div>

View File

@@ -0,0 +1,118 @@
import {
Description,
Dialog,
DialogBackdrop,
DialogPanel,
DialogTitle,
Transition,
TransitionChild,
} from '@headlessui/react';
import { Fragment, useState } from 'react';
import { toast } from 'sonner';
import { Chat } from '@/app/library/page';
interface BatchDeleteChatsProps {
chatIds: string[];
chats: Chat[];
setChats: (chats: Chat[]) => void;
onComplete: () => void;
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
}
const BatchDeleteChats = ({
chatIds,
chats,
setChats,
onComplete,
isOpen,
setIsOpen,
}: BatchDeleteChatsProps) => {
const [loading, setLoading] = useState(false);
const handleDelete = async () => {
if (chatIds.length === 0) return;
setLoading(true);
try {
for (const chatId of chatIds) {
await fetch(`/api/chats/${chatId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
}
const newChats = chats.filter(chat => !chatIds.includes(chat.id));
setChats(newChats);
toast.success(`${chatIds.length} thread${chatIds.length > 1 ? 's' : ''} deleted`);
onComplete();
} catch (err: any) {
toast.error('Failed to delete threads');
} finally {
setIsOpen(false);
setLoading(false);
}
};
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog
as="div"
className="relative z-50"
onClose={() => {
if (!loading) {
setIsOpen(false);
}
}}
>
<DialogBackdrop className="fixed inset-0 bg-black/30" />
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<TransitionChild
as={Fragment}
enter="ease-out duration-200"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-100"
leaveFrom="opacity-100 scale-200"
leaveTo="opacity-0 scale-95"
>
<DialogPanel className="w-full max-w-md transform rounded-2xl bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 p-6 text-left align-middle shadow-xl transition-all">
<DialogTitle className="text-lg font-medium leading-6 dark:text-white">
Delete Confirmation
</DialogTitle>
<Description className="text-sm dark:text-white/70 text-black/70">
Are you sure you want to delete {chatIds.length} selected thread{chatIds.length !== 1 ? 's' : ''}?
</Description>
<div className="flex flex-row items-end justify-end space-x-4 mt-6">
<button
onClick={() => {
if (!loading) {
setIsOpen(false);
}
}}
className="text-black/50 dark:text-white/50 text-sm hover:text-black/70 hover:dark:text-white/70 transition duration-200"
>
Cancel
</button>
<button
onClick={handleDelete}
className="text-red-400 text-sm hover:text-red-500 transition duration-200"
disabled={loading}
>
Delete
</button>
</div>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</Transition>
);
};
export default BatchDeleteChats;

View File

@@ -2,13 +2,31 @@
import { Fragment, useEffect, useRef, useState } from 'react'; import { Fragment, useEffect, useRef, useState } from 'react';
import MessageInput from './MessageInput'; import MessageInput from './MessageInput';
import { File, Message } from './ChatWindow';
import MessageBox from './MessageBox'; import MessageBox from './MessageBox';
import MessageBoxLoading from './MessageBoxLoading'; import MessageBoxLoading from './MessageBoxLoading';
import { useChat } from '@/lib/hooks/useChat';
const Chat = () => {
const { sections, chatTurns, loading, messageAppeared } = useChat();
const Chat = ({
loading,
messages,
sendMessage,
messageAppeared,
rewrite,
fileIds,
setFileIds,
files,
setFiles,
}: {
messages: Message[];
sendMessage: (message: string) => void;
loading: boolean;
messageAppeared: boolean;
rewrite: (messageId: string) => void;
fileIds: string[];
setFileIds: (fileIds: string[]) => void;
files: File[];
setFiles: (files: File[]) => void;
}) => {
const [dividerWidth, setDividerWidth] = useState(0); const [dividerWidth, setDividerWidth] = useState(0);
const dividerRef = useRef<HTMLDivElement | null>(null); const dividerRef = useRef<HTMLDivElement | null>(null);
const messageEnd = useRef<HTMLDivElement | null>(null); const messageEnd = useRef<HTMLDivElement | null>(null);
@@ -27,36 +45,41 @@ const Chat = () => {
return () => { return () => {
window.removeEventListener('resize', updateDividerWidth); window.removeEventListener('resize', updateDividerWidth);
}; };
}, []); });
useEffect(() => { useEffect(() => {
const scroll = () => { const scroll = () => {
messageEnd.current?.scrollIntoView({ behavior: 'smooth' }); messageEnd.current?.scrollIntoView({ behavior: 'smooth' });
}; };
if (chatTurns.length === 1) { if (messages.length === 1) {
document.title = `${chatTurns[0].content.substring(0, 30)} - Perplexica`; document.title = `${messages[0].content.substring(0, 30)} - Perplexica`;
} }
if (chatTurns[chatTurns.length - 1]?.role === 'user') { if (messages[messages.length - 1]?.role == 'user') {
scroll(); scroll();
} }
}, [chatTurns]); }, [messages]);
return ( return (
<div className="flex flex-col space-y-6 pt-8 pb-44 lg:pb-32 sm:mx-4 md:mx-8"> <div className="flex flex-col space-y-6 pt-8 pb-44 lg:pb-32 sm:mx-4 md:mx-8">
{sections.map((section, i) => { {messages.map((msg, i) => {
const isLast = i === sections.length - 1; const isLast = i === messages.length - 1;
return ( return (
<Fragment key={section.userMessage.messageId}> <Fragment key={msg.messageId}>
<MessageBox <MessageBox
section={section} key={i}
sectionIndex={i} message={msg}
messageIndex={i}
history={messages}
loading={loading}
dividerRef={isLast ? dividerRef : undefined} dividerRef={isLast ? dividerRef : undefined}
isLast={isLast} isLast={isLast}
rewrite={rewrite}
sendMessage={sendMessage}
/> />
{!isLast && ( {!isLast && msg.role === 'assistant' && (
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" /> <div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
)} )}
</Fragment> </Fragment>
@@ -69,7 +92,14 @@ const Chat = () => {
className="bottom-24 lg:bottom-10 fixed z-40" className="bottom-24 lg:bottom-10 fixed z-40"
style={{ width: dividerWidth }} style={{ width: dividerWidth }}
> >
<MessageInput /> <MessageInput
loading={loading}
sendMessage={sendMessage}
fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
/>
</div> </div>
)} )}
</div> </div>

View File

@@ -1,47 +1,27 @@
'use client'; 'use client';
import { useEffect, useRef, useState } from 'react';
import { Document } from '@langchain/core/documents'; import { Document } from '@langchain/core/documents';
import Navbar from './Navbar'; import Navbar from './Navbar';
import Chat from './Chat'; import Chat from './Chat';
import EmptyChat from './EmptyChat'; import EmptyChat from './EmptyChat';
import crypto from 'crypto';
import { toast } from 'sonner';
import { useSearchParams } from 'next/navigation';
import { getSuggestions } from '@/lib/actions';
import { Settings } from 'lucide-react'; import { Settings } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import NextError from 'next/error'; import NextError from 'next/error';
import { useChat } from '@/lib/hooks/useChat';
export interface BaseMessage { export type Message = {
chatId: string;
messageId: string; messageId: string;
chatId: string;
createdAt: Date; createdAt: Date;
}
export interface AssistantMessage extends BaseMessage {
role: 'assistant';
content: string; content: string;
role: 'user' | 'assistant';
suggestions?: string[]; suggestions?: string[];
} sources?: Document[];
};
export interface UserMessage extends BaseMessage {
role: 'user';
content: string;
}
export interface SourceMessage extends BaseMessage {
role: 'source';
sources: Document[];
}
export interface SuggestionMessage extends BaseMessage {
role: 'suggestion';
suggestions: string[];
}
export type Message =
| AssistantMessage
| UserMessage
| SourceMessage
| SuggestionMessage;
export type ChatTurn = UserMessage | AssistantMessage;
export interface File { export interface File {
fileName: string; fileName: string;
@@ -49,8 +29,512 @@ export interface File {
fileId: string; fileId: string;
} }
const ChatWindow = () => { interface ChatModelProvider {
const { hasError, isReady, notFound, messages } = useChat(); 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]);
if (hasError) { if (hasError) {
return ( return (
<div className="relative"> <div className="relative">
@@ -75,11 +559,31 @@ const ChatWindow = () => {
<div> <div>
{messages.length > 0 ? ( {messages.length > 0 ? (
<> <>
<Navbar /> <Navbar chatId={chatId!} messages={messages} />
<Chat /> <Chat
loading={loading}
messages={messages}
sendMessage={sendMessage}
messageAppeared={messageAppeared}
rewrite={rewrite}
fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
/>
</> </>
) : ( ) : (
<EmptyChat /> <EmptyChat
sendMessage={sendMessage}
focusMode={focusMode}
setFocusMode={setFocusMode}
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
/>
)} )}
</div> </div>
) )

View File

@@ -1,19 +0,0 @@
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;

View File

@@ -1,70 +0,0 @@
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;

View File

@@ -1,32 +0,0 @@
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;

View File

@@ -1,11 +1,32 @@
import { Settings } from 'lucide-react'; import { Settings } from 'lucide-react';
import EmptyChatMessageInput from './EmptyChatMessageInput'; import EmptyChatMessageInput from './EmptyChatMessageInput';
import { useState } from 'react';
import { File } from './ChatWindow'; import { File } from './ChatWindow';
import Link from 'next/link'; import Link from 'next/link';
import WeatherWidget from './WeatherWidget';
import NewsArticleWidget from './NewsArticleWidget';
const EmptyChat = () => { const EmptyChat = ({
sendMessage,
focusMode,
setFocusMode,
optimizationMode,
setOptimizationMode,
fileIds,
setFileIds,
files,
setFiles,
}: {
sendMessage: (message: string) => void;
focusMode: string;
setFocusMode: (mode: string) => void;
optimizationMode: string;
setOptimizationMode: (mode: string) => void;
fileIds: string[];
setFileIds: (fileIds: string[]) => void;
files: File[];
setFiles: (files: File[]) => void;
}) => {
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
return ( return (
<div className="relative"> <div className="relative">
<div className="absolute w-full flex flex-row items-center justify-end mr-5 mt-5"> <div className="absolute w-full flex flex-row items-center justify-end mr-5 mt-5">
@@ -13,21 +34,21 @@ const EmptyChat = () => {
<Settings className="cursor-pointer lg:hidden" /> <Settings className="cursor-pointer lg:hidden" />
</Link> </Link>
</div> </div>
<div className="flex flex-col items-center justify-center min-h-screen max-w-screen-sm mx-auto p-2 space-y-4"> <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 w-full space-y-8"> <h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8">
<h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8"> Research begins here.
Research begins here. </h2>
</h2> <EmptyChatMessageInput
<EmptyChatMessageInput /> sendMessage={sendMessage}
</div> focusMode={focusMode}
<div className="flex flex-col w-full gap-4 mt-2 sm:flex-row sm:justify-center"> setFocusMode={setFocusMode}
<div className="flex-1 w-full"> optimizationMode={optimizationMode}
<WeatherWidget /> setOptimizationMode={setOptimizationMode}
</div> fileIds={fileIds}
<div className="flex-1 w-full"> setFileIds={setFileIds}
<NewsArticleWidget /> files={files}
</div> setFiles={setFiles}
</div> />
</div> </div>
</div> </div>
); );

View File

@@ -1,15 +1,34 @@
import { ArrowRight } from 'lucide-react'; import { ArrowRight } from 'lucide-react';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import TextareaAutosize from 'react-textarea-autosize'; import TextareaAutosize from 'react-textarea-autosize';
import CopilotToggle from './MessageInputActions/Copilot';
import Focus from './MessageInputActions/Focus'; import Focus from './MessageInputActions/Focus';
import Optimization from './MessageInputActions/Optimization'; import Optimization from './MessageInputActions/Optimization';
import Attach from './MessageInputActions/Attach'; import Attach from './MessageInputActions/Attach';
import { useChat } from '@/lib/hooks/useChat'; import { File } from './ChatWindow';
const EmptyChatMessageInput = () => { const EmptyChatMessageInput = ({
const { sendMessage } = useChat(); sendMessage,
focusMode,
/* const [copilotEnabled, setCopilotEnabled] = useState(false); */ 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 [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const inputRef = useRef<HTMLTextAreaElement | null>(null); const inputRef = useRef<HTMLTextAreaElement | null>(null);
@@ -54,7 +73,7 @@ const EmptyChatMessageInput = () => {
}} }}
className="w-full" className="w-full"
> >
<div className="flex flex-col bg-light-secondary dark:bg-dark-secondary px-5 pt-5 pb-2 rounded-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"> <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">
<TextareaAutosize <TextareaAutosize
ref={inputRef} ref={inputRef}
value={message} value={message}
@@ -65,11 +84,20 @@ const EmptyChatMessageInput = () => {
/> />
<div className="flex flex-row items-center justify-between mt-4"> <div className="flex flex-row items-center justify-between mt-4">
<div className="flex flex-row items-center space-x-2 lg:space-x-4"> <div className="flex flex-row items-center space-x-2 lg:space-x-4">
<Focus /> <Focus focusMode={focusMode} setFocusMode={setFocusMode} />
<Attach showText /> <Attach
fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
showText
/>
</div> </div>
<div className="flex flex-row items-center space-x-1 sm:space-x-4"> <div className="flex flex-row items-center space-x-1 sm:space-x-4">
<Optimization /> <Optimization
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
/>
<button <button
disabled={message.trim().length === 0} disabled={message.trim().length === 0}
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 disabled:bg-[#e0e0dc] dark:disabled:bg-[#ececec21] hover:bg-opacity-85 transition duration-100 rounded-full p-2" className="bg-[#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"

View File

@@ -1,13 +1,12 @@
import { Check, ClipboardList } from 'lucide-react'; import { Check, ClipboardList } from 'lucide-react';
import { Message } from '../ChatWindow'; import { Message } from '../ChatWindow';
import { useState } from 'react'; import { useState } from 'react';
import { Section } from '@/lib/hooks/useChat';
const Copy = ({ const Copy = ({
section, message,
initialMessage, initialMessage,
}: { }: {
section: Section; message: Message;
initialMessage: string; initialMessage: string;
}) => { }) => {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@@ -15,7 +14,7 @@ const Copy = ({
return ( return (
<button <button
onClick={() => { onClick={() => {
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`)}`}`; 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`)}`}`;
navigator.clipboard.writeText(contentToCopy); navigator.clipboard.writeText(contentToCopy);
setCopied(true); setCopied(true);
setTimeout(() => setCopied(false), 1000); setTimeout(() => setCopied(false), 1000);

View File

@@ -1,7 +1,8 @@
'use client'; 'use client';
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import React, { MutableRefObject } from 'react'; import React, { MutableRefObject, useEffect, useState } from 'react';
import { Message } from './ChatWindow';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { import {
BookCopy, BookCopy,
@@ -19,37 +20,90 @@ import SearchImages from './SearchImages';
import SearchVideos from './SearchVideos'; import SearchVideos from './SearchVideos';
import { useSpeech } from 'react-text-to-speech'; import { useSpeech } from 'react-text-to-speech';
import ThinkBox from './ThinkBox'; import ThinkBox from './ThinkBox';
import { useChat, Section } from '@/lib/hooks/useChat';
import Citation from './Citation';
const ThinkTagProcessor = ({ const ThinkTagProcessor = ({ children }: { children: React.ReactNode }) => {
children, return <ThinkBox content={children as string} />;
thinkingEnded,
}: {
children: React.ReactNode;
thinkingEnded: boolean;
}) => {
return (
<ThinkBox content={children as string} thinkingEnded={thinkingEnded} />
);
}; };
const MessageBox = ({ const MessageBox = ({
section, message,
sectionIndex, messageIndex,
history,
loading,
dividerRef, dividerRef,
isLast, isLast,
rewrite,
sendMessage,
}: { }: {
section: Section; message: Message;
sectionIndex: number; messageIndex: number;
history: Message[];
loading: boolean;
dividerRef?: MutableRefObject<HTMLDivElement | null>; dividerRef?: MutableRefObject<HTMLDivElement | null>;
isLast: boolean; isLast: boolean;
rewrite: (messageId: string) => void;
sendMessage: (message: string) => void;
}) => { }) => {
const { loading, chatTurns, sendMessage, rewrite } = useChat(); const [parsedMessage, setParsedMessage] = useState(message.content);
const [speechMessage, setSpeechMessage] = useState(message.content);
const parsedMessage = section.parsedAssistantMessage || ''; useEffect(() => {
const speechMessage = section.speechMessage || ''; const citationRegex = /\[([^\]]+)\]/g;
const thinkingEnded = section.thinkingEnded; 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;
},
),
);
setSpeechMessage(message.content.replace(regex, ''));
return;
}
setSpeechMessage(message.content.replace(regex, ''));
setParsedMessage(processedMessage);
}, [message.content, message.sources, message.role]);
const { speechStatus, start, stop } = useSpeech({ text: speechMessage }); const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
@@ -57,31 +111,33 @@ const MessageBox = ({
overrides: { overrides: {
think: { think: {
component: ThinkTagProcessor, component: ThinkTagProcessor,
props: {
thinkingEnded: thinkingEnded,
},
},
citation: {
component: Citation,
}, },
}, },
}; };
return ( return (
<div className="space-y-6"> <div>
<div className={'w-full pt-8 break-words'}> {message.role === 'user' && (
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
{section.userMessage.content}
</h2>
</div>
<div className="flex flex-col space-y-9 lg:space-y-0 lg:flex-row lg:justify-between lg:space-x-9">
<div <div
ref={dividerRef} className={cn(
className="flex flex-col space-y-6 w-full lg:w-9/12" 'w-full',
messageIndex === 0 ? 'pt-16' : 'pt-8',
'break-words',
)}
> >
{section.sourceMessage && <h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
section.sourceMessage.sources.length > 0 && ( {message.content}
</h2>
</div>
)}
{message.role === 'assistant' && (
<div className="flex flex-col space-y-9 lg:space-y-0 lg:flex-row lg:justify-between lg:space-x-9">
<div
ref={dividerRef}
className="flex flex-col space-y-6 w-full lg:w-9/12"
>
{message.sources && message.sources.length > 0 && (
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
<div className="flex flex-row items-center space-x-2"> <div className="flex flex-row items-center space-x-2">
<BookCopy className="text-black dark:text-white" size={20} /> <BookCopy className="text-black dark:text-white" size={20} />
@@ -89,12 +145,10 @@ const MessageBox = ({
Sources Sources
</h3> </h3>
</div> </div>
<MessageSources sources={section.sourceMessage.sources} /> <MessageSources sources={message.sources} />
</div> </div>
)} )}
<div className="flex flex-col space-y-2">
<div className="flex flex-col space-y-2">
{section.sourceMessage && (
<div className="flex flex-row items-center space-x-2"> <div className="flex flex-row items-center space-x-2">
<Disc3 <Disc3
className={cn( className={cn(
@@ -107,115 +161,100 @@ const MessageBox = ({
Answer Answer
</h3> </h3>
</div> </div>
)}
{section.assistantMessage && ( <Markdown
<> className={cn(
<Markdown '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]',
className={cn( 'max-w-none break-words text-black dark:text-white',
'prose prose-h1:mb-3 prose-h2:mb-2 prose-h2:mt-6 prose-h2:font-[800] prose-h3:mt-4 prose-h3:mb-1.5 prose-h3:font-[600] dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 font-[400]',
'max-w-none break-words text-black dark:text-white',
)}
options={markdownOverrides}
>
{parsedMessage}
</Markdown>
{loading && isLast ? null : (
<div className="flex flex-row items-center justify-between w-full text-black dark:text-white py-4 -mx-2">
<div className="flex flex-row items-center space-x-1">
<Rewrite
rewrite={rewrite}
messageId={section.assistantMessage.messageId}
/>
</div>
<div className="flex flex-row items-center space-x-1">
<Copy
initialMessage={section.assistantMessage.content}
section={section}
/>
<button
onClick={() => {
if (speechStatus === 'started') {
stop();
} else {
start();
}
}}
className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
>
{speechStatus === 'started' ? (
<StopCircle size={18} />
) : (
<Volume2 size={18} />
)}
</button>
</div>
</div>
)} )}
options={markdownOverrides}
{isLast && >
section.suggestions && {parsedMessage}
section.suggestions.length > 0 && </Markdown>
section.assistantMessage && {loading && isLast ? null : (
!loading && ( <div className="flex flex-row items-center justify-between w-full text-black dark:text-white py-4 -mx-2">
<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-1">
<div className="flex flex-row items-center space-x-2 mb-4"> {/* <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">
<Layers3 <Share size={18} />
className="text-black dark:text-white" </button> */}
size={20} <Rewrite rewrite={rewrite} messageId={message.messageId} />
/> </div>
<h3 className="text-black dark:text-white font-medium text-xl"> <div className="flex flex-row items-center space-x-1">
Related <Copy initialMessage={message.content} message={message} />
</h3> <button
onClick={() => {
if (speechStatus === 'started') {
stop();
} else {
start();
}
}}
className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
>
{speechStatus === 'started' ? (
<StopCircle size={18} />
) : (
<Volume2 size={18} />
)}
</button>
</div>
</div>
)}
{isLast &&
message.suggestions &&
message.suggestions.length > 0 &&
message.role === 'assistant' &&
!loading && (
<>
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
<div className="flex flex-col space-y-3 text-black dark:text-white">
<div className="flex flex-row items-center space-x-2 mt-4">
<Layers3 />
<h3 className="text-xl font-medium">Related</h3>
</div> </div>
<div className="space-y-0"> <div className="flex flex-col space-y-3">
{section.suggestions.map( {message.suggestions.map((suggestion, i) => (
(suggestion: string, i: number) => ( <div
<div key={i}> className="flex flex-col space-y-3 text-sm"
{i > 0 && ( key={i}
<div className="h-px bg-light-200/40 dark:bg-dark-200/40 mx-3" /> >
)} <div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
<button <div
onClick={() => sendMessage(suggestion)} onClick={() => {
className="group w-full px-3 py-4 text-left transition-colors duration-200" sendMessage(suggestion);
> }}
<div className="flex items-center justify-between gap-3"> className="cursor-pointer flex flex-row justify-between font-medium space-x-2 items-center"
<p className="text-sm text-black/70 dark:text-white/70 group-hover:text-[#24A0ED] transition-colors duration-200 leading-relaxed"> >
{suggestion} <p className="transition duration-200 hover:text-[#24A0ED]">
</p> {suggestion}
<Plus </p>
size={16} <Plus
className="text-black/40 dark:text-white/40 group-hover:text-[#24A0ED] transition-colors duration-200 flex-shrink-0" size={20}
/> className="text-[#24A0ED] flex-shrink-0"
</div> />
</button>
</div> </div>
), </div>
)} ))}
</div> </div>
</div> </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"> <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 <SearchImages
query={section.userMessage.content} query={history[messageIndex - 1].content}
chatHistory={chatTurns.slice(0, sectionIndex * 2)} chatHistory={history.slice(0, messageIndex - 1)}
messageId={section.assistantMessage.messageId} messageId={message.messageId}
/> />
<SearchVideos <SearchVideos
chatHistory={chatTurns.slice(0, sectionIndex * 2)} chatHistory={history.slice(0, messageIndex - 1)}
query={section.userMessage.content} query={history[messageIndex - 1].content}
messageId={section.assistantMessage.messageId} messageId={message.messageId}
/> />
</div> </div>
)} </div>
</div> )}
</div> </div>
); );
}; };

View File

@@ -6,11 +6,22 @@ import Attach from './MessageInputActions/Attach';
import CopilotToggle from './MessageInputActions/Copilot'; import CopilotToggle from './MessageInputActions/Copilot';
import { File } from './ChatWindow'; import { File } from './ChatWindow';
import AttachSmall from './MessageInputActions/AttachSmall'; import AttachSmall from './MessageInputActions/AttachSmall';
import { useChat } from '@/lib/hooks/useChat';
const MessageInput = () => {
const { loading, sendMessage } = useChat();
const MessageInput = ({
sendMessage,
loading,
fileIds,
setFileIds,
files,
setFiles,
}: {
sendMessage: (message: string) => void;
loading: boolean;
fileIds: string[];
setFileIds: (fileIds: string[]) => void;
files: File[];
setFiles: (files: File[]) => void;
}) => {
const [copilotEnabled, setCopilotEnabled] = useState(false); const [copilotEnabled, setCopilotEnabled] = useState(false);
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [textareaRows, setTextareaRows] = useState(1); const [textareaRows, setTextareaRows] = useState(1);
@@ -64,11 +75,18 @@ const MessageInput = () => {
} }
}} }}
className={cn( className={cn(
'bg-light-secondary dark:bg-dark-secondary p-4 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 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', '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-2xl' : 'flex-row rounded-full', mode === 'multi' ? 'flex-col rounded-lg' : 'flex-row rounded-full',
)} )}
> >
{mode === 'single' && <AttachSmall />} {mode === 'single' && (
<AttachSmall
fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
/>
)}
<TextareaAutosize <TextareaAutosize
ref={inputRef} ref={inputRef}
value={message} value={message}
@@ -95,7 +113,12 @@ const MessageInput = () => {
)} )}
{mode === 'multi' && ( {mode === 'multi' && (
<div className="flex flex-row items-center justify-between w-full pt-2"> <div className="flex flex-row items-center justify-between w-full pt-2">
<AttachSmall /> <AttachSmall
fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
/>
<div className="flex flex-row items-center space-x-4"> <div className="flex flex-row items-center space-x-4">
<CopilotToggle <CopilotToggle
copilotEnabled={copilotEnabled} copilotEnabled={copilotEnabled}

View File

@@ -7,11 +7,21 @@ import {
} from '@headlessui/react'; } from '@headlessui/react';
import { CopyPlus, File, LoaderCircle, Plus, Trash } from 'lucide-react'; import { CopyPlus, File, LoaderCircle, Plus, Trash } from 'lucide-react';
import { Fragment, useRef, useState } from 'react'; import { Fragment, useRef, useState } from 'react';
import { useChat } from '@/lib/hooks/useChat'; import { File as FileType } from '../ChatWindow';
const Attach = ({ showText }: { showText?: boolean }) => {
const { files, setFiles, setFileIds, fileIds } = useChat();
const Attach = ({
fileIds,
setFileIds,
showText,
files,
setFiles,
}: {
fileIds: string[];
setFileIds: (fileIds: string[]) => void;
showText?: boolean;
files: FileType[];
setFiles: (files: FileType[]) => void;
}) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const fileInputRef = useRef<any>(); const fileInputRef = useRef<any>();
@@ -132,11 +142,8 @@ const Attach = ({ showText }: { showText?: boolean }) => {
key={i} key={i}
className="flex flex-row items-center justify-start w-full space-x-3 p-3" className="flex flex-row items-center justify-start w-full space-x-3 p-3"
> >
<div className="bg-light-100 dark:bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md"> <div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md">
<File <File size={16} className="text-white/70" />
size={16}
className="text-black/70 dark:text-white/70"
/>
</div> </div>
<p className="text-black/70 dark:text-white/70 text-sm"> <p className="text-black/70 dark:text-white/70 text-sm">
{file.fileName.length > 25 {file.fileName.length > 25

View File

@@ -8,11 +8,18 @@ import {
import { CopyPlus, File, LoaderCircle, Plus, Trash } from 'lucide-react'; import { CopyPlus, File, LoaderCircle, Plus, Trash } from 'lucide-react';
import { Fragment, useRef, useState } from 'react'; import { Fragment, useRef, useState } from 'react';
import { File as FileType } from '../ChatWindow'; import { File as FileType } from '../ChatWindow';
import { useChat } from '@/lib/hooks/useChat';
const AttachSmall = () => {
const { files, setFiles, setFileIds, fileIds } = useChat();
const AttachSmall = ({
fileIds,
setFileIds,
files,
setFiles,
}: {
fileIds: string[];
setFileIds: (fileIds: string[]) => void;
files: FileType[];
setFiles: (files: FileType[]) => void;
}) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const fileInputRef = useRef<any>(); const fileInputRef = useRef<any>();
@@ -107,11 +114,8 @@ const AttachSmall = () => {
key={i} key={i}
className="flex flex-row items-center justify-start w-full space-x-3 p-3" className="flex flex-row items-center justify-start w-full space-x-3 p-3"
> >
<div className="bg-light-100 dark:bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md"> <div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md">
<File <File size={16} className="text-white/70" />
size={16}
className="text-black/70 dark:text-white/70"
/>
</div> </div>
<p className="text-black/70 dark:text-white/70 text-sm"> <p className="text-black/70 dark:text-white/70 text-sm">
{file.fileName.length > 25 {file.fileName.length > 25

View File

@@ -15,7 +15,6 @@ import {
} from '@headlessui/react'; } from '@headlessui/react';
import { SiReddit, SiYoutube } from '@icons-pack/react-simple-icons'; import { SiReddit, SiYoutube } from '@icons-pack/react-simple-icons';
import { Fragment } from 'react'; import { Fragment } from 'react';
import { useChat } from '@/lib/hooks/useChat';
const focusModes = [ const focusModes = [
{ {
@@ -56,9 +55,13 @@ const focusModes = [
}, },
]; ];
const Focus = () => { const Focus = ({
const { focusMode, setFocusMode } = useChat(); focusMode,
setFocusMode,
}: {
focusMode: string;
setFocusMode: (mode: string) => void;
}) => {
return ( return (
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg mt-[6.5px]"> <Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg mt-[6.5px]">
<PopoverButton <PopoverButton

View File

@@ -7,7 +7,6 @@ import {
Transition, Transition,
} from '@headlessui/react'; } from '@headlessui/react';
import { Fragment } from 'react'; import { Fragment } from 'react';
import { useChat } from '@/lib/hooks/useChat';
const OptimizationModes = [ const OptimizationModes = [
{ {
@@ -35,9 +34,13 @@ const OptimizationModes = [
}, },
]; ];
const Optimization = () => { const Optimization = ({
const { optimizationMode, setOptimizationMode } = useChat(); optimizationMode,
setOptimizationMode,
}: {
optimizationMode: string;
setOptimizationMode: (mode: string) => void;
}) => {
return ( return (
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg"> <Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
<PopoverButton <PopoverButton

View File

@@ -1,223 +1,40 @@
import { Clock, Edit, Share, Trash, FileText, FileDown } from 'lucide-react'; import { Clock, Edit, Share, Trash } from 'lucide-react';
import { Message } from './ChatWindow'; import { Message } from './ChatWindow';
import { useEffect, useState, Fragment } from 'react'; import { useEffect, useState } from 'react';
import { formatTimeDifference } from '@/lib/utils'; import { formatTimeDifference } from '@/lib/utils';
import DeleteChat from './DeleteChat'; import DeleteChat from './DeleteChat';
import {
Popover,
PopoverButton,
PopoverPanel,
Transition,
} from '@headlessui/react';
import jsPDF from 'jspdf';
import { useChat, Section } from '@/lib/hooks/useChat';
const downloadFile = (filename: string, content: string, type: string) => { const Navbar = ({
const blob = new Blob([content], { type }); chatId,
const url = URL.createObjectURL(blob); messages,
const a = document.createElement('a'); }: {
a.href = url; messages: Message[];
a.download = filename; chatId: string;
document.body.appendChild(a); }) => {
a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 0);
};
const exportAsMarkdown = (sections: Section[], title: string) => {
const date = new Date(
sections[0]?.userMessage?.createdAt || Date.now(),
).toLocaleString();
let md = `# 💬 Chat Export: ${title}\n\n`;
md += `*Exported on: ${date}*\n\n---\n`;
sections.forEach((section, idx) => {
if (section.userMessage) {
md += `\n---\n`;
md += `**🧑 User**
`;
md += `*${new Date(section.userMessage.createdAt).toLocaleString()}*\n\n`;
md += `> ${section.userMessage.content.replace(/\n/g, '\n> ')}\n`;
}
if (section.assistantMessage) {
md += `\n---\n`;
md += `**🤖 Assistant**
`;
md += `*${new Date(section.assistantMessage.createdAt).toLocaleString()}*\n\n`;
md += `> ${section.assistantMessage.content.replace(/\n/g, '\n> ')}\n`;
}
if (
section.sourceMessage &&
section.sourceMessage.sources &&
section.sourceMessage.sources.length > 0
) {
md += `\n**Citations:**\n`;
section.sourceMessage.sources.forEach((src: any, i: number) => {
const url = src.metadata?.url || '';
md += `- [${i + 1}] [${url}](${url})\n`;
});
}
});
md += '\n---\n';
downloadFile(`${title || 'chat'}.md`, md, 'text/markdown');
};
const exportAsPDF = (sections: Section[], title: string) => {
const doc = new jsPDF();
const date = new Date(
sections[0]?.userMessage?.createdAt || Date.now(),
).toLocaleString();
let y = 15;
const pageHeight = doc.internal.pageSize.height;
doc.setFontSize(18);
doc.text(`Chat Export: ${title}`, 10, y);
y += 8;
doc.setFontSize(11);
doc.setTextColor(100);
doc.text(`Exported on: ${date}`, 10, y);
y += 8;
doc.setDrawColor(200);
doc.line(10, y, 200, y);
y += 6;
doc.setTextColor(30);
sections.forEach((section, idx) => {
if (section.userMessage) {
if (y > pageHeight - 30) {
doc.addPage();
y = 15;
}
doc.setFont('helvetica', 'bold');
doc.text('User', 10, y);
doc.setFont('helvetica', 'normal');
doc.setFontSize(10);
doc.setTextColor(120);
doc.text(
`${new Date(section.userMessage.createdAt).toLocaleString()}`,
40,
y,
);
y += 6;
doc.setTextColor(30);
doc.setFontSize(12);
const userLines = doc.splitTextToSize(section.userMessage.content, 180);
for (let i = 0; i < userLines.length; i++) {
if (y > pageHeight - 20) {
doc.addPage();
y = 15;
}
doc.text(userLines[i], 12, y);
y += 6;
}
y += 6;
doc.setDrawColor(230);
if (y > pageHeight - 10) {
doc.addPage();
y = 15;
}
doc.line(10, y, 200, y);
y += 4;
}
if (section.assistantMessage) {
if (y > pageHeight - 30) {
doc.addPage();
y = 15;
}
doc.setFont('helvetica', 'bold');
doc.text('Assistant', 10, y);
doc.setFont('helvetica', 'normal');
doc.setFontSize(10);
doc.setTextColor(120);
doc.text(
`${new Date(section.assistantMessage.createdAt).toLocaleString()}`,
40,
y,
);
y += 6;
doc.setTextColor(30);
doc.setFontSize(12);
const assistantLines = doc.splitTextToSize(
section.assistantMessage.content,
180,
);
for (let i = 0; i < assistantLines.length; i++) {
if (y > pageHeight - 20) {
doc.addPage();
y = 15;
}
doc.text(assistantLines[i], 12, y);
y += 6;
}
if (
section.sourceMessage &&
section.sourceMessage.sources &&
section.sourceMessage.sources.length > 0
) {
doc.setFontSize(11);
doc.setTextColor(80);
if (y > pageHeight - 20) {
doc.addPage();
y = 15;
}
doc.text('Citations:', 12, y);
y += 5;
section.sourceMessage.sources.forEach((src: any, i: number) => {
const url = src.metadata?.url || '';
if (y > pageHeight - 15) {
doc.addPage();
y = 15;
}
doc.text(`- [${i + 1}] ${url}`, 15, y);
y += 5;
});
doc.setTextColor(30);
}
y += 6;
doc.setDrawColor(230);
if (y > pageHeight - 10) {
doc.addPage();
y = 15;
}
doc.line(10, y, 200, y);
y += 4;
}
});
doc.save(`${title || 'chat'}.pdf`);
};
const Navbar = () => {
const [title, setTitle] = useState<string>(''); const [title, setTitle] = useState<string>('');
const [timeAgo, setTimeAgo] = useState<string>(''); const [timeAgo, setTimeAgo] = useState<string>('');
const { sections, chatId } = useChat();
useEffect(() => { useEffect(() => {
if (sections.length > 0 && sections[0].userMessage) { if (messages.length > 0) {
const newTitle = const newTitle =
sections[0].userMessage.content.length > 20 messages[0].content.length > 20
? `${sections[0].userMessage.content.substring(0, 20).trim()}...` ? `${messages[0].content.substring(0, 20).trim()}...`
: sections[0].userMessage.content; : messages[0].content;
setTitle(newTitle); setTitle(newTitle);
const newTimeAgo = formatTimeDifference( const newTimeAgo = formatTimeDifference(
new Date(), new Date(),
sections[0].userMessage.createdAt, messages[0].createdAt,
); );
setTimeAgo(newTimeAgo); setTimeAgo(newTimeAgo);
} }
}, [sections]); }, [messages]);
useEffect(() => { useEffect(() => {
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
if (sections.length > 0 && sections[0].userMessage) { if (messages.length > 0) {
const newTimeAgo = formatTimeDifference( const newTimeAgo = formatTimeDifference(
new Date(), new Date(),
sections[0].userMessage.createdAt, messages[0].createdAt,
); );
setTimeAgo(newTimeAgo); setTimeAgo(newTimeAgo);
} }
@@ -228,91 +45,25 @@ const Navbar = () => {
}, []); }, []);
return ( return (
<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="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="px-4 lg:px-6 py-4"> <a
<div className="flex items-center justify-between"> href="/"
<div className="flex items-center min-w-0"> className="active:scale-95 transition duration-100 cursor-pointer lg:hidden"
<a >
href="/" <Edit size={17} />
className="lg:hidden mr-3 p-2 -ml-2 rounded-lg hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors duration-200" </a>
> <div className="hidden lg:flex flex-row items-center justify-center space-x-2">
<Edit size={18} className="text-black/70 dark:text-white/70" /> <Clock size={17} />
</a> <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"> </div>
<Clock size={14} /> <p className="hidden lg:flex">{title}</p>
<span className="text-xs whitespace-nowrap">{timeAgo} ago</span>
</div>
</div>
<div className="flex-1 mx-4 min-w-0"> <div className="flex flex-row items-center space-x-4">
<h1 className="text-center text-sm font-medium text-black/80 dark:text-white/90 truncate"> <Share
{title || 'New Conversation'} size={17}
</h1> className="active:scale-95 transition duration-100 cursor-pointer"
</div> />
<DeleteChat redirect chatId={chatId} chats={[]} setChats={() => {}} />
<div className="flex items-center gap-1 min-w-0">
<Popover className="relative">
<PopoverButton className="p-2 rounded-lg hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors duration-200">
<Share size={16} className="text-black/60 dark:text-white/60" />
</PopoverButton>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<PopoverPanel className="absolute right-0 mt-2 w-64 origin-top-right rounded-2xl bg-light-primary dark:bg-dark-primary border border-light-200 dark:border-dark-200 shadow-xl shadow-black/10 dark:shadow-black/30 z-50">
<div className="p-3">
<div className="mb-2">
<p className="text-xs font-medium text-black/40 dark:text-white/40 uppercase tracking-wide">
Export Chat
</p>
</div>
<div className="space-y-1">
<button
className="w-full flex items-center gap-3 px-3 py-2 text-left rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors duration-200"
onClick={() => exportAsMarkdown(sections, title || '')}
>
<FileText size={16} className="text-[#24A0ED]" />
<div>
<p className="text-sm font-medium text-black dark:text-white">
Markdown
</p>
<p className="text-xs text-black/50 dark:text-white/50">
.md format
</p>
</div>
</button>
<button
className="w-full flex items-center gap-3 px-3 py-2 text-left rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors duration-200"
onClick={() => exportAsPDF(sections, title || '')}
>
<FileDown size={16} className="text-[#24A0ED]" />
<div>
<p className="text-sm font-medium text-black dark:text-white">
PDF
</p>
<p className="text-xs text-black/50 dark:text-white/50">
Document format
</p>
</div>
</button>
</div>
</div>
</PopoverPanel>
</Transition>
</Popover>
<DeleteChat
redirect
chatId={chatId!}
chats={[]}
setChats={() => {}}
/>
</div>
</div>
</div> </div>
</div> </div>
); );

View File

@@ -1,71 +0,0 @@
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;

View File

@@ -40,7 +40,7 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
return ( return (
<div> <div>
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-20 lg:flex-col"> <div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-20 lg:flex-col">
<div className="flex grow flex-col items-center justify-between gap-y-5 overflow-y-auto bg-light-secondary dark:bg-dark-secondary px-2 py-8 mx-2 my-2 rounded-2xl shadow-sm shadow-light-200/10 dark:shadow-black/25"> <div className="flex grow flex-col items-center justify-between gap-y-5 overflow-y-auto bg-light-secondary dark:bg-dark-secondary px-2 py-8">
<a href="/"> <a href="/">
<SquarePen className="cursor-pointer" /> <SquarePen className="cursor-pointer" />
</a> </a>

View File

@@ -1,23 +1,15 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { useState } from 'react';
import { cn } from '@/lib/utils';
import { ChevronDown, ChevronUp, BrainCircuit } from 'lucide-react'; import { ChevronDown, ChevronUp, BrainCircuit } from 'lucide-react';
interface ThinkBoxProps { interface ThinkBoxProps {
content: string; content: string;
thinkingEnded: boolean;
} }
const ThinkBox = ({ content, thinkingEnded }: ThinkBoxProps) => { const ThinkBox = ({ content }: ThinkBoxProps) => {
const [isExpanded, setIsExpanded] = useState(true); const [isExpanded, setIsExpanded] = useState(false);
useEffect(() => {
if (thinkingEnded) {
setIsExpanded(false);
} else {
setIsExpanded(true);
}
}, [thinkingEnded]);
return ( return (
<div className="my-4 bg-light-secondary/50 dark:bg-dark-secondary/50 rounded-xl border border-light-200 dark:border-dark-200 overflow-hidden"> <div className="my-4 bg-light-secondary/50 dark:bg-dark-secondary/50 rounded-xl border border-light-200 dark:border-dark-200 overflow-hidden">

View File

@@ -1,169 +0,0 @@
import { Cloud, Sun, CloudRain, CloudSnow, Wind } from 'lucide-react';
import { useEffect, useState } from 'react';
const WeatherWidget = () => {
const [data, setData] = useState({
temperature: 0,
condition: '',
location: '',
humidity: 0,
windSpeed: 0,
icon: '',
temperatureUnit: 'C',
windSpeedUnit: 'm/s',
});
const [loading, setLoading] = useState(true);
const getApproxLocation = async () => {
const res = await fetch('https://ipwhois.app/json/');
const data = await res.json();
return {
latitude: data.latitude,
longitude: data.longitude,
city: data.city,
};
};
const getLocation = async (
callback: (location: {
latitude: number;
longitude: number;
city: string;
}) => void,
) => {
if (navigator.geolocation) {
const result = await navigator.permissions.query({
name: 'geolocation',
});
if (result.state === 'granted') {
navigator.geolocation.getCurrentPosition(async (position) => {
const res = await fetch(
`https://api-bdc.io/data/reverse-geocode-client?latitude=${position.coords.latitude}&longitude=${position.coords.longitude}&localityLanguage=en`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
},
);
const data = await res.json();
callback({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
city: data.locality,
});
});
} else if (result.state === 'prompt') {
callback(await getApproxLocation());
navigator.geolocation.getCurrentPosition((position) => {});
} else if (result.state === 'denied') {
callback(await getApproxLocation());
}
} else {
callback(await getApproxLocation());
}
};
const updateWeather = async () => {
getLocation(async (location) => {
const res = await fetch(`/api/weather`, {
method: 'POST',
body: JSON.stringify({
lat: location.latitude,
lng: location.longitude,
measureUnit: localStorage.getItem('measureUnit') ?? 'Metric',
}),
});
const data = await res.json();
if (res.status !== 200) {
console.error('Error fetching weather data');
setLoading(false);
return;
}
setData({
temperature: data.temperature,
condition: data.condition,
location: location.city,
humidity: data.humidity,
windSpeed: data.windSpeed,
icon: data.icon,
temperatureUnit: data.temperatureUnit,
windSpeedUnit: data.windSpeedUnit,
});
setLoading(false);
});
};
useEffect(() => {
updateWeather();
const intervalId = setInterval(updateWeather, 2 * 60 * 1000);
return () => clearInterval(intervalId);
}, []);
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-center w-full h-24 min-h-[96px] max-h-[96px] px-3 py-2 gap-3">
{loading ? (
<>
<div className="flex flex-col items-center justify-center w-16 min-w-16 max-w-16 h-full animate-pulse">
<div className="h-10 w-10 rounded-full bg-light-200 dark:bg-dark-200 mb-2" />
<div className="h-4 w-10 rounded bg-light-200 dark:bg-dark-200" />
</div>
<div className="flex flex-col justify-between flex-1 h-full py-1 animate-pulse">
<div className="flex flex-row items-center justify-between">
<div className="h-3 w-20 rounded bg-light-200 dark:bg-dark-200" />
<div className="h-3 w-12 rounded bg-light-200 dark:bg-dark-200" />
</div>
<div className="h-3 w-16 rounded bg-light-200 dark:bg-dark-200 mt-1" />
<div className="flex flex-row justify-between w-full mt-auto pt-1 border-t border-light-200 dark:border-dark-200">
<div className="h-3 w-16 rounded bg-light-200 dark:bg-dark-200" />
<div className="h-3 w-8 rounded bg-light-200 dark:bg-dark-200" />
</div>
</div>
</>
) : (
<>
<div className="flex flex-col items-center justify-center w-16 min-w-16 max-w-16 h-full">
<img
src={`/weather-ico/${data.icon}.svg`}
alt={data.condition}
className="h-10 w-auto"
/>
<span className="text-base font-semibold text-black dark:text-white">
{data.temperature}°{data.temperatureUnit}
</span>
</div>
<div className="flex flex-col justify-between flex-1 h-full py-2">
<div className="flex flex-row items-center justify-between">
<span className="text-sm font-semibold text-black dark:text-white">
{data.location}
</span>
<span className="flex items-center text-xs text-black/60 dark:text-white/60 font-medium">
<Wind className="w-3 h-3 mr-1" />
{data.windSpeed} {data.windSpeedUnit}
</span>
</div>
<span className="text-xs text-black/50 dark:text-white/50 italic">
{data.condition}
</span>
<div className="flex flex-row justify-between w-full mt-auto pt-2 border-t border-light-200/50 dark:border-dark-200/50 text-xs text-black/50 dark:text-white/50 font-medium">
<span>Humidity {data.humidity}%</span>
<span className="font-semibold text-black/70 dark:text-white/70">
Now
</span>
</div>
</div>
</>
)}
</div>
);
};
export default WeatherWidget;

View File

@@ -1,6 +1,6 @@
import { Message } from '@/components/ChatWindow'; import { Message } from '@/components/ChatWindow';
export const getSuggestions = async (chatHistory: Message[]) => { export const getSuggestions = async (chatHisory: Message[]) => {
const chatModel = localStorage.getItem('chatModel'); const chatModel = localStorage.getItem('chatModel');
const chatModelProvider = localStorage.getItem('chatModelProvider'); const chatModelProvider = localStorage.getItem('chatModelProvider');
@@ -13,7 +13,7 @@ export const getSuggestions = async (chatHistory: Message[]) => {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
chatHistory: chatHistory, chatHistory: chatHisory,
chatModel: { chatModel: {
provider: chatModelProvider, provider: chatModelProvider,
model: chatModel, model: chatModel,

View File

@@ -3,18 +3,32 @@ import {
RunnableMap, RunnableMap,
RunnableLambda, RunnableLambda,
} from '@langchain/core/runnables'; } from '@langchain/core/runnables';
import { ChatPromptTemplate } from '@langchain/core/prompts'; import { PromptTemplate } from '@langchain/core/prompts';
import formatChatHistoryAsString from '../utils/formatHistory'; import formatChatHistoryAsString from '../utils/formatHistory';
import { BaseMessage } from '@langchain/core/messages'; import { BaseMessage } from '@langchain/core/messages';
import { StringOutputParser } from '@langchain/core/output_parsers'; import { StringOutputParser } from '@langchain/core/output_parsers';
import { searchSearxng } from '../searxng'; import { searchSearxng } from '../searxng';
import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import LineOutputParser from '../outputParsers/lineOutputParser';
const imageSearchChainPrompt = ` const imageSearchChainPrompt = `
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search the web for images. You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search the web for images.
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation. You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation.
Output only the rephrased query wrapped in an XML <query> element. Do not include any explanation or additional text.
Example:
1. Follow up question: What is a cat?
Rephrased: A cat
2. Follow up question: What is a car? How does it works?
Rephrased: Car working
3. Follow up question: How does an AC work?
Rephrased: AC working
Conversation:
{chat_history}
Follow up question: {query}
Rephrased question:
`; `;
type ImageSearchChainInput = { type ImageSearchChainInput = {
@@ -40,39 +54,12 @@ const createImageSearchChain = (llm: BaseChatModel) => {
return input.query; return input.query;
}, },
}), }),
ChatPromptTemplate.fromMessages([ PromptTemplate.fromTemplate(imageSearchChainPrompt),
['system', imageSearchChainPrompt],
[
'user',
'<conversation>\n</conversation>\n<follow_up>\nWhat is a cat?\n</follow_up>',
],
['assistant', '<query>A cat</query>'],
[
'user',
'<conversation>\n</conversation>\n<follow_up>\nWhat is a car? How does it work?\n</follow_up>',
],
['assistant', '<query>Car working</query>'],
[
'user',
'<conversation>\n</conversation>\n<follow_up>\nHow does an AC work?\n</follow_up>',
],
['assistant', '<query>AC working</query>'],
[
'user',
'<conversation>{chat_history}</conversation>\n<follow_up>\n{query}\n</follow_up>',
],
]),
llm, llm,
strParser, strParser,
RunnableLambda.from(async (input: string) => { RunnableLambda.from(async (input: string) => {
const queryParser = new LineOutputParser({ input = input.replace(/<think>.*?<\/think>/g, '');
key: 'query',
});
return await queryParser.parse(input);
}),
RunnableLambda.from(async (input: string) => {
const res = await searchSearxng(input, { const res = await searchSearxng(input, {
engines: ['bing images', 'google images'], engines: ['bing images', 'google images'],
}); });

View File

@@ -3,19 +3,33 @@ import {
RunnableMap, RunnableMap,
RunnableLambda, RunnableLambda,
} from '@langchain/core/runnables'; } from '@langchain/core/runnables';
import { ChatPromptTemplate } from '@langchain/core/prompts'; import { PromptTemplate } from '@langchain/core/prompts';
import formatChatHistoryAsString from '../utils/formatHistory'; import formatChatHistoryAsString from '../utils/formatHistory';
import { BaseMessage } from '@langchain/core/messages'; import { BaseMessage } from '@langchain/core/messages';
import { StringOutputParser } from '@langchain/core/output_parsers'; import { StringOutputParser } from '@langchain/core/output_parsers';
import { searchSearxng } from '../searxng'; import { searchSearxng } from '../searxng';
import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import LineOutputParser from '../outputParsers/lineOutputParser';
const videoSearchChainPrompt = ` const VideoSearchChainPrompt = `
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search Youtube for videos. You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search Youtube for videos.
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation. You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation.
Output only the rephrased query wrapped in an XML <query> element. Do not include any explanation or additional text.
`; Example:
1. Follow up question: How does a car work?
Rephrased: How does a car work?
2. Follow up question: What is the theory of relativity?
Rephrased: What is theory of relativity
3. Follow up question: How does an AC work?
Rephrased: How does an AC work
Conversation:
{chat_history}
Follow up question: {query}
Rephrased question:
`;
type VideoSearchChainInput = { type VideoSearchChainInput = {
chat_history: BaseMessage[]; chat_history: BaseMessage[];
@@ -41,37 +55,12 @@ const createVideoSearchChain = (llm: BaseChatModel) => {
return input.query; return input.query;
}, },
}), }),
ChatPromptTemplate.fromMessages([ PromptTemplate.fromTemplate(VideoSearchChainPrompt),
['system', videoSearchChainPrompt],
[
'user',
'<conversation>\n</conversation>\n<follow_up>\nHow does a car work?\n</follow_up>',
],
['assistant', '<query>How does a car work?</query>'],
[
'user',
'<conversation>\n</conversation>\n<follow_up>\nWhat is the theory of relativity?\n</follow_up>',
],
['assistant', '<query>Theory of relativity</query>'],
[
'user',
'<conversation>\n</conversation>\n<follow_up>\nHow does an AC work?\n</follow_up>',
],
['assistant', '<query>AC working</query>'],
[
'user',
'<conversation>{chat_history}</conversation>\n<follow_up>\n{query}\n</follow_up>',
],
]),
llm, llm,
strParser, strParser,
RunnableLambda.from(async (input: string) => { RunnableLambda.from(async (input: string) => {
const queryParser = new LineOutputParser({ input = input.replace(/<think>.*?<\/think>/g, '');
key: 'query',
});
return await queryParser.parse(input);
}),
RunnableLambda.from(async (input: string) => {
const res = await searchSearxng(input, { const res = await searchSearxng(input, {
engines: ['youtube'], engines: ['youtube'],
}); });
@@ -103,8 +92,8 @@ const handleVideoSearch = (
input: VideoSearchChainInput, input: VideoSearchChainInput,
llm: BaseChatModel, llm: BaseChatModel,
) => { ) => {
const videoSearchChain = createVideoSearchChain(llm); const VideoSearchChain = createVideoSearchChain(llm);
return videoSearchChain.invoke(input); return VideoSearchChain.invoke(input);
}; };
export default handleVideoSearch; export default handleVideoSearch;

View File

@@ -31,21 +31,13 @@ interface Config {
}; };
OLLAMA: { OLLAMA: {
API_URL: string; API_URL: string;
API_KEY: string;
}; };
DEEPSEEK: { DEEPSEEK: {
API_KEY: string; API_KEY: string;
}; };
AIMLAPI: {
API_KEY: string;
};
LM_STUDIO: { LM_STUDIO: {
API_URL: string; API_URL: string;
}; };
LEMONADE: {
API_URL: string;
API_KEY: string;
};
CUSTOM_OPENAI: { CUSTOM_OPENAI: {
API_URL: string; API_URL: string;
API_KEY: string; API_KEY: string;
@@ -91,12 +83,8 @@ export const getSearxngApiEndpoint = () =>
export const getOllamaApiEndpoint = () => loadConfig().MODELS.OLLAMA.API_URL; export const getOllamaApiEndpoint = () => loadConfig().MODELS.OLLAMA.API_URL;
export const getOllamaApiKey = () => loadConfig().MODELS.OLLAMA.API_KEY;
export const getDeepseekApiKey = () => loadConfig().MODELS.DEEPSEEK.API_KEY; export const getDeepseekApiKey = () => loadConfig().MODELS.DEEPSEEK.API_KEY;
export const getAimlApiKey = () => loadConfig().MODELS.AIMLAPI.API_KEY;
export const getCustomOpenaiApiKey = () => export const getCustomOpenaiApiKey = () =>
loadConfig().MODELS.CUSTOM_OPENAI.API_KEY; loadConfig().MODELS.CUSTOM_OPENAI.API_KEY;
@@ -106,13 +94,7 @@ export const getCustomOpenaiApiUrl = () =>
export const getCustomOpenaiModelName = () => export const getCustomOpenaiModelName = () =>
loadConfig().MODELS.CUSTOM_OPENAI.MODEL_NAME; loadConfig().MODELS.CUSTOM_OPENAI.MODEL_NAME;
export const getLMStudioApiEndpoint = () => export const getLMStudioApiEndpoint = () => loadConfig().MODELS.LM_STUDIO.API_URL;
loadConfig().MODELS.LM_STUDIO.API_URL;
export const getLemonadeApiEndpoint = () =>
loadConfig().MODELS.LEMONADE.API_URL;
export const getLemonadeApiKey = () => loadConfig().MODELS.LEMONADE.API_KEY;
const mergeConfigs = (current: any, update: any): any => { const mergeConfigs = (current: any, update: any): any => {
if (update === null || update === undefined) { if (update === null || update === undefined) {

View File

@@ -3,10 +3,42 @@ import Database from 'better-sqlite3';
import * as schema from './schema'; import * as schema from './schema';
import path from 'path'; import path from 'path';
const DATA_DIR = process.env.DATA_DIR || process.cwd(); // Create SQLite connection
const sqlite = new Database(path.join(DATA_DIR, './data/db.sqlite')); const sqlite = new Database(path.join(process.cwd(), 'data/db.sqlite'));
const db = drizzle(sqlite, { const db = drizzle(sqlite, {
schema: schema, schema: schema,
}); });
// Initialize database schema
(function initializeDatabase() {
console.log('[DB] Checking database schema...');
try {
// Check if userPreferences table exists
const tableExists = sqlite.prepare(`
SELECT name FROM sqlite_master
WHERE type='table' AND name=?;
`).all('userPreferences').length > 0;
if (!tableExists) {
console.log('[DB] Creating userPreferences table...');
sqlite.prepare(`
CREATE TABLE userPreferences (
id INTEGER PRIMARY KEY,
userId TEXT NOT NULL UNIQUE,
categories TEXT DEFAULT '[]' NOT NULL,
languages TEXT DEFAULT '[]' NOT NULL,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
`).run();
console.log('[DB] userPreferences table created successfully.');
} else {
console.log('[DB] userPreferences table already exists.');
}
} catch (error) {
console.error('[DB] Error during database initialization:', error);
}
})();
export default db; export default db;

View File

@@ -1,122 +1,61 @@
import Database from 'better-sqlite3'; import db from './index';
import path from 'path'; import { userPreferences } from './schema';
import fs from 'fs'; import { sql } from 'drizzle-orm';
const DATA_DIR = process.env.DATA_DIR || process.cwd(); /**
const dbPath = path.join(DATA_DIR, './data/db.sqlite'); * Run database migrations to ensure schema is up to date.
* This is designed to run once at application startup.
*/
export async function runMigrations() {
console.log('[DB Migration] Checking database schema...');
const db = new Database(dbPath); try {
// Check if userPreferences table exists
const tableExists = await checkIfTableExists('userPreferences');
const migrationsFolder = path.join(DATA_DIR, 'drizzle'); if (!tableExists) {
console.log('[DB Migration] Creating userPreferences table...');
await createUserPreferencesTable();
console.log('[DB Migration] userPreferences table created successfully.');
} else {
console.log('[DB Migration] userPreferences table already exists.');
}
db.exec(` console.log('[DB Migration] Database schema is up to date.');
CREATE TABLE IF NOT EXISTS ran_migrations ( } catch (error) {
id INTEGER PRIMARY KEY AUTOINCREMENT, console.error('[DB Migration] Error during migration:', error);
name TEXT NOT NULL UNIQUE, // Don't throw the error - we want the application to continue even if migration fails
run_on DATETIME DEFAULT CURRENT_TIMESTAMP }
);
`);
function sanitizeSql(content: string) {
return content
.split(/\r?\n/)
.filter(
(l) => !l.trim().startsWith('-->') && !l.includes('statement-breakpoint'),
)
.join('\n');
} }
fs.readdirSync(migrationsFolder) /**
.filter((f) => f.endsWith('.sql')) * Check if a table exists in the database
.sort() */
.forEach((file) => { async function checkIfTableExists(tableName: string): Promise<boolean> {
const filePath = path.join(migrationsFolder, file); const result = db.$client.prepare(`
let content = fs.readFileSync(filePath, 'utf-8'); SELECT name FROM sqlite_master
content = sanitizeSql(content); WHERE type='table' AND name=?;
`).all(tableName);
const migrationName = file.split('_')[0] || file; return result.length > 0;
}
const already = db /**
.prepare('SELECT 1 FROM ran_migrations WHERE name = ?') * Create the userPreferences table using the schema definition
.get(migrationName); */
if (already) { async function createUserPreferencesTable() {
console.log(`Skipping already-applied migration: ${file}`); // Create the table using a raw SQL query based on our schema
return; db.$client.prepare(`
} CREATE TABLE userPreferences (
id INTEGER PRIMARY KEY,
userId TEXT NOT NULL UNIQUE,
categories TEXT DEFAULT '[]' NOT NULL,
languages TEXT DEFAULT '[]' NOT NULL,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
`).run();
}
try { // Run migrations automatically when this module is imported
if (migrationName === '0001') { runMigrations();
const messages = db
.prepare(
'SELECT id, type, metadata, content, chatId, messageId FROM messages',
)
.all();
db.exec(`
CREATE TABLE IF NOT EXISTS messages_with_sources (
id INTEGER PRIMARY KEY,
type TEXT NOT NULL,
chatId TEXT NOT NULL,
createdAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
messageId TEXT NOT NULL,
content TEXT,
sources TEXT DEFAULT '[]'
);
`);
const insertMessage = db.prepare(`
INSERT INTO messages_with_sources (type, chatId, createdAt, messageId, content, sources)
VALUES (?, ?, ?, ?, ?, ?)
`);
messages.forEach((msg: any) => {
while (typeof msg.metadata === 'string') {
msg.metadata = JSON.parse(msg.metadata || '{}');
}
if (msg.type === 'user') {
insertMessage.run(
'user',
msg.chatId,
msg.metadata['createdAt'],
msg.messageId,
msg.content,
'[]',
);
} else if (msg.type === 'assistant') {
insertMessage.run(
'assistant',
msg.chatId,
msg.metadata['createdAt'],
msg.messageId,
msg.content,
'[]',
);
const sources = msg.metadata['sources'] || '[]';
if (sources && sources.length > 0) {
insertMessage.run(
'source',
msg.chatId,
msg.metadata['createdAt'],
`${msg.messageId}-source`,
'',
JSON.stringify(sources),
);
}
}
});
db.exec('DROP TABLE messages;');
db.exec('ALTER TABLE messages_with_sources RENAME TO messages;');
} else {
db.exec(content);
}
db.prepare('INSERT OR IGNORE INTO ran_migrations (name) VALUES (?)').run(
migrationName,
);
console.log(`Applied migration: ${file}`);
} catch (err) {
console.error(`Failed to apply migration ${file}:`, err);
throw err;
}
});

View File

@@ -1,23 +1,15 @@
import { sql } from 'drizzle-orm'; import { sql } from 'drizzle-orm';
import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core'; import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core';
import { Document } from 'langchain/document';
export const messages = sqliteTable('messages', { export const messages = sqliteTable('messages', {
id: integer('id').primaryKey(), id: integer('id').primaryKey(),
role: text('type', { enum: ['assistant', 'user', 'source'] }).notNull(), content: text('content').notNull(),
chatId: text('chatId').notNull(), chatId: text('chatId').notNull(),
createdAt: text('createdAt')
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
messageId: text('messageId').notNull(), messageId: text('messageId').notNull(),
role: text('type', { enum: ['assistant', 'user'] }),
content: text('content'), metadata: text('metadata', {
sources: text('sources', {
mode: 'json', mode: 'json',
}) }),
.$type<Document[]>()
.default(sql`'[]'`),
}); });
interface File { interface File {
@@ -34,3 +26,17 @@ export const chats = sqliteTable('chats', {
.$type<File[]>() .$type<File[]>()
.default(sql`'[]'`), .default(sql`'[]'`),
}); });
// Add user preferences table for Discover features
export const userPreferences = sqliteTable('userPreferences', {
id: integer('id').primaryKey(),
userId: text('userId').notNull().unique(),
categories: text('categories', { mode: 'json' })
.$type<string[]>()
.default(sql`'[]'`), // Categories will be set at the application level
languages: text('languages', { mode: 'json' })
.$type<string[]>()
.default(sql`'[]'`), // Languages will be set at the application level
createdAt: text('createdAt').notNull(),
updatedAt: text('updatedAt').notNull(),
});

View File

@@ -1,817 +0,0 @@
'use client';
import {
AssistantMessage,
ChatTurn,
Message,
SourceMessage,
SuggestionMessage,
UserMessage,
} from '@/components/ChatWindow';
import {
createContext,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import crypto from 'crypto';
import { useSearchParams } from 'next/navigation';
import { toast } from 'sonner';
import { getSuggestions } from '../actions';
export type Section = {
userMessage: UserMessage;
assistantMessage: AssistantMessage | undefined;
parsedAssistantMessage: string | undefined;
speechMessage: string | undefined;
sourceMessage: SourceMessage | undefined;
thinkingEnded: boolean;
suggestions?: string[];
};
type ChatContext = {
messages: Message[];
chatTurns: ChatTurn[];
sections: Section[];
chatHistory: [string, string][];
files: File[];
fileIds: string[];
focusMode: string;
chatId: string | undefined;
optimizationMode: string;
isMessagesLoaded: boolean;
loading: boolean;
notFound: boolean;
messageAppeared: boolean;
isReady: boolean;
hasError: boolean;
setOptimizationMode: (mode: string) => void;
setFocusMode: (mode: string) => void;
setFiles: (files: File[]) => void;
setFileIds: (fileIds: string[]) => void;
sendMessage: (
message: string,
messageId?: string,
rewrite?: boolean,
) => Promise<void>;
rewrite: (messageId: string) => void;
};
export interface File {
fileName: string;
fileExtension: string;
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;
const chatModelProvidersKeys = Object.keys(chatModelProviders);
if (!chatModelProviders || chatModelProvidersKeys.length === 0) {
return toast.error('No chat models available');
} else {
chatModelProvider =
chatModelProvidersKeys.find(
(provider) =>
Object.keys(chatModelProviders[provider]).length > 0,
) || chatModelProvidersKeys[0];
}
if (
chatModelProvider === 'custom_openai' &&
Object.keys(chatModelProviders[chatModelProvider]).length === 0
) {
toast.error(
"Looks like you haven't configured any chat model providers. Please configure them from the settings page or the config file.",
);
return setHasError(true);
}
chatModel = Object.keys(chatModelProviders[chatModelProvider])[0];
}
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] ||
Object.keys(chatModelProviders[chatModelProvider]).length === 0)
) {
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]
) {
if (
chatModelProvider === 'custom_openai' &&
Object.keys(chatModelProviders[chatModelProvider]).length === 0
) {
toast.error(
"Looks like you haven't configured any chat model providers. Please configure them from the settings page or the config file.",
);
return setHasError(true);
}
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 as Message[];
setMessages(messages);
const chatTurns = messages.filter(
(msg): msg is ChatTurn => msg.role === 'user' || msg.role === 'assistant',
);
const history = chatTurns.map((msg) => {
return [msg.role, msg.content];
}) as [string, string][];
console.debug(new Date(), 'app:messages_loaded');
if (chatTurns.length > 0) {
document.title = chatTurns[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);
};
export const chatContext = createContext<ChatContext>({
chatHistory: [],
chatId: '',
fileIds: [],
files: [],
focusMode: '',
hasError: false,
isMessagesLoaded: false,
isReady: false,
loading: false,
messageAppeared: false,
messages: [],
chatTurns: [],
sections: [],
notFound: false,
optimizationMode: '',
rewrite: () => {},
sendMessage: async () => {},
setFileIds: () => {},
setFiles: () => {},
setFocusMode: () => {},
setOptimizationMode: () => {},
});
export const ChatProvider = ({
children,
id,
}: {
children: React.ReactNode;
id?: string;
}) => {
const searchParams = useSearchParams();
const initialMessage = searchParams.get('q');
const [chatId, setChatId] = useState<string | undefined>(id);
const [newChatCreated, setNewChatCreated] = useState(false);
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);
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);
const messagesRef = useRef<Message[]>([]);
const chatTurns = useMemo((): ChatTurn[] => {
return messages.filter(
(msg): msg is ChatTurn => msg.role === 'user' || msg.role === 'assistant',
);
}, [messages]);
const sections = useMemo<Section[]>(() => {
const sections: Section[] = [];
messages.forEach((msg, i) => {
if (msg.role === 'user') {
const nextUserMessageIndex = messages.findIndex(
(m, j) => j > i && m.role === 'user',
);
const aiMessage = messages.find(
(m, j) =>
j > i &&
m.role === 'assistant' &&
(nextUserMessageIndex === -1 || j < nextUserMessageIndex),
) as AssistantMessage | undefined;
const sourceMessage = messages.find(
(m, j) =>
j > i &&
m.role === 'source' &&
m.sources &&
(nextUserMessageIndex === -1 || j < nextUserMessageIndex),
) as SourceMessage | undefined;
let thinkingEnded = false;
let processedMessage = aiMessage?.content ?? '';
let speechMessage = aiMessage?.content ?? '';
let suggestions: string[] = [];
if (aiMessage) {
const citationRegex = /\[([^\]]+)\]/g;
const regex = /\[(\d+)\]/g;
if (processedMessage.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>';
}
}
if (aiMessage.content.includes('</think>')) {
thinkingEnded = true;
}
if (
sourceMessage &&
sourceMessage.sources &&
sourceMessage.sources.length > 0
) {
processedMessage = 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 = sourceMessage.sources?.[number - 1];
const url = source?.metadata?.url;
if (url) {
return `<citation href="${url}">${numStr}</citation>`;
} else {
return ``;
}
})
.join('');
return linksHtml;
},
);
speechMessage = aiMessage.content.replace(regex, '');
} else {
processedMessage = processedMessage.replace(regex, '');
speechMessage = aiMessage.content.replace(regex, '');
}
const suggestionMessage = messages.find(
(m, j) =>
j > i &&
m.role === 'suggestion' &&
(nextUserMessageIndex === -1 || j < nextUserMessageIndex),
) as SuggestionMessage | undefined;
if (suggestionMessage && suggestionMessage.suggestions.length > 0) {
suggestions = suggestionMessage.suggestions;
}
}
sections.push({
userMessage: msg,
assistantMessage: aiMessage,
sourceMessage: sourceMessage,
parsedAssistantMessage: processedMessage,
speechMessage,
thinkingEnded,
suggestions: suggestions,
});
}
});
return sections;
}, [messages]);
useEffect(() => {
checkConfig(
setChatModelProvider,
setEmbeddingModelProvider,
setIsConfigReady,
setHasError,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
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
}, []);
useEffect(() => {
messagesRef.current = messages;
}, [messages]);
useEffect(() => {
if (isMessagesLoaded && isConfigReady) {
setIsReady(true);
console.debug(new Date(), 'app:ready');
} else {
setIsReady(false);
}
}, [isMessagesLoaded, isConfigReady]);
const rewrite = (messageId: string) => {
const index = messages.findIndex((msg) => msg.messageId === messageId);
const chatTurnsIndex = chatTurns.findIndex(
(msg) => msg.messageId === messageId,
);
if (index === -1) return;
const message = chatTurns[chatTurnsIndex - 1];
setMessages((prev) => {
return [
...prev.slice(0, messages.length > 2 ? messages.indexOf(message) : 0),
];
});
setChatHistory((prev) => {
return [...prev.slice(0, chatTurns.length > 2 ? chatTurnsIndex - 1 : 0)];
});
sendMessage(message.content, message.messageId, true);
};
useEffect(() => {
if (isReady && initialMessage && isConfigReady) {
if (!isConfigReady) {
toast.error('Cannot send message before the configuration is ready');
return;
}
sendMessage(initialMessage);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isConfigReady, isReady, initialMessage]);
const sendMessage: ChatContext['sendMessage'] = async (
message,
messageId,
rewrite = false,
) => {
if (loading) return;
setLoading(true);
setMessageAppeared(false);
if (messages.length <= 1) {
window.history.replaceState(null, '', `/c/${chatId}`);
}
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') {
setMessages((prevMessages) => [
...prevMessages,
{
messageId: data.messageId,
chatId: chatId!,
role: 'source',
sources: data.data,
createdAt: new Date(),
},
]);
if (data.data.length > 0) {
setMessageAppeared(true);
}
}
if (data.type === 'message') {
if (!added) {
setMessages((prevMessages) => [
...prevMessages,
{
content: data.data,
messageId: data.messageId,
chatId: chatId!,
role: 'assistant',
createdAt: new Date(),
},
]);
added = true;
setMessageAppeared(true);
} else {
setMessages((prev) =>
prev.map((message) => {
if (
message.messageId === data.messageId &&
message.role === 'assistant'
) {
return { ...message, content: message.content + data.data };
}
return message;
}),
);
}
recievedMessage += data.data;
}
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();
}
/* Check if there are sources after message id's index and no suggestions */
const userMessageIndex = messagesRef.current.findIndex(
(msg) => msg.messageId === messageId && msg.role === 'user',
);
const sourceMessage = messagesRef.current.find(
(msg, i) => i > userMessageIndex && msg.role === 'source',
) as SourceMessage | undefined;
const suggestionMessageIndex = messagesRef.current.findIndex(
(msg, i) => i > userMessageIndex && msg.role === 'suggestion',
);
if (
sourceMessage &&
sourceMessage.sources.length > 0 &&
suggestionMessageIndex == -1
) {
const suggestions = await getSuggestions(messagesRef.current);
setMessages((prev) => {
return [
...prev,
{
role: 'suggestion',
suggestions: suggestions,
chatId: chatId!,
createdAt: new Date(),
messageId: crypto.randomBytes(7).toString('hex'),
},
];
});
}
}
};
const messageIndex = messages.findIndex((m) => m.messageId === messageId);
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: rewrite
? chatHistory.slice(0, messageIndex === -1 ? undefined : messageIndex)
: 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...');
}
}
};
return (
<chatContext.Provider
value={{
messages,
chatTurns,
sections,
chatHistory,
files,
fileIds,
focusMode,
chatId,
hasError,
isMessagesLoaded,
isReady,
loading,
messageAppeared,
notFound,
optimizationMode,
setFileIds,
setFiles,
setFocusMode,
setOptimizationMode,
rewrite,
sendMessage,
}}
>
{children}
</chatContext.Provider>
);
};
export const useChat = () => {
const ctx = useContext(chatContext);
return ctx;
};

View File

@@ -4,7 +4,7 @@ interface LineOutputParserArgs {
key?: string; key?: string;
} }
class LineOutputParser extends BaseOutputParser<string | undefined> { class LineOutputParser extends BaseOutputParser<string> {
private key = 'questions'; private key = 'questions';
constructor(args?: LineOutputParserArgs) { constructor(args?: LineOutputParserArgs) {
@@ -18,7 +18,7 @@ class LineOutputParser extends BaseOutputParser<string | undefined> {
lc_namespace = ['langchain', 'output_parsers', 'line_output_parser']; lc_namespace = ['langchain', 'output_parsers', 'line_output_parser'];
async parse(text: string): Promise<string | undefined> { async parse(text: string): Promise<string> {
text = text.trim() || ''; text = text.trim() || '';
const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/; const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/;
@@ -26,7 +26,7 @@ class LineOutputParser extends BaseOutputParser<string | undefined> {
const endKeyIndex = text.indexOf(`</${this.key}>`); const endKeyIndex = text.indexOf(`</${this.key}>`);
if (startKeyIndex === -1 || endKeyIndex === -1) { if (startKeyIndex === -1 || endKeyIndex === -1) {
return undefined; return '';
} }
const questionsStartIndex = const questionsStartIndex =

View File

@@ -0,0 +1,69 @@
export const academicSearchRetrieverPrompt = `
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question if needed so it is a standalone question that can be used by the LLM to search the web for information.
If it is a writing task or a simple hi, hello rather than a question, you need to return \`not_needed\` as the response.
Example:
1. Follow up question: How does stable diffusion work?
Rephrased: Stable diffusion working
2. Follow up question: What is linear algebra?
Rephrased: Linear algebra
3. Follow up question: What is the third law of thermodynamics?
Rephrased: Third law of thermodynamics
Conversation:
{chat_history}
Follow up question: {query}
Rephrased question:
`;
export const academicSearchResponsePrompt = `
You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses.
Your task is to provide answers that are:
- **Informative and relevant**: Thoroughly address the user's query using the given context.
- **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically.
- **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights.
- **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included.
- **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable.
### Formatting Instructions
- **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate.
- **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience.
- **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability.
- **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition. Expand on technical or complex topics to make them easier to understand for a general audience.
- **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title.
- **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate.
### Citation Requirements
- Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`.
- Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]."
- Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context.
- Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]."
- Always prioritize credibility and accuracy by linking all statements back to their respective context sources.
- Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation.
### Special Instructions
- If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity.
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
- If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query.
- You are set on focus mode 'Academic', this means you will be searching for academic papers and articles on the web.
### User instructions
These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines.
{systemInstructions}
### Example Output
- Begin with a brief introduction summarizing the event or query topic.
- Follow with detailed sections under clear headings, covering all aspects of the query if possible.
- Provide explanations or historical context as needed to enhance understanding.
- End with a conclusion or overall perspective if relevant.
<context>
{context}
</context>
Current date & time in ISO format (UTC timezone) is: {date}.
`;

View File

@@ -1,13 +1,32 @@
import { import {
webSearchResponsePrompt, academicSearchResponsePrompt,
webSearchRetrieverFewShots, academicSearchRetrieverPrompt,
webSearchRetrieverPrompt, } from './academicSearch';
} from './webSearch'; import {
redditSearchResponsePrompt,
redditSearchRetrieverPrompt,
} from './redditSearch';
import { webSearchResponsePrompt, webSearchRetrieverPrompt } from './webSearch';
import {
wolframAlphaSearchResponsePrompt,
wolframAlphaSearchRetrieverPrompt,
} from './wolframAlpha';
import { writingAssistantPrompt } from './writingAssistant'; import { writingAssistantPrompt } from './writingAssistant';
import {
youtubeSearchResponsePrompt,
youtubeSearchRetrieverPrompt,
} from './youtubeSearch';
export default { export default {
webSearchResponsePrompt, webSearchResponsePrompt,
webSearchRetrieverPrompt, webSearchRetrieverPrompt,
webSearchRetrieverFewShots, academicSearchResponsePrompt,
academicSearchRetrieverPrompt,
redditSearchResponsePrompt,
redditSearchRetrieverPrompt,
wolframAlphaSearchResponsePrompt,
wolframAlphaSearchRetrieverPrompt,
writingAssistantPrompt, writingAssistantPrompt,
youtubeSearchResponsePrompt,
youtubeSearchRetrieverPrompt,
}; };

Some files were not shown because too many files have changed in this diff Show More