Compare commits

..

30 Commits

Author SHA1 Message Date
Willie Zutz
380216e062 feat(editing): Adds the ability to edit messages 2025-05-13 01:41:41 -06:00
Willie Zutz
b96c4234f4 update(autocomplete): removed unecessary API logging 2025-05-11 14:14:35 -06:00
Willie Zutz
c3a1e434e5 feat(opensearch): Add autocomplete functionality to opensearch. This gets proxied to the searxng instance. 2025-05-11 13:44:47 -06:00
Willie Zutz
03b27d9cbb fix(UI): Fix results showing in light mode
feat(AI): Enhance system prompt for more reliable and relevant results
fix(Reddit): Reddit focus should work again. Works around SearXNG limitations of broken reddit search by using `site:reddit.com`
2025-05-10 15:09:00 -06:00
Willie Zutz
2a37f672ab feat(UI): Tabbed interface for messages. 2025-05-09 01:13:18 -06:00
Willie Zutz
85605fe166 feat(opensearch): Add BASE_URL config to support reverse proxy deployments 2025-05-08 20:58:04 -06:00
Willie Zutz
d839769d7e fix(opensearch): Implement dynamic OpenSearch XML generation and update layout reference 2025-05-08 00:39:17 -06:00
Willie Zutz
ddfe8c607d feat(search): Implement OpenSearch support
feat(search): Add searchUrl to message
feat(parsers): Enhance parsers to deal with some thinking models better.
2025-05-08 00:21:31 -06:00
Willie Zutz
f65b168388 feat(UI): Add the search query to the response.
Also, tweaked the search retriever prompt so it gives better search queries.
2025-05-07 01:16:51 -06:00
Willie Zutz
8796009141 fix(api): History rewriting should delete the current message.
fix(UI): Model changes shouldn't submit the form.
2025-05-06 23:45:46 -06:00
Willie Zutz
6220822c7c feat(app): Allow selecting the AI model at any time without opening the settings page.
Allow changing focus mode at any time while chatting.
Styling tweaks.
2025-05-05 00:05:19 -06:00
Willie Zutz
8241c87784 feat(app):
Adds auto scrolling.
Adds syntax highlighting to code blocks.
2025-05-03 16:03:04 -06:00
Willie Zutz
c8def1989a fix(Library): Returns metadata back to original format so sources continue to work. 2025-05-01 12:17:08 -06:00
Willie Zutz
a71e4ae10d feat(app):
- Adds true chat mode. Moves writing mode to local research mode.
- Adds model stats that shows model name and response time for messages.
- Adds settings toggle to allow turning off automatic suggestions
2025-05-01 11:32:13 -06:00
Willie Zutz
abf9dbb8ba Merge remote-tracking branch 'upstream/master' 2025-04-29 10:23:52 -06:00
Willie Zutz
b3aafba30c Updates yarn.lock 2025-04-20 13:52:40 -06:00
Willie Zutz
9f7fd178e0 Cleans up unnecessary file. 2025-04-20 13:15:40 -06:00
Willie Zutz
59a10d7d00 Ran prettier formatting 2025-04-20 13:12:23 -06:00
Willie Zutz
67ee9eff53 Apply context window everywhere. Ensure styling is good on all screen sizes. Cleanup inconsistencies with upstream branch. 2025-04-20 13:10:59 -06:00
Willie Zutz
0bb860b154 Fixes history rewrite bug 2025-04-20 11:57:48 -06:00
Willie Zutz
c0705d1d9e Support for Ollama context window configuration 2025-04-20 01:37:10 -06:00
Willie Zutz
73b5e8832e Removed compact mode 2025-04-19 13:36:50 -06:00
Willie Zutz
b2da9faeed More merge 2025-04-19 12:52:15 -06:00
Willie Zutz
1a2ad8a59d Merge remote-tracking branch 'upstream/master' 2025-04-19 12:51:57 -06:00
Willie Zutz
e0817d1008 Merge branch 'master' of github.com:ItzCrazyKns/Perplexica 2025-03-06 22:03:19 -07:00
Willie Zutz
690ef42861 Fixes a bug with rewriting where history wouldn't get removed. 2025-02-17 01:22:34 -07:00
Willie Zutz
b84e4e4ce6 Added an icon to indicate that compact mode is enabled. 2025-02-16 15:08:30 -07:00
Willie Zutz
467905d9f2 Added compact mode for more concise answers.
Made optimization mode persist between page refreshes.
Added mode switcher to chat so it can be changed while researching.
2025-02-16 15:02:05 -07:00
Willie Zutz
18b6f5b674 Updated formatting 2025-02-15 16:07:19 -07:00
Willie Zutz
2bdcbf20fb User customizable context window for ollama models. 2025-02-15 16:03:24 -07:00
112 changed files with 17786 additions and 11235 deletions

View File

94
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,94 @@
# GitHub Copilot Instructions for Perplexica
This file provides context and guidance for GitHub Copilot when working with the Perplexica codebase.
## Project Overview
Perplexica is an open-source AI-powered search engine that uses advanced machine learning to provide intelligent search results. It combines web search capabilities with LLM-based processing to understand and answer user questions, similar to Perplexity AI but fully open source.
## Key Components
- **Frontend**: Next.js application with React components (in `/src/components` and `/src/app`)
- **Backend Logic**: Node.js backend with API routes (in `/src/app/api`) and library code (in `/src/lib`)
- **Search Engine**: Uses SearXNG as a metadata search engine
- **LLM Integration**: Supports multiple models including OpenAI, Anthropic, Groq, Ollama (local models)
- **Database**: SQLite database managed with Drizzle ORM
## Architecture
The system works through these main steps:
- User submits a query
- The system determines if web search is needed
- If needed, it searches the web using SearXNG
- Results are ranked using embedding-based similarity search
- LLMs are used to generate a comprehensive response with cited sources
## Key Technologies
- **Frontend**: React, Next.js, Tailwind CSS
- **Backend**: Node.js
- **Database**: SQLite with Drizzle ORM
- **AI/ML**: LangChain for orchestration, various LLM providers
- **Search**: SearXNG integration
- **Embedding Models**: For re-ranking search results
## Project Structure
- `/src/app`: Next.js app directory with page components and API routes
- `/src/components`: Reusable UI components
- `/src/lib`: Backend functionality
- `/lib/search`: Search functionality and meta search agent
- `/lib/db`: Database schema and operations
- `/lib/providers`: LLM and embedding model integrations
- `/lib/prompts`: Prompt templates for LLMs
- `/lib/chains`: LangChain chains for various operations
## Focus Modes
Perplexica supports multiple specialized search modes:
- All Mode: General web search
- Local Research Mode: Research and interact with local files with citations
- Chat Mode: Have a creative conversation
- Academic Search Mode: For academic research
- YouTube Search Mode: For video content
- Wolfram Alpha Search Mode: For calculations and data analysis
- Reddit Search Mode: For community discussions
## Development Workflow
- Use `npm run dev` for local development
- Format code with `npm run format:write` before committing
- Database migrations: `npm run db:push`
- Build for production: `npm run build`
- Start production server: `npm run start`
## Configuration
The application uses a `config.toml` file (created from `sample.config.toml`) for configuration, including:
- API keys for various LLM providers
- Database settings
- Search engine configuration
- Similarity measure settings
## Common Tasks
When working on this codebase, you might need to:
- Add new API endpoints in `/src/app/api`
- Modify UI components in `/src/components`
- Extend search functionality in `/src/lib/search`
- Add new LLM providers in `/src/lib/providers`
- Update database schema in `/src/lib/db/schema.ts`
- Create new prompt templates in `/src/lib/prompts`
- Build new chains in `/src/lib/chains`
## AI Behavior
- Avoid conciliatory language
- It is not necessary to apologize
- If you don't know the answer, ask for clarification
- Do not add additional packages or dependencies unless explicitly requested
- Only make changes to the code that are relevant to the task at hand

2
.gitignore vendored
View File

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

View File

