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

dev -> main #20

Merged
merged 14 commits into from
Aug 13, 2023
Binary file removed public/images/vertex-ai.jpeg
Binary file not shown.
Binary file added public/images/vertex-ai.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions src/app/api/disapprove-review/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { NextResponse, type NextRequest } from 'next/server';
import { authorize } from '~/lib/authorize';
import * as db from '~/lib/db';
import { disapproveReview } from '~/server/bigcommerce-api';

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 { productId: number; reviewId: number };

const review = await disapproveReview({
...reqBody,
accessToken,
storeHash: authorized.storeHash,
});

return NextResponse.json(review);
}
6 changes: 4 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Source_Sans_3 } from 'next/font/google';
import { type Metadata } from 'next/types';
import StyledComponentsRegistry from '~/lib/registry';
import { TailwindIndicator } from '~/components/TailwindIndicator';
import ThemeProvider from '~/components/ThemeProvider';
import StyledComponentsRegistry from '~/lib/registry';
import '~/styles/main.css';

const sourceSans = Source_Sans_3({
subsets: ['latin'],
weight: ['300', '400', '600', '700', '800'],
});

export const metadata: Metadata = { title: 'Product description generator' };
export const metadata: Metadata = { title: 'Review Pulse - ildecimo BigAI' };

