Xlnk commited on
Commit
e767f32
·
verified ·
1 Parent(s): 0671f22

Upload 13 files

Browse files
app.py CHANGED
@@ -1,7 +1,6 @@
1
- import os, zipfile, sqlite3, uuid, shutil, hashlib, secrets
2
  from datetime import datetime
3
- from functools import wraps
4
- from flask import Flask, request, render_template, redirect, url_for, abort, Response, send_from_directory, session, flash
5
 
6
  APP_NAME = "h by Xlnk"
7
  UPLOAD_DIR = "uploads"
@@ -13,7 +12,7 @@ BRAND_COMMENT = '<!-- h by Xlnk -->'
13
  os.makedirs(UPLOAD_DIR, exist_ok=True)
14
 
15
  app = Flask(__name__)
16
- app.secret_key = secrets.token_hex(32) # Generate a secure secret key
17
 
18
  # ---------- DATABASE ----------
19
  def db():
@@ -21,63 +20,67 @@ def db():
21
 
22
  def init_db():
23
  with db() as c:
24
- # Create users table
25
- c.execute("""
26
- CREATE TABLE IF NOT EXISTS users (
27
- id TEXT PRIMARY KEY,
28
- email TEXT UNIQUE NOT NULL,
29
- password_hash TEXT NOT NULL,
30
- created TEXT
31
- )
32
- """)
33
- # Create sites table with user_id foreign key
34
  c.execute("""
35
  CREATE TABLE IF NOT EXISTS sites (
36
  id TEXT PRIMARY KEY,
37
- user_id TEXT NOT NULL,
38
  created TEXT,
39
  views INTEGER DEFAULT 0,
40
  expires TEXT,
41
- FOREIGN KEY (user_id) REFERENCES users(id)
42
  )
43
  """)
 
 
 
 
 
44
 
45
  # Initialize database on module load (ensures it works with gunicorn)
46
  init_db()
47
 
48
- # ---------- AUTH HELPERS ----------
49
- def hash_password(password):
50
- """Hash password with SHA-256"""
51
- return hashlib.sha256(password.encode()).hexdigest()
52
-
53
- def login_required(f):
54
- """Decorator to require login for routes"""
55
- @wraps(f)
56
- def decorated_function(*args, **kwargs):
57
- if 'user_id' not in session:
58
- flash('Please login to access this page', 'error')
59
- return redirect(url_for('login'))
60
- return f(*args, **kwargs)
61
- return decorated_function
62
-
63
- def get_current_user():
64
- """Get current logged in user"""
65
- if 'user_id' in session:
66
- with db() as c:
67
- user = c.execute("SELECT * FROM users WHERE id=?", (session['user_id'],)).fetchone()
68
- return user
69
- return None
70
-
71
- def owns_site(site_id):
72
- """Check if current user owns the site"""
73
- if 'user_id' not in session:
 
 
 
 
 
74
  return False
75
  with db() as c:
76
- site = c.execute("SELECT user_id FROM sites WHERE id=?", (site_id,)).fetchone()
77
- if site and site[0] == session['user_id']:
78
  return True
79
  return False
80
 
 
 
 
 
 
81
  # ---------- HTML INJECTION ----------
82
  def inject_html(html):
83
  if "supreme-engine/a.js" in html:
@@ -101,91 +104,35 @@ def cleanup():
101
  shutil.rmtree(os.path.join(UPLOAD_DIR, site_id), ignore_errors=True)
102
  c.execute("DELETE FROM sites WHERE id=?", (site_id,))
103
 
104
- # ---------- AUTH ROUTES ----------
105
- @app.route("/login", methods=["GET", "POST"])
106
- def login():
107
- if 'user_id' in session:
108
- return redirect(url_for('dashboard'))
 
 
 
 
 
109
 
110
- if request.method == "POST":
111
- email = request.form.get("email", "").strip().lower()
112
- password = request.form.get("password", "")
113
-
114
- if not email or not password:
115
- flash("Please fill in all fields", "error")
116
- return render_template("login.html")
117
-
118
- password_hash = hash_password(password)
119
-
120
- with db() as c:
121
- user = c.execute("SELECT id, email FROM users WHERE email=? AND password_hash=?",
122
- (email, password_hash)).fetchone()
123
-
124
- if user:
125
- session['user_id'] = user[0]
126
- session['user_email'] = user[1]
127
- flash("Welcome back!", "success")
128
- return redirect(url_for('dashboard'))
129
- else:
130
- flash("Invalid email or password", "error")
131
 
132
- return render_template("login.html")
133
-
134
- @app.route("/register", methods=["GET", "POST"])
135
- def register():
136
- if 'user_id' in session:
137
- return redirect(url_for('dashboard'))
 
138
 
139
- if request.method == "POST":
140
- email = request.form.get("email", "").strip().lower()
141
- password = request.form.get("password", "")
142
- confirm_password = request.form.get("confirm_password", "")
143
-
144
- if not email or not password or not confirm_password:
145
- flash("Please fill in all fields", "error")
146
- return render_template("register.html")
147
-
148
- if password != confirm_password:
149
- flash("Passwords do not match", "error")
150
- return render_template("register.html")
151
-
152
- if len(password) < 6:
153
- flash("Password must be at least 6 characters", "error")
154
- return render_template("register.html")
155
-
156
- # Check if email already exists
157
- with db() as c:
158
- existing = c.execute("SELECT id FROM users WHERE email=?", (email,)).fetchone()
159
- if existing:
160
- flash("Email already registered", "error")
161
- return render_template("register.html")
162
-
163
- # Create new user
164
- user_id = uuid.uuid4().hex
165
- password_hash = hash_password(password)
166
- c.execute("INSERT INTO users VALUES (?, ?, ?, ?)",
167
- (user_id, email, password_hash, datetime.utcnow().isoformat()))
168
-
169
- flash("Registration successful! Please login.", "success")
170
- return redirect(url_for('login'))
171
 
172
- return render_template("register.html")
173
-
174
- @app.route("/logout")
175
- def logout():
176
- session.clear()
177
- flash("You have been logged out", "success")
178
- return redirect(url_for('login'))
179
-
180
- # ---------- ROUTES ----------
181
- @app.route("/")
182
- def home():
183
- if 'user_id' in session:
184
- return redirect(url_for('dashboard'))
185
- return redirect(url_for('login'))
186
 
187
  @app.route("/upload", methods=["GET", "POST"])
188
- @login_required
189
  def upload():
190
  cleanup()
191
 
@@ -193,6 +140,9 @@ def upload():
193
  file = request.files["file"]
194
  site_id = request.form.get("site_id") or uuid.uuid4().hex[:6]
195
  expires = request.form.get("expires") or None
 
 
 
196
 
197
  site_path = os.path.join(UPLOAD_DIR, site_id)
198
  if os.path.exists(site_path):
@@ -212,36 +162,50 @@ def upload():
212
 
213
  with db() as c:
214
  c.execute(
215
- "INSERT INTO sites VALUES (?, ?, ?, 0, ?)",
216
- (site_id, session['user_id'], datetime.utcnow().isoformat(), expires)
217
  )
218
 
219
  flash("Site uploaded successfully!", "success")
220
- return redirect(url_for("dashboard"))
 
 
 
 
221
 
222
  return render_template("upload.html")
223
 
224
- @app.route("/dashboard")
225
- @login_required
226
- def dashboard():
 
 
 
 
227
  with db() as c:
228
- # Only get sites belonging to the current user
229
- sites = c.execute("SELECT * FROM sites WHERE user_id=? ORDER BY created DESC",
230
- (session['user_id'],)).fetchall()
231
- return render_template("dashboard.html", sites=sites, user_email=session.get('user_email'))
 
 
232
 
233
  @app.route("/delete/<site_id>")
234
- @login_required
235
  def delete(site_id):
236
- if not owns_site(site_id):
237
- flash("Access denied", "error")
238
- return redirect(url_for('dashboard'))
 
239
 
240
  shutil.rmtree(os.path.join(UPLOAD_DIR, site_id), ignore_errors=True)
241
  with db() as c:
242
  c.execute("DELETE FROM sites WHERE id=?", (site_id,))
 
 
 
 
243
  flash("Site deleted", "success")
244
- return redirect(url_for("dashboard"))
245
 
246
  # ---------- FILE MANAGEMENT ----------
247
  def get_all_files(directory):
@@ -255,11 +219,11 @@ def get_all_files(directory):
255
  return sorted(files)
256
 
257
  @app.route("/files/<site_id>")
258
- @login_required
259
  def list_files(site_id):
260
- if not owns_site(site_id):
261
- flash("Access denied", "error")
262
- return redirect(url_for('dashboard'))
 
263
 
264
  site_path = os.path.join(UPLOAD_DIR, site_id)
265
  if not os.path.exists(site_path):
@@ -268,11 +232,11 @@ def list_files(site_id):
268
  return render_template("files.html", site_id=site_id, files=files)
269
 
270
  @app.route("/edit/<site_id>/<path:file>")
271
- @login_required
272
  def edit_file(site_id, file):
273
- if not owns_site(site_id):
274
- flash("Access denied", "error")
275
- return redirect(url_for('dashboard'))
 
276
 
277
  path = os.path.join(UPLOAD_DIR, site_id, file)
278
  if not os.path.exists(path):
@@ -285,11 +249,11 @@ def edit_file(site_id, file):
285
  return render_template("edit.html", site_id=site_id, file=file, content=content)
286
 
287
  @app.route("/save/<site_id>/<path:file>", methods=["POST"])
288
- @login_required
289
  def save_file(site_id, file):
290
- if not owns_site(site_id):
291
- flash("Access denied", "error")
292
- return redirect(url_for('dashboard'))
 
293
 
294
  path = os.path.join(UPLOAD_DIR, site_id, file)
295
  if not os.path.exists(path):
@@ -301,11 +265,11 @@ def save_file(site_id, file):
301
  return redirect(url_for("list_files", site_id=site_id))
302
 
303
  @app.route("/delete-file/<site_id>/<path:file>")
304
- @login_required
305
  def delete_file(site_id, file):
306
- if not owns_site(site_id):
307
- flash("Access denied", "error")
308
- return redirect(url_for('dashboard'))
 
309
 
310
  site_path = os.path.join(UPLOAD_DIR, site_id)
311
  path = os.path.join(site_path, file)
@@ -320,12 +284,114 @@ def delete_file(site_id, file):
320
  flash("File deleted", "success")
321
  return redirect(url_for("list_files", site_id=site_id))
322
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  @app.route("/upload/<site_id>", methods=["POST"])
324
- @login_required
325
  def upload_to_site(site_id):
326
- if not owns_site(site_id):
327
- flash("Access denied", "error")
328
- return redirect(url_for('dashboard'))
 
329
 
330
  site_path = os.path.join(UPLOAD_DIR, site_id)
331
  if not os.path.exists(site_path):
 
1
+ import os, zipfile, sqlite3, uuid, shutil, secrets, json
2
  from datetime import datetime
3
+ from flask import Flask, request, render_template, redirect, url_for, abort, Response, send_from_directory, flash, make_response
 
4
 
5
  APP_NAME = "h by Xlnk"
6
  UPLOAD_DIR = "uploads"
 
12
  os.makedirs(UPLOAD_DIR, exist_ok=True)
13
 
14
  app = Flask(__name__)
15
+ app.secret_key = secrets.token_hex(32)
16
 
17
  # ---------- DATABASE ----------
18
  def db():
 
20
 
21
  def init_db():
22
  with db() as c:
 
 
 
 
 
 
 
 
 
 
23
  c.execute("""
24
  CREATE TABLE IF NOT EXISTS sites (
25
  id TEXT PRIMARY KEY,
 
26
  created TEXT,
27
  views INTEGER DEFAULT 0,
28
  expires TEXT,
29
+ edit_token TEXT
30
  )
31
  """)
32
+ # Add edit_token column if it doesn't exist (migration)
33
+ try:
34
+ c.execute("ALTER TABLE sites ADD COLUMN edit_token TEXT")
35
+ except sqlite3.OperationalError:
36
+ pass # Column already exists
37
 
38
  # Initialize database on module load (ensures it works with gunicorn)
39
  init_db()
40
 
41
+ # ---------- TOKEN MANAGEMENT (COOKIES) ----------
42
+ def get_user_tokens():
43
+ """Get all tokens stored in user's cookies"""
44
+ tokens_json = request.cookies.get('site_tokens', '{}')
45
+ try:
46
+ return json.loads(tokens_json)
47
+ except:
48
+ return {}
49
+
50
+ def save_token_to_response(response, site_id, token):
51
+ """Add a token to the user's cookie"""
52
+ tokens = get_user_tokens()
53
+ tokens[site_id] = token
54
+ response.set_cookie('site_tokens', json.dumps(tokens), max_age=365*24*60*60, httponly=True, samesite='Lax')
55
+ return response
56
+
57
+ def remove_token_from_response(response, site_id):
58
+ """Remove a token from the user's cookie"""
59
+ tokens = get_user_tokens()
60
+ tokens.pop(site_id, None)
61
+ response.set_cookie('site_tokens', json.dumps(tokens), max_age=365*24*60*60, httponly=True, samesite='Lax')
62
+ return response
63
+
64
+ def get_token_for_site(site_id):
65
+ """Get token for a specific site from cookies"""
66
+ tokens = get_user_tokens()
67
+ return tokens.get(site_id)
68
+
69
+ def verify_token(site_id, token):
70
+ """Check if the provided token matches the site's edit token"""
71
+ if not token:
72
  return False
73
  with db() as c:
74
+ row = c.execute("SELECT edit_token FROM sites WHERE id=?", (site_id,)).fetchone()
75
+ if row and row[0] == token:
76
  return True
77
  return False
78
 
79
+ def user_owns_site(site_id):
80
+ """Check if the current user owns the site (has valid token in cookies)"""
81
+ token = get_token_for_site(site_id)
82
+ return verify_token(site_id, token)
83
+
84
  # ---------- HTML INJECTION ----------
85
  def inject_html(html):
86
  if "supreme-engine/a.js" in html:
 
104
  shutil.rmtree(os.path.join(UPLOAD_DIR, site_id), ignore_errors=True)
105
  c.execute("DELETE FROM sites WHERE id=?", (site_id,))
106
 
107
+ # ---------- ROUTES ----------
108
+ @app.route("/")
109
+ def home():
110
+ return redirect(url_for('my_sites'))
111
+
112
+ @app.route("/my-sites")
113
+ def my_sites():
114
+ """Show only the sites the user owns (has tokens for)"""
115
+ cleanup()
116
+ user_tokens = get_user_tokens()
117
 
118
+ if not user_tokens:
119
+ # No sites yet
120
+ return render_template("my_sites.html", sites=[])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
+ # Get only sites that the user has valid tokens for
123
+ my_sites_list = []
124
+ with db() as c:
125
+ for site_id, token in user_tokens.items():
126
+ row = c.execute("SELECT * FROM sites WHERE id=? AND edit_token=?", (site_id, token)).fetchone()
127
+ if row:
128
+ my_sites_list.append(row)
129
 
