From ffd762bdae72fe4dda26377ec9ba00f59950cc0d Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:23:47 +0400 Subject: [PATCH] style: more consistent style across pages --- e2e/tests/ui.test.mjs | 112 +++ package.json | 3 +- src/components/account-page.tsx | 599 +++++++++------- src/components/agent-activity.tsx | 36 +- src/components/agent-console-parts.tsx | 32 +- src/components/agent-conversation.tsx | 40 +- src/components/agent-page.tsx | 345 ++++----- src/components/agent-profiles.tsx | 93 ++- src/components/agent-recipe-recorder.tsx | 21 +- src/components/agent-recipe-steps.tsx | 13 +- src/components/agent-recipes.tsx | 45 +- src/components/code-examples-dialog.tsx | 251 +++++++ src/components/integration-diagnostics.tsx | 56 +- src/components/integrations-dialog.tsx | 770 ++++++++++++--------- src/components/ui/code-snippet.tsx | 116 ++++ src/components/ui/copy-to-clipboard.tsx | 31 +- src/components/ui/settings-panel.tsx | 119 ++++ src/components/ui/textarea.tsx | 28 +- src/i18n/locales/de.json | 58 ++ src/i18n/locales/en.json | 58 ++ src/i18n/locales/es.json | 58 ++ src/i18n/locales/fr.json | 58 ++ src/i18n/locales/ja.json | 58 ++ src/i18n/locales/ko.json | 58 ++ src/i18n/locales/pt.json | 58 ++ src/i18n/locales/ru.json | 58 ++ src/i18n/locales/tr.json | 58 ++ src/i18n/locales/vi.json | 58 ++ src/i18n/locales/zh.json | 58 ++ src/lib/api-examples.test.mjs | 154 +++++ src/lib/api-examples.ts | 383 ++++++++++ 31 files changed, 3001 insertions(+), 884 deletions(-) create mode 100644 src/components/code-examples-dialog.tsx create mode 100644 src/components/ui/code-snippet.tsx create mode 100644 src/components/ui/settings-panel.tsx create mode 100644 src/lib/api-examples.test.mjs create mode 100644 src/lib/api-examples.ts diff --git a/e2e/tests/ui.test.mjs b/e2e/tests/ui.test.mjs index e463c87b..31c426ae 100644 --- a/e2e/tests/ui.test.mjs +++ b/e2e/tests/ui.test.mjs @@ -4,6 +4,13 @@ import path from "node:path"; import test from "node:test"; import Color from "color"; import en from "../../src/i18n/locales/en.json" with { type: "json" }; +import { + curlSnippet, + javascriptSnippet, + LOCAL_API_EXAMPLES, + maskToken, + oneLine, +} from "../../src/lib/api-examples.ts"; import { getDerivedThemeColors, THEMES } from "../../src/lib/themes.ts"; import { withApp } from "../lib/app.mjs"; import { @@ -1387,6 +1394,111 @@ test("the integrations page ships only the Local API tab and never names remote }); }); +test("the local API page shows a one-line example and opens runnable examples in a dialog", async () => { + await withApp("ui-integrations-examples", async (app) => { + const settings = await app.invoke("get_app_settings"); + const saved = await app.invoke("save_app_settings", { + settings: { + ...settings, + api_enabled: true, + api_port: 0, + api_token: null, + }, + }); + const token = saved.api_token; + assert.ok(token?.length >= 32); + const port = await app.invoke("start_api_server", { port: 0 }); + const baseUrl = `http://127.0.0.1:${port}`; + + await app.clickSelector('[aria-label="Integrations"]'); + // The section label is set in capitals, so innerText reads it that way. + await app.waitForText(en.integrations.apiExampleRequest.toUpperCase()); + await app.execute( + `window.__copied = []; + navigator.clipboard.writeText = async (text) => { window.__copied.push(text); };`, + ); + const copied = () => app.execute("return window.__copied;"); + + // The page shows only the ends of the token, and the copy carries all of + // it, so a pasted command runs as is. + const quickStart = '[data-testid="integrations-api-example"]'; + const shown = await app.execute( + "return document.querySelector(arguments[0]).innerText;", + [quickStart], + ); + assert.ok(!shown.includes(token), "the full token is never on screen"); + assert.ok(shown.includes(maskToken(token))); + assert.ok(shown.includes(`${baseUrl}/v1/profiles`)); + await app.clickSelector( + `[data-slot="code-snippet"]:has(${quickStart}) button`, + ); + await app.waitFor(async () => (await copied()).length === 1, { + description: "the quick start was copied", + }); + assert.equal( + (await copied())[0], + oneLine(curlSnippet(LOCAL_API_EXAMPLES[0].request, { baseUrl, token })), + ); + await app.capture("integrations-local-api"); + + await app.clickText(en.integrations.examples.open); + await app.waitForText(en.integrations.examples.apiTitle); + for (const example of LOCAL_API_EXAMPLES) { + await app.clickSelector(`[data-testid="code-example-${example.id}"]`); + await app.waitFor( + async () => + ( + (await dialogText(app, en.integrations.examples.apiTitle)) ?? "" + ).includes(en.integrations.examples.items[example.id].description), + { description: `${example.id} example` }, + ); + } + + // The JavaScript the dialog hands over is real code against the real + // server: run the copy and read the profiles it prints. + await app.clickSelector('[data-testid="code-example-listProfiles"]'); + await app.clickText(en.integrations.examples.languages.javascript, { + roles: ["tab"], + }); + await app.clickSelector( + '[data-testid="code-examples-dialog"] [data-slot="code-snippet"] button', + ); + await app.waitFor(async () => (await copied()).length === 2, { + description: "the JavaScript example was copied", + }); + const javascript = (await copied())[1]; + assert.equal( + javascript, + javascriptSnippet(LOCAL_API_EXAMPLES[0].request, { baseUrl, token }), + ); + const printed = []; + await new (async () => {}).constructor("fetch", "console", javascript)( + fetch, + { log: (value) => printed.push(value) }, + ); + assert.equal(printed.length, 1); + assert.ok(Array.isArray(printed[0].profiles), JSON.stringify(printed[0])); + + // The language is the reader's choice and stays as they move on. + await app.clickSelector('[data-testid="code-example-createProxy"]'); + await app.waitFor( + async () => + ( + (await dialogText(app, en.integrations.examples.apiTitle)) ?? "" + ).includes("JSON.stringify({"), + { description: "JavaScript stays selected" }, + ); + await app.capture("integrations-local-api-examples"); + await dismissSurface(app); + await dismissSurface(app); + + await app.clickSelector(`[aria-label="${en.rail.account}"]`); + await app.waitForText(en.account.signedOutDescription); + await app.capture("account-signed-out"); + await dismissSurface(app); + }); +}); + test("all primary navigation buttons and sub-page tabs render and remain interactive", async () => { await withApp("ui-navigation", async (app) => { const surfaces = [ diff --git a/package.json b/package.json index 9048b4a5..f12f53e0 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "prebuild": "pnpm licenses:generate", "build": "next build", "start": "next start", - "test": "pnpm test:themes && pnpm test:tips && pnpm test:window-decorations && pnpm test:cookie-bot-limits && pnpm test:cookie-bot-outcomes && pnpm test:recipes && pnpm test:agent-console && pnpm test:backend-errors && pnpm test:i18n-parity && pnpm test:proxy-string && pnpm test:proxy-type && pnpm test:proxy-first-hop-claims && pnpm test:profile-search && pnpm test:table-tools && pnpm test:licenses && pnpm test:xray-packaging && pnpm test:rust:unit && pnpm test:sync-e2e", + "test": "pnpm test:themes && pnpm test:tips && pnpm test:window-decorations && pnpm test:cookie-bot-limits && pnpm test:cookie-bot-outcomes && pnpm test:recipes && pnpm test:agent-console && pnpm test:api-examples && pnpm test:backend-errors && pnpm test:i18n-parity && pnpm test:proxy-string && pnpm test:proxy-type && pnpm test:proxy-first-hop-claims && pnpm test:profile-search && pnpm test:table-tools && pnpm test:licenses && pnpm test:xray-packaging && pnpm test:rust:unit && pnpm test:sync-e2e", "test:themes": "node --test src/lib/themes.test.mjs", "test:tips": "node --test src/lib/tips.test.mjs", "test:window-decorations": "node --test src/lib/window-decorations.test.mjs", @@ -18,6 +18,7 @@ "test:cookie-bot-outcomes": "node --test src/lib/cookie-bot-outcomes.test.mjs", "test:recipes": "node --test src/lib/recipes.test.mjs", "test:agent-console": "node --test src/lib/agent-console.test.mjs", + "test:api-examples": "node --test src/lib/api-examples.test.mjs", "test:backend-errors": "node --test src/lib/backend-errors.test.mjs", "test:i18n-parity": "node --test src/lib/i18n-parity.test.mjs", "test:proxy-string": "node --test src/lib/proxy-string.test.mjs", diff --git a/src/components/account-page.tsx b/src/components/account-page.tsx index f4d0d395..9aa4527a 100644 --- a/src/components/account-page.tsx +++ b/src/components/account-page.tsx @@ -1,14 +1,18 @@ "use client"; import { invoke } from "@tauri-apps/api/core"; -import { useCallback, useEffect, useState } from "react"; +import { type ReactNode, useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { + LuArrowRight, + LuCircleCheck, LuCloud, LuEye, LuEyeOff, LuLogOut, LuRefreshCw, + LuServer, + LuTriangleAlert, LuUser, } from "react-icons/lu"; import { @@ -24,11 +28,18 @@ import { AnimatedTabsList, AnimatedTabsTrigger, } from "@/components/ui/animated-tabs"; -import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { SoftFields } from "@/components/ui/field-variant"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { RippleButton } from "@/components/ui/ripple"; +import { + FeatureIcon, + SECTION_LABEL_CLASS, + SettingsPanel, + StatusLight, +} from "@/components/ui/settings-panel"; import { useCloudAuth } from "@/hooks/use-cloud-auth"; import { cookieBotScopeFor, useCookieBot } from "@/hooks/use-cookie-bot"; import { translateBackendError } from "@/lib/backend-errors"; @@ -241,6 +252,72 @@ export function AccountPage({ } }, [t]); + const subscriptionTone = ( + status: string | null | undefined, + ): "success" | "warning" | "muted" => { + if (status === "active" || status === "trialing") return "success"; + if (status === "past_due" || status === "unpaid") return "warning"; + return "muted"; + }; + + const details: { id: string; label: string; value: ReactNode }[] = []; + if (isLoggedIn && user) { + details.push( + { + id: "plan", + label: t("account.fields.plan"), + value: {effectivePlan}, + }, + { + id: "status", + label: t("account.fields.status"), + value: ( + + + {user.subscriptionStatus ?? "—"} + + ), + }, + ); + if (user.teamRole) { + details.push({ + id: "teamRole", + label: t("account.fields.teamRole"), + value: user.teamRole, + }); + } + if (user.planPeriod) { + details.push({ + id: "period", + label: t("account.fields.period"), + value: user.planPeriod, + }); + } + if (typeof user.deviceOrdinal === "number") { + details.push({ + id: "device", + label: t("account.fields.device"), + value: t("account.deviceOrdinal", { + ordinal: user.deviceOrdinal, + count: user.deviceCount ?? user.deviceOrdinal, + }), + }); + } + } + + const connectionTone = { + unknown: "muted", + testing: "muted", + connected: "success", + error: "destructive", + } as const; + const connectionLabel = { + unknown: t("account.selfHosted.statusUnknown"), + testing: t("appFeedback.checking"), + connected: t("sync.status.connected"), + error: t("sync.status.error"), + }; + return ( @@ -269,16 +346,31 @@ export function AccountPage({ - -
-
-
- -
+ + +
+
{isLoggedIn && user ? ( <> -

+

{user.email}

@@ -292,7 +384,7 @@ export function AccountPage({ ) : ( <> -

+

{t("account.signedOut")}

@@ -301,120 +393,79 @@ export function AccountPage({ )}

+
+ {isLoggedIn ? ( + <> + + { + void handleLogout(); + }} + className="h-8 gap-1.5 rounded-lg text-xs text-destructive-text hover:bg-destructive/10" + > + + {t("account.logout")} + + + ) : ( + + + {t("account.signIn")} + + )} +
- {remoteHoursVisible && ( - // A headline block, not one field among six: the allowance - // is the number a customer needs before a launch is - // refused, which is the only way they ever saw it before. -
-
-

- {t("cookieBot.hours.label")} -

- {formatDate(quota?.period_end) && ( -

- {t("cookieBot.hours.resets", { - date: formatDate(quota?.period_end), - })} -

- )} -
-

- {quota ? formatHours(quota.remaining_hours) : "—"} - - {t("cookieBot.hours.remainingOf", { - total: quota - ? formatHours(quota.granted_hours) - : "—", - })} - -

- -
-

- {t("cookieBot.hours.used", { - used: quota ? formatHours(quota.used_hours) : "—", - total: quota - ? formatHours(quota.granted_hours) - : "—", - })} -

- {showTeamUsage && ( - - )} -
-
- )} - - {isLoggedIn && user && ( -
-
-

- {t("account.fields.plan")} -

-

- {effectivePlan} -

-
-
-

- {t("account.fields.status")} -

-

- {user.subscriptionStatus ?? "—"} -

-
- {user.teamRole && ( -
-

- {t("account.fields.teamRole")} -

-

{user.teamRole}

+ {details.length > 0 && ( +
+ {details.map((detail) => ( +
+
+ {detail.label} +
+
+ {detail.value} +
- )} - {user.planPeriod && ( -
-

- {t("account.fields.period")} -

-

{user.planPeriod}

-
- )} - {typeof user.deviceOrdinal === "number" && ( -
-

- {t("account.fields.device")} -

-

- {t("account.deviceOrdinal", { - ordinal: user.deviceOrdinal, - count: user.deviceCount ?? user.deviceOrdinal, - })} -

-
- )} -
+ ))} + )} {isLoggedIn && user && getEntitlements(user).browserAutomation && user.isPrimaryDevice === false && ( -

+

+ {t("account.automationPrimaryOnly")}

)} @@ -423,52 +474,68 @@ export function AccountPage({ getEntitlements(user).browserAutomation && user.isPrimaryDevice === true && (user.deviceCount ?? 1) > 1 && ( -

+

+ {t("account.automationActiveHere")}

)} + -
- {isLoggedIn ? ( - <> + {remoteHoursVisible && ( + // A headline block, not one field among six: the allowance + // is the number a customer needs before a launch is + // refused, which is the only way they ever saw it before. + +
+

+ {t("cookieBot.hours.label")} +

+ {formatDate(quota?.period_end) && ( +

+ {t("cookieBot.hours.resets", { + date: formatDate(quota?.period_end), + })} +

+ )} +
+

+ {quota ? formatHours(quota.remaining_hours) : "—"} + + {t("cookieBot.hours.remainingOf", { + total: quota ? formatHours(quota.granted_hours) : "—", + })} + +

+ +
+

+ {t("cookieBot.hours.used", { + used: quota ? formatHours(quota.used_hours) : "—", + total: quota ? formatHours(quota.granted_hours) : "—", + })} +

+ {showTeamUsage && ( - { - void handleLogout(); - }} - className="h-8 gap-1.5 text-xs" - > - - {t("account.logout")} - - - ) : ( - - )} -
-
+ )} +
+
+ )}
{showTeamUsage && ( @@ -477,7 +544,10 @@ export function AccountPage({ )} - + {selfHostedDisabled ? ( // Defensive: the tab trigger is disabled while the user is // logged in, so this branch shouldn't be reachable via UI — @@ -488,113 +558,125 @@ export function AccountPage({ {t("account.selfHosted.disabledWhileLoggedIn")}

) : ( -
-
-

- {t("account.selfHosted.title")} -

-

- {t("account.selfHosted.description")} -

-
- -
- - { - setServerUrl(e.target.value); - setConnectionStatus("unknown"); - }} - autoComplete="off" - spellCheck={false} - /> -
- -
- -
- { - setToken(e.target.value); - setConnectionStatus("unknown"); - }} - autoComplete="off" - spellCheck={false} - className="pr-9" - /> - + +
+ + + +
+

+ {t("account.selfHosted.title")} +

+

+ {t("account.selfHosted.description")} +

-
- - {t("account.selfHosted.connectionStatus")} - - {connectionStatus === "connected" && ( - - {t("sync.status.connected")} - - )} - {connectionStatus === "error" && ( - - {t("sync.status.error")} - - )} - {connectionStatus === "testing" && ( - - {t("sync.status.syncing")} - - )} - {connectionStatus === "unknown" && ( - - {t("account.selfHosted.statusUnknown")} - - )} -
+ +
+
+ + { + setServerUrl(e.target.value); + setConnectionStatus("unknown"); + }} + autoComplete="off" + spellCheck={false} + /> +
-
+
+ +
+ { + setToken(e.target.value); + setConnectionStatus("unknown"); + }} + autoComplete="off" + spellCheck={false} + className="pr-10" + /> + +
+
+
+ + +
+ + + {t("account.selfHosted.connectionStatus")} + + {connectionLabel[connectionStatus]} + + + {hasConfig && ( + + )} void handleTestConnection()} - className="h-8 text-xs" + className="h-8 rounded-lg text-xs" > {t("account.selfHosted.testConnection")} @@ -603,23 +685,12 @@ export function AccountPage({ isLoading={isSavingSelfHosted} disabled={!serverUrl || !token || isTestingConnection} onClick={() => void handleSaveSelfHosted()} - className="h-8 text-xs" + className="h-8 rounded-lg text-xs" > {t("common.buttons.save")} - {hasConfig && ( - - )}
-
+
)} @@ -629,3 +700,5 @@ export function AccountPage({
); } + +const FIELD_LABEL_CLASS = "text-xs font-medium text-muted-foreground"; diff --git a/src/components/agent-activity.tsx b/src/components/agent-activity.tsx index b870eb8e..1841f0ff 100644 --- a/src/components/agent-activity.tsx +++ b/src/components/agent-activity.tsx @@ -3,7 +3,7 @@ import { useVirtualizer } from "@tanstack/react-virtual"; import { useId, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { LuTrash2 } from "react-icons/lu"; +import { LuSearch, LuTrash2 } from "react-icons/lu"; import { activityDetailText, useTimeFormat, @@ -102,6 +102,7 @@ export function AgentActivityFeed({ { - setQuery(event.target.value); - }} - /> +
+ + { + setQuery(event.target.value); + }} + /> +