React-привязки
@neurosquad/card-sdk/react оборачивает карточку в провайдер и отдаёт её живое состояние через хуки.
React 18.2+ или 19 — peer-зависимость; шаблон React всё настраивает сам.
import { createRoot } from 'react-dom/client'
import { CardProvider, useCardContext, useStorage } from '@neurosquad/card-sdk/react'
function App() {
const { instance, workspace } = useCardContext()
const [count, setCount] = useStorage('count', 0)
return (
<button className="ns-btn ns-btn--primary" onClick={() => setCount(count + 1)}>
{instance.displayName} in {workspace.name}: {count}
</button>
)
}
createRoot(document.getElementById('root')!).render(
<CardProvider fallback={<p>Connecting…</p>}>
<App />
</CardProvider>
)CardProvider
| Свойство | Значение |
|---|---|
card | Карточка, которую вы подключили сами, — например, с мок-хоста. Без неё провайдер сам вызывает connect(). |
connectOptions | Параметры для connect(). |
fallback | Показывается, пока идёт подключение. |
errorFallback | (error) => ReactNode, показывается, если подключиться не удалось. По умолчанию — текст ошибки. |
Шаблон React сначала подключается, а потом передаёт card — так одна и та же точка входа работает и
в приложении, и, с мок-хостом, в превью в браузере.
Хуки
| Хук | Возвращает |
|---|---|
useCard() | Card — для всего, у чего нет своего хука. |
useCardContext() | Живой HostContext; перерисовывает при любом изменении. |
useSettings() | [settings, setSettings] — settings.values, settings.secrets; сеттер меняет несекретные значения. |
useStorage(key, initial, { scope? }) | [value, setValue, { loading, error }] — как useState, только с сохранением. Сеттер обновляет сразу и пишет в фоне; принимает и функцию. С scope: 'package' следит за записями других копий карточки. |
useAgents() | { agents, loading, error, refresh } — обновляется по событиям статуса и изменений. Нужно agents.read. |
useAgentStatus(agentId) | Статус одного агента или undefined. |
usePort(input?) | { data, message } — последнее значение, пришедшее на вход (или на любой вход). |
useEmit(output) | Стабильная функция (data) => Promise<number>, отправляющая в выход. |
usePortRequest(input, handler) | Отвечает на вход-запрос, пока компонент смонтирован. |
usePeers() | Подключённые карточки и их порты, вживую. |
useTool(name, handler) | Реализует инструмент из манифеста, пока компонент смонтирован; всегда вызывает последний обработчик. |
useCardEvent(event, handler) | Любое событие, пока компонент смонтирован; всегда вызывает последний обработчик. |
useTheme() | ThemeSnapshot, вживую (CSS-переменные уже применены). |
useLanguage() | { language, locale }, вживую. |
useTranslator(catalog) | Переводчик, перерисовывающий при смене языка. Каталог объявляйте вне компонента. |
useVisibility() | visible, offscreen, overview или hidden. |
usePaused() | true, когда тело карточки никто не видит, — ставьте на паузу анимации и опрос. |
useExpanded() | Развёрнута ли карточка. |
Пример побольше
import type { Catalog } from '@neurosquad/card-sdk'
import {
useAgents,
useCard,
useEmit,
usePaused,
usePort,
useTool,
useTranslator
} from '@neurosquad/card-sdk/react'
import { useEffect } from 'react'
const catalog: Catalog = {
en: { waiting_one: '{{count}} agent waits for you', waiting_other: '{{count}} agents wait for you' },
ru: {
waiting_one: '{{count}} агент ждёт вас',
waiting_few: '{{count}} агента ждут вас',
waiting_many: '{{count}} агентов ждут вас',
waiting_other: '{{count}} агента ждут вас'
},
zh: { waiting_other: '{{count}} 个智能体在等你' }
}
export function Waiting() {
const card = useCard()
const t = useTranslator(catalog)
const paused = usePaused()
const { agents } = useAgents()
const waiting = agents.filter((a) => a.status === 'needs-input')
const { data: note } = usePort<string>('notes')
const emit = useEmit<string>('digest')
// Keep the overview tile and attention in step with the data.
useEffect(() => {
void card.setOverview({ primary: t('waiting', { count: waiting.length }), icon: 'bell' })
void card.attention(waiting.length > 0 ? 'needs-input' : 'none').catch(() => undefined)
}, [card, t, waiting.length])
useTool('list_waiting', () => waiting.map((a) => a.name))
return (
<div className="ns-stack">
<p className={paused ? '' : 'pulse'}>{t('waiting', { count: waiting.length })}</p>
{note ? <pre className="ns-mono">{note}</pre> : null}
<button className="ns-btn" onClick={() => void emit(waiting.map((a) => `- ${a.name}`).join('\n'))}>
Send digest
</button>
</div>
)
}Хуки, которые обращаются к приложению (useAgents, useStorage), не бросают исключения, а
сообщают об ошибках в поле error — нехватка разрешения появится там как CardSdkError с
PERMISSION_DENIED.
Локальная копия SDK
Если карточка подключает SDK по ссылке file:, npm создаёт символическую ссылку, и Vite может
загрузить вторую копию React рядом с SDK — тогда хуки падают с «Invalid hook call». В
vite.config.ts шаблона React уже есть resolve: { dedupe: ['react', 'react-dom'] }; в своей
сборке добавьте его сами или укажите install-links=true в .npmrc карточки.