Skip to content

Commit 942595e

Browse files
authored
Merge pull request #107 from Stack-Knowledge/feature/gradeAI
[Admin] Add scoring AI
2 parents 24f0fe0 + de2d3db commit 942595e

File tree

8 files changed

+274
-89
lines changed

8 files changed

+274
-89
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"eslint-plugin-sort-exports": "^0.8.0",
2525
"eslint-plugin-storybook": "^0.6.15",
2626
"eslint-plugin-unused-imports": "^3.0.0",
27+
"ldrs": "^1.0.1",
2728
"next": "13.4.2",
2829
"react": "^18.2.0",
2930
"react-toastify": "^9.1.3",

packages/api/admin/src/hooks/user/useGetSolveDetail.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ interface SolveDetailResponseType {
66
solveId: string;
77
title: string;
88
solution: string;
9+
content: string;
910
}
1011

1112
import type { UseQueryOptions } from '@tanstack/react-query';

pnpm-lock.yaml

+136-30
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

projects/admin/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"@types/react-dom": "18.2.7",
1616
"common": "workspace:^",
1717
"next": "13.4.12",
18+
"openai": "^4.28.0",
1819
"react": "18.2.0",
1920
"react-dom": "18.2.0",
2021
"api": "workspace:^",

projects/admin/src/PageContainer/GradingPage/index.tsx

+57-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { useState } from 'react';
44

55
import { useRouter } from 'next/navigation';
66

7+
import { dotSpinner } from 'ldrs';
8+
import OpenAI from 'openai';
79
import { toast } from 'react-toastify';
810

911
import { GradingContainer } from 'admin/components';
@@ -16,9 +18,29 @@ interface GradingPageProps {
1618
solveId: string;
1719
}
1820

