mirror of
https://github.com/jaypyles/Scraperr.git
synced 2026-09-25 12:55:39 +00:00
feat: add AI integration (#27)
* wip: add AI integration| * wip: add job selection with popover * wip: add AI integration * wip: add in openai support * wip: add in query params and work on ui * wip: finalize UI * chore: cleanup and docs
This commit is contained in:
+2
-1
@@ -185,4 +185,5 @@ cython_debug/
|
||||
.pdm-python
|
||||
.next
|
||||
postgres_data
|
||||
.vscode
|
||||
.vscode
|
||||
ollama
|
||||
|
||||
@@ -52,6 +52,16 @@ From the table, users can download an excel sheet of the job's results, along wi
|
||||
|
||||

|
||||
|
||||
### AI Integration
|
||||
|
||||
- Include the results of a selected job into the context of a conversation
|
||||
- Currently supports:
|
||||
|
||||
1. Ollama
|
||||
2. OpenAI
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository:
|
||||
@@ -117,6 +127,22 @@ Use this service as an API for your own projects. Due to this using FastAPI, a d
|
||||
|
||||

|
||||
|
||||
## AI
|
||||
|
||||
Currently supports either an Ollama instance or OpenAI's ChatGPT, using your own API key. Setting up is easy as either setting the Ollama url or the OpenAI API key in the API's environmental variables in the `docker-compose.yml` file:
|
||||
|
||||
```yaml
|
||||
scraperr_api:
|
||||
environment:
|
||||
- OLLAMA_URL=http://ollama:11434
|
||||
- OLLAMA_MODEL=llama3.1
|
||||
# or
|
||||
- OPENAI_KEY=<your_key>
|
||||
- OPENAI_MODEL=gpt3.5-turbo
|
||||
```
|
||||
|
||||
The model's names are taken from the documentation of their respective technologies.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Q: When running Scraperr, I'm met with "404 Page not found".
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# STL
|
||||
import os
|
||||
import logging
|
||||
from collections.abc import Iterable, AsyncGenerator
|
||||
|
||||
# PDM
|
||||
from openai import OpenAI
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from openai.types.chat import ChatCompletionMessageParam
|
||||
|
||||
# LOCAL
|
||||
from ollama import Message, AsyncClient
|
||||
from api.backend.models import AI
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
ai_router = APIRouter()
|
||||
|
||||
# Load environment variables
|
||||
open_ai_key = os.getenv("OPENAI_KEY")
|
||||
open_ai_model = os.getenv("OPENAI_MODEL")
|
||||
llama_url = os.getenv("OLLAMA_URL")
|
||||
llama_model = os.getenv("OLLAMA_MODEL")
|
||||
|
||||
# Initialize clients
|
||||
openai_client = OpenAI(api_key=open_ai_key) if open_ai_key else None
|
||||
llama_client = AsyncClient(host=llama_url) if llama_url else None
|
||||
|
||||
|
||||
async def llama_chat(chat_messages: list[Message]) -> AsyncGenerator[str, None]:
|
||||
if llama_client and llama_model:
|
||||
try:
|
||||
async for part in await llama_client.chat(
|
||||
model=llama_model, messages=chat_messages, stream=True
|
||||
):
|
||||
yield part["message"]["content"]
|
||||
except Exception as e:
|
||||
LOG.error(f"Error during chat: {e}")
|
||||
yield "An error occurred while processing your request."
|
||||
|
||||
|
||||
async def openai_chat(
|
||||
chat_messages: Iterable[ChatCompletionMessageParam],
|
||||
) -> AsyncGenerator[str, None]:
|
||||
if openai_client and open_ai_model:
|
||||
try:
|
||||
response = openai_client.chat.completions.create(
|
||||
model=open_ai_model, messages=chat_messages, stream=True
|
||||
)
|
||||
for part in response:
|
||||
yield part.choices[0].delta.content or ""
|
||||
except Exception as e:
|
||||
LOG.error(f"Error during OpenAI chat: {e}")
|
||||
yield "An error occurred while processing your request."
|
||||
|
||||
|
||||
chat_function = llama_chat if llama_client else openai_chat
|
||||
|
||||
|
||||
@ai_router.post("/ai")
|
||||
async def ai(c: AI):
|
||||
return StreamingResponse(
|
||||
chat_function(chat_messages=c.messages), media_type="text/plain"
|
||||
)
|
||||
|
||||
|
||||
@ai_router.get("/ai/check")
|
||||
async def check():
|
||||
return JSONResponse(content=bool(open_ai_key or llama_model))
|
||||
+19
-2
@@ -26,10 +26,12 @@ from api.backend.job import (
|
||||
from api.backend.models import (
|
||||
UpdateJobs,
|
||||
DownloadJob,
|
||||
FetchOptions,
|
||||
SubmitScrapeJob,
|
||||
DeleteScrapeJobs,
|
||||
)
|
||||
from api.backend.schemas import User
|
||||
from api.backend.ai.ai_router import ai_router
|
||||
from api.backend.auth.auth_utils import get_current_user
|
||||
from api.backend.auth.auth_router import auth_router
|
||||
|
||||
@@ -59,6 +61,7 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(title="api")
|
||||
app.include_router(auth_router)
|
||||
app.include_router(ai_router)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
@@ -91,16 +94,30 @@ async def submit_scrape_job(job: SubmitScrapeJob, background_tasks: BackgroundTa
|
||||
|
||||
|
||||
@app.post("/retrieve-scrape-jobs")
|
||||
async def retrieve_scrape_jobs(user: User = Depends(get_current_user)):
|
||||
async def retrieve_scrape_jobs(
|
||||
fetch_options: FetchOptions, user: User = Depends(get_current_user)
|
||||
):
|
||||
LOG.info(f"Retrieving jobs for account: {user.email}")
|
||||
try:
|
||||
results = await query({"user": user.email})
|
||||
results = await query({"user": user.email}, fetch_options=fetch_options)
|
||||
return JSONResponse(content=jsonable_encoder(results[::-1]))
|
||||
except Exception as e:
|
||||
LOG.error(f"Exception occurred: {e}")
|
||||
return JSONResponse(content={"error": str(e)}, status_code=500)
|
||||
|
||||
|
||||
@app.get("/job/{id}")
|
||||
async def job(id: str, user: User = Depends(get_current_user)):
|
||||
LOG.info(f"Retrieving jobs for account: {user.email}")
|
||||
try:
|
||||
filter = {"user": user.email, "id": id}
|
||||
results = await query(filter)
|
||||
return JSONResponse(content=jsonable_encoder(results))
|
||||
except Exception as e:
|
||||
LOG.error(f"Exception occurred: {e}")
|
||||
return JSONResponse(content={"error": str(e)}, status_code=500)
|
||||
|
||||
|
||||
def clean_text(text: str):
|
||||
text = text.replace("\r\n", "\n") # Normalize newlines
|
||||
text = text.replace("\n", "\\n") # Escape newlines
|
||||
|
||||
+12
-3
@@ -1,9 +1,12 @@
|
||||
# STL
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, Optional
|
||||
|
||||
# PDM
|
||||
from pymongo import DESCENDING
|
||||
|
||||
# LOCAL
|
||||
from api.backend.models import FetchOptions
|
||||
from api.backend.database import get_job_collection
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@@ -22,13 +25,19 @@ async def get_queued_job():
|
||||
)
|
||||
|
||||
|
||||
async def query(filter: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
async def query(
|
||||
filter: dict[str, Any], fetch_options: Optional[FetchOptions] = None
|
||||
) -> list[dict[str, Any]]:
|
||||
collection = get_job_collection()
|
||||
cursor = collection.find(filter)
|
||||
results: list[dict[str, Any]] = []
|
||||
|
||||
async for document in cursor:
|
||||
del document["_id"]
|
||||
|
||||
if fetch_options and not fetch_options.chat and document.get("chat"):
|
||||
del document["chat"]
|
||||
|
||||
results.append(document)
|
||||
|
||||
return results
|
||||
@@ -46,7 +55,7 @@ async def update_job(ids: list[str], field: str, value: Any):
|
||||
async def delete_jobs(jobs: list[str]):
|
||||
collection = get_job_collection()
|
||||
result = await collection.delete_many({"id": {"$in": jobs}})
|
||||
LOG.info(f"RESULT: {result.deleted_count} documents deleted")
|
||||
LOG.info(f"{result.deleted_count} documents deleted")
|
||||
|
||||
return True if result.deleted_count > 0 else False
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ from datetime import datetime
|
||||
import pydantic
|
||||
|
||||
|
||||
class FetchOptions(pydantic.BaseModel):
|
||||
chat: Optional[bool] = None
|
||||
|
||||
|
||||
class Element(pydantic.BaseModel):
|
||||
name: str
|
||||
xpath: str
|
||||
@@ -32,6 +36,7 @@ class SubmitScrapeJob(pydantic.BaseModel):
|
||||
result: Optional[dict[str, Any]] = None
|
||||
job_options: JobOptions
|
||||
status: str = "Queued"
|
||||
chat: Optional[str] = None
|
||||
|
||||
|
||||
class RetrieveScrapeJobs(pydantic.BaseModel):
|
||||
@@ -54,3 +59,7 @@ class UpdateJobs(pydantic.BaseModel):
|
||||
ids: list[str]
|
||||
field: str
|
||||
value: Any
|
||||
|
||||
|
||||
class AI(pydantic.BaseModel):
|
||||
messages: list[Any]
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
version: "3"
|
||||
services:
|
||||
scraperr:
|
||||
environment:
|
||||
- HOSTNAME_DEV=localhost
|
||||
# - NEXT_PUBLIC_API_PATH=http://scraperr_api.$HOSTNAME_DEV
|
||||
command: ["npm", "run", "dev"]
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.scraperr.rule=Host(`${HOSTNAME_DEV}`)"
|
||||
- "traefik.http.routers.scraperr.rule=Host(`localhost`)"
|
||||
- "traefik.http.routers.scraperr.entrypoints=web"
|
||||
- "traefik.http.services.scraperr.loadbalancer.server.port=3000"
|
||||
- "traefik.http.routers.scraperr.tls=false"
|
||||
|
||||
@@ -21,6 +21,8 @@ services:
|
||||
dockerfile: docker/api/Dockerfile
|
||||
environment:
|
||||
- LOG_LEVEL=INFO
|
||||
- OLLAMA_URL=http://ollama:11434
|
||||
- OLLAMA_MODEL=phi3
|
||||
- MONGODB_URI=mongodb://root:example@webscrape-mongo:27017 # used to access MongoDB
|
||||
- SECRET_KEY=your_secret_key # used to encode authentication tokens (can be a random string)
|
||||
- ALGORITHM=HS256 # authentication encoding algorithm
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 60 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 46 KiB |
Generated
+463
@@ -14,6 +14,7 @@
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@fontsource/roboto": "^5.0.13",
|
||||
"@minchat/react-chat-ui": "^0.16.2",
|
||||
"@mui/icons-material": "^5.15.3",
|
||||
"@mui/material": "^5.16.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
@@ -36,6 +37,7 @@
|
||||
"react-router": "^6.14.1",
|
||||
"react-router-dom": "^6.14.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-spinners": "^0.14.1",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
@@ -3589,6 +3591,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/stylis": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
|
||||
"integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
|
||||
},
|
||||
"node_modules/@emotion/unitless": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
|
||||
@@ -4536,6 +4543,25 @@
|
||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
|
||||
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A=="
|
||||
},
|
||||
"node_modules/@minchat/react-chat-ui": {
|
||||
"version": "0.16.2",
|
||||
"resolved": "https://registry.npmjs.org/@minchat/react-chat-ui/-/react-chat-ui-0.16.2.tgz",
|
||||
"integrity": "sha512-qeYm7IxEXoTQNvjoHa17Go/adMo0ly5TrO84PsvPlLUcOpu1XZTQ4m47Ov7eY8mmmw+jIx6HUuKWtg/Qb5kg0w==",
|
||||
"dependencies": {
|
||||
"@types/styled-components": "^5.1.28",
|
||||
"postcss": "^8.4.31",
|
||||
"rollup-plugin-import-css": "^3.3.4",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-scss": "^4.0.0",
|
||||
"styled-components": "^5.3.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/base": {
|
||||
"version": "5.0.0-beta.40",
|
||||
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz",
|
||||
@@ -5936,6 +5962,15 @@
|
||||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hoist-non-react-statics": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz",
|
||||
"integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==",
|
||||
"dependencies": {
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/html-minifier-terser": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
|
||||
@@ -6383,6 +6418,16 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
||||
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw=="
|
||||
},
|
||||
"node_modules/@types/styled-components": {
|
||||
"version": "5.1.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.34.tgz",
|
||||
"integrity": "sha512-mmiVvwpYklFIv9E8qfxuPyIt/OuyIrn6gMOAMOFUO3WJfSrSE+sGUoa4PiZj77Ut7bKZpaa6o1fBKS/4TOEvnA==",
|
||||
"dependencies": {
|
||||
"@types/hoist-non-react-statics": "*",
|
||||
"@types/react": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/testing-library__jest-dom": {
|
||||
"version": "5.14.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.8.tgz",
|
||||
@@ -7597,6 +7642,21 @@
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-styled-components": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz",
|
||||
"integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==",
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.22.5",
|
||||
"@babel/helper-module-imports": "^7.22.5",
|
||||
"@babel/plugin-syntax-jsx": "^7.22.5",
|
||||
"lodash": "^4.17.21",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"styled-components": ">= 2"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-transform-react-remove-prop-types": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz",
|
||||
@@ -7952,6 +8012,14 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/camelize": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
|
||||
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-api": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
|
||||
@@ -8305,6 +8373,22 @@
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
},
|
||||
"node_modules/concat-with-sourcemaps": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz",
|
||||
"integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==",
|
||||
"dependencies": {
|
||||
"source-map": "^0.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-with-sourcemaps/node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/confusing-browser-globals": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
|
||||
@@ -8462,6 +8546,14 @@
|
||||
"tiny-invariant": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/css-color-keywords": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
|
||||
"integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/css-declaration-sorter": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz",
|
||||
@@ -8673,6 +8765,16 @@
|
||||
"resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
|
||||
"integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w=="
|
||||
},
|
||||
"node_modules/css-to-react-native": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
|
||||
"integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
|
||||
"dependencies": {
|
||||
"camelize": "^1.0.0",
|
||||
"css-color-keywords": "^1.0.0",
|
||||
"postcss-value-parser": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "1.0.0-alpha.37",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
|
||||
@@ -10918,6 +11020,22 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/generic-names": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/generic-names/-/generic-names-4.0.0.tgz",
|
||||
"integrity": "sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==",
|
||||
"dependencies": {
|
||||
"loader-utils": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/generic-names/node_modules/loader-utils": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz",
|
||||
"integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==",
|
||||
"engines": {
|
||||
"node": ">= 12.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
@@ -11539,6 +11657,11 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/icss-replace-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg=="
|
||||
},
|
||||
"node_modules/icss-utils": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
|
||||
@@ -11583,6 +11706,17 @@
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/import-cwd": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz",
|
||||
"integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==",
|
||||
"dependencies": {
|
||||
"import-from": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
@@ -11606,6 +11740,17 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/import-from": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz",
|
||||
"integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==",
|
||||
"dependencies": {
|
||||
"resolve-from": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/import-local": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",
|
||||
@@ -14458,6 +14603,11 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
@@ -15764,6 +15914,14 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
@@ -15792,6 +15950,21 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-queue": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
|
||||
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^4.0.4",
|
||||
"p-timeout": "^3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-retry": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
|
||||
@@ -15804,6 +15977,17 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-timeout": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
|
||||
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
|
||||
"dependencies": {
|
||||
"p-finally": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
@@ -16752,6 +16936,24 @@
|
||||
"postcss": "^8.2.15"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-modules": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-4.3.1.tgz",
|
||||
"integrity": "sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==",
|
||||
"dependencies": {
|
||||
"generic-names": "^4.0.0",
|
||||
"icss-replace-symbols": "^1.1.0",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"postcss-modules-extract-imports": "^3.0.0",
|
||||
"postcss-modules-local-by-default": "^4.0.0",
|
||||
"postcss-modules-scope": "^3.0.0",
|
||||
"postcss-modules-values": "^4.0.0",
|
||||
"string-hash": "^1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-modules-extract-imports": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
|
||||
@@ -17394,6 +17596,14 @@
|
||||
"asap": "~2.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/promise.series": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/promise.series/-/promise.series-0.2.0.tgz",
|
||||
"integrity": "sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ==",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/prompts": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||
@@ -18043,6 +18253,15 @@
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/react-spinners": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.14.1.tgz",
|
||||
"integrity": "sha512-2Izq+qgQ08HTofCVEdcAQCXFEYfqTDdfeDQJeo/HHQiQJD4imOicNLhkfN2eh1NYEWVOX4D9ok2lhuDB0z3Aag==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
|
||||
@@ -18436,6 +18655,183 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-import-css": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-import-css/-/rollup-plugin-import-css-3.5.0.tgz",
|
||||
"integrity": "sha512-JOVow6n00qt2C/NnsqPmIjFOfxIAudwWqC5SaC84CodMGiMFaP1gPAdgnJ8g8hcG+P85TCYp2kI98grYCEt5pg==",
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^5.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^2.x.x || ^3.x.x || ^4.x.x"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-import-css/node_modules/@rollup/pluginutils": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
|
||||
"integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-import-css/node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
|
||||
},
|
||||
"node_modules/rollup-plugin-postcss": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-postcss/-/rollup-plugin-postcss-4.0.2.tgz",
|
||||
"integrity": "sha512-05EaY6zvZdmvPUDi3uCcAQoESDcYnv8ogJJQRp6V5kZ6J6P7uAVJlrTZcaaA20wTH527YTnKfkAoPxWI/jPp4w==",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.0",
|
||||
"concat-with-sourcemaps": "^1.1.0",
|
||||
"cssnano": "^5.0.1",
|
||||
"import-cwd": "^3.0.0",
|
||||
"p-queue": "^6.6.2",
|
||||
"pify": "^5.0.0",
|
||||
"postcss-load-config": "^3.0.0",
|
||||
"postcss-modules": "^4.0.0",
|
||||
"promise.series": "^0.2.0",
|
||||
"resolve": "^1.19.0",
|
||||
"rollup-pluginutils": "^2.8.2",
|
||||
"safe-identifier": "^0.4.2",
|
||||
"style-inject": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-postcss/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-postcss/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-postcss/node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-postcss/node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/rollup-plugin-postcss/node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-postcss/node_modules/pify": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz",
|
||||
"integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-postcss/node_modules/postcss-load-config": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz",
|
||||
"integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==",
|
||||
"dependencies": {
|
||||
"lilconfig": "^2.0.5",
|
||||
"yaml": "^1.10.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": ">=8.0.9",
|
||||
"ts-node": ">=9.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"postcss": {
|
||||
"optional": true
|
||||
},
|
||||
"ts-node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-postcss/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-scss": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-scss/-/rollup-plugin-scss-4.0.0.tgz",
|
||||
"integrity": "sha512-wxasNXDYC2m+fDxCMgK00WebVWYmeFvShyNABmjvSJZ6D1/SepwqFeaMFMQromveI79gfvb64yJjiZZxSZxEIA==",
|
||||
"dependencies": {
|
||||
"rollup-pluginutils": "^2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-terser": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz",
|
||||
@@ -18491,6 +18887,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-pluginutils": {
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
|
||||
"integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
|
||||
"dependencies": {
|
||||
"estree-walker": "^0.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-pluginutils/node_modules/estree-walker": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
|
||||
"integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
@@ -18549,6 +18958,11 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/safe-identifier": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz",
|
||||
"integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w=="
|
||||
},
|
||||
"node_modules/safe-regex-test": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
|
||||
@@ -18817,6 +19231,11 @@
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||
},
|
||||
"node_modules/shallowequal": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
|
||||
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -19056,6 +19475,11 @@
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-hash": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz",
|
||||
"integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A=="
|
||||
},
|
||||
"node_modules/string-length": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
|
||||
@@ -19221,6 +19645,11 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/style-inject": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz",
|
||||
"integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw=="
|
||||
},
|
||||
"node_modules/style-loader": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz",
|
||||
@@ -19253,6 +19682,40 @@
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/styled-components": {
|
||||
"version": "5.3.11",
|
||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz",
|
||||
"integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.0.0",
|
||||
"@babel/traverse": "^7.4.5",
|
||||
"@emotion/is-prop-valid": "^1.1.0",
|
||||
"@emotion/stylis": "^0.8.4",
|
||||
"@emotion/unitless": "^0.7.4",
|
||||
"babel-plugin-styled-components": ">= 1.12.0",
|
||||
"css-to-react-native": "^3.0.0",
|
||||
"hoist-non-react-statics": "^3.0.0",
|
||||
"shallowequal": "^1.1.0",
|
||||
"supports-color": "^5.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/styled-components"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8.0",
|
||||
"react-dom": ">= 16.8.0",
|
||||
"react-is": ">= 16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/styled-components/node_modules/@emotion/unitless": {
|
||||
"version": "0.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
|
||||
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
|
||||
},
|
||||
"node_modules/styled-jsx": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@fontsource/roboto": "^5.0.13",
|
||||
"@minchat/react-chat-ui": "^0.16.2",
|
||||
"@mui/icons-material": "^5.15.3",
|
||||
"@mui/material": "^5.16.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
@@ -31,6 +32,7 @@
|
||||
"react-router": "^6.14.1",
|
||||
"react-router-dom": "^6.14.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-spinners": "^0.14.1",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
|
||||
@@ -34,6 +34,8 @@ dependencies = [
|
||||
"blinker<1.8.0",
|
||||
"setuptools>=71.0.4",
|
||||
"docker>=7.1.0",
|
||||
"ollama>=0.3.0",
|
||||
"openai>=1.37.1",
|
||||
]
|
||||
requires-python = ">=3.10"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import React from "react";
|
||||
|
||||
export const Chat = () => {
|
||||
return <h1>Chat</h1>;
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
import React, { useState, useEffect, Dispatch, useRef } from "react";
|
||||
import { Job } from "../../types";
|
||||
import { fetchJobs } from "../../lib";
|
||||
import Box from "@mui/material/Box";
|
||||
import InputLabel from "@mui/material/InputLabel";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import Select from "@mui/material/Select";
|
||||
import Popover from "@mui/material/Popover";
|
||||
import { Typography, MenuItem, useTheme } from "@mui/material";
|
||||
import { SxProps } from "@mui/material";
|
||||
|
||||
interface Props {
|
||||
sxProps: SxProps;
|
||||
setSelectedJob: Dispatch<React.SetStateAction<Job | null>>;
|
||||
selectedJob: Job | null;
|
||||
setJobs: Dispatch<React.SetStateAction<Job[]>>;
|
||||
jobs: Job[];
|
||||
}
|
||||
|
||||
export const JobSelector = ({
|
||||
sxProps,
|
||||
selectedJob,
|
||||
setSelectedJob,
|
||||
setJobs,
|
||||
jobs,
|
||||
}: Props) => {
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||
const [popoverJob, setPopoverJob] = useState<Job | null>(null);
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
fetchJobs(setJobs, { chat: true });
|
||||
}, []);
|
||||
|
||||
const handlePopoverOpen = (
|
||||
event: React.MouseEvent<HTMLElement>,
|
||||
job: Job
|
||||
) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setPopoverJob(job);
|
||||
};
|
||||
|
||||
const handlePopoverClose = () => {
|
||||
setAnchorEl(null);
|
||||
setPopoverJob(null);
|
||||
};
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
return (
|
||||
<Box sx={sxProps}>
|
||||
<FormControl fullWidth>
|
||||
{jobs.length ? (
|
||||
<>
|
||||
<InputLabel id="select-job">Job</InputLabel>
|
||||
<Select
|
||||
labelId="select-job"
|
||||
id="select-job"
|
||||
value={selectedJob?.id || ""}
|
||||
label="Job"
|
||||
onChange={(e) => {
|
||||
setSelectedJob(
|
||||
jobs.find((job) => job.id === e.target.value) || null
|
||||
);
|
||||
}}
|
||||
>
|
||||
{jobs.map((job) => (
|
||||
<MenuItem
|
||||
key={job.id}
|
||||
value={job.id}
|
||||
aria-owns={open ? "mouse-over-popover" : undefined}
|
||||
aria-haspopup="true"
|
||||
onMouseEnter={(e) => handlePopoverOpen(e, job)}
|
||||
onMouseLeave={handlePopoverClose}
|
||||
onClick={handlePopoverClose}
|
||||
>
|
||||
{job.id}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</>
|
||||
) : null}
|
||||
</FormControl>
|
||||
<Popover
|
||||
id="mouse-over-popover"
|
||||
sx={{
|
||||
pointerEvents: "none",
|
||||
padding: 0,
|
||||
}}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
onClose={handlePopoverClose}
|
||||
>
|
||||
{popoverJob && (
|
||||
<Box
|
||||
sx={{
|
||||
border:
|
||||
theme.palette.mode === "light"
|
||||
? "2px solid black"
|
||||
: "2px solid white",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ paddingLeft: 1, paddingRight: 1 }}
|
||||
>
|
||||
{popoverJob.url}
|
||||
</Typography>
|
||||
<div className="flex flex-row w-full justify-end mb-1">
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
paddingLeft: 1,
|
||||
paddingRight: 1,
|
||||
color: theme.palette.mode === "dark" ? "#d3d7e6" : "#5b5d63",
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
>
|
||||
{new Date(popoverJob.time_created).toLocaleString()}
|
||||
</Typography>
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
</Popover>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./Chat";
|
||||
export * from "./JobSelector";
|
||||
@@ -23,6 +23,7 @@ import HttpIcon from "@mui/icons-material/Http";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import TerminalIcon from "@mui/icons-material/Terminal";
|
||||
import BarChart from "@mui/icons-material/BarChart";
|
||||
import AutoAwesomeIcon from "@mui/icons-material/AutoAwesome";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
interface NavDrawerProps {
|
||||
@@ -81,6 +82,15 @@ export const NavDrawer: React.FC<NavDrawerProps> = ({
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<ListItemButton onClick={() => router.push("/chat")}>
|
||||
<ListItemIcon>
|
||||
<AutoAwesomeIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Chat" />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<ListItemButton onClick={() => router.push("/statistics")}>
|
||||
<ListItemIcon>
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import StarIcon from "@mui/icons-material/Star";
|
||||
import { Job } from "../../types";
|
||||
import { AutoAwesome } from "@mui/icons-material";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
interface stringMap {
|
||||
[key: string]: string;
|
||||
@@ -47,6 +49,7 @@ export const JobQueue = ({
|
||||
onFavorite,
|
||||
}: Props) => {
|
||||
const { selectedJobs, filteredJobs } = stateProps;
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Table sx={{ tableLayout: "fixed", width: "100%" }}>
|
||||
@@ -70,6 +73,22 @@ export const JobQueue = ({
|
||||
checked={selectedJobs.has(row.id)}
|
||||
onChange={() => onSelectJob(row.id)}
|
||||
/>
|
||||
<Tooltip title="Chat with AI">
|
||||
<span>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
router.push({
|
||||
pathname: "/chat",
|
||||
query: {
|
||||
job: row.id,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<AutoAwesome />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title="Favorite Job">
|
||||
<span>
|
||||
<IconButton
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./constants";
|
||||
export * from "./utils";
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import Cookies from "js-cookie";
|
||||
import React, { Dispatch } from "react";
|
||||
import { Job } from "../types";
|
||||
|
||||
interface fetchOptions {
|
||||
chat?: boolean;
|
||||
}
|
||||
|
||||
export const fetchJobs = async (
|
||||
setJobs: Dispatch<React.SetStateAction<Job[]>>,
|
||||
fetchOptions: fetchOptions = {}
|
||||
) => {
|
||||
const token = Cookies.get("token");
|
||||
await fetch(`/api/retrieve-scrape-jobs`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(fetchOptions),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => setJobs(data))
|
||||
.catch((error) => {
|
||||
console.error("Error fetching jobs:", error);
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchJob = async (id: string) => {
|
||||
const token = Cookies.get("token");
|
||||
try {
|
||||
const response = await fetch(`/api/job/${id}`, {
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching jobs:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const checkAI = async (
|
||||
setAiEnabled: Dispatch<React.SetStateAction<boolean>>
|
||||
) => {
|
||||
const token = Cookies.get("token");
|
||||
try {
|
||||
const response = await fetch(`/api/ai/check`, {
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
setAiEnabled(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching jobs:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateJob = async (ids: string[], field: string, value: any) => {
|
||||
const token = Cookies.get("token");
|
||||
const postBody = {
|
||||
ids: ids,
|
||||
field: field,
|
||||
value: value,
|
||||
};
|
||||
await fetch(`/api/update`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(postBody),
|
||||
}).catch((error) => {
|
||||
console.error("Error fetching jobs:", error);
|
||||
});
|
||||
};
|
||||
+4
-3
@@ -38,15 +38,16 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
<AuthProvider>
|
||||
<ThemeProvider theme={isDarkMode ? darkTheme : lightTheme}>
|
||||
<CssBaseline />
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<Box sx={{ height: "100%", display: "flex" }}>
|
||||
<NavDrawer isDarkMode={isDarkMode} toggleTheme={toggleTheme} />
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
p: 3,
|
||||
bgcolor: "background.default",
|
||||
minHeight: "100vh",
|
||||
overflow: "hidden",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
TextField,
|
||||
Typography,
|
||||
Paper,
|
||||
useTheme,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import { JobSelector } from "../components/ai";
|
||||
import { Job, Message } from "../types";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { checkAI, fetchJob, fetchJobs, updateJob } from "../lib";
|
||||
import SendIcon from "@mui/icons-material/Send";
|
||||
import EditNoteIcon from "@mui/icons-material/EditNote";
|
||||
|
||||
const AI: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
const [currentMessage, setCurrentMessage] = useState<string>("");
|
||||
const [selectedJob, setSelectedJob] = useState<Job | null>(null);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [aiEnabled, setAiEnabled] = useState<boolean>(false);
|
||||
const [jobs, setJobs] = useState<Job[]>([]);
|
||||
const [thinking, setThinking] = useState<boolean>(false);
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const getJobFromParam = async () => {
|
||||
const jobId = searchParams.get("job");
|
||||
if (jobId) {
|
||||
const job = await fetchJob(jobId);
|
||||
if (job.length) {
|
||||
setSelectedJob(job[0]);
|
||||
if (job[0].chat) {
|
||||
setMessages(job[0].chat);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkAI(setAiEnabled);
|
||||
getJobFromParam();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedJob?.chat) {
|
||||
setMessages(selectedJob?.chat);
|
||||
return;
|
||||
}
|
||||
|
||||
setMessages([]);
|
||||
}, [selectedJob]);
|
||||
|
||||
const handleMessageSend = async (msg: string) => {
|
||||
if (!selectedJob) {
|
||||
throw Error("Job is not currently selected, but should be.");
|
||||
}
|
||||
|
||||
const updatedMessages = await sendMessage(msg);
|
||||
await updateJob([selectedJob?.id], "chat", updatedMessages);
|
||||
};
|
||||
|
||||
const sendMessage = async (msg: string) => {
|
||||
const newMessage = {
|
||||
content: msg,
|
||||
role: "user",
|
||||
};
|
||||
|
||||
setMessages((prevMessages) => [...prevMessages, newMessage]);
|
||||
setCurrentMessage("");
|
||||
setThinking(true);
|
||||
|
||||
const jobMessage = {
|
||||
role: "system",
|
||||
content: `Here is the content return from a scraping job: ${JSON.stringify(
|
||||
selectedJob?.result
|
||||
)} for the url: ${
|
||||
selectedJob?.url
|
||||
}. The following messages will pertain to the content of the scraped job.`,
|
||||
};
|
||||
|
||||
const response = await fetch("/api/ai", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ messages: [jobMessage, ...messages, newMessage] }),
|
||||
});
|
||||
|
||||
const updatedMessages = [...messages, newMessage];
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
|
||||
let aiResponse = "";
|
||||
if (reader) {
|
||||
setThinking(false);
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
aiResponse += chunk;
|
||||
|
||||
setMessages((prevMessages) => {
|
||||
const lastMessage = prevMessages[prevMessages.length - 1];
|
||||
if (lastMessage && lastMessage.role === "assistant") {
|
||||
return [
|
||||
...prevMessages.slice(0, -1),
|
||||
{ ...lastMessage, content: aiResponse },
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
...prevMessages,
|
||||
{
|
||||
content: aiResponse,
|
||||
role: "assistant",
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return [...updatedMessages, { role: "assistant", content: aiResponse }];
|
||||
};
|
||||
|
||||
const handleNewChat = (selectedJob: Job) => {
|
||||
updateJob([selectedJob.id], "chat", []);
|
||||
setMessages([]);
|
||||
fetchJobs(setJobs, { chat: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "95vh",
|
||||
maxWidth: "100%",
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
borderRadius: "8px",
|
||||
border:
|
||||
theme.palette.mode === "light" ? "solid white" : "solid #4b5057",
|
||||
boxShadow: "0 4px 8px rgba(0, 0, 0, 0.1)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{aiEnabled ? (
|
||||
<>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
p: 2,
|
||||
textAlign: "center",
|
||||
fontSize: "1.2em",
|
||||
position: "relative",
|
||||
borderRadius: "8px 8px 0 0",
|
||||
borderBottom: `2px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
padding: theme.spacing(1),
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
flex: 1,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Chat with AI
|
||||
</Typography>
|
||||
<JobSelector
|
||||
selectedJob={selectedJob}
|
||||
setSelectedJob={setSelectedJob}
|
||||
setJobs={setJobs}
|
||||
jobs={jobs}
|
||||
sxProps={{
|
||||
position: "absolute",
|
||||
right: theme.spacing(2),
|
||||
width: "25%",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
flex: 1,
|
||||
p: 2,
|
||||
overflowY: "auto",
|
||||
maxHeight: "100%",
|
||||
}}
|
||||
>
|
||||
{!selectedJob ? (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
padding: 2,
|
||||
bgcolor: "rgba(128,128,128,0.1)",
|
||||
mt: 1,
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
className="rounded-md"
|
||||
>
|
||||
<Typography variant="body1">
|
||||
Select a Job to Begin Chatting
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
{messages.map((message, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
my: 2,
|
||||
p: 1,
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
|
||||
bgcolor:
|
||||
message.role === "user"
|
||||
? theme.palette.UserMessage.main
|
||||
: theme.palette.AIMessage.main,
|
||||
marginLeft: message.role === "user" ? "auto" : "",
|
||||
maxWidth: "40%",
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" sx={{ color: "white" }}>
|
||||
{message.content}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
{thinking && (
|
||||
<Box
|
||||
sx={{
|
||||
width: "full",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "start",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
bgcolor: "rgba(128,128,128,0.1)",
|
||||
maxWidth: "20%",
|
||||
my: 2,
|
||||
p: 1,
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
|
||||
}}
|
||||
variant="body1"
|
||||
>
|
||||
AI is thinking...
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
p: 2,
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<Tooltip title="New Chat" placement="top">
|
||||
<IconButton
|
||||
disabled={!(messages.length > 0)}
|
||||
sx={{ marginRight: 2 }}
|
||||
size="medium"
|
||||
onClick={() => {
|
||||
if (!selectedJob) {
|
||||
throw new Error("Selected job must be present but isn't.");
|
||||
}
|
||||
handleNewChat(selectedJob);
|
||||
}}
|
||||
>
|
||||
<EditNoteIcon fontSize="medium" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<TextField
|
||||
fullWidth
|
||||
placeholder="Type your message here..."
|
||||
disabled={!selectedJob}
|
||||
value={currentMessage}
|
||||
onChange={(e) => setCurrentMessage(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleMessageSend(currentMessage);
|
||||
}
|
||||
}}
|
||||
sx={{ borderRadius: "8px" }}
|
||||
/>
|
||||
|
||||
<Tooltip title="Send" placement="top">
|
||||
<IconButton
|
||||
color="primary"
|
||||
sx={{ ml: 2 }}
|
||||
disabled={!selectedJob}
|
||||
onClick={() => {
|
||||
handleMessageSend(currentMessage);
|
||||
}}
|
||||
>
|
||||
<SendIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<Box
|
||||
bgcolor="background.default"
|
||||
minHeight="100vh"
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<h4
|
||||
style={{
|
||||
color: "#fff",
|
||||
padding: "20px",
|
||||
borderRadius: "8px",
|
||||
background: "rgba(0, 0, 0, 0.6)",
|
||||
boxShadow: "0 4px 8px rgba(0, 0, 0, 0.2)",
|
||||
}}
|
||||
>
|
||||
Must set either OPENAI_KEY or OLLAMA_MODEL to use AI features.
|
||||
</h4>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AI;
|
||||
+1
-1
@@ -74,7 +74,7 @@ const Home = () => {
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
minHeight="100vh"
|
||||
height="100%"
|
||||
py={4}
|
||||
>
|
||||
<Container maxWidth="lg">
|
||||
|
||||
+5
-19
@@ -2,12 +2,12 @@ import React, { useEffect, useState } from "react";
|
||||
import { JobTable } from "../components/jobs";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { Box } from "@mui/system";
|
||||
import { Constants } from "../lib";
|
||||
import { Job } from "../types";
|
||||
import { GetServerSideProps } from "next/types";
|
||||
import axios from "axios";
|
||||
import { parseCookies } from "nookies";
|
||||
import Cookies from "js-cookie";
|
||||
import { fetchJobs } from "../lib";
|
||||
|
||||
interface JobsProps {
|
||||
initialJobs: Job[];
|
||||
@@ -59,7 +59,6 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const Jobs: React.FC<JobsProps> = ({ initialJobs, initialUser }) => {
|
||||
const { user, setUser } = useAuth();
|
||||
const [jobs, setJobs] = useState<Job[]>(initialJobs || []);
|
||||
const token = Cookies.get("token");
|
||||
|
||||
useEffect(() => {
|
||||
if (!user && initialUser) {
|
||||
@@ -67,31 +66,18 @@ const Jobs: React.FC<JobsProps> = ({ initialJobs, initialUser }) => {
|
||||
}
|
||||
}, [user, initialUser, setUser]);
|
||||
|
||||
const fetchJobs = async () => {
|
||||
await fetch(`${Constants.DOMAIN}/api/retrieve-scrape-jobs`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => setJobs(data))
|
||||
.catch((error) => {
|
||||
console.error("Error fetching jobs:", error);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
fetchJobs();
|
||||
fetchJobs(setJobs);
|
||||
} else {
|
||||
setJobs([]);
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(fetchJobs, 5000);
|
||||
const intervalId = setInterval(() => {
|
||||
fetchJobs(setJobs);
|
||||
}, 5000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
@tailwind utilities;
|
||||
|
||||
#__next {
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100vh;
|
||||
font-family: "Schibsted Grotesk", sans-serif;
|
||||
}
|
||||
|
||||
.MuiPopover-paper {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
+46
-16
@@ -1,4 +1,23 @@
|
||||
import { createTheme } from "@mui/material";
|
||||
import { createTheme } from "@mui/material/styles";
|
||||
|
||||
declare module "@mui/material/styles/createPalette" {
|
||||
interface Palette {
|
||||
AIMessage: Palette["primary"];
|
||||
UserMessage: Palette["primary"];
|
||||
customBorder: {
|
||||
light: string;
|
||||
dark: string;
|
||||
};
|
||||
}
|
||||
interface PaletteOptions {
|
||||
AIMessage?: PaletteOptions["primary"];
|
||||
UserMessage?: PaletteOptions["primary"];
|
||||
customBorder?: {
|
||||
light: string;
|
||||
dark: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const commonThemeOptions = {
|
||||
typography: {
|
||||
@@ -64,6 +83,14 @@ const lightTheme = createTheme({
|
||||
secondary: {
|
||||
main: "#dc004e",
|
||||
},
|
||||
AIMessage: {
|
||||
main: "#3863ff",
|
||||
contrastText: "#fff",
|
||||
},
|
||||
UserMessage: {
|
||||
main: "#606575",
|
||||
contrastText: "#fff",
|
||||
},
|
||||
background: {
|
||||
default: "#f4f6f8",
|
||||
paper: "#ffffff",
|
||||
@@ -73,8 +100,9 @@ const lightTheme = createTheme({
|
||||
secondary: "#333333",
|
||||
},
|
||||
},
|
||||
|
||||
...commonThemeOptions,
|
||||
typography: {
|
||||
...commonThemeOptions.typography,
|
||||
},
|
||||
components: {
|
||||
...commonThemeOptions.components,
|
||||
MuiButton: {
|
||||
@@ -90,6 +118,13 @@ const lightTheme = createTheme({
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPopover: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -102,6 +137,14 @@ const darkTheme = createTheme({
|
||||
secondary: {
|
||||
main: "#f48fb1",
|
||||
},
|
||||
AIMessage: {
|
||||
main: "rgba(3, 78, 252, 0.50)",
|
||||
contrastText: "#fff",
|
||||
},
|
||||
UserMessage: {
|
||||
main: "rgba(62, 72, 89, 1)",
|
||||
contrastText: "#fff",
|
||||
},
|
||||
background: {
|
||||
default: "#121212",
|
||||
paper: "#1e1e1e",
|
||||
@@ -139,19 +182,6 @@ const darkTheme = createTheme({
|
||||
},
|
||||
components: {
|
||||
...commonThemeOptions.components,
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
body: {
|
||||
fontFamily: '"Schibsted Grotesk", sans-serif',
|
||||
},
|
||||
html: {
|
||||
fontFamily: '"Schibsted Grotesk", sans-serif',
|
||||
},
|
||||
"*": {
|
||||
fontFamily: '"Schibsted Grotesk", sans-serif',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./element";
|
||||
export * from "./result";
|
||||
export * from "./job";
|
||||
export * from "./message";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Message } from "./message";
|
||||
export interface Job {
|
||||
id: string;
|
||||
url: string;
|
||||
@@ -7,4 +8,5 @@ export interface Job {
|
||||
status: string;
|
||||
job_options: Object;
|
||||
favorite: boolean;
|
||||
chat?: Message[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface Message {
|
||||
role: string;
|
||||
content: string;
|
||||
}
|
||||
Reference in New Issue
Block a user