HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-03-10
1 <p>В прошлом году Cбер запустил Салют - семейство виртуальных ассистентов, которые работают на разных платформах. Мы в SberDevices, кроме самого ассистента, занимаемся разработкой инструментов, позволяющих любому разработчику удобно создавать навыки, которые называются<strong>смартапы</strong>. Кроме общеизвестных диалоговых сценариев в формате чата - ChatApp, можно создавать смартапы в формате веб-приложения на любых известных веб-технологиях - Canvas App. О том, как создать простейший смартап такого типа, и пойдет сегодня речь.</p>
1 <p>В прошлом году Cбер запустил Салют - семейство виртуальных ассистентов, которые работают на разных платформах. Мы в SberDevices, кроме самого ассистента, занимаемся разработкой инструментов, позволяющих любому разработчику удобно создавать навыки, которые называются<strong>смартапы</strong>. Кроме общеизвестных диалоговых сценариев в формате чата - ChatApp, можно создавать смартапы в формате веб-приложения на любых известных веб-технологиях - Canvas App. О том, как создать простейший смартап такого типа, и пойдет сегодня речь.</p>
2 <p><strong>Canvas App</strong>- стандартное веб-приложение в привычном понимании, которое запускается и работает внутри WebView, но есть свои особенности.</p>
2 <p><strong>Canvas App</strong>- стандартное веб-приложение в привычном понимании, которое запускается и работает внутри WebView, но есть свои особенности.</p>
3 <p>Как это работает по шагам:</p>
3 <p>Как это работает по шагам:</p>
4 <ol><li>Пользователь произносит ключевую фразу, например "Салют, какие у меня задачи на сегодня".</li>
4 <ol><li>Пользователь произносит ключевую фразу, например "Салют, какие у меня задачи на сегодня".</li>
5 <li>Голосовой запрос приходит в NLP-платформу, где разбирается на фонемы, там же определяется его эмоциональный окрас и т.д..</li>
5 <li>Голосовой запрос приходит в NLP-платформу, где разбирается на фонемы, там же определяется его эмоциональный окрас и т.д..</li>
6 <li>В БД зарегистрированных смартапов находится тот, которому соответствует активационная фраза. Регистрация происходит через SmartApp Studio и доступна всем разработчикам без исключения.</li>
6 <li>В БД зарегистрированных смартапов находится тот, которому соответствует активационная фраза. Регистрация происходит через SmartApp Studio и доступна всем разработчикам без исключения.</li>
7 <li>Во время регистрации смартапа в SmartApp Studio разработчик указывает два эндпоинта: один для веб-приложения, второй для сценарного бэкенда. Именно их достанет из БД NLP-платформа, когда найдет соответствующий смартап.</li>
7 <li>Во время регистрации смартапа в SmartApp Studio разработчик указывает два эндпоинта: один для веб-приложения, второй для сценарного бэкенда. Именно их достанет из БД NLP-платформа, когда найдет соответствующий смартап.</li>
8 <li>В эндпоинт сценарного бэкенда будет отправлено сообщение с распознанной активационной фразой. Формат сообщений подробно описан в документации SmartApp API.</li>
8 <li>В эндпоинт сценарного бэкенда будет отправлено сообщение с распознанной активационной фразой. Формат сообщений подробно описан в документации SmartApp API.</li>
9 <li>Эндпоинт веб-приложения будет указан для загрузки в WebView.</li>
9 <li>Эндпоинт веб-приложения будет указан для загрузки в WebView.</li>
10 <li>Ответ от сценарного бэкенда придёт в веб-приложение в качестве JS-события, подписавшись на которое, можно управлять веб-приложением.</li>
10 <li>Ответ от сценарного бэкенда придёт в веб-приложение в качестве JS-события, подписавшись на которое, можно управлять веб-приложением.</li>
11 </ol><p>Предмет нашего разговора - веб-приложение. Делать будем смартап для ведения тудушек. Поскольку SmartApp Studio предоставляет онлайн-среду разработки сценариев, не будем подробно на этом останавливаться, а воспользуемся форком готового сценария, который в качестве примера<a>доступен на GitHub</a>. В одной из следующих статей расскажем, как написать такой сценарий на NodeJS.</p>
11 </ol><p>Предмет нашего разговора - веб-приложение. Делать будем смартап для ведения тудушек. Поскольку SmartApp Studio предоставляет онлайн-среду разработки сценариев, не будем подробно на этом останавливаться, а воспользуемся форком готового сценария, который в качестве примера<a>доступен на GitHub</a>. В одной из следующих статей расскажем, как написать такой сценарий на NodeJS.</p>
12 <p>В SmartApp Graph/IDE, той самой онлайн-среде, в качестве источника можно указать git-репозиторий, чем мы и воспользуемся, чтобы получить эндпоинт до сценарного бэкенда. Далее его надо указать при регистрации нашего смартапа в SmartApp Studio. В качестве эндпоинта веб-приложения укажем любой известный веб-ресурс, например, sberdevices.ru. Позже поменяем на URL нашего веб-приложения.</p>
12 <p>В SmartApp Graph/IDE, той самой онлайн-среде, в качестве источника можно указать git-репозиторий, чем мы и воспользуемся, чтобы получить эндпоинт до сценарного бэкенда. Далее его надо указать при регистрации нашего смартапа в SmartApp Studio. В качестве эндпоинта веб-приложения укажем любой известный веб-ресурс, например, sberdevices.ru. Позже поменяем на URL нашего веб-приложения.</p>
13 <h2>Шаблон проекта</h2>
13 <h2>Шаблон проекта</h2>
14 <p>Для примера будем делать веб-приложение на React. К React нет никакой привязки и пример ниже может быть написан на чём угодно. Для нетерпеливых выложили<a>конечный результат на GitHub</a>.</p>
14 <p>Для примера будем делать веб-приложение на React. К React нет никакой привязки и пример ниже может быть написан на чём угодно. Для нетерпеливых выложили<a>конечный результат на GitHub</a>.</p>
15 <p>Итак, что мы хотим от приложения:</p>
15 <p>Итак, что мы хотим от приложения:</p>
16 <ul><li>добавлять задачи;</li>
16 <ul><li>добавлять задачи;</li>
17 <li>выполнять задачи;</li>
17 <li>выполнять задачи;</li>
18 <li>удалять задачи;</li>
18 <li>удалять задачи;</li>
19 <li>и все это голосом, но не сразу.</li>
19 <li>и все это голосом, но не сразу.</li>
20 </ul><p>Для создания базового проекта воспользуемся CRA.</p>
20 </ul><p>Для создания базового проекта воспользуемся CRA.</p>
21 &gt; npx create-react-app todo-canvas-app<p>Для реализации UI нам понадобится как минимум пара компонентов и форма.</p>
21 &gt; npx create-react-app todo-canvas-app<p>Для реализации UI нам понадобится как минимум пара компонентов и форма.</p>
22 <p>Код формы:</p>
22 <p>Код формы:</p>
23 export const App: FC = memo(() =&gt; { const [note, setNote] = useState(""); return ( &lt;main className="container"&gt; &lt;form onSubmit={(event) =&gt; { event.preventDefault(); setNote(""); }} &gt; &lt;input className="add-note" type="text" value={note} onChange={({ target: { value } }) =&gt; setNote(value)} /&gt; &lt;/form&gt; &lt;ul className="notes"&gt; {appState.notes.map((note, index) =&gt; ( &lt;li className="note" key={note.id}&gt; &lt;span&gt; &lt;span style={{ fontWeight: "bold" }}&gt;{index + 1}. &lt;/span&gt; &lt;span style={{ textDecorationLine: note.completed ? "line-through" : "none", }} &gt; {note.title} &lt;/span&gt; &lt;/span&gt; &lt;input className="done-note" type="checkbox" checked={note.completed} /&gt; &lt;/li&gt; ))} &lt;/ul&gt; &lt;/main&gt; ); });<p>Дальше нам надо сделать базовую логику нашего приложения. Пользоваться будем стандартными средствами React, используя useReducer.</p>
23 export const App: FC = memo(() =&gt; { const [note, setNote] = useState(""); return ( &lt;main className="container"&gt; &lt;form onSubmit={(event) =&gt; { event.preventDefault(); setNote(""); }} &gt; &lt;input className="add-note" type="text" value={note} onChange={({ target: { value } }) =&gt; setNote(value)} /&gt; &lt;/form&gt; &lt;ul className="notes"&gt; {appState.notes.map((note, index) =&gt; ( &lt;li className="note" key={note.id}&gt; &lt;span&gt; &lt;span style={{ fontWeight: "bold" }}&gt;{index + 1}. &lt;/span&gt; &lt;span style={{ textDecorationLine: note.completed ? "line-through" : "none", }} &gt; {note.title} &lt;/span&gt; &lt;/span&gt; &lt;input className="done-note" type="checkbox" checked={note.completed} /&gt; &lt;/li&gt; ))} &lt;/ul&gt; &lt;/main&gt; ); });<p>Дальше нам надо сделать базовую логику нашего приложения. Пользоваться будем стандартными средствами React, используя useReducer.</p>
24 <p>Код редьюсера:</p>
24 <p>Код редьюсера:</p>
25 const reducer = (state, action) =&gt; { switch (action.type) { case "add_note": return { ...state, notes: [ ...state.notes, { id: Math.random().toString(36).substring(7), title: action.note, completed: false, }, ], }; case "done_note": return { ...state, notes: state.notes.map((note) =&gt; note.id === action.id ? { ...note, completed: !note.completed } : note ), }; case "delete_note": return { ...state, notes: state.notes.filter(({ id }) =&gt; id !== action.id), }; default: throw new Error(); } };<p>Далее будем диспатчить экшены их обработчиков на форме.</p>
25 const reducer = (state, action) =&gt; { switch (action.type) { case "add_note": return { ...state, notes: [ ...state.notes, { id: Math.random().toString(36).substring(7), title: action.note, completed: false, }, ], }; case "done_note": return { ...state, notes: state.notes.map((note) =&gt; note.id === action.id ? { ...note, completed: !note.completed } : note ), }; case "delete_note": return { ...state, notes: state.notes.filter(({ id }) =&gt; id !== action.id), }; default: throw new Error(); } };<p>Далее будем диспатчить экшены их обработчиков на форме.</p>
26 <p>Код подключения:</p>
26 <p>Код подключения:</p>
27 export const App: FC = memo(() =&gt; { const [appState, dispatch] = useReducer(reducer, { notes: [] }); //... return ( &lt;main className="container"&gt; &lt;form onSubmit={(event) =&gt; { event.preventDefault(); dispatch({ type: "add_note", note }); setNote(""); }} &gt; &lt;input className="add-note" type="text" placeholder="Add Note" value={note} onChange={({ target: { value } }) =&gt; setNote(value)} required autoFocus /&gt; &lt;/form&gt; &lt;ul className="notes"&gt; {appState.notes.map((note, index) =&gt; ( &lt;li className="note" key={note.id}&gt; &lt;span&gt; &lt;span style={{ fontWeight: "bold" }}&gt;{index + 1}. &lt;/span&gt; &lt;span style={{ textDecorationLine: note.completed ? "line-through" : "none", }} &gt; {note.title} &lt;/span&gt; &lt;/span&gt; &lt;input className="done-note" type="checkbox" checked={note.completed} onChange={() =&gt; dispatch({ type: "done_note", id: note.id })} /&gt; &lt;/li&gt; ))} &lt;/ul&gt; &lt;/main&gt; ); });<p>Запускаем и проверяем.</p>
27 export const App: FC = memo(() =&gt; { const [appState, dispatch] = useReducer(reducer, { notes: [] }); //... return ( &lt;main className="container"&gt; &lt;form onSubmit={(event) =&gt; { event.preventDefault(); dispatch({ type: "add_note", note }); setNote(""); }} &gt; &lt;input className="add-note" type="text" placeholder="Add Note" value={note} onChange={({ target: { value } }) =&gt; setNote(value)} required autoFocus /&gt; &lt;/form&gt; &lt;ul className="notes"&gt; {appState.notes.map((note, index) =&gt; ( &lt;li className="note" key={note.id}&gt; &lt;span&gt; &lt;span style={{ fontWeight: "bold" }}&gt;{index + 1}. &lt;/span&gt; &lt;span style={{ textDecorationLine: note.completed ? "line-through" : "none", }} &gt; {note.title} &lt;/span&gt; &lt;/span&gt; &lt;input className="done-note" type="checkbox" checked={note.completed} onChange={() =&gt; dispatch({ type: "done_note", id: note.id })} /&gt; &lt;/li&gt; ))} &lt;/ul&gt; &lt;/main&gt; ); });<p>Запускаем и проверяем.</p>
28 <h2>Работа с голосом</h2>
28 <h2>Работа с голосом</h2>
29 <p>Когда наше приложение базово работает, можно добавить немного магии голосового управления. Для этого надо установить Assistant Client - библиотеку для взаимодействия с виртуальным ассистентом.</p>
29 <p>Когда наше приложение базово работает, можно добавить немного магии голосового управления. Для этого надо установить Assistant Client - библиотеку для взаимодействия с виртуальным ассистентом.</p>
30 npm i @sberdevices/assistant-client<p>В момент открытия WebView платформа инжектит JS API для взаимодействия с ассистентом. Это биндиги до нативных методов платформы. Assistant Client - обёртка, которая в дев-режиме позволяет отлаживать взаимодействие с ассистентом в браузере, а в продакшене предоставляет удобный для веб-приложений API.</p>
30 npm i @sberdevices/assistant-client<p>В момент открытия WebView платформа инжектит JS API для взаимодействия с ассистентом. Это биндиги до нативных методов платформы. Assistant Client - обёртка, которая в дев-режиме позволяет отлаживать взаимодействие с ассистентом в браузере, а в продакшене предоставляет удобный для веб-приложений API.</p>
31 <p>Идём в app.js и там же, где наш основной редюсер, создаем инстанс Assistant Client.</p>
31 <p>Идём в app.js и там же, где наш основной редюсер, создаем инстанс Assistant Client.</p>
32 const initializeAssistant = () =&gt; { if (process.env.NODE_ENV === "development") { return createSmartappDebugger({ token: process.env.REACT_APP_TOKEN ?? "", initPhrase: `Запусти ${process.env.REACT_APP_SMARTAPP}`, }); } return createAssistant(); };<p>Судя по коду выше, нужен некий токен. Токен обеспечивает авторизацию сообщений в NLP-платформе. Токен автоматически приклеивается к сообщениям, когда смартап запускается на устройстве, но в нашем случае это браузер, поэтому токен надо передать вручную. Токен генерируется автоматически для каждого разработчика в SmartApp Studio.</p>
32 const initializeAssistant = () =&gt; { if (process.env.NODE_ENV === "development") { return createSmartappDebugger({ token: process.env.REACT_APP_TOKEN ?? "", initPhrase: `Запусти ${process.env.REACT_APP_SMARTAPP}`, }); } return createAssistant(); };<p>Судя по коду выше, нужен некий токен. Токен обеспечивает авторизацию сообщений в NLP-платформе. Токен автоматически приклеивается к сообщениям, когда смартап запускается на устройстве, но в нашем случае это браузер, поэтому токен надо передать вручную. Токен генерируется автоматически для каждого разработчика в SmartApp Studio.</p>
33 <p>После этого перезапустим наше приложение. Теперь мы видим панельку ассистента с лавашаром и текстовым полем. Лавашар это такое визуальное представление ассистента. По нажатию на лавашар включится микрофон и вы сможете отправить команду ассистенту так же, как вы бы это сделали, запуская смартап на устройстве. Относитесь к этому не как к эмулятору, а как к дев-тулзам, в продакшене всё это за нас будет делать платформа. Те же самые команды вы можете посылать не только голосом, но и текстом, используя текстовое поле рядом с лавашаром, чтобы не будить своих домашних по ночам.</p>
33 <p>После этого перезапустим наше приложение. Теперь мы видим панельку ассистента с лавашаром и текстовым полем. Лавашар это такое визуальное представление ассистента. По нажатию на лавашар включится микрофон и вы сможете отправить команду ассистенту так же, как вы бы это сделали, запуская смартап на устройстве. Относитесь к этому не как к эмулятору, а как к дев-тулзам, в продакшене всё это за нас будет делать платформа. Те же самые команды вы можете посылать не только голосом, но и текстом, используя текстовое поле рядом с лавашаром, чтобы не будить своих домашних по ночам.</p>
34 <p>Ассистент присылает структурированные команды в формате JSON. Полное описание формата можно найти в документации Assistant Client на GitHub.</p>
34 <p>Ассистент присылает структурированные команды в формате JSON. Полное описание формата можно найти в документации Assistant Client на GitHub.</p>
35 interface AssistantSmartAppCommand { // Тип команды type: "smart_app_data"; // Любые данные, которые нужны смартапу smart_app_data: Record&lt;string, any&gt;; sdkMeta: { requestId: string; }; }<p>Теперь подпишем наши экшены на команды от ассистента. Для этого в коде нашего сценария определены специальные интенты - ключевые слова в фразах, которые может говорить пользователь. Разные интенты генерируют разные команды веб-приложению.</p>
35 interface AssistantSmartAppCommand { // Тип команды type: "smart_app_data"; // Любые данные, которые нужны смартапу smart_app_data: Record&lt;string, any&gt;; sdkMeta: { requestId: string; }; }<p>Теперь подпишем наши экшены на команды от ассистента. Для этого в коде нашего сценария определены специальные интенты - ключевые слова в фразах, которые может говорить пользователь. Разные интенты генерируют разные команды веб-приложению.</p>
36 export const App: FC = memo(() =&gt; { const [appState, dispatch] = useReducer(reducer, { notes: [] }); const [note, setNote] = useState(""); const assistantRef = useRef(); useEffect(() =&gt; { assistantRef.current = initializeAssistant(); assistantRef.current.on("data", ({ action }) =&gt; { if (action) { dispatch(action); } }); }, []); // ...<p>Сохраняем, запускаем - ничего не работает. Не волнуйтесь, так и должно быть. Я приоткрою завесу того, как на самом деле работает магия.</p>
36 export const App: FC = memo(() =&gt; { const [appState, dispatch] = useReducer(reducer, { notes: [] }); const [note, setNote] = useState(""); const assistantRef = useRef(); useEffect(() =&gt; { assistantRef.current = initializeAssistant(); assistantRef.current.on("data", ({ action }) =&gt; { if (action) { dispatch(action); } }); }, []); // ...<p>Сохраняем, запускаем - ничего не работает. Не волнуйтесь, так и должно быть. Я приоткрою завесу того, как на самом деле работает магия.</p>
37 <p>Дело в том, что ваш сценарий сам по себе только лишь по фразе пользователя не может узнать то, что у вас сейчас на экране. Чтобы эта магия работала, к каждому голосовому запросу необходимо клеить стейт веб-приложения. Тут мы приходим к осознанию, что сценарный бэкенд получает на вход не только разобранную фразу, но и данные с экрана - стейт. Задача сценария провести пользователя к следующему шагу по этим двум параметрам, отправив команду веб-приложению на изменение стейта. Можно мыслить себе это как голосовой аналог клика. Разница лишь в том, что элемент управления для такого клика в интерфейсе может и не существовать физически. Например, если бы мы делали интернет-магазин, то кнопку добавления в корзину можно было бы и опустить в пользу голосовой команды "Афина, добавь в корзину красные туфли".</p>
37 <p>Дело в том, что ваш сценарий сам по себе только лишь по фразе пользователя не может узнать то, что у вас сейчас на экране. Чтобы эта магия работала, к каждому голосовому запросу необходимо клеить стейт веб-приложения. Тут мы приходим к осознанию, что сценарный бэкенд получает на вход не только разобранную фразу, но и данные с экрана - стейт. Задача сценария провести пользователя к следующему шагу по этим двум параметрам, отправив команду веб-приложению на изменение стейта. Можно мыслить себе это как голосовой аналог клика. Разница лишь в том, что элемент управления для такого клика в интерфейсе может и не существовать физически. Например, если бы мы делали интернет-магазин, то кнопку добавления в корзину можно было бы и опустить в пользу голосовой команды "Афина, добавь в корзину красные туфли".</p>
38 <p>Для того, чтобы это было удобно делать из веб-приложения, в Assistant Client есть API для передачи состояния - getState. В нашем случае стейт - это список тудушек и некоторая мета-информация.</p>
38 <p>Для того, чтобы это было удобно делать из веб-приложения, в Assistant Client есть API для передачи состояния - getState. В нашем случае стейт - это список тудушек и некоторая мета-информация.</p>
39 <p>Дополним код инициализации Asisstant Client.</p>
39 <p>Дополним код инициализации Asisstant Client.</p>
40 const initializeAssistant = (getState) =&gt; { if (process.env.NODE_ENV === "development") { return createSmartappDebugger({ token: process.env.REACT_APP_TOKEN ?? "", initPhrase: `Запусти ${process.env.REACT_APP_SMARTAPP}`, getState, }); } return createAssistant({ getState }); };<p>И передадим стейт в обработку ассистенту. Формат стейта также описан в документации Asisstant Client.</p>
40 const initializeAssistant = (getState) =&gt; { if (process.env.NODE_ENV === "development") { return createSmartappDebugger({ token: process.env.REACT_APP_TOKEN ?? "", initPhrase: `Запусти ${process.env.REACT_APP_SMARTAPP}`, getState, }); } return createAssistant({ getState }); };<p>И передадим стейт в обработку ассистенту. Формат стейта также описан в документации Asisstant Client.</p>
41 export const App: FC = memo(() =&gt; { // ... const assistantStateRef = useRef&lt;AssistantAppState&gt;(); // ... useEffect(() =&gt; { assistantRef.current = initializeAssistant(() =&gt; assistantStateRef.current); // ... }, []); useEffect(() =&gt; { assistantStateRef.current = { item_selector: { items: appState.notes.map(({ id, title }, index) =&gt; ({ number: index + 1, id, title, })), }, }; }, [appState]); // ...<p>Из кода выше видим появление мета-информации в виде нумерации. Зачем? Согласитесь, тудухи могут быть довольными длинными и иногда удобнее было бы говорить "Джой, я сделал первую задачу" вместо полного заголовка. Но погодите, как это работает? Где единичка превращается в "первую"? Эту магию кастования натуральных фраз, которые мы привыкли использовать в повседневной речи, в машинный формат делает за нас NLP-платформа. То же самое происходит, например, с командами навигации.</p>
41 export const App: FC = memo(() =&gt; { // ... const assistantStateRef = useRef&lt;AssistantAppState&gt;(); // ... useEffect(() =&gt; { assistantRef.current = initializeAssistant(() =&gt; assistantStateRef.current); // ... }, []); useEffect(() =&gt; { assistantStateRef.current = { item_selector: { items: appState.notes.map(({ id, title }, index) =&gt; ({ number: index + 1, id, title, })), }, }; }, [appState]); // ...<p>Из кода выше видим появление мета-информации в виде нумерации. Зачем? Согласитесь, тудухи могут быть довольными длинными и иногда удобнее было бы говорить "Джой, я сделал первую задачу" вместо полного заголовка. Но погодите, как это работает? Где единичка превращается в "первую"? Эту магию кастования натуральных фраз, которые мы привыкли использовать в повседневной речи, в машинный формат делает за нас NLP-платформа. То же самое происходит, например, с командами навигации.</p>
42 <p>Тудух может скопиться достаточное количество, чтобы они не влезли в экран. Само собой, мы хотим уметь скроллить экран, чтобы иметь возможность прочитать всё, что скопилось. На устройствах, где нет тач-интерфейса, например, на SberBox, мы можем скроллить пультом ДУ или голосом. Нажатия кнопок на пульте превращаются в события нажатий на стрелки клавиатуры на window, но что делать с голосом?</p>
42 <p>Тудух может скопиться достаточное количество, чтобы они не влезли в экран. Само собой, мы хотим уметь скроллить экран, чтобы иметь возможность прочитать всё, что скопилось. На устройствах, где нет тач-интерфейса, например, на SberBox, мы можем скроллить пультом ДУ или голосом. Нажатия кнопок на пульте превращаются в события нажатий на стрелки клавиатуры на window, но что делать с голосом?</p>
43 <p>Голосовые паттерны навигации встроены в NLP-платформу, и разработчику сценария ничего не надо делать самому. А для разработчика веб-приложения достаточно подписаться на специальный тип команд, приходящих от ассистента через Assistant Client. Все вариации навигационных фраз будут кастится в конечное число навигационных команд. Их всего пять: UP, DOWN, LEFT, RIGHT, BACK.</p>
43 <p>Голосовые паттерны навигации встроены в NLP-платформу, и разработчику сценария ничего не надо делать самому. А для разработчика веб-приложения достаточно подписаться на специальный тип команд, приходящих от ассистента через Assistant Client. Все вариации навигационных фраз будут кастится в конечное число навигационных команд. Их всего пять: UP, DOWN, LEFT, RIGHT, BACK.</p>
44 assistant.on('data', (command) =&gt; { if (command.navigation) { switch(command.navigation.command) { case 'UP': window.scrollTo(0, 0); break; case 'DOWN': window.scrollTo(0, 1000); break; } } });<p>Перезапускаем наше приложение и пробуем после нажатия на лавашар сказать: "Напомни купить коту корм". И вуаля!</p>
44 assistant.on('data', (command) =&gt; { if (command.navigation) { switch(command.navigation.command) { case 'UP': window.scrollTo(0, 0); break; case 'DOWN': window.scrollTo(0, 1000); break; } } });<p>Перезапускаем наше приложение и пробуем после нажатия на лавашар сказать: "Напомни купить коту корм". И вуаля!</p>
45 <p>Если у вас есть устройство под рукой, то можно проверить работу смартапа на нём. Для этого не обязательно его публиковать или деплоить куда-либо. Достаточно создать тоннель с локального хоста, например, с помощью ngrok.</p>
45 <p>Если у вас есть устройство под рукой, то можно проверить работу смартапа на нём. Для этого не обязательно его публиковать или деплоить куда-либо. Достаточно создать тоннель с локального хоста, например, с помощью ngrok.</p>
46 <p>Полученный URL с https указываем в SmartApp Studio, сохраняем черновик и говорим ассистенту: "Сбер, какие у меня задачи на сегодня?". Это cработает, если вы залогинены под одним и тем же SberID на устройстве и в SmartApp Studio. Черновики по умолчанию доступны к запуску на устройствах разработчика.</p>
46 <p>Полученный URL с https указываем в SmartApp Studio, сохраняем черновик и говорим ассистенту: "Сбер, какие у меня задачи на сегодня?". Это cработает, если вы залогинены под одним и тем же SberID на устройстве и в SmartApp Studio. Черновики по умолчанию доступны к запуску на устройствах разработчика.</p>
47 <h2>Вместо эпилога</h2>
47 <h2>Вместо эпилога</h2>
48 <p>Смысл статьи - в наглядной демонстрации того, как голосовое управление прозрачным образом можно интегрировать не только в специально для этого созданные приложения, но и в уже существующие. Например, если у вас уже есть рабочий веб-сервис, то научить его работать на платформе Салют не составит большого труда.</p>
48 <p>Смысл статьи - в наглядной демонстрации того, как голосовое управление прозрачным образом можно интегрировать не только в специально для этого созданные приложения, но и в уже существующие. Например, если у вас уже есть рабочий веб-сервис, то научить его работать на платформе Салют не составит большого труда.</p>
49 <p>Этот короткое интро - скорее, обзор возможностей на искусственном примере. Как сделать смартап с компонентами, оплатой, автотестами, обязательно расскажем в следующих статьях. Спасибо за внимание, ещё увидимся!</p>
49 <p>Этот короткое интро - скорее, обзор возможностей на искусственном примере. Как сделать смартап с компонентами, оплатой, автотестами, обязательно расскажем в следующих статьях. Спасибо за внимание, ещё увидимся!</p>
50  
50