brk9999 commited on
Commit
b7057ba
·
verified ·
1 Parent(s): 209006b

Upload folder using huggingface_hub

Browse files
Files changed (9) hide show
  1. web/app.py +8 -0
  2. web/js/api.js +3 -8
  3. web/js/chat.js +26 -275
  4. web/js/entry-gate.js +60 -0
  5. web/js/feedback.js +122 -0
  6. web/js/main.js +2 -61
  7. web/js/state.js +16 -0
  8. web/js/ui.js +111 -0
  9. web/js/utils.js +25 -0
web/app.py CHANGED
@@ -19,6 +19,14 @@ Prerequisites:
19
  =============================================================
20
  """
21
 
 
 
 
 
 
 
 
 
22
  # Import routes module to register all routes
23
  from routes import pages, auth, health, chat
24
 
 
19
  =============================================================
20
  """
21
 
22
+ import os
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ ROOT_DIR = Path(__file__).resolve().parent.parent
27
+ if str(ROOT_DIR) not in sys.path:
28
+ sys.path.insert(0, str(ROOT_DIR))
29
+
30
  # Import routes module to register all routes
31
  from routes import pages, auth, health, chat
32
 
web/js/api.js CHANGED
@@ -1,17 +1,12 @@
 
 
 
1
  export const API_BASE = window.location.origin + '/api';
2
- export const SESSION_ID = 'session_' + Math.random().toString(36).slice(2, 9);
3
 
4
  const fingerprintPromise = import('/vendor/fingerprintjs/fp.esm.js').then((FingerprintJS) =>
5
  FingerprintJS.load({ monitoring: false })
6
  );
7
 
8
- export const quotaInfo = {
9
- daily_used: 0,
10
- minute_used: 0,
11
- daily_limit: 40,
12
- minute_limit: 5,
13
- };
14
-
15
  let cachedFingerprint = '';
16
 
