nicolaydef commited on
Commit
3455ee3
·
verified ·
1 Parent(s): dceedb1

Update static/script.js

Browse files
Files changed (1) hide show
  1. static/script.js +149 -273
static/script.js CHANGED
@@ -1,338 +1,214 @@
1
  document.addEventListener('DOMContentLoaded', async () => {
2
- // --- STATE ---
3
  let currentUser = null;
4
- let currentChannel = 'general';
5
  let supabase;
6
- let peerConnection;
7
  let ws;
8
- let localStream;
9
  let pingInterval;
10
-
11
- // --- INIT CONFIG ---
12
- const config = await (await fetch('/api/config')).json();
13
- supabase = window.supabase.createClient(config.supabase_url, config.supabase_key);
14
-
15
- // --- UI REFS ---
16
- const loginModal = document.getElementById('login-modal');
17
- const appInterface = document.getElementById('app-interface');
18
- const msgContainer = document.getElementById('messages-container');
19
 
20
- // --- 1. AUTH SYSTEM ---
 
 
 
 
 
 
21
  document.getElementById('login-form').addEventListener('submit', async (e) => {
22
  e.preventDefault();
23
- const username = document.getElementById('login-user').value.trim();
24
- const pass = document.getElementById('login-pass').value.trim();
25
-
26
- // Check DB
27
- const { data: user, error } = await supabase
28
- .from('users')
29
- .select('*')
30
- .eq('username', username)
31
- .single();
32
-
33
- if (error || !user) {
34
- showError("USER NOT FOUND");
35
- return;
36
- }
37
-
38
- // Compare Hash (Client-side verify for simplicity in this demo, usually server-side)
39
- const isValid = dcodeIO.bcrypt.compareSync(pass, user.password_hash);
40
 
41
- if (isValid) {
42
  currentUser = user;
43
- loginModal.classList.remove('open');
44
- appInterface.classList.remove('hidden');
45
  document.getElementById('current-username').textContent = user.username;
 
 
 
 
46
  initApp();
47
  } else {
48
- showError("INVALID CREDENTIALS");
 
49
  }
50
  });
51
 
52
- function showError(msg) {
53
- const err = document.getElementById('login-error');
54
- err.textContent = msg;
55
- err.classList.remove('hidden');
56
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
- // --- 2. ADMIN SYSTEM ---
59
- const adminModal = document.getElementById('admin-modal');
60
- const adminTrigger = document.getElementById('admin-trigger');
61
 
62
- adminTrigger.addEventListener('click', () => {
63
- adminModal.classList.add('open');
64
- document.getElementById('admin-code').focus();
 
 
 
 
 
 
65
  });
66
 
67
- // Check Code Input
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  document.getElementById('admin-code').addEventListener('input', (e) => {
69
- if (e.target.value.length === 4) {
70
- // Check secret with backend (fake check first in UI, logic in backend)
71
- // Just reveal form for UX, real check happens on submit
72
- document.getElementById('admin-step-1').classList.add('hidden');
73
- document.getElementById('admin-create-form').classList.remove('hidden');
74
  }
75
  });
76
 
77
- // Create User
78
  document.getElementById('admin-create-form').addEventListener('submit', async (e) => {
79
  e.preventDefault();
80
  const code = document.getElementById('admin-code').value;
81
  const newU = document.getElementById('new-user').value;
82
  const newP = document.getElementById('new-pass').value;
83
-
84
  try {
85
  const res = await fetch('/api/admin/create_user', {
86
- method: 'POST',
87
- headers: {'Content-Type': 'application/json'},
88
- body: JSON.stringify({ admin_code: code, new_username: newU, new_password: newP })
89
  });
90
-
91
- if (!res.ok) throw new Error("ACCESS DENIED");
92
-
93
  const data = await res.json();
94
 
95
- // Push to Supabase
96
  await supabase.from('users').insert({
97
  username: data.username,
98
  password_hash: data.password_hash,
99
- badge: data.badge,
100
- is_admin: false
101
  });
102
-
103
- alert(`ENTITY ${data.username} CREATED`);
104
- adminModal.classList.remove('open');
105
- // Reset form
106
- document.getElementById('admin-create-form').reset();
107
- document.getElementById('admin-step-1').classList.remove('hidden');
108
- document.getElementById('admin-create-form').classList.add('hidden');
109
- document.getElementById('admin-code').value = '';
110
-
111
- } catch (err) {
112
- alert(err.message);
113
- document.getElementById('admin-code').value = '';
114
- document.getElementById('admin-step-1').classList.remove('hidden');
115
- document.getElementById('admin-create-form').classList.add('hidden');
116
- }
117
  });
118
 
119
- // Close admin modal on click outside
120
- adminModal.addEventListener('click', (e) => {
121
- if(e.target === adminModal) adminModal.classList.remove('open');
122
- });
123
-
124
- // --- 3. APP LOGIC ---
125
  function initApp() {
126
- subscribeToMessages();
127
- loadUsers();
128
- }
129
-
130
- async function subscribeToMessages() {
131
- // Load history
132
- const { data } = await supabase.from('messages').select('*').order('created_at', { ascending: true });
133
- data.forEach(renderMessage);
134
 
135
- // Realtime
136
  supabase.channel('public:messages')
137
  .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages' }, payload => {
138
- renderMessage(payload.new);
139
  })
140
  .subscribe();
 
 
 
141
  }
142
 
143
- async function loadUsers() {
144
- const list = document.getElementById('users-list');
145
- const { data } = await supabase.from('users').select('*');
 
 
 
146
 
147
- list.innerHTML = '';
148
- data.forEach(u => {
149
- if (u.username === currentUser.username) return;
150
- const div = document.createElement('div');
151
- div.className = 'channel-item text-sm opacity-70 hover:opacity-100';
152
- div.innerHTML = `
153
- <div class="w-2 h-2 rounded-full bg-gray-500 mr-2"></div>
154
- ${u.username} ${u.badge === 'BETA' ? '<span class="text-yellow-500 text-[10px] ml-1">★</span>' : ''}
155
- `;
156
- list.appendChild(div);
157
- });
158
  }
159
 
160
- // --- 4. MESSAGING ---
161
- const form = document.getElementById('chat-form');
162
- form.addEventListener('submit', async (e) => {
163
  e.preventDefault();
164
- const input = document.getElementById('msg-input');
165
- const text = input.value.trim();
166
- if (!text) return;
167
- input.value = '';
168
 
169
- // Mood Analysis
170
  const moodRes = await fetch('/api/analyze_mood', {
171
- method: 'POST',
172
- headers: {'Content-Type': 'application/json'},
173
- body: JSON.stringify({text})
174
  });
175
  const mood = await moodRes.json();
176
 
177
- // Save
178
  await supabase.from('messages').insert({
179
- content: text,
180
- user_id: currentUser.id,
181
- username: currentUser.username,
182
- mood_color: mood.mood_color,
183
- channel_id: currentChannel
184
  });
185
  });
186
 
187
  function renderMessage(msg) {
188
  const div = document.createElement('div');
189
-
190
- if (msg.is_system) {
191
- div.className = 'message w-full justify-center my-2';
192
- div.innerHTML = `<div class="system">🤖 CLEAN: ${msg.content}</div>`;
193
- } else {
194
- const isOwn = msg.username === currentUser.username;
195
- div.className = `message ${isOwn ? 'flex-row-reverse self-end' : ''}`;
196
-
197
- // Badge Logic
198
- // Note: In real app we fetch user badge, here we assume all are beta for demo visual
199
- const badgeHtml = `<span class="badge-beta">BETA</span>`;
200
-
201
- div.innerHTML = `
202
- <div class="flex flex-col ${isOwn ? 'items-end' : 'items-start'}">
203
- <div class="text-[10px] text-white/40 mb-1 px-1">
204
- ${msg.username} ${badgeHtml}
205
- </div>
206
- <div class="message-content" style="border-left: 3px solid ${msg.mood_color || '#fff'}">
207
- ${msg.content}
208
- </div>
209
- </div>
210
- `;
211
- }
212
- msgContainer.appendChild(div);
213
- msgContainer.scrollTop = msgContainer.scrollHeight;
214
- }
215
-
216
- // --- 5. WEBRTC PRO (Discord style) ---
217
- const voiceBtn = document.getElementById('voice-toggle');
218
- const voiceUI = document.getElementById('voice-ui');
219
- const voiceDot = document.getElementById('voice-dot');
220
- const voiceText = document.getElementById('voice-text');
221
- const voicePing = document.getElementById('voice-ping');
222
- let isVoiceActive = false;
223
-
224
- voiceBtn.addEventListener('click', async () => {
225
- if (isVoiceActive) disconnectVoice();
226
- else await connectVoice();
227
- });
228
-
229
- async function connectVoice() {
230
- isVoiceActive = true;
231
- voiceUI.classList.remove('hidden');
232
- updateVoiceStatus('connecting', 'Connecting to RTC...');
233
-
234
- try {
235
- localStream = await navigator.mediaDevices.getUserMedia({ audio: true });
236
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
237
- ws = new WebSocket(`${protocol}//${window.location.host}/ws/signal`);
238
-
239
- peerConnection = new RTCPeerConnection({
240
- iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
241
- });
242
-
243
- // Add Tracks
244
- localStream.getTracks().forEach(t => peerConnection.addTrack(t, localStream));
245
-
246
- // On Track
247
- peerConnection.ontrack = e => {
248
- const audio = new Audio();
249
- audio.srcObject = e.streams[0];
250
- audio.play();
251
- };
252
-
253
- // Signaling
254
- ws.onmessage = async (e) => {
255
- const data = JSON.parse(e.data);
256
- if (data.type === 'offer') {
257
- await peerConnection.setRemoteDescription(data);
258
- const answer = await peerConnection.createAnswer();
259
- await peerConnection.setLocalDescription(answer);
260
- ws.send(JSON.stringify(peerConnection.localDescription));
261
- } else if (data.type === 'answer') {
262
- await peerConnection.setRemoteDescription(data);
263
- } else if (data.candidate) {
264
- await peerConnection.addIceCandidate(data.candidate);
265
- }
266
- };
267
-
268
- peerConnection.oniceconnectionstatechange = () => {
269
- const state = peerConnection.iceConnectionState;
270
- if (state === 'connected' || state === 'completed') {
271
- updateVoiceStatus('connected', 'Voice Connected');
272
- startPingStats();
273
- } else if (state === 'failed' || state === 'disconnected') {
274
- botCleanReport("Connection lost to RTC Server.");
275
- disconnectVoice();
276
- }
277
- };
278
-
279
- ws.onopen = async () => {
280
- const offer = await peerConnection.createOffer();
281
- await peerConnection.setLocalDescription(offer);
282
- ws.send(JSON.stringify(offer));
283
- };
284
-
285
- } catch (e) {
286
- console.error(e);
287
- botCleanReport("Failed to initialize audio device.");
288
- disconnectVoice();
289
- }
290
- }
291
-
292
- function disconnectVoice() {
293
- isVoiceActive = false;
294
- voiceUI.classList.add('hidden');
295
- if (peerConnection) peerConnection.close();
296
- if (ws) ws.close();
297
- if (localStream) localStream.getTracks().forEach(t => t.stop());
298
- clearInterval(pingInterval);
299
- voiceBtn.classList.remove('bg-red-500/20');
300
- }
301
-
302
- function updateVoiceStatus(state, text) {
303
- voiceDot.className = `status-dot ${state}`;
304
- voiceText.textContent = text;
305
- if (state === 'connected') {
306
- voiceBtn.classList.add('bg-green-500/20');
307
- document.getElementById('mic-icon').classList.add('text-green-400');
308
- }
309
- }
310
-
311
- // --- PING SYSTEM ---
312
- function startPingStats() {
313
- pingInterval = setInterval(async () => {
314
- if (!peerConnection) return;
315
- const stats = await peerConnection.getStats();
316
- let rtt = 0;
317
- stats.forEach(report => {
318
- if (report.type === 'candidate-pair' && report.state === 'succeeded') {
319
- rtt = report.currentRoundTripTime * 1000; // ms
320
- }
321
- });
322
- if (rtt > 0) {
323
- voicePing.textContent = `${Math.round(rtt)}ms`;
324
- if (rtt > 200) voicePing.style.color = 'red';
325
- else voicePing.style.color = 'lime';
326
- }
327
- }, 1000);
328
- }
329
-
330
- // --- BOT CLEAN ---
331
- async function botCleanReport(errorMsg) {
332
- await supabase.from('messages').insert({
333
- content: `ERR_RTC: ${errorMsg}`,
334
- is_system: true,
335
- channel_id: currentChannel
336
- });
337
  }
338
  });
 
