diff --git a/app/api/server/routes.py b/app/api/server/routes.py index c468b21..6be4ca0 100644 --- a/app/api/server/routes.py +++ b/app/api/server/routes.py @@ -81,9 +81,7 @@ async def external_signup(app: str, accessToken: str, provider: str): response = await create_llm_session() response["token"] = token response["provider"] = provider - final_response = await store_fetcher_endpoint(response) - session_id = final_response.get("session_id") - return {"session_id": session_id} + return await store_fetcher_endpoint(response) @router.post("/pat") async def store_fetcher_endpoint(request: Request): @@ -111,8 +109,8 @@ async def store_fetcher_endpoint(request: Request): response = await create_llm_session() session_id = response.get("session_id") - store_fetcher(session_id, token, provider) - return {"session_id": session_id} + username = store_fetcher(session_id, token, provider) + return {"session_id": session_id, "username": username} async def create_llm_session( request: Optional[LlmConfig] = None diff --git a/app/api/server/websockets.py b/app/api/server/websockets.py index 90774ea..ba6a3e0 100644 --- a/app/api/server/websockets.py +++ b/app/api/server/websockets.py @@ -37,8 +37,8 @@ """ TRIGGER_PULL_REQUEST_PROMPT = """ -You will now receive a list of commit messages between two branches. -Using the system instructions provided above, generate a clear, concise, and professional **Pull Request Description** summarizing all changes. +You will now receive a list of commit messages between two branches. +Using the system instructions provided above, generate a clear, concise, and professional **Pull Request Description** summarizing all changes from branch `{SRC}` to be merged into `{TARGET}`. Commits: {COMMITS} @@ -103,7 +103,9 @@ async def websocket_endpoint( msg_json = json.loads(message) message_content = msg_json.get("actions") N = msg_json.get("n", 5) - + src_branch = msg_json.get("src") + target_branch = msg_json.get("target") + # Validate inputs assert int(N) <= 15, "N must be <= 15" assert message_content, "Message content is required" @@ -122,7 +124,10 @@ async def websocket_endpoint( ] elif action_type == "pull_request": history = [ - TRIGGER_PULL_REQUEST_PROMPT.format(COMMITS=message_content) + TRIGGER_PULL_REQUEST_PROMPT.format( + SRC=src_branch, + TARGET=target_branch, + COMMITS=message_content) ] # Stream LLM response back to client diff --git a/app/api/services/fetcher_service.py b/app/api/services/fetcher_service.py index 6776c6b..4c9691d 100644 --- a/app/api/services/fetcher_service.py +++ b/app/api/services/fetcher_service.py @@ -7,7 +7,7 @@ # In-memory store mapping session_id to its respective fetcher instance fetchers: Dict[str, BaseFetcher] = {} -def store_fetcher(session_id: str, pat: str, provider: Optional[str] = "GitHub") -> None: +def store_fetcher(session_id: str, pat: str, provider: Optional[str] = "GitHub") -> str: """ Store the provided PAT associated with the given session_id. @@ -34,6 +34,7 @@ def store_fetcher(session_id: str, pat: str, provider: Optional[str] = "GitHub") fetchers[session_id] = URLFetcher(url=pat) else: raise HTTPException(status_code=400, detail="Unsupported provider") + return fetchers[session_id].user.login except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: diff --git a/app/api/services/prompts.py b/app/api/services/prompts.py index 536c29b..9525727 100644 --- a/app/api/services/prompts.py +++ b/app/api/services/prompts.py @@ -149,7 +149,32 @@ PR_DESCRIPTION_SYSTEM = """ ### System Prompt for Pull Request Title and Description Generation -You are an AI assistant tasked with generating **professional**, **concise**, and **well-structured** pull request (PR) titles and descriptions based on a list of commit messages. Add a touch of expressiveness using **relevant emojis** to make the PR more engaging, without overdoing it ✨ +You are an AI assistant tasked with generating **professional**, **concise**, and **well-structured** pull request (PR) titles and descriptions based on a list of commit messages. +Add a touch of expressiveness using **relevant emojis** to make the PR more engaging, without overdoing it ✨ + +Your main goal is to produce a **final, meaningful summary of the net changes** introduced by the PR — not a chronological log of commits. + +--- + +#### 🔍 Core Behavior: Integrate and Summarize Meaningful Changes + +When analyzing commits: + +1. **Read and analyze all commits** included in the PR. +2. **Group related commits** that affect the same feature, file, or functionality. + - For example, if commits say: + - “add feature X” + - “fix bug in feature X” + - “refactor feature X for performance” + - These should be merged into a single conceptual change, e.g. + → “Implemented feature X with validation and performance improvements.” +3. **Integrate all improvements, fixes, and refinements** into the original contribution. + - Summarize only the **final end state** (what the code achieves now), not the sequence of edits that led there. +4. **Ignore intermediate or reverted states** — only include meaningful contributions that persist in the final version. +5. **Focus on global changes and user-facing impact**, not on verbs like “added / updated / deleted.” + - Emphasize the outcome and purpose. + +--- #### Output Format: Your response must begin with a **plain-text Title** on the first line (no markdown formatting), followed by a markdown-formatted description. @@ -214,42 +239,42 @@ - `## 📚 Documentation` - `## ✅ Tests` - `## 🗒️ Notes` - - Use bullet points for individual changes and consolidate redundant commits. + - Use bullet points for individual changes and **merge related commits** into unified, meaningful summaries. - Maintain a **professional**, **clear**, and **reviewer-friendly** tone. - Avoid commit hashes, timestamps, or author information. - - Avoid unnecessary repetition or overly technical jargon unless essential. + - Avoid unnecessary repetition, overly technical details, or references to intermediate commit states. --- #### Your Response Should: -1. **Start with a Title** summarizing the overall purpose of the PR. +1. **Start with a Title** summarizing the overall purpose of the PR. 2. **Follow with a structured Description** containing: - A high-level summary. - - Grouped, clear lists of changes under emoji-enhanced markdown headers. + - Grouped, clear lists of final changes under emoji-enhanced markdown headers. + - Consolidated, meaningful contributions only — ignoring intermediate commits. --- #### Example Output: -Title: 🚀 Add multi-repository tracking and fix authentication issues +Title: 🚀 Implement multi-repository tracking and enhance authentication ## 📝 Summary -This pull request introduces support for managing multiple repositories and resolves authentication issues affecting GitLab users. +This pull request introduces comprehensive multi-repository management and improves authentication stability and performance. ## ✨ Features -- Added support for tracking commits, pull requests, and issues across multiple repositories -- Implemented new API endpoints for repository management +- Implemented support for managing multiple repositories and their related resources +- Added endpoints for repository synchronization and metadata tracking ## 🐞 Bug Fixes -- Fixed authentication error preventing GitLab users from logging in -- Resolved issue with token expiration handling +- Fixed authentication token validation issues +- Resolved edge case errors during user login flow ## ⚙️ Improvements - Optimized release notes generation for better performance -- Improved UI responsiveness across dashboard components +- Enhanced error handling for repository sync jobs ## 📚 Documentation -- Added API documentation for new repository endpoints -- Updated README with setup instructions for multi-repo support - -""" \ No newline at end of file +- Added detailed API documentation for new endpoints +- Updated README with setup instructions for multi-repo configuration +""" diff --git a/app/git-recap/package.json b/app/git-recap/package.json index f57e580..50e265a 100644 --- a/app/git-recap/package.json +++ b/app/git-recap/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "gh-pages": "^6.3.0", + "html-to-image": "^1.11.11", "lucide-react": "^0.487.0", "pixel-retroui": "^2.0.0", "react": "^19.0.0", @@ -32,4 +33,4 @@ "typescript-eslint": "^8.24.1", "vite": "^6.2.0" } -} +} \ No newline at end of file diff --git a/app/git-recap/src/App.css b/app/git-recap/src/App.css index ef7a85e..fc7713a 100644 --- a/app/git-recap/src/App.css +++ b/app/git-recap/src/App.css @@ -1980,4 +1980,532 @@ a { .pr-auth-message { max-width: 600px; } +} + +/* Export Button Container */ +.export-button-container { + position: fixed; + bottom: 32px; + right: 32px; + z-index: 1000; + animation: slideInUp 0.3s ease-out; +} + +@keyframes slideInUp { + from { + transform: translateY(100px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +/* Export Button - Updated to match GitRecap theme */ +.export-button { + display: flex; + align-items: center; + gap: 8px; + padding: 16px 24px; + background-color: #cccccc; + color: #000; + /* border: 3px solid #000; */ + /* border-radius: 8px; */ + font-family: 'Courier New', monospace; + font-size: 16px; + font-weight: bold; + cursor: pointer; + /* box-shadow: 4px 4px 0 rgba(92, 64, 51, 0.3); */ + transition: all 0.2s ease; +} + +.export-button:hover { + transform: translate(-2px, -2px); + /* box-shadow: 6px 6px 0 rgba(92, 64, 51, 0.3); */ + border-color: #000; +} + +.export-button:active { + transform: translate(2px, 2px); + /* box-shadow: 2px 2px 0 rgba(92, 64, 51, 0.3); */ +} + +.export-icon { + font-size: 20px; +} + +.export-modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 2000; + animation: fadeIn 0.2s ease-out; + padding: 16px; + overflow-y: auto; /* Allow scrolling within overlay */ +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* Export Modal */ +.export-modal { + max-width: 500px; + margin: auto; + background: #fff; + border: 4px solid #000; + border-radius: 12px; + padding: 32px; + max-width: 500px; + width: 90%; + box-shadow: 8px 8px 0 rgba(0, 0, 0, 0.3); + animation: scaleIn 0.3s ease-out; +} + +/* Prevent horizontal overflow on body */ +body, html { + overflow-x: hidden; + max-width: 100vw; +} + +/* When modal is open */ +body.modal-open { + overflow: hidden; + position: fixed; /* Prevents background scroll and maintains viewport */ + width: 100%; +} + +@keyframes scaleIn { + from { + transform: scale(0.8); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +.export-modal h2 { + font-family: 'Courier New', monospace; + font-size: 24px; + margin-bottom: 24px; + text-align: center; + color: #333; +} + +/* Theme Selector Styles */ +.theme-selector { + margin-bottom: 20px; +} + +.theme-selector label { + display: block; + margin-bottom: 8px; + font-weight: bold; + color: #333; + font-family: 'Courier New', monospace; +} + +.theme-option { + padding: 8px 16px; + background: #cccccc; + /* border: 3px solid #333; */ + /* border-radius: 6px; */ + color: #000; + cursor: pointer; + font-family: 'Courier New', monospace; + font-size: 14px; + font-weight: bold; + transition: all 0.2s ease; +} + +.theme-option:hover { + background: #ff7f50; + transform: translateY(-1px); + border-color: #000; +} + +.theme-option.active { + background: #ff7f50; + border-color: #000; + color: #000; + /* box-shadow: 2px 2px 0 rgba(92, 64, 51, 0.3); */ +} + +/* Export Options */ +.export-options { + display: flex; + flex-direction: column; + gap: 16px; + margin-bottom: 24px; +} + +.export-option-btn { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + background-color: #ccc; + /* border: 3px solid #000; */ + /* border-radius: 8px; */ + cursor: pointer; + transition: all 0.2s ease; + font-family: 'Courier New', monospace; + color: #fff; +} + +.export-option-btn:hover { + background-color: #ff7f50; + transform: translateY(-2px); + /* box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.2); */ + /* border-color: #000; */ +} + +.export-option-btn:active { + transform: translateY(0); + /* box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.2); */ +} + +.option-icon { + font-size: 32px; + margin-bottom: 8px; +} + +.option-title { + font-size: 18px; + font-weight: bold; + color: #000; + margin-bottom: 4px; +} + +.option-desc { + font-size: 12px; + color: #000; +} + +/* Close Modal Button */ +.close-modal-btn { + width: 100%; + padding: 12px; + background-color: #cccccc; + color: #000; + /* border: 3px solid #000; */ + /* border-radius: 8px; */ + font-family: 'Courier New', monospace; + font-size: 14px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s ease; +} + +.close-modal-btn:hover { + background-color: #e06e42; + border-color: #000; +} + +/* Badge Preview (Hidden for PNG Generation) */ +.badge-preview { + width: 800px; + position: absolute; + left: -9999px; +} + +/* Base Badge Styles with 16:10 Aspect Ratio */ +.gitrecap-badge { + font-family: 'Courier New', monospace; + width: 800px; + aspect-ratio: 16/10; + height: auto; + background: linear-gradient(135deg, #e8dcc8 0%, #c9b896 100%); + border: 4px solid #4a3728; + border-radius: 8px; + padding: 24px; + box-shadow: 8px 8px 0 rgba(74, 55, 40, 0.3); + color: #4a3728; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +/* Theme Default - Darker GitRecap beige/brown palette */ +.gitrecap-badge.theme-default { + background: linear-gradient(135deg, #e8dcc8 0%, #c9b896 100%); + border-color: #4a3728; + color: #4a3728; +} + +.gitrecap-badge.theme-default .badge-title { + color: #4a3728; + font-family: 'Minecraft-Maus', 'Courier New', monospace; + text-shadow: 2px 2px 0 rgba(74, 55, 40, 0.2); +} + +.gitrecap-badge.theme-default .badge-content { + background: rgba(201, 184, 150, 0.4); +} + +.gitrecap-badge.theme-default .badge-summary { + color: #4a3728; +} + +.gitrecap-badge.theme-default .badge-meta-header, +.gitrecap-badge.theme-default .badge-footer { + color: #4a3728; +} + +.gitrecap-badge.theme-default .badge-footer a { + color: #4a3728; +} + +/* Theme Dark - Darker grays/blacks with high contrast */ +.gitrecap-badge.theme-dark { + background: linear-gradient(135deg, #2d2d2d 0%, #1a1a1a 100%); + border-color: #000000; + color: #e0e0e0; +} + +.gitrecap-badge.theme-dark .badge-logo { + border-color: #e0e0e0; +} + +.gitrecap-badge.theme-dark .badge-title { + color: #e0e0e0; + font-family: 'Minecraft-Maus', 'Courier New', monospace; + text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.5); +} + +.gitrecap-badge.theme-dark .badge-content { + background: rgba(0, 0, 0, 0.3); +} + +.gitrecap-badge.theme-dark .badge-summary { + color: #e0e0e0; +} + +.gitrecap-badge.theme-dark .badge-meta-header, +.gitrecap-badge.theme-dark .badge-footer { + color: #e0e0e0; +} + +.gitrecap-badge.theme-dark .badge-footer a { + color: #e0e0e0; +} + +.gitrecap-badge.theme-dark .badge-footer a:hover { + color: #ffffff; +} + +/* Theme Light - Darker whites/light grays with improved contrast */ +.gitrecap-badge.theme-light { + background: linear-gradient(135deg, #e8e8e8 0%, #d0d0d0 100%); + border-color: #4a4a4a; + color: #2a2a2a; +} + +.gitrecap-badge.theme-light .badge-logo { + border-color: #4a4a4a; +} + +.gitrecap-badge.theme-light .badge-title { + color: #2a2a2a; + font-family: 'Minecraft-Maus', 'Courier New', monospace; + text-shadow: 2px 2px 0 rgba(42, 42, 42, 0.2); +} + +.gitrecap-badge.theme-light .badge-content { + background: rgba(208, 208, 208, 0.4); +} + +.gitrecap-badge.theme-light .badge-summary { + color: #2a2a2a; +} + +.gitrecap-badge.theme-light .badge-meta-header, +.gitrecap-badge.theme-light .badge-footer { + color: #2a2a2a; +} + +.gitrecap-badge.theme-light .badge-footer a { + color: #2a2a2a; +} + +.gitrecap-badge.theme-light .badge-footer a:hover { + color: #000000; +} + +/* Badge Header with User/Repo Info */ +.badge-header { + display: flex; + align-items: flex-start; + gap: 12px; + margin-bottom: 12px; + flex-wrap: wrap; +} + +.badge-logo { + width: 48px; + height: 48px; + border: 2px solid #4a3728; + border-radius: 4px; + background: #fff; + flex-shrink: 0; +} + +.badge-title { + font-size: 28px; + font-family: 'Minecraft-Maus', 'Courier New', monospace; + font-weight: bold; + text-shadow: 2px 2px 0 rgba(74, 55, 40, 0.2); + color: #4a3728; + flex-grow: 1; +} + +/* Badge Meta Header - User/Repo info in header section */ +.badge-meta-header { + font-size: 11px; + color: #4a3728; + margin-left: auto; + text-align: right; + line-height: 1.4; +} + +.badge-meta-header strong { + font-weight: bold; +} + +/* Badge Content Area */ +.badge-content { + flex: 1; + height: auto; + min-height: auto; + background: rgba(201, 184, 150, 0.4); + padding: 16px; + border-radius: 4px; + margin-bottom: 16px; +} + +.badge-summary { + font-size: 14px; + line-height: 1.8; + white-space: pre-wrap; + color: #4a3728; +} + +.badge-summary h1, +.badge-summary h2, +.badge-summary h3 { + margin-top: 12px; + margin-bottom: 8px; + color: #4a3728; +} + +.badge-summary p { + margin-bottom: 8px; +} + +.badge-summary ul, +.badge-summary ol { + margin-left: 20px; + margin-bottom: 8px; +} + +/* Badge Footer */ +.badge-footer { + font-size: 11px; + text-align: center; + opacity: 0.8; + color: #4a3728; +} + +.badge-footer a { + color: #4a3728; + text-decoration: underline; +} + +.badge-footer a:hover { + color: #2d1f15; +} + +/* Responsive Design for Export Features */ +@media (max-width: 768px) { + .export-button-container { + bottom: 150px; + right: 16px; + left: 255px + } + + .export-button { + padding: 12px 20px; + font-size: 14px; + max-width: 90px; + } + + .export-modal { + padding: 24px; + width: 95%; + max-width: 300px; + left: 1; + } + + .export-modal h2 { + font-size: 20px; + margin-bottom: 16px; + } + + .theme-selector { + margin-bottom: 16px; + } + + .theme-selector > div { + flex-direction: column; + align-items: stretch; + } + + .theme-option { + padding: 6px 12px; + font-size: 12px; + width: 100%; + } + + .export-options { + gap: 12px; + margin-bottom: 16px; + } + + .export-option-btn { + padding: 16px; + width: 100%; + } + + .option-icon { + font-size: 24px; + margin-bottom: 6px; + } + + .option-title { + font-size: 16px; + } + + .option-desc { + font-size: 11px; + } + + .close-modal-btn { + padding: 10px; + font-size: 13px; + width: 100%; + } } \ No newline at end of file diff --git a/app/git-recap/src/App.tsx b/app/git-recap/src/App.tsx index df82ab8..1e09a96 100644 --- a/app/git-recap/src/App.tsx +++ b/app/git-recap/src/App.tsx @@ -1,6 +1,8 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { Github, Hammer, BookText, Plus, Minus } from 'lucide-react'; import githubIcon from './assets/github-mark-white.png'; +import { toPng } from 'html-to-image'; +import ReactMarkdown from 'react-markdown'; import './App.css'; import { Info } from "lucide-react"; @@ -41,6 +43,7 @@ function App() { const [isExecuting, setIsExecuting] = useState(false); // Release Notes states + const [badgeTheme, setBadgeTheme] = useState<'default' | 'dark' | 'light'>('default'); const [numOldReleases, setNumOldReleases] = useState(1); const [isExecutingReleaseNotes, setIsExecutingReleaseNotes] = useState(false); @@ -60,7 +63,7 @@ function App() { const [isCreatingPR, setIsCreatingPR] = useState(false); const [prCreationSuccess, setPrCreationSuccess] = useState(false); const [prUrl, setPrUrl] = useState(''); - const [prGenerated, setPrGenerated] = useState(false); // New state to track if PR was generated + const [prGenerated, setPrGenerated] = useState(false); // Auth states const [isPATAuthorized, setIsPATAuthorized] = useState(false); @@ -79,17 +82,28 @@ function App() { const [isPopupOpen, setIsPopupOpen] = useState(false); const [popupMessage, setPopupMessage] = useState(''); const [selectedN, setSelectedN] = useState(5); + const [showExportButton, setShowExportButton] = useState(false); + const [exportModalOpen, setExportModalOpen] = useState(false); + const badgeRef = useRef(null); + const [githubUsername, setGithubUsername] = useState(''); const [recapDone, setRecapDone] = useState(true); const [isReposLoading, setIsReposLoading] = useState(true); const [repoProgress, setRepoProgress] = useState(0); - // UI mode for recap/release/pr const [showReleaseMode, setShowReleaseMode] = useState(false); + const [showMenu, setShowMenu] = useState(false); const actionsLogRef = useRef(null); const summaryLogRef = useRef(null); const textAreaRef = useRef(null); const [currentWebSocket, setCurrentWebSocket] = useState(null); + // Track when recap is complete and show export button + useEffect(() => { + if (!showReleaseMode && !showPRMode && progressWs === 100 && dummyOutput && recapDone) { + setShowExportButton(true); + } + }, [showReleaseMode, showPRMode, progressWs, dummyOutput, recapDone]); + const handleCloneRepo = useCallback(async () => { if (!repoUrl) return; setIsCloning(true); @@ -164,6 +178,7 @@ function App() { setDummyOutput(''); setProgressActions(0); setProgressWs(0); + setShowExportButton(false); setRecapDone(true); handleRecap(); }; @@ -175,6 +190,7 @@ function App() { } setProgressWs(0); setDummyOutput(''); + setShowExportButton(false); if (commitsOutput) { recallWebSocket(commitsOutput, n); } @@ -241,14 +257,12 @@ function App() { }; const handleReleaseNotes = async () => { - // Validation for GitHub provider if (codeHost !== 'github') { setPopupMessage('Release Notes generation is only supported for GitHub repositories. Please select GitHub as your provider.'); setIsPopupOpen(true); return; } - // Validation for single repo selection if (selectedRepos.length === 0) { setPopupMessage('Please select exactly one repository to generate release notes.'); setIsPopupOpen(true); @@ -269,6 +283,7 @@ function App() { setProgressActions(0); setProgressWs(0); setIsExecutingReleaseNotes(true); + setShowExportButton(false); setRecapDone(false); setTimeout(() => { actionsLogRef.current?.scrollIntoView({ behavior: 'smooth' }); @@ -330,6 +345,7 @@ function App() { setProgressActions(0); setProgressWs(0); setIsExecuting(true); + setShowExportButton(false); setTimeout(() => { actionsLogRef.current?.scrollIntoView({ behavior: 'smooth' }); }, 100); @@ -372,23 +388,19 @@ function App() { } }; - // PR Mode Navigation Handlers const handleShowPRMode = useCallback(() => { - // Validation: single repository selection if (selectedRepos.length !== 1) { setPopupMessage('Please select exactly one repository to create a pull request.'); setIsPopupOpen(true); return; } - // Validation: GitHub provider only if (codeHost !== 'github') { setPopupMessage('Pull request creation is only supported for GitHub repositories.'); setIsPopupOpen(true); return; } - // Reset PR mode state setSourceBranch(''); setTargetBranch(''); setTargetBranches([]); @@ -397,11 +409,11 @@ function App() { setPrValidationMessage(''); setPrCreationSuccess(false); setPrUrl(''); - setPrGenerated(false); // Reset PR generated state + setPrGenerated(false); + setShowExportButton(false); setShowPRMode(true); - // Fetch available branches fetchAvailableBranches(); }, [selectedRepos, codeHost, sessionId]); @@ -411,9 +423,9 @@ function App() { setCurrentWebSocket(null); } setShowPRMode(false); + setShowExportButton(false); }, [currentWebSocket]); - // Fetch available branches when entering PR mode const fetchAvailableBranches = useCallback(async () => { if (!sessionId || selectedRepos.length !== 1) return; @@ -443,7 +455,6 @@ function App() { } }, [sessionId, selectedRepos]); - // Handle source branch selection const handleSourceBranchChange = useCallback(async (branch: string) => { setSourceBranch(branch); setTargetBranch(''); @@ -451,11 +462,10 @@ function App() { setPrDiff(''); setPrDescription(''); setPrValidationMessage(''); - setPrGenerated(false); // Reset PR generated state when branch changes + setPrGenerated(false); if (!branch) return; - // Fetch valid target branches setIsLoadingTargets(true); try { @@ -486,18 +496,16 @@ function App() { } }, [sessionId, selectedRepos]); - // Handle target branch selection and fetch diff const handleTargetBranchChange = useCallback(async (branch: string) => { setTargetBranch(branch); setPrDiff(''); setPrDescription(''); setPrValidationMessage(''); setProgressActions(0); - setPrGenerated(false); // Reset PR generated state when branch changes + setPrGenerated(false); if (!branch || !sourceBranch) return; - // Fetch PR diff setIsLoadingDiff(true); const progressActionsInterval = setInterval(() => { @@ -529,7 +537,6 @@ function App() { return; } - // Format commits as readable log const formattedDiff = data.actions; setPrDiff(formattedDiff); @@ -545,7 +552,6 @@ function App() { } }, [sessionId, selectedRepos, sourceBranch]); - // Generate PR description const handleGeneratePRDescription = useCallback(async () => { if (!sourceBranch || !targetBranch || !prDiff) { setPrValidationMessage('Please select both branches and ensure there are changes to summarize.'); @@ -561,7 +567,6 @@ function App() { setPrValidationMessage(''); setProgressWs(0); - // Scroll to summary section when starting generation setTimeout(() => { summaryLogRef.current?.scrollIntoView({ behavior: 'smooth' }); }, 100); @@ -577,7 +582,7 @@ function App() { }, 500); ws.onopen = () => { - ws.send(JSON.stringify({ actions: prDiff })); + ws.send(JSON.stringify({ actions: prDiff, src: sourceBranch, target: targetBranch})); }; ws.onmessage = (event) => { @@ -586,7 +591,7 @@ function App() { clearInterval(progressWsInterval); setProgressWs(100); setIsGeneratingPR(false); - setPrGenerated(true); // Mark PR as generated + setPrGenerated(true); ws.close(); setCurrentWebSocket(null); } else { @@ -614,7 +619,6 @@ function App() { }; }, [sessionId, sourceBranch, targetBranch, prDiff, currentWebSocket]); - // Create PR on GitHub const handleCreatePR = useCallback(async () => { if (!prDescription.trim()) { setPrValidationMessage('Please generate a PR description first.'); @@ -662,12 +666,9 @@ function App() { } }, [sessionId, sourceBranch, targetBranch, prDescription, selectedRepos]); - // 1. Add this to your state declarations (around line 60): - const [showMenu, setShowMenu] = useState(false); - - // 2. Add these handlers after handleShowPRMode (around line 280): const handleShowReleaseMode = useCallback(() => { setShowMenu(false); + setShowExportButton(false); setShowReleaseMode(true); setShowPRMode(false); }, []); @@ -677,7 +678,6 @@ function App() { handleShowPRMode(); }, [handleShowPRMode]); - // Handle GitHub OAuth callback useEffect(() => { const params = new URLSearchParams(window.location.search); const code = params.get("code"); @@ -707,6 +707,7 @@ function App() { .then(data => { setIsGithubAuthorized(true); setSessionId(data.session_id); + setGithubUsername(data.username || 'user'); sessionStorage.setItem('githubSessionId', data.session_id); if (window.history.replaceState) { @@ -750,6 +751,7 @@ function App() { const data = await response.json(); setSessionId(data.session_id); + setGithubUsername(data.username || 'user'); setIsPATAuthorized(true); } catch (error) { console.error('Error authorizing PAT:', error); @@ -763,6 +765,275 @@ function App() { } }; + const generateBadgeContent = useCallback((theme: 'default' | 'dark' | 'light' = 'default') => { + return { + logo: 'https://brunov21.github.io/GitRecap/favicon.ico', + title: 'GitRecap', + link: 'https://brunov21.github.io/GitRecap/', + summary: dummyOutput, + theme: theme, + username: githubUsername, + repositories: selectedRepos.join(', '), + footer: 'github.io/GitRecap' + }; + }, [dummyOutput, githubUsername, selectedRepos]); + + const handleExportPNG = useCallback(() => { + if (!badgeRef.current) { + return; + } + + toPng(badgeRef.current, { + cacheBust: true, + backgroundColor: '#ffffff', + pixelRatio: 2 + }) + .then((dataUrl) => { + const link = document.createElement('a'); + link.download = `gitrecap-${githubUsername}-${Date.now()}.png`; + link.href = dataUrl; + link.click(); + setExportModalOpen(false); + }) + .catch((err) => { + console.error('Error exporting PNG:', err); + }); + }, [badgeRef, githubUsername]); + + const handleExportHTML = useCallback(() => { + const badgeData = generateBadgeContent(badgeTheme); + + const getThemeStyles = (theme: 'default' | 'dark' | 'light') => { + const baseStyles = ` + .gitrecap-badge { + font-family: 'Courier New', monospace; + width: 800px; + aspect-ratio: 16/10; + height: auto; + border-radius: 8px; + padding: 24px; + display: flex; + flex-direction: column; + justify-content: space-between; + } + .badge-header { + display: flex; + align-items: flex-start; + gap: 12px; + margin-bottom: 12px; + flex-wrap: wrap; + } + .badge-logo { + width: 48px; + height: 48px; + border-radius: 4px; + background: #fff; + flex-shrink: 0; + } + .badge-title { + font-size: 40px; + font-family: 'Maus', monospace; + font-weight: bold; + flex-grow: 1; + } + .badge-meta-header { + font-size: 11px; + margin-left: auto; + text-align: right; + line-height: 1.4; + } + .badge-meta-header strong { + font-weight: bold; + } + .badge-content { + flex: 1; + height: auto; + min-height: auto; + padding: 16px; + border-radius: 4px; + margin-bottom: 16px; + } + .badge-summary { + font-size: 14px; + line-height: 1.8; + white-space: pre-wrap; + } + .badge-summary h1, .badge-summary h2, .badge-summary h3 { + margin-top: 12px; + margin-bottom: 8px; + } + .badge-summary p { + margin-bottom: 8px; + } + .badge-summary ul, .badge-summary ol { + margin-left: 20px; + margin-bottom: 8px; + } + .badge-footer { + font-size: 11px; + text-align: center; + opacity: 0.8; + } + .badge-footer a { + text-decoration: underline; + } + `; + + if (theme === 'dark') { + return baseStyles + ` + .gitrecap-badge { + background: linear-gradient(135deg, #2d2d2d 0%, #1a1a1a 100%); + border: 4px solid #000000; + box-shadow: 8px 8px 0 rgba(0, 0, 0, 0.5); + color: #e0e0e0; + } + .badge-logo { + border: 2px solid #e0e0e0; + } + .badge-title { + color: #e0e0e0; + text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.5); + } + .badge-meta-header { + color: #e0e0e0; + } + .badge-content { + background: rgba(0, 0, 0, 0.3); + } + .badge-summary { + color: #e0e0e0; + } + .badge-summary h1, .badge-summary h2, .badge-summary h3 { + color: #e0e0e0; + } + .badge-footer { + color: #e0e0e0; + } + .badge-footer a { + color: #e0e0e0; + } + .badge-footer a:hover { + color: #ffffff; + } + `; + } else if (theme === 'light') { + return baseStyles + ` + .gitrecap-badge { + background: linear-gradient(135deg, #e8e8e8 0%, #d0d0d0 100%); + border: 4px solid #4a4a4a; + box-shadow: 8px 8px 0 rgba(74, 74, 74, 0.3); + color: #2a2a2a; + } + .badge-logo { + border: 2px solid #4a4a4a; + } + .badge-title { + color: #2a2a2a; + text-shadow: 2px 2px 0 rgba(42, 42, 42, 0.2); + } + .badge-meta-header { + color: #2a2a2a; + } + .badge-content { + background: rgba(208, 208, 208, 0.4); + } + .badge-summary { + color: #2a2a2a; + } + .badge-summary h1, .badge-summary h2, .badge-summary h3 { + color: #2a2a2a; + } + .badge-footer { + color: #2a2a2a; + } + .badge-footer a { + color: #2a2a2a; + } + .badge-footer a:hover { + color: #000000; + } + `; + } else { + return baseStyles + ` + .gitrecap-badge { + background: linear-gradient(135deg, #f9f4e8 0%, #f9f4e8 100%); + border: 4px solid #4a3728; + box-shadow: 8px 8px 0 rgba(74, 55, 40, 0.3); + color: #4a3728; + } + .badge-logo { + border: 2px solid #4a3728; + } + .badge-title { + color: #4a3728; + text-shadow: 2px 2px 0 rgba(74, 55, 40, 0.2); + } + .badge-meta-header { + color: #4a3728; + } + .badge-content { + background: #fff8e1; + } + .badge-summary { + color: #4a3728; + } + .badge-summary h1, .badge-summary h2, .badge-summary h3 { + color: #4a3728; + } + .badge-footer { + color: #4a3728; + } + .badge-footer a { + color: #4a3728; + } + .badge-footer a:hover { + color: #2d1f15; + } + `; + } + }; + + const htmlBadge = ` + + + + + GitRecap Badge + + + + +
+
+ +
${badgeData.title}
+
+ User: ${badgeData.username}
+ Repositories: ${badgeData.repositories} +
+
+
+
${badgeData.summary}
+
+ +
+ +`; + + const blob = new Blob([htmlBadge], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.download = `gitrecap-badge-${githubUsername}-${Date.now()}.html`; + link.href = url; + link.click(); + URL.revokeObjectURL(url); + setExportModalOpen(false); + }, [githubUsername, badgeTheme, generateBadgeContent]); + return (
@@ -989,7 +1260,6 @@ function App() {
- {/* Recap Mode */}
- {/* Release Notes Mode */}
- {/* PR Mode */}
@@ -1260,7 +1528,6 @@ function App() {
)} - {/* New PR Creation Button or Message */} {showPRMode && prDescription && (
{isPATAuthorized ? ( @@ -1282,6 +1549,76 @@ function App() {
+ {showExportButton && !showReleaseMode && !showPRMode && ( +
+ +
+ )} + + {exportModalOpen && ( +
setExportModalOpen(false)}> +
e.stopPropagation()}> +

Export Your Recap

+
+ +
+ + + +
+
+
+ + +
+ +
+
+ )} + +
+
+
+ GitRecap Logo +
GitRecap
+
+ User: {githubUsername}
+ Repositories: {selectedRepos.join(', ')} +
+
+
+
+ {dummyOutput} +
+
+
+ Grab your recap at github.io/GitRecap +
+
+
+ setIsPopupOpen(false)}