17
  export function setQuotaFromStatus(data) {
 
1
+ import { SESSION_ID, quotaInfo } from './state.js';
2
+
3
+ export { SESSION_ID };
4
  export const API_BASE = window.location.origin + '/api';
 
5
 
6
  const fingerprintPromise = import('/vendor/fingerprintjs/fp.esm.js').then((FingerprintJS) =>
7
  FingerprintJS.load({ monitoring: false })
8
  );
9
 
 
 
 
 
 
 
 
10
  let cachedFingerprint = '';
11
 
12
  export function setQuotaFromStatus(data) {
web/js/chat.js CHANGED
@@ -1,287 +1,36 @@
1
- import { API_BASE, SESSION_ID, apiFetch, buildApiHeaders, loadAuthStatus, quotaInfo, setQuotaFromStatus } from './api.js';
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- const messagesEl = document.getElementById('messages');
4
  const inputEl = document.getElementById('user-input');
5
  const sendBtn = document.getElementById('send-btn');
6
  const charCount = document.getElementById('char-count');
7
- const noticeLayer = document.getElementById('notice-layer');
8
-
9
- let isStreaming = false;
10
- let lastQuestionIndex = null;
11
- let limitNoticeVisible = false;
12
- let congestionNoticeVisible = false;
13
- const notifiedFallbackModels = new Set();
14
-
15
- function scrollToBottom() {
16
- if (messagesEl) {
17
- messagesEl.scrollTop = messagesEl.scrollHeight;
18
- }
19
- }
20
 
21
  function updateQuotaDisplay() {
22
  const dailyRemaining = Math.max(quotaInfo.daily_limit - quotaInfo.daily_used, 0);
23
  const minuteRemaining = Math.max(quotaInfo.minute_limit - quotaInfo.minute_used, 0);
24
 
25
  if (dailyRemaining <= 10 || minuteRemaining <= 1) {
26
- showLimitNotice(dailyRemaining);
27
- }
28
- }
29
-
30
- function showLimitNotice(remaining = null) {
31
- if (!noticeLayer || limitNoticeVisible) return;
32
-
33
- limitNoticeVisible = true;
34
- noticeLayer.innerHTML = '';
35
-
36
- const pill = document.createElement('div');
37
- pill.className = 'notice-pill';
38
- const countText = Number.isFinite(remaining) ? ` ${remaining} günlük hakkın kaldı.` : '';
39
- pill.innerHTML = `
40
- <span>Hakkın az kaldı.${countText}</span>
41
- <button class="notice-close" type="button" aria-label="Kapat">×</button>
42
- `;
43
-
44
- pill.querySelector('.notice-close')?.addEventListener('click', () => {
45
- limitNoticeVisible = false;
46
- pill.remove();
47
- });
48
-
49
- noticeLayer.appendChild(pill);
50
- }
51
-
52
- function showCongestionNotice() {
53
- if (!noticeLayer || congestionNoticeVisible) return;
54
-
55
- congestionNoticeVisible = true;
56
- const pill = document.createElement('div');
57
- pill.className = 'notice-pill';
58
- pill.innerHTML = `
59
- <span>⚠️ Şu anda yoğunluk var, yanıtlar normalden geç gelebilir.</span>
60
- <button class="notice-close" type="button" aria-label="Kapat">×</button>
61
- `;
62
-
63
- pill.querySelector('.notice-close')?.addEventListener('click', () => {
64
- congestionNoticeVisible = false;
65
- pill.remove();
66
- });
67
-
68
- noticeLayer.appendChild(pill);
69
- }
70
-
71
- function hideCongestionNotice() {
72
- if (!noticeLayer || !congestionNoticeVisible) return;
73
-
74
- congestionNoticeVisible = false;
75
- const pills = noticeLayer.querySelectorAll('.notice-pill');
76
- pills.forEach((pill) => {
77
- if (pill.textContent.includes('yoğunluk')) pill.remove();
78
- });
79
- }
80
-
81
- function showModelFallbackNotice(notice) {
82
- if (!noticeLayer) return;
83
-
84
- const targetModel = notice?.to_model || 'unknown';
85
- if (notifiedFallbackModels.has(targetModel)) return;
86
- notifiedFallbackModels.add(targetModel);
87
-
88
- const pill = document.createElement('div');
89
- pill.className = 'notice-pill';
90
- pill.innerHTML = `
91
- <span>Yoğunluk nedeniyle farklı bir model kullanılıyor.</span>
92
- <button class="notice-close" type="button" aria-label="Kapat">×</button>
93
- `;
94
-
95
- noticeLayer.appendChild(pill);
96
- }
97
-
98
- export function appendMessage(role, text) {
99
- document.getElementById('welcome-msg')?.remove();
100
-
101
- const wrap = document.createElement('div');
102
- wrap.className = `msg-wrap ${role}`;
103
-
104
- const bubble = document.createElement('div');
105
- bubble.className = 'bubble';
106
- bubble.innerHTML = formatText(text);
107
-
108
- wrap.appendChild(bubble);
109
- messagesEl?.appendChild(wrap);
110
- scrollToBottom();
111
- return bubble;
112
- }
113
-
114
- function appendTypingIndicator() {
115
- document.getElementById('welcome-msg')?.remove();
116
-
117
- const wrap = document.createElement('div');
118
- wrap.className = 'msg-wrap bot';
119
- wrap.id = 'typing-wrap';
120
-
121
- const indicator = document.createElement('div');
122
- indicator.className = 'typing-indicator';
123
- indicator.innerHTML = '<span></span><span></span><span></span>';
124
-
125
- wrap.appendChild(indicator);
126
- messagesEl?.appendChild(wrap);
127
- scrollToBottom();
128
- return wrap;
129
- }
130
-
131
- function formatText(text) {
132
- return text
133
- .replace(/&/g, '&amp;')
134
- .replace(/</g, '&lt;')
135
- .replace(/>/g, '&gt;')
136
- .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
137
- .replace(/\*(.+?)\*/g, '<em>$1</em>')
138
- .replace(/`(.+?)`/g, '<code>$1</code>')
139
- .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
140
- .replace(/^#{1,3}\s+(.+)$/gm, '<strong>$1</strong>')
141
- .replace(/^[-•]\s+(.+)$/gm, '• $1')
142
- .replace(/\n\n/g, '</p><p>')
143
- .replace(/\n/g, '<br>')
144
- .replace(/^/, '<p>')
145
- .replace(/$/, '</p>');
146
- }
147
-
148
- function stripReasoningText(text) {
149
- return text
150
- .replace(/<think\b[^>]*>[\s\S]*?<\/think>/gi, '')
151
- .replace(/<thinking\b[^>]*>[\s\S]*?<\/thinking>/gi, '')
152
- .replace(/<think\b[^>]*>[\s\S]*$/gi, '')
153
- .replace(/<thinking\b[^>]*>[\s\S]*$/gi, '')
154
- .trim();
155
- }
156
-
157
- function sendFeedback(questionIndex, type) {
158
- const btns = document.querySelectorAll(`[data-qidx="${questionIndex}"] .fb-btn`);
159
- btns.forEach((button) => {
160
- const feedbackType = button.dataset.fbType;
161
- button.classList.toggle('fb-active', feedbackType === type);
162
- });
163
-
164
- fetch(`${API_BASE}/chat/feedback`, {
165
- method: 'POST',
166
- credentials: 'same-origin',
167
- headers: { 'Content-Type': 'application/json' },
168
- body: JSON.stringify({ question_index: questionIndex, feedback: type }),
169
- }).catch(() => {});
170
- }
171
-
172
- function toggleFeedbackBox(questionIndex) {
173
- const box = document.querySelector(`[data-qidx="${questionIndex}"] .fb-text-box`);
174
- if (!box) return;
175
-
176
- const isVisible = box.style.display === 'flex';
177
- box.style.display = isVisible ? 'none' : 'flex';
178
-
179
- if (!isVisible) {
180
- const textarea = box.querySelector('textarea');
181
- if (textarea) {
182
- textarea.value = '';
183
- textarea.focus();
184
- }
185
  }
186
  }
187
 
188
- function submitFeedbackText(questionIndex) {
189
- const box = document.querySelector(`[data-qidx="${questionIndex}"] .fb-text-box`);
190
- if (!box) return;
191
-
192
- const textarea = box.querySelector('textarea');
193
- const text = textarea ? textarea.value.trim() : '';
194
- if (!text) {
195
- toggleFeedbackBox(questionIndex);
196
- return;
197
- }
198
-
199
- fetch(`${API_BASE}/chat/feedback`, {
200
- method: 'POST',
201
- credentials: 'same-origin',
202
- headers: { 'Content-Type': 'application/json' },
203
- body: JSON.stringify({ question_index: questionIndex, feedback_text: text }),
204
- }).catch(() => {});
205
-
206
- box.style.display = 'none';
207
-
208
- const bar = box.closest('.fb-bar');
209
- if (bar) {
210
- const existing = bar.querySelector('.fb-thanks');
211
- if (existing) existing.remove();
212
-
213
- const thanks = document.createElement('div');
214
- thanks.className = 'fb-thanks';
215
- thanks.textContent = 'Geri bildiriminiz için teşekkür ederiz.';
216
- bar.appendChild(thanks);
217
- setTimeout(() => {
218
- thanks.remove();
219
- }, 2500);
220
- }
221
- }
222
-
223
- function createFeedbackBar(questionIndex) {
224
- const bar = document.createElement('div');
225
- bar.className = 'fb-bar';
226
- bar.dataset.qidx = questionIndex;
227
-
228
- const likeBtn = document.createElement('button');
229
- likeBtn.className = 'fb-btn';
230
- likeBtn.dataset.fbType = 'like';
231
- likeBtn.title = 'Yararlı';
232
- likeBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3H14zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/></svg>`;
233
- likeBtn.addEventListener('click', () => sendFeedback(questionIndex, 'like'));
234
-
235
- const dislikeBtn = document.createElement('button');
236
- dislikeBtn.className = 'fb-btn';
237
- dislikeBtn.dataset.fbType = 'dislike';
238
- dislikeBtn.title = 'Yanlış veya yetersiz';
239
- dislikeBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V7H7.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3H10zM17 2h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-3"/></svg>`;
240
- dislikeBtn.addEventListener('click', () => sendFeedback(questionIndex, 'dislike'));
241
-
242
- const chatBtn = document.createElement('button');
243
- chatBtn.className = 'fb-btn';
244
- chatBtn.title = 'Geri bildirim yaz';
245
- chatBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`;
246
- chatBtn.addEventListener('click', () => toggleFeedbackBox(questionIndex));
247
-
248
- bar.appendChild(likeBtn);
249
- bar.appendChild(dislikeBtn);
250
- bar.appendChild(chatBtn);
251
-
252
- const textBox = document.createElement('div');
253
- textBox.className = 'fb-text-box';
254
- textBox.style.display = 'none';
255
- textBox.innerHTML = `
256
- <textarea class="fb-textarea" placeholder="Geri bildiriminizi yazın..." rows="2" maxlength="500"></textarea>
257
- <div class="fb-text-actions">
258
- <button class="fb-text-cancel" type="button">İptal</button>
259
- <button class="fb-text-send" type="button">Gönder</button>
260
- </div>
261
- `;
262
-
263
- textBox.querySelector('.fb-text-send')?.addEventListener('click', () => submitFeedbackText(questionIndex));
264
- textBox.querySelector('.fb-text-cancel')?.addEventListener('click', () => {
265
- textBox.style.display = 'none';
266
- });
267
- textBox.querySelector('textarea')?.addEventListener('keydown', (event) => {
268
- if (event.key === 'Enter' && !event.shiftKey) {
269
- event.preventDefault();
270
- submitFeedbackText(questionIndex);
271
- }
272
- });
273
-
274
- bar.appendChild(textBox);
275
- return bar;
276
- }
277
-
278
  export async function sendMessage() {
279
  if (!inputEl || !sendBtn || !charCount) return;
280
 
281
  const message = inputEl.value.trim();
282
- if (!message || isStreaming) return;
283
 
284
- isStreaming = true;
285
  sendBtn.disabled = true;
286
  inputEl.value = '';
287
  inputEl.style.height = 'auto';
@@ -300,7 +49,7 @@ export async function sendMessage() {
300
 
301
  if (!response.ok) {
302
  const data = await response.json().catch(() => ({}));
303
- if (data.near_limit) showLimitNotice();
304
  const errorType = data.error_type || 'technical';
305
  const userMsg = errorType === 'quota' ? (data.error || 'Limit aşıldı.') : 'Teknik bir sorun oluştu. Lütfen daha sonra tekrar deneyin.';
306
  throw new Error(userMsg);
@@ -317,9 +66,12 @@ export async function sendMessage() {
317
  bubble.innerHTML = '<p></p>';
318
 
319
  wrap.appendChild(bubble);
 
320
  messagesEl?.appendChild(wrap);
321
 
322
- const reader = response.body.getReader();
 
 
323
  const decoder = new TextDecoder();
324
  let rawText = '';
325
  let sseBuffer = '';
@@ -346,17 +98,16 @@ export async function sendMessage() {
346
  }
347
 
348
  if (data.congestion) {
349
- showCongestionNotice();
350
  }
351
 
352
  if (data.model_fallback) {
353
- showModelFallbackNotice(data.model_fallback);
354
  }
355
 
356
  if (data.done && data.question_index) {
357
- lastQuestionIndex = data.question_index;
358
- const feedbackBar = createFeedbackBar(lastQuestionIndex);
359
- wrap.appendChild(feedbackBar);
360
  }
361
  } catch {
362
  // Ignore incomplete or malformed SSE fragments.
@@ -383,7 +134,6 @@ export async function sendMessage() {
383
 
384
  if (!sseHadError) {
385
  await loadAuthStatus();
386
- setQuotaFromStatus(quotaInfo);
387
  updateQuotaDisplay();
388
  }
389
  } catch (err) {
@@ -392,7 +142,7 @@ export async function sendMessage() {
392
  appendMessage('bot', errorText);
393
  }
394
 
395
- isStreaming = false;
396
  sendBtn.disabled = false;
397
  inputEl?.focus();
398
  scrollToBottom();
@@ -438,6 +188,7 @@ export async function clearChat() {
438
  // Continue silently even if the reset request fails.
439
  }
440
 
 
441
  if (messagesEl) {
442
  messagesEl.innerHTML = welcomeMarkup(
443
  'Sohbet sıfırlandı',
 
1
+ import { API_BASE, buildApiHeaders, loadAuthStatus } from './api.js';
2
+ import { SESSION_ID, appState, quotaInfo } from './state.js';
3
+ import {
4
+ appendMessage,
5
+ appendTypingIndicator,
6
+ scrollToBottom,
7
+ showCongestionNotice,
8
+ showLimitNotice,
9
+ showModelFallbackNotice,
10
+ } from './ui.js';
11
+ import { createFeedbackBar } from './feedback.js';
12
+ import { formatText, stripReasoningText } from './utils.js';
13
 
 
14
  const inputEl = document.getElementById('user-input');
15
  const sendBtn = document.getElementById('send-btn');
16
  const charCount = document.getElementById('char-count');
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  function updateQuotaDisplay() {
19
  const dailyRemaining = Math.max(quotaInfo.daily_limit - quotaInfo.daily_used, 0);
20
  const minuteRemaining = Math.max(quotaInfo.minute_limit - quotaInfo.minute_used, 0);
21
 
22
  if (dailyRemaining <= 10 || minuteRemaining <= 1) {
23
+ showLimitNotice(dailyRemaining, appState);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  }
25
  }
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  export async function sendMessage() {
28
  if (!inputEl || !sendBtn || !charCount) return;
29
 
30
  const message = inputEl.value.trim();
31
+ if (!message || appState.isStreaming) return;
32
 
33
+ appState.isStreaming = true;
34
  sendBtn.disabled = true;
35
  inputEl.value = '';
36
  inputEl.style.height = 'auto';
 
49
 
50
  if (!response.ok) {
51
  const data = await response.json().catch(() => ({}));
52
+ if (data.near_limit) showLimitNotice(null, appState);
53
  const errorType = data.error_type || 'technical';
54
  const userMsg = errorType === 'quota' ? (data.error || 'Limit aşıldı.') : 'Teknik bir sorun oluştu. Lütfen daha sonra tekrar deneyin.';
55
  throw new Error(userMsg);
 
66
  bubble.innerHTML = '<p></p>';
67
 
68
  wrap.appendChild(bubble);
69
+ const messagesEl = document.getElementById('messages');
70
  messagesEl?.appendChild(wrap);
71
 
72
+ const reader = response.body?.getReader();
73
+ if (!reader) throw new Error('Akış yanıtı desteklenmiyor.');
74
+
75
  const decoder = new TextDecoder();
76
  let rawText = '';
77
  let sseBuffer = '';
 
98
  }
99
 
100
  if (data.congestion) {
101
+ showCongestionNotice(appState);
102
  }
103
 
104
  if (data.model_fallback) {
105
+ showModelFallbackNotice(data.model_fallback, appState);
106
  }
107
 
108
  if (data.done && data.question_index) {
109
+ appState.lastQuestionIndex = data.question_index;
110
+ wrap.appendChild(createFeedbackBar(appState.lastQuestionIndex));
 
111
  }
112
  } catch {
113
  // Ignore incomplete or malformed SSE fragments.
 
134
 
135
  if (!sseHadError) {
136
  await loadAuthStatus();
 
137
  updateQuotaDisplay();
138
  }
139
  } catch (err) {
 
142
  appendMessage('bot', errorText);
143
  }
144
 
145
+ appState.isStreaming = false;
146
  sendBtn.disabled = false;
147
  inputEl?.focus();
148
  scrollToBottom();
 
188
  // Continue silently even if the reset request fails.
189
  }
190
 
191
+ const messagesEl = document.getElementById('messages');
192
  if (messagesEl) {
193
  messagesEl.innerHTML = welcomeMarkup(
194
  'Sohbet sıfırlandı',
web/js/entry-gate.js ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const ENTRY_ACCEPTED_KEY = 'bal_asistan_entry_accepted_v1';
2
+
3
+ export function initializeEntryGate() {
4
+ const gateTabs = document.querySelectorAll('.gate-tab');
5
+ const gateSections = document.querySelectorAll('.gate-section');
6
+ const gateAgree = document.getElementById('gate-agree');
7
+ const gateContinue = document.getElementById('gate-continue');
8
+ const gateStatus = document.getElementById('gate-status');
9
+ const inputEl = document.getElementById('user-input');
10
+
11
+ const visitedGateTabs = {
12
+ terms: true,
13
+ about: false,
14
+ };
15
+
16
+ function openGateTab(tabName) {
17
+ visitedGateTabs[tabName] = true;
18
+
19
+ gateTabs.forEach((tab) => {
20
+ const isActive = tab.dataset.tab === tabName;
21
+ tab.classList.toggle('active', isActive);
22
+ tab.classList.toggle('visited', visitedGateTabs[tab.dataset.tab]);
23
+ tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
24
+ });
25
+
26
+ gateSections.forEach((section) => {
27
+ section.classList.toggle('active', section.id === `panel-${tabName}`);
28
+ });
29
+
30
+ updateGateContinueState();
31
+ }
32
+
33
+ function updateGateContinueState() {
34
+ if (!gateAgree || !gateContinue || !gateStatus) return;
35
+
36
+ const canContinue = gateAgree.checked;
37
+ gateContinue.disabled = !canContinue;
38
+ gateStatus.textContent = canContinue
39
+ ? 'Hazır. Devam ederek BAL Asistanı açabilirsin.'
40
+ : 'Devam etmek için onay kutusunu işaretle.';
41
+ }
42
+
43
+ function enterChat() {
44
+ document.body.classList.remove('gate-active');
45
+ inputEl?.focus();
46
+ }
47
+
48
+ localStorage.removeItem(ENTRY_ACCEPTED_KEY);
49
+
50
+ gateTabs.forEach((tab) => {
51
+ tab.addEventListener('click', () => openGateTab(tab.dataset.tab));
52
+ });
53
+
54
+ gateAgree?.addEventListener('change', updateGateContinueState);
55
+ gateContinue?.addEventListener('click', () => {
56
+ if (!gateContinue.disabled) enterChat();
57
+ });
58
+
59
+ updateGateContinueState();
60
+ }
web/js/feedback.js ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { API_BASE } from './api.js';
2
+
3
+ export function sendFeedback(questionIndex, type) {
4
+ const btns = document.querySelectorAll(`[data-qidx="${questionIndex}"] .fb-btn`);
5
+ btns.forEach((button) => {
6
+ const feedbackType = button.dataset.fbType;
7
+ button.classList.toggle('fb-active', feedbackType === type);
8
+ });
9
+
10
+ fetch(`${API_BASE}/chat/feedback`, {
11
+ method: 'POST',
12
+ credentials: 'same-origin',
13
+ headers: { 'Content-Type': 'application/json' },
14
+ body: JSON.stringify({ question_index: questionIndex, feedback: type }),
15
+ }).catch(() => {});
16
+ }
17
+
18
+ export function toggleFeedbackBox(questionIndex) {
19
+ const box = document.querySelector(`[data-qidx="${questionIndex}"] .fb-text-box`);
20
+ if (!box) return;
21
+
22
+ const isVisible = box.style.display === 'flex';
23
+ box.style.display = isVisible ? 'none' : 'flex';
24
+
25
+ if (!isVisible) {
26
+ const textarea = box.querySelector('textarea');
27
+ if (textarea) {
28
+ textarea.value = '';
29
+ textarea.focus();
30
+ }
31
+ }
32
+ }
33
+
34
+ export function submitFeedbackText(questionIndex) {
35
+ const box = document.querySelector(`[data-qidx="${questionIndex}"] .fb-text-box`);
36
+ if (!box) return;
37
+
38
+ const textarea = box.querySelector('textarea');
39
+ const text = textarea ? textarea.value.trim() : '';
40
+ if (!text) {
41
+ toggleFeedbackBox(questionIndex);
42
+ return;
43
+ }
44
+
45
+ fetch(`${API_BASE}/chat/feedback`, {
46
+ method: 'POST',
47
+ credentials: 'same-origin',
48
+ headers: { 'Content-Type': 'application/json' },
49
+ body: JSON.stringify({ question_index: questionIndex, feedback_text: text }),
50
+ }).catch(() => {});
51
+
52
+ box.style.display = 'none';
53
+
54
+ const bar = box.closest('.fb-bar');
55
+ if (bar) {
56
+ const existing = bar.querySelector('.fb-thanks');
57
+ if (existing) existing.remove();
58
+
59
+ const thanks = document.createElement('div');
60
+ thanks.className = 'fb-thanks';
61
+ thanks.textContent = 'Geri bildiriminiz için teşekkür ederiz.';
62
+ bar.appendChild(thanks);
63
+ setTimeout(() => {
64
+ thanks.remove();
65
+ }, 2500);
66
+ }
67
+ }
68
+
69
+ export function createFeedbackBar(questionIndex) {
70
+ const bar = document.createElement('div');
71
+ bar.className = 'fb-bar';
72
+ bar.dataset.qidx = questionIndex;
73
+
74
+ const likeBtn = document.createElement('button');
75
+ likeBtn.className = 'fb-btn';
76
+ likeBtn.dataset.fbType = 'like';
77
+ likeBtn.title = 'Yararlı';
78
+ likeBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3H14zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/></svg>`;
79
+ likeBtn.addEventListener('click', () => sendFeedback(questionIndex, 'like'));
80
+
81
+ const dislikeBtn = document.createElement('button');
82
+ dislikeBtn.className = 'fb-btn';
83
+ dislikeBtn.dataset.fbType = 'dislike';
84
+ dislikeBtn.title = 'Yanlış veya yetersiz';
85
+ dislikeBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V7H7.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3H10zM17 2h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-3"/></svg>`;
86
+ dislikeBtn.addEventListener('click', () => sendFeedback(questionIndex, 'dislike'));
87
+
88
+ const chatBtn = document.createElement('button');
89
+ chatBtn.className = 'fb-btn';
90
+ chatBtn.title = 'Geri bildirim yaz';
91
+ chatBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`;
92
+ chatBtn.addEventListener('click', () => toggleFeedbackBox(questionIndex));
93
+
94
+ bar.appendChild(likeBtn);
95
+ bar.appendChild(dislikeBtn);
96
+ bar.appendChild(chatBtn);
97
+
98
+ const textBox = document.createElement('div');
99
+ textBox.className = 'fb-text-box';
100
+ textBox.style.display = 'none';
101
+ textBox.innerHTML = `
102
+ <textarea class="fb-textarea" placeholder="Geri bildiriminizi yazın..." rows="2" maxlength="500"></textarea>
103
+ <div class="fb-text-actions">
104
+ <button class="fb-text-cancel" type="button">İptal</button>
105
+ <button class="fb-text-send" type="button">Gönder</button>
106
+ </div>
107
+ `;
108
+
109
+ textBox.querySelector('.fb-text-send')?.addEventListener('click', () => submitFeedbackText(questionIndex));
110
+ textBox.querySelector('.fb-text-cancel')?.addEventListener('click', () => {
111
+ textBox.style.display = 'none';
112
+ });
113
+ textBox.querySelector('textarea')?.addEventListener('keydown', (event) => {
114
+ if (event.key === 'Enter' && !event.shiftKey) {
115
+ event.preventDefault();
116
+ submitFeedbackText(questionIndex);
117
+ }
118
+ });
119
+
120
+ bar.appendChild(textBox);
121
+ return bar;
122
+ }
web/js/main.js CHANGED
@@ -1,68 +1,9 @@
1
  import { checkHealth, loadAuthStatus } from './api.js';
2
  import { clearChat, initializeChatApp, sendMessage, sendSuggestion } from './chat.js';
3
-
4
- const ENTRY_ACCEPTED_KEY = 'bal_asistan_entry_accepted_v1';
5
- const gateTabs = document.querySelectorAll('.gate-tab');
6
- const gateSections = document.querySelectorAll('.gate-section');
7
- const gateAgree = document.getElementById('gate-agree');
8
- const gateContinue = document.getElementById('gate-continue');
9
- const gateStatus = document.getElementById('gate-status');
10
- const inputEl = document.getElementById('user-input');
11
-
12
- const visitedGateTabs = {
13
- terms: true,
14
- about: false,
15
- };
16
-
17
- function openGateTab(tabName) {
18
- visitedGateTabs[tabName] = true;
19
-
20
- gateTabs.forEach((tab) => {
21
- const isActive = tab.dataset.tab === tabName;
22
- tab.classList.toggle('active', isActive);
23
- tab.classList.toggle('visited', visitedGateTabs[tab.dataset.tab]);
24
- tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
25
- });
26
-
27
- gateSections.forEach((section) => {
28
- section.classList.toggle('active', section.id === `panel-${tabName}`);
29
- });
30
-
31
- updateGateContinueState();
32
- }
33
-
34
- function updateGateContinueState() {
35
- if (!gateAgree || !gateContinue || !gateStatus) return;
36
-
37
- const canContinue = gateAgree.checked;
38
- gateContinue.disabled = !canContinue;
39
- gateStatus.textContent = canContinue
40
- ? 'Hazır. Devam ederek BAL Asistanı açabilirsin.'
41
- : 'Devam etmek için onay kutusunu işaretle.';
42
- }
43
-
44
- function enterChat() {
45
- document.body.classList.remove('gate-active');
46
- inputEl?.focus();
47
- }
48
-
49
- function initEntryGate() {
50
- localStorage.removeItem(ENTRY_ACCEPTED_KEY);
51
-
52
- gateTabs.forEach((tab) => {
53
- tab.addEventListener('click', () => openGateTab(tab.dataset.tab));
54
- });
55
-
56
- gateAgree?.addEventListener('change', updateGateContinueState);
57
- gateContinue?.addEventListener('click', () => {
58
- if (!gateContinue.disabled) enterChat();
59
- });
60
-
61
- updateGateContinueState();
62
- }
63
 
64
  function bootstrap() {
65
- initEntryGate();
66
  initializeChatApp();
67
 
68
  window.sendSuggestion = sendSuggestion;
 
1
  import { checkHealth, loadAuthStatus } from './api.js';
2
  import { clearChat, initializeChatApp, sendMessage, sendSuggestion } from './chat.js';
3
+ import { initializeEntryGate } from './entry-gate.js';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  function bootstrap() {
6
+ initializeEntryGate();
7
  initializeChatApp();
8
 
9
  window.sendSuggestion = sendSuggestion;
web/js/state.js ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const SESSION_ID = 'session_' + Math.random().toString(36).slice(2, 9);
2
+
3
+ export const quotaInfo = {
4
+ daily_used: 0,
5
+ minute_used: 0,
6
+ daily_limit: 40,
7
+ minute_limit: 5,
8
+ };
9
+
10
+ export const appState = {
11
+ isStreaming: false,
12
+ lastQuestionIndex: null,
13
+ limitNoticeVisible: false,
14
+ congestionNoticeVisible: false,
15
+ notifiedFallbackModels: new Set(),
16
+ };
web/js/ui.js ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { formatText } from './utils.js';
2
+
3
+ const messagesEl = document.getElementById('messages');
4
+ const noticeLayer = document.getElementById('notice-layer');
5
+
6
+ export function scrollToBottom() {
7
+ if (messagesEl) {
8
+ messagesEl.scrollTop = messagesEl.scrollHeight;
9
+ }
10
+ }
11
+
12
+ export function appendMessage(role, text) {
13
+ document.getElementById('welcome-msg')?.remove();
14
+
15
+ const wrap = document.createElement('div');
16
+ wrap.className = `msg-wrap ${role}`;
17
+
18
+ const bubble = document.createElement('div');
19
+ bubble.className = 'bubble';
20
+ bubble.innerHTML = formatText(text);
21
+
22
+ wrap.appendChild(bubble);
23
+ messagesEl?.appendChild(wrap);
24
+ scrollToBottom();
25
+ return bubble;
26
+ }
27
+
28
+ export function appendTypingIndicator() {
29
+ document.getElementById('welcome-msg')?.remove();
30
+
31
+ const wrap = document.createElement('div');
32
+ wrap.className = 'msg-wrap bot';
33
+ wrap.id = 'typing-wrap';
34
+
35
+ const indicator = document.createElement('div');
36
+ indicator.className = 'typing-indicator';
37
+ indicator.innerHTML = '<span></span><span></span><span></span>';
38
+
39
+ wrap.appendChild(indicator);
40
+ messagesEl?.appendChild(wrap);
41
+ scrollToBottom();
42
+ return wrap;
43
+ }
44
+
45
+ export function showLimitNotice(remaining = null, stateRef) {
46
+ if (!noticeLayer || stateRef.limitNoticeVisible) return;
47
+
48
+ stateRef.limitNoticeVisible = true;
49
+ noticeLayer.innerHTML = '';
50
+
51
+ const pill = document.createElement('div');
52
+ pill.className = 'notice-pill';
53
+ const countText = Number.isFinite(remaining) ? ` ${remaining} günlük hakkın kaldı.` : '';
54
+ pill.innerHTML = `
55
+ <span>Hakkın az kaldı.${countText}</span>
56
+ <button class="notice-close" type="button" aria-label="Kapat">×</button>
57
+ `;
58
+
59
+ pill.querySelector('.notice-close')?.addEventListener('click', () => {
60
+ stateRef.limitNoticeVisible = false;
61
+ pill.remove();
62
+ });
63
+
64
+ noticeLayer.appendChild(pill);
65
+ }
66
+
67
+ export function showCongestionNotice(stateRef) {
68
+ if (!noticeLayer || stateRef.congestionNoticeVisible) return;
69
+
70
+ stateRef.congestionNoticeVisible = true;
71
+ const pill = document.createElement('div');
72
+ pill.className = 'notice-pill';
73
+ pill.innerHTML = `
74
+ <span>⚠️ Şu anda yoğunluk var, yanıtlar normalden geç gelebilir.</span>
75
+ <button class="notice-close" type="button" aria-label="Kapat">×</button>
76
+ `;
77
+
78
+ pill.querySelector('.notice-close')?.addEventListener('click', () => {
79
+ stateRef.congestionNoticeVisible = false;
80
+ pill.remove();
81
+ });
82
+
83
+ noticeLayer.appendChild(pill);
84
+ }
85
+
86
+ export function hideCongestionNotice(stateRef) {
87
+ if (!noticeLayer || !stateRef.congestionNoticeVisible) return;
88
+
89
+ stateRef.congestionNoticeVisible = false;
90
+ const pills = noticeLayer.querySelectorAll('.notice-pill');
91
+ pills.forEach((pill) => {
92
+ if (pill.textContent.includes('yoğunluk')) pill.remove();
93
+ });
94
+ }
95
+
96
+ export function showModelFallbackNotice(notice, stateRef) {
97
+ if (!noticeLayer) return;
98
+
99
+ const targetModel = notice?.to_model || 'unknown';
100
+ if (stateRef.notifiedFallbackModels.has(targetModel)) return;
101
+ stateRef.notifiedFallbackModels.add(targetModel);
102
+
103
+ const pill = document.createElement('div');
104
+ pill.className = 'notice-pill';
105
+ pill.innerHTML = `
106
+ <span>Yoğunluk nedeniyle farklı bir model kullanılıyor.</span>
107
+ <button class="notice-close" type="button" aria-label="Kapat">×</button>
108
+ `;
109
+
110
+ noticeLayer.appendChild(pill);
111
+ }
web/js/utils.js ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function formatText(text) {
2
+ return text
3
+ .replace(/&/g, '&amp;')
4
+ .replace(/</g, '&lt;')
5
+ .replace(/>/g, '&gt;')
6
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
7
+ .replace(/\*(.+?)\*/g, '<em>$1</em>')
8
+ .replace(/`(.+?)`/g, '<code>$1</code>')
9
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
10
+ .replace(/^#{1,3}\s+(.+)$/gm, '<strong>$1</strong>')
11
+ .replace(/^[-•]\s+(.+)$/gm, '• $1')
12
+ .replace(/\n\n/g, '</p><p>')
13
+ .replace(/\n/g, '<br>')
14
+ .replace(/^/, '<p>')
15
+ .replace(/$/, '</p>');
16
+ }
17
+
18
+ export function stripReasoningText(text) {
19
+ return text
20
+ .replace(/<think\b[^>]*>[\s\S]*?<\/think>/gi, '')
21
+ .replace(/<thinking\b[^>]*>[\s\S]*?<\/thinking>/gi, '')
22
+ .replace(/<think\b[^>]*>[\s\S]*$/gi, '')
23
+ .replace(/<thinking\b[^>]*>[\s\S]*$/gi, '')
24
+ .trim();
25
+ }