I've created an adaptive card(using json) in my chatbot that takes input from users. I want to add a button that enables the user to add a new text field every time the user clicks on the insert field. (i.e., the user can click on insert button to enter details of education (school, college etc.))
Can this be achieved in adaptive cards?
I also wanted to know, can adaptive cards be designed in any other language (excluding json)
The easiest way to do this is with Action.ShowCard:
{
"type": "AdaptiveCard",
"body": [
{
"type": "Input.Text",
"placeholder": "Placeholder 1",
"id": "text1"
}
],
"actions": [
{
"type": "Action.ShowCard",
"title": "Add field",
"card": {
"type": "AdaptiveCard",
"body": [
{
"type": "Input.Text",
"placeholder": "Placeholder 2",
"id": "text2"
}
],
"actions": [
{
"type": "Action.ShowCard",
"title": "Add field",
"card": {
"type": "AdaptiveCard",
"body": [
{
"type": "Input.Text",
"placeholder": "Placeholder 3",
"id": "text3"
}
],
"actions": [
{
"type": "Action.ShowCard",
"title": "Add field",
"card": {
"type": "AdaptiveCard",
"body": [
{
"type": "Input.Text",
"placeholder": "Placeholder 4",
"id": "text4"
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json"
}
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json"
}
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json"
}
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.0"
}
You may not like the way that looks, but there is an alternative. Microsoft Teams allows you to update messages, so you can update the card with more input fields in response to a submit action. First, you'll need a way of saving state for your card so you can update the card's activity. In C# you can declare a state property accessor like this:
public IStatePropertyAccessor<Dictionary<string, (string ActivityId, int InputCount)>> InputCardStateAccessor { get; internal set; }
Then you can instantiate it like this:
InputCardStateAccessor = _conversationState.CreateProperty<Dictionary<string, (string, int)>>("cardState");
In Node.js you won't need to declare anything but you can instantiate it like this:
this.inputCardState = this.conversationState.createProperty('cardState');
You'll want a consistent way to generate your card that you can use when you send the card initially and when you update the card. I'm using the AdaptiveCards NuGet package in C#:
public static IActivity GenerateAdaptiveCardActivityWithInputs(int inputCount, object valueObject)
{
var cardData = JObject.FromObject(valueObject);
var cardId = Convert.ToString(cardData[KEYCARDID]);
var card = new AdaptiveCard(new AdaptiveSchemaVersion(1, 0))
{
Body = Enumerable.Range(0, inputCount).Select(i =>
{
var inputId = $"text{i}";
return new AdaptiveTextInput
{
Id = inputId,
Value = Convert.ToString(cardData[inputId]),
};
}).ToList<AdaptiveElement>(),
Actions = new List<AdaptiveAction>
{
new AdaptiveSubmitAction
{
Title = "Add field",
Data = new Dictionary<string, string>
{
{ KEYCARDID, cardId },
{ KEYSUBMITACTIONID, ACTIONSUBMITADDFIELD },
},
},
new AdaptiveSubmitAction
{
Title = "Submit",
},
},
};
return MessageFactory.Attachment(new Attachment(AdaptiveCard.ContentType, content: JObject.FromObject(card)));
}
Node.js:
generateAdaptiveCardActivityWithInputs(inputCount, cardData) {
var cardId = cardData[KEYCARDID];
var body = [];
for (let i = 0; i < inputCount; i++) {
var inputId = `text${i}`;
body.push({
type: "Input.Text",
id: inputId,
value: cardData[inputId]
});
}
var card = {
type: "AdaptiveCard",
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
version: "1.0",
body,
actions: [
{
type: "Action.Submit",
title: "Add field",
data: {
[KEYCARDID]: cardId,
[KEYSUBMITACTIONID]: ACTIONSUBMITADDFIELD
},
},
{
type: "Action.Submit",
title: "Submit",
}
]
};
return MessageFactory.attachment(CardFactory.adaptiveCard(card));
}
Using this function, you can send the card initially like this in C#:
var inputCount = 1;
var cardId = Guid.NewGuid().ToString();
var reply = GenerateAdaptiveCardActivityWithInputs(inputCount, new Dictionary<string, string> { { KEYCARDID, cardId } });
var response = await turnContext.SendActivityAsync(reply, cancellationToken);
var dict = await InputCardStateAccessor.GetAsync(turnContext, () => new Dictionary<string, (string, int)>(), cancellationToken);
dict[cardId] = (response.Id, inputCount);
Node.js:
var inputCount = 1;
var cardId = Date.now().toString();
var reply = this.generateAdaptiveCardActivityWithInputs(inputCount, { [KEYCARDID]: cardId });
var response = await turnContext.sendActivity(reply);
var dict = await this.inputCardState.get(turnContext, {});
dict[cardId] = {
activityId: response.id,
inputCount: inputCount
};
await this.inputCardState.set(turnContext, dict);
And you can update the card in response to the card's "add field" submit action like this in C#:
private async Task AddFieldAsync(ITurnContext turnContext, CancellationToken cancellationToken)
{
var activity = turnContext.Activity;
if (activity.ChannelId == Channels.Msteams)
{
var value = JObject.FromObject(activity.Value);
var cardId = Convert.ToString(value[KEYCARDID]);
var dict = await InputCardStateAccessor.GetAsync(turnContext, () => new Dictionary<string, (string, int)>(), cancellationToken);
if (dict.TryGetValue(cardId, out var cardInfo))
{
var update = GenerateAdaptiveCardActivityWithInputs(++cardInfo.InputCount, value);
update.Id = cardInfo.ActivityId;
update.Conversation = activity.Conversation;
await turnContext.UpdateActivityAsync(update, cancellationToken);
dict[cardId] = cardInfo;
}
}
}
Node.js:
async addField(turnContext) {
var activity = turnContext.activity;
if (activity.channelId == 'msteams') {
var value = activity.value;
var cardId = value[KEYCARDID];
var dict = await this.inputCardState.get(turnContext, {});
var cardInfo = dict[cardId];
if (cardInfo) {
var update = this.generateAdaptiveCardActivityWithInputs(++cardInfo.inputCount, value);
update.id = cardInfo.activityId;
update.conversation = activity.conversation;
update.serviceUrl = activity.serviceUrl;
dict[cardId] = cardInfo;
await this.inputCardState.set(turnContext, dict);
await turnContext.updateActivity(update);
}
}
}
yes this is possible you can look about the addRow in javascript
Related
I need to build a tree like structure using data from an API.
The structure i start with is as follows:
{
"type": "group",
"id": 1,
"name": "rootGroup",
"members": [],
}
There will always be a root group as the base of the tree.
I have a function named getMembersInGroup(groupId) which is an API call and returns something like:
[
{
"type": "group",
"id": 77,
"name": "IT group",
},
{
"type": "user",
"id": 40,
"name": "John"
}
]
Members can either be of type user or another group. So a user would look like:
{
"type": "user",
"id": 40,
"name": "John"
}
If it's another group it needs to recursively fetch those until there are only users or empty array left in members.
Any group can have users at any level with the tree.
A mock of getMembersInGroup:
const getMembersInGroup = async (groupId) => {
try {
const members = await fetch.callApi('/groups/' + groupId + '/members');
if (members) {
return members;
}
else {
return [];
}
} catch (error) {
return { error };
}
}
The end result should look like this:
{
"type": "group",
"id": 1,
"name": "rootGroup",
"members": [
{
"type": "group",
"id": 88,
"name": "Some group",
"members": [
{
"type": "user",
"id": 231,
"name": "SALLY"
},
{
"type": "user",
"id": 232,
"name": "Henry"
}
]
},
{
"type": "user",
"id": 41,
"name": "Chris"
}
],
}
I need help with the algorithm to create the tree.
Your getMembersInGroup function could look like this:
const getMembersInGroup = async (groupId) => {
const members = (await fetch.callApi(`/groups/${groupId}/members`)) ?? [];
for (const member of members) {
if (member.type == "group") {
member.members = await getMembersInGroup(member.id);
}
}
return members;
}
Call it like this:
async function loadTree() {
return {
type: "group",
id: 1,
name: "rootGroup",
members: await getMembersInGroup(1)
};
}
loadTree().then(result =>
console.log(result);
// Work with the result ...
).catch(error =>
console.log("error: ", error)
);
Demo with a mock implementation of fetch.callApi:
// Mock for fetch.callApi
const fetch = {
mockData: [0,[2,3,4],[5,6,7],[8,9],0,0,0,[10],0,0,[11,12],0,0],
callApi(url) {
return new Promise((resolve, reject) => {
const groupId = +url.split("/")[2];
const children = this.mockData[groupId];
if (!children) return reject("not found: " + groupId);
const result = children.map(id => {
const type = this.mockData[id] ? "group" : "user";
return {type, id, name: type + "_" + id};
});
setTimeout(() => resolve(result), 50);
});
}
}
async function loadTree() {
return {
type: "group",
id: 1,
name: "rootGroup",
members: await getMembersInGroup(1)
};
}
const getMembersInGroup = async (groupId) => {
const members = (await fetch.callApi('/groups/' + groupId + '/members')) ?? [];
for (const member of members) {
if (member.type == "group") {
member.members = await getMembersInGroup(member.id);
}
}
return members;
}
loadTree().then(result =>
console.log(JSON.stringify(result, null, 2))
).catch(error =>
console.log("error: ", error)
);
You can do something like:
const getMembersInGroup = async (groupId) => {
try {
const members = await fetch.callApi('/groups/' + groupId + '/members');
if (members) {
foreach(member in members) {
if (member.type == 'groups') {
member.members = getMembersInGroup(member.groupid)
}
}
return members;
}
else {
return [];
}
} catch (error) {
return { error };
}
}
So you have the recursion only if it's a group type, otherwise the member is returned as is.
I'm currently using JsForce + Express, but I find that when I attempt to manipulate the data in the backend, the API call is extremely slow. Specifically, when I drill down to the Ids/Names of each of the individual objects, it slows down dramatically/fails to load.
It loads in less a second if I use this code:
router.get("/testChain", (req, res) => {
conn
.query(
"SELECT Id, Name, (SELECT Id, Part_Type__c FROM Monster_Parts__r) FROM Monster_Frame__c"
)
.then((result) => {
let gMonster = {};
let frames = result.records;
// Select Frame
let sFrameNo = randomNumber(frames.length);
let sFrame = frames[sFrameNo];
gMonster["Monster_Frame__c"] = sFrame.Id;
// Break into parts
let parts = sFrame.Monster_Parts__r.records;
let sortedParts = parts.reduce((r, o) => {
var k = o.Part_Type__c;
if (r[k] || (r[k] = [])) r[k].push(o);
return r;
}, {});
// Select Part
Object.keys(sortedParts).forEach((key, i, arr) => {
let parts = sortedParts[key];
let partNo = randomNumber(parts.length);
let selectedPart = parts[partNo];
gMonster[key] = selectedPart;
});
// Generate Name
gMonster["Name"] = sFrame.Name + "-" + randomNumber();
return gMonster;
})
.then((result) => {
res.json(result);
});
});
And gives me the following output:
{
"Monster_Frame__c": "a025j000004GsdXAAS",
"Tail__c": {
"attributes": {
"type": "Monster_Parts__c",
"url": "/services/data/v42.0/sobjects/Monster_Parts__c/a015j00000CfGUXAA3"
},
"Id": "a015j00000CfGUXAA3",
"Part_Type__c": "Tail__c"
},
"Leg__c": {
"attributes": {
"type": "Monster_Parts__c",
"url": "/services/data/v42.0/sobjects/Monster_Parts__c/a015j00000CfGUeAAN"
},
"Id": "a015j00000CfGUeAAN",
"Part_Type__c": "Leg__c"
},
"Head__c": {
"attributes": {
"type": "Monster_Parts__c",
"url": "/services/data/v42.0/sobjects/Monster_Parts__c/a015j00000CfGU0AAN"
},
"Id": "a015j00000CfGU0AAN",
"Part_Type__c": "Head__c"
},
"Body__c": {
"attributes": {
"type": "Monster_Parts__c",
"url": "/services/data/v42.0/sobjects/Monster_Parts__c/a015j00000CfGUKAA3"
},
"Id": "a015j00000CfGUKAA3",
"Part_Type__c": "Body__c"
},
"Back__c": {
"attributes": {
"type": "Monster_Parts__c",
"url": "/services/data/v42.0/sobjects/Monster_Parts__c/a015j00000CfGUPAA3"
},
"Id": "a015j00000CfGUPAA3",
"Part_Type__c": "Back__c"
},
"Name": "Serpent-7210"
}
However, if I change this line from:
gMonster[key] = selectedPart;
to populate the specific Id (instead of all the fields):
gMonster[key] = selectedPart.Id;
The API call fails 60% of the time. (Sometimes it returns with the desired output of carrying only the Ids.
Does anyone know how to fix this?
I want to create a context menu item depending on what the context link is.
For instance, I want to display "AAA" for a link that starts with "https://www.google.com" and "BBB" for a link that starts with "https://twitter.com".
var context_twitter_search = chrome.contextMenus.create({
"title":'AAA', // This is what I want to change depending on the info.linkUrl.
"contexts":["link"],
"onclick":function(info, tab) {
var target_content = '';
target_content = info.linkUrl; // I can ONLY get the content after right-click.
chrome.tabs.create({url:"https://twitter.com/search?q="+target_content});
}
});
Try this in your background script:
function onClickHandler(info, tab) {
let tp = info.menuItemId;
let url = info.linkUrl;
switch (tp) {
case 'AAA':
alert('hi google user');
break;
case 'BBB':
alert('hi Tweeter user');
break;
}
}
function handleInstalled(details) {
var contextMenuItem = [
{
"id": "AAA",
"title": "AAA",
"contexts": ["link"],
"targetUrlPatterns": ["https://*.google.com/*"]
}, {
"id": "BBB",
"title": "BBB",
"contexts": ["link"],
"targetUrlPatterns": ["https://*.twitter.com/*"]
}
];
let cmi;
for (let k = 0; k < contextMenuItem.length; k++) {
cmi = contextMenuItem[k];
chrome.contextMenus.create({
"id": cmi.id,
"title": cmi.title,
"contexts": cmi.contexts,
"enabled": true,
"targetUrlPatterns": cmi.targetUrlPatterns
}, () => {
if (chrome.runtime.lastError) {}
else {}
})
}
chrome.contextMenus.onClicked.addListener(onClickHandler)
}
chrome.runtime.onInstalled.addListener(handleInstalled);
I am using Microsoft teams bot with nodejs. I am rendering a carousel of adaptive cards with action on each card. My requirement is to delete an individual card out on which the action was clicked. Is it possible?
Current code looks like below. i have given a try to deleteActive but that deletes entire carousel
const {
TurnContext,
TeamsActivityHandler,
CardFactory,
AttachmentLayoutTypes,
ActionTypes
} = require('botbuilder');
class TeamsConversationBot extends TeamsActivityHandler {
constructor() {
super();
this.onMessage(async (context:any, next:any) => {
TurnContext.removeRecipientMention(context.activity);
console.log("context activigty at the begin is:" + JSON.stringify(context.activity))
let msg = context.activity.text
let action = context.activity.value
if(msg.startsWith('lead')){
msg = 'lead'
}
if(action !== undefined){
console.log("user did some action on a card")
msg = action.action
}
switch (msg) {
case 'lead':
await this.lead(context)
break;
case 'qualify_lead':
await this.qualifyLead(context)
break;
}
await next();
});
}
/**
*
* #param context this method does a lead qualification
*/
async qualifyLead(context:any){
console.log("in qualifyLead:" + JSON.stringify(context.activity))
//await context.deleteActivity(context.activity.replyToId)
const leadId = context.activity.value.objectId
console.log("Lead to qualify is:" + leadId)
await context.sendActivity('Lead is qualified')
}
/**
* Search contact by name
* #param context
* #param keyword
*/
async lead(context:any){
console.log("Start of lead with context:" + JSON.stringify(context))
const cardArr = []
let items = [
{"Name": 'x', "LeadId": "1"},
{"Name": 'a', "LeadId": "2"},
{"Name": 'b', "LeadId": "3"},
{"Name": 'c', "LeadId": "4"},
{"Name": 'd', "LeadId": "5"}
]
for(const item of items){
const header = {
"type": "TextBlock",
"size": "Medium",
"weight": "Bolder",
"text": item.Name
}
const actions = [
{
"type": "Action.Submit",
"title": "Qualify",
"data": { "action" : "qualify_lead", "objectId" : item.LeadId }
}
]
const acard = CardFactory.adaptiveCard(
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.0",
"body": [
header,
''
],
"actions": actions
}
)
cardArr.push(acard)
console.log("payload is::::" + JSON.stringify(acard))
}
const reply = {
"attachments" : cardArr,
"attachmentLayout" : AttachmentLayoutTypes.Carousel
}
await context.sendActivity(reply);
}
}
module.exports.TeamsConversationBot = TeamsConversationBot;
As with this other answer, the answer will be similar to this one. I can see you're trying to use TypeScript but your code deviates very little from JavaScript so I'll just write my answer in JavaScript.
First, you'll need a way of saving state for your [carousel] so you can update the [carousel]'s activity.
this.carouselState = this.conversationState.createProperty('carouselState');
You'll want a consistent way to generate your [carousel] that you can use when you send the [carousel] initially and when you update the [carousel].
createCarousel(batchId, leads)
{
const cardArr = [];
let items = [
{ "Name": 'x', "LeadId": 1 },
{ "Name": 'a', "LeadId": 2 },
{ "Name": 'b', "LeadId": 3 },
{ "Name": 'c', "LeadId": 4 },
{ "Name": 'd', "LeadId": 5 }
];
items = items.filter(item => leads.includes(item.LeadId));
for (const item of items) {
const header = {
"type": "TextBlock",
"size": "Medium",
"weight": "Bolder",
"text": item.Name
};
const actions = [
{
"type": "Action.Submit",
"title": "Qualify",
"data": { [KEYACTION]: ACTIONQUALIFYLEAD, [KEYOBJECTID]: item.LeadId, [KEYBATCHID]: batchId }
}
];
const acard = CardFactory.adaptiveCard(
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.0",
"body": [
header
],
"actions": actions
}
);
cardArr.push(acard);
}
return {
"type": "message",
"attachments": cardArr,
"attachmentLayout": AttachmentLayoutTypes.Carousel
};
}
This is similar to your code but there are some important differences. First, I'm filtering the items array to allow for fewer items, which is how you'll end up deleting cards from your carousel. Second, I'm including a "batch ID" in each action's data, which is how your bot will know which activity to update when it receives the action's payload. Also, this isn't relevant to your question but I'm using string constants instead of string literals most everywhere I expect to use that string more than once, which is a practice I follow to avoid typo-related bugs etc.
Using this function, you can send the [carousel] initially like this
async testCarousel(turnContext) {
const batchId = Date.now();
const leads = [1, 2, 3, 4, 5];
const reply = this.createCarousel(batchId, leads);
const response = await turnContext.sendActivity(reply);
const dict = await this.carouselState.get(turnContext, {});
dict[batchId] = {
[KEYACTIVITYID]: response.id,
[KEYLEADS]: leads
};
}
And you can update the [carousel] in response to the card's [qualify] submit action like this
async handleSubmitAction(turnContext) {
const value = turnContext.activity.value;
switch (value[KEYACTION]) {
case ACTIONQUALIFYLEAD:
const dict = await this.carouselState.get(turnContext, {});
const batchId = value[KEYBATCHID];
const info = dict[batchId];
if (info) {
const leads = info[KEYLEADS];
const objectId = value[KEYOBJECTID];
var index = leads.indexOf(objectId);
if (index !== -1) leads.splice(index, 1);
const update = this.createCarousel(batchId, leads);
update.id = info[KEYACTIVITYID];
if (update.attachments.length) {
await turnContext.updateActivity(update);
} else {
await turnContext.deleteActivity(update.id);
}
}
break;
}
}
I just want to ask two simple questions and then show the card. Problem is, in the second "sendActivity" keeps on repeating "please give password" just forever. I tried to place another onTurn after and even inside the function, with worst or same results. Dont want to implement a whole waterfall just for 2 questions. Which ActivityHandler fits better what am trying to achieve?
async processLogin(context, next, res) {
await context.sendActivity({
text: 'please give username'
})
const SelectedCard2 = CARDS2[0];
this.onTurn(async (context, next, res) => {
let txt = `"${context.activity.text}"`;
if (txt) {
var name = JSON.parse(txt);
console.log(name)
}
await context.sendActivity({
text: 'please give password'
})
let txt2 = `"${context.activity.text}"`;
if (txt2) {
var password = JSON.parse(txt2);
console.log(password)
res = password;
}
await next();
});
}
enter link description hereIf you just want to collect some info from user by an easy , you can use adaptive card in one step, try the code below :
const { ActivityHandler,CardFactory } = require('botbuilder');
class EchoBot extends ActivityHandler {
constructor() {
super();
// See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types.
var adaptiveCard = {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.0",
"body": [
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"width": 2,
"items": [
{
"type": "TextBlock",
"text": "Pls type your info here . Don't worry, we'll never share or sell your information.",
"isSubtle": true,
"wrap": true,
"size": "Small"
},
{
"type": "TextBlock",
"text": "Username",
"wrap": true
},
{
"type": "Input.Text",
"id": "username",
"placeholder": "your user name here"
},
{
"type": "TextBlock",
"text": "Password",
"wrap": true
},
{
"type": "Input.Text",
"id": "password",
"placeholder": "makre sure no one is around you ..."
}
]
}
]
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Submit"
}
]
};
this.onMessage(async (context, next) => {
if(context.activity.text==="login"){
await context.sendActivity({ attachments: [CardFactory.adaptiveCard(adaptiveCard)] });
}else if(context.activity.value != undefined){
var user = context.activity.value;
await context.sendActivity("hello , your username : " + user.username + ",password :" + user.password);
}else {
await context.sendActivity("send login to do test");
}
await next();
});
this.onMembersAdded(async (context, next) => {
const membersAdded = context.activity.membersAdded;
for (let cnt = 0; cnt < membersAdded.length; ++cnt) {
if (membersAdded[cnt].id !== context.activity.recipient.id) {
await context.sendActivity('Hello and welcome!');
}
}
// By calling next() you ensure that the next BotHandler is run.
await next();
});
}
}
module.exports.EchoBot = EchoBot;
This code is based on official nodejs echo bot , just cover the content of bot.js file to test it :
Hope it helps .