From a56912b43f4678e80f6713ae04325b8aa70c4cd4 Mon Sep 17 00:00:00 2001 From: rfprod Date: Sat, 19 Sep 2026 00:10:22 +0400 Subject: [PATCH] feat(api): add a simple user interface for the app --- .vscode/extensions.json | 2 +- src/api.py | 9 +++++- src/app.py | 4 ++- src/start.sh | 18 ++++++----- src/static/index.css | 67 +++++++++++++++++++++++++++++++++++++++++ src/static/index.html | 34 +++++++++++++++++++++ src/static/index.js | 49 ++++++++++++++++++++++++++++++ 7 files changed, 173 insertions(+), 10 deletions(-) create mode 100644 src/static/index.css create mode 100644 src/static/index.html create mode 100644 src/static/index.js diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 30d084b..9ac84ed 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,6 @@ { "recommendations": [ - "pkief.material-icon-theme", + "github.vscode-github-actions", "atishay-jain.all-autocomplete", "sadesyllas.vscode-workspace-switcher", "editorconfig.editorconfig", diff --git a/src/api.py b/src/api.py index 32c03d8..fc18b99 100644 --- a/src/api.py +++ b/src/api.py @@ -1,5 +1,5 @@ from contextlib import asynccontextmanager -from fastapi import FastAPI +from fastapi import FastAPI, responses, staticfiles import uvicorn import app as rag_module @@ -13,6 +13,8 @@ async def lifespan(app: FastAPI): app = FastAPI(lifespan=lifespan) +app.mount("/static", staticfiles.StaticFiles(directory="static"), name="static") + @app.get("/") async def root(question: str | None = None): @@ -24,5 +26,10 @@ async def root(question: str | None = None): } +@app.get("/ui") +async def ui(): + return responses.FileResponse("static/index.html") + + if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/src/app.py b/src/app.py index 7807e18..31f037b 100644 --- a/src/app.py +++ b/src/app.py @@ -92,5 +92,7 @@ def initialize(): question = "What operating systems does nx-ng-starter support?" answer = rag_application.run(question) + print("--- startup test ---") print("Question:", question) - print("Answer:", answer) + print("Answer:", answer.strip()) + print("--- startup test ---") diff --git a/src/start.sh b/src/start.sh index 50e1982..aaa376c 100644 --- a/src/start.sh +++ b/src/start.sh @@ -9,26 +9,27 @@ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' +NC_BOLD='\033[1m' if docker ps | grep -q qdrant; then - echo "Qdrant is already running. Skipping docker run." + echo -e "${GREEN}[ok]${NC} Qdrant is already running. Skipping docker run." else - echo "Starting Qdrant container..." + echo -e "${YELLOW}[i]${NC} Starting Qdrant container..." if [ -f .env ]; then # shellcheck source=/dev/null source .env else - echo "Warning: .env file not found." + echo -e "${YELLOW}[warn]${NC} .env file not found." fi if [ -z "$QDRANT_API_KEY" ]; then - echo "Error: QDRANT_API_KEY not found in .env and is not set globally" + echo -e "${RED}[err]${NC} QDRANT_API_KEY not found in .env and is not set globally.}" exit 1 fi if [ -z "$QDRANT_API_KEY_READ_ONLY" ]; then - echo "Error: QDRANT_API_KEY_READ_ONLY not found in .env and is not set globally" + echo -e "${RED}[err]${NC} QDRANT_API_KEY_READ_ONLY not found in .env and is not set globally." exit 1 fi @@ -60,6 +61,8 @@ trap cleanup EXIT echo -e "${YELLOW}[i]${NC} Qdrant is running with data dir $DATA_DIR. Waiting for Qdrant..." +echo -e "${YELLOW}[i]${NC} Qdrant dashboard is available on ${NC_BOLD}http://localhost:6333/dashboard${NC}" + MAX_WAIT_SEC=60 SLEEP_SEC=1 ELAPSED=0 @@ -73,6 +76,7 @@ while [ "$ELAPSED" -lt "$MAX_WAIT_SEC" ]; do sleep "$SLEEP_SEC" ELAPSED=$((ELAPSED + SLEEP_SEC)) done + if [ "$STARTUP_STATUS" != "200" ]; then echo -e "${RED}[err]{$NC} Qdrant startup timeout. Last HTTP status: $STARTUP_STATUS" echo -e "${YELLOW}[i]${NC} Qdrant container logs:" @@ -80,10 +84,10 @@ if [ "$STARTUP_STATUS" != "200" ]; then exit 1 fi -echo "Qdrant status:" +echo -e "${YELLOW}[i]${NC} Qdrant status:" docker ps | grep qdrant -echo "Qdrant version check:" +echo -e "${YELLOW}[i]${NC} Qdrant version check:" curl -H "api-key: $QDRANT_API_KEY_READ_ONLY" http://localhost:6333 | grep version cd "$(dirname "$0")" || exit 1 diff --git a/src/static/index.css b/src/static/index.css new file mode 100644 index 0000000..6c1d9e2 --- /dev/null +++ b/src/static/index.css @@ -0,0 +1,67 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: Arial, sans-serif; + background: #f4f4f4; + color: #222; +} + +.container { + width: min(600px, 90%); + margin: 60px auto; + padding: 24px; + background: white; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +h1 { + margin-top: 0; +} + +label { + display: block; + margin-bottom: 8px; + font-weight: bold; +} + +textarea { + width: 100%; + min-height: 120px; + padding: 12px; + resize: vertical; + border: 1px solid #ccc; + border-radius: 5px; + font: inherit; +} + +button { + margin-top: 12px; + padding: 10px 18px; + border: 0; + border-radius: 5px; + background: #2563eb; + color: white; + cursor: pointer; + font-size: 16px; +} + +button:disabled { + background: #999; + cursor: not-allowed; +} + +.result { + margin-top: 24px; + padding: 16px; + background: #eef6ff; + border-radius: 5px; +} + +.error { + color: #c00; + margin-top: 16px; +} diff --git a/src/static/index.html b/src/static/index.html new file mode 100644 index 0000000..9211e25 --- /dev/null +++ b/src/static/index.html @@ -0,0 +1,34 @@ + + + + + + + RAG User Interface + + + + +
+

Ask a Question

+ +
+ + + + + +
+ + + +

+
+ + + + + diff --git a/src/static/index.js b/src/static/index.js new file mode 100644 index 0000000..79b9fbc --- /dev/null +++ b/src/static/index.js @@ -0,0 +1,49 @@ +document.addEventListener('DOMContentLoaded', () => { + console.log('DOM content loaded'); + + const form = document.getElementById("question-form"); + const questionInput = document.getElementById("question"); + const answerElement = document.getElementById("answer"); + const resultElement = document.getElementById("result"); + const errorElement = document.getElementById("error"); + const submitButton = form.querySelector("button"); + + form.addEventListener("submit", async (event) => { + event.preventDefault(); + + const question = questionInput.value.trim(); + + if (!question) { + return; + } + + resultElement.hidden = true; + errorElement.textContent = ""; + submitButton.disabled = true; + submitButton.textContent = "Asking..."; + + try { + const response = await fetch(`/?question=${question}`, { + method: "GET", + headers: { + "Content-Type": "application/json" + } + }); + + if (!response.ok) { + throw new Error("Request failed"); + } + + const data = await response.json(); + + answerElement.textContent = data.answer; + resultElement.hidden = false; + } catch (error) { + errorElement.textContent = + "Unable to get an answer. Please try again."; + } finally { + submitButton.disabled = false; + submitButton.textContent = "Ask"; + } + }); +})