Spaces:
Sleeping
Sleeping
Upload 10 files
Browse files- .gitattributes +1 -0
- app.py +103 -0
- generated-icon.png +3 -0
- main.py +4 -0
- pyproject.toml +13 -0
- requirements.txt +6 -0
- static/css/style.css +354 -0
- static/js/chat.js +410 -0
- templates/index.html +113 -0
- uv.lock +0 -0
- vercel.json +28 -0
.gitattributes
CHANGED
@@ -33,3 +33,4 @@ 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 |
+
generated-icon.png filter=lfs diff=lfs merge=lfs -text
|
app.py
ADDED
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import logging
|
3 |
+
from flask import Flask, render_template, request, jsonify
|
4 |
+
import g4f
|
5 |
+
from g4f.client import Client
|
6 |
+
import os
|
7 |
+
import logging
|
8 |
+
from flask import Flask, render_template, request, jsonify
|
9 |
+
import g4f
|
10 |
+
from g4f.client import Client
|
11 |
+
|
12 |
+
|
13 |
+
# Configure logging
|
14 |
+
|
15 |
+
# Configure logging
|
16 |
+
logging.basicConfig(level=logging.DEBUG)
|
17 |
+
logger = logging.getLogger(__name__)
|
18 |
+
|
19 |
+
# Create Flask app
|
20 |
+
app = Flask(__name__)
|
21 |
+
app.secret_key = os.environ.get("SESSION_SECRET", "default_secret_key")
|
22 |
+
|
23 |
+
# Initialize g4f client
|
24 |
+
client = Client()
|
25 |
+
|
26 |
+
|
27 |
+
@app.route('/')
|
28 |
+
def index():
|
29 |
+
return render_template('index.html')
|
30 |
+
|
31 |
+
|
32 |
+
@app.route('/api/chat', methods=['POST'])
|
33 |
+
def chat():
|
34 |
+
try:
|
35 |
+
data = request.json
|
36 |
+
messages = data.get('messages', [])
|
37 |
+
model = data.get('model', 'gpt-4o-mini')
|
38 |
+
|
39 |
+
# Add system prompt
|
40 |
+
system_prompt = {
|
41 |
+
"role":
|
42 |
+
"system",
|
43 |
+
"content":
|
44 |
+
"You are orion helpful AI assistant. You provide accurate, informative, and friendly responses while keeping them concise and relevant and you are make by Abdullah ali who is 13 years old "
|
45 |
+
}
|
46 |
+
|
47 |
+
# Insert system prompt at the beginning if not already present
|
48 |
+
if not messages or messages[0].get('role') != 'system':
|
49 |
+
messages.insert(0, system_prompt)
|
50 |
+
|
51 |
+
logger.debug(
|
52 |
+
f"Sending request to g4f with model: {model} and messages: {messages}"
|
53 |
+
)
|
54 |
+
|
55 |
+
# Call the g4f API
|
56 |
+
response = client.chat.completions.create(model=model,
|
57 |
+
messages=messages,
|
58 |
+
web_search=False)
|
59 |
+
|
60 |
+
ai_response = response.choices[0].message.content
|
61 |
+
logger.debug(f"Received response from g4f: {ai_response}")
|
62 |
+
|
63 |
+
return jsonify({'status': 'success', 'message': ai_response})
|
64 |
+
except Exception as e:
|
65 |
+
logger.error(f"Error in chat endpoint: {str(e)}")
|
66 |
+
return jsonify({
|
67 |
+
'status': 'error',
|
68 |
+
'message': f"An error occurred: {str(e)}"
|
69 |
+
}), 500
|
70 |
+
|
71 |
+
|
72 |
+
|
73 |
+
@app.route('/api/conversations/<conversation_id>', methods=['DELETE'])
|
74 |
+
def delete_conversation(conversation_id):
|
75 |
+
try:
|
76 |
+
return jsonify({'status': 'success', 'message': f'Conversation {conversation_id} deleted'})
|
77 |
+
except Exception as e:
|
78 |
+
logger.error(f"Error deleting conversation: {str(e)}")
|
79 |
+
return jsonify({
|
80 |
+
'status': 'error',
|
81 |
+
'message': f"An error occurred: {str(e)}"
|
82 |
+
}), 500
|
83 |
+
|
84 |
+
@app.route('/api/models', methods=['GET'])
|
85 |
+
def get_models():
|
86 |
+
try:
|
87 |
+
# Return a list of available models
|
88 |
+
# You can customize this list based on what g4f supports
|
89 |
+
models = [{
|
90 |
+
"id": "gpt-4o-mini",
|
91 |
+
"name": "GPT-4o"
|
92 |
+
}]
|
93 |
+
return jsonify({'status': 'success', 'models': models})
|
94 |
+
except Exception as e:
|
95 |
+
logger.error(f"Error in models endpoint: {str(e)}")
|
96 |
+
return jsonify({
|
97 |
+
'status': 'error',
|
98 |
+
'message': f"An error occurred: {str(e)}"
|
99 |
+
}), 500
|
100 |
+
|
101 |
+
|
102 |
+
if __name__ == "__main__":
|
103 |
+
app.run(host="0.0.0.0", port=5000, debug=True)
|
generated-icon.png
ADDED
![]() |
Git LFS Details
|
main.py
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from app import app
|
2 |
+
|
3 |
+
if __name__ == "__main__":
|
4 |
+
app.run(host="0.0.0.0", port=5000, debug=True)
|
pyproject.toml
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[project]
|
2 |
+
name = "repl-nix-workspace"
|
3 |
+
version = "0.1.0"
|
4 |
+
description = "Add your description here"
|
5 |
+
requires-python = ">=3.11"
|
6 |
+
dependencies = [
|
7 |
+
"email-validator>=2.2.0",
|
8 |
+
"flask>=3.1.0",
|
9 |
+
"flask-sqlalchemy>=3.1.1",
|
10 |
+
"g4f>=0.4.8.6",
|
11 |
+
"gunicorn>=23.0.0",
|
12 |
+
"psycopg2-binary>=2.9.10",
|
13 |
+
]
|
requirements.txt
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
email-validator>=2.2.0
|
2 |
+
flask>=3.1.0
|
3 |
+
flask-sqlalchemy>=3.1.1
|
4 |
+
g4f>=0.4.8.6
|
5 |
+
gunicorn>=23.0.0
|
6 |
+
psycopg2-binary>=2.9.10
|
static/css/style.css
ADDED
@@ -0,0 +1,354 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* Global styles */
|
2 |
+
body {
|
3 |
+
overflow: hidden;
|
4 |
+
font-family: 'Inter', sans-serif;
|
5 |
+
background-color: #ffffff;
|
6 |
+
color: #212529;
|
7 |
+
}
|
8 |
+
|
9 |
+
:root {
|
10 |
+
--sidebar-bg: #f8f9fa;
|
11 |
+
--main-bg: #ffffff;
|
12 |
+
--border-color: #e9ecef;
|
13 |
+
--chat-bg: #f8f9fa;
|
14 |
+
--user-message-bg: #0d6efd;
|
15 |
+
--bot-message-bg: #f0f0f0;
|
16 |
+
}
|
17 |
+
|
18 |
+
/* Sidebar styles */
|
19 |
+
.sidebar {
|
20 |
+
height: 100vh;
|
21 |
+
overflow-y: auto;
|
22 |
+
display: flex;
|
23 |
+
flex-direction: column;
|
24 |
+
background-color: var(--sidebar-bg);
|
25 |
+
border-right: 1px solid var(--border-color);
|
26 |
+
}
|
27 |
+
|
28 |
+
.chat-history-list {
|
29 |
+
max-height: calc(100vh - 250px);
|
30 |
+
overflow-y: auto;
|
31 |
+
scrollbar-width: none;
|
32 |
+
padding: 10px 0;
|
33 |
+
margin: 0;
|
34 |
+
display: flex;
|
35 |
+
flex-direction: column;
|
36 |
+
gap: 2px;
|
37 |
+
}
|
38 |
+
|
39 |
+
.chat-history-list::-webkit-scrollbar {
|
40 |
+
width: 6px;
|
41 |
+
}
|
42 |
+
|
43 |
+
.chat-history-list::-webkit-scrollbar-track {
|
44 |
+
background: rgba(255, 255, 255, 0.1);
|
45 |
+
border-radius: 3px;
|
46 |
+
}
|
47 |
+
|
48 |
+
.chat-history-list::-webkit-scrollbar-thumb {
|
49 |
+
background: rgba(255, 255, 255, 0.2);
|
50 |
+
border-radius: 3px;
|
51 |
+
}
|
52 |
+
|
53 |
+
.chat-history-list::-webkit-scrollbar-thumb:hover {
|
54 |
+
background: rgba(255, 255, 255, 0.3);
|
55 |
+
}
|
56 |
+
|
57 |
+
.history-item {
|
58 |
+
cursor: pointer;
|
59 |
+
text-overflow: ellipsis;
|
60 |
+
white-space: nowrap;
|
61 |
+
overflow: hidden;
|
62 |
+
}
|
63 |
+
|
64 |
+
/* Chat container styles */
|
65 |
+
.main-content {
|
66 |
+
height: 100vh;
|
67 |
+
background-color: var(--main-bg);
|
68 |
+
}
|
69 |
+
|
70 |
+
.chat-header {
|
71 |
+
background-color: var(--sidebar-bg);
|
72 |
+
border-bottom: 1px solid var(--border-color);
|
73 |
+
}
|
74 |
+
|
75 |
+
.messages-container {
|
76 |
+
overflow-y: auto;
|
77 |
+
padding-bottom: 20px;
|
78 |
+
display: flex;
|
79 |
+
flex-direction: column;
|
80 |
+
}
|
81 |
+
|
82 |
+
/* Message styles */
|
83 |
+
.message {
|
84 |
+
margin-bottom: 20px;
|
85 |
+
max-width: 85%;
|
86 |
+
display: inline-block;
|
87 |
+
}
|
88 |
+
|
89 |
+
.user-message {
|
90 |
+
margin-left: auto;
|
91 |
+
background-color: var(--user-message-bg);
|
92 |
+
color: white;
|
93 |
+
border-radius: 18px 18px 0 18px;
|
94 |
+
padding: 10px 14px;
|
95 |
+
float: right;
|
96 |
+
clear: both;
|
97 |
+
max-width: fit-content;
|
98 |
+
}
|
99 |
+
|
100 |
+
.ai-message {
|
101 |
+
margin-right: auto;
|
102 |
+
background-color: var(--bot-message-bg);
|
103 |
+
color: #212529;
|
104 |
+
border-radius: 18px 18px 18px 0;
|
105 |
+
padding: 10px 14px;
|
106 |
+
float: left;
|
107 |
+
clear: both;
|
108 |
+
max-width: fit-content;
|
109 |
+
}
|
110 |
+
|
111 |
+
.ai-message pre {
|
112 |
+
background-color: #1e1e1e;
|
113 |
+
border-radius: 8px;
|
114 |
+
padding: 12px;
|
115 |
+
overflow-x: auto;
|
116 |
+
position: relative;
|
117 |
+
margin: 10px 0;
|
118 |
+
border: 1px solid rgba(255,255,255,0.1);
|
119 |
+
}
|
120 |
+
|
121 |
+
.ai-message pre code {
|
122 |
+
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
|
123 |
+
font-size: 14px;
|
124 |
+
line-height: 1.4;
|
125 |
+
}
|
126 |
+
|
127 |
+
.ai-message pre::before {
|
128 |
+
content: attr(data-language);
|
129 |
+
position: absolute;
|
130 |
+
top: 0;
|
131 |
+
right: 0;
|
132 |
+
padding: 4px 8px;
|
133 |
+
font-size: 12px;
|
134 |
+
color: #999;
|
135 |
+
background: #2d2d2d;
|
136 |
+
border-radius: 0 8px 0 8px;
|
137 |
+
}
|
138 |
+
|
139 |
+
.ai-message pre .copy-btn {
|
140 |
+
position: absolute;
|
141 |
+
top: 4px;
|
142 |
+
right: 4px;
|
143 |
+
padding: 4px 8px;
|
144 |
+
background: transparent;
|
145 |
+
border: none;
|
146 |
+
color: #999;
|
147 |
+
cursor: pointer;
|
148 |
+
opacity: 0;
|
149 |
+
transition: opacity 0.2s;
|
150 |
+
}
|
151 |
+
|
152 |
+
.ai-message pre:hover .copy-btn {
|
153 |
+
opacity: 1;
|
154 |
+
}
|
155 |
+
|
156 |
+
.ai-message pre .copy-btn:hover {
|
157 |
+
color: #fff;
|
158 |
+
}
|
159 |
+
|
160 |
+
.ai-message img {
|
161 |
+
max-width: 100%;
|
162 |
+
border-radius: 5px;
|
163 |
+
}
|
164 |
+
|
165 |
+
.system-message {
|
166 |
+
color: var(--bs-secondary);
|
167 |
+
text-align: center;
|
168 |
+
}
|
169 |
+
|
170 |
+
/* Loading animation */
|
171 |
+
.typing-indicator {
|
172 |
+
display: flex;
|
173 |
+
align-items: center;
|
174 |
+
margin: 10px 0;
|
175 |
+
}
|
176 |
+
|
177 |
+
.typing-dot {
|
178 |
+
width: 8px;
|
179 |
+
height: 8px;
|
180 |
+
margin: 0 2px;
|
181 |
+
background-color: var(--bs-secondary);
|
182 |
+
border-radius: 50%;
|
183 |
+
animation: typingAnimation 1.4s infinite ease-in-out;
|
184 |
+
}
|
185 |
+
|
186 |
+
.typing-dot:nth-child(1) {
|
187 |
+
animation-delay: 0s;
|
188 |
+
}
|
189 |
+
|
190 |
+
.typing-dot:nth-child(2) {
|
191 |
+
animation-delay: 0.2s;
|
192 |
+
}
|
193 |
+
|
194 |
+
.typing-dot:nth-child(3) {
|
195 |
+
animation-delay: 0.4s;
|
196 |
+
}
|
197 |
+
|
198 |
+
@keyframes typingAnimation {
|
199 |
+
0%, 60%, 100% {
|
200 |
+
transform: translateY(0);
|
201 |
+
}
|
202 |
+
30% {
|
203 |
+
transform: translateY(-5px);
|
204 |
+
}
|
205 |
+
}
|
206 |
+
|
207 |
+
/* Input area styling */
|
208 |
+
.input-area {
|
209 |
+
background-color: var(--sidebar-bg);
|
210 |
+
border-top: 1px solid var(--border-color);
|
211 |
+
}
|
212 |
+
|
213 |
+
.model-badge {
|
214 |
+
font-size: 0.7rem;
|
215 |
+
padding: 0.2rem 0.6rem;
|
216 |
+
border-radius: 12px;
|
217 |
+
font-weight: 500;
|
218 |
+
background-color: rgba(13, 110, 253, 0.1);
|
219 |
+
color: #0d6efd;
|
220 |
+
}
|
221 |
+
|
222 |
+
#message-input {
|
223 |
+
resize: none;
|
224 |
+
overflow-y: auto;
|
225 |
+
max-height: 120px;
|
226 |
+
}
|
227 |
+
|
228 |
+
/* Chat history styling */
|
229 |
+
.chat-history-list {
|
230 |
+
margin-top: 10px;
|
231 |
+
}
|
232 |
+
|
233 |
+
.chat-history-list .history-item {
|
234 |
+
background: transparent;
|
235 |
+
border: none;
|
236 |
+
border-radius: 0;
|
237 |
+
margin: 0;
|
238 |
+
padding: 8px 12px;
|
239 |
+
font-size: 0.85em;
|
240 |
+
display: flex;
|
241 |
+
align-items: center;
|
242 |
+
justify-content: space-between;
|
243 |
+
transition: background 0.2s ease;
|
244 |
+
min-height: 40px;
|
245 |
+
}
|
246 |
+
|
247 |
+
.chat-history-list .history-item:hover {
|
248 |
+
background: rgba(13, 110, 253, 0.05);
|
249 |
+
}
|
250 |
+
|
251 |
+
.chat-history-list .history-item.active {
|
252 |
+
background: rgba(var(--bs-primary-rgb), 0.2);
|
253 |
+
border-color: var(--bs-primary);
|
254 |
+
}
|
255 |
+
|
256 |
+
.chat-history-list .btn-danger {
|
257 |
+
padding: 4px 8px;
|
258 |
+
font-size: 14px;
|
259 |
+
background-color: transparent;
|
260 |
+
border: none;
|
261 |
+
color: #dc3545;
|
262 |
+
opacity: 0.7;
|
263 |
+
transition: all 0.2s ease;
|
264 |
+
}
|
265 |
+
|
266 |
+
.chat-history-list .btn-danger:hover {
|
267 |
+
background-color: transparent;
|
268 |
+
color: #dc3545;
|
269 |
+
opacity: 1;
|
270 |
+
}
|
271 |
+
|
272 |
+
.chat-history-list .delete-button:hover {
|
273 |
+
opacity: 1;
|
274 |
+
color: #dc3545;
|
275 |
+
}
|
276 |
+
|
277 |
+
.chat-history-list .history-item:hover .delete-btn {
|
278 |
+
opacity: 0.7;
|
279 |
+
}
|
280 |
+
|
281 |
+
.chat-history-list .delete-btn:hover {
|
282 |
+
opacity: 1 !important;
|
283 |
+
color: #dc3545;
|
284 |
+
}
|
285 |
+
|
286 |
+
/* Responsive adjustments */
|
287 |
+
@media (max-width: 768px) {
|
288 |
+
.sidebar {
|
289 |
+
position: fixed;
|
290 |
+
top: 0;
|
291 |
+
left: -100%;
|
292 |
+
width: 75%;
|
293 |
+
z-index: 1000;
|
294 |
+
transition: all 0.3s ease;
|
295 |
+
}
|
296 |
+
|
297 |
+
.sidebar.active {
|
298 |
+
left: 0;
|
299 |
+
}
|
300 |
+
|
301 |
+
.mobile-toggle {
|
302 |
+
display: block;
|
303 |
+
}
|
304 |
+
}
|
305 |
+
|
306 |
+
/* Code block styling */
|
307 |
+
pre code {
|
308 |
+
border-radius: 5px;
|
309 |
+
font-family: monospace;
|
310 |
+
}
|
311 |
+
|
312 |
+
/* Markdown content styling */
|
313 |
+
.markdown-content h1,
|
314 |
+
.markdown-content h2,
|
315 |
+
.markdown-content h3,
|
316 |
+
.markdown-content h4,
|
317 |
+
.markdown-content h5,
|
318 |
+
.markdown-content h6 {
|
319 |
+
margin-top: 1rem;
|
320 |
+
margin-bottom: 0.5rem;
|
321 |
+
}
|
322 |
+
|
323 |
+
.markdown-content p {
|
324 |
+
margin-bottom: 1rem;
|
325 |
+
}
|
326 |
+
|
327 |
+
.markdown-content ul,
|
328 |
+
.markdown-content ol {
|
329 |
+
margin-bottom: 1rem;
|
330 |
+
padding-left: 1.5rem;
|
331 |
+
}
|
332 |
+
|
333 |
+
.markdown-content blockquote {
|
334 |
+
border-left: 3px solid var(--bs-primary);
|
335 |
+
padding-left: 1rem;
|
336 |
+
margin-left: 0;
|
337 |
+
color: var(--bs-secondary);
|
338 |
+
}
|
339 |
+
|
340 |
+
.markdown-content table {
|
341 |
+
width: 100%;
|
342 |
+
margin-bottom: 1rem;
|
343 |
+
border-collapse: collapse;
|
344 |
+
}
|
345 |
+
|
346 |
+
.markdown-content table th,
|
347 |
+
.markdown-content table td {
|
348 |
+
padding: 0.5rem;
|
349 |
+
border: 1px solid var(--bs-secondary);
|
350 |
+
}
|
351 |
+
|
352 |
+
.markdown-content table th {
|
353 |
+
background-color: rgba(var(--bs-secondary-rgb), 0.2);
|
354 |
+
}
|
static/js/chat.js
ADDED
@@ -0,0 +1,410 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
document.addEventListener('DOMContentLoaded', function() {
|
2 |
+
// DOM Elements
|
3 |
+
const chatForm = document.getElementById('chat-form');
|
4 |
+
const messageInput = document.getElementById('message-input');
|
5 |
+
const messagesContainer = document.getElementById('messages-container');
|
6 |
+
const newChatBtn = document.getElementById('new-chat-btn');
|
7 |
+
const modelSelect = document.getElementById('model-select');
|
8 |
+
const currentModelLabel = document.getElementById('current-model-label');
|
9 |
+
const historyList = document.getElementById('history-list');
|
10 |
+
|
11 |
+
// State variables
|
12 |
+
let chatHistory = [];
|
13 |
+
let conversations = loadConversations();
|
14 |
+
let currentConversationId = null;
|
15 |
+
|
16 |
+
// Load available models
|
17 |
+
loadModels();
|
18 |
+
|
19 |
+
// Start a new conversation
|
20 |
+
startNewConversation();
|
21 |
+
|
22 |
+
// Event listeners
|
23 |
+
chatForm.addEventListener('submit', handleChatSubmit);
|
24 |
+
newChatBtn.addEventListener('click', startNewConversation);
|
25 |
+
modelSelect.addEventListener('change', handleModelChange);
|
26 |
+
messageInput.addEventListener('keydown', handleInputKeydown);
|
27 |
+
|
28 |
+
// Auto-resize textarea as user types
|
29 |
+
messageInput.addEventListener('input', function() {
|
30 |
+
this.style.height = 'auto';
|
31 |
+
this.style.height = (this.scrollHeight) + 'px';
|
32 |
+
// Cap the height
|
33 |
+
if (parseInt(this.style.height) > 120) {
|
34 |
+
this.style.height = '120px';
|
35 |
+
}
|
36 |
+
});
|
37 |
+
|
38 |
+
// Load available models from the backend
|
39 |
+
function loadModels() {
|
40 |
+
fetch('/api/models')
|
41 |
+
.then(response => response.json())
|
42 |
+
.then(data => {
|
43 |
+
if (data.status === 'success') {
|
44 |
+
modelSelect.innerHTML = '';
|
45 |
+
data.models.forEach(model => {
|
46 |
+
const option = document.createElement('option');
|
47 |
+
option.value = model.id;
|
48 |
+
option.textContent = model.name;
|
49 |
+
modelSelect.appendChild(option);
|
50 |
+
});
|
51 |
+
}
|
52 |
+
})
|
53 |
+
.catch(error => {
|
54 |
+
console.error('Error loading models:', error);
|
55 |
+
});
|
56 |
+
}
|
57 |
+
|
58 |
+
// Handle model change
|
59 |
+
function handleModelChange() {
|
60 |
+
const selectedModel = modelSelect.value;
|
61 |
+
const selectedModelName = modelSelect.options[modelSelect.selectedIndex].text;
|
62 |
+
currentModelLabel.textContent = selectedModelName;
|
63 |
+
|
64 |
+
// Update current conversation model
|
65 |
+
if (currentConversationId) {
|
66 |
+
conversations[currentConversationId].model = selectedModel;
|
67 |
+
saveConversations();
|
68 |
+
}
|
69 |
+
}
|
70 |
+
|
71 |
+
// Handle chat submission
|
72 |
+
function handleChatSubmit(e) {
|
73 |
+
e.preventDefault();
|
74 |
+
const message = messageInput.value.trim();
|
75 |
+
|
76 |
+
if (!message) return;
|
77 |
+
|
78 |
+
// Add user message to UI
|
79 |
+
addMessageToUI('user', message);
|
80 |
+
|
81 |
+
// Add to chat history
|
82 |
+
chatHistory.push({
|
83 |
+
role: 'user',
|
84 |
+
content: message
|
85 |
+
});
|
86 |
+
|
87 |
+
// Update conversation title if it's the first message
|
88 |
+
if (chatHistory.length === 1) {
|
89 |
+
const title = message.substring(0, 30) + (message.length > 30 ? '...' : '');
|
90 |
+
conversations[currentConversationId].title = title;
|
91 |
+
updateConversationsList();
|
92 |
+
}
|
93 |
+
|
94 |
+
// Save to local storage
|
95 |
+
conversations[currentConversationId].messages = chatHistory;
|
96 |
+
saveConversations();
|
97 |
+
|
98 |
+
// Clear input
|
99 |
+
messageInput.value = '';
|
100 |
+
messageInput.style.height = 'auto';
|
101 |
+
|
102 |
+
// Show typing indicator
|
103 |
+
showTypingIndicator();
|
104 |
+
|
105 |
+
// Send to backend
|
106 |
+
const selectedModel = modelSelect.value;
|
107 |
+
|
108 |
+
fetch('/api/chat', {
|
109 |
+
method: 'POST',
|
110 |
+
headers: {
|
111 |
+
'Content-Type': 'application/json'
|
112 |
+
},
|
113 |
+
body: JSON.stringify({
|
114 |
+
messages: chatHistory,
|
115 |
+
model: selectedModel
|
116 |
+
})
|
117 |
+
})
|
118 |
+
.then(response => response.json())
|
119 |
+
.then(data => {
|
120 |
+
// Remove typing indicator
|
121 |
+
hideTypingIndicator();
|
122 |
+
|
123 |
+
if (data.status === 'success') {
|
124 |
+
// Add AI response to UI
|
125 |
+
addMessageToUI('ai', data.message);
|
126 |
+
|
127 |
+
// Add to chat history
|
128 |
+
chatHistory.push({
|
129 |
+
role: 'assistant',
|
130 |
+
content: data.message
|
131 |
+
});
|
132 |
+
|
133 |
+
// Save to local storage
|
134 |
+
conversations[currentConversationId].messages = chatHistory;
|
135 |
+
saveConversations();
|
136 |
+
|
137 |
+
// Scroll to bottom
|
138 |
+
scrollToBottom();
|
139 |
+
} else {
|
140 |
+
// Show error
|
141 |
+
addMessageToUI('system', `Error: ${data.message}`);
|
142 |
+
}
|
143 |
+
})
|
144 |
+
.catch(error => {
|
145 |
+
hideTypingIndicator();
|
146 |
+
console.error('Error:', error);
|
147 |
+
addMessageToUI('system', `Error: ${error.message || 'Failed to send message'}`);
|
148 |
+
});
|
149 |
+
|
150 |
+
// Scroll to bottom
|
151 |
+
scrollToBottom();
|
152 |
+
}
|
153 |
+
|
154 |
+
// Handle input keydown (for Enter key submission with Shift+Enter for new line)
|
155 |
+
function handleInputKeydown(e) {
|
156 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
157 |
+
e.preventDefault();
|
158 |
+
chatForm.dispatchEvent(new Event('submit'));
|
159 |
+
}
|
160 |
+
}
|
161 |
+
|
162 |
+
// Add message to UI
|
163 |
+
function addMessageToUI(sender, content) {
|
164 |
+
// Create a wrapper for each message group
|
165 |
+
let messageWrapper;
|
166 |
+
if (sender === 'system') {
|
167 |
+
messageWrapper = document.createElement('div');
|
168 |
+
messageWrapper.className = 'w-100 d-flex justify-content-center my-3';
|
169 |
+
const messageDiv = document.createElement('div');
|
170 |
+
messageDiv.className = 'system-message text-center';
|
171 |
+
messageDiv.innerHTML = `<p>${content}</p>`;
|
172 |
+
messageWrapper.appendChild(messageDiv);
|
173 |
+
} else {
|
174 |
+
messageWrapper = document.createElement('div');
|
175 |
+
messageWrapper.className = 'w-100 d-flex ' +
|
176 |
+
(sender === 'user' ? 'justify-content-end' : 'justify-content-start');
|
177 |
+
|
178 |
+
const messageDiv = renderMessage(content, sender === 'user');
|
179 |
+
messageWrapper.appendChild(messageDiv);
|
180 |
+
}
|
181 |
+
|
182 |
+
messagesContainer.appendChild(messageWrapper);
|
183 |
+
scrollToBottom();
|
184 |
+
}
|
185 |
+
|
186 |
+
// Show typing indicator
|
187 |
+
function showTypingIndicator() {
|
188 |
+
const typingWrapper = document.createElement('div');
|
189 |
+
typingWrapper.className = 'w-100 d-flex justify-content-start';
|
190 |
+
typingWrapper.id = 'typing-indicator-wrapper';
|
191 |
+
|
192 |
+
const typingDiv = document.createElement('div');
|
193 |
+
typingDiv.className = 'typing-indicator ai-message';
|
194 |
+
typingDiv.id = 'typing-indicator';
|
195 |
+
typingDiv.innerHTML = `
|
196 |
+
<div class="typing-dot"></div>
|
197 |
+
<div class="typing-dot"></div>
|
198 |
+
<div class="typing-dot"></div>
|
199 |
+
`;
|
200 |
+
|
201 |
+
typingWrapper.appendChild(typingDiv);
|
202 |
+
messagesContainer.appendChild(typingWrapper);
|
203 |
+
scrollToBottom();
|
204 |
+
}
|
205 |
+
|
206 |
+
// Hide typing indicator
|
207 |
+
function hideTypingIndicator() {
|
208 |
+
const typingWrapper = document.getElementById('typing-indicator-wrapper');
|
209 |
+
if (typingWrapper) {
|
210 |
+
typingWrapper.remove();
|
211 |
+
}
|
212 |
+
}
|
213 |
+
|
214 |
+
// Scroll to bottom of messages container
|
215 |
+
function scrollToBottom() {
|
216 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
217 |
+
}
|
218 |
+
|
219 |
+
// Start a new conversation
|
220 |
+
function startNewConversation() {
|
221 |
+
// Clear chat history
|
222 |
+
chatHistory = [];
|
223 |
+
|
224 |
+
// Clear messages container
|
225 |
+
messagesContainer.innerHTML = `
|
226 |
+
<div class="system-message text-center my-5">
|
227 |
+
<h3>Welcome to AI Chat</h3>
|
228 |
+
<p class="text-muted">Ask me anything! I'm powered by g4f and ready to help.</p>
|
229 |
+
</div>
|
230 |
+
`;
|
231 |
+
|
232 |
+
// Create a new conversation ID
|
233 |
+
currentConversationId = Date.now().toString();
|
234 |
+
|
235 |
+
// Add to conversations object
|
236 |
+
conversations[currentConversationId] = {
|
237 |
+
id: currentConversationId,
|
238 |
+
title: 'New Conversation',
|
239 |
+
model: modelSelect.value,
|
240 |
+
messages: []
|
241 |
+
};
|
242 |
+
|
243 |
+
// Save to local storage
|
244 |
+
saveConversations();
|
245 |
+
|
246 |
+
// Update UI
|
247 |
+
updateConversationsList();
|
248 |
+
}
|
249 |
+
|
250 |
+
// Load conversation by ID
|
251 |
+
function loadConversation(id) {
|
252 |
+
if (!conversations[id]) return;
|
253 |
+
|
254 |
+
// Set current conversation ID
|
255 |
+
currentConversationId = id;
|
256 |
+
|
257 |
+
// Load chat history
|
258 |
+
chatHistory = conversations[id].messages || [];
|
259 |
+
|
260 |
+
// Set model
|
261 |
+
if (conversations[id].model) {
|
262 |
+
modelSelect.value = conversations[id].model;
|
263 |
+
const selectedModelName = modelSelect.options[modelSelect.selectedIndex].text;
|
264 |
+
currentModelLabel.textContent = selectedModelName;
|
265 |
+
}
|
266 |
+
|
267 |
+
// Clear messages container
|
268 |
+
messagesContainer.innerHTML = '';
|
269 |
+
|
270 |
+
// Add messages to UI
|
271 |
+
if (chatHistory.length === 0) {
|
272 |
+
messagesContainer.innerHTML = `
|
273 |
+
<div class="system-message text-center my-5">
|
274 |
+
<h3>Welcome to AI Chat</h3>
|
275 |
+
<p class="text-muted">Ask me anything! I'm powered by g4f and ready to help.</p>
|
276 |
+
</div>
|
277 |
+
`;
|
278 |
+
} else {
|
279 |
+
chatHistory.forEach(msg => {
|
280 |
+
if (msg.role === 'user') {
|
281 |
+
addMessageToUI('user', msg.content);
|
282 |
+
} else if (msg.role === 'assistant') {
|
283 |
+
addMessageToUI('ai', msg.content);
|
284 |
+
} else if (msg.role === 'system') {
|
285 |
+
addMessageToUI('system', msg.content);
|
286 |
+
}
|
287 |
+
});
|
288 |
+
}
|
289 |
+
|
290 |
+
// Update UI
|
291 |
+
updateConversationsList();
|
292 |
+
}
|
293 |
+
|
294 |
+
// Update conversations list in sidebar
|
295 |
+
function updateConversationsList() {
|
296 |
+
historyList.innerHTML = '';
|
297 |
+
|
298 |
+
// Sort conversations by ID (newest first)
|
299 |
+
const sortedIds = Object.keys(conversations).sort((a, b) => b - a);
|
300 |
+
|
301 |
+
sortedIds.forEach(id => {
|
302 |
+
const conv = conversations[id];
|
303 |
+
const item = document.createElement('li');
|
304 |
+
item.className = `list-group-item history-item d-flex justify-content-between align-items-center ${id === currentConversationId ? 'active' : ''}`;
|
305 |
+
|
306 |
+
const titleSpan = document.createElement('span');
|
307 |
+
titleSpan.textContent = conv.title;
|
308 |
+
titleSpan.style.cursor = 'pointer';
|
309 |
+
titleSpan.addEventListener('click', () => {
|
310 |
+
loadConversation(id);
|
311 |
+
});
|
312 |
+
|
313 |
+
const deleteBtn = document.createElement('button');
|
314 |
+
deleteBtn.className = 'btn btn-sm btn-danger';
|
315 |
+
deleteBtn.innerHTML = '<i class="fas fa-trash"></i>';
|
316 |
+
deleteBtn.addEventListener('click', (e) => {
|
317 |
+
e.stopPropagation();
|
318 |
+
deleteConversation(id);
|
319 |
+
});
|
320 |
+
|
321 |
+
item.appendChild(titleSpan);
|
322 |
+
item.appendChild(deleteBtn);
|
323 |
+
item.dataset.id = id;
|
324 |
+
|
325 |
+
historyList.appendChild(item);
|
326 |
+
});
|
327 |
+
}
|
328 |
+
|
329 |
+
// Load conversations from local storage
|
330 |
+
function loadConversations() {
|
331 |
+
try {
|
332 |
+
const saved = localStorage.getItem('g4f_conversations');
|
333 |
+
return saved ? JSON.parse(saved) : {};
|
334 |
+
} catch (error) {
|
335 |
+
console.error('Error loading conversations:', error);
|
336 |
+
return {};
|
337 |
+
}
|
338 |
+
}
|
339 |
+
|
340 |
+
// Save conversations to local storage
|
341 |
+
function saveConversations() {
|
342 |
+
try {
|
343 |
+
localStorage.setItem('g4f_conversations', JSON.stringify(conversations));
|
344 |
+
} catch (error) {
|
345 |
+
console.error('Error saving conversations:', error);
|
346 |
+
}
|
347 |
+
}
|
348 |
+
|
349 |
+
// Delete specific conversation
|
350 |
+
function deleteConversation(id) {
|
351 |
+
if (confirm('Are you sure you want to delete this conversation? This cannot be undone.')) {
|
352 |
+
fetch(`/api/conversations/${id}`, {
|
353 |
+
method: 'DELETE',
|
354 |
+
})
|
355 |
+
.then(response => response.json())
|
356 |
+
.then(data => {
|
357 |
+
if (data.status === 'success') {
|
358 |
+
delete conversations[id];
|
359 |
+
|
360 |
+
// If current conversation was deleted, start a new one
|
361 |
+
if (id === currentConversationId) {
|
362 |
+
startNewConversation();
|
363 |
+
}
|
364 |
+
|
365 |
+
saveConversations();
|
366 |
+
updateConversationsList();
|
367 |
+
addMessageToUI('system', 'Conversation deleted');
|
368 |
+
}
|
369 |
+
})
|
370 |
+
.catch(error => {
|
371 |
+
console.error('Error:', error);
|
372 |
+
addMessageToUI('system', 'Failed to delete conversation');
|
373 |
+
});
|
374 |
+
}
|
375 |
+
}
|
376 |
+
|
377 |
+
function renderMessage(content, isUser = false) {
|
378 |
+
const messageDiv = document.createElement('div');
|
379 |
+
messageDiv.className = `message ${isUser ? 'user-message' : 'ai-message'}`;
|
380 |
+
|
381 |
+
if (!isUser) {
|
382 |
+
const formattedContent = marked.parse(content);
|
383 |
+
messageDiv.innerHTML = formattedContent;
|
384 |
+
|
385 |
+
// Add copy buttons and language labels to code blocks
|
386 |
+
messageDiv.querySelectorAll('pre code').forEach((block) => {
|
387 |
+
hljs.highlightElement(block);
|
388 |
+
const pre = block.parentElement;
|
389 |
+
const language = block.className.split('-')[1] || 'plaintext';
|
390 |
+
pre.setAttribute('data-language', language);
|
391 |
+
|
392 |
+
const copyBtn = document.createElement('button');
|
393 |
+
copyBtn.className = 'copy-btn';
|
394 |
+
copyBtn.innerHTML = '<i class="fas fa-copy"></i>';
|
395 |
+
copyBtn.onclick = async () => {
|
396 |
+
await navigator.clipboard.writeText(block.textContent);
|
397 |
+
copyBtn.innerHTML = '<i class="fas fa-check"></i>';
|
398 |
+
setTimeout(() => {
|
399 |
+
copyBtn.innerHTML = '<i class="fas fa-copy"></i>';
|
400 |
+
}, 2000);
|
401 |
+
};
|
402 |
+
pre.appendChild(copyBtn);
|
403 |
+
});
|
404 |
+
} else {
|
405 |
+
messageDiv.textContent = content;
|
406 |
+
}
|
407 |
+
|
408 |
+
return messageDiv;
|
409 |
+
}
|
410 |
+
});
|
templates/index.html
ADDED
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en" data-bs-theme="light">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>AI Chat</title>
|
7 |
+
|
8 |
+
<!-- Bootstrap CSS -->
|
9 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
10 |
+
|
11 |
+
<!-- Font Awesome for icons -->
|
12 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
13 |
+
|
14 |
+
<!-- Google Fonts -->
|
15 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
16 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
17 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
18 |
+
|
19 |
+
<!-- Custom CSS -->
|
20 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
21 |
+
</head>
|
22 |
+
<body>
|
23 |
+
<div class="container-fluid px-0">
|
24 |
+
<div class="row g-0 vh-100">
|
25 |
+
<!-- Sidebar -->
|
26 |
+
<div class="col-md-3 col-lg-2 sidebar p-3 border-end">
|
27 |
+
<!-- Logo and Title -->
|
28 |
+
<div class="d-flex align-items-center mb-4">
|
29 |
+
<i class="fas fa-robot me-2 fs-4"></i>
|
30 |
+
<h5 class="mb-0 fw-bold">AI Chat</h5>
|
31 |
+
</div>
|
32 |
+
|
33 |
+
<!-- New Chat Button -->
|
34 |
+
<div class="mb-4">
|
35 |
+
<button id="new-chat-btn" class="btn btn-outline-primary w-100 d-flex align-items-center">
|
36 |
+
<i class="fas fa-plus me-2"></i> New Chat
|
37 |
+
</button>
|
38 |
+
</div>
|
39 |
+
|
40 |
+
<!-- Model Selection -->
|
41 |
+
<div class="mb-4">
|
42 |
+
<label for="model-select" class="form-label small text-muted">Model</label>
|
43 |
+
<select id="model-select" class="form-select">
|
44 |
+
<option value="gpt-4o" selected>GPT-4o</option>
|
45 |
+
<!-- Other models will be loaded dynamically -->
|
46 |
+
</select>
|
47 |
+
</div>
|
48 |
+
|
49 |
+
<!-- Chat History -->
|
50 |
+
<div class="chat-history mb-3">
|
51 |
+
<label class="form-label small text-muted mb-2">Chat History</label>
|
52 |
+
<ul id="history-list" class="list-group chat-history-list mb-0">
|
53 |
+
<!-- Chat history items will appear here -->
|
54 |
+
</ul>
|
55 |
+
</div>
|
56 |
+
|
57 |
+
<!-- App Info -->
|
58 |
+
<div class="mt-auto text-center text-secondary small pt-5">
|
59 |
+
<p class="mb-1">Powered by Abdullah ali</p>
|
60 |
+
<p>© 2025 AI Chat. All rights reserved.</p>
|
61 |
+
</div>
|
62 |
+
</div>
|
63 |
+
|
64 |
+
<!-- Main Chat Area -->
|
65 |
+
<div class="col-md-9 col-lg-10 main-content d-flex flex-column p-0">
|
66 |
+
<!-- Chat Header -->
|
67 |
+
<div class="chat-header p-3 border-bottom d-flex align-items-center">
|
68 |
+
<h6 class="mb-0">Chat with AI</h6>
|
69 |
+
<span class="model-badge ms-2">GPT-4o</span>
|
70 |
+
</div>
|
71 |
+
|
72 |
+
<!-- Messages Container -->
|
73 |
+
<div id="messages-container" class="messages-container flex-grow-1 p-4">
|
74 |
+
<!-- Welcome message -->
|
75 |
+
<div class="system-message text-center my-5">
|
76 |
+
<h2 class="fw-bold">Welcome to AI Chat</h2>
|
77 |
+
<p class="text-muted">Ask me anything! I'm powered by Abdullah Ali and ready .to help.</p>
|
78 |
+
</div>
|
79 |
+
|
80 |
+
<!-- Messages will appear here -->
|
81 |
+
</div>
|
82 |
+
|
83 |
+
<!-- Input Area -->
|
84 |
+
<div class="input-area p-3 border-top">
|
85 |
+
<form id="chat-form" class="d-flex">
|
86 |
+
<input
|
87 |
+
id="message-input"
|
88 |
+
class="form-control me-2"
|
89 |
+
placeholder="Type a message..."
|
90 |
+
required>
|
91 |
+
<button type="submit" id="send-button" class="btn btn-primary rounded-circle">
|
92 |
+
<i class="fas fa-paper-plane"></i>
|
93 |
+
</button>
|
94 |
+
</form>
|
95 |
+
</div>
|
96 |
+
</div>
|
97 |
+
</div>
|
98 |
+
</div>
|
99 |
+
|
100 |
+
<!-- Bootstrap JS Bundle -->
|
101 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
102 |
+
|
103 |
+
<!-- Marked.js for Markdown Support -->
|
104 |
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
105 |
+
|
106 |
+
<!-- Highlight.js for Code Syntax Highlighting -->
|
107 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css">
|
108 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
109 |
+
|
110 |
+
<!-- Custom JS -->
|
111 |
+
<script src="{{ url_for('static', filename='js/chat.js') }}"></script>
|
112 |
+
</body>
|
113 |
+
</html>
|
uv.lock
ADDED
The diff for this file is too large to render.
See raw diff
|
|
vercel.json
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"version": 2,
|
3 |
+
"builds": [
|
4 |
+
{
|
5 |
+
"src": "app.py",
|
6 |
+
"use": "@vercel/python",
|
7 |
+
"config": { "runtime": "3.11" }
|
8 |
+
}
|
9 |
+
],
|
10 |
+
"routes": [
|
11 |
+
{
|
12 |
+
"src": "/(.*)",
|
13 |
+
"dest": "app.py"
|
14 |
+
}
|
15 |
+
],
|
16 |
+
"env": {
|
17 |
+
"PYTHON_VERSION": "3.11"
|
18 |
+
},
|
19 |
+
|
20 |
+
|
21 |
+
{
|
22 |
+
"functions": {
|
23 |
+
"app/api/**/route.ts": {
|
24 |
+
"maxDuration": 60
|
25 |
+
}
|
26 |
+
|
27 |
+
|
28 |
+
}
|