jacktol commited on
Commit
6079f1a
1 Parent(s): 1dd3296

deploy at 2024-09-16 03:04:07.561156

Browse files
Files changed (10) hide show
  1. .gitattributes +2 -35
  2. .gitignore +3 -0
  3. Dockerfile +24 -0
  4. README.md +73 -10
  5. main.py +179 -0
  6. requirements.txt +6 -0
  7. script.py +123 -0
  8. static/favicon-dark.ico +0 -0
  9. static/favicon-light.ico +0 -0
  10. styles.py +254 -0
.gitattributes CHANGED
@@ -1,35 +1,2 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
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: Fastgpt Test
3
- emoji: 🚀
4
- colorFrom: purple
5
- colorTo: pink
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """