Pomilon commited on
Commit
7599a27
·
verified ·
1 Parent(s): 65407be

Create static/js/app.js

Browse files
Files changed (1) hide show
  1. static/js/app.js +163 -0
static/js/app.js ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const chatWindow = document.getElementById('chat-window');
3
+ const inputArea = document.querySelector('textarea');
4
+ const sendBtn = document.querySelector('#send-btn');
5
+ const stopBtn = document.querySelector('#stop-btn');
6
+
7
+ // Auto-focus input on load
8
+ if(inputArea) inputArea.focus();
9
+
10
+ // Determine current model from body class
11
+ const isCRSM = document.body.classList.contains('crsm-theme');
12
+ const modelName = isCRSM ? 'crsm' : 'aetheris';
13
+
14
+ let controller = null;
15
+
16
+ // --- Auto-resize Textarea ---
17
+ if (inputArea) {
18
+ inputArea.addEventListener('input', function() {
19
+ this.style.height = 'auto';
20
+ this.style.height = Math.min(this.scrollHeight, 200) + 'px';
21
+ });
22
+
23
+ inputArea.addEventListener('keydown', (e) => {
24
+ if (e.key === 'Enter' && !e.shiftKey) {
25
+ e.preventDefault();
26
+ sendMessage();
27
+ }
28
+ });
29
+ }
30
+
31
+ if (sendBtn) sendBtn.addEventListener('click', sendMessage);
32
+
33
+ if (stopBtn) {
34
+ stopBtn.addEventListener('click', () => {
35
+ if (controller) {
36
+ controller.abort();
37
+ controller = null;
38
+ endGenerationState();
39
+ appendSystemNote('Generation stopped by user.');
40
+ }
41
+ });
42
+ }
43
+
44
+ async function sendMessage() {
45
+ const text = inputArea.value.trim();
46
+ if (!text) return;
47
+
48
+ // Reset UI
49
+ inputArea.value = '';
50
+ inputArea.style.height = 'auto';
51
+
52
+ // 1. Add User Message
53
+ appendMessage('user', text);
54
+
55
+ // 2. Prepare UI for Bot
56
+ startGenerationState();
57
+
58
+ // 3. Add Placeholder Bubble
59
+ const { row, bubble } = createMessageRow('bot');
60
+ chatWindow.appendChild(row);
61
+
62
+ // Add "Thinking" State
63
+ const thinkingIndicator = document.createElement('div');
64
+ thinkingIndicator.className = 'thinking-indicator';
65
+ thinkingIndicator.innerHTML = `<span></span><span></span><span></span>`;
66
+ bubble.appendChild(thinkingIndicator);
67
+ scrollToBottom();
68
+
69
+ try {
70
+ controller = new AbortController();
71
+ const response = await fetch('/api/generate', {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json' },
74
+ body: JSON.stringify({ model: modelName, prompt: text }),
75
+ signal: controller.signal
76
+ });
77
+
78
+ // Remove thinking indicator immediately on first byte
79
+ thinkingIndicator.remove();
80
+
81
+ const reader = response.body.getReader();
82
+ const decoder = new TextDecoder();
83
+ let rawText = '';
84
+
85
+ while (true) {
86
+ const { value, done } = await reader.read();
87
+ if (done) break;
88
+
89
+ const chunk = decoder.decode(value, { stream: true });
90
+ rawText += chunk;
91
+
92
+ // Use Marked.js for parsing if available, else fallback to text
93
+ bubble.innerHTML = window.marked ? window.marked.parse(rawText) : rawText;
94
+
95
+ scrollToBottom();
96
+ }
97
+
98
+ } catch (error) {
99
+ thinkingIndicator.remove();
100
+ if (error.name !== 'AbortError') {
101
+ console.error(error);
102
+ bubble.innerText = "Error: Connection to the neural interface failed.";
103
+ bubble.classList.add('error');
104
+ }
105
+ } finally {
106
+ controller = null;
107
+ endGenerationState();
108
+ }
109
+ }
110
+
111
+ // --- Helpers ---
112
+
113
+ function createMessageRow(role) {
114
+ const row = document.createElement('div');
115
+ row.className = `message-row ${role} animate-slide-in`;
116
+
117
+ const avatar = document.createElement('div');
118
+ avatar.className = `avatar ${role}`;
119
+ if(role === 'user') {
120
+ avatar.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>`;
121
+ }
122
+
123
+ const bubble = document.createElement('div');
124
+ bubble.className = 'bubble prose'; // 'prose' for markdown styling
125
+
126
+ row.appendChild(avatar);
127
+ row.appendChild(bubble);
128
+
129
+ return { row, bubble };
130
+ }
131
+
132
+ function appendMessage(role, text) {
133
+ const { row, bubble } = createMessageRow(role);
134
+ bubble.innerText = text; // User text is plain text
135
+ chatWindow.appendChild(row);
136
+ scrollToBottom();
137
+ }
138
+
139
+ function appendSystemNote(text) {
140
+ const note = document.createElement('div');
141
+ note.className = 'system-note';
142
+ note.innerText = text;
143
+ chatWindow.appendChild(note);
144
+ scrollToBottom();
145
+ }
146
+
147
+ function scrollToBottom() {
148
+ chatWindow.scrollTop = chatWindow.scrollHeight;
149
+ }
150
+
151
+ function startGenerationState() {
152
+ if(sendBtn) sendBtn.classList.add('hidden');
153
+ if(stopBtn) stopBtn.classList.remove('hidden');
154
+ inputArea.disabled = true;
155
+ }
156
+
157
+ function endGenerationState() {
158
+ if(sendBtn) sendBtn.classList.remove('hidden');
159
+ if(stopBtn) stopBtn.classList.add('hidden');
160
+ inputArea.disabled = false;
161
+ setTimeout(() => inputArea.focus(), 100);
162
+ }
163
+ });