siddheshrj commited on
Commit
646538b
·
verified ·
1 Parent(s): dd57275

Deploy via SDK (Stranger Things RAG v2)

Browse files
.gitattributes CHANGED
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ faiss_index/index.faiss filter=lfs diff=lfs merge=lfs -text
37
+ static/bg.jpg filter=lfs diff=lfs merge=lfs -text
38
+ static/bot_icon.png filter=lfs diff=lfs merge=lfs -text
39
+ static/user_icon.png filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use official Python runtime as a parent image
2
+ FROM python:3.10-slim
3
+
4
+ # Set the working directory in the container
5
+ WORKDIR /app
6
+
7
+ # Copy the current directory contents into the container at /app
8
+ COPY . /app
9
+
10
+ # Install system dependencies (build-essential + BLAS for FAISS/Numpy)
11
+ RUN apt-get update && apt-get install -y \
12
+ build-essential \
13
+ libopenblas-dev \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+ # Install any needed packages specified in requirements.txt
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Create a user to avoid running as root (HF Spaces requirement)
20
+ RUN useradd -m -u 1000 user
21
+
22
+ # CRITICAL: Change ownership of the app directory to the new user
23
+ RUN chown -R user:user /app
24
+
25
+ USER user
26
+ ENV HOME=/home/user \
27
+ PATH=/home/user/.local/bin:$PATH
28
+
29
+ # Expose port 7860 (Hugging Face Spaces default port)
30
+ EXPOSE 7860
31
+
32
+ # Run main.py using Uvicorn
33
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
faiss_index/index.faiss ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b656cc86b4bf9be2aec64fd9f54ee63015a2a1257b1d57d6552609ed7ce7405c
3
+ size 1164333
faiss_index/index.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4b80fe6b883ab149d077f9f24ed642b4c18f0b27da15a73df2f8d79e7fedde8d
3
+ size 375276
main.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from fastapi import FastAPI, Request
3
+ from fastapi.responses import HTMLResponse
4
+ from fastapi.staticfiles import StaticFiles
5
+ from fastapi.templating import Jinja2Templates
6
+ from pydantic import BaseModel
7
+ from langchain_community.vectorstores import FAISS
8
+ from langchain_huggingface import HuggingFaceEmbeddings
9
+ from langchain_huggingface import HuggingFaceEndpoint
10
+ from dotenv import load_dotenv
11
+
12
+ # Load environment variables
13
+ load_dotenv()
14
+
15
+ app = FastAPI()
16
+
17
+ # Mount static files
18
+ app.mount("/static", StaticFiles(directory="static"), name="static")
19
+
20
+ # Templates
21
+ templates = Jinja2Templates(directory="templates")
22
+
23
+ # Initialize RAG Components
24
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
25
+ FAISS_PATH = os.path.join(BASE_DIR, "faiss_index")
26
+ embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
27
+
28
+ print(f"DEBUG: Checking for database at {FAISS_PATH}")
29
+ # Check if DB exists
30
+ if os.path.exists(FAISS_PATH):
31
+ print("DEBUG: Database found. Loading FAISS...")
32
+ try:
33
+ vector_db = FAISS.load_local(FAISS_PATH, embeddings, allow_dangerous_deserialization=True)
34
+ retriever = vector_db.as_retriever(search_kwargs={"k": 5})
35
+ print("DEBUG: Retriever initialized.")
36
+ except Exception as e:
37
+ print(f"DEBUG: Failed to load FAISS: {e}")
38
+ retriever = None
39
+ else:
40
+ print("WARNING: FAISS index not found at path. Run ingest.py first.")
41
+ # Hugging Face Inference API (Serverless & Free)
42
+ if not os.getenv("HUGGINGFACEHUB_API_TOKEN"):
43
+ print("CRITICAL WARNING: HUGGINGFACEHUB_API_TOKEN not found. API calls will fail.")
44
+
45
+ repo_id = "mistralai/Mistral-7B-Instruct-v0.2"
46
+
47
+ try:
48
+ print(f"Connecting to Hugging Face API ({repo_id})...")
49
+ llm = HuggingFaceEndpoint(
50
+ repo_id=repo_id,
51
+ task="text-generation",
52
+ max_new_tokens=512,
53
+ do_sample=True,
54
+ temperature=0.7,
55
+ top_p=0.9,
56
+ )
57
+ print("Hugging Face API Client connected!")
58
+ except Exception as e:
59
+ print(f"FAILED to connect to HF API: {e}")
60
+ llm = None
61
+
62
+ from langchain_core.prompts import PromptTemplate
63
+ from langchain_core.output_parsers import StrOutputParser
64
+ from langchain_core.runnables import RunnablePassthrough
65
+
66
+ # ... (Previous LLM setup remains)
67
+
68
+ # LCEL RAG Chain
69
+ template = """You are an expert on Stranger Things. Use the context below to generate a natural, engaging answer in your own words.
70
+ Do not just copy the text. Synthesize the information.
71
+ Format your answer as a detailed response (at least 3-4 sentences).
72
+ Crucial: If the question is about a character, YOU MUST INCLUDE:
73
+ 1. Their key relationships (girlfriend/boyfriend, best friends).
74
+ 2. Their role or passion (e.g., Dungeon Master, journalist, sheriff).
75
+ 3. Any iconic traits.
76
+
77
+ Context:
78
+ {context}
79
+
80
+ Question:
81
+ {question}
82
+
83
+ Answer:"""
84
+
85
+ prompt = PromptTemplate.from_template(template)
86
+
87
+ def format_docs(docs):
88
+ return "\n\n".join(doc.page_content for doc in docs)
89
+
90
+ if retriever and llm:
91
+ rag_chain = (
92
+ {"context": retriever | format_docs, "question": RunnablePassthrough()}
93
+ | prompt
94
+ | llm
95
+ | StrOutputParser()
96
+ )
97
+ print("DEBUG: rag_chain constructed successfully.")
98
+ else:
99
+ print(f"DEBUG: rag_chain initialization skipped. Retriever: {retriever is not None}, LLM: {llm is not None}")
100
+ rag_chain = None
101
+
102
+ class QueryRequest(BaseModel):
103
+ query: str
104
+
105
+ @app.get("/", response_class=HTMLResponse)
106
+ async def read_root(request: Request):
107
+ return templates.TemplateResponse("index.html", {"request": request})
108
+
109
+ @app.post("/query")
110
+ async def query_rag(request: QueryRequest):
111
+ print(f"DEBUG: Incoming query: {request.query}")
112
+ print(f"DEBUG: rag_chain type: {type(rag_chain)}")
113
+ print(f"DEBUG: rag_chain is: {rag_chain}")
114
+
115
+ if not rag_chain:
116
+ return {"answer": "System is initializing or data is missing. Please check server logs.", "sources": []}
117
+
118
+ try:
119
+ # Get answer
120
+ answer = rag_chain.invoke(request.query)
121
+
122
+ # Get sources separately since LCEL simple chain doesn't return them by default
123
+ # unless we modify the runable to return a dict. For now, we'll re-retrieve for sources
124
+ # or just skip sources to keep it simple as per user request for "|" operator specific demo.
125
+ # But to keep sources, let's do a quick retrieve:
126
+ source_docs = retriever.invoke(request.query)
127
+ sources = [doc.metadata.get("source", "Unknown") for doc in source_docs]
128
+ sources = list(set(sources))
129
+
130
+ return {"answer": answer, "sources": sources}
131
+ except Exception as e:
132
+ return {"answer": f"Error: {str(e)}", "sources": []}
133
+
134
+ if __name__ == "__main__":
135
+ import uvicorn
136
+ uvicorn.run(app, host="0.0.0.0", port=8001)
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ python-dotenv
4
+ langchain-community
5
+ langchain-huggingface
6
+ faiss-cpu
7
+ jinja2
8
+ sentence-transformers
9
+ huggingface_hub
static/bg.jpg ADDED

