This commit is contained in:
parent
9d778f6fd6
commit
8649b278a0
3 changed files with 453 additions and 1 deletions
|
|
@ -161,6 +161,56 @@
|
|||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
/* Action button container */
|
||||
.action-buttons {
|
||||
margin-top: 0.75rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.action-btn {
|
||||
background: #0f3460;
|
||||
border: 1px solid #e94560;
|
||||
color: #e94560;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.action-btn:hover {
|
||||
background: #e94560;
|
||||
color: white;
|
||||
}
|
||||
.action-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.action-btn .spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid currentColor;
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.action-btn.success {
|
||||
background: #1a1a2e;
|
||||
border-color: #4ade80;
|
||||
color: #4ade80;
|
||||
}
|
||||
.action-btn.error {
|
||||
background: #1a1a2e;
|
||||
border-color: #f87171;
|
||||
color: #f87171;
|
||||
}
|
||||
.input-area {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
|
@ -404,11 +454,28 @@
|
|||
function addMessage(role, content, streaming = false) {
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.className = `message ${role}`;
|
||||
|
||||
// Parse action markers if this is an assistant message
|
||||
let contentHtml = escapeHtml(content);
|
||||
let actions = [];
|
||||
|
||||
if (role === 'assistant' && !streaming) {
|
||||
const parsed = parseActionMarkers(content, messagesDiv.children.length);
|
||||
contentHtml = parsed.html;
|
||||
actions = parsed.actions;
|
||||
}
|
||||
|
||||
msgDiv.innerHTML = `
|
||||
<div class="role">${role}</div>
|
||||
<div class="content${streaming ? ' streaming' : ''}">${escapeHtml(content)}</div>
|
||||
<div class="content${streaming ? ' streaming' : ''}">${contentHtml}</div>
|
||||
`;
|
||||
messagesDiv.appendChild(msgDiv);
|
||||
|
||||
// Render action buttons for assistant messages
|
||||
if (actions.length > 0) {
|
||||
renderActionButtons(msgDiv, actions, messagesDiv.children.length - 1);
|
||||
}
|
||||
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
return msgDiv.querySelector('.content');
|
||||
}
|
||||
|
|
@ -430,6 +497,121 @@
|
|||
return div.innerHTML.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
// Action buttons state - track pending actions by message index
|
||||
const pendingActions = new Map();
|
||||
|
||||
// Parse action markers from content and return HTML with action buttons
|
||||
function parseActionMarkers(content, messageIndex) {
|
||||
const actionPattern = /<action type="([^"]+)">(.*?)<\/action>/gs;
|
||||
const hasActions = actionPattern.test(content);
|
||||
|
||||
if (!hasActions) {
|
||||
return { html: escapeHtml(content), actions: [] };
|
||||
}
|
||||
|
||||
// Reset pending actions for this message
|
||||
pendingActions.set(messageIndex, []);
|
||||
|
||||
let html = content;
|
||||
const actions = [];
|
||||
|
||||
// Replace action markers with placeholders and collect actions
|
||||
html = html.replace(actionPattern, (match, type, jsonStr) => {
|
||||
try {
|
||||
const action = JSON.parse(jsonStr);
|
||||
actions.push({ type, payload: action, id: `${messageIndex}-${actions.length}` });
|
||||
// Replace with placeholder that will be rendered as button
|
||||
return `<div class="action-placeholder" data-action-id="${actions[actions.length - 1].id}"></div>`;
|
||||
} catch (e) {
|
||||
// If JSON parsing fails, keep the original marker
|
||||
return match;
|
||||
}
|
||||
});
|
||||
|
||||
// Convert newlines to <br> for HTML output
|
||||
html = html.replace(/\n/g, '<br>');
|
||||
|
||||
return { html, actions };
|
||||
}
|
||||
|
||||
// Render action buttons for a message
|
||||
function renderActionButtons(msgDiv, actions, messageIndex) {
|
||||
if (actions.length === 0) return;
|
||||
|
||||
const buttonsDiv = document.createElement('div');
|
||||
buttonsDiv.className = 'action-buttons';
|
||||
|
||||
actions.forEach(action => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'action-btn';
|
||||
btn.dataset.actionId = action.id;
|
||||
btn.dataset.messageIndex = messageIndex;
|
||||
|
||||
let btnText = 'Execute';
|
||||
let icon = '';
|
||||
|
||||
switch (action.type) {
|
||||
case 'ci-run':
|
||||
icon = '🚀';
|
||||
btnText = `Run CI for ${action.payload.branch || 'default'}`;
|
||||
break;
|
||||
case 'issue-create':
|
||||
icon = '📝';
|
||||
btnText = `Create Issue: ${action.payload.title ? action.payload.title.substring(0, 30) + (action.payload.title.length > 30 ? '...' : '') : 'New Issue'}`;
|
||||
break;
|
||||
case 'pr-create':
|
||||
icon = '🔀';
|
||||
btnText = `Create PR: ${action.payload.title ? action.payload.title.substring(0, 30) + (action.payload.title.length > 30 ? '...' : '') : 'New PR'}`;
|
||||
break;
|
||||
default:
|
||||
btnText = `Execute ${action.type}`;
|
||||
}
|
||||
|
||||
btn.innerHTML = `<span>${icon}</span><span>${btnText}</span>`;
|
||||
|
||||
btn.addEventListener('click', () => executeAction(btn, action));
|
||||
buttonsDiv.appendChild(btn);
|
||||
});
|
||||
|
||||
msgDiv.appendChild(buttonsDiv);
|
||||
}
|
||||
|
||||
// Execute an action
|
||||
async function executeAction(btn, action) {
|
||||
const messageIndex = btn.dataset.messageIndex;
|
||||
const actionId = btn.dataset.actionId;
|
||||
|
||||
// Disable button and show loading state
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Executing...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/chat/action/${action.type}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...action.payload,
|
||||
conversation_id: currentConversationId,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
btn.className = 'action-btn success';
|
||||
btn.innerHTML = '<span>✓</span> Executed successfully';
|
||||
} else {
|
||||
btn.className = 'action-btn error';
|
||||
btn.innerHTML = `<span>✗</span> Error: ${result.error || 'Unknown error'}`;
|
||||
}
|
||||
} catch (error) {
|
||||
btn.className = 'action-btn error';
|
||||
btn.innerHTML = `<span>✗</span> Error: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Send message handler
|
||||
async function sendMessage() {
|
||||
const message = textarea.value.trim();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue