Skip to content

Commit

Permalink
Feat: 스트리밍 + 코드 수정 (#114)
Browse files Browse the repository at this point in the history
  • Loading branch information
yonghyun421 authored May 16, 2024
1 parent 80d300b commit d3ced63
Show file tree
Hide file tree
Showing 6 changed files with 2,393 additions and 2,292 deletions.
4,495 changes: 2,253 additions & 2,242 deletions .pnp.cjs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"clsx": "^2.1.1",
"firebase": "^10.11.1",
"openai": "^4.40.2",
"openai-edge": "^1.2.2",
"qs": "^6.12.1",
"react": "^18.2.0",
"react-bootstrap": "^2.10.2",
Expand Down
115 changes: 65 additions & 50 deletions src/components/Editor/Components/AiGenerator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
// @ts-nocheck
import React, { useEffect, useRef, useState } from "react";
import axios from "axios";
import OpenAI from "openai";
import { Configuration, OpenAIApi } from "openai-edge";
import LoadingSpinner from "components/Common/LoadingSpinner/LoadingSpinner";
import ResultContentsModal from "../Modal/ResultContentsModal";
import { useTab } from "context/TabContext";
import { useSection } from "context/SectionContext";
import { handleStreamResponse } from "utils/streamHandler";

const AiGenerator = ({
githubAddress,
Expand All @@ -20,14 +22,16 @@ const AiGenerator = ({
const [repos, setRepos] = useState({});
const [githubRepo, setGithubRepo] = useState([]);
const [openAiToken, setOpenAiToken] = useState("");
const [responseData, setResponseData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [aboutRepo, setAboutRepo] = useState([]);
const [memberList, setMemberList] = useState("");
const [techStackList, setTechStackList] = useState("");
const [packageManagerList, setPackageManagerList] = useState("");
const [descriptionList, setDescriptionList] = useState("");
const prevResponseData = useRef();
const [responseData, setResponseData] = useState<string | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const prevResponseData = useRef<string | null>(null);
const responseContainerRef = useRef<HTMLDivElement | null>(null);
const { setTab } = useTab();

useEffect(() => {
Expand Down Expand Up @@ -55,26 +59,12 @@ const AiGenerator = ({
}, [formList]);

useEffect(() => {
if (prevResponseData.current !== responseData && responseData) {
prevResponseData.current = responseData;

const newSectionId = state.editSections.length + state.selectSections.length + 1;
const newSection = {
id: newSectionId,
name: "자동생성RM",
title: "자동생성RM",
markdown: responseData,
};

actions.setEditSections(prev => [...prev, newSection]);
actions.setEditorMarkDown(prev => ({ ...prev, ...newSection }));
actions.setFocusSection(newSection.id);

setTab("Builder");
if (isModalOpen && responseContainerRef.current) {
responseContainerRef.current.innerText = ""; // 초기화
}
}, [responseData]);
}, [isModalOpen]);

const getRepos = async (username: string, token: string) => {
const getRepos = async () => {
if (!githubRepo.length || !openAiToken) {
alert("Please enter the GitHub repository address and OpenAI key.");
return;
Expand All @@ -92,13 +82,12 @@ const AiGenerator = ({
});

if (response.data) {
const aiResponse = await createReadme(
setIsModalOpen(true); // 모달 열기
await createReadme(
response.data,
member?.data.map(ele => ele.login).join(),
member?.data.map(ele => ele.avatar_url).join(),
);
console.log("aiResponse", aiResponse);
setResponseData(aiResponse.choices[0].message.content.trim());
}
} catch (error) {
console.error("Error fetching repository:", error);
Expand All @@ -108,10 +97,10 @@ const AiGenerator = ({
};

async function createReadme(data, member, avatar_url) {
const openai = new OpenAI({
const configuration = new Configuration({
apiKey: openAiToken,
dangerouslyAllowBrowser: true,
});
const openai = new OpenAIApi(configuration);

const prompt = `
레포지토리의 이름: ${data.name}
Expand Down Expand Up @@ -145,7 +134,7 @@ const AiGenerator = ({
"아래 이미지는 무조건 추가해줘"
![프로젝트-로고나 메인화면-입력해주세요](https://github.com/Readme-Monster/readme-monster/assets/88364280/96e680e5-613f-4818-8603-8afbb0c9acb1)
![프로젝트-로고나 메인화면-입력해주세요](https://github.com/Readme-Monster/readme-monster/assets/88364280/96e680e5-613f-4818-8603-8afbb0c9acb1)
"최근 커밋이나 업데이트에 해당하는 값을 아래와 같은 형식으로 가져와서 넣어주세요."
Expand Down Expand Up @@ -197,38 +186,64 @@ const AiGenerator = ({
`;

console.log("prompt", prompt);
try {
const response = await openai.chat.completions.create({
model: "gpt-3.5-turbo-16k",
messages: [
{
role: "system",
content:
"너의 역할은 user가 넘겨주는 정보와 예시로 주어진 템플릿에 맞게 해당 레포지토리에 대한 Readme 파일을 작성해주는 것입니다.",
},
{ role: "user", content: prompt },
],
max_tokens: 6000,
});
return response;
} catch (error) {
console.error("Error generating README:", error);
throw new Error("Failed to generate README");
}

const completion = await openai.createChatCompletion({
model: "gpt-3.5-turbo-16k",
messages: [
{
role: "system",
content:
"너의 역할은 user가 넘겨주는 정보와 예시로 주어진 템플릿에 맞게 해당 레포지토리에 대한 Readme 파일을 작성해주는 것입니다.",
},
{ role: "user", content: prompt },
],
stream: true,
});

const stream = completion.body;
await handleStreamResponse(stream, chunk => {
if (responseContainerRef.current) {
responseContainerRef.current.innerText += chunk;
}
setResponseData(prev => (prev ?? "") + chunk);
});
}

console.log("memberList", memberList);
const handleCloseModal = () => {
setIsModalOpen(false);
if (prevResponseData.current !== responseData && responseData) {
prevResponseData.current = responseData;

const newSectionId = state.editSections.length + state.selectSections.length + 1;
const newSection = {
id: newSectionId,
name: "자동생성RM",
title: "자동생성RM",
markdown: responseData,
};

actions.setEditSections(prev => [...prev, newSection]);
actions.setEditorMarkDown(prev => ({ ...prev, ...newSection }));
actions.setFocusSection(newSection.id);

setTab("Builder");
}
};

return (
<>
{isLoading ? (
<div className="w-1/2 flex justify-center items-center">
<LoadingSpinner />
</div> // 스피너 표시
</div>
) : (
<button onClick={getRepos} className="w-1/2 rounded-[8px] bg-textBlue text-white hover:bg-[#6E9EFF]">
Create README
</button>
<>
<button onClick={getRepos} className="w-1/2 rounded-[8px] bg-textBlue text-white hover:bg-[#6E9EFF]">
Create README
</button>
</>
)}
<ResultContentsModal isOpen={isModalOpen} onClose={handleCloseModal} ref={responseContainerRef} />
</>
);
};
Expand Down
26 changes: 26 additions & 0 deletions src/components/Editor/Modal/ResultContentsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// ResultContentsModal.tsx
import React, { forwardRef } from "react";

interface ModalProps {
isOpen: boolean;
onClose: () => void;
}

const ResultContentsModal = forwardRef<HTMLDivElement, ModalProps>(({ isOpen, onClose }, ref) => {
if (!isOpen) return null;

return (
<div className="fixed inset-0 flex items-center justify-center z-50 bg-gray-700 bg-opacity-75">
<div className="bg-white w-1/2 p-4 h-[80%] flex flex-col justify-around rounded-md">
<div ref={ref} className="p-4 h-[90%] overflow-scroll bg-gray-100 rounded-md" />
<button onClick={onClose} className="mt-4 w-full bg-blue-500 text-white p-2 rounded-md hover:bg-blue-600">
닫기
</button>
</div>
</div>
);
});

ResultContentsModal.displayName = "ResultContentsModal";

export default ResultContentsModal;
40 changes: 40 additions & 0 deletions src/utils/streamHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// utils/streamHandler.ts
export async function handleStreamResponse(
stream: ReadableStream<Uint8Array> | null,
callback: { (chunk: any): void; (arg0: any): void },
) {
if (!stream) {
throw new Error("Stream is null");
}

const reader = stream.getReader();
const decoder = new TextDecoder();
let done = false;

while (!done) {
const { value, done: readerDone } = await reader.read();
done = readerDone;
const chunk = decoder.decode(value, { stream: true });

// 각 줄을 분리하여 처리
const lines = chunk.split("\n").filter(line => line.trim() !== "");
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.replace("data: ", "");
if (data === "[DONE]") {
done = true;
break;
}
try {
const json = JSON.parse(data);
const content = json.choices[0].delta.content;
if (content) {
callback(content);
}
} catch (e) {
console.error("Error parsing stream data:", e);
}
}
}
}
}
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -13871,6 +13871,13 @@ __metadata:
languageName: node
linkType: hard

"openai-edge@npm:^1.2.2":
version: 1.2.2
resolution: "openai-edge@npm:1.2.2"
checksum: 10c0/ff8d6fe10ddb489a42d04cae5fc02c7261d9a9cc477be9bc4effd40552edbc442c2608c0d98c49f40467791d0369db37df2e63d14d88047fa7862dd0cc452b89
languageName: node
linkType: hard

"openai@npm:^4.40.2":
version: 4.40.2
resolution: "openai@npm:4.40.2"
Expand Down Expand Up @@ -15812,6 +15819,7 @@ __metadata:
jest-environment-jsdom: "npm:^29.7.0"
lint-staged: "npm:^15.2.2"
openai: "npm:^4.40.2"
openai-edge: "npm:^1.2.2"
postcss: "npm:^8.4.38"
prettier: "npm:^3.2.5"
qs: "npm:^6.12.1"
Expand Down

0 comments on commit d3ced63

Please sign in to comment.