soxogvv commited on
Commit
a160f16
Β·
verified Β·
1 Parent(s): 1b30513

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +140 -90
  2. index.html +1051 -0
app.py CHANGED
@@ -1,69 +1,96 @@
1
  import os
2
- import json
3
  import uuid
4
- import time
5
  import threading
6
  from datetime import datetime, timedelta
7
- from flask import Flask, render_template, request, jsonify, session, redirect, url_for, send_from_directory
8
  from flask_sqlalchemy import SQLAlchemy
9
  from werkzeug.security import generate_password_hash, check_password_hash
10
- import anthropic
 
 
 
11
 
12
  app = Flask(__name__)
13
- app.secret_key = os.environ.get('SECRET_KEY', 'supersecretkey-change-in-production-123')
 
 
 
 
14
  app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///claudeai.db'
15
  app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
16
  app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30)
17
  app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
 
 
18
 
19
- # Ensure output directory exists
20
  os.makedirs('outputs', exist_ok=True)
21
  os.makedirs('static', exist_ok=True)
22
 
23
  db = SQLAlchemy(app)
24
 
25
- # ─── Models ────────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  class User(db.Model):
28
- id = db.Column(db.Integer, primary_key=True)
29
- username = db.Column(db.String(80), unique=True, nullable=False)
30
- email = db.Column(db.String(120), unique=True, nullable=False)
31
  password_hash = db.Column(db.String(256), nullable=False)
32
- created_at = db.Column(db.DateTime, default=datetime.utcnow)
33
- chats = db.relationship('Chat', backref='user', lazy=True, cascade='all, delete-orphan')
34
 
35
  class Chat(db.Model):
36
- id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
37
- user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
38
- title = db.Column(db.String(200), default='New Chat')
 
39
  created_at = db.Column(db.DateTime, default=datetime.utcnow)
40
  updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
41
- messages = db.relationship('Message', backref='chat', lazy=True, cascade='all, delete-orphan', order_by='Message.created_at')
 
42
 
43
  class Message(db.Model):
44
- id = db.Column(db.Integer, primary_key=True)
45
- chat_id = db.Column(db.String(36), db.ForeignKey('chat.id'), nullable=False)
46
- role = db.Column(db.String(20), nullable=False) # 'user' or 'assistant'
47
- content = db.Column(db.Text, nullable=False)
48
  created_at = db.Column(db.DateTime, default=datetime.utcnow)
49
 
50
  class GeneratedFile(db.Model):
51
- id = db.Column(db.Integer, primary_key=True)
52
- user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
53
- chat_id = db.Column(db.String(36), db.ForeignKey('chat.id'), nullable=True)
54
- filename = db.Column(db.String(200), nullable=False)
55
- filepath = db.Column(db.String(500), nullable=False)
56
- file_type = db.Column(db.String(50), default='code')
57
  created_at = db.Column(db.DateTime, default=datetime.utcnow)
58
- user = db.relationship('User', backref='files')
59
 
60
- # ─── Auth routes ────────────────────────────────────────────────────────────────
61
 
62
  @app.route('/api/register', methods=['POST'])
63
  def register():
64
- data = request.json
65
  username = data.get('username', '').strip()
66
- email = data.get('email', '').strip().lower()
67
  password = data.get('password', '')
68
  if not username or not email or not password:
69
  return jsonify({'error': 'All fields required'}), 400
@@ -75,22 +102,22 @@ def register():
75
  db.session.add(user)
76
  db.session.commit()
77
  session.permanent = True
78
- session['user_id'] = user.id
79
  session['username'] = user.username
80
  return jsonify({'success': True, 'username': user.username})
81
 
82
  @app.route('/api/login', methods=['POST'])
83
  def login():
84
- data = request.json
85
  identifier = data.get('identifier', '').strip()
86
- password = data.get('password', '')
87
  user = User.query.filter(
88
  (User.username == identifier) | (User.email == identifier.lower())
89
  ).first()
90
  if not user or not check_password_hash(user.password_hash, password):
91
  return jsonify({'error': 'Invalid credentials'}), 401
92
  session.permanent = True
93
- session['user_id'] = user.id
94
  session['username'] = user.username
95
  return jsonify({'success': True, 'username': user.username})
96
 
@@ -109,7 +136,13 @@ def me():
109
  return jsonify({'authenticated': False}), 401
110
  return jsonify({'authenticated': True, 'username': user.username, 'email': user.email})
111
 
112
- # ─── Chat routes ────────────────────────────────────────────────────────────────
 
 
 
 
 
 
113
 
114
  @app.route('/api/chats', methods=['GET'])
115
  def get_chats():
@@ -117,8 +150,8 @@ def get_chats():
117
  return jsonify({'error': 'Unauthorized'}), 401
118
  chats = Chat.query.filter_by(user_id=session['user_id']).order_by(Chat.updated_at.desc()).all()
119
  return jsonify([{
120
- 'id': c.id,
121
- 'title': c.title,
122
  'created_at': c.created_at.isoformat(),
123
  'updated_at': c.updated_at.isoformat()
124
  } for c in chats])
@@ -127,10 +160,14 @@ def get_chats():
127
  def create_chat():
128
  if 'user_id' not in session:
129
  return jsonify({'error': 'Unauthorized'}), 401
130
- chat = Chat(user_id=session['user_id'])
 
 
 
 
131
  db.session.add(chat)
132
  db.session.commit()
133
- return jsonify({'id': chat.id, 'title': chat.title})
134
 
135
  @app.route('/api/chats/<chat_id>', methods=['GET'])
136
  def get_chat(chat_id):
@@ -139,8 +176,10 @@ def get_chat(chat_id):
139
  chat = Chat.query.filter_by(id=chat_id, user_id=session['user_id']).first()
140
  if not chat:
141
  return jsonify({'error': 'Not found'}), 404
142
- messages = [{'role': m.role, 'content': m.content, 'created_at': m.created_at.isoformat()} for m in chat.messages]
143
- return jsonify({'id': chat.id, 'title': chat.title, 'messages': messages})
 
 
144
 
145
  @app.route('/api/chats/<chat_id>', methods=['DELETE'])
146
  def delete_chat(chat_id):
@@ -157,12 +196,18 @@ def delete_chat(chat_id):
157
  def send_message(chat_id):
158
  if 'user_id' not in session:
159
  return jsonify({'error': 'Unauthorized'}), 401
 
160
  chat = Chat.query.filter_by(id=chat_id, user_id=session['user_id']).first()
161
  if not chat:
162
  return jsonify({'error': 'Not found'}), 404
163
 
164
- data = request.json
165
  user_content = data.get('content', '').strip()
 
 
 
 
 
166
  if not user_content:
167
  return jsonify({'error': 'Empty message'}), 400
168
 
@@ -170,39 +215,39 @@ def send_message(chat_id):
170
  user_msg = Message(chat_id=chat_id, role='user', content=user_content)
171
  db.session.add(user_msg)
172
 
173
- # Update chat title from first message
174
  if len(chat.messages) == 0:
175
- title = user_content[:60] + ('…' if len(user_content) > 60 else '')
176
- chat.title = title
177
 
 
178
  db.session.commit()
179
 
180
  # Build message history for API
181
  history = [{'role': m.role, 'content': m.content} for m in chat.messages]
182
 
183
- # Call Anthropic API
184
- api_key = os.environ.get('ANTHROPIC_API_KEY', '')
185
- if not api_key:
186
- ai_response = "⚠️ ANTHROPIC_API_KEY not set. Please set your API key in the environment."
 
 
 
 
 
 
187
  else:
188
  try:
189
- client = anthropic.Anthropic(api_key=api_key)
190
- response = client.messages.create(
191
- model="claude-opus-4-6", # Best model for coding & reasoning
 
 
192
  max_tokens=8192,
193
- system="""You are Claude, an expert AI assistant specializing in coding, reasoning, and problem-solving.
194
- You produce the highest quality, low-error code possible. When writing code:
195
- - Always use best practices and clean architecture
196
- - Include error handling and edge cases
197
- - Add clear comments and documentation
198
- - Provide complete, runnable solutions
199
- - Explain your reasoning step by step
200
- You are running on a Hugging Face Spaces deployment.""",
201
- messages=history
202
  )
203
- ai_response = response.content[0].text
204
  except Exception as e:
205
- ai_response = f"❌ API Error: {str(e)}"
206
 
207
  # Save assistant message
208
  ai_msg = Message(chat_id=chat_id, role='assistant', content=ai_response)
