Actions-On-Google NodeJS v2 alpha: More than one conv.close() - actions-on-google

Will we be able to use conv.close() multiple times within an intent to provide more than just a single element on exit?
Similar to how you can provide multiple conv.ask() in the one intent.
Or can you include more than one 'new element' in a conv.close() tag?

Yes, of course to both ways! At least it's how it works currently during the alpha (which can change depending on feedback).
conv.ask and conv.close are implemented almost identically just that conv.close sets expectUserResponse to false which means you don't expect more responses from the user and the mic will be closed.
This means you can use conv.close just like conv.ask and call it multiple times.
For example, this code:
const { dialogflow } = require('actions-on-google')
const app = dialogflow()
app.intent('Default Welcome Intent', conv => {
conv.close(`Here's a cat image`)
conv.close(new Image({
url: 'https://developers.google.com/web/fundamentals/accessibility/' +
'semantics-builtin/imgs/160204193356-01-cat-500.jpg',
alt: 'A Cat',
}))
})
when the IntentHandler function is done executing (or if it returns a Promise, when the Promise is resolved), constructs a RichResponse based on the response fragments you provided and sends it back to Dialogflow or the Google Assistant.
It closes the mic and shows this as a result in the simulator.
Alternatively, conv.ask and conv.close also allow you to call it with an arbitrary number of response arguments. So this code will also work identical to the example before:
app.intent('Default Welcome Intent', conv => {
conv.close(`Here's a cat image`, new Image({
url: 'https://developers.google.com/web/fundamentals/accessibility/' +
'semantics-builtin/imgs/160204193356-01-cat-500.jpg',
alt: 'A Cat',
}))
})

Related

My useQuery hook is refetching everytime its called. I thought it is suppose to hand back the cache?

I'm a little confused here. I thought react-query, when using useQuery will hand back 'cache' n subsequent calls to the same "useQuery". But everytime I call it it, it refetches and makes the network call.
Is this the "proper way" to do this? I figured it would just auto hand me the "cache" versions. I tried extending staleTime and cacheTime, neither worked. Always made a network call. I also tried initialData with the cache there.. didn't work.
SO, I am doing the following, but seems dirty.
Here is the what I have for the hook:
export default function useProducts ({
queryKey="someDefaultKey", id
}){
const queryClient = useQueryClient();
return useQuery(
[queryKey, id],
async () => {
const cachedData = await queryClient.getQueryData([queryKey, id]);
if (cachedData) return cachedData;
return await products.getOne({ id })
}, {
enabled: !!id
}
);
}
This is initiated like so:
const { refetch, data } = useProducts(
{
id
}
}
);
I call "refetch" with an onclick in two diff locations.. I'd assume after I retrieve the data.. then subsequent clicks will hand back cache?
I’m afraid there are multiple misconceptions here:
react query operates on stale-while-revalidate, so it will give you data from the cache and then refetch in the background. You can customize this behavior by setting staleTime, which will tell the library how long the data can be considered fresh. No background updates will happen.
when you call refetch, it will refetch. It’s an imperative action. If you don’t want it, don’t call refetch.
you don’t need to manually read from the cache in the queryFn - the library will do that for you.

How to close conversation from the webhook in #assistant/conversation