@@ -1,22 +1,6 @@
# 🚀 Perplexica - An AI-powered search engine 🔎 <!-- omit in toc --> # 🚀 Perplexica - An AI-powered search engine 🔎 <!-- omit in toc -->
<div align="center" markdown="1"> [![Discord](https://dcbadge.vercel.app/api/server/26aArMy8tT?style=flat&compact=true)](https://discord.gg/26aArMy8tT)
<sup>Special thanks to:</sup>
<br>
<br>
<a href="https://www.warp.dev/perplexica">
<img alt="Warp sponsorship" width="400" src="https://github.com/user-attachments/assets/775dd593-9b5f-40f1-bf48-479faff4c27b">
</a>
### [Warp, the AI Devtool that lives in your terminal](https://www.warp.dev/perplexica)
[Available for MacOS, Linux, & Windows](https://www.warp.dev/perplexica)
</div>
<hr/>
[![Discord](https://dcbadge.limes.pink/api/server/26aArMy8tT?style=flat)](https://discord.gg/26aArMy8tT)
![preview](.assets/perplexica-screenshot.png?) ![preview](.assets/perplexica-screenshot.png?)
@@ -53,13 +37,14 @@ 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.
- **Focus Modes:** Special modes to better answer specific types of questions. Perplexica currently has 6 focus modes: - **Focus Modes:** Special modes to better answer specific types of questions. Perplexica currently has 7 focus modes:
- **All Mode:** Searches the entire web to find the best results. - **All Mode:** Searches the entire web to find the best results.
- **Writing Assistant Mode:** Helpful for writing tasks that do not require searching the web. - **Local Research Mode:** Research and interact with local files with citations.
- **Chat Mode:** Have a truly creative conversation without web search.
- **Academic Search Mode:** Finds articles and papers, ideal for academic research. - **Academic Search Mode:** Finds articles and papers, ideal for academic research.
- **YouTube Search Mode:** Finds YouTube videos based on the search query. - **YouTube Search Mode:** Finds YouTube videos based on the search query.
- **Wolfram Alpha Search Mode:** Answers queries that need calculations or data analysis using Wolfram Alpha. - **Wolfram Alpha Search Mode:** Answers queries that need calculations or data analysis using Wolfram Alpha.
@@ -87,13 +72,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**.
- `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.
@@ -115,23 +96,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:
@@ -146,7 +117,7 @@ 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.
@@ -169,6 +140,40 @@ For more details, check out the full documentation [here](https://github.com/Itz
Perplexica runs on Next.js and handles all API requests. It works right away on the same network and stays accessible even with port forwarding. Perplexica runs on Next.js and handles all API requests. It works right away on the same network and stays accessible even with port forwarding.
### Running Behind a Reverse Proxy
When running Perplexica behind a reverse proxy (like Nginx, Apache, or Traefik), follow these steps to ensure proper functionality:
1. **Configure the BASE_URL setting**:
- In `config.toml`, set the `BASE_URL` parameter under the `[GENERAL]` section to your public-facing URL (e.g., `https://perplexica.yourdomain.com`)
2. **Ensure proper headers forwarding**:
- Your reverse proxy should forward the following headers:
- `X-Forwarded-Host`
- `X-Forwarded-Proto`
- `X-Forwarded-Port` (if using non-standard ports)
3. **Example Nginx configuration**:
```nginx
server {
listen 80;
server_name perplexica.yourdomain.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
}
```
This ensures that OpenSearch descriptions, browser integrations, and all URLs work properly when accessing Perplexica through your reverse proxy.
## One-Click Deployment ## One-Click Deployment
[![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)

View File

@@ -12,9 +12,6 @@ COPY public ./public
RUN mkdir -p /home/perplexica/data RUN mkdir -p /home/perplexica/data
RUN yarn build RUN yarn build
RUN yarn add --dev @vercel/ncc
RUN yarn ncc build ./src/lib/db/migrate.ts -o migrator
FROM node:20.18.0-slim FROM node:20.18.0-slim
WORKDIR /home/perplexica WORKDIR /home/perplexica
@@ -24,12 +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
CMD ["./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

@@ -55,7 +55,7 @@ The API accepts a JSON object in the request body, where you define the focus mo
- **`focusMode`** (string, required): Specifies which focus mode to use. Available modes: - **`focusMode`** (string, required): Specifies which focus mode to use. Available modes:
- `webSearch`, `academicSearch`, `writingAssistant`, `wolframAlphaSearch`, `youtubeSearch`, `redditSearch`. - `webSearch`, `academicSearch`, `localResearch`, `chat`, `wolframAlphaSearch`, `youtubeSearch`, `redditSearch`.
- **`optimizationMode`** (string, optional): Specifies the optimization mode to control the balance between performance and quality. Available modes: - **`optimizationMode`** (string, optional): Specifies the optimization mode to control the balance between performance and quality. Available modes:

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,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,13 +0,0 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1748405503809,
"tag": "0000_fuzzy_randall",
"breakpoints": true
}
]
}

View File

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

11860
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
{ {
"name": "perplexica-frontend", "name": "perplexica-frontend",
"version": "1.11.0-rc2", "version": "1.10.2",
"license": "MIT", "license": "MIT",
"author": "ItzCrazyKns", "author": "ItzCrazyKns",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev --turbopack",
"build": "npm run db:push && next build", "build": "npm run db:push && next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
@@ -15,15 +15,15 @@
"@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/ollama": "^0.2.0",
"@langchain/ollama": "^0.2.3", "@langchain/openai": "^0.0.25",
"@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",
"@types/react-syntax-highlighter": "^15.5.13",
"@xenova/transformers": "^2.17.2", "@xenova/transformers": "^2.17.2",
"axios": "^1.8.3", "axios": "^1.8.3",
"better-sqlite3": "^11.9.1", "better-sqlite3": "^11.9.1",
@@ -32,16 +32,15 @@
"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",
"pdf-parse": "^1.1.1", "pdf-parse": "^1.1.1",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-syntax-highlighter": "^15.6.1",
"react-text-to-speech": "^0.14.5", "react-text-to-speech": "^0.14.5",
"react-textarea-autosize": "^8.5.3", "react-textarea-autosize": "^8.5.3",
"sonner": "^1.4.41", "sonner": "^1.4.41",
@@ -53,7 +52,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.

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

@@ -1,6 +1,7 @@
[GENERAL] [GENERAL]
SIMILARITY_MEASURE = "cosine" # "cosine" or "dot" SIMILARITY_MEASURE = "cosine" # "cosine" or "dot"
KEEP_ALIVE = "5m" # How long to keep Ollama models loaded into memory. (Instead of using -1 use "-1m") KEEP_ALIVE = "5m" # How long to keep Ollama models loaded into memory. (Instead of using -1 use "-1m")
BASE_URL = "" # Optional. When set, overrides detected URL for OpenSearch and other public URLs
[MODELS.OPENAI] [MODELS.OPENAI]
API_KEY = "" API_KEY = ""
@@ -25,9 +26,6 @@ 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

View File

@@ -0,0 +1,78 @@
import { NextResponse } from 'next/server';
import { getSearxngApiEndpoint } from '@/lib/config';
/**
* Proxies autocomplete requests to SearXNG
*/
export async function GET(request: Request) {
try {
// Get the query parameter from the request URL
const { searchParams } = new URL(request.url);
const query = searchParams.get('q');
// Check if query exists
if (!query) {
return new NextResponse(JSON.stringify([query || '', []]), {
headers: {
'Content-Type': 'application/x-suggestions+json',
},
});
}
// Get the SearXNG API endpoint
const searxngUrl = getSearxngApiEndpoint();
if (!searxngUrl) {
console.error('SearXNG API endpoint not configured');
return new NextResponse(JSON.stringify([query, []]), {
headers: {
'Content-Type': 'application/x-suggestions+json',
},
});
}
// Format the URL (remove trailing slashes)
const formattedSearxngUrl = searxngUrl.replace(/\/+$/, '');
const autocompleteUrl = `${formattedSearxngUrl}/autocompleter?q=${encodeURIComponent(query)}`;
// Make the request to SearXNG
const response = await fetch(autocompleteUrl, {
method: 'POST', // The example XML used POST method
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
signal: AbortSignal.timeout(3000), // 3 second timeout
});
if (!response.ok) {
console.error(
`SearXNG autocompleter returned status: ${response.status}`,
);
return new NextResponse(JSON.stringify([query, []]), {
headers: {
'Content-Type': 'application/x-suggestions+json',
},
});
}
// Get the JSON response from SearXNG
const suggestions = await response.json();
// Return the suggestions in the expected format
return new NextResponse(JSON.stringify(suggestions), {
headers: {
'Content-Type': 'application/x-suggestions+json',
},
});
} catch (error) {
console.error('Error proxying to SearXNG autocompleter:', error);
// Return an empty suggestion list on error
const query = new URL(request.url).searchParams.get('q') || '';
return new NextResponse(JSON.stringify([query, []]), {
headers: {
'Content-Type': 'application/x-suggestions+json',
},
});
}
}

View File

@@ -1,22 +1,23 @@
import crypto from 'crypto';
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
import { EventEmitter } from 'stream';
import {
getAvailableChatModelProviders,
getAvailableEmbeddingModelProviders,
} from '@/lib/providers';
import db from '@/lib/db';
import { chats, messages as messagesSchema } from '@/lib/db/schema';
import { and, eq, gt } from 'drizzle-orm';
import { getFileDetails } from '@/lib/utils/files';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { ChatOpenAI } from '@langchain/openai';
import { import {
getCustomOpenaiApiKey, getCustomOpenaiApiKey,
getCustomOpenaiApiUrl, getCustomOpenaiApiUrl,
getCustomOpenaiModelName, getCustomOpenaiModelName,
} from '@/lib/config'; } from '@/lib/config';
import db from '@/lib/db';
import { chats, messages as messagesSchema } from '@/lib/db/schema';
import {
getAvailableChatModelProviders,
getAvailableEmbeddingModelProviders,
} from '@/lib/providers';
import { searchHandlers } from '@/lib/search'; import { searchHandlers } from '@/lib/search';
import { getFileDetails } from '@/lib/utils/files';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
import { ChatOllama } from '@langchain/ollama';
import { ChatOpenAI } from '@langchain/openai';
import crypto from 'crypto';
import { and, eq, gt } from 'drizzle-orm';
import { EventEmitter } from 'stream';
export const runtime = 'nodejs'; export const runtime = 'nodejs';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -30,6 +31,7 @@ type Message = {
type ChatModel = { type ChatModel = {
provider: string; provider: string;
name: string; name: string;
ollamaContextWindow?: number;
}; };
type EmbeddingModel = { type EmbeddingModel = {
@@ -48,15 +50,23 @@ type Body = {
systemInstructions: string; systemInstructions: string;
}; };
type ModelStats = {
modelName: string;
responseTime?: number;
};
const handleEmitterEvents = async ( const handleEmitterEvents = async (
stream: EventEmitter, stream: EventEmitter,
writer: WritableStreamDefaultWriter, writer: WritableStreamDefaultWriter,
encoder: TextEncoder, encoder: TextEncoder,
aiMessageId: string, aiMessageId: string,
chatId: string, chatId: string,
startTime: number,
) => { ) => {
let recievedMessage = ''; let recievedMessage = '';
let sources: any[] = []; let sources: any[] = [];
let searchQuery: string | undefined;
let searchUrl: string | undefined;
stream.on('data', (data) => { stream.on('data', (data) => {
const parsedData = JSON.parse(data); const parsedData = JSON.parse(data);
@@ -73,12 +83,22 @@ const handleEmitterEvents = async (
recievedMessage += parsedData.data; recievedMessage += parsedData.data;
} else if (parsedData.type === 'sources') { } else if (parsedData.type === 'sources') {
// Capture the search query if available
if (parsedData.searchQuery) {
searchQuery = parsedData.searchQuery;
}
if (parsedData.searchUrl) {
searchUrl = parsedData.searchUrl;
}
writer.write( writer.write(
encoder.encode( encoder.encode(
JSON.stringify({ JSON.stringify({
type: 'sources', type: 'sources',
data: parsedData.data, data: parsedData.data,
searchQuery: parsedData.searchQuery,
messageId: aiMessageId, messageId: aiMessageId,
searchUrl: searchUrl,
}) + '\n', }) + '\n',
), ),
); );
@@ -86,12 +106,34 @@ const handleEmitterEvents = async (
sources = parsedData.data; sources = parsedData.data;
} }
}); });
let modelStats: ModelStats = {
modelName: '',
};
stream.on('stats', (data) => {
const parsedData = JSON.parse(data);
if (parsedData.type === 'modelStats') {
modelStats = parsedData.data;
}
});
stream.on('end', () => { stream.on('end', () => {
const endTime = Date.now();
const duration = endTime - startTime;
modelStats = {
...modelStats,
responseTime: duration,
};
writer.write( writer.write(
encoder.encode( encoder.encode(
JSON.stringify({ JSON.stringify({
type: 'messageEnd', type: 'messageEnd',
messageId: aiMessageId, messageId: aiMessageId,
modelStats: modelStats,
searchQuery: searchQuery,
searchUrl: searchUrl,
}) + '\n', }) + '\n',
), ),
); );
@@ -106,6 +148,9 @@ const handleEmitterEvents = async (
metadata: JSON.stringify({ metadata: JSON.stringify({
createdAt: new Date(), createdAt: new Date(),
...(sources && sources.length > 0 && { sources }), ...(sources && sources.length > 0 && { sources }),
...(searchQuery && { searchQuery }),
modelStats: modelStats,
...(searchUrl && { searchUrl }),
}), }),
}) })
.execute(); .execute();
@@ -134,8 +179,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)
@@ -144,15 +187,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({
@@ -173,6 +210,16 @@ const handleHistorySave = async (
}) })
.execute(); .execute();
} else { } else {
await db
.update(messagesSchema)
.set({
content: message.content,
metadata: JSON.stringify({
createdAt: new Date(),
}),
})
.where(eq(messagesSchema.messageId, humanMessageId))
.execute();
await db await db
.delete(messagesSchema) .delete(messagesSchema)
.where( .where(
@@ -187,6 +234,7 @@ const handleHistorySave = async (
export const POST = async (req: Request) => { export const POST = async (req: Request) => {
try { try {
const startTime = Date.now();
const body = (await req.json()) as Body; const body = (await req.json()) as Body;
const { message } = body; const { message } = body;
@@ -227,7 +275,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: {
@@ -236,6 +284,11 @@ export const POST = async (req: Request) => {
}) as unknown as BaseChatModel; }) as unknown as BaseChatModel;
} else if (chatModelProvider && chatModel) { } else if (chatModelProvider && chatModel) {
llm = chatModel.model; llm = chatModel.model;
// Set context window size for Ollama models
if (llm instanceof ChatOllama && body.chatModel?.provider === 'ollama') {
llm.numCtx = body.chatModel.ollamaContextWindow || 2048;
}
} }
if (!llm) { if (!llm) {
@@ -290,7 +343,14 @@ export const POST = async (req: Request) => {
const writer = responseStream.writable.getWriter(); const writer = responseStream.writable.getWriter();
const encoder = new TextEncoder(); const encoder = new TextEncoder();
handleEmitterEvents(stream, writer, encoder, aiMessageId, message.chatId); handleEmitterEvents(
stream,
writer,
encoder,
aiMessageId,
message.chatId,
startTime,
);
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

@@ -1,5 +1,6 @@
import { import {
getAnthropicApiKey, getAnthropicApiKey,
getBaseUrl,
getCustomOpenaiApiKey, getCustomOpenaiApiKey,
getCustomOpenaiApiUrl, getCustomOpenaiApiUrl,
getCustomOpenaiModelName, getCustomOpenaiModelName,
@@ -8,10 +9,8 @@ import {
getOllamaApiEndpoint, getOllamaApiEndpoint,
getOpenaiApiKey, getOpenaiApiKey,
getDeepseekApiKey, getDeepseekApiKey,
getAimlApiKey,
getLMStudioApiEndpoint, getLMStudioApiEndpoint,
updateConfig, updateConfig,
getOllamaApiKey,
} from '@/lib/config'; } from '@/lib/config';
import { import {
getAvailableChatModelProviders, getAvailableChatModelProviders,
@@ -54,16 +53,15 @@ 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['lmStudioApiUrl'] = getLMStudioApiEndpoint();
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();
config['baseUrl'] = getBaseUrl();
return Response.json({ ...config }, { status: 200 }); return Response.json({ ...config }, { status: 200 });
} catch (err) { } catch (err) {
@@ -95,14 +93,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: { LM_STUDIO: {
API_URL: config.lmStudioApiUrl, API_URL: config.lmStudioApiUrl,
}, },

View File

@@ -1,80 +1,43 @@
import { searchSearxng } from '@/lib/searxng'; import { searchSearxng } from '@/lib/searxng';
const websitesForTopic = { const articleWebsites = [
tech: { 'yahoo.com',
query: ['technology news', 'latest tech', 'AI', 'science and innovation'], 'www.exchangewire.com',
links: ['techcrunch.com', 'wired.com', 'theverge.com'], 'businessinsider.com',
}, /* 'wired.com',
finance: { 'mashable.com',
query: ['finance news', 'economy', 'stock market', 'investing'], 'theverge.com',
links: ['bloomberg.com', 'cnbc.com', 'marketwatch.com'], 'gizmodo.com',
}, 'cnet.com',
art: { 'venturebeat.com', */
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; const topics = ['AI', 'tech']; /* TODO: Add UI to customize this */
export const GET = async (req: Request) => { export const GET = async (req: Request) => {
try { try {
const params = new URL(req.url).searchParams; const data = (
await Promise.all([
const mode: 'normal' | 'preview' = ...new Array(articleWebsites.length * topics.length)
(params.get('mode') as 'normal' | 'preview') || 'normal'; .fill(0)
const topic: Topic = (params.get('topic') as Topic) || 'tech'; .map(async (_, i) => {
const selectedTopic = websitesForTopic[topic];
let data = [];
if (mode === 'normal') {
const seenUrls = new Set();
data = (
await Promise.all(
selectedTopic.links.flatMap((link) =>
selectedTopic.query.map(async (query) => {
return ( 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( await searchSearxng(
`site:${selectedTopic.links[Math.floor(Math.random() * selectedTopic.links.length)]} ${selectedTopic.query[Math.floor(Math.random() * selectedTopic.query.length)]}`, `site:${articleWebsites[i % articleWebsites.length]} ${
topics[i % topics.length]
}`,
{ {
engines: ['bing news'], engines: ['bing news'],
pageno: 1, pageno: 1,
language: 'en',
}, },
) )
).results; ).results;
} }),
])
)
.map((result) => result)
.flat()
.sort(() => Math.random() - 0.5);
return Response.json( return Response.json(
{ {

View File

@@ -7,11 +7,13 @@ import {
import { getAvailableChatModelProviders } from '@/lib/providers'; import { getAvailableChatModelProviders } from '@/lib/providers';
import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
import { ChatOllama } from '@langchain/ollama';
import { ChatOpenAI } from '@langchain/openai'; import { ChatOpenAI } from '@langchain/openai';
interface ChatModel { interface ChatModel {
provider: string; provider: string;
model: string; model: string;
ollamaContextWindow?: number;
} }
interface ImageSearchBody { interface ImageSearchBody {
@@ -49,7 +51,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: {
@@ -58,6 +60,10 @@ export const POST = async (req: Request) => {
}) as unknown as BaseChatModel; }) as unknown as BaseChatModel;
} else if (chatModelProvider && chatModel) { } else if (chatModelProvider && chatModel) {
llm = chatModel.model; llm = chatModel.model;
// Set context window size for Ollama models
if (llm instanceof ChatOllama && body.chatModel?.provider === 'ollama') {
llm.numCtx = body.chatModel.ollamaContextWindow || 2048;
}
} }
if (!llm) { if (!llm) {

View File

@@ -0,0 +1,70 @@
import { NextResponse } from 'next/server';
import { getBaseUrl } from '@/lib/config';
/**
* Creates an OpenSearch XML response with the given origin URL
*/
function generateOpenSearchResponse(origin: string): NextResponse {
const opensearchXml = `<?xml version="1.0" encoding="utf-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
<ShortName>Perplexica</ShortName>
<LongName>Search with Perplexica AI</LongName>
<Description>Perplexica is a powerful AI-driven search engine that understands your queries and delivers relevant results.</Description>
<InputEncoding>UTF-8</InputEncoding>
<Image width="16" height="16" type="image/x-icon">${origin}/favicon.ico</Image>
<Url type="text/html" template="${origin}/?q={searchTerms}"/>
<Url rel="suggestions" type="application/x-suggestions+json" template="${origin}/api/autocomplete?q={searchTerms}"/>
<Url type="application/opensearchdescription+xml" rel="self" template="${origin}/api/opensearch"/>
</OpenSearchDescription>`;
return new NextResponse(opensearchXml, {
headers: {
'Content-Type': 'application/opensearchdescription+xml',
},
});
}
export async function GET(request: Request) {
// Check if a BASE_URL is explicitly configured
const configBaseUrl = getBaseUrl();
// If BASE_URL is configured, use it, otherwise detect from request
if (configBaseUrl) {
// Remove any trailing slashes for consistency
let origin = configBaseUrl.replace(/\/+$/, '');
return generateOpenSearchResponse(origin);
}
// Detect the correct origin, taking into account reverse proxy headers
const url = new URL(request.url);
let origin = url.origin;
// Extract headers
const headers = Object.fromEntries(request.headers);
// Check for X-Forwarded-Host and related headers to handle reverse proxies
if (headers['x-forwarded-host']) {
// Determine protocol: prefer X-Forwarded-Proto, fall back to original or https
const protocol =
headers['x-forwarded-proto'] || url.protocol.replace(':', '');
// Build the correct public-facing origin
origin = `${protocol}://${headers['x-forwarded-host']}`;
// Handle non-standard ports if specified in X-Forwarded-Port
if (headers['x-forwarded-port']) {
const port = headers['x-forwarded-port'];
// Don't append standard ports (80 for HTTP, 443 for HTTPS)
if (
!(
(protocol === 'http' && port === '80') ||
(protocol === 'https' && port === '443')
)
) {
origin = `${origin}:${port}`;
}
}
}
// Generate and return the OpenSearch response
return generateOpenSearchResponse(origin);
}

View File

@@ -13,12 +13,14 @@ import {
getCustomOpenaiModelName, getCustomOpenaiModelName,
} from '@/lib/config'; } from '@/lib/config';
import { searchHandlers } from '@/lib/search'; import { searchHandlers } from '@/lib/search';
import { ChatOllama } from '@langchain/ollama';
interface chatModel { interface chatModel {
provider: string; provider: string;
name: string; name: string;
customOpenAIKey?: string; customOpenAIKey?: string;
customOpenAIBaseURL?: string; customOpenAIBaseURL?: string;
ollamaContextWindow?: number;
} }
interface embeddingModel { interface embeddingModel {
@@ -81,7 +83,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:
@@ -96,6 +99,10 @@ export const POST = async (req: Request) => {
.model as unknown as BaseChatModel | undefined; .model as unknown as BaseChatModel | undefined;
} }
if (llm instanceof ChatOllama && body.chatModel?.provider === 'ollama') {
llm.numCtx = body.chatModel.ollamaContextWindow || 2048;
}
if ( if (
embeddingModelProviders[embeddingModelProvider] && embeddingModelProviders[embeddingModelProvider] &&
embeddingModelProviders[embeddingModelProvider][embeddingModel] embeddingModelProviders[embeddingModelProvider][embeddingModel]

View File

@@ -8,10 +8,12 @@ import { getAvailableChatModelProviders } from '@/lib/providers';
import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
import { ChatOpenAI } from '@langchain/openai'; import { ChatOpenAI } from '@langchain/openai';
import { ChatOllama } from '@langchain/ollama';
interface ChatModel { interface ChatModel {
provider: string; provider: string;
model: string; model: string;
ollamaContextWindow?: number;
} }
interface SuggestionsGenerationBody { interface SuggestionsGenerationBody {
@@ -48,7 +50,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: {
@@ -57,6 +59,10 @@ export const POST = async (req: Request) => {
}) as unknown as BaseChatModel; }) as unknown as BaseChatModel;
} else if (chatModelProvider && chatModel) { } else if (chatModelProvider && chatModel) {
llm = chatModel.model; llm = chatModel.model;
// Set context window size for Ollama models
if (llm instanceof ChatOllama && body.chatModel?.provider === 'ollama') {
llm.numCtx = body.chatModel.ollamaContextWindow || 2048;
}
} }
if (!llm) { if (!llm) {

View File

@@ -7,11 +7,13 @@ import {
import { getAvailableChatModelProviders } from '@/lib/providers'; import { getAvailableChatModelProviders } from '@/lib/providers';
import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
import { ChatOllama } from '@langchain/ollama';
import { ChatOpenAI } from '@langchain/openai'; import { ChatOpenAI } from '@langchain/openai';
interface ChatModel { interface ChatModel {
provider: string; provider: string;
model: string; model: string;
ollamaContextWindow?: number;
} }
interface VideoSearchBody { interface VideoSearchBody {
@@ -49,7 +51,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: {
@@ -58,6 +60,10 @@ export const POST = async (req: Request) => {
}) as unknown as BaseChatModel; }) as unknown as BaseChatModel;
} else if (chatModelProvider && chatModel) { } else if (chatModelProvider && chatModel) {
llm = chatModel.model; llm = chatModel.model;
// Set context window size for Ollama models
if (llm instanceof ChatOllama && body.chatModel?.provider === 'ollama') {
llm.numCtx = body.chatModel.ollamaContextWindow || 2048;
}
} }
if (!llm) { if (!llm) {

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

@@ -4,7 +4,6 @@ import { Search } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { cn } from '@/lib/utils';
interface Discover { interface Discover {
title: string; title: string;
@@ -13,38 +12,14 @@ interface Discover {
thumbnail: string; thumbnail: string;
} }
const topics: { key: string; display: string }[] = [
{
display: 'Tech & Science',
key: 'tech',
},
{
display: 'Finance',
key: 'finance',
},
{
display: 'Art & Culture',
key: 'art',
},
{
display: 'Sports',
key: 'sports',
},
{
display: 'Entertainment',
key: 'entertainment',
},
];
const Page = () => { const Page = () => {
const [discover, setDiscover] = useState<Discover[] | null>(null); const [discover, setDiscover] = useState<Discover[] | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [activeTopic, setActiveTopic] = useState<string>(topics[0].key);
const fetchArticles = async (topic: string) => { useEffect(() => {
setLoading(true); const fetchData = async () => {
try { try {
const res = await fetch(`/api/discover?topic=${topic}`, { const res = await fetch(`/api/discover`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -68,39 +43,10 @@ const Page = () => {
} }
}; };
useEffect(() => { fetchData();
fetchArticles(activeTopic); }, []);
}, [activeTopic]);
return ( return loading ? (
<>
<div>
<div className="flex flex-col pt-4">
<div className="flex items-center">
<Search />
<h1 className="text-3xl font-medium p-2">Discover</h1>
</div>
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
</div>
<div className="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-300 bg-cyan-300/30 border-cyan-300/60'
: '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>
{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
aria-hidden="true" aria-hidden="true"
@@ -120,7 +66,17 @@ const Page = () => {
</svg> </svg>
</div> </div>
) : ( ) : (
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-4 pb-28 pt-5 lg:pb-8 w-full justify-items-center lg:justify-items-start"> <>
<div>
<div className="flex flex-col pt-4">
<div className="flex items-center">
<Search />
<h1 className="text-3xl font-medium p-2">Discover</h1>
</div>
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
</div>
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-4 pb-28 lg:pb-8 w-full justify-items-center lg:justify-items-start">
{discover && {discover &&
discover?.map((item, i) => ( discover?.map((item, i) => (
<Link <Link
@@ -149,7 +105,6 @@ const Page = () => {
</Link> </Link>
))} ))}
</div> </div>
)}
</div> </div>
</> </>
); );

View File

@@ -11,11 +11,3 @@
display: none; display: none;
} }
} }
@media screen and (-webkit-min-device-pixel-ratio: 0) {
select,
textarea,
input {
font-size: 16px !important;
}
}

View File

@@ -26,6 +26,14 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html className="h-full" lang="en" suppressHydrationWarning> <html className="h-full" lang="en" suppressHydrationWarning>
<head>
<link
rel="search"
type="application/opensearchdescription+xml"
title="Perplexica Search"
href="/api/opensearch"
/>
</head>
<body className={cn('h-full', montserrat.className)}> <body className={cn('h-full', montserrat.className)}>
<ThemeProvider> <ThemeProvider>
<Sidebar>{children}</Sidebar> <Sidebar>{children}</Sidebar>

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

@@ -5,7 +5,7 @@ import { useEffect, useState } from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Switch } from '@headlessui/react'; import { Switch } from '@headlessui/react';
import ThemeSwitcher from '@/components/theme/Switcher'; import ThemeSwitcher from '@/components/theme/Switcher';
import { ImagesIcon, VideoIcon } from 'lucide-react'; import { ImagesIcon, VideoIcon, Layers3 } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { PROVIDER_METADATA } from '@/lib/providers'; import { PROVIDER_METADATA } from '@/lib/providers';
@@ -21,13 +21,12 @@ interface SettingsType {
anthropicApiKey: string; anthropicApiKey: string;
geminiApiKey: string; geminiApiKey: string;
ollamaApiUrl: string; ollamaApiUrl: string;
ollamaApiKey: string;
lmStudioApiUrl: string; lmStudioApiUrl: string;
deepseekApiKey: string; deepseekApiKey: string;
aimlApiKey: string;
customOpenaiApiKey: string; customOpenaiApiKey: string;
customOpenaiApiUrl: string; customOpenaiApiUrl: string;
customOpenaiModelName: string; customOpenaiModelName: string;
ollamaContextWindow: number;
} }
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
@@ -145,17 +144,19 @@ 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 [automaticSuggestions, setAutomaticSuggestions] = useState(true);
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>>({});
const [contextWindowSize, setContextWindowSize] = useState(2048);
const [isCustomContextWindow, setIsCustomContextWindow] = useState(false);
const predefinedContextSizes = [
1024, 2048, 3072, 4096, 8192, 16384, 32768, 65536, 131072,
];
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',
@@ -163,6 +164,7 @@ const Page = () => {
}); });
const data = (await res.json()) as SettingsType; const data = (await res.json()) as SettingsType;
setConfig(data); setConfig(data);
const chatModelProvidersKeys = Object.keys(data.chatModelProviders || {}); const chatModelProvidersKeys = Object.keys(data.chatModelProviders || {});
@@ -205,19 +207,19 @@ const Page = () => {
setChatModels(data.chatModelProviders || {}); setChatModels(data.chatModelProviders || {});
setEmbeddingModels(data.embeddingModelProviders || {}); setEmbeddingModels(data.embeddingModelProviders || {});
setAutomaticImageSearch( setAutomaticSuggestions(
localStorage.getItem('autoImageSearch') === 'true', localStorage.getItem('autoSuggestions') !== 'false', // default to true if not set
); );
setAutomaticVideoSearch( const storedContextWindow = parseInt(
localStorage.getItem('autoVideoSearch') === 'true', localStorage.getItem('ollamaContextWindow') ?? '2048',
);
setContextWindowSize(storedContextWindow);
setIsCustomContextWindow(
!predefinedContextSizes.includes(storedContextWindow),
); );
setSystemInstructions(localStorage.getItem('systemInstructions')!); setSystemInstructions(localStorage.getItem('systemInstructions')!);
setMeasureUnit(
localStorage.getItem('measureUnit')! as 'Imperial' | 'Metric',
);
setIsLoading(false); setIsLoading(false);
}; };
@@ -362,10 +364,8 @@ const Page = () => {
setConfig(data); setConfig(data);
} }
if (key === 'automaticImageSearch') { if (key === 'automaticSuggestions') {
localStorage.setItem('autoImageSearch', value.toString()); localStorage.setItem('autoSuggestions', value.toString());
} else if (key === 'automaticVideoSearch') {
localStorage.setItem('autoVideoSearch', value.toString());
} else if (key === 'chatModelProvider') { } else if (key === 'chatModelProvider') {
localStorage.setItem('chatModelProvider', value); localStorage.setItem('chatModelProvider', value);
} else if (key === 'chatModel') { } else if (key === 'chatModel') {
@@ -374,10 +374,10 @@ const Page = () => {
localStorage.setItem('embeddingModelProvider', value); localStorage.setItem('embeddingModelProvider', value);
} else if (key === 'embeddingModel') { } else if (key === 'embeddingModel') {
localStorage.setItem('embeddingModel', value); localStorage.setItem('embeddingModel', value);
} else if (key === 'ollamaContextWindow') {
localStorage.setItem('ollamaContextWindow', value.toString());
} 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);
@@ -426,35 +426,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">
@@ -462,29 +440,28 @@ const Page = () => {
<div className="flex items-center justify-between p-3 bg-light-secondary dark:bg-dark-secondary rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition-colors"> <div className="flex items-center justify-between p-3 bg-light-secondary dark:bg-dark-secondary rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition-colors">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="p-2 bg-light-200 dark:bg-dark-200 rounded-lg"> <div className="p-2 bg-light-200 dark:bg-dark-200 rounded-lg">
<ImagesIcon <Layers3
size={18} size={18}
className="text-black/70 dark:text-white/70" className="text-black/70 dark:text-white/70"
/> />
</div> </div>
<div> <div>
<p className="text-sm text-black/90 dark:text-white/90 font-medium"> <p className="text-sm text-black/90 dark:text-white/90 font-medium">
Automatic Image Search Automatic Suggestions
</p> </p>
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5"> <p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
Automatically search for relevant images in chat Automatically show related suggestions after responses
responses
</p> </p>
</div> </div>
</div> </div>
<Switch <Switch
checked={automaticImageSearch} checked={automaticSuggestions}
onChange={(checked) => { onChange={(checked) => {
setAutomaticImageSearch(checked); setAutomaticSuggestions(checked);
saveConfig('automaticImageSearch', checked); saveConfig('automaticSuggestions', checked);
}} }}
className={cn( className={cn(
automaticImageSearch automaticSuggestions
? 'bg-[#24A0ED]' ? 'bg-[#24A0ED]'
: 'bg-light-200 dark:bg-dark-200', : 'bg-light-200 dark:bg-dark-200',
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none', 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none',
@@ -492,49 +469,7 @@ const Page = () => {
> >
<span <span
className={cn( className={cn(
automaticImageSearch automaticSuggestions
? 'translate-x-6'
: 'translate-x-1',
'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
)}
/>
</Switch>
</div>
<div className="flex items-center justify-between p-3 bg-light-secondary dark:bg-dark-secondary rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition-colors">
<div className="flex items-center space-x-3">
<div className="p-2 bg-light-200 dark:bg-dark-200 rounded-lg">
<VideoIcon
size={18}
className="text-black/70 dark:text-white/70"
/>
</div>
<div>
<p className="text-sm text-black/90 dark:text-white/90 font-medium">
Automatic Video Search
</p>
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
Automatically search for relevant videos in chat
responses
</p>
</div>
</div>
<Switch
checked={automaticVideoSearch}
onChange={(checked) => {
setAutomaticVideoSearch(checked);
saveConfig('automaticVideoSearch', checked);
}}
className={cn(
automaticVideoSearch
? 'bg-[#24A0ED]'
: 'bg-light-200 dark:bg-dark-200',
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none',
)}
>
<span
className={cn(
automaticVideoSearch
? 'translate-x-6' ? 'translate-x-6'
: 'translate-x-1', : 'translate-x-1',
'inline-block h-4 w-4 transform rounded-full bg-white transition-transform', 'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
@@ -548,7 +483,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);
@@ -631,6 +566,78 @@ const Page = () => {
]; ];
})()} })()}
/> />
{selectedChatModelProvider === 'ollama' && (
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Chat Context Window Size
</p>
<Select
value={
isCustomContextWindow
? 'custom'
: contextWindowSize.toString()
}
onChange={(e) => {
const value = e.target.value;
if (value === 'custom') {
setIsCustomContextWindow(true);
} else {
setIsCustomContextWindow(false);
const numValue = parseInt(value);
setContextWindowSize(numValue);
setConfig((prev) => ({
...prev!,
ollamaContextWindow: numValue,
}));
saveConfig('ollamaContextWindow', numValue);
}
}}
options={[
...predefinedContextSizes.map((size) => ({
value: size.toString(),
label: `${size.toLocaleString()} tokens`,
})),
{ value: 'custom', label: 'Custom...' },
]}
/>
{isCustomContextWindow && (
<div className="mt-2">
<Input
type="number"
min={512}
value={contextWindowSize}
placeholder="Custom context window size (minimum 512)"
isSaving={savingStates['ollamaContextWindow']}
onChange={(e) => {
// Allow any value to be typed
const value =
parseInt(e.target.value) ||
contextWindowSize;
setContextWindowSize(value);
}}
onSave={(value) => {
// Validate only when saving
const numValue = Math.max(
512,
parseInt(value) || 2048,
);
setContextWindowSize(numValue);
setConfig((prev) => ({
...prev!,
ollamaContextWindow: numValue,
}));
saveConfig('ollamaContextWindow', numValue);
}}
/>
</div>
)}
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
{isCustomContextWindow
? 'Adjust the context window size for Ollama models (minimum 512 tokens)'
: 'Adjust the context window size for Ollama models'}
</p>
</div>
)}
</div> </div>
)} )}
</div> </div>
@@ -819,25 +826,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
@@ -914,25 +902,6 @@ const Page = () => {
/> />
</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"> <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">
LM Studio API URL LM Studio API URL

View File

@@ -1,35 +1,118 @@
'use client'; 'use client';
import { Fragment, useEffect, useRef, useState } from 'react'; import { Fragment, useEffect, useRef, useState } from 'react';
import MessageInput from './MessageInput';
import { File, Message } from './ChatWindow'; 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'; import MessageInput from './MessageInput';
const Chat = () => { const Chat = ({
const { messages, loading, messageAppeared } = useChat(); loading,
messages,
const [dividerWidth, setDividerWidth] = useState(0); sendMessage,
const dividerRef = useRef<HTMLDivElement | null>(null); scrollTrigger,
rewrite,
fileIds,
setFileIds,
files,
setFiles,
optimizationMode,
setOptimizationMode,
focusMode,
setFocusMode,
handleEditMessage,
}: {
messages: Message[];
sendMessage: (
message: string,
options?: {
messageId?: string;
rewriteIndex?: number;
suggestions?: string[];
},
) => void;
loading: boolean;
scrollTrigger: number;
rewrite: (messageId: string) => void;
fileIds: string[];
setFileIds: (fileIds: string[]) => void;
files: File[];
setFiles: (files: File[]) => void;
optimizationMode: string;
setOptimizationMode: (mode: string) => void;
focusMode: string;
setFocusMode: (mode: string) => void;
handleEditMessage: (messageId: string, content: string) => void;
}) => {
const [isAtBottom, setIsAtBottom] = useState(true);
const [manuallyScrolledUp, setManuallyScrolledUp] = useState(false);
const [inputStyle, setInputStyle] = useState<React.CSSProperties>({});
const messageEnd = useRef<HTMLDivElement | null>(null); const messageEnd = useRef<HTMLDivElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const SCROLL_THRESHOLD = 250; // pixels from bottom to consider "at bottom"
// Check if user is at bottom of page
useEffect(() => { useEffect(() => {
const updateDividerWidth = () => { const checkIsAtBottom = () => {
if (dividerRef.current) { const position = window.innerHeight + window.scrollY;
setDividerWidth(dividerRef.current.scrollWidth); const height = document.body.scrollHeight;
const atBottom = position >= height - SCROLL_THRESHOLD;
setIsAtBottom(atBottom);
};
// Initial check
checkIsAtBottom();
// Add scroll event listener
window.addEventListener('scroll', checkIsAtBottom);
return () => {
window.removeEventListener('scroll', checkIsAtBottom);
};
}, []);
// Detect wheel and touch events to identify user's scrolling direction
useEffect(() => {
const checkIsAtBottom = () => {
const position = window.innerHeight + window.scrollY;
const height = document.body.scrollHeight;
const atBottom = position >= height - SCROLL_THRESHOLD;
// If user scrolls to bottom, reset the manuallyScrolledUp flag
if (atBottom) {
setManuallyScrolledUp(false);
}
setIsAtBottom(atBottom);
};
const handleWheel = (e: WheelEvent) => {
// Positive deltaY means scrolling down, negative means scrolling up
if (e.deltaY < 0) {
// User is scrolling up
setManuallyScrolledUp(true);
} else if (e.deltaY > 0) {
checkIsAtBottom();
} }
}; };
updateDividerWidth(); const handleTouchStart = (e: TouchEvent) => {
// Immediately stop auto-scrolling on any touch interaction
setManuallyScrolledUp(true);
};
window.addEventListener('resize', updateDividerWidth); // Add event listeners
window.addEventListener('wheel', handleWheel, { passive: true });
window.addEventListener('touchstart', handleTouchStart, { passive: true });
return () => { return () => {
window.removeEventListener('resize', updateDividerWidth); window.removeEventListener('wheel', handleWheel);
window.removeEventListener('touchstart', handleTouchStart);
}; };
}); }, [isAtBottom]);
// Scroll when user sends a message
useEffect(() => { useEffect(() => {
const scroll = () => { const scroll = () => {
messageEnd.current?.scrollIntoView({ behavior: 'smooth' }); messageEnd.current?.scrollIntoView({ behavior: 'smooth' });
@@ -39,13 +122,52 @@ const Chat = () => {
document.title = `${messages[0].content.substring(0, 30)} - Perplexica`; document.title = `${messages[0].content.substring(0, 30)} - Perplexica`;
} }
if (messages[messages.length - 1]?.role == 'user') { // Always scroll when user sends a message
if (messages[messages.length - 1]?.role === 'user') {
scroll(); scroll();
setIsAtBottom(true); // Reset to true when user sends a message
setManuallyScrolledUp(false); // Reset manually scrolled flag when user sends a message
} }
}, [messages]); }, [messages]);
// Auto-scroll for assistant responses only if user is at bottom and hasn't manually scrolled up
useEffect(() => {
const position = window.innerHeight + window.scrollY;
const height = document.body.scrollHeight;
const atBottom = position >= height - SCROLL_THRESHOLD;
setIsAtBottom(atBottom);
if (isAtBottom && !manuallyScrolledUp && messages.length > 0) {
messageEnd.current?.scrollIntoView({ behavior: 'smooth' });
}
}, [scrollTrigger, isAtBottom, messages.length, manuallyScrolledUp]);
// Sync input width with main container width
useEffect(() => {
const updateInputStyle = () => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
setInputStyle({
width: rect.width,
left: rect.left,
right: window.innerWidth - rect.right,
});
}
};
// Initial calculation
updateInputStyle();
// Update on resize
window.addEventListener('resize', updateInputStyle);
return () => {
window.removeEventListener('resize', updateInputStyle);
};
}, []);
return ( return (
<div className="flex flex-col space-y-6 pt-8 pb-44 lg:pb-32 sm:mx-4 md:mx-8"> <div ref={containerRef} className="space-y-6 pt-8 pb-48 sm:mx-4 md:mx-8">
{messages.map((msg, i) => { {messages.map((msg, i) => {
const isLast = i === messages.length - 1; const isLast = i === messages.length - 1;
@@ -55,8 +177,12 @@ const Chat = () => {
key={i} key={i}
message={msg} message={msg}
messageIndex={i} messageIndex={i}
dividerRef={isLast ? dividerRef : undefined} history={messages}
loading={loading}
isLast={isLast} isLast={isLast}
rewrite={rewrite}
sendMessage={sendMessage}
handleEditMessage={handleEditMessage}
/> />
{!isLast && msg.role === 'assistant' && ( {!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" />
@@ -64,16 +190,53 @@ const Chat = () => {
</Fragment> </Fragment>
); );
})} })}
{loading && !messageAppeared && <MessageBoxLoading />} {loading && <MessageBoxLoading />}
<div ref={messageEnd} className="h-0" /> <div className="fixed bottom-24 lg:bottom-10 z-40" style={inputStyle}>
{dividerWidth > 0 && ( {/* Scroll to bottom button - appears above the MessageInput when user has scrolled up */}
<div {manuallyScrolledUp && !isAtBottom && (
className="bottom-24 lg:bottom-10 fixed z-40" <div className="absolute -top-14 right-2 z-10">
style={{ width: dividerWidth }} <button
onClick={() => {
setManuallyScrolledUp(false);
setIsAtBottom(true);
messageEnd.current?.scrollIntoView({ behavior: 'smooth' });
}}
className="bg-[#24A0ED] text-white hover:bg-opacity-85 transition duration-100 rounded-full px-4 py-2 shadow-lg flex items-center justify-center"
aria-label="Scroll to bottom"
> >
<MessageInput /> <svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 mr-1"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z"
clipRule="evenodd"
transform="rotate(180 10 10)"
/>
</svg>
<span className="text-sm">Scroll to bottom</span>
</button>
</div> </div>
)} )}
<MessageInput
firstMessage={messages.length === 0}
loading={loading}
sendMessage={sendMessage}
fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
focusMode={focusMode}
setFocusMode={setFocusMode}
/>
</div>
<div ref={messageEnd} className="h-0" />
</div> </div>
); );
}; };

View File

@@ -1,13 +1,22 @@
'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 type ModelStats = {
modelName: string;
responseTime?: number;
};
export type Message = { export type Message = {
messageId: string; messageId: string;
@@ -17,6 +26,9 @@ export type Message = {
role: 'user' | 'assistant'; role: 'user' | 'assistant';
suggestions?: string[]; suggestions?: string[];
sources?: Document[]; sources?: Document[];
modelStats?: ModelStats;
searchQuery?: string;
searchUrl?: string;
}; };
export interface File { export interface File {
@@ -25,8 +37,585 @@ 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 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 [scrollTrigger, setScrollTrigger] = useState(0);
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(() => {
const savedOptimizationMode = localStorage.getItem('optimizationMode');
if (savedOptimizationMode !== null) {
setOptimizationMode(savedOptimizationMode);
} else {
localStorage.setItem('optimizationMode', optimizationMode);
}
}, []);
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,
options?: {
messageId?: string;
suggestions?: string[];
editMode?: boolean;
},
) => {
setScrollTrigger((x) => (x === 0 ? -1 : 0));
// Special case: If we're just updating an existing message with suggestions
if (options?.suggestions && options.messageId) {
setMessages((prev) =>
prev.map((msg) => {
if (msg.messageId === options.messageId) {
return { ...msg, suggestions: options.suggestions };
}
return msg;
}),
);
return;
}
if (loading) return;
if (!isConfigReady) {
toast.error('Cannot send message before the configuration is ready');
return;
}
setLoading(true);
let sources: Document[] | undefined = undefined;
let recievedMessage = '';
let added = false;
let messageChatHistory = chatHistory;
// If the user is editing or rewriting a message, we need to remove the messages after it
const rewriteIndex = messages.findIndex(
(msg) => msg.messageId === options?.messageId,
);
if (rewriteIndex !== -1) {
setMessages((prev) => {
return [...prev.slice(0, rewriteIndex)];
});
messageChatHistory = chatHistory.slice(0, rewriteIndex);
setChatHistory(messageChatHistory);
setScrollTrigger((prev) => prev + 1);
}
const messageId =
options?.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,
searchQuery: data.searchQuery,
searchUrl: data.searchUrl,
createdAt: new Date(),
},
]);
added = true;
setScrollTrigger((prev) => prev + 1);
}
}
if (data.type === 'message') {
if (!added) {
setMessages((prevMessages) => [
...prevMessages,
{
content: data.data,
messageId: data.messageId,
chatId: chatId!,
role: 'assistant',
sources: sources,
createdAt: new Date(),
modelStats: {
modelName: data.modelName,
},
},
]);
added = true;
}
setMessages((prev) =>
prev.map((message) => {
if (message.messageId === data.messageId) {
return { ...message, content: message.content + data.data };
}
return message;
}),
);
recievedMessage += data.data;
setScrollTrigger((prev) => prev + 1);
}
if (data.type === 'messageEnd') {
setChatHistory((prevHistory) => [
...prevHistory,
['human', message],
['assistant', recievedMessage],
]);
// Always update the message, adding modelStats if available
setMessages((prev) =>
prev.map((message) => {
if (message.messageId === data.messageId) {
return {
...message,
// Include model stats if available, otherwise null
modelStats: data.modelStats || null,
// Make sure the searchQuery is preserved (if available in the message data)
searchQuery: message.searchQuery || data.searchQuery,
searchUrl: message.searchUrl || data.searchUrl,
};
}
return message;
}),
);
setLoading(false);
setScrollTrigger((prev) => prev + 1);
const lastMsg = messagesRef.current[messagesRef.current.length - 1];
const autoSuggestions = localStorage.getItem('autoSuggestions');
if (
lastMsg.role === 'assistant' &&
lastMsg.sources &&
lastMsg.sources.length > 0 &&
!lastMsg.suggestions &&
autoSuggestions !== 'false' // Default to true if not set
) {
const suggestions = await getSuggestions(messagesRef.current);
setMessages((prev) =>
prev.map((msg) => {
if (msg.messageId === lastMsg.messageId) {
return { ...msg, suggestions: suggestions };
}
return msg;
}),
);
}
}
};
const ollamaContextWindow =
localStorage.getItem('ollamaContextWindow') || '2048';
// Get the latest model selection from localStorage
const currentChatModelProvider = localStorage.getItem('chatModelProvider');
const currentChatModel = localStorage.getItem('chatModel');
// Use the most current model selection from localStorage, falling back to the state if not available
const modelProvider =
currentChatModelProvider || chatModelProvider.provider;
const modelName = currentChatModel || chatModelProvider.name;
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: messageChatHistory,
chatModel: {
name: modelName,
provider: modelProvider,
...(chatModelProvider.provider === 'ollama' && {
ollamaContextWindow: parseInt(ollamaContextWindow),
}),
},
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 messageIndex = messages.findIndex(
(msg) => msg.messageId === messageId,
);
if (messageIndex == -1) return;
sendMessage(messages[messageIndex - 1].content, {
messageId: messages[messageIndex - 1].messageId,
});
};
const handleEditMessage = async (messageId: string, newContent: string) => {
// Get the index of the message being edited
const messageIndex = messages.findIndex(
(msg) => msg.messageId === messageId,
);
if (messageIndex === -1) return;
try {
sendMessage(newContent, {
messageId,
editMode: true,
});
} catch (error) {
console.error('Error updating message:', error);
toast.error('Failed to update message');
}
};
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">
@@ -51,11 +640,36 @@ const ChatWindow = () => {
<div> <div>
{messages.length > 0 ? ( {messages.length > 0 ? (
<> <>
<Navbar /> <Navbar chatId={chatId!} messages={messages} />
<Chat /> <Chat
loading={loading}
messages={messages}
sendMessage={sendMessage}
scrollTrigger={scrollTrigger}
rewrite={rewrite}
fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
focusMode={focusMode}
setFocusMode={setFocusMode}
handleEditMessage={handleEditMessage}
/>
</> </>
) : ( ) : (
<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,11 +1,32 @@
import { Settings } from 'lucide-react'; import { Settings } from 'lucide-react';
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 MessageInput from './MessageInput';
import NewsArticleWidget from './NewsArticleWidget';
const EmptyChat = ({
sendMessage,
focusMode,
setFocusMode,
optimizationMode,
setOptimizationMode,
fileIds,
setFileIds,
files,
setFiles,
}: {
sendMessage: (message: string) => void;
focusMode: string;
setFocusMode: (mode: string) => void;
optimizationMode: string;
setOptimizationMode: (mode: string) => void;
fileIds: string[];
setFileIds: (fileIds: string[]) => void;
files: File[];
setFiles: (files: File[]) => void;
}) => {
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const EmptyChat = () => {
return ( 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,23 @@ 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 /> <MessageInput
</div> firstMessage={true}
<div className="flex flex-col w-full gap-4 mt-2 sm:flex-row sm:justify-center"> loading={false}
<div className="flex-1 w-full"> sendMessage={sendMessage}
<WeatherWidget /> focusMode={focusMode}
</div> setFocusMode={setFocusMode}
<div className="flex-1 w-full"> optimizationMode={optimizationMode}
<NewsArticleWidget /> setOptimizationMode={setOptimizationMode}
</div> fileIds={fileIds}
</div> setFileIds={setFileIds}
files={files}
setFiles={setFiles}
/>
</div> </div>
</div> </div>
); );

View File

@@ -1,86 +0,0 @@
import { ArrowRight } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import Focus from './MessageInputActions/Focus';
import Optimization from './MessageInputActions/Optimization';
import Attach from './MessageInputActions/Attach';
import { useChat } from '@/lib/hooks/useChat';
const EmptyChatMessageInput = () => {
const { sendMessage } = useChat();
/* const [copilotEnabled, setCopilotEnabled] = useState(false); */
const [message, setMessage] = useState('');
const inputRef = useRef<HTMLTextAreaElement | null>(null);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const activeElement = document.activeElement;
const isInputFocused =
activeElement?.tagName === 'INPUT' ||
activeElement?.tagName === 'TEXTAREA' ||
activeElement?.hasAttribute('contenteditable');
if (e.key === '/' && !isInputFocused) {
e.preventDefault();
inputRef.current?.focus();
}
};
document.addEventListener('keydown', handleKeyDown);
inputRef.current?.focus();
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, []);
return (
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage(message);
setMessage('');
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage(message);
setMessage('');
}
}}
className="w-full"
>
<div className="flex flex-col bg-light-secondary dark:bg-dark-secondary px-5 pt-5 pb-2 rounded-lg w-full border border-light-200 dark:border-dark-200">
<TextareaAutosize
ref={inputRef}
value={message}
onChange={(e) => setMessage(e.target.value)}
minRows={2}
className="bg-transparent placeholder:text-black/50 dark:placeholder:text-white/50 text-sm text-black dark:text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48"
placeholder="Ask anything..."
/>
<div className="flex flex-row items-center justify-between mt-4">
<div className="flex flex-row items-center space-x-2 lg:space-x-4">
<Focus />
<Attach showText />
</div>
<div className="flex flex-row items-center space-x-1 sm:space-x-4">
<Optimization />
<button
disabled={message.trim().length === 0}
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 disabled:bg-[#e0e0dc] dark:disabled:bg-[#ececec21] hover:bg-opacity-85 transition duration-100 rounded-full p-2"
>
<ArrowRight className="bg-background" size={17} />
</button>
</div>
</div>
</div>
</form>
);
};
export default EmptyChatMessageInput;

View File

@@ -0,0 +1,82 @@
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { Info } from 'lucide-react';
import { ModelStats } from '../ChatWindow';
import { cn } from '@/lib/utils';
interface ModelInfoButtonProps {
modelStats: ModelStats | null;
}
const ModelInfoButton: React.FC<ModelInfoButtonProps> = ({ modelStats }) => {
const [showPopover, setShowPopover] = useState(false);
const popoverRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
// Always render, using "Unknown" as fallback if model info isn't available
const modelName = modelStats?.modelName || 'Unknown';
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
popoverRef.current &&
!popoverRef.current.contains(event.target as Node) &&
buttonRef.current &&
!buttonRef.current.contains(event.target as Node)
) {
setShowPopover(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<div className="relative">
<button
ref={buttonRef}
className="p-1 ml-1 text-black/70 dark:text-white/70 rounded-full hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
onClick={() => setShowPopover(!showPopover)}
aria-label="Show model information"
>
<Info size={18} />
</button>
{showPopover && (
<div
ref={popoverRef}
className="absolute z-10 left-6 top-0 w-64 rounded-md shadow-lg bg-white dark:bg-dark-secondary border border-light-200 dark:border-dark-200"
>
<div className="py-2 px-3">
<h4 className="text-sm font-medium mb-2 text-black dark:text-white">
Model Information
</h4>
<div className="space-y-1 text-xs">
<div className="flex justify-between">
<span className="text-black/70 dark:text-white/70">Model:</span>
<span className="text-black dark:text-white font-medium">
{modelName}
</span>
</div>
{modelStats?.responseTime && (
<div className="flex justify-between">
<span className="text-black/70 dark:text-white/70">
Response time:
</span>
<span className="text-black dark:text-white font-medium">
{(modelStats.responseTime / 1000).toFixed(2)}s
</span>
</div>
)}
</div>
</div>
</div>
)}
</div>
);
};
export default ModelInfoButton;

View File

@@ -1,139 +1,56 @@
'use client';
/* eslint-disable @next/next/no-img-element */
import React, { MutableRefObject, useEffect, useState } from 'react';
import { Message } from './ChatWindow';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { import { Check, Pencil, X } from 'lucide-react';
BookCopy, import { useState } from 'react';
Disc3, import { Message } from './ChatWindow';
Volume2, import MessageTabs from './MessageTabs';
StopCircle,
Layers3,
Plus,
} from 'lucide-react';
import Markdown, { MarkdownToJSX } from 'markdown-to-jsx';
import Copy from './MessageActions/Copy';
import Rewrite from './MessageActions/Rewrite';
import MessageSources from './MessageSources';
import SearchImages from './SearchImages';
import SearchVideos from './SearchVideos';
import { useSpeech } from 'react-text-to-speech';
import ThinkBox from './ThinkBox';
import { useChat } from '@/lib/hooks/useChat';
const ThinkTagProcessor = ({
children,
thinkingEnded,
}: {
children: React.ReactNode;
thinkingEnded: boolean;
}) => {
return (
<ThinkBox content={children as string} thinkingEnded={thinkingEnded} />
);
};
const MessageBox = ({ const MessageBox = ({
message, message,
messageIndex, messageIndex,
dividerRef, history,
loading,
isLast, isLast,
rewrite,
sendMessage,
handleEditMessage,
}: { }: {
message: Message; message: Message;
messageIndex: number; messageIndex: number;
dividerRef?: MutableRefObject<HTMLDivElement | null>; history: Message[];
loading: boolean;
isLast: boolean; isLast: boolean;
rewrite: (messageId: string) => void;
sendMessage: (
message: string,
options?: {
messageId?: string;
rewriteIndex?: number;
suggestions?: string[];
},
) => void;
handleEditMessage: (messageId: string, content: string) => void;
}) => { }) => {
const { loading, messages: history, sendMessage, rewrite } = useChat(); // Local state for editing functionality
const [isEditing, setIsEditing] = useState(false);
const [editedContent, setEditedContent] = useState('');
const [parsedMessage, setParsedMessage] = useState(message.content); // Initialize editing
const [speechMessage, setSpeechMessage] = useState(message.content); const startEditMessage = () => {
const [thinkingEnded, setThinkingEnded] = useState(false); setIsEditing(true);
setEditedContent(message.content);
useEffect(() => {
const citationRegex = /\[([^\]]+)\]/g;
const regex = /\[(\d+)\]/g;
let processedMessage = message.content;
if (message.role === 'assistant' && message.content.includes('<think>')) {
const openThinkTag = processedMessage.match(/<think>/g)?.length || 0;
const closeThinkTag = processedMessage.match(/<\/think>/g)?.length || 0;
if (openThinkTag > closeThinkTag) {
processedMessage += '</think> <a> </a>'; // The extra <a> </a> is to prevent the the think component from looking bad
}
}
if (message.role === 'assistant' && message.content.includes('</think>')) {
setThinkingEnded(true);
}
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 ``;
}
})
.join('');
return linksHtml;
},
),
);
setSpeechMessage(message.content.replace(regex, ''));
return;
} else if (
message.role === 'assistant' &&
message?.sources &&
message.sources.length === 0
) {
setParsedMessage(processedMessage.replace(regex, ''));
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 markdownOverrides: MarkdownToJSX.Options = {
overrides: {
think: {
component: ThinkTagProcessor,
props: {
thinkingEnded: thinkingEnded,
},
},
},
}; };
// Cancel editing
const cancelEditMessage = () => {
setIsEditing(false);
setEditedContent('');
};
// Save edits
const saveEditMessage = () => {
handleEditMessage(message.messageId, editedContent);
setIsEditing(false);
};
return ( return (
<div> <div>
{message.role === 'user' && ( {message.role === 'user' && (
@@ -144,135 +61,65 @@ const MessageBox = ({
'break-words', 'break-words',
)} )}
> >
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12"> {isEditing ? (
{message.content} <div className="w-full">
</h2> <textarea
</div> className="w-full p-3 text-lg bg-light-100 dark:bg-dark-100 rounded-lg border border-light-secondary dark:border-dark-secondary text-black dark:text-white focus:outline-none focus:border-[#24A0ED] transition duration-200 min-h-[120px] font-medium"
)} value={editedContent}
onChange={(e) => setEditedContent(e.target.value)}
{message.role === 'assistant' && ( autoFocus
<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-row items-center space-x-2">
<BookCopy className="text-black dark:text-white" size={20} />
<h3 className="text-black dark:text-white font-medium text-xl">
Sources
</h3>
</div>
<MessageSources sources={message.sources} />
</div>
)}
<div className="flex flex-col space-y-2">
<div className="flex flex-row items-center space-x-2">
<Disc3
className={cn(
'text-black dark:text-white',
isLast && loading ? 'animate-spin' : 'animate-none',
)}
size={20}
/> />
<h3 className="text-black dark:text-white font-medium text-xl"> <div className="flex flex-row space-x-2 mt-3 justify-end">
Answer
</h3>
</div>
<Markdown
className={cn(
'prose prose-h1:mb-3 prose-h2:mb-2 prose-h2:mt-6 prose-h2:font-[800] prose-h3:mt-4 prose-h3:mb-1.5 prose-h3:font-[600] dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 font-[400]',
'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">
{/* <button className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black text-black dark:hover:text-white">
<Share size={18} />
</button> */}
<Rewrite rewrite={rewrite} messageId={message.messageId} />
</div>
<div className="flex flex-row items-center space-x-1">
<Copy initialMessage={message.content} message={message} />
<button <button
onClick={() => { onClick={cancelEditMessage}
if (speechStatus === 'started') { className="p-2 rounded-full bg-light-secondary dark:bg-dark-secondary hover:bg-light-200 dark:hover:bg-dark-200 transition duration-200 text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white"
stop(); aria-label="Cancel"
} else { title="Cancel"
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' ? ( <X size={18} />
<StopCircle size={18} /> </button>
) : ( <button
<Volume2 size={18} /> onClick={saveEditMessage}
)} className="p-2 rounded-full bg-[#24A0ED] hover:bg-[#1a8ad3] transition duration-200 text-white disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Save changes"
title="Save changes"
disabled={!editedContent.trim()}
>
<Check size={18} className="text-white" />
</button> </button>
</div> </div>
</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 items-center">
<div className="flex flex-col space-y-3 text-black dark:text-white"> <h2 className="text-black dark:text-white font-medium text-3xl">
<div className="flex flex-row items-center space-x-2 mt-4"> {message.content}
<Layers3 /> </h2>
<h3 className="text-xl font-medium">Related</h3> <button
</div> onClick={startEditMessage}
<div className="flex flex-col space-y-3"> className="ml-3 p-2 rounded-xl bg-light-secondary dark:bg-dark-secondary text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white flex-shrink-0"
{message.suggestions.map((suggestion, i) => ( aria-label="Edit message"
<div title="Edit message"
className="flex flex-col space-y-3 text-sm"
key={i}
> >
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" /> <Pencil size={18} />
<div </button>
onClick={() => {
sendMessage(suggestion);
}}
className="cursor-pointer flex flex-row justify-between font-medium space-x-2 items-center"
>
<p className="transition duration-200 hover:text-[#24A0ED]">
{suggestion}
</p>
<Plus
size={20}
className="text-[#24A0ED] flex-shrink-0"
/>
</div>
</div>
))}
</div>
</div> </div>
</> </>
)} )}
</div> </div>
</div> )}
<div className="lg:sticky lg:top-20 flex flex-col items-center space-y-3 w-full lg:w-3/12 z-30 h-full pb-4">
<SearchImages {message.role === 'assistant' && (
<MessageTabs
query={history[messageIndex - 1].content} query={history[messageIndex - 1].content}
chatHistory={history.slice(0, messageIndex - 1)} chatHistory={history.slice(0, messageIndex - 1)}
messageId={message.messageId} messageId={message.messageId}
message={message}
isLast={isLast}
loading={loading}
rewrite={rewrite}
sendMessage={sendMessage}
/> />
<SearchVideos
chatHistory={history.slice(0, messageIndex - 1)}
query={history[messageIndex - 1].content}
messageId={message.messageId}
/>
</div>
</div>
)} )}
</div> </div>
); );

View File

@@ -1,115 +1,148 @@
import { cn } from '@/lib/utils'; import { ArrowRight, ArrowUp } from 'lucide-react';
import { ArrowUp } 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 Attach from './MessageInputActions/Attach';
import CopilotToggle from './MessageInputActions/Copilot';
import { File } from './ChatWindow'; import { File } from './ChatWindow';
import AttachSmall from './MessageInputActions/AttachSmall'; import Attach from './MessageInputActions/Attach';
import { useChat } from '@/lib/hooks/useChat'; import Focus from './MessageInputActions/Focus';
import ModelSelector from './MessageInputActions/ModelSelector';
import Optimization from './MessageInputActions/Optimization';
const MessageInput = () => { const MessageInput = ({
const { loading, sendMessage } = useChat(); sendMessage,
loading,
const [copilotEnabled, setCopilotEnabled] = useState(false); fileIds,
setFileIds,
files,
setFiles,
optimizationMode,
setOptimizationMode,
focusMode,
setFocusMode,
firstMessage,
}: {
sendMessage: (message: string) => void;
loading: boolean;
fileIds: string[];
setFileIds: (fileIds: string[]) => void;
files: File[];
setFiles: (files: File[]) => void;
optimizationMode: string;
setOptimizationMode: (mode: string) => void;
focusMode: string;
setFocusMode: (mode: string) => void;
firstMessage: boolean;
}) => {
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [textareaRows, setTextareaRows] = useState(1); const [selectedModel, setSelectedModel] = useState<{
const [mode, setMode] = useState<'multi' | 'single'>('single'); provider: string;
model: string;
} | null>(null);
useEffect(() => { useEffect(() => {
if (textareaRows >= 2 && message && mode === 'single') { // Load saved model preferences from localStorage
setMode('multi'); const chatModelProvider = localStorage.getItem('chatModelProvider');
} else if (!message && mode === 'multi') { const chatModel = localStorage.getItem('chatModel');
setMode('single');
if (chatModelProvider && chatModel) {
setSelectedModel({
provider: chatModelProvider,
model: chatModel,
});
} }
}, [textareaRows, mode, message]); }, []);
const inputRef = useRef<HTMLTextAreaElement | null>(null); const inputRef = useRef<HTMLTextAreaElement | null>(null);
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
const activeElement = document.activeElement; const activeElement = document.activeElement;
const isInputFocused = const isInputFocused =
activeElement?.tagName === 'INPUT' || activeElement?.tagName === 'INPUT' ||
activeElement?.tagName === 'TEXTAREA' || activeElement?.tagName === 'TEXTAREA' ||
activeElement?.hasAttribute('contenteditable'); activeElement?.hasAttribute('contenteditable');
if (e.key === '/' && !isInputFocused) { if (e.key === '/' && !isInputFocused) {
e.preventDefault(); e.preventDefault();
inputRef.current?.focus(); inputRef.current?.focus();
} }
}; };
document.addEventListener('keydown', handleKeyDown); document.addEventListener('keydown', handleKeyDown);
return () => { return () => {
document.removeEventListener('keydown', handleKeyDown); document.removeEventListener('keydown', handleKeyDown);
}; };
}, []); }, []);
// Function to handle message submission
const handleSubmitMessage = () => {
// Only submit if we have a non-empty message and not currently loading
if (loading || message.trim().length === 0) return;
// Make sure the selected model is used when sending a message
if (selectedModel) {
localStorage.setItem('chatModelProvider', selectedModel.provider);
localStorage.setItem('chatModel', selectedModel.model);
}
sendMessage(message);
setMessage('');
};
return ( return (
<form <form
onSubmit={(e) => { onSubmit={(e) => {
if (loading) return;
e.preventDefault(); e.preventDefault();
sendMessage(message); handleSubmitMessage();
setMessage('');
}} }}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && !loading) { if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
sendMessage(message); handleSubmitMessage();
setMessage('');
} }
}} }}
className={cn( className="w-full"
'bg-light-secondary dark:bg-dark-secondary p-4 flex items-center overflow-hidden border border-light-200 dark:border-dark-200',
mode === 'multi' ? 'flex-col rounded-lg' : 'flex-row rounded-full',
)}
> >
{mode === 'single' && <AttachSmall />} <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}
onChange={(e) => setMessage(e.target.value)} onChange={(e) => setMessage(e.target.value)}
onHeightChange={(height, props) => { minRows={2}
setTextareaRows(Math.ceil(height / props.rowHeight)); className="bg-transparent placeholder:text-black/50 dark:placeholder:text-white/50 text-sm text-black dark:text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48"
}} placeholder={firstMessage ? 'Ask anything...' : 'Ask a follow-up'}
className="transition bg-transparent dark:placeholder:text-white/50 placeholder:text-sm text-sm dark:text-white resize-none focus:outline-none w-full px-2 max-h-24 lg:max-h-36 xl:max-h-48 flex-grow flex-shrink"
placeholder="Ask a follow-up"
/> />
{mode === 'single' && ( <div className="flex flex-row items-center justify-between mt-4">
<div className="flex flex-row items-center space-x-4"> <div className="flex flex-row items-center space-x-2 lg:space-x-4">
<CopilotToggle <Focus focusMode={focusMode} setFocusMode={setFocusMode} />
copilotEnabled={copilotEnabled} <Attach
setCopilotEnabled={setCopilotEnabled} fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
showText={firstMessage}
/>
<ModelSelector
selectedModel={selectedModel}
setSelectedModel={setSelectedModel}
/> />
<button
disabled={message.trim().length === 0 || loading}
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#e0e0dc79] dark:disabled:bg-[#ececec21] rounded-full p-2"
>
<ArrowUp className="bg-background" size={17} />
</button>
</div> </div>
)} <div className="flex flex-row items-center space-x-1 sm:space-x-4">
{mode === 'multi' && ( <Optimization
<div className="flex flex-row items-center justify-between w-full pt-2"> optimizationMode={optimizationMode}
<AttachSmall /> setOptimizationMode={setOptimizationMode}
<div className="flex flex-row items-center space-x-4">
<CopilotToggle
copilotEnabled={copilotEnabled}
setCopilotEnabled={setCopilotEnabled}
/> />
<button <button
disabled={message.trim().length === 0 || loading} disabled={message.trim().length === 0}
className="bg-[#24A0ED] text-white text-black/50 dark:disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#e0e0dc79] dark:disabled:bg-[#ececec21] 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"
type="submit"
> >
{firstMessage ? (
<ArrowRight className="bg-background" size={17} />
) : (
<ArrowUp className="bg-background" size={17} /> <ArrowUp className="bg-background" size={17} />
)}
</button> </button>
</div> </div>
</div> </div>
)} </div>
</form> </form>
); );
}; };

View File

@@ -5,13 +5,23 @@ import {
PopoverPanel, PopoverPanel,
Transition, Transition,
} from '@headlessui/react'; } from '@headlessui/react';
import { CopyPlus, File, LoaderCircle, Plus, Trash } from 'lucide-react'; import { File, LoaderCircle, Paperclip, 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,8 +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 size={16} className="text-black/70 dark:text-white/70" /> <File size={16} className="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
@@ -166,8 +176,10 @@ const Attach = ({ showText }: { showText?: boolean }) => {
multiple multiple
hidden hidden
/> />
<CopyPlus size={showText ? 18 : undefined} /> <Paperclip size="18" />
{showText && <p className="text-xs font-medium pl-[1px]">Attach</p>} {showText && (
<p className="text-xs font-medium pl-[1px] hidden lg:block">Attach</p>
)}
</button> </button>
); );
}; };

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,8 +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 size={16} className="text-black/70 dark:text-white/70" /> <File size={16} className="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

@@ -2,6 +2,7 @@ import {
BadgePercent, BadgePercent,
ChevronDown, ChevronDown,
Globe, Globe,
MessageCircle,
Pencil, Pencil,
ScanEye, ScanEye,
SwatchBook, SwatchBook,
@@ -15,7 +16,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 = [
{ {
@@ -31,11 +31,23 @@ const focusModes = [
icon: <SwatchBook size={20} />, icon: <SwatchBook size={20} />,
}, },
{ {
key: 'writingAssistant', key: 'chat',
title: 'Writing', title: 'Chat',
description: 'Chat without searching the web', description: 'Have a creative conversation',
icon: <MessageCircle size={16} />,
},
{
key: 'localResearch',
title: 'Local Research',
description: 'Research and interact with local files with citations',
icon: <Pencil size={16} />, icon: <Pencil size={16} />,
}, },
{
key: 'redditSearch',
title: 'Reddit',
description: 'Search for discussions and opinions',
icon: <SiReddit className="h-5 w-auto mr-0.5" />,
},
{ {
key: 'wolframAlphaSearch', key: 'wolframAlphaSearch',
title: 'Wolfram Alpha', title: 'Wolfram Alpha',
@@ -48,17 +60,15 @@ const focusModes = [
description: 'Search and watch videos', description: 'Search and watch videos',
icon: <SiYoutube className="h-5 w-auto mr-0.5" />, icon: <SiYoutube className="h-5 w-auto mr-0.5" />,
}, },
{
key: 'redditSearch',
title: 'Reddit',
description: 'Search for discussions and opinions',
icon: <SiReddit className="h-5 w-auto mr-0.5" />,
},
]; ];
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
@@ -83,13 +93,13 @@ const Focus = () => {
<Transition <Transition
as={Fragment} as={Fragment}
enter="transition ease-out duration-150" enter="transition ease-out duration-150"
enterFrom="opacity-0 translate-y-1" enterFrom="opacity-0 -translate-y-1"
enterTo="opacity-100 translate-y-0" enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150" leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0" leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1" leaveTo="opacity-0 -translate-y-1"
> >
<PopoverPanel className="absolute z-10 w-64 md:w-[500px] left-0"> <PopoverPanel className="absolute z-10 w-64 md:w-[500px] left-0 bottom-full mb-2">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto">
{focusModes.map((mode, i) => ( {focusModes.map((mode, i) => (
<PopoverButton <PopoverButton

View File

@@ -0,0 +1,305 @@
import { useEffect, useState } from 'react';
import { Cpu, ChevronDown, ChevronRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import {
Popover,
PopoverButton,
PopoverPanel,
Transition,
} from '@headlessui/react';
import { Fragment } from 'react';
interface ModelOption {
provider: string;
model: string;
displayName: string;
}
interface ProviderModelMap {
[provider: string]: {
displayName: string;
models: ModelOption[];
};
}
const ModelSelector = ({
selectedModel,
setSelectedModel,
}: {
selectedModel: { provider: string; model: string } | null;
setSelectedModel: (model: { provider: string; model: string }) => void;
}) => {
const [providerModels, setProviderModels] = useState<ProviderModelMap>({});
const [providersList, setProvidersList] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
const [selectedModelDisplay, setSelectedModelDisplay] = useState<string>('');
const [selectedProviderDisplay, setSelectedProviderDisplay] =
useState<string>('');
const [expandedProviders, setExpandedProviders] = useState<
Record<string, boolean>
>({});
useEffect(() => {
const fetchModels = async () => {
try {
const response = await fetch('/api/models', {
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch models: ${response.status}`);
}
const data = await response.json();
const providersData: ProviderModelMap = {};
// Organize models by provider
Object.entries(data.chatModelProviders).forEach(
([provider, models]: [string, any]) => {
const providerDisplayName =
provider.charAt(0).toUpperCase() + provider.slice(1);
providersData[provider] = {
displayName: providerDisplayName,
models: [],
};
Object.entries(models).forEach(
([modelKey, modelData]: [string, any]) => {
providersData[provider].models.push({
provider,
model: modelKey,
displayName: modelData.displayName || modelKey,
});
},
);
},
);
// Filter out providers with no models
Object.keys(providersData).forEach((provider) => {
if (providersData[provider].models.length === 0) {
delete providersData[provider];
}
});
// Sort providers by name (only those that have models)
const sortedProviders = Object.keys(providersData).sort();
setProvidersList(sortedProviders);
// Initialize expanded state for all providers
const initialExpandedState: Record<string, boolean> = {};
sortedProviders.forEach((provider) => {
initialExpandedState[provider] = selectedModel?.provider === provider;
});
// Expand the first provider if none is selected
if (sortedProviders.length > 0 && !selectedModel) {
initialExpandedState[sortedProviders[0]] = true;
}
setExpandedProviders(initialExpandedState);
setProviderModels(providersData);
// Find the current model in our options to display its name
if (selectedModel) {
const provider = providersData[selectedModel.provider];
if (provider) {
const currentModel = provider.models.find(
(option) => option.model === selectedModel.model,
);
if (currentModel) {
setSelectedModelDisplay(currentModel.displayName);
setSelectedProviderDisplay(provider.displayName);
}
}
}
setLoading(false);
} catch (error) {
console.error('Error fetching models:', error);
setLoading(false);
}
};
fetchModels();
}, [selectedModel, setSelectedModel]);
const toggleProviderExpanded = (provider: string) => {
setExpandedProviders((prev) => ({
...prev,
[provider]: !prev[provider],
}));
};
const handleSelectModel = (option: ModelOption) => {
setSelectedModel({
provider: option.provider,
model: option.model,
});
setSelectedModelDisplay(option.displayName);
setSelectedProviderDisplay(
providerModels[option.provider]?.displayName || option.provider,
);
// Save to localStorage for persistence
localStorage.setItem('chatModelProvider', option.provider);
localStorage.setItem('chatModel', option.model);
};
const getDisplayText = () => {
if (loading) return 'Loading...';
if (!selectedModelDisplay) return 'Select model';
return `${selectedModelDisplay} (${selectedProviderDisplay})`;
};
return (
<Popover className="relative">
{({ open }) => (
<>
<div className="relative">
<PopoverButton className="group flex items-center justify-center text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white">
<Cpu size={18} />
<span className="mx-2 text-xs font-medium overflow-hidden text-ellipsis whitespace-nowrap max-w-44 hidden lg:block">
{getDisplayText()}
</span>
<ChevronDown
size={16}
className={cn(
'transition-transform',
open ? 'rotate-180' : 'rotate-0',
)}
/>
</PopoverButton>
</div>
<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 z-10 w-72 transform bottom-full mb-2">
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black/5 dark:ring-white/5 bg-white dark:bg-dark-secondary divide-y divide-light-200 dark:divide-dark-200">
<div className="px-4 py-3">
<h3 className="text-sm font-medium text-black/90 dark:text-white/90">
Select Model
</h3>
<p className="text-xs text-black/60 dark:text-white/60 mt-1">
Choose a provider and model for your conversation
</p>
</div>
<div className="max-h-72 overflow-y-auto">
{loading ? (
<div className="px-4 py-3 text-sm text-black/70 dark:text-white/70">
Loading available models...
</div>
) : providersList.length === 0 ? (
<div className="px-4 py-3 text-sm text-black/70 dark:text-white/70">
No models available
</div>
) : (
<div className="py-1">
{providersList.map((providerKey) => {
const provider = providerModels[providerKey];
const isExpanded = expandedProviders[providerKey];
return (
<div
key={providerKey}
className="border-t border-light-200 dark:border-dark-200 first:border-t-0"
>
{/* Provider header */}
<button
className={cn(
'w-full flex items-center justify-between px-4 py-2 text-sm text-left',
'hover:bg-light-100 dark:hover:bg-dark-100',
selectedModel?.provider === providerKey
? 'bg-light-50 dark:bg-dark-50'
: '',
)}
onClick={() =>
toggleProviderExpanded(providerKey)
}
>
<div className="font-medium flex items-center">
<Cpu
size={14}
className="mr-2 text-black/70 dark:text-white/70"
/>
{provider.displayName}
{selectedModel?.provider === providerKey && (
<span className="ml-2 text-xs text-[#24A0ED]">
(active)
</span>
)}
</div>
<ChevronRight
size={14}
className={cn(
'transition-transform',
isExpanded ? 'rotate-90' : '',
)}
/>
</button>
{/* Models list */}
{isExpanded && (
<div className="pl-6">
{provider.models.map((modelOption) => (
<PopoverButton
key={`${modelOption.provider}-${modelOption.model}`}
className={cn(
'w-full text-left px-4 py-2 text-sm flex items-center',
selectedModel?.provider ===
modelOption.provider &&
selectedModel?.model ===
modelOption.model
? 'bg-light-100 dark:bg-dark-100 text-black dark:text-white'
: 'text-black/70 dark:text-white/70 hover:bg-light-100 dark:hover:bg-dark-100',
)}
onClick={() =>
handleSelectModel(modelOption)
}
>
<div className="flex flex-col flex-1">
<span className="font-medium">
{modelOption.displayName}
</span>
</div>
{/* Active indicator */}
{selectedModel?.provider ===
modelOption.provider &&
selectedModel?.model ===
modelOption.model && (
<div className="ml-auto bg-[#24A0ED] text-white text-xs px-1.5 py-0.5 rounded">
Active
</div>
)}
</PopoverButton>
))}
</div>
)}
</div>
);
})}
</div>
)}
</div>
</div>
</PopoverPanel>
</Transition>
</>
)}
</Popover>
);
};
export default ModelSelector;

View File

@@ -1,4 +1,4 @@
import { ChevronDown, Sliders, Star, Zap } from 'lucide-react'; import { ChevronDown, Minimize2, Sliders, Star, Zap } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { import {
Popover, Popover,
@@ -7,8 +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 = [
{ {
key: 'speed', key: 'speed',
@@ -35,11 +33,20 @@ const OptimizationModes = [
}, },
]; ];
const Optimization = () => { const Optimization = ({
const { optimizationMode, setOptimizationMode } = useChat(); optimizationMode,
setOptimizationMode,
}: {
optimizationMode: string;
setOptimizationMode: (mode: string) => void;
}) => {
const handleOptimizationChange = (mode: string) => {
setOptimizationMode(mode);
localStorage.setItem('optimizationMode', mode);
};
return ( return (
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg"> <Popover className="relative">
<PopoverButton <PopoverButton
type="button" type="button"
className="p-2 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white" className="p-2 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white"
@@ -49,12 +56,12 @@ const Optimization = () => {
OptimizationModes.find((mode) => mode.key === optimizationMode) OptimizationModes.find((mode) => mode.key === optimizationMode)
?.icon ?.icon
} }
<p className="text-xs font-medium"> {/* <p className="text-xs font-medium hidden lg:block">
{ {
OptimizationModes.find((mode) => mode.key === optimizationMode) OptimizationModes.find((mode) => mode.key === optimizationMode)
?.title ?.title
} }
</p> </p> */}
<ChevronDown size={20} /> <ChevronDown size={20} />
</div> </div>
</PopoverButton> </PopoverButton>
@@ -67,11 +74,11 @@ const Optimization = () => {
leaveFrom="opacity-100 translate-y-0" leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1" leaveTo="opacity-0 translate-y-1"
> >
<PopoverPanel className="absolute z-10 w-64 md:w-[250px] right-0"> <PopoverPanel className="absolute z-10 bottom-[100%] mb-2 left-1/2 transform -translate-x-1/2">
<div className="flex flex-col gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto"> <div className="flex flex-col gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-max max-w-[300px] p-4 max-h-[200px] md:max-h-none overflow-y-auto">
{OptimizationModes.map((mode, i) => ( {OptimizationModes.map((mode, i) => (
<PopoverButton <PopoverButton
onClick={() => setOptimizationMode(mode.key)} onClick={() => handleOptimizationChange(mode.key)}
key={i} key={i}
disabled={mode.key === 'quality'} disabled={mode.key === 'quality'}
className={cn( className={cn(

View File

@@ -1,31 +1,11 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import {
Dialog,
DialogPanel,
DialogTitle,
Transition,
TransitionChild,
} from '@headlessui/react';
import { Document } from '@langchain/core/documents'; import { Document } from '@langchain/core/documents';
import { File } from 'lucide-react'; import { File } from 'lucide-react';
import { Fragment, useState } from 'react';
const MessageSources = ({ sources }: { sources: Document[] }) => { const MessageSources = ({ sources }: { sources: Document[] }) => {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const closeModal = () => {
setIsDialogOpen(false);
document.body.classList.remove('overflow-hidden-scrollable');
};
const openModal = () => {
setIsDialogOpen(true);
document.body.classList.add('overflow-hidden-scrollable');
};
return ( return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-2"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-2">
{sources.slice(0, 3).map((source, i) => ( {sources.map((source, i) => (
<a <a
className="bg-light-100 hover:bg-light-200 dark:bg-dark-100 dark:hover:bg-dark-200 transition duration-200 rounded-lg p-3 flex flex-col space-y-2 font-medium" className="bg-light-100 hover:bg-light-200 dark:bg-dark-100 dark:hover:bg-dark-200 transition duration-200 rounded-lg p-3 flex flex-col space-y-2 font-medium"
key={i} key={i}
@@ -61,101 +41,6 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
</div> </div>
</a> </a>
))} ))}
{sources.length > 3 && (
<button
onClick={openModal}
className="bg-light-100 hover:bg-light-200 dark:bg-dark-100 dark:hover:bg-dark-200 transition duration-200 rounded-lg p-3 flex flex-col space-y-2 font-medium"
>
<div className="flex flex-row items-center space-x-1">
{sources.slice(3, 6).map((source, i) => {
return source.metadata.url === 'File' ? (
<div
key={i}
className="bg-dark-200 hover:bg-dark-100 transition duration-200 flex items-center justify-center w-6 h-6 rounded-full"
>
<File size={12} className="text-white/70" />
</div>
) : (
<img
key={i}
src={`https://s2.googleusercontent.com/s2/favicons?domain_url=${source.metadata.url}`}
width={16}
height={16}
alt="favicon"
className="rounded-lg h-4 w-4"
/>
);
})}
</div>
<p className="text-xs text-black/50 dark:text-white/50">
View {sources.length - 3} more
</p>
</button>
)}
<Transition appear show={isDialogOpen} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={closeModal}>
<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">
Sources
</DialogTitle>
<div className="grid grid-cols-2 gap-2 overflow-auto max-h-[300px] mt-2 pr-2">
{sources.map((source, i) => (
<a
className="bg-light-secondary hover:bg-light-200 dark:bg-dark-secondary dark:hover:bg-dark-200 border border-light-200 dark:border-dark-200 transition duration-200 rounded-lg p-3 flex flex-col space-y-2 font-medium"
key={i}
href={source.metadata.url}
target="_blank"
>
<p className="dark:text-white text-xs overflow-hidden whitespace-nowrap text-ellipsis">
{source.metadata.title}
</p>
<div className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center space-x-1">
{source.metadata.url === 'File' ? (
<div className="bg-dark-200 hover:bg-dark-100 transition duration-200 flex items-center justify-center w-6 h-6 rounded-full">
<File size={12} className="text-white/70" />
</div>
) : (
<img
src={`https://s2.googleusercontent.com/s2/favicons?domain_url=${source.metadata.url}`}
width={16}
height={16}
alt="favicon"
className="rounded-lg h-4 w-4"
/>
)}
<p className="text-xs text-black/50 dark:text-white/50 overflow-hidden whitespace-nowrap text-ellipsis">
{source.metadata.url.replace(
/.+\/\/|www.|\..+/g,
'',
)}
</p>
</div>
<div className="flex flex-row items-center space-x-1 text-black/50 dark:text-white/50 text-xs">
<div className="bg-black/50 dark:bg-white/50 h-[4px] w-[4px] rounded-full" />
<span>{i + 1}</span>
</div>
</div>
</a>
))}
</div>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</Transition>
</div> </div>
); );
}; };

View File

@@ -0,0 +1,535 @@
/* eslint-disable @next/next/no-img-element */
'use client';
import { getSuggestions } from '@/lib/actions';
import { cn } from '@/lib/utils';
import {
BookCopy,
CheckCheck,
Copy as CopyIcon,
Disc3,
ImagesIcon,
Layers3,
Plus,
Sparkles,
StopCircle,
VideoIcon,
Volume2,
} from 'lucide-react';
import Markdown, { MarkdownToJSX } from 'markdown-to-jsx';
import { useEffect, useState } from 'react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import { useSpeech } from 'react-text-to-speech';
import { Message } from './ChatWindow';
import Copy from './MessageActions/Copy';
import ModelInfoButton from './MessageActions/ModelInfo';
import Rewrite from './MessageActions/Rewrite';
import MessageSources from './MessageSources';
import SearchImages from './SearchImages';
import SearchVideos from './SearchVideos';
import ThinkBox from './ThinkBox';
const ThinkTagProcessor = ({ children }: { children: React.ReactNode }) => {
return <ThinkBox content={children as string} />;
};
const CodeBlock = ({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) => {
// Extract language from className (format could be "language-javascript" or "lang-javascript")
let language = '';
if (className) {
if (className.startsWith('language-')) {
language = className.replace('language-', '');
} else if (className.startsWith('lang-')) {
language = className.replace('lang-', '');
}
}
const content = children as string;
const [isCopied, setIsCopied] = useState(false);
const handleCopyCode = () => {
navigator.clipboard.writeText(content);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
};
return (
<div className="rounded-md overflow-hidden my-4 relative group border border-dark-secondary">
<div className="flex justify-between items-center px-4 py-2 bg-dark-200 border-b border-dark-secondary text-xs text-white/70 font-mono">
<span>{language}</span>
<button
onClick={handleCopyCode}
className="p-1 rounded-md hover:bg-dark-secondary transition duration-200"
aria-label="Copy code to clipboard"
>
{isCopied ? (
<CheckCheck size={14} className="text-green-500" />
) : (
<CopyIcon size={14} className="text-white/70" />
)}
</button>
</div>
<SyntaxHighlighter
language={language || 'text'}
style={oneDark}
customStyle={{
margin: 0,
padding: '1rem',
borderRadius: 0,
backgroundColor: '#1c1c1c',
}}
wrapLines={true}
wrapLongLines={true}
showLineNumbers={language !== '' && content.split('\n').length > 1}
useInlineStyles={true}
PreTag="div"
>
{content}
</SyntaxHighlighter>
</div>
);
};
type TabType = 'text' | 'sources' | 'images' | 'videos';
interface SearchTabsProps {
chatHistory: Message[];
query: string;
messageId: string;
message: Message;
isLast: boolean;
loading: boolean;
rewrite: (messageId: string) => void;
sendMessage: (
message: string,
options?: {
messageId?: string;
rewriteIndex?: number;
suggestions?: string[];
},
) => void;
}
const MessageTabs = ({
chatHistory,
query,
messageId,
message,
isLast,
loading,
rewrite,
sendMessage,
}: SearchTabsProps) => {
const [activeTab, setActiveTab] = useState<TabType>('text');
const [imageCount, setImageCount] = useState(0);
const [videoCount, setVideoCount] = useState(0);
const [parsedMessage, setParsedMessage] = useState(message.content);
const [speechMessage, setSpeechMessage] = useState(message.content);
const [loadingSuggestions, setLoadingSuggestions] = useState(false);
const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
// Callback functions to update counts
const updateImageCount = (count: number) => {
setImageCount(count);
};
const updateVideoCount = (count: number) => {
setVideoCount(count);
};
// Load suggestions handling
const handleLoadSuggestions = async () => {
if (
loadingSuggestions ||
(message?.suggestions && message.suggestions.length > 0)
)
return;
setLoadingSuggestions(true);
try {
const suggestions = await getSuggestions([...chatHistory, message]);
// Update the message.suggestions property through parent component
sendMessage('', { messageId: message.messageId, suggestions });
} catch (error) {
console.error('Error loading suggestions:', error);
} finally {
setLoadingSuggestions(false);
}
};
// Process message content
useEffect(() => {
const citationRegex = /\[([^\]]+)\]/g;
const regex = /\[(\d+)\]/g;
let processedMessage = message.content;
if (message.role === 'assistant' && message.content.includes('<think>')) {
const openThinkTag = processedMessage.match(/<think>/g)?.length || 0;
const closeThinkTag = processedMessage.match(/<\/think>/g)?.length || 0;
if (openThinkTag > closeThinkTag) {
processedMessage += '</think> <a> </a>'; // The extra <a> </a> is to prevent the 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]);
// Auto-suggest effect (similar to MessageBox)
useEffect(() => {
const autoSuggestions = localStorage.getItem('autoSuggestions');
if (
isLast &&
message.role === 'assistant' &&
!loading &&
autoSuggestions === 'true'
) {
handleLoadSuggestions();
}
}, [isLast, loading, message.role]);
// Markdown formatting options
const markdownOverrides: MarkdownToJSX.Options = {
overrides: {
think: {
component: ThinkTagProcessor,
},
code: {
component: ({ className, children }) => {
// Check if it's an inline code block or a fenced code block
if (className) {
// This is a fenced code block (```code```)
return <CodeBlock className={className}>{children}</CodeBlock>;
}
// This is an inline code block (`code`)
return (
<code className="px-1.5 py-0.5 rounded bg-dark-secondary text-white font-mono text-sm">
{children}
</code>
);
},
},
pre: {
component: ({ children }) => children,
},
},
};
return (
<div className="flex flex-col w-full">
{/* Tabs */}
<div className="flex border-b border-light-200 dark:border-dark-200 overflow-x-auto no-scrollbar sticky top-0 bg-light-primary dark:bg-dark-primary z-10 -mx-4 px-4 mb-2 shadow-sm">
<button
onClick={() => setActiveTab('text')}
className={cn(
'flex items-center px-4 py-3 text-sm font-medium transition-all duration-200 relative',
activeTab === 'text'
? 'border-b-2 border-[#24A0ED] text-[#24A0ED] bg-light-100 dark:bg-dark-100'
: 'text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white hover:bg-light-100 dark:hover:bg-dark-100',
)}
aria-selected={activeTab === 'text'}
role="tab"
>
<Disc3 size={16} className="mr-2" />
<span className="whitespace-nowrap">Answer</span>
</button>
{message.sources && message.sources.length > 0 && (
<button
onClick={() => setActiveTab('sources')}
className={cn(
'flex items-center space-x-2 px-4 py-3 text-sm font-medium transition-all duration-200 relative',
activeTab === 'sources'
? 'border-b-2 border-[#24A0ED] text-[#24A0ED] bg-light-100 dark:bg-dark-100'
: 'text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white hover:bg-light-100 dark:hover:bg-dark-100',
)}
aria-selected={activeTab === 'sources'}
role="tab"
>
<BookCopy size={16} />
<span className="whitespace-nowrap">Sources</span>
<span
className={cn(
'ml-1.5 px-1.5 py-0.5 text-xs rounded-full',
activeTab === 'sources'
? 'bg-[#24A0ED]/20 text-[#24A0ED]'
: 'bg-light-200 dark:bg-dark-200 text-black/70 dark:text-white/70',
)}
>
{message.sources.length}
</span>
</button>
)}
<button
onClick={() => setActiveTab('images')}
className={cn(
'flex items-center space-x-2 px-4 py-3 text-sm font-medium transition-all duration-200 relative',
activeTab === 'images'
? 'border-b-2 border-[#24A0ED] text-[#24A0ED] bg-light-100 dark:bg-dark-100'
: 'text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white hover:bg-light-100 dark:hover:bg-dark-100',
)}
aria-selected={activeTab === 'images'}
role="tab"
>
<ImagesIcon size={16} />
<span className="whitespace-nowrap">Images</span>
{imageCount > 0 && (
<span
className={cn(
'ml-1.5 px-1.5 py-0.5 text-xs rounded-full',
activeTab === 'images'
? 'bg-[#24A0ED]/20 text-[#24A0ED]'
: 'bg-light-200 dark:bg-dark-200 text-black/70 dark:text-white/70',
)}
>
{imageCount}
</span>
)}
</button>
<button
onClick={() => setActiveTab('videos')}
className={cn(
'flex items-center space-x-2 px-4 py-3 text-sm font-medium transition-all duration-200 relative',
activeTab === 'videos'
? 'border-b-2 border-[#24A0ED] text-[#24A0ED] bg-light-100 dark:bg-dark-100'
: 'text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white hover:bg-light-100 dark:hover:bg-dark-100',
)}
aria-selected={activeTab === 'videos'}
role="tab"
>
<VideoIcon size={16} />
<span className="whitespace-nowrap">Videos</span>
{videoCount > 0 && (
<span
className={cn(
'ml-1.5 px-1.5 py-0.5 text-xs rounded-full',
activeTab === 'videos'
? 'bg-[#24A0ED]/20 text-[#24A0ED]'
: 'bg-light-200 dark:bg-dark-200 text-black/70 dark:text-white/70',
)}
>
{videoCount}
</span>
)}
</button>
</div>
{/* Tab content */}
<div
className="min-h-[150px] transition-all duration-200 ease-in-out"
role="tabpanel"
>
{/* Answer Tab */}
{activeTab === 'text' && (
<div className="flex flex-col space-y-4 animate-fadeIn">
<Markdown
className={cn(
'prose prose-h1:mb-3 prose-h2:mb-2 prose-h2:mt-6 prose-h2:font-[800] prose-h3:mt-4 prose-h3:mb-1.5 prose-h3:font-[600] prose-invert prose-p:leading-relaxed prose-pre:p-0 font-[400]',
'prose-code:bg-transparent prose-code:p-0 prose-code:text-inherit prose-code:font-normal prose-code:before:content-none prose-code:after:content-none',
'prose-pre:bg-transparent prose-pre:border-0 prose-pre:m-0 prose-pre:p-0',
'max-w-none break-words px-4 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 px-4 py-4">
<div className="flex flex-row items-center space-x-1">
<Rewrite rewrite={rewrite} messageId={message.messageId} />
{message.modelStats && (
<ModelInfoButton modelStats={message.modelStats} />
)}
</div>
<div className="flex flex-row items-center space-x-1">
<Copy initialMessage={message.content} message={message} />
<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.role === 'assistant' && !loading && (
<>
<div className="border-t border-light-secondary dark:border-dark-secondary px-4 pt-4 mt-4">
<div className="flex flex-row items-center space-x-2 mb-3">
<Layers3 size={20} />
<h3 className="text-xl font-medium">Related</h3>
{(!message.suggestions ||
message.suggestions.length === 0) && (
<button
onClick={handleLoadSuggestions}
disabled={loadingSuggestions}
className="px-4 py-2 flex flex-row items-center justify-center space-x-2 rounded-lg bg-light-secondary dark:bg-dark-secondary hover:bg-light-200 dark:hover:bg-dark-200 transition duration-200 text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white"
>
{loadingSuggestions ? (
<div className="w-4 h-4 border-2 border-t-transparent border-gray-400 dark:border-gray-500 rounded-full animate-spin" />
) : (
<Sparkles size={16} />
)}
<span>
{loadingSuggestions
? 'Loading suggestions...'
: 'Load suggestions'}
</span>
</button>
)}
</div>
{message.suggestions && message.suggestions.length > 0 && (
<div className="flex flex-col space-y-3 mt-2">
{message.suggestions.map((suggestion, i) => (
<div
className="flex flex-col space-y-3 text-sm"
key={i}
>
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
<div
onClick={() => {
sendMessage(suggestion);
}}
className="cursor-pointer flex flex-row justify-between font-medium space-x-2 items-center"
>
<p className="transition duration-200 hover:text-[#24A0ED]">
{suggestion}
</p>
<Plus
size={20}
className="text-[#24A0ED] flex-shrink-0"
/>
</div>
</div>
))}
</div>
)}
</div>
</>
)}
</div>
)}
{/* Sources Tab */}
{activeTab === 'sources' &&
message.sources &&
message.sources.length > 0 && (
<div className="p-4 animate-fadeIn">
{message.searchQuery && (
<div className="mb-4 text-sm bg-light-secondary dark:bg-dark-secondary rounded-lg p-3">
<span className="font-medium text-black/70 dark:text-white/70">
Search query:
</span>{' '}
{message.searchUrl ? (
<a
href={message.searchUrl}
target="_blank"
rel="noopener noreferrer"
className="dark:text-white text-black hover:underline"
>
{message.searchQuery}
</a>
) : (
<span className="text-black dark:text-white">
{message.searchQuery}
</span>
)}
</div>
)}
<MessageSources sources={message.sources} />
</div>
)}
{/* Images Tab */}
{activeTab === 'images' && (
<div className="p-3 animate-fadeIn">
<SearchImages
query={query}
chatHistory={chatHistory}
messageId={messageId}
onImagesLoaded={updateImageCount}
/>
</div>
)}
{/* Videos Tab */}
{activeTab === 'videos' && (
<div className="p-3 animate-fadeIn">
<SearchVideos
query={query}
chatHistory={chatHistory}
messageId={messageId}
onVideosLoaded={updateVideoCount}
/>
</div>
)}
</div>
</div>
);
};
export default MessageTabs;

View File

@@ -1,130 +1,19 @@
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 } 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 = (messages: Message[], title: string) => {
const date = new Date(messages[0]?.createdAt || Date.now()).toLocaleString();
let md = `# 💬 Chat Export: ${title}\n\n`;
md += `*Exported on: ${date}*\n\n---\n`;
messages.forEach((msg, idx) => {
md += `\n---\n`;
md += `**${msg.role === 'user' ? '🧑 User' : '🤖 Assistant'}**
`;
md += `*${new Date(msg.createdAt).toLocaleString()}*\n\n`;
md += `> ${msg.content.replace(/\n/g, '\n> ')}\n`;
if (msg.sources && msg.sources.length > 0) {
md += `\n**Citations:**\n`;
msg.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 = (messages: Message[], title: string) => {
const doc = new jsPDF();
const date = new Date(messages[0]?.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);
messages.forEach((msg, idx) => {
if (y > pageHeight - 30) {
doc.addPage();
y = 15;
}
doc.setFont('helvetica', 'bold');
doc.text(`${msg.role === 'user' ? 'User' : 'Assistant'}`, 10, y);
doc.setFont('helvetica', 'normal');
doc.setFontSize(10);
doc.setTextColor(120);
doc.text(`${new Date(msg.createdAt).toLocaleString()}`, 40, y);
y += 6;
doc.setTextColor(30);
doc.setFontSize(12);
const lines = doc.splitTextToSize(msg.content, 180);
for (let i = 0; i < lines.length; i++) {
if (y > pageHeight - 20) {
doc.addPage();
y = 15;
}
doc.text(lines[i], 12, y);
y += 6;
}
if (msg.sources && msg.sources.length > 0) {
doc.setFontSize(11);
doc.setTextColor(80);
if (y > pageHeight - 20) {
doc.addPage();
y = 15;
}
doc.text('Citations:', 12, y);
y += 5;
msg.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 { messages, chatId } = useChat();
useEffect(() => { useEffect(() => {
if (messages.length > 0) { if (messages.length > 0) {
const newTitle = const newTitle =
@@ -170,40 +59,11 @@ const Navbar = () => {
<p className="hidden lg:flex">{title}</p> <p className="hidden lg:flex">{title}</p>
<div className="flex flex-row items-center space-x-4"> <div className="flex flex-row items-center space-x-4">
<Popover className="relative"> <Share
<PopoverButton className="active:scale-95 transition duration-100 cursor-pointer p-2 rounded-full hover:bg-light-secondary dark:hover:bg-dark-secondary"> size={17}
<Share size={17} /> className="active:scale-95 transition duration-100 cursor-pointer"
</PopoverButton> />
<Transition <DeleteChat redirect chatId={chatId} chats={[]} setChats={() => {}} />
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-75"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<PopoverPanel className="absolute right-0 mt-2 w-64 rounded-xl shadow-xl bg-light-primary dark:bg-dark-primary border border-light-200 dark:border-dark-200 z-50">
<div className="flex flex-col py-3 px-3 gap-2">
<button
className="flex items-center gap-2 px-4 py-2 text-left hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors text-black dark:text-white rounded-lg font-medium"
onClick={() => exportAsMarkdown(messages, title || '')}
>
<FileText size={17} className="text-[#24A0ED]" />
Export as Markdown
</button>
<button
className="flex items-center gap-2 px-4 py-2 text-left hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors text-black dark:text-white rounded-lg font-medium"
onClick={() => exportAsPDF(messages, title || '')}
>
<FileDown size={17} className="text-[#24A0ED]" />
Export as PDF
</button>
</div>
</PopoverPanel>
</Transition>
</Popover>
<DeleteChat redirect chatId={chatId!} chats={[]} setChats={() => {}} />
</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-xl border border-light-200 dark:border-dark-200 shadow-sm flex flex-row items-center w-full h-24 min-h-[96px] max-h-[96px] px-3 py-2 gap-3 overflow-hidden">
{loading ? (
<>
<div className="animate-pulse flex flex-row items-center w-full h-full">
<div className="rounded-lg w-16 min-w-16 max-w-16 h-16 min-h-16 max-h-16 bg-light-200 dark:bg-dark-200 mr-3" />
<div className="flex flex-col justify-center flex-1 h-full w-0 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-center w-full h-full group"
>
<img
className="object-cover rounded-lg w-16 min-w-16 max-w-16 h-16 min-h-16 max-h-16 border border-light-200 dark:border-dark-200 bg-light-200 dark:bg-dark-200 group-hover:opacity-90 transition"
src={
new URL(article.thumbnail).origin +
new URL(article.thumbnail).pathname +
`?id=${new URL(article.thumbnail).searchParams.get('id')}`
}
alt={article.title}
/>
<div className="flex flex-col justify-center flex-1 h-full pl-3 w-0">
<div className="font-bold text-xs text-black dark:text-white leading-tight truncate overflow-hidden whitespace-nowrap">
{article.title}
</div>
<p className="text-black/70 dark:text-white/70 text-xs leading-snug truncate overflow-hidden whitespace-nowrap">
{article.content}
</p>
</div>
</a>
) : null}
</div>
);
};
export default NewsArticleWidget;

View File

@@ -1,6 +1,5 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import { ImagesIcon, PlusIcon } from 'lucide-react'; import { useEffect, useRef, useState } from 'react';
import { useState } from 'react';
import Lightbox from 'yet-another-react-lightbox'; import Lightbox from 'yet-another-react-lightbox';
import 'yet-another-react-lightbox/styles.css'; import 'yet-another-react-lightbox/styles.css';
import { Message } from './ChatWindow'; import { Message } from './ChatWindow';
@@ -15,30 +14,50 @@ const SearchImages = ({
query, query,
chatHistory, chatHistory,
messageId, messageId,
onImagesLoaded,
}: { }: {
query: string; query: string;
chatHistory: Message[]; chatHistory: Message[];
messageId: string; messageId: string;
onImagesLoaded?: (count: number) => void;
}) => { }) => {
const [images, setImages] = useState<Image[] | null>(null); const [images, setImages] = useState<Image[] | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(true);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [slides, setSlides] = useState<any[]>([]); const [slides, setSlides] = useState<any[]>([]);
const [displayLimit, setDisplayLimit] = useState(10); // Initially show only 10 images
const loadedMessageIdsRef = useRef<Set<string>>(new Set());
return ( // Function to show more images when the Show More button is clicked
<> const handleShowMore = () => {
{!loading && images === null && ( // If we're already showing all images, don't do anything
<button if (images && displayLimit >= images.length) return;
id={`search-images-${messageId}`}
onClick={async () => { // Otherwise, increase the display limit by 10, or show all images
setDisplayLimit((prev) =>
images ? Math.min(prev + 10, images.length) : prev,
);
};
useEffect(() => {
// Skip fetching if images are already loaded for this message
if (loadedMessageIdsRef.current.has(messageId)) {
return;
}
const fetchImages = async () => {
// Mark as loaded to prevent refetching
loadedMessageIdsRef.current.add(messageId);
setLoading(true); setLoading(true);
const chatModelProvider = localStorage.getItem('chatModelProvider'); const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel'); const chatModel = localStorage.getItem('chatModel');
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL'); const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
const customOpenAIKey = localStorage.getItem('openAIApiKey'); const customOpenAIKey = localStorage.getItem('openAIApiKey');
const ollamaContextWindow =
localStorage.getItem('ollamaContextWindow') || '2048';
try {
const res = await fetch(`/api/images`, { const res = await fetch(`/api/images`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -54,6 +73,9 @@ const SearchImages = ({
customOpenAIBaseURL: customOpenAIBaseURL, customOpenAIBaseURL: customOpenAIBaseURL,
customOpenAIKey: customOpenAIKey, customOpenAIKey: customOpenAIKey,
}), }),
...(chatModelProvider === 'ollama' && {
ollamaContextWindow: parseInt(ollamaContextWindow),
}),
}, },
}), }),
}); });
@@ -69,17 +91,21 @@ const SearchImages = ({
}; };
}), }),
); );
if (onImagesLoaded && images.length > 0) {
onImagesLoaded(images.length);
}
} catch (error) {
console.error('Error fetching images:', error);
} finally {
setLoading(false); setLoading(false);
}} }
className="border border-dashed border-light-200 dark:border-dark-200 hover:bg-light-200 dark:hover:bg-dark-200 active:scale-95 duration-200 transition px-4 py-2 flex flex-row items-center justify-between rounded-lg dark:text-white text-sm w-full" };
>
<div className="flex flex-row items-center space-x-2"> fetchImages();
<ImagesIcon size={17} /> }, [query, messageId, chatHistory, onImagesLoaded]);
<p>Search images</p>
</div> return (
<PlusIcon className="text-[#24A0ED]" size={17} /> <>
</button>
)}
{loading && ( {loading && (
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{[...Array(4)].map((_, i) => ( {[...Array(4)].map((_, i) => (
@@ -92,61 +118,40 @@ const SearchImages = ({
)} )}
{images !== null && images.length > 0 && ( {images !== null && images.length > 0 && (
<> <>
<div className="grid grid-cols-2 gap-2"> <div
{images.length > 4 className="grid grid-cols-2 gap-2"
? images.slice(0, 3).map((image, i) => ( key={`image-results-${messageId}`}
<img
onClick={() => {
setOpen(true);
setSlides([
slides[i],
...slides.slice(0, i),
...slides.slice(i + 1),
]);
}}
key={i}
src={image.img_src}
alt={image.title}
className="h-full w-full aspect-video object-cover rounded-lg transition duration-200 active:scale-95 hover:scale-[1.02] cursor-zoom-in"
/>
))
: images.map((image, i) => (
<img
onClick={() => {
setOpen(true);
setSlides([
slides[i],
...slides.slice(0, i),
...slides.slice(i + 1),
]);
}}
key={i}
src={image.img_src}
alt={image.title}
className="h-full w-full aspect-video object-cover rounded-lg transition duration-200 active:scale-95 hover:scale-[1.02] cursor-zoom-in"
/>
))}
{images.length > 4 && (
<button
onClick={() => setOpen(true)}
className="bg-light-100 hover:bg-light-200 dark:bg-dark-100 dark:hover:bg-dark-200 transition duration-200 active:scale-95 hover:scale-[1.02] h-auto w-full rounded-lg flex flex-col justify-between text-white p-2"
> >
<div className="flex flex-row items-center space-x-1"> {images.slice(0, displayLimit).map((image, i) => (
{images.slice(3, 6).map((image, i) => (
<img <img
onClick={() => {
setOpen(true);
setSlides([
slides[i],
...slides.slice(0, i),
...slides.slice(i + 1),
]);
}}
key={i} key={i}
src={image.img_src} src={image.img_src}
alt={image.title} alt={image.title}
className="h-6 w-12 rounded-md lg:h-3 lg:w-6 lg:rounded-sm aspect-video object-cover" className="h-full w-full aspect-video object-cover rounded-lg transition duration-200 active:scale-95 hover:scale-[1.02] cursor-zoom-in"
/> />
))} ))}
</div> </div>
<p className="text-black/70 dark:text-white/70 text-xs"> {images.length > displayLimit && (
View {images.length - 3} more <div className="flex justify-center mt-4">
</p> <button
onClick={handleShowMore}
className="px-4 py-2 bg-light-secondary dark:bg-dark-secondary hover:bg-light-200 dark:hover:bg-dark-200 text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white rounded-md transition duration-200 flex items-center space-x-2"
>
<span>Show More Images</span>
<span className="text-sm opacity-75">
({displayLimit} of {images.length})
</span>
</button> </button>
)}
</div> </div>
)}
<Lightbox open={open} close={() => setOpen(false)} slides={slides} /> <Lightbox open={open} close={() => setOpen(false)} slides={slides} />
</> </>
)} )}

View File

@@ -1,6 +1,6 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import { PlayCircle, PlayIcon, PlusIcon, VideoIcon } from 'lucide-react'; import { PlayCircle } from 'lucide-react';
import { useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import Lightbox, { GenericSlide, VideoSlide } from 'yet-another-react-lightbox'; import Lightbox, { GenericSlide, VideoSlide } from 'yet-another-react-lightbox';
import 'yet-another-react-lightbox/styles.css'; import 'yet-another-react-lightbox/styles.css';
import { Message } from './ChatWindow'; import { Message } from './ChatWindow';
@@ -28,32 +28,50 @@ const Searchvideos = ({
query, query,
chatHistory, chatHistory,
messageId, messageId,
onVideosLoaded,
}: { }: {
query: string; query: string;
chatHistory: Message[]; chatHistory: Message[];
messageId: string; messageId: string;
onVideosLoaded?: (count: number) => void;
}) => { }) => {
const [videos, setVideos] = useState<Video[] | null>(null); const [videos, setVideos] = useState<Video[] | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(true);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [slides, setSlides] = useState<VideoSlide[]>([]); const [slides, setSlides] = useState<VideoSlide[]>([]);
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const [displayLimit, setDisplayLimit] = useState(10); // Initially show only 10 videos
const videoRefs = useRef<(HTMLIFrameElement | null)[]>([]); const videoRefs = useRef<(HTMLIFrameElement | null)[]>([]);
const loadedMessageIdsRef = useRef<Set<string>>(new Set());
return ( // Function to show more videos when the Show More button is clicked
<> const handleShowMore = () => {
{!loading && videos === null && ( // If we're already showing all videos, don't do anything
<button if (videos && displayLimit >= videos.length) return;
id={`search-videos-${messageId}`}
onClick={async () => { // Otherwise, increase the display limit by 10, or show all videos
setDisplayLimit((prev) =>
videos ? Math.min(prev + 10, videos.length) : prev,
);
};
useEffect(() => {
// Skip fetching if videos are already loaded for this message
if (loadedMessageIdsRef.current.has(messageId)) {
return;
}
const fetchVideos = async () => {
setLoading(true); setLoading(true);
const chatModelProvider = localStorage.getItem('chatModelProvider'); const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel'); const chatModel = localStorage.getItem('chatModel');
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL'); const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
const customOpenAIKey = localStorage.getItem('openAIApiKey'); const customOpenAIKey = localStorage.getItem('openAIApiKey');
const ollamaContextWindow =
localStorage.getItem('ollamaContextWindow') || '2048';
try {
const res = await fetch(`/api/videos`, { const res = await fetch(`/api/videos`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -69,6 +87,9 @@ const Searchvideos = ({
customOpenAIBaseURL: customOpenAIBaseURL, customOpenAIBaseURL: customOpenAIBaseURL,
customOpenAIKey: customOpenAIKey, customOpenAIKey: customOpenAIKey,
}), }),
...(chatModelProvider === 'ollama' && {
ollamaContextWindow: parseInt(ollamaContextWindow),
}),
}, },
}), }),
}); });
@@ -86,17 +107,23 @@ const Searchvideos = ({
}; };
}), }),
); );
if (onVideosLoaded && videos.length > 0) {
onVideosLoaded(videos.length);
}
// Mark as loaded to prevent refetching
loadedMessageIdsRef.current.add(messageId);
} catch (error) {
console.error('Error fetching videos:', error);
} finally {
setLoading(false); setLoading(false);
}} }
className="border border-dashed border-light-200 dark:border-dark-200 hover:bg-light-200 dark:hover:bg-dark-200 active:scale-95 duration-200 transition px-4 py-2 flex flex-row items-center justify-between rounded-lg dark:text-white text-sm w-full" };
>
<div className="flex flex-row items-center space-x-2"> fetchVideos();
<VideoIcon size={17} /> }, [query, messageId, chatHistory, onVideosLoaded]);
<p>Search videos</p>
</div> return (
<PlusIcon className="text-[#24A0ED]" size={17} /> <>
</button>
)}
{loading && ( {loading && (
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{[...Array(4)].map((_, i) => ( {[...Array(4)].map((_, i) => (
@@ -109,33 +136,11 @@ const Searchvideos = ({
)} )}
{videos !== null && videos.length > 0 && ( {videos !== null && videos.length > 0 && (
<> <>
<div className="grid grid-cols-2 gap-2">
{videos.length > 4
? videos.slice(0, 3).map((video, i) => (
<div <div
onClick={() => { className="grid grid-cols-2 gap-2"
setOpen(true); key={`video-results-${messageId}`}
setSlides([
slides[i],
...slides.slice(0, i),
...slides.slice(i + 1),
]);
}}
className="relative transition duration-200 active:scale-95 hover:scale-[1.02] cursor-pointer"
key={i}
> >
<img {videos.slice(0, displayLimit).map((video, i) => (
src={video.img_src}
alt={video.title}
className="relative h-full w-full aspect-video object-cover rounded-lg"
/>
<div className="absolute bg-white/70 dark:bg-black/70 text-black/70 dark:text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
<PlayCircle size={15} />
<p className="text-xs">Video</p>
</div>
</div>
))
: videos.map((video, i) => (
<div <div
onClick={() => { onClick={() => {
setOpen(true); setOpen(true);
@@ -159,27 +164,20 @@ const Searchvideos = ({
</div> </div>
</div> </div>
))} ))}
{videos.length > 4 && ( </div>
{videos.length > displayLimit && (
<div className="flex justify-center mt-4">
<button <button
onClick={() => setOpen(true)} onClick={handleShowMore}
className="bg-light-100 hover:bg-light-200 dark:bg-dark-100 dark:hover:bg-dark-200 transition duration-200 active:scale-95 hover:scale-[1.02] h-auto w-full rounded-lg flex flex-col justify-between text-white p-2" className="px-4 py-2 bg-light-secondary dark:bg-dark-secondary hover:bg-light-200 dark:hover:bg-dark-200 text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white rounded-md transition duration-200 flex items-center space-x-2"
> >
<div className="flex flex-row items-center space-x-1"> <span>Show More Videos</span>
{videos.slice(3, 6).map((video, i) => ( <span className="text-sm opacity-75">
<img ({displayLimit} of {videos.length})
key={i} </span>
src={video.img_src}
alt={video.title}
className="h-6 w-12 rounded-md lg:h-3 lg:w-6 lg:rounded-sm aspect-video object-cover"
/>
))}
</div>
<p className="text-black/70 dark:text-white/70 text-xs">
View {videos.length - 3} more
</p>
</button> </button>
)}
</div> </div>
)}
<Lightbox <Lightbox
open={open} open={open}
close={() => setOpen(false)} close={() => setOpen(false)}

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,161 +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);
useEffect(() => {
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());
}
};
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);
});
}, []);
return (
<div className="bg-light-secondary dark:bg-dark-secondary rounded-xl border border-light-200 dark:border-dark-200 shadow-sm 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-1">
<div className="flex flex-row items-center justify-between">
<span className="text-xs font-medium text-black dark:text-white">
{data.location}
</span>
<span className="flex items-center text-xs text-black/60 dark:text-white/60">
<Wind className="w-3 h-3 mr-1" />
{data.windSpeed} {data.windSpeedUnit}
</span>
</div>
<span className="text-xs text-black/60 dark:text-white/60 mt-1">
{data.condition}
</span>
<div className="flex flex-row justify-between w-full mt-auto pt-1 border-t border-light-200 dark:border-dark-200 text-xs text-black/60 dark:text-white/60">
<span>Humidity: {data.humidity}%</span>
<span>Now</span>
</div>
</div>
</>
)}
</div>
);
};
export default WeatherWidget;

View File

@@ -6,6 +6,8 @@ export const getSuggestions = async (chatHisory: Message[]) => {
const customOpenAIKey = localStorage.getItem('openAIApiKey'); const customOpenAIKey = localStorage.getItem('openAIApiKey');
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL'); const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
const ollamaContextWindow =
localStorage.getItem('ollamaContextWindow') || '2048';
const res = await fetch(`/api/suggestions`, { const res = await fetch(`/api/suggestions`, {
method: 'POST', method: 'POST',
@@ -21,6 +23,9 @@ export const getSuggestions = async (chatHisory: Message[]) => {
customOpenAIKey, customOpenAIKey,
customOpenAIBaseURL, customOpenAIBaseURL,
}), }),
...(chatModelProvider === 'ollama' && {
ollamaContextWindow: parseInt(ollamaContextWindow),
}),
}, },
}), }),
}); });

View File

@@ -3,18 +3,76 @@ 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 LineOutputParser from '../outputParsers/lineOutputParser';
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. # Instructions
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation. - You will be given a question from a user and a conversation history
Output only the rephrased query wrapped in an XML <query> element. Do not include any explanation or additional text. - Rephrase the question based on the conversation so it is a standalone question that can be used to search for images that are relevant to the question
- Ensure the rephrased question agrees with the conversation and is relevant to the conversation
- If you are thinking or reasoning, use <think> tags to indicate your thought process
- If you are thinking or reasoning, do not use <answer> and </answer> tags in your thinking. Those tags should only be used in the final output
- Use the provided date to ensure the rephrased question is relevant to the current date and time if applicable
# Data locations
- The history is contained in the <conversation> tag after the <examples> below
- The user question is contained in the <question> tag after the <examples> below
- Output your answer in an <answer> tag
- Current date & time in ISO format (UTC timezone) is: {date}
- Do not include any other text in your answer
<examples>
## Example 1 input
<conversation>
Who won the last F1 race?\nAyrton Senna won the Monaco Grand Prix. It was a tight race with lots of overtakes. Alain Prost was in the lead for most of the race until the last lap when Senna overtook them.
</conversation>
<question>
What were the highlights of the race?
</question>
## Example 1 output
<answer>
F1 Monaco Grand Prix highlights
</answer>
## Example 2 input
<conversation>
What is the theory of relativity?
</conversation>
<question>
What is the theory of relativity?
</question>
## Example 2 output
<answer>
Theory of relativity
</answer>
## Example 3 input
<conversation>
I'm looking for a nice vacation spot. Where do you suggest?\nI suggest you go to Hawaii. It's a beautiful place with lots of beaches and activities to do.\nI love the beach! What are some activities I can do there?\nYou can go surfing, snorkeling, or just relax on the beach.
</conversation>
<question>
What are some activities I can do in Hawaii?
</question>
## Example 3 output
<answer>
Hawaii activities
</answer>
</examples>
<conversation>
{chat_history}
</conversation>
<question>
{query}
</question>
`; `;
type ImageSearchChainInput = { type ImageSearchChainInput = {
@@ -28,7 +86,9 @@ interface ImageSearchResult {
title: string; title: string;
} }
const strParser = new StringOutputParser(); const outputParser = new LineOutputParser({
key: 'answer',
});
const createImageSearchChain = (llm: BaseChatModel) => { const createImageSearchChain = (llm: BaseChatModel) => {
return RunnableSequence.from([ return RunnableSequence.from([
@@ -39,41 +99,13 @@ const createImageSearchChain = (llm: BaseChatModel) => {
query: (input: ImageSearchChainInput) => { query: (input: ImageSearchChainInput) => {
return input.query; return input.query;
}, },
date: () => new Date().toISOString(),
}), }),
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, outputParser,
RunnableLambda.from(async (input: string) => { RunnableLambda.from(async (searchQuery: string) => {
const queryParser = new LineOutputParser({ const res = await searchSearxng(searchQuery, {
key: 'query',
});
return await queryParser.parse(input);
}),
RunnableLambda.from(async (input: string) => {
const res = await searchSearxng(input, {
engines: ['bing images', 'google images'], engines: ['bing images', 'google images'],
}); });
@@ -89,7 +121,7 @@ const createImageSearchChain = (llm: BaseChatModel) => {
} }
}); });
return images.slice(0, 10); return images;
}), }),
]); ]);
}; };

View File

@@ -10,6 +10,7 @@ const suggestionGeneratorPrompt = `
You are an AI suggestion generator for an AI powered search engine. You will be given a conversation below. You need to generate 4-5 suggestions based on the conversation. The suggestion should be relevant to the conversation that can be used by the user to ask the chat model for more information. You are an AI suggestion generator for an AI powered search engine. You will be given a conversation below. You need to generate 4-5 suggestions based on the conversation. The suggestion should be relevant to the conversation that can be used by the user to ask the chat model for more information.
You need to make sure the suggestions are relevant to the conversation and are helpful to the user. Keep a note that the user might use these suggestions to ask a chat model for more information. You need to make sure the suggestions are relevant to the conversation and are helpful to the user. Keep a note that the user might use these suggestions to ask a chat model for more information.
Make sure the suggestions are medium in length and are informative and relevant to the conversation. Make sure the suggestions are medium in length and are informative and relevant to the conversation.
If you are a thinking or reasoning AI, you should avoid using \`<suggestions>\` and \`</suggestions>\` tags in your thinking. Those tags should only be used in the final output.
Provide these suggestions separated by newlines between the XML tags <suggestions> and </suggestions>. For example: Provide these suggestions separated by newlines between the XML tags <suggestions> and </suggestions>. For example:

View File

@@ -3,18 +3,76 @@ 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 LineOutputParser from '../outputParsers/lineOutputParser';
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. # Instructions
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation. - You will be given a question from a user and a conversation history
Output only the rephrased query wrapped in an XML <query> element. Do not include any explanation or additional text. - Rephrase the question based on the conversation so it is a standalone question that can be used to search Youtube for videos
- Ensure the rephrased question agrees with the conversation and is relevant to the conversation
- If you are thinking or reasoning, use <think> tags to indicate your thought process
- If you are thinking or reasoning, do not use <answer> and </answer> tags in your thinking. Those tags should only be used in the final output
- Use the provided date to ensure the rephrased question is relevant to the current date and time if applicable
# Data locations
- The history is contained in the <conversation> tag after the <examples> below
- The user question is contained in the <question> tag after the <examples> below
- Output your answer in an <answer> tag
- Current date & time in ISO format (UTC timezone) is: {date}
- Do not include any other text in your answer
<examples>
## Example 1 input
<conversation>
Who won the last F1 race?\nAyrton Senna won the Monaco Grand Prix. It was a tight race with lots of overtakes. Alain Prost was in the lead for most of the race until the last lap when Senna overtook them.
</conversation>
<question>
What were the highlights of the race?
</question>
## Example 1 output
<answer>
F1 Monaco Grand Prix highlights
</answer>
## Example 2 input
<conversation>
What is the theory of relativity?
</conversation>
<question>
What is the theory of relativity?
</question>
## Example 2 output
<answer>
What is the theory of relativity?
</answer>
## Example 3 input
<conversation>
I'm looking for a nice vacation spot. Where do you suggest?\nI suggest you go to Hawaii. It's a beautiful place with lots of beaches and activities to do.\nI love the beach! What are some activities I can do there?\nYou can go surfing, snorkeling, or just relax on the beach.
</conversation>
<question>
What are some activities I can do in Hawaii?
</question>
## Example 3 output
<answer>
Activities to do in Hawaii
</answer>
</examples>
<conversation>
{chat_history}
</conversation>
<question>
{query}
</question>
`; `;
type VideoSearchChainInput = { type VideoSearchChainInput = {
@@ -29,7 +87,9 @@ interface VideoSearchResult {
iframe_src: string; iframe_src: string;
} }
const strParser = new StringOutputParser(); const answerParser = new LineOutputParser({
key: 'answer',
});
const createVideoSearchChain = (llm: BaseChatModel) => { const createVideoSearchChain = (llm: BaseChatModel) => {
return RunnableSequence.from([ return RunnableSequence.from([
@@ -40,39 +100,13 @@ const createVideoSearchChain = (llm: BaseChatModel) => {
query: (input: VideoSearchChainInput) => { query: (input: VideoSearchChainInput) => {
return input.query; return input.query;
}, },
date: () => new Date().toISOString(),
}), }),
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, answerParser,
RunnableLambda.from(async (input: string) => { RunnableLambda.from(async (searchQuery: string) => {
const queryParser = new LineOutputParser({ const res = await searchSearxng(searchQuery, {
key: 'query',
});
return await queryParser.parse(input);
}),
RunnableLambda.from(async (input: string) => {
const res = await searchSearxng(input, {
engines: ['youtube'], engines: ['youtube'],
}); });
@@ -94,7 +128,7 @@ const createVideoSearchChain = (llm: BaseChatModel) => {
} }
}); });
return videos.slice(0, 10); return videos;
}), }),
]); ]);
}; };
@@ -103,8 +137,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

@@ -15,6 +15,7 @@ interface Config {
GENERAL: { GENERAL: {
SIMILARITY_MEASURE: string; SIMILARITY_MEASURE: string;
KEEP_ALIVE: string; KEEP_ALIVE: string;
BASE_URL?: string;
}; };
MODELS: { MODELS: {
OPENAI: { OPENAI: {
@@ -31,14 +32,10 @@ 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;
}; };
@@ -74,6 +71,8 @@ export const getSimilarityMeasure = () =>
export const getKeepAlive = () => loadConfig().GENERAL.KEEP_ALIVE; export const getKeepAlive = () => loadConfig().GENERAL.KEEP_ALIVE;
export const getBaseUrl = () => loadConfig().GENERAL.BASE_URL;
export const getOpenaiApiKey = () => loadConfig().MODELS.OPENAI.API_KEY; export const getOpenaiApiKey = () => loadConfig().MODELS.OPENAI.API_KEY;
export const getGroqApiKey = () => loadConfig().MODELS.GROQ.API_KEY; export const getGroqApiKey = () => loadConfig().MODELS.GROQ.API_KEY;
@@ -87,12 +86,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;

View File

@@ -3,8 +3,7 @@ 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(); const sqlite = new Database(path.join(process.cwd(), 'data/db.sqlite'));
const sqlite = new Database(path.join(DATA_DIR, './data/db.sqlite'));
const db = drizzle(sqlite, { const db = drizzle(sqlite, {
schema: schema, schema: schema,
}); });

View File

@@ -1,5 +0,0 @@
import db from './';
import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
import path from 'path';
migrate(db, { migrationsFolder: path.join(process.cwd(), 'drizzle') });

View File

@@ -1,643 +0,0 @@
'use client';
import { Message } from '@/components/ChatWindow';
import { createContext, useContext, useEffect, useRef, useState } from 'react';
import crypto from 'crypto';
import { useSearchParams } from 'next/navigation';
import { toast } from 'sonner';
import { Document } from '@langchain/core/documents';
import { getSuggestions } from '../actions';
type ChatContext = {
messages: Message[];
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.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);
};
export const chatContext = createContext<ChatContext>({
chatHistory: [],
chatId: '',
fileIds: [],
files: [],
focusMode: '',
hasError: false,
isMessagesLoaded: false,
isReady: false,
loading: false,
messageAppeared: false,
messages: [],
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[]>([]);
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);
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, 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);
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 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,
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

@@ -21,6 +21,10 @@ class LineOutputParser extends BaseOutputParser<string> {
async parse(text: string): Promise<string> { async parse(text: string): Promise<string> {
text = text.trim() || ''; text = text.trim() || '';
// First, remove all <think>...</think> blocks to avoid parsing tags inside thinking content
// This might be a little aggressive. Prompt massaging might be all we need, but this is a guarantee and should rarely mess anything up.
text = this.removeThinkingBlocks(text);
const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/; const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/;
const startKeyIndex = text.indexOf(`<${this.key}>`); const startKeyIndex = text.indexOf(`<${this.key}>`);
const endKeyIndex = text.indexOf(`</${this.key}>`); const endKeyIndex = text.indexOf(`</${this.key}>`);
@@ -40,6 +44,17 @@ class LineOutputParser extends BaseOutputParser<string> {
return line; return line;
} }
/**
* Removes all content within <think>...</think> blocks
* @param text The input text containing thinking blocks
* @returns The text with all thinking blocks removed
*/
private removeThinkingBlocks(text: string): string {
// Use regex to identify and remove all <think>...</think> blocks
// Using the 's' flag to make dot match newlines
return text.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
}
getFormatInstructions(): string { getFormatInstructions(): string {
throw new Error('Not implemented.'); throw new Error('Not implemented.');
} }

View File

@@ -21,6 +21,10 @@ class LineListOutputParser extends BaseOutputParser<string[]> {
async parse(text: string): Promise<string[]> { async parse(text: string): Promise<string[]> {
text = text.trim() || ''; text = text.trim() || '';
// First, remove all <think>...</think> blocks to avoid parsing tags inside thinking content
// This might be a little aggressive. Prompt massaging might be all we need, but this is a guarantee and should rarely mess anything up.
text = this.removeThinkingBlocks(text);
const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/; const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/;
const startKeyIndex = text.indexOf(`<${this.key}>`); const startKeyIndex = text.indexOf(`<${this.key}>`);
const endKeyIndex = text.indexOf(`</${this.key}>`); const endKeyIndex = text.indexOf(`</${this.key}>`);
@@ -42,6 +46,17 @@ class LineListOutputParser extends BaseOutputParser<string[]> {
return lines; return lines;
} }
/**
* Removes all content within <think>...</think> blocks
* @param text The input text containing thinking blocks
* @returns The text with all thinking blocks removed
*/
private removeThinkingBlocks(text: string): string {
// Use regex to identify and remove all <think>...</think> blocks
// Using [\s\S] pattern to match all characters including newlines
return text.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
}
getFormatInstructions(): string { getFormatInstructions(): string {
throw new Error('Not implemented.'); throw new Error('Not implemented.');
} }

19
src/lib/prompts/chat.ts Normal file
View File

@@ -0,0 +1,19 @@
export const chatPrompt = `
You are Perplexica, an AI model who is expert at having creative conversations with users. You are currently set on focus mode 'Chat', which means you will engage in a truly creative conversation without searching the web or citing sources.
In Chat mode, you should be:
- Creative and engaging in your responses
- Helpful and informative based on your internal knowledge
- Conversational and natural in your tone
- Willing to explore ideas, hypothetical scenarios, and creative topics
Since you are in Chat mode, you would not perform web searches or cite sources. If the user asks a question that would benefit from web search or specific data, you can suggest they switch to a different focus mode like 'All Mode' for general web search or another specialized mode.
### 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}
<context>
{context}
</context>
`;

View File

@@ -11,7 +11,8 @@ import {
wolframAlphaSearchResponsePrompt, wolframAlphaSearchResponsePrompt,
wolframAlphaSearchRetrieverPrompt, wolframAlphaSearchRetrieverPrompt,
} from './wolframAlpha'; } from './wolframAlpha';
import { writingAssistantPrompt } from './writingAssistant'; import { localResearchPrompt } from './localResearch';
import { chatPrompt } from './chat';
import { import {
youtubeSearchResponsePrompt, youtubeSearchResponsePrompt,
youtubeSearchRetrieverPrompt, youtubeSearchRetrieverPrompt,
@@ -26,7 +27,8 @@ export default {
redditSearchRetrieverPrompt, redditSearchRetrieverPrompt,
wolframAlphaSearchResponsePrompt, wolframAlphaSearchResponsePrompt,
wolframAlphaSearchRetrieverPrompt, wolframAlphaSearchRetrieverPrompt,
writingAssistantPrompt, localResearchPrompt,
chatPrompt,
youtubeSearchResponsePrompt, youtubeSearchResponsePrompt,
youtubeSearchRetrieverPrompt, youtubeSearchRetrieverPrompt,
}; };

View File

@@ -1,6 +1,6 @@
export const writingAssistantPrompt = ` export const localResearchPrompt = `
You are Perplexica, an AI model who is expert at searching the web and answering user's queries. You are currently set on focus mode 'Writing Assistant', this means you will be helping the user write a response to a given query. You are Perplexica, an AI model who is expert at searching the web and answering user's queries. You are currently set on focus mode 'Local Research', this means you will be helping the user research and interact with local files with citations.
Since you are a writing assistant, you would not perform web searches. If you think you lack information to answer the query, you can ask the user for more information or suggest them to switch to a different focus mode. Since you are in local research mode, you would not perform web searches. If you think you lack information to answer the query, you can ask the user for more information or suggest them to switch to a different focus mode.
You will be shared a context that can contain information from files user has uploaded to get answers from. You will have to generate answers upon that. You will be shared a context that can contain information from files user has uploaded to get answers from. You will have to generate answers upon that.
You have to cite the answer using [number] notation. You must cite the sentences with their relevent context number. You must cite each and every part of the answer so the user can know where the information is coming from. You have to cite the answer using [number] notation. You must cite the sentences with their relevent context number. You must cite each and every part of the answer so the user can know where the information is coming from.

View File

@@ -1,64 +1,131 @@
export const webSearchRetrieverPrompt = ` export const webSearchRetrieverPrompt = `
You are an AI question rephraser. You will be given a conversation and a follow-up question, you will have to rephrase the follow up question so it is a standalone question and can be used by another LLM to search the web for information to answer it. # Instructions
If it is a simple writing task or a greeting (unless the greeting contains a question after it) like Hi, Hello, How are you, etc. than a question then you need to return \`not_needed\` as the response (This is because the LLM won't need to search the web for finding information on this topic). - You are an AI question rephraser
If the user asks some question from some URL or wants you to summarize a PDF or a webpage (via URL) you need to return the links inside the \`links\` XML block and the question inside the \`question\` XML block. If the user wants to you to summarize the webpage or the PDF you need to return \`summarize\` inside the \`question\` XML block in place of a question and the link to summarize in the \`links\` XML block. - You will be given a conversation and a user question
You must always return the rephrased question inside the \`question\` XML block, if there are no links in the follow-up question then don't insert a \`links\` XML block in your response. - Rephrase the question so it is appropriate for web search
- Only add additional information or change the meaning of the question if it is necessary for clarity or relevance to the conversation
- Condense the question to its essence and remove any unnecessary details
- Ensure the question is grammatically correct and free of spelling errors
- If it is a simple writing task or a greeting (unless the greeting contains a question after it) like Hi, Hello, How are you, etc. than a question then you need to return \`not_needed\` as the response in the <answer> XML block
- If the user includes URLs or a PDF in their question, return the URLs or PDF links inside the <links> XML block and the question inside the <answer> XML block
- If the user wants to you to summarize the webpage or the PDF, return summarize inside the <answer> XML block in place of a question and the URLs to summarize in the <links> XML block
- If you are a thinking or reasoning AI, do not use <answer> and </answer> or <links> and </links> tags in your thinking. Those tags should only be used in the final output
- If applicable, use the provided date to ensure the rephrased question is relevant to the current date and time
There are several examples attached for your reference inside the below \`examples\` XML block # Data
- The history is contained in the <conversation> tag after the <examples> below
- The user question is contained in the <question> tag after the <examples> below
- You must always return the rephrased question inside an <answer> XML block, if there are no links in the follow-up question then don't insert a <links> XML block in your response
- Current date & time in ISO format (UTC timezone) is: {date}
- Do not include any other text in your answer
There are several examples attached for your reference inside the below examples XML block
<examples> <examples>
1. Follow up question: What is the capital of France ## Example 1 input
Rephrased question:\` <conversation>
Who won the last F1 race?\nAyrton Senna won the Monaco Grand Prix. It was a tight race with lots of overtakes. Alain Prost was in the lead for most of the race until the last lap when Senna overtook them.
</conversation>
<question> <question>
Capital of france What were the highlights of the race?
</question> </question>
\`
2. Hi, how are you? ## Example 1 output
Rephrased question\` <answer>
F1 Monaco Grand Prix highlights
</answer>
## Example 2 input
<conversation>
</conversation>
<question> <question>
What is the capital of France
</question>
## Example 2 output
<answer>
Capital of France
</answer>
## Example 3 input
<conversation>
</conversation>
<question>
Hi, how are you?
</question>
## Example 3 output
<answer>
not_needed not_needed
</question> </answer>
\`
3. Follow up question: What is Docker? ## Example 4 input
Rephrased question: \` <conversation>
</conversation>
<question> <question>
What is Docker Can you tell me what is X from https://example.com
</question> </question>
\`
4. Follow up question: Can you tell me what is X from https://example.com ## Example 4 output
Rephrased question: \` <answer>
<question> Can you tell me what is X
Can you tell me what is X? </answer>
</question>
<links> <links>
https://example.com https://example.com
</links> </links>
\`
5. Follow up question: Summarize the content from https://example.com ## Example 5 input
Rephrased question: \` <conversation>
</conversation>
<question> <question>
Summarize the content from https://example.com
</question>
## Example 5 output
<answer>
summarize summarize
</question> </answer>
<links> <links>
https://example.com https://example.com
</links> </links>
\`
## Example 6 input
<conversation>
</conversation>
<question>
Get the current F1 constructor standings and return the results in a table
</question>
## Example 6 output
<answer>
{date} F1 constructor standings
</answer>
## Example 7 input
<conversation>
</conversation>
<question>
What are the top 10 restaurants in New York? Show the results in a table and include a short description of each restaurant
</question>
## Example 7 output
<answer>
Top 10 restaurants in New York on {date}
</answer>
</examples> </examples>
Anything below is the part of the actual conversation and you need to use conversation and the follow-up question to rephrase the follow-up question as a standalone question based on the guidelines shared above. Everything below is the part of the actual conversation
<conversation> <conversation>
{chat_history} {chat_history}
</conversation> </conversation>
Follow up question: {query} <question>
Rephrased question: {query}
</question>
`; `;
export const webSearchResponsePrompt = ` export const webSearchResponsePrompt = `

View File

@@ -1,94 +0,0 @@
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
import { getAimlApiKey } from '../config';
import { ChatModel, EmbeddingModel } from '.';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { Embeddings } from '@langchain/core/embeddings';
import axios from 'axios';
export const PROVIDER_INFO = {
key: 'aimlapi',
displayName: 'AI/ML API',
};
interface AimlApiModel {
id: string;
name?: string;
type?: string;
}
const API_URL = 'https://api.aimlapi.com';
export const loadAimlApiChatModels = async () => {
const apiKey = getAimlApiKey();
if (!apiKey) return {};
try {
const response = await axios.get(`${API_URL}/models`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
});
const chatModels: Record<string, ChatModel> = {};
response.data.data.forEach((model: AimlApiModel) => {
if (model.type === 'chat-completion') {
chatModels[model.id] = {
displayName: model.name || model.id,
model: new ChatOpenAI({
apiKey: apiKey,
modelName: model.id,
temperature: 0.7,
configuration: {
baseURL: API_URL,
},
}) as unknown as BaseChatModel,
};
}
});
return chatModels;
} catch (err) {
console.error(`Error loading AI/ML API models: ${err}`);
return {};
}
};
export const loadAimlApiEmbeddingModels = async () => {
const apiKey = getAimlApiKey();
if (!apiKey) return {};
try {
const response = await axios.get(`${API_URL}/models`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
});
const embeddingModels: Record<string, EmbeddingModel> = {};
response.data.data.forEach((model: AimlApiModel) => {
if (model.type === 'embedding') {
embeddingModels[model.id] = {
displayName: model.name || model.id,
model: new OpenAIEmbeddings({
apiKey: apiKey,
modelName: model.id,
configuration: {
baseURL: API_URL,
},
}) as unknown as Embeddings,
};
}
});
return embeddingModels;
} catch (err) {
console.error(`Error loading AI/ML API embeddings models: ${err}`);
return {};
}
};

View File

@@ -9,18 +9,6 @@ export const PROVIDER_INFO = {
import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { BaseChatModel } from '@langchain/core/language_models/chat_models';
const anthropicChatModels: Record<string, string>[] = [ const anthropicChatModels: Record<string, string>[] = [
{
displayName: 'Claude 4.1 Opus',
key: 'claude-opus-4-1-20250805',
},
{
displayName: 'Claude 4 Opus',
key: 'claude-opus-4-20250514',
},
{
displayName: 'Claude 4 Sonnet',
key: 'claude-sonnet-4-20250514',
},
{ {
displayName: 'Claude 3.7 Sonnet', displayName: 'Claude 3.7 Sonnet',
key: 'claude-3-7-sonnet-20250219', key: 'claude-3-7-sonnet-20250219',

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