-
Notifications
You must be signed in to change notification settings - Fork 3
[Test/tts] test 완료 #6
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
base: main
Are you sure you want to change the base?
Conversation
WalkthroughThe changes introduce a complete rewrite of the popup component for a Chrome extension. The previous multi-component structure with scrolling features was replaced by a single, minimal React functional component. This new component directly interacts with the Chrome extension API to retrieve visible page text from the active tab and uses the Web Speech API to read it aloud in Korean. The UI is now a single button labeled "읽어줘!", and error handling was added. The entry point was updated to render this new default component, and styling is handled with Tailwind CSS. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Popup (Component)
participant Chrome Extension API
participant Active Tab
participant Web Speech API
User->>Popup (Component): Clicks "읽어줘!" button
Popup (Component)->>Chrome Extension API: Query active tab
Chrome Extension API->>Active Tab: Send message to get page text
Active Tab-->>Chrome Extension API: Respond with page text
Chrome Extension API-->>Popup (Component): Return page text
Popup (Component)->>Web Speech API: Speak text in Korean
Poem
Tip ⚡💬 Agentic Chat (Pro Plan, General Availability)
✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (5)
src/popup/component.tsx (3)
4-4: Consider using a more descriptive component nameThe generic name "Component" doesn't convey the purpose of this component. A more descriptive name would improve code readability.
-const Component = () => { +const TextToSpeechPopup = () => {Don't forget to update the export and import in index.tsx as well.
18-23: Consider adding voice options and speech configurationThe current implementation uses default settings for speech synthesis. Consider adding options for voice selection, speed, and pitch to improve user experience.
const utterance = new SpeechSynthesisUtterance( response.text, ); utterance.lang = "ko-KR"; +utterance.rate = 1.0; // Speech rate (0.1 to 10) +utterance.pitch = 1.0; // Speech pitch (0 to 2) + +// Optional: Select a specific voice if available +const voices = speechSynthesis.getVoices(); +const koreanVoice = voices.find(voice => voice.lang === 'ko-KR'); +if (koreanVoice) { + utterance.voice = koreanVoice; +} speechSynthesis.speak(utterance);
34-35: Replace test title with a production-ready titleThe current title "VOIM tts test" suggests this is a test component. If this is intended for production, consider updating the title.
<h1 className="text-xl font-semibold mb-2 text-gray-800"> - VOIM tts test + VOIM 텍스트 읽기 </h1>src/popup/index.tsx (2)
3-4: Consider using the imported browser polyfill consistentlyThe file imports the webextension-polyfill as
browser, but the component uses the directchromeAPI. Consider using the polyfill consistently across your codebase for better cross-browser compatibility.Either:
- Use the polyfill in your Component implementation:
// In component.tsx -chrome.tabs.query +browser.tabs.query
- Or remove the unused polyfill if you're targeting Chrome only:
-import browser from "webextension-polyfill";
4-4: Update import to match suggested component nameIf you rename the component as suggested in the previous file review, update the import here as well.
-import Component from "./component"; +import TextToSpeechPopup from "./component";And update the render call accordingly:
-root.render(<Component />); +root.render(<TextToSpeechPopup />);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
dist/manifest.jsonis excluded by!**/dist/**dist/popup.htmlis excluded by!**/dist/**
📒 Files selected for processing (2)
src/popup/component.tsx(1 hunks)src/popup/index.tsx(2 hunks)
🔇 Additional comments (1)
src/popup/index.tsx (1)
22-22: Ensure the component implementation aligns with the PR objectivesAccording to the PR objectives, this PR is focused on completing tests. However, the changes appear to be a complete reimplementation of the component. Ensure that proper testing has been included as mentioned in the PR objectives.
Have you included Jest tests for this component as mentioned in the PR checklist? The PR description mentions running
yarn testto ensure all tests pass.
| chrome.tabs.sendMessage( | ||
| tabs[0].id, | ||
| { type: "GET_PAGE_TEXT" }, | ||
| (response) => { | ||
| if (response?.text) { | ||
| setPageText(response.text); | ||
|
|
||
| const utterance = new SpeechSynthesisUtterance( | ||
| response.text, | ||
| ); | ||
| utterance.lang = "ko-KR"; | ||
| speechSynthesis.speak(utterance); | ||
| } else { | ||
| alert("페이지의 텍스트를 가져오지 못했어요."); | ||
| } | ||
| }, | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add error handling for chrome.runtime.lastError
When using callback-based Chrome API methods, you should check for chrome.runtime.lastError to handle potential errors from the messaging system.
chrome.tabs.sendMessage(
tabs[0].id,
{ type: "GET_PAGE_TEXT" },
(response) => {
+ if (chrome.runtime.lastError) {
+ alert(`오류: ${chrome.runtime.lastError.message}`);
+ return;
+ }
if (response?.text) {
setPageText(response.text);
const utterance = new SpeechSynthesisUtterance(
response.text,
);
utterance.lang = "ko-KR";
speechSynthesis.speak(utterance);
} else {
alert("페이지의 텍스트를 가져오지 못했어요.");
}
},
);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| chrome.tabs.sendMessage( | |
| tabs[0].id, | |
| { type: "GET_PAGE_TEXT" }, | |
| (response) => { | |
| if (response?.text) { | |
| setPageText(response.text); | |
| const utterance = new SpeechSynthesisUtterance( | |
| response.text, | |
| ); | |
| utterance.lang = "ko-KR"; | |
| speechSynthesis.speak(utterance); | |
| } else { | |
| alert("페이지의 텍스트를 가져오지 못했어요."); | |
| } | |
| }, | |
| ); | |
| }); | |
| chrome.tabs.sendMessage( | |
| tabs[0].id, | |
| { type: "GET_PAGE_TEXT" }, | |
| (response) => { | |
| if (chrome.runtime.lastError) { | |
| alert(`오류: ${chrome.runtime.lastError.message}`); | |
| return; | |
| } | |
| if (response?.text) { | |
| setPageText(response.text); | |
| const utterance = new SpeechSynthesisUtterance( | |
| response.text, | |
| ); | |
| utterance.lang = "ko-KR"; | |
| speechSynthesis.speak(utterance); | |
| } else { | |
| alert("페이지의 텍스트를 가져오지 못했어요."); | |
| } | |
| }, | |
| ); |
| import React, { useState } from "react"; | ||
| import "../css/app.css"; | ||
|
|
||
| const Component = () => { | ||
| const [pageText, setPageText] = useState(""); | ||
|
|
||
| const getTextAndSpeak = () => { | ||
| chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { | ||
| if (!tabs[0]?.id) return; | ||
|
|
||
| chrome.tabs.sendMessage( | ||
| tabs[0].id, | ||
| { type: "GET_PAGE_TEXT" }, | ||
| (response) => { | ||
| if (response?.text) { | ||
| setPageText(response.text); | ||
|
|
||
| const utterance = new SpeechSynthesisUtterance( | ||
| response.text, | ||
| ); | ||
| utterance.lang = "ko-KR"; | ||
| speechSynthesis.speak(utterance); | ||
| } else { | ||
| alert("페이지의 텍스트를 가져오지 못했어요."); | ||
| } | ||
| }, | ||
| ); | ||
| }); | ||
| } | ||
|
|
||
| // // // // | ||
| }; | ||
|
|
||
| export function Popup() { | ||
| // Sends the `popupMounted` event | ||
| React.useEffect(() => { | ||
| browser.runtime.sendMessage({ popupMounted: true }); | ||
| }, []); | ||
|
|
||
| // Renders the component tree | ||
| return ( | ||
| <div> | ||
| <div className="mx-4 my-4 bg-black"> | ||
| <Hello /> | ||
| <hr /> | ||
| <Scroller | ||
| onClickScrollTop={() => { | ||
| executeScript(scrollToTopPosition); | ||
| }} | ||
| onClickScrollBottom={() => { | ||
| executeScript(scrollToBottomPosition); | ||
| }} | ||
| /> | ||
| </div> | ||
| <div className="w-64 p-4 bg-white rounded-lg shadow-lg"> | ||
| <h1 className="text-xl font-semibold mb-2 text-gray-800"> | ||
| VOIM tts test | ||
| </h1> | ||
| <button | ||
| onClick={getTextAndSpeak} | ||
| className="w-full mt-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded text-sm" | ||
| > | ||
| 읽어줘! | ||
| </button> | ||
| </div> | ||
| ); | ||
| } | ||
| }; | ||
|
|
||
| export default Component; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Functional implementation, but lacks user experience considerations
The implementation is functional but has a few issues that should be addressed:
- No cleanup mechanism for speech synthesis when the popup closes
- No way to stop reading once started
- Limited error handling
- Hardcoded Korean language without user options
- No loading/in-progress indicators
Consider enhancing the user experience with these improvements:
import React, { useState, useEffect } from "react";
import "../css/app.css";
const Component = () => {
const [pageText, setPageText] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+ const [isSpeaking, setIsSpeaking] = useState(false);
+ useEffect(() => {
+ return () => {
+ // Cleanup speech synthesis when component unmounts
+ speechSynthesis.cancel();
+ };
+ }, []);
const getTextAndSpeak = () => {
+ setIsLoading(true);
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (!tabs[0]?.id) return;
chrome.tabs.sendMessage(
tabs[0].id,
{ type: "GET_PAGE_TEXT" },
(response) => {
+ setIsLoading(false);
if (response?.text) {
setPageText(response.text);
const utterance = new SpeechSynthesisUtterance(
response.text,
);
utterance.lang = "ko-KR";
+ setIsSpeaking(true);
+ utterance.onend = () => setIsSpeaking(false);
+ utterance.onerror = () => {
+ setIsSpeaking(false);
+ alert("읽기 중 오류가 발생했습니다.");
+ };
speechSynthesis.speak(utterance);
} else {
alert("페이지의 텍스트를 가져오지 못했어요.");
}
},
);
});
};
+ const stopSpeaking = () => {
+ speechSynthesis.cancel();
+ setIsSpeaking(false);
+ };
return (
<div className="w-64 p-4 bg-white rounded-lg shadow-lg">
<h1 className="text-xl font-semibold mb-2 text-gray-800">
VOIM tts test
</h1>
<button
onClick={getTextAndSpeak}
+ disabled={isLoading || isSpeaking}
className="w-full mt-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded text-sm"
>
- 읽어줘!
+ {isLoading ? "로딩 중..." : isSpeaking ? "읽는 중..." : "읽어줘!"}
</button>
+ {isSpeaking && (
+ <button
+ onClick={stopSpeaking}
+ className="w-full mt-2 py-2 bg-red-500 hover:bg-red-600 text-white rounded text-sm"
+ >
+ 중지
+ </button>
+ )}
</div>
);
};
export default Component;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import React, { useState } from "react"; | |
| import "../css/app.css"; | |
| const Component = () => { | |
| const [pageText, setPageText] = useState(""); | |
| const getTextAndSpeak = () => { | |
| chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { | |
| if (!tabs[0]?.id) return; | |
| chrome.tabs.sendMessage( | |
| tabs[0].id, | |
| { type: "GET_PAGE_TEXT" }, | |
| (response) => { | |
| if (response?.text) { | |
| setPageText(response.text); | |
| const utterance = new SpeechSynthesisUtterance( | |
| response.text, | |
| ); | |
| utterance.lang = "ko-KR"; | |
| speechSynthesis.speak(utterance); | |
| } else { | |
| alert("페이지의 텍스트를 가져오지 못했어요."); | |
| } | |
| }, | |
| ); | |
| }); | |
| } | |
| // // // // | |
| }; | |
| export function Popup() { | |
| // Sends the `popupMounted` event | |
| React.useEffect(() => { | |
| browser.runtime.sendMessage({ popupMounted: true }); | |
| }, []); | |
| // Renders the component tree | |
| return ( | |
| <div> | |
| <div className="mx-4 my-4 bg-black"> | |
| <Hello /> | |
| <hr /> | |
| <Scroller | |
| onClickScrollTop={() => { | |
| executeScript(scrollToTopPosition); | |
| }} | |
| onClickScrollBottom={() => { | |
| executeScript(scrollToBottomPosition); | |
| }} | |
| /> | |
| </div> | |
| <div className="w-64 p-4 bg-white rounded-lg shadow-lg"> | |
| <h1 className="text-xl font-semibold mb-2 text-gray-800"> | |
| VOIM tts test | |
| </h1> | |
| <button | |
| onClick={getTextAndSpeak} | |
| className="w-full mt-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded text-sm" | |
| > | |
| 읽어줘! | |
| </button> | |
| </div> | |
| ); | |
| } | |
| }; | |
| export default Component; | |
| import React, { useState, useEffect } from "react"; | |
| import "../css/app.css"; | |
| const Component = () => { | |
| const [pageText, setPageText] = useState(""); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [isSpeaking, setIsSpeaking] = useState(false); | |
| useEffect(() => { | |
| return () => { | |
| // Cleanup speech synthesis when component unmounts | |
| speechSynthesis.cancel(); | |
| }; | |
| }, []); | |
| const getTextAndSpeak = () => { | |
| setIsLoading(true); | |
| chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { | |
| if (!tabs[0]?.id) return; | |
| chrome.tabs.sendMessage( | |
| tabs[0].id, | |
| { type: "GET_PAGE_TEXT" }, | |
| (response) => { | |
| setIsLoading(false); | |
| if (response?.text) { | |
| setPageText(response.text); | |
| const utterance = new SpeechSynthesisUtterance( | |
| response.text, | |
| ); | |
| utterance.lang = "ko-KR"; | |
| setIsSpeaking(true); | |
| utterance.onend = () => setIsSpeaking(false); | |
| utterance.onerror = () => { | |
| setIsSpeaking(false); | |
| alert("읽기 중 오류가 발생했습니다."); | |
| }; | |
| speechSynthesis.speak(utterance); | |
| } else { | |
| alert("페이지의 텍스트를 가져오지 못했어요."); | |
| } | |
| }, | |
| ); | |
| }); | |
| }; | |
| const stopSpeaking = () => { | |
| speechSynthesis.cancel(); | |
| setIsSpeaking(false); | |
| }; | |
| return ( | |
| <div className="w-64 p-4 bg-white rounded-lg shadow-lg"> | |
| <h1 className="text-xl font-semibold mb-2 text-gray-800"> | |
| VOIM tts test | |
| </h1> | |
| <button | |
| onClick={getTextAndSpeak} | |
| disabled={isLoading || isSpeaking} | |
| className="w-full mt-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded text-sm" | |
| > | |
| {isLoading ? "로딩 중..." : isSpeaking ? "읽는 중..." : "읽어줘!"} | |
| </button> | |
| {isSpeaking && ( | |
| <button | |
| onClick={stopSpeaking} | |
| className="w-full mt-2 py-2 bg-red-500 hover:bg-red-600 text-white rounded text-sm" | |
| > | |
| 중지 | |
| </button> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| export default Component; |
🧐 체크리스트
yarn build또는npm run build실행 후 정상적으로 동작하는지 확인했나요?yarn test)yarn lint및yarn prettify실행 후 문제가 없나요?feat:,fix:,chore:등 커밋 컨벤션을 따르고 있나요?README.md또는 관련 문서를 업데이트했나요?Summary by CodeRabbit
New Features
Bug Fixes
Style