import { OnboardingScreen } from "./components/OnboardingScreen.js"; import { CategorizationScreen } from "./components/CategorizationScreen.js"; import { CustomTaskInput } from "./components/CustomTaskInput.js"; import { WorkflowPicker } from "./components/WorkflowPicker.js"; import { DeployingScreen } from "./components/DeployingScreen.js"; import { DashboardScreen } from "./components/DashboardScreen.js"; import { EscalationPanel } from "./components/EscalationPanel.js"; import { apiRequest } from "./hooks/useApi.js"; const root = document.getElementById("root"); const defaultDescription = "I run an apple orchard. Customers email me orders, I check inventory in my Google Sheet, reply with pickup details, and every Friday I count weekly orders."; const defaultBackendUrl = "http://localhost:8000/api"; const setupSteps = [ { id: 1, label: "Describe" }, { id: 2, label: "Review" }, { id: 3, label: "Rule" }, { id: 4, label: "Modes" }, { id: 5, label: "Launch" } ]; const state = { view: "setup", currentStep: 1, settingsOpen: false, backendUrl: defaultBackendUrl, ownerEmail: "", ownerId: "", description: defaultDescription, analysis: null, selectedTaskIds: [], customRule: "When a restaurant client emails, flag it as priority and move it to the top of my orders sheet.", workflowSelections: {}, workflowCursor: 0, liveStats: { activeWorkflows: 0, executionsToday: 0, pendingEscalations: 0 }, escalations: [], status: "Ready.", statusError: false }; init(); async function init() { const profile = await getIdentityProfile(); if (profile) { await chrome.storage.local.set({ flowpilotOwnerProfile: profile }); } const stored = await chrome.storage.local.get(["flowpilotBackendUrl", "flowpilotOwnerProfile"]); state.backendUrl = stored.flowpilotBackendUrl || defaultBackendUrl; state.ownerEmail = stored.flowpilotOwnerProfile?.email || ""; state.ownerId = stored.flowpilotOwnerProfile?.ownerId || ""; if (!state.ownerEmail) { setStatus("Sign into Chrome with Google to let FlowPilot identify the mailbox.", false); } await refreshLiveData(); render(); } function render() { root.innerHTML = `
`; bindEvents(); } function renderHeader() { const indicator = getViewIndicator(); return `
F

FlowPilot

${escapeHtml(indicator.label)}

`; } function renderSettings() { return `
`; } function renderBody() { if (state.view === "escalation") { return EscalationPanel({ escalation: state.escalations[0] || null }); } if (state.view === "quiet") { return DashboardScreen({ stats: state.liveStats }); } if (state.currentStep === 1) { return OnboardingScreen({ description: state.description, status: state.status, statusError: state.statusError, ownerEmail: state.ownerEmail }); } if (state.currentStep === 2) { return CategorizationScreen({ summary: state.analysis?.summary || "Review the suggested workstreams.", groups: buildTaskGroups(), selectedTaskIds: state.selectedTaskIds }); } if (state.currentStep === 3) { return CustomTaskInput({ value: state.customRule }); } if (state.currentStep === 4) { const tasks = getWorkflowTasks(); const task = tasks[state.workflowCursor]; return WorkflowPicker({ task, selectedOption: task ? state.workflowSelections[task.id] || null : null, current: state.workflowCursor + 1, total: tasks.length }); } return DeployingScreen(); } function bindEvents() { document.getElementById("flowpilot-save-url")?.addEventListener("click", saveBackendUrl); document.getElementById("flowpilot-close-settings")?.addEventListener("click", toggleSettings); document.getElementById("flowpilot-open-settings")?.addEventListener("click", toggleSettings); document.getElementById("flowpilot-back-button")?.addEventListener("click", goBack); document.getElementById("flowpilot-back-dashboard")?.addEventListener("click", () => { state.view = "quiet"; render(); }); if (state.view === "setup" && state.currentStep === 1) { document.getElementById("flowpilot-business-description")?.addEventListener("input", (event) => { state.description = event.target.value; }); document.getElementById("flowpilot-analyze-button")?.addEventListener("click", analyzeBusiness); } if (state.view === "setup" && state.currentStep === 2) { document.querySelectorAll("[data-task-id]").forEach((input) => { input.addEventListener("change", toggleTaskSelection); }); document.getElementById("flowpilot-continue-tasks")?.addEventListener("click", continueFromTasks); } if (state.view === "setup" && state.currentStep === 3) { document.getElementById("flowpilot-custom-rule")?.addEventListener("input", (event) => { state.customRule = event.target.value; }); document.getElementById("flowpilot-skip-rule")?.addEventListener("click", goToWorkflowStep); document.getElementById("flowpilot-save-rule")?.addEventListener("click", goToWorkflowStep); } if (state.view === "setup" && state.currentStep === 4) { document.querySelectorAll("[data-workflow-option]").forEach((button) => { button.addEventListener("click", selectWorkflowOption); }); } if (state.view === "setup" && state.currentStep === 5) { document.getElementById("flowpilot-deploy-button")?.addEventListener("click", completeSetup); } if (state.view === "quiet") { document.getElementById("flowpilot-open-escalation")?.addEventListener("click", () => { if (!state.escalations.length) { return; } state.view = "escalation"; render(); }); document.getElementById("flowpilot-stop-automation")?.addEventListener("click", stopAutomation); } if (state.view === "escalation") { document.getElementById("flowpilot-resolve-escalation")?.addEventListener("click", () => resolveEscalation("approve")); document.getElementById("flowpilot-ask-customer")?.addEventListener("click", () => resolveEscalation("ask_customer")); } } function toggleSettings() { state.settingsOpen = !state.settingsOpen; render(); } function goBack() { if (state.view !== "setup") { return; } if (state.currentStep === 2) { state.currentStep = 1; } else if (state.currentStep === 3) { state.currentStep = 2; } else if (state.currentStep === 4) { if (state.workflowCursor > 0) { state.workflowCursor -= 1; } else { state.currentStep = 3; } } else if (state.currentStep === 5) { if (getWorkflowTasks().length) { state.currentStep = 4; state.workflowCursor = Math.max(getWorkflowTasks().length - 1, 0); } else { state.currentStep = 3; } } render(); } async function saveBackendUrl() { const input = document.getElementById("flowpilot-backend-url"); const nextUrl = (input?.value || "").trim().replace(/\/$/, ""); if (!nextUrl) { setStatus("Enter a backend URL.", true); render(); return; } if (!isSupportedBackendUrl(nextUrl)) { setStatus("Use either http://localhost:8000/api or an https://...hf.space/api backend.", true); render(); return; } await chrome.storage.local.set({ flowpilotBackendUrl: nextUrl }); state.backendUrl = nextUrl; state.settingsOpen = false; setStatus("Backend saved."); render(); } async function analyzeBusiness() { if (!state.description.trim()) { setStatus("Add a short business description before running analysis.", true); render(); return; } if (!state.ownerEmail || !state.ownerId) { setStatus("Could not detect the signed-in Gmail account yet.", true); render(); return; } setStatus("Analyzing..."); render(); try { const payload = await apiRequest("/analyze", { method: "POST", body: JSON.stringify({ owner_id: state.ownerId, owner_email: state.ownerEmail, description: state.description.trim() }) }); state.analysis = payload; state.selectedTaskIds = getDefaultSelectedTaskIds(payload); state.currentStep = 2; setStatus("Ready."); await refreshLiveData(); } catch (error) { setStatus(error.message || "Could not reach the backend.", true); } render(); } function toggleTaskSelection(event) { const taskId = event.target.dataset.taskId; if (!taskId) { return; } if (event.target.checked) { if (!state.selectedTaskIds.includes(taskId)) { state.selectedTaskIds.push(taskId); } } else { state.selectedTaskIds = state.selectedTaskIds.filter((id) => id !== taskId); } } function continueFromTasks() { if (!state.selectedTaskIds.length) { setStatus("Select at least one task to continue.", true); render(); return; } state.currentStep = 3; setStatus("Ready."); render(); } function goToWorkflowStep() { state.currentStep = 4; state.workflowCursor = 0; render(); } function selectWorkflowOption(event) { const optionId = event.currentTarget.dataset.workflowOption; const taskId = event.currentTarget.dataset.taskId; if (!optionId || !taskId) { return; } state.workflowSelections[taskId] = optionId; if (state.workflowCursor < getWorkflowTasks().length - 1) { state.workflowCursor += 1; render(); return; } state.currentStep = 5; render(); } async function completeSetup() { await refreshLiveData(); state.view = "quiet"; render(); } async function stopAutomation() { if (!state.ownerId) { return; } try { await apiRequest("/stop", { method: "POST", body: JSON.stringify({ owner_id: state.ownerId }) }); await refreshLiveData(); setStatus("Automation stopped."); } catch (error) { setStatus(error.message || "Could not stop automation.", true); } render(); } async function resolveEscalation(response) { const escalation = state.escalations[0]; if (!escalation) { state.view = "quiet"; render(); return; } try { await apiRequest("/escalation-reply", { method: "POST", body: JSON.stringify({ escalation_id: escalation.id, response }) }); } catch (error) { setStatus(error.message || "Could not resolve escalation.", true); } await refreshLiveData(); state.view = "quiet"; render(); } function getDefaultSelectedTaskIds(analysis) { return [ ...(analysis?.tasks?.fully_automatable || []), ...(analysis?.tasks?.ai_assisted || []) ].map((task) => task.id); } function buildTaskGroups() { return [ { title: "Automate", items: state.analysis?.tasks?.fully_automatable || [] }, { title: "Assist", items: state.analysis?.tasks?.ai_assisted || [] }, { title: "Manual", items: state.analysis?.tasks?.manual || [] } ]; } function getWorkflowTasks() { const lookup = new Map(); for (const group of buildTaskGroups()) { for (const task of group.items) { lookup.set(task.id, task); } } return state.selectedTaskIds.map((id) => lookup.get(id)).filter(Boolean); } function getViewIndicator() { if (state.view === "escalation") { return { label: "Needs review" }; } if (state.view === "quiet") { return { label: "Running live" }; } return { label: `${setupSteps[state.currentStep - 1].label}` }; } function setStatus(message, isError = false) { state.status = message; state.statusError = isError; } async function refreshLiveData() { if (!state.ownerId) { state.liveStats = { activeWorkflows: 0, executionsToday: 0, pendingEscalations: 0 }; state.escalations = []; return; } try { const [statusPayload, escalationPayload] = await Promise.all([ apiRequest(`/status?owner_id=${encodeURIComponent(state.ownerId)}`), apiRequest(`/escalations?owner_id=${encodeURIComponent(state.ownerId)}`) ]); const workflows = statusPayload.workflows || []; const executions = statusPayload.recent_executions || []; const pendingEscalations = (escalationPayload.items || []).filter((item) => item.status === "pending"); state.liveStats = { activeWorkflows: workflows.filter((item) => item.status === "active").length, executionsToday: executions.filter((item) => isToday(item.executed_at)).length, pendingEscalations: pendingEscalations.length }; state.escalations = pendingEscalations; } catch { state.liveStats = { activeWorkflows: 0, executionsToday: 0, pendingEscalations: 0 }; state.escalations = []; } } function isSupportedBackendUrl(value) { try { const url = new URL(value); const isLocal = url.protocol === "http:" && url.hostname === "localhost" && url.pathname === "/api"; const isSpace = url.protocol === "https:" && url.hostname.endsWith(".hf.space") && url.pathname === "/api"; return isLocal || isSpace; } catch { return false; } } function escapeAttribute(value) { return String(value).replaceAll("&", "&").replaceAll('"', """); } function escapeHtml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function isToday(value) { if (!value) { return false; } const candidate = new Date(value); const now = new Date(); return ( candidate.getFullYear() === now.getFullYear() && candidate.getMonth() === now.getMonth() && candidate.getDate() === now.getDate() ); } async function getIdentityProfile() { if (!chrome.identity?.getProfileUserInfo) { return null; } try { const profile = await new Promise((resolve) => { chrome.identity.getProfileUserInfo({ accountStatus: "ANY" }, resolve); }); if (!profile.email) { return null; } return { email: profile.email, ownerId: profile.id || profile.email.toLowerCase() }; } catch { return null; } }