Used main branch as base branch in the last time

This commit is contained in:
Carlos Mesquita
2024-07-25 16:59:15 +01:00
parent 10a3243756
commit 877d2f359f
17 changed files with 9051 additions and 1463 deletions

View File

@@ -0,0 +1,193 @@
import Image from "next/image";
import clsx from "clsx";
import RadialProgressBar from "./RadialProgressBar";
import { AIDetectionAttributes } from "@/interfaces/exam";
import { Tooltip } from 'react-tooltip';
import SegmentedProgressBar from "./SegmentedProgressBar";
// Colors and texts scrapped from gpt's zero react bundle
const AIDetection: React.FC<AIDetectionAttributes> = ({ predicted_class, confidence_category, class_probabilities, sentences }) => {
const probabilityTooltipContent = `
GTP's Zero deep learning model predicts the <br/>
probability this text has been entirely <br/>
generated by AI. For instance, a 40% AI <br/>
probability does not indicate that the text<br/>
contains 40% AI-written content. Rather, it<br/>
indicates the text is more likely to be partially<br/>
human written than be entirely AI-written.
`;
const confidenceTooltipContent = `
Confidence scores are a safeguard to better<br/>
understand AI identification results. GTP Zero<br/>
trained it's deep learning model on a diverse<br/>
dataset of millions of human and AI-written<br/>
documents. Green scores indicate that you can scan<br/>
with confidence that the model has classified<br/>
many similar documents with high accuracy.<br/>
Red scores indicate that this text is dissimilar<br/>
to the ones in their training set, which can impact<br/>
the model's accuracy, and to proceed with caution.
`;
const confidenceKeywords = ["moderately", "highly", "confident", "uncertain"];
var confidence = {
low: {
ai: "GPT Zero is uncertain about this text. If GPT Zero had to classify it, it would be considered",
human: "GPT Zero is uncertain about this text. If GPT Zero had to classify it, it would likely be considered",
mixed: "GPT Zero is uncertain about this text. If GPT Zero had to classify it, it would likely be a"
},
medium: {
ai: "GPT Zero is moderately confident this text was",
human: "GPT Zero is moderately confident this text is entirely",
mixed: "GPT Zero is moderately confident this text is a"
},
high: {
ai: "GPT Zero is highly confident this text was",
human: "GPT Zero is highly confident this text is entirely",
mixed: "GPT Zero is highly confident this text is a"
}
}
var classPrediction = {
ai: {
background: "bg-ai-detection-result-ai-bg",
color: "text-ai-detection-result-ai",
text: "ai generated"
},
mixed: {
background: "bg-ai-detection-result-mixed-bg",
color: "text-ai-detection-result-mixed",
text: "mix of ai and human"
},
human: {
background: "bg-ai-detection-result-human-bg",
color: "text-ai-detection-result-human",
text: "human"
}
}
const segments = [
{ percentage: Math.round(class_probabilities["human"] * 100), subtitle: 'human', color: "ai-detection-result-human" },
{ percentage: Math.round(class_probabilities["mixed"] * 100), subtitle: 'mixed', color: "ai-detection-result-mixed" },
{ percentage: Math.round(class_probabilities["ai"] * 100), subtitle: 'ai', color: "ai-detection-result-ai" }
];
const styleConfidenceText = (text: string): [string, string[]] => {
const keywords: string[] = [];
const styledText = text.split(" ").map(word => {
if (confidenceKeywords.includes(word)) {
keywords.push(word);
return `<span style="font-weight: 500; text-decoration: underline;">${word}</span>`;
}
return word
}).join(" ");
return [styledText, keywords];
};
const confidenceText = confidence[confidence_category][predicted_class];
const [styledText, keywords] = styleConfidenceText(confidenceText);
const tooltipStyle = {
"backgroundColor": "rgb(255, 255, 255)",
"color": "#8992B1",
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
borderRadius: '0.125rem'
}
const highestProbability = Math.max(class_probabilities["ai"], class_probabilities["human"], class_probabilities["mixed"]);
const spanTextColor = highestProbability === class_probabilities["ai"]
? "#f4bf4f"
: highestProbability === class_probabilities["human"]
? "#50c08a"
: "#93aafb";
let spanClassName = highestProbability === class_probabilities["ai"]
? "text-ai-detection-result-ai"
: highestProbability === class_probabilities["human"]
? "text-ai-detection-result-human"
: "text-ai-detection-result-mixed";
spanClassName = `${spanClassName} font-bold text-lg`
const percentage = Math.round(highestProbability * 100)
const hasHighlightedForAI = sentences.some(item => item.highlight_sentence_for_ai);
return (
<>
<Tooltip id="probability-tooltip" className="z-50 bg-white shadow-md rounded-sm" style={tooltipStyle} />
<Tooltip id="confidence-tooltip" className="z-50 bg-white shadow-md rounded-sm" style={tooltipStyle} />
<div className="flex flex-col bg-white p-6 rounded-lg shadow-lg gap-16">
<h1 className="text-lg font-semibold">GPT Zero AI Detection Results</h1>
<div className="flex flex-row -md:flex-col -lg:gap-0 -xl:gap-10 gap-20 items-stretch -md:items-center">
<div className="flex -md:w-5/6 w-1/2 justify-center">
<div className="flex flex-col border rounded-xl">
<h1 className="border-b p-6 font-medium">Text Classification</h1>
<div className="flex flex-row gap-8 items-center p-6">
<RadialProgressBar
percentage={percentage}
text={predicted_class}
color={spanTextColor}
spanClassName={spanClassName}
/>
<div className="flex flex-col gap-1 text-sm">
<div className="flex flex-row items-center">
<span className="mr-2 text-ai-detection-result-ai-text font-semibold text-xl">
{`${Math.round(class_probabilities["ai"] * 100)}%`}
</span>
<span className="text-sm -md:text-xs text-ai-detection-text">Probability AI generated</span>
<a data-tooltip-id="probability-tooltip" data-tooltip-html={probabilityTooltipContent} className='ml-1 flex items-center justify-center'>
<Image src="/mat-icon-info.svg" width={24} height={24} alt="Probability Tooltip" />
</a>
</div>
<div className="flex flex-row items-center gap-1">
<div className={clsx(
"rounded-full w-3 h-3",
confidence_category == 'low' ?
"bg-ai-detection-confidence-low border border-ai-detection-confidence-border" : "bg-ai-detection-confidence-low-transparent"
)}></div>
<div className={clsx(
"rounded-full w-3 h-3",
confidence_category == 'medium' ?
"bg-ai-detection-confidence-medium border border-ai-detection-confidence-border" : "bg-ai-detection-confidence-medium-transparent"
)}></div>
<div className={clsx(
"rounded-full w-3 h-3 mr-2",
confidence_category == 'high' ?
"bg-ai-detection-confidence-high border border-ai-detection-confidence-border" : "bg-ai-detection-confidence-high-transparent"
)}></div>
<span className="text-sm -md:text-xs text-ai-detection-text">{keywords.join(' ')}</span>
<a data-tooltip-id="confidence-tooltip" data-tooltip-html={confidenceTooltipContent} className='ml-1 flex items-center justify-center'>
<Image src="/mat-icon-info.svg" width={24} height={24} alt="Probability Tooltip" />
</a>
</div>
</div>
</div>
</div>
</div>
<div className="flex flex-col border rounded-xl -md:w-5/6 w-2/6">
<h1 className="border-b p-6 font-medium">Probability Breakdown</h1>
<div className="flex items-center w-full h-full">
<SegmentedProgressBar segments={segments} className="w-full px-8 -md:py-8 text-ai-detection-text" />
</div>
</div>
</div>
<div className="flex flex-col gap-2">
<div className="flex flex-row items-center">
<div dangerouslySetInnerHTML={{ __html: styledText }} className="mr-2"></div>
<div className={clsx(
"flex items-center justify-center p-2 rounded",
classPrediction[predicted_class]['color'],
classPrediction[predicted_class]['background']
)}>
<span className="text-sm">{classPrediction[predicted_class]['text']}</span>
</div>
</div>
{(hasHighlightedForAI && <div>
Sentences that are likely written by AI are <span className="font-semibold bg-ai-detection-highlight">highlighted</span>.
</div>)}
</div>
</div >
<div>
{sentences.map((item, index) => (
<span
key={index}
className={item.highlight_sentence_for_ai ? 'bg-ai-detection-highlight' : ''}
>
{item.sentence}{' '}
</span>
))}
</div>
</>
)
}
export default AIDetection;

View File

@@ -1,138 +0,0 @@
import { DurationUnit } from "@/interfaces/paypal";
import {
CreateOrderActions,
CreateOrderData,
OnApproveActions,
OnApproveData,
OnCancelledActions,
OrderResponseBody,
} from "@paypal/paypal-js";
import {
PayPalButtons,
PayPalScriptProvider,
usePayPalScriptReducer,
} from "@paypal/react-paypal-js";
import axios from "axios";
import { useState, useEffect } from "react";
import { toast } from "react-toastify";
interface Props {
clientID: string;
currency: string;
price: number;
duration: number;
duration_unit: DurationUnit;
loadScript?: boolean;
setIsLoading: (isLoading: boolean) => void;
onSuccess: (duration: number, duration_unit: DurationUnit) => void;
trackingId?: string;
}
export default function PayPalPayment({
clientID,
price,
currency,
duration,
duration_unit,
loadScript,
setIsLoading,
onSuccess,
trackingId,
}: Props) {
const createOrder = async (
data: CreateOrderData,
actions: CreateOrderActions
): Promise<string> => {
if (!trackingId) {
throw new Error("trackingId is not set");
}
setIsLoading(true);
return axios
.post<OrderResponseBody>("/api/paypal", {
currencyCode: currency,
price,
trackingId,
})
.then((response) => response.data)
.then((data) => {
setIsLoading(false);
return data.id;
})
.catch((err) => {
setIsLoading(false);
return err;
});
};
const onApprove = async (data: OnApproveData, actions: OnApproveActions) => {
if (!trackingId) {
throw new Error("trackingId is not set");
}
axios
.post<{ ok: boolean; reason?: string }>("/api/paypal/approve", {
id: data.orderID,
duration,
duration_unit,
trackingId,
})
.then((request) => {
if (request.status !== 200) {
toast.error("Something went wrong, please try again later");
return;
}
toast.success("Your account has been credited more time!");
return onSuccess(duration, duration_unit);
})
.catch((err) => {
console.error(err);
toast.error("Something went wrong, please try again later");
});
};
const onError = async (data: Record<string, unknown>) => {
setIsLoading(false);
};
const onCancel = async (
data: Record<string, unknown>,
actions: OnCancelledActions
) => {
setIsLoading(false);
};
if (trackingId) {
return loadScript ? (
<PayPalScriptProvider
options={{
clientId: clientID,
currency,
intent: "capture",
commit: true,
}}
>
<PayPalButtons
className="w-full"
style={{ layout: "vertical" }}
createOrder={createOrder}
onApprove={onApprove}
onCancel={onCancel}
onError={onError}
/>
</PayPalScriptProvider>
) : (
<PayPalButtons
className="w-full"
style={{ layout: "vertical" }}
createOrder={createOrder}
onApprove={onApprove}
onCancel={onCancel}
onError={onError}
/>
);
}
return null;
}

