diff --git a/fern/assets/images/sigmond-thumbnail.jpg b/fern/assets/images/sigmond-thumbnail.jpg new file mode 100644 index 0000000000..d30ed74129 Binary files /dev/null and b/fern/assets/images/sigmond-thumbnail.jpg differ diff --git a/fern/components/index.tsx b/fern/components/index.tsx index 289efdaf69..be70e4621d 100644 --- a/fern/components/index.tsx +++ b/fern/components/index.tsx @@ -6,3 +6,4 @@ // component's public surface (components + types) without enumerating them. export * from "./voice-widget/index"; export * from "./skeleton/index"; +export * from "./sigmond-card/index"; diff --git a/fern/components/sigmond-card/index.tsx b/fern/components/sigmond-card/index.tsx new file mode 100644 index 0000000000..cba00df3cc --- /dev/null +++ b/fern/components/sigmond-card/index.tsx @@ -0,0 +1,146 @@ +import { useEffect } from "react"; + +const WIDGET_UMD_URL = + "https://cdn.signalwire.com/npm/@signalwire/address-widget@dev/dist/address-widget.umd.js"; + +const DEFAULT_TOKEN = + "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwidHlwIjoiU0FUIiwiY2giOiJwdWMuc2lnbmFsd2lyZS5jb20ifQ..IKPXZDR3ZYlpnUuw.yU8NBgPFQN1LH0Ws-Rs2ca-vAbUEP7Gvr0upOf-hDaTUfp-Aa5TALLMkJdUli8UX0ina34hng0G2XPLh7NZ2VZ5cphdd8AUCDKWkJ5HCI8538aNxPWcUWG25B-eELha7yEIrHZlO5BIxuyGVPskf0BIPGgiFSlctotyGB-VyYwSp_Uf6kdcwTCiEgra9LcyUMpNZyz5xH3p93xDU9bvzunFj50qIbfmQ2S0tDgEdduQWlTIfqUjRT6ePuwLGDBZYCCz9JtCQM0S2Fs7ZwvdSOzLFLqThkWnH3mSnkgKtkSTTXpnKUplPBJQaNRn0P4rJiwkk61BGyCJQ8Y8XYNeUHa-m1pRERQNweohQNBA0Ph3ie3vZLFJFjOeWuuztK8JSnfu4m9fxlkiA6icaHYuUm7KTSQ6n7oV5a2dkbLs7s51GRi7di0mm6Xw3jr2Zw1EGS8NgIrAU2HTv7stMAjpvDneYDfPaysCrmhCwu_TJgpG7PARiA4glKv9IuPeF5xpnymM5p0V2KL4HSEz-MJwziOwPpQRED_Brd2DiE0cz.CEYQUqZhcs5wKCtJQqUAMQ"; + +const DEFAULT_DESTINATION = "/public/sigmond"; +const DEFAULT_LABEL = "Talk with Sigmond"; +const DEFAULT_POSTER = "https://mcdn.signalwire.com/images/sigmond_still.png"; + +function pageSlug(): string { + if (typeof window === "undefined") return ""; + const segments = (window.location.pathname || "/").split("/").filter(Boolean); + if (segments.length === 0) return "home"; + return segments + .map((s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "")) + .filter(Boolean) + .join("__"); +} + +type WidgetInstance = { open: () => Promise; close: () => Promise; theme?: string }; +type WidgetGlobal = { + mount: (target: Element | string, options: Record) => WidgetInstance; + unmount?: (widget: WidgetInstance) => Promise; +}; + +declare global { + interface Window { + SignalWireAddressWidget?: WidgetGlobal; + } +} + +let loaderPromise: Promise | null = null; +function loadWidgetGlobal(): Promise { + if (typeof window === "undefined") return Promise.reject(new Error("no window")); + if (window.SignalWireAddressWidget) return Promise.resolve(window.SignalWireAddressWidget); + if (!loaderPromise) { + loaderPromise = new Promise((resolve, reject) => { + const s = document.createElement("script"); + s.src = WIDGET_UMD_URL; + s.async = true; + s.onload = () => + window.SignalWireAddressWidget + ? resolve(window.SignalWireAddressWidget) + : reject(new Error("address-widget UMD loaded but global missing")); + s.onerror = () => { + loaderPromise = null; + reject(new Error("failed to load address-widget UMD")); + }; + document.head.appendChild(s); + }); + } + return loaderPromise; +} + +export interface SigmondWidgetProps { + token?: string; +} + +export function SigmondWidget({ token = DEFAULT_TOKEN }: SigmondWidgetProps) { + useEffect(() => { + const teardown: Array<() => void> = []; + + if (!document.head.querySelector("link[data-signalwire-address-fonts]")) { + const marker = document.createElement("link"); + marker.setAttribute("data-signalwire-address-fonts", "host"); + document.head.appendChild(marker); + } + + void loadWidgetGlobal().catch(() => {}); + + const launchers = Array.from( + document.querySelectorAll("[data-sigmond-launcher]"), + ).filter((el) => !el.dataset.sigmondWired); + + for (const el of launchers) { + el.dataset.sigmondWired = "true"; + let widget: WidgetInstance | null = null; + let busy = false; + + const open = async () => { + if (busy) return; + busy = true; + el.classList.add("sigmond-card--opening"); + try { + const global = await loadWidgetGlobal(); + const theme = document.documentElement.classList.contains("dark") ? "dark" : "light"; + if (!widget) { + const host = document.createElement("div"); + host.className = "sigmond-widget-host"; + host.setAttribute("aria-hidden", "true"); + document.body.appendChild(host); + widget = global.mount(host, { + token, + destination: el.dataset.destination || DEFAULT_DESTINATION, + label: el.dataset.label || DEFAULT_LABEL, + poster: el.dataset.poster || DEFAULT_POSTER, + theme, + layout: "stacked", + video: true, + audio: true, + autoGainControl: false, + inputVolume: 125, + userVariables: { page_slug: pageSlug() }, + }); + const mounted = widget; + teardown.push(() => { + host.remove(); + if (window.SignalWireAddressWidget?.unmount) { + void window.SignalWireAddressWidget.unmount(mounted); + } + }); + } + widget.theme = theme; + void widget.open(); + } catch (e) { + console.error("SigmondWidget:", e); + } finally { + busy = false; + el.classList.remove("sigmond-card--opening"); + } + }; + + const onClick = () => void open(); + const onKey = (e: KeyboardEvent) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + void open(); + } + }; + el.addEventListener("click", onClick); + el.addEventListener("keydown", onKey); + teardown.push(() => { + el.removeEventListener("click", onClick); + el.removeEventListener("keydown", onKey); + delete el.dataset.sigmondWired; + }); + } + + return () => teardown.forEach((fn) => fn()); + }, [token]); + + return null; +} diff --git a/fern/components/sigmond-card/styles.css b/fern/components/sigmond-card/styles.css new file mode 100644 index 0000000000..1bb6e551d7 --- /dev/null +++ b/fern/components/sigmond-card/styles.css @@ -0,0 +1,127 @@ +.sigmond-card-wrap { + margin: 1.5rem 0; +} + +.sigmond-card-wrap.sigmond-card-wrap--inset { + margin: 2rem 2rem 3rem; +} + +.sigmond-card { + position: relative; + display: flex; + align-items: stretch; + gap: 1.25rem; + width: 100%; + text-align: left; + padding: 1rem; + border: 1px solid var(--grayscale-5); + border-radius: 0.75rem; + background: var(--grayscale-a2); + cursor: pointer; + color: inherit; + font: inherit; + transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease, + box-shadow 0.15s ease; +} + +.sigmond-card:hover { + border-color: var(--sw-fuchsia); + background: var(--grayscale-a3, var(--grayscale-a2)); + transform: translateY(-1px); + box-shadow: 0 6px 20px -12px rgba(var(--sw-fuchsia-rgb), 0.6); +} + +.sigmond-card:focus-visible { + outline: 2px solid var(--sw-fuchsia); + outline-offset: 2px; +} + +.sigmond-card--opening { + cursor: wait; +} + +.sigmond-card-media { + position: relative; + flex: 0 0 auto; + width: 160px; + height: 90px; + align-self: center; + border-radius: 0.6rem; + background-color: var(--grayscale-4); + overflow: hidden; + pointer-events: none; +} + +.sigmond-card-media img { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.sigmond-card-badge { + position: absolute; + right: 6px; + bottom: 6px; + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + border-radius: 999px; + background: var(--status-success); + color: #fff; + box-shadow: 0 2px 8px -2px rgba(22, 163, 74, 0.7); +} + +.sigmond-card-body { + display: flex; + flex-direction: column; + justify-content: center; + gap: 0.25rem; + min-width: 0; +} + +.sigmond-card-title { + font-size: 1rem; + font-weight: 700; + color: var(--grayscale-12); +} + +.sigmond-card-desc { + font-size: 0.875rem; + line-height: 1.45; + color: var(--grayscale-a11); +} + +.sigmond-card-cta { + display: inline-flex; + align-items: center; + gap: 0.35rem; + margin-top: 0.35rem; + font-size: 0.8125rem; + font-weight: 600; + color: var(--sw-fuchsia); +} + +.sigmond-widget-host { + position: absolute; + width: 0; + height: 0; +} + +signalwire-address::part(launcher) { + display: none !important; +} + +@media (max-width: 640px) { + .sigmond-card-wrap.sigmond-card-wrap--inset { + margin: 1.5rem 1rem 2rem; + } + + .sigmond-card-media { + width: 120px; + } +} diff --git a/fern/docs.yml b/fern/docs.yml index 6ed86549ae..ff198417cf 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -180,6 +180,7 @@ css: - styles.css - components/skeleton/styles.css - components/voice-widget/styles.css + - components/sigmond-card/styles.css js: - reo.js diff --git a/fern/products/home/pages/welcome.mdx b/fern/products/home/pages/welcome.mdx index dc10ab0371..c3cd7bad70 100644 --- a/fern/products/home/pages/welcome.mdx +++ b/fern/products/home/pages/welcome.mdx @@ -7,6 +7,8 @@ hide-toc: true layout: custom --- +import { SigmondWidget } from "@/components/index"; +
@@ -32,6 +34,26 @@ layout: custom
+ {/* Talk with Sigmond */} + + +
+
+ + Sigmond + + + + Talk with Sigmond + Jump into a live video call with our AI agent. + Start call + +
+
+ + +
+ {/* What are you building? */}
diff --git a/fern/products/platform/pages/ai/overview.mdx b/fern/products/platform/pages/ai/overview.mdx index b3ea8a3015..0433eb44d1 100644 --- a/fern/products/platform/pages/ai/overview.mdx +++ b/fern/products/platform/pages/ai/overview.mdx @@ -9,10 +9,20 @@ description: Build programmable, realtime voice AI agents with SWML, the SignalW max-toc-depth: 3 --- +import { SigmondWidget } from "@/components/index"; + SignalWire AI runs programmable voice agents on the same realtime platform that carries your calls. Deploy a minimum viable product with no-code and low-code tools, then scale it with [SWML](/docs/swml) or the [Server SDKs](/docs/server-sdks). + + + + + + + + ## Quickstart Deploy a serverless AI agent and call it over the public switched telephone network (PSTN) in under 5 minutes. diff --git a/fern/snippets/common/talk-with-sigmond.mdx b/fern/snippets/common/talk-with-sigmond.mdx new file mode 100644 index 0000000000..48857a696d --- /dev/null +++ b/fern/snippets/common/talk-with-sigmond.mdx @@ -0,0 +1,9 @@ +
+
+ + Talk with Sigmond + Jump into a live video call with Sigmond, a helpful AI agent built with SignalWire. + Start call + +
+