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:
Jayden Pyles
2024-07-31 20:08:56 -05:00
committed by GitHub
parent 99480f3481
commit 55181ec349
31 changed files with 2864 additions and 2121 deletions
+2 -1
View File
@@ -185,4 +185,5 @@ cython_debug/
.pdm-python
.next
postgres_data
.vscode
.vscode
ollama
+26
View File
@@ -52,6 +52,16 @@ From the table, users can download an excel sheet of the job's results, along wi
![statistics](https://github.com/jaypyles/www-scrape/blob/master/docs/stats_page.png)
### AI Integration
- Include the results of a selected job into the context of a conversation
- Currently supports:
1. Ollama
2. OpenAI
![chat](https://github.com/jaypyles/www-scrape/blob/master/docs/chat_page.png)
## 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
![docs](https://github.com/jaypyles/www-scrape/blob/master/docs/docs_page.png)
## 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".
+70
View File
@@ -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
View File
@@ -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
View File
@@ -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
+9
View File
@@ -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 -4
View File
@@ -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"
+2
View File
@@ -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

BIN
View File
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

+463
View File
@@ -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",
+2
View File
@@ -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"
},
Generated
+1588 -2071
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -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"
+5
View File
@@ -0,0 +1,5 @@
import React from "react";
export const Chat = () => {
return <h1>Chat</h1>;
};
+135
View File
@@ -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>
);
};
+2
View File
@@ -0,0 +1,2 @@
export * from "./Chat";
export * from "./JobSelector";
+10
View File
@@ -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
View File
@@ -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
View File
@@ -1 +1,2 @@
export * from "./constants";
export * from "./utils";
+82
View File
@@ -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
View File
@@ -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} />
+345
View File
@@ -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
View File
@@ -74,7 +74,7 @@ const Home = () => {
flexDirection="column"
justifyContent="center"
alignItems="center"
minHeight="100vh"
height="100%"
py={4}
>
<Container maxWidth="lg">
+5 -19
View File
@@ -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);
}, []);
+6 -1
View File
@@ -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
View File
@@ -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
View File
@@ -1,3 +1,4 @@
export * from "./element";
export * from "./result";
export * from "./job";
export * from "./message";
+2
View File
@@ -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[];
}
+4
View File
@@ -0,0 +1,4 @@
export interface Message {
role: string;
content: string;
}