Git LFS Details

  • SHA256: aee9ab7f9d1c2bf547958b0469a1fbe075836ae8280415188fd2c42206beab4b
  • Pointer size: 131 Bytes
  • Size of remote file: 119 kB
static/bot_icon.png ADDED

Git LFS Details

  • SHA256: 4f4c1be9fe149bf7fccab72967bd482ad19cd3a8462bc5b53a1ec916cda93bb8
  • Pointer size: 131 Bytes
  • Size of remote file: 500 kB
static/script.js ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const input = document.getElementById('user-input');
3
+ const sendBtn = document.getElementById('send-btn');
4
+ const chatContainer = document.getElementById('chat-container');
5
+ const loadingIndicator = document.getElementById('loading');
6
+
7
+ function addMessage(text, sender) { // sender is 'user' or 'bot'
8
+ const div = document.createElement('div');
9
+ div.classList.add('message', sender + '-message');
10
+
11
+ const img = document.createElement('img');
12
+ img.src = `/static/${sender}_icon.png`;
13
+ img.classList.add('avatar', sender + '-avatar');
14
+
15
+ const contentDiv = document.createElement('div');
16
+ contentDiv.classList.add('message-content');
17
+
18
+ // Allow basic HTML formatting like line breaks if needed, or just text
19
+ contentDiv.innerHTML = text.replace(/\n/g, '<br>');
20
+
21
+ div.appendChild(img);
22
+ div.appendChild(contentDiv);
23
+
24
+ // Insert before the loading indicator so loading always stays at bottom
25
+ chatContainer.insertBefore(div, loadingIndicator);
26
+
27
+ chatContainer.scrollTop = chatContainer.scrollHeight;
28
+ }
29
+
30
+ async function sendMessage() {
31
+ const query = input.value.trim();
32
+ if (!query) return;
33
+
34
+ addMessage(query, 'user');
35
+ input.value = '';
36
+ input.disabled = true;
37
+
38
+ // Show loading
39
+ loadingIndicator.style.display = 'block';
40
+ chatContainer.scrollTop = chatContainer.scrollHeight;
41
+
42
+ // Typing Animation
43
+ let dots = '';
44
+ const baseText = "Demogorgon is typing";
45
+ const typingInterval = setInterval(() => {
46
+ dots = dots.length < 3 ? dots + '.' : '';
47
+ loadingIndicator.innerText = baseText + dots;
48
+ }, 500);
49
+
50
+ try {
51
+ const response = await fetch('/query', {
52
+ method: 'POST',
53
+ headers: { 'Content-Type': 'application/json' },
54
+ body: JSON.stringify({ query: query })
55
+ });
56
+
57
+ const data = await response.json();
58
+
59
+ // Hide loading & Stop Animation
60
+ clearInterval(typingInterval);
61
+ loadingIndicator.style.display = 'none';
62
+
63
+ addMessage(data.answer, 'bot');
64
+
65
+ // Optional: Handle sources if you want to display them
66
+ } catch (error) {
67
+ // Stop animation on error too
68
+ if (typeof typingInterval !== 'undefined') clearInterval(typingInterval);
69
+
70
+ loadingIndicator.style.display = 'none';
71
+ addMessage("ERR: CONNECTION LOST WITH UPSIDE DOWN.", 'bot');
72
+ console.error(error);
73
+ } finally {
74
+ input.disabled = false;
75
+ input.focus();
76
+ }
77
+ }
78
+
79
+ sendBtn.addEventListener('click', sendMessage);
80
+ input.addEventListener('keypress', (e) => {
81
+ if (e.key === 'Enter') sendMessage();
82
+ });
83
+ });
static/style.css ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Fonts */
2
+ body {
3
+ margin: 0;
4
+ padding: 0;
5
+ font-family: 'VT323', monospace;
6
+ background-color: #000;
7
+ color: #e0e0e0;
8
+ overflow: hidden;
9
+ height: 100vh;
10
+ display: flex;
11
+ justify-content: center;
12
+ align-items: center;
13
+ }
14
+
15
+ /* Background */
16
+ .background-overlay {
17
+ position: fixed;
18
+ top: 0;
19
+ left: 0;
20
+ width: 100%;
21
+ height: 100%;
22
+ background: url('/static/bg.jpg') no-repeat center center/cover;
23
+ z-index: -2;
24
+ filter: brightness(0.6) contrast(1.2);
25
+ }
26
+
27
+ /* CRT Scanline Effect */
28
+ .scanlines {
29
+ position: fixed;
30
+ top: 0;
31
+ left: 0;
32
+ width: 100%;
33
+ height: 100%;
34
+ background: linear-gradient(to bottom,
35
+ rgba(255, 255, 255, 0),
36
+ rgba(255, 255, 255, 0) 50%,
37
+ rgba(0, 0, 0, 0.2) 50%,
38
+ rgba(0, 0, 0, 0.2));
39
+ background-size: 100% 4px;
40
+ z-index: 10;
41
+ pointer-events: none;
42
+ animation: scrollLines 10s linear infinite;
43
+ mix-blend-mode: overlay;
44
+ }
45
+
46
+ @keyframes scrollLines {
47
+ 0% {
48
+ background-position: 0 0;
49
+ }
50
+
51
+ 100% {
52
+ background-position: 0 100%;
53
+ }
54
+ }
55
+
56
+ /* Terminal Container */
57
+ .terminal-container {
58
+ width: 900px;
59
+ height: 600px;
60
+ background: rgba(10, 10, 20, 0.85);
61
+ border: 3px double #00e5ff;
62
+ /* Metallic/Neon Blue */
63
+ box-shadow: 0 0 20px rgba(0, 229, 255, 0.5), 0 0 40px rgba(255, 0, 0, 0.2);
64
+ display: flex;
65
+ flex-direction: column;
66
+ position: relative;
67
+ z-index: 5;
68
+ backdrop-filter: blur(5px);
69
+ }
70
+
71
+ /* Terminal Header */
72
+ .terminal-header {
73
+ background: #00334d;
74
+ color: #00e5ff;
75
+ padding: 10px 15px;
76
+ display: flex;
77
+ justify-content: space-between;
78
+ align-items: center;
79
+ border-bottom: 2px solid #00e5ff;
80
+ font-family: 'Press Start 2P', monospace;
81
+ font-size: 12px;
82
+ letter-spacing: 2px;
83
+ }
84
+
85
+
86
+
87
+ /* Chat Window */
88
+ .chat-window {
89
+ flex: 1;
90
+ padding: 20px;
91
+ overflow-y: auto;
92
+ font-size: 1.4rem;
93
+ scrollbar-width: thin;
94
+ scrollbar-color: #ff0000 #111;
95
+ }
96
+
97
+ /* Messages */
98
+ .message {
99
+ display: flex;
100
+ align-items: flex-start;
101
+ margin-bottom: 20px;
102
+ animation: fadeIn 0.3s ease;
103
+ }
104
+
105
+ @keyframes fadeIn {
106
+ from {
107
+ opacity: 0;
108
+ transform: translateY(10px);
109
+ }
110
+
111
+ to {
112
+ opacity: 1;
113
+ transform: translateY(0);
114
+ }
115
+ }
116
+
117
+ .avatar {
118
+ width: 50px;
119
+ height: 50px;
120
+ margin-right: 15px;
121
+ border: 2px solid;
122
+ image-rendering: pixelated;
123
+ }
124
+
125
+ .bot-avatar {
126
+ border-color: #ff0000;
127
+ box-shadow: 0 0 10px #ff0000;
128
+ }
129
+
130
+ .user-avatar {
131
+ border-color: #00ff00;
132
+ box-shadow: 0 0 10px #00ff00;
133
+ }
134
+
135
+ .message-content {
136
+ background: rgba(0, 0, 0, 0.6);
137
+ padding: 10px 15px;
138
+ border-radius: 4px;
139
+ border-left: 3px solid #ff0000;
140
+ color: #ffcccc;
141
+ max-width: 80%;
142
+ line-height: 1.4;
143
+ text-shadow: 0 0 5px rgba(255, 0, 0, 0.6);
144
+ }
145
+
146
+ .user-message {
147
+ flex-direction: row-reverse;
148
+ }
149
+
150
+ .user-message .avatar {
151
+ margin-right: 0;
152
+ margin-left: 15px;
153
+ }
154
+
155
+ .user-message .message-content {
156
+ border-left: none;
157
+ border-right: 3px solid #00ff00;
158
+ color: #ccffcc;
159
+ text-shadow: 0 0 5px rgba(0, 255, 0, 0.6);
160
+ }
161
+
162
+ /* Input Area */
163
+ .input-area {
164
+ padding: 20px;
165
+ background: #050510;
166
+ border-top: 2px solid #00e5ff;
167
+ display: flex;
168
+ align-items: center;
169
+ }
170
+
171
+ #user-input {
172
+ flex: 1;
173
+ background: #000;
174
+ border: 2px solid #ff0000;
175
+ color: #ff0000;
176
+ padding: 15px;
177
+ font-family: 'VT323', monospace;
178
+ font-size: 1.5rem;
179
+ outline: none;
180
+ box-shadow: inset 0 0 10px rgba(255, 0, 0, 0.5);
181
+ }
182
+
183
+ #user-input::placeholder {
184
+ color: rgba(255, 0, 0, 0.5);
185
+ }
186
+
187
+ #send-btn {
188
+ background: linear-gradient(#333, #111);
189
+ border: 2px solid #888;
190
+ color: #ff0000;
191
+ padding: 10px 30px;
192
+ margin-left: 15px;
193
+ font-family: 'Press Start 2P', cursive;
194
+ font-size: 12px;
195
+ cursor: pointer;
196
+ box-shadow: 2px 2px 0 #000;
197
+ text-shadow: 0 0 5px #ff0000;
198
+ }
199
+
200
+ #send-btn:hover {
201
+ background: linear-gradient(#444, #222);
202
+ box-shadow: 0 0 10px #ff0000;
203
+ }
204
+
205
+ #send-btn:active {
206
+ transform: translate(2px, 2px);
207
+ box-shadow: none;
208
+ }
209
+
210
+ /* Loadings/Typing */
211
+ .typing-indicator {
212
+ color: #ff0000;
213
+ display: none;
214
+ margin-left: 65px;
215
+ font-size: 1.2rem;
216
+ font-family: 'VT323', monospace;
217
+ }
218
+
219
+ .typing-indicator::after {
220
+ content: '_';
221
+ animation: blink 1s infinite;
222
+ }
223
+
224
+ @keyframes blink {
225
+
226
+ 0%,
227
+ 100% {
228
+ opacity: 1;
229
+ }
230
+
231
+ 50% {
232
+ opacity: 0;
233
+ }
234
+ }
235
+
236
+ /* Custom Scrollbar */
237
+ ::-webkit-scrollbar {
238
+ width: 10px;
239
+ }
240
+
241
+ ::-webkit-scrollbar-track {
242
+ background: #111;
243
+ }
244
+
245
+ ::-webkit-scrollbar-thumb {
246
+ background: #ff0000;
247
+ border: 1px solid #000;
248
+ }
249
+
250
+ /* Logo Overlay */
251
+ .logo-overlay {
252
+ position: absolute;
253
+ top: 20px;
254
+ width: 100%;
255
+ text-align: center;
256
+ font-family: 'ITC Benguiat', serif;
257
+ /* Or fallback */
258
+ font-weight: bold;
259
+ font-size: 3rem;
260
+ color: transparent;
261
+ -webkit-text-stroke: 2px #ff0000;
262
+ text-shadow: 0 0 20px #ff0000;
263
+ z-index: 2;
264
+ pointer-events: none;
265
+ letter-spacing: 5px;
266
+ }
static/user_icon.png ADDED

