PageSourceSearch

https://latex2web.app/assets/controllers/chat_controller-b9fUbuw.js

js latex2web.app collected 2026-10-04 14:35:02 UTC 16,253 bytes, 419 lines download raw bytes

1import { Controller } from '@hotwired/stimulus';
2import { marked } from 'marked';
3
4export default class extends Controller {
5    static targets = ['pastList', 'history', 'input', 'vendorSelect', 'modelSelect', 'personaSelect'];
6    static values = {
7        vendor: String,
8        currentConversationId: Number
9    };
10
11    connect() {
12        console.log('Chat controller connected');
13        
14        // Restore from sessionStorage
15        const savedVendor = sessionStorage.getItem('ai_chat_vendor');
16        const savedModel = sessionStorage.getItem('ai_chat_model');
17        const savedPersona = sessionStorage.getItem('ai_chat_persona');
18        const savedConvId = sessionStorage.getItem('ai_chat_conversation_id');
19
20        if (savedVendor) {
21            this.vendorValue = savedVendor;
22            if (this.hasVendorSelectTarget) {
23                this.vendorSelectTarget.value = savedVendor;
24            }
25        }
26
27        console.log('Vendor:', this.vendorValue);
28        
29        // Initial load of models for the current vendor
30        this.loadModels(this.vendorValue).then(() => {
31            if (savedModel && this.hasModelSelectTarget) {
32                this.modelSelectTarget.value = savedModel;
33            }
34        });
35
36        // Load personas
37        this.loadPersonas().then(() => {
38            if (savedPersona && this.hasPersonaSelectTarget) {
39                this.personaSelectTarget.value = savedPersona;
40            }
41        });
42
43        if (savedConvId && savedConvId !== '0') {
44            this.loadConversation(this.vendorValue, savedConvId);
45        }
46        
47        // Configure marked with highlight.js if available
48        if (window.hljs) {
49            const renderer = new marked.Renderer();
50            renderer.code = ({ text, lang }) => {
51                const validLang = !!(lang && hljs.getLanguage(lang));
52                const highlighted = validLang
53                    ? hljs.highlight(text, { language: lang }).value
54                    : hljs.highlightAuto(text).value;
55                return `<pre><code class="hljs ${validLang ? `language-${lang}` : ''}">${highlighted}</code></pre>`;
56            };
57            marked.setOptions({ renderer });
58        }
59        
60        // Auto-grow textarea
61        this.inputTarget.addEventListener('input', () => {
62            this.inputTarget.style.height = 'auto';
63            this.inputTarget.style.height = (this.inputTarget.scrollHeight) + 'px';
64        });
65
66        // Render existing assistant messages as markdown
67        this.historyTarget.querySelectorAll('.ai-message .ai-chat-content').forEach(el => {
68            const content = el.textContent.trim();
69            // Check if it's already HTML (e.g. from server persistence) or needs marked
70            // el.textContent might be HTML if it was rendered by Twig, but actually 
71            // the Twig template just outputs {{ message.content }}
72            // If it's already HTML, don't parse it with marked.
73            if (!/<[a-z][\s\S]*>/i.test(el.innerHTML)) {
74                el.innerHTML = marked.parse(content);
75            }
76            
77            // Highlight code blocks
78            if (window.hljs) {
79                el.querySelectorAll('pre code').forEach((block) => {
80                    hljs.highlightElement(block);
81                });
82            }
83        });
84    }
85
86    togglePastConversations(event) {
87        event.preventDefault();
88        const rootId = event.params.id;
89        console.log(`Toggle past conversations (Root ID: ${rootId})`);
90        
91        if (this.pastListTarget.classList.contains('d-none')) {
92            this.loadHistoryList(rootId);
93        }
94        
95        this.pastListTarget.classList.toggle('d-none');
96    }
97
98    loadHistoryList(rootId) {
99        // Fetch global history regardless of current vendor
100        fetch(`/ai/chat/all/history`)
101            .then(response => response.json())
102            .then(data => {
103                if (data.status === 'success') {
104                    this.pastListTarget.innerHTML = '';
105                    if (data.conversations.length === 0) {
106                        const li = document.createElement('li');
107                        li.className = 'list-group-item';
108                        li.textContent = 'No past conversations.';
109                        this.pastListTarget.appendChild(li);
110                    } else {
111                        data.conversations.forEach(conv => {
112                            const li = document.createElement('li');
113                            li.className = 'list-group-item list-group-item-action';
114                            li.style.cursor = 'pointer';
115                            li.textContent = conv.title;
116                            li.setAttribute('data-action', 'click->chat#selectConversation:params');
117                            li.setAttribute('data-chat-vendor-param', conv.vendor);
118                            li.setAttribute('data-chat-conversation-id-p
118aram', conv.id);
119                            li.setAttribute('data-chat-id-param', rootId);
120                            this.pastListTarget.appendChild(li);
121                        });
122                    }
123                }
124            });
125    }
126
127    sendRequest(event) {
128        event.preventDefault();
129        const vendor = this.vendorValue;
130        const rootId = event.params.id;
131        const message = this.inputTarget.value.trim();
132        if (!message) return;
133
134        console.log(`Sending request to ${vendor} (Root ID: ${rootId}):`, message);
135        
136        // Use fetch to send the request to the backend
137        fetch(`/ai/chat/${vendor}/send`, {
138            method: 'POST',
139            headers: {
140                'Content-Type': 'application/json',
141            },
142            body: JSON.stringify({
143                vendor: vendor,
144                message: message,
145                conversationId: this.currentConversationIdValue || null
146            })
147        })
148        .then(response => response.json())
149        .then(data => {
150            console.log('Success:', data);
151            if (data.status === 'success') {
152                this.appendMessage('user', message, rootId);
153                this.appendMessage('assistant', data.response, rootId);
154                this.inputTarget.value = '';
155                this.inputTarget.style.height = 'auto'; // Reset height
156                
157                // Update current conversation ID if it's a new conversation
158                if (data.conversationId) {
159                    this.currentConversationIdValue = data.conversationId;
160                    // Refresh the history list to include the new/updated conversation
161                    this.loadHistoryList(rootId);
162                }
163                
164                // Manually call highlightAll if hljs is available
165                if (window.hljs) {
166                    hljs.highlightAll();
167                }
168
169                this.scrollToBottom();
170            } else {
171                alert('Error: ' + data.message);
172            }
173        })
174        .catch((error) => {
175            console.error('Error:', error);
176            alert('An error occurred while sending the message.');
177        });
178    }
179
180    appendMessage(role, content, rootId = null) {
181        const messageDiv = document.createElement('div');
182        const timestamp = Date.now() + Math.floor(Math.random() * 1000);
183        const messageId = rootId ? `${rootId}-message-${timestamp}` : `message-${timestamp}`;
184        
185        const alignment = role === 'user' ? 'align-self-end bg-primary text-white shadow-sm' : 'align-self-start bg-light shadow-sm border';
186        const label = role === 'user' ? 'You' : 'AI';
187        const labelClass = role === 'user' ? 'border-white border-opacity-25' : '';
188        
189        messageDiv.id = messageId;
190        messageDiv.className = `ai-chat-message ${role}-message ${alignment} p-2 rounded mb-2`;
191        messageDiv.style.maxWidth = '80%';
192        
193        // If it's an assistant message, it might be HTML from the server or raw text that needs parsing
194        let renderedContent = content;
195        if (role === 'assistant') {
196            // Check if it's already HTML (simple check) or needs marked
197            const isHtml = /<[a-z][\s\S]*>/i.test(content);
198            renderedContent = isHtml ? content : marked.parse(content);
199        }
200
201        messageDiv.innerHTML = `<div class="small fw-bold border-bottom ${labelClass} mb-1">${label}</div><div class="${role === 'assistant' ? 'ai-chat-content' : ''}">${renderedContent}</div>`;
202        
203        this.historyTarget.appendChild(messageDiv);
204
205        // Highlight code blocks if hljs is available
206        if (role === 'assistant' && window.hljs) {
207            messageDiv.querySelectorAll('pre code').forEach((block) => {
208                hljs.highlightElement(block);
209            });
210        }
211    }
212
213    scrollToBottom() {
214        this.historyTarget.scrollTop = this.historyTarget.scrollHeight;
215    }
216
217    vendorValueChanged(vendor) {
218        if (vendor) {
219            sessionStorage.setItem('ai_chat_vendor', vendor);
220        }
221    }
222
223    currentConversationIdValueChanged(id) {
224        sessionStorage.setItem('ai_chat_conversation_id', id);
225    }
226
227    selectConversation(event) {
228        event.preventDefault();
229        const vendor = event.params.vendor; // Use vendor from the conversation
230        const conversationId = event.params.conversationId;
231        const rootId = event.params.id;
232
233        // Update UI to reflect the vendor of the selected conversation
234        if (this.vendorValue !== vendor) {
235            this.vendorValue = vendor;
236            if (this.hasVendorSelectTarget) {
237                this.vendorSelectTarget.value = vendor;
238            }
239            // Load models for the new vendor before loading the conversation
240            this.loadModels(vendor).then(() => {
241                this.loadConversation(vendor, conversationId, rootId);
242            });
243        } else {
244            this.loadConversation(vendor, conversationId, rootId);
245        }
246    }
247
248    loadConversation(vendor, conversationId, rootId = null) {
249        console.log(`Loading conversation ${conversationId} for vendor ${vendor} (Root ID: ${rootId})`);
250
251        return fetch(`/ai/chat/${vendor}/conversation/${conversationId}`, {
252            method: 'POST',
253            headers: {
254                'Content-Type': 'application/json',
255            },
256            body: JSON.stringify({
257                vendor: vendor
258            })
259        })
260        .then(response => response.json())
261        .then(data => {
262            console.log('Conversation loaded:', data);
263            if (data.status === 'success') {
264                // Update current conversation ID
265                this.currentConversationIdValue = conversationId;
266
267                // Update model select if model is returned
268                if (data.model && this.hasModelSelectTarget) {
269                    this.modelSelectTarget.value = data.model;
270                    sessionStorage.setItem('ai_chat_model', data.model);
271                }
272
273                // Update persona select if persona is returned
274                if (data.persona && this.hasPersonaSelectTarget) {
275                    this.personaSelectTarget.value = data.persona;
276                    sessionStorage.setItem('ai_chat_persona', data.persona);
277                }
278
279                // Clear current history
280                this.historyTarget.innerHTML = '';
281                // Append loaded history
282                data.history.forEach((msg, index) => {
283                    let role = msg.type === 'request' ? 'user' : 'assistant';
284                    this.appendMessage(role, msg.text, rootId);
285                });
286                this.scrollToBottom();
287                // Optionally hide the list on mobile
288                this.pastListTarget.classList.add('d-none');
289            } else {
290                alert('Error: ' + data.message);
291            }
292        })
293        .catch((error) => {
294            console.error('Error:', error);
295            // alert('An error occurred while loading the conversation.');
296        });
297    }
298
299    startNewConversation(event) {
300        if (event) event.preventDefault();
301        const vendor = this.vendorValue;
302        console.log(`Starting new conversation for vendor: ${vendor}`);
303        
304        // Reset current conversation ID
305        this.currentConversationIdValue = 0;
306        sessionStorage.removeItem('ai_chat_conversation_id');
307        
308        // Clear history UI
309        this.historyTarget.innerHTML = '';
310        
311        // Clear input
312        this.inputTarget.value = '';
313        this.inputTarget.style.height = 'auto';
314        
315        // Close history list if open
316        this.pastListTarget.classList.add('d-none');
317    }
318
319    changeVendor(event) {
320        const vendor = event.target.value;
321        console.log(`Vendor changed to: ${vendor}`);
322        this.vendorValue = vendor;
323        this.loadModels(vendor);
324        
325        // Clear history and reset conversation when switching vendors
326        this.startNewConversation(event);
327        sessionStorage.removeItem('ai_chat_model');
328    }
329
330    loadModels(vendor) {
331        return fetch(`/ai/chat/${vendor}/models`)
332            .then(response => response.json())
333            .then(data => {
334                if (data.status === 'success') {
335                    this.modelSelectTarget.innerHTML = '<option value="">Select Model</option>';
336                    if (data.models && data.models.length > 0) {
337                        data.models.forEach(model => {
338                            const option = document.createElement('option');
339                            option.value = model;
340                            option.textContent = model;
341                            this.modelSelectTarget.appendChild(option);
342                        });
343                        this.modelSelectTarget.disabled = false;
344                    } else {
345                        this.modelSelectTarget.disabled = true;
346                    }
347                }
348            });
349    }
350
351    changeModel(event) {
352        const model = event.target.value;
353        const vendor = this.vendorValue;
354        console.log(`Model changed to: ${model} for vendor: ${vendor}`);
355        
356        if (!model) {
357            sessionStorage.removeItem('ai_chat_model');
358            return;
359        }
360
361        sessionStorage.setItem('ai_chat_model', model);
362
363        fetch(`/ai/chat/${vendor}/set-model`, {
364            method: 'POST',
365            headers: {
366                'Content-Type': 'application/json',
367            },
368            body: JSON.stringify({ model: model })
369        })
370        .then(response => response.json())
371        .then(data => {
372            if (data.status !== 'success') {
373                alert('Error setting model: ' + data.message);
374            }
375        });
376    }
377
378    loadPersonas() {
379        return fetch(`/ai/chat/personas`)
380            .then(response => response.json())
381            .then(data => {
382                if (data.status === 'success') {
383                    this.personaSelectTarget.innerHTML = '<option value="">Select Persona</option>';
384                    Object.entries(data.personas).forEach(([name, prompt]) => {
385                        const option = document.createElement('option');
386                        option.value = prompt;
387                        option.textContent = name;
388                        this.personaSelectTarget.appendChild(option);
389                    });
390                }
391            });
392    }
393
394    changePersona(event) {
395        const persona = event.target.value;
396        console.log(`Persona changed to: ${persona}`);
397        
398        if (!persona) {
399            sessionStorage.removeItem('ai_chat_persona');
400            return;
401        }
402
403        sessionStorage.setItem('ai_chat_persona', persona);
404
405        fetch(`/ai/chat/set-persona`, {
406            method: 'POST',
407            headers: {
408                'Content-Type': 'application/json',
409            },
410            body: JSON.stringify({ persona: persona })
411        })
412        .then(response => response.json())
413        .then(data => {
414            if (data.status !== 'success') {
415                alert('Error setting persona: ' + data.message);
416            }
417        });
418    }
419}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.