1
  document.addEventListener('DOMContentLoaded', async () => {
 
2
  let currentUser = null;
 
3
  let supabase;
 
4
  let ws;
 
5
  let pingInterval;
 
 
 
 
 
 
 
 
 
6
 
7
+ // --- 0. INIT ---
8
+ try {
9
+ const config = await (await fetch('/api/config')).json();
10
+ supabase = window.supabase.createClient(config.supabase_url, config.supabase_key);
11
+ } catch(e) { console.error("Config failed", e); }
12
+
13
+ // --- 1. LOGIN ---
14
  document.getElementById('login-form').addEventListener('submit', async (e) => {
15
  e.preventDefault();
16
+ const u = document.getElementById('login-user').value.trim();
17
+ const p = document.getElementById('login-pass').value.trim();
18
+
19
+ const { data: user } = await supabase.from('users').select('*').eq('username', u).single();
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
+ if (user && dcodeIO.bcrypt.compareSync(p, user.password_hash)) {
22
  currentUser = user;
23
+ document.getElementById('login-modal').classList.remove('open');
24
+ document.getElementById('app-interface').classList.remove('hidden');
25
  document.getElementById('current-username').textContent = user.username;
26
+
27
+ // Инициализация чекбокса настроек
28
+ document.getElementById('setting-discovery').checked = user.is_looking_for_friends || false;
29
+
30
  initApp();
31
  } else {
32
+ const err = document.getElementById('login-error');
33
+ err.textContent = "INVALID ACCESS"; err.classList.remove('hidden');
34
  }
35
  });
36
 
37
+ // --- 2. TABS LOGIC ---
38
+ const navBtns = document.querySelectorAll('.nav-btn[data-tab]');
39
+ const viewChats = document.getElementById('view-chats');
40
+ const viewDiscovery = document.getElementById('view-discovery');
41
+ const sidebarTitle = document.getElementById('sidebar-title');
42
+
43
+ navBtns.forEach(btn => {
44
+ btn.addEventListener('click', () => {
45
+ // Remove active class
46
+ navBtns.forEach(b => b.classList.remove('active'));
47
+ btn.classList.add('active');
48
+
49
+ const tab = btn.dataset.tab;
50
+ if (tab === 'chats') {
51
+ viewChats.classList.remove('hidden');
52
+ viewDiscovery.classList.add('hidden');
53
+ sidebarTitle.textContent = "CHANNELS";
54
+ } else if (tab === 'discovery') {
55
+ viewChats.classList.add('hidden');
56
+ viewDiscovery.classList.remove('hidden');
57
+ sidebarTitle.textContent = "FIND FRIENDS";
58
+ loadDiscoveryUsers();
59
+ }
60
+ });
61
+ });
62
 
63
+ // --- 3. DISCOVERY & SETTINGS ---
64
+ const settingsModal = document.getElementById('settings-modal');
65
+ document.getElementById('open-settings').addEventListener('click', () => settingsModal.classList.add('open'));
66
 
67
+ document.getElementById('save-settings').addEventListener('click', async () => {
68
+ const isLooking = document.getElementById('setting-discovery').checked;
69
+
70
+ // Update DB
71
+ await supabase.from('users').update({ is_looking_for_friends: isLooking }).eq('id', currentUser.id);
72
+ currentUser.is_looking_for_friends = isLooking; // local update
73
+
74
+ settingsModal.classList.remove('open');
75
+ if(isLooking) alert("You are now visible in Discovery!");
76
  });
77
 
78
+ async function loadDiscoveryUsers() {
79
+ const list = document.getElementById('discovery-list');
80
+ list.innerHTML = '<div class="text-white/30 text-xs italic">Scanning sector...</div>';
81
+
82
+ const { data: users } = await supabase
83
+ .from('users')
84
+ .select('*')
85
+ .eq('is_looking_for_friends', true)
86
+ .neq('username', currentUser.username); // Exclude self
87
+
88
+ list.innerHTML = '';
89
+ if (!users || users.length === 0) {
90
+ list.innerHTML = '<div class="text-white/30 text-xs p-2">No signals found.</div>';
91
+ return;
92
+ }
93
+
94
+ users.forEach(u => {
95
+ const div = document.createElement('div');
96
+ div.className = 'flex items-center justify-between p-2 bg-white/5 rounded hover:bg-white/10 transition cursor-pointer';
97
+ div.innerHTML = `
98
+ <div class="flex items-center gap-2">
99
+ <div class="w-6 h-6 rounded-full bg-gray-500"></div>
100
+ <span class="text-sm font-bold">${u.username}</span>
101
+ </div>
102
+ <button class="text-[10px] border border-cyan-400 text-cyan-400 px-2 py-1 rounded hover:bg-cyan-400 hover:text-black">
103
+ CONNECT
104
+ </button>
105
+ `;
106
+ // Logic for "Connect" -> Create DM channel would go here
107
+ list.appendChild(div);
108
+ });
109
+ }
110
+
111
+ // --- 4. ADMIN & CHAT ---
112
+ // (Admin Logic code from previous answer remains similar but linked to new modals)
113
+ document.getElementById('admin-trigger').addEventListener('click', () => {
114
+ document.getElementById('admin-modal').classList.add('open');
115
+ });
116
+
117
  document.getElementById('admin-code').addEventListener('input', (e) => {
118
+ if(e.target.value.length >= 4) {
119
+ document.getElementById('admin-create-form').classList.remove('hidden');
 
 
 
120
  }
121
  });
122
 
 
123
  document.getElementById('admin-create-form').addEventListener('submit', async (e) => {
124
  e.preventDefault();
125
  const code = document.getElementById('admin-code').value;
126
  const newU = document.getElementById('new-user').value;
127
  const newP = document.getElementById('new-pass').value;
128
+
129
  try {
130
  const res = await fetch('/api/admin/create_user', {
131
+ method: 'POST', headers: {'Content-Type': 'application/json'},
132
+ body: JSON.stringify({admin_code: code, new_username: newU, new_password: newP})
 
133
  });
134
+ if(!res.ok) throw new Error("DENIED");
 
 
135
  const data = await res.json();
136
 
 
137
  await supabase.from('users').insert({
138
  username: data.username,
139
  password_hash: data.password_hash,
140
+ badge: "BETA"
 
141
  });
142
+ alert("CREATED");
143
+ document.getElementById('admin-modal').classList.remove('open');
144
+ } catch(e) { alert("ACCESS DENIED"); }
 
 
 
 
 
 
 
 
 
 
 
 
145
  });
146
 
147
+ // --- 5. APP INIT & WEBSOCKET FIX ---
 
 
 
 
 
148
  function initApp() {
149
+ const msgContainer = document.getElementById('messages-container');
150
+
151
+ // Load Chat History
152
+ supabase.from('messages').select('*').eq('channel_id', 'general').order('created_at')
153
+ .then(({data}) => { if(data) data.forEach(renderMessage); });
 
 
 
154
 
155
+ // Realtime Subscription
156
  supabase.channel('public:messages')
157
  .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages' }, payload => {
158
+ if(payload.new.channel_id === 'general') renderMessage(payload.new);
159
  })
160
  .subscribe();
161
+
162
+ // WebSocket for Voice/Signaling
163
+ connectWebSocket();
164
  }
