Show app URL notification, play sound on completion
- Show prominent app URL notification when task completes - Play completion sound using Web Audio API - Extract app_url from task completion details - Add slideInRight/slideOutRight animations - Make toast clickable to open app URL - Refresh task details on completion
This commit is contained in:
parent
ca34a05d4c
commit
baf6c75d26
1 changed files with 163 additions and 9 deletions
|
|
@ -334,16 +334,33 @@ function handleWebSocketMessage(data) {
|
||||||
case "task_completed":
|
case "task_completed":
|
||||||
console.log("[Tasks WS] TASK_COMPLETED:", data.message);
|
console.log("[Tasks WS] TASK_COMPLETED:", data.message);
|
||||||
addAgentLog("success", `[COMPLETE] ${data.message}`);
|
addAgentLog("success", `[COMPLETE] ${data.message}`);
|
||||||
|
|
||||||
|
// Extract app_url from details if present
|
||||||
|
let appUrl = null;
|
||||||
|
if (data.details && data.details.startsWith("app_url:")) {
|
||||||
|
appUrl = data.details.substring(8);
|
||||||
|
addAgentLog("success", `🚀 App URL: ${appUrl}`);
|
||||||
|
showAppUrlNotification(appUrl);
|
||||||
|
}
|
||||||
|
|
||||||
if (data.activity) {
|
if (data.activity) {
|
||||||
updateActivityMetrics(data.activity);
|
updateActivityMetrics(data.activity);
|
||||||
logFinalStats(data.activity);
|
logFinalStats(data.activity);
|
||||||
}
|
}
|
||||||
completeFloatingProgress(data.message, data.activity);
|
completeFloatingProgress(data.message, data.activity, appUrl);
|
||||||
updateProgressUI(data);
|
updateProgressUI(data);
|
||||||
onTaskCompleted(data);
|
onTaskCompleted(data, appUrl);
|
||||||
|
|
||||||
|
// Play completion sound
|
||||||
|
playCompletionSound();
|
||||||
|
|
||||||
|
// Refresh task list and details
|
||||||
if (typeof htmx !== "undefined") {
|
if (typeof htmx !== "undefined") {
|
||||||
htmx.trigger(document.body, "taskCreated");
|
htmx.trigger(document.body, "taskCreated");
|
||||||
}
|
}
|
||||||
|
if (data.task_id && data.task_id === TasksState.selectedTaskId) {
|
||||||
|
loadTaskDetails(data.task_id);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "task_error":
|
case "task_error":
|
||||||
|
|
@ -588,7 +605,7 @@ function updateFloatingProgressBar(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function completeFloatingProgress(message, activity) {
|
function completeFloatingProgress(message, activity, appUrl) {
|
||||||
const fillEl = document.getElementById("floating-progress-fill");
|
const fillEl = document.getElementById("floating-progress-fill");
|
||||||
if (fillEl) fillEl.style.width = "100%";
|
if (fillEl) fillEl.style.width = "100%";
|
||||||
|
|
||||||
|
|
@ -1188,10 +1205,120 @@ function showDetailedView(taskId) {
|
||||||
// TASK LIFECYCLE
|
// TASK LIFECYCLE
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
function onTaskCompleted(task) {
|
function onTaskCompleted(data, appUrl) {
|
||||||
showToast(`Task completed: ${task.title}`, "success");
|
const title = data.title || data.message || "Task";
|
||||||
addAgentLog("success", `[COMPLETE] Task #${task.id}: ${task.title}`);
|
const taskId = data.task_id || data.id;
|
||||||
updateTaskCard(task);
|
|
||||||
|
if (appUrl) {
|
||||||
|
showToast(`App ready! Click to open: ${appUrl}`, "success", 10000, () => {
|
||||||
|
window.open(appUrl, "_blank");
|
||||||
|
});
|
||||||
|
addAgentLog("success", `[COMPLETE] Task #${taskId}: ${title}`);
|
||||||
|
addAgentLog("success", `[URL] ${appUrl}`);
|
||||||
|
} else {
|
||||||
|
showToast(`Task completed: ${title}`, "success");
|
||||||
|
addAgentLog("success", `[COMPLETE] Task #${taskId}: ${title}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.task) {
|
||||||
|
updateTaskCard(data.task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAppUrlNotification(appUrl) {
|
||||||
|
// Create a prominent notification for the app URL
|
||||||
|
let notification = document.getElementById("app-url-notification");
|
||||||
|
if (!notification) {
|
||||||
|
notification = document.createElement("div");
|
||||||
|
notification.id = "app-url-notification";
|
||||||
|
notification.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 80px;
|
||||||
|
right: 24px;
|
||||||
|
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(34, 197, 94, 0.4);
|
||||||
|
z-index: 10001;
|
||||||
|
max-width: 400px;
|
||||||
|
animation: slideInRight 0.5s ease;
|
||||||
|
`;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div style="font-weight: 600; margin-bottom: 8px;">🎉 App Created Successfully!</div>
|
||||||
|
<div style="font-size: 13px; opacity: 0.9; margin-bottom: 12px;">Your app is ready to use</div>
|
||||||
|
<a href="${appUrl}" target="_blank" style="
|
||||||
|
display: inline-block;
|
||||||
|
background: white;
|
||||||
|
color: #16a34a;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
">Open App →</a>
|
||||||
|
<button onclick="this.parentElement.remove()" style="
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
opacity: 0.7;
|
||||||
|
">×</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Auto-hide after 30 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentElement) {
|
||||||
|
notification.style.animation = "slideOutRight 0.5s ease forwards";
|
||||||
|
setTimeout(() => notification.remove(), 500);
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function playCompletionSound() {
|
||||||
|
try {
|
||||||
|
// Create a simple beep sound using Web Audio API
|
||||||
|
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
const oscillator = audioCtx.createOscillator();
|
||||||
|
const gainNode = audioCtx.createGain();
|
||||||
|
|
||||||
|
oscillator.connect(gainNode);
|
||||||
|
gainNode.connect(audioCtx.destination);
|
||||||
|
|
||||||
|
oscillator.frequency.value = 800;
|
||||||
|
oscillator.type = "sine";
|
||||||
|
gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
|
||||||
|
gainNode.gain.exponentialRampToValueAtTime(
|
||||||
|
0.01,
|
||||||
|
audioCtx.currentTime + 0.5,
|
||||||
|
);
|
||||||
|
|
||||||
|
oscillator.start(audioCtx.currentTime);
|
||||||
|
oscillator.stop(audioCtx.currentTime + 0.5);
|
||||||
|
|
||||||
|
// Play a second higher tone for success feel
|
||||||
|
setTimeout(() => {
|
||||||
|
const osc2 = audioCtx.createOscillator();
|
||||||
|
const gain2 = audioCtx.createGain();
|
||||||
|
osc2.connect(gain2);
|
||||||
|
gain2.connect(audioCtx.destination);
|
||||||
|
osc2.frequency.value = 1200;
|
||||||
|
osc2.type = "sine";
|
||||||
|
gain2.gain.setValueAtTime(0.3, audioCtx.currentTime);
|
||||||
|
gain2.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
|
||||||
|
osc2.start(audioCtx.currentTime);
|
||||||
|
osc2.stop(audioCtx.currentTime + 0.3);
|
||||||
|
}, 150);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("[Tasks] Could not play completion sound:", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTaskFailed(task, error) {
|
function onTaskFailed(task, error) {
|
||||||
|
|
@ -1204,7 +1331,7 @@ function onTaskFailed(task, error) {
|
||||||
// TOAST NOTIFICATIONS
|
// TOAST NOTIFICATIONS
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
function showToast(message, type = "info") {
|
function showToast(message, type = "info", duration = 4000, onClick = null) {
|
||||||
let container = document.getElementById("toast-container");
|
let container = document.getElementById("toast-container");
|
||||||
if (!container) {
|
if (!container) {
|
||||||
container = document.createElement("div");
|
container = document.createElement("div");
|
||||||
|
|
@ -1255,12 +1382,17 @@ function showToast(message, type = "info") {
|
||||||
<span>${message}</span>
|
<span>${message}</span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
if (onClick) {
|
||||||
|
toast.style.cursor = "pointer";
|
||||||
|
toast.addEventListener("click", onClick);
|
||||||
|
}
|
||||||
|
|
||||||
container.appendChild(toast);
|
container.appendChild(toast);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
toast.style.animation = "fadeOut 0.3s ease forwards";
|
toast.style.animation = "fadeOut 0.3s ease forwards";
|
||||||
setTimeout(() => toast.remove(), 300);
|
setTimeout(() => toast.remove(), 300);
|
||||||
}, 4000);
|
}, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -1328,6 +1460,28 @@ style.textContent = `
|
||||||
transform: translateX(20px);
|
transform: translateX(20px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOutRight {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100px);
|
||||||
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue