ลองเล่น Interactive Canvas เมื่อหน้าเว็บไปโผล่บน Google Assistant และคุมได้ด้วยเสียง

บล็อกนี้น่าจะเป็นบล็อก Technical แรกบนเว็บนี้ จากเดิมที่เคยเขียนไปนิดนึงบน Medium แต่สุดท้ายก็ตัดสินใจย้ายมาที่นี่แทน โฮสท์เอง คุมเองหาเรื่องใส่ตัวเองไปเลย

เอาล่ะ สำหรับบล็อกนี้จะหยิบเรื่องใหม่ที่เพิ่งโผล่มาในงาน Google I/O 2019 ที่ผ่านมา โดยเป็นการเปิดโลกใหม่สำหรับนักพัฒนาเว็บ เพราะว่าทางกูเกิลได้เปิดตัวสิ่งที่เรียกว่า Interactive Canvas ที่เป็นการทำให้ Google Assistant นั้นเปิด Web App ขึ้นมาได้จากคำสั่งของผู้ใช้ จากเดิมที่เป็นการสนทนาเฉยๆ หรือเปิดแอปบนอุปกรณ์อย่างเดียว พูดให้สั้นก็คือ เอา Google Assistant มาใช้งานเว็บไซต์ได้

ที่มาที่ไปของ Interactive Canvas นั้นเหมือนจะมาจากการที่กูเกิลเริ่มนำเสนออุปกรณ์กลุ่ม Smart Display มาเสริมในบ้านมากขึ้น จากเดิมที่คุยกันอย่างเดียว เลยอยากให้มีการแสดงผลมาเสริมด้วย

สถิคิชี้ว่าคนมี Smart Speaker แล้วเริ่มซื้อ Smart Display ตามด้วย

แน่นอนว่าพอมีช่องทางการใช้งานใหม่ พฤติกรรมการใช้งานของผู้ใช้ก็น่าจะเปลี่ยนไป จากเดิมที่เอามือเอานิ้วจิ้มๆ จอ แตะไปมาบนเว็บ พอมาเป็นบน Interactive Canvas ปุ๊บ จะกลายเป็นผู้ใช้สามารถพูดสั่งงานเว็บไซต์ได้ด้วย เพราะมี Google Assistant เป็นตัวกลางที่รับคำสั่งไปแปลงเป็นข้อมูลส่งให้หน้าเว็บที่แสดงนั่นเอง เริ่มดูน่าสนแล้วใช่มั้ย เขียนเว็บแล้วสั่งด้วยเสียง ดูน่าจะเท่สุดๆ ไปเลย งั้นมาดูกันดีกว่าว่าจะลองทำมันเองขึ้นมาสักอันเนี่ยทำยังไง

สำหรับโค้ดที่ผมจะเอามาบอกเล่ากันในนี้นั้นจะอ้างอิงจาก source code ตัวอย่างที่กูเกิลแจกไว้ให้ แล้วผมก็เอามาดัดแปลงเพิ่มไปเล็กน้อยครับ ถ้าใครสนใจ repo ต้นฉบับก็สามาถจิ้มเบาๆ ตรงนี้ไปดูได้

องค์ประกอบของการสร้าง Interactive Canvas นั้นมีด้วยกัน 3 ส่วนหลักๆ ได้แก่

  1. ส่วนทำความเข้าใจคำสั่งเสียง (Actions on Google + Dialogflow)
  2. ส่วนประมวลผลค่าจากคำสั่งเสียง (Back-end, ในตัวอย่างเป็น Node.js)
  3. ส่วนแสดงผล (Front-end, ในตัวอย่างเป็น HTML + JavaScript)

ทำความเข้าใจคำสั่งเสียง

ส่วนแรกที่จะพูดถึงก็คือการที่โปรแกรมจะแปลหาความหมายของคำสั่งเสียงเรา โดยท่ามาตรฐานของยุคนี้ก็คงหนีไม่พ้นการยืมแรงของ Dialogflow นั่นเอง แล้วจับผูกเข้ากับ Actions on Google ที่เราสามารถสร้างโปรเจคเพื่อรันบน Google Assistant ได้ ก็จะครบในส่วนของการรับคำสั่ง

เชื่อว่าน่าจะมีคนทำบล็อกแนะนำ Dialogflow กันไปเยอะแล้ว ผมจะผ่านตรงนี้ไปเร็วๆ โดยประเด็นสำคัญคือเราจะสร้างโปรเจคบน Actions on Google แล้วในส่วนของ Actions ให้เลือกเป็นหมวด Play Game แล้วจับต่อไปที่ Dialogflow

เมื่อมาที่ Dialogflow แล้วเราก็จะสร้างชุด Intents เอาไว้ให้แปลค่าจากเสียงออกมาได้ เพื่อความง่ายผมแนะนำให้ไปดาวน์โหลด หรือโคลนโปรเจคของผมลงมา แล้วเอาไฟล์ agent.zip ไป import เข้าใน Dialogflow ก็จะได้ข้อมูล Intents ที่เทรนมาล่วงหน้าแล้วครับ และจุดสำคัญอีกจุดคือ ถ้าเราเข้าไปที่รายละเอียดของแต่ละ Intent แล้วเลื่อนลงมาดูส่วนของ Fulfillment ต้องเลือกให้มีการใช้ webhook ที่เดี๋ยวเราจะกลับมาตั้งค่าเพิ่มเติมอีกทีหนึ่ง

ประมวลผลค่าที่ได้จากเสียง

ส่วนต่อไปเราจะกระโดดไปที่ระบบหลังบ้านกันบ้าง โดยผมเลือกใช้ Firebase Cloud Functions เพื่อความสะดวกรวดเร็วและเป็น HTTPS ในตัว เราก็สร้างโปรเจคกันคือมาด้วย firebase init พระเอกคนเดิมของเรา จากนั้นก็เลือกโปรเจคที่ใช้ Cloud Functions และ Hosting ครับ เพราะเดี๋ยวเราจะใช้สำหรับโฮสท์หน้าเว็บเราไปด้วยเลย

หลังจาก Initialize และติดตั้ง modules เบื้องต้นเสร็จสิ้นเราต้องลง module เพิ่มให้กับโปรเจคอีกตัวครับคือ actions-on-google อย่าลืมว่าต้องย้ายที่อยู่ directory ของเราไปที่โฟลเดอร์ functions ก่อนนะครับแล้วค่อยลง yarn add actions-on-google จะใช้เวลาค่อนข้างนานหน่อย เราก็แอบไปเขียนโค้ดไว้ก่อนก็ได้เปิดไฟล์ index.js ที่อยู่ในโฟลเดอร์ functions ขึ้นมาเลยแล้วก็โยนโค้ดผัวะเข้าไป

const functions = require('firebase-functions');
const {dialogflow, ImmersiveResponse} = require('actions-on-google');

const app = dialogflow({debug: true});
app.intent('welcome', (conv) => {
  conv.ask('Welcome!');
  conv.ask(new ImmersiveResponse({
    url: 'WEBSITE_URL',
  }));
});

const colorSet = ['red','blue','green'];

app.intent('textColor', (conv, {textColor}) => {
  if (colorSet.includes(textColor)) {
    conv.ask(`Ok, I changed my text color to ${textColor}. What else?`);
    conv.ask(new ImmersiveResponse({
      state: {
        textColor
      },
    }));
    return;
  }
  conv.ask(`Sorry, I don't know that color. What else?`);
  conv.ask(new ImmersiveResponse({
    state: {
      query: conv.query,
    },
  }));
});

exports.conversation = functions.https.onRequest(app);

เรามาดูโค้ดหลักกันที่ตรงนี้ครับ

จะเห็นว่ามีการตั้งค่า ImmersiveResponse อยู่ในตอนเริ่มรันระบบพร้อมกันให้เรากรอก URL ของเว็บไซต์ที่ตัว Google Assistant จะนำไปแสดงผลและตอบโต้กับผู้ใช้งาน แต่ทั้งหมดทั้งหมวดก็อยู่ภายในการทำงานของ Dialogflow นั่นเอง  (เข้าใจว่าชื่อยังไม่ลงตัว ตอนทำในโค้ดเลยเรียก ImmersiveResponse แต่ตอนโฆษณาดันใช้ชื่อ Interactive Canvas ฮ่าๆ)

เท่านี้ก็เป็นอันเสร็จระบบหลังบ้านของเรา แต่ยังไม่ต้องรีบขึ้น Deploy ก็ได้ครับเพราะว่าเรายังไม่มี Website URL มาใส่ให้กับโค้ดของเรา จังหวะนี้เราก็กระโดดไปที่ Front-end กันต่อ

ส่วนแสดงผล

มาถึงกันที่ส่วนสุดท้ายแต่กลับโดดเด่นที่สุดของเรา นั่นก็คือส่วนแสดงผลหรือ Web Front-end ครับ โดยสำหรับตัวอย่างจะเลือกใช้เป็นเว็บแบบบ้านๆ คือ HTML, CSS และ JavaScript กันครับ เริ่มมาก็สร้าง index.html ในโปรเจคเลย แล้วฟาดโค้ดป้าบเข้าไปตามนี้

<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Interactive Canvas Sample</title>
    <link rel="shortcut icon" type="image/x-icon" href="data:image/x-icon;,">
    <link rel="stylesheet" href="https://www.gstatic.com/assistant/immersivecanvas/css/styles.css">
    <script src="https://www.gstatic.com/assistant/immersivecanvas/js/immersive_canvas_api.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/stats.js/r16/Stats.min.js"></script>
    <link rel="stylesheet" href="css/main.css">
  </head>
  <body>
    <div id="view" class="view">
      <div id="text">Watch this text carefully -</div>
      <div class="debug">
        <div class="stats"></div>
        <div class="logs"></div>
      </div>
    </div>
    <!-- Load custom JavaScript after elements are on page -->
    <script src="js/main.js"></script>
    <script src="js/log.js"></script>
  </body>
</html>

ส่วนสำคัญของ index.html นั้นจะอยู่ที่เราโหลด script ที่ชื่อว่า immersive_canvas_api.js เข้ามาเพื่อที่เราจะเอาฟังก์ชันมาจัดการการแสดงผลต่อ

จากนั้นแต่งหน้าตามันหน่อยด้วย CSS ผมสร้างไฟล์ main.css ไว้ใน public/css ครับ ส่วนใหญ่จะเป็นสไตล์ที่ยึดมาตามตัวอย่างโค้ด เพื่อให้หน้าตาที่แสดงออกมาเข้ากับมือถือ และมีสไตล์ให้กับตัว log สำหรับ debug ส่วนจุดหลักที่ผมจะเน้นในตัวอย่างนี้คือ <div> ที่มี id คือ text ดังนั้นไฟล์ CSS ก็จะมี Style ตามนี้

html {
  display: flex;
  height: 100%;
}

body {
  display: flex;
  flex: 1;
  margin: 0;
  background-color: #F5F5F5;
}

.view {
  position: relative;
  flex: 1;
}

::-webkit-scrollbar {
  width: 0px;
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: transparent;
}

.debug {
  display: flex;
  flex-direction: column;

  /* Uncomment below to disable the debug overlay */
  /* display: none !important; */

  width: 200px;

  position: absolute;
  right: 8px;
  top: 8px;
  bottom: 8px;
}

.stats {
  display: flex;
  justify-content: flex-end;
}

.logs {
  display: block;
  flex: 1;
  opacity: 0.8;
  overflow-y: scroll;
  text-align: right;

  font-size: 12px;
}

.logs > p {
  display: inline-block;
  padding-top: 1px;
  padding-bottom: 1px;
  margin: 0px;
  margin-top: 1px;
  background: black;
  max-width: 100%;

  font-family: Arial, sans-serif;
  color: white;
  text-align: right;
  overflow-wrap: break-word;
}

.logs > p.error {
  color: red;
}

#text {
  color: black;
  font-weight: 600;
  font-size: 32px;
}

ต่อด้วยไฟล์ log.js สำหรับไว้ช่วย debug จัดเข้าโฟลเดอร์ตามสะดวกเลยครับ ของผมตั้งอยู่ใน public/js อีกที

const log = document.getElementsByClassName('logs')[0];

const prepend = (level, line) => {
  const p = document.createElement('p');
  p.textContent = line;
  p.className = level;
  log.prepend(document.createElement('br'));
  log.prepend(p);
};

const info = console.log.bind(console);
console.log = (...messages) => {
  prepend('log', messages.join(' '));
  info(...messages);
};

const error = console.error.bind(console);
console.error = (...messages) => {
  prepend('error', messages.join(' '));
  error(...messages);
};

แล้วก็ตัวหลักอีกตัวที่เราต้องใช้ก็คือ main.js

'use strict';

// register assistant canvas callbacks
const callbacks = {
  onUpdate(state) {
    console.log('onUpdate', JSON.stringify(state));
    if ('textColor' in state) {
      document.getElementById('text').style.color = state.textColor;
    }
  },
};
assistantCanvas.ready(callbacks);

ในไฟล์ main.js จะมีการเรียกใช้ assistantCanvas ที่มาจาก script ที่เราโหลดในหน้า index.html โดยตัว assistantCanvas นั้นจะถูกฝังเข้ากับตัวแปร window นั่นเอง

ส่วนโค้ดเสร็จเรียบร้อยเราก็จัดการ deploy ส่วนของ hosting ขึ้นได้เลยครับ

firebase deploy --only hosting

เมื่อ deploy เสร็จก็หยิบเอา URL ของหน้าเว็บมาลองเปิดดู จะได้หน้าตาทำนองนี้ โล่งๆ พร้อมกันข้อความที่เขียนไว้บนหน้า HTML

ถ้าได้ยังงี้แล้วก็ก๊อปปี้เอา URL ไปใส่ในโค้ดส่วนของ Cloud Funtions ครับ แล้วก็จัดการ deploy ต่อเลยด้วยคำสั่ง

firebase deploy --only functions

รอไปพักใหญ่จนมันจัดการเสร็จ ก็ตามไปเปิดดูบน Firebase Console ครับแล้วไปที่หน้า Functions จะเห็นว่ามี URL โผล่มาหนึ่งตัวหน้าตาประมาณนี้

https://{REGION}-{PROJECT_NAME}.cloudfunctions.net/conversation

ก็ก๊อปปี้มาแล้วกลับไปที่ Dialogflow กันอีกครั้ง คราวนี้มองไปที่เมนูซ้ายมือครับ หาคำว่า Fulfillment

จากนั้นก็กด Enable Webhook แล้วในช่อง URL ก็ใส่ลิงค์ที่เราได้จาก Cloud Functions เมื่อครู่เข้าไปครับ เท่านี้ก็เป็นการเชื่อมระบบกับ Dialogflow เป็นที่เรียบร้อย

ทดลองเล่น

ช่วงเวลาสนุกมาถึงแล้วครับ หลังจากวางโค้ดติดตั้งประกอบนู่นนี่มานาน เราจะทดสอบระบบของเรากันผ่าน Simulator บนเว็บกัน โดยจิ้มที่เมนู Integrations บน Dialogflow ครับ จากนั้นก็จะเห็น Google Assistant กล่องเบอเร่ออยู่ กดเข้าไปแล้วตั้งค่า Intent ตามนี้

จากนั้นก็กด TEST ข้างล่างครับ มันจะเด้งแท็บใหม่ไปยัง Actions on Google ให้เราเล่นกับ Simulator ที่จำลองการทำงานของ Google Assistant เราก็สามารถกด Talk to My Test App ซ้ายล่างเพิ่มเริ่มคุยได้

จากนั้นถ้าโค้ดทำงานได้ปกติ จะมีคำทักทายกลับมาตามที่เราโค้ดไปคือ Welcome!

หลังจากนั้นลองพิมพ์คำสั่งทำนองนี้ครับ

  • change text color to green
  • change text to blue
  • red text

จะเห็นว่าระบบสามารถเข้าใจคำสั่งของเรา แยกชื่อสีออกมาส่งมาที่หลังบ้านแล้วในส่วนตรงนี้