@@ -210,51 +255,56 @@ You are running on a Hugging Face Spaces deployment.""",
210
  chat.updated_at = datetime.utcnow()
211
  db.session.commit()
212
 
213
- # Background: extract and save code files
214
- threading.Thread(target=extract_and_save_code, args=(ai_response, chat_id, session['user_id']), daemon=True).start()
 
 
215
 
216
- return jsonify({'role': 'assistant', 'content': ai_response})
 
217
 
218
- # ─── File routes ────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
219
 
220
  def extract_and_save_code(content, chat_id, user_id):
221
- """Extract code blocks from AI response and save as files."""
222
  with app.app_context():
223
- import re
224
  pattern = r'```(\w+)?\n(.*?)```'
225
  matches = re.findall(pattern, content, re.DOTALL)
226
- ext_map = {
227
- 'python': 'py', 'py': 'py', 'javascript': 'js', 'js': 'js',
228
- 'typescript': 'ts', 'ts': 'ts', 'html': 'html', 'css': 'css',
229
- 'java': 'java', 'cpp': 'cpp', 'c': 'c', 'go': 'go',
230
- 'rust': 'rs', 'bash': 'sh', 'sh': 'sh', 'sql': 'sql',
231
- 'json': 'json', 'yaml': 'yaml', 'xml': 'xml', 'php': 'php',
232
- 'ruby': 'rb', 'swift': 'swift', 'kotlin': 'kt', 'r': 'r'
233
- }
234
  for i, (lang, code) in enumerate(matches):
235
  if not lang or len(code.strip()) < 10:
236
  continue
237
- ext = ext_map.get(lang.lower(), 'txt')
238
  timestamp = datetime.utcnow().strftime('%Y%m%d_%H%M%S')
239
- filename = f"code_{timestamp}_{i+1}.{ext}"
240
- user_dir = os.path.join('outputs', str(user_id))
241
  os.makedirs(user_dir, exist_ok=True)
242
- filepath = os.path.join(user_dir, filename)
243
  with open(filepath, 'w', encoding='utf-8') as f:
244
  f.write(code.strip())
245
- gf = GeneratedFile(
246
- user_id=user_id, chat_id=chat_id,
247
- filename=filename, filepath=filepath,
248
- file_type=lang.lower() or 'code'
249
- )
250
  db.session.add(gf)
251
  db.session.commit()
252
 
 
 
253
  @app.route('/api/files')
254
  def get_files():
255
  if 'user_id' not in session:
256
  return jsonify({'error': 'Unauthorized'}), 401
257
- files = GeneratedFile.query.filter_by(user_id=session['user_id']).order_by(GeneratedFile.created_at.desc()).limit(50).all()
 
 
 
258
  return jsonify([{
259
  'id': f.id, 'filename': f.filename,
260
  'file_type': f.file_type, 'chat_id': f.chat_id,
@@ -271,16 +321,16 @@ def download_file(file_id):
271
  directory = os.path.abspath(os.path.dirname(gf.filepath))
272
  return send_from_directory(directory, os.path.basename(gf.filepath), as_attachment=True)
273
 
274
- # ─── Main page ──────────────────────────────────────────────────────────────────
275
 
276
  @app.route('/')
277
  def index():
278
  return render_template('index.html')
279
 
280
- # ─── Init DB ────────────────────────────────────────────────────────────────────
281
 
282
  with app.app_context():
283
  db.create_all()
284
 
285
  if __name__ == '__main__':
286
- app.run(host='0.0.0.0', port=7860, debug=False)
 
1
  import os
2
+ import re
3
  import uuid
 
4
  import threading
5
  from datetime import datetime, timedelta
6
+ from flask import Flask, render_template, request, jsonify, session, send_from_directory
7
  from flask_sqlalchemy import SQLAlchemy
8
  from werkzeug.security import generate_password_hash, check_password_hash
9
+ from werkzeug.middleware.proxy_fix import ProxyFix # ← CRITICAL fix for HF Spaces
10
+
11
+ # ── HuggingFace official SDK ─────────────────────────────────────────────────
12
+ from huggingface_hub import InferenceClient
13
 
14
  app = Flask(__name__)
15
+
16
+ # ── CRITICAL: Fix session cookies behind HF Spaces reverse proxy ──────────────
17
+ app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)
18
+
19
+ app.secret_key = os.environ.get('SECRET_KEY', 'hf-spaces-secret-key-change-me-9182736455')
20
  app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///claudeai.db'
21
  app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
22
  app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30)
23
  app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
24
+ app.config['SESSION_COOKIE_HTTPONLY'] = True
25
+ app.config['SESSION_COOKIE_SECURE'] = False # keep False for HF Spaces HTTP
26
 
 
27
  os.makedirs('outputs', exist_ok=True)
28
  os.makedirs('static', exist_ok=True)
29
 
30
  db = SQLAlchemy(app)
31
 
32
+ # ── Available HuggingFace Models ─────────────────────────────────────────────
33
+ HF_MODELS = {
34
+ "Qwen/Qwen2.5-72B-Instruct": "Qwen 2.5 72B",
35
+ "meta-llama/Llama-3.3-70B-Instruct": "Llama 3.3 70B",
36
+ "mistralai/Mistral-7B-Instruct-v0.3": "Mistral 7B",
37
+ "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B": "DeepSeek-R1 32B",
38
+ }
39
+ DEFAULT_MODEL = "Qwen/Qwen2.5-72B-Instruct"
40
+
41
+ SYSTEM_PROMPT = """You are an expert AI assistant specializing in coding, reasoning, and problem-solving.
42
+ You produce the highest quality, low-error code possible. When writing code:
43
+ - Always use best practices and clean architecture
44
+ - Include error handling and edge cases
45
+ - Add clear comments and documentation
46
+ - Provide complete, runnable solutions
47
+ - Explain your reasoning step by step
48
+ You are running on a Hugging Face Spaces deployment powered by HuggingFace Inference API."""
49
+
50
+ # ─── Models ──────────────────────────────────────────────────────────────────
51
 
52
  class User(db.Model):
53
+ id = db.Column(db.Integer, primary_key=True)
54
+ username = db.Column(db.String(80), unique=True, nullable=False)
55
+ email = db.Column(db.String(120), unique=True, nullable=False)
56
  password_hash = db.Column(db.String(256), nullable=False)
57
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
58
+ chats = db.relationship('Chat', backref='user', lazy=True, cascade='all, delete-orphan')
59
 
60
  class Chat(db.Model):
61
+ id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
62
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
63
+ title = db.Column(db.String(200), default='New Chat')
64
+ model_used = db.Column(db.String(100), default=DEFAULT_MODEL)
65
  created_at = db.Column(db.DateTime, default=datetime.utcnow)
66
  updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
67
+ messages = db.relationship('Message', backref='chat', lazy=True,
68
+ cascade='all, delete-orphan', order_by='Message.created_at')
69
 
70
  class Message(db.Model):
71
+ id = db.Column(db.Integer, primary_key=True)
72
+ chat_id = db.Column(db.String(36), db.ForeignKey('chat.id'), nullable=False)
73
+ role = db.Column(db.String(20), nullable=False)
74
+ content = db.Column(db.Text, nullable=False)
75
  created_at = db.Column(db.DateTime, default=datetime.utcnow)
76
 
77
  class GeneratedFile(db.Model):
78
+ id = db.Column(db.Integer, primary_key=True)
79
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
80
+ chat_id = db.Column(db.String(36), db.ForeignKey('chat.id'), nullable=True)
81
+ filename = db.Column(db.String(200), nullable=False)
82
+ filepath = db.Column(db.String(500), nullable=False)
83
+ file_type = db.Column(db.String(50), default='code')
84
  created_at = db.Column(db.DateTime, default=datetime.utcnow)
85
+ user = db.relationship('User', backref='files')
86
 
87
+ # ─── Auth routes ─────────────────────────────────────────────────────────────
88
 
89
  @app.route('/api/register', methods=['POST'])
90
  def register():
91
+ data = request.json
92
  username = data.get('username', '').strip()
93
+ email = data.get('email', '').strip().lower()
94
  password = data.get('password', '')
95
  if not username or not email or not password:
96
  return jsonify({'error': 'All fields required'}), 400
 
102
  db.session.add(user)
103
  db.session.commit()
104
  session.permanent = True
105
+ session['user_id'] = user.id
106
  session['username'] = user.username
107
  return jsonify({'success': True, 'username': user.username})
108
 
109
  @app.route('/api/login', methods=['POST'])
110
  def login():
111
+ data = request.json
112
  identifier = data.get('identifier', '').strip()
113
+ password = data.get('password', '')
114
  user = User.query.filter(
115
  (User.username == identifier) | (User.email == identifier.lower())
116
  ).first()
117
  if not user or not check_password_hash(user.password_hash, password):
118
  return jsonify({'error': 'Invalid credentials'}), 401
119
  session.permanent = True
120
+ session['user_id'] = user.id
121
  session['username'] = user.username
122
  return jsonify({'success': True, 'username': user.username})
123
 
 
136
  return jsonify({'authenticated': False}), 401
137
  return jsonify({'authenticated': True, 'username': user.username, 'email': user.email})
138
 
139
+ # ─── Models info route ───────────────────────────────────────────────────────
140
+
141
+ @app.route('/api/models')
142
+ def get_models():
143
+ return jsonify([{'id': k, 'name': v} for k, v in HF_MODELS.items()])
144
+
145
+ # ─── Chat routes ─────────────────────────────────────────────────────────────
146
 
147
  @app.route('/api/chats', methods=['GET'])
148
  def get_chats():
 
150
  return jsonify({'error': 'Unauthorized'}), 401
151
  chats = Chat.query.filter_by(user_id=session['user_id']).order_by(Chat.updated_at.desc()).all()
152
  return jsonify([{
153
+ 'id': c.id, 'title': c.title,
154
+ 'model_used': c.model_used,
155
  'created_at': c.created_at.isoformat(),
156
  'updated_at': c.updated_at.isoformat()
157
  } for c in chats])
 
160
  def create_chat():
161
  if 'user_id' not in session:
162
  return jsonify({'error': 'Unauthorized'}), 401
163
+ data = request.json or {}
164
+ model = data.get('model', DEFAULT_MODEL)
165
+ if model not in HF_MODELS:
166
+ model = DEFAULT_MODEL
167
+ chat = Chat(user_id=session['user_id'], model_used=model)
168
  db.session.add(chat)
169
  db.session.commit()
170
+ return jsonify({'id': chat.id, 'title': chat.title, 'model_used': chat.model_used})
171
 
172
  @app.route('/api/chats/<chat_id>', methods=['GET'])
173
  def get_chat(chat_id):
 
176
  chat = Chat.query.filter_by(id=chat_id, user_id=session['user_id']).first()
177
  if not chat:
178
  return jsonify({'error': 'Not found'}), 404
179
+ messages = [{'role': m.role, 'content': m.content,
180
+ 'created_at': m.created_at.isoformat()} for m in chat.messages]
181
+ return jsonify({'id': chat.id, 'title': chat.title,
182
+ 'model_used': chat.model_used, 'messages': messages})
183
 
184
  @app.route('/api/chats/<chat_id>', methods=['DELETE'])
185
  def delete_chat(chat_id):
 
196
  def send_message(chat_id):
197
  if 'user_id' not in session:
198
  return jsonify({'error': 'Unauthorized'}), 401
199
+
200
  chat = Chat.query.filter_by(id=chat_id, user_id=session['user_id']).first()
201
  if not chat:
202
  return jsonify({'error': 'Not found'}), 404
203
 
204
+ data = request.json
205
  user_content = data.get('content', '').strip()
206
+ model_id = data.get('model', chat.model_used or DEFAULT_MODEL)
207
+
208
+ if model_id not in HF_MODELS:
209
+ model_id = DEFAULT_MODEL
210
+
211
  if not user_content:
212
  return jsonify({'error': 'Empty message'}), 400
213
 
 
215
  user_msg = Message(chat_id=chat_id, role='user', content=user_content)
216
  db.session.add(user_msg)
217
 
218
+ # Auto-title from first message
219
  if len(chat.messages) == 0:
220
+ chat.title = user_content[:60] + ('…' if len(user_content) > 60 else '')
 
221
 
222
+ chat.model_used = model_id
223
  db.session.commit()
224
 
225
  # Build message history for API
226
  history = [{'role': m.role, 'content': m.content} for m in chat.messages]
227
 
228
+ # ── Call HuggingFace Inference API ────────────────────────────────────────
229
+ hf_token = os.environ.get('HF_TOKEN', '')
230
+ if not hf_token:
231
+ ai_response = (
232
+ "⚠️ **HF_TOKEN not set.**\n\n"
233
+ "Please add your Hugging Face token:\n"
234
+ "1. Go to your Space β†’ **Settings** β†’ **Variables and secrets**\n"
235
+ "2. Add a new secret: `HF_TOKEN` = your token from https://huggingface.co/settings/tokens\n"
236
+ "3. Restart the Space."
237
+ )
238
  else:
239
  try:
240
+ client = InferenceClient(api_key=hf_token)
241
+ messages_payload = [{"role": "system", "content": SYSTEM_PROMPT}] + history
242
+ response = client.chat.completions.create(
243
+ model=model_id,
244
+ messages=messages_payload,
245
  max_tokens=8192,
246
+ temperature=0.7,
 
 
 
 
 
 
 
 
247
  )
248
+ ai_response = response.choices[0].message.content
249
  except Exception as e:
250
+ ai_response = f"❌ **HuggingFace API Error:** `{str(e)}`"
251
 
252
  # Save assistant message
253
  ai_msg = Message(chat_id=chat_id, role='assistant', content=ai_response)
 
255
  chat.updated_at = datetime.utcnow()
256
  db.session.commit()
257
 
258
+ # Background: extract and save generated code files
259
+ uid = session['user_id']
260
+ threading.Thread(target=extract_and_save_code,
261
+ args=(ai_response, chat_id, uid), daemon=True).start()
262
 
263
+ return jsonify({'role': 'assistant', 'content': ai_response,
264
+ 'model': model_id, 'model_name': HF_MODELS.get(model_id, model_id)})
265
 
266
+ # ─── File extraction ──────────────────────────────────────────────────────────
267
+
268
+ EXT_MAP = {
269
+ 'python': 'py', 'py': 'py', 'javascript': 'js', 'js': 'js',
270
+ 'typescript': 'ts', 'ts': 'ts', 'html': 'html', 'css': 'css',
271
+ 'java': 'java', 'cpp': 'cpp', 'c': 'c', 'go': 'go',
272
+ 'rust': 'rs', 'bash': 'sh', 'sh': 'sh', 'sql': 'sql',
273
+ 'json': 'json', 'yaml': 'yaml', 'xml': 'xml', 'php': 'php',
274
+ 'ruby': 'rb', 'swift': 'swift', 'kotlin': 'kt', 'r': 'r',
275
+ }
276
 
277
  def extract_and_save_code(content, chat_id, user_id):
 
278
  with app.app_context():
 
279
  pattern = r'```(\w+)?\n(.*?)```'
280
  matches = re.findall(pattern, content, re.DOTALL)
 
 
 
 
 
 
 
 
281
  for i, (lang, code) in enumerate(matches):
282
  if not lang or len(code.strip()) < 10:
283
  continue
284
+ ext = EXT_MAP.get(lang.lower(), 'txt')
285
  timestamp = datetime.utcnow().strftime('%Y%m%d_%H%M%S')
286
+ filename = f"code_{timestamp}_{i+1}.{ext}"
287
+ user_dir = os.path.join('outputs', str(user_id))
288
  os.makedirs(user_dir, exist_ok=True)
289
+ filepath = os.path.join(user_dir, filename)
290
  with open(filepath, 'w', encoding='utf-8') as f:
291
  f.write(code.strip())
292
+ gf = GeneratedFile(user_id=user_id, chat_id=chat_id,
293
+ filename=filename, filepath=filepath,
294
+ file_type=lang.lower() or 'code')
 
 
295
  db.session.add(gf)
296
  db.session.commit()
297
 
298
+ # ─── File routes ─────────────────────────────────────────────────────────────
299
+
300
  @app.route('/api/files')
301
  def get_files():
302
  if 'user_id' not in session:
303
  return jsonify({'error': 'Unauthorized'}), 401
304
+ files = (GeneratedFile.query
305
+ .filter_by(user_id=session['user_id'])
306
+ .order_by(GeneratedFile.created_at.desc())
307
+ .limit(50).all())
308
  return jsonify([{
309
  'id': f.id, 'filename': f.filename,
310
  'file_type': f.file_type, 'chat_id': f.chat_id,
 
321
  directory = os.path.abspath(os.path.dirname(gf.filepath))
322
  return send_from_directory(directory, os.path.basename(gf.filepath), as_attachment=True)
323
 
324
+ # ─── Main page ───────────────────────────────────────────────────────────────
325
 
326
  @app.route('/')
327
  def index():
328
  return render_template('index.html')
329
 
330
+ # ─── Init DB ─────────────────────────────────────────────────────────────────
331
 
332
  with app.app_context():
333
  db.create_all()
334
 
335
  if __name__ == '__main__':
336
+ app.run(host='0.0.0.0', port=7860, debug=False)
index.html ADDED
@@ -0,0 +1,1051 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>AI Chat β€” HuggingFace Models</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com"/>
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
9
+ <link href="https://fonts.googleapis.com/css2?family=Sora:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/>
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"/>
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
12
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js"></script>
13
+ <style>
14
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
15
+ :root {
16
+ --bg-base: #0d0d0d;
17
+ --bg-sidebar: #111111;
18
+ --bg-card: #181818;
19
+ --bg-input: #1e1e1e;
20
+ --bg-hover: #222222;
21
+ --bg-active: #2a2a2a;
22
+ --border: #2a2a2a;
23
+ --border-lt: #333;
24
+ --text-pri: #f0ede8;
25
+ --text-sec: #8a8580;
26
+ --text-muted: #555;
27
+ --accent: #FF9900; /* HuggingFace orange */
28
+ --accent2: #6366f1; /* indigo highlight */
29
+ --accent-h: #FFB347;
30
+ --accent-dim: rgba(255,153,0,0.12);
31
+ --accent-glow:rgba(255,153,0,0.28);
32
+ --success: #4ade80;
33
+ --error: #f87171;
34
+ --sidebar-w: 280px;
35
+ --radius: 12px;
36
+ --radius-sm: 8px;
37
+ --radius-lg: 16px;
38
+ --font: 'Sora', sans-serif;
39
+ --mono: 'JetBrains Mono', monospace;
40
+ --shadow: 0 8px 32px rgba(0,0,0,0.5);
41
+ --shadow-sm: 0 2px 8px rgba(0,0,0,0.3);
42
+ }
43
+ html, body { height: 100%; overflow: hidden; }
44
+ body {
45
+ font-family: var(--font);
46
+ background: var(--bg-base);
47
+ color: var(--text-pri);
48
+ font-size: 15px;
49
+ line-height: 1.6;
50
+ -webkit-font-smoothing: antialiased;
51
+ }
52
+ ::-webkit-scrollbar { width: 4px; height: 4px; }
53
+ ::-webkit-scrollbar-track { background: transparent; }
54
+ ::-webkit-scrollbar-thumb { background: var(--border-lt); border-radius: 4px; }
55
+
56
+ /* ── Auth Overlay ── */
57
+ #auth-overlay {
58
+ position: fixed; inset: 0; z-index: 100;
59
+ background: var(--bg-base);
60
+ display: flex; align-items: center; justify-content: center;
61
+ padding: 20px;
62
+ }
63
+ .auth-box {
64
+ background: var(--bg-card);
65
+ border: 1px solid var(--border);
66
+ border-radius: var(--radius-lg);
67
+ padding: 48px 40px;
68
+ width: 100%; max-width: 420px;
69
+ box-shadow: var(--shadow);
70
+ animation: slideUp 0.4s cubic-bezier(0.16,1,0.3,1);
71
+ }
72
+ @keyframes slideUp {
73
+ from { opacity: 0; transform: translateY(24px); }
74
+ to { opacity: 1; transform: translateY(0); }
75
+ }
76
+ .auth-logo {
77
+ display: flex; align-items: center; gap: 12px;
78
+ margin-bottom: 8px;
79
+ }
80
+ .auth-logo-img { width: 38px; height: 38px; }
81
+ .auth-logo-text { font-size: 22px; font-weight: 700; color: var(--text-pri); }
82
+ .auth-subtitle { font-size: 13px; color: var(--text-sec); margin-bottom: 28px; }
83
+ .auth-tabs {
84
+ display: flex; gap: 4px;
85
+ background: var(--bg-input);
86
+ border-radius: var(--radius-sm);
87
+ padding: 4px; margin-bottom: 24px;
88
+ }
89
+ .auth-tab {
90
+ flex: 1; padding: 10px;
91
+ border: none; background: transparent;
92
+ color: var(--text-sec); font-family: var(--font);
93
+ font-size: 14px; font-weight: 500;
94
+ border-radius: 6px; cursor: pointer;
95
+ transition: all 0.2s;
96
+ }
97
+ .auth-tab.active {
98
+ background: var(--bg-card);
99
+ color: var(--text-pri);
100
+ box-shadow: var(--shadow-sm);
101
+ }
102
+ .auth-form { display: flex; flex-direction: column; gap: 14px; }
103
+ .auth-form.hidden { display: none; }
104
+ .field-group { display: flex; flex-direction: column; gap: 5px; }
105
+ .field-group label { font-size: 12px; font-weight: 600; color: var(--text-sec); text-transform: uppercase; letter-spacing: 0.05em; }
106
+ .field-group input {
107
+ background: var(--bg-input);
108
+ border: 1px solid var(--border);
109
+ border-radius: var(--radius-sm);
110
+ padding: 12px 14px;
111
+ color: var(--text-pri);
112
+ font-family: var(--font); font-size: 14px;
113
+ outline: none; transition: border-color 0.2s, box-shadow 0.2s;
114
+ width: 100%;
115
+ }
116
+ .field-group input:focus {
117
+ border-color: var(--accent);
118
+ box-shadow: 0 0 0 3px var(--accent-dim);
119
+ }
120
+ .btn-primary {
121
+ background: var(--accent); color: #000;
122
+ border: none; border-radius: var(--radius-sm);
123
+ padding: 13px 20px; font-family: var(--font);
124
+ font-size: 15px; font-weight: 700; cursor: pointer;
125
+ transition: all 0.2s; margin-top: 4px;
126
+ letter-spacing: 0.01em;
127
+ }
128
+ .btn-primary:hover { background: var(--accent-h); transform: translateY(-1px); }
129
+ .btn-primary:active { transform: translateY(0); }
130
+ .auth-error {
131
+ color: var(--error); font-size: 13px; text-align: center;
132
+ padding: 10px; background: rgba(248,113,113,0.1);
133
+ border-radius: var(--radius-sm); display: none;
134
+ }
135
+ .auth-error.show { display: block; }
136
+
137
+ /* ── App Layout ── */
138
+ #app { display: flex; height: 100vh; }
139
+ #app.hidden { display: none; }
140
+
141
+ /* ── Sidebar ── */
142
+ #sidebar {
143
+ width: var(--sidebar-w);
144
+ background: var(--bg-sidebar);
145
+ border-right: 1px solid var(--border);
146
+ display: flex; flex-direction: column;
147
+ flex-shrink: 0;
148
+ transition: transform 0.3s cubic-bezier(0.16,1,0.3,1);
149
+ z-index: 10; height: 100vh;
150
+ }
151
+ .sidebar-header {
152
+ padding: 18px 14px 12px;
153
+ border-bottom: 1px solid var(--border);
154
+ display: flex; align-items: center; justify-content: space-between;
155
+ }
156
+ .sidebar-logo { display: flex; align-items: center; gap: 10px; }
157
+ .hf-logo { width: 26px; height: 26px; }
158
+ .sidebar-logo span { font-size: 15px; font-weight: 700; }
159
+ .btn-new-chat {
160
+ display: flex; align-items: center; gap: 6px;
161
+ background: var(--accent-dim);
162
+ border: 1px solid var(--accent-glow);
163
+ border-radius: var(--radius-sm);
164
+ color: var(--accent); font-family: var(--font);
165
+ font-size: 12px; font-weight: 700; padding: 7px 12px;
166
+ cursor: pointer; transition: all 0.2s; white-space: nowrap;
167
+ }
168
+ .btn-new-chat:hover { background: var(--accent); color: #000; }
169
+ .sidebar-section-label {
170
+ padding: 14px 14px 4px;
171
+ font-size: 10px; font-weight: 700; letter-spacing: 0.1em;
172
+ color: var(--text-muted); text-transform: uppercase;
173
+ }
174
+ .chat-list { flex: 1; overflow-y: auto; padding: 4px 6px; }
175
+ .chat-item {
176
+ display: flex; align-items: center; gap: 8px;
177
+ padding: 9px 10px; border-radius: var(--radius-sm);
178
+ cursor: pointer; transition: background 0.15s;
179
+ margin-bottom: 2px; position: relative;
180
+ }
181
+ .chat-item:hover { background: var(--bg-hover); }
182
+ .chat-item.active { background: var(--bg-active); }
183
+ .chat-item-icon { font-size: 13px; opacity: 0.5; flex-shrink: 0; }
184
+ .chat-item-info { flex: 1; min-width: 0; }
185
+ .chat-item-title {
186
+ font-size: 12.5px; color: var(--text-sec);
187
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
188
+ }
189
+ .chat-item.active .chat-item-title { color: var(--text-pri); }
190
+ .chat-item-model {
191
+ font-size: 10px; color: var(--text-muted); margin-top: 1px;
192
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
193
+ }
194
+ .chat-item-del {
195
+ opacity: 0; background: none; border: none;
196
+ color: var(--text-muted); cursor: pointer; font-size: 15px;
197
+ padding: 2px 4px; border-radius: 4px; transition: all 0.15s; flex-shrink: 0;
198
+ }
199
+ .chat-item:hover .chat-item-del { opacity: 1; }
200
+ .chat-item-del:hover { color: var(--error); background: rgba(248,113,113,0.1); }
201
+ .sidebar-footer { border-top: 1px solid var(--border); padding: 14px; }
202
+ .user-card {
203
+ display: flex; align-items: center; gap: 10px;
204
+ padding: 10px 10px; border-radius: var(--radius-sm);
205
+ cursor: pointer; transition: background 0.15s;
206
+ }
207
+ .user-card:hover { background: var(--bg-hover); }
208
+ .user-avatar {
209
+ width: 32px; height: 32px; border-radius: 50%;
210
+ background: var(--accent); display: flex;
211
+ align-items: center; justify-content: center;
212
+ font-size: 13px; font-weight: 800; color: #000; flex-shrink: 0;
213
+ }
214
+ .user-info { flex: 1; min-width: 0; }
215
+ .user-name { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
216
+ .user-plan { font-size: 10px; color: var(--text-muted); }
217
+ .btn-logout {
218
+ background: none; border: none; color: var(--text-muted);
219
+ font-size: 17px; cursor: pointer; padding: 4px;
220
+ border-radius: 4px; transition: color 0.15s; flex-shrink: 0;
221
+ }
222
+ .btn-logout:hover { color: var(--error); }
223
+ .files-btn {
224
+ display: flex; align-items: center; gap: 8px;
225
+ padding: 9px 10px; border-radius: var(--radius-sm);
226
+ font-size: 12px; color: var(--text-sec);
227
+ margin: 4px 6px; border: none; background: none;
228
+ font-family: var(--font); width: calc(100% - 12px); cursor: pointer;
229
+ transition: background 0.15s;
230
+ }
231
+ .files-btn:hover { background: var(--bg-hover); color: var(--text-pri); }
232
+
233
+ /* ── Main ── */
234
+ #main { flex: 1; display: flex; flex-direction: column; min-width: 0; height: 100vh; }
235
+
236
+ /* ── Topbar ── */
237
+ #topbar {
238
+ display: flex; align-items: center; gap: 10px;
239
+ padding: 12px 18px; border-bottom: 1px solid var(--border);
240
+ background: var(--bg-base); flex-shrink: 0;
241
+ }
242
+ #sidebar-toggle {
243
+ display: none; background: none; border: none;
244
+ color: var(--text-sec); cursor: pointer;
245
+ padding: 6px; border-radius: var(--radius-sm); font-size: 20px;
246
+ transition: all 0.15s;
247
+ }
248
+ #sidebar-toggle:hover { background: var(--bg-hover); color: var(--text-pri); }
249
+ .topbar-title { font-size: 14px; font-weight: 500; flex: 1; color: var(--text-sec); min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
250
+ .model-badge {
251
+ display: flex; align-items: center; gap: 6px;
252
+ background: var(--bg-card); border: 1px solid var(--border);
253
+ border-radius: 20px; padding: 5px 12px;
254
+ font-size: 11px; color: var(--text-sec); white-space: nowrap;
255
+ }
256
+ .model-dot { width: 6px; height: 6px; background: var(--success); border-radius: 50%; flex-shrink: 0; }
257
+
258
+ /* ── Messages ── */
259
+ #messages-container { flex: 1; overflow-y: auto; padding: 0 0 16px; }
260
+ #messages { max-width: 760px; margin: 0 auto; padding: 24px 18px; }
261
+
262
+ /* ── Welcome ── */
263
+ #welcome {
264
+ display: flex; flex-direction: column; align-items: center;
265
+ justify-content: center; min-height: 60vh; text-align: center; gap: 18px;
266
+ }
267
+ .welcome-icon { font-size: 52px; animation: pulse 3s ease-in-out infinite; }
268
+ @keyframes pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.06); } }
269
+ .welcome-title { font-size: 30px; font-weight: 700; }
270
+ .welcome-title span { color: var(--accent); }
271
+ .welcome-sub { color: var(--text-sec); font-size: 15px; max-width: 440px; line-height: 1.7; }
272
+ .welcome-chips { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 6px; }
273
+ .chip {
274
+ background: var(--bg-card); border: 1px solid var(--border);
275
+ border-radius: 20px; padding: 8px 16px; font-size: 13px;
276
+ color: var(--text-sec); cursor: pointer; transition: all 0.2s;
277
+ }
278
+ .chip:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-dim); }
279
+
280
+ /* ── Message rows ── */
281
+ .message-row {
282
+ display: flex; gap: 14px; margin-bottom: 22px;
283
+ animation: msgIn 0.3s cubic-bezier(0.16,1,0.3,1);
284
+ }
285
+ @keyframes msgIn {
286
+ from { opacity: 0; transform: translateY(8px); }
287
+ to { opacity: 1; transform: translateY(0); }
288
+ }
289
+ .message-row.user { flex-direction: row-reverse; }
290
+ .msg-avatar {
291
+ width: 30px; height: 30px; border-radius: 50%;
292
+ display: flex; align-items: center; justify-content: center;
293
+ font-size: 12px; font-weight: 800; flex-shrink: 0; margin-top: 2px;
294
+ }
295
+ .msg-avatar.user { background: var(--accent); color: #000; }
296
+ .msg-avatar.assistant {
297
+ background: var(--bg-card); border: 1px solid var(--border);
298
+ font-size: 15px;
299
+ }
300
+ .msg-body { flex: 1; min-width: 0; }
301
+ .msg-name { font-size: 11px; font-weight: 700; color: var(--text-muted); margin-bottom: 5px; text-transform: uppercase; letter-spacing: 0.05em; }
302
+ .message-row.user .msg-name { text-align: right; }
303
+ .msg-content {
304
+ background: var(--bg-card); border: 1px solid var(--border);
305
+ border-radius: var(--radius); padding: 14px 18px;
306
+ font-size: 14.5px; line-height: 1.75; word-break: break-word;
307
+ }
308
+ .message-row.user .msg-content {
309
+ background: var(--accent-dim); border-color: var(--accent-glow);
310
+ }
311
+ .msg-content p { margin: 0 0 10px; }
312
+ .msg-content p:last-child { margin-bottom: 0; }
313
+ .msg-content h1,.msg-content h2,.msg-content h3 { margin: 14px 0 6px; font-weight: 600; }
314
+ .msg-content ul,.msg-content ol { padding-left: 20px; margin: 6px 0; }
315
+ .msg-content li { margin-bottom: 3px; }
316
+ .msg-content pre {
317
+ background: #0d1117; border: 1px solid var(--border);
318
+ border-radius: var(--radius-sm); overflow-x: auto; margin: 10px 0;
319
+ position: relative;
320
+ }
321
+ .msg-content pre code {
322
+ font-family: var(--mono); font-size: 13px; padding: 14px 18px; display: block;
323
+ }
324
+ .msg-content code:not(pre code) {
325
+ font-family: var(--mono); font-size: 13px;
326
+ background: var(--bg-input); border: 1px solid var(--border);
327
+ border-radius: 4px; padding: 2px 6px; color: var(--accent);
328
+ }
329
+ .copy-btn {
330
+ position: absolute; top: 8px; right: 8px;
331
+ background: var(--bg-hover); border: 1px solid var(--border);
332
+ border-radius: 6px; color: var(--text-muted); font-family: var(--font);
333
+ font-size: 11px; padding: 4px 10px; cursor: pointer; transition: all 0.15s;
334
+ }
335
+ .copy-btn:hover { background: var(--bg-active); color: var(--text-pri); }
336
+
337
+ /* ── Typing ── */
338
+ .typing-row { display: flex; gap: 14px; margin-bottom: 22px; }
339
+ .typing-dots {
340
+ background: var(--bg-card); border: 1px solid var(--border);
341
+ border-radius: var(--radius); padding: 14px 18px;
342
+ display: flex; align-items: center; gap: 6px;
343
+ }
344
+ .typing-dot {
345
+ width: 7px; height: 7px; background: var(--text-muted);
346
+ border-radius: 50%; animation: typingBounce 1.2s ease-in-out infinite;
347
+ }
348
+ .typing-dot:nth-child(2) { animation-delay: 0.2s; }
349
+ .typing-dot:nth-child(3) { animation-delay: 0.4s; }
350
+ @keyframes typingBounce {
351
+ 0%,60%,100% { transform: translateY(0); opacity: 0.4; }
352
+ 30% { transform: translateY(-8px); opacity: 1; }
353
+ }
354
+
355
+ /* ── Input area ── */
356
+ #input-area {
357
+ border-top: 1px solid var(--border);
358
+ padding: 14px 18px 18px;
359
+ background: var(--bg-base); flex-shrink: 0;
360
+ }
361
+ .input-box {
362
+ max-width: 760px; margin: 0 auto;
363
+ background: var(--bg-card); border: 1px solid var(--border);
364
+ border-radius: var(--radius-lg); padding: 4px 8px 8px;
365
+ transition: border-color 0.2s, box-shadow 0.2s;
366
+ }
367
+ .input-box:focus-within {
368
+ border-color: var(--accent);
369
+ box-shadow: 0 0 0 3px var(--accent-dim);
370
+ }
371
+ #msg-input {
372
+ width: 100%; min-height: 52px; max-height: 200px;
373
+ background: transparent; border: none; outline: none;
374
+ resize: none; color: var(--text-pri);
375
+ font-family: var(--font); font-size: 15px; line-height: 1.6;
376
+ padding: 12px 12px 4px; overflow-y: auto;
377
+ }
378
+ #msg-input::placeholder { color: var(--text-muted); }
379
+ .input-toolbar {
380
+ display: flex; align-items: center; justify-content: space-between;
381
+ padding: 0 6px 2px; gap: 8px;
382
+ }
383
+ .input-left { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
384
+ .model-select {
385
+ background: var(--bg-input); border: 1px solid var(--border);
386
+ color: var(--text-sec); font-family: var(--font); font-size: 12px;
387
+ padding: 5px 10px; border-radius: 20px; cursor: pointer; outline: none;
388
+ max-width: 180px;
389
+ }
390
+ .model-select:focus { border-color: var(--accent); }
391
+ #send-btn {
392
+ background: var(--accent); border: none; color: #000;
393
+ width: 36px; height: 36px; border-radius: 10px;
394
+ cursor: pointer; font-size: 18px; font-weight: 700;
395
+ display: flex; align-items: center; justify-content: center;
396
+ transition: all 0.2s; flex-shrink: 0;
397
+ }
398
+ #send-btn:hover { background: var(--accent-h); transform: scale(1.05); }
399
+ #send-btn:disabled { background: var(--bg-input); cursor: not-allowed; transform: none; color: var(--text-muted); }
400
+ .input-hint { text-align: center; font-size: 11px; color: var(--text-muted); margin-top: 8px; }
401
+
402
+ /* ── Files modal ── */
403
+ #files-modal {
404
+ position: fixed; inset: 0; z-index: 50;
405
+ background: rgba(0,0,0,0.7); backdrop-filter: blur(4px);
406
+ display: flex; align-items: center; justify-content: center; padding: 20px;
407
+ }
408
+ #files-modal.hidden { display: none; }
409
+ .files-panel {
410
+ background: var(--bg-card); border: 1px solid var(--border);
411
+ border-radius: var(--radius-lg); width: 100%; max-width: 540px;
412
+ max-height: 80vh; display: flex; flex-direction: column;
413
+ box-shadow: var(--shadow); animation: slideUp 0.3s cubic-bezier(0.16,1,0.3,1);
414
+ }
415
+ .files-panel-header {
416
+ display: flex; align-items: center; justify-content: space-between;
417
+ padding: 18px 22px; border-bottom: 1px solid var(--border);
418
+ }
419
+ .files-panel-header h3 { font-size: 16px; font-weight: 600; }
420
+ .btn-close {
421
+ background: none; border: none; color: var(--text-muted);
422
+ font-size: 20px; cursor: pointer; padding: 4px; border-radius: 6px; transition: all 0.15s;
423
+ }
424
+ .btn-close:hover { background: var(--bg-hover); color: var(--text-pri); }
425
+ .files-list { flex: 1; overflow-y: auto; padding: 10px; }
426
+ .file-item {
427
+ display: flex; align-items: center; gap: 10px;
428
+ padding: 10px 14px; border-radius: var(--radius-sm);
429
+ border: 1px solid var(--border); margin-bottom: 6px; transition: background 0.15s;
430
+ }
431
+ .file-item:hover { background: var(--bg-hover); }
432
+ .file-icon {
433
+ width: 34px; height: 34px; border-radius: var(--radius-sm);
434
+ background: var(--accent-dim); border: 1px solid var(--accent-glow);
435
+ display: flex; align-items: center; justify-content: center;
436
+ font-size: 16px; flex-shrink: 0;
437
+ }
438
+ .file-info { flex: 1; min-width: 0; }
439
+ .file-name { font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
440
+ .file-meta { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
441
+ .btn-dl {
442
+ background: var(--accent-dim); border: 1px solid var(--accent-glow);
443
+ color: var(--accent); font-family: var(--font);
444
+ font-size: 11px; padding: 5px 12px; border-radius: 20px;
445
+ cursor: pointer; transition: all 0.2s; white-space: nowrap;
446
+ }
447
+ .btn-dl:hover { background: var(--accent); color: #000; }
448
+ .empty-files { text-align: center; padding: 36px; color: var(--text-muted); font-size: 13px; line-height: 1.8; }
449
+
450
+ /* ── Sidebar mobile overlay ── */
451
+ #sidebar-overlay {
452
+ display: none; position: fixed; inset: 0;
453
+ background: rgba(0,0,0,0.6); z-index: 9;
454
+ }
455
+
456
+ /* ── Toast ── */
457
+ #toast {
458
+ position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(20px);
459
+ background: var(--bg-card); border: 1px solid var(--border);
460
+ border-radius: var(--radius-sm); padding: 10px 20px;
461
+ font-size: 13px; opacity: 0; pointer-events: none;
462
+ transition: all 0.3s; z-index: 200; white-space: nowrap;
463
+ box-shadow: var(--shadow);
464
+ }
465
+ #toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
466
+
467
+ /* ── Responsive ── */
468
+ @media (max-width: 768px) {
469
+ #sidebar {
470
+ position: fixed; left: 0; top: 0; bottom: 0;
471
+ transform: translateX(-100%); z-index: 20;
472
+ }
473
+ #sidebar.open { transform: translateX(0); }
474
+ #sidebar-overlay { display: block; opacity: 0; pointer-events: none; transition: opacity 0.3s; }
475
+ #sidebar-overlay.show { opacity: 1; pointer-events: all; }
476
+ #sidebar-toggle { display: flex !important; }
477
+ .auth-box { padding: 28px 22px; }
478
+ .welcome-title { font-size: 24px; }
479
+ #topbar { padding: 10px 14px; }
480
+ #messages { padding: 14px 10px; }
481
+ #input-area { padding: 10px 10px 14px; }
482
+ .msg-content { padding: 12px 14px; }
483
+ }
484
+ @media (min-width: 1400px) {
485
+ #messages, .input-box { max-width: 860px; }
486
+ }
487
+ </style>
488
+ </head>
489
+ <body>
490
+
491
+ <!-- Auth Overlay -->
492
+ <div id="auth-overlay">
493
+ <div class="auth-box">
494
+ <div class="auth-logo">
495
+ <!-- HuggingFace logo SVG -->
496
+ <svg class="auth-logo-img" viewBox="0 0 95 88" fill="none" xmlns="http://www.w3.org/2000/svg">
497
+ <path d="M47.5 0C21.3 0 0 19.7 0 44s21.3 44 47.5 44S95 68.3 95 44 73.7 0 47.5 0z" fill="#FF9900" opacity=".15"/>
498
+ <text x="50%" y="62%" dominant-baseline="middle" text-anchor="middle" font-size="52" font-family="serif">πŸ€—</text>
499
+ </svg>
500
+ <span class="auth-logo-text">HF Chat</span>
501
+ </div>
502
+ <p class="auth-subtitle">Powered by HuggingFace Inference API</p>
503
+ <div class="auth-tabs">
504
+ <button class="auth-tab active" onclick="switchTab('login')">Sign In</button>
505
+ <button class="auth-tab" onclick="switchTab('register')">Create Account</button>
506
+ </div>
507
+ <div id="auth-error" class="auth-error"></div>
508
+ <div class="auth-form" id="login-form">
509
+ <div class="field-group">
510
+ <label>Email or Username</label>
511
+ <input type="text" id="login-id" placeholder="you@example.com" autocomplete="username"/>
512
+ </div>
513
+ <div class="field-group">
514
+ <label>Password</label>
515
+ <input type="password" id="login-pass" placeholder="β€’β€’β€’β€’β€’β€’β€’β€’" autocomplete="current-password"/>
516
+ </div>
517
+ <button class="btn-primary" onclick="doLogin()">Sign In β†’</button>
518
+ </div>
519
+ <div class="auth-form hidden" id="register-form">
520
+ <div class="field-group">
521
+ <label>Username</label>
522
+ <input type="text" id="reg-user" placeholder="yourusername" autocomplete="username"/>
523
+ </div>
524
+ <div class="field-group">
525
+ <label>Email</label>
526
+ <input type="email" id="reg-email" placeholder="you@example.com" autocomplete="email"/>
527
+ </div>
528
+ <div class="field-group">
529
+ <label>Password</label>
530
+ <input type="password" id="reg-pass" placeholder="Create a strong password" autocomplete="new-password"/>
531
+ </div>
532
+ <button class="btn-primary" onclick="doRegister()">Create Account β†’</button>
533
+ </div>
534
+ </div>
535
+ </div>
536
+
537
+ <!-- Mobile sidebar overlay -->
538
+ <div id="sidebar-overlay" onclick="closeSidebar()"></div>
539
+
540
+ <!-- Main App -->
541
+ <div id="app" class="hidden">
542
+ <!-- Sidebar -->
543
+ <aside id="sidebar">
544
+ <div class="sidebar-header">
545
+ <div class="sidebar-logo">
546
+ <span style="font-size:22px">πŸ€—</span>
547
+ <span>HF Chat</span>
548
+ </div>
549
+ <button class="btn-new-chat" onclick="newChat()">οΌ‹ New</button>
550
+ </div>
551
+ <div class="sidebar-section-label">Recent Chats</div>
552
+ <div class="chat-list" id="chat-list"></div>
553
+ <button class="files-btn" onclick="openFilesModal()">πŸ“ Generated Files</button>
554
+ <div class="sidebar-footer">
555
+ <div class="user-card">
556
+ <div class="user-avatar" id="user-avatar">?</div>
557
+ <div class="user-info">
558
+ <div class="user-name" id="user-name-display">Loading…</div>
559
+ <div class="user-plan" id="user-plan-display">HuggingFace Models</div>
560
+ </div>
561
+ <button class="btn-logout" onclick="doLogout(event)" title="Logout">βŽ‹</button>
562
+ </div>
563
+ </div>
564
+ </aside>
565
+
566
+ <!-- Main content -->
567
+ <div id="main">
568
+ <div id="topbar">
569
+ <button id="sidebar-toggle" onclick="toggleSidebar()" style="display:none">☰</button>
570
+ <div class="topbar-title" id="chat-title">New Chat</div>
571
+ <div class="model-badge">
572
+ <div class="model-dot"></div>
573
+ <span id="topbar-model-name">Qwen 2.5 72B</span>
574
+ </div>
575
+ </div>
576
+
577
+ <div id="messages-container">
578
+ <div id="messages">
579
+ <div id="welcome">
580
+ <div class="welcome-icon">πŸ€—</div>
581
+ <div class="welcome-title">Hello! I'm <span>HF Chat</span></div>
582
+ <div class="welcome-sub">Powered by HuggingFace's best open-source models. Ask me anything β€” I specialize in coding, reasoning, and problem-solving.</div>
583
+ <div class="welcome-chips">
584
+ <div class="chip" onclick="useChip(this)">πŸ§‘β€πŸ’» Write Python code</div>
585
+ <div class="chip" onclick="useChip(this)">πŸ” Debug my code</div>
586
+ <div class="chip" onclick="useChip(this)">πŸ—οΈ Design system architecture</div>
587
+ <div class="chip" onclick="useChip(this)">πŸ“– Explain a concept</div>
588
+ <div class="chip" onclick="useChip(this)">⚑ Optimize performance</div>
589
+ <div class="chip" onclick="useChip(this)">🌐 Build a Flask app</div>
590
+ </div>
591
+ </div>
592
+ </div>
593
+ </div>
594
+
595
+ <div id="input-area">
596
+ <div class="input-box">
597
+ <textarea id="msg-input" placeholder="Message HF Chat…" rows="1"></textarea>
598
+ <div class="input-toolbar">
599
+ <div class="input-left">
600
+ <select class="model-select" id="model-select" onchange="onModelChange()">
601
+ <option value="Qwen/Qwen2.5-72B-Instruct">🧠 Qwen 2.5 72B</option>
602
+ <option value="meta-llama/Llama-3.3-70B-Instruct">πŸ¦™ Llama 3.3 70B</option>
603
+ <option value="mistralai/Mistral-7B-Instruct-v0.3">⚑ Mistral 7B (Fast)</option>
604
+ <option value="deepseek-ai/DeepSeek-R1-Distill-Qwen-32B">πŸ” DeepSeek-R1 32B</option>
605
+ </select>
606
+ </div>
607
+ <button id="send-btn" onclick="sendMessage()" disabled title="Send (Enter)">↑</button>
608
+ </div>
609
+ </div>
610
+ <div class="input-hint">AI can make mistakes. Always verify important information.</div>
611
+ </div>
612
+ </div>
613
+ </div>
614
+
615
+ <!-- Files Modal -->
616
+ <div id="files-modal" class="hidden">
617
+ <div class="files-panel">
618
+ <div class="files-panel-header">
619
+ <h3>πŸ“ Generated Files</h3>
620
+ <button class="btn-close" onclick="closeFilesModal()">βœ•</button>
621
+ </div>
622
+ <div class="files-list" id="files-list">
623
+ <div class="empty-files">No files yet.<br>Ask the AI to write some code!</div>
624
+ </div>
625
+ </div>
626
+ </div>
627
+
628
+ <div id="toast"></div>
629
+
630
+ <script>
631
+ // ─── CRITICAL FIX: All API calls use credentials:'include' ────────────────────
632
+ // This ensures session cookies are always sent, fixing the "Unauthorized" error
633
+ // that occurs on HuggingFace Spaces behind a reverse proxy.
634
+ function apiFetch(url, opts = {}) {
635
+ return fetch(url, { ...opts, credentials: 'include' });
636
+ }
637
+
638
+ // ─── State ────────────────────────────────────────────────────────────────────
639
+ let currentChatId = null;
640
+ let username = '';
641
+ let isLoading = false;
642
+
643
+ const MODEL_NAMES = {
644
+ 'Qwen/Qwen2.5-72B-Instruct': 'Qwen 2.5 72B',
645
+ 'meta-llama/Llama-3.3-70B-Instruct': 'Llama 3.3 70B',
646
+ 'mistralai/Mistral-7B-Instruct-v0.3': 'Mistral 7B',
647
+ 'deepseek-ai/DeepSeek-R1-Distill-Qwen-32B': 'DeepSeek-R1 32B',
648
+ };
649
+
650
+ // ─── Init ─────────────────────────────────────────────────────────────────────
651
+ window.addEventListener('load', init);
652
+
653
+ async function init() {
654
+ try {
655
+ const res = await apiFetch('/api/me');
656
+ if (res.ok) {
657
+ const data = await res.json();
658
+ if (data.authenticated) {
659
+ username = data.username;
660
+ showApp();
661
+ }
662
+ }
663
+ } catch(e) { /* not authenticated */ }
664
+ setupInputHandlers();
665
+ }
666
+
667
+ function showApp() {
668
+ document.getElementById('auth-overlay').style.display = 'none';
669
+ document.getElementById('app').classList.remove('hidden');
670
+ document.getElementById('user-name-display').textContent = username;
671
+ document.getElementById('user-avatar').textContent = username.charAt(0).toUpperCase();
672
+ document.getElementById('user-plan-display').textContent = 'HuggingFace Models Β· Free';
673
+ loadChats();
674
+ if (window.innerWidth <= 768) {
675
+ document.getElementById('sidebar-toggle').style.display = 'flex';
676
+ }
677
+ }
678
+
679
+ // ─── Auth ─────────────────────────────────────────────────────────────────────
680
+ function switchTab(tab) {
681
+ document.querySelectorAll('.auth-tab').forEach((t, i) =>
682
+ t.classList.toggle('active', tab === 'login' ? i === 0 : i === 1));
683
+ document.getElementById('login-form').classList.toggle('hidden', tab !== 'login');
684
+ document.getElementById('register-form').classList.toggle('hidden', tab !== 'register');
685
+ document.getElementById('auth-error').classList.remove('show');
686
+ }
687
+
688
+ function showAuthError(msg) {
689
+ const el = document.getElementById('auth-error');
690
+ el.textContent = msg; el.classList.add('show');
691
+ }
692
+
693
+ async function doLogin() {
694
+ const id = document.getElementById('login-id').value.trim();
695
+ const pass = document.getElementById('login-pass').value;
696
+ if (!id || !pass) return showAuthError('Please fill all fields');
697
+ const res = await apiFetch('/api/login', {
698
+ method: 'POST',
699
+ headers: {'Content-Type': 'application/json'},
700
+ body: JSON.stringify({identifier: id, password: pass})
701
+ });
702
+ const data = await res.json();
703
+ if (res.ok) { username = data.username; showApp(); }
704
+ else showAuthError(data.error || 'Login failed');
705
+ }
706
+
707
+ async function doRegister() {
708
+ const user = document.getElementById('reg-user').value.trim();
709
+ const email = document.getElementById('reg-email').value.trim();
710
+ const pass = document.getElementById('reg-pass').value;
711
+ if (!user || !email || !pass) return showAuthError('Please fill all fields');
712
+ const res = await apiFetch('/api/register', {
713
+ method: 'POST',
714
+ headers: {'Content-Type': 'application/json'},
715
+ body: JSON.stringify({username: user, email: email, password: pass})
716
+ });
717
+ const data = await res.json();
718
+ if (res.ok) { username = data.username; showApp(); }
719
+ else showAuthError(data.error || 'Registration failed');
720
+ }
721
+
722
+ async function doLogout(e) {
723
+ e.stopPropagation();
724
+ await apiFetch('/api/logout', {method: 'POST'});
725
+ location.reload();
726
+ }
727
+
728
+ // Enter key for auth forms
729
+ document.addEventListener('keydown', e => {
730
+ if (e.key === 'Enter') {
731
+ const overlay = document.getElementById('auth-overlay');
732
+ if (overlay.style.display !== 'none') {
733
+ const loginVisible = !document.getElementById('login-form').classList.contains('hidden');
734
+ if (loginVisible) doLogin(); else doRegister();
735
+ }
736
+ }
737
+ });
738
+
739
+ // ─── Model selector ───────────────────────────────────────────────────────────
740
+ function onModelChange() {
741
+ const sel = document.getElementById('model-select');
742
+ const modelId = sel.value;
743
+ document.getElementById('topbar-model-name').textContent = MODEL_NAMES[modelId] || modelId;
744
+ }
745
+
746
+ // ─── Chats ────────────────────────────────────────────────────────────────────
747
+ async function loadChats() {
748
+ const res = await apiFetch('/api/chats');
749
+ if (!res.ok) return;
750
+ const chats = await res.json();
751
+ const list = document.getElementById('chat-list');
752
+ list.innerHTML = '';
753
+ chats.forEach(c => {
754
+ const el = document.createElement('div');
755
+ el.className = 'chat-item' + (c.id === currentChatId ? ' active' : '');
756
+ el.dataset.id = c.id;
757
+ const shortModel = MODEL_NAMES[c.model_used] || c.model_used || '';
758
+ el.innerHTML = `
759
+ <span class="chat-item-icon">πŸ’¬</span>
760
+ <div class="chat-item-info">
761
+ <div class="chat-item-title">${escHtml(c.title)}</div>
762
+ ${shortModel ? `<div class="chat-item-model">${escHtml(shortModel)}</div>` : ''}
763
+ </div>
764
+ <button class="chat-item-del" onclick="deleteChat(event,'${c.id}')" title="Delete">βœ•</button>`;
765
+ el.addEventListener('click', () => openChat(c.id));
766
+ list.appendChild(el);
767
+ });
768
+ }
769
+
770
+ async function newChat() {
771
+ currentChatId = null;
772
+ document.getElementById('messages').innerHTML = `
773
+ <div id="welcome">
774
+ <div class="welcome-icon">πŸ€—</div>
775
+ <div class="welcome-title">New Conversation</div>
776
+ <div class="welcome-sub">Choose a model below and start chatting!</div>
777
+ <div class="welcome-chips">
778
+ <div class="chip" onclick="useChip(this)">πŸ§‘β€πŸ’» Write Python code</div>
779
+ <div class="chip" onclick="useChip(this)">πŸ” Debug my code</div>
780
+ <div class="chip" onclick="useChip(this)">πŸ—οΈ Design system architecture</div>
781
+ <div class="chip" onclick="useChip(this)">πŸ“– Explain a concept</div>
782
+ </div>
783
+ </div>`;
784
+ document.getElementById('chat-title').textContent = 'New Chat';
785
+ document.getElementById('topbar-model-name').textContent =
786
+ MODEL_NAMES[document.getElementById('model-select').value] || 'Qwen 2.5 72B';
787
+ loadChats();
788
+ closeSidebar();
789
+ }
790
+
791
+ async function openChat(chatId) {
792
+ currentChatId = chatId;
793
+ const res = await apiFetch(`/api/chats/${chatId}`);
794
+ if (!res.ok) return;
795
+ const chat = await res.json();
796
+ document.getElementById('chat-title').textContent = chat.title;
797
+ if (chat.model_used) {
798
+ document.getElementById('model-select').value = chat.model_used;
799
+ document.getElementById('topbar-model-name').textContent =
800
+ MODEL_NAMES[chat.model_used] || chat.model_used;
801
+ }
802
+ const msgs = document.getElementById('messages');
803
+ msgs.innerHTML = '';
804
+ chat.messages.forEach(m => appendMessage(m.role, m.content, false));
805
+ msgs.scrollTop = msgs.scrollHeight;
806
+ loadChats();
807
+ closeSidebar();
808
+ }
809
+
810
+ async function deleteChat(e, chatId) {
811
+ e.stopPropagation();
812
+ await apiFetch(`/api/chats/${chatId}`, {method: 'DELETE'});
813
+ if (currentChatId === chatId) {
814
+ currentChatId = null;
815
+ document.getElementById('messages').innerHTML =
816
+ '<div style="color:var(--text-muted);text-align:center;padding:60px">Select or start a chat</div>';
817
+ document.getElementById('chat-title').textContent = 'New Chat';
818
+ }
819
+ loadChats();
820
+ }
821
+
822
+ // ─── Send message ─────────────────────────────────────────────────────────────
823
+ function setupInputHandlers() {
824
+ const inp = document.getElementById('msg-input');
825
+ const btn = document.getElementById('send-btn');
826
+ inp.addEventListener('input', () => {
827
+ btn.disabled = !inp.value.trim() || isLoading;
828
+ inp.style.height = 'auto';
829
+ inp.style.height = Math.min(inp.scrollHeight, 200) + 'px';
830
+ });
831
+ inp.addEventListener('keydown', e => {
832
+ if (e.key === 'Enter' && !e.shiftKey) {
833
+ e.preventDefault(); sendMessage();
834
+ }
835
+ });
836
+ }
837
+
838
+ async function sendMessage() {
839
+ const inp = document.getElementById('msg-input');
840
+ const content = inp.value.trim();
841
+ if (!content || isLoading) return;
842
+
843
+ const modelId = document.getElementById('model-select').value;
844
+
845
+ // Create chat if needed
846
+ if (!currentChatId) {
847
+ const res = await apiFetch('/api/chats', {
848
+ method: 'POST',
849
+ headers: {'Content-Type': 'application/json'},
850
+ body: JSON.stringify({model: modelId})
851
+ });
852
+ const chat = await res.json();
853
+ currentChatId = chat.id;
854
+ }
855
+
856
+ // Remove welcome
857
+ const welcome = document.getElementById('welcome');
858
+ if (welcome) welcome.remove();
859
+
860
+ inp.value = ''; inp.style.height = 'auto';
861
+ document.getElementById('send-btn').disabled = true;
862
+ isLoading = true;
863
+
864
+ appendMessage('user', content, true);
865
+ const typingId = showTyping();
866
+
867
+ try {
868
+ const res = await apiFetch(`/api/chats/${currentChatId}/messages`, {
869
+ method: 'POST',
870
+ headers: {'Content-Type': 'application/json'},
871
+ body: JSON.stringify({content, model: modelId})
872
+ });
873
+
874
+ removeTyping(typingId);
875
+ const data = await res.json();
876
+
877
+ if (res.ok) {
878
+ appendMessage('assistant', data.content, true);
879
+ // Update topbar model name if returned
880
+ if (data.model_name) {
881
+ document.getElementById('topbar-model-name').textContent = data.model_name;
882
+ }
883
+ loadChats();
884
+ const chatRes = await apiFetch(`/api/chats/${currentChatId}`);
885
+ if (chatRes.ok) {
886
+ const chat = await chatRes.json();
887
+ document.getElementById('chat-title').textContent = chat.title;
888
+ }
889
+ } else {
890
+ appendMessage('assistant', `❌ Error: ${data.error}`, true);
891
+ }
892
+ } catch(err) {
893
+ removeTyping(typingId);
894
+ appendMessage('assistant', `❌ Network error: ${err.message}`, true);
895
+ }
896
+
897
+ isLoading = false;
898
+ document.getElementById('send-btn').disabled = false;
899
+ }
900
+
901
+ function appendMessage(role, content, animate) {
902
+ const msgs = document.getElementById('messages');
903
+ const row = document.createElement('div');
904
+ row.className = 'message-row ' + role;
905
+ const initials = role === 'user' ? (username.charAt(0).toUpperCase() || 'U') : 'πŸ€—';
906
+ const name = role === 'user' ? username : 'HF Chat';
907
+
908
+ let rendered;
909
+ try { rendered = marked.parse(content); }
910
+ catch(e) { rendered = `<p>${escHtml(content)}</p>`; }
911
+
912
+ row.innerHTML = `
913
+ <div class="msg-avatar ${role}">${initials}</div>
914
+ <div class="msg-body">
915
+ <div class="msg-name">${name}</div>
916
+ <div class="msg-content">${rendered}</div>
917
+ </div>`;
918
+
919
+ if (!animate) row.style.animation = 'none';
920
+ msgs.appendChild(row);
921
+
922
+ // Copy buttons for code
923
+ row.querySelectorAll('pre').forEach(pre => {
924
+ const btn = document.createElement('button');
925
+ btn.className = 'copy-btn';
926
+ btn.textContent = 'Copy';
927
+ btn.onclick = () => {
928
+ navigator.clipboard.writeText(pre.querySelector('code')?.textContent || pre.textContent);
929
+ btn.textContent = 'Copied!';
930
+ setTimeout(() => btn.textContent = 'Copy', 2000);
931
+ };
932
+ pre.appendChild(btn);
933
+ });
934
+
935
+ // Syntax highlighting
936
+ row.querySelectorAll('pre code').forEach(el => hljs.highlightElement(el));
937
+ msgs.scrollTop = msgs.scrollHeight;
938
+ }
939
+
940
+ function showTyping() {
941
+ const id = 'typing-' + Date.now();
942
+ const msgs = document.getElementById('messages');
943
+ const row = document.createElement('div');
944
+ row.className = 'typing-row'; row.id = id;
945
+ row.innerHTML = `
946
+ <div class="msg-avatar assistant">πŸ€—</div>
947
+ <div class="typing-dots">
948
+ <div class="typing-dot"></div>
949
+ <div class="typing-dot"></div>
950
+ <div class="typing-dot"></div>
951
+ </div>`;
952
+ msgs.appendChild(row);
953
+ msgs.scrollTop = msgs.scrollHeight;
954
+ return id;
955
+ }
956
+
957
+ function removeTyping(id) {
958
+ const el = document.getElementById(id);
959
+ if (el) el.remove();
960
+ }
961
+
962
+ function useChip(el) {
963
+ const inp = document.getElementById('msg-input');
964
+ inp.value = el.textContent.replace(/^[^\s]+\s/, '');
965
+ document.getElementById('send-btn').disabled = false;
966
+ inp.focus();
967
+ sendMessage();
968
+ }
969
+
970
+ // ─── Files modal ──────────────────────────────────────────────────────────────
971
+ async function openFilesModal() {
972
+ document.getElementById('files-modal').classList.remove('hidden');
973
+ const res = await apiFetch('/api/files');
974
+ const files = res.ok ? await res.json() : [];
975
+ const list = document.getElementById('files-list');
976
+ if (!files.length) {
977
+ list.innerHTML = '<div class="empty-files">No files generated yet.<br>Ask the AI to write some code!</div>';
978
+ return;
979
+ }
980
+ const extIcons = {py:'🐍',js:'🟨',ts:'πŸ”·',html:'🌐',css:'🎨',sh:'⚑',sql:'πŸ—„οΈ',json:'πŸ“‹',go:'πŸ”΅',rs:'πŸ¦€',java:'β˜•'};
981
+ list.innerHTML = files.map(f => {
982
+ const ext = f.filename.split('.').pop().toLowerCase();
983
+ const icon = extIcons[ext] || 'πŸ“„';
984
+ const date = new Date(f.created_at).toLocaleDateString();
985
+ return `<div class="file-item">
986
+ <div class="file-icon">${icon}</div>
987
+ <div class="file-info">
988
+ <div class="file-name">${escHtml(f.filename)}</div>
989
+ <div class="file-meta">${f.file_type} Β· ${date}</div>
990
+ </div>
991
+ <button class="btn-dl" onclick="downloadFile(${f.id},'${escHtml(f.filename)}')">⬇ Download</button>
992
+ </div>`;
993
+ }).join('');
994
+ }
995
+
996
+ function closeFilesModal() {
997
+ document.getElementById('files-modal').classList.add('hidden');
998
+ }
999
+
1000
+ async function downloadFile(id, filename) {
1001
+ const a = document.createElement('a');
1002
+ a.href = `/api/files/${id}/download`;
1003
+ a.download = filename;
1004
+ document.body.appendChild(a); a.click(); document.body.removeChild(a);
1005
+ showToast('Downloading ' + filename);
1006
+ }
1007
+
1008
+ // ─── Sidebar ──────────────────────────────────────────────────────────────────
1009
+ function toggleSidebar() {
1010
+ document.getElementById('sidebar').classList.toggle('open');
1011
+ document.getElementById('sidebar-overlay').classList.toggle('show');
1012
+ }
1013
+
1014
+ function closeSidebar() {
1015
+ if (window.innerWidth > 768) return;
1016
+ document.getElementById('sidebar').classList.remove('open');
1017
+ document.getElementById('sidebar-overlay').classList.remove('show');
1018
+ }
1019
+
1020
+ // ─── Utils ────────────────────────────────────────────────────────────────────
1021
+ function escHtml(s) {
1022
+ return String(s)
1023
+ .replace(/&/g,'&amp;').replace(/</g,'&lt;')
1024
+ .replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1025
+ }
1026
+
1027
+ let toastTimer;
1028
+ function showToast(msg) {
1029
+ const t = document.getElementById('toast');
1030
+ t.textContent = msg; t.classList.add('show');
1031
+ clearTimeout(toastTimer);
1032
+ toastTimer = setTimeout(() => t.classList.remove('show'), 2500);
1033
+ }
1034
+
1035
+ window.addEventListener('resize', () => {
1036
+ const toggle = document.getElementById('sidebar-toggle');
1037
+ if (window.innerWidth > 768) {
1038
+ toggle.style.display = 'none';
1039
+ document.getElementById('sidebar').classList.remove('open');
1040
+ document.getElementById('sidebar-overlay').classList.remove('show');
1041
+ } else {
1042
+ toggle.style.display = 'flex';
1043
+ }
1044
+ });
1045
+
1046
+ document.getElementById('files-modal').addEventListener('click', function(e) {
1047
+ if (e.target === this) closeFilesModal();
1048
+ });
1049
+ </script>
1050
+ </body>
1051
+ </html>