From 6ed3ff79223d3eef7412a42d5586c338d52ac3e8 Mon Sep 17 00:00:00 2001 From: jessicat <8797119+jess-cat@users.noreply.github.com> Date: Thu, 25 Jun 2026 20:17:46 +0100 Subject: [PATCH 1/5] =?UTF-8?q?Messengers:=20redesign=20flow=20=E2=80=94?= =?UTF-8?q?=20card=20grid,=20family=20status,=20Telegram=20+=20Discord?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI - Crew-style card grid on the Messengers page: one card per platform (header icon + name + family-status badge, blurb, bot list, connect CTA). - Simplified Telegram onboarding: a single bot-token input wired to adapter.connect, numbered guidance + doc links; adapter/account/webhook fields hidden. Discord shares the flow. - Family status everywhere (not-enabled / connected / disconnected / attention + "N connected / M disconnected" tooltip). Always render Telegram + Discord — no "NO MESSENGERS" / "NO OBJECTS" empty state, in the page, the settings overview, and the desktop object map. - Deep-link: a not-enabled platform entry opens "Connect " directly instead of the roster. Backend / dev - Gateway binds CHANNEL_TELEGRAM (TelegramChannel) and drops CHANNEL_WHATSAPP; dev-stack runs the telegram adapter worker. - Vite dev proxy forwards gateway HTTP + WebSocket to :8787 so the app can log in from the :5173 dev server (with HMR). Co-Authored-By: Claude Opus 4.8 --- gateway/wrangler.jsonc | 6 +- scripts/dev-stack.sh | 4 +- .../messengers/MessengerOnboardingFlow.css | 167 +++++-- .../messengers/MessengerOnboardingFlow.tsx | 426 +++++++----------- .../gsv-console/messengers/MessengersPage.css | 163 +++++++ .../gsv-console/messengers/MessengersPage.tsx | 317 ++++++++----- .../gsv-console/messengers/messengerDocs.ts | 40 ++ .../messengers/messengerPresentation.ts | 102 +++++ .../pages/ConsoleOverviewPanels.tsx | 29 +- .../gsv-shell/domain/desktopObjects.test.ts | 9 +- .../gsv-shell/domain/desktopObjects.ts | 61 ++- web/vite.config.ts | 16 + 12 files changed, 872 insertions(+), 468 deletions(-) create mode 100644 web/src/app/features/gsv-console/messengers/MessengersPage.css create mode 100644 web/src/app/features/gsv-console/messengers/messengerDocs.ts diff --git a/gateway/wrangler.jsonc b/gateway/wrangler.jsonc index a3115647..510b7c9f 100644 --- a/gateway/wrangler.jsonc +++ b/gateway/wrangler.jsonc @@ -57,9 +57,9 @@ // Channel service bindings (Gateway → Channel RPC) "services": [ { - "binding": "CHANNEL_WHATSAPP", - "service": "gsv-channel-whatsapp", - "entrypoint": "WhatsAppChannelEntrypoint" + "binding": "CHANNEL_TELEGRAM", + "service": "gsv-channel-telegram", + "entrypoint": "TelegramChannel" }, { "binding": "CHANNEL_DISCORD", diff --git a/scripts/dev-stack.sh b/scripts/dev-stack.sh index 0c0101b0..587a8074 100755 --- a/scripts/dev-stack.sh +++ b/scripts/dev-stack.sh @@ -7,13 +7,13 @@ STATE_ROOT="$ROOT_DIR/.wrangler/dev-state/v3" mkdir -p "$STATE_ROOT/do/ripgit-Repository" mkdir -p "$STATE_ROOT/do/gsv-Kernel" mkdir -p "$STATE_ROOT/do/gsv-Process" -mkdir -p "$STATE_ROOT/do/gsv-channel-whatsapp-WhatsAppAccount" +mkdir -p "$STATE_ROOT/do/gsv-channel-telegram-TelegramAccount" cd "$ROOT_DIR/ripgit" exec npm exec -- wrangler dev \ -c ../gateway/wrangler.jsonc \ -c ../assembler/wrangler.toml \ - -c ../adapters/whatsapp/wrangler.jsonc \ + -c ../adapters/telegram/wrangler.jsonc \ -c wrangler.toml \ --ip 0.0.0.0 \ --persist-to ../.wrangler/dev-state diff --git a/web/src/app/features/gsv-console/messengers/MessengerOnboardingFlow.css b/web/src/app/features/gsv-console/messengers/MessengerOnboardingFlow.css index 6a95e094..1c7d66cb 100644 --- a/web/src/app/features/gsv-console/messengers/MessengerOnboardingFlow.css +++ b/web/src/app/features/gsv-console/messengers/MessengerOnboardingFlow.css @@ -66,71 +66,165 @@ overflow: hidden; } -.gsv-messenger-flow-panel > .gsv-messenger-form-grid, +.gsv-messenger-flow-panel > .gsv-messenger-steps, +.gsv-messenger-flow-panel > .gsv-messenger-caps, +.gsv-messenger-flow-panel > .gsv-messenger-success-lede, .gsv-messenger-flow-panel > .gsv-messenger-form-note, .gsv-messenger-flow-panel > .gsv-messenger-form-error, -.gsv-messenger-flow-panel > .gsv-messenger-challenge, .gsv-messenger-flow-panel > p { margin-inline: 22px; } -.gsv-messenger-form-grid { +/* Numbered prep steps + token field */ +.gsv-messenger-steps { display: grid; - gap: 18px; + gap: 20px; + margin: 0; + padding: 0; + list-style: none; } -.gsv-messenger-form-grid .lr { - border: 1px solid var(--rule-inner) !important; - background: rgba(11, 8, 30, 0.48) !important; +.gsv-messenger-step { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 14px; + align-items: start; } -.gsv-messenger-form-note, -.gsv-messenger-form-error { +.gsv-messenger-step-num { + display: grid; + place-items: center; + width: 26px; + height: 26px; + flex: none; + border: 1px solid var(--rule-inner); + background: rgba(11, 8, 30, 0.48); + color: var(--accent); + font-family: var(--gsv-font-mono); + font-size: 12px; + line-height: 1; +} + +.gsv-messenger-step-body { + display: grid; + gap: 7px; + min-width: 0; +} + +.gsv-messenger-step-title { + color: var(--text-hi); + font-family: var(--gsv-font-mono); + font-size: 12.5px; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.gsv-messenger-step-text { margin: 0; + max-width: 60ch; + color: var(--label); font-family: var(--gsv-font-prose); font-size: 13px; - line-height: 1.55; + line-height: 1.5; } -.gsv-messenger-form-note { - color: var(--online); +.gsv-messenger-step-links { + display: flex; + flex-wrap: wrap; + gap: 6px 16px; } -.gsv-messenger-form-error { - color: var(--error); +.gsv-messenger-token-field { + margin-top: 4px; } -.gsv-messenger-challenge { - display: grid; - gap: 16px; - padding: 18px; +/* Help links */ +.gsv-messenger-help-link { + display: inline-flex; + align-items: center; + color: var(--accent); + font-family: var(--gsv-font-mono); + font-size: 11px; + letter-spacing: 0.06em; + text-decoration: none; + border-bottom: 1px solid color-mix(in srgb, var(--accent) 45%, transparent); + padding-bottom: 1px; + transition: color 0.12s ease, border-color 0.12s ease; } -.gsv-messenger-challenge p, -.gsv-messenger-challenge small { +.gsv-messenger-help-link:hover, +.gsv-messenger-help-link:focus-visible { + color: var(--text-hi); + border-bottom-color: var(--text-hi); + outline: none; +} + +/* Success — capabilities */ +.gsv-messenger-success-lede { margin: 0; - color: #9d96d8; + color: var(--label); font-family: var(--gsv-font-prose); font-size: 13px; - line-height: 1.55; + line-height: 1.5; } -.gsv-messenger-challenge img { - width: min(280px, 100%); - image-rendering: pixelated; - background: #fff; +.gsv-messenger-caps { + display: grid; + gap: 14px; +} + +.gsv-messenger-caps-title { + color: var(--text-hi); + font-family: var(--gsv-font-mono); + font-size: 12px; + letter-spacing: 0.1em; + text-transform: uppercase; } -.gsv-messenger-challenge pre { - max-width: 100%; - overflow: auto; +.gsv-messenger-caps-list { + display: grid; + gap: 12px; margin: 0; - padding: 14px; + padding: 0; + list-style: none; +} + +.gsv-messenger-caps-list li { + display: grid; + gap: 3px; + padding: 12px 14px; border: 1px solid var(--rule-inner); - color: var(--text); - background: #08061d; - white-space: pre-wrap; - overflow-wrap: anywhere; + background: rgba(11, 8, 30, 0.48); +} + +.gsv-messenger-cap-title { + color: var(--text-hi); + font-family: var(--gsv-font-mono); + font-size: 12px; + letter-spacing: 0.04em; +} + +.gsv-messenger-cap-detail { + color: var(--label); + font-family: var(--gsv-font-prose); + font-size: 12.5px; + line-height: 1.45; +} + +.gsv-messenger-form-note, +.gsv-messenger-form-error { + margin: 0; + font-family: var(--gsv-font-prose); + font-size: 13px; + line-height: 1.55; +} + +.gsv-messenger-form-note { + color: var(--online); +} + +.gsv-messenger-form-error { + color: var(--error); } .gsv-messenger-flow-actions { @@ -155,10 +249,11 @@ grid-column: 2; } - .gsv-messenger-flow-panel > .gsv-messenger-form-grid, + .gsv-messenger-flow-panel > .gsv-messenger-steps, + .gsv-messenger-flow-panel > .gsv-messenger-caps, + .gsv-messenger-flow-panel > .gsv-messenger-success-lede, .gsv-messenger-flow-panel > .gsv-messenger-form-note, .gsv-messenger-flow-panel > .gsv-messenger-form-error, - .gsv-messenger-flow-panel > .gsv-messenger-challenge, .gsv-messenger-flow-panel > p { margin-inline: 16px; } diff --git a/web/src/app/features/gsv-console/messengers/MessengerOnboardingFlow.tsx b/web/src/app/features/gsv-console/messengers/MessengerOnboardingFlow.tsx index afe962ed..78c04658 100644 --- a/web/src/app/features/gsv-console/messengers/MessengerOnboardingFlow.tsx +++ b/web/src/app/features/gsv-console/messengers/MessengerOnboardingFlow.tsx @@ -1,210 +1,91 @@ -import { useMemo, useState } from "preact/hooks"; +import type { JSX } from "preact"; +import { useState } from "preact/hooks"; import { Button } from "../../../components/ui/Button"; -import { Checkbox } from "../../../components/ui/Checkbox"; import { IconButton } from "../../../components/ui/IconButton"; -import { ListRow } from "../../../components/ui/ListRow"; -import { Select } from "../../../components/ui/Select"; import { SectionHeader } from "../../../components/ui/SectionHeader"; import { Stepper } from "../../../components/ui/Stepper"; import { Surface } from "../../../components/ui/Surface"; import { Tag } from "../../../components/ui/Tag"; import { TextInput } from "../../../components/ui/TextInput"; import type { ConnectConsoleAdapterResult } from "../backend/consoleService"; -import { listRowStatusForTone } from "../components/consoleDetailRows"; -import type { ConsoleAdapter } from "../domain/consoleModels"; import { useConnectConsoleAdapter } from "../hooks/useConsoleData"; -import { - adapterDetailId, - adapterName, - iconForAdapterName, - statusForAdapterFamily, - toneForAdapterFamily, -} from "./messengerPresentation"; +import { BOTFATHER_URL, MESSENGER_CAPABILITIES, adapterDocUrl } from "./messengerDocs"; +import { adapterDetailId, adapterName, deriveTelegramAccountId } from "./messengerPresentation"; import "./MessengerOnboardingFlow.css"; type MessengerOnboardingFlowProps = { - adapters: readonly ConsoleAdapter[]; - initialAccountId?: string | null; - initialAdapter?: string | null; + adapterId: string; onBack: () => void; onConnected: (detailId: string) => void; }; -type Challenge = NonNullable; - -function defaultAccountId(adapter: string): string { - if (adapter === "discord") return "main"; - if (adapter === "telegram") return "bot"; - return "primary"; -} - function errorText(error: unknown): string { return error instanceof Error ? error.message : error ? String(error) : ""; } -function validUrl(value: string): boolean { - if (!value.trim()) return true; - try { - const url = new URL(value); - return url.protocol === "http:" || url.protocol === "https:"; - } catch { - return false; - } -} - -function buildAdapterConfig(args: { - adapter: string; - botToken: string; - forcePairing: boolean; - webhookBaseUrl: string; - webhookSecret: string; -}): Record | undefined { - const config: Record = {}; - if (args.adapter === "whatsapp") { - config.force = args.forcePairing; - } - if ((args.adapter === "discord" || args.adapter === "telegram") && args.botToken.trim()) { - config.botToken = args.botToken.trim(); - } - if (args.adapter === "telegram" && args.webhookBaseUrl.trim()) { - config.webhookBaseUrl = args.webhookBaseUrl.trim(); - } - if (args.adapter === "telegram" && args.webhookSecret.trim()) { - config.webhookSecret = args.webhookSecret.trim(); - } - return Object.keys(config).length > 0 ? config : undefined; -} - -function ChallengePanel({ challenge }: { challenge: Challenge }) { - const isImage = /^data:image\//i.test(challenge.data); +function HelpLink({ href, label }: { href: string; label: string }): JSX.Element { return ( - - -

