Telegraf.js: leave WizardScene with a button - node.js

I would put a button after a message in a WizardScene with a "Cancel" Button. But i retrieve some error:
This is my wizard scene:
const superWizard = new WizardScene('super-wizard',
async ctx => {
ctx.wizard.state.data = {};
ctx.telegram.sendMessage(ctx.from.id, "Insert name", {
parse_mode: 'MarkdownV2',
reply_markup: cancelOrder()
})
return ctx.wizard.next();
},
ctx => {
ctx.wizard.state.data.name = ctx.message.text;
ctx.reply("here is your name: "+ctx.wizard.state.data.name);
return ctx.scene.leave();
}
);
const stage = new Stage([superWizard]);
bot.use(session());
bot.use(stage.middleware());
Here is my cancel order function:
function cancelOrder() {
const annullaBtn = Markup.inlineKeyboard([
Markup.callbackButton('CANCEL', `cancelOrder_btn`),
])
return annullaBtn;
}
and the button action:
bot.action("cancelOrder_btn", (ctx) => {
ctx.replyWithMarkdown(`Ordine *ANNULLATO* correttamente`)
return ctx.scene.leave('super-wizard');
});
The program writes correctly the text, and puts the button. But if i press "CANCEL" it gives error at:
ctx.wizard.state.data.name = ctx.message.text;
as "text is undefined" because i press cancel and i didn't write anything.
So how can i leave the scene without going forward, but if i write a text it goes forward in the wizardScene?
Thank you

Replace
ctx.message.text;
with
ctx.update.callback_query.data;
as there is no message returned by callback button

Related

Tiptap how to create a paragraph (p) on Shift-Enter, instead of a br?

Using TipTap, I'm trying to avoid adding a <br />, but create a <p></p> instead, with the focus inside that <p>|</p> when the user hit shift-Enter but I can't make it work.
Here's what I did so far:
new (class extends Extension {
keys () {
return {
'Shift-Enter' (state, dispatch, view) {
const { schema, tr } = view.state
const paragraph = schema.nodes.paragraph
console.log(tr.storedMarks)
const transaction = tr.deleteSelection().replaceSelectionWith(paragraph.create(), true).scrollIntoView()
view.dispatch(transaction)
return true
}
}
}
})()
How can I do this?
I don't know if this is still relevant but as I was looking for the same thing, I found two ways to make this work.
NOTE:
I'm using tiptap v2, if that's not a problem, then:
I overrode the HardBreak extension, since it's the one that use the Shift-Enter keybinding. It looks something like;
const CustomHardBreak = HardBreak.extend({
addKeyboardShortcuts() {
return {
"Mod-Enter": () => this.editor.commands.setHardBreak(),
"Shift-Enter": () => this.editor.commands.addNewline(),
};
},
});
And used it like so;
editor = new Editor({
extensions: [
customNewline,
CustomHardBreak,
]
});
Use the default editor command createParagraphNear. E.g this.editor.commands.createParagraphNear()
I tried creating a custom extension from your code and ended up with something similar to the command above, i.e;
export const customNewline = Extension.create({
name: "newline",
priority: 1000, // Optional
addCommands() {
return {
addNewline:
() =>
({ state, dispatch }) => {
const { schema, tr } = state;
const paragraph = schema.nodes.paragraph;
const transaction = tr
.deleteSelection()
.replaceSelectionWith(paragraph.create(), true)
.scrollIntoView();
if (dispatch) dispatch(transaction);
return true;
},
};
},
addKeyboardShortcuts() {
return {
"Shift-Enter": () => this.editor.commands.addNewline(),
};
},
});
And added this as an extension in my editor instance.
PS:
They both work, almost exactly the same, I haven't found a difference yet. But there's somewhat of a 'catch' if you would call it that; Both these methods don't work on empty lines/nodes, a character has to be added before the cursor for it to work, any character, even a space.
In TipTap 2.0 I am able to use this custom extension:
const ShiftEnterCreateExtension = Extension.create({
addKeyboardShortcuts() {
return {
"Shift-Enter": ({ editor }) => {
editor.commands.enter();
return true;
},
};
},
});
To make shift + enter behave like enter.
In my case I actually wanted enter to do something different. So I use prosemirror events to set a ref flag on whether shift was pressed. Than I check that flag under the "Enter" keyboard event -- which could be triggered normally or through the shift + enter extension.

microsoft bots to teams using nodejs fails with missing activityid when updating the same activity

My code has a simple card carousel which has action button like below:
actions = [
{
"type": "Action.Submit",
"title": "Qualify",
"data": { "action" : "qualify_lead" }
},
{
"type": "Action.OpenUrl",
"title": "Retire",
"url": "{viewUrl}"
},
{
"type": "Action.ShowCard",
"title": "Add Note",
"card": this.noteCard(item.LeadId, "leads")
}
]
I am having a method to handle qualify_lead action like below
async qualifyLead(context:any){
console.log("in qualifyLead:" + JSON.stringify(context.activity))
await context.updateActivity(this.successCard('Lead is qualified'))
}
All I am doing on purpose is to replace entire carousel with a simple text message. But it fails with error:
Error: BotFrameworkAdapter.updateActivity(): missing activity.id
Where do i get this ?
I am using google firebase for this and the wrapper code is like below
const {
TurnContext,
TeamsActivityHandler,
CardFactory,
AttachmentLayoutTypes,
ActionTypes
} = require('botbuilder');
class TeamsConversationBot extends TeamsActivityHandler {
constructor() {
super();
this.leadState =
this.conversationState.createProperty('leadCarouselState');
this.onMessage(async (context:any, next:any) => {
TurnContext.removeRecipientMention(context.activity);
let msg = context.activity.text
const action = context.activity.value
let objNum = ''
let keyword = ''
if(msg === undefined && action === undefined)
msg = 'help'
else if(msg !== undefined){
msg = msg.trim().toLowerCase()
if(msg.indexOf("help") > -1)
msg = 'help'
else{
if(msg.startsWith('lead')){
msg = 'lead'
}
}
}
switch (msg) {
case 'lead':
await this.lead(context, userKey, platform)
break;
case 'qualify_lead':
await this.qualifyLead(context)
break;
}
await next();
});
}
I'm not sure exactly what this.successCard('Lead is qualified') does, but presumably it returns an Activity. To my knowledge, in order for this Activity to replace another one, you need to set it's Id property to match the previous message. That means that, when you send the previous message (i.e. the card), you need to capture the reference that's returned from the send method on the context (e.g. into bot state), and then use it for this new activity.
As I explained in my answer to your other question, you need to save the activity ID in bot state and then apply it to the update that you're sending. The Bot Framework can't update an activity unless you tell it which activity to update, and you do that using an activity ID.
This was the part that saves the ID:
const dict = await this.carouselState.get(turnContext, {});
dict[batchId] = {
[KEYACTIVITYID]: response.id,
[KEYLEADS]: leads
};
And this was the part that applies it to the updated activity:
const update = this.createCarousel(batchId, leads);
update.id = info[KEYACTIVITYID];

dialog.showMessageBoxSync(null, options) getting hidden

I have an Electron app. If I use dialog.showmessageBoxSync normally it has to wait for user input. The options are: close, cancel or ok.
It is working fine but if I click outside of the dialog box (anywhere inside my app) then this message box hidden. I'm unable to click on any option.
How can I make the message box stay focused until the user chooses a button to click or closes the dialog box? The user should be forced to respond to the message box before continuing to work in the rest of the app.
dialog.showMessageBoxSync({
type: "info",
buttons: ["Ok,", "Cancel"],
defaultId: 0,
title: "",
message:""
cancelId: 1,
})
I'd suggest passing in a parent window
From the docs
The browserWindow argument allows the dialog to attach itself to a
parent window, making it modal.
const iconPath = upath.toUnix(upath.join(__dirname, "app", "assets", "icon.png"));
const dialogIcon = nativeImage.createFromPath(iconPath);
var options = {
type: 'question',
buttons: ['&Yes', '&No'],
title: 'Confirm Quit',
icon: dialogIcon,
normalizeAccessKeys: true,
message: 'Do you really want to close the application?'
};
const win = BrowserWindow.getFocusedWindow();
dialog.showMessageBox(win, options)
.then((choice) => {
if (choice.response === 0) {
quitApplication();
}
}).catch(err => {
console.log('ERROR', err);
});

Including dialogs or reusing dialogs from different file

I am trying to do begindialog from another another dialog js file. I am getting error.
<b>[onTurnError]: Error: DialogContext.beginDialog(): A dialog with an id of 'FollowUpDialog' wasn't found. </b>
this is dialog structure-
dialogs
orderstatus
orderstatus.js
index.js
FollowUp
followUp.js
index.js
i am trying to include FollowUp dialog in OrderStatus Dialog, similary i have other dialogs where i want to begin followup or orderstatus dialog. trying to reuse the dialogs.
One way to do use how we include the file in botjs amd to do adddialog same way i can include in otherfile. But it is redundant work. I am trying to avoid that. Can some one tell me better approach to include the dialog in different dialogs.
code:
Below code is from greeting.js
If you see line where i am doing begindialog.
return await step.beginDialog(ORDER_STATUS_DIALOG);
return await step.beginDialog(ENTITLEMENT_CHECK_DIALOG);
It is error. I am trying to include the dialog which is part of different JS files.
async step => {
if (step.context.activity.channelId == 'directline') {
const buttons = [{
type: ActionTypes.ImBack,
title: 'Repair Order Status',
value: symbolicString.ZEB_GR_STR_013
}, {
type: ActionTypes.ImBack,
title: 'Entitlement Check',
value: symbolicString.ZEB_GR_STR_014
}];
const card = CardFactory.heroCard('', undefined, buttons, {
text: symbolicString.ZEB_GR_STR_015
});
const reply = {type: ActivityTypes.Message};
reply.attachments = [card];
return await step.context.sendActivity(reply);
} else {
return await step.prompt(MENU_PROMPT, symbolicString.ZEB_GR_STR_028);
}
},
async step => {
if (step.context.activity.channelId != 'directline'){
console.log("step greeting dialog next step");
console.log(step);
console.log(step.context.activity);
if (step.context.activity.text == '1'){
return await step.beginDialog(ORDER_STATUS_DIALOG);
}else if (step.context.activity.text == '2'){
return await step.beginDialog(ENTITLEMENT_CHECK_DIALOG);
}
}
return await step.endDialog();
}
]));

