Skip to content

Commit

Permalink
feat(webhook): display search result from multiple searches
Browse files Browse the repository at this point in the history
  • Loading branch information
MrOrz committed Jan 2, 2024
1 parent a1d3be2 commit 7bd87a7
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 16 deletions.
56 changes: 46 additions & 10 deletions src/webhook/handlers/askingArticleSubmissionConsent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { t } from 'ttag';
import { Message } from '@line/bot-sdk';
import { z } from 'zod';

import { ChatbotPostbackHandler } from 'src/types/chatbotState';
Expand All @@ -25,6 +24,9 @@ import {
createNotificationSettingsBubble,
getLineContentProxyURL,
createAIReply,
searchText,
searchMedia,
createCooccurredSearchResultsCarouselContents,
} from './utils';

// Input should be array of context.msgs idx. Empty if the user does not want to submit.
Expand Down Expand Up @@ -138,9 +140,6 @@ const askingArticleSubmissionConsent: ChatbotPostbackHandler = async ({
UserArticleLink.createOrUpdateByUserIdAndArticleId(userId, article.id)
);

// Create new session, make article submission button expire after submission
context.sessionId = Date.now();

// Use first article as representative article
const articleUrl = getArticleURL(createdArticles[0].id);
const articleCreatedMsg = t`Your submission is now recorded at ${articleUrl}`;
Expand All @@ -154,19 +153,56 @@ const askingArticleSubmissionConsent: ChatbotPostbackHandler = async ({
if (context.msgs.length > 1) {
// Continue with the rest of the messages
// FIXME: implement this

const searchResults = await Promise.all(
context.msgs.map(async (msg) =>
msg.type === 'text'
? searchText(msg.text)
: searchMedia(getLineContentProxyURL(msg.id), userId)
)
);

return {
context,
replies: [
createTextMessage({
text: `🔍 ${t`There are some messages that looks similar to the ones you have sent to me.`}`,
}),
createTextMessage({
text:
t`Internet rumors are often mutated and shared.
Please choose the version that looks the most similar` + '👇',
}),
{
type: 'flex',
altText: t`Please choose the most similar message from the list.`,
contents: {
type: 'carousel',
contents: createCooccurredSearchResultsCarouselContents(
searchResults,
context.sessionId
),
},
},
],
};
}

// The user only asks for one article
//
const article = createdArticles[0];
const aiReply = await aiReplyPromises[0];

const { allowNewReplyUpdate } = await UserSettings.findOrInsertByUserId(
userId
);
const [aiReply, { allowNewReplyUpdate }] = await Promise.all([
aiReplyPromises[0],
UserSettings.findOrInsertByUserId(userId),
]);

return {
context,
context: {
...context,
// Create new session, make article submission button expire after submission
//
sessionId: Date.now(),
},
replies: [
{
type: 'flex',
Expand Down
29 changes: 26 additions & 3 deletions src/webhook/handlers/askingCooccurrence.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { z } from 'zod';
import { msgid, ngettext, t } from 'ttag';
import { FlexSpan } from '@line/bot-sdk';

import ga from 'src/lib/ga';
import gql from 'src/lib/gql';
import { ChatbotPostbackHandler } from 'src/types/chatbotState';

import {
Expand All @@ -13,9 +15,8 @@ import {
searchMedia,
getLineContentProxyURL,
createPostbackAction,
createCooccurredSearchResultsCarouselContents,
} from './utils';
import gql from 'src/lib/gql';
import { FlexSpan } from '@line/bot-sdk';

const inputSchema = z.enum([POSTBACK_NO, POSTBACK_YES]);

Expand Down Expand Up @@ -176,11 +177,33 @@ const askingCooccurence: ChatbotPostbackHandler = async ({
],
};
}

// Get first few search results for each message, and make at most 10 options
//

return {
context,
replies: [],
replies: [
createTextMessage({
text: `🔍 ${t`There are some messages that looks similar to the ones you have sent to me.`}`,
}),
createTextMessage({
text:
t`Internet rumors are often mutated and shared.
Please choose the version that looks the most similar` + '👇',
}),
{
type: 'flex',
altText: t`Please choose the most similar message from the list.`,
contents: {
type: 'carousel',
contents: createCooccurredSearchResultsCarouselContents(
searchResults,
context.sessionId
),
},
},
],
};
}

Expand Down
65 changes: 62 additions & 3 deletions src/webhook/handlers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import type { Input as AskingArticleSourceInput } from './askingArticleSource';
import type { Input as AskingArticleSubmissionConsentInput } from './askingArticleSubmissionConsent';
import type { Input as askingCooccurenceInput } from './askingCooccurrence';

const MAX_CAROUSEL_BUBBLE_COUNT = 9;

const splitter = new GraphemeSplitter();

/**
Expand Down Expand Up @@ -1026,8 +1028,8 @@ export function createTextCarouselContents(
},
};
}
) /* flex carousel has at most 10 bubbles */
.slice(0, 9);
) /* Avoid too many bubbles */
.slice(0, MAX_CAROUSEL_BUBBLE_COUNT);
}

type SearchMediaResult = Omit<
Expand Down Expand Up @@ -1214,5 +1216,62 @@ export function createMediaCarouselContents(
};
}
)
.slice(0, 9); /* flex carousel has at most 10 bubbles */
.slice(0, MAX_CAROUSEL_BUBBLE_COUNT); /* Avoid too many bubbles */
}

function getSimilarity(
edge: SearchMediaResult['edges'][number] | SearchTextResult['edges'][number]
) {
return 'mediaSimilarity' in edge ? edge.mediaSimilarity : edge.similarity;
}

export function createCooccurredSearchResultsCarouselContents(
searchResults: (SearchMediaResult | SearchTextResult)[],
sessionId: number
): FlexBubble[] {
const idEdgeMap: Record<
string,
SearchMediaResult['edges'][number] | SearchTextResult['edges'][number]
> = {};

// We try to get equal number of items out of every search result,
// starting from the first ranked items from each list.
//
for (
let idx = 0, depletedSearchResultCount = 0;
Object.keys(idEdgeMap).length < MAX_CAROUSEL_BUBBLE_COUNT &&
depletedSearchResultCount < searchResults.length;
idx += 1
) {
for (const searchResult of searchResults) {
if (idx == searchResult.edges.length) {
depletedSearchResultCount += 1;
continue;
} else if (idx > searchResult.edges.length) {
continue;
}

// Update idEdgeMap if the edge is not in the map or has higher similarity
const currentEdge = searchResult.edges[idx];
if (
!idEdgeMap[currentEdge.node.id] ||
getSimilarity(idEdgeMap[currentEdge.node.id]) <
getSimilarity(currentEdge)
) {
idEdgeMap[currentEdge.node.id] = currentEdge;
}
}
}

return (
Object.values(idEdgeMap)
// Sort all edges by similarity
.sort((a, b) => getSimilarity(b) - getSimilarity(a))
.flatMap((edge) =>
// For each edge, call its corresponding create*CarouselContents function
'mediaSimilarity' in edge
? createMediaCarouselContents([edge], sessionId)
: createTextCarouselContents([edge], sessionId)
)
);
}

0 comments on commit 7bd87a7

Please sign in to comment.