diff --git a/.gitignore b/.gitignore index 9139aaf..6390194 100644 --- a/.gitignore +++ b/.gitignore @@ -173,4 +173,5 @@ cython_debug/ # PyPI configuration file .pypirc -observability_data/* \ No newline at end of file +observability_data/* +storage/ diff --git a/app/api/server/routes.py b/app/api/server/routes.py index 5bbdf3a..bbbc778 100644 --- a/app/api/server/routes.py +++ b/app/api/server/routes.py @@ -1,10 +1,9 @@ from fastapi import APIRouter, HTTPException, Request, Query from pydantic import BaseModel -from models.schemas import ChatRequest -from services.llm_service import initialize_llm_session, set_llm, get_llm, trim_messages +from services.llm_service import set_llm, get_llm, trim_messages from services.fetcher_service import store_fetcher, get_fetcher -from git_recap.utils import parse_entries_to_txt +from git_recap.utils import parse_entries_to_txt, parse_releases_to_txt from aicore.llm.config import LlmConfig from datetime import datetime, timezone from typing import Optional, List @@ -197,6 +196,83 @@ async def get_actions( return {"actions": parse_entries_to_txt(actions)} +@router.get("/release_notes") +async def get_release_notes( + session_id: str, + repo_filter: Optional[List[str]] = Query(None), + num_old_releases: int = Query(..., ge=1) +): + """ + Generate release notes for the latest release of a single repository. + Validates input, fetches releases, fetches actions since latest release, and returns compiled release notes. + """ + # Validate repo_filter: must be a single repo + if repo_filter is None or len(repo_filter) != 1: + raise HTTPException(status_code=400, detail="repo_filter must be a list containing exactly one repository name.") + repo = repo_filter[0] + + # Get fetcher for session + try: + fetcher = get_fetcher(session_id) + except HTTPException as e: + raise + + # Check if fetcher supports fetch_releases + try: + releases = fetcher.fetch_releases() + except NotImplementedError: + raise HTTPException(status_code=400, detail="Release fetching is not supported for this provider.") + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error fetching releases: {str(e)}") + + releases_txt = parse_releases_to_txt(releases[:num_old_releases]) + # Filter releases for the requested repo + repo_releases = [r for r in releases if r.get("repo") == repo] + n_releases = len(repo_releases) + if n_releases < 1: + raise HTTPException(status_code=400, detail="Not enough releases found for the specified repository (need at least 1).") + if num_old_releases < 1 or num_old_releases >= n_releases: + raise HTTPException( + status_code=400, + detail=f"num_old_releases must be at least 1 and less than the number of releases available ({n_releases}) for this repository." + ) + + # Sort releases by published_at descending (latest first) + try: + repo_releases.sort(key=lambda r: r.get("published_at") or r.get("created_at"), reverse=True) + except Exception: + raise HTTPException(status_code=500, detail="Failed to sort releases by date.") + + latest_release = repo_releases[0] + + # Determine the start_date for actions (latest release date) + release_date = latest_release.get("published_at") or latest_release.get("created_at") + if not release_date: + raise HTTPException(status_code=500, detail="Latest release does not have a valid date.") + # Accept both datetime and string + if isinstance(release_date, datetime): + start_date_iso = release_date.astimezone(timezone.utc).isoformat() + else: + try: + dt = datetime.fromisoformat(release_date) + start_date_iso = dt.astimezone(timezone.utc).isoformat() + except Exception: + raise HTTPException(status_code=500, detail="Release date is not a valid ISO format.") + + # Fetch actions since latest release for this repo + # Reuse get_actions logic, but inline to avoid async call + # Set fetcher filters + fetcher.start_date = datetime.fromisoformat(start_date_iso) + fetcher.end_dt = None + fetcher.repo_filter = [repo] + + llm = get_llm(session_id) + actions = fetcher.get_authored_messages() + actions = trim_messages(actions, llm.tokenizer) + actions_txt = parse_entries_to_txt(actions) + + return {"actions": "\n\n".join([actions_txt, releases_txt])} + # @router.post("/chat") # async def chat( # chat_request: ChatRequest diff --git a/app/api/server/websockets.py b/app/api/server/websockets.py index fbc98ae..c33a002 100644 --- a/app/api/server/websockets.py +++ b/app/api/server/websockets.py @@ -1,10 +1,10 @@ -from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query +from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect import json from typing import Optional -from services.llm_service import initialize_llm_session, trim_messages, run_concurrent_tasks, get_llm +from services.prompts import SELECT_QUIRKY_REMARK_SYSTEM, SYSTEM, RELEASE_NOTES_SYSTEM, quirky_remarks +from services.llm_service import get_random_quirky_remarks, run_concurrent_tasks, get_llm from aicore.const import SPECIAL_TOKENS, STREAM_END_TOKEN -import ulid import asyncio router = APIRouter() @@ -14,17 +14,39 @@ active_histories = {} TRIGGER_PROMPT = """ -Consider the following history of actionables from Git and in return me the summary with N = '{N}' bullet points: +Consider the following history of actionables from Git and return me the summary with N = '{N}' bullet points: {ACTIONS} """ -@router.websocket("/ws/{session_id}") +TRIGGER_RELEASE_PROMPT = """ +Consider the following history of actionables from Git and the previous Release Notes (if available). +Generate me the next Release Notes based on the new Git Actionables matching the format of the previous releases: + +{ACTIONS} +""" + + +@router.websocket("/ws/{session_id}/{action_type}") async def websocket_endpoint( websocket: WebSocket, - session_id: Optional[str] = None + session_id: Optional[str] = None, + action_type: str="recap" ): await websocket.accept() + + if action_type == "recap": + QUIRKY_SYSTEM = SELECT_QUIRKY_REMARK_SYSTEM.format( + examples=json.dumps(get_random_quirky_remarks(quirky_remarks), indent=4) + ) + + system = [SYSTEM, QUIRKY_SYSTEM] + + elif action_type == "release": + system = RELEASE_NOTES_SYSTEM + + else: + raise HTTPException(status_code=404) # Store the connection active_connections[session_id] = websocket @@ -40,16 +62,23 @@ async def websocket_endpoint( N = msg_json.get("n", 5) assert int(N) <= 15 assert message - history = [ - TRIGGER_PROMPT.format( - N=N, - ACTIONS=message - ) - ] + if action_type == "recap": + history = [ + TRIGGER_PROMPT.format( + N=N, + ACTIONS=message + ) + ] + elif action_type == "release": + history = [ + TRIGGER_RELEASE_PROMPT.format(ACTIONS=message) + ] + response = [] async for chunk in run_concurrent_tasks( llm, - message=history + message=history, + system_prompt=system ): if chunk == STREAM_END_TOKEN: await websocket.send_text(json.dumps({"chunk": chunk})) diff --git a/app/api/services/llm_service.py b/app/api/services/llm_service.py index c1a8ad9..509fc70 100644 --- a/app/api/services/llm_service.py +++ b/app/api/services/llm_service.py @@ -1,7 +1,7 @@ import json import os import uuid -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Union from fastapi import HTTPException import asyncio import random @@ -10,7 +10,6 @@ from aicore.config import Config from aicore.llm import Llm from aicore.llm.config import LlmConfig -from services.prompts import SELECT_QUIRKY_REMARK_SYSTEM, SYSTEM, quirky_remarks def get_random_quirky_remarks(remarks_list, n=5): """ @@ -113,7 +112,7 @@ def trim_messages(messages, tokenizer_fn, max_tokens: Optional[int] = None): messages.pop(0) # Remove from the beginning return messages -async def run_concurrent_tasks(llm, message): +async def run_concurrent_tasks(llm, message, system_prompt :Union[str, List[str]]): """ Run concurrent tasks for the LLM and logger. @@ -124,10 +123,7 @@ async def run_concurrent_tasks(llm, message): Yields: Chunks of logs from the logger. """ - QUIRKY_SYSTEM = SELECT_QUIRKY_REMARK_SYSTEM.format( - examples=json.dumps(get_random_quirky_remarks(quirky_remarks), indent=4) - ) - asyncio.create_task(llm.acomplete(message, system_prompt=[SYSTEM, QUIRKY_SYSTEM])) + asyncio.create_task(llm.acomplete(message, system_prompt=system_prompt)) asyncio.create_task(_logger.distribute()) # Stream logger output while LLM is running. while True: diff --git a/app/api/services/prompts.py b/app/api/services/prompts.py index 3b5f3d5..9e5280d 100644 --- a/app/api/services/prompts.py +++ b/app/api/services/prompts.py @@ -110,4 +110,38 @@ "The code was optimized so hard it ascended to another paradigm.", "A linter ran — and it judged the code as a whole.", "The logic branch spiraled — and so did the afternoon." -] \ No newline at end of file +] + +### TODO improve prompts to infer if release is major, minor or whatever +RELEASE_NOTES_SYSTEM = """ +### System Prompt for Release Notes Generation + +You are an AI assistant tasked with generating professional, concise, and informative release notes for a software project. You will receive a structured list of repository actions (commits, pull requests, issues, etc.) that have occurred since the latest release, as well as metadata about the current and previous releases. + +#### Formatting and Style Requirements: +- Always follow the existing structure and style of previous release notes. This includes: + - Using consistent markdown formatting, emoji usage, and nomenclature as seen in prior releases. + - Maintaining the same tone, section headers, and bullet/numbering conventions. +- Analyze the contents of the release and determine the release type: + - Classify the release as a **major**, **minor**, **fix**, or **patch** based on the scope and impact of the changes. + - Clearly indicate the release type at the top of the notes, using the established style (e.g., with an emoji or header). + - Ensure the summary and highlights reflect the chosen release type. + +#### Your response should: +1. **Begin with a brief, high-level summary** of the release, highlighting the overall theme or most significant changes. +2. **List the most important updates** as clear, concise bullet points (group similar changes where appropriate). Each bullet should reference the type of change (e.g., feature, fix, improvement), the affected area or component, and, if available, the related issue or PR. +3. **Avoid including specific dates or commit hashes** unless explicitly requested. +4. **Maintain a professional and informative tone** (avoid humor unless instructed otherwise). +5. **End with a short call to action or note for users** (e.g., upgrade instructions, thanks to contributors, or next steps). + +#### Example Output: + +**Release v2.3.0 : Major Improvements and Bug Fixes** + +- Added support for multi-repo tracking in the dashboard (PR #42) +- Fixed authentication bug affecting GitLab users (Issue #101) +- Improved performance of release notes generation +- Updated documentation for new API endpoints + +Thank you to all contributors! Please upgrade to enjoy the latest features and improvements. +""" diff --git a/app/git-recap/package-lock.json b/app/git-recap/package-lock.json index ab5c063..05a9167 100644 --- a/app/git-recap/package-lock.json +++ b/app/git-recap/package-lock.json @@ -1,6 +1,6 @@ { "name": "git-recap", - "version": "0.0.0", + "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/app/git-recap/package.json b/app/git-recap/package.json index 257b8a6..f57e580 100644 --- a/app/git-recap/package.json +++ b/app/git-recap/package.json @@ -1,7 +1,7 @@ { "name": "git-recap", "homepage": "https://BrunoV21.github.io/GitRecap/", - "version": "0.0.0", + "version": "0.0.1", "type": "module", "scripts": { "dev": "vite", diff --git a/app/git-recap/src/App.css b/app/git-recap/src/App.css index d57cc00..6d21d5b 100644 --- a/app/git-recap/src/App.css +++ b/app/git-recap/src/App.css @@ -50,6 +50,12 @@ button, margin-right: auto; } +.authorize-btn, +.authorized-btn, +.Button { + width: 100%; +} + /* Headings styling with a dark brown tone */ h1 { font-size: 5rem; @@ -393,11 +399,17 @@ progress, .ProgressBar { } } +/* Remove the width: 100% from all buttons and add it only to specific classes */ button, .Button { text-align: center; display: flex; align-items: center; justify-content: center; + /* Remove width: 100% from here */ +} + +/* Add width: 100% only to specific classes that need it */ +.w-full, .github-signin-btn, .pat-group button { width: 100%; } @@ -406,19 +418,388 @@ button, .Button { width: -webkit-fill-available; } -.recap-button.mt-8 { +/* Recap/Release Switcher Styles - Simplified Retro UI */ + +/* Main container */ +.recap-release-switcher-container { + width: 100%; + display: flex; + justify-content: center; + align-items: center; + min-height: 80px; +} + +.recap-release-switcher { + width: 100%; + max-width: 600px; + margin: 0 auto; + display: flex; + align-items: center; + position: relative; + min-height: 80px; +} + +/* Base retro button styles */ +.retro-btn { + background: #cccccc; + color: #fff; + border: 3px solid #333; + border-radius: 8px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 4px 4px 0 #bfa16b, 0 2px 8px rgba(0,0,0,0.09); + display: flex; + align-items: center; + justify-content: center; + outline: none; +} + +.retro-btn:hover, +.retro-btn:focus { + background: #e06e42; + border-color: #e06e42; + color: #fff; +} + +.retro-btn:active { + box-shadow: 2px 2px 0 #bfa16b; +} + +.retro-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + background: #cccccc; + border-color: #333; + color: #fff; +} + +/* Mode switching areas */ +.recap-main-btn-area, +.release-main-btn-area { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + left: 0; + top: 0; + width: 100%; + min-height: 80px; + z-index: 2; + transition: opacity 0.5s cubic-bezier(0.77,0,0.18,1), + transform 0.5s cubic-bezier(0.77,0,0.18,1); +} + +/* Animation states */ +.slide-in { + opacity: 1; + transform: translateX(0); + pointer-events: auto; +} + +.slide-left-out { + opacity: 0; + transform: translateX(-80px) scale(0.95); + pointer-events: none; +} + +.slide-right-out { + opacity: 0; + transform: translateX(80px) scale(0.95); + pointer-events: none; +} + +/* Recap mode buttons */ +.recap-main-btn { + width: 70vw; + font-size: 1.15rem; + padding: 0.7rem 0; + height: 44px; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; + background-color: #cccccc; +} + +.recap-3dots-rect-btn { + position: relative; + margin-left: 18px; + width: 44px !important; + height: 44px !important; + min-width: 44px !important; + max-width: 44px !important; + min-height: 44px !important; + max-height: 44px !important; + padding: 0 !important; + flex-shrink: 0; + flex-grow: 0; + background-color: #cccccc; +} + +.recap-3dots-rect-inner { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 3px; +} + +.recap-dot { + width: 7px; + height: 7px; + background: #fff; + border-radius: 2px; + box-shadow: 1px 1px 0 #333; +} + +.recap-3dots-badge { + position: absolute; + top: -18px; + right: -18px; + background: #e06e42; + color: #fff; + font-size: 0.7rem; + font-weight: bold; + border-radius: 8px; + padding: 2px 7px; + box-shadow: 0 1px 4px rgba(0,0,0,0.12); + z-index: 3; + pointer-events: none; + letter-spacing: 0.5px; +} + +/* Release mode buttons */ +.release-main-btn-area { + gap: 1.2rem; +} + +.release-main-btn { + min-width: 200px; + font-size: 1.05rem; + padding: 0.6rem 0.5rem; + height: 44px; + min-height: 44px; + background-color: #cccccc; + margin-right: 1.2rem; + display: flex; + align-items: center; + justify-content: center; +} + +.release-main-btn:disabled { + min-width: 200px; + font-size: 1.05rem; + padding: 0.6rem 0.5rem; + height: 44px; + min-height: 44px; + background-color: #cccccc; + margin-right: 1.2rem; + display: flex; + align-items: center; + justify-content: center; + width: auto !important; +} + +/* Base styles for button areas */ +.recap-main-btn-area, +.release-main-btn-area { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + position: absolute; + top: 0; + left: 0; + transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out; +} + +/* Desktop styles - horizontal layout (default) */ +@media (min-width: 768px) { + .recap-main-btn-area, + .release-main-btn-area { + flex-direction: row; + justify-content: center; + flex-wrap: nowrap; + } +} + +/* Mobile styles - vertical layout */ +@media (max-width: 767px) { + .recap-main-btn-area, + .release-main-btn-area { + flex-direction: column; + align-items: center; + gap: 8px; + padding: 0 16px; + } + + /* Remove margin-right on mobile for back button */ + .release-back-rect-btn { + margin-right: 0; + } +} + +/* Your existing component styles */ +.release-back-rect-btn { + background-color: beige; + min-width: 90px; + height: 44px; + font-size: 1rem; + padding: 0.6rem 0.7rem; + margin-right: 1.2rem; + gap: 0.5rem; + display: flex; + align-items: center; + justify-content: center; +} + +.release-back-rect-btn:disabled { + background-color: beige; + min-width: 90px; + height: 44px; + font-size: 1rem; + padding: 0.6rem 0.7rem; + margin-right: 1.2rem; + gap: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + width: auto !important; +} + +.release-back-arrow { + font-size: 1.2rem; + margin-right: 0.3rem; +} + +/* Counter container */ +.release-counter-rect { + display: flex; + align-items: center; + gap: 0.3rem; + background: beige; + border: 3px solid #333; + border-radius: 8px; + padding: 0.2rem 0.7rem; + min-width: 110px; + height: 44px; + box-shadow: 4px 4px 0 #bfa16b, 0 2px 8px rgba(0,0,0,0.09); +} + +.counter-btn-rect:disabled { + opacity: 0.6; + cursor: not-allowed; + background: #cccccc; + color: #333; + border-color: #333; + width: auto !important; /* Override any inherited width */ + max-width: none !important; /* Remove any max-width constraints */ +} + +.counter-btn-rect { + background: #cccccc; + color: #333; + border: 2px solid #333; + border-radius: 6px; + min-width: 32px; + height: 32px; + font-size: 1.1rem; + padding: 0 0.2rem; + display: flex; + align-items: center; + justify-content: center; + margin: 0 2px; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 2px 2px 0 #bfa16b; +} + +.counter-btn-rect:hover:not(:disabled), +.counter-btn-rect:focus:not(:disabled) { + background: #e06e42; + border-color: #e06e42; + color: #333; +} + +.counter-btn-rect:active:not(:disabled) { + box-shadow: 1px 1px 0 #bfa16b; +} + +.counter-btn-rect:disabled { + opacity: 0.6; + cursor: not-allowed; + background: #cccccc; + color: #333; + border-color: #333; +} + +.counter-value-rect { display: inline-block; - font-family: Minecraft, sans-serif; - margin: .0rem auto; - padding: 0rem; /* Increased padding for height */ - width: 30%; /* Slightly wider */ + min-width: 2rem; text-align: center; + font-size: 1.1rem; + color: #333; + background: #cccccc; + border: 2px solid #333; + border-radius: 6px; + margin: 0 4px; + padding: 0 0.7rem; + height: 32px; + line-height: 32px; + box-shadow: 1px 1px 0 #bfa16b; } -/* Preserve styles when disabled */ -.recap-button.mt-8:disabled { - opacity: 0.7; /* Optional: make it look slightly faded */ - cursor: not-allowed; +/* Animation classes */ +.slide-in { + transform: translateX(0); + opacity: 1; +} + +.slide-left-out { + transform: translateX(-100%); + opacity: 0; +} + +.slide-right-out { + transform: translateX(100%); + opacity: 0; +} + +/* Desktop heights */ +@media (min-width: 700px) { + .recap-release-switcher { + height: 60px; /* Single row height for desktop */ + } +} + +/* Mobile heights */ +@media (max-width: 700px) { + .recap-release-switcher:not(.show-release) { + height: 120px; /* Height for recap mode (2 buttons vertically) */ + } + + .recap-release-switcher.show-release { + height: 180px; /* Height for release mode (3 elements vertically) */ + } +} + +@media (max-width: 700px) { + .recap-main-btn, .release-main-btn { + width: 70vw; + min-width: 0; + font-size: 1.05rem; + padding: 0.6rem 0.2rem; + height: 40px; + min-height: 40px; + } + .release-main-btn-area, .recap-main-btn-area { + gap: 0.5rem; + } + .recap-3dots-rect-btn, .release-back-rect-btn, .release-counter-rect { + height: 40px; + min-height: 40px; + } } .retro-input { @@ -476,7 +857,7 @@ button, .Button { } .btn-same-height { - /* Match the input’s height or padding here */ + /* Match the input's height or padding here */ height: 60px; display: inline-flex; align-items: center; @@ -621,6 +1002,69 @@ button, .Button { z-index: 10; } +.button-with-tooltip { + position: relative; + display: inline-block; +} + +.button-with-tooltip .tooltip-text { + display: none; + opacity: 0; + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + margin-bottom: 8px; + + height: 230%; + + background-color: #f5f5dc; + border: 1px solid #d2b48c; + color: #5c4033; + border-radius: 4px; + padding: 12px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + z-index: 100; + + font-size: 0.75rem; + white-space: normal; + width: 280px; + text-align: center; + line-height: 1.4; + min-height: auto; + + transition: opacity 0.2s ease, visibility 0.2s ease; + visibility: hidden; + pointer-events: none; + box-sizing: border-box; +} + +/* Show tooltip when the container is clicked and contains a disabled button */ +.button-with-tooltip:active .recap-3dots-rect-btn:disabled ~ .tooltip-text { + display: block; + opacity: 1; + visibility: visible; +} + +/* Alternative: Show on hover if button is disabled */ +.button-with-tooltip:hover .recap-3dots-rect-btn:disabled ~ .tooltip-text { + display: block; + opacity: 1; + visibility: visible; +} + +/* Arrow pointing to the badge */ +.button-with-tooltip .tooltip-text::after { + content: ""; + position: absolute; + top: 100%; + left: 75%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: #f5f5dc transparent transparent transparent; +} + /* Add to your existing CSS */ .justify-between { justify-content: space-between; diff --git a/app/git-recap/src/App.tsx b/app/git-recap/src/App.tsx index fbcc566..98519be 100644 --- a/app/git-recap/src/App.tsx +++ b/app/git-recap/src/App.tsx @@ -1,6 +1,5 @@ - import { useState, useEffect, useRef, useCallback } from 'react'; -import { Github, Hammer, BookText } from 'lucide-react'; +import { Github, Hammer, BookText, Plus, Minus } from 'lucide-react'; import githubIcon from './assets/github-mark-white.png'; import './App.css'; @@ -41,6 +40,10 @@ function App() { const [progressWs, setProgressWs] = useState(0); const [isExecuting, setIsExecuting] = useState(false); + // Release Notes states + const [numOldReleases, setNumOldReleases] = useState(1); + const [isExecutingReleaseNotes, setIsExecutingReleaseNotes] = useState(false); + // Auth states const [isPATAuthorized, setIsPATAuthorized] = useState(false); const [authProgress, setAuthProgress] = useState(0); @@ -58,8 +61,11 @@ function App() { const [isPopupOpen, setIsPopupOpen] = useState(false); const [popupMessage, setPopupMessage] = useState(''); const [selectedN, setSelectedN] = useState(5); + const [recapDone, setRecapDone] = useState(true); const [isReposLoading, setIsReposLoading] = useState(true); const [repoProgress, setRepoProgress] = useState(0); + // UI mode for recap/release + const [showReleaseMode, setShowReleaseMode] = useState(false); const actionsLogRef = useRef(null); const summaryLogRef = useRef(null); @@ -136,11 +142,11 @@ function App() { if (currentWebSocket) { currentWebSocket.close(); } - setCommitsOutput(''); setDummyOutput(''); setProgressActions(0); setProgressWs(0); + setRecapDone(true); handleRecap(); }; @@ -149,14 +155,11 @@ function App() { currentWebSocket.close(); setCurrentWebSocket(null); } - setProgressWs(0); setDummyOutput(''); - if (commitsOutput) { recallWebSocket(commitsOutput, n); } - setSelectedN(n); }; @@ -168,9 +171,9 @@ function App() { }, 0); }; - const recallWebSocket = (actions: string, n?: number) => { + const recallWebSocket = (actions: string, n?: number, actionType: string = "recap") => { const backendUrl = import.meta.env.VITE_AICORE_API; - const wsUrl = `${backendUrl.replace(/^http/, 'ws')}/ws/${sessionId}`; + const wsUrl = `${backendUrl.replace(/^http/, 'ws')}/ws/${sessionId}/${actionType}`; const ws = new WebSocket(wsUrl); setCurrentWebSocket(ws); @@ -218,26 +221,103 @@ function App() { const handleRecap = async () => { await fetchInitialActions(); }; + + 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); + return; + } + + if (selectedRepos.length > 1) { + setPopupMessage('Please select only one repository for release notes generation. Multiple repositories are not supported for this feature.'); + setIsPopupOpen(true); + return; + } + + if (currentWebSocket) { + currentWebSocket.close(); + } + setCommitsOutput(''); + setDummyOutput(''); + setProgressActions(0); + setProgressWs(0); + setIsExecutingReleaseNotes(true); + setRecapDone(false); + setTimeout(() => { + actionsLogRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, 100); + await fetchReleaseNotes(); + }; + + const fetchReleaseNotes = async () => { + if (currentWebSocket) { + currentWebSocket.close(); + } + setIsExecutingReleaseNotes(true); + setTimeout(() => { + actionsLogRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, 100); + const progressReleaseNotesInterval = setInterval(() => { + setProgressActions((prev) => (prev < 95 ? prev + 1 : prev)); + }, 500); + try { + const backendUrl = import.meta.env.VITE_AICORE_API; + const queryParams = new URLSearchParams({ + session_id: sessionId, + num_old_releases: numOldReleases.toString(), + repo_filter: selectedRepos[0] + }).toString(); + const response = await fetch(`${backendUrl}/release_notes?${queryParams}`, { + method: 'GET' + }); + if (!response.ok) throw new Error(`Request failed! Status: ${response.status}`); + const data = await response.json(); + if (!data.actions) { + setPopupMessage('Got no actionables for release notes. Please check your repository selection or ensure the repository has releases.'); + setIsPopupOpen(true); + clearInterval(progressReleaseNotesInterval); + setProgressActions(100); + return; + } + const mergedOutput = commitsOutput + (commitsOutput ? '\n\n' : '') + data.actions; + setCommitsOutput(mergedOutput); + clearInterval(progressReleaseNotesInterval); + setProgressActions(100); + summaryLogRef.current?.scrollIntoView({ behavior: 'smooth' }); + recallWebSocket(mergedOutput, undefined, "release"); + } catch (error) { + console.error('Error during release notes generation:', error); + setPopupMessage('Error retrieving release notes. Please try again.'); + setIsPopupOpen(true); + } finally { + setIsExecutingReleaseNotes(false); + } + }; const fetchInitialActions = async () => { if (currentWebSocket) { currentWebSocket.close(); } - setCommitsOutput(''); setDummyOutput(''); setProgressActions(0); setProgressWs(0); - setIsExecuting(true); - + setIsExecuting(true); setTimeout(() => { actionsLogRef.current?.scrollIntoView({ behavior: 'smooth' }); }, 100); - const progressActionsInterval = setInterval(() => { setProgressActions((prev) => (prev < 95 ? prev + 1 : prev)); }, 500); - try { const backendUrl = import.meta.env.VITE_AICORE_API; const queryParams = new URLSearchParams({ @@ -247,15 +327,11 @@ function App() { ...(selectedRepos.length ? { repo_filter: selectedRepos.join(",") } : {}), ...(authors.length ? { authors: authors.join(",") } : {}), }).toString(); - const response = await fetch(`${backendUrl}/actions?${queryParams}`, { method: 'GET' }); - if (!response.ok) throw new Error(`Request failed! Status: ${response.status}`); - const data = await response.json(); - if (!data.actions) { setPopupMessage('Got no actionables from Git. Please check your filters or date range. If you are signing with GitHub, you will need to install GitRecap from the Marketplace or authenticate with a PAT instead.'); setIsPopupOpen(true); @@ -263,7 +339,6 @@ function App() { setProgressActions(100); return; } - setCommitsOutput(data.actions); clearInterval(progressActionsInterval); setProgressActions(100); @@ -588,17 +663,87 @@ function App() { - -
- + + +
+
+ {/* Recap Mode */} +
+ +
+ +
+ You can now generate releases - only supported for GitHub repos (requires sign in or PAT authorization) and select one repo from a dropdown from above! +
+
+
+ {/* Release Notes Mode */} +
+ + +
+ + + {numOldReleases} + + +
+
+
+

Actions Log

@@ -621,13 +766,25 @@ function App() {

Summary (by `{import.meta.env.VITE_LLM_MODEL}`)

- - -
@@ -639,9 +796,9 @@ function App() { borderColor="black" className="w-full mb-4" /> -