diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1d17dae --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.venv diff --git a/Dockerfile b/Dockerfile index 20cad4f..2ea0ab1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,10 +19,12 @@ RUN --mount=type=cache,target=/root/.cache/uv \ && uv run -m nltk.downloader punkt_tab -d /app/.venv/nltk_data \ && uv run -m nltk.downloader averaged_perceptron_tagger_eng -d /app/.venv/nltk_data -COPY src/ /app/src/ +COPY . /app/ FROM python:3.13-slim-trixie@sha256:087a9f3b880e8b2c7688debb9df2a5106e060225ebd18c264d5f1d7a73399db0 AS runtime +WORKDIR /app + COPY --from=builder /app /app ENV PATH="/app/.venv/bin:$PATH" diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..6ce83e4 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,123 @@ + + + + + + + PAPA + + + + + + +
+

PAPA: Amazing Python Analytics

+ +
+
+ +
+
+ + + API Docs + +
+
+ +
+
+ Only .json files are supported. +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+ +
+ +
+ +
+
+ Analytics +
+ +
+ +
+ Analytics will be displayed here +
+ +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/script.js b/frontend/script.js new file mode 100644 index 0000000..c122f99 --- /dev/null +++ b/frontend/script.js @@ -0,0 +1,66 @@ +document.getElementById("submitButton").addEventListener("click", async () => { + const selectedFile = document.getElementById("fileInput").files[0]; + + if (!selectedFile) { + alert("Please select a file first."); + return; + } + + const reader = new FileReader(); + const nodeLevel = document.getElementById("node_level"); + const speaker = document.getElementById("speaker"); + const forecast = document.getElementById("forecast"); + + reader.onload = async (e) => { + const rawJSON = JSON.parse(e.target.result); + + // log to see if the json is being parsed correctly + console.log("Parsed JSON:", rawJSON); + console.log(nodeLevel.checked); + console.log(speaker.checked); + console.log(forecast.checked); + + const response = await fetch("/api/all_analytics", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + xaif: rawJSON, + node_level: nodeLevel.checked, + speaker: speaker.checked, + forecast: forecast.checked, + }), + }); + + const data = await response.json(); + console.log("Server response:", data); + + // creating a downloadable file for the user + const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + + a.href = url; + a.download = "updated.json"; // name of the file + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + + // displaying the json file on frontend if there is a response + if (response.status === 200) { + document.getElementById("json").style = "display: flex"; + document.querySelector('#json').data = data.analytics; + document.getElementById("responseBox").style = "display: none"; + } + // displaying an error message in response box if papa fails + else { + document.getElementById('json').style = "display: none"; + document.getElementById("responseBox").style = "display: flex"; + document.getElementById("responseBox").innerHTML = "There was an error in the analytics code. Open the downloaded file to see more details" + } + }; + + reader.readAsText(selectedFile); +}); diff --git a/frontend/style.css b/frontend/style.css new file mode 100644 index 0000000..6fd4d0c --- /dev/null +++ b/frontend/style.css @@ -0,0 +1,111 @@ +:root { + --bg: #f5f7fa; + --card-bg: #ffffff; + --muted: #6c757d; + --primary: #0b4fc6; /* navy blue */ + --accent: #0d9488; /* teal */ + --accent-2: #475ea8; /* indigo */ + --border: #e6e9ef; +} + +body { + background-color: var(--bg); + font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + color: #222; + padding: 48px 16px; +} + +.container { + max-width: 960px; +} + +h1.app-title { + text-align: center; + color: var(--primary); + margin-bottom: 40px; + font-weight: 600; + letter-spacing: -0.3px; + font-size: 2.9rem; +} + +.app-card { + border: none; + border-radius: 12px; + background: var(--card-bg); + box-shadow: 0 10px 30px rgba(28, 40, 60, 0.08); + overflow: hidden; +} + +.app-card::before { + content: ""; + display: block; + height: 6px; + background: linear-gradient(90deg, var(--primary), var(--accent)); +} + +.app-card .card-body { + padding: 1.5rem; +} + +label.form-label { + font-weight: 600; + color: #24324a; +} + +.file-row { + display: flex; + gap: 0.6rem; + align-items: center; + flex-wrap: wrap; +} + +.form-text.small-muted { + color: var(--muted); +} + +.form-control:focus { + box-shadow: 0 6px 18px rgba(13, 148, 136, 0.08); + border-color: var(--accent); +} + +.form-check-input:checked { + background-color: var(--accent-2); + border-color: var(--accent-2); +} + +.btn-primary { + background-color: var(--primary); + border-color: var(--primary); + border-radius: 999px; + padding: 0.55rem 1rem; + font-weight: 600; + transition: transform 0.12s ease, box-shadow 0.12s ease; +} +.btn-primary:hover { + transform: translateY(-3px); + box-shadow: 0 12px 28px rgba(11, 79, 198, 0.12); +} + +.btn-sm:hover { + transform: none; +} + +.divider { + height: 1px; + background: linear-gradient(90deg, transparent, var(--border), transparent); + margin: 1rem 0; +} + +#responseBox { + background: linear-gradient(to bottom, #f9fbfd, #f6f8fa); + border: 1px solid #dce3eb; + border-radius: 8px; + padding: 1.25rem; + min-height: 150px; + font-size: 0.95rem; + color: #2b3b50; + text-align: center; + display: flex; + align-items: center; + justify-content: center; +} \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..0110998 --- /dev/null +++ b/run.sh @@ -0,0 +1,3 @@ +export PYTHONPATH=src + +uv run fastapi dev src \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py index 473f879..17ec246 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,6 +1,7 @@ import traceback from fastapi import FastAPI, HTTPException +from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, ConfigDict from . import papa @@ -29,3 +30,13 @@ def all_analytics(body: RequestBody | dict) -> dict: return papa.all_analytics(body) except Exception: raise HTTPException(status_code=500, detail=traceback.format_exc()) + + +app.mount( + "/", + StaticFiles( + directory="frontend", + html=True, + ), + name="web", +)