I want to close the conversation after the media started playing in #assistant/conversation. As I am doing here
app.intent("media", conv => {
conv.ask(`Playing your Radio`);
conv.ask(
new MediaObject({
url: ""
})
);
return conv.close(new Suggestions(`exit`));
});
As Jordi had mentioned, suggestion chips cannot be used to close a conversation. Additionally, the syntax of the #assistant/conversation is different from actions-on-google. As you're using the tag dialogflow-es-fulfillment but also actions-builder, I really don't know which answer you want. As such, I'm going to put two answers depending on which you're using.
Dialogflow
If you are using Dialogflow, you are pretty much set. You should switch to using actions-on-google and instantiate the dialogflow constant.
const {dialogflow} = require('actions-on-google')
const app = dialogflow()
Actions Builder
The syntax of the #assistant/conversation lib is different. Some method names are different. Additionally, you will need to go through Actions Builder to canonically close the conversation.
In your scene, you will need to transition the scene to End Conversation to close, rather than specifying it as part of your response. Still, your end transition should not have suggestion chips.
You will need to refactor your webhook:
const {conversation} = require('#assistant/conversation')
const app = conversation()
app.handle("media", conv => {
conv.add(`Playing your Radio`);
conv.add(
new MediaObject({
url: ""
})
);
conv.add(new Suggestions(`exit`));
});
As it seems you are trying to have a media control and after that to end the conversation, you should refer to the doc (https://developers.google.com/assistant/conversational/prompts-media) to check the available events as you have the chance to control each one for the media playback.
For example
// Media status
app.handle('media_status', (conv) => {
const mediaStatus = conv.intent.params.MEDIA_STATUS.resolved;
switch(mediaStatus) {
case 'FINISHED':
conv.add('Media has finished playing.');
break;
case 'FAILED':
conv.add('Media has failed.');
break;
case 'PAUSED' || 'STOPPED':
if (conv.request.context) {
// Persist the media progress value
const progress = conv.request.context.media.progress;
}
// Acknowledge pause/stop
conv.add(new Media({
mediaType: 'MEDIA_STATUS_ACK'
}));
break;
default:
conv.add('Unknown media status received.');
}
});
Once you get the FINISHED status you can offer the suggestion chip to exit the conversation.

Google Assistant make GET request and reply with server response

I'd like to create an action in Google Assistant such that when a voice command is issued, the Assistant will make a GET request to a URL, like http://example.com/response.txt and just read out the plaintext response. How do I go about doing that?
You would need to create an Action using Actions Builder or Dialogflow.
This Action would start with a 'Default Welcome Intent' that you should connect it to a webhook:
This webhook can be written simply using a language like Node.js
import {conversation} from '#assistant/conversation'
const fetch = require('node-fetch')
const app = conversation()
const URL = 'http://example.com/response.txt'
app.handle('Default Welcome Intent', async conv => {
const apiResponse = await fetch(URL)
const text = await apiResponse.text()
conv.add(text)
})
Depending on whether you just want static information or not, you may want to then add a transition to 'end conversation' to close it out.

How to determine whether user is ready and nonce can be submitted to server?

This is one of many (imho rather incomplete) examples in the docs:
var button = document.querySelector('#submit-button');
braintree.dropin.create({
authorization: 'CLIENT_AUTHORIZATION',
container: '#dropin-container'
}, function (createErr, instance) {
button.addEventListener('click', function () {
instance.requestPaymentMethod(function (requestPaymentMethodErr, payload) {
// Submit payload.nonce to your server
});
});
});
It's all nice and easy but I don't see how I can change the state of button according to the state of "is the user done with adding a payment method?".
Is this even possible? It seems that the click on the button actually performs the fetching of the nonce (which comes as payload.nonce). However, how can I disable button until the user has finished his conversation with Braintree/PayPal?
And the answer is look in the docs (no not those docs) - those docs.
I still don't know how I found that link.
The dropin instance has a on() function where you can register a callback for certain events (just do it - look at the docs already):
instance.on('paymentMethodRequestable', function(event) {
thiz._logger.info('Payment method is now requestable');
setTimeout(() => thiz.paymentMethodAvailable = true, 400);
});

Meteor/Blaze/Mongo/Leaflet - Dynamically filled Leaflet popups do not pass data to button for entry into database?

I'm working with a Leaflet map that displays marker data based on a MongoDB query. The query results are saved into a variable (I know, bad form for large volumes of info but okay if you only have ~25 pieces) as an array, and then I've iterated over that variable and it's stored information using a for loop to create my leaflet map markers and populate their popups with the information specific to each entry. This part works great.
this.autorun(function(){
fsqresults = FsqResults.find().fetch({});
container = $('<div />');
for (i=0; i < fsqresults.length; i++) {
marker = L.marker([fsqresults[i].geometry.coordinates[1], fsqresults[i].geometry.coordinates[0]], {icon: violetIcon}).addTo(mymap);
container.html("<b>" + "Name: " + "</b>" + fsqresults[i].properties.name + "<br>" +
"<b>" + "Address: " + "</b>" + fsqresults[i].properties.address + "<br>" +
"<b>" + "Checkins: " + "</b>" + fsqresults[i].properties.checkIns + "<br>" +
"<b>" + "Users: " + "</b>" + fsqresults[i].properties.usersCount + "<br>" +
"<b>" + "Tips: " + "</b>" + fsqresults[i].properties.tips + "<br>");
marker.bindPopup(container[0]);
} // end for loop
For each marker, there is a button to log a "checkin" event to another Mongo collection to house the checkin entries. The button fires an event successfully, and creates the entry into the second database, but will not bind the dynamically populated data to the entry so I can see which marker the user has clicked on.
container.append($('<button class="btn btn-sm btn-outline-primary js-checkin">').text("Check In"));
container.on('click', '.js-checkin', function() {
var currentVenue = fsqresults[i].properties.name;
console.log(currentVenue);
console.log("You clicked the button!");
if (!Meteor.user()) {
alert("You need to login first!");
}
if (Meteor.user()) {
console.log("Meteor User Verified");
Checkins.insert({user: Meteor.user(), name: currentVenue});
}
});
}); // end this.autorun
The console tells me that currentVenue is undefined. I know this has something to do with the fact that fsqresults is a dynamically populated variable. I have tried to find ways to "solidify" the information in it (i.e. - creating a second variable with an empty array, pushing the data from fsqresults into it, and then having the markers iterate over that variable) but that hasn't worked as the MongoDB query results, despite being in an array format themselves, will not push or concat into the variable with an empty array successfully.
I've been searching for an answer to this problem and I'm coming up short. I'm lost; is there any other solution which could be staring me in the face?
Some things to note: All of this code lives in the Template.map.onRendered() function. Leaflet has scoping issues if I delegate the code into helpers and events, which is why I haven't created a {markers} template and just done {{#each markers}} over it for iteration. Therefore I am relegated to jQuery style coding for creating DOM elements and firing event triggers. The code above is wrapped in a this.autorun function to ensure it does indeed run upon map rendering. I don't think this is the issue (although one can never rule it out!).
As pointed out in the question comments, you have a scope issue with your i index iterator, and there should be no technical problem in integrating Leaflet with Meteor (although with Blaze that may not be totally trivial nor interesting).
1. Iteration scope issue
The console tells me that currentVenue is undefined.
That is because you try to access fsqresults[i].properties.name in your container.on('click' event listener / callback, which will be called on user click, i.e. after your for loop is complete, hence your i index iterator variable will be equal to fsqresults.length.
You are in the case of Example 6 of the accepted answer of: How do JavaScript closures work?
2. Leaflet integration with Meteor (Blaze)
Since you mention having tried helpers, events, and {{#each markers}}, I assume you use Blaze as your Meteor rendering engine.
While React-Leaflet and Vue2Leaflet indeed offer the possibility to use a kind of "<Marker>" component (same for other types of Leaflet Layer), the latter is only for template declaration purpose, i.e. it does not directly render any DOM / HTML, but only calls some Leaflet methods, which will be in charge of manipulating the DOM. As stated on React-Leaflet limitations:
The components exposed are abstractions for Leaflet layers, not DOM elements.
Side note: interesting to see that angular-leaflet-directive and #asymmetrik/ngx-leaflet did not fall into the same temptation and sticked to JS declaration of Leaflet layers.
Therefore trying to create a Template.Marker (used as {{> Marker}}) in Blaze might be overkill, as you would basically just call some Leaflet factories (like L.marker) within your Template.Marker.onCreated (and needing to access somehow the parent map object to add your Marker into…), without rendering any DOM node yourself (i.e. you would have an HTML file with empty <template name="Marker"></template>).
While we forget about a Marker template in Blaze (as you have already done), we can still leverage Blaze events management to handle user clicks in your Leaflet Popup. For that, we would need a few Blaze features, that I admit could benefit being better documented:
We can attach arbitrary JS data to our template instance.
Template events are delegated, hence we do not need to attach them to each <button> before hand.
We can easily access the template instance in event handlers (as the 2nd argument of the event listener).
2.1. Attaching arbitrary JS data to our template instance
As stated in the Template Instances API:
[…] you can assign additional properties of your choice to the object. Use the onCreated and onDestroyed methods to add callbacks performing initialization or clean-up on the object.
Therefore you can store your fsqresults on your Template instance, so that you can access it later on (typically in your event listener):
Template.myTemplate.onRendered(function () {
this.autorun(() => { // Using an arrow function to keep the same `this`, but you could do `const self = this` beforehand.
const fsqresults = this.fsqresults = FsqResults.find().fetch();
});
});
But since we want to access specific Features later on, it might be more interesting to convert fsqresults to a dictionary. Since your ID seems to be feature.properties.name, you could do:
Template.myTemplate.onCreated(function () {
this.autorun(() => {
const fsqresults = this.fsqresults = FsqResults.find().fetch();
const markersDict = this.markersDict = {};
L.geoJSON(fsqresults, {
pointToLayer(feature, latlng) {
const props = feature.properties;
const markerName = props.name;
// Save a direct reference to the Feature data,
// using the `markerName` as key (ID).
markersDict[markerName] = feature;
// Store the `markerName` in the button `dataset`
// (i.e. as a `data-` attribute),
// as already suggested in the question comments,
// so that we can easily retrieve the ID / key
// of the Marker data associated with the button the user clicked on.
return L.marker(latlng).bindPopup(`
<p>${markerName}</p>
<button role="popupClick" data-marker-name="${markerName}">
Popup action
</button>
`);
},
});
});
});
2.2. Template event handler delegation
As stated on the Blaze Overview Details:
DOM engine […] which features […] event delegation
(sorry there does not seem to be any other mention of this feature in the official doc… let me know if you find a better one!)
Therefore, as long as we create a Template event handler with the appropriate selector, we do not need to attach the event listener on each button, which anyway we may not create as a Node but leave it as String passed to Leaflet .bindPopup (as done in the above code sample).
For example:
Template.myTemplate.events({
// Even if the `<button role="popupClick">` are not DOM nodes yet
// (because Leaflet will create them from the HTML String
// only when the user opens the popup by clicking on the Marker),
// the "click" event will bubble up to the template instance,
// which will call this event handler if it matches the selector.
'click button[role="popupClick"]'() {
console.log('clicked on a button that has been built in a Leaflet Popup');
}
});
2.3. Access the template instance, and our Feature data
The Blaze event handler are called with an extra 2nd argument, which is the current template instance:
The handler function receives two arguments: event, an object with information about the event, and template, a template instance for the template where the handler is defined.
Therefore in our case we can easily retrieve the markersDict variable that we have defined in onCreated, and use it to retrieve the exact Marker's Feature data associated with the button the user clicked on:
Template.myTemplate.events({
'click button[role="popupClick"]'(event, templateInstance) {
const button = event.currentTarget;
const markerName = button.dataset.markerName;
const markerFeature = templateInstance.markersDict[markerName];
// Do something with `markerFeature`…
console.log(markerFeature);
}
});
If you only need the property name, then you could even skip step 2.1 and directly use the markerName string retrieved from the <button> dataset.
I've come up with a solution to the first part of my issue - at first a javascript closure/scope issue to the inner and outer function scopes. I spent about 2 days wrapping my head around this SO answer: the concept of using the first for loop to produce individual instances of the function (if this were a play, the first for loop would "set the stage" for the show), and using the second for loop to execute each instance of the function ("lights, camera, action!").
I also decided that I could maintain scope if I declared my variables inside the first for loop - but I still had this issue of it only pulling the last value. Then I tried simply redeclaring my variables as constants. To my surprise, using const allowed me to write each instance to each map marker, and I could reliably access the correct iteration of the data upon each correspondent map marker! So no need for a second for loop.
this.autorun(function(){
fsqresults_fetch = FsqResults.find().fetch({});
// console.log(fsqresults_fetch);
for (i = 0; i < fsqresults_fetch.length; i++) {
container = $('<div />');
const fsq_marker = L.marker([fsqresults_fetch[i].geometry.coordinates[1], fsqresults_fetch[i].geometry.coordinates[0]], {icon: blueIcon}).addTo(mymap);
const fsq_venueAddress = fsqresults_fetch[i].properties.address;
const fsq_venueName = fsqresults_fetch[i].properties.name;
const fsq_geometry = {type: "Point",
coordinates: [fsqresults_fetch[i].geometry.coordinates[0], fsqresults_fetch[i].geometry.coordinates[1]]};
container.html("<b>" + "Name: " + "</b>" + fsqresults_fetch[i].properties.name + "<br>" +
"<b>" + "Address: " + "</b>" + fsqresults_fetch[i].properties.address + "<br>");
container.append($('<button class="btn btn-sm btn-outline-primary" id="js-checkin">').text("Check In"));
fsq_marker.bindPopup(container[0]);
container.on('click', '#js-checkin', function() {
console.log("You clicked the button!");
if (!Meteor.user()) {
alert("You need to login first!");
}
if (Meteor.user()) {
console.log("Meteor User Verified");
Checkins.insert({type: "Feature", geometry: fsq_geometry, properties: {name: fsq_venueName, address: fsq_venueAddress, user: Meteor.user()}});
}
}); //end container.on
} //end for loop
}); //end this.autorun
As I said in the comment on the last response, it's a bit hack-y, but functional enough to do the job successfully.
Now what I'm really curious to try is the solution that #ghybs posted so I have my events grouped and firing as Blaze is supposed to work!