21+
const Answer = {
22+
true: 'CORRECT_ANSWER',
23+
false: 'WRONG_ANSWER',
24+
} as const;
25+
26+
const Word = {
27+
true: '정답',
28+
false: '오답',
29+
} as const;
30+
1931
const GradingPage: React.FC<GradingPageProps> = ({ solveId }) => {
2032
const { push } = useRouter();
2133
const [selectedAnswer, setSelectedAnswer] = useState<boolean>(true);
34+
const [isLoading, setIsLoading] = useState<boolean>(false);
35+
36+
const key = process.env.NEXT_PUBLIC_OPENAI_KEY;
37+
38+
const openai = new OpenAI({
39+
apiKey: key,
40+
dangerouslyAllowBrowser: true,
41+
});
42+
43+
dotSpinner.register();
2244

2345
const { data } = useGetSolveDetail(solveId);
2446
const { mutate, isSuccess } = usePostScoringResult(solveId);
@@ -27,22 +49,47 @@ const GradingPage: React.FC<GradingPageProps> = ({ solveId }) => {
2749
setSelectedAnswer(isTrue);
2850
};
2951

30-
const handleSubmit = () => {
31-
const solveStatus = selectedAnswer ? 'CORRECT_ANSWER' : 'WRONG_ANSWER';
52+
const aiScoring = async () => {
53+
await setIsLoading(true);
54+
try {
55+
const completion = await openai.chat.completions.create({
56+
messages: [
57+
{
58+
role: 'user',
59+
content: `문제 : ${data.title} 내용 : ${data.content} 이 문제의 답이 맞는지 틀린지 알려줘. 답 : ${data.solution} 답이 오류가 없는 것 같으면 true 아니면 false로 대답해줘.`,
60+
},
61+
],
62+
model: 'gpt-3.5-turbo',
63+
});
64+
65+
const answer = completion.choices[0].message.content;
66+
handleSubmit(answer.includes('true').toString() as 'true' | 'false');
67+
} catch (error) {
68+
toast.success(error);
69+
}
70+
};
71+
72+
const handleSubmit = (answer: 'true' | 'false') => {
73+
const solveStatus = Answer[answer];
74+
toast.success(`${Word[answer]}으로 처리되었습니다.`);
3275

3376
mutate({ solveStatus: solveStatus });
3477
};
3578

3679
if (isSuccess) {
3780
push('/mission/scoring');
38-
toast.success('성공적으로 채점되었습니다.');
3981
}
4082

4183
return (
4284
<S.PageWrapper>
4385
{data && (
4486
<div>
4587
<S.TopContentWrapper>
88+
{isLoading && (
89+
<S.SpinnerWrapper>
90+
<l-dot-spinner size='60' speed='0.9' color='#FFA927' />
91+
</S.SpinnerWrapper>
92+
)}
4693
<S.Title>채점하기</S.Title>
4794
<S.SectionContainer>
4895
<S.SectionWrapper>
@@ -67,7 +114,13 @@ const GradingPage: React.FC<GradingPageProps> = ({ solveId }) => {
67114
</S.IncorrectWrapper>
68115
</S.SectionContainer>
69116
</S.TopContentWrapper>
70-
<GradingContainer onClick={handleSubmit}>
117+
<GradingContainer
118+
isLoading={isLoading}
119+
onAiClick={aiScoring}
120+
onClick={() =>
121+
handleSubmit(selectedAnswer.toString() as 'true' | 'false')
122+
}
123+
>
71124
{data.solution}
72125
</GradingContainer>
73126
</div>
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,42 @@
11
import styled from '@emotion/styled';
22

3-
export const PageWrapper = styled.div`
4-
width: 100%;
5-
height: 100vh;
6-
display: flex;
7-
justify-content: center;
8-
align-items: center;
3+
export const AnswerSection = styled.span`
4+
color: ${({ theme }) => theme.color.black};
5+
${({ theme }) => theme.typo.body2};
96
`;
107

11-
export const TopContentWrapper = styled.div`
12-
padding-left: 0.4375rem;
8+
export const AnswerWrapper = styled.div`
139
display: flex;
14-
flex-direction: column;
15-
gap: 3.75rem;
10+
gap: 0.375rem;
1611
`;
1712

18-
export const Title = styled.span`
19-
color: ${({ theme }) => theme.color.black};
20-
${({ theme }) => theme.typo.h2};
21-
font-weight: 600;
13+
export const ClickSection = styled.div<{ isSelected: boolean }>`
14+
width: 1rem;
15+
height: 1rem;
16+
border-radius: 50%;
17+
border: ${({ isSelected, theme }) =>
18+
isSelected
19+
? `0.1875rem solid ${theme.color.primary}`
20+
: '0.0625rem solid #787878'};
21+
cursor: pointer;
2222
`;
2323

24-
export const SectionContainer = styled.span`
24+
export const IncorrectWrapper = styled.div`
2525
display: flex;
26+
gap: 0.75rem;
27+
`;
28+
29+
export const NotAnswerWrapper = styled.div`
2630
display: flex;
27-
justify-content: space-between;
28-
padding-bottom: 1.25rem;
31+
gap: 0.375rem;
2932
`;
3033

31-
export const SectionWrapper = styled.div`
34+
export const PageWrapper = styled.div`
35+
width: 100%;
36+
height: 100vh;
3237
display: flex;
38+
justify-content: center;
39+
align-items: center;
3340
`;
3441

3542
export const Section = styled.div`
@@ -40,33 +47,32 @@ export const Section = styled.div`
4047
margin: 0 0.3125rem 0 0.3125rem;
4148
`;
4249

43-
export const IncorrectWrapper = styled.div`
50+
export const SectionContainer = styled.span`
4451
display: flex;
45-
gap: 0.75rem;
52+
display: flex;
53+
justify-content: space-between;
54+
padding-bottom: 1.25rem;
4655
`;
4756

48-
export const AnswerSection = styled.span`
49-
color: ${({ theme }) => theme.color.black};
50-
${({ theme }) => theme.typo.body2};
57+
export const SectionWrapper = styled.div`
58+
display: flex;
5159
`;
5260

53-
export const AnswerWrapper = styled.div`
54-
display: flex;
55-
gap: 0.375rem;
61+
export const SpinnerWrapper = styled.div`
62+
position: absolute;
63+
top: 50%;
64+
left: 50%;
5665
`;
5766

58-
export const NotAnswerWrapper = styled.div`
59-
display: flex;
60-
gap: 0.375rem;
67+
export const Title = styled.span`
68+
color: ${({ theme }) => theme.color.black};
69+
${({ theme }) => theme.typo.h2};
70+
font-weight: 600;
6171
`;
6272

63-
export const ClickSection = styled.div<{ isSelected: boolean }>`
64-
width: 1rem;
65-
height: 1rem;
66-
border-radius: 50%;
67-
border: ${({ isSelected, theme }) =>
68-
isSelected
69-
? `0.1875rem solid ${theme.color.primary}`
70-
: `0.0625rem solid #787878`};
71-
cursor: pointer;
73+
export const TopContentWrapper = styled.div`
74+
padding-left: 0.4375rem;
75+
display: flex;
76+
flex-direction: column;
77+
gap: 3.75rem;
7278
`;

projects/admin/src/components/GradingCotainer/index.tsx

+14-2
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,27 @@ import * as S from './style';
55
interface GradingProps {
66
children: React.ReactNode;
77
onClick: () => void;
8+
onAiClick: () => void;
9+
isLoading: boolean;
810
}
911

10-
const GradingContainer: React.FC<GradingProps> = ({ children, onClick }) => (
12+
const GradingContainer: React.FC<GradingProps> = ({
13+
children,
14+
onClick,
15+
onAiClick,
16+
isLoading,
17+
}) => (
1118
<S.GradingtContainer>
1219
<S.MissionDetailInputWrapper>
1320
<S.GradingWrapper>{children}</S.GradingWrapper>
1421
</S.MissionDetailInputWrapper>
1522
<S.SubmitButtonWrapper>
16-
<S.SubmitButton onClick={onClick}>제출하기</S.SubmitButton>
23+
<S.SubmitButton disabled={isLoading} onClick={onClick}>
24+
제출하기
25+
</S.SubmitButton>
26+
<S.AIButton disabled={isLoading} onClick={onAiClick}>
27+
AI로 채첨하기
28+
</S.AIButton>
1729
</S.SubmitButtonWrapper>
1830
</S.GradingtContainer>
1931
);
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,5 @@
11
import styled from '@emotion/styled';
22

3-
export const GradingtContainer = styled.div`
4-
background: ${({ theme }) => theme.color.gray['010']};
5-
width: 59.375rem;
6-
height: 18.75rem;
7-
border-radius: 0.625rem;
8-
box-shadow: 0.1875rem 0.1875rem 0.25rem 0rem rgba(120, 120, 120, 0.25);
9-
`;
10-
export const MissionDetailInputWrapper = styled.div`
11-
display: flex;
12-
justify-content: center;
13-
`;
14-
153
export const GradingWrapper = styled.div`
164
background: ${({ theme }) => theme.color.gray['010']};
175
${({ theme }) => theme.typo.body1};
@@ -20,11 +8,17 @@ export const GradingWrapper = styled.div`
208
height: 13.75rem;
219
margin-top: 1.375rem;
2210
`;
11+
export const GradingtContainer = styled.div`
12+
background: ${({ theme }) => theme.color.gray['010']};
13+
width: 59.375rem;
14+
height: 18.75rem;
15+
border-radius: 0.625rem;
16+
box-shadow: 0.1875rem 0.1875rem 0.25rem 0rem rgba(120, 120, 120, 0.25);
17+
`;
2318

24-
export const SubmitButtonWrapper = styled.div`
19+
export const MissionDetailInputWrapper = styled.div`
2520
display: flex;
26-
justify-content: flex-end;
27-
margin: 0 2.375rem 1.25rem 0;
21+
justify-content: center;
2822
`;
2923

3024
export const SubmitButton = styled.button`
@@ -38,3 +32,14 @@ export const SubmitButton = styled.button`
3832
border: 0;
3933
cursor: pointer;
4034
`;
35+
36+
// eslint-disable-next-line sort-exports/sort-exports
37+
export const AIButton = styled(SubmitButton)`
38+
margin-left: 12px;
39+
`;
40+
41+
export const SubmitButtonWrapper = styled.div`
42+
display: flex;
43+
justify-content: flex-end;
44+
margin: 0 2.375rem 1.25rem 0;
45+
`;

0 commit comments

Comments
 (0)