deploy at 2024-09-16 03:04:07.561156
Browse files- .gitattributes +2 -35
- .gitignore +3 -0
- Dockerfile +24 -0
- README.md +73 -10
- main.py +179 -0
- requirements.txt +6 -0
- script.py +123 -0
- static/favicon-dark.ico +0 -0
- static/favicon-light.ico +0 -0
- styles.py +254 -0
.gitattributes
CHANGED
@@ -1,35 +1,2 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
32 |
-
*.xz 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
|
|
|
1 |
+
# Auto detect text files and perform LF normalization
|
2 |
+
* text=auto
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
.sesskey
|
2 |
+
__pycache__/
|
3 |
+
venv/
|
Dockerfile
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use Python 3.10 as the base image
|
2 |
+
FROM python:3.10
|
3 |
+
|
4 |
+
# Set the working directory in the container
|
5 |
+
WORKDIR /code
|
6 |
+
|
7 |
+
# Copy the current directory contents into the container with correct ownership
|
8 |
+
COPY --link --chown=1000 . .
|
9 |
+
|
10 |
+
# Create a cache directory for Hugging Face Hub and set permissions
|
11 |
+
RUN mkdir -p /tmp/cache/
|
12 |
+
RUN chmod a+rwx -R /tmp/cache/
|
13 |
+
|
14 |
+
# Set Hugging Face Hub cache directory environment variable
|
15 |
+
ENV HF_HUB_CACHE=HF_HOME
|
16 |
+
|
17 |
+
# Install any needed packages specified in requirements.txt
|
18 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
19 |
+
|
20 |
+
# Set environment variables
|
21 |
+
ENV PYTHONUNBUFFERED=1 PORT=7860
|
22 |
+
|
23 |
+
# Run the FastAPI app using Uvicorn
|
24 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
@@ -1,10 +1,73 @@
|
|
1 |
-
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
-
sdk: docker
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: FastGPT
|
3 |
+
emoji: 🤖
|
4 |
+
colorFrom: blue
|
5 |
+
colorTo: green
|
6 |
+
sdk: docker
|
7 |
+
app_file: main.py
|
8 |
+
pinned: false
|
9 |
+
---
|
10 |
+
|
11 |
+
# FastGPT: A Lightweight ChatGPT Implementation Using FastHTML
|
12 |
+
|
13 |
+
**FastGPT** is a minimalistic ChatGPT implementation built using FastHTML, designed to be fast and lightweight, while providing a seamless experience for users. It connects to the GPT-4o model and supports session-based memory to allow for back-and-forth interactions.
|
14 |
+
|
15 |
+
We aim to progressively and iteratively add features to better align with the current state of ChatGPT. This project is open-source, so feel free to contribute by submitting a pull request or forking the repo and building upon it yourself!
|
16 |
+
|
17 |
+
**Created by Jack Tol**
|
18 |
+
[https://jacktol.net](https://jacktol.net)
|
19 |
+
[https://conversationai.io](https://conversationai.io)
|
20 |
+
|
21 |
+
## Video Demo
|
22 |
+
|
23 |
+
Watch the demo of FastGPT on YouTube:
|
24 |
+
|
25 |
+
[![FastGPT Demo](https://img.youtube.com/vi/24aGmm_0mTw/0.jpg)](https://www.youtube.com/watch?v=24aGmm_0mTw)
|
26 |
+
|
27 |
+
## Features
|
28 |
+
|
29 |
+
- **GPT-4o Model Integration**: Connects to the latest version of OpenAI's GPT-4o model for advanced text-based interactions.
|
30 |
+
- **Session-Based Memory**: Keeps track of the current conversation, allowing for coherent back-and-forth dialogue.
|
31 |
+
- **Session Management**: Refreshing the page or pressing the "New Chat" button will terminate the current session and initiate a fresh one.
|
32 |
+
- **Automatic Markdown Parsing**: Both the home page and model responses support Markdown, allowing for easy formatting and enhanced readability.
|
33 |
+
- **Dynamic Input Box**: The chat input dynamically grows for better visibility when entering long messages.
|
34 |
+
- **Lightweight Design**: Built without front-end frameworks, leveraging vanilla CSS for a lightweight, fast user experience.
|
35 |
+
- **Token-by-Token Streaming**: Responses from the model stream onto the screen in real-time, providing instant feedback.
|
36 |
+
- **Interaction Management**: Users cannot send a message while the model is still generating a response, ensuring a smooth interaction.
|
37 |
+
- **"New Chat" Button**: Easily initiate a new session with a simple button click.
|
38 |
+
|
39 |
+
## Usage Instructions
|
40 |
+
|
41 |
+
1. **Clone the Repo**
|
42 |
+
|
43 |
+
```
|
44 |
+
git clone https://github.com/jack-tol/fastgpt.git
|
45 |
+
```
|
46 |
+
|
47 |
+
2. **Set your OpenAI API Key**
|
48 |
+
Ensure your `OPENAI_API_KEY` environment variable is set to your OpenAI API Key.
|
49 |
+
|
50 |
+
```
|
51 |
+
export OPENAI_API_KEY=your-openai-api-key-here
|
52 |
+
```
|
53 |
+
|
54 |
+
3. **Install Dependencies**
|
55 |
+
Navigate into the cloned directory and install the required dependencies.
|
56 |
+
|
57 |
+
```
|
58 |
+
pip install -r requirements.txt
|
59 |
+
```
|
60 |
+
|
61 |
+
4. **Run the Application**
|
62 |
+
Open up your terminal, `cd` into the directory where the `main.py` file is located, and run the following command:
|
63 |
+
|
64 |
+
```
|
65 |
+
uvicorn main:app --port 8080 --reload
|
66 |
+
```
|
67 |
+
|
68 |
+
5. **Start Chatting**
|
69 |
+
Open your browser and navigate to `http://localhost:8080` to start your local ChatGPT experience!
|
70 |
+
|
71 |
+
## Contributing
|
72 |
+
|
73 |
+
Feel free to contribute to FastGPT by submitting a pull request or creating a fork of the repository. As the project evolves, more features will be added to better align with the capabilities of the ChatGPT platform. Help us make FastGPT even better!
|
main.py
ADDED
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fasthtml_hf import setup_hf_backup
|
2 |
+
import os
|
3 |
+
from fastapi import FastAPI, HTTPException, Request
|
4 |
+
from sse_starlette.sse import EventSourceResponse
|
5 |
+
from fastapi.staticfiles import StaticFiles
|
6 |
+
from openai import AsyncOpenAI
|
7 |
+
from fasthtml.common import FastHTML, Html, Head, Title, Body, Div, Button, Textarea, Script, Style, P, Favicon
|
8 |
+
from fasthtml.common import ft_hx
|
9 |
+
import bleach
|
10 |
+
|
11 |
+
from styles import styles
|
12 |
+
from script import script
|
13 |
+
|
14 |
+
# Set the secret key directly from environment variable or default
|
15 |
+
secret_key = os.getenv('SECRET_KEY')
|
16 |
+
|
17 |
+
# Initialize FastHTML with the provided secret key
|
18 |
+
app = FastHTML(secret_key=secret_key)
|
19 |
+
|
20 |
+
|
21 |
+
# Mount static files
|
22 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
23 |
+
|
24 |
+
# Setup Hugging Face backup, explicitly setting a writable directory
|
25 |
+
setup_hf_backup(app)
|
26 |
+
|
27 |
+
client = AsyncOpenAI()
|
28 |
+
|
29 |
+
# Dictionary to store user conversations by session ID
|
30 |
+
conversations = {}
|
31 |
+
|
32 |
+
# Allow additional HTML tags and attributes for sanitization
|
33 |
+
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + [
|
34 |
+
"h1", "h2", "h3", "p", "strong", "em", "ul", "ol", "li", "code", "pre", "blockquote"
|
35 |
+
]
|
36 |
+
ALLOWED_ATTRIBUTES = bleach.sanitizer.ALLOWED_ATTRIBUTES
|
37 |
+
|
38 |
+
# Resolve paths to favicon images
|
39 |
+
static_dir = os.path.join(os.path.dirname(__file__), "static")
|
40 |
+
light_icon = os.path.join(static_dir, "favicon-light.ico")
|
41 |
+
dark_icon = os.path.join(static_dir, "favicon-dark.ico")
|
42 |
+
|
43 |
+
# Custom SVG component
|
44 |
+
def Svg(*c, viewBox=None, **kwargs):
|
45 |
+
return ft_hx('svg', *c, viewBox=viewBox, **kwargs)
|
46 |
+
|
47 |
+
# Custom Path component for SVG with color
|
48 |
+
def Path(*c, d=None, fill=None, **kwargs):
|
49 |
+
return ft_hx('path', *c, d=d, fill=fill, **kwargs)
|
50 |
+
|
51 |
+
# Homepage route
|
52 |
+
@app.get("/")
|
53 |
+
def home():
|
54 |
+
"""Render homepage with FastGPT UI."""
|
55 |
+
home_text = """
|
56 |
+
## FastGPT - A ChatGPT Implementation Using FastHTML
|
57 |
+
"""
|
58 |
+
|
59 |
+
page = Html(
|
60 |
+
Head(
|
61 |
+
Title('FastGPT'),
|
62 |
+
Favicon(light_icon="/static/favicon-light.ico", dark_icon="/static/favicon-dark.ico"), # Serve static favicon files
|
63 |
+
Style(styles),
|
64 |
+
Script(src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"),
|
65 |
+
Script(src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.2.9/purify.min.js")
|
66 |
+
),
|
67 |
+
Body(
|
68 |
+
Div(
|
69 |
+
Div("FastGPT", _class="logo-text"),
|
70 |
+
Div(
|
71 |
+
Button(
|
72 |
+
Svg(
|
73 |
+
Path(
|
74 |
+
d="M441 58.9L453.1 71c9.4 9.4 9.4 24.6 0 33.9L424 134.1 377.9 88 407 58.9c9.4-9.4 24.6-9.4 33.9 0zM209.8 256.2L344 121.9 390.1 168 255.8 302.2c-2.9 2.9-6.5 5-10.4 6.1l-58.5 16.7 16.7-58.5c1.1-3.9 3.2-7.5 6.1-10.4zM373.1 25L175.8 222.2c-8.7 8.7-15 19.4-18.3 31.1l-28.6 100c-2.4 8.4-.1 17.4 6.1 23.6s15.2 8.5 23.6 6.1l100-28.6c11.8-3.4 22.5-9.7 31.1-18.3L487 138.9c28.1-28.1 28.1-73.7 0-101.8L474.9 25C446.8-3.1 401.2-3.1 373.1 25zM88 64C39.4 64 0 103.4 0 152L0 424c0 48.6 39.4 88 88 88l272 0c48.6 0 88-39.4 88-88l0-112c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 112c0 22.1-17.9 40-40 40L88 464c-22.1 0-40-17.9-40-40l0-272c0-22.1 17.9-40 40-40l112 0c13.3 0 24-10.7 24-24s-10.7-24-24-24L88 64z",
|
75 |
+
fill="#b4b4b4"
|
76 |
+
),
|
77 |
+
viewBox="0 0 512 512",
|
78 |
+
_class="refresh-icon"
|
79 |
+
),
|
80 |
+
onclick="location.reload()",
|
81 |
+
_class="refresh-button"
|
82 |
+
),
|
83 |
+
_class='refresh-container'
|
84 |
+
),
|
85 |
+
_class="header"
|
86 |
+
),
|
87 |
+
Div(
|
88 |
+
Div(
|
89 |
+
Div(id="home-text-container", _class="markdown-container", **{"data-home-text": home_text}),
|
90 |
+
_class='title-wrapper'
|
91 |
+
),
|
92 |
+
P(id='output'),
|
93 |
+
Div(
|
94 |
+
Textarea(
|
95 |
+
id='message',
|
96 |
+
rows=1,
|
97 |
+
cols=50,
|
98 |
+
placeholder="Message FastGPT",
|
99 |
+
oninput="autoResizeTextarea()",
|
100 |
+
onkeypress="checkEnter(event)"
|
101 |
+
),
|
102 |
+
Button(
|
103 |
+
Svg(
|
104 |
+
Path(
|
105 |
+
d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2 160 448c0 17.7 14.3 32 32 32s32-14.3 32-32l0-306.7L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"
|
106 |
+
),
|
107 |
+
viewBox="0 0 384 512",
|
108 |
+
_class="send-icon"
|
109 |
+
),
|
110 |
+
onclick="sendMessage()",
|
111 |
+
_class="send-button"
|
112 |
+
),
|
113 |
+
_class="container"
|
114 |
+
),
|
115 |
+
_class="wrapper"
|
116 |
+
),
|
117 |
+
Script(script)
|
118 |
+
)
|
119 |
+
)
|
120 |
+
return page
|
121 |
+
|
122 |
+
# Route to stream responses based on user input
|
123 |
+
@app.get("/stream")
|
124 |
+
async def stream_response(request: Request, message: str, session_id: str = None):
|
125 |
+
"""Stream responses for the given user input."""
|
126 |
+
if not message:
|
127 |
+
raise HTTPException(status_code=400, detail="Message parameter is required")
|
128 |
+
if not session_id:
|
129 |
+
raise HTTPException(status_code=400, detail="Session ID is required")
|
130 |
+
|
131 |
+
# Initialize conversation if the session ID is new
|
132 |
+
if session_id not in conversations:
|
133 |
+
conversations[session_id] = [
|
134 |
+
{"role": "system", "content": "You are a helpful assistant. Use Markdown for formatting."}
|
135 |
+
]
|
136 |
+
|
137 |
+
# Add user's message to the conversation
|
138 |
+
conversations[session_id].append({"role": "user", "content": message})
|
139 |
+
|
140 |
+
async def event_generator():
|
141 |
+
try:
|
142 |
+
# Call OpenAI API to stream response
|
143 |
+
response = await client.chat.completions.create(
|
144 |
+
model="gpt-4o-mini",
|
145 |
+
messages=conversations[session_id],
|
146 |
+
stream=True
|
147 |
+
)
|
148 |
+
|
149 |
+
assistant_response = ""
|
150 |
+
|
151 |
+
# Stream response chunks to the client
|
152 |
+
async for chunk in response:
|
153 |
+
if await request.is_disconnected():
|
154 |
+
print(f"Client for session {session_id} disconnected")
|
155 |
+
break
|
156 |
+
|
157 |
+
content = chunk.choices[0].delta.content
|
158 |
+
if content:
|
159 |
+
assistant_response += content
|
160 |
+
yield {"data": content}
|
161 |
+
|
162 |
+
# Save assistant's response to the conversation
|
163 |
+
conversations[session_id].append({"role": "assistant", "content": assistant_response})
|
164 |
+
|
165 |
+
except Exception as e:
|
166 |
+
yield {"data": f"Error: {str(e)}"}
|
167 |
+
|
168 |
+
finally:
|
169 |
+
print(f"Streaming finished for session {session_id}")
|
170 |
+
|
171 |
+
return EventSourceResponse(event_generator())
|
172 |
+
|
173 |
+
# Route to reset conversation for a given session ID
|
174 |
+
@app.get("/reset")
|
175 |
+
def reset_conversation(session_id: str):
|
176 |
+
"""Reset the conversation for the specified session ID."""
|
177 |
+
if session_id in conversations:
|
178 |
+
del conversations[session_id]
|
179 |
+
return {"message": "Conversation reset."}
|
requirements.txt
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
bleach
|
2 |
+
fastapi
|
3 |
+
openai
|
4 |
+
python-fasthtml
|
5 |
+
sse-starlette
|
6 |
+
fasthtml-hf
|
script.py
ADDED
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
script = """
|
2 |
+
// Generate a new session ID on every page load
|
3 |
+
let sessionId = Math.random().toString(36).substring(2);
|
4 |
+
|
5 |
+
// Handle page load
|
6 |
+
window.onload = function() {
|
7 |
+
autoResizeTextarea(); // Handle the initial state of the textarea
|
8 |
+
renderHomeText(); // Render the home text (markdown content)
|
9 |
+
};
|
10 |
+
|
11 |
+
// Function to handle sending a message
|
12 |
+
async function sendMessage() {
|
13 |
+
const messageElem = document.getElementById('message');
|
14 |
+
const sendButton = document.querySelector('.send-button');
|
15 |
+
const message = messageElem.value.trim();
|
16 |
+
|
17 |
+
// Return early if the message is empty
|
18 |
+
if (!message) return;
|
19 |
+
|
20 |
+
const output = document.getElementById('output');
|
21 |
+
|
22 |
+
// Disable textarea and button while processing
|
23 |
+
messageElem.disabled = true;
|
24 |
+
sendButton.disabled = true;
|
25 |
+
sendButton.classList.add('disabled'); // Visual indication
|
26 |
+
|
27 |
+
// Display user's message in the chat
|
28 |
+
const userMessage = document.createElement('p');
|
29 |
+
userMessage.classList.add('message', 'user');
|
30 |
+
userMessage.innerHTML = message.replace(/\\n/g, '<br>'); // Preserve line breaks
|
31 |
+
output.appendChild(userMessage);
|
32 |
+
|
33 |
+
// Clear the textarea and reset its height
|
34 |
+
messageElem.value = '';
|
35 |
+
autoResizeTextarea();
|
36 |
+
|
37 |
+
// Scroll to the bottom of the output
|
38 |
+
output.scrollTop = output.scrollHeight;
|
39 |
+
|
40 |
+
// Create a new div for the AI's response
|
41 |
+
let aiMessage = document.createElement('p');
|
42 |
+
aiMessage.classList.add('message', 'ai');
|
43 |
+
output.appendChild(aiMessage);
|
44 |
+
|
45 |
+
// Open a connection to stream the AI's response
|
46 |
+
const eventSource = new EventSource(`/stream?message=${encodeURIComponent(message)}&session_id=${encodeURIComponent(sessionId)}`);
|
47 |
+
let partialResponse = ''; // Accumulate streaming response
|
48 |
+
|
49 |
+
eventSource.onmessage = function(event) {
|
50 |
+
partialResponse += event.data;
|
51 |
+
|
52 |
+
// Convert markdown to HTML and sanitize it
|
53 |
+
const sanitizedHtml = DOMPurify.sanitize(marked.parse(partialResponse));
|
54 |
+
aiMessage.innerHTML = sanitizedHtml;
|
55 |
+
output.scrollTop = output.scrollHeight; // Scroll to the bottom
|
56 |
+
};
|
57 |
+
|
58 |
+
// Handle errors during the SSE connection
|
59 |
+
eventSource.onerror = function() {
|
60 |
+
console.error("Error occurred with SSE");
|
61 |
+
resetInputState(messageElem, sendButton); // Re-enable input on error
|
62 |
+
eventSource.close(); // Close the connection
|
63 |
+
};
|
64 |
+
|
65 |
+
eventSource.onopen = function() {
|
66 |
+
console.log("Connection to server opened.");
|
67 |
+
};
|
68 |
+
|
69 |
+
// Re-enable textarea and button after the AI finishes responding
|
70 |
+
eventSource.onclose = function() {
|
71 |
+
console.log("Connection to server closed.");
|
72 |
+
resetInputState(messageElem, sendButton); // Re-enable input after response
|
73 |
+
};
|
74 |
+
}
|
75 |
+
|
76 |
+
// Function to reset the input state (re-enable textarea and send button)
|
77 |
+
function resetInputState(messageElem, sendButton) {
|
78 |
+
messageElem.disabled = false;
|
79 |
+
sendButton.disabled = false;
|
80 |
+
sendButton.classList.remove('disabled');
|
81 |
+
messageElem.focus();
|
82 |
+
}
|
83 |
+
|
84 |
+
// Auto-resize the textarea as the user types and manage send button state
|
85 |
+
function autoResizeTextarea() {
|
86 |
+
const textarea = document.getElementById('message');
|
87 |
+
const sendButton = document.querySelector('.send-button');
|
88 |
+
|
89 |
+
textarea.style.height = 'auto'; // Reset height to auto
|
90 |
+
const maxHeight = 220;
|
91 |
+
let newHeight = textarea.scrollHeight;
|
92 |
+
|
93 |
+
if (newHeight > maxHeight) {
|
94 |
+
textarea.style.height = maxHeight + 'px';
|
95 |
+
textarea.style.overflowY = 'auto';
|
96 |
+
} else {
|
97 |
+
textarea.style.height = newHeight + 'px';
|
98 |
+
textarea.style.overflowY = 'hidden';
|
99 |
+
}
|
100 |
+
|
101 |
+
// Enable/disable the send button based on textarea content
|
102 |
+
sendButton.disabled = textarea.value.trim() === '';
|
103 |
+
sendButton.classList.toggle('disabled', !textarea.value.trim());
|
104 |
+
}
|
105 |
+
|
106 |
+
// Enable sending message on Enter key press (without shift)
|
107 |
+
function checkEnter(e) {
|
108 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
109 |
+
e.preventDefault();
|
110 |
+
sendMessage();
|
111 |
+
}
|
112 |
+
}
|
113 |
+
|
114 |
+
// Function to render home page text from Markdown (on page load)
|
115 |
+
function renderHomeText() {
|
116 |
+
const homeTextContainer = document.getElementById('home-text-container');
|
117 |
+
const markdownContent = homeTextContainer.getAttribute('data-home-text');
|
118 |
+
|
119 |
+
// Parse markdown and sanitize HTML
|
120 |
+
const sanitizedHtml = DOMPurify.sanitize(marked.parse(markdownContent));
|
121 |
+
homeTextContainer.innerHTML = sanitizedHtml;
|
122 |
+
}
|
123 |
+
"""
|
static/favicon-dark.ico
ADDED
static/favicon-light.ico
ADDED
styles.py
ADDED
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
styles = """
|
2 |
+
/* General Styling */
|
3 |
+
html, body {
|
4 |
+
margin: 0;
|
5 |
+
padding: 0;
|
6 |
+
height: 100%;
|
7 |
+
background-color: #212121;
|
8 |
+
font-family: 'Inter', sans-serif;
|
9 |
+
color: white;
|
10 |
+
display: block;
|
11 |
+
overflow: auto;
|
12 |
+
}
|
13 |
+
|
14 |
+
p {
|
15 |
+
line-height: 1.75;
|
16 |
+
}
|
17 |
+
|
18 |
+
/* Header */
|
19 |
+
.header {
|
20 |
+
position: fixed;
|
21 |
+
top: 0;
|
22 |
+
left: 0;
|
23 |
+
right: 0;
|
24 |
+
display: flex;
|
25 |
+
justify-content: space-between;
|
26 |
+
align-items: center;
|
27 |
+
padding: 0 20px;
|
28 |
+
z-index: 10;
|
29 |
+
height: 60px;
|
30 |
+
}
|
31 |
+
|
32 |
+
.logo-text {
|
33 |
+
font-size: 40px;
|
34 |
+
font-weight: 600;
|
35 |
+
color: white;
|
36 |
+
display: flex;
|
37 |
+
align-items: center;
|
38 |
+
justify-content: flex-start;
|
39 |
+
padding-top: 20px;
|
40 |
+
}
|
41 |
+
|
42 |
+
/* Wrapper */
|
43 |
+
.wrapper {
|
44 |
+
margin-top: 60px;
|
45 |
+
display: flex;
|
46 |
+
flex-direction: column;
|
47 |
+
justify-content: flex-start;
|
48 |
+
align-items: center;
|
49 |
+
height: calc(100vh - 60px);
|
50 |
+
width: 100%;
|
51 |
+
padding: 20px 0;
|
52 |
+
box-sizing: border-box;
|
53 |
+
position: relative;
|
54 |
+
}
|
55 |
+
|
56 |
+
/* Title Section */
|
57 |
+
.title-wrapper {
|
58 |
+
width: 90%;
|
59 |
+
max-width: 740px;
|
60 |
+
margin-bottom: 10px;
|
61 |
+
display: flex;
|
62 |
+
justify-content: center;
|
63 |
+
}
|
64 |
+
|
65 |
+
.title {
|
66 |
+
color: #b4b4b4;
|
67 |
+
margin: 0;
|
68 |
+
padding-top: 50px;
|
69 |
+
text-align: left;
|
70 |
+
}
|
71 |
+
|
72 |
+
/* Output Section */
|
73 |
+
#output {
|
74 |
+
width: 90%;
|
75 |
+
max-width: 740px;
|
76 |
+
flex-grow: 1;
|
77 |
+
padding: 10px;
|
78 |
+
background-color: #212121;
|
79 |
+
overflow-y: auto;
|
80 |
+
display: flex;
|
81 |
+
flex-direction: column;
|
82 |
+
justify-content: flex-start;
|
83 |
+
border-radius: 20px;
|
84 |
+
margin: 10px auto;
|
85 |
+
box-sizing: border-box;
|
86 |
+
max-height: calc(100vh - 180px);
|
87 |
+
}
|
88 |
+
|
89 |
+
/* Container */
|
90 |
+
.container {
|
91 |
+
display: flex;
|
92 |
+
align-items: center;
|
93 |
+
justify-content: space-between;
|
94 |
+
background-color: #2f2f2f;
|
95 |
+
padding: 10px;
|
96 |
+
border-radius: 30px;
|
97 |
+
width: 90%;
|
98 |
+
max-width: 740px;
|
99 |
+
min-height: 38px;
|
100 |
+
margin: 0 auto;
|
101 |
+
box-sizing: border-box;
|
102 |
+
position: relative;
|
103 |
+
flex-shrink: 0;
|
104 |
+
}
|
105 |
+
|
106 |
+
/* Textarea */
|
107 |
+
textarea {
|
108 |
+
flex: 1;
|
109 |
+
background-color: transparent;
|
110 |
+
color: #ececec;
|
111 |
+
border: none;
|
112 |
+
padding: 4px 20px 2px 20px;
|
113 |
+
font-family: 'Inter', sans-serif;
|
114 |
+
font-size: 18px;
|
115 |
+
line-height: 1.5;
|
116 |
+
resize: none;
|
117 |
+
outline: none;
|
118 |
+
overflow-y: hidden;
|
119 |
+
min-height: 30px;
|
120 |
+
max-height: 220px;
|
121 |
+
margin: 0;
|
122 |
+
box-sizing: border-box;
|
123 |
+
display: flex;
|
124 |
+
align-items: center;
|
125 |
+
flex-shrink: 0;
|
126 |
+
}
|
127 |
+
|
128 |
+
textarea:disabled {
|
129 |
+
background-color: #2f2f2f;
|
130 |
+
opacity: 0.5;
|
131 |
+
cursor: default;
|
132 |
+
}
|
133 |
+
|
134 |
+
textarea::placeholder {
|
135 |
+
color: #b4b4b4;
|
136 |
+
font-size: 18px;
|
137 |
+
}
|
138 |
+
|
139 |
+
/* Buttons */
|
140 |
+
.refresh-button, .send-button {
|
141 |
+
display: flex;
|
142 |
+
justify-content: center;
|
143 |
+
align-items: center;
|
144 |
+
border: none;
|
145 |
+
cursor: pointer;
|
146 |
+
transition: background-color 0.3s ease;
|
147 |
+
}
|
148 |
+
|
149 |
+
/* Refresh Button */
|
150 |
+
.refresh-button {
|
151 |
+
width: 40px;
|
152 |
+
height: 40px;
|
153 |
+
background-color: transparent;
|
154 |
+
border-radius: 50%;
|
155 |
+
padding: 0;
|
156 |
+
}
|
157 |
+
|
158 |
+
.refresh-button:hover {
|
159 |
+
background-color: #333;
|
160 |
+
}
|
161 |
+
|
162 |
+
.refresh-icon {
|
163 |
+
width: 24px;
|
164 |
+
height: 24px;
|
165 |
+
fill: #b4b4b4;
|
166 |
+
transition: fill 0.3s ease;
|
167 |
+
}
|
168 |
+
|
169 |
+
.refresh-button:hover .refresh-icon {
|
170 |
+
fill: #fff;
|
171 |
+
}
|
172 |
+
|
173 |
+
/* Send Button */
|
174 |
+
.send-button {
|
175 |
+
background-color: #ffffff;
|
176 |
+
color: #212121;
|
177 |
+
border-radius: 50%;
|
178 |
+
width: 35px;
|
179 |
+
height: 35px;
|
180 |
+
margin-left: 10px;
|
181 |
+
box-sizing: border-box;
|
182 |
+
padding: 5px;
|
183 |
+
transition: background-color 0.3s ease, fill 0.3s ease;
|
184 |
+
}
|
185 |
+
|
186 |
+
.send-button svg {
|
187 |
+
width: 18px;
|
188 |
+
height: 18px;
|
189 |
+
fill: #000000;
|
190 |
+
transition: fill 0.3s ease;
|
191 |
+
}
|
192 |
+
|
193 |
+
.send-button:hover {
|
194 |
+
background-color: #ccc;
|
195 |
+
}
|
196 |
+
|
197 |
+
/* Disabled Send Button */
|
198 |
+
.send-button.disabled {
|
199 |
+
background-color: #676767;
|
200 |
+
cursor: default;
|
201 |
+
}
|
202 |
+
|
203 |
+
.send-button.disabled svg {
|
204 |
+
fill: #2f2f2f;
|
205 |
+
}
|
206 |
+
|
207 |
+
/* Message Styles */
|
208 |
+
.message.user {
|
209 |
+
background-color: #2f2f2f;
|
210 |
+
color: white;
|
211 |
+
display: inline-block;
|
212 |
+
padding: 10px 20px;
|
213 |
+
border-radius: 20px;
|
214 |
+
max-width: 500px;
|
215 |
+
margin: 10px 0;
|
216 |
+
align-self: flex-end;
|
217 |
+
word-wrap: break-word;
|
218 |
+
word-break: break-word;
|
219 |
+
}
|
220 |
+
|
221 |
+
|
222 |
+
.message.ai {
|
223 |
+
background-color: transparent;
|
224 |
+
color: #f5f5f5;
|
225 |
+
display: inline-block;
|
226 |
+
padding: 10px 0;
|
227 |
+
margin: 10px 0;
|
228 |
+
max-width: 100%;
|
229 |
+
align-self: flex-start;
|
230 |
+
word-wrap: break-word;
|
231 |
+
}
|
232 |
+
|
233 |
+
/* Scrollbars */
|
234 |
+
#output::-webkit-scrollbar, textarea::-webkit-scrollbar {
|
235 |
+
width: 10px;
|
236 |
+
background-color: #424242;
|
237 |
+
}
|
238 |
+
|
239 |
+
#output::-webkit-scrollbar-thumb, textarea::-webkit-scrollbar-thumb {
|
240 |
+
background-color: #686868;
|
241 |
+
border-radius: 5px;
|
242 |
+
}
|
243 |
+
|
244 |
+
#output::-webkit-scrollbar-thumb:hover, textarea::-webkit-scrollbar-thumb:hover {
|
245 |
+
background-color: #555;
|
246 |
+
}
|
247 |
+
|
248 |
+
/* Media Queries */
|
249 |
+
@media (max-width: 768px) {
|
250 |
+
#output, .container, .title-wrapper {
|
251 |
+
width: 95%;
|
252 |
+
}
|
253 |
+
}
|
254 |
+
"""
|