export default function RootLayout({
children,
Expand All @@ -25,6 +26,7 @@ export default function RootLayout({
<StyledComponentsRegistry>
<ThemeProvider>
<main className={sourceSans.className}>{children}</main>
<TailwindIndicator />
</ThemeProvider>
</StyledComponentsRegistry>
</body>
Expand Down
26 changes: 15 additions & 11 deletions src/app/product/[productId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ import {
} from '~/server/bigcommerce-api';

import { ProductReviewList } from '~/components/ProductReviewList';
import { getReviewAnalysesByProductId } from '~/lib/db';

interface PageProps {
params: { productId: string };
}

export default async function Page(props: PageProps) {
const { productId } = props.params;

const authorized = authorize();

if (!authorized) {
Expand All @@ -27,19 +26,24 @@ export default async function Page(props: PageProps) {
throw new Error('Access token not found. Try to re-install the app.');
}

const id = Number(productId);

const product = await fetchProductWithAttributes(
id,
accessToken,
authorized.storeHash
);
const productId = Number(props.params.productId);

const reviews = await fetchReviews(id, accessToken, authorized.storeHash);
const [product, reviews, reviewAnalyses] = await Promise.all([
fetchProductWithAttributes(productId, accessToken, authorized.storeHash),
fetchReviews(productId, accessToken, authorized.storeHash),
getReviewAnalysesByProductId({
productId,
storeHash: authorized.storeHash,
}),
]);

return (
<div>
<ProductReviewList product={product} reviews={reviews} />
<ProductReviewList
product={product}
reviews={reviews}
reviewAnalyses={reviewAnalyses ?? []}
/>
</div>
);
}
43 changes: 28 additions & 15 deletions src/app/product/[productId]/review/[reviewId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ import {
} from '~/server/bigcommerce-api';

import { ReviewDetail } from '~/components/ReviewDetail';
import {
analyzeIssuesCategory,
analyzeReview,
} from '~/server/google-ai/analyze-review';
import { analyzeReview } from '~/server/google-ai/analyze-review';

interface PageProps {
params: { reviewId: string; productId: string };
Expand Down Expand Up @@ -48,22 +45,38 @@ export default async function Page(props: PageProps) {

const customerReviews = reviews.filter((r) => r.email === review.email);

const sentimentAnalysis = await analyzeReview({
rating: review.rating,
text: review.text,
title: review.title,
let sentimentAnalysis = await db.getReviewAnalysis({
productId,
reviewId,
storeHash: authorized.storeHash,
});

const issuesCategories = await analyzeIssuesCategory({
rating: review.rating,
text: review.text,
title: review.title,
});
if (!sentimentAnalysis) {
const freshAnalysis = await analyzeReview({
rating: review.rating,
text: review.text,
title: review.title,
});

if (freshAnalysis && typeof freshAnalysis !== 'string') {
sentimentAnalysis = freshAnalysis;

await db.setReviewAnalysis({
analysis: freshAnalysis,
productId,
reviewId,
storeHash: authorized.storeHash,
});
}
}

return (
<ReviewDetail
sentimentAnalysis={sentimentAnalysis}
issuesCategories={issuesCategories}
sentimentAnalysis={
!sentimentAnalysis || typeof sentimentAnalysis === 'string'
? undefined
: sentimentAnalysis
}
customerOrders={customerOrders}
customerReviews={customerReviews}
product={product}
Expand Down
18 changes: 6 additions & 12 deletions src/components/AIChatBubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,22 @@ interface AIChatBubbleProps {

export const AIChatBubble = ({ message, hideAvatar }: AIChatBubbleProps) => {
return (
<div className="mb-4 flex max-w-[450px] items-center">
<div className="flex max-w-[450px] items-center">
{!hideAvatar && (
<div className="mr-4 flex flex-none flex-col items-center space-y-1">
<Image
src="/images/vertex-ai.jpeg"
alt="AI Avatar"
width={32}
height={32}
className="rounded-full"
/>
<p className="block text-xs">Vertex AI</p>
<Image src="/images/vertex-ai.png" alt="" width={32} height={32} />
<p className="block text-xs text-gray-600">Vertex AI</p>
</div>
)}
<div
className={`relative mb-2 flex-1 rounded-lg bg-gray-100/80 p-2 text-lg ${
className={`relative flex-1 rounded-lg bg-gray-100 px-3 py-2 text-lg ${
hideAvatar ? 'ml-[60px]' : ''
}`}
>
<div>{message}</div>
<div className="text-gray-800">{message}</div>

{!hideAvatar && (
<div className="absolute left-0 top-1/2 h-2 w-2 -translate-x-1/2 rotate-45 transform bg-gray-100/80"></div>
<div className="absolute left-0 top-1/2 h-3 w-3 -translate-x-1/2 rotate-45 transform bg-gray-100"></div>
)}
</div>
</div>
Expand Down
22 changes: 11 additions & 11 deletions src/components/IssueBadges.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { Badge } from '@bigcommerce/big-design';

interface IssuesBadgesProps {
issuesCategoriesArray: string[];
issuesCategoriesArray?: string[];
}

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

{issuesCategoriesArray.map((issue) => (
<Badge
key={issue}
label={issue}
variant={issue === 'no issues' ? 'success' : 'danger'}
>
{issue}
</Badge>
))}
{issuesCategoriesArray.length > 0 ? (
issuesCategoriesArray.map((issue) => (
<Badge key={issue} label={issue} variant="warning" />
))
) : (
<Badge label="No issues" variant="success" />
)}
</div>
);
};
41 changes: 41 additions & 0 deletions src/components/ProductReviewList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use client';

import { type Product, type Review } from 'types';

import { Table } from '@bigcommerce/big-design';
Expand All @@ -11,16 +12,20 @@ import { NextLink } from '~/components/NextLink';
import { ReviewStatusBadge } from '~/components/ReviewStatusBadge';
import { StarRating } from '~/components/StarRating';

import { ScoreCircle } from '~/components/ScoreCircle';
import { type ReviewAnalysesByProductIdResponse } from '~/lib/db';
import { convertToDateString } from '~/utils/utils';

interface ProductReviewListProps {
product: Product;
reviews: Review[];
reviewAnalyses: ReviewAnalysesByProductIdResponse;
}

export const ProductReviewList = ({
product,
reviews,
reviewAnalyses,
}: ProductReviewListProps) => {
const averageRating = useMemo(() => {
return (
Expand All @@ -39,6 +44,15 @@ export const ProductReviewList = ({
);
}, [approvedReviews]);

const averageSentiment = useMemo(
() =>
Math.floor(
reviewAnalyses.reduce((acc, analysis) => acc + analysis.data.score, 0) /
reviewAnalyses.length
),
[reviewAnalyses]
);

return (
<div>
<div>
Expand Down Expand Up @@ -94,11 +108,38 @@ export const ProductReviewList = ({
</div>
</>
}
topRightContent={
<ScoreCircle
score={averageSentiment}
tooltip="Average product sentiment"
/>
}
/>
</div>

<Table
columns={[
{
header: 'Score',
hash: 'score',
render: (review) => {
const score = reviewAnalyses?.find(
(r) => r.id === `${review.id}`
)?.data?.score;

return (
<ScoreCircle
score={score}
tooltip={
typeof score === 'number'
? 'Sentiment score'
: 'Unanalyzed review'
}
tooltipPlacement="right"
/>
);
},
},
{
header: 'Rating',
hash: 'rating',
Expand Down
Loading