130
+ # Sort by created date (newest first)
131
+ my_sites_list.sort(key=lambda x: x[1] if x[1] else '', reverse=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
+ return render_template("my_sites.html", sites=my_sites_list)
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
  @app.route("/upload", methods=["GET", "POST"])
 
136
  def upload():
137
  cleanup()
138
 
 
140
  file = request.files["file"]
141
  site_id = request.form.get("site_id") or uuid.uuid4().hex[:6]
142
  expires = request.form.get("expires") or None
143
+
144
+ # Generate a unique edit token for this site
145
+ edit_token = secrets.token_urlsafe(32)
146
 
147
  site_path = os.path.join(UPLOAD_DIR, site_id)
148
  if os.path.exists(site_path):
 
162
 
163
  with db() as c:
164
  c.execute(
165
+ "INSERT INTO sites VALUES (?, ?, 0, ?, ?)",
166
+ (site_id, datetime.utcnow().isoformat(), expires, edit_token)
167
  )
168
 
169
  flash("Site uploaded successfully!", "success")
170
+
171
+ # Create response and save token to cookie
172
+ response = make_response(redirect(url_for("manage_site", site_id=site_id)))
173
+ save_token_to_response(response, site_id, edit_token)
174
+ return response
175
 
176
  return render_template("upload.html")
177
 
178
+ @app.route("/manage/<site_id>")
179
+ def manage_site(site_id):
180
+ """Manage a specific site - auto-checks token from cookies"""
181
+ if not user_owns_site(site_id):
182
+ flash("You don't have access to this site.", "error")
183
+ return redirect(url_for('my_sites'))
184
+
185
  with db() as c:
186
+ site = c.execute("SELECT * FROM sites WHERE id=?", (site_id,)).fetchone()
187
+
188
+ if not site:
189
+ abort(404)
190
+
191
+ return render_template("manage.html", site=site, site_id=site_id)
192
 
193
  @app.route("/delete/<site_id>")
 
194
  def delete(site_id):
195
+ """Delete a site - auto-checks token from cookies"""
196
+ if not user_owns_site(site_id):
197
+ flash("You don't have access to this site.", "error")
198
+ return redirect(url_for('my_sites'))
199
 
200
  shutil.rmtree(os.path.join(UPLOAD_DIR, site_id), ignore_errors=True)
201
  with db() as c:
202
  c.execute("DELETE FROM sites WHERE id=?", (site_id,))
203
+
204
+ # Remove token from cookies
205
+ response = make_response(redirect(url_for("my_sites")))
206
+ remove_token_from_response(response, site_id)
207
  flash("Site deleted", "success")
208
+ return response
209
 
210
  # ---------- FILE MANAGEMENT ----------
211
  def get_all_files(directory):
 
219
  return sorted(files)
220
 
221
  @app.route("/files/<site_id>")
 
222
  def list_files(site_id):
223
+ """List files in a site - auto-checks token from cookies"""
224
+ if not user_owns_site(site_id):
225
+ flash("You don't have access to this site.", "error")
226
+ return redirect(url_for('my_sites'))
227
 
228
  site_path = os.path.join(UPLOAD_DIR, site_id)
229
  if not os.path.exists(site_path):
 
232
  return render_template("files.html", site_id=site_id, files=files)
233
 
234
  @app.route("/edit/<site_id>/<path:file>")
 
235
  def edit_file(site_id, file):
236
+ """Edit a file - auto-checks token from cookies"""
237
+ if not user_owns_site(site_id):
238
+ flash("You don't have access to this site.", "error")
239
+ return redirect(url_for('my_sites'))
240
 
241
  path = os.path.join(UPLOAD_DIR, site_id, file)
242
  if not os.path.exists(path):
 
249
  return render_template("edit.html", site_id=site_id, file=file, content=content)
250
 
251
  @app.route("/save/<site_id>/<path:file>", methods=["POST"])
 
252
  def save_file(site_id, file):
253
+ """Save a file - auto-checks token from cookies"""
254
+ if not user_owns_site(site_id):
255
+ flash("You don't have access to this site.", "error")
256
+ return redirect(url_for('my_sites'))
257
 
258
  path = os.path.join(UPLOAD_DIR, site_id, file)
259
  if not os.path.exists(path):
 
265
  return redirect(url_for("list_files", site_id=site_id))
266
 
267
  @app.route("/delete-file/<site_id>/<path:file>")
 
268
  def delete_file(site_id, file):
269
+ """Delete a file - auto-checks token from cookies"""
270
+ if not user_owns_site(site_id):
271
+ flash("You don't have access to this site.", "error")
272
+ return redirect(url_for('my_sites'))
273
 
274
  site_path = os.path.join(UPLOAD_DIR, site_id)
275
  path = os.path.join(site_path, file)
 
284
  flash("File deleted", "success")
285
  return redirect(url_for("list_files", site_id=site_id))
286
 
287
+ @app.route("/create-file/<site_id>", methods=["GET", "POST"])
288
+ def create_file(site_id):
289
+ """Create a new file - auto-checks token from cookies"""
290
+ if not user_owns_site(site_id):
291
+ flash("You don't have access to this site.", "error")
292
+ return redirect(url_for('my_sites'))
293
+
294
+ site_path = os.path.join(UPLOAD_DIR, site_id)
295
+ if not os.path.exists(site_path):
296
+ abort(404)
297
+
298
+ if request.method == "POST":
299
+ filename = request.form.get("filename", "").strip()
300
+ content = request.form.get("content", "")
301
+
302
+ # Validate filename
303
+ if not filename:
304
+ flash("Please enter a filename", "error")
305
+ return render_template("create_file.html", site_id=site_id)
306
+
307
+ # Security: prevent path traversal
308
+ if ".." in filename or filename.startswith("/") or filename.startswith("\\"):
309
+ flash("Invalid filename", "error")
310
+ return render_template("create_file.html", site_id=site_id)
311
+
312
+ # Normalize path
313
+ filename = filename.replace("\\", "/")
314
+ file_path = os.path.join(site_path, filename)
315
+
316
+ # Check if file already exists
317
+ if os.path.exists(file_path):
318
+ flash("A file with this name already exists", "error")
319
+ return render_template("create_file.html", site_id=site_id, filename=filename, content=content)
320
+
321
+ # Create parent directories if needed
322
+ parent_dir = os.path.dirname(file_path)
323
+ if parent_dir and not os.path.exists(parent_dir):
324
+ os.makedirs(parent_dir, exist_ok=True)
325
+
326
+ # Create the file
327
+ with open(file_path, "w", encoding="utf-8") as f:
328
+ f.write(content)
329
+
330
+ flash(f"File '{filename}' created", "success")
331
+ return redirect(url_for("list_files", site_id=site_id))
332
+
333
+ return render_template("create_file.html", site_id=site_id)
334
+
335
+ @app.route("/rename-file/<site_id>/<path:file>", methods=["GET", "POST"])
336
+ def rename_file(site_id, file):
337
+ """Rename a file - auto-checks token from cookies"""
338
+ if not user_owns_site(site_id):
339
+ flash("You don't have access to this site.", "error")
340
+ return redirect(url_for('my_sites'))
341
+
342
+ site_path = os.path.join(UPLOAD_DIR, site_id)
343
+ old_path = os.path.join(site_path, file)
344
+
345
+ if not os.path.exists(old_path):
346
+ abort(404)
347
+
348
+ if request.method == "POST":
349
+ new_name = request.form.get("new_name", "").strip()
350
+
351
+ # Validate new filename
352
+ if not new_name:
353
+ flash("Please enter a new filename", "error")
354
+ return render_template("rename_file.html", site_id=site_id, file=file)
355
+
356
+ # Security: prevent path traversal
357
+ if ".." in new_name or new_name.startswith("/") or new_name.startswith("\\"):
358
+ flash("Invalid filename", "error")
359
+ return render_template("rename_file.html", site_id=site_id, file=file)
360
+
361
+ # Normalize path
362
+ new_name = new_name.replace("\\", "/")
363
+ new_path = os.path.join(site_path, new_name)
364
+
365
+ # Check if target already exists
366
+ if os.path.exists(new_path):
367
+ flash("A file with this name already exists", "error")
368
+ return render_template("rename_file.html", site_id=site_id, file=file, new_name=new_name)
369
+
370
+ # Create parent directories if needed
371
+ parent_dir = os.path.dirname(new_path)
372
+ if parent_dir and not os.path.exists(parent_dir):
373
+ os.makedirs(parent_dir, exist_ok=True)
374
+
375
+ # Rename the file
376
+ shutil.move(old_path, new_path)
377
+
378
+ # Clean up empty parent directories from old location
379
+ old_parent = os.path.dirname(old_path)
380
+ while old_parent != site_path and os.path.isdir(old_parent) and not os.listdir(old_parent):
381
+ os.rmdir(old_parent)
382
+ old_parent = os.path.dirname(old_parent)
383
+
384
+ flash(f"File renamed to '{new_name}'", "success")
385
+ return redirect(url_for("list_files", site_id=site_id))
386
+
387
+ return render_template("rename_file.html", site_id=site_id, file=file)
388
+
389
  @app.route("/upload/<site_id>", methods=["POST"])
 
390
  def upload_to_site(site_id):
391
+ """Upload files to a site - auto-checks token from cookies"""
392
+ if not user_owns_site(site_id):
393
+ flash("You don't have access to this site.", "error")
394
+ return redirect(url_for('my_sites'))
395
 
396
  site_path = os.path.join(UPLOAD_DIR, site_id)
397
  if not os.path.exists(site_path):
templates/create_file.html ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+
4
+ <head>
5
+ <title>Create File - {{site_id}} | h by Xlnk</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <link
8
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Fira+Code:wght@400;500&display=swap"
9
+ rel="stylesheet">
10
+ <style>
11
+ * {
12
+ margin: 0;
13
+ padding: 0;
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ body {
18
+ background: linear-gradient(135deg, #0f0f23 0%, #1a1a3e 50%, #0d0d1f 100%);
19
+ min-height: 100vh;
20
+ font-family: 'Inter', system-ui, sans-serif;
21
+ color: #e2e8f0;
22
+ }
23
+
24
+ .navbar {
25
+ background: rgba(15, 23, 42, 0.9);
26
+ backdrop-filter: blur(20px);
27
+ border-bottom: 1px solid rgba(99, 102, 241, 0.2);
28
+ padding: 16px 30px;
29
+ display: flex;
30
+ justify-content: space-between;
31
+ align-items: center;
32
+ position: sticky;
33
+ top: 0;
34
+ z-index: 100;
35
+ }
36
+
37
+ .nav-brand {
38
+ font-size: 1.5rem;
39
+ font-weight: 700;
40
+ background: linear-gradient(135deg, #818cf8, #c084fc);
41
+ -webkit-background-clip: text;
42
+ -webkit-text-fill-color: transparent;
43
+ background-clip: text;
44
+ text-decoration: none;
45
+ }
46
+
47
+ .nav-links {
48
+ display: flex;
49
+ gap: 20px;
50
+ align-items: center;
51
+ }
52
+
53
+ .nav-links a {
54
+ color: #94a3b8;
55
+ text-decoration: none;
56
+ font-size: 0.9rem;
57
+ transition: color 0.3s ease;
58
+ }
59
+
60
+ .nav-links a:hover {
61
+ color: #818cf8;
62
+ }
63
+
64
+ .container {
65
+ max-width: 900px;
66
+ margin: 0 auto;
67
+ padding: 40px 20px;
68
+ }
69
+
70
+ .breadcrumb {
71
+ margin-bottom: 20px;
72
+ color: #64748b;
73
+ }
74
+
75
+ .breadcrumb a {
76
+ color: #818cf8;
77
+ text-decoration: none;
78
+ transition: color 0.3s ease;
79
+ }
80
+
81
+ .breadcrumb a:hover {
82
+ color: #a5b4fc;
83
+ }
84
+
85
+ .page-header {
86
+ margin-bottom: 30px;
87
+ }
88
+
89
+ .page-header h1 {
90
+ font-size: 1.8rem;
91
+ font-weight: 700;
92
+ background: linear-gradient(135deg, #818cf8, #c084fc, #f472b6);
93
+ -webkit-background-clip: text;
94
+ -webkit-text-fill-color: transparent;
95
+ background-clip: text;
96
+ }
97
+
98
+ .page-header p {
99
+ color: #94a3b8;
100
+ margin-top: 8px;
101
+ }
102
+
103
+ .flash-messages {
104
+ margin-bottom: 20px;
105
+ }
106
+
107
+ .flash {
108
+ padding: 12px 16px;
109
+ border-radius: 12px;
110
+ font-size: 0.9rem;
111
+ margin-bottom: 10px;
112
+ animation: slideIn 0.3s ease;
113
+ }
114
+
115
+ @keyframes slideIn {
116
+ from {
117
+ opacity: 0;
118
+ transform: translateY(-10px);
119
+ }
120
+
121
+ to {
122
+ opacity: 1;
123
+ transform: translateY(0);
124
+ }
125
+ }
126
+
127
+ .flash.error {
128
+ background: rgba(239, 68, 68, 0.15);
129
+ border: 1px solid rgba(239, 68, 68, 0.3);
130
+ color: #fca5a5;
131
+ }
132
+
133
+ .flash.success {
134
+ background: rgba(34, 197, 94, 0.15);
135
+ border: 1px solid rgba(34, 197, 94, 0.3);
136
+ color: #86efac;
137
+ }
138
+
139
+ .card {
140
+ background: rgba(15, 23, 42, 0.8);
141
+ backdrop-filter: blur(20px);
142
+ border: 1px solid rgba(99, 102, 241, 0.15);
143
+ border-radius: 16px;
144
+ padding: 30px;
145
+ }
146
+
147
+ .form-group {
148
+ margin-bottom: 24px;
149
+ }
150
+
151
+ label {
152
+ display: block;
153
+ color: #e2e8f0;
154
+ font-size: 0.9rem;
155
+ font-weight: 500;
156
+ margin-bottom: 10px;
157
+ }
158
+
159
+ input[type="text"] {
160
+ width: 100%;
161
+ padding: 14px 16px;
162
+ background: rgba(30, 41, 59, 0.8);
163
+ border: 1px solid rgba(148, 163, 184, 0.2);
164
+ border-radius: 12px;
165
+ color: #f1f5f9;
166
+ font-size: 1rem;
167
+ font-family: 'Fira Code', monospace;
168
+ transition: all 0.3s ease;
169
+ }
170
+
171
+ input[type="text"]:focus {
172
+ outline: none;
173
+ border-color: #818cf8;
174
+ box-shadow: 0 0 0 3px rgba(129, 140, 248, 0.15);
175
+ }
176
+
177
+ input::placeholder {
178
+ color: #64748b;
179
+ }
180
+
181
+ textarea {
182
+ width: 100%;
183
+ height: 300px;
184
+ background: rgba(30, 41, 59, 0.8);
185
+ border: 1px solid rgba(148, 163, 184, 0.2);
186
+ border-radius: 12px;
187
+ color: #f1f5f9;
188
+ font-size: 14px;
189
+ font-family: 'Fira Code', monospace;
190
+ padding: 16px;
191
+ line-height: 1.6;
192
+ resize: vertical;
193
+ transition: all 0.3s ease;
194
+ }
195
+
196
+ textarea:focus {
197
+ outline: none;
198
+ border-color: #818cf8;
199
+ box-shadow: 0 0 0 3px rgba(129, 140, 248, 0.15);
200
+ }
201
+
202
+ .hint {
203
+ color: #64748b;
204
+ font-size: 0.8rem;
205
+ margin-top: 8px;
206
+ }
207
+
208
+ .actions {
209
+ display: flex;
210
+ gap: 12px;
211
+ margin-top: 30px;
212
+ }
213
+
214
+ .btn {
215
+ padding: 12px 24px;
216
+ border-radius: 12px;
217
+ border: none;
218
+ cursor: pointer;
219
+ font-weight: 600;
220
+ font-family: inherit;
221
+ font-size: 0.95rem;
222
+ transition: all 0.3s ease;
223
+ text-decoration: none;
224
+ display: inline-flex;
225
+ align-items: center;
226
+ gap: 8px;
227
+ }
228
+
229
+ .btn-primary {
230
+ background: linear-gradient(135deg, #22c55e, #16a34a);
231
+ color: white;
232
+ }
233
+
234
+ .btn-primary:hover {
235
+ transform: translateY(-2px);
236
+ box-shadow: 0 10px 30px rgba(34, 197, 94, 0.4);
237
+ }
238
+
239
+ .btn-secondary {
240
+ background: rgba(100, 116, 139, 0.2);
241
+ border: 1px solid rgba(100, 116, 139, 0.3);
242
+ color: #94a3b8;
243
+ }
244
+
245
+ .btn-secondary:hover {
246
+ background: rgba(100, 116, 139, 0.3);
247
+ color: #e2e8f0;
248
+ }
249
+
250
+ .glow-orb {
251
+ position: fixed;
252
+ width: 400px;
253
+ height: 400px;
254
+ border-radius: 50%;
255
+ filter: blur(100px);
256
+ opacity: 0.15;
257
+ pointer-events: none;
258
+ z-index: -1;
259
+ }
260
+
261
+ .orb-1 {
262
+ background: #22c55e;
263
+ top: 20%;
264
+ right: -100px;
265
+ }
266
+
267
+ .orb-2 {
268
+ background: #6366f1;
269
+ bottom: 20%;
270
+ left: -100px;
271
+ }
272
+ </style>
273
+ </head>
274
+
275
+ <body>
276
+ <div class="glow-orb orb-1"></div>
277
+ <div class="glow-orb orb-2"></div>
278
+
279
+ <nav class="navbar">
280
+ <a href="/my-sites" class="nav-brand">h by Xlnk</a>
281
+ <div class="nav-links">
282
+ <a href="/files/{{site_id}}">Back to Files</a>
283
+ <a href="/my-sites">My Sites</a>
284
+ </div>
285
+ </nav>
286
+
287
+ <div class="container">
288
+ <div class="breadcrumb">
289
+ <a href="/my-sites">My Sites</a> / <a href="/manage/{{site_id}}">{{site_id}}</a> / <a
290
+ href="/files/{{site_id}}">Files</a> /
291
+ <strong>Create New</strong>
292
+ </div>
293
+
294
+ <div class="page-header">
295
+ <h1>📝 Create New File</h1>
296
+ <p>Create a new file in your site</p>
297
+ </div>
298
+
299
+ {% with messages = get_flashed_messages(with_categories=true) %}
300
+ {% if messages %}
301
+ <div class="flash-messages">
302
+ {% for category, message in messages %}
303
+ <div class="flash {{ category }}">{{ message }}</div>
304
+ {% endfor %}
305
+ </div>
306
+ {% endif %}
307
+ {% endwith %}
308
+
309
+ <div class="card">
310
+ <form method="post">
311
+ <div class="form-group">
312
+ <label for="filename">Filename</label>
313
+ <input type="text" id="filename" name="filename" placeholder="e.g., about.html or css/styles.css"
314
+ value="{{ filename or '' }}" required autofocus>
315
+ <p class="hint">💡 You can include folder paths like "css/styles.css" to create files in subfolders
316
+ </p>
317
+ </div>
318
+
319
+ <div class="form-group">
320
+ <label for="content">Content (optional)</label>
321
+ <textarea id="content" name="content"
322
+ placeholder="Enter file content here...">{{ content or '' }}</textarea>
323
+ </div>
324
+
325
+ <div class="actions">
326
+ <a href="/files/{{site_id}}" class="btn btn-secondary">Cancel</a>
327
+ <button type="submit" class="btn btn-primary">✨ Create File</button>
328
+ </div>
329
+ </form>
330
+ </div>
331
+ </div>
332
+ </body>
333
+
334
+ </html>
templates/edit.html CHANGED
@@ -61,10 +61,6 @@
61
  color: #818cf8;
62
  }
63
 
64
- .nav-links .logout {
65
- color: #f87171;
66
- }
67
-
68
  .container {
69
  max-width: 1200px;
70
  margin: 0 auto;
@@ -246,17 +242,17 @@
246
  <div class="glow-orb orb-1"></div>
247
 
248
  <nav class="navbar">
249
- <a href="/dashboard" class="nav-brand">h by Xlnk</a>
250
  <div class="nav-links">
251
- <a href="/dashboard">Dashboard</a>
252
- <a href="/upload">Upload</a>
253
- <a href="/logout" class="logout">Logout</a>
254
  </div>
255
  </nav>
256
 
257
  <div class="container">
258
  <div class="breadcrumb">
259
- <a href="/dashboard">Dashboard</a> / <a href="/files/{{site_id}}">{{site_id}}</a> /
 
260
  <strong>{{file}}</strong>
261
  </div>
262
 
 
61
  color: #818cf8;
62
  }
63
 
 
 
 
 
64
  .container {
65
  max-width: 1200px;
66
  margin: 0 auto;
 
242
  <div class="glow-orb orb-1"></div>
243
 
244
  <nav class="navbar">
245
+ <a href="/my-sites" class="nav-brand">h by Xlnk</a>
246
  <div class="nav-links">
247
+ <a href="/files/{{site_id}}">Back to Files</a>
248
+ <a href="/my-sites">My Sites</a>
 
249
  </div>
250
  </nav>
251
 
252
  <div class="container">
253
  <div class="breadcrumb">
254
+ <a href="/my-sites">My Sites</a> / <a href="/manage/{{site_id}}">{{site_id}}</a> / <a
255
+ href="/files/{{site_id}}">Files</a> /
256
  <strong>{{file}}</strong>
257
  </div>
258
 
templates/files.html CHANGED
@@ -59,10 +59,6 @@
59
  color: #818cf8;
60
  }
61
 
62
- .nav-links .logout {
63
- color: #f87171;
64
- }
65
-
66
  .container {
67
  max-width: 900px;
68
  margin: 0 auto;
@@ -306,21 +302,24 @@
306
  <div class="glow-orb orb-2"></div>
307
 
308
  <nav class="navbar">
309
- <a href="/dashboard" class="nav-brand">h by Xlnk</a>
310
  <div class="nav-links">
311
- <a href="/dashboard">Dashboard</a>
312
- <a href="/upload">Upload</a>
313
- <a href="/logout" class="logout">Logout</a>
314
  </div>
315
  </nav>
316
 
317
  <div class="container">
318
  <div class="breadcrumb">
319
- <a href="/dashboard">Dashboard</a> / <strong>{{site_id}}</strong>
320
  </div>
321
 
322
- <div class="page-header">
 
323
  <h1>📁 Files in {{site_id}}</h1>
 
 
 
324
  </div>
325
 
326
  {% with messages = get_flashed_messages(with_categories=true) %}
@@ -340,6 +339,7 @@
340
  <span class="file-name">{{file}}</span>
341
  <div class="file-actions">
342
  <a href="/edit/{{site_id}}/{{file}}">✏️ Edit</a>
 
343
  <a href="/h/{{site_id}}/{{file}}" target="_blank">👁️ View</a>
344
  <a href="/delete-file/{{site_id}}/{{file}}" class="btn-delete"
345
  onclick="return confirm('Delete {{file}}?')">🗑️ Delete</a>
 
59
  color: #818cf8;
60
  }
61
 
 
 
 
 
62
  .container {
63
  max-width: 900px;
64
  margin: 0 auto;
 
302
  <div class="glow-orb orb-2"></div>
303
 
304
  <nav class="navbar">
305
+ <a href="/my-sites" class="nav-brand">h by Xlnk</a>
306
  <div class="nav-links">
307
+ <a href="/manage/{{site_id}}">Back to Manage</a>
308
+ <a href="/my-sites">My Sites</a>
 
309
  </div>
310
  </nav>
311
 
312
  <div class="container">
313
  <div class="breadcrumb">
314
+ <a href="/my-sites">My Sites</a> / <a href="/manage/{{site_id}}">{{site_id}}</a> / <strong>Files</strong>
315
  </div>
316
 
317
+ <div class="page-header"
318
+ style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px;">
319
  <h1>📁 Files in {{site_id}}</h1>
320
+ <a href="/create-file/{{site_id}}" class="btn-upload"
321
+ style="display: inline-flex; align-items: center; gap: 8px; padding: 10px 20px; text-decoration: none;">✨
322
+ Create New File</a>
323
  </div>
324
 
325
  {% with messages = get_flashed_messages(with_categories=true) %}
 
339
  <span class="file-name">{{file}}</span>
340
  <div class="file-actions">
341
  <a href="/edit/{{site_id}}/{{file}}">✏️ Edit</a>
342
+ <a href="/rename-file/{{site_id}}/{{file}}">📝 Rename</a>
343
  <a href="/h/{{site_id}}/{{file}}" target="_blank">👁️ View</a>
344
  <a href="/delete-file/{{site_id}}/{{file}}" class="btn-delete"
345
  onclick="return confirm('Delete {{file}}?')">🗑️ Delete</a>
templates/manage.html ADDED
@@ -0,0 +1,418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+
4
+ <head>
5
+ <title>Manage {{site_id}} - h by Xlnk</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ background: linear-gradient(135deg, #0f0f23 0%, #1a1a3e 50%, #0d0d1f 100%);
17
+ min-height: 100vh;
18
+ font-family: 'Inter', system-ui, sans-serif;
19
+ color: #e2e8f0;
20
+ }
21
+
22
+ .navbar {
23
+ background: rgba(15, 23, 42, 0.9);
24
+ backdrop-filter: blur(20px);
25
+ border-bottom: 1px solid rgba(99, 102, 241, 0.2);
26
+ padding: 16px 30px;
27
+ display: flex;
28
+ justify-content: space-between;
29
+ align-items: center;
30
+ position: sticky;
31
+ top: 0;
32
+ z-index: 100;
33
+ }
34
+
35
+ .nav-brand {
36
+ font-size: 1.5rem;
37
+ font-weight: 700;
38
+ background: linear-gradient(135deg, #818cf8, #c084fc);
39
+ -webkit-background-clip: text;
40
+ -webkit-text-fill-color: transparent;
41
+ background-clip: text;
42
+ text-decoration: none;
43
+ }
44
+
45
+ .nav-links {
46
+ display: flex;
47
+ gap: 20px;
48
+ align-items: center;
49
+ }
50
+
51
+ .nav-links a {
52
+ color: #94a3b8;
53
+ text-decoration: none;
54
+ font-size: 0.9rem;
55
+ transition: color 0.3s ease;
56
+ }
57
+
58
+ .nav-links a:hover {
59
+ color: #818cf8;
60
+ }
61
+
62
+ .container {
63
+ max-width: 800px;
64
+ margin: 0 auto;
65
+ padding: 40px 20px;
66
+ }
67
+
68
+ .page-header {
69
+ margin-bottom: 30px;
70
+ }
71
+
72
+ .page-header h1 {
73
+ font-size: 2rem;
74
+ font-weight: 700;
75
+ background: linear-gradient(135deg, #818cf8, #c084fc, #f472b6);
76
+ -webkit-background-clip: text;
77
+ -webkit-text-fill-color: transparent;
78
+ background-clip: text;
79
+ margin-bottom: 8px;
80
+ }
81
+
82
+ .page-header p {
83
+ color: #94a3b8;
84
+ }
85
+
86
+ .flash-messages {
87
+ margin-bottom: 20px;
88
+ }
89
+
90
+ .flash {
91
+ padding: 12px 16px;
92
+ border-radius: 12px;
93
+ font-size: 0.9rem;
94
+ margin-bottom: 10px;
95
+ animation: slideIn 0.3s ease;
96
+ }
97
+
98
+ @keyframes slideIn {
99
+ from {
100
+ opacity: 0;
101
+ transform: translateY(-10px);
102
+ }
103
+
104
+ to {
105
+ opacity: 1;
106
+ transform: translateY(0);
107
+ }
108
+ }
109
+
110
+ .flash.error {
111
+ background: rgba(239, 68, 68, 0.15);
112
+ border: 1px solid rgba(239, 68, 68, 0.3);
113
+ color: #fca5a5;
114
+ }
115
+
116
+ .flash.success {
117
+ background: rgba(34, 197, 94, 0.15);
118
+ border: 1px solid rgba(34, 197, 94, 0.3);
119
+ color: #86efac;
120
+ }
121
+
122
+ .site-card {
123
+ background: rgba(15, 23, 42, 0.8);
124
+ backdrop-filter: blur(20px);
125
+ border: 1px solid rgba(99, 102, 241, 0.15);
126
+ border-radius: 16px;
127
+ padding: 30px;
128
+ margin-bottom: 24px;
129
+ }
130
+
131
+ .site-id {
132
+ font-size: 1.4rem;
133
+ font-weight: 600;
134
+ color: #f1f5f9;
135
+ margin-bottom: 16px;
136
+ }
137
+
138
+ .site-id a {
139
+ color: inherit;
140
+ text-decoration: none;
141
+ transition: color 0.3s ease;
142
+ }
143
+
144
+ .site-id a:hover {
145
+ color: #818cf8;
146
+ }
147
+
148
+ .site-stats {
149
+ display: flex;
150
+ gap: 30px;
151
+ margin-bottom: 20px;
152
+ flex-wrap: wrap;
153
+ }
154
+
155
+ .stat {
156
+ display: flex;
157
+ flex-direction: column;
158
+ }
159
+
160
+ .stat-label {
161
+ font-size: 0.75rem;
162
+ color: #64748b;
163
+ text-transform: uppercase;
164
+ letter-spacing: 0.5px;
165
+ }
166
+
167
+ .stat-value {
168
+ font-size: 1.1rem;
169
+ font-weight: 600;
170
+ color: #94a3b8;
171
+ }
172
+
173
+ .stat-value.views {
174
+ color: #22c55e;
175
+ }
176
+
177
+ .site-actions {
178
+ display: flex;
179
+ gap: 12px;
180
+ flex-wrap: wrap;
181
+ border-top: 1px solid rgba(148, 163, 184, 0.1);
182
+ padding-top: 20px;
183
+ margin-top: 10px;
184
+ }
185
+
186
+ .action-btn {
187
+ padding: 12px 20px;
188
+ border-radius: 10px;
189
+ font-size: 0.9rem;
190
+ font-weight: 500;
191
+ text-decoration: none;
192
+ text-align: center;
193
+ transition: all 0.3s ease;
194
+ display: inline-flex;
195
+ align-items: center;
196
+ gap: 8px;
197
+ }
198
+
199
+ .action-btn.view {
200
+ background: rgba(99, 102, 241, 0.15);
201
+ color: #a5b4fc;
202
+ border: 1px solid rgba(99, 102, 241, 0.3);
203
+ }
204
+
205
+ .action-btn.view:hover {
206
+ background: rgba(99, 102, 241, 0.25);
207
+ transform: translateY(-2px);
208
+ }
209
+
210
+ .action-btn.edit {
211
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
212
+ color: white;
213
+ border: none;
214
+ }
215
+
216
+ .action-btn.edit:hover {
217
+ transform: translateY(-2px);
218
+ box-shadow: 0 10px 30px rgba(99, 102, 241, 0.4);
219
+ }
220
+
221
+ .action-btn.delete {
222
+ background: rgba(239, 68, 68, 0.15);
223
+ color: #fca5a5;
224
+ border: 1px solid rgba(239, 68, 68, 0.3);
225
+ }
226
+
227
+ .action-btn.delete:hover {
228
+ background: rgba(239, 68, 68, 0.25);
229
+ }
230
+
231
+ .info-box {
232
+ background: rgba(30, 41, 59, 0.6);
233
+ border-radius: 12px;
234
+ padding: 20px;
235
+ margin-top: 16px;
236
+ }
237
+
238
+ .info-box h3 {
239
+ color: #a5b4fc;
240
+ font-size: 0.85rem;
241
+ text-transform: uppercase;
242
+ letter-spacing: 0.5px;
243
+ margin-bottom: 10px;
244
+ }
245
+
246
+ .info-value {
247
+ background: rgba(15, 23, 42, 0.8);
248
+ border: 1px solid rgba(99, 102, 241, 0.2);
249
+ border-radius: 8px;
250
+ padding: 12px 16px;
251
+ font-family: 'Fira Code', monospace;
252
+ font-size: 0.85rem;
253
+ color: #94a3b8;
254
+ word-break: break-all;
255
+ display: flex;
256
+ align-items: center;
257
+ justify-content: space-between;
258
+ gap: 10px;
259
+ }
260
+
261
+ .info-value a {
262
+ color: #818cf8;
263
+ text-decoration: none;
264
+ }
265
+
266
+ .info-value a:hover {
267
+ text-decoration: underline;
268
+ }
269
+
270
+ .copy-btn {
271
+ background: rgba(99, 102, 241, 0.2);
272
+ border: 1px solid rgba(99, 102, 241, 0.3);
273
+ color: #a5b4fc;
274
+ padding: 6px 12px;
275
+ border-radius: 6px;
276
+ cursor: pointer;
277
+ font-size: 0.75rem;
278
+ transition: all 0.2s;
279
+ white-space: nowrap;
280
+ }
281
+
282
+ .copy-btn:hover {
283
+ background: rgba(99, 102, 241, 0.3);
284
+ }
285
+
286
+ .glow-orb {
287
+ position: fixed;
288
+ width: 400px;
289
+ height: 400px;
290
+ border-radius: 50%;
291
+ filter: blur(100px);
292
+ opacity: 0.2;
293
+ pointer-events: none;
294
+ z-index: -1;
295
+ }
296
+
297
+ .orb-1 {
298
+ background: #6366f1;
299
+ top: 10%;
300
+ right: -100px;
301
+ }
302
+
303
+ .orb-2 {
304
+ background: #c084fc;
305
+ bottom: 20%;
306
+ left: -100px;
307
+ }
308
+
309
+ @media (max-width: 600px) {
310
+ .navbar {
311
+ flex-direction: column;
312
+ gap: 15px;
313
+ }
314
+
315
+ .site-stats {
316
+ flex-direction: column;
317
+ gap: 15px;
318
+ }
319
+
320
+ .site-actions {
321
+ flex-direction: column;
322
+ }
323
+
324
+ .action-btn {
325
+ justify-content: center;
326
+ }
327
+ }
328
+ </style>
329
+ </head>
330
+
331
+ <body>
332
+ <div class="glow-orb orb-1"></div>
333
+ <div class="glow-orb orb-2"></div>
334
+
335
+ <nav class="navbar">
336
+ <a href="/my-sites" class="nav-brand">h by Xlnk</a>
337
+ <div class="nav-links">
338
+ <a href="/my-sites">My Sites</a>
339
+ <a href="/upload">Upload</a>
340
+ </div>
341
+ </nav>
342
+
343
+ <div class="container">
344
+ <div class="page-header">
345
+ <h1>⚙️ Manage Site</h1>
346
+ <p>Edit files, view stats, and manage your site</p>
347
+ </div>
348
+
349
+ {% with messages = get_flashed_messages(with_categories=true) %}
350
+ {% if messages %}
351
+ <div class="flash-messages">
352
+ {% for category, message in messages %}
353
+ <div class="flash {{ category }}">{{ message }}</div>
354
+ {% endfor %}
355
+ </div>
356
+ {% endif %}
357
+ {% endwith %}
358
+
359
+ <div class="site-card">
360
+ <div class="site-id">
361
+ <a href="/h/{{site_id}}/" target="_blank">📁 {{ site_id }} ↗</a>
362
+ </div>
363
+
364
+ <div class="site-stats">
365
+ <div class="stat">
366
+ <span class="stat-label">Views</span>
367
+ <span class="stat-value views">{{ site[2] }}</span>
368
+ </div>
369
+ <div class="stat">
370
+ <span class="stat-label">Created</span>
371
+ <span class="stat-value">{{ site[1][:10] if site[1] else "Unknown" }}</span>
372
+ </div>
373
+ <div class="stat">
374
+ <span class="stat-label">Expires</span>
375
+ <span class="stat-value">{{ site[3] or "Never" }}</span>
376
+ </div>
377
+ </div>
378
+
379
+ <div class="site-actions">
380
+ <a href="/h/{{site_id}}/" target="_blank" class="action-btn view">👁️ View Site</a>
381
+ <a href="/files/{{site_id}}" class="action-btn edit">📝 Edit Files</a>
382
+ <a href="/delete/{{site_id}}" onclick="return confirm('Delete this site? This cannot be undone.')"
383
+ class="action-btn delete">🗑️ Delete Site</a>
384
+ </div>
385
+
386
+ <div class="info-box">
387
+ <h3>🌐 Site URL</h3>
388
+ <div class="info-value">
389
+ <a href="/h/{{site_id}}/" target="_blank" id="siteUrl">{{ request.host_url }}h/{{site_id}}/</a>
390
+ <button class="copy-btn" onclick="copyToClipboard('siteUrl')">📋 Copy</button>
391
+ </div>
392
+ </div>
393
+ </div>
394
+ </div>
395
+
396
+ <script>
397
+ function copyToClipboard(elementId) {
398
+ const element = document.getElementById(elementId);
399
+ const text = element.textContent || element.innerText;
400
+ navigator.clipboard.writeText(text).then(() => {
401
+ const btn = element.parentElement.querySelector('.copy-btn');
402
+ const originalText = btn.textContent;
403
+ btn.textContent = '✓ Copied!';
404
+ btn.style.background = 'rgba(34, 197, 94, 0.2)';
405
+ btn.style.borderColor = 'rgba(34, 197, 94, 0.3)';
406
+ btn.style.color = '#86efac';
407
+ setTimeout(() => {
408
+ btn.textContent = originalText;
409
+ btn.style.background = '';
410
+ btn.style.borderColor = '';
411
+ btn.style.color = '';
412
+ }, 2000);
413
+ });
414
+ }
415
+ </script>
416
+ </body>
417
+
418
+ </html>
templates/my_sites.html ADDED
@@ -0,0 +1,399 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+
4
+ <head>
5
+ <title>My Sites - h by Xlnk</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ background: linear-gradient(135deg, #0f0f23 0%, #1a1a3e 50%, #0d0d1f 100%);
17
+ min-height: 100vh;
18
+ font-family: 'Inter', system-ui, sans-serif;
19
+ color: #e2e8f0;
20
+ }
21
+
22
+ .navbar {
23
+ background: rgba(15, 23, 42, 0.9);
24
+ backdrop-filter: blur(20px);
25
+ border-bottom: 1px solid rgba(99, 102, 241, 0.2);
26
+ padding: 16px 30px;
27
+ display: flex;
28
+ justify-content: space-between;
29
+ align-items: center;
30
+ position: sticky;
31
+ top: 0;
32
+ z-index: 100;
33
+ }
34
+
35
+ .nav-brand {
36
+ font-size: 1.5rem;
37
+ font-weight: 700;
38
+ background: linear-gradient(135deg, #818cf8, #c084fc);
39
+ -webkit-background-clip: text;
40
+ -webkit-text-fill-color: transparent;
41
+ background-clip: text;
42
+ text-decoration: none;
43
+ }
44
+
45
+ .nav-links {
46
+ display: flex;
47
+ gap: 20px;
48
+ align-items: center;
49
+ }
50
+
51
+ .nav-links a {
52
+ color: #94a3b8;
53
+ text-decoration: none;
54
+ font-size: 0.9rem;
55
+ transition: color 0.3s ease;
56
+ }
57
+
58
+ .nav-links a:hover {
59
+ color: #818cf8;
60
+ }
61
+
62
+ .nav-links a.active {
63
+ color: #818cf8;
64
+ }
65
+
66
+ .container {
67
+ max-width: 1200px;
68
+ margin: 0 auto;
69
+ padding: 40px 20px;
70
+ }
71
+
72
+ .page-header {
73
+ display: flex;
74
+ justify-content: space-between;
75
+ align-items: center;
76
+ margin-bottom: 30px;
77
+ flex-wrap: wrap;
78
+ gap: 20px;
79
+ }
80
+
81
+ .page-header h1 {
82
+ font-size: 2rem;
83
+ font-weight: 700;
84
+ background: linear-gradient(135deg, #818cf8, #c084fc, #f472b6);
85
+ -webkit-background-clip: text;
86
+ -webkit-text-fill-color: transparent;
87
+ background-clip: text;
88
+ }
89
+
90
+ .upload-btn {
91
+ display: inline-flex;
92
+ align-items: center;
93
+ gap: 8px;
94
+ padding: 12px 24px;
95
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
96
+ border: none;
97
+ border-radius: 12px;
98
+ color: white;
99
+ font-size: 0.95rem;
100
+ font-weight: 600;
101
+ text-decoration: none;
102
+ transition: all 0.3s ease;
103
+ }
104
+
105
+ .upload-btn:hover {
106
+ transform: translateY(-2px);
107
+ box-shadow: 0 10px 30px rgba(99, 102, 241, 0.4);
108
+ }
109
+
110
+ .flash-messages {
111
+ margin-bottom: 20px;
112
+ }
113
+
114
+ .flash {
115
+ padding: 12px 16px;
116
+ border-radius: 12px;
117
+ font-size: 0.9rem;
118
+ margin-bottom: 10px;
119
+ animation: slideIn 0.3s ease;
120
+ }
121
+
122
+ @keyframes slideIn {
123
+ from {
124
+ opacity: 0;
125
+ transform: translateY(-10px);
126
+ }
127
+
128
+ to {
129
+ opacity: 1;
130
+ transform: translateY(0);
131
+ }
132
+ }
133
+
134
+ .flash.error {
135
+ background: rgba(239, 68, 68, 0.15);
136
+ border: 1px solid rgba(239, 68, 68, 0.3);
137
+ color: #fca5a5;
138
+ }
139
+
140
+ .flash.success {
141
+ background: rgba(34, 197, 94, 0.15);
142
+ border: 1px solid rgba(34, 197, 94, 0.3);
143
+ color: #86efac;
144
+ }
145
+
146
+ .sites-grid {
147
+ display: grid;
148
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
149
+ gap: 20px;
150
+ }
151
+
152
+ .site-card {
153
+ background: rgba(15, 23, 42, 0.8);
154
+ backdrop-filter: blur(20px);
155
+ border: 1px solid rgba(99, 102, 241, 0.15);
156
+ border-radius: 16px;
157
+ padding: 24px;
158
+ transition: all 0.3s ease;
159
+ }
160
+
161
+ .site-card:hover {
162
+ border-color: rgba(99, 102, 241, 0.4);
163
+ transform: translateY(-4px);
164
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
165
+ }
166
+
167
+ .site-id {
168
+ font-size: 1.2rem;
169
+ font-weight: 600;
170
+ color: #f1f5f9;
171
+ margin-bottom: 12px;
172
+ }
173
+
174
+ .site-id a {
175
+ color: inherit;
176
+ text-decoration: none;
177
+ transition: color 0.3s ease;
178
+ }
179
+
180
+ .site-id a:hover {
181
+ color: #818cf8;
182
+ }
183
+
184
+ .site-stats {
185
+ display: flex;
186
+ gap: 20px;
187
+ margin-bottom: 16px;
188
+ }
189
+
190
+ .stat {
191
+ display: flex;
192
+ flex-direction: column;
193
+ }
194
+
195
+ .stat-label {
196
+ font-size: 0.75rem;
197
+ color: #64748b;
198
+ text-transform: uppercase;
199
+ letter-spacing: 0.5px;
200
+ }
201
+
202
+ .stat-value {
203
+ font-size: 1rem;
204
+ font-weight: 600;
205
+ color: #94a3b8;
206
+ }
207
+
208
+ .stat-value.views {
209
+ color: #22c55e;
210
+ }
211
+
212
+ .site-actions {
213
+ display: flex;
214
+ gap: 10px;
215
+ border-top: 1px solid rgba(148, 163, 184, 0.1);
216
+ padding-top: 16px;
217
+ margin-top: 8px;
218
+ }
219
+
220
+ .action-btn {
221
+ flex: 1;
222
+ padding: 10px 16px;
223
+ border-radius: 10px;
224
+ font-size: 0.85rem;
225
+ font-weight: 500;
226
+ text-decoration: none;
227
+ text-align: center;
228
+ transition: all 0.3s ease;
229
+ }
230
+
231
+ .action-btn.view {
232
+ background: rgba(99, 102, 241, 0.15);
233
+ color: #a5b4fc;
234
+ border: 1px solid rgba(99, 102, 241, 0.3);
235
+ }
236
+
237
+ .action-btn.view:hover {
238
+ background: rgba(99, 102, 241, 0.25);
239
+ }
240
+
241
+ .action-btn.edit {
242
+ background: rgba(34, 197, 94, 0.15);
243
+ color: #86efac;
244
+ border: 1px solid rgba(34, 197, 94, 0.3);
245
+ }
246
+
247
+ .action-btn.edit:hover {
248
+ background: rgba(34, 197, 94, 0.25);
249
+ }
250
+
251
+ .action-btn.delete {
252
+ background: rgba(239, 68, 68, 0.15);
253
+ color: #fca5a5;
254
+ border: 1px solid rgba(239, 68, 68, 0.3);
255
+ }
256
+
257
+ .action-btn.delete:hover {
258
+ background: rgba(239, 68, 68, 0.25);
259
+ }
260
+
261
+ .empty-state {
262
+ text-align: center;
263
+ padding: 60px 20px;
264
+ background: rgba(15, 23, 42, 0.5);
265
+ border-radius: 20px;
266
+ border: 1px dashed rgba(99, 102, 241, 0.3);
267
+ }
268
+
269
+ .empty-state-icon {
270
+ font-size: 4rem;
271
+ margin-bottom: 20px;
272
+ }
273
+
274
+ .empty-state h2 {
275
+ font-size: 1.5rem;
276
+ color: #e2e8f0;
277
+ margin-bottom: 10px;
278
+ }
279
+
280
+ .empty-state p {
281
+ color: #64748b;
282
+ margin-bottom: 24px;
283
+ }
284
+
285
+ .glow-orb {
286
+ position: fixed;
287
+ width: 400px;
288
+ height: 400px;
289
+ border-radius: 50%;
290
+ filter: blur(100px);
291
+ opacity: 0.2;
292
+ pointer-events: none;
293
+ z-index: -1;
294
+ }
295
+
296
+ .orb-1 {
297
+ background: #6366f1;
298
+ top: 10%;
299
+ right: -100px;
300
+ }
301
+
302
+ .orb-2 {
303
+ background: #c084fc;
304
+ bottom: 20%;
305
+ left: -100px;
306
+ }
307
+
308
+ @media (max-width: 600px) {
309
+ .navbar {
310
+ flex-direction: column;
311
+ gap: 15px;
312
+ }
313
+
314
+ .page-header {
315
+ flex-direction: column;
316
+ text-align: center;
317
+ }
318
+
319
+ .site-stats {
320
+ flex-wrap: wrap;
321
+ }
322
+
323
+ .site-actions {
324
+ flex-wrap: wrap;
325
+ }
326
+ }
327
+ </style>
328
+ </head>
329
+
330
+ <body>
331
+ <div class="glow-orb orb-1"></div>
332
+ <div class="glow-orb orb-2"></div>
333
+
334
+ <nav class="navbar">
335
+ <a href="/my-sites" class="nav-brand">h by Xlnk</a>
336
+ <div class="nav-links">
337
+ <a href="/my-sites" class="active">My Sites</a>
338
+ <a href="/upload">Upload</a>
339
+ </div>
340
+ </nav>
341
+
342
+ <div class="container">
343
+ <div class="page-header">
344
+ <h1>My Sites</h1>
345
+ <a href="/upload" class="upload-btn">
346
+ <span>🚀</span> Upload New Site
347
+ </a>
348
+ </div>
349
+
350
+ {% with messages = get_flashed_messages(with_categories=true) %}
351
+ {% if messages %}
352
+ <div class="flash-messages">
353
+ {% for category, message in messages %}
354
+ <div class="flash {{ category }}">{{ message }}</div>
355
+ {% endfor %}
356
+ </div>
357
+ {% endif %}
358
+ {% endwith %}
359
+
360
+ {% if sites %}
361
+ <div class="sites-grid">
362
+ {% for s in sites %}
363
+ <div class="site-card">
364
+ <div class="site-id">
365
+ <a href="/h/{{s[0]}}/" target="_blank">{{ s[0] }} ↗</a>
366
+ </div>
367
+ <div class="site-stats">
368
+ <div class="stat">
369
+ <span class="stat-label">Views</span>
370
+ <span class="stat-value views">{{ s[2] }}</span>
371
+ </div>
372
+ <div class="stat">
373
+ <span class="stat-label">Expires</span>
374
+ <span class="stat-value">{{ s[3] or "Never" }}</span>
375
+ </div>
376
+ </div>
377
+ <div class="site-actions">
378
+ <a href="/h/{{s[0]}}/" target="_blank" class="action-btn view">View</a>
379
+ <a href="/manage/{{s[0]}}" class="action-btn edit">Manage</a>
380
+ <a href="/delete/{{s[0]}}" onclick="return confirm('Delete this site? This cannot be undone.')"
381
+ class="action-btn delete">Delete</a>
382
+ </div>
383
+ </div>
384
+ {% endfor %}
385
+ </div>
386
+ {% else %}
387
+ <div class="empty-state">
388
+ <div class="empty-state-icon">📭</div>
389
+ <h2>No sites yet</h2>
390
+ <p>Upload your first site to get started</p>
391
+ <a href="/upload" class="upload-btn">
392
+ <span>🚀</span> Upload Site
393
+ </a>
394
+ </div>
395
+ {% endif %}
396
+ </div>
397
+ </body>
398
+
399
+ </html>
templates/rename_file.html ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+
4
+ <head>
5
+ <title>Rename File - {{site_id}} | h by Xlnk</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <link
8
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Fira+Code:wght@400;500&display=swap"
9
+ rel="stylesheet">
10
+ <style>
11
+ * {
12
+ margin: 0;
13
+ padding: 0;
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ body {
18
+ background: linear-gradient(135deg, #0f0f23 0%, #1a1a3e 50%, #0d0d1f 100%);
19
+ min-height: 100vh;
20
+ font-family: 'Inter', system-ui, sans-serif;
21
+ color: #e2e8f0;
22
+ }
23
+
24
+ .navbar {
25
+ background: rgba(15, 23, 42, 0.9);
26
+ backdrop-filter: blur(20px);
27
+ border-bottom: 1px solid rgba(99, 102, 241, 0.2);
28
+ padding: 16px 30px;
29
+ display: flex;
30
+ justify-content: space-between;
31
+ align-items: center;
32
+ position: sticky;
33
+ top: 0;
34
+ z-index: 100;
35
+ }
36
+
37
+ .nav-brand {
38
+ font-size: 1.5rem;
39
+ font-weight: 700;
40
+ background: linear-gradient(135deg, #818cf8, #c084fc);
41
+ -webkit-background-clip: text;
42
+ -webkit-text-fill-color: transparent;
43
+ background-clip: text;
44
+ text-decoration: none;
45
+ }
46
+
47
+ .nav-links {
48
+ display: flex;
49
+ gap: 20px;
50
+ align-items: center;
51
+ }
52
+
53
+ .nav-links a {
54
+ color: #94a3b8;
55
+ text-decoration: none;
56
+ font-size: 0.9rem;
57
+ transition: color 0.3s ease;
58
+ }
59
+
60
+ .nav-links a:hover {
61
+ color: #818cf8;
62
+ }
63
+
64
+ .container {
65
+ max-width: 600px;
66
+ margin: 0 auto;
67
+ padding: 40px 20px;
68
+ }
69
+
70
+ .breadcrumb {
71
+ margin-bottom: 20px;
72
+ color: #64748b;
73
+ }
74
+
75
+ .breadcrumb a {
76
+ color: #818cf8;
77
+ text-decoration: none;
78
+ transition: color 0.3s ease;
79
+ }
80
+
81
+ .breadcrumb a:hover {
82
+ color: #a5b4fc;
83
+ }
84
+
85
+ .page-header {
86
+ margin-bottom: 30px;
87
+ }
88
+
89
+ .page-header h1 {
90
+ font-size: 1.8rem;
91
+ font-weight: 700;
92
+ background: linear-gradient(135deg, #818cf8, #c084fc, #f472b6);
93
+ -webkit-background-clip: text;
94
+ -webkit-text-fill-color: transparent;
95
+ background-clip: text;
96
+ }
97
+
98
+ .page-header p {
99
+ color: #94a3b8;
100
+ margin-top: 8px;
101
+ }
102
+
103
+ .flash-messages {
104
+ margin-bottom: 20px;
105
+ }
106
+
107
+ .flash {
108
+ padding: 12px 16px;
109
+ border-radius: 12px;
110
+ font-size: 0.9rem;
111
+ margin-bottom: 10px;
112
+ animation: slideIn 0.3s ease;
113
+ }
114
+
115
+ @keyframes slideIn {
116
+ from {
117
+ opacity: 0;
118
+ transform: translateY(-10px);
119
+ }
120
+
121
+ to {
122
+ opacity: 1;
123
+ transform: translateY(0);
124
+ }
125
+ }
126
+
127
+ .flash.error {
128
+ background: rgba(239, 68, 68, 0.15);
129
+ border: 1px solid rgba(239, 68, 68, 0.3);
130
+ color: #fca5a5;
131
+ }
132
+
133
+ .flash.success {
134
+ background: rgba(34, 197, 94, 0.15);
135
+ border: 1px solid rgba(34, 197, 94, 0.3);
136
+ color: #86efac;
137
+ }
138
+
139
+ .card {
140
+ background: rgba(15, 23, 42, 0.8);
141
+ backdrop-filter: blur(20px);
142
+ border: 1px solid rgba(99, 102, 241, 0.15);
143
+ border-radius: 16px;
144
+ padding: 30px;
145
+ }
146
+
147
+ .current-name {
148
+ background: rgba(30, 41, 59, 0.6);
149
+ border-radius: 12px;
150
+ padding: 16px 20px;
151
+ margin-bottom: 24px;
152
+ }
153
+
154
+ .current-name-label {
155
+ color: #64748b;
156
+ font-size: 0.8rem;
157
+ text-transform: uppercase;
158
+ letter-spacing: 0.5px;
159
+ margin-bottom: 6px;
160
+ }
161
+
162
+ .current-name-value {
163
+ color: #f1f5f9;
164
+ font-family: 'Fira Code', monospace;
165
+ font-size: 1rem;
166
+ }
167
+
168
+ .form-group {
169
+ margin-bottom: 24px;
170
+ }
171
+
172
+ label {
173
+ display: block;
174
+ color: #e2e8f0;
175
+ font-size: 0.9rem;
176
+ font-weight: 500;
177
+ margin-bottom: 10px;
178
+ }
179
+
180
+ input[type="text"] {
181
+ width: 100%;
182
+ padding: 14px 16px;
183
+ background: rgba(30, 41, 59, 0.8);
184
+ border: 1px solid rgba(148, 163, 184, 0.2);
185
+ border-radius: 12px;
186
+ color: #f1f5f9;
187
+ font-size: 1rem;
188
+ font-family: 'Fira Code', monospace;
189
+ transition: all 0.3s ease;
190
+ }
191
+
192
+ input[type="text"]:focus {
193
+ outline: none;
194
+ border-color: #818cf8;
195
+ box-shadow: 0 0 0 3px rgba(129, 140, 248, 0.15);
196
+ }
197
+
198
+ input::placeholder {
199
+ color: #64748b;
200
+ }
201
+
202
+ .hint {
203
+ color: #64748b;
204
+ font-size: 0.8rem;
205
+ margin-top: 8px;
206
+ }
207
+
208
+ .actions {
209
+ display: flex;
210
+ gap: 12px;
211
+ margin-top: 30px;
212
+ }
213
+
214
+ .btn {
215
+ padding: 12px 24px;
216
+ border-radius: 12px;
217
+ border: none;
218
+ cursor: pointer;
219
+ font-weight: 600;
220
+ font-family: inherit;
221
+ font-size: 0.95rem;
222
+ transition: all 0.3s ease;
223
+ text-decoration: none;
224
+ display: inline-flex;
225
+ align-items: center;
226
+ gap: 8px;
227
+ }
228
+
229
+ .btn-primary {
230
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
231
+ color: white;
232
+ }
233
+
234
+ .btn-primary:hover {
235
+ transform: translateY(-2px);
236
+ box-shadow: 0 10px 30px rgba(99, 102, 241, 0.4);
237
+ }
238
+
239
+ .btn-secondary {
240
+ background: rgba(100, 116, 139, 0.2);
241
+ border: 1px solid rgba(100, 116, 139, 0.3);
242
+ color: #94a3b8;
243
+ }
244
+
245
+ .btn-secondary:hover {
246
+ background: rgba(100, 116, 139, 0.3);
247
+ color: #e2e8f0;
248
+ }
249
+
250
+ .glow-orb {
251
+ position: fixed;
252
+ width: 400px;
253
+ height: 400px;
254
+ border-radius: 50%;
255
+ filter: blur(100px);
256
+ opacity: 0.15;
257
+ pointer-events: none;
258
+ z-index: -1;
259
+ }
260
+
261
+ .orb-1 {
262
+ background: #6366f1;
263
+ top: 20%;
264
+ right: -100px;
265
+ }
266
+
267
+ .orb-2 {
268
+ background: #c084fc;
269
+ bottom: 20%;
270
+ left: -100px;
271
+ }
272
+ </style>
273
+ </head>
274
+
275
+ <body>
276
+ <div class="glow-orb orb-1"></div>
277
+ <div class="glow-orb orb-2"></div>
278
+
279
+ <nav class="navbar">
280
+ <a href="/my-sites" class="nav-brand">h by Xlnk</a>
281
+ <div class="nav-links">
282
+ <a href="/files/{{site_id}}">Back to Files</a>
283
+ <a href="/my-sites">My Sites</a>
284
+ </div>
285
+ </nav>
286
+
287
+ <div class="container">
288
+ <div class="breadcrumb">
289
+ <a href="/my-sites">My Sites</a> / <a href="/manage/{{site_id}}">{{site_id}}</a> / <a
290
+ href="/files/{{site_id}}">Files</a> /
291
+ <strong>Rename</strong>
292
+ </div>
293
+
294
+ <div class="page-header">
295
+ <h1>✏️ Rename File</h1>
296
+ <p>Change the name or location of this file</p>
297
+ </div>
298
+
299
+ {% with messages = get_flashed_messages(with_categories=true) %}
300
+ {% if messages %}
301
+ <div class="flash-messages">
302
+ {% for category, message in messages %}
303
+ <div class="flash {{ category }}">{{ message }}</div>
304
+ {% endfor %}
305
+ </div>
306
+ {% endif %}
307
+ {% endwith %}
308
+
309
+ <div class="card">
310
+ <div class="current-name">
311
+ <div class="current-name-label">Current Name</div>
312
+ <div class="current-name-value">{{file}}</div>
313
+ </div>
314
+
315
+ <form method="post">
316
+ <div class="form-group">
317
+ <label for="new_name">New Name</label>
318
+ <input type="text" id="new_name" name="new_name" placeholder="Enter new filename"
319
+ value="{{ new_name or file }}" required autofocus>
320
+ <p class="hint">💡 You can move the file to a subfolder by including a path like "css/styles.css"
321
+ </p>
322
+ </div>
323
+
324
+ <div class="actions">
325
+ <a href="/files/{{site_id}}" class="btn btn-secondary">Cancel</a>
326
+ <button type="submit" class="btn btn-primary">📝 Rename File</button>
327
+ </div>
328
+ </form>
329
+ </div>
330
+ </div>
331
+ </body>
332
+
333
+ </html>
templates/upload.html CHANGED
@@ -59,8 +59,8 @@
59
  color: #818cf8;
60
  }
61
 
62
- .nav-links .logout {
63
- color: #f87171;
64
  }
65
 
66
  .container {
@@ -271,11 +271,10 @@
271
  <div class="glow-orb orb-2"></div>
272
 
273
  <nav class="navbar">
274
- <a href="/dashboard" class="nav-brand">h by Xlnk</a>
275
  <div class="nav-links">
276
- <a href="/dashboard">Dashboard</a>
277
- <a href="/upload">Upload</a>
278
- <a href="/logout" class="logout">Logout</a>
279
  </div>
280
  </nav>
281
 
 
59
  color: #818cf8;
60
  }
61
 
62
+ .nav-links a.active {
63
+ color: #818cf8;
64
  }
65
 
66
  .container {
 
271
  <div class="glow-orb orb-2"></div>
272
 
273
  <nav class="navbar">
274
+ <a href="/my-sites" class="nav-brand">h by Xlnk</a>
275
  <div class="nav-links">
276
+ <a href="/my-sites">My Sites</a>
277
+ <a href="/upload" class="active">Upload</a>
 
278
  </div>
279
  </nav>
280
 
uploads/1/index.html ADDED
@@ -0,0 +1,1592 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Xlnk AI Chat</title>
7
+ <style>
8
+ /* Green Animated Profile Styles */
9
+ .message.assistant .avatar {
10
+ background: linear-gradient(135deg, #10a37f 0%, #19c37d 100%);
11
+ border: 2px solid #10a37f;
12
+ box-shadow:
13
+ 0 0 10px #10a37f,
14
+ 0 0 20px #10a37f,
15
+ inset 0 0 10px rgba(16, 163, 127, 0.3);
16
+ animation: avatar-glow 3s infinite alternate, subtle-float 4s infinite ease-in-out;
17
+ overflow: hidden;
18
+ position: relative;
19
+ }
20
+
21
+ .message.assistant .avatar::before {
22
+ content: '';
23
+ position: absolute;
24
+ top: -50%;
25
+ left: -50%;
26
+ width: 200%;
27
+ height: 200%;
28
+ background: linear-gradient(
29
+ 45deg,
30
+ transparent 30%,
31
+ rgba(16, 163, 127, 0.3) 50%,
32
+ transparent 70%
33
+ );
34
+ animation: shine 3s infinite linear;
35
+ }
36
+
37
+ @keyframes avatar-glow {
38
+ 0% {
39
+ box-shadow:
40
+ 0 0 10px #10a37f,
41
+ 0 0 20px #10a37f,
42
+ inset 0 0 10px rgba(16, 163, 127, 0.3);
43
+ }
44
+ 100% {
45
+ box-shadow:
46
+ 0 0 15px #10a37f,
47
+ 0 0 30px #10a37f,
48
+ 0 0 40px #10a37f,
49
+ inset 0 0 15px rgba(16, 163, 127, 0.5);
50
+ }
51
+ }
52
+
53
+ @keyframes subtle-float {
54
+ 0%, 100% { transform: translateY(0px); }
55
+ 50% { transform: translateY(-5px); }
56
+ }
57
+
58
+ @keyframes shine {
59
+ 0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); }
60
+ 100% { transform: translateX(100%) translateY(100%) rotate(45deg); }
61
+ }
62
+
63
+ .message.assistant .avatar img {
64
+ width: 100%;
65
+ height: 100%;
66
+ object-fit: cover;
67
+ border-radius: 4px;
68
+ position: relative;
69
+ z-index: 1;
70
+ animation: image-pulse 2s infinite;
71
+ }
72
+
73
+ @keyframes image-pulse {
74
+ 0%, 100% { opacity: 0.95; }
75
+ 50% { opacity: 1; }
76
+ }
77
+
78
+ /* When AI is typing/thinking, enhance the animation */
79
+ .message.assistant.thinking .avatar {
80
+ animation: avatar-glow 1.5s infinite alternate, subtle-float 3s infinite ease-in-out;
81
+ }
82
+
83
+ .message.assistant.thinking .avatar::before {
84
+ animation: shine 1.5s infinite linear;
85
+ }
86
+
87
+ /* Typing Cursor Styles - Centered Image */
88
+ .typing-cursor {
89
+ display: inline-flex;
90
+ align-items: center;
91
+ justify-content: center;
92
+ width: 20px;
93
+ height: 20px;
94
+ border-radius: 50%;
95
+ animation: cursor-blink 1s infinite;
96
+ position: relative;
97
+ overflow: hidden;
98
+ border: 2px solid #10a37f;
99
+ box-shadow:
100
+ 0 0 6px #10a37f,
101
+ 0 0 12px #10a37f,
102
+ inset 0 0 6px rgba(16, 163, 127, 0.5);
103
+ vertical-align: middle;
104
+ margin-left: 4px;
105
+ margin-bottom: -2px; /* Fine adjustment for vertical centering */
106
+ }
107
+
108
+ .typing-cursor img {
109
+ width: 100%;
110
+ height: 100%;
111
+ object-fit: cover;
112
+ border-radius: 50%;
113
+ display: block;
114
+ position: relative;
115
+ z-index: 2;
116
+ }
117
+
118
+ @keyframes cursor-blink {
119
+ 0%, 50% { opacity: 1; }
120
+ 51%, 100% { opacity: 0.3; }
121
+ }
122
+
123
+ .typing-cursor::before {
124
+ content: '';
125
+ position: absolute;
126
+ top: -50%;
127
+ left: -50%;
128
+ width: 200%;
129
+ height: 200%;
130
+ background: linear-gradient(
131
+ 45deg,
132
+ transparent 30%,
133
+ rgba(16, 163, 127, 0.4) 50%,
134
+ transparent 70%
135
+ );
136
+ animation: cursor-shine 2s infinite linear;
137
+ z-index: 1;
138
+ }
139
+
140
+ @keyframes cursor-shine {
141
+ 0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); }
142
+ 100% { transform: translateX(100%) translateY(100%) rotate(45deg); }
143
+ }
144
+
145
+ /* Enhanced cursor animation when typing */
146
+ .message.assistant.thinking .typing-cursor {
147
+ animation: cursor-blink 0.8s infinite, cursor-pulse 1.5s infinite;
148
+ }
149
+
150
+ @keyframes cursor-pulse {
151
+ 0%, 100% { transform: scale(1); }
152
+ 50% { transform: scale(1.1); }
153
+ }
154
+
155
+ /* Ensure proper text alignment */
156
+ .message-content {
157
+ position: relative;
158
+ display: inline-block;
159
+ line-height: 1.6;
160
+ }
161
+
162
+ .typing-indicator {
163
+ display: inline-block;
164
+ width: 8px;
165
+ height: 8px;
166
+ border-radius: 50%;
167
+ background: #9ca3af;
168
+ animation: pulse 1.4s infinite;
169
+ vertical-align: middle;
170
+ }
171
+
172
+ @keyframes pulse {
173
+ 0%, 100% {
174
+ opacity: 0.3;
175
+ }
176
+ 50% {
177
+ opacity: 1;
178
+ }
179
+ }
180
+
181
+ /* Existing styles remain unchanged */
182
+ * {
183
+ margin: 0;
184
+ padding: 0;
185
+ box-sizing: border-box;
186
+ }
187
+
188
+ body {
189
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', sans-serif;
190
+ background: #1a1a1a;
191
+ height: 100vh;
192
+ display: flex;
193
+ flex-direction: column;
194
+ }
195
+
196
+ .header {
197
+ background: #2a2a2a;
198
+ border-bottom: 1px solid #3a3a3a;
199
+ padding: 12px 20px;
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: space-between;
203
+ box-shadow: 0 1px 3px rgba(0,0,0,0.3);
204
+ }
205
+
206
+ .header h1 {
207
+ font-size: 18px;
208
+ font-weight: 600;
209
+ color: #e5e5e5;
210
+ }
211
+
212
+ .model-selector {
213
+ display: flex;
214
+ align-items: center;
215
+ gap: 8px;
216
+ }
217
+
218
+ .model-selector label {
219
+ font-size: 13px;
220
+ color: #9ca3af;
221
+ font-weight: 500;
222
+ }
223
+
224
+ .model-selector select {
225
+ padding: 6px 12px;
226
+ border: 1px solid #4a4a4a;
227
+ border-radius: 6px;
228
+ font-size: 13px;
229
+ background: #3a3a3a;
230
+ color: #e5e5e5;
231
+ cursor: pointer;
232
+ outline: none;
233
+ transition: border-color 0.2s;
234
+ }
235
+
236
+ .model-selector select:hover {
237
+ border-color: #6a6a6a;
238
+ }
239
+
240
+ .model-selector select:focus {
241
+ border-color: #10a37f;
242
+ }
243
+
244
+ .sidebar {
245
+ position: fixed;
246
+ left: 0;
247
+ top: 0;
248
+ width: 260px;
249
+ height: 100vh;
250
+ background: #2a2a2a;
251
+ border-right: 1px solid #3a3a3a;
252
+ display: flex;
253
+ flex-direction: column;
254
+ transition: transform 0.3s;
255
+ z-index: 10;
256
+ }
257
+
258
+ .sidebar-header {
259
+ padding: 16px;
260
+ border-bottom: 1px solid #3a3a3a;
261
+ display: flex;
262
+ align-items: center;
263
+ gap: 8px;
264
+ }
265
+
266
+ .new-chat-btn {
267
+ flex: 1;
268
+ padding: 10px;
269
+ background: #3a3a3a;
270
+ color: #e5e5e5;
271
+ border: 1px solid #4a4a4a;
272
+ border-radius: 6px;
273
+ font-size: 13px;
274
+ cursor: pointer;
275
+ transition: background 0.2s;
276
+ }
277
+
278
+ .new-chat-btn:hover {
279
+ background: #4a4a4a;
280
+ }
281
+
282
+ .chat-history {
283
+ flex: 1;
284
+ overflow-y: auto;
285
+ padding: 8px;
286
+ }
287
+
288
+ .sidebar-footer {
289
+ padding: 12px;
290
+ border-top: 1px solid #3a3a3a;
291
+ }
292
+
293
+ .clear-all-btn {
294
+ width: 100%;
295
+ padding: 10px;
296
+ background: transparent;
297
+ color: #ef4444;
298
+ border: 1px solid #ef4444;
299
+ border-radius: 6px;
300
+ font-size: 13px;
301
+ cursor: pointer;
302
+ transition: all 0.2s;
303
+ }
304
+
305
+ .clear-all-btn:hover {
306
+ background: #ef4444;
307
+ color: white;
308
+ }
309
+
310
+ .history-item {
311
+ padding: 10px 12px;
312
+ margin-bottom: 4px;
313
+ border-radius: 6px;
314
+ cursor: pointer;
315
+ font-size: 13px;
316
+ color: #9ca3af;
317
+ background: transparent;
318
+ transition: background 0.2s;
319
+ display: flex;
320
+ align-items: center;
321
+ justify-content: space-between;
322
+ gap: 8px;
323
+ }
324
+
325
+ .history-item:hover {
326
+ background: #3a3a3a;
327
+ }
328
+
329
+ .history-item.active {
330
+ background: #3a3a3a;
331
+ color: #e5e5e5;
332
+ }
333
+
334
+ .history-item-title {
335
+ flex: 1;
336
+ white-space: nowrap;
337
+ overflow: hidden;
338
+ text-overflow: ellipsis;
339
+ }
340
+
341
+ .delete-chat-btn {
342
+ padding: 4px 8px;
343
+ background: transparent;
344
+ color: #ef4444;
345
+ border: none;
346
+ border-radius: 4px;
347
+ font-size: 12px;
348
+ cursor: pointer;
349
+ opacity: 0;
350
+ transition: all 0.2s;
351
+ }
352
+
353
+ .history-item:hover .delete-chat-btn {
354
+ opacity: 1;
355
+ }
356
+
357
+ .delete-chat-btn:hover {
358
+ background: #ef4444;
359
+ color: white;
360
+ }
361
+
362
+ .close-sidebar-btn {
363
+ background: #3a3a3a;
364
+ color: #e5e5e5;
365
+ border: 1px solid #4a4a4a;
366
+ padding: 10px 14px;
367
+ border-radius: 6px;
368
+ font-size: 18px;
369
+ font-weight: bold;
370
+ cursor: pointer;
371
+ transition: all 0.2s;
372
+ line-height: 1;
373
+ display: flex;
374
+ align-items: center;
375
+ justify-content: center;
376
+ }
377
+
378
+ .close-sidebar-btn:hover {
379
+ background: #ef4444;
380
+ border-color: #ef4444;
381
+ }
382
+
383
+ .main-content {
384
+ margin-left: 260px;
385
+ display: flex;
386
+ flex-direction: column;
387
+ height: 100vh;
388
+ }
389
+
390
+ .toggle-sidebar {
391
+ display: none;
392
+ position: fixed;
393
+ top: 16px;
394
+ left: 16px;
395
+ background: #3a3a3a;
396
+ border: 1px solid #4a4a4a;
397
+ color: #e5e5e5;
398
+ padding: 8px 12px;
399
+ border-radius: 6px;
400
+ cursor: pointer;
401
+ z-index: 5;
402
+ }
403
+
404
+ @media (max-width: 768px) {
405
+ .sidebar {
406
+ transform: translateX(-100%);
407
+ width: 100%;
408
+ max-width: 300px;
409
+ }
410
+
411
+ .sidebar.open {
412
+ transform: translateX(0);
413
+ }
414
+
415
+ .main-content {
416
+ margin-left: 0;
417
+ }
418
+
419
+ .toggle-sidebar {
420
+ display: block;
421
+ }
422
+
423
+ .header {
424
+ padding-left: 60px;
425
+ }
426
+
427
+ .header h1 {
428
+ font-size: 16px;
429
+ }
430
+
431
+ .model-selector label {
432
+ display: none;
433
+ }
434
+
435
+ .search-controls {
436
+ flex-direction: column;
437
+ align-items: flex-start;
438
+ }
439
+
440
+ .search-mode-badge {
441
+ font-size: 10px;
442
+ }
443
+ }
444
+
445
+ .sidebar-overlay {
446
+ display: none;
447
+ position: fixed;
448
+ top: 0;
449
+ left: 0;
450
+ right: 0;
451
+ bottom: 0;
452
+ background: rgba(0,0,0,0.5);
453
+ z-index: 9;
454
+ }
455
+
456
+ .sidebar-overlay.active {
457
+ display: block;
458
+ }
459
+
460
+ .chat-container {
461
+ flex: 1;
462
+ overflow-y: auto;
463
+ padding: 20px;
464
+ padding-bottom: 160px;
465
+ display: flex;
466
+ flex-direction: column;
467
+ gap: 16px;
468
+ }
469
+
470
+ .message {
471
+ display: flex;
472
+ gap: 12px;
473
+ max-width: 800px;
474
+ width: 100%;
475
+ animation: fadeIn 0.3s ease-in;
476
+ }
477
+
478
+ @keyframes fadeIn {
479
+ from {
480
+ opacity: 0;
481
+ transform: translateY(10px);
482
+ }
483
+ to {
484
+ opacity: 1;
485
+ transform: translateY(0);
486
+ }
487
+ }
488
+
489
+ .message.user {
490
+ align-self: flex-end;
491
+ flex-direction: row-reverse;
492
+ }
493
+
494
+ .message.assistant {
495
+ align-self: flex-start;
496
+ }
497
+
498
+ .avatar {
499
+ width: 32px;
500
+ height: 32px;
501
+ border-radius: 50%;
502
+ display: flex;
503
+ align-items: center;
504
+ justify-content: center;
505
+ font-weight: 600;
506
+ font-size: 14px;
507
+ flex-shrink: 0;
508
+ overflow: hidden;
509
+ }
510
+
511
+ .message.user .avatar {
512
+ background: #19c37d;
513
+ color: white;
514
+ border: 2px solid #10a37f;
515
+ box-shadow: 0 0 8px #10a37f;
516
+ }
517
+
518
+ .message-content {
519
+ background: #2a2a2a;
520
+ padding: 12px 16px;
521
+ border-radius: 12px;
522
+ line-height: 1.6;
523
+ color: #e5e5e5;
524
+ box-shadow: 0 1px 2px rgba(0,0,0,0.3);
525
+ word-wrap: break-word;
526
+ }
527
+
528
+ .message.user .message-content {
529
+ background: #4a4a4a;
530
+ color: white;
531
+ }
532
+
533
+ .message.assistant .message-content {
534
+ background: #2a2a2a;
535
+ }
536
+
537
+ .message-content pre {
538
+ background: #1a1a1a;
539
+ padding: 12px;
540
+ border-radius: 6px;
541
+ overflow-x: auto;
542
+ margin: 8px 0;
543
+ border: 1px solid #3a3a3a;
544
+ }
545
+
546
+ .message-content code {
547
+ background: #1a1a1a;
548
+ padding: 2px 6px;
549
+ border-radius: 4px;
550
+ font-family: 'Courier New', monospace;
551
+ font-size: 13px;
552
+ color: #10a37f;
553
+ }
554
+
555
+ .message-content pre code {
556
+ background: transparent;
557
+ padding: 0;
558
+ color: #e5e5e5;
559
+ }
560
+
561
+ .message-content a {
562
+ color: #10a37f;
563
+ text-decoration: none;
564
+ border-bottom: 1px solid transparent;
565
+ transition: border-color 0.2s;
566
+ }
567
+
568
+ .message-content a:hover {
569
+ border-bottom-color: #10a37f;
570
+ }
571
+
572
+ .search-results-preview {
573
+ background: #1a1a1a;
574
+ border: 1px solid #3a3a3a;
575
+ border-radius: 8px;
576
+ padding: 12px;
577
+ margin: 8px 0;
578
+ font-size: 13px;
579
+ }
580
+
581
+ .search-results-preview .result-item {
582
+ padding: 8px 0;
583
+ border-bottom: 1px solid #2a2a2a;
584
+ }
585
+
586
+ .search-results-preview .result-item:last-child {
587
+ border-bottom: none;
588
+ }
589
+
590
+ .search-results-preview .result-title {
591
+ color: #10a37f;
592
+ font-weight: 500;
593
+ margin-bottom: 4px;
594
+ }
595
+
596
+ .search-results-preview .result-snippet {
597
+ color: #9ca3af;
598
+ font-size: 12px;
599
+ line-height: 1.4;
600
+ }
601
+
602
+ .search-results-preview .result-url {
603
+ color: #6a6a6a;
604
+ font-size: 11px;
605
+ margin-top: 4px;
606
+ }
607
+
608
+ .search-results-preview .result-url a {
609
+ color: #6a6a6a;
610
+ }
611
+
612
+ .search-results-preview .result-url a:hover {
613
+ color: #10a37f;
614
+ }
615
+
616
+ .input-container {
617
+ position: fixed;
618
+ bottom: 20px;
619
+ left: 280px;
620
+ right: 20px;
621
+ background: #2a2a2a;
622
+ border: 1px solid #3a3a3a;
623
+ border-radius: 16px;
624
+ padding: 16px 20px;
625
+ box-shadow: 0 4px 20px rgba(0,0,0,0.4);
626
+ z-index: 5;
627
+ }
628
+
629
+ @media (max-width: 768px) {
630
+ .input-container {
631
+ left: 20px;
632
+ }
633
+ }
634
+
635
+ .input-wrapper {
636
+ max-width: 800px;
637
+ margin: 0 auto;
638
+ display: flex;
639
+ gap: 8px;
640
+ flex-wrap: wrap;
641
+ }
642
+
643
+ .search-controls {
644
+ width: 100%;
645
+ display: flex;
646
+ align-items: center;
647
+ justify-content: space-between;
648
+ gap: 12px;
649
+ margin-bottom: 12px;
650
+ }
651
+
652
+ .web-search-toggle {
653
+ display: flex;
654
+ align-items: center;
655
+ gap: 10px;
656
+ padding: 8px 14px;
657
+ background: #3a3a3a;
658
+ border: 1px solid #4a4a4a;
659
+ border-radius: 24px;
660
+ cursor: pointer;
661
+ transition: all 0.2s;
662
+ }
663
+
664
+ .web-search-toggle:hover {
665
+ border-color: #5a5a5a;
666
+ }
667
+
668
+ .web-search-toggle.active {
669
+ background: #10a37f20;
670
+ border-color: #10a37f;
671
+ }
672
+
673
+ .web-search-toggle .toggle-switch {
674
+ width: 36px;
675
+ height: 20px;
676
+ background: #4a4a4a;
677
+ border-radius: 10px;
678
+ position: relative;
679
+ transition: all 0.2s;
680
+ }
681
+
682
+ .web-search-toggle.active .toggle-switch {
683
+ background: #10a37f;
684
+ }
685
+
686
+ .web-search-toggle .toggle-switch::after {
687
+ content: '';
688
+ position: absolute;
689
+ width: 16px;
690
+ height: 16px;
691
+ background: white;
692
+ border-radius: 50%;
693
+ top: 2px;
694
+ left: 2px;
695
+ transition: all 0.2s;
696
+ }
697
+
698
+ .web-search-toggle.active .toggle-switch::after {
699
+ left: 18px;
700
+ }
701
+
702
+ .web-search-toggle .toggle-label {
703
+ font-size: 13px;
704
+ color: #9ca3af;
705
+ font-weight: 500;
706
+ }
707
+
708
+ .web-search-toggle.active .toggle-label {
709
+ color: #10a37f;
710
+ }
711
+
712
+ .web-search-toggle .search-icon {
713
+ font-size: 14px;
714
+ }
715
+
716
+ .search-mode-badge {
717
+ display: none;
718
+ padding: 6px 12px;
719
+ background: #10a37f;
720
+ color: white;
721
+ border-radius: 16px;
722
+ font-size: 11px;
723
+ font-weight: 600;
724
+ text-transform: uppercase;
725
+ letter-spacing: 0.5px;
726
+ }
727
+
728
+ .search-mode-badge.active {
729
+ display: flex;
730
+ align-items: center;
731
+ gap: 6px;
732
+ }
733
+
734
+ .search-indicator {
735
+ display: none;
736
+ align-items: center;
737
+ gap: 8px;
738
+ padding: 10px 16px;
739
+ background: linear-gradient(90deg, #10a37f20, #10a37f10);
740
+ border: 1px solid #10a37f;
741
+ border-radius: 12px;
742
+ font-size: 13px;
743
+ color: #10a37f;
744
+ margin-bottom: 12px;
745
+ animation: pulse-border 1.5s infinite;
746
+ }
747
+
748
+ @keyframes pulse-border {
749
+ 0%, 100% { border-color: #10a37f; }
750
+ 50% { border-color: #10a37f80; }
751
+ }
752
+
753
+ .search-indicator .spinner {
754
+ width: 16px;
755
+ height: 16px;
756
+ border: 2px solid #10a37f40;
757
+ border-top-color: #10a37f;
758
+ border-radius: 50%;
759
+ animation: spin 0.8s linear infinite;
760
+ }
761
+
762
+ @keyframes spin {
763
+ to { transform: rotate(360deg); }
764
+ }
765
+
766
+ .search-indicator.active {
767
+ display: flex;
768
+ }
769
+
770
+ .input-row {
771
+ display: flex;
772
+ gap: 8px;
773
+ flex: 1;
774
+ width: 100%;
775
+ }
776
+
777
+ #messageInput {
778
+ flex: 1;
779
+ padding: 12px 16px;
780
+ border: 1px solid #4a4a4a;
781
+ border-radius: 8px;
782
+ font-size: 14px;
783
+ font-family: inherit;
784
+ outline: none;
785
+ resize: none;
786
+ min-height: 48px;
787
+ max-height: 200px;
788
+ transition: border-color 0.2s;
789
+ background: #3a3a3a;
790
+ color: #e5e5e5;
791
+ }
792
+
793
+ #messageInput:focus {
794
+ border-color: #10a37f;
795
+ }
796
+
797
+ #messageInput:disabled {
798
+ background: #2a2a2a;
799
+ cursor: not-allowed;
800
+ }
801
+
802
+ #messageInput::placeholder {
803
+ color: #6a6a6a;
804
+ }
805
+
806
+ button {
807
+ padding: 12px 20px;
808
+ border: none;
809
+ border-radius: 8px;
810
+ font-size: 14px;
811
+ font-weight: 600;
812
+ cursor: pointer;
813
+ transition: all 0.2s;
814
+ outline: none;
815
+ }
816
+
817
+ #sendBtn {
818
+ background: #10a37f;
819
+ color: white;
820
+ }
821
+
822
+ #sendBtn:hover:not(:disabled) {
823
+ background: #0e8f6f;
824
+ }
825
+
826
+ #sendBtn:disabled {
827
+ background: #d1d5db;
828
+ cursor: not-allowed;
829
+ }
830
+
831
+ #stopBtn {
832
+ background: #ef4444;
833
+ color: white;
834
+ display: none;
835
+ }
836
+
837
+ #stopBtn:hover {
838
+ background: #dc2626;
839
+ }
840
+
841
+ .empty-state {
842
+ text-align: center;
843
+ color: #6a6a6a;
844
+ margin-top: 100px;
845
+ }
846
+
847
+ .empty-state h2 {
848
+ font-size: 24px;
849
+ margin-bottom: 8px;
850
+ color: #e5e5e5;
851
+ }
852
+
853
+ .empty-state p {
854
+ font-size: 14px;
855
+ }
856
+
857
+ .error-message {
858
+ background: #3a1a1a;
859
+ color: #ff6b6b;
860
+ padding: 12px 16px;
861
+ border-radius: 8px;
862
+ margin: 8px 0;
863
+ font-size: 14px;
864
+ border: 1px solid #5a2a2a;
865
+ }
866
+ </style>
867
+
868
+ <link rel="manifest" href="manifest.json">
869
+ <meta name="theme-color" content="#10a37f">
870
+ <link rel="icon" href="icon.svg" type="image/svg+xml">
871
+
872
+ </head>
873
+ <body>
874
+ <button class="toggle-sidebar" id="toggleSidebar">☰</button>
875
+ <div class="sidebar-overlay" id="sidebarOverlay"></div>
876
+
877
+ <div class="sidebar" id="sidebar">
878
+ <div class="sidebar-header">
879
+ <button class="new-chat-btn" id="newChatBtn">+ New Chat</button>
880
+ <button class="close-sidebar-btn" id="closeSidebarBtn">&#10005;</button>
881
+ </div>
882
+ <div class="chat-history" id="chatHistory">
883
+ <!-- Chat history items will be added here -->
884
+ </div>
885
+ <div class="sidebar-footer">
886
+ <button class="clear-all-btn" id="clearAllBtn">Clear All Chats</button>
887
+ </div>
888
+ </div>
889
+
890
+ <div class="main-content">
891
+ <div class="header">
892
+ <h1>Xlnk AI</h1>
893
+ <div class="model-selector">
894
+ <label for="modelSelect">Model:</label>
895
+ <select id="modelSelect">
896
+ <option value="https://xlnk-350m.hf.space/v1/chat/completions">Xlnkai 350M (Fastest)</option>
897
+ <option value="https://xlnk-ai.hf.space/v1/chat/completions" selected>Xlnkai 700M (Balanced)</option>
898
+ <option value="https://xlnk-corelm.hf.space/v1/chat/completions">Xlnkai 3B (Best)</option>
899
+ </select>
900
+ </div>
901
+ </div>
902
+
903
+ <div class="chat-container" id="chatContainer">
904
+ <div class="empty-state">
905
+ <h2>Welcome to Xlnk AI</h2>
906
+ <p>Ask me anything to get started</p>
907
+ </div>
908
+ </div>
909
+
910
+ <div class="input-container">
911
+ <div class="input-wrapper">
912
+ <div class="search-controls">
913
+ <div class="web-search-toggle" id="webSearchToggle">
914
+ <span class="search-icon">&#128269;</span>
915
+ <div class="toggle-switch"></div>
916
+ <span class="toggle-label">Web Search</span>
917
+ </div>
918
+ <div class="search-mode-badge" id="searchModeBadge">
919
+ <span>&#127760;</span> Web Only Mode
920
+ </div>
921
+ </div>
922
+ <div class="search-indicator" id="searchIndicator">
923
+ <div class="spinner"></div>
924
+ <span>Searching the web for real-time information...</span>
925
+ </div>
926
+ <div class="input-row">
927
+ <textarea id="messageInput" placeholder="Type your message..." rows="1"></textarea>
928
+ <button id="sendBtn">Send</button>
929
+ <button id="stopBtn">Stop</button>
930
+ </div>
931
+ </div>
932
+ </div>
933
+ </div>
934
+
935
+ <script>
936
+ const chatContainer = document.getElementById('chatContainer');
937
+ const messageInput = document.getElementById('messageInput');
938
+ const sendBtn = document.getElementById('sendBtn');
939
+ const stopBtn = document.getElementById('stopBtn');
940
+ const modelSelect = document.getElementById('modelSelect');
941
+ const webSearchToggle = document.getElementById('webSearchToggle');
942
+ const searchModeBadge = document.getElementById('searchModeBadge');
943
+ const chatHistoryEl = document.getElementById('chatHistory');
944
+ const newChatBtn = document.getElementById('newChatBtn');
945
+ const toggleSidebarBtn = document.getElementById('toggleSidebar');
946
+ const closeSidebarBtn = document.getElementById('closeSidebarBtn');
947
+ const sidebar = document.getElementById('sidebar');
948
+ const searchIndicator = document.getElementById('searchIndicator');
949
+ const clearAllBtn = document.getElementById('clearAllBtn');
950
+ const sidebarOverlay = document.getElementById('sidebarOverlay');
951
+
952
+ let webSearchEnabled = false;
953
+
954
+ const SYSTEM_PROMPT = "You are Xlnk AI, a fast, lightweight, and helpful AI assistant.\nYou give clear, concise, and accurate answers.\nYou avoid unnecessary verbosity.\nYou explain step by step only when the user asks.\nYou are optimized for low-latency responses on llama.cpp.\nIf unsure, say you are not sure.\nNever hallucinate facts.";
955
+
956
+ const SYSTEM_PROMPT_WITH_SEARCH = `You are Xlnk AI operating in WEB SEARCH ONLY MODE.
957
+
958
+ CRITICAL INSTRUCTIONS:
959
+ - You MUST ONLY use the web search results provided below to answer the user's question.
960
+ - DO NOT use any information from your training data.
961
+ - If the search results don't contain relevant information, clearly state: "The web search did not return relevant results for this query."
962
+ - ALWAYS cite your sources with the exact URL from the search results.
963
+ - Format citations as [Source: URL] at the end of relevant statements.
964
+ - If multiple sources support a fact, cite all of them.
965
+ - Be explicit about what information came from which source.
966
+ - Never make up or hallucinate information not present in the search results.
967
+
968
+ Your response MUST be based EXCLUSIVELY on the provided web search results.`;
969
+
970
+ // Real web search using multiple APIs for best results
971
+ async function performWebSearch(query) {
972
+ let results = [];
973
+
974
+ try {
975
+ // Try Wikipedia API first - most reliable
976
+ const wikiSearchUrl = `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(query)}&format=json&origin=*&srlimit=3`;
977
+ const wikiSearchResponse = await fetch(wikiSearchUrl);
978
+ const wikiSearchData = await wikiSearchResponse.json();
979
+
980
+ if (wikiSearchData.query?.search?.length > 0) {
981
+ for (const result of wikiSearchData.query.search) {
982
+ const pageUrl = `https://en.wikipedia.org/wiki/${encodeURIComponent(result.title.replace(/ /g, '_'))}`;
983
+ results.push({
984
+ title: result.title,
985
+ snippet: result.snippet.replace(/<[^>]*>/g, ''),
986
+ url: pageUrl
987
+ });
988
+ }
989
+ }
990
+
991
+ // Also try to get a summary for the main topic
992
+ const wikiSummaryUrl = `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(query)}`;
993
+ try {
994
+ const wikiResponse = await fetch(wikiSummaryUrl);
995
+ if (wikiResponse.ok) {
996
+ const wikiData = await wikiResponse.json();
997
+ if (wikiData.extract && wikiData.type !== 'disambiguation') {
998
+ // Add as first result if we got a direct match
999
+ results.unshift({
1000
+ title: wikiData.title || query,
1001
+ snippet: wikiData.extract,
1002
+ url: wikiData.content_urls?.desktop?.page || `https://en.wikipedia.org/wiki/${encodeURIComponent(query)}`
1003
+ });
1004
+ }
1005
+ }
1006
+ } catch (e) {
1007
+ // Silently fail
1008
+ }
1009
+
1010
+ // Try DuckDuckGo Instant Answer API as additional source
1011
+ try {
1012
+ const ddgUrl = `https://xlnk-search.hf.space/search?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`;
1013
+ const ddgResponse = await fetch(ddgUrl);
1014
+ const ddgData = await ddgResponse.json();
1015
+
1016
+ if (ddgData.Abstract && ddgData.AbstractURL) {
1017
+ // Check if we don't already have this
1018
+ const exists = results.some(r => r.url === ddgData.AbstractURL);
1019
+ if (!exists) {
1020
+ results.push({
1021
+ title: ddgData.Heading || 'DuckDuckGo Result',
1022
+ snippet: ddgData.Abstract,
1023
+ url: ddgData.AbstractURL
1024
+ });
1025
+ }
1026
+ }
1027
+
1028
+ // Add related topics
1029
+ if (ddgData.RelatedTopics) {
1030
+ for (const topic of ddgData.RelatedTopics.slice(0, 3)) {
1031
+ if (topic.Text && topic.FirstURL) {
1032
+ const exists = results.some(r => r.url === topic.FirstURL);
1033
+ if (!exists) {
1034
+ results.push({
1035
+ title: topic.Text.split(' - ')[0] || 'Related',
1036
+ snippet: topic.Text,
1037
+ url: topic.FirstURL
1038
+ });
1039
+ }
1040
+ }
1041
+ }
1042
+ }
1043
+ } catch (e) {
1044
+ // Silently fail
1045
+ }
1046
+
1047
+ // Limit to 6 results max
1048
+ results = results.slice(0, 6);
1049
+
1050
+ } catch (error) {
1051
+ console.error('Web search error:', error);
1052
+ }
1053
+
1054
+ return results;
1055
+ }
1056
+
1057
+ // Store last search results for display
1058
+ let lastSearchResults = [];
1059
+
1060
+ function formatSearchResults(results) {
1061
+ lastSearchResults = results;
1062
+
1063
+ if (results.length === 0) {
1064
+ return "\n[WEB SEARCH RETURNED NO RESULTS]\nPlease inform the user that no web search results were found and you cannot answer this question in Web Search Only mode.\n";
1065
+ }
1066
+
1067
+ let formatted = "\n========== WEB SEARCH RESULTS (USE ONLY THESE) ==========\n";
1068
+ results.forEach((result, index) => {
1069
+ formatted += `\n--- RESULT ${index + 1} ---\n`;
1070
+ formatted += `Title: ${result.title}\n`;
1071
+ formatted += `Content: ${result.snippet}\n`;
1072
+ if (result.url) {
1073
+ formatted += `URL: ${result.url}\n`;
1074
+ }
1075
+ });
1076
+ formatted += "\n========== END OF WEB SEARCH RESULTS ==========\n\n";
1077
+ formatted += "REMEMBER: Base your answer EXCLUSIVELY on the above search results. Include clickable links using markdown format [text](url). Cite sources.\n\nUser's question: ";
1078
+ return formatted;
1079
+ }
1080
+
1081
+ function renderSearchResultsPreview(results) {
1082
+ if (results.length === 0) return '';
1083
+
1084
+ let html = '<div class="search-results-preview"><strong>Sources found:</strong>';
1085
+ results.forEach((result, index) => {
1086
+ html += `<div class="result-item">`;
1087
+ html += `<div class="result-title">${index + 1}. ${escapeHtml(result.title)}</div>`;
1088
+ html += `<div class="result-url"><a href="${escapeHtml(result.url)}" target="_blank" rel="noopener">${escapeHtml(result.url)}</a></div>`;
1089
+ html += `</div>`;
1090
+ });
1091
+ html += '</div>';
1092
+ return html;
1093
+ }
1094
+
1095
+ let conversationHistory = [];
1096
+ let abortController = null;
1097
+ let isGenerating = false;
1098
+ let chatSessions = [];
1099
+ let currentSessionId = null;
1100
+ let currentAssistantMessageDiv = null;
1101
+
1102
+ // Load chat sessions from localStorage
1103
+ function loadChatSessions() {
1104
+ const saved = localStorage.getItem('xlnkChatSessions');
1105
+ if (saved) {
1106
+ chatSessions = JSON.parse(saved);
1107
+ renderChatHistory();
1108
+ }
1109
+ }
1110
+
1111
+ // Save chat sessions to localStorage
1112
+ function saveChatSessions() {
1113
+ localStorage.setItem('xlnkChatSessions', JSON.stringify(chatSessions));
1114
+ }
1115
+
1116
+ // Create new chat session
1117
+ function createNewSession() {
1118
+ const sessionId = Date.now().toString();
1119
+ const session = {
1120
+ id: sessionId,
1121
+ title: 'New Chat',
1122
+ messages: [],
1123
+ createdAt: Date.now()
1124
+ };
1125
+ chatSessions.unshift(session);
1126
+ currentSessionId = sessionId;
1127
+ conversationHistory = [];
1128
+ clearChat();
1129
+ saveChatSessions();
1130
+ renderChatHistory();
1131
+ }
1132
+
1133
+ // Load a chat session
1134
+ function loadSession(sessionId) {
1135
+ const session = chatSessions.find(s => s.id === sessionId);
1136
+ if (session) {
1137
+ currentSessionId = sessionId;
1138
+ conversationHistory = [...session.messages];
1139
+ clearChat();
1140
+ session.messages.forEach(msg => {
1141
+ addMessage(msg.role, msg.content);
1142
+ });
1143
+ renderChatHistory();
1144
+ }
1145
+ }
1146
+
1147
+ // Update current session
1148
+ function updateCurrentSession() {
1149
+ if (!currentSessionId) {
1150
+ createNewSession();
1151
+ }
1152
+ const session = chatSessions.find(s => s.id === currentSessionId);
1153
+ if (session) {
1154
+ session.messages = [...conversationHistory];
1155
+ if (session.title === 'New Chat' && conversationHistory.length > 0) {
1156
+ const firstUserMsg = conversationHistory.find(m => m.role === 'user');
1157
+ if (firstUserMsg) {
1158
+ session.title = firstUserMsg.content.substring(0, 50) + (firstUserMsg.content.length > 50 ? '...' : '');
1159
+ }
1160
+ }
1161
+ saveChatSessions();
1162
+ renderChatHistory();
1163
+ }
1164
+ }
1165
+
1166
+ // Delete a chat session
1167
+ function deleteSession(sessionId, event) {
1168
+ event.stopPropagation();
1169
+
1170
+ const index = chatSessions.findIndex(s => s.id === sessionId);
1171
+ if (index === -1) return;
1172
+
1173
+ chatSessions.splice(index, 1);
1174
+ saveChatSessions();
1175
+
1176
+ // If we deleted the current session, switch to another or create new
1177
+ if (sessionId === currentSessionId) {
1178
+ if (chatSessions.length > 0) {
1179
+ loadSession(chatSessions[0].id);
1180
+ } else {
1181
+ createNewSession();
1182
+ }
1183
+ } else {
1184
+ renderChatHistory();
1185
+ }
1186
+ }
1187
+
1188
+ // Render chat history sidebar
1189
+ function renderChatHistory() {
1190
+ chatHistoryEl.innerHTML = '';
1191
+ chatSessions.forEach(session => {
1192
+ const item = document.createElement('div');
1193
+ item.className = 'history-item' + (session.id === currentSessionId ? ' active' : '');
1194
+
1195
+ const title = document.createElement('div');
1196
+ title.className = 'history-item-title';
1197
+ title.textContent = session.title;
1198
+
1199
+ const deleteBtn = document.createElement('button');
1200
+ deleteBtn.className = 'delete-chat-btn';
1201
+ deleteBtn.textContent = '🗑️';
1202
+ deleteBtn.onclick = (e) => deleteSession(session.id, e);
1203
+
1204
+ item.appendChild(title);
1205
+ item.appendChild(deleteBtn);
1206
+ item.onclick = () => loadSession(session.id);
1207
+
1208
+ chatHistoryEl.appendChild(item);
1209
+ });
1210
+ }
1211
+
1212
+ // Clear chat display
1213
+ function clearChat() {
1214
+ chatContainer.innerHTML = '<div class="empty-state"><h2>Welcome to Xlnk AI</h2><p>Ask me anything to get started</p></div>';
1215
+ }
1216
+
1217
+ // Toggle sidebar
1218
+ function openSidebar() {
1219
+ sidebar.classList.add('open');
1220
+ sidebarOverlay.classList.add('active');
1221
+ }
1222
+
1223
+ function closeSidebar() {
1224
+ sidebar.classList.remove('open');
1225
+ sidebarOverlay.classList.remove('active');
1226
+ }
1227
+
1228
+ toggleSidebarBtn.addEventListener('click', () => {
1229
+ if (sidebar.classList.contains('open')) {
1230
+ closeSidebar();
1231
+ } else {
1232
+ openSidebar();
1233
+ }
1234
+ });
1235
+
1236
+ // Close sidebar
1237
+ closeSidebarBtn.addEventListener('click', closeSidebar);
1238
+
1239
+ // Close sidebar when clicking overlay
1240
+ sidebarOverlay.addEventListener('click', closeSidebar);
1241
+
1242
+ // Web search toggle
1243
+ webSearchToggle.addEventListener('click', () => {
1244
+ webSearchEnabled = !webSearchEnabled;
1245
+ webSearchToggle.classList.toggle('active', webSearchEnabled);
1246
+ searchModeBadge.classList.toggle('active', webSearchEnabled);
1247
+
1248
+ if (webSearchEnabled) {
1249
+ messageInput.placeholder = "Ask anything - I'll search the web for real-time answers...";
1250
+ } else {
1251
+ messageInput.placeholder = "Type your message...";
1252
+ }
1253
+ });
1254
+
1255
+ // New chat button
1256
+ newChatBtn.addEventListener('click', createNewSession);
1257
+
1258
+ // Clear all chats
1259
+ clearAllBtn.addEventListener('click', () => {
1260
+ if (confirm('Are you sure you want to delete all chat history? This cannot be undone.')) {
1261
+ chatSessions = [];
1262
+ localStorage.removeItem('xlnkChatSessions');
1263
+ createNewSession();
1264
+ }
1265
+ });
1266
+
1267
+ // Initialize
1268
+ loadChatSessions();
1269
+ if (chatSessions.length === 0) {
1270
+ createNewSession();
1271
+ } else {
1272
+ currentSessionId = chatSessions[0].id;
1273
+ renderChatHistory();
1274
+ }
1275
+
1276
+ // Auto-resize textarea
1277
+ messageInput.addEventListener('input', function() {
1278
+ this.style.height = 'auto';
1279
+ this.style.height = Math.min(this.scrollHeight, 200) + 'px';
1280
+ });
1281
+
1282
+ // Send on Enter, new line on Shift+Enter
1283
+ messageInput.addEventListener('keydown', function(e) {
1284
+ if (e.key === 'Enter' && !e.shiftKey) {
1285
+ e.preventDefault();
1286
+ sendMessage();
1287
+ }
1288
+ });
1289
+
1290
+ sendBtn.addEventListener('click', sendMessage);
1291
+ stopBtn.addEventListener('click', stopGeneration);
1292
+
1293
+ function clearEmptyState() {
1294
+ const emptyState = chatContainer.querySelector('.empty-state');
1295
+ if (emptyState) {
1296
+ emptyState.remove();
1297
+ }
1298
+ }
1299
+
1300
+ // Simple markdown renderer with link support
1301
+ function renderMarkdown(text, includeCursor = false) {
1302
+ // Remove existing cursor if present
1303
+ text = text.replace(/<span class="typing-cursor"><img[^>]*><\/span>/g, '');
1304
+
1305
+ // Code blocks
1306
+ text = text.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
1307
+ return `<pre><code>${escapeHtml(code.trim())}</code></pre>`;
1308
+ });
1309
+
1310
+ // Inline code
1311
+ text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
1312
+
1313
+ // Links [text](url)
1314
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
1315
+
1316
+ // Plain URLs - make them clickable
1317
+ text = text.replace(/(https?:\/\/[^\s<]+)/g, (match) => {
1318
+ // Don't double-wrap URLs that are already in links
1319
+ if (text.indexOf(`href="${match}"`) !== -1) return match;
1320
+ return `<a href="${match}" target="_blank" rel="noopener">${match}</a>`;
1321
+ });
1322
+
1323
+ // Bold
1324
+ text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
1325
+
1326
+ // Italic
1327
+ text = text.replace(/\*(.+?)\*/g, '<em>$1</em>');
1328
+
1329
+ // Line breaks
1330
+ text = text.replace(/\n/g, '<br>');
1331
+
1332
+ // Add typing cursor at the end if requested
1333
+ if (includeCursor && isGenerating) {
1334
+ text += '<span class="typing-cursor"><img src="https://me.xo.je/11.webp" alt="Typing cursor"></span>';
1335
+ }
1336
+
1337
+ return text;
1338
+ }
1339
+
1340
+ function escapeHtml(text) {
1341
+ const div = document.createElement('div');
1342
+ div.textContent = text;
1343
+ return div.innerHTML;
1344
+ }
1345
+
1346
+ function addMessage(role, content, isStreaming = false) {
1347
+ clearEmptyState();
1348
+
1349
+ const messageDiv = document.createElement('div');
1350
+ messageDiv.className = `message ${role}`;
1351
+ if (role === 'assistant' && isStreaming) {
1352
+ messageDiv.classList.add('thinking');
1353
+ }
1354
+
1355
+ const avatar = document.createElement('div');
1356
+ avatar.className = 'avatar';
1357
+
1358
+ if (role === 'user') {
1359
+ avatar.textContent = 'U';
1360
+ } else {
1361
+ // Use the provided webp image for AI assistant with green animation
1362
+ const img = document.createElement('img');
1363
+ img.src = 'https://me.xo.je/11.webp';
1364
+ img.alt = 'Xlnk AI';
1365
+ avatar.appendChild(img);
1366
+
1367
+ // Store reference to current assistant message
1368
+ currentAssistantMessageDiv = messageDiv;
1369
+ }
1370
+
1371
+ const contentDiv = document.createElement('div');
1372
+ contentDiv.className = 'message-content';
1373
+
1374
+ if (role === 'assistant') {
1375
+ if (isStreaming) {
1376
+ // Add initial centered typing cursor
1377
+ contentDiv.innerHTML = '<span class="typing-cursor"><img src="https://me.xo.je/11.webp" alt="Typing cursor"></span>';
1378
+ } else {
1379
+ contentDiv.innerHTML = renderMarkdown(content);
1380
+ }
1381
+ } else {
1382
+ contentDiv.textContent = content;
1383
+ }
1384
+
1385
+ messageDiv.appendChild(avatar);
1386
+ messageDiv.appendChild(contentDiv);
1387
+ chatContainer.appendChild(messageDiv);
1388
+
1389
+ scrollToBottom();
1390
+
1391
+ return contentDiv;
1392
+ }
1393
+
1394
+ function scrollToBottom() {
1395
+ chatContainer.scrollTop = chatContainer.scrollHeight;
1396
+ }
1397
+
1398
+ function showError(message) {
1399
+ const errorDiv = document.createElement('div');
1400
+ errorDiv.className = 'error-message';
1401
+ errorDiv.textContent = message;
1402
+ chatContainer.appendChild(errorDiv);
1403
+ scrollToBottom();
1404
+ }
1405
+
1406
+ function updateButtonStates(generating) {
1407
+ isGenerating = generating;
1408
+ sendBtn.disabled = generating;
1409
+ messageInput.disabled = generating;
1410
+ sendBtn.style.display = generating ? 'none' : 'block';
1411
+ stopBtn.style.display = generating ? 'block' : 'none';
1412
+
1413
+ // Update thinking animation
1414
+ if (currentAssistantMessageDiv) {
1415
+ if (generating) {
1416
+ currentAssistantMessageDiv.classList.add('thinking');
1417
+ } else {
1418
+ currentAssistantMessageDiv.classList.remove('thinking');
1419
+ }
1420
+ }
1421
+ }
1422
+
1423
+ function stopGeneration() {
1424
+ if (abortController) {
1425
+ abortController.abort();
1426
+ abortController = null;
1427
+ }
1428
+ updateButtonStates(false);
1429
+ }
1430
+
1431
+ async function sendMessage() {
1432
+ const userMessage = messageInput.value.trim();
1433
+ if (!userMessage || isGenerating) return;
1434
+
1435
+ const useWebSearch = webSearchEnabled;
1436
+
1437
+ // Add user message to UI and history
1438
+ addMessage('user', userMessage);
1439
+ conversationHistory.push({
1440
+ role: 'user',
1441
+ content: userMessage
1442
+ });
1443
+
1444
+ // Clear input
1445
+ messageInput.value = '';
1446
+ messageInput.style.height = 'auto';
1447
+
1448
+ updateButtonStates(true);
1449
+
1450
+ // Create abort controller for this request
1451
+ abortController = new AbortController();
1452
+
1453
+ // Perform web search if enabled
1454
+ let searchResults = '';
1455
+ let searchResultsForDisplay = [];
1456
+ if (useWebSearch) {
1457
+ searchIndicator.classList.add('active');
1458
+ const results = await performWebSearch(userMessage);
1459
+ searchResultsForDisplay = results;
1460
+ searchResults = formatSearchResults(results);
1461
+ searchIndicator.classList.remove('active');
1462
+
1463
+ // Show sources found message
1464
+ if (results.length > 0) {
1465
+ const sourcesDiv = document.createElement('div');
1466
+ sourcesDiv.className = 'message assistant';
1467
+ sourcesDiv.innerHTML = `
1468
+ <div class="avatar"><img src="https://me.xo.je/11.webp" alt="Xlnk AI"></div>
1469
+ <div class="message-content">
1470
+ ${renderSearchResultsPreview(results)}
1471
+ </div>
1472
+ `;
1473
+ clearEmptyState();
1474
+ chatContainer.appendChild(sourcesDiv);
1475
+ scrollToBottom();
1476
+ }
1477
+ }
1478
+
1479
+ // Prepare messages with system prompt
1480
+ const systemPrompt = useWebSearch ? SYSTEM_PROMPT_WITH_SEARCH : SYSTEM_PROMPT;
1481
+ let userContent = userMessage;
1482
+
1483
+ if (useWebSearch) {
1484
+ userContent = searchResults + userMessage;
1485
+ }
1486
+
1487
+ const messages = [
1488
+ { role: 'system', content: systemPrompt },
1489
+ ...conversationHistory.slice(0, -1),
1490
+ { role: 'user', content: userContent }
1491
+ ];
1492
+
1493
+ // Create assistant message container
1494
+ const assistantContent = addMessage('assistant', '', true);
1495
+ let fullResponse = '';
1496
+
1497
+ try {
1498
+ const endpoint = modelSelect.value;
1499
+ const response = await fetch(endpoint, {
1500
+ method: 'POST',
1501
+ headers: {
1502
+ 'Content-Type': 'application/json',
1503
+ },
1504
+ body: JSON.stringify({
1505
+ messages: messages,
1506
+ stream: true,
1507
+ max_tokens: -1,
1508
+ temperature: 0,
1509
+ top_p: 0
1510
+ }),
1511
+ signal: abortController.signal
1512
+ });
1513
+
1514
+ if (!response.ok) {
1515
+ throw new Error(`HTTP error! status: ${response.status}`);
1516
+ }
1517
+
1518
+ const reader = response.body.getReader();
1519
+ const decoder = new TextDecoder();
1520
+
1521
+ while (true) {
1522
+ const { done, value } = await reader.read();
1523
+ if (done) break;
1524
+
1525
+ const chunk = decoder.decode(value, { stream: true });
1526
+ const lines = chunk.split('\n');
1527
+
1528
+ for (const line of lines) {
1529
+ if (line.startsWith('data: ')) {
1530
+ const data = line.slice(6);
1531
+ if (data === '[DONE]') continue;
1532
+
1533
+ try {
1534
+ const parsed = JSON.parse(data);
1535
+ const token = parsed.choices?.[0]?.delta?.content;
1536
+
1537
+ if (token) {
1538
+ fullResponse += token;
1539
+ assistantContent.innerHTML = renderMarkdown(fullResponse, true);
1540
+ scrollToBottom();
1541
+ }
1542
+ } catch (e) {
1543
+ // Skip invalid JSON
1544
+ }
1545
+ }
1546
+ }
1547
+ }
1548
+
1549
+ // Remove cursor when done
1550
+ assistantContent.innerHTML = renderMarkdown(fullResponse, false);
1551
+
1552
+ // Add to conversation history
1553
+ conversationHistory.push({
1554
+ role: 'assistant',
1555
+ content: fullResponse
1556
+ });
1557
+
1558
+ // Update session
1559
+ updateCurrentSession();
1560
+
1561
+ } catch (error) {
1562
+ if (error.name === 'AbortError') {
1563
+ assistantContent.innerHTML = renderMarkdown(fullResponse + ' [Stopped]');
1564
+ conversationHistory.push({
1565
+ role: 'assistant',
1566
+ content: fullResponse
1567
+ });
1568
+ updateCurrentSession();
1569
+ } else {
1570
+ console.error('Error:', error);
1571
+ assistantContent.remove();
1572
+ showError(`Error: ${error.message}. Please try again.`);
1573
+ conversationHistory.pop(); // Remove user message if failed
1574
+ }
1575
+ } finally {
1576
+ updateButtonStates(false);
1577
+ abortController = null;
1578
+ currentAssistantMessageDiv = null;
1579
+ }
1580
+ }
1581
+ </script>
1582
+
1583
+ <script>
1584
+ if ("serviceWorker" in navigator) {
1585
+ window.addEventListener("load", () => {
1586
+ navigator.serviceWorker.register("sw.js");
1587
+ });
1588
+ }
1589
+ </script>
1590
+
1591
+ </body>
1592
+ </html>