Spaces:
Sleeping
Sleeping
Commit
·
7712aa8
1
Parent(s):
558414b
Update and restructure the main app and dockerfile to be work with uv
Browse files- Dockerfile +36 -11
- app.py → app/app/main.py +116 -8
- frontend.html → app/static/index.html +23 -0
- app/static/login.html +184 -0
Dockerfile
CHANGED
@@ -1,19 +1,44 @@
|
|
1 |
-
FROM python:3.13-slim-bookworm
|
|
|
|
|
2 |
|
3 |
RUN apt-get update \
|
4 |
-
&& apt-get install -y --no-install-recommends gcc g++ cmake git \
|
5 |
&& apt-get clean \
|
6 |
&& rm -rf /var/lib/apt/lists/*
|
7 |
|
8 |
-
|
9 |
-
USER user
|
10 |
-
ENV PATH="/home/user/.local/bin:$PATH"
|
11 |
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
|
14 |
-
COPY --chown=
|
15 |
-
|
|
|
|
|
|
|
|
|
16 |
|
17 |
-
|
18 |
-
|
19 |
-
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
1 |
+
FROM python:3.13-slim-bookworm AS builder
|
2 |
+
|
3 |
+
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy CMAKE_ARGS="-DGGML_BLAS=ON -DGGML_BLAS_VENDOR=OpenBLAS -DGGML_NATIVE=OFF"
|
4 |
|
5 |
RUN apt-get update \
|
6 |
+
&& apt-get install -y --no-install-recommends gcc g++ cmake ninja-build git pkg-config libopenblas-dev \
|
7 |
&& apt-get clean \
|
8 |
&& rm -rf /var/lib/apt/lists/*
|
9 |
|
10 |
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
|
|
|
|
11 |
|
12 |
+
ENV PATH="/root/.local/bin/:$PATH"
|
13 |
+
|
14 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
15 |
+
--mount=type=bind,source=uv.lock,target=uv.lock \
|
16 |
+
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
17 |
+
uv sync --frozen --no-install-project --no-group dev --no-group test
|
18 |
+
|
19 |
+
FROM python:3.13-slim-bookworm AS runtime
|
20 |
+
|
21 |
+
ENV \
|
22 |
+
PYTHONUNBUFFERED=1 \
|
23 |
+
PYTHONFAULTHANDLER=1 \
|
24 |
+
DEBIAN_FRONTEND=noninteractive
|
25 |
+
|
26 |
+
RUN apt-get update \
|
27 |
+
&& apt-get install -y libgomp1 libopenblas-dev \
|
28 |
+
&& apt-get clean \
|
29 |
+
&& rm -rf /var/lib/apt/lists/*
|
30 |
+
|
31 |
+
RUN useradd -m -u 1000 arcana
|
32 |
+
USER arcana
|
33 |
+
|
34 |
+
COPY --chown=arcana --from=builder /.venv /.venv
|
35 |
|
36 |
+
COPY --chown=arcana ./app /app
|
37 |
+
|
38 |
+
ENV PATH="/.venv/bin:$PATH"
|
39 |
+
ENV PYTHONPATH="/app:/.venv/lib/python3.13/dist-packages"
|
40 |
+
|
41 |
+
WORKDIR /app
|
42 |
|
43 |
+
EXPOSE 7860
|
44 |
+
CMD ["uvicorn", "app.main:app", "--workers", "2", "--host", "0.0.0.0", "--port", "7860"]
|
|
app.py → app/app/main.py
RENAMED
@@ -2,18 +2,25 @@ import logging
|
|
2 |
import os
|
3 |
import time
|
4 |
from contextlib import asynccontextmanager
|
|
|
5 |
from enum import StrEnum
|
|
|
6 |
|
7 |
from arcana_codex import (
|
8 |
AdUnitsFetchModel,
|
9 |
AdUnitsIntegrateModel,
|
10 |
ArcanaCodexClient,
|
11 |
)
|
|
|
|
|
|
|
12 |
from llama_cpp import Llama
|
13 |
-
from
|
14 |
-
from
|
15 |
from starlette.responses import FileResponse
|
16 |
|
|
|
|
|
17 |
|
18 |
class SupportedModelPipes(StrEnum):
|
19 |
Gemma3 = "gemma3"
|
@@ -22,9 +29,14 @@ class SupportedModelPipes(StrEnum):
|
|
22 |
SmolLLM2Reasoning = "smollm2-reasoning"
|
23 |
|
24 |
|
|
|
|
|
|
|
|
|
|
|
25 |
smollm2_pipeline = Llama.from_pretrained(
|
26 |
-
repo_id="
|
27 |
-
filename="
|
28 |
verbose=False,
|
29 |
)
|
30 |
|
@@ -56,16 +68,67 @@ class ChatResponse(BaseModel):
|
|
56 |
response: str
|
57 |
|
58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
# Configure logging
|
60 |
logging.basicConfig(level=logging.INFO)
|
61 |
logger = logging.getLogger(__name__)
|
62 |
|
63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
@asynccontextmanager
|
65 |
async def lifespan(app: FastAPI): # noqa: ARG001
|
66 |
# Set API key in FastAPI app
|
67 |
app.ARCANA_API_KEY = os.environ.get("ARCANA_API_KEY", "")
|
68 |
|
|
|
|
|
|
|
|
|
69 |
logging.info("Application started")
|
70 |
|
71 |
yield
|
@@ -79,11 +142,43 @@ async def lifespan(app: FastAPI): # noqa: ARG001
|
|
79 |
app = FastAPI(lifespan=lifespan)
|
80 |
|
81 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
@app.post("/chat", response_model=ChatResponse)
|
83 |
-
def chat(
|
|
|
|
|
|
|
|
|
84 |
logger.info(f"Received message: {payload.message}")
|
85 |
|
86 |
-
client = ArcanaCodexClient(
|
|
|
|
|
87 |
fetch_payload = AdUnitsFetchModel(query=payload.message)
|
88 |
ad_fetch_response = client.fetch_ad_units(fetch_payload)
|
89 |
|
@@ -122,9 +217,22 @@ def chat(payload: ChatRequest, request: Request):
|
|
122 |
"integrated_content"
|
123 |
)
|
124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
125 |
return ChatResponse(response=integrated_content)
|
126 |
|
127 |
|
128 |
@app.get("/")
|
129 |
-
def
|
130 |
-
return FileResponse("
|
|
|
|
|
|
|
|
|
|
|
|
2 |
import os
|
3 |
import time
|
4 |
from contextlib import asynccontextmanager
|
5 |
+
from datetime import UTC, datetime
|
6 |
from enum import StrEnum
|
7 |
+
from typing import Annotated
|
8 |
|
9 |
from arcana_codex import (
|
10 |
AdUnitsFetchModel,
|
11 |
AdUnitsIntegrateModel,
|
12 |
ArcanaCodexClient,
|
13 |
)
|
14 |
+
from bson.objectid import ObjectId
|
15 |
+
from fastapi import Depends, FastAPI, Header, HTTPException, Request, status
|
16 |
+
from fastapi.responses import JSONResponse
|
17 |
from llama_cpp import Llama
|
18 |
+
from pydantic import BaseModel, EmailStr
|
19 |
+
from pymongo.mongo_client import MongoClient
|
20 |
from starlette.responses import FileResponse
|
21 |
|
22 |
+
__version__ = "0.0.0"
|
23 |
+
|
24 |
|
25 |
class SupportedModelPipes(StrEnum):
|
26 |
Gemma3 = "gemma3"
|
|
|
29 |
SmolLLM2Reasoning = "smollm2-reasoning"
|
30 |
|
31 |
|
32 |
+
class LogEvent(StrEnum):
|
33 |
+
CHAT_INTERACTION = "chat_interaction"
|
34 |
+
LOGIN = "login"
|
35 |
+
|
36 |
+
|
37 |
smollm2_pipeline = Llama.from_pretrained(
|
38 |
+
repo_id="HuggingFaceTB/SmolLM2-360M-Instruct-GGUF",
|
39 |
+
filename="smollm2-360m-instruct-q8_0.gguf",
|
40 |
verbose=False,
|
41 |
)
|
42 |
|
|
|
68 |
response: str
|
69 |
|
70 |
|
71 |
+
class LoginRequest(BaseModel):
|
72 |
+
email_id: EmailStr
|
73 |
+
access_key: str
|
74 |
+
|
75 |
+
|
76 |
+
class LoginResponse(BaseModel):
|
77 |
+
verified_id: str
|
78 |
+
|
79 |
+
|
80 |
+
class User(BaseModel):
|
81 |
+
_id: ObjectId
|
82 |
+
email_id: EmailStr
|
83 |
+
|
84 |
+
|
85 |
# Configure logging
|
86 |
logging.basicConfig(level=logging.INFO)
|
87 |
logger = logging.getLogger(__name__)
|
88 |
|
89 |
|
90 |
+
def verify_authorization_header(
|
91 |
+
request: Request, authorization: str | None = Header(None)
|
92 |
+
) -> User:
|
93 |
+
if not authorization:
|
94 |
+
raise HTTPException(
|
95 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
96 |
+
detail="Authorization header is missing",
|
97 |
+
)
|
98 |
+
|
99 |
+
try:
|
100 |
+
scheme, token = authorization.split()
|
101 |
+
if scheme.lower() != "bearer":
|
102 |
+
raise HTTPException(
|
103 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
104 |
+
detail="Invalid authentication scheme. Bearer required.",
|
105 |
+
)
|
106 |
+
|
107 |
+
user = request.app.mongo_db["users"].find_one({"_id": ObjectId(token)})
|
108 |
+
if not user:
|
109 |
+
raise HTTPException(
|
110 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
111 |
+
detail="Invalid verified_id",
|
112 |
+
)
|
113 |
+
|
114 |
+
return User(**user)
|
115 |
+
|
116 |
+
except ValueError:
|
117 |
+
raise HTTPException(
|
118 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
119 |
+
detail="Invalid authorization header format",
|
120 |
+
)
|
121 |
+
|
122 |
+
|
123 |
@asynccontextmanager
|
124 |
async def lifespan(app: FastAPI): # noqa: ARG001
|
125 |
# Set API key in FastAPI app
|
126 |
app.ARCANA_API_KEY = os.environ.get("ARCANA_API_KEY", "")
|
127 |
|
128 |
+
app.mongo_db = MongoClient(
|
129 |
+
os.environ.get("MONGO_URI", "mongodb+srv://localhost:27017/")
|
130 |
+
)["arcana_hf_demo"]
|
131 |
+
|
132 |
logging.info("Application started")
|
133 |
|
134 |
yield
|
|
|
142 |
app = FastAPI(lifespan=lifespan)
|
143 |
|
144 |
|
145 |
+
@app.get("/health")
|
146 |
+
async def health_check():
|
147 |
+
return JSONResponse({"health_check": "pass"})
|
148 |
+
|
149 |
+
|
150 |
+
@app.post("/login", response_model=LoginResponse)
|
151 |
+
def login(payload: LoginRequest, request: Request):
|
152 |
+
user = request.app.mongo_db["users"].find_one(
|
153 |
+
{"email_id": payload.email_id, "access_key": payload.access_key}
|
154 |
+
)
|
155 |
+
if user:
|
156 |
+
request.app.mongo_db["logs"].insert_one(
|
157 |
+
{
|
158 |
+
"email_id": user["email_id"],
|
159 |
+
"timestamp": datetime.now(UTC),
|
160 |
+
"event": LogEvent.LOGIN,
|
161 |
+
}
|
162 |
+
)
|
163 |
+
verified_id = user["_id"]
|
164 |
+
return LoginResponse(verified_id=str(verified_id))
|
165 |
+
else:
|
166 |
+
raise HTTPException(
|
167 |
+
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials"
|
168 |
+
)
|
169 |
+
|
170 |
+
|
171 |
@app.post("/chat", response_model=ChatResponse)
|
172 |
+
def chat(
|
173 |
+
payload: ChatRequest,
|
174 |
+
request: Request,
|
175 |
+
user: Annotated[User, Depends(verify_authorization_header)],
|
176 |
+
):
|
177 |
logger.info(f"Received message: {payload.message}")
|
178 |
|
179 |
+
client = ArcanaCodexClient(
|
180 |
+
api_key=request.app.ARCANA_API_KEY, base_url="http://gateway-backend/api/public"
|
181 |
+
)
|
182 |
fetch_payload = AdUnitsFetchModel(query=payload.message)
|
183 |
ad_fetch_response = client.fetch_ad_units(fetch_payload)
|
184 |
|
|
|
217 |
"integrated_content"
|
218 |
)
|
219 |
|
220 |
+
request.app.mongo_db["logs"].insert_one(
|
221 |
+
{
|
222 |
+
"email_id": user.email_id,
|
223 |
+
"timestamp": datetime.now(UTC),
|
224 |
+
"event": LogEvent.CHAT_INTERACTION,
|
225 |
+
}
|
226 |
+
)
|
227 |
+
|
228 |
return ChatResponse(response=integrated_content)
|
229 |
|
230 |
|
231 |
@app.get("/")
|
232 |
+
async def read_index():
|
233 |
+
return FileResponse("./static/index.html")
|
234 |
+
|
235 |
+
|
236 |
+
@app.get("/login")
|
237 |
+
async def read_login():
|
238 |
+
return FileResponse("./static/login.html")
|
frontend.html → app/static/index.html
RENAMED
@@ -41,6 +41,7 @@
|
|
41 |
|
42 |
<nav class="flex items-center space-x-6">
|
43 |
<a href="https://arcana.ad/" class="text-gray-600 hover:text-gray-900 font-medium">Home</a>
|
|
|
44 |
</nav>
|
45 |
</div>
|
46 |
</header>
|
@@ -297,6 +298,20 @@
|
|
297 |
|
298 |
<script>
|
299 |
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
300 |
const chatForm = document.getElementById('chat-form');
|
301 |
const userInput = document.getElementById('user-input');
|
302 |
const chatContainer = document.getElementById('chat-container');
|
@@ -481,10 +496,12 @@
|
|
481 |
|
482 |
try {
|
483 |
// Send message to API
|
|
|
484 |
const response = await fetch('/chat', {
|
485 |
method: 'POST',
|
486 |
headers: {
|
487 |
'Content-Type': 'application/json',
|
|
|
488 |
},
|
489 |
body: JSON.stringify({
|
490 |
message: message,
|
@@ -493,6 +510,12 @@
|
|
493 |
});
|
494 |
|
495 |
if (!response.ok) {
|
|
|
|
|
|
|
|
|
|
|
|
|
496 |
throw new Error('API request failed');
|
497 |
}
|
498 |
|
|
|
41 |
|
42 |
<nav class="flex items-center space-x-6">
|
43 |
<a href="https://arcana.ad/" class="text-gray-600 hover:text-gray-900 font-medium">Home</a>
|
44 |
+
<button id="logout-button" class="text-gray-600 hover:text-gray-900 font-medium">Logout</button>
|
45 |
</nav>
|
46 |
</div>
|
47 |
</header>
|
|
|
298 |
|
299 |
<script>
|
300 |
document.addEventListener('DOMContentLoaded', () => {
|
301 |
+
// Check if user is logged in
|
302 |
+
const verifiedId = sessionStorage.getItem('verifiedId');
|
303 |
+
if (!verifiedId) {
|
304 |
+
window.location.href = '/login';
|
305 |
+
return;
|
306 |
+
}
|
307 |
+
|
308 |
+
// Logout functionality
|
309 |
+
const logoutButton = document.getElementById('logout-button');
|
310 |
+
logoutButton.addEventListener('click', () => {
|
311 |
+
sessionStorage.removeItem('verifiedId');
|
312 |
+
window.location.href = '/login';
|
313 |
+
});
|
314 |
+
|
315 |
const chatForm = document.getElementById('chat-form');
|
316 |
const userInput = document.getElementById('user-input');
|
317 |
const chatContainer = document.getElementById('chat-container');
|
|
|
496 |
|
497 |
try {
|
498 |
// Send message to API
|
499 |
+
const verifiedId = sessionStorage.getItem('verifiedId');
|
500 |
const response = await fetch('/chat', {
|
501 |
method: 'POST',
|
502 |
headers: {
|
503 |
'Content-Type': 'application/json',
|
504 |
+
'Authorization': `Bearer ${verifiedId}`
|
505 |
},
|
506 |
body: JSON.stringify({
|
507 |
message: message,
|
|
|
510 |
});
|
511 |
|
512 |
if (!response.ok) {
|
513 |
+
// If unauthorized, redirect to login
|
514 |
+
if (response.status === 401) {
|
515 |
+
sessionStorage.removeItem('verifiedId');
|
516 |
+
window.location.href = '/login';
|
517 |
+
return;
|
518 |
+
}
|
519 |
throw new Error('API request failed');
|
520 |
}
|
521 |
|
app/static/login.html
ADDED
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
|
4 |
+
<head>
|
5 |
+
<meta charset="UTF-8">
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7 |
+
<title>Login - AI Chat Assistant</title>
|
8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
9 |
+
<script>
|
10 |
+
tailwind.config = {
|
11 |
+
theme: {
|
12 |
+
extend: {
|
13 |
+
colors: {
|
14 |
+
primary: {
|
15 |
+
DEFAULT: '#3b82f6',
|
16 |
+
foreground: '#ffffff',
|
17 |
+
},
|
18 |
+
secondary: {
|
19 |
+
DEFAULT: '#f3f4f6',
|
20 |
+
foreground: '#1f2937',
|
21 |
+
},
|
22 |
+
}
|
23 |
+
}
|
24 |
+
}
|
25 |
+
}
|
26 |
+
</script>
|
27 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
28 |
+
</head>
|
29 |
+
|
30 |
+
<body class="bg-gray-50 text-gray-800 font-['Inter'] min-h-screen flex flex-col">
|
31 |
+
<!-- Header -->
|
32 |
+
<header class="border-b border-gray-200 p-4">
|
33 |
+
<div class="max-w-screen-xl mx-auto flex items-center justify-between">
|
34 |
+
<div class="flex items-center">
|
35 |
+
<h1 class="text-xl font-medium">OpenGPT with <span class="font-semibold"
|
36 |
+
style="color: #067ff3;">Ar</span><span class="font-semibold"
|
37 |
+
style="color: #f7cd1b;">ca</span><span class="font-semibold" style="color: #07b682;">na</span>
|
38 |
+
</h1>
|
39 |
+
</div>
|
40 |
+
|
41 |
+
<nav class="flex items-center space-x-6">
|
42 |
+
<a href="https://arcana.ad/" class="text-gray-600 hover:text-gray-900 font-medium">Home</a>
|
43 |
+
</nav>
|
44 |
+
</div>
|
45 |
+
</header>
|
46 |
+
|
47 |
+
<main class="flex-1 flex items-center justify-center p-4">
|
48 |
+
<div class="w-full max-w-md">
|
49 |
+
<div class="bg-white rounded-lg shadow-md p-5 sm:p-8">
|
50 |
+
<div class="text-center mb-6">
|
51 |
+
<div
|
52 |
+
class="w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center text-2xl font-semibold mb-4 mx-auto">
|
53 |
+
AR
|
54 |
+
</div>
|
55 |
+
<h2 class="text-xl sm:text-2xl font-semibold">Welcome</h2>
|
56 |
+
<p class="text-gray-500 text-sm sm:text-base">Sign in to access the <span
|
57 |
+
class="font-medium">OpenGPT with </span><span class="font-semibold"
|
58 |
+
style="color: #067ff3;">Ar</span><span class="font-semibold"
|
59 |
+
style="color: #f7cd1b;">ca</span><span class="font-semibold"
|
60 |
+
style="color: #07b682;">na</span>
|
61 |
+
</p>
|
62 |
+
</div>
|
63 |
+
|
64 |
+
<form id="login-form" class="space-y-4">
|
65 |
+
<div id="error-message" class="hidden bg-red-50 text-red-700 p-3 rounded-md text-sm"></div>
|
66 |
+
|
67 |
+
<div>
|
68 |
+
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">Email ID</label>
|
69 |
+
<input type="email" id="email" name="email_id"
|
70 |
+
class="w-full px-3 py-2 sm:px-4 sm:py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-base"
|
71 |
+
placeholder="Enter your email" required>
|
72 |
+
</div>
|
73 |
+
|
74 |
+
<div>
|
75 |
+
<label for="access-key" class="block text-sm font-medium text-gray-700 mb-1">Access Key</label>
|
76 |
+
<input type="password" id="access-key" name="access_key"
|
77 |
+
class="w-full px-3 py-2 sm:px-4 sm:py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-base"
|
78 |
+
placeholder="Enter your access key" required>
|
79 |
+
</div>
|
80 |
+
|
81 |
+
<button type="submit" id="login-button"
|
82 |
+
class="w-full bg-blue-600 text-white py-2.5 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors text-base font-medium mt-2">
|
83 |
+
Sign In
|
84 |
+
</button>
|
85 |
+
</form>
|
86 |
+
|
87 |
+
<div class="mt-8 pt-6 border-t border-gray-200">
|
88 |
+
<p class="text-gray-600 mb-3 text-center text-sm sm:text-base">Don't have an access key?</p>
|
89 |
+
<a href="https://forms.google.com/request-access" id="request-access-button"
|
90 |
+
class="flex items-center justify-center w-full bg-gradient-to-r from-indigo-50 to-blue-50 text-blue-600 py-3 px-4 rounded-md hover:from-indigo-100 hover:to-blue-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all duration-200 border border-blue-200 shadow-sm text-sm sm:text-base font-medium"
|
91 |
+
target="_blank">
|
92 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
|
93 |
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
94 |
+
class="mr-2">
|
95 |
+
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
|
96 |
+
<circle cx="9" cy="7" r="4"></circle>
|
97 |
+
<path d="M22 21v-2a4 4 0 0 0-3-3.87"></path>
|
98 |
+
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
99 |
+
</svg>
|
100 |
+
Request Access Key
|
101 |
+
</a>
|
102 |
+
</div>
|
103 |
+
</div>
|
104 |
+
</div>
|
105 |
+
</main>
|
106 |
+
|
107 |
+
<footer class="text-center p-4 text-gray-500 text-sm">
|
108 |
+
<p>© 2025 Arcana Inc. All rights reserved.</p>
|
109 |
+
</footer>
|
110 |
+
|
111 |
+
<script>
|
112 |
+
document.addEventListener('DOMContentLoaded', () => {
|
113 |
+
// Check if user is already logged in
|
114 |
+
const verifiedId = sessionStorage.getItem('verifiedId');
|
115 |
+
if (verifiedId) {
|
116 |
+
window.location.href = '/';
|
117 |
+
return;
|
118 |
+
}
|
119 |
+
|
120 |
+
const loginForm = document.getElementById('login-form');
|
121 |
+
const loginButton = document.getElementById('login-button');
|
122 |
+
const errorMessage = document.getElementById('error-message');
|
123 |
+
|
124 |
+
loginForm.addEventListener('submit', async (e) => {
|
125 |
+
e.preventDefault();
|
126 |
+
|
127 |
+
// Get form data
|
128 |
+
const email = document.getElementById('email').value.trim();
|
129 |
+
const accessKey = document.getElementById('access-key').value.trim();
|
130 |
+
|
131 |
+
if (!email || !accessKey) {
|
132 |
+
showError('Please enter both email and access key');
|
133 |
+
return;
|
134 |
+
}
|
135 |
+
|
136 |
+
// Disable button and show loading state
|
137 |
+
loginButton.disabled = true;
|
138 |
+
loginButton.innerHTML = 'Signing in...';
|
139 |
+
errorMessage.classList.add('hidden');
|
140 |
+
|
141 |
+
try {
|
142 |
+
// Send login request
|
143 |
+
const response = await fetch('/login', {
|
144 |
+
method: 'POST',
|
145 |
+
headers: {
|
146 |
+
'Content-Type': 'application/json',
|
147 |
+
},
|
148 |
+
body: JSON.stringify({
|
149 |
+
email_id: email,
|
150 |
+
access_key: accessKey
|
151 |
+
}),
|
152 |
+
});
|
153 |
+
|
154 |
+
const data = await response.json();
|
155 |
+
|
156 |
+
if (!response.ok) {
|
157 |
+
throw new Error(data.message || 'Login failed. Please check your credentials.');
|
158 |
+
}
|
159 |
+
|
160 |
+
// Store auth token or user info
|
161 |
+
sessionStorage.setItem('verifiedId', data.verified_id);
|
162 |
+
|
163 |
+
// Redirect to chat page
|
164 |
+
window.location.href = '/';
|
165 |
+
|
166 |
+
} catch (error) {
|
167 |
+
console.error('Login error:', error);
|
168 |
+
showError(error.message || 'Login failed. Please try again.');
|
169 |
+
|
170 |
+
// Reset button state
|
171 |
+
loginButton.disabled = false;
|
172 |
+
loginButton.innerHTML = 'Sign In';
|
173 |
+
}
|
174 |
+
});
|
175 |
+
|
176 |
+
function showError(message) {
|
177 |
+
errorMessage.textContent = message;
|
178 |
+
errorMessage.classList.remove('hidden');
|
179 |
+
}
|
180 |
+
});
|
181 |
+
</script>
|
182 |
+
</body>
|
183 |
+
|
184 |
+
</html>
|