DevelopedBy-Siva commited on
Commit
b032b2d
·
1 Parent(s): d28fc5d

change Ui

Browse files
backend/api/routes.py CHANGED
@@ -20,6 +20,8 @@ from backend.models.schemas import (
20
  FileUploadRequest,
21
  FileUploadResponse,
22
  OwnerStatusResponse,
 
 
23
  WorkflowSuggestionRequest,
24
  )
25
  from backend.storage.database import db
@@ -114,6 +116,22 @@ def deploy_workflow(request: DeployRequest) -> DeploymentResponse:
114
  )
115
 
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  @router.post("/upload-data", response_model=FileUploadResponse)
118
  def upload_data(request: FileUploadRequest) -> FileUploadResponse:
119
  parsed = parse_uploaded_payload(request.filename, request.content, request.purpose)
 
20
  FileUploadRequest,
21
  FileUploadResponse,
22
  OwnerStatusResponse,
23
+ StopAutomationRequest,
24
+ StopAutomationResponse,
25
  WorkflowSuggestionRequest,
26
  )
27
  from backend.storage.database import db
 
116
  )
117
 
118
 
119
+ @router.post("/stop", response_model=StopAutomationResponse)
120
+ def stop_automation(request: StopAutomationRequest) -> StopAutomationResponse:
121
+ try:
122
+ owner = db.get_owner(request.owner_id)
123
+ except KeyError as exc:
124
+ raise HTTPException(status_code=404, detail=str(exc)) from exc
125
+ workflows = db.deactivate_workflows(request.owner_id)
126
+ owner["state"] = "paused"
127
+ db.save_owner(owner)
128
+ return StopAutomationResponse(
129
+ status="stopped",
130
+ workflows=workflows,
131
+ message="Automation stopped.",
132
+ )
133
+
134
+
135
  @router.post("/upload-data", response_model=FileUploadResponse)
136
  def upload_data(request: FileUploadRequest) -> FileUploadResponse:
137
  parsed = parse_uploaded_payload(request.filename, request.content, request.purpose)
backend/models/schemas.py CHANGED
@@ -67,6 +67,16 @@ class DeploymentResponse(BaseModel):
67
  message: str
68
 
69
 
 
 
 
 
 
 
 
 
 
 
70
  class FileUploadRequest(BaseModel):
71
  owner_id: str
72
  filename: str
 
67
  message: str
68
 
69
 
70
+ class StopAutomationRequest(BaseModel):
71
+ owner_id: str
72
+
73
+
74
+ class StopAutomationResponse(BaseModel):
75
+ status: str
76
+ workflows: list[dict[str, Any]]
77
+ message: str
78
+
79
+
80
  class FileUploadRequest(BaseModel):
81
  owner_id: str
82
  filename: str
backend/storage/database.py CHANGED
@@ -63,6 +63,12 @@ class InMemoryDatabase:
63
  def list_workflows(self, owner_id: str) -> list[dict]:
64
  return deepcopy(self.workflows.get(owner_id, []))
65
 
 
 
 
 
 
 
66
  def get_workflow(self, owner_id: str, workflow_id: str) -> dict:
67
  for workflow in self.workflows.get(owner_id, []):
68
  if workflow["id"] == workflow_id:
 
63
  def list_workflows(self, owner_id: str) -> list[dict]:
64
  return deepcopy(self.workflows.get(owner_id, []))
65
 
66
+ def deactivate_workflows(self, owner_id: str) -> list[dict]:
67
+ workflows = self.workflows.get(owner_id, [])
68
+ for workflow in workflows:
69
+ workflow["status"] = "inactive"
70
+ return deepcopy(workflows)
71
+
72
  def get_workflow(self, owner_id: str, workflow_id: str) -> dict:
73
  for workflow in self.workflows.get(owner_id, []):
74
  if workflow["id"] == workflow_id:
extension/content.js CHANGED
@@ -3,21 +3,27 @@ function injectSidebar() {
3
 
4
  const launcher = document.createElement("button");
5
  launcher.id = "flowpilot-sidebar-toggle";
6
- launcher.textContent = "FlowPilot";
7
  Object.assign(launcher.style, {
8
  position: "fixed",
9
- top: "16px",
10
- right: "16px",
11
  zIndex: "1000000",
12
- border: "0",
 
 
 
 
13
  borderRadius: "999px",
14
- padding: "10px 14px",
15
- background: "#a44a1f",
16
- color: "#fff7f0",
17
- font: '600 13px Georgia, "Times New Roman", serif',
18
  cursor: "pointer",
19
- boxShadow: "0 12px 28px rgba(0,0,0,0.18)"
 
20
  });
 
 
21
 
22
  const frame = document.createElement("iframe");
23
  frame.id = "flowpilot-sidebar-frame";
@@ -26,27 +32,34 @@ function injectSidebar() {
26
  position: "fixed",
27
  top: "0",
28
  right: "0",
29
- width: "420px",
30
  height: "100vh",
31
  border: "0",
32
  zIndex: "999999",
33
- boxShadow: "0 0 24px rgba(0,0,0,0.15)",
34
- background: "#f7f2e8",
35
  transform: "translateX(100%)",
36
  transition: "transform 180ms ease",
37
  pointerEvents: "none"
38
  });
39
 
40
  let isOpen = false;
 
 
 
 
 
 
41
  launcher.addEventListener("click", () => {
42
  isOpen = !isOpen;
43
  frame.style.transform = isOpen ? "translateX(0)" : "translateX(100%)";
44
  frame.style.pointerEvents = isOpen ? "auto" : "none";
45
- launcher.textContent = isOpen ? "Close FlowPilot" : "FlowPilot";
46
  });
47
 
48
  document.body.appendChild(launcher);
49
  document.body.appendChild(frame);
 
50
  }
51
 
52
  injectSidebar();
 
3
 
4
  const launcher = document.createElement("button");
5
  launcher.id = "flowpilot-sidebar-toggle";
 
6
  Object.assign(launcher.style, {
7
  position: "fixed",
8
+ top: "20px",
9
+ right: "20px",
10
  zIndex: "1000000",
11
+ width: "44px",
12
+ height: "44px",
13
+ display: "grid",
14
+ placeItems: "center",
15
+ border: "1px solid rgba(15,23,42,0.12)",
16
  borderRadius: "999px",
17
+ padding: "0",
18
+ background: "#ffffff",
19
+ color: "#2563eb",
20
+ font: '700 16px "Avenir Next", "Segoe UI", sans-serif',
21
  cursor: "pointer",
22
+ boxShadow: "0 8px 24px rgba(15,23,42,0.14)",
23
+ transition: "right 180ms ease, transform 180ms ease, box-shadow 180ms ease"
24
  });
25
+ launcher.setAttribute("aria-label", "Open FlowPilot");
26
+ launcher.textContent = ">";
27
 
28
  const frame = document.createElement("iframe");
29
  frame.id = "flowpilot-sidebar-frame";
 
32
  position: "fixed",
33
  top: "0",
34
  right: "0",
35
+ width: "396px",
36
  height: "100vh",
37
  border: "0",
38
  zIndex: "999999",
39
+ boxShadow: "0 0 30px rgba(15,23,42,0.12)",
40
+ background: "#f7f9fc",
41
  transform: "translateX(100%)",
42
  transition: "transform 180ms ease",
43
  pointerEvents: "none"
44
  });
45
 
46
  let isOpen = false;
47
+ function syncLauncher() {
48
+ launcher.style.right = isOpen ? "408px" : "20px";
49
+ launcher.textContent = isOpen ? "<" : ">";
50
+ launcher.setAttribute("aria-label", isOpen ? "Close FlowPilot" : "Open FlowPilot");
51
+ }
52
+
53
  launcher.addEventListener("click", () => {
54
  isOpen = !isOpen;
55
  frame.style.transform = isOpen ? "translateX(0)" : "translateX(100%)";
56
  frame.style.pointerEvents = isOpen ? "auto" : "none";
57
+ syncLauncher();
58
  });
59
 
60
  document.body.appendChild(launcher);
61
  document.body.appendChild(frame);
62
+ syncLauncher();
63
  }
64
 
65
  injectSidebar();
extension/manifest.json CHANGED
@@ -3,7 +3,7 @@
3
  "name": "FlowPilot",
4
  "version": "0.1.0",
5
  "description": "AI workflow builder for Gmail-based small businesses.",
