Skip to content

Commit

Permalink
Merge pull request #21 from ildecimo/develop
Browse files Browse the repository at this point in the history
email generator
  • Loading branch information
maxdyy committed Aug 13, 2023
2 parents 4490b21 + 81829fc commit 472ae92
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 64 deletions.
37 changes: 37 additions & 0 deletions src/app/api/generate-email/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NextResponse, type NextRequest } from 'next/server';
import { type Review } from 'types';

import { authorize } from '~/lib/authorize';
import * as db from '~/lib/db';

import { generateAISuggestedEmail } from '~/server/google-ai/generate-email';

export async function POST(req: NextRequest) {
const authorized = authorize();

if (!authorized) {
return new NextResponse('Unauthorized', { status: 401 });
}

const accessToken = await db.getStoreToken(authorized.storeHash);

if (!accessToken) {
return new NextResponse(
'Access token not found. Try to re-install the app.',
{ status: 401 }
);
}

const reqBody = (await req.json()) as { review: Review; emailType: string };

const email = await generateAISuggestedEmail({
rating: reqBody.review.rating,
title: reqBody.review.title,
text: reqBody.review.text,
customer: reqBody.review.name,
emailType:
reqBody.emailType === 'thank-you' ? 'Thank you email' : 'Follow-up email',
});

return NextResponse.json(email);
}
23 changes: 23 additions & 0 deletions src/components/ComplimentBadges.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Badge } from '@bigcommerce/big-design';

interface ComplimentBadgesProps {
complimentCategoriesArray?: string[];
}

export const ComplimentBadges = ({
complimentCategoriesArray = [],
}: ComplimentBadgesProps) => {
return (
<div className="flex flex-wrap items-center">
<span className="mr-2">Compliments:</span>

{complimentCategoriesArray.length > 0 ? (
complimentCategoriesArray.map((compliment) => (
<Badge key={compliment} label={compliment} variant="success" />
))
) : (
<Badge label="No compliment" variant="warning" />
)}
</div>
);
};
64 changes: 64 additions & 0 deletions src/components/GenerateEmailButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Button } from '@bigcommerce/big-design';
import { EnvelopeIcon, HeartIcon } from '@heroicons/react/24/solid';
import { useState } from 'react';

import { type Review } from 'types';
import { type AnalyzeEmailOutputOptions } from '~/server/google-ai/generate-email';

interface GenerateEmailButtonProps {
variant: 'thank-you' | 'follow-up';
review: Review;
}

export const GenerateEmailButton = ({
variant,
review,
}: GenerateEmailButtonProps) => {
const [isGenerating, setIsGenerating] = useState(false);
const isThankYou = variant === 'thank-you';

const handleGenerateEmail = () => {
console.log('Generating email...');

setIsGenerating(true);
fetch('/api/generate-email', {
method: 'POST',
body: JSON.stringify({
review,
emailType: isThankYou ? 'thank-you' : 'follow-up',
}),
})
.then((res) => res.json() as Promise<AnalyzeEmailOutputOptions>)
.then((res) => {
const { body, subject } = res;

if (body && subject) {
window.open(
`mailto:${review.email}?subject=${subject}&body=${body}`,
'_blank'
);
} else {
console.warn('Email not generated!', res);
}
})
.catch((err) => console.log(err))
.finally(() => setIsGenerating(false));
};

return (
<Button
iconLeft={
isThankYou ? (
<HeartIcon className="h-6 w-6" />
) : (
<EnvelopeIcon className="h-6 w-6" />
)
}
variant="secondary"
onClick={handleGenerateEmail}
isLoading={isGenerating}
>
{isThankYou ? 'Thank You Email' : 'Follow Up Email'}
</Button>
);
};
4 changes: 2 additions & 2 deletions src/components/IssueBadges.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ interface IssuesBadgesProps {
issuesCategoriesArray?: string[];
}