165
 
166
+ function connectWebSocket() {
167
+ // Secure WebSocket handling for HF Spaces
168
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
169
+ // Remove trailing slash if present
170
+ const host = window.location.host;
171
+ const wsUrl = `${protocol}//${host}/ws/signal`;
172
 
173
+ console.log("Attempting WS connect to:", wsUrl);
174
+
175
+ ws = new WebSocket(wsUrl);
176
+ ws.onopen = () => console.log("Signal Line Established");
177
+ ws.onerror = (e) => console.log("Signal Error (Ignore if not using Voice)", e);
 
 
 
 
 
 
178
  }
179
 
180
+ const chatForm = document.getElementById('chat-form');
181
+ chatForm.addEventListener('submit', async (e) => {
 
182
  e.preventDefault();
183
+ const inp = document.getElementById('msg-input');
184
+ const text = inp.value.trim();
185
+ if(!text) return;
186
+ inp.value = '';
187
 
 
188
  const moodRes = await fetch('/api/analyze_mood', {
189
+ method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({text})
 
 
190
  });
191
  const mood = await moodRes.json();
192
 
 
193
  await supabase.from('messages').insert({
194
+ content: text, user_id: currentUser.id, username: currentUser.username,
195
+ mood_color: mood.mood_color, channel_id: 'general'
 
 
 
196
  });
197
  });
198
 
199
  function renderMessage(msg) {
200
  const div = document.createElement('div');
201
+ const isOwn = msg.username === currentUser.username;
202
+ div.className = `flex gap-2 mb-4 ${isOwn ? 'flex-row-reverse' : ''} px-4`;
203
+ div.innerHTML = `
204
+ <div class="bg-white/5 border border-white/5 p-3 rounded-xl max-w-[80%] text-sm"
205
+ style="border-left: 3px solid ${msg.mood_color}">
206
+ <div class="text-[10px] text-white/40 mb-1">${msg.username}</div>
207
+ ${msg.content}
208
+ </div>
209
+ `;
210
+ const c = document.getElementById('messages-container');
211
+ c.appendChild(div);
212
+ c.scrollTop = c.scrollHeight;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  }
214
  });