6
- "permissions": ["storage", "activeTab", "scripting"],
7
  "host_permissions": [
8
  "https://mail.google.com/*",
9
  "http://localhost:8000/*",
 
3
  "name": "FlowPilot",
4
  "version": "0.1.0",
5
  "description": "AI workflow builder for Gmail-based small businesses.",
6
+ "permissions": ["storage", "activeTab", "scripting", "identity", "identity.email"],
7
  "host_permissions": [
8
  "https://mail.google.com/*",
9
  "http://localhost:8000/*",
extension/sidebar/app.js CHANGED
@@ -2,130 +2,468 @@ import { OnboardingScreen } from "./components/OnboardingScreen.js";
2
  import { CategorizationScreen } from "./components/CategorizationScreen.js";
3
  import { CustomTaskInput } from "./components/CustomTaskInput.js";
4
  import { WorkflowPicker } from "./components/WorkflowPicker.js";
5
- import { ConnectSheet } from "./components/ConnectSheet.js";
6
- import { FileUpload } from "./components/FileUpload.js";
7
  import { DeployingScreen } from "./components/DeployingScreen.js";
8
  import { DashboardScreen } from "./components/DashboardScreen.js";
9
  import { EscalationPanel } from "./components/EscalationPanel.js";
10
  import { apiRequest } from "./hooks/useApi.js";
11
- import { TaskCategoryGroup } from "./components/TaskCategoryGroup.js";
12
 
13
  const root = document.getElementById("root");
14
  const defaultDescription =
15
  "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.";
16
-
17
- const steps = [
18
- OnboardingScreen(),
19
- CategorizationScreen(),
20
- CustomTaskInput(),
21
- WorkflowPicker(),
22
- ConnectSheet(),
23
- FileUpload(),
24
- DeployingScreen(),
25
- DashboardScreen(),
26
- EscalationPanel()
27
  ];
28
 
29
- root.innerHTML = `
30
- <div class="sidebar-shell">
31
- <header class="hero">
32
- <p class="eyebrow">FlowPilot</p>
33
- <h1>Build inbox automations without leaving Gmail</h1>
34
- <p class="lede">Describe the business, choose workflows, and let the backend deploy them.</p>
35
- </header>
36
- <main class="screen-stack">${steps.join("")}</main>
37
- </div>
38
- `;
39
-
40
- const backendUrlInput = document.getElementById("flowpilot-backend-url");
41
- const saveUrlButton = document.getElementById("flowpilot-save-url");
42
- const descriptionInput = document.getElementById("flowpilot-business-description");
43
- const analyzeButton = document.getElementById("flowpilot-analyze-button");
44
- const onboardingStatus = document.getElementById("flowpilot-onboarding-status");
45
- const summary = document.getElementById("flowpilot-summary");
46
- const categories = document.getElementById("flowpilot-categories");
 
 
 
 
47
 
48
  init();
49
 
50
  async function init() {
51
- const stored = await chrome.storage.local.get("flowpilotBackendUrl");
52
- backendUrlInput.value = stored.flowpilotBackendUrl || "https://technophyle-flow-pilot.hf.space/api";
53
- descriptionInput.value = defaultDescription;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
- saveUrlButton.addEventListener("click", saveBackendUrl);
56
- analyzeButton.addEventListener("click", analyzeBusiness);
57
  }
58
 
59
  async function saveBackendUrl() {
60
- const nextUrl = backendUrlInput.value.trim().replace(/\/$/, "");
 
61
  if (!nextUrl) {
62
- setStatus("Enter a backend URL ending in /api.", true);
63
- return false;
 
64
  }
65
  if (!isSupportedBackendUrl(nextUrl)) {
66
  setStatus("Use either http://localhost:8000/api or an https://...hf.space/api backend.", true);
67
- return false;
 
68
  }
69
  await chrome.storage.local.set({ flowpilotBackendUrl: nextUrl });
70
- setStatus(`Backend saved: ${nextUrl}`);
71
- return true;
 
 
72
  }
73
 
74
  async function analyzeBusiness() {
75
- const description = descriptionInput.value.trim();
76
- if (!description) {
77
  setStatus("Add a short business description before running analysis.", true);
 
 
 
 
 
 
78
  return;
79
  }
80
 
81
- analyzeButton.disabled = true;
82
- analyzeButton.textContent = "Analyzing...";
83
- setStatus("Running analysis against the configured backend.");
84
 
85
  try {
86
- const saved = await saveBackendUrl();
87
- if (!saved) {
88
- return;
89
- }
90
  const payload = await apiRequest("/analyze", {
91
  method: "POST",
92
  body: JSON.stringify({
93
- owner_id: "extension-demo-owner",
94
- owner_email: "owner@example.com",
95
- description
96
  })
97
  });
98
- renderAnalysis(payload);
99
- setStatus("Analysis loaded into the extension.");
 
 
 
100
  } catch (error) {
101
  setStatus(error.message || "Could not reach the backend.", true);
102
- } finally {
103
- analyzeButton.disabled = false;
104
- analyzeButton.textContent = "Analyze My Process";
105
- }
106
- }
107
-
108
- function renderAnalysis(payload) {
109
- summary.textContent = payload.summary || "Analysis complete.";
110
- categories.innerHTML = [
111
- TaskCategoryGroup(
112
- "Can Be Fully Automated",
113
- (payload.tasks?.fully_automatable || []).map((task) => task.name)
114
- ),
115
- TaskCategoryGroup(
116
- "AI-Assisted",
117
- (payload.tasks?.ai_assisted || []).map((task) => task.name)
118
- ),
119
- TaskCategoryGroup(
120
- "Keep Manual",
121
- (payload.tasks?.manual || []).map((task) => task.name)
122
- )
123
- ].join("");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  }
125
 
126
  function setStatus(message, isError = false) {
127
- onboardingStatus.textContent = message;
128
- onboardingStatus.classList.toggle("error-copy", isError);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  }
130
 
131
  function isSupportedBackendUrl(value) {
@@ -138,3 +476,50 @@ function isSupportedBackendUrl(value) {
138
  return false;
139
  }
140
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import { CategorizationScreen } from "./components/CategorizationScreen.js";
3
  import { CustomTaskInput } from "./components/CustomTaskInput.js";
4
  import { WorkflowPicker } from "./components/WorkflowPicker.js";
 
 
5
  import { DeployingScreen } from "./components/DeployingScreen.js";
6
  import { DashboardScreen } from "./components/DashboardScreen.js";
7
  import { EscalationPanel } from "./components/EscalationPanel.js";
8
  import { apiRequest } from "./hooks/useApi.js";
 
9
 
10
  const root = document.getElementById("root");
11
  const defaultDescription =
12
  "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.";
13
+ const defaultBackendUrl = "http://localhost:8000/api";
14
+ const setupSteps = [
15
+ { id: 1, label: "Describe" },
16
+ { id: 2, label: "Review" },
17
+ { id: 3, label: "Rule" },
18
+ { id: 4, label: "Modes" },
19
+ { id: 5, label: "Launch" }
 
 
 
 
20
  ];
21
 
22
+ const state = {
23
+ view: "setup",
24
+ currentStep: 1,
25
+ settingsOpen: false,
26
+ backendUrl: defaultBackendUrl,
27
+ ownerEmail: "",
28
+ ownerId: "",
29
+ description: defaultDescription,
30
+ analysis: null,
31
+ selectedTaskIds: [],
32
+ customRule: "When a restaurant client emails, flag it as priority and move it to the top of my orders sheet.",
33
+ workflowSelections: {},
34
+ workflowCursor: 0,
35
+ liveStats: {
36
+ activeWorkflows: 0,
37
+ executionsToday: 0,
38
+ pendingEscalations: 0
39
+ },
40
+ escalations: [],
41
+ status: "Ready.",
42
+ statusError: false
43
+ };
44
 
45
  init();
46
 
47
  async function init() {
48
+ const profile = await getIdentityProfile();
49
+ if (profile) {
50
+ await chrome.storage.local.set({ flowpilotOwnerProfile: profile });
51
+ }
52
+ const stored = await chrome.storage.local.get(["flowpilotBackendUrl", "flowpilotOwnerProfile"]);
53
+ state.backendUrl = stored.flowpilotBackendUrl || defaultBackendUrl;
54
+ state.ownerEmail = stored.flowpilotOwnerProfile?.email || "";
55
+ state.ownerId = stored.flowpilotOwnerProfile?.ownerId || "";
56
+ if (!state.ownerEmail) {
57
+ setStatus("Sign into Chrome with Google to let FlowPilot identify the mailbox.", false);
58
+ }
59
+ await refreshLiveData();
60
+ render();
61
+ }
62
+
63
+ function render() {
64
+ root.innerHTML = `
65
+ <div class="app-shell ${state.view === "quiet" ? "app-live" : ""} ${state.view === "escalation" ? "app-alert" : ""}">
66
+ <div class="sidebar-shell">
67
+ ${renderHeader()}
68
+ ${state.settingsOpen ? renderSettings() : ""}
69
+ <main class="screen-stage">${renderBody()}</main>
70
+ </div>
71
+ </div>
72
+ `;
73
+ bindEvents();
74
+ }
75
+
76
+ function renderHeader() {
77
+ const indicator = getViewIndicator();
78
+ return `
79
+ <header class="topbar surface-card">
80
+ <div class="brand-lockup">
81
+ <div class="brand-mark">F</div>
82
+ <div>
83
+ <p class="brand-name">FlowPilot</p>
84
+ <p class="brand-subtitle">${escapeHtml(indicator.label)}</p>
85
+ </div>
86
+ </div>
87
+ </header>
88
+ `;
89
+ }
90
+
91
+ function renderSettings() {
92
+ return `
93
+ <section class="settings-panel surface-card">
94
+ <label class="field-label" for="flowpilot-backend-url">Backend URL</label>
95
+ <input id="flowpilot-backend-url" class="text-input" value="${escapeAttribute(state.backendUrl)}" />
96
+ <div class="button-row split-actions compact-actions">
97
+ <button id="flowpilot-close-settings" class="ghost-button">Close</button>
98
+ <button id="flowpilot-save-url" class="secondary-button">Save</button>
99
+ </div>
100
+ </section>
101
+ `;
102
+ }
103
+
104
+ function renderBody() {
105
+ if (state.view === "escalation") {
106
+ return EscalationPanel({ escalation: state.escalations[0] || null });
107
+ }
108
+ if (state.view === "quiet") {
109
+ return DashboardScreen({ stats: state.liveStats });
110
+ }
111
+
112
+ if (state.currentStep === 1) {
113
+ return OnboardingScreen({
114
+ description: state.description,
115
+ status: state.status,
116
+ statusError: state.statusError,
117
+ ownerEmail: state.ownerEmail
118
+ });
119
+ }
120
+ if (state.currentStep === 2) {
121
+ return CategorizationScreen({
122
+ summary: state.analysis?.summary || "Review the suggested workstreams.",
123
+ groups: buildTaskGroups(),
124
+ selectedTaskIds: state.selectedTaskIds
125
+ });
126
+ }
127
+ if (state.currentStep === 3) {
128
+ return CustomTaskInput({ value: state.customRule });
129
+ }
130
+ if (state.currentStep === 4) {
131
+ const tasks = getWorkflowTasks();
132
+ const task = tasks[state.workflowCursor];
133
+ return WorkflowPicker({
134
+ task,
135
+ selectedOption: task ? state.workflowSelections[task.id] || null : null,
136
+ current: state.workflowCursor + 1,
137
+ total: tasks.length
138
+ });
139
+ }
140
+ return DeployingScreen();
141
+ }
142
+
143
+ function bindEvents() {
144
+ document.getElementById("flowpilot-save-url")?.addEventListener("click", saveBackendUrl);
145
+ document.getElementById("flowpilot-close-settings")?.addEventListener("click", toggleSettings);
146
+ document.getElementById("flowpilot-open-settings")?.addEventListener("click", toggleSettings);
147
+ document.getElementById("flowpilot-back-button")?.addEventListener("click", goBack);
148
+ document.getElementById("flowpilot-back-dashboard")?.addEventListener("click", () => {
149
+ state.view = "quiet";
150
+ render();
151
+ });
152
+
153
+ if (state.view === "setup" && state.currentStep === 1) {
154
+ document.getElementById("flowpilot-business-description")?.addEventListener("input", (event) => {
155
+ state.description = event.target.value;
156
+ });
157
+ document.getElementById("flowpilot-analyze-button")?.addEventListener("click", analyzeBusiness);
158
+ }
159
+
160
+ if (state.view === "setup" && state.currentStep === 2) {
161
+ document.querySelectorAll("[data-task-id]").forEach((input) => {
162
+ input.addEventListener("change", toggleTaskSelection);
163
+ });
164
+ document.getElementById("flowpilot-continue-tasks")?.addEventListener("click", continueFromTasks);
165
+ }
166
+
167
+ if (state.view === "setup" && state.currentStep === 3) {
168
+ document.getElementById("flowpilot-custom-rule")?.addEventListener("input", (event) => {
169
+ state.customRule = event.target.value;
170
+ });
171
+ document.getElementById("flowpilot-skip-rule")?.addEventListener("click", goToWorkflowStep);
172
+ document.getElementById("flowpilot-save-rule")?.addEventListener("click", goToWorkflowStep);
173
+ }
174
+
175
+ if (state.view === "setup" && state.currentStep === 4) {
176
+ document.querySelectorAll("[data-workflow-option]").forEach((button) => {
177
+ button.addEventListener("click", selectWorkflowOption);
178
+ });
179
+ }
180
+
181
+ if (state.view === "setup" && state.currentStep === 5) {
182
+ document.getElementById("flowpilot-deploy-button")?.addEventListener("click", completeSetup);
183
+ }
184
+
185
+ if (state.view === "quiet") {
186
+ document.getElementById("flowpilot-open-escalation")?.addEventListener("click", () => {
187
+ if (!state.escalations.length) {
188
+ return;
189
+ }
190
+ state.view = "escalation";
191
+ render();
192
+ });
193
+ document.getElementById("flowpilot-stop-automation")?.addEventListener("click", stopAutomation);
194
+ }
195
+
196
+ if (state.view === "escalation") {
197
+ document.getElementById("flowpilot-resolve-escalation")?.addEventListener("click", () => resolveEscalation("approve"));
198
+ document.getElementById("flowpilot-ask-customer")?.addEventListener("click", () => resolveEscalation("ask_customer"));
199
+ }
200
+ }
201
+
202
+ function toggleSettings() {
203
+ state.settingsOpen = !state.settingsOpen;
204
+ render();
205
+ }
206
+
207
+ function goBack() {
208
+ if (state.view !== "setup") {
209
+ return;
210
+ }
211
+
212
+ if (state.currentStep === 2) {
213
+ state.currentStep = 1;
214
+ } else if (state.currentStep === 3) {
215
+ state.currentStep = 2;
216
+ } else if (state.currentStep === 4) {
217
+ if (state.workflowCursor > 0) {
218
+ state.workflowCursor -= 1;
219
+ } else {
220
+ state.currentStep = 3;
221
+ }
222
+ } else if (state.currentStep === 5) {
223
+ if (getWorkflowTasks().length) {
224
+ state.currentStep = 4;
225
+ state.workflowCursor = Math.max(getWorkflowTasks().length - 1, 0);
226
+ } else {
227
+ state.currentStep = 3;
228
+ }
229
+ }
230
 
231
+ render();
 
232
  }
233
 
234
  async function saveBackendUrl() {
235
+ const input = document.getElementById("flowpilot-backend-url");
236
+ const nextUrl = (input?.value || "").trim().replace(/\/$/, "");
237
  if (!nextUrl) {
238
+ setStatus("Enter a backend URL.", true);
239
+ render();
240
+ return;
241
  }
242
  if (!isSupportedBackendUrl(nextUrl)) {
243
  setStatus("Use either http://localhost:8000/api or an https://...hf.space/api backend.", true);
244
+ render();
245
+ return;
246
  }
247
  await chrome.storage.local.set({ flowpilotBackendUrl: nextUrl });
248
+ state.backendUrl = nextUrl;
249
+ state.settingsOpen = false;
250
+ setStatus("Backend saved.");
251
+ render();
252
  }
253
 
254
  async function analyzeBusiness() {
255
+ if (!state.description.trim()) {
 
256
  setStatus("Add a short business description before running analysis.", true);
257
+ render();
258
+ return;
259
+ }
260
+ if (!state.ownerEmail || !state.ownerId) {
261
+ setStatus("Could not detect the signed-in Gmail account yet.", true);
262
+ render();
263
  return;
264
  }
265
 
266
+ setStatus("Analyzing...");
267
+ render();
 
268
 
269
  try {
 
 
 
 
270
  const payload = await apiRequest("/analyze", {
271
  method: "POST",
272
  body: JSON.stringify({
273
+ owner_id: state.ownerId,
274
+ owner_email: state.ownerEmail,
275
+ description: state.description.trim()
276
  })
277
  });
278
+ state.analysis = payload;
279
+ state.selectedTaskIds = getDefaultSelectedTaskIds(payload);
280
+ state.currentStep = 2;
281
+ setStatus("Ready.");
282
+ await refreshLiveData();
283
  } catch (error) {
284
  setStatus(error.message || "Could not reach the backend.", true);
285
+ }
286
+ render();
287
+ }
288
+
289
+ function toggleTaskSelection(event) {
290
+ const taskId = event.target.dataset.taskId;
291
+ if (!taskId) {
292
+ return;
293
+ }
294
+ if (event.target.checked) {
295
+ if (!state.selectedTaskIds.includes(taskId)) {
296
+ state.selectedTaskIds.push(taskId);
297
+ }
298
+ } else {
299
+ state.selectedTaskIds = state.selectedTaskIds.filter((id) => id !== taskId);
300
+ }
301
+ }
302
+
303
+ function continueFromTasks() {
304
+ if (!state.selectedTaskIds.length) {
305
+ setStatus("Select at least one task to continue.", true);
306
+ render();
307
+ return;
308
+ }
309
+ state.currentStep = 3;
310
+ setStatus("Ready.");
311
+ render();
312
+ }
313
+
314
+ function goToWorkflowStep() {
315
+ state.currentStep = 4;
316
+ state.workflowCursor = 0;
317
+ render();
318
+ }
319
+
320
+ function selectWorkflowOption(event) {
321
+ const optionId = event.currentTarget.dataset.workflowOption;
322
+ const taskId = event.currentTarget.dataset.taskId;
323
+ if (!optionId || !taskId) {
324
+ return;
325
+ }
326
+ state.workflowSelections[taskId] = optionId;
327
+ if (state.workflowCursor < getWorkflowTasks().length - 1) {
328
+ state.workflowCursor += 1;
329
+ render();
330
+ return;
331
+ }
332
+ state.currentStep = 5;
333
+ render();
334
+ }
335
+
336
+ async function completeSetup() {
337
+ await refreshLiveData();
338
+ state.view = "quiet";
339
+ render();
340
+ }
341
+
342
+ async function stopAutomation() {
343
+ if (!state.ownerId) {
344
+ return;
345
+ }
346
+ try {
347
+ await apiRequest("/stop", {
348
+ method: "POST",
349
+ body: JSON.stringify({
350
+ owner_id: state.ownerId
351
+ })
352
+ });
353
+ await refreshLiveData();
354
+ setStatus("Automation stopped.");
355
+ } catch (error) {
356
+ setStatus(error.message || "Could not stop automation.", true);
357
+ }
358
+ render();
359
+ }
360
+
361
+ async function resolveEscalation(response) {
362
+ const escalation = state.escalations[0];
363
+ if (!escalation) {
364
+ state.view = "quiet";
365
+ render();
366
+ return;
367
+ }
368
+ try {
369
+ await apiRequest("/escalation-reply", {
370
+ method: "POST",
371
+ body: JSON.stringify({
372
+ escalation_id: escalation.id,
373
+ response
374
+ })
375
+ });
376
+ } catch (error) {
377
+ setStatus(error.message || "Could not resolve escalation.", true);
378
+ }
379
+ await refreshLiveData();
380
+ state.view = "quiet";
381
+ render();
382
+ }
383
+
384
+ function getDefaultSelectedTaskIds(analysis) {
385
+ return [
386
+ ...(analysis?.tasks?.fully_automatable || []),
387
+ ...(analysis?.tasks?.ai_assisted || [])
388
+ ].map((task) => task.id);
389
+ }
390
+
391
+ function buildTaskGroups() {
392
+ return [
393
+ {
394
+ title: "Automate",
395
+ items: state.analysis?.tasks?.fully_automatable || []
396
+ },
397
+ {
398
+ title: "Assist",
399
+ items: state.analysis?.tasks?.ai_assisted || []
400
+ },
401
+ {
402
+ title: "Manual",
403
+ items: state.analysis?.tasks?.manual || []
404
+ }
405
+ ];
406
+ }
407
+
408
+ function getWorkflowTasks() {
409
+ const lookup = new Map();
410
+ for (const group of buildTaskGroups()) {
411
+ for (const task of group.items) {
412
+ lookup.set(task.id, task);
413
+ }
414
+ }
415
+ return state.selectedTaskIds.map((id) => lookup.get(id)).filter(Boolean);
416
+ }
417
+
418
+ function getViewIndicator() {
419
+ if (state.view === "escalation") {
420
+ return { label: "Needs review" };
421
+ }
422
+ if (state.view === "quiet") {
423
+ return { label: "Running live" };
424
+ }
425
+ return { label: `${setupSteps[state.currentStep - 1].label}` };
426
  }
427
 
428
  function setStatus(message, isError = false) {
429
+ state.status = message;
430
+ state.statusError = isError;
431
+ }
432
+
433
+ async function refreshLiveData() {
434
+ if (!state.ownerId) {
435
+ state.liveStats = {
436
+ activeWorkflows: 0,
437
+ executionsToday: 0,
438
+ pendingEscalations: 0
439
+ };
440
+ state.escalations = [];
441
+ return;
442
+ }
443
+
444
+ try {
445
+ const [statusPayload, escalationPayload] = await Promise.all([
446
+ apiRequest(`/status?owner_id=${encodeURIComponent(state.ownerId)}`),
447
+ apiRequest(`/escalations?owner_id=${encodeURIComponent(state.ownerId)}`)
448
+ ]);
449
+ const workflows = statusPayload.workflows || [];
450
+ const executions = statusPayload.recent_executions || [];
451
+ const pendingEscalations = (escalationPayload.items || []).filter((item) => item.status === "pending");
452
+
453
+ state.liveStats = {
454
+ activeWorkflows: workflows.filter((item) => item.status === "active").length,
455
+ executionsToday: executions.filter((item) => isToday(item.executed_at)).length,
456
+ pendingEscalations: pendingEscalations.length
457
+ };
458
+ state.escalations = pendingEscalations;
459
+ } catch {
460
+ state.liveStats = {
461
+ activeWorkflows: 0,
462
+ executionsToday: 0,
463
+ pendingEscalations: 0
464
+ };
465
+ state.escalations = [];
466
+ }
467
  }
468
 
469
  function isSupportedBackendUrl(value) {
 
476
  return false;
477
  }
478
  }
479
+
480
+ function escapeAttribute(value) {
481
+ return String(value).replaceAll("&", "&amp;").replaceAll('"', "&quot;");
482
+ }
483
+
484
+ function escapeHtml(value) {
485
+ return String(value)
486
+ .replaceAll("&", "&amp;")
487
+ .replaceAll("<", "&lt;")
488
+ .replaceAll(">", "&gt;")
489
+ .replaceAll('"', "&quot;")
490
+ .replaceAll("'", "&#39;");
491
+ }
492
+
493
+ function isToday(value) {
494
+ if (!value) {
495
+ return false;
496
+ }
497
+ const candidate = new Date(value);
498
+ const now = new Date();
499
+ return (
500
+ candidate.getFullYear() === now.getFullYear() &&
501
+ candidate.getMonth() === now.getMonth() &&
502
+ candidate.getDate() === now.getDate()
503
+ );
504
+ }
505
+
506
+ async function getIdentityProfile() {
507
+ if (!chrome.identity?.getProfileUserInfo) {
508
+ return null;
509
+ }
510
+
511
+ try {
512
+ const profile = await new Promise((resolve) => {
513
+ chrome.identity.getProfileUserInfo({ accountStatus: "ANY" }, resolve);
514
+ });
515
+ if (!profile.email) {
516
+ return null;
517
+ }
518
+ return {
519
+ email: profile.email,
520
+ ownerId: profile.id || profile.email.toLowerCase()
521
+ };
522
+ } catch {
523
+ return null;
524
+ }
525
+ }
extension/sidebar/components/CategorizationScreen.js CHANGED
@@ -1,10 +1,49 @@
1
- export function CategorizationScreen() {
2
  return `
3
- <section class="screen-card" id="flowpilot-categorization-card">
4
- <div class="step-label">2. AI Categorization</div>
5
- <h2>Here’s what FlowPilot found</h2>
6
- <p id="flowpilot-summary" class="muted">Run the analysis above to load real recommendations from the backend.</p>
7
- <div id="flowpilot-categories"></div>
 
 
 
 
 
 
 
 
 
8
  </section>
9
  `;
10
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function CategorizationScreen({ summary, groups, selectedTaskIds }) {
2
  return `
3
+ <section class="screen-card">
4
+ <div class="screen-intro">
5
+ <p class="step-label">Step 2</p>
6
+ <h2>Review suggestions</h2>
7
+ <p id="flowpilot-summary" class="screen-copy">${escapeHtml(summary)}</p>
8
+ </div>
9
+ <div class="section-topline"><span class="metric-badge">${selectedTaskIds.length} selected</span></div>
10
+ <div id="flowpilot-categories" class="category-grid">
11
+ ${groups.map((group) => renderGroup(group, selectedTaskIds)).join("")}
12
+ </div>
13
+ <div class="button-row split-actions">
14
+ <button id="flowpilot-back-button" class="ghost-button">Back</button>
15
+ <button id="flowpilot-continue-tasks" class="primary-button">Continue to rules</button>
16
+ </div>
17
  </section>
18
  `;
19
  }
20
+
21
+ function renderGroup(group, selectedTaskIds) {
22
+ return `
23
+ <div class="category-group">
24
+ <div class="group-header">
25
+ <p class="group-title">${escapeHtml(group.title)}</p>
26
+ <span class="group-count">${group.items.length}</span>
27
+ </div>
28
+ <ul class="selection-list">
29
+ ${group.items.length ? group.items.map((task) => `
30
+ <li>
31
+ <label class="selection-tile">
32
+ <input type="checkbox" data-task-id="${escapeHtml(task.id)}" ${selectedTaskIds.includes(task.id) ? "checked" : ""} />
33
+ <span>${escapeHtml(task.name)}</span>
34
+ </label>
35
+ </li>
36
+ `).join("") : `<li class="empty-copy">No suggestions in this category.</li>`}
37
+ </ul>
38
+ </div>
39
+ `;
40
+ }
41
+
42
+ function escapeHtml(value) {
43
+ return String(value)
44
+ .replaceAll("&", "&amp;")
45
+ .replaceAll("<", "&lt;")
46
+ .replaceAll(">", "&gt;")
47
+ .replaceAll('"', "&quot;")
48
+ .replaceAll("'", "&#39;");
49
+ }
extension/sidebar/components/ConnectSheet.js CHANGED
@@ -1,8 +1,8 @@
1
  export function ConnectSheet() {
2
  return `
3
  <section class="screen-card">
4
- <div class="step-label">5. Connect Data</div>
5
- <h2>Connect Google Sheets</h2>
6
  <div class="status-row"><span>Inventory sheet</span><strong>Inventory</strong></div>
7
  <div class="status-row"><span>Orders sheet</span><strong>Orders</strong></div>
8
  <button class="secondary-button">Connect Sheet</button>
 
1
  export function ConnectSheet() {
2
  return `
3
  <section class="screen-card">
4
+ <div class="step-label">Data</div>
5
+ <h2>Google Sheets</h2>
6
  <div class="status-row"><span>Inventory sheet</span><strong>Inventory</strong></div>
7
  <div class="status-row"><span>Orders sheet</span><strong>Orders</strong></div>
8
  <button class="secondary-button">Connect Sheet</button>
extension/sidebar/components/CustomTaskInput.js CHANGED
@@ -1,9 +1,26 @@
1
- export function CustomTaskInput() {
2
  return `
3
  <section class="screen-card">
4
- <div class="step-label">3. Custom Task</div>
5
- <h2>Add something specific</h2>
6
- <input class="text-input" value="When a restaurant client emails, flag it as priority and move it to the top of my orders sheet." />
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  </section>
8
  `;
9
  }
 
 
 
 
 
1
+ export function CustomTaskInput({ value }) {
2
  return `
3
  <section class="screen-card">
4
+ <div class="screen-intro">
5
+ <p class="step-label">Step 3</p>
6
+ <h2>Add one custom instruction</h2>
7
+ </div>
8
+ <label class="field-label" for="flowpilot-custom-rule">Custom rule</label>
9
+ <input
10
+ id="flowpilot-custom-rule"
11
+ class="text-input"
12
+ value="${escapeAttribute(value)}"
13
+ placeholder="Optional"
14
+ />
15
+ <div class="button-row split-actions">
16
+ <button id="flowpilot-back-button" class="ghost-button">Back</button>
17
+ <button id="flowpilot-skip-rule" class="ghost-button">Skip</button>
18
+ <button id="flowpilot-save-rule" class="primary-button">Continue</button>
19
+ </div>
20
  </section>
21
  `;
22
  }
23
+
24
+ function escapeAttribute(value) {
25
+ return String(value).replaceAll("&", "&amp;").replaceAll('"', "&quot;");
26
+ }
extension/sidebar/components/DashboardScreen.js CHANGED
@@ -1,18 +1,39 @@
1
- import { StatusBadge } from "./StatusBadge.js";
2
-
3
- export function DashboardScreen() {
4
  return `
5
- <section class="screen-card">
6
- <div class="step-label">8. Dashboard</div>
7
- <h2>Dashboard Preview</h2>
8
- <p class="muted">Real workflow health and counts will appear here after deploy/status wiring.</p>
9
- <div class="dashboard-row">
10
- <span>3 active workflows</span>
11
- ${StatusBadge("active")}
12
  </div>
13
- <div class="dashboard-row">
14
- <span>12 orders processed this week</span>
15
- <strong>4.5h saved</strong>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  </div>
17
  </section>
18
  `;
 
1
+ export function DashboardScreen({ stats }) {
2
+ const metricLine = `${stats.executionsToday} runs today`;
3
+ const isLive = stats.activeWorkflows > 0;
4
  return `
5
+ <section class="screen-card dashboard-card">
6
+ <div class="screen-intro">
7
+ <p class="step-label">Live operations</p>
8
+ <h2>${isLive ? "Your inbox operator is live" : "Automation is stopped"}</h2>
 
 
 
9
  </div>
10
+ <div class="dashboard-hero">
11
+ <div>
12
+ <p class="dashboard-kicker">Today</p>
13
+ <p class="dashboard-metric">${metricLine}</p>
14
+ </div>
15
+ <span class="topbar-status ${isLive ? "status-success" : "status-warning"}">
16
+ <span class="status-dot"></span>
17
+ <span>${isLive ? "Stable" : "Stopped"}</span>
18
+ </span>
19
+ </div>
20
+ <div class="dashboard-stats">
21
+ <div class="stat-card">
22
+ <span>Active workflows</span>
23
+ <strong>${stats.activeWorkflows}</strong>
24
+ </div>
25
+ <div class="stat-card">
26
+ <span>Pending escalations</span>
27
+ <strong>${stats.pendingEscalations}</strong>
28
+ </div>
29
+ <div class="stat-card">
30
+ <span>Runs completed</span>
31
+ <strong>${stats.executionsToday}</strong>
32
+ </div>
33
+ </div>
34
+ <div class="button-row split-actions compact-actions">
35
+ ${stats.pendingEscalations ? `<button id="flowpilot-open-escalation" class="secondary-button">Open escalation</button>` : ""}
36
+ ${isLive ? `<button id="flowpilot-stop-automation" class="ghost-button">Stop automation</button>` : ""}
37
  </div>
38
  </section>
39
  `;
extension/sidebar/components/DeployingScreen.js CHANGED
@@ -1,14 +1,22 @@
1
  export function DeployingScreen() {
2
  return `
3
- <section class="screen-card">
4
- <div class="step-label">7. Deploy</div>
5
- <h2>Deployment Preview</h2>
6
- <p class="muted">This section is a visual stub until deploy actions are connected.</p>
7
- <ul class="status-list">
8
- <li>Order Processing (Full Auto) deployed</li>
9
- <li>Weekly Order Summary deployed for Fridays at 8:00 AM</li>
10
- <li>Restaurant Priority Flag deployed</li>
11
- </ul>
 
 
 
 
 
 
 
 
12
  </section>
13
  `;
14
  }
 
1
  export function DeployingScreen() {
2
  return `
3
+ <section class="screen-card launch-card">
4
+ <div class="screen-intro">
5
+ <p class="step-label">Step 5</p>
6
+ <h2>Launch</h2>
7
+ </div>
8
+ <div class="launch-panel compact-panel">
9
+ <p class="launch-title">Quiet execution</p>
10
+ <div class="deploy-status">
11
+ <span class="live-dot"></span>
12
+ <span class="live-dot"></span>
13
+ <span class="live-dot"></span>
14
+ </div>
15
+ </div>
16
+ <div class="button-row split-actions">
17
+ <button id="flowpilot-back-button" class="ghost-button">Back</button>
18
+ <button id="flowpilot-deploy-button" class="primary-button">Go live</button>
19
+ </div>
20
  </section>
21
  `;
22
  }
extension/sidebar/components/EscalationPanel.js CHANGED
@@ -1,13 +1,53 @@
1
- export function EscalationPanel() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  return `
3
- <section class="screen-card accent-card">
4
- <div class="step-label">9. Escalations</div>
5
- <h2>Decision needed</h2>
6
- <p>An order requests mixed varieties without exact quantities.</p>
7
- <div class="action-row">
8
- <button class="primary-button">Ask Customer</button>
9
- <button class="ghost-button">Handle Myself</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  </div>
11
  </section>
12
  `;
13
  }
 
 
 
 
 
 
 
 
 
 
1
+ export function EscalationPanel({ escalation }) {
2
+ if (!escalation) {
3
+ return `
4
+ <section class="screen-card">
5
+ <div class="screen-intro">
6
+ <p class="step-label">Escalations</p>
7
+ <h2>No pending escalations</h2>
8
+ </div>
9
+ <div class="button-row compact-actions">
10
+ <button id="flowpilot-back-dashboard" class="ghost-button">Back</button>
11
+ </div>
12
+ </section>
13
+ `;
14
+ }
15
+
16
+ const reason = escapeHtml(escalation.reason || "Needs review");
17
+ const source = escapeHtml(escalation.context?.source_email || escalation.context?.email_body || "No email preview available.");
18
+ const issue = escapeHtml(escalation.context?.message || escalation.reason || "FlowPilot paused this action.");
19
  return `
20
+ <section class="screen-card escalation-card">
21
+ <div class="screen-intro">
22
+ <p class="step-label">Needs your input</p>
23
+ <h2>${reason}</h2>
24
+ </div>
25
+ <div class="escalation-grid">
26
+ <div class="escalation-block">
27
+ <p class="field-label">Original email</p>
28
+ <p>${source}</p>
29
+ </div>
30
+ <div class="escalation-block">
31
+ <p class="field-label">Why it paused</p>
32
+ <p>${issue}</p>
33
+ </div>
34
+ </div>
35
+ <div class="action-row two-up">
36
+ <button id="flowpilot-ask-customer" class="primary-button">Ask customer</button>
37
+ <button id="flowpilot-resolve-escalation" class="ghost-button">Approve anyway</button>
38
+ </div>
39
+ <div class="button-row compact-actions">
40
+ <button id="flowpilot-back-dashboard" class="ghost-button">Back</button>
41
  </div>
42
  </section>
43
  `;
44
  }
45
+
46
+ function escapeHtml(value) {
47
+ return String(value)
48
+ .replaceAll("&", "&amp;")
49
+ .replaceAll("<", "&lt;")
50
+ .replaceAll(">", "&gt;")
51
+ .replaceAll('"', "&quot;")
52
+ .replaceAll("'", "&#39;");
53
+ }
extension/sidebar/components/FileUpload.js CHANGED
@@ -1,8 +1,8 @@
1
  export function FileUpload() {
2
  return `
3
  <section class="screen-card">
4
- <div class="step-label">6. Upload Data</div>
5
- <h2>Bring in price lists or external orders</h2>
6
  <div class="upload-box">
7
  <p>Supported: CSV, Excel, JSON</p>
8
  <button class="secondary-button">Upload File</button>
 
1
  export function FileUpload() {
2
  return `
3
  <section class="screen-card">
4
+ <div class="step-label">Files</div>
5
+ <h2>Upload data</h2>
6
  <div class="upload-box">
7
  <p>Supported: CSV, Excel, JSON</p>
8
  <button class="secondary-button">Upload File</button>
extension/sidebar/components/OnboardingScreen.js CHANGED
@@ -1,28 +1,31 @@
1
- export function OnboardingScreen() {
2
  return `
3
  <section class="screen-card">
4
- <div class="step-label">1. Describe Business</div>
5
- <h2>What does your business handle every day?</h2>
6
- <label class="field-label" for="flowpilot-backend-url">Backend URL</label>
7
- <input
8
- id="flowpilot-backend-url"
9
- class="text-input"
10
- placeholder="https://technophyle-flow-pilot.hf.space/api"
11
- />
12
- <p class="muted helper-copy">Supported today: localhost for development or a *.hf.space/api backend.</p>
13
- <div class="button-row">
14
- <button id="flowpilot-save-url" class="ghost-button">Save URL</button>
15
  </div>
16
- <label class="field-label" for="flowpilot-business-description">Business Description</label>
 
 
 
 
17
  <textarea
18
  id="flowpilot-business-description"
19
  class="input-area"
20
- placeholder="I run an apple orchard. Customers email me orders..."
21
- ></textarea>
22
- <div class="button-row">
23
- <button id="flowpilot-analyze-button" class="primary-button">Analyze My Process</button>
24
  </div>
25
- <p id="flowpilot-onboarding-status" class="muted status-copy">Connect the extension to your backend, then run analysis.</p>
26
  </section>
27
  `;
28
  }
 
 
 
 
 
 
 
 
1
+ export function OnboardingScreen({ description, status, statusError, ownerEmail }) {
2
  return `
3
  <section class="screen-card">
4
+ <div class="screen-intro">
5
+ <p class="step-label">Step 1</p>
6
+ <h2>Map how work moves through your inbox</h2>
 
 
 
 
 
 
 
 
7
  </div>
8
+ <div class="section-topline">
9
+ <div class="identity-chip">${ownerEmail ? escapeHtml(ownerEmail) : "Detecting Gmail account..."}</div>
10
+ <button id="flowpilot-open-settings" class="text-button" type="button">Backend</button>
11
+ </div>
12
+ <label class="field-label" for="flowpilot-business-description">Business description</label>
13
  <textarea
14
  id="flowpilot-business-description"
15
  class="input-area"
16
+ placeholder="Customers email orders, I check inventory, reply, and send a weekly summary."
17
+ >${escapeHtml(description)}</textarea>
18
+ <div class="button-row action-cluster compact-actions">
19
+ <button id="flowpilot-analyze-button" class="primary-button">Analyze business</button>
20
  </div>
21
+ <p id="flowpilot-onboarding-status" class="${statusError ? "status-copy error-copy" : "status-copy"}">${escapeHtml(status)}</p>
22
  </section>
23
  `;
24
  }
25
+
26
+ function escapeHtml(value) {
27
+ return String(value)
28
+ .replaceAll("&", "&amp;")
29
+ .replaceAll("<", "&lt;")
30
+ .replaceAll(">", "&gt;");
31
+ }
extension/sidebar/components/WorkflowCard.js CHANGED
@@ -1,14 +1,21 @@
1
- export function WorkflowCard(title, mode, steps, ownerInvolvement) {
 
2
  return `
3
- <article class="workflow-card">
4
- <p class="workflow-kicker">${title}</p>
 
 
 
 
 
 
 
 
5
  <h3>${mode}</h3>
6
  <ul>
7
  ${steps.map((step) => `<li>${step}</li>`).join("")}
8
  </ul>
9
- <p class="muted">You do: ${ownerInvolvement}</p>
10
- <button class="primary-button small-button" disabled>Choose</button>
11
- <p class="muted helper-copy">Preview only. Workflow selection is the next wiring step.</p>
12
- </article>
13
  `;
14
  }
 
1
+ export function WorkflowCard(taskId, optionId, mode, steps, ownerInvolvement, selectedOption) {
2
+ const isSelected = selectedOption === optionId;
3
  return `
4
+ <button
5
+ class="workflow-card ${isSelected ? "workflow-card-selected" : ""}"
6
+ data-task-id="${taskId}"
7
+ data-workflow-option="${optionId}"
8
+ aria-pressed="${isSelected ? "true" : "false"}"
9
+ >
10
+ <div class="workflow-card-topline">
11
+ <p class="workflow-kicker">Option ${optionId}</p>
12
+ <span class="selection-indicator ${isSelected ? "selection-indicator-on" : ""}">${isSelected ? "Selected" : "Pick"}</span>
13
+ </div>
14
  <h3>${mode}</h3>
15
  <ul>
16
  ${steps.map((step) => `<li>${step}</li>`).join("")}
17
  </ul>
18
+ <p class="muted workflow-owner">Owner touch: ${ownerInvolvement}</p>
19
+ </button>
 
 
20
  `;
21
  }
extension/sidebar/components/WorkflowPicker.js CHANGED
@@ -1,16 +1,42 @@
1
  import { WorkflowCard } from "./WorkflowCard.js";
2
 
3
- export function WorkflowPicker() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  return `
5
  <section class="screen-card">
6
- <div class="step-label">4. Pick Workflows</div>
7
- <h2>Order Processing</h2>
8
- <p class="muted">These cards are illustrative until we wire real workflow suggestions from the backend.</p>
 
 
9
  <div class="workflow-grid">
10
- ${WorkflowCard("Workflow A", "Full Auto", ["AI extracts order", "Checks sheet inventory", "Updates sheet and replies"], "Nothing unless alerted")}
11
- ${WorkflowCard("Workflow B", "Review First", ["AI extracts order", "Shows summary in sidebar", "You approve before sending"], "One click per order")}
12
- ${WorkflowCard("Workflow C", "Log Only", ["AI extracts order", "Adds to sheet", "You reply manually"], "Reply yourself")}
 
 
 
13
  </div>
14
  </section>
15
  `;
16
  }
 
 
 
 
 
 
 
 
1
  import { WorkflowCard } from "./WorkflowCard.js";
2
 
3
+ export function WorkflowPicker({ task, selectedOption, current, total }) {
4
+ if (!task) {
5
+ return `
6
+ <section class="screen-card">
7
+ <div class="screen-intro">
8
+ <p class="step-label">Step 4</p>
9
+ <h2>No workflow selected</h2>
10
+ </div>
11
+ <div class="button-row compact-actions">
12
+ <button id="flowpilot-back-button" class="ghost-button">Back</button>
13
+ </div>
14
+ </section>
15
+ `;
16
+ }
17
+
18
  return `
19
  <section class="screen-card">
20
+ <div class="screen-intro">
21
+ <p class="step-label">Step 4</p>
22
+ <h2>${escapeHtml(task.name)}</h2>
23
+ </div>
24
+ <div class="section-topline"><span class="metric-badge">${current}/${total}</span></div>
25
  <div class="workflow-grid">
26
+ ${WorkflowCard(task.id, "A", "Full Auto", ["Runs the task end to end", "Updates records automatically", "Sends the final customer reply"], "No review", selectedOption)}
27
+ ${WorkflowCard(task.id, "B", "Review First", ["Prepares the work for you", "Waits for a quick approval", "Sends only after review"], "One approval", selectedOption)}
28
+ ${WorkflowCard(task.id, "C", "Log Only", ["Captures request details", "Stores the context for later", "Leaves the customer reply to you"], "Manual reply", selectedOption)}
29
+ </div>
30
+ <div class="button-row compact-actions">
31
+ <button id="flowpilot-back-button" class="ghost-button">Back</button>
32
  </div>
33
  </section>
34
  `;
35
  }
36
+
37
+ function escapeHtml(value) {
38
+ return String(value)
39
+ .replaceAll("&", "&amp;")
40
+ .replaceAll("<", "&lt;")
41
+ .replaceAll(">", "&gt;");
42
+ }
extension/sidebar/styles/sidebar.css CHANGED
@@ -1,187 +1,516 @@
1
  :root {
2
- --bg: linear-gradient(180deg, #f7f2e8 0%, #efe4cf 100%);
3
- --panel: rgba(255, 252, 246, 0.92);
4
- --ink: #2f261d;
5
- --muted: #6a5b4d;
6
- --accent: #a44a1f;
7
- --accent-dark: #7b3513;
8
- --line: rgba(68, 50, 33, 0.12);
9
- --success: #2f6a44;
10
- font-family: Georgia, "Times New Roman", serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  }
12
 
13
  * {
14
  box-sizing: border-box;
15
  }
16
 
 
 
 
 
 
17
  body {
18
  margin: 0;
 
 
 
19
  color: var(--ink);
20
- background: var(--bg);
21
  }
22
 
23
- .sidebar-shell {
 
 
 
 
 
 
 
 
 
 
24
  min-height: 100vh;
25
- padding: 18px;
26
  }
27
 
28
- .hero {
29
- padding: 20px;
30
- border-radius: 22px;
31
  background:
32
- radial-gradient(circle at top left, rgba(255, 255, 255, 0.9), transparent 45%),
33
- linear-gradient(135deg, rgba(164, 74, 31, 0.16), rgba(67, 104, 68, 0.18));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  border: 1px solid var(--line);
 
 
 
 
 
 
 
35
  }
36
 
37
- .eyebrow,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  .step-label,
39
  .workflow-kicker,
40
- .group-title {
41
- text-transform: uppercase;
42
- letter-spacing: 0.08em;
43
- font-size: 11px;
44
- color: var(--muted);
 
 
 
 
 
 
 
 
 
 
 
 
45
  }
46
 
47
- .lede,
48
- .muted {
 
 
 
 
 
 
 
49
  color: var(--muted);
50
  }
51
 
52
- .screen-stack {
53
- display: grid;
54
- gap: 14px;
55
- margin-top: 14px;
56
  }
57
 
58
- .screen-card,
59
- .workflow-card {
60
- background: var(--panel);
61
- border: 1px solid var(--line);
62
- border-radius: 20px;
63
  padding: 16px;
64
- backdrop-filter: blur(10px);
65
  }
66
 
67
- .accent-card {
68
- border-color: rgba(164, 74, 31, 0.35);
 
 
69
  }
70
 
71
- .input-area,
72
- .text-input {
73
- width: 100%;
74
- border: 1px solid var(--line);
75
- border-radius: 14px;
76
- padding: 12px;
77
- background: rgba(255, 255, 255, 0.8);
78
- color: var(--ink);
79
  }
80
 
81
- .input-area {
82
- min-height: 128px;
 
 
 
 
 
 
 
 
 
83
  }
84
 
85
  .field-label {
86
  display: block;
87
- margin: 14px 0 8px;
 
 
 
 
 
 
 
 
 
 
 
88
  font-size: 12px;
89
- font-weight: 600;
90
- color: var(--muted);
91
- text-transform: uppercase;
92
- letter-spacing: 0.06em;
93
  }
94
 
95
- .workflow-grid,
96
- .action-row {
97
- display: grid;
98
- gap: 12px;
 
99
  }
100
 
101
- .button-row {
102
- display: flex;
103
- gap: 10px;
104
- margin-top: 12px;
 
 
105
  }
106
 
107
- .dashboard-row,
108
- .status-row {
109
  display: flex;
110
  align-items: center;
111
  justify-content: space-between;
112
- gap: 12px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  margin-top: 10px;
114
  }
115
 
 
 
 
 
 
 
 
 
116
  .primary-button,
117
  .secondary-button,
118
  .ghost-button {
119
- border: 0;
 
120
  border-radius: 999px;
121
- padding: 10px 14px;
122
  cursor: pointer;
123
- font: inherit;
124
  }
125
 
126
  .primary-button {
127
  background: var(--accent);
128
- color: #fff7f0;
 
129
  }
130
 
131
  .secondary-button {
132
- background: rgba(164, 74, 31, 0.12);
133
- color: var(--accent-dark);
 
134
  }
135
 
136
  .ghost-button {
137
- background: transparent;
138
- border: 1px solid var(--line);
139
  color: var(--ink);
 
140
  }
141
 
142
- .small-button {
143
- width: 100%;
 
 
 
144
  }
145
 
146
- .status-copy {
147
- margin: 12px 0 0;
 
 
 
148
  }
149
 
150
- .helper-copy {
151
- margin: 8px 0 0;
152
  font-size: 12px;
153
- line-height: 1.5;
154
  }
155
 
156
  .error-copy {
157
- color: #9e2f1f;
158
  }
159
 
160
- .status-badge {
161
- display: inline-flex;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  align-items: center;
163
- border-radius: 999px;
164
- padding: 6px 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  font-size: 12px;
166
- text-transform: capitalize;
167
  }
168
 
169
- .status-active {
170
- background: rgba(47, 106, 68, 0.16);
171
- color: var(--success);
 
 
172
  }
173
 
174
- .upload-box {
175
- border: 1px dashed var(--line);
176
- border-radius: 16px;
177
- padding: 18px;
178
  }
179
 
180
- button[disabled] {
181
- cursor: not-allowed;
182
- opacity: 0.55;
 
 
 
 
 
 
 
 
 
 
 
 
183
  }
184
 
185
- ul {
186
- padding-left: 18px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  }
 
1
  :root {
2
+ --bg: #eef3fb;
3
+ --bg-alt: #f7f9fc;
4
+ --surface: #ffffff;
5
+ --surface-muted: #f8fafc;
6
+ --ink: #0f172a;
7
+ --muted: #5b6b82;
8
+ --line: #d9e2ef;
9
+ --line-strong: #c3d1e3;
10
+ --accent: #2563eb;
11
+ --accent-strong: #1d4ed8;
12
+ --accent-soft: #dbeafe;
13
+ --accent-soft-strong: #bfdbfe;
14
+ --success: #15803d;
15
+ --success-soft: #dcfce7;
16
+ --warning: #b45309;
17
+ --warning-soft: #fef3c7;
18
+ --danger: #c2410c;
19
+ --shadow-lg: 0 16px 40px rgba(15, 23, 42, 0.08);
20
+ --shadow-md: 0 8px 24px rgba(15, 23, 42, 0.06);
21
+ --shadow-sm: 0 2px 10px rgba(15, 23, 42, 0.05);
22
+ --radius-xl: 22px;
23
+ --radius-lg: 16px;
24
+ --radius-md: 12px;
25
+ --radius-sm: 10px;
26
+ font-family: "Avenir Next", "Segoe UI", "Helvetica Neue", Arial, sans-serif;
27
  }
28
 
29
  * {
30
  box-sizing: border-box;
31
  }
32
 
33
+ html,
34
+ body {
35
+ min-height: 100%;
36
+ }
37
+
38
  body {
39
  margin: 0;
40
+ background:
41
+ radial-gradient(circle at top right, rgba(37, 99, 235, 0.08), transparent 34%),
42
+ linear-gradient(180deg, var(--bg), var(--bg-alt));
43
  color: var(--ink);
44
+ font: 14px/1.5 "Avenir Next", "Segoe UI", "Helvetica Neue", Arial, sans-serif;
45
  }
46
 
47
+ button,
48
+ input,
49
+ textarea {
50
+ font: inherit;
51
+ }
52
+
53
+ button {
54
+ border: 0;
55
+ }
56
+
57
+ .app-shell {
58
  min-height: 100vh;
 
59
  }
60
 
61
+ .app-shell.app-live {
 
 
62
  background:
63
+ radial-gradient(circle at top right, rgba(21, 128, 61, 0.06), transparent 34%),
64
+ linear-gradient(180deg, var(--bg), var(--bg-alt));
65
+ }
66
+
67
+ .app-shell.app-alert {
68
+ background:
69
+ radial-gradient(circle at top right, rgba(180, 83, 9, 0.07), transparent 34%),
70
+ linear-gradient(180deg, var(--bg), var(--bg-alt));
71
+ }
72
+
73
+ .sidebar-shell {
74
+ min-height: 100vh;
75
+ padding: 14px;
76
+ }
77
+
78
+ .surface-card,
79
+ .screen-card,
80
+ .settings-panel,
81
+ .workflow-card,
82
+ .category-group,
83
+ .launch-panel,
84
+ .selection-tile,
85
+ .stat-card,
86
+ .escalation-block {
87
+ background: var(--surface);
88
  border: 1px solid var(--line);
89
+ box-shadow: var(--shadow-md);
90
+ }
91
+
92
+ .topbar,
93
+ .settings-panel,
94
+ .screen-card {
95
+ border-radius: var(--radius-xl);
96
  }
97
 
98
+ .topbar {
99
+ display: flex;
100
+ align-items: center;
101
+ gap: 12px;
102
+ padding: 14px 16px;
103
+ margin-bottom: 10px;
104
+ }
105
+
106
+ .brand-lockup {
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 12px;
110
+ min-width: 0;
111
+ }
112
+
113
+ .brand-mark {
114
+ display: grid;
115
+ place-items: center;
116
+ width: 36px;
117
+ height: 36px;
118
+ border-radius: 12px;
119
+ background: linear-gradient(180deg, var(--accent), var(--accent-strong));
120
+ color: #fff;
121
+ font-size: 16px;
122
+ font-weight: 800;
123
+ }
124
+
125
+ .brand-name,
126
+ .brand-subtitle,
127
+ .field-label,
128
  .step-label,
129
  .workflow-kicker,
130
+ .dashboard-kicker,
131
+ .group-title,
132
+ .screen-card h2,
133
+ .screen-copy,
134
+ .status-copy,
135
+ .group-count,
136
+ .dashboard-metric,
137
+ .launch-title,
138
+ .workflow-card h3,
139
+ .workflow-card p {
140
+ margin: 0;
141
+ }
142
+
143
+ .brand-name {
144
+ font-size: 18px;
145
+ font-weight: 700;
146
+ line-height: 1.1;
147
  }
148
 
149
+ .brand-subtitle,
150
+ .field-label,
151
+ .step-label,
152
+ .workflow-kicker,
153
+ .dashboard-kicker {
154
+ font-size: 11px;
155
+ font-weight: 700;
156
+ letter-spacing: 0.08em;
157
+ text-transform: uppercase;
158
  color: var(--muted);
159
  }
160
 
161
+ .settings-panel {
162
+ margin-bottom: 10px;
163
+ padding: 14px;
 
164
  }
165
 
166
+ .screen-card {
167
+ min-height: calc(100vh - 92px);
 
 
 
168
  padding: 16px;
 
169
  }
170
 
171
+ .screen-intro {
172
+ display: grid;
173
+ gap: 4px;
174
+ margin-bottom: 12px;
175
  }
176
 
177
+ .screen-card h2,
178
+ .dashboard-metric,
179
+ .launch-title {
180
+ letter-spacing: -0.03em;
 
 
 
 
181
  }
182
 
183
+ .screen-card h2 {
184
+ font-size: 28px;
185
+ line-height: 1.08;
186
+ font-weight: 750;
187
+ }
188
+
189
+ .screen-copy,
190
+ .muted,
191
+ .settings-note,
192
+ .empty-copy {
193
+ color: var(--muted);
194
  }
195
 
196
  .field-label {
197
  display: block;
198
+ margin-bottom: 8px;
199
+ }
200
+
201
+ .identity-chip,
202
+ .metric-badge,
203
+ .group-count,
204
+ .selection-indicator {
205
+ display: inline-flex;
206
+ align-items: center;
207
+ min-height: 30px;
208
+ padding: 6px 11px;
209
+ border-radius: 999px;
210
  font-size: 12px;
211
+ font-weight: 700;
 
 
 
212
  }
213
 
214
+ .identity-chip,
215
+ .selection-indicator {
216
+ border: 1px solid var(--line);
217
+ background: var(--surface-muted);
218
+ color: var(--ink);
219
  }
220
 
221
+ .metric-badge,
222
+ .group-count,
223
+ .selection-indicator-on {
224
+ border: 1px solid var(--accent-soft-strong);
225
+ background: var(--accent-soft);
226
+ color: var(--accent-strong);
227
  }
228
 
229
+ .section-topline {
 
230
  display: flex;
231
  align-items: center;
232
  justify-content: space-between;
233
+ gap: 8px;
234
+ margin-bottom: 12px;
235
+ }
236
+
237
+ .text-button {
238
+ padding: 0;
239
+ background: transparent;
240
+ color: var(--muted);
241
+ font-size: 12px;
242
+ font-weight: 700;
243
+ cursor: pointer;
244
+ }
245
+
246
+ .text-button:hover {
247
+ color: var(--ink);
248
+ }
249
+
250
+ .text-input,
251
+ .input-area {
252
+ width: 100%;
253
+ border: 1px solid var(--line);
254
+ border-radius: 16px;
255
+ padding: 14px 15px;
256
+ background: var(--surface);
257
+ color: var(--ink);
258
+ outline: none;
259
+ transition: border-color 140ms ease, box-shadow 140ms ease;
260
+ }
261
+
262
+ .text-input:focus,
263
+ .input-area:focus {
264
+ border-color: var(--accent);
265
+ box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.12);
266
+ }
267
+
268
+ .input-area {
269
+ min-height: 220px;
270
+ resize: vertical;
271
+ }
272
+
273
+ .button-row,
274
+ .action-row {
275
+ display: flex;
276
+ gap: 10px;
277
+ margin-top: 14px;
278
+ }
279
+
280
+ .compact-actions {
281
  margin-top: 10px;
282
  }
283
 
284
+ .centered-actions {
285
+ justify-content: center;
286
+ }
287
+
288
+ .split-actions > * {
289
+ flex: 1;
290
+ }
291
+
292
  .primary-button,
293
  .secondary-button,
294
  .ghost-button {
295
+ min-height: 42px;
296
+ padding: 11px 16px;
297
  border-radius: 999px;
298
+ font-weight: 700;
299
  cursor: pointer;
300
+ transition: transform 140ms ease, box-shadow 140ms ease;
301
  }
302
 
303
  .primary-button {
304
  background: var(--accent);
305
+ color: #fff;
306
+ box-shadow: 0 8px 20px rgba(37, 99, 235, 0.22);
307
  }
308
 
309
  .secondary-button {
310
+ background: var(--accent-soft);
311
+ color: var(--accent-strong);
312
+ border: 1px solid var(--accent-soft-strong);
313
  }
314
 
315
  .ghost-button {
316
+ background: var(--surface);
 
317
  color: var(--ink);
318
+ border: 1px solid var(--line);
319
  }
320
 
321
+ .primary-button:hover,
322
+ .secondary-button:hover,
323
+ .ghost-button:hover,
324
+ .workflow-card:hover {
325
+ transform: translateY(-1px);
326
  }
327
 
328
+ button[disabled] {
329
+ opacity: 0.48;
330
+ cursor: not-allowed;
331
+ transform: none;
332
+ box-shadow: none;
333
  }
334
 
335
+ .status-copy {
336
+ margin-top: 10px;
337
  font-size: 12px;
 
338
  }
339
 
340
  .error-copy {
341
+ color: var(--danger);
342
  }
343
 
344
+ .category-grid,
345
+ .workflow-grid,
346
+ .dashboard-stats,
347
+ .escalation-grid {
348
+ display: grid;
349
+ gap: 10px;
350
+ }
351
+
352
+ .category-group,
353
+ .workflow-card,
354
+ .stat-card,
355
+ .launch-panel,
356
+ .escalation-block {
357
+ padding: 14px;
358
+ border-radius: var(--radius-lg);
359
+ }
360
+
361
+ .group-header,
362
+ .workflow-card-topline,
363
+ .dashboard-hero {
364
+ display: flex;
365
+ align-items: flex-start;
366
+ justify-content: space-between;
367
+ gap: 10px;
368
+ }
369
+
370
+ .selection-list,
371
+ .workflow-card ul {
372
+ margin: 10px 0 0;
373
+ padding-left: 0;
374
+ list-style: none;
375
+ }
376
+
377
+ .selection-list li + li,
378
+ .workflow-card li + li {
379
+ margin-top: 8px;
380
+ }
381
+
382
+ .selection-tile {
383
+ display: flex;
384
  align-items: center;
385
+ gap: 12px;
386
+ padding: 11px 12px;
387
+ border-radius: var(--radius-md);
388
+ background: var(--surface-muted);
389
+ }
390
+
391
+ .selection-tile input {
392
+ width: 18px;
393
+ height: 18px;
394
+ accent-color: var(--accent);
395
+ }
396
+
397
+ .selection-tile span {
398
+ font-weight: 600;
399
+ }
400
+
401
+ .workflow-card {
402
+ width: 100%;
403
+ text-align: left;
404
+ cursor: pointer;
405
+ }
406
+
407
+ .workflow-card-selected {
408
+ border-color: var(--accent-soft-strong);
409
+ background: #f8fbff;
410
+ box-shadow: 0 10px 24px rgba(37, 99, 235, 0.08);
411
+ }
412
+
413
+ .workflow-card h3 {
414
+ margin-top: 10px;
415
+ font-size: 20px;
416
+ font-weight: 700;
417
+ }
418
+
419
+ .workflow-owner {
420
+ margin-top: 10px;
421
+ }
422
+
423
+ .dashboard-hero {
424
+ align-items: center;
425
+ padding: 14px 16px;
426
+ border-radius: var(--radius-lg);
427
+ background: linear-gradient(180deg, #f8fbff, #f3f7ff);
428
+ border: 1px solid var(--accent-soft-strong);
429
+ }
430
+
431
+ .dashboard-metric {
432
+ margin-top: 4px;
433
+ font-size: 28px;
434
+ font-weight: 750;
435
+ }
436
+
437
+ .dashboard-stats {
438
+ grid-template-columns: repeat(3, minmax(0, 1fr));
439
+ margin-top: 12px;
440
+ }
441
+
442
+ .stat-card {
443
+ background: var(--surface-muted);
444
+ box-shadow: var(--shadow-sm);
445
+ }
446
+
447
+ .stat-card span {
448
+ display: block;
449
  font-size: 12px;
450
+ color: var(--muted);
451
  }
452
 
453
+ .stat-card strong {
454
+ display: block;
455
+ margin-top: 4px;
456
+ font-size: 26px;
457
+ font-weight: 700;
458
  }
459
 
460
+ .compact-panel {
461
+ display: flex;
462
+ align-items: center;
463
+ justify-content: space-between;
464
  }
465
 
466
+ .deploy-status {
467
+ display: flex;
468
+ gap: 8px;
469
+ }
470
+
471
+ .live-dot {
472
+ width: 10px;
473
+ height: 10px;
474
+ border-radius: 999px;
475
+ background: var(--success);
476
+ }
477
+
478
+ .two-up {
479
+ display: grid;
480
+ grid-template-columns: repeat(2, minmax(0, 1fr));
481
  }
482
 
483
+ @media (max-width: 560px) {
484
+ .sidebar-shell {
485
+ padding: 12px;
486
+ }
487
+
488
+ .topbar,
489
+ .settings-panel,
490
+ .screen-card {
491
+ border-radius: 20px;
492
+ }
493
+
494
+ .screen-card {
495
+ min-height: calc(100vh - 84px);
496
+ padding: 14px;
497
+ }
498
+
499
+ .screen-card h2 {
500
+ font-size: 24px;
501
+ }
502
+
503
+ .dashboard-stats,
504
+ .two-up,
505
+ .split-actions {
506
+ grid-template-columns: 1fr;
507
+ }
508
+
509
+ .button-row,
510
+ .action-row,
511
+ .section-topline,
512
+ .compact-panel {
513
+ flex-direction: column;
514
+ align-items: stretch;
515
+ }
516
  }