Git LFS Details

  • SHA256: b15f2102ae39c2363557e621074096d19773941ef4823aedbf253864b7d03706
  • Pointer size: 131 Bytes
  • Size of remote file: 359 kB
templates/index.html ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Hawkins National Lab - Terminal</title>
8
+ <link rel="stylesheet" href="/static/style.css">
9
+ <!-- Import Google Fonts for Retro Look -->
10
+ <link href="https://fonts.googleapis.com/css2?family=VT323&family=Press+Start+2P&display=swap" rel="stylesheet">
11
+ </head>
12
+
13
+ <body>
14
+ <div class="scanlines"></div>
15
+ <div class="background-overlay"></div>
16
+
17
+ <div class="terminal-container">
18
+ <div class="terminal-header">
19
+ <span class="header-title">HAWKINS NATIONAL LABORATORY</span>
20
+
21
+ </div>
22
+
23
+ <div class="chat-window" id="chat-container">
24
+ <!-- Messages will be populated here by script.js -->
25
+ <div class="message bot-message">
26
+ <img src="/static/bot_icon.png" class="avatar bot-avatar">
27
+ <div class="message-content">
28
+ CONNECTION ESTABLISHED...<br>
29
+ Welcome to the Upside Down uplink.
30
+ </div>
31
+ </div>
32
+ <div id="loading" class="typing-indicator"></div>
33
+ </div>
34
+
35
+ <div class="input-area">
36
+ <div class="user-avatar-container">
37
+ <img src="/static/user_icon.png" class="avatar user-avatar">
38
+ </div>
39
+ <input type="text" id="user-input" placeholder="Transmit message..." autofocus>
40
+ <button id="send-btn">SEND</button>
41
+ </div>
42
+ </div>
43
+
44
+ <div class="logo-overlay">
45
+ STRANGER THINGS
46
+ </div>
47
+
48
+ <script src="/static/script.js"></script>
49
+ </body>
50
+
51
+ </html>