Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

email generator #21

Merged
merged 1 commit into from
Aug 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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