{challenge.message || "Complete the adapter authentication step, then check account status."}

- {challenge.data ? ( - isImage ? ( - Adapter authentication challenge - ) : ( -
{challenge.data}
- ) - ) : null} - {challenge.expiresAt ? EXPIRES {new Date(challenge.expiresAt).toLocaleString()} : null} -
+ + {label} + ); } export function MessengerOnboardingFlow({ - adapters, - initialAccountId = null, - initialAdapter = null, + adapterId, onBack, onConnected, -}: MessengerOnboardingFlowProps) { - const availableAdapters = adapters.filter((adapter) => adapter.available && adapter.supportsConnect); - const selectableAdapters = availableAdapters.length > 0 ? availableAdapters : adapters; - const initialIndex = Math.max(0, selectableAdapters.findIndex((adapter) => adapter.adapter === initialAdapter)); +}: MessengerOnboardingFlowProps): JSX.Element { const connect = useConnectConsoleAdapter(); - const [adapterIndex, setAdapterIndex] = useState(initialIndex); - const selectedAdapter = selectableAdapters[adapterIndex] ?? selectableAdapters[0] ?? null; - const selectedAdapterId = selectedAdapter?.adapter ?? ""; - const initialAccount = initialAccountId?.trim() || defaultAccountId(selectedAdapterId); - const [accountId, setAccountId] = useState(initialAccount); - const [accountTouched, setAccountTouched] = useState(Boolean(initialAccountId?.trim())); - const [botToken, setBotToken] = useState(""); - const [forcePairing, setForcePairing] = useState(false); - const [webhookBaseUrl, setWebhookBaseUrl] = useState(""); - const [webhookSecret, setWebhookSecret] = useState(""); + const [token, setToken] = useState(""); const [result, setResult] = useState(null); const [formError, setFormError] = useState(""); - const webhookValid = validUrl(webhookBaseUrl); - const canSubmit = Boolean(selectedAdapterId && accountId.trim()) && webhookValid && !connect.isPending; - const currentStep = result?.challenge ? 2 : accountId.trim() ? 1 : 0; - const options = selectableAdapters.map((adapter) => adapterName(adapter.adapter)); - const selectedRow = useMemo(() => selectedAdapter ? { - icon: iconForAdapterName(selectedAdapter.adapter), - label: adapterName(selectedAdapter.adapter), - status: listRowStatusForTone(toneForAdapterFamily(selectedAdapter)), - statusLabel: statusForAdapterFamily(selectedAdapter), - sub: selectedAdapter.available - ? `${selectedAdapter.accounts.length} configured account${selectedAdapter.accounts.length === 1 ? "" : "s"}` - : "Adapter worker not deployed", - } : null, [selectedAdapter]); - - const selectAdapter = (index: number) => { - const next = selectableAdapters[index] ?? null; - setAdapterIndex(index); - setResult(null); - setFormError(""); - if (next && !accountTouched) { - setAccountId(next.adapter === initialAdapter && initialAccountId?.trim() ? initialAccountId.trim() : defaultAccountId(next.adapter)); - } - }; + const isTelegram = adapterId === "telegram"; + const name = adapterName(adapterId); + const docUrl = adapterDocUrl(adapterId); + const connected = Boolean(result?.ok); + const canSubmit = token.trim().length > 0 && !connect.isPending; const submit = async () => { - if (!selectedAdapter || !canSubmit) { + if (!canSubmit) { return; } setFormError(""); setResult(null); try { + const accountId = isTelegram ? deriveTelegramAccountId(token) : "main"; const next = await connect.mutateAsync({ - adapter: selectedAdapter.adapter, + adapter: adapterId, accountId, - config: buildAdapterConfig({ - adapter: selectedAdapter.adapter, - botToken, - forcePairing, - webhookBaseUrl, - webhookSecret, - }), + config: { botToken: token.trim() }, }); - setResult(next); - if (!next.ok) { - setFormError(next.error || next.message); + if (next.ok) { + setResult(next); return; } - if (!next.challenge) { - onConnected(adapterDetailId({ - adapter: next.adapter, - accountId: next.accountId, - connected: next.connected, - authenticated: next.authenticated, - mode: "", - lastActivity: null, - error: "", - extra: {}, - })); - } + setFormError(next.error || next.message); } catch (error) { setFormError(errorText(error)); } }; - if (selectableAdapters.length === 0) { - return ( -
-
-
- -
- FLEET / NEW MESSENGER -

Connect messenger

-

Add an external messaging account to route conversations into GSV.

-
- -
- - -

No adapter workers were discovered on this GSV instance.

-
-
-
-
-
- ); - } + const goToDetail = () => { + if (!result) { + onBack(); + return; + } + onConnected(adapterDetailId({ + adapter: result.adapter, + accountId: result.accountId, + connected: result.connected, + authenticated: result.authenticated, + mode: "", + lastActivity: null, + error: "", + extra: {}, + })); + }; return (
@@ -213,136 +94,131 @@ export function MessengerOnboardingFlow({
FLEET / NEW MESSENGER -

Connect messenger

-

Add an external messaging account to route conversations into GSV.

+

Connect {name}

+

Link a {name} bot to your GSV so you can message it from anywhere.

- +
- +
- - -
-