';
canvasHtml += '
';
canvasHtml += '' + (data.title || 'Canvas') + '';
canvasHtml += '' + (data.canvas_id || '').substring(0, 8) + '
';
canvasHtml += '
';
this.messages.push({ id: ++msgId, role: 'agent', text: canvasHtml, meta: 'canvas', isHtml: true, tools: [] });
this.scrollToBottom();
break;
case 'pong': break;
}
},
// Format timestamp for display
formatTime: function(ts) {
if (!ts) return '';
var d = new Date(ts);
var h = d.getHours();
var m = d.getMinutes();
var ampm = h >= 12 ? 'PM' : 'AM';
h = h % 12 || 12;
return h + ':' + (m < 10 ? '0' : '') + m + ' ' + ampm;
},
// Copy message text to clipboard
copyMessage: function(msg) {
var text = msg.text || '';
navigator.clipboard.writeText(text).then(function() {
msg._copied = true;
setTimeout(function() { msg._copied = false; }, 2000);
}).catch(function() {});
},
// Process queued messages after current response completes
_processQueue: function() {
if (!this.messageQueue.length || this.sending) return;
var next = this.messageQueue.shift();
this._sendPayload(next.text, next.files, next.images);
},
async sendMessage() {
if (!this.currentAgent || (!this.inputText.trim() && !this.attachments.length)) return;
var text = this.inputText.trim();
// Handle slash commands
if (text.startsWith('/') && !this.attachments.length) {
var cmd = text.split(' ')[0].toLowerCase();
var cmdArgs = text.substring(cmd.length).trim();
var matched = this.slashCommands.find(function(c) { return c.cmd === cmd; });
if (matched) {
this.executeSlashCommand(matched.cmd, cmdArgs);
return;
}
}
this.inputText = '';
// Reset textarea height to single line
var ta = document.getElementById('msg-input');
if (ta) ta.style.height = '';
// Upload attachments first if any
var fileRefs = [];
var uploadedFiles = [];
if (this.attachments.length) {
for (var i = 0; i < this.attachments.length; i++) {
var att = this.attachments[i];
att.uploading = true;
try {
var uploadRes = await OpenFangAPI.upload(this.currentAgent.id, att.file);
fileRefs.push('[File: ' + att.file.name + ']');
uploadedFiles.push({ file_id: uploadRes.file_id, filename: uploadRes.filename, content_type: uploadRes.content_type });
} catch(e) {
OpenFangToast.error('Failed to upload ' + att.file.name);
fileRefs.push('[File: ' + att.file.name + ' (upload failed)]');
}
att.uploading = false;
}
// Clean up previews
for (var j = 0; j < this.attachments.length; j++) {
if (this.attachments[j].preview) URL.revokeObjectURL(this.attachments[j].preview);
}
this.attachments = [];
}
// Build final message text
var finalText = text;
if (fileRefs.length) {
finalText = (text ? text + '\n' : '') + fileRefs.join('\n');
}
// Collect image references for inline rendering
var msgImages = uploadedFiles.filter(function(f) { return f.content_type && f.content_type.startsWith('image/'); });
// Always show user message immediately
this.messages.push({ id: ++msgId, role: 'user', text: finalText, meta: '', tools: [], images: msgImages, ts: Date.now() });
this.scrollToBottom();
localStorage.setItem('of-first-msg', 'true');
// If already streaming, queue this message
if (this.sending) {
this.messageQueue.push({ text: finalText, files: uploadedFiles, images: msgImages });
return;
}
this._sendPayload(finalText, uploadedFiles, msgImages);
},
async _sendPayload(finalText, uploadedFiles, msgImages) {
this.sending = true;
// Try WebSocket first
var wsPayload = { type: 'message', content: finalText };
if (uploadedFiles && uploadedFiles.length) wsPayload.attachments = uploadedFiles;
if (OpenFangAPI.wsSend(wsPayload)) {
this.messages.push({ id: ++msgId, role: 'agent', text: '', meta: '', thinking: true, streaming: true, tools: [], ts: Date.now() });
this.scrollToBottom();
return;
}
// HTTP fallback
var t = typeof window.t === 'function' ? window.t : function(s) { return s; };
if (!OpenFangAPI.isWsConnected()) {
OpenFangToast.info(t('chat.using_http_mode'));
}
this.messages.push({ id: ++msgId, role: 'agent', text: '', meta: '', thinking: true, tools: [], ts: Date.now() });
this.scrollToBottom();
try {
var httpBody = { message: finalText };
if (uploadedFiles && uploadedFiles.length) httpBody.attachments = uploadedFiles;
var res = await OpenFangAPI.post('/api/agents/' + this.currentAgent.id + '/message', httpBody);
this.messages = this.messages.filter(function(m) { return !m.thinking; });
var httpMeta = (res.input_tokens || 0) + ' in / ' + (res.output_tokens || 0) + ' out';
if (res.cost_usd != null) httpMeta += ' | $' + res.cost_usd.toFixed(4);
if (res.iterations) httpMeta += ' | ' + res.iterations + ' iter';
this.messages.push({ id: ++msgId, role: 'agent', text: res.response, meta: httpMeta, tools: [], ts: Date.now() });
} catch(e) {
this.messages = this.messages.filter(function(m) { return !m.thinking; });
this.messages.push({ id: ++msgId, role: 'system', text: 'Error: ' + e.message, meta: '', tools: [], ts: Date.now() });
}
this.sending = false;
this.scrollToBottom();
// Process next queued message
var self = this;
this.$nextTick(function() {
var el = document.getElementById('msg-input'); if (el) el.focus();
self._processQueue();
});
},
// Stop the current agent run
stopAgent: function() {
if (!this.currentAgent) return;
var self = this;
OpenFangAPI.post('/api/agents/' + this.currentAgent.id + '/stop', {}).then(function(res) {
self.messages.push({ id: ++msgId, role: 'system', text: res.message || 'Run cancelled', meta: '', tools: [], ts: Date.now() });
self.sending = false;
self.scrollToBottom();
self.$nextTick(function() { self._processQueue(); });
}).catch(function(e) { OpenFangToast.error('Stop failed: ' + e.message); });
},
killAgent() {
if (!this.currentAgent) return;
var self = this;
var t = typeof window.t === 'function' ? window.t : function(s) { return s; };
var name = this.currentAgent.name;
OpenFangToast.confirm(t('chat.stop_agent_title'), t('chat.stop_agent_confirm') + ' "' + name + '"?', async function() {
try {
await OpenFangAPI.del('/api/agents/' + self.currentAgent.id);
OpenFangAPI.wsDisconnect();
self._wsAgent = null;
self.currentAgent = null;
self.messages = [];
try { localStorage.removeItem('of-active-agent'); } catch(e) { /* ignore */ }
OpenFangToast.success(t('chat.agent_stopped') + ' "' + name + '"');
Alpine.store('app').refreshAgents();
} catch(e) {
OpenFangToast.error(t('chat.stop_agent_failed') + ': ' + e.message);
}
});
},
// Permanently uninstall the agent: kill + remove ~/.openfang/agents/