22
33import React , { useState , useEffect } from 'react' ;
44import { useProjectsContext } from '@/context/ProjectsContext' ;
5- import { getProjectReadme , regenerateProjectReadme , modifyReadmeWithQna , getReadmeQnaHistory , createReadmeShare , revokeReadmeShare , getReadmeShare , deleteReadmeQnaRecord , deleteAllReadmeQnaHistory } from '@/lib/actions' ;
5+ import { getProjectReadme , regenerateProjectReadme , modifyReadmeWithQna , getReadmeQnaHistory , createReadmeShare , revokeReadmeShare , getReadmeShare , deleteReadmeQnaRecord , deleteAllReadmeQnaHistory , updateProjectGithubToken } from '@/lib/actions' ;
66import { Card , CardContent , CardDescription , CardHeader , CardTitle } from '@/components/ui/card' ;
77import { Button } from '@/components/ui/button' ;
88import { Badge } from '@/components/ui/badge' ;
@@ -95,6 +95,10 @@ function ReadmePage() {
9595 const [ qnaToDelete , setQnaToDelete ] = useState < string | null > ( null ) ;
9696 const [ showDeleteAllDialog , setShowDeleteAllDialog ] = useState ( false ) ;
9797 const [ showProcessingNotice , setShowProcessingNotice ] = useState ( true ) ;
98+ const [ showTokenModal , setShowTokenModal ] = useState ( false ) ;
99+ const [ githubTokenInput , setGithubTokenInput ] = useState ( '' ) ;
100+ const [ isSavingToken , setIsSavingToken ] = useState ( false ) ;
101+ const [ tokenError , setTokenError ] = useState < string | null > ( null ) ;
98102
99103 const selectedProject = projects . find ( p => p . id === selectedProjectId ) ;
100104
@@ -132,6 +136,38 @@ function ReadmePage() {
132136 return { title, description, stars, forks, language, license } ;
133137 } ;
134138
139+ const handleSaveGithubToken = async ( ) => {
140+ if ( ! selectedProjectId ) return ;
141+
142+ const token = githubTokenInput . trim ( ) ;
143+ if ( ! token ) {
144+ setTokenError ( 'Please enter your GitHub personal access token.' ) ;
145+ return ;
146+ }
147+
148+ setIsSavingToken ( true ) ;
149+ setTokenError ( null ) ;
150+
151+ try {
152+ await updateProjectGithubToken ( selectedProjectId , token ) ;
153+ toast . success ( 'GitHub token saved' , {
154+ description : 'We will use this token for future indexing and regeneration.' ,
155+ } ) ;
156+ setShowTokenModal ( false ) ;
157+ setGithubTokenInput ( '' ) ;
158+ await regenerateReadme ( { suppressRateLimitPrompt : true } ) ;
159+ } catch ( err ) {
160+ console . error ( 'Error saving GitHub token:' , err ) ;
161+ const message = err instanceof Error ? err . message : 'Failed to save GitHub token' ;
162+ setTokenError ( message ) ;
163+ toast . error ( 'Failed to save token' , {
164+ description : message ,
165+ } ) ;
166+ } finally {
167+ setIsSavingToken ( false ) ;
168+ }
169+ } ;
170+
135171 const handleCopyCode = async ( ) => {
136172 if ( ! readmeData ?. content ) return ;
137173
@@ -173,7 +209,7 @@ function ReadmePage() {
173209 }
174210 } ;
175211
176- const handleRegenerateReadme = async ( ) => {
212+ const regenerateReadme = async ( options ?: { suppressRateLimitPrompt ?: boolean } ) => {
177213 if ( ! selectedProjectId ) return ;
178214
179215 setIsRegenerating ( true ) ;
@@ -193,17 +229,26 @@ function ReadmePage() {
193229 const rawMessage = err instanceof Error ? err . message : 'Failed to regenerate README' ;
194230 const isRateLimit = rawMessage . toLowerCase ( ) . includes ( 'github api rate limit exceeded' ) ;
195231 const errorMessage = isRateLimit
196- ? 'GitHub rate limit hit. Add a GitHub personal access token ( with repo scope) when creating the project to avoid this limit .'
232+ ? 'GitHub API rate limit hit (this comes from GitHub) . Add a GitHub personal access token with repo scope to this project to continue .'
197233 : rawMessage ;
198234 setError ( errorMessage ) ;
199- toast . error ( 'Failed to regenerate README' , {
235+ toast . error ( isRateLimit ? 'GitHub rate limit exceeded' : 'Failed to regenerate README' , {
200236 description : errorMessage ,
201237 } ) ;
238+ if ( isRateLimit && ! options ?. suppressRateLimitPrompt ) {
239+ setGithubTokenInput ( '' ) ;
240+ setTokenError ( null ) ;
241+ setShowTokenModal ( true ) ;
242+ }
202243 } finally {
203244 setIsRegenerating ( false ) ;
204245 }
205246 } ;
206247
248+ const handleRegenerateReadme = ( ) => {
249+ void regenerateReadme ( ) ;
250+ } ;
251+
207252 const handleQnaSubmit = async ( ) => {
208253 if ( ! selectedProjectId || ! qnaQuestion . trim ( ) ) return ;
209254
@@ -456,7 +501,7 @@ function ReadmePage() {
456501 < AlertDescription className = "mt-2 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 text-sm text-blue-100/90" >
457502 < span >
458503 Large repositories can take longer to index, especially on the first load. If GitHub rate limits the request,
459- add a personal access token with < code className = "font-mono" > repo</ code > scope when creating the project to keep things moving quickly.
504+ add a personal access token with < code className = "font-mono" > repo</ code > scope (use “Manage GitHub Token” above) to keep things moving quickly.
460505 </ span >
461506 < Button
462507 variant = "ghost"
@@ -484,6 +529,17 @@ function ReadmePage() {
484529 </ div >
485530 </ div >
486531 < div className = "flex flex-col sm:flex-row items-stretch sm:items-center gap-2 sm:gap-3" >
532+ < Button
533+ onClick = { ( ) => {
534+ setShowTokenModal ( true ) ;
535+ setGithubTokenInput ( '' ) ;
536+ setTokenError ( null ) ;
537+ } }
538+ variant = "outline"
539+ className = "border-blue-400/40 text-blue-200 hover:bg-blue-500/10 px-3 sm:px-4 md:px-6 py-2 rounded-lg transition-all duration-200 w-full sm:w-auto text-xs sm:text-sm"
540+ >
541+ Manage GitHub Token
542+ </ Button >
487543 { shareToken ? (
488544 < Button
489545 onClick = { ( ) => setShowShareModal ( true ) }
@@ -997,6 +1053,75 @@ function ReadmePage() {
9971053 </ div >
9981054 ) }
9991055
1056+ { /* GitHub Token Modal */ }
1057+ < Dialog open = { showTokenModal } onOpenChange = { setShowTokenModal } >
1058+ < DialogContent className = "bg-gray-900 border-white/20" >
1059+ < DialogHeader >
1060+ < DialogTitle className = "text-white" > Add GitHub Personal Access Token</ DialogTitle >
1061+ < DialogDescription className = "text-white/60" >
1062+ We use this token to fetch repository files without hitting GitHub’s rate limits. Generate a token with
1063+ < span className = "font-mono text-white/80" > repo </ span > scope and paste it below.
1064+ </ DialogDescription >
1065+ </ DialogHeader >
1066+ < div className = "space-y-4" >
1067+ < div className = "space-y-2" >
1068+ < label className = "text-sm font-medium text-white/80" htmlFor = "github-token-input-readme" >
1069+ Token
1070+ </ label >
1071+ < Input
1072+ id = "github-token-input-readme"
1073+ type = "password"
1074+ value = { githubTokenInput }
1075+ onChange = { ( event ) => setGithubTokenInput ( event . target . value ) }
1076+ placeholder = "ghp_XXXXXXXXXXXXXXXXXXXX"
1077+ className = "bg-white/5 border-white/20 text-white text-sm"
1078+ autoComplete = "off"
1079+ spellCheck = { false }
1080+ />
1081+ < p className = "text-xs text-white/50" >
1082+ You can create a new token at{ ' ' }
1083+ < a
1084+ href = "https://github.com/settings/tokens"
1085+ target = "_blank"
1086+ rel = "noopener noreferrer"
1087+ className = "text-blue-300 hover:underline"
1088+ >
1089+ github.com/settings/tokens
1090+ </ a >
1091+ . Keep this token secret—never share it publicly.
1092+ </ p >
1093+ { tokenError && < p className = "text-xs text-red-400" > { tokenError } </ p > }
1094+ </ div >
1095+ </ div >
1096+ < DialogFooter >
1097+ < Button
1098+ variant = "outline"
1099+ onClick = { ( ) => {
1100+ setShowTokenModal ( false ) ;
1101+ setTokenError ( null ) ;
1102+ } }
1103+ className = "bg-white/10 hover:bg-white/20 text-white border-white/20"
1104+ >
1105+ Cancel
1106+ </ Button >
1107+ < Button
1108+ onClick = { handleSaveGithubToken }
1109+ disabled = { isSavingToken }
1110+ className = "bg-blue-600 hover:bg-blue-700 text-white"
1111+ >
1112+ { isSavingToken ? (
1113+ < >
1114+ < Loader2 className = "h-4 w-4 mr-2 animate-spin" />
1115+ Saving...
1116+ </ >
1117+ ) : (
1118+ 'Save Token'
1119+ ) }
1120+ </ Button >
1121+ </ DialogFooter >
1122+ </ DialogContent >
1123+ </ Dialog >
1124+
10001125 { /* Delete Single Q&A Dialog */ }
10011126 < Dialog open = { showDeleteDialog } onOpenChange = { setShowDeleteDialog } >
10021127 < DialogContent className = "bg-gray-900 border-white/20" >
0 commit comments