Bot framework (v4) Prompt choice in carousel using HeroCards not going to next step

I’m trying to use HeroCards along with a prompt choice in a carousel. So the options to be selected by the user are displayed as HeroCards. As soon as the user clicks in the button of a card it should goes to the next waterfall function.
Here is a working example in bot framework v3. It does work as expected.
const cards = (data || []).map(i => {
return new builder.HeroCard(session)
.title(`${i.productName} ${i.brandName}`)
.subtitle(‘product details’)
.text(‘Choose a product’)
.images([builder.CardImage.create(session, i.image)])
.buttons([builder.CardAction.postBack(session, `${i.id.toString()}`, ‘buy’)]);
});
const msg = new builder.Message(session);
msg.attachmentLayout(builder.AttachmentLayout.carousel);
msg.attachments(cards);
builder.Prompts.choice(session, msg, data.map(i => `${i.id.toString()}`), {
retryPrompt: msg,
});
Below I’m trying to do the same with bot framework v4 but it does not work. It never goes to the next function in my waterfall.
How can I do the same with v4?
…
this.addDialog(new ChoicePrompt(PRODUCTS_CAROUSEL));
…
const productOptions: Partial<Activity> = MessageFactory.carousel(
item.map((p: Product) =>
CardFactory.heroCard(
p.productName,
‘product details’,
[p.image || ''],
[
{
type: ActionTypes.PostBack,
title: ‘buy’,
value: p.id,
},
],
),
),
‘Choose a product’,
);
return await step.prompt(PRODUCTS_CAROUSEL, productOptions);
…
UPDATE:
Follow full code with the suggestion from #Drew Marsh
export class ProductSelectionDialog extends ComponentDialog {
private selectedProducts: Product[] = [];
private productResult: Product[][];
private stateAccessor: StatePropertyAccessor<State>;
static get Name() {
return PRODUCT_SELECTION_DIALOG;
}
constructor(stateAccessor: StatePropertyAccessor<State>) {
super(PRODUCT_SELECTION_DIALOG);
if (!stateAccessor) {
throw Error('Missing parameter. stateAccessor is required');
}
this.stateAccessor = stateAccessor;
const choicePrompt = new ChoicePrompt(PRODUCTS_CAROUSEL);
choicePrompt.style = ListStyle.none;
this.addDialog(
new WaterfallDialog<State>(REVIEW_PRODUCT_OPTIONS_LOOP, [
this.init.bind(this),
this.selectionStep.bind(this),
this.loopStep.bind(this),
]),
);
this.addDialog(choicePrompt);
}
private init = async (step: WaterfallStepContext<State>) => {
const state = await this.stateAccessor.get(step.context);
if (!this.productResult) this.productResult = state.search.productResult;
return await step.next();
};
private selectionStep = async (step: WaterfallStepContext<State>) => {
const item = this.productResult.shift();
const productOptions: Partial<Activity> = MessageFactory.carousel(
item.map((p: Product) =>
CardFactory.heroCard(
p.productName,
'some text',
[p.image || ''],
[
{
type: ActionTypes.ImBack,
title: 'buy',
value: p.id,
},
],
),
),
'Choose a product',
);
return await step.prompt(PRODUCTS_CAROUSEL, {
prompt: productOptions,
choices: item.map((p: Product) => p.id),
});
};
private loopStep = async (step: WaterfallStepContext<State>) => {
console.log('step.result: ', step.result);
};
}
PARENT DIALOG BELOW:
...
this.addDialog(new ProductSelectionDialog(stateAccessor));
...
if (search.hasIncompletedProducts) await step.beginDialog(ProductSelectionDialog.Name);
...
return await step.next();
...
MY BOT DIALOG STRUCTURE
onTurn()
>>> await this.dialogContext.beginDialog(MainSearchDialog.Name) (LUIS)
>>>>>> await step.beginDialog(QuoteDialog.Name)
>>>>>>>>> await step.beginDialog(ProductSelectionDialog.Name)
UPDATE
Replacing the ChoicePrompt with TextPromt (as suggested by Kyle Delaney) seems to have the same result (do not go to the next step) but I realised that if remove return from the prompt like this:
return await step.prompt(PRODUCTS_CAROUSEL, `What is your name, human?`); TO await step.prompt(PRODUCTS_CAROUSEL, `What is your name, human?`);
it does work but when I'm returning the original code with ChoicePrompt without return like this:
await step.prompt(PRODUCTS_CAROUSEL, {
prompt: productOptions,
choices: item.map((p: Product) => p.id),
});
I'm getting another error in the framework:
error: TypeError: Cannot read property 'length' of undefined
at values.sort (/xxx/Workspace/temp/13.basic-bot/node_modules/botbuilder-dialogs/lib/choices/findValues.js:84:48)
at Array.sort (native)
at Object.findValues (/xxx/Workspace/temp/13.basic-bot/node_modules/botbuilder-dialogs/lib/choices/findValues.js:84:25)
at Object.findChoices (/xxx/Workspace/temp/13.basic-bot/node_modules/botbuilder-dialogs/lib/choices/findChoices.js:58:25)
at Object.recognizeChoices (/xxx/Workspace/temp/13.basic-bot/node_modules/botbuilder-dialogs/lib/choices/recognizeChoices.js:75:33)
at ChoicePrompt.<anonymous> (/xxx/Workspace/temp/13.basic-bot/node_modules/botbuilder-dialogs/lib/prompts/choicePrompt.js:62:39)
at Generator.next (<anonymous>)
at /xxx/Workspace/temp/13.basic-bot/node_modules/botbuilder-dialogs/lib/prompts/choicePrompt.js:7:71
at new Promise (<anonymous>)
at __awaiter (/xxx/Workspace/temp/13.basic-bot/node_modules/botbuilder-dialogs/lib/prompts/choicePrompt.js:3:12)
this is the line:
// Sort values in descending order by length so that the longest value is searched over first.
const list = values.sort((a, b) => b.value.length - a.value.length);
I can see the data from my state is coming properly
prompt: <-- the data is ok
choices: <-- the data is ok too
Sometimes I'm getting this error too:
error: TypeError: Cannot read property 'status' of undefined
at ProductSelectionDialog.<anonymous> (/xxxx/Workspace/temp/13.basic-bot/node_modules/botbuilder-dialogs/lib/componentDialog.js:92:28)
at Generator.next (<anonymous>)
at fulfilled (/xxxx/Workspace/temp/13.basic-bot/node_modules/botbuilder-dialogs/lib/componentDialog.js:4:58)
at <anonymous>
at process._tickDomainCallback (internal/process/next_tick.js:228:7)
this line
// Check for end of inner dialog
if (turnResult.status !== dialog_1.DialogTurnStatus.waiting) {
You're using a ChoicePrompt, but when you call prompt you're only passing through an activity (the carousel). ChoicePrompt is going to try to validate the input against a set of choices that you should be passing in when you call prompt. Because you're not doing this, the prompt is not recognizing the post back value as valid and technically should be reprompting you with the carousel again to make a valid choice.
The fix here should be to call prompt with PromptOptions instead of just a raw Activity and set the choices of the PromptOptions to an array that contains all the values you expect back (e.g. the same value you set for the value of the post back button).
This should end up looking a little something like this:
Since you're providing the choices UX with your cards, you want to set the ListStyle on the ChoicePrompt to none
const productsPrompt = new ChoicePrompt(PRODUCTS_CAROUSEL);
productsPrompt.style = ListStyle.none;
this.addDialog(productsPrompt);
Then, set the available choices for the specific prompt:
return await step.prompt(PRODUCTS_CAROUSEL, {
prompt: productOptions,
choices: items.map((p: Product) => p.id),
});
Basically Drew Marsh was right.
I just would like to add some other details that I had to tweak to make it work. In case someone else is going crazy like I was. It could give some insights. It's all about how you handle the returns of nested dialogs.
First change. I had to transform the identifier of the Choice prompt into string:
{
type: ActionTypes.PostBack,
title: 'buy',
value: p.id.toString(),
},
and
return await step.prompt(PRODUCTS_CAROUSEL, {
prompt: productOptions,
choices: item.map((p: Product) => p.id.toString()),
});
Another problem that I found was in the parent dialog:
I was basically trying to do this:
if (search.hasIncompletedProducts) await step.beginDialog(ProductSelectionDialog.Name);
return await step.next();
Which makes no sense, then I changed it to:
if (search.hasIncompletedProducts) {
return await step.beginDialog(ProductSelectionDialog.Name);
} else {
return await step.next();
}
And then the final change in the parent of the parent dialog:
Before was like this:
switch (step.result) {
case ESearchOptions.OPT1:
await step.beginDialog(OPT1Dialog.Name);
break;
default:
break;
}
await step.endDialog();
Which again does not make sense since I should return the beginDialog or endDialog. It was changed to:
switch (step.result) {
case ESearchOptions.OPT1:
return await step.beginDialog(OPT1Dialog.Name);
default:
break;
}

Resources