ก็จะเอาชื่อสี textColor ห่อเข้าใน object state ส่งต่อไปยังหน้าบ้านของเราที่มี assistantCanvas รอฟังอีเวนท์ onUpdate อยู่ แล้วฝั่งหน้าบ้านของเราก็ใช้ JavaScript ไปดึง element id text มาแล้วปรับค่า style color ใหม่เป็นตัวแปรที่ส่งมาจากหลังบ้านนั่นเอง

จะเห็นว่าหลักการเบื้องต้นของ Interactive Canvas นั้นง่ายมาก เพราะมันคือการส่งค่าให้กันระหว่างหลังบ้านและหน้าบ้าน เพียงแต่การได้มาซึ่งค่านั้นผ่านกระบวนการแปลคำพูดของ Google Assistant มาก่อน เลยทำให้ดูซับซ้อนขึ้น

เนื่องจาก Interactive Canvas นั้นรันอยู่บนหน้าเว็บไซต์ที่ใช้งาน JavaScript ได้ ทำให้ไม่ว่าจะเป็น JavaScript Framework ตัวไหนก็ควรจะนำมาใช้ได้เช่นกัน ไม่ว่าจะเป็น React, Vue หรือ Angular ก็ตามครับ ทำให้นักพัฒนาสามารถนำไปประยุกต์สร้างของแปลกใหม่ได้อีกเพียบเลย

อย่างไรก็ดี กูเกิลก็มีกำหนดข้อจำกัดบางอย่างเอาไว้เช่นกัน ไม่ว่าจะเป็นเรื่องประเภทของแอปพลิเคชัน ที่ตอนนี้จำกัดว่าจะทำขึ้น Production จริงได้ต้องเป็นกลุ่ม Gaming Experience เท่านั้น และข้อจำกัดทางเทคนิคของระบบเว็บไซต์ก็มีดังนี้

  • No cookies
  • No local storage
  • No geolocation
  • No camera usage
  • No popups
  • Origin is set to null for AJAX
  • Stay under the 200mb memory limit
  • 3P Header takes up upper portion of screen
  • No styles can be applied to videos
  • Only one media element may be used at a time
    No HLS video
  • Assets must accept requests from null origins

จะเห็นว่ามีบางข้อจำกัดที่น่าลำบากใจอยู่เหมือนกัน เอาจริงๆ ตัวผมเองก็เจอประเด็นข้อสุดท้ายที่ติด null origins ตอนจะลองโค้ดต้นฉบับของกูเกิลที่จะเอา svg เข้ามาแสดงผล เลยปรับโค้ดเล่นเองเพิ่มเพื่อที่จะได้เห็นผล

และนอกจากข้อจำกัดแล้ว ก็ยังมี Design Guideline ที่ทางกูเกิลแนะนำให้นักพัฒนาศึกษา เพื่อที่จะได้ออกแบบหน้าตาของ Interactive Canvas ออกมาได้สวยงาม ครบถ้วน และรองรับการตอบสนองผู้ใช้ได้ดีนั่นเอง

สำหรับใครที่รู้สึกว่าน่าสนใจขึ้นมาแล้ว ก็แนะนำให้ลองติดตามหรือลองเล่นอยู่เรื่อยๆ ครับ เพราะตัวผมเองก็มองว่ามีโอกาสที่หลังจากคน Adopt ผลิตภัณฑ์ Smart Home กันมากขึ้นแล้วการเข้าถึงก็จะมากขึ้นตาม และอาจจะเป็นโอกาสที่ดีทางธุรกิจก็เป็นได้

อ้อ เกือบลืมบอกว่าถ้าใครมี Google Assistant ในมือถือหรือ Smart Display อยู่แล้ว ลองทักด้วยคำว่า 'Talk to My Test App' แบบบน Simulator ดูครับ เพราะสามารถลองเล่นแอปที่เราเขียนเมื่อครู่ผ่านช่องทางนั้นได้เช่นกัน และถ้าสนใจลองศึกษาโค้ดที่ผมปรับแต่งไปเพิ่มเติมได้บน Github ครับ

หรือลองดูคลิปจากงาน Google I/O ที่ผ่านมาก็ได้ครับ อธิบายค่อนข้างละเอียดทีเดียว (ผมเองก็ยังดูไม่จบเลย)

ขอให้สนุกกับการเล่น Interactive Canvas ครับ :)