View File

@@ -0,0 +1,64 @@
import clsx from 'clsx';
import React from 'react';
interface RadialProgressBarProps {
percentage: number;
text: string;
color: string;
spanClassName?: string;
size?: number;
strokeWidth?: number;
strokeOpacity?: number;
}
// https://gist.github.com/eYinka/873be69fae3ef27b103681b8a9f5e379 Omarmarei's answer
const RadialProgressBar: React.FC<RadialProgressBarProps> = ({
percentage,
text,
color,
spanClassName = "",
size = 100,
strokeWidth = 10,
strokeOpacity = 0.5
}) => {
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const offset = circumference - (percentage / 100) * circumference;
return (
<div className='relative flex items-center justify-center' style={{ width: size, height: size}}>
<svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`
}
className="circular-progress-bar"
>
<circle className="circle-bg" stroke="#e6e6e6" strokeWidth={strokeWidth}
fill="none"
cx={size / 2}
cy={size / 2}
r={radius}
strokeOpacity={strokeOpacity}
/>
<circle
className="circle-progress"
stroke={color}
strokeWidth={strokeWidth}
strokeLinecap="round"
fill="none"
cx={size / 2}
cy={size / 2}
r={radius}
strokeDasharray={circumference}
strokeDashoffset={offset}
transform={`rotate(-90 ${size / 2} ${size / 2})`}
strokeOpacity={strokeOpacity}
/>
</svg>
<span className={clsx('absolute', spanClassName)}>{text}</span>
</div>
);
};
export default RadialProgressBar;

View File

@@ -0,0 +1,48 @@
import React from 'react';
import clsx from 'clsx';
interface Segment {
percentage: number;
subtitle: string;
color: string;
}
interface SegmentedProgressBarProps {
segments: Segment[];
height?: number;
className?: string;
}
const SegmentedProgressBar: React.FC<SegmentedProgressBarProps> = ({ segments, height=15, className="" }) => {
return (
<div className={className}>
<div className="relative flex rounded-full overflow-hidden bg-gray-200" style={{height: `${height}px`}}>
{segments.map((segment, index) => (
<div
key={index}
className={clsx(
'h-full opacity-50',
'transition-all duration-500 ease-out',
`bg-${segment.color}`,
{
'rounded-l-full': index === 0,
'rounded-r-full': index === segments.length - 1,
'rounded-none': index !== 0 && index !== segments.length - 1
}
)}
style={{width: `${segment.percentage}%`}}
></div>
))}
</div>
<div className="mt-2 flex text-sm justify-between">
{segments.map((segment, index) => (
<div
key={index}
className="flex flex-col text-center w-fit"
>
<span className={clsx('font-semibold',`text-${segment.color}`)}>{segment.subtitle}</span>
<span>{`${segment.percentage}%`}</span>
</div>
))}
</div>
</div>
);
};
export default SegmentedProgressBar;

View File

@@ -7,11 +7,17 @@ import {Dialog, Tab, Transition} from "@headlessui/react";
import {writingReverseMarking} from "@/utils/score";
import clsx from "clsx";
import ReactDiffViewer, {DiffMethod} from "react-diff-viewer";
import useUser from "@/hooks/useUser";
import AIDetection from "../AIDetection";
export default function Writing({id, type, prompt, attachment, userSolutions, onNext, onBack}: WritingExercise & CommonProps) {
const [isModalOpen, setIsModalOpen] = useState(false);
const [showDiff, setShowDiff] = useState(false);
const { user } = useUser();
const aiEval = userSolutions && userSolutions.length > 0 ? userSolutions[0].evaluation?.ai_detection : undefined;
return (
<>
{attachment && (
@@ -159,6 +165,19 @@ export default function Writing({id, type, prompt, attachment, userSolutions, on
}>
Recommended Answer
</Tab>
{aiEval && user?.type !== "student" && (
<Tab
className={({ selected }) =>
clsx(
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-writing/80",
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-writing focus:outline-none focus:ring-2",
"transition duration-300 ease-in-out",
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-writing",
)
}>
AI Use
</Tab>
)}
</Tab.List>
<Tab.Panels>
<Tab.Panel className="w-full bg-ielts-writing/10 h-fit rounded-xl p-6 flex flex-col gap-4">
@@ -169,6 +188,11 @@ export default function Writing({id, type, prompt, attachment, userSolutions, on
{userSolutions[0].evaluation!.perfect_answer.replaceAll(/\s{2,}/g, "\n\n").replaceAll("\\n", "\n")}
</span>
</Tab.Panel>
{aiEval && user?.type !== "student" && (
<Tab.Panel className="w-full bg-ielts-writing/10 h-fit rounded-xl p-6 flex flex-col gap-4">
<AIDetection {...aiEval} />
</Tab.Panel>
)}
</Tab.Panels>
</Tab.Group>
) : (