1+ // --------------- This script is injected into the LeetCode problem page. ------------------
2+
3+ // isUIRunning helps to only inject UI once.
4+ let isUIRunning = false ;
5+ let panel ;
6+
7+ function main ( ) {
8+ if ( isUIRunning ) return ;
9+
10+ // find area to inject button
11+ const targetArea = document . querySelector ( 'div.flex.items-center.gap-4' ) ;
12+ if ( targetArea ) {
13+ const analyzeBtn = document . createElement ( 'button' ) ;
14+ analyzeBtn . textContent = 'Analyze Problem' ;
15+ analyzeBtn . id = 'analyze-btn' ;
16+ analyzeBtn . onclick = togglePanel ;
17+ targetArea . appendChild ( analyzeBtn ) ;
18+
19+ // inject panel
20+ fetch ( chrome . runtime . getURL ( 'panel.html' ) )
21+ . then ( response => response . text ( ) )
22+ . then ( html => {
23+ document . body . insertAdjacentHTML ( 'beforeend' , html ) ;
24+ panel = document . getElementById ( 'panel' ) ;
25+ setupPanelBtn ( ) ;
26+ } ) . catch ( err => console . error ( 'Failed to load panel HTML:' , err ) ) ;
27+
28+ isUIRunning = true ;
29+ }
30+ }
31+
32+ // toggle panel
33+ function togglePanel ( ) {
34+ if ( ! panel ) return ;
35+ panel . classList . toggle ( 'visible' ) ;
36+ }
37+
38+ // setup panel button
39+ function setupPanelBtn ( ) {
40+ const closeBtn = document . getElementById ( 'close-btn' ) ;
41+ closeBtn . addEventListener ( 'click' , togglePanel ) ;
42+
43+ const revealBtn = document . querySelectorAll ( '.btn' ) ;
44+ revealBtn . forEach ( button => {
45+ button . addEventListener ( 'click' , handleRevealBtn ) ;
46+ } ) ;
47+
48+ const retryBtns = document . querySelectorAll ( '.btn-retry' ) ;
49+ retryBtns . forEach ( button => {
50+ button . addEventListener ( 'click' , handleRevealBtn ) ;
51+ } ) ;
52+ }
53+
54+
55+ // handle reveal button click
56+ function handleRevealBtn ( event ) {
57+ const button = event . target ;
58+ const stage = button . dataset . stage ;
59+
60+ let problemTitle = '' ;
61+ let problemDescription = '' ;
62+
63+ // find title using different methods
64+ const titleSelectors = [
65+ '.text-title-large a' , 'div[data-cy="question-title"]' , '.mr-2.text-label-1'
66+ ] ;
67+ let title = null ;
68+ for ( const selector of titleSelectors ) {
69+ title = document . querySelector ( selector ) ;
70+ if ( title ) {
71+ problemTitle = title . innerText ;
72+ break ;
73+ }
74+ }
75+
76+ // find description element
77+ const descSelectors = [
78+ 'div[data-track-load="description_content"]' ,
79+ 'div[class*="elfjS"]' ,
80+ 'div.prose' ,
81+ 'div[class^="description__"]' ,
82+ 'div[class*="question-content"]'
83+ ] ;
84+ let description = null ;
85+ for ( const selector of descSelectors ) {
86+ description = document . querySelector ( selector ) ;
87+ if ( description ) {
88+ problemDescription = description . innerText ;
89+ break ;
90+ }
91+ }
92+
93+ if ( ! problemTitle || ! problemDescription ) {
94+ console . error ( "Error Info:" , {
95+ titleFound : ! ! problemTitle ,
96+ descriptionFound : ! ! problemDescription ,
97+ url : window . location . href
98+ } ) ;
99+ alert ( "LeetCode Assistant Error: Could not find the problem title or description on the page. LeetCode may have updated its layout. Please check the browser console for more details." ) ;
100+
101+ // try again
102+ const revealBtn = document . querySelector ( `.btn[data-stage="${ stage } "]` ) ;
103+ if ( revealBtn ) {
104+ revealBtn . textContent = `Reveal ${ stage . charAt ( 0 ) . toUpperCase ( ) + stage . slice ( 1 ) } ` ;
105+ revealBtn . disabled = false ;
106+ }
107+ return ;
108+ }
109+
110+
111+ const problemText = `Title: ${ problemTitle } \n\nDescription:\n${ problemDescription } ` ;
112+
113+ // --- hide retry and show loading ---
114+ const retryBtn = document . querySelector ( `.btn-retry[data-stage="${ stage } "]` ) ;
115+ if ( retryBtn ) {
116+ retryBtn . style . display = 'none' ;
117+ }
118+ const revealBtn = document . querySelector ( `.btn[data-stage="${ stage } "]` ) ;
119+ if ( revealBtn ) {
120+ revealBtn . textContent = 'Loading...' ;
121+ revealBtn . disabled = true ;
122+ revealBtn . style . display = 'inline-block' ;
123+ }
124+
125+ // send request to background.js
126+ chrome . runtime . sendMessage ( {
127+ type : 'getAnalysis' ,
128+ stage : stage ,
129+ problem : problemText
130+ } ) ;
131+ }
132+
133+
134+ // receive response from background.js script
135+ chrome . runtime . onMessage . addListener ( ( message , sender , sendResponse ) => {
136+ if ( message . type === 'analysisResult' ) {
137+ const { stage, content } = message ;
138+
139+ const contentDiv = document . getElementById ( `${ stage } -content` ) ;
140+ const button = document . querySelector ( `.btn[data-stage="${ stage } "]` ) ;
141+ const retryBtn = document . querySelector ( `.btn-retry[data-stage="${ stage } "]` ) ;
142+
143+ if ( contentDiv && button && retryBtn ) {
144+ if ( content . startsWith ( 'Error:' ) ) {
145+ contentDiv . innerHTML = `<p class="error-message">${ content } </p>` ;
146+ contentDiv . style . display = 'block' ;
147+ button . style . display = 'none' ;
148+ retryBtn . style . display = 'inline-block' ;
149+ } else {
150+ contentDiv . innerHTML = formatMarkdown ( content ) ;
151+ contentDiv . style . display = 'block' ;
152+ button . style . display = 'none' ;
153+ retryBtn . style . display = 'none' ;
154+ }
155+ }
156+ }
157+ } ) ;
158+
159+
160+ // --- markdown formatting ---
161+ function formatMarkdown ( text ) {
162+ let Text = text . replace ( / ` ` ` ( \w * ) \n ( [ \s \S ] * ?) ` ` ` / g, ( match , lang , code ) => {
163+ const language = lang || 'plaintext' ;
164+ return `<pre><code class="language-${ language } ">${ code . trim ( ) } </code></pre>` ;
165+ } ) ;
166+
167+ // Headings
168+ Text = Text . replace ( / ^ # # # ( .* $ ) / gim, '<h3>$1</h3>' ) ;
169+ Text = Text . replace ( / ^ # # ( .* $ ) / gim, '<h2>$1</h2>' ) ;
170+ Text = Text . replace ( / ^ # ( .* $ ) / gim, '<h1>$1</h1>' ) ;
171+
172+ // Bold text
173+ Text = Text . replace ( / \* \* ( .* ?) \* \* / g, '<strong>$1</strong>' ) ;
174+
175+ // Unordered list (grouped)
176+ Text = Text . replace ( / (?: ^ | \n ) [ * - ] ( .* ?) (? = \n | $ ) / g, ( _ , item ) => `<li>${ item } </li>` ) ;
177+ Text = Text . replace ( / ( < l i > [ \s \S ] * ?< \/ l i > ) / g, '<ul>$1</ul>' ) ;
178+ Text = Text . replace ( / < \/ u l > \s * < u l > / g, '' ) ;
179+
180+ // Ordered list (grouped)
181+ Text = Text . replace ( / (?: ^ | \n ) \d + \. ( .* ?) (? = \n | $ ) / g, ( _ , item ) => `<li>${ item } </li>` ) ;
182+ Text = Text . replace ( / ( < l i > [ \s \S ] * ?< \/ l i > ) / g, '<ol>$1</ol>' ) ;
183+ Text = Text . replace ( / < \/ o l > \s * < o l > / g, '' ) ;
184+
185+ // Paragraphs
186+ Text = Text . split ( '\n' ) . map ( p => {
187+ if ( p . trim ( ) === '' || p . startsWith ( '<h' ) || p . startsWith ( '<ul' ) || p . startsWith ( '<ol' ) || p . startsWith ( '<pre' ) ) {
188+ return p ;
189+ }
190+ return `<p>${ p } </p>` ;
191+ } ) . join ( '' ) ;
192+
193+ return Text ;
194+ }
195+
196+
197+ // detect changes in the page
198+ const detect = new MutationObserver ( ( mutations ) => {
199+ if ( window . location . href . includes ( '/problems/' ) && ! document . getElementById ( 'analyze-btn' ) ) {
200+ // delay for load completely
201+ setTimeout ( main , 1000 ) ;
202+ }
203+ } ) ;
204+ detect . observe ( document . body , { childList : true , subtree : true } ) ;
205+
206+
207+ // run Extension
208+ setTimeout ( main , 1500 ) ;
0 commit comments