Initial portal commit: landing + 9 AI-powered apps
Apps: - dwg-rooms: extract room numbers from DWG/DXF - dwg-counting: count symbols in PDF drawings (OpenCV template matching) - contract-check: review PDF contracts against a checklist (Claude vision + Tesseract OCR fallback) - email-drafter: bullet notes → polished Czech/English business emails - invoice-extractor: PDF/image invoice → structured data → Excel - translator: Czech-first translator across 19 languages with tone control - vv-check: find inconsistent unit prices across VV sheets in one workbook - vv-compare: diff original vs new VV files (changes / added / removed) - feature-request: portal users submit ideas + sample files Infrastructure: - LiteLLM gateway with per-app virtual keys + budgets - Langfuse observability - Geist font, shared theme, cross-subdomain back link + theme sync via cookie/URL - Caddy reverse proxy on *.klas.chat
This commit is contained in:
94
feature-request/main.py
Normal file
94
feature-request/main.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""FastAPI: collect feature requests from portal users.
|
||||
|
||||
Stores each request to a timestamped folder on disk so the team can review
|
||||
later. No LLM, no external calls — pure form handler.
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(title="Feature Request")
|
||||
app.add_middleware(CORSMiddleware, allow_origins=["*"],
|
||||
allow_methods=["*"], allow_headers=["*"])
|
||||
|
||||
# Volume-mounted; survives container rebuilds.
|
||||
STORAGE = Path(os.getenv("STORAGE_DIR", "/data/requests"))
|
||||
STORAGE.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
MAX_FILE_BYTES = 25 * 1024 * 1024 # 25 MB cap
|
||||
|
||||
|
||||
def _slug(text: str) -> str:
|
||||
text = re.sub(r"[^\w\s-]", "", text, flags=re.UNICODE).strip().lower()
|
||||
text = re.sub(r"[\s_-]+", "-", text)
|
||||
return text[:40] or "request"
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return FileResponse("static/index.html")
|
||||
|
||||
|
||||
@app.post("/api/submit")
|
||||
async def submit(
|
||||
title: str = Form(..., min_length=3, max_length=120),
|
||||
description: str = Form(..., min_length=10, max_length=8000),
|
||||
name: str = Form("", max_length=120),
|
||||
email: str = Form("", max_length=200),
|
||||
file: UploadFile | None = File(None),
|
||||
):
|
||||
title = title.strip()
|
||||
description = description.strip()
|
||||
if not title or not description:
|
||||
raise HTTPException(400, "Vyplňte název a popis")
|
||||
|
||||
now = datetime.datetime.now()
|
||||
folder_name = f"{now.strftime('%Y%m%d_%H%M%S')}_{_slug(title)}_{uuid.uuid4().hex[:8]}"
|
||||
folder = STORAGE / folder_name
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
record = {
|
||||
"submitted_at": now.isoformat(timespec="seconds"),
|
||||
"title": title,
|
||||
"description": description,
|
||||
"name": name.strip(),
|
||||
"email": email.strip(),
|
||||
"attachment": None,
|
||||
}
|
||||
|
||||
if file is not None and file.filename:
|
||||
raw = await file.read()
|
||||
if len(raw) > MAX_FILE_BYTES:
|
||||
raise HTTPException(400, f"Soubor je příliš velký (max {MAX_FILE_BYTES // (1024*1024)} MB)")
|
||||
safe_name = re.sub(r"[^\w\.\-]", "_", file.filename)[:200]
|
||||
attachment_path = folder / safe_name
|
||||
attachment_path.write_bytes(raw)
|
||||
record["attachment"] = safe_name
|
||||
record["attachment_bytes"] = len(raw)
|
||||
record["attachment_content_type"] = file.content_type or ""
|
||||
|
||||
(folder / "request.json").write_text(
|
||||
json.dumps(record, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
logger.info("Stored feature request → %s (%s)", folder_name, title)
|
||||
return {"ok": True, "id": folder_name}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
Reference in New Issue
Block a user