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;
}
}