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..306756627f --- /dev/null +++ b/fern/components/sigmond-card/styles.css @@ -0,0 +1,308 @@ +.sigmond-card-wrap { + margin: 1.5rem 0; +} + +.sigmond-card { + position: relative; + display: flex; + align-items: stretch; + gap: 1.25rem; + width: 100%; + text-align: left; + padding: 1rem; + border: none; + border-radius: var(--radius-xl); + background: var(--bg-surface-raised); + box-shadow: var(--card-shadow), var(--card-inset); + cursor: pointer; + color: inherit; + font: inherit; + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.sigmond-card:hover { + transform: translateY(-3px); + box-shadow: 0 0 0 1.5px rgba(var(--sw-fuchsia-rgb), 0.15), + var(--card-hover-shadow), var(--card-inset-hover), + inset 0 -1px 0 var(--card-hover-border); +} + +.sigmond-card:focus-visible { + outline: 2px solid var(--accent); + 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 *:not(.sigmond-card-badge):not(.sigmond-card-badge *) { + position: absolute !important; + inset: 0 !important; + width: 100% !important; + height: 100% !important; + max-width: none !important; + margin: 0 !important; + padding: 0 !important; + transform: none !important; + border-radius: inherit; +} + +.sigmond-card-media img { + 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(--accent); + color: var(--accent-contrast); + box-shadow: var(--card-shadow); +} + +.sigmond-card-badge svg { + display: block; + width: 0.875rem; + height: 0.875rem; + color: currentColor; + fill: currentColor; +} + +.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(--accent-a11); +} + +.sigmond-card-cta svg { + display: block; + width: 1em; + height: 1em; + flex: 0 0 auto; + color: currentColor; + fill: currentColor; +} + +/* --- Homepage below-the-fold band --- */ + +.sigmond-band { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.lp-page-container .sigmond-band h2 { + margin-bottom: 0; + font-size: clamp(1.25rem, 3.5vw, 1.75rem); + text-wrap: balance; +} + +.sigmond-band-eyebrow { + display: block; + margin-bottom: 0.6rem; + font-size: 0.6875rem; + font-weight: 700; + letter-spacing: var(--type-letter-spacing-eyebrow); + text-transform: uppercase; + color: var(--accent-a11); +} + +.sigmond-band-desc { + max-width: 36rem; + margin-top: 0.75rem; + font-size: 0.875rem; + line-height: 1.6; + color: var(--fg-muted); + text-wrap: balance; +} + +.sigmond-band-actions { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + gap: 0.75rem 1.5rem; + margin-top: 1.5rem; +} + +.sigmond-band-cta { + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; + gap: 0.5rem; + padding: 0.7rem 1.25rem; + border-radius: 999px; + background: var(--accent); + color: var(--accent-contrast); + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + transition: transform 0.15s ease, box-shadow 0.15s ease, filter 0.15s ease; +} + +.sigmond-band-cta svg, +.sigmond-band-link svg { + display: block; + width: 1em; + height: 1em; + flex: 0 0 auto; + color: currentColor; + fill: currentColor; +} + +.sigmond-band-cta svg * { + fill: currentColor; +} + +.sigmond-band-cta:hover { + transform: translateY(-1px); + filter: brightness(1.08); + box-shadow: var(--card-hover-shadow); +} + +.sigmond-band-cta:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.sigmond-band-cta.sigmond-card--opening { + cursor: wait; +} + +.sigmond-band-sep { + width: 4px; + height: 4px; + border-radius: 999px; + background: var(--fg-subtle); + opacity: 0.6; +} + +.sigmond-band-link { + display: inline-flex; + align-items: center; + gap: 0.35rem; + font-size: 0.8125rem; + font-weight: 600; + color: var(--interactive-link-default); +} + +.sigmond-band-link:hover { + color: var(--interactive-link-hover); +} + +.sigmond-band-media { + flex: 0 0 auto; + width: clamp(120px, 22vw, 180px); + height: clamp(120px, 22vw, 180px); + margin-top: 2rem; + border-radius: 999px; + overflow: hidden; + border: 1px solid var(--border-default); + background-color: var(--bg-surface); + pointer-events: none; +} + +.sigmond-band-media img, +.sigmond-band-media span, +.sigmond-band-media div { + width: 100%; + height: 100%; + margin: 0; + border-radius: inherit; +} + +.sigmond-band-media img { + display: block; + object-fit: cover; +} + +@media (max-width: 640px) { + .sigmond-band-media { + margin-top: 1.5rem; + } + + .sigmond-band-actions { + margin-top: 1.25rem; + gap: 0.75rem; + } + + .sigmond-band-sep { + display: none; + } + + .sigmond-band-cta { + min-height: 48px; + padding: 0.75rem 1.5rem; + } +} + +.sigmond-widget-host { + position: absolute; + width: 0; + height: 0; +} + +signalwire-address::part(launcher) { + display: none !important; +} + +@media (max-width: 560px) { + .sigmond-card { + flex-direction: column; + align-items: stretch; + gap: 0.875rem; + } + + .sigmond-card-media { + width: 100%; + height: auto; + aspect-ratio: 16 / 9; + align-self: stretch; + } +} diff --git a/fern/docs.yml b/fern/docs.yml index e0dceab668..fda6a892b9 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 redirects: - source: /docs/agents-sdk diff --git a/fern/products/home/pages/welcome.mdx b/fern/products/home/pages/welcome.mdx index dc10ab0371..8d98a65e51 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"; +
@@ -104,6 +106,31 @@ layout: custom
+ {/* Talk with Sigmond */} + + +
+ Try it now +

Sigmond runs on SignalWire

+

+ A voice and video AI agent built with the SignalWire SDKs, wired to a live knowledge base, and able to see what you show it. +

+
+ Sigmond, the SignalWire AI agent +
+
+ + + Call Sigmond + + + Build an agent like this +
+
+ + +
+ {/* Twilio Migration */}
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..5b8cf490b8 --- /dev/null +++ b/fern/snippets/common/talk-with-sigmond.mdx @@ -0,0 +1,12 @@ +
+
+ + Sigmond, the SignalWire AI agent + + + Call Sigmond + A voice and video AI agent built with the SignalWire SDKs, wired to a live knowledge base, and able to see what you show it. The same agent runs on voice, phone, WhatsApp, or SIP. + Start a video call + +
+