Compare commits
32 Commits
v1.11.0
...
e4f695ec9b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4f695ec9b | ||
|
|
9c7ccf42fc | ||
|
|
380216e062 | ||
|
|
b96c4234f4 | ||
|
|
c3a1e434e5 | ||
|
|
03b27d9cbb | ||
|
|
2a37f672ab | ||
|
|
85605fe166 | ||
|
|
d839769d7e | ||
|
|
ddfe8c607d | ||
|
|
f65b168388 | ||
|
|
8796009141 | ||
|
|
6220822c7c | ||
|
|
8241c87784 | ||
|
|
c8def1989a | ||
|
|
a71e4ae10d | ||
|
|
abf9dbb8ba | ||
|
|
b3aafba30c | ||
|
|
9f7fd178e0 | ||
|
|
59a10d7d00 | ||
|
|
67ee9eff53 | ||
|
|
0bb860b154 | ||
|
|
c0705d1d9e | ||
|
|
73b5e8832e | ||
|
|
b2da9faeed | ||
|
|
1a2ad8a59d | ||
|
|
e0817d1008 | ||
|
|
690ef42861 | ||
|
|
b84e4e4ce6 | ||
|
|
467905d9f2 | ||
|
|
18b6f5b674 | ||
|
|
2bdcbf20fb |
|
Before Width: | Height: | Size: 16 MiB After Width: | Height: | Size: 15 MiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 641 KiB |
94
.github/copilot-instructions.md
vendored
Normal 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
|
||||
139
.github/workflows/docker-build.yaml
vendored
@@ -4,20 +4,12 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- canary
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-amd64:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
variant:
|
||||
- name: full
|
||||
dockerfile: Dockerfile
|
||||
- name: slim
|
||||
dockerfile: Dockerfile.slim
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
@@ -38,54 +30,34 @@ jobs:
|
||||
id: version
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push AMD64 Docker image (master)
|
||||
- name: Build and push AMD64 Docker image
|
||||
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
||||
run: |
|
||||
DOCKERFILE=${{ matrix.variant.dockerfile }}
|
||||
VARIANT=${{ matrix.variant.name }}
|
||||
DOCKERFILE=app.dockerfile
|
||||
IMAGE_NAME=perplexica
|
||||
docker buildx build --platform linux/amd64 \
|
||||
--cache-from=type=registry,ref=itzcrazykns1337/perplexica:${VARIANT}-amd64 \
|
||||
--cache-from=type=registry,ref=itzcrazykns1337/${IMAGE_NAME}:amd64 \
|
||||
--cache-to=type=inline \
|
||||
--provenance false \
|
||||
-f $DOCKERFILE \
|
||||
-t itzcrazykns1337/perplexica:${VARIANT}-amd64 \
|
||||
--push .
|
||||
|
||||
- name: Build and push AMD64 Canary Docker image
|
||||
if: github.ref == 'refs/heads/canary' && github.event_name == 'push'
|
||||
run: |
|
||||
DOCKERFILE=${{ matrix.variant.dockerfile }}
|
||||
VARIANT=${{ matrix.variant.name }}
|
||||
docker buildx build --platform linux/amd64 \
|
||||
--cache-from=type=registry,ref=itzcrazykns1337/perplexica:${VARIANT}-canary-amd64 \
|
||||
--cache-to=type=inline \
|
||||
--provenance false \
|
||||
-f $DOCKERFILE \
|
||||
-t itzcrazykns1337/perplexica:${VARIANT}-canary-amd64 \
|
||||
-t itzcrazykns1337/${IMAGE_NAME}:amd64 \
|
||||
--push .
|
||||
|
||||
- name: Build and push AMD64 release Docker image
|
||||
if: github.event_name == 'release'
|
||||
run: |
|
||||
DOCKERFILE=${{ matrix.variant.dockerfile }}
|
||||
VARIANT=${{ matrix.variant.name }}
|
||||
DOCKERFILE=app.dockerfile
|
||||
IMAGE_NAME=perplexica
|
||||
docker buildx build --platform linux/amd64 \
|
||||
--cache-from=type=registry,ref=itzcrazykns1337/perplexica:${VARIANT}-${{ env.RELEASE_VERSION }}-amd64 \
|
||||
--cache-from=type=registry,ref=itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }}-amd64 \
|
||||
--cache-to=type=inline \
|
||||
--provenance false \
|
||||
-f $DOCKERFILE \
|
||||
-t itzcrazykns1337/perplexica:${VARIANT}-${{ env.RELEASE_VERSION }}-amd64 \
|
||||
-t itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }}-amd64 \
|
||||
--push .
|
||||
|
||||
build-arm64:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
strategy:
|
||||
matrix:
|
||||
variant:
|
||||
- name: full
|
||||
dockerfile: Dockerfile
|
||||
- name: slim
|
||||
dockerfile: Dockerfile.slim
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
@@ -106,51 +78,35 @@ jobs:
|
||||
id: version
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push ARM64 Docker image (master)
|
||||
- name: Build and push ARM64 Docker image
|
||||
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
||||
run: |
|
||||
DOCKERFILE=${{ matrix.variant.dockerfile }}
|
||||
VARIANT=${{ matrix.variant.name }}
|
||||
DOCKERFILE=app.dockerfile
|
||||
IMAGE_NAME=perplexica
|
||||
docker buildx build --platform linux/arm64 \
|
||||
--cache-from=type=registry,ref=itzcrazykns1337/perplexica:${VARIANT}-arm64 \
|
||||
--cache-from=type=registry,ref=itzcrazykns1337/${IMAGE_NAME}:arm64 \
|
||||
--cache-to=type=inline \
|
||||
--provenance false \
|
||||
-f $DOCKERFILE \
|
||||
-t itzcrazykns1337/perplexica:${VARIANT}-arm64 \
|
||||
--push .
|
||||
|
||||
- name: Build and push ARM64 Canary Docker image
|
||||
if: github.ref == 'refs/heads/canary' && github.event_name == 'push'
|
||||
run: |
|
||||
DOCKERFILE=${{ matrix.variant.dockerfile }}
|
||||
VARIANT=${{ matrix.variant.name }}
|
||||
docker buildx build --platform linux/arm64 \
|
||||
--cache-from=type=registry,ref=itzcrazykns1337/perplexica:${VARIANT}-canary-arm64 \
|
||||
--cache-to=type=inline \
|
||||
--provenance false \
|
||||
-f $DOCKERFILE \
|
||||
-t itzcrazykns1337/perplexica:${VARIANT}-canary-arm64 \
|
||||
-t itzcrazykns1337/${IMAGE_NAME}:arm64 \
|
||||
--push .
|
||||
|
||||
- name: Build and push ARM64 release Docker image
|
||||
if: github.event_name == 'release'
|
||||
run: |
|
||||
DOCKERFILE=${{ matrix.variant.dockerfile }}
|
||||
VARIANT=${{ matrix.variant.name }}
|
||||
DOCKERFILE=app.dockerfile
|
||||
IMAGE_NAME=perplexica
|
||||
docker buildx build --platform linux/arm64 \
|
||||
--cache-from=type=registry,ref=itzcrazykns1337/perplexica:${VARIANT}-${{ env.RELEASE_VERSION }}-arm64 \
|
||||
--cache-from=type=registry,ref=itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }}-arm64 \
|
||||
--cache-to=type=inline \
|
||||
--provenance false \
|
||||
-f $DOCKERFILE \
|
||||
-t itzcrazykns1337/perplexica:${VARIANT}-${{ env.RELEASE_VERSION }}-arm64 \
|
||||
-t itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }}-arm64 \
|
||||
--push .
|
||||
|
||||
manifest:
|
||||
needs: [build-amd64, build-arm64]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
variant: [full, slim]
|
||||
steps:
|
||||
- name: Log in to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
@@ -163,55 +119,20 @@ jobs:
|
||||
id: version
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Create and push manifest for main
|
||||
- name: Create and push multi-arch manifest for main
|
||||
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
||||
run: |
|
||||
VARIANT=${{ matrix.variant }}
|
||||
docker manifest create itzcrazykns1337/perplexica:${VARIANT}-latest \
|
||||
--amend itzcrazykns1337/perplexica:${VARIANT}-amd64 \
|
||||
--amend itzcrazykns1337/perplexica:${VARIANT}-arm64
|
||||
docker manifest push itzcrazykns1337/perplexica:${VARIANT}-latest
|
||||
IMAGE_NAME=perplexica
|
||||
docker manifest create itzcrazykns1337/${IMAGE_NAME}:main \
|
||||
--amend itzcrazykns1337/${IMAGE_NAME}:amd64 \
|
||||
--amend itzcrazykns1337/${IMAGE_NAME}:arm64
|
||||
docker manifest push itzcrazykns1337/${IMAGE_NAME}:main
|
||||
|
||||
if [ "$VARIANT" = "full" ]; then
|
||||
docker manifest create itzcrazykns1337/perplexica:latest \
|
||||
--amend itzcrazykns1337/perplexica:${VARIANT}-amd64 \
|
||||
--amend itzcrazykns1337/perplexica:${VARIANT}-arm64
|
||||
docker manifest push itzcrazykns1337/perplexica:latest
|
||||
|
||||
docker manifest create itzcrazykns1337/perplexica:main \
|
||||
--amend itzcrazykns1337/perplexica:${VARIANT}-amd64 \
|
||||
--amend itzcrazykns1337/perplexica:${VARIANT}-arm64
|
||||
docker manifest push itzcrazykns1337/perplexica:main
|
||||
fi
|
||||
|
||||
- name: Create and push manifest for canary
|
||||
if: github.ref == 'refs/heads/canary' && github.event_name == 'push'
|
||||
run: |
|
||||
VARIANT=${{ matrix.variant }}
|
||||
docker manifest create itzcrazykns1337/perplexica:${VARIANT}-canary \
|
||||
--amend itzcrazykns1337/perplexica:${VARIANT}-canary-amd64 \
|
||||
--amend itzcrazykns1337/perplexica:${VARIANT}-canary-arm64
|
||||
docker manifest push itzcrazykns1337/perplexica:${VARIANT}-canary
|
||||
|
||||
if [ "$VARIANT" = "full" ]; then
|
||||
docker manifest create itzcrazykns1337/perplexica:canary \
|
||||
--amend itzcrazykns1337/perplexica:${VARIANT}-canary-amd64 \
|
||||
--amend itzcrazykns1337/perplexica:${VARIANT}-canary-arm64
|
||||
docker manifest push itzcrazykns1337/perplexica:canary
|
||||
fi
|
||||
|
||||
- name: Create and push manifest for releases
|
||||
- name: Create and push multi-arch manifest for releases
|
||||
if: github.event_name == 'release'
|
||||
run: |
|
||||
VARIANT=${{ matrix.variant }}
|
||||
docker manifest create itzcrazykns1337/perplexica:${VARIANT}-${{ env.RELEASE_VERSION }} \
|
||||
--amend itzcrazykns1337/perplexica:${VARIANT}-${{ env.RELEASE_VERSION }}-amd64 \
|
||||
--amend itzcrazykns1337/perplexica:${VARIANT}-${{ env.RELEASE_VERSION }}-arm64
|
||||
docker manifest push itzcrazykns1337/perplexica:${VARIANT}-${{ env.RELEASE_VERSION }}
|
||||
|
||||
if [ "$VARIANT" = "full" ]; then
|
||||
docker manifest create itzcrazykns1337/perplexica:${{ env.RELEASE_VERSION }} \
|
||||
--amend itzcrazykns1337/perplexica:${VARIANT}-${{ env.RELEASE_VERSION }}-amd64 \
|
||||
--amend itzcrazykns1337/perplexica:${VARIANT}-${{ env.RELEASE_VERSION }}-arm64
|
||||
docker manifest push itzcrazykns1337/perplexica:${{ env.RELEASE_VERSION }}
|
||||
fi
|
||||
IMAGE_NAME=perplexica
|
||||
docker manifest create itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }} \
|
||||
--amend itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }}-amd64 \
|
||||
--amend itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }}-arm64
|
||||
docker manifest push itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }}
|
||||
|
||||
2
.gitignore
vendored
@@ -37,5 +37,3 @@ Thumbs.db
|
||||
# Db
|
||||
db.sqlite
|
||||
/searxng
|
||||
|
||||
certificates
|
||||
@@ -36,7 +36,7 @@ Before diving into coding, setting up your local environment is key. Here's what
|
||||
1. In the root directory, locate the `sample.config.toml` file.
|
||||
2. Rename it to `config.toml` and fill in the necessary configuration fields.
|
||||
3. Run `npm install` to install all dependencies.
|
||||
4. Run `npm run db:migrate` to set up the local sqlite database.
|
||||
4. Run `npm run db:push` to set up the local sqlite database.
|
||||
5. Use `npm run dev` to start the application in development mode.
|
||||
|
||||
**Please note**: Docker configurations are present for setting up production environments, whereas `npm run dev` is used for development purposes.
|
||||
|
||||
74
Dockerfile
@@ -1,74 +0,0 @@
|
||||
FROM node:24.5.0-slim AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 python3-pip sqlite3 && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /home/perplexica
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile --network-timeout 600000
|
||||
|
||||
COPY tsconfig.json next.config.mjs next-env.d.ts postcss.config.js drizzle.config.ts tailwind.config.ts ./
|
||||
COPY src ./src
|
||||
COPY public ./public
|
||||
COPY drizzle ./drizzle
|
||||
|
||||
RUN mkdir -p /home/perplexica/data
|
||||
RUN yarn build
|
||||
|
||||
FROM node:24.5.0-slim
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
python3-dev \
|
||||
sqlite3 \
|
||||
git \
|
||||
build-essential \
|
||||
libxslt-dev \
|
||||
zlib1g-dev \
|
||||
libffi-dev \
|
||||
libssl-dev \
|
||||
uwsgi \
|
||||
uwsgi-plugin-python3 \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /home/perplexica
|
||||
|
||||
COPY --from=builder /home/perplexica/public ./public
|
||||
COPY --from=builder /home/perplexica/.next/static ./public/_next/static
|
||||
COPY --from=builder /home/perplexica/.next/standalone ./
|
||||
COPY --from=builder /home/perplexica/data ./data
|
||||
COPY drizzle ./drizzle
|
||||
|
||||
RUN mkdir /home/perplexica/uploads
|
||||
|
||||
RUN useradd --system --home-dir /usr/local/searxng --shell /bin/sh searxng
|
||||
|
||||
WORKDIR /usr/local/searxng
|
||||
RUN git clone https://github.com/searxng/searxng.git . && \
|
||||
python3 -m venv venv && \
|
||||
. venv/bin/activate && \
|
||||
pip install --upgrade pip setuptools wheel pyyaml && \
|
||||
pip install -r requirements.txt && \
|
||||
pip install uwsgi
|
||||
|
||||
RUN mkdir -p /etc/searxng
|
||||
COPY searxng/settings.yml /etc/searxng/settings.yml
|
||||
COPY searxng/limiter.toml /etc/searxng/limiter.toml
|
||||
COPY searxng/uwsgi.ini /etc/searxng/uwsgi.ini
|
||||
|
||||
RUN chown -R searxng:searxng /usr/local/searxng /etc/searxng
|
||||
|
||||
WORKDIR /home/perplexica
|
||||
COPY entrypoint.sh ./entrypoint.sh
|
||||
RUN chmod +x ./entrypoint.sh
|
||||
RUN sed -i 's/\r$//' ./entrypoint.sh || true
|
||||
|
||||
EXPOSE 3000 8080
|
||||
|
||||
ENV SEARXNG_API_URL=http://localhost:8080
|
||||
|
||||
CMD ["/home/perplexica/entrypoint.sh"]
|
||||
222
README.md
@@ -1,22 +1,6 @@
|
||||
# 🚀 Perplexica - An AI-powered search engine 🔎 <!-- omit in toc -->
|
||||
|
||||
<div align="center" markdown="1">
|
||||
<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/>
|
||||
|
||||
[](https://discord.gg/26aArMy8tT)
|
||||
*This is a fork of [ItzCrazyKns/Perplexica](https://github.com/ItzCrazyKns/Perplexica) with additional features and improvements.*
|
||||
|
||||

|
||||
|
||||
@@ -29,14 +13,18 @@
|
||||
- [Getting Started with Docker (Recommended)](#getting-started-with-docker-recommended)
|
||||
- [Non-Docker Installation](#non-docker-installation)
|
||||
- [Ollama Connection Errors](#ollama-connection-errors)
|
||||
- [Lemonade Connection Errors](#lemonade-connection-errors)
|
||||
- [Using as a Search Engine](#using-as-a-search-engine)
|
||||
- [Using Perplexica's API](#using-perplexicas-api)
|
||||
- [Expose Perplexica to a network](#expose-perplexica-to-network)
|
||||
- [Expose Perplexica to network](#expose-perplexica-to-network)
|
||||
- [Running Behind a Reverse Proxy](#running-behind-a-reverse-proxy)
|
||||
- [One-Click Deployment](#one-click-deployment)
|
||||
- [Upcoming Features](#upcoming-features)
|
||||
- [Fork Improvements](#fork-improvements)
|
||||
- [UI Improvements](#ui-improvements)
|
||||
- [Search and Integration Enhancements](#search-and-integration-enhancements)
|
||||
- [AI Functionality](#ai-functionality)
|
||||
- [Bug Fixes](#bug-fixes)
|
||||
- [Support Us](#support-us)
|
||||
- [Donations](#donations)
|
||||
- [Contribution](#contribution)
|
||||
- [Help and Support](#help-and-support)
|
||||
|
||||
@@ -54,13 +42,14 @@ Want to know more about its architecture and how it works? You can read it [here
|
||||
|
||||
## 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:**
|
||||
- **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.
|
||||
- **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.
|
||||
- **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.
|
||||
- **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.
|
||||
@@ -76,35 +65,6 @@ There are mainly 2 ways of installing Perplexica - With Docker, Without Docker.
|
||||
|
||||
### Getting Started with Docker (Recommended)
|
||||
|
||||
Perplexica can be easily run using Docker. Simply run the following command:
|
||||
|
||||
```bash
|
||||
docker run -p 3000:3000 --name perplexica itzcrazykns1337/perplexica:latest
|
||||
```
|
||||
|
||||
This will pull and start the Perplexica container with the bundled SearxNG search engine. Once running, open your browser and navigate to http://localhost:3000. You can then configure your settings (API keys, models, etc.) directly in the setup screen.
|
||||
|
||||
**Note**: The image includes both Perplexica and SearxNG, so no additional setup is required.
|
||||
|
||||
#### Using Perplexica with Your Own SearxNG Instance
|
||||
|
||||
If you already have SearxNG running, you can use the slim version of Perplexica:
|
||||
|
||||
```bash
|
||||
docker run -p 3000:3000 -e SEARXNG_API_URL=http://your-searxng-url:8080 --name perplexica itzcrazykns1337/perplexica:slim-latest
|
||||
```
|
||||
|
||||
**Important**: Make sure your SearxNG instance has:
|
||||
|
||||
- JSON format enabled in the settings
|
||||
- Wolfram Alpha search engine enabled
|
||||
|
||||
Replace `http://your-searxng-url:8080` with your actual SearxNG URL. Then configure your AI provider settings in the setup screen at http://localhost:3000.
|
||||
|
||||
#### Advanced Setup (Building from Source)
|
||||
|
||||
If you prefer to build from source or need more control:
|
||||
|
||||
1. Ensure Docker is installed and running on your system.
|
||||
2. Clone the Perplexica repository:
|
||||
|
||||
@@ -114,62 +74,40 @@ If you prefer to build from source or need more control:
|
||||
|
||||
3. After cloning, navigate to the directory containing the project files.
|
||||
|
||||
4. Build and run using Docker:
|
||||
4. Rename the `sample.config.toml` file to `config.toml`. For Docker setups, you need only fill in the following fields:
|
||||
|
||||
- `OPENAI`: Your OpenAI API key. **You only need to fill this if you wish to use OpenAI's models**.
|
||||
- `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**.
|
||||
- `ANTHROPIC`: Your Anthropic API key. **You only need to fill this if you wish to use Anthropic models**.
|
||||
|
||||
**Note**: You can change these after starting Perplexica from the settings dialog.
|
||||
|
||||
- `SIMILARITY_MEASURE`: The similarity measure to use (This is filled by default; you can leave it as is if you are unsure about it.)
|
||||
|
||||
5. Ensure you are in the directory containing the `docker-compose.yaml` file and execute:
|
||||
|
||||
```bash
|
||||
docker build -t perplexica .
|
||||
docker run -p 3000:3000 --name perplexica perplexica
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
5. Access Perplexica at http://localhost:3000 and configure your settings in the setup screen.
|
||||
6. Wait a few minutes for the setup to complete. You can access Perplexica at http://localhost:3000 in your web browser.
|
||||
|
||||
**Note**: After the containers are built, you can start Perplexica directly from Docker without having to open a terminal.
|
||||
|
||||
### Non-Docker Installation
|
||||
|
||||
1. Install SearXNG and allow `JSON` format in the SearXNG settings. Make sure Wolfram Alpha search engine is also enabled.
|
||||
2. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/ItzCrazyKns/Perplexica.git
|
||||
cd Perplexica
|
||||
```
|
||||
|
||||
3. Install dependencies:
|
||||
|
||||
```bash
|
||||
npm i
|
||||
```
|
||||
|
||||
4. Build the application:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
5. Start the application:
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
6. Open your browser and navigate to http://localhost:3000 to complete the setup and configure your settings (API keys, models, SearxNG URL, etc.) in the setup screen.
|
||||
1. Install SearXNG and allow `JSON` format in the SearXNG settings.
|
||||
2. Clone the repository and rename the `sample.config.toml` file to `config.toml` in the root directory. Ensure you complete all required fields in this file.
|
||||
3. After populating the configuration run `npm i`.
|
||||
4. Install the dependencies and then execute `npm run build`.
|
||||
5. Finally, start the app by running `npm rum start`
|
||||
|
||||
**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.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### Local OpenAI-API-Compliant Servers
|
||||
|
||||
If Perplexica tells you that you haven't configured any chat model providers, ensure that:
|
||||
|
||||
1. Your server is running on `0.0.0.0` (not `127.0.0.1`) and on the same port you put in the API URL.
|
||||
2. You have specified the correct model name loaded by your local LLM server.
|
||||
3. You have specified the correct API key, or if one is not defined, you have put _something_ in the API key field and not left it empty.
|
||||
|
||||
#### Ollama Connection Errors
|
||||
### 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:
|
||||
|
||||
@@ -184,29 +122,10 @@ If you're encountering an Ollama connection error, it is likely due to the backe
|
||||
|
||||
3. **Linux Users - Expose Ollama to Network:**
|
||||
|
||||
- Inside `/etc/systemd/system/ollama.service`, you need to add `Environment="OLLAMA_HOST=0.0.0.0: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.
|
||||
|
||||
#### Lemonade Connection Errors
|
||||
|
||||
If you're encountering a Lemonade connection error, it is likely due to the backend being unable to connect to Lemonade's API. To fix this issue you can:
|
||||
|
||||
1. **Check your Lemonade API URL:** Ensure that the API URL is correctly set in the settings menu.
|
||||
2. **Update API URL Based on OS:**
|
||||
|
||||
- **Windows:** Use `http://host.docker.internal:8000`
|
||||
- **Mac:** Use `http://host.docker.internal:8000`
|
||||
- **Linux:** Use `http://<private_ip_of_host>:8000`
|
||||
|
||||
Adjust the port number if you're using a different one.
|
||||
|
||||
3. **Ensure Lemonade Server is Running:**
|
||||
|
||||
- Make sure your Lemonade server is running and accessible on the configured port (default is 8000).
|
||||
- Verify that Lemonade is configured to accept connections from all interfaces (`0.0.0.0`), not just localhost (`127.0.0.1`).
|
||||
- Ensure that the port (default is 8000) is not blocked by your firewall.
|
||||
|
||||
## Using as a Search Engine
|
||||
|
||||
If you wish to use Perplexica as an alternative to traditional search engines like Google or Bing, or if you want to add a shortcut for quick access from your browser's search bar, follow these steps:
|
||||
@@ -226,12 +145,45 @@ 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.
|
||||
|
||||
### 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
|
||||
|
||||
[](https://usw.sealos.io/?openapp=system-template%3FtemplateName%3Dperplexica)
|
||||
[](https://repocloud.io/details/?app_id=267)
|
||||
[](https://template.run.claw.cloud/?referralCode=U11MRQ8U9RM4&openapp=system-fastdeploy%3FtemplateName%3Dperplexica)
|
||||
[](https://www.hostinger.com/vps/docker-hosting?compose_url=https://raw.githubusercontent.com/ItzCrazyKns/Perplexica/refs/heads/master/docker-compose.yaml)
|
||||
|
||||
## Upcoming Features
|
||||
|
||||
@@ -243,17 +195,41 @@ Perplexica runs on Next.js and handles all API requests. It works right away on
|
||||
- [x] Adding Discover
|
||||
- [ ] Finalizing Copilot Mode
|
||||
|
||||
## Fork Improvements
|
||||
|
||||
This fork adds several enhancements to the original Perplexica project:
|
||||
|
||||
### UI Improvements
|
||||
- ✅ Tabbed interface for message results
|
||||
- ✅ Added message editing capability
|
||||
- ✅ Ability to select AI models directly while chatting without opening settings
|
||||
- ✅ Change focus mode at any time during chat sessions
|
||||
- ✅ Auto-scrolling
|
||||
- ✅ Syntax highlighting for code blocks
|
||||
- ✅ Display search query with the response
|
||||
- ✅ Improved styling for all screen sizes
|
||||
- ✅ Added model statistics showing model name and response time
|
||||
|
||||
### Search and Integration Enhancements
|
||||
- ✅ OpenSearch support with dynamic XML generation
|
||||
- Added BASE_URL config to support reverse proxy deployments
|
||||
- Added autocomplete functionality proxied to SearxNG
|
||||
- ✅ Enhanced Reddit focus mode to work around SearxNG limitations
|
||||
|
||||
### AI Functionality
|
||||
- ✅ True chat mode implementation (moved writing mode to local research mode)
|
||||
- ✅ Enhanced system prompts for more reliable and relevant results
|
||||
- ✅ Better parsing for reasoning models
|
||||
- ✅ User customizable context window for Ollama models
|
||||
- ✅ Toggle for automatic suggestions
|
||||
|
||||
### Bug Fixes
|
||||
- ✅ Improved history rewriting
|
||||
|
||||
## Support Us
|
||||
|
||||
If you find Perplexica useful, consider giving us a star on GitHub. This helps more people discover Perplexica and supports the development of new features. Your support is greatly appreciated.
|
||||
|
||||
### Donations
|
||||
|
||||
We also accept donations to help sustain our project. If you would like to contribute, you can use the following options to donate. Thank you for your support!
|
||||
|
||||
| Ethereum |
|
||||
| ----------------------------------------------------- |
|
||||
| Address: `0xB025a84b2F269570Eb8D4b05DEdaA41D8525B6DD` |
|
||||
|
||||
## Contribution
|
||||
|
||||
@@ -261,6 +237,6 @@ Perplexica is built on the idea that AI and large language models should be easy
|
||||
|
||||
## Help and Support
|
||||
|
||||
If you have any questions or feedback, please feel free to reach out to us. You can create an issue on GitHub or join our Discord server. There, you can connect with other users, share your experiences and reviews, and receive more personalized help. [Click here](https://discord.gg/EFwsmQDgAu) to join the Discord server. To discuss matters outside of regular support, feel free to contact me on Discord at `itzcrazykns`.
|
||||
If you have any questions or feedback, please feel free to reach out to us. You can create an issue on GitHub to get support or report bugs.
|
||||
|
||||
Thank you for exploring Perplexica, the AI-powered search engine designed to enhance your search experience. We are constantly working to improve Perplexica and expand its capabilities. We value your feedback and contributions which help us make Perplexica even better. Don't forget to check back for updates and new features!
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
FROM node:24.5.0-slim AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 python3-pip sqlite3 && rm -rf /var/lib/apt/lists/*
|
||||
FROM node:20.18.0-slim AS builder
|
||||
|
||||
WORKDIR /home/perplexica
|
||||
|
||||
@@ -10,14 +8,11 @@ RUN yarn install --frozen-lockfile --network-timeout 600000
|
||||
COPY tsconfig.json next.config.mjs next-env.d.ts postcss.config.js drizzle.config.ts tailwind.config.ts ./
|
||||
COPY src ./src
|
||||
COPY public ./public
|
||||
COPY drizzle ./drizzle
|
||||
|
||||
RUN mkdir -p /home/perplexica/data
|
||||
RUN yarn build
|
||||
|
||||
FROM node:24.5.0-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 python3-pip sqlite3 && rm -rf /var/lib/apt/lists/*
|
||||
FROM node:20.18.0-slim
|
||||
|
||||
WORKDIR /home/perplexica
|
||||
|
||||
@@ -26,10 +21,7 @@ COPY --from=builder /home/perplexica/.next/static ./public/_next/static
|
||||
|
||||
COPY --from=builder /home/perplexica/.next/standalone ./
|
||||
COPY --from=builder /home/perplexica/data ./data
|
||||
COPY drizzle ./drizzle
|
||||
|
||||
RUN mkdir /home/perplexica/uploads
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
CMD ["node", "server.js"]
|
||||
34
docker-compose.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
services:
|
||||
searxng:
|
||||
image: docker.io/searxng/searxng:latest
|
||||
volumes:
|
||||
- ./searxng:/etc/searxng:rw
|
||||
ports:
|
||||
- 4000:8080
|
||||
networks:
|
||||
- perplexica-network
|
||||
restart: unless-stopped
|
||||
|
||||
app:
|
||||
image: itzcrazykns1337/perplexica:main
|
||||
build:
|
||||
context: .
|
||||
dockerfile: app.dockerfile
|
||||
environment:
|
||||
- SEARXNG_API_URL=http://searxng:8080
|
||||
ports:
|
||||
- 3000:3000
|
||||
networks:
|
||||
- perplexica-network
|
||||
volumes:
|
||||
- backend-dbstore:/home/perplexica/data
|
||||
- uploads:/home/perplexica/uploads
|
||||
- ./config.toml:/home/perplexica/config.toml
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
perplexica-network:
|
||||
|
||||
volumes:
|
||||
backend-dbstore:
|
||||
uploads:
|
||||
@@ -4,55 +4,11 @@
|
||||
|
||||
Perplexica’s Search API makes it easy to use our AI-powered search engine. You can run different types of searches, pick the models you want to use, and get the most recent info. Follow the following headings to learn more about Perplexica's search API.
|
||||
|
||||
## Endpoints
|
||||
## Endpoint
|
||||
|
||||
### Get Available Providers and Models
|
||||
### **POST** `http://localhost:3000/api/search`
|
||||
|
||||
Before making search requests, you'll need to get the available providers and their models.
|
||||
|
||||
#### **GET** `/api/providers`
|
||||
|
||||
**Full URL**: `http://localhost:3000/api/providers`
|
||||
|
||||
Returns a list of all active providers with their available chat and embedding models.
|
||||
|
||||
**Response Example:**
|
||||
```json
|
||||
{
|
||||
"providers": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "OpenAI",
|
||||
"chatModels": [
|
||||
{
|
||||
"name": "GPT 4 Omni Mini",
|
||||
"key": "gpt-4o-mini"
|
||||
},
|
||||
{
|
||||
"name": "GPT 4 Omni",
|
||||
"key": "gpt-4o"
|
||||
}
|
||||
],
|
||||
"embeddingModels": [
|
||||
{
|
||||
"name": "Text Embedding 3 Large",
|
||||
"key": "text-embedding-3-large"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Use the `id` field as the `providerId` and the `key` field from the models arrays when making search requests.
|
||||
|
||||
### Search Query
|
||||
|
||||
#### **POST** `/api/search`
|
||||
|
||||
**Full URL**: `http://localhost:3000/api/search`
|
||||
|
||||
**Note**: Replace `localhost:3000` with your Perplexica instance URL if running on a different host or port
|
||||
**Note**: Replace `3000` with any other port if you've changed the default PORT
|
||||
|
||||
### Request
|
||||
|
||||
@@ -63,12 +19,12 @@ The API accepts a JSON object in the request body, where you define the focus mo
|
||||
```json
|
||||
{
|
||||
"chatModel": {
|
||||
"providerId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"key": "gpt-4o-mini"
|
||||
"provider": "openai",
|
||||
"name": "gpt-4o-mini"
|
||||
},
|
||||
"embeddingModel": {
|
||||
"providerId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"key": "text-embedding-3-large"
|
||||
"provider": "openai",
|
||||
"name": "text-embedding-3-large"
|
||||
},
|
||||
"optimizationMode": "speed",
|
||||
"focusMode": "webSearch",
|
||||
@@ -82,23 +38,24 @@ The API accepts a JSON object in the request body, where you define the focus mo
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: The `providerId` must be a valid UUID obtained from the `/api/providers` endpoint. The example above uses a sample UUID for demonstration.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
- **`chatModel`** (object, optional): Defines the chat model to be used for the query. To get available providers and models, send a GET request to `http://localhost:3000/api/providers`.
|
||||
- **`chatModel`** (object, optional): Defines the chat model to be used for the query. For model details you can send a GET request at `http://localhost:3000/api/models`. Make sure to use the key value (For example "gpt-4o-mini" instead of the display name "GPT 4 omni mini").
|
||||
|
||||
- `providerId` (string): The UUID of the provider. You can get this from the `/api/providers` endpoint response.
|
||||
- `key` (string): The model key/identifier (e.g., `gpt-4o-mini`, `llama3.1:latest`). Use the `key` value from the provider's `chatModels` array, not the display name.
|
||||
- `provider`: Specifies the provider for the chat model (e.g., `openai`, `ollama`).
|
||||
- `name`: The specific model from the chosen provider (e.g., `gpt-4o-mini`).
|
||||
- Optional fields for custom OpenAI configuration:
|
||||
- `customOpenAIBaseURL`: If you’re using a custom OpenAI instance, provide the base URL.
|
||||
- `customOpenAIKey`: The API key for a custom OpenAI instance.
|
||||
|
||||
- **`embeddingModel`** (object, optional): Defines the embedding model for similarity-based searching. To get available providers and models, send a GET request to `http://localhost:3000/api/providers`.
|
||||
- **`embeddingModel`** (object, optional): Defines the embedding model for similarity-based searching. For model details you can send a GET request at `http://localhost:3000/api/models`. Make sure to use the key value (For example "text-embedding-3-large" instead of the display name "Text Embedding 3 Large").
|
||||
|
||||
- `providerId` (string): The UUID of the embedding provider. You can get this from the `/api/providers` endpoint response.
|
||||
- `key` (string): The embedding model key (e.g., `text-embedding-3-large`, `nomic-embed-text`). Use the `key` value from the provider's `embeddingModels` array, not the display name.
|
||||
- `provider`: The provider for the embedding model (e.g., `openai`).
|
||||
- `name`: The specific embedding model (e.g., `text-embedding-3-large`).
|
||||
|
||||
- **`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:
|
||||
|
||||
@@ -151,7 +108,7 @@ The response from the API includes both the final message and the sources used t
|
||||
|
||||
#### Streaming Response (stream: true)
|
||||
|
||||
When streaming is enabled, the API returns a stream of newline-delimited JSON objects using Server-Sent Events (SSE). Each line contains a complete, valid JSON object. The response has `Content-Type: text/event-stream`.
|
||||
When streaming is enabled, the API returns a stream of newline-delimited JSON objects. Each line contains a complete, valid JSON object. The response has Content-Type: application/json.
|
||||
|
||||
Example of streamed response objects:
|
||||
|
||||
|
||||
@@ -2,80 +2,45 @@
|
||||
|
||||
To update Perplexica to the latest version, follow these steps:
|
||||
|
||||
## For Docker users (Using pre-built images)
|
||||
## For Docker users
|
||||
|
||||
Simply pull the latest image and restart your container:
|
||||
|
||||
```bash
|
||||
docker pull itzcrazykns1337/perplexica:latest
|
||||
docker stop perplexica
|
||||
docker rm perplexica
|
||||
docker run -p 3000:3000 --name perplexica itzcrazykns1337/perplexica:latest
|
||||
```
|
||||
|
||||
For slim version:
|
||||
|
||||
```bash
|
||||
docker pull itzcrazykns1337/perplexica:slim-latest
|
||||
docker stop perplexica
|
||||
docker rm perplexica
|
||||
docker run -p 3000:3000 -e SEARXNG_API_URL=http://your-searxng-url:8080 --name perplexica itzcrazykns1337/perplexica:slim-latest
|
||||
```
|
||||
|
||||
Once updated, go to http://localhost:3000 and verify the latest changes. Your settings are preserved automatically.
|
||||
|
||||
## For Docker users (Building from source)
|
||||
|
||||
1. Navigate to your Perplexica directory and pull the latest changes:
|
||||
1. Clone the latest version of Perplexica from GitHub:
|
||||
|
||||
```bash
|
||||
cd Perplexica
|
||||
git pull origin master
|
||||
git clone https://github.com/ItzCrazyKns/Perplexica.git
|
||||
```
|
||||
|
||||
2. Rebuild the Docker image:
|
||||
2. Navigate to the project directory.
|
||||
|
||||
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. Pull the latest images from the registry.
|
||||
|
||||
```bash
|
||||
docker build -t perplexica .
|
||||
docker compose pull
|
||||
```
|
||||
|
||||
3. Stop and remove the old container, then start the new one:
|
||||
5. Update and recreate the containers.
|
||||
|
||||
```bash
|
||||
docker stop perplexica
|
||||
docker rm perplexica
|
||||
docker run -p 3000:3000 -p 8080:8080 --name perplexica perplexica
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
4. Once the command completes, go to http://localhost:3000 and verify the latest changes.
|
||||
6. Once the command completes, go to http://localhost:3000 and verify the latest changes.
|
||||
|
||||
## For non-Docker users
|
||||
|
||||
1. Navigate to your Perplexica directory and pull the latest changes:
|
||||
1. Clone the latest version of Perplexica from GitHub:
|
||||
|
||||
```bash
|
||||
cd Perplexica
|
||||
git pull origin master
|
||||
git clone https://github.com/ItzCrazyKns/Perplexica.git
|
||||
```
|
||||
|
||||
2. Install any new dependencies:
|
||||
2. Navigate to the project directory.
|
||||
|
||||
```bash
|
||||
npm i
|
||||
```
|
||||
|
||||
3. Rebuild the application:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
4. Restart the application:
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
5. Go to http://localhost:3000 and verify the latest changes. Your settings are preserved automatically.
|
||||
3. Check for changes in the configuration files. If the `sample.config.toml` file contains new fields, delete your existing `config.toml` file, rename `sample.config.toml` to `config.toml`, and update the configuration accordingly.
|
||||
4. After populating the configuration run `npm i`.
|
||||
5. Install the dependencies and then execute `npm run build`.
|
||||
6. Finally, start the app by running `npm rum start`
|
||||
|
||||
---
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
dialect: 'sqlite',
|
||||
schema: './src/lib/db/schema.ts',
|
||||
out: './drizzle',
|
||||
dbCredentials: {
|
||||
url: path.join(process.cwd(), 'data', 'db.sqlite'),
|
||||
url: './data/db.sqlite',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
/* Do nothing */
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "6dedf55f-0e44-478f-82cf-14a21ac686f8",
|
||||
"prevId": "ef3a044b-0f34-40b5-babb-2bb3a909ba27",
|
||||
"tables": {
|
||||
"chats": {
|
||||
"name": "chats",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"focusMode": {
|
||||
"name": "focusMode",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"files": {
|
||||
"name": "files",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"messages": {
|
||||
"name": "messages",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"chatId": {
|
||||
"name": "chatId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
},
|
||||
"messageId": {
|
||||
"name": "messageId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"content": {
|
||||
"name": "content",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"sources": {
|
||||
"name": "sources",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1748405503809,
|
||||
"tag": "0000_fuzzy_randall",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1758863991284,
|
||||
"tag": "0001_wise_rockslide",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
cd /usr/local/searxng
|
||||
export SEARXNG_SETTINGS_PATH=/etc/searxng/settings.yml
|
||||
|
||||
# Start SearXNG in background with all output redirected to /dev/null
|
||||
/usr/local/searxng/venv/bin/uwsgi \
|
||||
--http-socket 0.0.0.0:8080 \
|
||||
--ini /etc/searxng/uwsgi.ini \
|
||||
--virtualenv /usr/local/searxng/venv \
|
||||
--disable-logging > /dev/null 2>&1 &
|
||||
|
||||
echo "Starting SearXNG..."
|
||||
sleep 5
|
||||
|
||||
until curl -s http://localhost:8080 > /dev/null 2>&1; do
|
||||
sleep 1
|
||||
done
|
||||
echo "SearXNG started successfully"
|
||||
|
||||
cd /home/perplexica
|
||||
echo "Starting Perplexica..."
|
||||
exec node server.js
|
||||
11860
package-lock.json
generated
Normal file
40
package.json
@@ -1,47 +1,46 @@
|
||||
{
|
||||
"name": "perplexica-frontend",
|
||||
"version": "1.11.0",
|
||||
"version": "1.10.2",
|
||||
"license": "MIT",
|
||||
"author": "ItzCrazyKns",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "npm run db:push && next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"format:write": "prettier . --write"
|
||||
"format:write": "prettier . --write",
|
||||
"db:push": "drizzle-kit push"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.0",
|
||||
"@headlessui/tailwindcss": "^0.2.2",
|
||||
"@huggingface/transformers": "^3.7.5",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@icons-pack/react-simple-icons": "^12.3.0",
|
||||
"@langchain/anthropic": "^1.0.0",
|
||||
"@langchain/community": "^1.0.0",
|
||||
"@langchain/core": "^1.0.1",
|
||||
"@langchain/google-genai": "^1.0.0",
|
||||
"@langchain/groq": "^1.0.0",
|
||||
"@langchain/ollama": "^1.0.0",
|
||||
"@langchain/openai": "^1.0.0",
|
||||
"@langchain/textsplitters": "^1.0.0",
|
||||
"@langchain/anthropic": "^0.3.15",
|
||||
"@langchain/community": "^0.3.36",
|
||||
"@langchain/core": "^0.3.42",
|
||||
"@langchain/google-genai": "^0.1.12",
|
||||
"@langchain/ollama": "^0.2.0",
|
||||
"@langchain/openai": "^0.0.25",
|
||||
"@langchain/textsplitters": "^0.1.0",
|
||||
"@tailwindcss/typography": "^0.5.12",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@xenova/transformers": "^2.17.2",
|
||||
"axios": "^1.8.3",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"clsx": "^2.1.0",
|
||||
"compute-cosine-similarity": "^1.1.0",
|
||||
"compute-dot": "^1.1.0",
|
||||
"drizzle-orm": "^0.40.1",
|
||||
"framer-motion": "^12.23.24",
|
||||
"html-to-text": "^9.0.5",
|
||||
"jspdf": "^3.0.1",
|
||||
"langchain": "^1.0.1",
|
||||
"langchain": "^0.1.30",
|
||||
"lucide-react": "^0.363.0",
|
||||
"mammoth": "^1.9.1",
|
||||
"markdown-to-jsx": "^7.7.2",
|
||||
"next": "^15.2.2",
|
||||
"next-themes": "^0.3.0",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"react-text-to-speech": "^0.14.5",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"sonner": "^1.4.41",
|
||||
@@ -53,8 +52,7 @@
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.12",
|
||||
"@types/html-to-text": "^9.0.4",
|
||||
"@types/jspdf": "^2.0.0",
|
||||
"@types/node": "^24.8.1",
|
||||
"@types/node": "^20",
|
||||
"@types/pdf-parse": "^1.1.4",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
@@ -65,6 +63,6 @@
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.2.5",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 916 B |
|
Before Width: | Height: | Size: 515 B |
BIN
public/icon.png
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 627 KiB |
|
Before Width: | Height: | Size: 202 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
33
sample.config.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[GENERAL]
|
||||
SIMILARITY_MEASURE = "cosine" # "cosine" or "dot"
|
||||
KEEP_ALIVE = "5m" # How long to keep Ollama models loaded into memory. (Instead of using -1 use "-1m")
|
||||
BASE_URL = "" # Optional. When set, overrides detected URL for OpenSearch and other public URLs
|
||||
|
||||
[MODELS.OPENAI]
|
||||
API_KEY = ""
|
||||
|
||||
[MODELS.GROQ]
|
||||
API_KEY = ""
|
||||
|
||||
[MODELS.ANTHROPIC]
|
||||
API_KEY = ""
|
||||
|
||||
[MODELS.GEMINI]
|
||||
API_KEY = ""
|
||||
|
||||
[MODELS.CUSTOM_OPENAI]
|
||||
API_KEY = ""
|
||||
API_URL = ""
|
||||
MODEL_NAME = ""
|
||||
|
||||
[MODELS.OLLAMA]
|
||||
API_URL = "" # Ollama API URL - http://host.docker.internal:11434
|
||||
|
||||
[MODELS.DEEPSEEK]
|
||||
API_KEY = ""
|
||||
|
||||
[MODELS.LM_STUDIO]
|
||||
API_URL = "" # LM Studio API URL - http://host.docker.internal:1234
|
||||
|
||||
[API_ENDPOINTS]
|
||||
SEARXNG = "" # SearxNG API URL - http://localhost:32768
|
||||
78
src/app/api/autocomplete/route.ts
Normal 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',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
18
src/app/api/chat/cancel/route.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
import { cancelRequest } from '@/lib/cancel-tokens';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { messageId } = await req.json();
|
||||
if (!messageId) {
|
||||
return Response.json({ error: 'Missing messageId' }, { status: 400 });
|
||||
}
|
||||
const cancelled = cancelRequest(messageId);
|
||||
if (cancelled) {
|
||||
return Response.json({ success: true });
|
||||
} else {
|
||||
return Response.json(
|
||||
{ error: 'No in-progress request for this messageId' },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,104 +1,77 @@
|
||||
import crypto from 'crypto';
|
||||
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
|
||||
import { EventEmitter } from 'stream';
|
||||
import {
|
||||
getCustomOpenaiApiKey,
|
||||
getCustomOpenaiApiUrl,
|
||||
getCustomOpenaiModelName,
|
||||
} from '@/lib/config';
|
||||
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 {
|
||||
getAvailableChatModelProviders,
|
||||
getAvailableEmbeddingModelProviders,
|
||||
} from '@/lib/providers';
|
||||
import { searchHandlers } from '@/lib/search';
|
||||
import { z } from 'zod';
|
||||
import ModelRegistry from '@/lib/models/registry';
|
||||
import { ModelWithProvider } from '@/lib/models/types';
|
||||
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';
|
||||
import {
|
||||
registerCancelToken,
|
||||
cleanupCancelToken,
|
||||
} from '@/lib/cancel-tokens';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const messageSchema = z.object({
|
||||
messageId: z.string().min(1, 'Message ID is required'),
|
||||
chatId: z.string().min(1, 'Chat ID is required'),
|
||||
content: z.string().min(1, 'Message content is required'),
|
||||
});
|
||||
type Message = {
|
||||
messageId: string;
|
||||
chatId: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
const chatModelSchema: z.ZodType<ModelWithProvider> = z.object({
|
||||
providerId: z.string({
|
||||
errorMap: () => ({
|
||||
message: 'Chat model provider id must be provided',
|
||||
}),
|
||||
}),
|
||||
key: z.string({
|
||||
errorMap: () => ({
|
||||
message: 'Chat model key must be provided',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
type ChatModel = {
|
||||
provider: string;
|
||||
name: string;
|
||||
ollamaContextWindow?: number;
|
||||
};
|
||||
|
||||
const embeddingModelSchema: z.ZodType<ModelWithProvider> = z.object({
|
||||
providerId: z.string({
|
||||
errorMap: () => ({
|
||||
message: 'Embedding model provider id must be provided',
|
||||
}),
|
||||
}),
|
||||
key: z.string({
|
||||
errorMap: () => ({
|
||||
message: 'Embedding model key must be provided',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
type EmbeddingModel = {
|
||||
provider: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const bodySchema = z.object({
|
||||
message: messageSchema,
|
||||
optimizationMode: z.enum(['speed', 'balanced', 'quality'], {
|
||||
errorMap: () => ({
|
||||
message: 'Optimization mode must be one of: speed, balanced, quality',
|
||||
}),
|
||||
}),
|
||||
focusMode: z.string().min(1, 'Focus mode is required'),
|
||||
history: z
|
||||
.array(
|
||||
z.tuple([z.string(), z.string()], {
|
||||
errorMap: () => ({
|
||||
message: 'History items must be tuples of two strings',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.optional()
|
||||
.default([]),
|
||||
files: z.array(z.string()).optional().default([]),
|
||||
chatModel: chatModelSchema,
|
||||
embeddingModel: embeddingModelSchema,
|
||||
systemInstructions: z.string().nullable().optional().default(''),
|
||||
});
|
||||
type Body = {
|
||||
message: Message;
|
||||
optimizationMode: 'speed' | 'balanced' | 'quality';
|
||||
focusMode: string;
|
||||
history: Array<[string, string]>;
|
||||
files: Array<string>;
|
||||
chatModel: ChatModel;
|
||||
embeddingModel: EmbeddingModel;
|
||||
systemInstructions: string;
|
||||
};
|
||||
|
||||
type Message = z.infer<typeof messageSchema>;
|
||||
type Body = z.infer<typeof bodySchema>;
|
||||
|
||||
const safeValidateBody = (data: unknown) => {
|
||||
const result = bodySchema.safeParse(data);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.error.errors.map((e) => ({
|
||||
path: e.path.join('.'),
|
||||
message: e.message,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.data,
|
||||
};
|
||||
type ModelStats = {
|
||||
modelName: string;
|
||||
responseTime?: number;
|
||||
};
|
||||
|
||||
const handleEmitterEvents = async (
|
||||
stream: EventEmitter,
|
||||
writer: WritableStreamDefaultWriter,
|
||||
encoder: TextEncoder,
|
||||
aiMessageId: string,
|
||||
chatId: string,
|
||||
startTime: number,
|
||||
userMessageId: string,
|
||||
) => {
|
||||
let receivedMessage = '';
|
||||
const aiMessageId = crypto.randomBytes(7).toString('hex');
|
||||
let recievedMessage = '';
|
||||
let sources: any[] = [];
|
||||
let searchQuery: string | undefined;
|
||||
let searchUrl: string | undefined;
|
||||
|
||||
stream.on('data', (data) => {
|
||||
const parsedData = JSON.parse(data);
|
||||
@@ -113,48 +86,80 @@ const handleEmitterEvents = async (
|
||||
),
|
||||
);
|
||||
|
||||
receivedMessage += parsedData.data;
|
||||
recievedMessage += parsedData.data;
|
||||
} 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(
|
||||
encoder.encode(
|
||||
JSON.stringify({
|
||||
type: 'sources',
|
||||
data: parsedData.data,
|
||||
searchQuery: parsedData.searchQuery,
|
||||
messageId: aiMessageId,
|
||||
searchUrl: searchUrl,
|
||||
}) + '\n',
|
||||
),
|
||||
);
|
||||
|
||||
const sourceMessageId = crypto.randomBytes(7).toString('hex');
|
||||
|
||||
db.insert(messagesSchema)
|
||||
.values({
|
||||
chatId: chatId,
|
||||
messageId: sourceMessageId,
|
||||
role: 'source',
|
||||
sources: parsedData.data,
|
||||
createdAt: new Date().toString(),
|
||||
})
|
||||
.execute();
|
||||
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', () => {
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
modelStats = {
|
||||
...modelStats,
|
||||
responseTime: duration,
|
||||
};
|
||||
|
||||
writer.write(
|
||||
encoder.encode(
|
||||
JSON.stringify({
|
||||
type: 'messageEnd',
|
||||
messageId: aiMessageId,
|
||||
modelStats: modelStats,
|
||||
searchQuery: searchQuery,
|
||||
searchUrl: searchUrl,
|
||||
}) + '\n',
|
||||
),
|
||||
);
|
||||
writer.close();
|
||||
|
||||
// Clean up the abort controller reference
|
||||
cleanupCancelToken(userMessageId);
|
||||
|
||||
db.insert(messagesSchema)
|
||||
.values({
|
||||
content: receivedMessage,
|
||||
content: recievedMessage,
|
||||
chatId: chatId,
|
||||
messageId: aiMessageId,
|
||||
role: 'assistant',
|
||||
createdAt: new Date().toString(),
|
||||
metadata: JSON.stringify({
|
||||
createdAt: new Date(),
|
||||
...(sources && sources.length > 0 && { sources }),
|
||||
...(searchQuery && { searchQuery }),
|
||||
modelStats: modelStats,
|
||||
...(searchUrl && { searchUrl }),
|
||||
}),
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
@@ -182,8 +187,6 @@ const handleHistorySave = async (
|
||||
where: eq(chats.id, message.chatId),
|
||||
});
|
||||
|
||||
const fileData = files.map(getFileDetails);
|
||||
|
||||
if (!chat) {
|
||||
await db
|
||||
.insert(chats)
|
||||
@@ -192,15 +195,9 @@ const handleHistorySave = async (
|
||||
title: message.content,
|
||||
createdAt: new Date().toString(),
|
||||
focusMode: focusMode,
|
||||
files: fileData,
|
||||
})
|
||||
.execute();
|
||||
} else if (JSON.stringify(chat.files ?? []) != JSON.stringify(fileData)) {
|
||||
db.update(chats)
|
||||
.set({
|
||||
files: files.map(getFileDetails),
|
||||
})
|
||||
.where(eq(chats.id, message.chatId));
|
||||
.execute();
|
||||
}
|
||||
|
||||
const messageExists = await db.query.messages.findFirst({
|
||||
@@ -215,10 +212,22 @@ const handleHistorySave = async (
|
||||
chatId: message.chatId,
|
||||
messageId: humanMessageId,
|
||||
role: 'user',
|
||||
createdAt: new Date().toString(),
|
||||
metadata: JSON.stringify({
|
||||
createdAt: new Date(),
|
||||
}),
|
||||
})
|
||||
.execute();
|
||||
} else {
|
||||
await db
|
||||
.update(messagesSchema)
|
||||
.set({
|
||||
content: message.content,
|
||||
metadata: JSON.stringify({
|
||||
createdAt: new Date(),
|
||||
}),
|
||||
})
|
||||
.where(eq(messagesSchema.messageId, humanMessageId))
|
||||
.execute();
|
||||
await db
|
||||
.delete(messagesSchema)
|
||||
.where(
|
||||
@@ -233,17 +242,8 @@ const handleHistorySave = async (
|
||||
|
||||
export const POST = async (req: Request) => {
|
||||
try {
|
||||
const reqBody = (await req.json()) as Body;
|
||||
|
||||
const parseBody = safeValidateBody(reqBody);
|
||||
if (!parseBody.success) {
|
||||
return Response.json(
|
||||
{ message: 'Invalid request body', error: parseBody.error },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const body = parseBody.data as Body;
|
||||
const startTime = Date.now();
|
||||
const body = (await req.json()) as Body;
|
||||
const { message } = body;
|
||||
|
||||
if (message.content === '') {
|
||||
@@ -255,18 +255,64 @@ export const POST = async (req: Request) => {
|
||||
);
|
||||
}
|
||||
|
||||
const registry = new ModelRegistry();
|
||||
|
||||
const [llm, embedding] = await Promise.all([
|
||||
registry.loadChatModel(body.chatModel.providerId, body.chatModel.key),
|
||||
registry.loadEmbeddingModel(
|
||||
body.embeddingModel.providerId,
|
||||
body.embeddingModel.key,
|
||||
),
|
||||
const [chatModelProviders, embeddingModelProviders] = await Promise.all([
|
||||
getAvailableChatModelProviders(),
|
||||
getAvailableEmbeddingModelProviders(),
|
||||
]);
|
||||
|
||||
const chatModelProvider =
|
||||
chatModelProviders[
|
||||
body.chatModel?.provider || Object.keys(chatModelProviders)[0]
|
||||
];
|
||||
const chatModel =
|
||||
chatModelProvider[
|
||||
body.chatModel?.name || Object.keys(chatModelProvider)[0]
|
||||
];
|
||||
|
||||
const embeddingProvider =
|
||||
embeddingModelProviders[
|
||||
body.embeddingModel?.provider || Object.keys(embeddingModelProviders)[0]
|
||||
];
|
||||
const embeddingModel =
|
||||
embeddingProvider[
|
||||
body.embeddingModel?.name || Object.keys(embeddingProvider)[0]
|
||||
];
|
||||
|
||||
let llm: BaseChatModel | undefined;
|
||||
let embedding = embeddingModel.model;
|
||||
|
||||
if (body.chatModel?.provider === 'custom_openai') {
|
||||
llm = new ChatOpenAI({
|
||||
openAIApiKey: getCustomOpenaiApiKey(),
|
||||
modelName: getCustomOpenaiModelName(),
|
||||
temperature: 0.7,
|
||||
configuration: {
|
||||
baseURL: getCustomOpenaiApiUrl(),
|
||||
},
|
||||
}) as unknown as BaseChatModel;
|
||||
} else if (chatModelProvider && chatModel) {
|
||||
llm = chatModel.model;
|
||||
|
||||
// Set context window size for Ollama models
|
||||
if (llm instanceof ChatOllama && body.chatModel?.provider === 'ollama') {
|
||||
llm.numCtx = body.chatModel.ollamaContextWindow || 2048;
|
||||
}
|
||||
}
|
||||
|
||||
if (!llm) {
|
||||
return Response.json({ error: 'Invalid chat model' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!embedding) {
|
||||
return Response.json(
|
||||
{ error: 'Invalid embedding model' },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const humanMessageId =
|
||||
message.messageId ?? crypto.randomBytes(7).toString('hex');
|
||||
const aiMessageId = crypto.randomBytes(7).toString('hex');
|
||||
|
||||
const history: BaseMessage[] = body.history.map((msg) => {
|
||||
if (msg[0] === 'human') {
|
||||
@@ -291,6 +337,28 @@ export const POST = async (req: Request) => {
|
||||
);
|
||||
}
|
||||
|
||||
const responseStream = new TransformStream();
|
||||
const writer = responseStream.writable.getWriter();
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
// --- Cancellation logic ---
|
||||
const abortController = new AbortController();
|
||||
registerCancelToken(message.messageId, abortController);
|
||||
|
||||
abortController.signal.addEventListener('abort', () => {
|
||||
console.log('Stream aborted, sending cancel event');
|
||||
writer.write(
|
||||
encoder.encode(
|
||||
JSON.stringify({
|
||||
type: 'error',
|
||||
data: 'Request cancelled by user',
|
||||
}),
|
||||
),
|
||||
);
|
||||
cleanupCancelToken(message.messageId);
|
||||
});
|
||||
|
||||
// Pass the abort signal to the search handler
|
||||
const stream = await handler.searchAndAnswer(
|
||||
message.content,
|
||||
history,
|
||||
@@ -298,14 +366,20 @@ export const POST = async (req: Request) => {
|
||||
embedding,
|
||||
body.optimizationMode,
|
||||
body.files,
|
||||
body.systemInstructions as string,
|
||||
body.systemInstructions,
|
||||
abortController.signal,
|
||||
);
|
||||
|
||||
const responseStream = new TransformStream();
|
||||
const writer = responseStream.writable.getWriter();
|
||||
const encoder = new TextEncoder();
|
||||
handleEmitterEvents(
|
||||
stream,
|
||||
writer,
|
||||
encoder,
|
||||
aiMessageId,
|
||||
message.chatId,
|
||||
startTime,
|
||||
message.messageId,
|
||||
);
|
||||
|
||||
handleEmitterEvents(stream, writer, encoder, message.chatId);
|
||||
handleHistorySave(message, humanMessageId, body.focusMode, body.files);
|
||||
|
||||
return new Response(responseStream.readable, {
|
||||
|
||||
@@ -1,76 +1,120 @@
|
||||
import configManager from '@/lib/config';
|
||||
import ModelRegistry from '@/lib/models/registry';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { ConfigModelProvider } from '@/lib/config/types';
|
||||
import {
|
||||
getAnthropicApiKey,
|
||||
getBaseUrl,
|
||||
getCustomOpenaiApiKey,
|
||||
getCustomOpenaiApiUrl,
|
||||
getCustomOpenaiModelName,
|
||||
getGeminiApiKey,
|
||||
getGroqApiKey,
|
||||
getOllamaApiEndpoint,
|
||||
getOpenaiApiKey,
|
||||
getDeepseekApiKey,
|
||||
getLMStudioApiEndpoint,
|
||||
updateConfig,
|
||||
} from '@/lib/config';
|
||||
import {
|
||||
getAvailableChatModelProviders,
|
||||
getAvailableEmbeddingModelProviders,
|
||||
} from '@/lib/providers';
|
||||
|
||||
type SaveConfigBody = {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export const GET = async (req: NextRequest) => {
|
||||
export const GET = async (req: Request) => {
|
||||
try {
|
||||
const values = configManager.getCurrentConfig();
|
||||
const fields = configManager.getUIConfigSections();
|
||||
const config: Record<string, any> = {};
|
||||
|
||||
const modelRegistry = new ModelRegistry();
|
||||
const modelProviders = await modelRegistry.getActiveProviders();
|
||||
const [chatModelProviders, embeddingModelProviders] = await Promise.all([
|
||||
getAvailableChatModelProviders(),
|
||||
getAvailableEmbeddingModelProviders(),
|
||||
]);
|
||||
|
||||
values.modelProviders = values.modelProviders.map(
|
||||
(mp: ConfigModelProvider) => {
|
||||
const activeProvider = modelProviders.find((p) => p.id === mp.id);
|
||||
config['chatModelProviders'] = {};
|
||||
config['embeddingModelProviders'] = {};
|
||||
|
||||
for (const provider in chatModelProviders) {
|
||||
config['chatModelProviders'][provider] = Object.keys(
|
||||
chatModelProviders[provider],
|
||||
).map((model) => {
|
||||
return {
|
||||
...mp,
|
||||
chatModels: activeProvider?.chatModels ?? mp.chatModels,
|
||||
embeddingModels:
|
||||
activeProvider?.embeddingModels ?? mp.embeddingModels,
|
||||
name: model,
|
||||
displayName: chatModelProviders[provider][model].displayName,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
values,
|
||||
fields,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error in getting config: ', err);
|
||||
return Response.json(
|
||||
{ message: 'An error has occurred.' },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
try {
|
||||
const body: SaveConfigBody = await req.json();
|
||||
|
||||
if (!body.key || !body.value) {
|
||||
return Response.json(
|
||||
{
|
||||
message: 'Key and value are required.',
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
configManager.updateConfig(body.key, body.value);
|
||||
for (const provider in embeddingModelProviders) {
|
||||
config['embeddingModelProviders'][provider] = Object.keys(
|
||||
embeddingModelProviders[provider],
|
||||
).map((model) => {
|
||||
return {
|
||||
name: model,
|
||||
displayName: embeddingModelProviders[provider][model].displayName,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
message: 'Config updated successfully.',
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
},
|
||||
);
|
||||
config['openaiApiKey'] = getOpenaiApiKey();
|
||||
config['ollamaApiUrl'] = getOllamaApiEndpoint();
|
||||
config['lmStudioApiUrl'] = getLMStudioApiEndpoint();
|
||||
config['anthropicApiKey'] = getAnthropicApiKey();
|
||||
config['groqApiKey'] = getGroqApiKey();
|
||||
config['geminiApiKey'] = getGeminiApiKey();
|
||||
config['deepseekApiKey'] = getDeepseekApiKey();
|
||||
config['customOpenaiApiUrl'] = getCustomOpenaiApiUrl();
|
||||
config['customOpenaiApiKey'] = getCustomOpenaiApiKey();
|
||||
config['customOpenaiModelName'] = getCustomOpenaiModelName();
|
||||
config['baseUrl'] = getBaseUrl();
|
||||
|
||||
return Response.json({ ...config }, { status: 200 });
|
||||
} catch (err) {
|
||||
console.error('Error in getting config: ', err);
|
||||
console.error('An error occurred while getting config:', err);
|
||||
return Response.json(
|
||||
{ message: 'An error has occurred.' },
|
||||
{ message: 'An error occurred while getting config' },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const POST = async (req: Request) => {
|
||||
try {
|
||||
const config = await req.json();
|
||||
|
||||
const updatedConfig = {
|
||||
MODELS: {
|
||||
OPENAI: {
|
||||
API_KEY: config.openaiApiKey,
|
||||
},
|
||||
GROQ: {
|
||||
API_KEY: config.groqApiKey,
|
||||
},
|
||||
ANTHROPIC: {
|
||||
API_KEY: config.anthropicApiKey,
|
||||
},
|
||||
GEMINI: {
|
||||
API_KEY: config.geminiApiKey,
|
||||
},
|
||||
OLLAMA: {
|
||||
API_URL: config.ollamaApiUrl,
|
||||
},
|
||||
DEEPSEEK: {
|
||||
API_KEY: config.deepseekApiKey,
|
||||
},
|
||||
LM_STUDIO: {
|
||||
API_URL: config.lmStudioApiUrl,
|
||||
},
|
||||
CUSTOM_OPENAI: {
|
||||
API_URL: config.customOpenaiApiUrl,
|
||||
API_KEY: config.customOpenaiApiKey,
|
||||
MODEL_NAME: config.customOpenaiModelName,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
updateConfig(updatedConfig);
|
||||
|
||||
return Response.json({ message: 'Config updated' }, { status: 200 });
|
||||
} catch (err) {
|
||||
console.error('An error occurred while updating config:', err);
|
||||
return Response.json(
|
||||
{ message: 'An error occurred while updating config' },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import configManager from '@/lib/config';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
try {
|
||||
configManager.markSetupComplete();
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
message: 'Setup marked as complete.',
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Error marking setup as complete: ', err);
|
||||
return Response.json(
|
||||
{ message: 'An error has occurred.' },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,80 +1,43 @@
|
||||
import { searchSearxng } from '@/lib/searxng';
|
||||
|
||||
const websitesForTopic = {
|
||||
tech: {
|
||||
query: ['technology news', 'latest tech', 'AI', 'science and innovation'],
|
||||
links: ['techcrunch.com', 'wired.com', 'theverge.com'],
|
||||
},
|
||||
finance: {
|
||||
query: ['finance news', 'economy', 'stock market', 'investing'],
|
||||
links: ['bloomberg.com', 'cnbc.com', 'marketwatch.com'],
|
||||
},
|
||||
art: {
|
||||
query: ['art news', 'culture', 'modern art', 'cultural events'],
|
||||
links: ['artnews.com', 'hyperallergic.com', 'theartnewspaper.com'],
|
||||
},
|
||||
sports: {
|
||||
query: ['sports news', 'latest sports', 'cricket football tennis'],
|
||||
links: ['espn.com', 'bbc.com/sport', 'skysports.com'],
|
||||
},
|
||||
entertainment: {
|
||||
query: ['entertainment news', 'movies', 'TV shows', 'celebrities'],
|
||||
links: ['hollywoodreporter.com', 'variety.com', 'deadline.com'],
|
||||
},
|
||||
};
|
||||
const articleWebsites = [
|
||||
'yahoo.com',
|
||||
'www.exchangewire.com',
|
||||
'businessinsider.com',
|
||||
/* 'wired.com',
|
||||
'mashable.com',
|
||||
'theverge.com',
|
||||
'gizmodo.com',
|
||||
'cnet.com',
|
||||
'venturebeat.com', */
|
||||
];
|
||||
|
||||
type Topic = keyof typeof websitesForTopic;
|
||||
const topics = ['AI', 'tech']; /* TODO: Add UI to customize this */
|
||||
|
||||
export const GET = async (req: Request) => {
|
||||
try {
|
||||
const params = new URL(req.url).searchParams;
|
||||
|
||||
const mode: 'normal' | 'preview' =
|
||||
(params.get('mode') as 'normal' | 'preview') || 'normal';
|
||||
const topic: Topic = (params.get('topic') as Topic) || 'tech';
|
||||
|
||||
const selectedTopic = websitesForTopic[topic];
|
||||
|
||||
let data = [];
|
||||
|
||||
if (mode === 'normal') {
|
||||
const seenUrls = new Set();
|
||||
|
||||
data = (
|
||||
await Promise.all(
|
||||
selectedTopic.links.flatMap((link) =>
|
||||
selectedTopic.query.map(async (query) => {
|
||||
return (
|
||||
await searchSearxng(`site:${link} ${query}`, {
|
||||
const data = (
|
||||
await Promise.all([
|
||||
...new Array(articleWebsites.length * topics.length)
|
||||
.fill(0)
|
||||
.map(async (_, i) => {
|
||||
return (
|
||||
await searchSearxng(
|
||||
`site:${articleWebsites[i % articleWebsites.length]} ${
|
||||
topics[i % topics.length]
|
||||
}`,
|
||||
{
|
||||
engines: ['bing news'],
|
||||
pageno: 1,
|
||||
language: 'en',
|
||||
})
|
||||
).results;
|
||||
}),
|
||||
),
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.filter((item) => {
|
||||
const url = item.url?.toLowerCase().trim();
|
||||
if (seenUrls.has(url)) return false;
|
||||
seenUrls.add(url);
|
||||
return true;
|
||||
})
|
||||
.sort(() => Math.random() - 0.5);
|
||||
} else {
|
||||
data = (
|
||||
await searchSearxng(
|
||||
`site:${selectedTopic.links[Math.floor(Math.random() * selectedTopic.links.length)]} ${selectedTopic.query[Math.floor(Math.random() * selectedTopic.query.length)]}`,
|
||||
{
|
||||
engines: ['bing news'],
|
||||
pageno: 1,
|
||||
language: 'en',
|
||||
},
|
||||
)
|
||||
).results;
|
||||
}
|
||||
},
|
||||
)
|
||||
).results;
|
||||
}),
|
||||
])
|
||||
)
|
||||
.map((result) => result)
|
||||
.flat()
|
||||
.sort(() => Math.random() - 0.5);
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import handleImageSearch from '@/lib/chains/imageSearchAgent';
|
||||
import ModelRegistry from '@/lib/models/registry';
|
||||
import { ModelWithProvider } from '@/lib/models/types';
|
||||
import {
|
||||
getCustomOpenaiApiKey,
|
||||
getCustomOpenaiApiUrl,
|
||||
getCustomOpenaiModelName,
|
||||
} from '@/lib/config';
|
||||
import { getAvailableChatModelProviders } from '@/lib/providers';
|
||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
|
||||
import { ChatOllama } from '@langchain/ollama';
|
||||
import { ChatOpenAI } from '@langchain/openai';
|
||||
|
||||
interface ChatModel {
|
||||
provider: string;
|
||||
model: string;
|
||||
ollamaContextWindow?: number;
|
||||
}
|
||||
|
||||
interface ImageSearchBody {
|
||||
query: string;
|
||||
chatHistory: any[];
|
||||
chatModel: ModelWithProvider;
|
||||
chatModel?: ChatModel;
|
||||
}
|
||||
|
||||
export const POST = async (req: Request) => {
|
||||
@@ -23,12 +36,39 @@ export const POST = async (req: Request) => {
|
||||
})
|
||||
.filter((msg) => msg !== undefined) as BaseMessage[];
|
||||
|
||||
const registry = new ModelRegistry();
|
||||
const chatModelProviders = await getAvailableChatModelProviders();
|
||||
|
||||
const llm = await registry.loadChatModel(
|
||||
body.chatModel.providerId,
|
||||
body.chatModel.key,
|
||||
);
|
||||
const chatModelProvider =
|
||||
chatModelProviders[
|
||||
body.chatModel?.provider || Object.keys(chatModelProviders)[0]
|
||||
];
|
||||
const chatModel =
|
||||
chatModelProvider[
|
||||
body.chatModel?.model || Object.keys(chatModelProvider)[0]
|
||||
];
|
||||
|
||||
let llm: BaseChatModel | undefined;
|
||||
|
||||
if (body.chatModel?.provider === 'custom_openai') {
|
||||
llm = new ChatOpenAI({
|
||||
openAIApiKey: getCustomOpenaiApiKey(),
|
||||
modelName: getCustomOpenaiModelName(),
|
||||
temperature: 0.7,
|
||||
configuration: {
|
||||
baseURL: getCustomOpenaiApiUrl(),
|
||||
},
|
||||
}) as unknown as BaseChatModel;
|
||||
} else if (chatModelProvider && chatModel) {
|
||||
llm = chatModel.model;
|
||||
// Set context window size for Ollama models
|
||||
if (llm instanceof ChatOllama && body.chatModel?.provider === 'ollama') {
|
||||
llm.numCtx = body.chatModel.ollamaContextWindow || 2048;
|
||||
}
|
||||
}
|
||||
|
||||
if (!llm) {
|
||||
return Response.json({ error: 'Invalid chat model' }, { status: 400 });
|
||||
}
|
||||
|
||||
const images = await handleImageSearch(
|
||||
{
|
||||
|
||||
47
src/app/api/models/route.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
getAvailableChatModelProviders,
|
||||
getAvailableEmbeddingModelProviders,
|
||||
} from '@/lib/providers';
|
||||
|
||||
export const GET = async (req: Request) => {
|
||||
try {
|
||||
const [chatModelProviders, embeddingModelProviders] = await Promise.all([
|
||||
getAvailableChatModelProviders(),
|
||||
getAvailableEmbeddingModelProviders(),
|
||||
]);
|
||||
|
||||
Object.keys(chatModelProviders).forEach((provider) => {
|
||||
Object.keys(chatModelProviders[provider]).forEach((model) => {
|
||||
delete (chatModelProviders[provider][model] as { model?: unknown })
|
||||
.model;
|
||||
});
|
||||
});
|
||||
|
||||
Object.keys(embeddingModelProviders).forEach((provider) => {
|
||||
Object.keys(embeddingModelProviders[provider]).forEach((model) => {
|
||||
delete (embeddingModelProviders[provider][model] as { model?: unknown })
|
||||
.model;
|
||||
});
|
||||
});
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
chatModelProviders,
|
||||
embeddingModelProviders,
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('An error occurred while fetching models', err);
|
||||
return Response.json(
|
||||
{
|
||||
message: 'An error has occurred.',
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
70
src/app/api/opensearch/route.ts
Normal 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);
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import ModelRegistry from '@/lib/models/registry';
|
||||
import { Model } from '@/lib/models/types';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
export const POST = async (
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) => {
|
||||
try {
|
||||
const { id } = await params;
|
||||
|
||||
const body: Partial<Model> & { type: 'embedding' | 'chat' } =
|
||||
await req.json();
|
||||
|
||||
if (!body.key || !body.name) {
|
||||
return Response.json(
|
||||
{
|
||||
message: 'Key and name must be provided',
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const registry = new ModelRegistry();
|
||||
|
||||
await registry.addProviderModel(id, body.type, body);
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
message: 'Model added successfully',
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('An error occurred while adding provider model', err);
|
||||
return Response.json(
|
||||
{
|
||||
message: 'An error has occurred.',
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const DELETE = async (
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) => {
|
||||
try {
|
||||
const { id } = await params;
|
||||
|
||||
const body: { key: string; type: 'embedding' | 'chat' } = await req.json();
|
||||
|
||||
if (!body.key) {
|
||||
return Response.json(
|
||||
{
|
||||
message: 'Key and name must be provided',
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const registry = new ModelRegistry();
|
||||
|
||||
await registry.removeProviderModel(id, body.type, body.key);
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
message: 'Model added successfully',
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('An error occurred while deleting provider model', err);
|
||||
return Response.json(
|
||||
{
|
||||
message: 'An error has occurred.',
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,89 +0,0 @@
|
||||
import ModelRegistry from '@/lib/models/registry';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
export const DELETE = async (
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) => {
|
||||
try {
|
||||
const { id } = await params;
|
||||
|
||||
if (!id) {
|
||||
return Response.json(
|
||||
{
|
||||
message: 'Provider ID is required.',
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const registry = new ModelRegistry();
|
||||
await registry.removeProvider(id);
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
message: 'Provider deleted successfully.',
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
},
|
||||
);
|
||||
} catch (err: any) {
|
||||
console.error('An error occurred while deleting provider', err.message);
|
||||
return Response.json(
|
||||
{
|
||||
message: 'An error has occurred.',
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const PATCH = async (
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) => {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { name, config } = body;
|
||||
const { id } = await params;
|
||||
|
||||
if (!id || !name || !config) {
|
||||
return Response.json(
|
||||
{
|
||||
message: 'Missing required fields.',
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const registry = new ModelRegistry();
|
||||
|
||||
const updatedProvider = await registry.updateProvider(id, name, config);
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
provider: updatedProvider,
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
},
|
||||
);
|
||||
} catch (err: any) {
|
||||
console.error('An error occurred while updating provider', err.message);
|
||||
return Response.json(
|
||||
{
|
||||
message: 'An error has occurred.',
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
import ModelRegistry from '@/lib/models/registry';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
export const GET = async (req: Request) => {
|
||||
try {
|
||||
const registry = new ModelRegistry();
|
||||
|
||||
const activeProviders = await registry.getActiveProviders();
|
||||
|
||||
const filteredProviders = activeProviders.filter((p) => {
|
||||
return !p.chatModels.some((m) => m.key === 'error');
|
||||
});
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
providers: filteredProviders,
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('An error occurred while fetching providers', err);
|
||||
return Response.json(
|
||||
{
|
||||
message: 'An error has occurred.',
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { type, name, config } = body;
|
||||
|
||||
if (!type || !name || !config) {
|
||||
return Response.json(
|
||||
{
|
||||
message: 'Missing required fields.',
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const registry = new ModelRegistry();
|
||||
|
||||
const newProvider = await registry.addProvider(type, name, config);
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
provider: newProvider,
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('An error occurred while creating provider', err);
|
||||
return Response.json(
|
||||
{
|
||||
message: 'An error has occurred.',
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,14 +1,38 @@
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import type { Embeddings } from '@langchain/core/embeddings';
|
||||
import { ChatOpenAI } from '@langchain/openai';
|
||||
import {
|
||||
getAvailableChatModelProviders,
|
||||
getAvailableEmbeddingModelProviders,
|
||||
} from '@/lib/providers';
|
||||
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
|
||||
import { MetaSearchAgentType } from '@/lib/search/metaSearchAgent';
|
||||
import {
|
||||
getCustomOpenaiApiKey,
|
||||
getCustomOpenaiApiUrl,
|
||||
getCustomOpenaiModelName,
|
||||
} from '@/lib/config';
|
||||
import { searchHandlers } from '@/lib/search';
|
||||
import ModelRegistry from '@/lib/models/registry';
|
||||
import { ModelWithProvider } from '@/lib/models/types';
|
||||
import { ChatOllama } from '@langchain/ollama';
|
||||
|
||||
interface chatModel {
|
||||
provider: string;
|
||||
name: string;
|
||||
customOpenAIKey?: string;
|
||||
customOpenAIBaseURL?: string;
|
||||
ollamaContextWindow?: number;
|
||||
}
|
||||
|
||||
interface embeddingModel {
|
||||
provider: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface ChatRequestBody {
|
||||
optimizationMode: 'speed' | 'balanced';
|
||||
focusMode: string;
|
||||
chatModel: ModelWithProvider;
|
||||
embeddingModel: ModelWithProvider;
|
||||
chatModel?: chatModel;
|
||||
embeddingModel?: embeddingModel;
|
||||
query: string;
|
||||
history: Array<[string, string]>;
|
||||
stream?: boolean;
|
||||
@@ -36,21 +60,72 @@ export const POST = async (req: Request) => {
|
||||
: new AIMessage({ content: msg[1] });
|
||||
});
|
||||
|
||||
const registry = new ModelRegistry();
|
||||
|
||||
const [llm, embeddings] = await Promise.all([
|
||||
registry.loadChatModel(body.chatModel.providerId, body.chatModel.key),
|
||||
registry.loadEmbeddingModel(
|
||||
body.embeddingModel.providerId,
|
||||
body.embeddingModel.key,
|
||||
),
|
||||
const [chatModelProviders, embeddingModelProviders] = await Promise.all([
|
||||
getAvailableChatModelProviders(),
|
||||
getAvailableEmbeddingModelProviders(),
|
||||
]);
|
||||
|
||||
const chatModelProvider =
|
||||
body.chatModel?.provider || Object.keys(chatModelProviders)[0];
|
||||
const chatModel =
|
||||
body.chatModel?.name ||
|
||||
Object.keys(chatModelProviders[chatModelProvider])[0];
|
||||
|
||||
const embeddingModelProvider =
|
||||
body.embeddingModel?.provider || Object.keys(embeddingModelProviders)[0];
|
||||
const embeddingModel =
|
||||
body.embeddingModel?.name ||
|
||||
Object.keys(embeddingModelProviders[embeddingModelProvider])[0];
|
||||
|
||||
let llm: BaseChatModel | undefined;
|
||||
let embeddings: Embeddings | undefined;
|
||||
|
||||
if (body.chatModel?.provider === 'custom_openai') {
|
||||
llm = new ChatOpenAI({
|
||||
modelName: body.chatModel?.name || getCustomOpenaiModelName(),
|
||||
openAIApiKey:
|
||||
body.chatModel?.customOpenAIKey || getCustomOpenaiApiKey(),
|
||||
temperature: 0.7,
|
||||
configuration: {
|
||||
baseURL:
|
||||
body.chatModel?.customOpenAIBaseURL || getCustomOpenaiApiUrl(),
|
||||
},
|
||||
}) as unknown as BaseChatModel;
|
||||
} else if (
|
||||
chatModelProviders[chatModelProvider] &&
|
||||
chatModelProviders[chatModelProvider][chatModel]
|
||||
) {
|
||||
llm = chatModelProviders[chatModelProvider][chatModel]
|
||||
.model as unknown as BaseChatModel | undefined;
|
||||
}
|
||||
|
||||
if (llm instanceof ChatOllama && body.chatModel?.provider === 'ollama') {
|
||||
llm.numCtx = body.chatModel.ollamaContextWindow || 2048;
|
||||
}
|
||||
|
||||
if (
|
||||
embeddingModelProviders[embeddingModelProvider] &&
|
||||
embeddingModelProviders[embeddingModelProvider][embeddingModel]
|
||||
) {
|
||||
embeddings = embeddingModelProviders[embeddingModelProvider][
|
||||
embeddingModel
|
||||
].model as Embeddings | undefined;
|
||||
}
|
||||
|
||||
if (!llm || !embeddings) {
|
||||
return Response.json(
|
||||
{ message: 'Invalid model selected' },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const searchHandler: MetaSearchAgentType = searchHandlers[body.focusMode];
|
||||
|
||||
if (!searchHandler) {
|
||||
return Response.json({ message: 'Invalid focus mode' }, { status: 400 });
|
||||
}
|
||||
const abortController = new AbortController();
|
||||
const { signal } = abortController;
|
||||
|
||||
const emitter = await searchHandler.searchAndAnswer(
|
||||
body.query,
|
||||
@@ -60,6 +135,7 @@ export const POST = async (req: Request) => {
|
||||
body.optimizationMode,
|
||||
[],
|
||||
body.systemInstructions || '',
|
||||
signal,
|
||||
);
|
||||
|
||||
if (!body.stream) {
|
||||
@@ -107,9 +183,6 @@ export const POST = async (req: Request) => {
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const abortController = new AbortController();
|
||||
const { signal } = abortController;
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
let sources: any[] = [];
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
import generateSuggestions from '@/lib/chains/suggestionGeneratorAgent';
|
||||
import ModelRegistry from '@/lib/models/registry';
|
||||
import { ModelWithProvider } from '@/lib/models/types';
|
||||
import {
|
||||
getCustomOpenaiApiKey,
|
||||
getCustomOpenaiApiUrl,
|
||||
getCustomOpenaiModelName,
|
||||
} from '@/lib/config';
|
||||
import { getAvailableChatModelProviders } from '@/lib/providers';
|
||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
|
||||
import { ChatOpenAI } from '@langchain/openai';
|
||||
import { ChatOllama } from '@langchain/ollama';
|
||||
|
||||
interface ChatModel {
|
||||
provider: string;
|
||||
model: string;
|
||||
ollamaContextWindow?: number;
|
||||
}
|
||||
|
||||
interface SuggestionsGenerationBody {
|
||||
chatHistory: any[];
|
||||
chatModel: ModelWithProvider;
|
||||
chatModel?: ChatModel;
|
||||
}
|
||||
|
||||
export const POST = async (req: Request) => {
|
||||
@@ -23,12 +35,39 @@ export const POST = async (req: Request) => {
|
||||
})
|
||||
.filter((msg) => msg !== undefined) as BaseMessage[];
|
||||
|
||||
const registry = new ModelRegistry();
|
||||
const chatModelProviders = await getAvailableChatModelProviders();
|
||||
|
||||
const llm = await registry.loadChatModel(
|
||||
body.chatModel.providerId,
|
||||
body.chatModel.key,
|
||||
);
|
||||
const chatModelProvider =
|
||||
chatModelProviders[
|
||||
body.chatModel?.provider || Object.keys(chatModelProviders)[0]
|
||||
];
|
||||
const chatModel =
|
||||
chatModelProvider[
|
||||
body.chatModel?.model || Object.keys(chatModelProvider)[0]
|
||||
];
|
||||
|
||||
let llm: BaseChatModel | undefined;
|
||||
|
||||
if (body.chatModel?.provider === 'custom_openai') {
|
||||
llm = new ChatOpenAI({
|
||||
openAIApiKey: getCustomOpenaiApiKey(),
|
||||
modelName: getCustomOpenaiModelName(),
|
||||
temperature: 0.7,
|
||||
configuration: {
|
||||
baseURL: getCustomOpenaiApiUrl(),
|
||||
},
|
||||
}) as unknown as BaseChatModel;
|
||||
} else if (chatModelProvider && chatModel) {
|
||||
llm = chatModel.model;
|
||||
// Set context window size for Ollama models
|
||||
if (llm instanceof ChatOllama && body.chatModel?.provider === 'ollama') {
|
||||
llm.numCtx = body.chatModel.ollamaContextWindow || 2048;
|
||||
}
|
||||
}
|
||||
|
||||
if (!llm) {
|
||||
return Response.json({ error: 'Invalid chat model' }, { status: 400 });
|
||||
}
|
||||
|
||||
const suggestions = await generateSuggestions(
|
||||
{
|
||||
|
||||
@@ -2,11 +2,11 @@ import { NextResponse } from 'next/server';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
import { getAvailableEmbeddingModelProviders } from '@/lib/providers';
|
||||
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf';
|
||||
import { DocxLoader } from '@langchain/community/document_loaders/fs/docx';
|
||||
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
|
||||
import { Document } from '@langchain/core/documents';
|
||||
import ModelRegistry from '@/lib/models/registry';
|
||||
import { Document } from 'langchain/document';
|
||||
|
||||
interface FileRes {
|
||||
fileName: string;
|
||||
@@ -30,8 +30,8 @@ export async function POST(req: Request) {
|
||||
const formData = await req.formData();
|
||||
|
||||
const files = formData.getAll('files') as File[];
|
||||
const embedding_model = formData.get('embedding_model_key') as string;
|
||||
const embedding_model_provider = formData.get('embedding_model_provider_id') as string;
|
||||
const embedding_model = formData.get('embedding_model');
|
||||
const embedding_model_provider = formData.get('embedding_model_provider');
|
||||
|
||||
if (!embedding_model || !embedding_model_provider) {
|
||||
return NextResponse.json(
|
||||
@@ -40,9 +40,20 @@ export async function POST(req: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
const registry = new ModelRegistry();
|
||||
const embeddingModels = await getAvailableEmbeddingModelProviders();
|
||||
const provider =
|
||||
embedding_model_provider ?? Object.keys(embeddingModels)[0];
|
||||
const embeddingModel =
|
||||
embedding_model ?? Object.keys(embeddingModels[provider as string])[0];
|
||||
|
||||
const model = await registry.loadEmbeddingModel(embedding_model_provider, embedding_model);
|
||||
let embeddingsModel =
|
||||
embeddingModels[provider as string]?.[embeddingModel as string]?.model;
|
||||
if (!embeddingsModel) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Invalid embedding model selected' },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const processedFiles: FileRes[] = [];
|
||||
|
||||
@@ -87,7 +98,7 @@ export async function POST(req: Request) {
|
||||
}),
|
||||
);
|
||||
|
||||
const embeddings = await model.embedDocuments(
|
||||
const embeddings = await embeddingsModel.embedDocuments(
|
||||
splitted.map((doc) => doc.pageContent),
|
||||
);
|
||||
const embeddingsDataPath = filePath.replace(
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import handleVideoSearch from '@/lib/chains/videoSearchAgent';
|
||||
import ModelRegistry from '@/lib/models/registry';
|
||||
import { ModelWithProvider } from '@/lib/models/types';
|
||||
import {
|
||||
getCustomOpenaiApiKey,
|
||||
getCustomOpenaiApiUrl,
|
||||
getCustomOpenaiModelName,
|
||||
} from '@/lib/config';
|
||||
import { getAvailableChatModelProviders } from '@/lib/providers';
|
||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
|
||||
import { ChatOllama } from '@langchain/ollama';
|
||||
import { ChatOpenAI } from '@langchain/openai';
|
||||
|
||||
interface ChatModel {
|
||||
provider: string;
|
||||
model: string;
|
||||
ollamaContextWindow?: number;
|
||||
}
|
||||
|
||||
interface VideoSearchBody {
|
||||
query: string;
|
||||
chatHistory: any[];
|
||||
chatModel: ModelWithProvider;
|
||||
chatModel?: ChatModel;
|
||||
}
|
||||
|
||||
export const POST = async (req: Request) => {
|
||||
@@ -23,12 +36,39 @@ export const POST = async (req: Request) => {
|
||||
})
|
||||
.filter((msg) => msg !== undefined) as BaseMessage[];
|
||||
|
||||
const registry = new ModelRegistry();
|
||||
const chatModelProviders = await getAvailableChatModelProviders();
|
||||
|
||||
const llm = await registry.loadChatModel(
|
||||
body.chatModel.providerId,
|
||||
body.chatModel.key,
|
||||
);
|
||||
const chatModelProvider =
|
||||
chatModelProviders[
|
||||
body.chatModel?.provider || Object.keys(chatModelProviders)[0]
|
||||
];
|
||||
const chatModel =
|
||||
chatModelProvider[
|
||||
body.chatModel?.model || Object.keys(chatModelProvider)[0]
|
||||
];
|
||||
|
||||
let llm: BaseChatModel | undefined;
|
||||
|
||||
if (body.chatModel?.provider === 'custom_openai') {
|
||||
llm = new ChatOpenAI({
|
||||
openAIApiKey: getCustomOpenaiApiKey(),
|
||||
modelName: getCustomOpenaiModelName(),
|
||||
temperature: 0.7,
|
||||
configuration: {
|
||||
baseURL: getCustomOpenaiApiUrl(),
|
||||
},
|
||||
}) as unknown as BaseChatModel;
|
||||
} else if (chatModelProvider && chatModel) {
|
||||
llm = chatModel.model;
|
||||
// Set context window size for Ollama models
|
||||
if (llm instanceof ChatOllama && body.chatModel?.provider === 'ollama') {
|
||||
llm.numCtx = body.chatModel.ollamaContextWindow || 2048;
|
||||
}
|
||||
}
|
||||
|
||||
if (!llm) {
|
||||
return Response.json({ error: 'Invalid chat model' }, { status: 400 });
|
||||
}
|
||||
|
||||
const videos = await handleVideoSearch(
|
||||
{
|
||||
|
||||
@@ -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}¤t=weather_code,temperature_2m,is_day,relative_humidity_2m,wind_speed_10m&timezone=auto${
|
||||
body.measureUnit === 'Metric' ? '' : '&temperature_unit=fahrenheit'
|
||||
}${body.measureUnit === 'Metric' ? '' : '&wind_speed_unit=mph'}`,
|
||||
);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.error) {
|
||||
console.error(`Error fetching weather data: ${data.reason}`);
|
||||
return Response.json(
|
||||
{
|
||||
message: 'An error has occurred.',
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
const weather: {
|
||||
temperature: number;
|
||||
condition: string;
|
||||
humidity: number;
|
||||
windSpeed: number;
|
||||
icon: string;
|
||||
temperatureUnit: 'C' | 'F';
|
||||
windSpeedUnit: 'm/s' | 'mph';
|
||||
} = {
|
||||
temperature: data.current.temperature_2m,
|
||||
condition: '',
|
||||
humidity: data.current.relative_humidity_2m,
|
||||
windSpeed: data.current.wind_speed_10m,
|
||||
icon: '',
|
||||
temperatureUnit: body.measureUnit === 'Metric' ? 'C' : 'F',
|
||||
windSpeedUnit: body.measureUnit === 'Metric' ? 'm/s' : 'mph',
|
||||
};
|
||||
|
||||
const code = data.current.weather_code;
|
||||
const isDay = data.current.is_day === 1;
|
||||
const dayOrNight = isDay ? 'day' : 'night';
|
||||
|
||||
switch (code) {
|
||||
case 0:
|
||||
weather.icon = `clear-${dayOrNight}`;
|
||||
weather.condition = 'Clear';
|
||||
break;
|
||||
|
||||
case 1:
|
||||
weather.condition = 'Mainly Clear';
|
||||
case 2:
|
||||
weather.condition = 'Partly Cloudy';
|
||||
case 3:
|
||||
weather.icon = `cloudy-1-${dayOrNight}`;
|
||||
weather.condition = 'Cloudy';
|
||||
break;
|
||||
|
||||
case 45:
|
||||
weather.condition = 'Fog';
|
||||
case 48:
|
||||
weather.icon = `fog-${dayOrNight}`;
|
||||
weather.condition = 'Fog';
|
||||
break;
|
||||
|
||||
case 51:
|
||||
weather.condition = 'Light Drizzle';
|
||||
case 53:
|
||||
weather.condition = 'Moderate Drizzle';
|
||||
case 55:
|
||||
weather.icon = `rainy-1-${dayOrNight}`;
|
||||
weather.condition = 'Dense Drizzle';
|
||||
break;
|
||||
|
||||
case 56:
|
||||
weather.condition = 'Light Freezing Drizzle';
|
||||
case 57:
|
||||
weather.icon = `frost-${dayOrNight}`;
|
||||
weather.condition = 'Dense Freezing Drizzle';
|
||||
break;
|
||||
|
||||
case 61:
|
||||
weather.condition = 'Slight Rain';
|
||||
case 63:
|
||||
weather.condition = 'Moderate Rain';
|
||||
case 65:
|
||||
weather.condition = 'Heavy Rain';
|
||||
weather.icon = `rainy-2-${dayOrNight}`;
|
||||
break;
|
||||
|
||||
case 66:
|
||||
weather.condition = 'Light Freezing Rain';
|
||||
case 67:
|
||||
weather.condition = 'Heavy Freezing Rain';
|
||||
weather.icon = 'rain-and-sleet-mix';
|
||||
break;
|
||||
|
||||
case 71:
|
||||
weather.condition = 'Slight Snow Fall';
|
||||
case 73:
|
||||
weather.condition = 'Moderate Snow Fall';
|
||||
case 75:
|
||||
weather.condition = 'Heavy Snow Fall';
|
||||
weather.icon = `snowy-2-${dayOrNight}`;
|
||||
break;
|
||||
|
||||
case 77:
|
||||
weather.condition = 'Snow';
|
||||
weather.icon = `snowy-1-${dayOrNight}`;
|
||||
break;
|
||||
|
||||
case 80:
|
||||
weather.condition = 'Slight Rain Showers';
|
||||
case 81:
|
||||
weather.condition = 'Moderate Rain Showers';
|
||||
case 82:
|
||||
weather.condition = 'Heavy Rain Showers';
|
||||
weather.icon = `rainy-3-${dayOrNight}`;
|
||||
break;
|
||||
|
||||
case 85:
|
||||
weather.condition = 'Slight Snow Showers';
|
||||
case 86:
|
||||
weather.condition = 'Moderate Snow Showers';
|
||||
case 87:
|
||||
weather.condition = 'Heavy Snow Showers';
|
||||
weather.icon = `snowy-3-${dayOrNight}`;
|
||||
break;
|
||||
|
||||
case 95:
|
||||
weather.condition = 'Thunderstorm';
|
||||
weather.icon = `scattered-thunderstorms-${dayOrNight}`;
|
||||
break;
|
||||
|
||||
case 96:
|
||||
weather.condition = 'Thunderstorm with Slight Hail';
|
||||
case 99:
|
||||
weather.condition = 'Thunderstorm with Heavy Hail';
|
||||
weather.icon = 'severe-thunderstorm';
|
||||
break;
|
||||
|
||||
default:
|
||||
weather.icon = `clear-${dayOrNight}`;
|
||||
weather.condition = 'Clear';
|
||||
break;
|
||||
}
|
||||
|
||||
return Response.json(weather);
|
||||
} catch (err) {
|
||||
console.error('An error occurred while getting home widgets', err);
|
||||
return Response.json(
|
||||
{
|
||||
message: 'An error has occurred.',
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,17 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import ChatWindow from '@/components/ChatWindow';
|
||||
import { useParams } from 'next/navigation';
|
||||
import React from 'react';
|
||||
import { ChatProvider } from '@/lib/hooks/useChat';
|
||||
|
||||
const Page = () => {
|
||||
const { chatId }: { chatId: string } = useParams();
|
||||
return (
|
||||
<ChatProvider id={chatId}>
|
||||
<ChatWindow />
|
||||
</ChatProvider>
|
||||
);
|
||||
const Page = ({ params }: { params: Promise<{ chatId: string }> }) => {
|
||||
const { chatId } = React.use(params);
|
||||
return <ChatWindow id={chatId} />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
@@ -1,268 +1,110 @@
|
||||
'use client';
|
||||
|
||||
import { Globe2Icon } from 'lucide-react';
|
||||
import { Search } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import SmallNewsCard from '@/components/Discover/SmallNewsCard';
|
||||
import MajorNewsCard from '@/components/Discover/MajorNewsCard';
|
||||
|
||||
export interface Discover {
|
||||
interface Discover {
|
||||
title: string;
|
||||
content: string;
|
||||
url: string;
|
||||
thumbnail: string;
|
||||
}
|
||||
|
||||
const topics: { key: string; display: string }[] = [
|
||||
{
|
||||
display: 'Tech & Science',
|
||||
key: 'tech',
|
||||
},
|
||||
{
|
||||
display: 'Finance',
|
||||
key: 'finance',
|
||||
},
|
||||
{
|
||||
display: 'Art & Culture',
|
||||
key: 'art',
|
||||
},
|
||||
{
|
||||
display: 'Sports',
|
||||
key: 'sports',
|
||||
},
|
||||
{
|
||||
display: 'Entertainment',
|
||||
key: 'entertainment',
|
||||
},
|
||||
];
|
||||
|
||||
const Page = () => {
|
||||
const [discover, setDiscover] = useState<Discover[] | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTopic, setActiveTopic] = useState<string>(topics[0].key);
|
||||
|
||||
const fetchArticles = async (topic: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/discover?topic=${topic}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
|
||||
data.blogs = data.blogs.filter((blog: Discover) => blog.thumbnail);
|
||||
|
||||
setDiscover(data.blogs);
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching data:', err.message);
|
||||
toast.error('Error fetching data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchArticles(activeTopic);
|
||||
}, [activeTopic]);
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/discover`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
|
||||
data.blogs = data.blogs.filter((blog: Discover) => blog.thumbnail);
|
||||
|
||||
setDiscover(data.blogs);
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching data:', err.message);
|
||||
toast.error('Error fetching data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return loading ? (
|
||||
<div className="flex flex-row items-center justify-center min-h-screen">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="w-8 h-8 text-light-200 fill-light-secondary dark:text-[#202020] animate-spin dark:fill-[#ffffff3b]"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100.003 78.2051 78.1951 100.003 50.5908 100C22.9765 99.9972 0.997224 78.018 1 50.4037C1.00281 22.7993 22.8108 0.997224 50.4251 1C78.0395 1.00281 100.018 22.8108 100 50.4251ZM9.08164 50.594C9.06312 73.3997 27.7909 92.1272 50.5966 92.1457C73.4023 92.1642 92.1298 73.4365 92.1483 50.6308C92.1669 27.8251 73.4392 9.0973 50.6335 9.07878C27.8278 9.06026 9.10003 27.787 9.08164 50.594Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4037 97.8624 35.9116 96.9801 33.5533C95.1945 28.8227 92.871 24.3692 90.0681 20.348C85.6237 14.1775 79.4473 9.36872 72.0454 6.45794C64.6435 3.54717 56.3134 2.65431 48.3133 3.89319C45.869 4.27179 44.3768 6.77534 45.014 9.20079C45.6512 11.6262 48.1343 13.0956 50.5786 12.717C56.5073 11.8281 62.5542 12.5399 68.0406 14.7911C73.527 17.0422 78.2187 20.7487 81.5841 25.4923C83.7976 28.5886 85.4467 32.059 86.4416 35.7474C87.1273 38.1189 89.5423 39.6781 91.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex flex-col pt-10 border-b border-light-200/20 dark:border-dark-200/20 pb-6 px-2">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
||||
<div className="flex items-center justify-center">
|
||||
<Globe2Icon size={45} className="mb-2.5" />
|
||||
<h1
|
||||
className="text-5xl font-normal p-2"
|
||||
style={{ fontFamily: 'PP Editorial, serif' }}
|
||||
>
|
||||
Discover
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex flex-row items-center space-x-2 overflow-x-auto">
|
||||
{topics.map((t, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn(
|
||||
'border-[0.1px] rounded-full text-sm px-3 py-1 text-nowrap transition duration-200 cursor-pointer',
|
||||
activeTopic === t.key
|
||||
? 'text-cyan-700 dark:text-cyan-300 bg-cyan-300/20 border-cyan-700/60 dar:bg-cyan-300/30 dark:border-cyan-300/40'
|
||||
: 'border-black/30 dark:border-white/30 text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white hover:border-black/40 dark:hover:border-white/40 hover:bg-black/5 dark:hover:bg-white/5',
|
||||
)}
|
||||
onClick={() => setActiveTopic(t.key)}
|
||||
>
|
||||
<span>{t.display}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div 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>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex flex-row items-center justify-center min-h-screen">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="w-8 h-8 text-light-200 fill-light-secondary dark:text-[#202020] animate-spin dark:fill-[#ffffff3b]"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100.003 78.2051 78.1951 100.003 50.5908 100C22.9765 99.9972 0.997224 78.018 1 50.4037C1.00281 22.7993 22.8108 0.997224 50.4251 1C78.0395 1.00281 100.018 22.8108 100 50.4251ZM9.08164 50.594C9.06312 73.3997 27.7909 92.1272 50.5966 92.1457C73.4023 92.1642 92.1298 73.4365 92.1483 50.6308C92.1669 27.8251 73.4392 9.0973 50.6335 9.07878C27.8278 9.06026 9.10003 27.787 9.08164 50.594Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4037 97.8624 35.9116 96.9801 33.5533C95.1945 28.8227 92.871 24.3692 90.0681 20.348C85.6237 14.1775 79.4473 9.36872 72.0454 6.45794C64.6435 3.54717 56.3134 2.65431 48.3133 3.89319C45.869 4.27179 44.3768 6.77534 45.014 9.20079C45.6512 11.6262 48.1343 13.0956 50.5786 12.717C56.5073 11.8281 62.5542 12.5399 68.0406 14.7911C73.527 17.0422 78.2187 20.7487 81.5841 25.4923C83.7976 28.5886 85.4467 32.059 86.4416 35.7474C87.1273 38.1189 89.5423 39.6781 91.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 pb-28 pt-5 lg:pb-8 w-full">
|
||||
<div className="block lg:hidden">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{discover?.map((item, i) => (
|
||||
<SmallNewsCard key={`mobile-${i}`} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:block">
|
||||
{discover &&
|
||||
discover.length > 0 &&
|
||||
(() => {
|
||||
const sections = [];
|
||||
let index = 0;
|
||||
|
||||
while (index < discover.length) {
|
||||
if (sections.length > 0) {
|
||||
sections.push(
|
||||
<hr
|
||||
key={`sep-${index}`}
|
||||
className="border-t border-light-200/20 dark:border-dark-200/20 my-3 w-full"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (index < discover.length) {
|
||||
sections.push(
|
||||
<MajorNewsCard
|
||||
key={`major-${index}`}
|
||||
item={discover[index]}
|
||||
isLeft={false}
|
||||
/>,
|
||||
);
|
||||
index++;
|
||||
}
|
||||
|
||||
if (index < discover.length) {
|
||||
sections.push(
|
||||
<hr
|
||||
key={`sep-${index}-after`}
|
||||
className="border-t border-light-200/20 dark:border-dark-200/20 my-3 w-full"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (index < discover.length) {
|
||||
const smallCards = discover.slice(index, index + 3);
|
||||
sections.push(
|
||||
<div
|
||||
key={`small-group-${index}`}
|
||||
className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-4"
|
||||
>
|
||||
{smallCards.map((item, i) => (
|
||||
<SmallNewsCard
|
||||
key={`small-${index + i}`}
|
||||
item={item}
|
||||
/>
|
||||
))}
|
||||
</div>,
|
||||
);
|
||||
index += 3;
|
||||
}
|
||||
|
||||
if (index < discover.length) {
|
||||
sections.push(
|
||||
<hr
|
||||
key={`sep-${index}-after-small`}
|
||||
className="border-t border-light-200/20 dark:border-dark-200/20 my-3 w-full"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (index < discover.length - 1) {
|
||||
const twoMajorCards = discover.slice(index, index + 2);
|
||||
twoMajorCards.forEach((item, i) => {
|
||||
sections.push(
|
||||
<MajorNewsCard
|
||||
key={`double-${index + i}`}
|
||||
item={item}
|
||||
isLeft={i === 0}
|
||||
/>,
|
||||
);
|
||||
if (i === 0) {
|
||||
sections.push(
|
||||
<hr
|
||||
key={`sep-double-${index + i}`}
|
||||
className="border-t border-light-200/20 dark:border-dark-200/20 my-3 w-full"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
});
|
||||
index += 2;
|
||||
} else if (index < discover.length) {
|
||||
sections.push(
|
||||
<MajorNewsCard
|
||||
key={`final-major-${index}`}
|
||||
item={discover[index]}
|
||||
isLeft={true}
|
||||
/>,
|
||||
);
|
||||
index++;
|
||||
}
|
||||
|
||||
if (index < discover.length) {
|
||||
sections.push(
|
||||
<hr
|
||||
key={`sep-${index}-after-major`}
|
||||
className="border-t border-light-200/20 dark:border-dark-200/20 my-3 w-full"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (index < discover.length) {
|
||||
const smallCards = discover.slice(index, index + 3);
|
||||
sections.push(
|
||||
<div
|
||||
key={`small-group-2-${index}`}
|
||||
className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-4"
|
||||
>
|
||||
{smallCards.map((item, i) => (
|
||||
<SmallNewsCard
|
||||
key={`small-2-${index + i}`}
|
||||
item={item}
|
||||
/>
|
||||
))}
|
||||
</div>,
|
||||
);
|
||||
index += 3;
|
||||
}
|
||||
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-4 pb-28 lg:pb-8 w-full justify-items-center lg:justify-items-start">
|
||||
{discover &&
|
||||
discover?.map((item, i) => (
|
||||
<Link
|
||||
href={`/?q=Summary: ${item.url}`}
|
||||
key={i}
|
||||
className="max-w-sm rounded-lg overflow-hidden bg-light-secondary dark:bg-dark-secondary hover:-translate-y-[1px] transition duration-200"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
className="object-cover w-full aspect-video"
|
||||
src={
|
||||
new URL(item.thumbnail).origin +
|
||||
new URL(item.thumbnail).pathname +
|
||||
`?id=${new URL(item.thumbnail).searchParams.get('id')}`
|
||||
}
|
||||
|
||||
return sections;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
alt={item.title}
|
||||
/>
|
||||
<div className="px-6 py-4">
|
||||
<div className="font-bold text-lg mb-2">
|
||||
{item.title.slice(0, 100)}...
|
||||
</div>
|
||||
<p className="text-black-70 dark:text-white/70 text-sm">
|
||||
{item.content.slice(0, 100)}...
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,14 +2,6 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: 'PP Editorial';
|
||||
src: url('/fonts/pp-ed-ul.otf') format('opentype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
.overflow-hidden-scrollable {
|
||||
-ms-overflow-style: none;
|
||||
@@ -18,82 +10,4 @@
|
||||
.overflow-hidden-scrollable::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #e8edf1 transparent; /* light-200 */
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: #e8edf1; /* light-200 */
|
||||
border-radius: 3px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: #d0d7de; /* light-300 */
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
* {
|
||||
scrollbar-color: #21262d transparent; /* dark-200 */
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: #21262d; /* dark-200 */
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: #30363d; /* dark-300 */
|
||||
}
|
||||
}
|
||||
|
||||
:root.dark *,
|
||||
html.dark *,
|
||||
body.dark * {
|
||||
scrollbar-color: #21262d transparent; /* dark-200 */
|
||||
}
|
||||
|
||||
:root.dark *::-webkit-scrollbar-thumb,
|
||||
html.dark *::-webkit-scrollbar-thumb,
|
||||
body.dark *::-webkit-scrollbar-thumb {
|
||||
background: #21262d; /* dark-200 */
|
||||
}
|
||||
|
||||
:root.dark *::-webkit-scrollbar-thumb:hover,
|
||||
html.dark *::-webkit-scrollbar-thumb:hover,
|
||||
body.dark *::-webkit-scrollbar-thumb:hover {
|
||||
background: #30363d; /* dark-300 */
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
||||
select,
|
||||
textarea,
|
||||
input {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import type { Metadata } from 'next';
|
||||
import { Montserrat } from 'next/font/google';
|
||||
import './globals.css';
|
||||
@@ -7,8 +5,6 @@ import { cn } from '@/lib/utils';
|
||||
import Sidebar from '@/components/Sidebar';
|
||||
import { Toaster } from 'sonner';
|
||||
import ThemeProvider from '@/components/theme/Provider';
|
||||
import configManager from '@/lib/config';
|
||||
import SetupWizard from '@/components/Setup/SetupWizard';
|
||||
|
||||
const montserrat = Montserrat({
|
||||
weight: ['300', '400', '500', '700'],
|
||||
@@ -28,29 +24,28 @@ export default function RootLayout({
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const setupComplete = configManager.isSetupComplete();
|
||||
const configSections = configManager.getUIConfigSections();
|
||||
|
||||
return (
|
||||
<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)}>
|
||||
<ThemeProvider>
|
||||
{setupComplete ? (
|
||||
<>
|
||||
<Sidebar>{children}</Sidebar>
|
||||
<Toaster
|
||||
toastOptions={{
|
||||
unstyled: true,
|
||||
classNames: {
|
||||
toast:
|
||||
'bg-light-secondary dark:bg-dark-secondary dark:text-white/70 text-black-70 rounded-lg p-4 flex flex-row items-center space-x-2',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<SetupWizard configSections={configSections} />
|
||||
)}
|
||||
<Sidebar>{children}</Sidebar>
|
||||
<Toaster
|
||||
toastOptions={{
|
||||
unstyled: true,
|
||||
classNames: {
|
||||
toast:
|
||||
'bg-light-primary dark:bg-dark-secondary dark:text-white/70 text-black-70 rounded-lg p-4 flex flex-row items-center space-x-2',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import ChatWindow from '@/components/ChatWindow';
|
||||
import { ChatProvider } from '@/lib/hooks/useChat';
|
||||
import { Metadata } from 'next';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
@@ -12,9 +11,7 @@ const Home = () => {
|
||||
return (
|
||||
<div>
|
||||
<Suspense>
|
||||
<ChatProvider>
|
||||
<ChatWindow />
|
||||
</ChatProvider>
|
||||
<ChatWindow />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
|
||||
932
src/app/settings/page.tsx
Normal file
@@ -0,0 +1,932 @@
|
||||
'use client';
|
||||
|
||||
import { Settings as SettingsIcon, ArrowLeft, Loader2 } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Switch } from '@headlessui/react';
|
||||
import ThemeSwitcher from '@/components/theme/Switcher';
|
||||
import { ImagesIcon, VideoIcon, Layers3 } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { PROVIDER_METADATA } from '@/lib/providers';
|
||||
|
||||
interface SettingsType {
|
||||
chatModelProviders: {
|
||||
[key: string]: [Record<string, any>];
|
||||
};
|
||||
embeddingModelProviders: {
|
||||
[key: string]: [Record<string, any>];
|
||||
};
|
||||
openaiApiKey: string;
|
||||
groqApiKey: string;
|
||||
anthropicApiKey: string;
|
||||
geminiApiKey: string;
|
||||
ollamaApiUrl: string;
|
||||
lmStudioApiUrl: string;
|
||||
deepseekApiKey: string;
|
||||
customOpenaiApiKey: string;
|
||||
customOpenaiApiUrl: string;
|
||||
customOpenaiModelName: string;
|
||||
ollamaContextWindow: number;
|
||||
}
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
isSaving?: boolean;
|
||||
onSave?: (value: string) => void;
|
||||
}
|
||||
|
||||
const Input = ({ className, isSaving, onSave, ...restProps }: InputProps) => {
|
||||
return (
|
||||
<div className="relative">
|
||||
<input
|
||||
{...restProps}
|
||||
className={cn(
|
||||
'bg-light-secondary dark:bg-dark-secondary w-full px-3 py-2 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 dark:text-white rounded-lg text-sm',
|
||||
isSaving && 'pr-10',
|
||||
className,
|
||||
)}
|
||||
onBlur={(e) => onSave?.(e.target.value)}
|
||||
/>
|
||||
{isSaving && (
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
||||
<Loader2
|
||||
size={16}
|
||||
className="animate-spin text-black/70 dark:text-white/70"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface TextareaProps extends React.InputHTMLAttributes<HTMLTextAreaElement> {
|
||||
isSaving?: boolean;
|
||||
onSave?: (value: string) => void;
|
||||
}
|
||||
|
||||
const Textarea = ({
|
||||
className,
|
||||
isSaving,
|
||||
onSave,
|
||||
...restProps
|
||||
}: TextareaProps) => {
|
||||
return (
|
||||
<div className="relative">
|
||||
<textarea
|
||||
placeholder="Any special instructions for the LLM"
|
||||
className="placeholder:text-sm text-sm w-full flex items-center justify-between p-3 bg-light-secondary dark:bg-dark-secondary rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition-colors"
|
||||
rows={4}
|
||||
onBlur={(e) => onSave?.(e.target.value)}
|
||||
{...restProps}
|
||||
/>
|
||||
{isSaving && (
|
||||
<div className="absolute right-3 top-3">
|
||||
<Loader2
|
||||
size={16}
|
||||
className="animate-spin text-black/70 dark:text-white/70"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Select = ({
|
||||
className,
|
||||
options,
|
||||
...restProps
|
||||
}: React.SelectHTMLAttributes<HTMLSelectElement> & {
|
||||
options: { value: string; label: string; disabled?: boolean }[];
|
||||
}) => {
|
||||
return (
|
||||
<select
|
||||
{...restProps}
|
||||
className={cn(
|
||||
'bg-light-secondary dark:bg-dark-secondary px-3 py-2 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 dark:text-white rounded-lg text-sm',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{options.map(({ label, value, disabled }) => (
|
||||
<option key={value} value={value} disabled={disabled}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
const SettingsSection = ({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<div className="flex flex-col space-y-4 p-4 bg-light-secondary/50 dark:bg-dark-secondary/50 rounded-xl border border-light-200 dark:border-dark-200">
|
||||
<h2 className="text-black/90 dark:text-white/90 font-medium">{title}</h2>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const Page = () => {
|
||||
const [config, setConfig] = useState<SettingsType | null>(null);
|
||||
const [chatModels, setChatModels] = useState<Record<string, any>>({});
|
||||
const [embeddingModels, setEmbeddingModels] = useState<Record<string, any>>(
|
||||
{},
|
||||
);
|
||||
const [selectedChatModelProvider, setSelectedChatModelProvider] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [selectedChatModel, setSelectedChatModel] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [selectedEmbeddingModelProvider, setSelectedEmbeddingModelProvider] =
|
||||
useState<string | null>(null);
|
||||
const [selectedEmbeddingModel, setSelectedEmbeddingModel] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [automaticSuggestions, setAutomaticSuggestions] = useState(true);
|
||||
const [systemInstructions, setSystemInstructions] = useState<string>('');
|
||||
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(() => {
|
||||
const fetchConfig = async () => {
|
||||
setIsLoading(true);
|
||||
const res = await fetch(`/api/config`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const data = (await res.json()) as SettingsType;
|
||||
|
||||
setConfig(data);
|
||||
|
||||
const chatModelProvidersKeys = Object.keys(data.chatModelProviders || {});
|
||||
const embeddingModelProvidersKeys = Object.keys(
|
||||
data.embeddingModelProviders || {},
|
||||
);
|
||||
|
||||
const defaultChatModelProvider =
|
||||
chatModelProvidersKeys.length > 0 ? chatModelProvidersKeys[0] : '';
|
||||
const defaultEmbeddingModelProvider =
|
||||
embeddingModelProvidersKeys.length > 0
|
||||
? embeddingModelProvidersKeys[0]
|
||||
: '';
|
||||
|
||||
const chatModelProvider =
|
||||
localStorage.getItem('chatModelProvider') ||
|
||||
defaultChatModelProvider ||
|
||||
'';
|
||||
const chatModel =
|
||||
localStorage.getItem('chatModel') ||
|
||||
(data.chatModelProviders &&
|
||||
data.chatModelProviders[chatModelProvider]?.length > 0
|
||||
? data.chatModelProviders[chatModelProvider][0].name
|
||||
: undefined) ||
|
||||
'';
|
||||
const embeddingModelProvider =
|
||||
localStorage.getItem('embeddingModelProvider') ||
|
||||
defaultEmbeddingModelProvider ||
|
||||
'';
|
||||
const embeddingModel =
|
||||
localStorage.getItem('embeddingModel') ||
|
||||
(data.embeddingModelProviders &&
|
||||
data.embeddingModelProviders[embeddingModelProvider]?.[0].name) ||
|
||||
'';
|
||||
|
||||
setSelectedChatModelProvider(chatModelProvider);
|
||||
setSelectedChatModel(chatModel);
|
||||
setSelectedEmbeddingModelProvider(embeddingModelProvider);
|
||||
setSelectedEmbeddingModel(embeddingModel);
|
||||
setChatModels(data.chatModelProviders || {});
|
||||
setEmbeddingModels(data.embeddingModelProviders || {});
|
||||
|
||||
setAutomaticSuggestions(
|
||||
localStorage.getItem('autoSuggestions') !== 'false', // default to true if not set
|
||||
);
|
||||
const storedContextWindow = parseInt(
|
||||
localStorage.getItem('ollamaContextWindow') ?? '2048',
|
||||
);
|
||||
setContextWindowSize(storedContextWindow);
|
||||
setIsCustomContextWindow(
|
||||
!predefinedContextSizes.includes(storedContextWindow),
|
||||
);
|
||||
|
||||
setSystemInstructions(localStorage.getItem('systemInstructions')!);
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
fetchConfig();
|
||||
}, []);
|
||||
|
||||
const saveConfig = async (key: string, value: any) => {
|
||||
setSavingStates((prev) => ({ ...prev, [key]: true }));
|
||||
|
||||
try {
|
||||
const updatedConfig = {
|
||||
...config,
|
||||
[key]: value,
|
||||
} as SettingsType;
|
||||
|
||||
const response = await fetch(`/api/config`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(updatedConfig),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update config');
|
||||
}
|
||||
|
||||
setConfig(updatedConfig);
|
||||
|
||||
if (
|
||||
key.toLowerCase().includes('api') ||
|
||||
key.toLowerCase().includes('url')
|
||||
) {
|
||||
const res = await fetch(`/api/config`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch updated config');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
setChatModels(data.chatModelProviders || {});
|
||||
setEmbeddingModels(data.embeddingModelProviders || {});
|
||||
|
||||
const currentChatProvider = selectedChatModelProvider;
|
||||
const newChatProviders = Object.keys(data.chatModelProviders || {});
|
||||
|
||||
if (!currentChatProvider && newChatProviders.length > 0) {
|
||||
const firstProvider = newChatProviders[0];
|
||||
const firstModel = data.chatModelProviders[firstProvider]?.[0]?.name;
|
||||
|
||||
if (firstModel) {
|
||||
setSelectedChatModelProvider(firstProvider);
|
||||
setSelectedChatModel(firstModel);
|
||||
localStorage.setItem('chatModelProvider', firstProvider);
|
||||
localStorage.setItem('chatModel', firstModel);
|
||||
}
|
||||
} else if (
|
||||
currentChatProvider &&
|
||||
(!data.chatModelProviders ||
|
||||
!data.chatModelProviders[currentChatProvider] ||
|
||||
!Array.isArray(data.chatModelProviders[currentChatProvider]) ||
|
||||
data.chatModelProviders[currentChatProvider].length === 0)
|
||||
) {
|
||||
const firstValidProvider = Object.entries(
|
||||
data.chatModelProviders || {},
|
||||
).find(
|
||||
([_, models]) => Array.isArray(models) && models.length > 0,
|
||||
)?.[0];
|
||||
|
||||
if (firstValidProvider) {
|
||||
setSelectedChatModelProvider(firstValidProvider);
|
||||
setSelectedChatModel(
|
||||
data.chatModelProviders[firstValidProvider][0].name,
|
||||
);
|
||||
localStorage.setItem('chatModelProvider', firstValidProvider);
|
||||
localStorage.setItem(
|
||||
'chatModel',
|
||||
data.chatModelProviders[firstValidProvider][0].name,
|
||||
);
|
||||
} else {
|
||||
setSelectedChatModelProvider(null);
|
||||
setSelectedChatModel(null);
|
||||
localStorage.removeItem('chatModelProvider');
|
||||
localStorage.removeItem('chatModel');
|
||||
}
|
||||
}
|
||||
|
||||
const currentEmbeddingProvider = selectedEmbeddingModelProvider;
|
||||
const newEmbeddingProviders = Object.keys(
|
||||
data.embeddingModelProviders || {},
|
||||
);
|
||||
|
||||
if (!currentEmbeddingProvider && newEmbeddingProviders.length > 0) {
|
||||
const firstProvider = newEmbeddingProviders[0];
|
||||
const firstModel =
|
||||
data.embeddingModelProviders[firstProvider]?.[0]?.name;
|
||||
|
||||
if (firstModel) {
|
||||
setSelectedEmbeddingModelProvider(firstProvider);
|
||||
setSelectedEmbeddingModel(firstModel);
|
||||
localStorage.setItem('embeddingModelProvider', firstProvider);
|
||||
localStorage.setItem('embeddingModel', firstModel);
|
||||
}
|
||||
} else if (
|
||||
currentEmbeddingProvider &&
|
||||
(!data.embeddingModelProviders ||
|
||||
!data.embeddingModelProviders[currentEmbeddingProvider] ||
|
||||
!Array.isArray(
|
||||
data.embeddingModelProviders[currentEmbeddingProvider],
|
||||
) ||
|
||||
data.embeddingModelProviders[currentEmbeddingProvider].length === 0)
|
||||
) {
|
||||
const firstValidProvider = Object.entries(
|
||||
data.embeddingModelProviders || {},
|
||||
).find(
|
||||
([_, models]) => Array.isArray(models) && models.length > 0,
|
||||
)?.[0];
|
||||
|
||||
if (firstValidProvider) {
|
||||
setSelectedEmbeddingModelProvider(firstValidProvider);
|
||||
setSelectedEmbeddingModel(
|
||||
data.embeddingModelProviders[firstValidProvider][0].name,
|
||||
);
|
||||
localStorage.setItem('embeddingModelProvider', firstValidProvider);
|
||||
localStorage.setItem(
|
||||
'embeddingModel',
|
||||
data.embeddingModelProviders[firstValidProvider][0].name,
|
||||
);
|
||||
} else {
|
||||
setSelectedEmbeddingModelProvider(null);
|
||||
setSelectedEmbeddingModel(null);
|
||||
localStorage.removeItem('embeddingModelProvider');
|
||||
localStorage.removeItem('embeddingModel');
|
||||
}
|
||||
}
|
||||
|
||||
setConfig(data);
|
||||
}
|
||||
|
||||
if (key === 'automaticSuggestions') {
|
||||
localStorage.setItem('autoSuggestions', value.toString());
|
||||
} else if (key === 'chatModelProvider') {
|
||||
localStorage.setItem('chatModelProvider', value);
|
||||
} else if (key === 'chatModel') {
|
||||
localStorage.setItem('chatModel', value);
|
||||
} else if (key === 'embeddingModelProvider') {
|
||||
localStorage.setItem('embeddingModelProvider', value);
|
||||
} else if (key === 'embeddingModel') {
|
||||
localStorage.setItem('embeddingModel', value);
|
||||
} else if (key === 'ollamaContextWindow') {
|
||||
localStorage.setItem('ollamaContextWindow', value.toString());
|
||||
} else if (key === 'systemInstructions') {
|
||||
localStorage.setItem('systemInstructions', value);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to save:', err);
|
||||
setConfig((prev) => ({ ...prev! }));
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
setSavingStates((prev) => ({ ...prev, [key]: false }));
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="flex flex-col pt-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link href="/" className="lg:hidden">
|
||||
<ArrowLeft className="text-black/70 dark:text-white/70" />
|
||||
</Link>
|
||||
<div className="flex flex-row space-x-0.5 items-center">
|
||||
<SettingsIcon size={23} />
|
||||
<h1 className="text-3xl font-medium p-2">Settings</h1>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row items-center justify-center min-h-[50vh]">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="w-8 h-8 text-light-200 fill-light-secondary dark:text-[#202020] animate-spin dark:fill-[#ffffff3b]"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100.003 78.2051 78.1951 100.003 50.5908 100C22.9765 99.9972 0.997224 78.018 1 50.4037C1.00281 22.7993 22.8108 0.997224 50.4251 1C78.0395 1.00281 100.018 22.8108 100 50.4251ZM9.08164 50.594C9.06312 73.3997 27.7909 92.1272 50.5966 92.1457C73.4023 92.1642 92.1298 73.4365 92.1483 50.6308C92.1669 27.8251 73.4392 9.0973 50.6335 9.07878C27.8278 9.06026 9.10003 27.787 9.08164 50.594Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4037 97.8624 35.9116 96.9801 33.5533C95.1945 28.8227 92.871 24.3692 90.0681 20.348C85.6237 14.1775 79.4473 9.36872 72.0454 6.45794C64.6435 3.54717 56.3134 2.65431 48.3133 3.89319C45.869 4.27179 44.3768 6.77534 45.014 9.20079C45.6512 11.6262 48.1343 13.0956 50.5786 12.717C56.5073 11.8281 62.5542 12.5399 68.0406 14.7911C73.527 17.0422 78.2187 20.7487 81.5841 25.4923C83.7976 28.5886 85.4467 32.059 86.4416 35.7474C87.1273 38.1189 89.5423 39.6781 91.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
config && (
|
||||
<div className="flex flex-col space-y-6 pb-28 lg:pb-8">
|
||||
<SettingsSection title="Appearance">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Theme
|
||||
</p>
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title="Automatic Search">
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex items-center justify-between p-3 bg-light-secondary dark:bg-dark-secondary rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition-colors">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 bg-light-200 dark:bg-dark-200 rounded-lg">
|
||||
<Layers3
|
||||
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 Suggestions
|
||||
</p>
|
||||
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
|
||||
Automatically show related suggestions after responses
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={automaticSuggestions}
|
||||
onChange={(checked) => {
|
||||
setAutomaticSuggestions(checked);
|
||||
saveConfig('automaticSuggestions', checked);
|
||||
}}
|
||||
className={cn(
|
||||
automaticSuggestions
|
||||
? '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(
|
||||
automaticSuggestions
|
||||
? 'translate-x-6'
|
||||
: 'translate-x-1',
|
||||
'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title="System Instructions">
|
||||
<div className="flex flex-col space-y-4">
|
||||
<Textarea
|
||||
value={systemInstructions}
|
||||
isSaving={savingStates['systemInstructions']}
|
||||
onChange={(e) => {
|
||||
setSystemInstructions(e.target.value);
|
||||
}}
|
||||
onSave={(value) => saveConfig('systemInstructions', value)}
|
||||
/>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title="Model Settings">
|
||||
{config.chatModelProviders && (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Chat Model Provider
|
||||
</p>
|
||||
<Select
|
||||
value={selectedChatModelProvider ?? undefined}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSelectedChatModelProvider(value);
|
||||
saveConfig('chatModelProvider', value);
|
||||
const firstModel =
|
||||
config.chatModelProviders[value]?.[0]?.name;
|
||||
if (firstModel) {
|
||||
setSelectedChatModel(firstModel);
|
||||
saveConfig('chatModel', firstModel);
|
||||
}
|
||||
}}
|
||||
options={Object.keys(config.chatModelProviders).map(
|
||||
(provider) => ({
|
||||
value: provider,
|
||||
label:
|
||||
(PROVIDER_METADATA as any)[provider]?.displayName ||
|
||||
provider.charAt(0).toUpperCase() +
|
||||
provider.slice(1),
|
||||
}),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedChatModelProvider &&
|
||||
selectedChatModelProvider != 'custom_openai' && (
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Chat Model
|
||||
</p>
|
||||
<Select
|
||||
value={selectedChatModel ?? undefined}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSelectedChatModel(value);
|
||||
saveConfig('chatModel', value);
|
||||
}}
|
||||
options={(() => {
|
||||
const chatModelProvider =
|
||||
config.chatModelProviders[
|
||||
selectedChatModelProvider
|
||||
];
|
||||
return chatModelProvider
|
||||
? chatModelProvider.length > 0
|
||||
? chatModelProvider.map((model) => ({
|
||||
value: model.name,
|
||||
label: model.displayName,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
value: '',
|
||||
label: 'No models available',
|
||||
disabled: true,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
value: '',
|
||||
label:
|
||||
'Invalid provider, please check backend logs',
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
})()}
|
||||
/>
|
||||
{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>
|
||||
)}
|
||||
|
||||
{selectedChatModelProvider &&
|
||||
selectedChatModelProvider === 'custom_openai' && (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Model Name
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Model name"
|
||||
value={config.customOpenaiModelName}
|
||||
isSaving={savingStates['customOpenaiModelName']}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setConfig((prev) => ({
|
||||
...prev!,
|
||||
customOpenaiModelName: e.target.value,
|
||||
}));
|
||||
}}
|
||||
onSave={(value) =>
|
||||
saveConfig('customOpenaiModelName', value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Custom OpenAI API Key
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Custom OpenAI API Key"
|
||||
value={config.customOpenaiApiKey}
|
||||
isSaving={savingStates['customOpenaiApiKey']}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setConfig((prev) => ({
|
||||
...prev!,
|
||||
customOpenaiApiKey: e.target.value,
|
||||
}));
|
||||
}}
|
||||
onSave={(value) =>
|
||||
saveConfig('customOpenaiApiKey', value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Custom OpenAI Base URL
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Custom OpenAI Base URL"
|
||||
value={config.customOpenaiApiUrl}
|
||||
isSaving={savingStates['customOpenaiApiUrl']}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setConfig((prev) => ({
|
||||
...prev!,
|
||||
customOpenaiApiUrl: e.target.value,
|
||||
}));
|
||||
}}
|
||||
onSave={(value) =>
|
||||
saveConfig('customOpenaiApiUrl', value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{config.embeddingModelProviders && (
|
||||
<div className="flex flex-col space-y-4 mt-4 pt-4 border-t border-light-200 dark:border-dark-200">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Embedding Model Provider
|
||||
</p>
|
||||
<Select
|
||||
value={selectedEmbeddingModelProvider ?? undefined}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSelectedEmbeddingModelProvider(value);
|
||||
saveConfig('embeddingModelProvider', value);
|
||||
const firstModel =
|
||||
config.embeddingModelProviders[value]?.[0]?.name;
|
||||
if (firstModel) {
|
||||
setSelectedEmbeddingModel(firstModel);
|
||||
saveConfig('embeddingModel', firstModel);
|
||||
}
|
||||
}}
|
||||
options={Object.keys(config.embeddingModelProviders).map(
|
||||
(provider) => ({
|
||||
value: provider,
|
||||
label:
|
||||
(PROVIDER_METADATA as any)[provider]?.displayName ||
|
||||
provider.charAt(0).toUpperCase() +
|
||||
provider.slice(1),
|
||||
}),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedEmbeddingModelProvider && (
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Embedding Model
|
||||
</p>
|
||||
<Select
|
||||
value={selectedEmbeddingModel ?? undefined}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSelectedEmbeddingModel(value);
|
||||
saveConfig('embeddingModel', value);
|
||||
}}
|
||||
options={(() => {
|
||||
const embeddingModelProvider =
|
||||
config.embeddingModelProviders[
|
||||
selectedEmbeddingModelProvider
|
||||
];
|
||||
return embeddingModelProvider
|
||||
? embeddingModelProvider.length > 0
|
||||
? embeddingModelProvider.map((model) => ({
|
||||
value: model.name,
|
||||
label: model.displayName,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
value: '',
|
||||
label: 'No models available',
|
||||
disabled: true,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
value: '',
|
||||
label:
|
||||
'Invalid provider, please check backend logs',
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
})()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title="API Keys">
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
OpenAI API Key
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="OpenAI API Key"
|
||||
value={config.openaiApiKey}
|
||||
isSaving={savingStates['openaiApiKey']}
|
||||
onChange={(e) => {
|
||||
setConfig((prev) => ({
|
||||
...prev!,
|
||||
openaiApiKey: e.target.value,
|
||||
}));
|
||||
}}
|
||||
onSave={(value) => saveConfig('openaiApiKey', value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Ollama API URL
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Ollama API URL"
|
||||
value={config.ollamaApiUrl}
|
||||
isSaving={savingStates['ollamaApiUrl']}
|
||||
onChange={(e) => {
|
||||
setConfig((prev) => ({
|
||||
...prev!,
|
||||
ollamaApiUrl: e.target.value,
|
||||
}));
|
||||
}}
|
||||
onSave={(value) => saveConfig('ollamaApiUrl', value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
GROQ API Key
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="GROQ API Key"
|
||||
value={config.groqApiKey}
|
||||
isSaving={savingStates['groqApiKey']}
|
||||
onChange={(e) => {
|
||||
setConfig((prev) => ({
|
||||
...prev!,
|
||||
groqApiKey: e.target.value,
|
||||
}));
|
||||
}}
|
||||
onSave={(value) => saveConfig('groqApiKey', value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Anthropic API Key
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Anthropic API key"
|
||||
value={config.anthropicApiKey}
|
||||
isSaving={savingStates['anthropicApiKey']}
|
||||
onChange={(e) => {
|
||||
setConfig((prev) => ({
|
||||
...prev!,
|
||||
anthropicApiKey: e.target.value,
|
||||
}));
|
||||
}}
|
||||
onSave={(value) => saveConfig('anthropicApiKey', value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Gemini API Key
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Gemini API key"
|
||||
value={config.geminiApiKey}
|
||||
isSaving={savingStates['geminiApiKey']}
|
||||
onChange={(e) => {
|
||||
setConfig((prev) => ({
|
||||
...prev!,
|
||||
geminiApiKey: e.target.value,
|
||||
}));
|
||||
}}
|
||||
onSave={(value) => saveConfig('geminiApiKey', value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Deepseek API Key
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Deepseek API Key"
|
||||
value={config.deepseekApiKey}
|
||||
isSaving={savingStates['deepseekApiKey']}
|
||||
onChange={(e) => {
|
||||
setConfig((prev) => ({
|
||||
...prev!,
|
||||
deepseekApiKey: e.target.value,
|
||||
}));
|
||||
}}
|
||||
onSave={(value) => saveConfig('deepseekApiKey', value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
LM Studio API URL
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="LM Studio API URL"
|
||||
value={config.lmStudioApiUrl}
|
||||
isSaving={savingStates['lmStudioApiUrl']}
|
||||
onChange={(e) => {
|
||||
setConfig((prev) => ({
|
||||
...prev!,
|
||||
lmStudioApiUrl: e.target.value,
|
||||
}));
|
||||
}}
|
||||
onSave={(value) => saveConfig('lmStudioApiUrl', value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -1,86 +1,273 @@
|
||||
'use client';
|
||||
|
||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||
import MessageInput from './MessageInput';
|
||||
import { File, Message } from './ChatWindow';
|
||||
import MessageBox from './MessageBox';
|
||||
import MessageBoxLoading from './MessageBoxLoading';
|
||||
import { useChat } from '@/lib/hooks/useChat';
|
||||
import MessageInput from './MessageInput';
|
||||
|
||||
const Chat = () => {
|
||||
const { sections, chatTurns, loading, messageAppeared } = useChat();
|
||||
|
||||
const [dividerWidth, setDividerWidth] = useState(0);
|
||||
const dividerRef = useRef<HTMLDivElement | null>(null);
|
||||
const Chat = ({
|
||||
loading,
|
||||
messages,
|
||||
sendMessage,
|
||||
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 containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const SCROLL_THRESHOLD = 250; // pixels from bottom to consider "at bottom"
|
||||
const [currentMessageId, setCurrentMessageId] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
// Check if user is at bottom of page
|
||||
useEffect(() => {
|
||||
const updateDividerWidth = () => {
|
||||
if (dividerRef.current) {
|
||||
setDividerWidth(dividerRef.current.offsetWidth);
|
||||
}
|
||||
const checkIsAtBottom = () => {
|
||||
const position = window.innerHeight + window.scrollY;
|
||||
const height = document.body.scrollHeight;
|
||||
const atBottom = position >= height - SCROLL_THRESHOLD;
|
||||
|
||||
setIsAtBottom(atBottom);
|
||||
};
|
||||
|
||||
updateDividerWidth();
|
||||
// Initial check
|
||||
checkIsAtBottom();
|
||||
|
||||
window.addEventListener('resize', updateDividerWidth);
|
||||
// Add scroll event listener
|
||||
window.addEventListener('scroll', checkIsAtBottom);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateDividerWidth);
|
||||
window.removeEventListener('scroll', checkIsAtBottom);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Detect wheel and touch events to identify user's scrolling direction
|
||||
useEffect(() => {
|
||||
const scroll = () => {
|
||||
messageEnd.current?.scrollIntoView({ behavior: 'auto' });
|
||||
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);
|
||||
};
|
||||
|
||||
if (chatTurns.length === 1) {
|
||||
document.title = `${chatTurns[0].content.substring(0, 30)} - Perplexica`;
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchStart = (e: TouchEvent) => {
|
||||
// Immediately stop auto-scrolling on any touch interaction
|
||||
setManuallyScrolledUp(true);
|
||||
};
|
||||
|
||||
// Add event listeners
|
||||
window.addEventListener('wheel', handleWheel, { passive: true });
|
||||
window.addEventListener('touchstart', handleTouchStart, { passive: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('wheel', handleWheel);
|
||||
window.removeEventListener('touchstart', handleTouchStart);
|
||||
};
|
||||
}, [isAtBottom]);
|
||||
|
||||
// Scroll when user sends a message
|
||||
useEffect(() => {
|
||||
const scroll = () => {
|
||||
messageEnd.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
if (messages.length === 1) {
|
||||
document.title = `${messages[0].content.substring(0, 30)} - Perplexica`;
|
||||
}
|
||||
|
||||
const messageEndBottom =
|
||||
messageEnd.current?.getBoundingClientRect().bottom ?? 0;
|
||||
|
||||
const distanceFromMessageEnd = window.innerHeight - messageEndBottom;
|
||||
|
||||
if (distanceFromMessageEnd >= -100) {
|
||||
// Always scroll when user sends a message
|
||||
if (messages[messages.length - 1]?.role === 'user') {
|
||||
scroll();
|
||||
setIsAtBottom(true); // Reset to true when user sends a message
|
||||
setManuallyScrolledUp(false); // Reset manually scrolled flag when user sends a message
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
if (chatTurns[chatTurns.length - 1]?.role === 'user') {
|
||||
scroll();
|
||||
// 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' });
|
||||
}
|
||||
}, [chatTurns]);
|
||||
}, [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);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Track the last user messageId when loading starts
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
// Find the last user message
|
||||
const lastUserMsg = [...messages]
|
||||
.reverse()
|
||||
.find((m) => m.role === 'user');
|
||||
setCurrentMessageId(lastUserMsg?.messageId);
|
||||
} else {
|
||||
setCurrentMessageId(undefined);
|
||||
}
|
||||
}, [loading, messages]);
|
||||
|
||||
// Cancel handler
|
||||
const handleCancel = async () => {
|
||||
if (!currentMessageId) return;
|
||||
try {
|
||||
await fetch('/api/chat/cancel', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ messageId: currentMessageId }),
|
||||
});
|
||||
} catch (e) {
|
||||
// Optionally handle error
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-6 pt-8 pb-44 lg:pb-32 sm:mx-4 md:mx-8">
|
||||
{sections.map((section, i) => {
|
||||
const isLast = i === sections.length - 1;
|
||||
<div ref={containerRef} className="space-y-6 pt-8 pb-48 sm:mx-4 md:mx-8">
|
||||
{messages.map((msg, i) => {
|
||||
const isLast = i === messages.length - 1;
|
||||
|
||||
return (
|
||||
<Fragment key={section.userMessage.messageId}>
|
||||
<Fragment key={msg.messageId}>
|
||||
<MessageBox
|
||||
section={section}
|
||||
sectionIndex={i}
|
||||
dividerRef={isLast ? dividerRef : undefined}
|
||||
key={i}
|
||||
message={msg}
|
||||
messageIndex={i}
|
||||
history={messages}
|
||||
loading={loading}
|
||||
isLast={isLast}
|
||||
rewrite={rewrite}
|
||||
sendMessage={sendMessage}
|
||||
handleEditMessage={handleEditMessage}
|
||||
/>
|
||||
{!isLast && (
|
||||
{!isLast && msg.role === 'assistant' && (
|
||||
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
{loading && !messageAppeared && <MessageBoxLoading />}
|
||||
{loading && <MessageBoxLoading />}
|
||||
<div className="fixed bottom-24 lg:bottom-10 z-40" style={inputStyle}>
|
||||
{/* Scroll to bottom button - appears above the MessageInput when user has scrolled up */}
|
||||
{manuallyScrolledUp && !isAtBottom && (
|
||||
<div className="absolute -top-14 right-2 z-10">
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
|
||||
<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}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</div>
|
||||
<div ref={messageEnd} className="h-0" />
|
||||
{dividerWidth > 0 && (
|
||||
<div
|
||||
className="bottom-24 lg:bottom-10 fixed z-40"
|
||||
style={{ width: dividerWidth }}
|
||||
>
|
||||
<MessageInput />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,48 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Document } from '@langchain/core/documents';
|
||||
import Navbar from './Navbar';
|
||||
import Chat from './Chat';
|
||||
import EmptyChat from './EmptyChat';
|
||||
import crypto from 'crypto';
|
||||
import { toast } from 'sonner';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { getSuggestions } from '@/lib/actions';
|
||||
import { Settings } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import NextError from 'next/error';
|
||||
import { useChat } from '@/lib/hooks/useChat';
|
||||
import Loader from './ui/Loader';
|
||||
|
||||
export interface BaseMessage {
|
||||
chatId: string;
|
||||
export type ModelStats = {
|
||||
modelName: string;
|
||||
responseTime?: number;
|
||||
};
|
||||
|
||||
export type Message = {
|
||||
messageId: string;
|
||||
chatId: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface AssistantMessage extends BaseMessage {
|
||||
role: 'assistant';
|
||||
content: string;
|
||||
role: 'user' | 'assistant';
|
||||
suggestions?: string[];
|
||||
}
|
||||
|
||||
export interface UserMessage extends BaseMessage {
|
||||
role: 'user';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface SourceMessage extends BaseMessage {
|
||||
role: 'source';
|
||||
sources: Document[];
|
||||
}
|
||||
|
||||
export interface SuggestionMessage extends BaseMessage {
|
||||
role: 'suggestion';
|
||||
suggestions: string[];
|
||||
}
|
||||
|
||||
export type Message =
|
||||
| AssistantMessage
|
||||
| UserMessage
|
||||
| SourceMessage
|
||||
| SuggestionMessage;
|
||||
export type ChatTurn = UserMessage | AssistantMessage;
|
||||
sources?: Document[];
|
||||
modelStats?: ModelStats;
|
||||
searchQuery?: string;
|
||||
searchUrl?: string;
|
||||
};
|
||||
|
||||
export interface File {
|
||||
fileName: string;
|
||||
@@ -50,8 +37,585 @@ export interface File {
|
||||
fileId: string;
|
||||
}
|
||||
|
||||
const ChatWindow = () => {
|
||||
const { hasError, isReady, notFound, messages } = useChat();
|
||||
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 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) {
|
||||
return (
|
||||
<div className="relative">
|
||||
@@ -76,17 +640,57 @@ const ChatWindow = () => {
|
||||
<div>
|
||||
{messages.length > 0 ? (
|
||||
<>
|
||||
<Navbar />
|
||||
<Chat />
|
||||
<Navbar chatId={chatId!} messages={messages} />
|
||||
<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 className="flex flex-row items-center justify-center min-h-screen">
|
||||
<Loader />
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="w-8 h-8 text-light-200 fill-light-secondary dark:text-[#202020] animate-spin dark:fill-[#ffffff3b]"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100.003 78.2051 78.1951 100.003 50.5908 100C22.9765 99.9972 0.997224 78.018 1 50.4037C1.00281 22.7993 22.8108 0.997224 50.4251 1C78.0395 1.00281 100.018 22.8108 100 50.4251ZM9.08164 50.594C9.06312 73.3997 27.7909 92.1272 50.5966 92.1457C73.4023 92.1642 92.1298 73.4365 92.1483 50.6308C92.1669 27.8251 73.4392 9.0973 50.6335 9.07878C27.8278 9.06026 9.10003 27.787 9.08164 50.594Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4037 97.8624 35.9116 96.9801 33.5533C95.1945 28.8227 92.871 24.3692 90.0681 20.348C85.6237 14.1775 79.4473 9.36872 72.0454 6.45794C64.6435 3.54717 56.3134 2.65431 48.3133 3.89319C45.869 4.27179 44.3768 6.77534 45.014 9.20079C45.6512 11.6262 48.1343 13.0956 50.5786 12.717C56.5073 11.8281 62.5542 12.5399 68.0406 14.7911C73.527 17.0422 78.2187 20.7487 81.5841 25.4923C83.7976 28.5886 85.4467 32.059 86.4416 35.7474C87.1273 38.1189 89.5423 39.6781 91.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
const Citation = ({
|
||||
href,
|
||||
children,
|
||||
}: {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
className="bg-light-secondary dark:bg-dark-secondary px-1 rounded ml-1 no-underline text-xs text-black/70 dark:text-white/70 relative"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default Citation;
|
||||
@@ -1,70 +0,0 @@
|
||||
import { Discover } from '@/app/discover/page';
|
||||
import Link from 'next/link';
|
||||
|
||||
const MajorNewsCard = ({
|
||||
item,
|
||||
isLeft = true,
|
||||
}: {
|
||||
item: Discover;
|
||||
isLeft?: boolean;
|
||||
}) => (
|
||||
<Link
|
||||
href={`/?q=Summary: ${item.url}`}
|
||||
className="w-full group flex flex-row items-stretch gap-6 h-60 py-3"
|
||||
target="_blank"
|
||||
>
|
||||
{isLeft ? (
|
||||
<>
|
||||
<div className="relative w-80 h-full overflow-hidden rounded-2xl flex-shrink-0">
|
||||
<img
|
||||
className="object-cover w-full h-full group-hover:scale-105 transition-transform duration-500"
|
||||
src={
|
||||
new URL(item.thumbnail).origin +
|
||||
new URL(item.thumbnail).pathname +
|
||||
`?id=${new URL(item.thumbnail).searchParams.get('id')}`
|
||||
}
|
||||
alt={item.title}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center flex-1 py-4">
|
||||
<h2
|
||||
className="text-3xl font-light mb-3 leading-tight line-clamp-3 group-hover:text-cyan-500 dark:group-hover:text-cyan-300 transition duration-200"
|
||||
style={{ fontFamily: 'PP Editorial, serif' }}
|
||||
>
|
||||
{item.title}
|
||||
</h2>
|
||||
<p className="text-black/60 dark:text-white/60 text-base leading-relaxed line-clamp-4">
|
||||
{item.content}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col justify-center flex-1 py-4">
|
||||
<h2
|
||||
className="text-3xl font-light mb-3 leading-tight line-clamp-3 group-hover:text-cyan-500 dark:group-hover:text-cyan-300 transition duration-200"
|
||||
style={{ fontFamily: 'PP Editorial, serif' }}
|
||||
>
|
||||
{item.title}
|
||||
</h2>
|
||||
<p className="text-black/60 dark:text-white/60 text-base leading-relaxed line-clamp-4">
|
||||
{item.content}
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative w-80 h-full overflow-hidden rounded-2xl flex-shrink-0">
|
||||
<img
|
||||
className="object-cover w-full h-full group-hover:scale-105 transition-transform duration-500"
|
||||
src={
|
||||
new URL(item.thumbnail).origin +
|
||||
new URL(item.thumbnail).pathname +
|
||||
`?id=${new URL(item.thumbnail).searchParams.get('id')}`
|
||||
}
|
||||
alt={item.title}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
|
||||
export default MajorNewsCard;
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Discover } from '@/app/discover/page';
|
||||
import Link from 'next/link';
|
||||
|
||||
const SmallNewsCard = ({ item }: { item: Discover }) => (
|
||||
<Link
|
||||
href={`/?q=Summary: ${item.url}`}
|
||||
className="rounded-3xl overflow-hidden bg-light-secondary dark:bg-dark-secondary shadow-sm shadow-light-200/10 dark:shadow-black/25 group flex flex-col"
|
||||
target="_blank"
|
||||
>
|
||||
<div className="relative aspect-video overflow-hidden">
|
||||
<img
|
||||
className="object-cover w-full h-full group-hover:scale-105 transition-transform duration-300"
|
||||
src={
|
||||
new URL(item.thumbnail).origin +
|
||||
new URL(item.thumbnail).pathname +
|
||||
`?id=${new URL(item.thumbnail).searchParams.get('id')}`
|
||||
}
|
||||
alt={item.title}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<h3 className="font-semibold text-sm mb-2 leading-tight line-clamp-2 group-hover:text-cyan-500 dark:group-hover:text-cyan-300 transition duration-200">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="text-black/60 dark:text-white/60 text-xs leading-relaxed line-clamp-2">
|
||||
{item.content}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
|
||||
export default SmallNewsCard;
|
||||
@@ -1,32 +1,56 @@
|
||||
import { Settings } from 'lucide-react';
|
||||
import EmptyChatMessageInput from './EmptyChatMessageInput';
|
||||
import { useState } from 'react';
|
||||
import { File } from './ChatWindow';
|
||||
import Link from 'next/link';
|
||||
import WeatherWidget from './WeatherWidget';
|
||||
import NewsArticleWidget from './NewsArticleWidget';
|
||||
import SettingsButtonMobile from '@/components/Settings/SettingsButtonMobile';
|
||||
import MessageInput from './MessageInput';
|
||||
|
||||
const EmptyChat = ({
|
||||
sendMessage,
|
||||
focusMode,
|
||||
setFocusMode,
|
||||
optimizationMode,
|
||||
setOptimizationMode,
|
||||
fileIds,
|
||||
setFileIds,
|
||||
files,
|
||||
setFiles,
|
||||
}: {
|
||||
sendMessage: (message: string) => void;
|
||||
focusMode: string;
|
||||
setFocusMode: (mode: string) => void;
|
||||
optimizationMode: string;
|
||||
setOptimizationMode: (mode: string) => void;
|
||||
fileIds: string[];
|
||||
setFileIds: (fileIds: string[]) => void;
|
||||
files: File[];
|
||||
setFiles: (files: File[]) => void;
|
||||
}) => {
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
|
||||
const EmptyChat = () => {
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="absolute w-full flex flex-row items-center justify-end mr-5 mt-5">
|
||||
<SettingsButtonMobile />
|
||||
<Link href="/settings">
|
||||
<Settings className="cursor-pointer lg:hidden" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center min-h-screen max-w-screen-sm mx-auto p-2 space-y-4">
|
||||
<div className="flex flex-col items-center justify-center w-full space-y-8">
|
||||
<h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8">
|
||||
Research begins here.
|
||||
</h2>
|
||||
<EmptyChatMessageInput />
|
||||
</div>
|
||||
<div className="flex flex-col w-full gap-4 mt-2 sm:flex-row sm:justify-center">
|
||||
<div className="flex-1 w-full">
|
||||
<WeatherWidget />
|
||||
</div>
|
||||
<div className="flex-1 w-full">
|
||||
<NewsArticleWidget />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center min-h-screen max-w-screen-sm mx-auto p-2 space-y-8">
|
||||
<h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8">
|
||||
Research begins here.
|
||||
</h2>
|
||||
<MessageInput
|
||||
firstMessage={true}
|
||||
loading={false}
|
||||
sendMessage={sendMessage}
|
||||
focusMode={focusMode}
|
||||
setFocusMode={setFocusMode}
|
||||
optimizationMode={optimizationMode}
|
||||
setOptimizationMode={setOptimizationMode}
|
||||
fileIds={fileIds}
|
||||
setFileIds={setFileIds}
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,88 +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';
|
||||
import ModelSelector from './MessageInputActions/ChatModelSelector';
|
||||
|
||||
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-3 pt-5 pb-3 rounded-2xl w-full border border-light-200 dark:border-dark-200 shadow-sm shadow-light-200/10 dark:shadow-black/20 transition-all duration-200 focus-within:border-light-300 dark:focus-within:border-dark-300">
|
||||
<TextareaAutosize
|
||||
ref={inputRef}
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
minRows={2}
|
||||
className="px-2 bg-transparent placeholder:text-[15px] placeholder:text-black/50 dark:placeholder:text-white/50 text-sm text-black dark:text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48"
|
||||
placeholder="Ask anything..."
|
||||
/>
|
||||
<div className="flex flex-row items-center justify-between mt-4">
|
||||
<Optimization />
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<div className="flex flex-row items-center space-x-1">
|
||||
<ModelSelector />
|
||||
<Focus />
|
||||
<Attach />
|
||||
</div>
|
||||
<button
|
||||
disabled={message.trim().length === 0}
|
||||
className="bg-sky-500 text-white disabled:text-black/50 dark:disabled:text-white/50 disabled:bg-[#e0e0dc] dark:disabled:bg-[#ececec21] hover:bg-opacity-85 transition duration-100 rounded-full p-2"
|
||||
>
|
||||
<ArrowRight className="bg-background" size={17} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyChatMessageInput;
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Check, ClipboardList } from 'lucide-react';
|
||||
import { Message } from '../ChatWindow';
|
||||
import { useState } from 'react';
|
||||
import { Section } from '@/lib/hooks/useChat';
|
||||
|
||||
const Copy = ({
|
||||
section,
|
||||
message,
|
||||
initialMessage,
|
||||
}: {
|
||||
section: Section;
|
||||
message: Message;
|
||||
initialMessage: string;
|
||||
}) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
@@ -15,7 +14,7 @@ const Copy = ({
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
const contentToCopy = `${initialMessage}${section?.sourceMessage?.sources && section.sourceMessage.sources.length > 0 && `\n\nCitations:\n${section.sourceMessage.sources?.map((source: any, i: any) => `[${i + 1}] ${source.metadata.url}`).join(`\n`)}`}`;
|
||||
const contentToCopy = `${initialMessage}${message.sources && message.sources.length > 0 && `\n\nCitations:\n${message.sources?.map((source: any, i: any) => `[${i + 1}] ${source.metadata.url}`).join(`\n`)}`}`;
|
||||
navigator.clipboard.writeText(contentToCopy);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1000);
|
||||
|
||||
82
src/components/MessageActions/ModelInfo.tsx
Normal 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;
|
||||
@@ -1,221 +1,127 @@
|
||||
'use client';
|
||||
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import React, { MutableRefObject } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
BookCopy,
|
||||
Disc3,
|
||||
Volume2,
|
||||
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, Section } from '@/lib/hooks/useChat';
|
||||
import Citation from './Citation';
|
||||
|
||||
const ThinkTagProcessor = ({
|
||||
children,
|
||||
thinkingEnded,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
thinkingEnded: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<ThinkBox content={children as string} thinkingEnded={thinkingEnded} />
|
||||
);
|
||||
};
|
||||
import { Check, Pencil, X } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { Message } from './ChatWindow';
|
||||
import MessageTabs from './MessageTabs';
|
||||
|
||||
const MessageBox = ({
|
||||
section,
|
||||
sectionIndex,
|
||||
dividerRef,
|
||||
message,
|
||||
messageIndex,
|
||||
history,
|
||||
loading,
|
||||
isLast,
|
||||
rewrite,
|
||||
sendMessage,
|
||||
handleEditMessage,
|
||||
}: {
|
||||
section: Section;
|
||||
sectionIndex: number;
|
||||
dividerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
message: Message;
|
||||
messageIndex: number;
|
||||
history: Message[];
|
||||
loading: boolean;
|
||||
isLast: boolean;
|
||||
}) => {
|
||||
const { loading, chatTurns, sendMessage, rewrite } = useChat();
|
||||
|
||||
const parsedMessage = section.parsedAssistantMessage || '';
|
||||
const speechMessage = section.speechMessage || '';
|
||||
const thinkingEnded = section.thinkingEnded;
|
||||
|
||||
const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
|
||||
|
||||
const markdownOverrides: MarkdownToJSX.Options = {
|
||||
overrides: {
|
||||
think: {
|
||||
component: ThinkTagProcessor,
|
||||
props: {
|
||||
thinkingEnded: thinkingEnded,
|
||||
},
|
||||
},
|
||||
citation: {
|
||||
component: Citation,
|
||||
},
|
||||
rewrite: (messageId: string) => void;
|
||||
sendMessage: (
|
||||
message: string,
|
||||
options?: {
|
||||
messageId?: string;
|
||||
rewriteIndex?: number;
|
||||
suggestions?: string[];
|
||||
},
|
||||
) => void;
|
||||
handleEditMessage: (messageId: string, content: string) => void;
|
||||
}) => {
|
||||
// Local state for editing functionality
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editedContent, setEditedContent] = useState('');
|
||||
|
||||
// Initialize editing
|
||||
const startEditMessage = () => {
|
||||
setIsEditing(true);
|
||||
setEditedContent(message.content);
|
||||
};
|
||||
|
||||
// Cancel editing
|
||||
const cancelEditMessage = () => {
|
||||
setIsEditing(false);
|
||||
setEditedContent('');
|
||||
};
|
||||
|
||||
// Save edits
|
||||
const saveEditMessage = () => {
|
||||
handleEditMessage(message.messageId, editedContent);
|
||||
setIsEditing(false);
|
||||
};
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className={'w-full pt-8 break-words'}>
|
||||
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
|
||||
{section.userMessage.content}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-9 lg:space-y-0 lg:flex-row lg:justify-between lg:space-x-9">
|
||||
<div>
|
||||
{message.role === 'user' && (
|
||||
<div
|
||||
ref={dividerRef}
|
||||
className="flex flex-col space-y-6 w-full lg:w-9/12"
|
||||
className={cn(
|
||||
'w-full',
|
||||
messageIndex === 0 ? 'pt-16' : 'pt-8',
|
||||
'break-words',
|
||||
)}
|
||||
>
|
||||
{section.sourceMessage &&
|
||||
section.sourceMessage.sources.length > 0 && (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<BookCopy className="text-black dark:text-white" size={20} />
|
||||
<h3 className="text-black dark:text-white font-medium text-xl">
|
||||
Sources
|
||||
</h3>
|
||||
</div>
|
||||
<MessageSources sources={section.sourceMessage.sources} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col space-y-2">
|
||||
{section.sourceMessage && (
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<Disc3
|
||||
className={cn(
|
||||
'text-black dark:text-white',
|
||||
isLast && loading ? 'animate-spin' : 'animate-none',
|
||||
)}
|
||||
size={20}
|
||||
/>
|
||||
<h3 className="text-black dark:text-white font-medium text-xl">
|
||||
Answer
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{section.assistantMessage && (
|
||||
<>
|
||||
<Markdown
|
||||
className={cn(
|
||||
'prose prose-h1:mb-3 prose-h2:mb-2 prose-h2:mt-6 prose-h2:font-[800] prose-h3:mt-4 prose-h3:mb-1.5 prose-h3:font-[600] dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 font-[400]',
|
||||
'max-w-none break-words text-black dark:text-white',
|
||||
)}
|
||||
options={markdownOverrides}
|
||||
{isEditing ? (
|
||||
<div className="w-full">
|
||||
<textarea
|
||||
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)}
|
||||
placeholder="Edit your message..."
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex flex-row space-x-2 mt-3 justify-end">
|
||||
<button
|
||||
onClick={cancelEditMessage}
|
||||
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"
|
||||
aria-label="Cancel"
|
||||
title="Cancel"
|
||||
>
|
||||
{parsedMessage}
|
||||
</Markdown>
|
||||
|
||||
{loading && isLast ? null : (
|
||||
<div className="flex flex-row items-center justify-between w-full text-black dark:text-white py-4 -mx-2">
|
||||
<div className="flex flex-row items-center space-x-1">
|
||||
<Rewrite
|
||||
rewrite={rewrite}
|
||||
messageId={section.assistantMessage.messageId}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row items-center space-x-1">
|
||||
<Copy
|
||||
initialMessage={section.assistantMessage.content}
|
||||
section={section}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (speechStatus === 'started') {
|
||||
stop();
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
}}
|
||||
className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
|
||||
>
|
||||
{speechStatus === 'started' ? (
|
||||
<StopCircle size={18} />
|
||||
) : (
|
||||
<Volume2 size={18} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLast &&
|
||||
section.suggestions &&
|
||||
section.suggestions.length > 0 &&
|
||||
section.assistantMessage &&
|
||||
!loading && (
|
||||
<div className="mt-8 pt-6 border-t border-light-200/50 dark:border-dark-200/50">
|
||||
<div className="flex flex-row items-center space-x-2 mb-4">
|
||||
<Layers3
|
||||
className="text-black dark:text-white"
|
||||
size={20}
|
||||
/>
|
||||
<h3 className="text-black dark:text-white font-medium text-xl">
|
||||
Related
|
||||
</h3>
|
||||
</div>
|
||||
<div className="space-y-0">
|
||||
{section.suggestions.map(
|
||||
(suggestion: string, i: number) => (
|
||||
<div key={i}>
|
||||
{i > 0 && (
|
||||
<div className="h-px bg-light-200/40 dark:bg-dark-200/40 mx-3" />
|
||||
)}
|
||||
<button
|
||||
onClick={() => sendMessage(suggestion)}
|
||||
className="group w-full px-3 py-4 text-left transition-colors duration-200"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-sm text-black/70 dark:text-white/70 group-hover:text-[#24A0ED] transition-colors duration-200 leading-relaxed">
|
||||
{suggestion}
|
||||
</p>
|
||||
<Plus
|
||||
size={16}
|
||||
className="text-black/40 dark:text-white/40 group-hover:text-[#24A0ED] transition-colors duration-200 flex-shrink-0"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<X size={18} />
|
||||
</button>
|
||||
<button
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<h2 className="text-black dark:text-white font-medium text-3xl">
|
||||
{message.content}
|
||||
</h2>
|
||||
<button
|
||||
onClick={startEditMessage}
|
||||
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"
|
||||
aria-label="Edit message"
|
||||
title="Edit message"
|
||||
>
|
||||
<Pencil size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{section.assistantMessage && (
|
||||
<div className="lg:sticky lg:top-20 flex flex-col items-center space-y-3 w-full lg:w-3/12 z-30 h-full pb-4">
|
||||
<SearchImages
|
||||
query={section.userMessage.content}
|
||||
chatHistory={chatTurns.slice(0, sectionIndex * 2)}
|
||||
messageId={section.assistantMessage.messageId}
|
||||
/>
|
||||
<SearchVideos
|
||||
chatHistory={chatTurns.slice(0, sectionIndex * 2)}
|
||||
query={section.userMessage.content}
|
||||
messageId={section.assistantMessage.messageId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{message.role === 'assistant' && (
|
||||
<MessageTabs
|
||||
query={history[messageIndex - 1].content}
|
||||
chatHistory={history.slice(0, messageIndex - 1)}
|
||||
messageId={message.messageId}
|
||||
message={message}
|
||||
isLast={isLast}
|
||||
loading={loading}
|
||||
rewrite={rewrite}
|
||||
sendMessage={sendMessage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,115 +1,166 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ArrowUp } from 'lucide-react';
|
||||
import { ArrowRight, ArrowUp, Square } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import Attach from './MessageInputActions/Attach';
|
||||
import CopilotToggle from './MessageInputActions/Copilot';
|
||||
import { File } from './ChatWindow';
|
||||
import AttachSmall from './MessageInputActions/AttachSmall';
|
||||
import { useChat } from '@/lib/hooks/useChat';
|
||||
import Attach from './MessageInputActions/Attach';
|
||||
import Focus from './MessageInputActions/Focus';
|
||||
import ModelSelector from './MessageInputActions/ModelSelector';
|
||||
import Optimization from './MessageInputActions/Optimization';
|
||||
|
||||
const MessageInput = () => {
|
||||
const { loading, sendMessage } = useChat();
|
||||
|
||||
const [copilotEnabled, setCopilotEnabled] = useState(false);
|
||||
const MessageInput = ({
|
||||
sendMessage,
|
||||
loading,
|
||||
fileIds,
|
||||
setFileIds,
|
||||
files,
|
||||
setFiles,
|
||||
optimizationMode,
|
||||
setOptimizationMode,
|
||||
focusMode,
|
||||
setFocusMode,
|
||||
firstMessage,
|
||||
onCancel,
|
||||
}: {
|
||||
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;
|
||||
onCancel?: () => void;
|
||||
}) => {
|
||||
const [message, setMessage] = useState('');
|
||||
const [textareaRows, setTextareaRows] = useState(1);
|
||||
const [mode, setMode] = useState<'multi' | 'single'>('single');
|
||||
const [selectedModel, setSelectedModel] = useState<{
|
||||
provider: string;
|
||||
model: string;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (textareaRows >= 2 && message && mode === 'single') {
|
||||
setMode('multi');
|
||||
} else if (!message && mode === 'multi') {
|
||||
setMode('single');
|
||||
// Load saved model preferences from localStorage
|
||||
const chatModelProvider = localStorage.getItem('chatModelProvider');
|
||||
const chatModel = localStorage.getItem('chatModel');
|
||||
|
||||
if (chatModelProvider && chatModel) {
|
||||
setSelectedModel({
|
||||
provider: chatModelProvider,
|
||||
model: chatModel,
|
||||
});
|
||||
}
|
||||
}, [textareaRows, mode, message]);
|
||||
}, []);
|
||||
|
||||
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);
|
||||
|
||||
return () => {
|
||||
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 (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
if (loading) return;
|
||||
e.preventDefault();
|
||||
sendMessage(message);
|
||||
setMessage('');
|
||||
handleSubmitMessage();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey && !loading) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage(message);
|
||||
setMessage('');
|
||||
handleSubmitMessage();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'bg-light-secondary dark:bg-dark-secondary p-4 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 shadow-sm shadow-light-200/10 dark:shadow-black/20 transition-all duration-200 focus-within:border-light-300 dark:focus-within:border-dark-300',
|
||||
mode === 'multi' ? 'flex-col rounded-2xl' : 'flex-row rounded-full',
|
||||
)}
|
||||
className="w-full"
|
||||
>
|
||||
{mode === 'single' && <AttachSmall />}
|
||||
<TextareaAutosize
|
||||
ref={inputRef}
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onHeightChange={(height, props) => {
|
||||
setTextareaRows(Math.ceil(height / props.rowHeight));
|
||||
}}
|
||||
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 space-x-4">
|
||||
<CopilotToggle
|
||||
copilotEnabled={copilotEnabled}
|
||||
setCopilotEnabled={setCopilotEnabled}
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
{mode === 'multi' && (
|
||||
<div className="flex flex-row items-center justify-between w-full pt-2">
|
||||
<AttachSmall />
|
||||
<div className="flex flex-row items-center space-x-4">
|
||||
<CopilotToggle
|
||||
copilotEnabled={copilotEnabled}
|
||||
setCopilotEnabled={setCopilotEnabled}
|
||||
<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={firstMessage ? 'Ask anything...' : 'Ask a follow-up'}
|
||||
/>
|
||||
<div className="flex flex-row items-center justify-between mt-4">
|
||||
<div className="flex flex-row items-center space-x-2 lg:space-x-4">
|
||||
<Focus focusMode={focusMode} setFocusMode={setFocusMode} />
|
||||
<Attach
|
||||
fileIds={fileIds}
|
||||
setFileIds={setFileIds}
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
showText={firstMessage}
|
||||
/>
|
||||
<button
|
||||
disabled={message.trim().length === 0 || loading}
|
||||
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"
|
||||
>
|
||||
<ArrowUp className="bg-background" size={17} />
|
||||
</button>
|
||||
<ModelSelector
|
||||
selectedModel={selectedModel}
|
||||
setSelectedModel={setSelectedModel}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row items-center space-x-1 sm:space-x-4">
|
||||
<Optimization
|
||||
optimizationMode={optimizationMode}
|
||||
setOptimizationMode={setOptimizationMode}
|
||||
/>
|
||||
{loading ? (
|
||||
<button
|
||||
type="button"
|
||||
className="bg-red-700 text-white hover:bg-red-800 transition duration-100 rounded-full p-2 relative group"
|
||||
onClick={onCancel}
|
||||
aria-label="Cancel"
|
||||
>
|
||||
{loading && (
|
||||
<div className="absolute inset-0 rounded-full border-2 border-white/30 border-t-white animate-spin" />
|
||||
)}
|
||||
<span className="relative flex items-center justify-center w-[17px] h-[17px]">
|
||||
<Square size={17} className="text-white" />
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<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"
|
||||
type="submit"
|
||||
>
|
||||
{firstMessage ? (
|
||||
<ArrowRight className="bg-background" size={17} />
|
||||
) : (
|
||||
<ArrowUp className="bg-background" size={17} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,21 +5,23 @@ import {
|
||||
PopoverPanel,
|
||||
Transition,
|
||||
} from '@headlessui/react';
|
||||
import {
|
||||
CopyPlus,
|
||||
File,
|
||||
Link,
|
||||
LoaderCircle,
|
||||
Paperclip,
|
||||
Plus,
|
||||
Trash,
|
||||
} from 'lucide-react';
|
||||
import { File, LoaderCircle, Paperclip, Plus, Trash } from 'lucide-react';
|
||||
import { Fragment, useRef, useState } from 'react';
|
||||
import { useChat } from '@/lib/hooks/useChat';
|
||||
|
||||
const Attach = () => {
|
||||
const { files, setFiles, setFileIds, fileIds } = useChat();
|
||||
import { File as FileType } from '../ChatWindow';
|
||||
|
||||
const Attach = ({
|
||||
fileIds,
|
||||
setFileIds,
|
||||
showText,
|
||||
files,
|
||||
setFiles,
|
||||
}: {
|
||||
fileIds: string[];
|
||||
setFileIds: (fileIds: string[]) => void;
|
||||
showText?: boolean;
|
||||
files: FileType[];
|
||||
setFiles: (files: FileType[]) => void;
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const fileInputRef = useRef<any>();
|
||||
|
||||
@@ -32,12 +34,12 @@ const Attach = () => {
|
||||
}
|
||||
|
||||
const embeddingModelProvider = localStorage.getItem(
|
||||
'embeddingModelProviderId',
|
||||
'embeddingModelProvider',
|
||||
);
|
||||
const embeddingModel = localStorage.getItem('embeddingModelKey');
|
||||
const embeddingModel = localStorage.getItem('embeddingModel');
|
||||
|
||||
data.append('embedding_model_provider_id', embeddingModelProvider!);
|
||||
data.append('embedding_model_key', embeddingModel!);
|
||||
data.append('embedding_model_provider', embeddingModelProvider!);
|
||||
data.append('embedding_model', embeddingModel!);
|
||||
|
||||
const res = await fetch(`/api/uploads`, {
|
||||
method: 'POST',
|
||||
@@ -52,16 +54,42 @@ const Attach = () => {
|
||||
};
|
||||
|
||||
return loading ? (
|
||||
<div className="active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none text-black/50 dark:text-white/50 transition duration-200">
|
||||
<LoaderCircle size={16} className="text-sky-400 animate-spin" />
|
||||
<div className="flex flex-row items-center justify-between space-x-1">
|
||||
<LoaderCircle size={18} className="text-sky-400 animate-spin" />
|
||||
<p className="text-sky-400 inline whitespace-nowrap text-xs font-medium">
|
||||
Uploading..
|
||||
</p>
|
||||
</div>
|
||||
) : files.length > 0 ? (
|
||||
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
|
||||
<PopoverButton
|
||||
type="button"
|
||||
className="active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none headless-open:text-black dark:headless-open:text-white text-black/50 dark:text-white/50 active:scale-95 transition duration-200 hover:text-black dark:hover:text-white"
|
||||
className={cn(
|
||||
'flex flex-row items-center justify-between space-x-1 p-2 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white',
|
||||
files.length > 0 ? '-ml-2 lg:-ml-3' : '',
|
||||
)}
|
||||
>
|
||||
<File size={16} className="text-sky-400" />
|
||||
{files.length > 1 && (
|
||||
<>
|
||||
<File size={19} className="text-sky-400" />
|
||||
<p className="text-sky-400 inline whitespace-nowrap text-xs font-medium">
|
||||
{files.length} files
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{files.length === 1 && (
|
||||
<>
|
||||
<File size={18} className="text-sky-400" />
|
||||
<p className="text-sky-400 text-xs font-medium">
|
||||
{files[0].fileName.length > 10
|
||||
? files[0].fileName.replace(/\.\w+$/, '').substring(0, 3) +
|
||||
'...' +
|
||||
files[0].fileExtension
|
||||
: files[0].fileName}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</PopoverButton>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
@@ -82,7 +110,7 @@ const Attach = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current.click()}
|
||||
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200 focus:outline-none"
|
||||
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
@@ -92,7 +120,7 @@ const Attach = () => {
|
||||
multiple
|
||||
hidden
|
||||
/>
|
||||
<Plus size={16} />
|
||||
<Plus size={18} />
|
||||
<p className="text-xs">Add</p>
|
||||
</button>
|
||||
<button
|
||||
@@ -100,7 +128,7 @@ const Attach = () => {
|
||||
setFiles([]);
|
||||
setFileIds([]);
|
||||
}}
|
||||
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200 focus:outline-none"
|
||||
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
|
||||
>
|
||||
<Trash size={14} />
|
||||
<p className="text-xs">Clear</p>
|
||||
@@ -114,11 +142,8 @@ const Attach = () => {
|
||||
key={i}
|
||||
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">
|
||||
<File
|
||||
size={16}
|
||||
className="text-black/70 dark:text-white/70"
|
||||
/>
|
||||
<div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md">
|
||||
<File size={16} className="text-white/70" />
|
||||
</div>
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
{file.fileName.length > 25
|
||||
@@ -139,7 +164,8 @@ const Attach = () => {
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current.click()}
|
||||
className={cn(
|
||||
'flex items-center justify-center active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none headless-open:text-black dark:headless-open:text-white text-black/50 dark:text-white/50 active:scale-95 transition duration-200 hover:text-black dark:hover:text-white',
|
||||
'flex flex-row items-center space-x-1 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white',
|
||||
showText ? '' : 'p-2',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
@@ -150,7 +176,10 @@ const Attach = () => {
|
||||
multiple
|
||||
hidden
|
||||
/>
|
||||
<Paperclip size={16} />
|
||||
<Paperclip size="18" />
|
||||
{showText && (
|
||||
<p className="text-xs font-medium pl-[1px] hidden lg:block">Attach</p>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,21 +5,21 @@ import {
|
||||
PopoverPanel,
|
||||
Transition,
|
||||
} from '@headlessui/react';
|
||||
import {
|
||||
CopyPlus,
|
||||
File,
|
||||
LoaderCircle,
|
||||
Paperclip,
|
||||
Plus,
|
||||
Trash,
|
||||
} from 'lucide-react';
|
||||
import { CopyPlus, File, LoaderCircle, Plus, Trash } from 'lucide-react';
|
||||
import { Fragment, useRef, useState } from 'react';
|
||||
import { File as FileType } from '../ChatWindow';
|
||||
import { useChat } from '@/lib/hooks/useChat';
|
||||
|
||||
const AttachSmall = () => {
|
||||
const { files, setFiles, setFileIds, fileIds } = useChat();
|
||||
|
||||
const AttachSmall = ({
|
||||
fileIds,
|
||||
setFileIds,
|
||||
files,
|
||||
setFiles,
|
||||
}: {
|
||||
fileIds: string[];
|
||||
setFileIds: (fileIds: string[]) => void;
|
||||
files: FileType[];
|
||||
setFiles: (files: FileType[]) => void;
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const fileInputRef = useRef<any>();
|
||||
|
||||
@@ -32,12 +32,12 @@ const AttachSmall = () => {
|
||||
}
|
||||
|
||||
const embeddingModelProvider = localStorage.getItem(
|
||||
'embeddingModelProviderId',
|
||||
'embeddingModelProvider',
|
||||
);
|
||||
const embeddingModel = localStorage.getItem('embeddingModelKey');
|
||||
const embeddingModel = localStorage.getItem('embeddingModel');
|
||||
|
||||
data.append('embedding_model_provider_id', embeddingModelProvider!);
|
||||
data.append('embedding_model_key', embeddingModel!);
|
||||
data.append('embedding_model_provider', embeddingModelProvider!);
|
||||
data.append('embedding_model', embeddingModel!);
|
||||
|
||||
const res = await fetch(`/api/uploads`, {
|
||||
method: 'POST',
|
||||
@@ -52,7 +52,7 @@ const AttachSmall = () => {
|
||||
};
|
||||
|
||||
return loading ? (
|
||||
<div className="flex flex-row items-center justify-between space-x-1 p-1 ">
|
||||
<div className="flex flex-row items-center justify-between space-x-1 p-1">
|
||||
<LoaderCircle size={20} className="text-sky-400 animate-spin" />
|
||||
</div>
|
||||
) : files.length > 0 ? (
|
||||
@@ -114,11 +114,8 @@ const AttachSmall = () => {
|
||||
key={i}
|
||||
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">
|
||||
<File
|
||||
size={16}
|
||||
className="text-black/70 dark:text-white/70"
|
||||
/>
|
||||
<div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md">
|
||||
<File size={16} className="text-white/70" />
|
||||
</div>
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
{file.fileName.length > 25
|
||||
@@ -148,7 +145,7 @@ const AttachSmall = () => {
|
||||
multiple
|
||||
hidden
|
||||
/>
|
||||
<Paperclip size={16} />
|
||||
<CopyPlus size={20} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Cpu, Loader2, Search } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverPanel,
|
||||
Transition,
|
||||
} from '@headlessui/react';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { MinimalProvider } from '@/lib/models/types';
|
||||
|
||||
const ModelSelector = () => {
|
||||
const [providers, setProviders] = useState<MinimalProvider[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedModel, setSelectedModel] = useState<{
|
||||
providerId: string;
|
||||
modelKey: string;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadProviders = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await fetch('/api/providers');
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch providers');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
setProviders(data.providers || []);
|
||||
|
||||
const savedProviderId = localStorage.getItem('chatModelProviderId');
|
||||
const savedModelKey = localStorage.getItem('chatModelKey');
|
||||
|
||||
if (savedProviderId && savedModelKey) {
|
||||
setSelectedModel({
|
||||
providerId: savedProviderId,
|
||||
modelKey: savedModelKey,
|
||||
});
|
||||
} else if (data.providers && data.providers.length > 0) {
|
||||
const firstProvider = data.providers.find(
|
||||
(p: MinimalProvider) => p.chatModels.length > 0,
|
||||
);
|
||||
if (firstProvider && firstProvider.chatModels[0]) {
|
||||
setSelectedModel({
|
||||
providerId: firstProvider.id,
|
||||
modelKey: firstProvider.chatModels[0].key,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading providers:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadProviders();
|
||||
}, []);
|
||||
|
||||
const handleModelSelect = (providerId: string, modelKey: string) => {
|
||||
setSelectedModel({ providerId, modelKey });
|
||||
localStorage.setItem('chatModelProviderId', providerId);
|
||||
localStorage.setItem('chatModelKey', modelKey);
|
||||
};
|
||||
|
||||
const filteredProviders = providers
|
||||
.map((provider) => ({
|
||||
...provider,
|
||||
chatModels: provider.chatModels.filter(
|
||||
(model) =>
|
||||
model.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
provider.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
),
|
||||
}))
|
||||
.filter((provider) => provider.chatModels.length > 0);
|
||||
|
||||
return (
|
||||
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
|
||||
<PopoverButton
|
||||
type="button"
|
||||
className="active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none headless-open:text-black dark:headless-open:text-white text-black/50 dark:text-white/50 active:scale-95 transition duration-200 hover:text-black dark:hover:text-white"
|
||||
>
|
||||
<Cpu size={16} className="text-sky-500" />
|
||||
</PopoverButton>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<PopoverPanel className="absolute z-10 w-[230px] sm:w-[270px] md:w-[300px] -right-4">
|
||||
<div className="bg-light-primary dark:bg-dark-primary max-h-[300px] sm:max-w-none border rounded-lg border-light-200 dark:border-dark-200 w-full flex flex-col shadow-lg overflow-hidden">
|
||||
<div className="p-4 border-b border-light-200 dark:border-dark-200">
|
||||
<div className="relative">
|
||||
<Search
|
||||
size={16}
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 text-black/40 dark:text-white/40"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search models..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-9 pr-3 py-2 bg-light-secondary dark:bg-dark-secondary rounded-lg placeholder:text-sm text-sm text-black dark:text-white placeholder:text-black/40 dark:placeholder:text-white/40 focus:outline-none focus:ring-2 focus:ring-sky-500/20 border border-transparent focus:border-sky-500/30 transition duration-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[320px] overflow-y-auto">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<Loader2
|
||||
className="animate-spin text-black/40 dark:text-white/40"
|
||||
size={24}
|
||||
/>
|
||||
</div>
|
||||
) : filteredProviders.length === 0 ? (
|
||||
<div className="text-center py-16 px-4 text-black/60 dark:text-white/60 text-sm">
|
||||
{searchQuery
|
||||
? 'No models found'
|
||||
: 'No chat models configured'}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col">
|
||||
{filteredProviders.map((provider, providerIndex) => (
|
||||
<div key={provider.id}>
|
||||
<div className="px-4 py-2.5 sticky top-0 bg-light-primary dark:bg-dark-primary border-b border-light-200/50 dark:border-dark-200/50">
|
||||
<p className="text-xs text-black/50 dark:text-white/50 uppercase tracking-wider">
|
||||
{provider.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col px-2 py-2 space-y-0.5">
|
||||
{provider.chatModels.map((model) => (
|
||||
<PopoverButton
|
||||
key={model.key}
|
||||
onClick={() =>
|
||||
handleModelSelect(provider.id, model.key)
|
||||
}
|
||||
className={cn(
|
||||
'px-3 py-2 flex items-center justify-between text-start duration-200 cursor-pointer transition rounded-lg group',
|
||||
selectedModel?.providerId === provider.id &&
|
||||
selectedModel?.modelKey === model.key
|
||||
? 'bg-light-secondary dark:bg-dark-secondary'
|
||||
: 'hover:bg-light-secondary dark:hover:bg-dark-secondary',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center space-x-2.5 min-w-0 flex-1">
|
||||
<Cpu
|
||||
size={15}
|
||||
className={cn(
|
||||
'shrink-0',
|
||||
selectedModel?.providerId === provider.id &&
|
||||
selectedModel?.modelKey === model.key
|
||||
? 'text-sky-500'
|
||||
: 'text-black/50 dark:text-white/50 group-hover:text-black/70 group-hover:dark:text-white/70',
|
||||
)}
|
||||
/>
|
||||
<p
|
||||
className={cn(
|
||||
'text-sm truncate',
|
||||
selectedModel?.providerId === provider.id &&
|
||||
selectedModel?.modelKey === model.key
|
||||
? 'text-sky-500 font-medium'
|
||||
: 'text-black/70 dark:text-white/70 group-hover:text-black dark:group-hover:text-white',
|
||||
)}
|
||||
>
|
||||
{model.name}
|
||||
</p>
|
||||
</div>
|
||||
</PopoverButton>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{providerIndex < filteredProviders.length - 1 && (
|
||||
<div className="h-px bg-light-200 dark:bg-dark-200" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelSelector;
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
BadgePercent,
|
||||
ChevronDown,
|
||||
Globe,
|
||||
MessageCircle,
|
||||
Pencil,
|
||||
ScanEye,
|
||||
SwatchBook,
|
||||
@@ -15,83 +16,97 @@ import {
|
||||
} from '@headlessui/react';
|
||||
import { SiReddit, SiYoutube } from '@icons-pack/react-simple-icons';
|
||||
import { Fragment } from 'react';
|
||||
import { useChat } from '@/lib/hooks/useChat';
|
||||
|
||||
const focusModes = [
|
||||
{
|
||||
key: 'webSearch',
|
||||
title: 'All',
|
||||
description: 'Searches across all of the internet',
|
||||
icon: <Globe size={16} />,
|
||||
icon: <Globe size={20} />,
|
||||
},
|
||||
{
|
||||
key: 'academicSearch',
|
||||
title: 'Academic',
|
||||
description: 'Search in published academic papers',
|
||||
icon: <SwatchBook size={16} />,
|
||||
icon: <SwatchBook size={20} />,
|
||||
},
|
||||
{
|
||||
key: 'writingAssistant',
|
||||
title: 'Writing',
|
||||
description: 'Chat without searching the web',
|
||||
key: 'chat',
|
||||
title: 'Chat',
|
||||
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} />,
|
||||
},
|
||||
{
|
||||
key: 'wolframAlphaSearch',
|
||||
title: 'Wolfram Alpha',
|
||||
description: 'Computational knowledge engine',
|
||||
icon: <BadgePercent size={16} />,
|
||||
},
|
||||
{
|
||||
key: 'youtubeSearch',
|
||||
title: 'Youtube',
|
||||
description: 'Search and watch videos',
|
||||
icon: <SiYoutube className="h-[16px] w-auto mr-0.5" />,
|
||||
},
|
||||
{
|
||||
key: 'redditSearch',
|
||||
title: 'Reddit',
|
||||
description: 'Search for discussions and opinions',
|
||||
icon: <SiReddit className="h-[16px] w-auto mr-0.5" />,
|
||||
icon: <SiReddit className="h-5 w-auto mr-0.5" />,
|
||||
},
|
||||
{
|
||||
key: 'wolframAlphaSearch',
|
||||
title: 'Wolfram Alpha',
|
||||
description: 'Computational knowledge engine',
|
||||
icon: <BadgePercent size={20} />,
|
||||
},
|
||||
{
|
||||
key: 'youtubeSearch',
|
||||
title: 'Youtube',
|
||||
description: 'Search and watch videos',
|
||||
icon: <SiYoutube className="h-5 w-auto mr-0.5" />,
|
||||
},
|
||||
];
|
||||
|
||||
const Focus = () => {
|
||||
const { focusMode, setFocusMode } = useChat();
|
||||
|
||||
const Focus = ({
|
||||
focusMode,
|
||||
setFocusMode,
|
||||
}: {
|
||||
focusMode: string;
|
||||
setFocusMode: (mode: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
|
||||
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg mt-[6.5px]">
|
||||
<PopoverButton
|
||||
type="button"
|
||||
className="active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none headless-open:text-black dark:headless-open:text-white text-black/50 dark:text-white/50 active:scale-95 transition duration-200 hover:text-black dark:hover:text-white"
|
||||
className=" text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white"
|
||||
>
|
||||
{focusMode !== 'webSearch' ? (
|
||||
<div className="flex flex-row items-center space-x-1">
|
||||
{focusModes.find((mode) => mode.key === focusMode)?.icon}
|
||||
<p className="text-xs font-medium hidden lg:block">
|
||||
{focusModes.find((mode) => mode.key === focusMode)?.title}
|
||||
</p>
|
||||
<ChevronDown size={20} className="-translate-x-1" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-row items-center space-x-1">
|
||||
<Globe size={16} />
|
||||
<ScanEye size={20} />
|
||||
<p className="text-xs font-medium hidden lg:block">Focus</p>
|
||||
</div>
|
||||
)}
|
||||
</PopoverButton>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-150"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
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"
|
||||
leaveTo="opacity-0 -translate-y-1"
|
||||
>
|
||||
<PopoverPanel className="absolute z-10 w-64 md:w-[500px] -right-4">
|
||||
<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">
|
||||
{focusModes.map((mode, i) => (
|
||||
<PopoverButton
|
||||
onClick={() => setFocusMode(mode.key)}
|
||||
key={i}
|
||||
className={cn(
|
||||
'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-2 duration-200 cursor-pointer transition focus:outline-none',
|
||||
'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-2 duration-200 cursor-pointer transition',
|
||||
focusMode === mode.key
|
||||
? 'bg-light-secondary dark:bg-dark-secondary'
|
||||
: 'hover:bg-light-secondary dark:hover:bg-dark-secondary',
|
||||
|
||||
305
src/components/MessageInputActions/ModelSelector.tsx
Normal 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;
|
||||
@@ -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 {
|
||||
Popover,
|
||||
@@ -7,20 +7,18 @@ import {
|
||||
Transition,
|
||||
} from '@headlessui/react';
|
||||
import { Fragment } from 'react';
|
||||
import { useChat } from '@/lib/hooks/useChat';
|
||||
|
||||
const OptimizationModes = [
|
||||
{
|
||||
key: 'speed',
|
||||
title: 'Speed',
|
||||
description: 'Prioritize speed and get the quickest possible answer.',
|
||||
icon: <Zap size={16} className="text-[#FF9800]" />,
|
||||
icon: <Zap size={20} className="text-[#FF9800]" />,
|
||||
},
|
||||
{
|
||||
key: 'balanced',
|
||||
title: 'Balanced',
|
||||
description: 'Find the right balance between speed and accuracy',
|
||||
icon: <Sliders size={16} className="text-[#4CAF50]" />,
|
||||
icon: <Sliders size={20} className="text-[#4CAF50]" />,
|
||||
},
|
||||
{
|
||||
key: 'quality',
|
||||
@@ -35,69 +33,74 @@ const OptimizationModes = [
|
||||
},
|
||||
];
|
||||
|
||||
const Optimization = () => {
|
||||
const { optimizationMode, setOptimizationMode } = useChat();
|
||||
const Optimization = ({
|
||||
optimizationMode,
|
||||
setOptimizationMode,
|
||||
}: {
|
||||
optimizationMode: string;
|
||||
setOptimizationMode: (mode: string) => void;
|
||||
}) => {
|
||||
const handleOptimizationChange = (mode: string) => {
|
||||
setOptimizationMode(mode);
|
||||
localStorage.setItem('optimizationMode', mode);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<PopoverButton
|
||||
type="button"
|
||||
className="p-2 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white focus:outline-none"
|
||||
>
|
||||
<div className="flex flex-row items-center space-x-1">
|
||||
{
|
||||
OptimizationModes.find((mode) => mode.key === optimizationMode)
|
||||
?.icon
|
||||
}
|
||||
<ChevronDown
|
||||
size={16}
|
||||
<Popover className="relative">
|
||||
<PopoverButton
|
||||
type="button"
|
||||
className="p-2 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white"
|
||||
>
|
||||
<div className="flex flex-row items-center space-x-1">
|
||||
{
|
||||
OptimizationModes.find((mode) => mode.key === optimizationMode)
|
||||
?.icon
|
||||
}
|
||||
{/* <p className="text-xs font-medium hidden lg:block">
|
||||
{
|
||||
OptimizationModes.find((mode) => mode.key === optimizationMode)
|
||||
?.title
|
||||
}
|
||||
</p> */}
|
||||
<ChevronDown size={20} />
|
||||
</div>
|
||||
</PopoverButton>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-150"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<PopoverPanel className="absolute z-10 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-max max-w-[300px] p-4 max-h-[200px] md:max-h-none overflow-y-auto">
|
||||
{OptimizationModes.map((mode, i) => (
|
||||
<PopoverButton
|
||||
onClick={() => handleOptimizationChange(mode.key)}
|
||||
key={i}
|
||||
disabled={mode.key === 'quality'}
|
||||
className={cn(
|
||||
open ? 'rotate-180' : 'rotate-0',
|
||||
'transition duration:200',
|
||||
'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-1 duration-200 cursor-pointer transition',
|
||||
optimizationMode === mode.key
|
||||
? 'bg-light-secondary dark:bg-dark-secondary'
|
||||
: 'hover:bg-light-secondary dark:hover:bg-dark-secondary',
|
||||
mode.key === 'quality' && 'opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</PopoverButton>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-150"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<PopoverPanel className="absolute z-10 w-64 md:w-[250px] left-0">
|
||||
<div className="flex flex-col gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto">
|
||||
{OptimizationModes.map((mode, i) => (
|
||||
<PopoverButton
|
||||
onClick={() => setOptimizationMode(mode.key)}
|
||||
key={i}
|
||||
disabled={mode.key === 'quality'}
|
||||
className={cn(
|
||||
'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-1 duration-200 cursor-pointer transition focus:outline-none',
|
||||
optimizationMode === mode.key
|
||||
? 'bg-light-secondary dark:bg-dark-secondary'
|
||||
: 'hover:bg-light-secondary dark:hover:bg-dark-secondary',
|
||||
mode.key === 'quality' && 'opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row items-center space-x-1 text-black dark:text-white">
|
||||
{mode.icon}
|
||||
<p className="text-sm font-medium">{mode.title}</p>
|
||||
</div>
|
||||
<p className="text-black/70 dark:text-white/70 text-xs">
|
||||
{mode.description}
|
||||
</p>
|
||||
</PopoverButton>
|
||||
))}
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row items-center space-x-1 text-black dark:text-white">
|
||||
{mode.icon}
|
||||
<p className="text-sm font-medium">{mode.title}</p>
|
||||
</div>
|
||||
<p className="text-black/70 dark:text-white/70 text-xs">
|
||||
{mode.description}
|
||||
</p>
|
||||
</PopoverButton>
|
||||
))}
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,31 +1,11 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import {
|
||||
Dialog,
|
||||
DialogPanel,
|
||||
DialogTitle,
|
||||
Transition,
|
||||
TransitionChild,
|
||||
} from '@headlessui/react';
|
||||
import { Document } from '@langchain/core/documents';
|
||||
import { File } from 'lucide-react';
|
||||
import { Fragment, useState } from 'react';
|
||||
|
||||
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 (
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-2">
|
||||
{sources.slice(0, 3).map((source, i) => (
|
||||
{sources.map((source, i) => (
|
||||
<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"
|
||||
key={i}
|
||||
@@ -61,101 +41,6 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
535
src/components/MessageTabs.tsx
Normal 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;
|
||||
@@ -1,223 +1,40 @@
|
||||
import { Clock, Edit, Share, Trash, FileText, FileDown } from 'lucide-react';
|
||||
import { Clock, Edit, Share, Trash } from 'lucide-react';
|
||||
import { Message } from './ChatWindow';
|
||||
import { useEffect, useState, Fragment } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { formatTimeDifference } from '@/lib/utils';
|
||||
import DeleteChat from './DeleteChat';
|
||||
import {
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverPanel,
|
||||
Transition,
|
||||
} from '@headlessui/react';
|
||||
import jsPDF from 'jspdf';
|
||||
import { useChat, Section } from '@/lib/hooks/useChat';
|
||||
|
||||
const downloadFile = (filename: string, content: string, type: string) => {
|
||||
const blob = new Blob([content], { type });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const exportAsMarkdown = (sections: Section[], title: string) => {
|
||||
const date = new Date(
|
||||
sections[0]?.userMessage?.createdAt || Date.now(),
|
||||
).toLocaleString();
|
||||
let md = `# 💬 Chat Export: ${title}\n\n`;
|
||||
md += `*Exported on: ${date}*\n\n---\n`;
|
||||
|
||||
sections.forEach((section, idx) => {
|
||||
if (section.userMessage) {
|
||||
md += `\n---\n`;
|
||||
md += `**🧑 User**
|
||||
`;
|
||||
md += `*${new Date(section.userMessage.createdAt).toLocaleString()}*\n\n`;
|
||||
md += `> ${section.userMessage.content.replace(/\n/g, '\n> ')}\n`;
|
||||
}
|
||||
|
||||
if (section.assistantMessage) {
|
||||
md += `\n---\n`;
|
||||
md += `**🤖 Assistant**
|
||||
`;
|
||||
md += `*${new Date(section.assistantMessage.createdAt).toLocaleString()}*\n\n`;
|
||||
md += `> ${section.assistantMessage.content.replace(/\n/g, '\n> ')}\n`;
|
||||
}
|
||||
|
||||
if (
|
||||
section.sourceMessage &&
|
||||
section.sourceMessage.sources &&
|
||||
section.sourceMessage.sources.length > 0
|
||||
) {
|
||||
md += `\n**Citations:**\n`;
|
||||
section.sourceMessage.sources.forEach((src: any, i: number) => {
|
||||
const url = src.metadata?.url || '';
|
||||
md += `- [${i + 1}] [${url}](${url})\n`;
|
||||
});
|
||||
}
|
||||
});
|
||||
md += '\n---\n';
|
||||
downloadFile(`${title || 'chat'}.md`, md, 'text/markdown');
|
||||
};
|
||||
|
||||
const exportAsPDF = (sections: Section[], title: string) => {
|
||||
const doc = new jsPDF();
|
||||
const date = new Date(
|
||||
sections[0]?.userMessage?.createdAt || Date.now(),
|
||||
).toLocaleString();
|
||||
let y = 15;
|
||||
const pageHeight = doc.internal.pageSize.height;
|
||||
doc.setFontSize(18);
|
||||
doc.text(`Chat Export: ${title}`, 10, y);
|
||||
y += 8;
|
||||
doc.setFontSize(11);
|
||||
doc.setTextColor(100);
|
||||
doc.text(`Exported on: ${date}`, 10, y);
|
||||
y += 8;
|
||||
doc.setDrawColor(200);
|
||||
doc.line(10, y, 200, y);
|
||||
y += 6;
|
||||
doc.setTextColor(30);
|
||||
|
||||
sections.forEach((section, idx) => {
|
||||
if (section.userMessage) {
|
||||
if (y > pageHeight - 30) {
|
||||
doc.addPage();
|
||||
y = 15;
|
||||
}
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('User', 10, y);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(120);
|
||||
doc.text(
|
||||
`${new Date(section.userMessage.createdAt).toLocaleString()}`,
|
||||
40,
|
||||
y,
|
||||
);
|
||||
y += 6;
|
||||
doc.setTextColor(30);
|
||||
doc.setFontSize(12);
|
||||
const userLines = doc.splitTextToSize(section.userMessage.content, 180);
|
||||
for (let i = 0; i < userLines.length; i++) {
|
||||
if (y > pageHeight - 20) {
|
||||
doc.addPage();
|
||||
y = 15;
|
||||
}
|
||||
doc.text(userLines[i], 12, y);
|
||||
y += 6;
|
||||
}
|
||||
y += 6;
|
||||
doc.setDrawColor(230);
|
||||
if (y > pageHeight - 10) {
|
||||
doc.addPage();
|
||||
y = 15;
|
||||
}
|
||||
doc.line(10, y, 200, y);
|
||||
y += 4;
|
||||
}
|
||||
|
||||
if (section.assistantMessage) {
|
||||
if (y > pageHeight - 30) {
|
||||
doc.addPage();
|
||||
y = 15;
|
||||
}
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('Assistant', 10, y);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(120);
|
||||
doc.text(
|
||||
`${new Date(section.assistantMessage.createdAt).toLocaleString()}`,
|
||||
40,
|
||||
y,
|
||||
);
|
||||
y += 6;
|
||||
doc.setTextColor(30);
|
||||
doc.setFontSize(12);
|
||||
const assistantLines = doc.splitTextToSize(
|
||||
section.assistantMessage.content,
|
||||
180,
|
||||
);
|
||||
for (let i = 0; i < assistantLines.length; i++) {
|
||||
if (y > pageHeight - 20) {
|
||||
doc.addPage();
|
||||
y = 15;
|
||||
}
|
||||
doc.text(assistantLines[i], 12, y);
|
||||
y += 6;
|
||||
}
|
||||
|
||||
if (
|
||||
section.sourceMessage &&
|
||||
section.sourceMessage.sources &&
|
||||
section.sourceMessage.sources.length > 0
|
||||
) {
|
||||
doc.setFontSize(11);
|
||||
doc.setTextColor(80);
|
||||
if (y > pageHeight - 20) {
|
||||
doc.addPage();
|
||||
y = 15;
|
||||
}
|
||||
doc.text('Citations:', 12, y);
|
||||
y += 5;
|
||||
section.sourceMessage.sources.forEach((src: any, i: number) => {
|
||||
const url = src.metadata?.url || '';
|
||||
if (y > pageHeight - 15) {
|
||||
doc.addPage();
|
||||
y = 15;
|
||||
}
|
||||
doc.text(`- [${i + 1}] ${url}`, 15, y);
|
||||
y += 5;
|
||||
});
|
||||
doc.setTextColor(30);
|
||||
}
|
||||
y += 6;
|
||||
doc.setDrawColor(230);
|
||||
if (y > pageHeight - 10) {
|
||||
doc.addPage();
|
||||
y = 15;
|
||||
}
|
||||
doc.line(10, y, 200, y);
|
||||
y += 4;
|
||||
}
|
||||
});
|
||||
doc.save(`${title || 'chat'}.pdf`);
|
||||
};
|
||||
|
||||
const Navbar = () => {
|
||||
const Navbar = ({
|
||||
chatId,
|
||||
messages,
|
||||
}: {
|
||||
messages: Message[];
|
||||
chatId: string;
|
||||
}) => {
|
||||
const [title, setTitle] = useState<string>('');
|
||||
const [timeAgo, setTimeAgo] = useState<string>('');
|
||||
|
||||
const { sections, chatId } = useChat();
|
||||
|
||||
useEffect(() => {
|
||||
if (sections.length > 0 && sections[0].userMessage) {
|
||||
if (messages.length > 0) {
|
||||
const newTitle =
|
||||
sections[0].userMessage.content.length > 20
|
||||
? `${sections[0].userMessage.content.substring(0, 20).trim()}...`
|
||||
: sections[0].userMessage.content;
|
||||
messages[0].content.length > 20
|
||||
? `${messages[0].content.substring(0, 20).trim()}...`
|
||||
: messages[0].content;
|
||||
setTitle(newTitle);
|
||||
const newTimeAgo = formatTimeDifference(
|
||||
new Date(),
|
||||
sections[0].userMessage.createdAt,
|
||||
messages[0].createdAt,
|
||||
);
|
||||
setTimeAgo(newTimeAgo);
|
||||
}
|
||||
}, [sections]);
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(() => {
|
||||
if (sections.length > 0 && sections[0].userMessage) {
|
||||
if (messages.length > 0) {
|
||||
const newTimeAgo = formatTimeDifference(
|
||||
new Date(),
|
||||
sections[0].userMessage.createdAt,
|
||||
messages[0].createdAt,
|
||||
);
|
||||
setTimeAgo(newTimeAgo);
|
||||
}
|
||||
@@ -228,91 +45,25 @@ const Navbar = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="sticky -mx-4 lg:mx-0 top-0 z-40 bg-light-primary/95 dark:bg-dark-primary/95 backdrop-blur-sm border-b border-light-200/50 dark:border-dark-200/30">
|
||||
<div className="px-4 lg:px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center min-w-0">
|
||||
<a
|
||||
href="/"
|
||||
className="lg:hidden mr-3 p-2 -ml-2 rounded-lg hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors duration-200"
|
||||
>
|
||||
<Edit size={18} className="text-black/70 dark:text-white/70" />
|
||||
</a>
|
||||
<div className="hidden lg:flex items-center gap-2 text-black/50 dark:text-white/50 min-w-0">
|
||||
<Clock size={14} />
|
||||
<span className="text-xs whitespace-nowrap">{timeAgo} ago</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="fixed z-40 top-0 left-0 right-0 px-4 lg:pl-[104px] lg:pr-6 lg:px-8 flex flex-row items-center justify-between w-full py-4 text-sm text-black dark:text-white/70 border-b bg-light-primary dark:bg-dark-primary border-light-100 dark:border-dark-200">
|
||||
<a
|
||||
href="/"
|
||||
className="active:scale-95 transition duration-100 cursor-pointer lg:hidden"
|
||||
>
|
||||
<Edit size={17} />
|
||||
</a>
|
||||
<div className="hidden lg:flex flex-row items-center justify-center space-x-2">
|
||||
<Clock size={17} />
|
||||
<p className="text-xs">{timeAgo} ago</p>
|
||||
</div>
|
||||
<p className="hidden lg:flex">{title}</p>
|
||||
|
||||
<div className="flex-1 mx-4 min-w-0">
|
||||
<h1 className="text-center text-sm font-medium text-black/80 dark:text-white/90 truncate">
|
||||
{title || 'New Conversation'}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 min-w-0">
|
||||
<Popover className="relative">
|
||||
<PopoverButton className="p-2 rounded-lg hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors duration-200">
|
||||
<Share size={16} className="text-black/60 dark:text-white/60" />
|
||||
</PopoverButton>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<PopoverPanel className="absolute right-0 mt-2 w-64 origin-top-right rounded-2xl bg-light-primary dark:bg-dark-primary border border-light-200 dark:border-dark-200 shadow-xl shadow-black/10 dark:shadow-black/30 z-50">
|
||||
<div className="p-3">
|
||||
<div className="mb-2">
|
||||
<p className="text-xs font-medium text-black/40 dark:text-white/40 uppercase tracking-wide">
|
||||
Export Chat
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<button
|
||||
className="w-full flex items-center gap-3 px-3 py-2 text-left rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors duration-200"
|
||||
onClick={() => exportAsMarkdown(sections, title || '')}
|
||||
>
|
||||
<FileText size={16} className="text-[#24A0ED]" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-black dark:text-white">
|
||||
Markdown
|
||||
</p>
|
||||
<p className="text-xs text-black/50 dark:text-white/50">
|
||||
.md format
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
className="w-full flex items-center gap-3 px-3 py-2 text-left rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors duration-200"
|
||||
onClick={() => exportAsPDF(sections, title || '')}
|
||||
>
|
||||
<FileDown size={16} className="text-[#24A0ED]" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-black dark:text-white">
|
||||
PDF
|
||||
</p>
|
||||
<p className="text-xs text-black/50 dark:text-white/50">
|
||||
Document format
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
<DeleteChat
|
||||
redirect
|
||||
chatId={chatId!}
|
||||
chats={[]}
|
||||
setChats={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row items-center space-x-4">
|
||||
<Share
|
||||
size={17}
|
||||
className="active:scale-95 transition duration-100 cursor-pointer"
|
||||
/>
|
||||
<DeleteChat redirect chatId={chatId} chats={[]} setChats={() => {}} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface Article {
|
||||
title: string;
|
||||
content: string;
|
||||
url: string;
|
||||
thumbnail: string;
|
||||
}
|
||||
|
||||
const NewsArticleWidget = () => {
|
||||
const [article, setArticle] = useState<Article | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/discover?mode=preview')
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const articles = (data.blogs || []).filter((a: Article) => a.thumbnail);
|
||||
setArticle(articles[Math.floor(Math.random() * articles.length)]);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-light-secondary dark:bg-dark-secondary rounded-2xl border border-light-200 dark:border-dark-200 shadow-sm shadow-light-200/10 dark:shadow-black/25 flex flex-row items-stretch w-full h-24 min-h-[96px] max-h-[96px] p-0 overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="animate-pulse flex flex-row items-stretch w-full h-full">
|
||||
<div className="w-24 min-w-24 max-w-24 h-full bg-light-200 dark:bg-dark-200" />
|
||||
<div className="flex flex-col justify-center flex-1 px-3 py-2 gap-2">
|
||||
<div className="h-4 w-3/4 rounded bg-light-200 dark:bg-dark-200" />
|
||||
<div className="h-3 w-1/2 rounded bg-light-200 dark:bg-dark-200" />
|
||||
</div>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="w-full text-xs text-red-400">Could not load news.</div>
|
||||
) : article ? (
|
||||
<a
|
||||
href={`/?q=Summary: ${article.url}`}
|
||||
className="flex flex-row items-stretch w-full h-full relative overflow-hidden group"
|
||||
>
|
||||
<div className="relative w-24 min-w-24 max-w-24 h-full overflow-hidden">
|
||||
<img
|
||||
className="object-cover w-full h-full bg-light-200 dark:bg-dark-200 group-hover:scale-110 transition-transform duration-300"
|
||||
src={
|
||||
new URL(article.thumbnail).origin +
|
||||
new URL(article.thumbnail).pathname +
|
||||
`?id=${new URL(article.thumbnail).searchParams.get('id')}`
|
||||
}
|
||||
alt={article.title}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center flex-1 px-3 py-2">
|
||||
<div className="font-semibold text-xs text-black dark:text-white leading-tight line-clamp-2 mb-1">
|
||||
{article.title}
|
||||
</div>
|
||||
<p className="text-black/60 dark:text-white/60 text-[10px] leading-relaxed line-clamp-2">
|
||||
{article.content}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewsArticleWidget;
|
||||