secubox-openwrt/package/secubox/luci-app-ollama/htdocs/luci-static/resources/view/ollama/chat.js
CyberMind-FR 48deeccb99 feat(luci-app-ollama): Add LuCI dashboard for Ollama LLM
New LuCI application for Ollama management:
- Dashboard with service status and controls
- Model management (pull, remove, list)
- Chat interface with model selection
- Settings page for configuration

Files:
- RPCD backend (luci.ollama)
- Dashboard, Models, Chat, Settings views
- ACL and menu definitions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 19:02:12 +01:00

398 lines
10 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
'require view';
'require ui';
'require rpc';
var callModels = rpc.declare({
object: 'luci.ollama',
method: 'models',
expect: { models: [] }
});
var callChat = rpc.declare({
object: 'luci.ollama',
method: 'chat',
params: ['model', 'message'],
expect: { response: '' }
});
var callStatus = rpc.declare({
object: 'luci.ollama',
method: 'status',
expect: { running: false }
});
return view.extend({
title: _('Ollama Chat'),
chatHistory: [],
selectedModel: null,
load: function() {
return Promise.all([
callModels(),
callStatus()
]).then(function(results) {
return {
models: Array.isArray(results[0]) ? results[0] : [],
status: results[1] || {}
};
});
},
render: function(data) {
var self = this;
var models = data.models;
var status = data.status;
if (!status.running) {
return E('div', { 'class': 'ollama-chat' }, [
E('style', {}, this.getCSS()),
E('div', { 'class': 'oll-chat-offline' }, [
E('span', { 'class': 'oll-offline-icon' }, '⚠️'),
E('h3', {}, _('Ollama is not running')),
E('p', {}, _('Start the service to use chat')),
E('code', {}, '/etc/init.d/ollama start')
])
]);
}
if (models.length === 0) {
return E('div', { 'class': 'ollama-chat' }, [
E('style', {}, this.getCSS()),
E('div', { 'class': 'oll-chat-offline' }, [
E('span', { 'class': 'oll-offline-icon' }, '📦'),
E('h3', {}, _('No models available')),
E('p', {}, _('Download a model first')),
E('code', {}, 'ollamactl pull tinyllama')
])
]);
}
this.selectedModel = models[0].name;
var container = E('div', { 'class': 'ollama-chat' }, [
E('style', {}, this.getCSS()),
// Header
E('div', { 'class': 'oll-chat-header' }, [
E('div', { 'class': 'oll-chat-title' }, [
E('span', { 'class': 'oll-chat-icon' }, '🦙'),
E('span', {}, _('Ollama Chat'))
]),
E('div', { 'class': 'oll-model-select-wrapper' }, [
E('label', {}, _('Model:')),
E('select', {
'class': 'oll-model-select',
'id': 'model-select',
'change': function(ev) { self.selectedModel = ev.target.value; }
}, models.map(function(m) {
return E('option', { 'value': m.name }, m.name);
}))
])
]),
// Chat Messages
E('div', { 'class': 'oll-chat-messages', 'id': 'chat-messages' }, [
E('div', { 'class': 'oll-chat-welcome' }, [
E('span', { 'class': 'oll-welcome-icon' }, '👋'),
E('h3', {}, _('Welcome to Ollama Chat')),
E('p', {}, _('Select a model and start chatting. Your conversation is processed locally.'))
])
]),
// Input Area
E('div', { 'class': 'oll-chat-input-area' }, [
E('textarea', {
'class': 'oll-chat-input',
'id': 'chat-input',
'placeholder': _('Type your message...'),
'rows': 3,
'keydown': function(ev) {
if (ev.key === 'Enter' && !ev.shiftKey) {
ev.preventDefault();
self.sendMessage();
}
}
}),
E('button', {
'class': 'oll-send-btn',
'id': 'send-btn',
'click': function() { self.sendMessage(); }
}, [E('span', {}, ''), _('Send')])
])
]);
return container;
},
sendMessage: function() {
var self = this;
var input = document.getElementById('chat-input');
var message = input.value.trim();
if (!message) return;
var messagesContainer = document.getElementById('chat-messages');
var sendBtn = document.getElementById('send-btn');
// Clear welcome message if present
var welcome = messagesContainer.querySelector('.oll-chat-welcome');
if (welcome) welcome.remove();
// Add user message
this.addMessage('user', message);
input.value = '';
sendBtn.disabled = true;
// Add typing indicator
var typingId = 'typing-' + Date.now();
this.addTypingIndicator(typingId);
// Send to API
callChat(this.selectedModel, message).then(function(result) {
self.removeTypingIndicator(typingId);
sendBtn.disabled = false;
if (result.error) {
self.addMessage('error', result.error);
} else if (result.response) {
self.addMessage('assistant', result.response);
} else {
self.addMessage('error', _('No response received'));
}
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}).catch(function(err) {
self.removeTypingIndicator(typingId);
sendBtn.disabled = false;
self.addMessage('error', err.message);
});
},
addMessage: function(role, content) {
var messagesContainer = document.getElementById('chat-messages');
var msgClass = 'oll-message oll-message-' + role;
var icon = role === 'user' ? '👤' : (role === 'error' ? '' : '🦙');
var msg = E('div', { 'class': msgClass }, [
E('div', { 'class': 'oll-message-icon' }, icon),
E('div', { 'class': 'oll-message-content' }, content)
]);
messagesContainer.appendChild(msg);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
},
addTypingIndicator: function(id) {
var messagesContainer = document.getElementById('chat-messages');
var typing = E('div', { 'class': 'oll-message oll-message-assistant oll-typing', 'id': id }, [
E('div', { 'class': 'oll-message-icon' }, '🦙'),
E('div', { 'class': 'oll-message-content' }, [
E('div', { 'class': 'oll-typing-dots' }, [
E('span', {}), E('span', {}), E('span', {})
])
])
]);
messagesContainer.appendChild(typing);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
},
removeTypingIndicator: function(id) {
var el = document.getElementById(id);
if (el) el.remove();
},
getCSS: function() {
return `
.ollama-chat {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: #030712;
color: #f8fafc;
height: calc(100vh - 100px);
display: flex;
flex-direction: column;
border-radius: 12px;
overflow: hidden;
}
.oll-chat-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background: #0f172a;
border-bottom: 1px solid #334155;
}
.oll-chat-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
}
.oll-chat-icon { font-size: 24px; }
.oll-model-select-wrapper {
display: flex;
align-items: center;
gap: 10px;
}
.oll-model-select-wrapper label {
color: #94a3b8;
font-size: 13px;
}
.oll-model-select {
background: #1e293b;
border: 1px solid #334155;
border-radius: 8px;
padding: 8px 12px;
color: #f8fafc;
font-size: 13px;
cursor: pointer;
}
.oll-chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.oll-chat-welcome {
text-align: center;
padding: 60px 20px;
color: #64748b;
}
.oll-welcome-icon { font-size: 48px; display: block; margin-bottom: 16px; }
.oll-chat-welcome h3 { margin: 0 0 8px; color: #f8fafc; }
.oll-chat-welcome p { margin: 0; }
.oll-message {
display: flex;
gap: 12px;
max-width: 85%;
}
.oll-message-user {
align-self: flex-end;
flex-direction: row-reverse;
}
.oll-message-assistant {
align-self: flex-start;
}
.oll-message-error {
align-self: center;
}
.oll-message-icon {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.oll-message-user .oll-message-icon {
background: linear-gradient(135deg, #3b82f6, #2563eb);
}
.oll-message-assistant .oll-message-icon {
background: linear-gradient(135deg, #f97316, #ea580c);
}
.oll-message-error .oll-message-icon {
background: linear-gradient(135deg, #ef4444, #dc2626);
}
.oll-message-content {
background: #1e293b;
padding: 12px 16px;
border-radius: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.oll-message-user .oll-message-content {
background: linear-gradient(135deg, #3b82f6, #2563eb);
border-radius: 12px 12px 4px 12px;
}
.oll-message-assistant .oll-message-content {
border-radius: 12px 12px 12px 4px;
}
.oll-message-error .oll-message-content {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.oll-typing-dots {
display: flex;
gap: 4px;
}
.oll-typing-dots span {
width: 8px;
height: 8px;
background: #64748b;
border-radius: 50%;
animation: typing 1.4s infinite;
}
.oll-typing-dots span:nth-child(2) { animation-delay: 0.2s; }
.oll-typing-dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing {
0%, 60%, 100% { opacity: 0.3; transform: scale(0.8); }
30% { opacity: 1; transform: scale(1); }
}
.oll-chat-input-area {
padding: 16px 20px;
background: #0f172a;
border-top: 1px solid #334155;
display: flex;
gap: 12px;
}
.oll-chat-input {
flex: 1;
background: #1e293b;
border: 1px solid #334155;
border-radius: 12px;
padding: 12px 16px;
color: #f8fafc;
font-size: 14px;
resize: none;
font-family: inherit;
}
.oll-chat-input:focus {
outline: none;
border-color: #f97316;
}
.oll-send-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
background: linear-gradient(135deg, #f97316, #ea580c);
border: none;
border-radius: 12px;
color: white;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
}
.oll-send-btn:hover { opacity: 0.9; }
.oll-send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.oll-chat-offline {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
color: #64748b;
padding: 40px;
}
.oll-offline-icon { font-size: 48px; margin-bottom: 16px; }
.oll-chat-offline h3 { margin: 0 0 8px; color: #f8fafc; }
.oll-chat-offline p { margin: 0 0 16px; }
.oll-chat-offline code {
background: #1e293b;
padding: 8px 16px;
border-radius: 8px;
font-size: 13px;
}
`;
}
});