export const IssuesBadges = ({
export const IssueBadges = ({
issuesCategoriesArray = [],
}: IssuesBadgesProps) => {
return (
<div className="flex flex-wrap items-center">
<span className="mr-2">Issues found:</span>
<span className="mr-2">Issues:</span>

{issuesCategoriesArray.length > 0 ? (
issuesCategoriesArray.map((issue) => (
Expand Down
41 changes: 23 additions & 18 deletions src/components/ReviewDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
'use client';

import { Box, Button } from '@bigcommerce/big-design';
import { CheckIcon, EnvelopeIcon, HeartIcon } from '@heroicons/react/24/solid';
import { CloseIcon } from '@bigcommerce/big-design-icons';
import { CheckIcon } from '@heroicons/react/24/solid';
import clsx from 'clsx';
import { useState } from 'react';
import GaugeComponent from 'react-gauge-component';

import { type Product, type Review } from 'types';
import { type Orders } from '~/server/bigcommerce-api/schemas';
import { type AnalyzeReviewOutputValues } from '~/server/google-ai/analyze-review';

import { AIChatBubble } from '~/components/AIChatBubble';
import { Breadcrumbs } from '~/components/Breadcrumbs';
import { Card } from '~/components/Card';
import { IssuesBadges } from '~/components/IssueBadges';
import { ComplimentBadges } from '~/components/ComplimentBadges';
import { GenerateEmailButton } from '~/components/GenerateEmailButton';
import { IssueBadges } from '~/components/IssueBadges';
import { ReviewStatusBadge } from '~/components/ReviewStatusBadge';
import { ScoreCircle } from '~/components/ScoreCircle';
import { StarRating } from '~/components/StarRating';

import { CloseIcon } from '@bigcommerce/big-design-icons';
import { ScoreCircle } from '~/components/ScoreCircle';
import { type AnalyzeReviewOutputValues } from '~/server/google-ai/analyze-review';
import { convertToDateString, convertToUDS, parseScore } from '~/utils/utils';

interface ReviewDetailProps {
Expand Down Expand Up @@ -177,7 +179,7 @@ export const ReviewDetail = ({
<div className="w-[calc(100%-62px)]">
<AIChatBubble
message={
<IssuesBadges
<IssueBadges
issuesCategoriesArray={
sentimentAnalysis?.issueCategories ?? []
}
Expand All @@ -187,6 +189,19 @@ export const ReviewDetail = ({
/>
</div>

<div className="w-[calc(100%-62px)]">
<AIChatBubble
message={
<ComplimentBadges
complimentCategoriesArray={
sentimentAnalysis?.complimentCategories ?? []
}
/>
}
hideAvatar
/>
</div>

<div className="pl-16">
{review.status !== 'approved' &&
(parsedScore.isNeutral || parsedScore.isPositive) && (
Expand All @@ -210,21 +225,11 @@ export const ReviewDetail = ({
)}

{parsedScore.isPositive && (
<Button
iconLeft={<HeartIcon className="h-6 w-6" />}
variant="secondary"
>
Thank you email
</Button>
<GenerateEmailButton variant="thank-you" review={review} />
)}

{(parsedScore.isNeutral || parsedScore.isNegative) && (
<Button
iconLeft={<EnvelopeIcon className="h-6 w-6" />}
variant="secondary"
>
Follow-up email
</Button>
<GenerateEmailButton variant="follow-up" review={review} />
)}
</div>
</div>
Expand Down
48 changes: 4 additions & 44 deletions src/server/google-ai/analyze-review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type AnalyzeReviewInputOptions = z.infer<typeof analyzeReviewInputSchema>;
export const analyzeReviewOutputSchema = z.object({
description: z.string(),
issueCategories: z.array(z.string()),
complimentCategories: z.array(z.string()),
keywords: z.array(z.string()),
score: z.number(),
});
Expand All @@ -25,22 +26,6 @@ export type AnalyzeReviewOutputValues = Zod.infer<
typeof analyzeReviewOutputSchema
>;

const analyzeIssuesCategorySchema = z.object({
rating: z.number(),
title: z.string(),
text: z.string(),
});

type AnalyzeIssuesCategoryOptions = z.infer<typeof analyzeIssuesCategorySchema>;

const prepareInput = (options: AnalyzeReviewInputOptions): string => {
return `
"Title": ${options.title}
"Description": ${options.text}
"Rating": ${options.rating} / 5
`;
};

export async function analyzeReview(options: AnalyzeReviewInputOptions) {
// @todo: store results in firestore to avoid recalling the API for already-analysed reviews.

Expand All @@ -59,6 +44,7 @@ Output Format:
{
"description": string,
"issueCategories": Array<"shipping" | "product quality" | "product packaging" | "customer service" | "payment process" | "price" | "return and refund" | "sales and promotions" | "website experience" | "customer expectations">,
"complimentCategories": Array<"shipping" | "product quality" | "product packaging" | "customer service" | "payment process" | "price" | "return and refund" | "sales and promotions" | "website experience" | "customer expectations">,
"keywords": Array<string>,
"score": number
}
Expand All @@ -69,6 +55,7 @@ Output Format details:
- "description:" A text description of 40 to 50 words explaining the customer's feelings based on their review.
- "issueCategories": Based on provided review, if there are issues, provide categories of the issue from the specified union type. If there are no issues provide an empty array.
- "complimentCategories": Based on provided review, if there are compliments, provide categories of the compliment from the specified union type. If there are no compliments provide an empty array.
- "keywords": The main words or phrases from the review that most influenced the determined sentiment.
- "score": The sentiment score evaluated from the customer's review. This must be a number from 0 to 100, where 0 - 32 is the negative range, 33 - 65 is the neutral range, and 66 - 100 is the positive range.
Expand All @@ -93,7 +80,7 @@ The review to analyze:
const output = response[0].candidates[0]?.output;

if (env.NODE_ENV === 'development') {
console.log('*** [Vertex Output] ::', output);
console.log('*** [Vertex Review Analysis Output] ::', output);
}

if (output) {
Expand All @@ -115,31 +102,4 @@ The review to analyze:
return 'No response from Google AI';
}

export async function analyzeIssuesCategory(
options: AnalyzeIssuesCategoryOptions
) {
const input = prepareInput(options);

const prompt = `Act as an e-commerce customer care expert who analyzes product reviews.
Task: Based on provided review, if there are issues respond with one or multiple categories of the issue from [shipping, product quality, product packaging, customer service, payment process, price, return and refund, sales and promotions, website experience, customer expectations].
If the review does not contain issues, respond with "no issues". If the review contains multiple issues, respond with all of them separated by commas.
Review: ${input}
`;

try {
const client = new TextServiceClient({
authClient: new GoogleAuth().fromAPIKey(API_KEY),
});

const response = await client.generateText({
model: MODEL_NAME,
prompt: { text: prompt },
});

if (response && response[0] && response[0].candidates) {
return response[0].candidates[0]?.output || 'No response from Google AI';
}
} catch (error) {
console.error(error);
}
}
Loading

0 comments on commit 472ae92

Please sign in to comment.