In this module we learn how to check your blurb for plagiarism using the CopyLeaks API, Firebase Realtime Database and Webhooks.
The flow for a plagiarism check is as follows:
- The frontend calls our Next.js
plagiarismCheck
API with some text to be checked. AscanId
is returned. We then listen to Firebase for any changes on the nodescan/[scanId]
. - This text is then passed to an online plagiarism detection tool(CopyLeaks).
- Once the plagiarism check has completed it sends high level details of the results to our
pages/api/copy-leaks/completed/[scanId]
Webhook. It can take up to two minutes before we receive these results. - When we receive the high level results in our
pages/api/copy-leaks/completed/[scanId]
Webhook. We do the following:
- We write the scan results to Firebase. Once our listener, mentioned in step 1, gets notified of this change we then calculate the plagiarism score based off this data.
- We find the source within the results which has the highest amount of suspected plagiarism and pass it to the plagiarism detection tool
exportResults
API.
- The plagiarism detection tool(CopyLeaks)
exportResults
API gives us further information about a particular source such as which words in our text it thinks were plagiarised. Once the plagiarism detection tool has finished exporting the results of a source it sends the low level details of the results to ourpages/api/copy-leaks/export/[scanId]/[resultId]
Webhook. It can take up to a minute before we receive these results. - When we receive the results in our
pages/api/copy-leaks/export/[scanId]/[resultId]
Webhook. We do the following:
- We write the results to Firebase.
- Once our listener, mentioned in step 1, gets notified of this change we highlight which words in our blurb were plagiarised based off this data.
3.1 Plagiarism UI
3.2 Verify UI with Dummy Values
3.3 Webhooks
3.4 Writing a Firebase Library
3.5 Validate Webhooks in the UI Using Firebase
3.6 Next.js Plagiarism Check API
3.7 Hookup API to Frontend
Firstly, let's start creating the UI to show our plagiarism results.
Ensure you are running pnpm dev
before solving the next tasks.
3.1.1 Plagiarism Progress Bar
Step 1: Before we add the progress bar, we have to add a new component called CenterBox
that allows you to keep your elements in the middle.
Solution
Under your components
folder create a new file called centerBox.tsx
and copy the code below into it.
import { SxProps } from "@mui/material";
import Box from "@mui/material/Box";
import * as React from "react";
export default function CenterBox({
children,
sx,
}: {
children: React.ReactNode;
sx?: SxProps;
}) {
return (
<Box
sx={{
top: 0,
left: 0,
bottom: 0,
right: 0,
position: "absolute",
display: "flex",
alignItems: "center",
justifyContent: "center",
...sx,
}}
>
{children}
</Box>
);
}
Step 2: Now let's add another component for our progress bar. Write a component called loading.tsx
in the components
folder. You will need MUI's CircularProgress
component for this
Solution
- In the
components
folder, add a file namedloading.tsx
- This will return muis
CircularProgress
component. - Underneath this component add the text
Analysing Plagiarism
- This will return muis
import { Box, CircularProgress } from "@mui/material";
export default function Loading() {
return (
<>
<Box
sx={{
height: "100%",
display: "flex",
justifyContent: "center",
flexDirection: "column",
}}
>
<CircularProgress size="4em" />
</Box>
<Box paddingTop="0.5em" textAlign="center">
Analysing Plagiarism
</Box>
</>
);
}
It should look like this:
Step 3: In the components
folder, add a new component named score.tsx
- This function will take a
value
number variable and alabel
string variable as parameters. - Use Mui's
CircularProgress
component with thedeterminate
variant with thevalue
property set tovalue
. - Underneath this component add the text in
label
Solution
import * as React from "react";
import CircularProgress, {
CircularProgressProps,
} from "@mui/material/CircularProgress";
import Box from "@mui/material/Box";
import CenterBox from "./centerBox";
import Typography from "@mui/material/Typography";
export default function Score(
props: CircularProgressProps & { value: number; label: string }
) {
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Box width="100%" display="inline-flex" position="relative">
<CircularProgress
variant="determinate"
value={props.value}
color="secondary"
size="4em"
/>
<CenterBox>
<Typography variant="caption" component="div" color="text.secondary">
{props.label}
</Typography>
</CenterBox>
</Box>
</Box>
);
}
It should look like this:
Step 4: In the components
folder, add a file named plagiarism.tsx
- This function will take a
loading
boolean variable and ascore
number variable as parameters. - If
loading
is true we will show ourLoading
component. - If
loading
is false we will show ourScore
component.
Solution
import { Box } from "@mui/material";
import Loading from "./loading";
import Score from "./score";
interface Props {
loading: boolean;
score?: number;
}
export default function Plagiarism({ loading, score }: Props) {
return (
<Box
sx={{
display: "flex",
height: "100%",
alignItems: "center",
flexDirection: "column",
justifyContent: "center",
}}
>
{loading ? (
<Loading />
) : (
typeof score === "number" && (
<Score value={score} label={`${Math.round(score)}%`} />
)
)}
</Box>
);
}
Step 5: In blurb.tsx
component
- Add a boolean state variable named
plagiarismLoading
with the default value of false. - Add a number state variable named
plagiarisedScore
with the default value of 0. - At the bottom of the HTML
Stack
add thePlagiarism
component with theloading
property having the valueplagiarismLoading
and thescore
property having the valueplagiarisedScore
.
Your blurb.tsx
should look like this:
Solution
import { Card, CardContent, Stack } from "@mui/material";
import Plagiarism from "./plagiarism";
import { useState } from "react";
interface Props {
generatingPost: string;
}
export default function Blurb({ generatingPost }: Props) {
const [plagiarismLoading, setPlagiarismLoading] = useState<boolean>(false);
const [plagiarisedScore, setPlagiarisedScore] = useState<number>(0);
return (
<Stack direction="row" spacing="1em">
<Card sx={{ width: "37em" }}>
<CardContent>{generatingPost}</CardContent>
</Card>
<Stack
alignItems="center"
justifyContent="center"
width="12em"
className="bg-white rounded-xl shadow-md p-4 border"
>
<Plagiarism loading={plagiarismLoading} score={plagiarisedScore} />
</Stack>
</Stack>
);
}
3.1.2 Add Plagiarism Score Column Name
In index.tsx
, add the heading Plagiarism Score
above the plagiarism score components.
Solution
Your index.tsx
should look like this:
...
{generatingPosts && (
<>
<Stack direction="row-reverse" width="100%">
<Typography width="12em" textAlign="center">
Plagiarism Score
</Typography>
</Stack>
{generatingPosts
.substring(generatingPosts.indexOf("1.") + 3)
.split(/2\.|3\./)
.map((generatingPost, index) => {
return (
<Blurb key={index} generatingPost={generatingPost}></Blurb>
);
})}
</>
)}
...
Your final component should look like this:
Before we write our APIs, lets use some dummy objects to validate our changes. This will allow us to avoid using time consuming APIs and provide us with quicker feedback. Let's assume for now that our database already has results. We will work backwards starting from step 6 in our flow mentioned at the top of this page.
3.2.1 Check For Plagiarism When Blurb Has Finished Generating
We should only check for plagiarism once all the blurbs have finished generating.
Create a boolean state variable in index.tsx
that tracks if all the blurbs have finished generating. Then pass this value as a prop
to the Blurb
component.
Solution
...
+ const [blurbsFinishedGenerating, setBlurbsFinishedGenerating] = useState<boolean>(false);
...
const generateBlurb = useCallback(async () => {
+ setBlurbsFinishedGenerating(false);
...
while(!done){
...
}
+ setBlurbsFinishedGenerating(true);
...
<Blurb
key={index}
generatingPost={generatingPost}
+ blurbsFinishedGenerating={blurbsFinishedGenerating}
></Blurb>
...
Now let's update your Blurb component in Blurb.tsx
to also reflect the new prop.
...
interface Props {
generatingPost: string;
blurbsFinishedGenerating: boolean;
}
export function Blurb({ generatingPost, blurbsFinishedGenerating }: Props) {
...
3.2.2 Handle Scan Results
Now connect our frontend to display the results from some dummy backend response. To do that:
- Download the zip folder from dummy-data
- Unzip the folder
- Copy the folder into your
./utils
folder
Write a function in blurb.tsx
that uses the dummy results file to calculate the percentage of the blurb which was plagiarised.
Solution
-
In
blurb.tsx
create a function calledhandleScan
which takes atext
string variable as a parameter and ascan
object parameter. -
Calculate the total number of words in our blurb by doing a
string.split()
on our blurb and finding the length of this array. -
Get the total number of
matchedWords
from our scan. -
Set the
plagiarisedScore
to be(matchedWords/totalWords) * 100
.... const [plagiarismLoading, setPlagiarismLoading] = useState<boolean>(false); const [plagiarisedScore, setPlagiarisedScore] = useState<number>(0); function handleScan(text: string, scan: any) { const totalBlurbWords = text.split(" ").length; const matchedWords = scan.matchedWords; setPlagiarisedScore((matchedWords / totalBlurbWords) * 100); } ...
-
In
checkPlagiarism
setplagiarismLoading
to be true. -
In
checkPlagiarism
assign a variable calledscan
to have the value of our dummy object. -
In
checkPlagiarism
callhandleScan
and setplagiarismLoading
to be false.import dummyScanResults from "../../utils/dummy-data/dummyScanResults.json"; ... const checkPlagiarism = async (streamedBlurb: string) => { setPlagiarismLoading(true); const scan = dummyScanResults; handleScan(streamedBlurb, scan); setPlagiarismLoading(false); }; function handleScan(text: string, scan: any) { ...
3.2.3 Using useEffect to Call checkPlagiarism
Next step we would like to store the final blurb value after it has finished streaming. To do this we are using react useEffect
which essentially only effect the block of code inside the useEffect when its dependent state has been updated.
As we can only check for plagiarism once all the blurbs have finished generating. You should use useEffect
to call our checkPlagiarism
function while having blurbsFinishedGenerating
as a dependency.
Solution
Inside components/blurb.tsx
add below snippet after your handleScan function.
...
useEffect(() => {
if (blurbsFinishedGenerating) {
checkPlagiarism(generatingPost);
}
}, [blurbsFinishedGenerating]);
As this runs pretty quickly we don't actually get to see our loading spinner. Let's put a timeout for 5 seconds in our checkPlagiarism
function to force our loading spinner to show. Your function should look like this:
const checkPlagiarism = async (streamedBlurb: string) => {
setPlagiarismLoading(true);
await new Promise((r) => setTimeout(r, 5000));
const scan = dummyScanResults;
handleScan(streamedBlurb, scan);
setPlagiarismLoading(false);
};
Test your app, it should look like this:
Now that we tested that the loading spinner works. We can remove the timeout.
3.2.4 Handle Detailed Results
Let's extend your handleScan
function to handle detailed results. Copy and paste this function into blurb.tsx
. This should highlight the text in the blurb which has been plagiarised.
import { Card, CardContent, Stack, Box } from "@mui/material";
...
function getHighlightedHTMLBlurb(
text: string,
characterStarts: number[],
characterLengths: number[]
) {
let characterStartsIndex = 0;
let highlightedHTMLBlurb = "";
for (let i = 0; i < text.length; i++) {
if (i == characterStarts[characterStartsIndex]) {
const segmentStart = characterStarts[characterStartsIndex];
const segmentEnd =
characterStarts[characterStartsIndex] +
characterLengths[characterStartsIndex];
highlightedHTMLBlurb += `<mark style="background:#FF9890">${text.substring(
segmentStart,
segmentEnd
)}</mark>`;
i = segmentEnd - 1;
characterStartsIndex = characterStartsIndex + 1;
} else {
highlightedHTMLBlurb += text[i];
}
}
return <Box dangerouslySetInnerHTML={{ __html: highlightedHTMLBlurb }}></Box>;
}
...
You can check the Copy Leaks documentation for more information about how to handle detailed results of plagiarism checker.
This is what it should look like:
Now extend your handleScan
function to use the getHighlightedHTMLBlurb
function.
Solution
-
Create a state variable called
highlightedHTMLBlurb
of typeJSX.Element
const [highlightedHTMLBlurb, setHighlightedHTMLBlurb] = useState<JSX.Element>();
-
Replace your
handleScan
function with below codefunction handleScan(text: string, scan: any) { const totalBlurbWords = text.split(" ").length; const matchedWords = scan.matchedWords; setPlagiarisedScore((matchedWords / totalBlurbWords) * 100); const characterStarts = scan.results.identical.source.chars.starts; const characterLengths = scan.results.identical.source.chars.lengths; const highlightedHTMLBlurb = getHighlightedHTMLBlurb( text, characterStarts, characterLengths ); setHighlightedHTMLBlurb(highlightedHTMLBlurb); }
-
Change the
useEffect
hook to to set thehighlightedHTMLBlurb
to be the a HTML element with the finished Blurb as it's contentuseEffect(() => { if (blurbsFinishedGenerating) { checkPlagiarism(generatingPost); setHighlightedHTMLBlurb(<>{generatingPost}</>); } }, [blurbsFinishedGenerating]);
-
Change the HTML to show the new
highlightedHTMLBlurb
instead or thegeneratingPost
only whenblurbsFinishedGenerating
is true.<Stack direction="row" spacing="1em"> <Card sx={{ width: "37em" }}> - <CardContent>{generatingPost}</CardContent> + <CardContent> + {!blurbsFinishedGenerating ? generatingPost : highlightedHTMLBlurb} + </CardContent> </Card> <Stack alignItems="center" justifyContent="center" width="12em" className="bg-white rounded-xl shadow-md p-4 border" > <Plagiarism loading={plagiarismLoading} score={plagiarismScore} /> </Stack> </Stack>
In order to receive the results of scans and exports from the Copy Leaks servers we need to create two webhooks. These webhooks will then write their data to our Firebase database. Before we setup our Webhooks, let's create a Firebase Realtime Database.
Firebase Realtime Database is a cloud-hosted NoSQL database provided by Google as part of the Firebase platform. It is a real-time, scalable database solution designed to store and synchronize data across multiple clients in real-time. This database will only be used to store the results from a CopyLeaks plagiarism check and then notify the front end that results have been returned.
Although our plagiarism call is not realtime, Copy Leaks API will send the response to a webhook. The webhook will then sends the response to the Firebase database and Firebase is connected to our UI to update the frontend as soon as any new data is available in the db. The main aim of this module is to show you how to connect your frontend with a realtime database and display the latest data in your application as soon as new data becomes available.
For more information on Firebase check their documentation here.
- Go to https://console.firebase.google.com/
- Sign in with a Google account
- Click on
Create Project
- Enter a project name - this can be anything like
latency-blurb-workshop
- Accept the terms and conditions and click
Continue
- Do not enable Google Analytics and click
Create Project
- Click continue
- On the left navigation bar, select the
Build
accordion and the click onRealtime Database
- Click
Create Database
- Select
Singapore
as your database location. ClickNext
- Select
Start in Test mode
. ClickEnable
- Copy your database URL which will be something like
https://latency-blurb-workshop-default-rtdb.asia-southeast1.firebasedatabase.app/
depending on what you named your project in step 4. - Add your database URL to .env.local for the variable
NEXT_PUBLIC_FIREBASE_REALTIME_DATABASE_URL
3.3.2.1 Create an Export Webhook
As we are working backwards through the workflow lets first write the export
Webhook, mentioned in step 5. The export webhook is called by CopyLeaks with which words in our blurb have been plagiarised.
In Next.js, dynamic routes allow you to create pages with dynamic content based on the values in the URL. For example, if you create a file named [id].js inside the pages directory, it will match any route that has a dynamic segment in the URL.
In our case we want a dynamic route for the scanId
and the exportId
as this will be constantly changing for each scan that we do.
- Create a dynamic route Edge function named
[exportId].ts
inpages/api/copy-leaks/export/[scanId]/
which receives the results of an export and returns a response with{message: "Result exported successfully"}
. This should also write the results to the database using the Firebase PUT API under the nodescans/<scanId>/results.json
. We only need the object intext.comparison
.
More information:
- Copy Leaks: https://api.copyleaks.com/documentation/v3/webhooks/result.
- Firebase: https://firebase.google.com/docs/database/rest/save-data#section-put
Solution
- Create a file named
[exportId].ts
inpages/api/copy-leaks/export/[scanId]/
. - Create a handler which takes a
req
parameter. - Get the scan ID from the
req.url.searchParams
parameter. - Write the result details to the database.
- Return the
{message: "Result exported successfully"}
as a response.
import { NextRequest, NextResponse } from "next/server";
export const config = {
runtime: "edge",
regions: ["syd1"],
};
export default async function handler(req: NextRequest) {
const params = new URL(req.url).searchParams;
const scanId = params.get("scanId");
const body = await req.json();
try {
await fetch(
`${process.env.NEXT_PUBLIC_FIREBASE_REALTIME_DATABASE_URL}/scans/${scanId}/results.json`,
{
method: "PUT",
body: JSON.stringify(body.text.comparison),
}
);
} catch (e) {
console.error("Error writing to Firebase database", e);
throw e;
}
return NextResponse.json({ message: "Result exported successfully" });
}
3.3.2.2 Create a Scan Webhook
Now let's write the scan
Webhook, mentioned in step 4. The scan
webhook is called by CopyLeaks with the number of words in our blurb which have been plagiarised.
- Create a dynamic route Edge function named
[scanId].ts
inpages/api/copy-leaks/completed
which receives the results of a scan and returns a response with{message: "Scan Completed"}
. This should also write the scan to the database using the Firebase PUT API under the nodescans/<scanId>.json
.
More information:
- Copy Leaks: https://api.copyleaks.com/documentation/v3/webhooks/completed.
- Firebase: https://firebase.google.com/docs/database/rest/save-data#section-put
Solution
This webhook receives the scanned results from multiple sources, we then pickup a source with highest plagiarism found and write the number of words into firebase
- Create a file named
[scanId].ts
inpages/api/copy-leaks/completed
. - Copy below code into your webhook
import { NextRequest, NextResponse } from "next/server";
export const config = {
runtime: "edge",
regions: ["syd1"],
};
type SourceResult = {
resultId: string;
matchedWords: number;
};
function getHighestSourceResult(
completedScanWebhookResponse: any
): SourceResult {
let matchedWords = 0;
let resultId = "";
if (completedScanWebhookResponse.results.internet.length > 0) {
const sortedResults = completedScanWebhookResponse.results.internet.sort(
(a: SourceResult, b: SourceResult) => a.matchedWords - b.matchedWords
);
const highestResult = sortedResults[0];
resultId = highestResult.id;
matchedWords = highestResult.matchedWords;
}
return {
resultId,
matchedWords: matchedWords,
};
}
export default async function handler(req: NextRequest) {
const body = await req.json();
const scanId = body.scannedDocument.scanId;
const matchedWords = getHighestSourceResult(body);
try {
await fetch(
`${process.env.NEXT_PUBLIC_FIREBASE_REALTIME_DATABASE_URL}/scans/${scanId}.json`,
{
method: "PUT",
body: JSON.stringify({ matchedWords: matchedWords }),
}
);
} catch (e) {
console.error("Error writing to Firebase Database", e);
throw e;
}
return NextResponse.json({ message: "Scan complete" });
}
Before we can test our Webhooks in the frontend we first need to write a Firebase class. The Firebase class that we will be writing will be a wrapper around the Firebase SDK which we will use in the frontend to listen to events on the database.
Step1: Install the Firebase SDK Package
Solution
- In your terminal run
pnpm i firebase
Step2: Create an empty FirebaseWrapper class
Solution
- Create a
lib
folder in the root directory - In your
lib
folder create a class namedfirebaseWrapper.tsx
in a sub-folder namedfirebase
. - In
firebaseWrapper.tsx
add the following code:
export class FirebaseWrapper {}
Step3: Write a Get Database Function
Write a function called getInstance
that will return an instance of your database - https://firebase.google.com/docs/database/web/start#add_the_js_sdk_and_initialize
Solution
- Create a
public
function calledgetInstance
. - Add a
firebaseConfig
object which hasdatabaseURL
as a key and a value of your Firebase URL. In 3.3.1.13, we set the environment variableNEXT_PUBLIC_FIREBASE_REALTIME_DATABASE_URL
to be our database URL. With this in mind the value of our Firebase URL will be${process.env.NEXT_PUBLIC_FIREBASE_REALTIME_DATABASE_URL}
- Initialise the Firebase app instance with
const app = initializeApp(firebaseConfig)
- Get the database instance from the app instance.
Your firebaseWrapper.tsx
should now look like this:
import { Database, getDatabase, ref } from "firebase/database";
import { initializeApp } from "firebase/app";
export class FirebaseWrapper {
public getInstance(): Database {
const firebaseConfig = {
databaseURL: `${process.env.NEXT_PUBLIC_FIREBASE_REALTIME_DATABASE_URL}`,
};
const app = initializeApp(firebaseConfig);
const database = getDatabase(app);
return database;
}
}
Step4: Convert the FirebaseWrapper Class to Return a Singleton Instance
Imagine a scenario where we have to get 3 items from our database in the frontend. With our current implementation, every time we initialise the class in the frontend we would have to initialise a new connection to the database as well. This would mean that we would have to initialise a connection to our database 3 times, this can be time-consuming and is considered bad practice.
Singletons
A singleton is a design pattern that restricts the number of instantiations of a class to one for the lifetime of the application. In this case every time we call the instance we would always be returned the same instance which in turn means we would not have any overheads in establishing multiple connections to the database. More information: https://refactoring.guru/design-patterns/singleton
Change firebaseWrapper.tsx
as below to make your database a Singleton instance.
import { Database, getDatabase, ref } from "firebase/database";
import { initializeApp } from "firebase/app";
export class FirebaseWrapper {
+ private database?: Database;
private getInstance(): Database {
if (!this.database) {
const firebaseConfig = {
databaseURL: `${process.env.NEXT_PUBLIC_FIREBASE_REALTIME_DATABASE_URL}`,
};
const app = initializeApp(firebaseConfig);
- const database = getDatabase(app);
+ this.database = getDatabase(app);
}
- return database;
+ return this.database;
}
}
We should also write a helper function to get a reference to the node in the database with a particular scan ID. Copy and paste the function below.
Your FirebaseWrapper.tsx
should now look like this:
export class FirebaseWrapper {
private database?: Database;
public getScanReference(scanId: string) {
return ref(this.getInstance(), `scans/${scanId}`);
}
...
}
Now that our Webhooks can write data to Firebase we can test their responses in the frontend. To do this we can manually call the Webhooks from the frontend, while we use Firebase to listen for events on the scans/[scanId]
node.
Ensure to use this scanId
f1d0db14-c4d2-487d-9615-5a1b8ef6f4c2
and this 7e514eabb3
3.5.1 Manually call the Scan and Export Webook
The dummy data for this task can be found in /utils/dummy-data/dummyCompletedExportResultsWebhookResponse.json
and /utils/dummy-data/dummyCompletedScanWebhookResponse.json
. You should have already downloaded those previously.
- In
blurb.tsx
incheckPlagiarism
make a manual API call to thescan
webhook with a fake request body. - In
blurb.tsx
incheckPlagiarism
make a manual API call to theexport
webhook with a fake request body.
Solution
import dummyCompletedExportResultsWebhookResponse from "@/utils/dummy-data/dummyCompletedExportResultsWebhookResponse.json";
import dummyCompletedScanWebhookResponse from "@/utils/dummy-data/dummyCompletedScanWebhookResponse.json";
...
const checkPlagiarism = async (streamedBlurb: string) => {
setPlagiarismLoading(true);
const scan = dummyScanResults;
const completedScanWebhookResponse = await fetch(
"/api/copy-leaks/completed/f1d0db14-c4d2-487d-9615-5a1b8ef6f4c2",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(dummyCompletedScanWebhookResponse),
}
);
const completedExportResultsWebhookResponse = await fetch(
"/api/copy-leaks/export/f1d0db14-c4d2-487d-9615-5a1b8ef6f4c2/7e514eabb3",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(dummyCompletedExportResultsWebhookResponse),
}
);
handleScan(streamedBlurb, scan);
setPlagiarismLoading(false);
};
3.5.2 Listening to Firebase Events
Now that we can send scan results and export results to the database via Webhook lets listen to Firebase for when the results are returned.
Using the Firebase SDK listen for scan results on a specific node based on scanId
. Use Firebases onValue
function. Remember to use the scanId
f1d0db14-c4d2-487d-9615-5a1b8ef6f4c2
.
More information: https://firebase.google.com/docs/database/web/read-and-write#read_data
Solution
- After we get our
scanId
from ourscan
API but before we set theplagiarismLoading
to be false inuseEffect
, instantiate aFirebaseWrapper
. - Get a
scanRef
by callingFirebaseWrapper.getScanReference(scanId)
. - Use Firebases
onValue
function to listen for events on ourscanRef
. - If the
scanRecord
does not exist, do nothing. This means that CopyLeaks has not returned the results as yet. - Remove the
setPlagiarismLoading(false)
line. This will now be handled by thehandleScan
function. - Call our
handleScan
function with thescanRecord.val()
as a parameter.
import { FirebaseWrapper } from "../../lib/firebase/firebaseWrapper";
import { onValue } from "firebase/database";
...
const checkPlagiarism = async (streamedBlurb: string) => {
setPlagiarismLoading(true);
const scanId = "f1d0db14-c4d2-487d-9615-5a1b8ef6f4c2";
const completedScanWebhookResponse = await fetch(
"/api/copy-leaks/completed/f1d0db14-c4d2-487d-9615-5a1b8ef6f4c2",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(dummyCompletedScanWebhookResponse),
}
);
const completedExportResultsWebhookResponse = await fetch(
"/api/copy-leaks/export/f1d0db14-c4d2-487d-9615-5a1b8ef6f4c2/7e514eabb3",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(dummyCompletedExportResultsWebhookResponse),
}
);
const firebase = new FirebaseWrapper();
const scanRef = firebase.getScanReference(scanId);
onValue(scanRef, async (scanRecord: any) => {
// Only continue if a <scanId> node is present in Firebase
if (scanRecord.exists()) {
const scan = scanRecord.val();
handleScan(streamedBlurb, scan);
}
});
};
- Change our
handleScan
function to take a scan as a parameter.- Remove the
setPlagiarismLoading(false)
line from thecheckPlagiarism
function. - Add a check to see og there are 0
matchedWords
if this is the case none of our blurb will be highlighted. - Add a check above the assignment of the
characterStarts
variable to check ifscan.results
exists. This is because ascan
will return beforescan.results
is in the database as we are only writing thescan.results
node once we have thescan
information and the time between these calls will be close to 40 seconds. See ourapi/copy-leaks/completed/[scanId].ts
for more information. - Add the
setPlagiarismLoading(false)
line after we receive the results from Firebase.
- Remove the
function handleScan(text: string, scan) {
const totalBlurbWords = text.split(" ").length;
const matchedWords = scan.matchedWords;
setPlagiarisedScore((matchedWords / totalBlurbWords) * 100);
if (matchedWords == 0) {
setPlagiarismLoading(false);
} else if (scan.results) {
const characterStarts = scan.results.identical.source.chars.starts;
const characterLengths = scan.results.identical.source.chars.lengths;
const highlightedHTMLBlurb = getHighlightedHTMLBlurb(
text,
characterStarts,
characterLengths
);
setHighlightedHTMLBlurb(highlightedHTMLBlurb);
setPlagiarismLoading(false);
}
}
You can now test your application and see the dummy results being returned by the Firebase database.
In order to call the the CopyLeaks scan function we need to create an API. Before we write our API we first need to write a library that can call the CopyLeaks API. Copyleaks is an online plagiarism detection and content verification platform. It utilizes advanced algorithms and artificial intelligence (AI) technology to compare submitted content against a vast database of sources, including web pages, academic journals, publications, and more.
It is important to note that as this is a free account we are only entitled to 25 plagiarism checks. If you run out of credits you will have to go through this process again but with a different email address. Credits will only be used when you use the deployed version of your app. Credits will not be used in local development, in local development CopyLeaks will run in sandbox mode, this means that a valid response will be returned but the response results will contain mocked data.
- Go to https://copyleaks.com/
- Click on
Login
- Click on
Sign Up
- Enter an email and password
- Verify your email address - check your junk folder for the code
- Enter in your details - this can be anything◊
- Go to https://api.copyleaks.com/dashboard/
- In the
API Access Credentials
Tile click onGenerate
for one of the keys - Copy this key into the
COPY_LEAKS_API_KEY
variable in.env.local
- Add the email address you used to sign up with to
COPY_LEAKS_EMAIL
variable in.env.local
Since that we have tested our webhooks and firebase interactions using dummy responses from Copy Leaks apis, we shall now replace the dummy responses with actual api calls.
Step1: Install the CopyLeaks SDK package
- Install the CopyLeaks SDK Package - https://www.npmjs.com/package/plagiarism-checker.
- Install the
uuid
package to generate Ids - https://www.npmjs.com/package/uuid - Install the
types/uuid
package - https://www.npmjs.com/package/@types/uuid
Solution
- In your terminal run
pnpm i plagiarism-checker
- In your terminal run
pnpm i uuid
- In your terminal run
pnpm i --save-dev @types/uuid
Step2: Download copyLeaksWrapper.ts
file from here and place it into your lib/copy-leaks/
folder.
Step3: Create a Plagiarism Check API
- Create an Edge function named
plagiarismCheck.ts
which calls ourCopyLeaksWrapper.scan
function the the text to be scanned. More information: https://vercel.com/docs/concepts/functions/edge-functions. - Deploy your API
Important: When CopyLeaks.scan is called, this request returns an HTTP Code of 201. This function does not return the results directly, instead when the results are ready, CopyLeaks sends a response to one of our APIs via webhook. The webhook will only be called on a deployed public website. ie. Having localhost as the webhook domain will not work as localhost will not have a publicly accessible IP.
Solution
- Create a file named
plagiarismCheck.ts
inpages/api
. - Create a handler which takes a
req
parameter. - Instantiate the
CopyLeaksWrapper
. - Get the text to be scanned from the
req
parameter. - Call the
CopyLeaks.scan
method with the text. - Return the
scanId
. - Push your code to main to deploy your API.
import { CopyLeaksWrapper } from "@/lib/copy-leaks/copyLeaksWrapper";
import { NextRequest, NextResponse } from "next/server";
export const config = {
runtime: "edge",
regions: ["syd1"],
};
type ScanRequest = {
text: string;
};
export default async function handler(req: NextRequest) {
const copyLeaks = new CopyLeaksWrapper();
const body = (await req.json()) as ScanRequest;
const scanId = await copyLeaks.scan(body.text);
return NextResponse.json({ scanId });
}
Step4: Calling the Export Function from the Scan Webhook
Once the scan
Webhook receives a result we want to immediately call the CopyLeaksWrapper function getDetailedResults
in order to get more details on the source with the highest number of matched words. This is step 3.3.2.2 in our workflow.
Edit your scan
webhook to call the CopyLeaksWrapper.getDetailedResults
function with the scanId
and resultId
on the source with the highest number of matched words.
Solution
- Instantiate the
CopyLeaksWrapper
. - Call
CopyLeaksWrapper.getDetailedResults
with thescanId
andresultId
.
import { CopyLeaksWrapper } from "@/lib/copy-leaks/copyLeaksWrapper";
...
export default async function handler(req: NextRequest) {
const body = await req.json();
const scanId = body.scannedDocument.scanId;
const { resultId, matchedWords } = getHighestSourceResult(body);
if (matchedWords != 0) {
const copyLeaks = new CopyLeaksWrapper();
await copyLeaks.getDetailedResults(scanId, resultId);
}
try {
await fetch(
`${process.env.NEXT_PUBLIC_FIREBASE_REALTIME_DATABASE_URL}/scans/${scanId}.json`,
{
method: "PUT",
body: JSON.stringify({ matchedWords: matchedWords }),
}
);
} catch (e) {
console.error("Error writing to Firebase Database", e);
throw e;
}
return NextResponse.json({ message: "Scan complete" });
}
Step5: Push your code to main to deploy your API. Your code has to be deployed before we can test out Copy Leaks Apis.
Now that we know the UI works with dummy values, lets use real world values. Important: From this point on your application must be deployed in order for the plagiarism check to work
3.7.1 Calling the Scan API
- In
blurb.tsx
, remove the direct calls to thescan
and theexport
Webhooks. - Write a
useEfffect
function call to ourscan
API.
Solution
- Remove the direct calls to the
scan
and theexport
Webhooks. - Append the
useEfffect
function to watch for when ablurb
is set andfinishedStreaming
is true. When this is true call thecheckPlagiarism
function. - Create a
checkPlagiarism
function which takes thetext
as a parameter.- Set
plagiarismLoading
to be true. - Call our
scan
API with our text. Get thescanId
from this response. - Set
plagiarismLoading
to be false.
- Set
type ScanResponse = {
scanId: string;
};
...
const checkPlagiarism = async (streamedBlurb: string) => {
setPlagiarismLoading(true);
// Send blurb to be scanned for plagiarism
const scanResponse = await fetch("/api/plagiarismCheck", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
text: streamedBlurb,
}),
});
const scanId = ((await scanResponse.json()) as ScanResponse).scanId;
const firebase = new FirebaseWrapper();
const scanRef = firebase.getScanReference(scanId);
onValue(scanRef, async (scanRecord: any) => {
// Only continue if a <scanId> node is present in Firebase
if (scanRecord.exists()) {
const scan = scanRecord.val();
handleScan(streamedBlurb, scan);
}
});
setPlagiarismLoading(false);
};
Finally, push your code to deploy your app and test your blurbs with real plagiarism check. Important: Once we deploy, we are no longer in sandbox mode, the response from CopyLeaks may take up to two minutes so you may see the loading spinner for a long time.
If a response hasn't come back after two minutes, check https://api.copyleaks.com/dashboard to see if you have enough credits. Remember we are checking 3 blurbs at once so we will use 3 credits every time we generate blurbs. If you run out of credits you will have to create a new Copy Leaks account with a different email address as outlined in step 3.6.1
Congratulations you have now completed module 3 and ready to move on to the fourth module. If you have any issues finishing off module 3, you can download the app from Module3- Final Demo and move on to the next module.
Module4 Tweeting your Blurb -> Get started