Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
f4fc585
feat: implement chat tree functionality and integrate AI SDK
alimkhann Nov 24, 2025
fab6171
feat: add chat route and integrate backend communication; enhance err…
alimkhann Nov 24, 2025
0ff2a32
fix: lint
alimkhann Nov 24, 2025
32e07f3
fix: lint imports
alimkhann Nov 24, 2025
eb14832
feat: enhance chat functionality with safety settings and improved st…
alimkhann Nov 24, 2025
11f7046
feat: add guide chat sessions table and implement chat history API in…
alimkhann Nov 24, 2025
d16bc92
feat: implement chat history clearing and enhance chat tree state man…
alimkhann Nov 24, 2025
daf6208
feat: add TimelineView component with roadmap functionality
alimkhann Nov 24, 2025
6ee5876
feat: revive commitly with supabase edge api and responsive fixes
alimkhann Mar 3, 2026
9e1e6b7
chore: upgrade frontend deps for vercel security gate
alimkhann Mar 3, 2026
1c2362f
fix: route edge API paths correctly and expose public waitlist endpoints
alimkhann Mar 3, 2026
4c56a37
feat(edge): add progressive roadmap jobs and oauth error handling
alimkhann Mar 4, 2026
9746490
feat(frontend): modernize dashboard UI and add progressive roadmap cl…
alimkhann Mar 4, 2026
beb9667
fix(landing): pin waitlist counter to edge API source
alimkhann Mar 4, 2026
52766e0
fix: normalize edge api base urls across frontend and landing
alimkhann Mar 4, 2026
2828db3
feat: add shiny generation state animation for roadmap loading
alimkhann Mar 4, 2026
dac31de
fix: harden edge roadmap generation and token accounting
alimkhann Mar 4, 2026
92129e7
fix: preserve fullName in repo routes to avoid slug parsing errors
alimkhann Mar 4, 2026
56b067b
feat(edge): ship syllabus pipeline schema and robust progressive gene…
alimkhann Mar 4, 2026
fff216c
feat(frontend): honest settings, real bug reports, and cleaner dashbo…
alimkhann Mar 4, 2026
5b61792
feat: stabilize roadmap UX and theme-aware dashboard
alimkhann Mar 4, 2026
e0a1faf
fix: enforce strict syllabus quality and stabilize generation UX
alimkhann Mar 4, 2026
0c9c935
fix: add model fallback retries for syllabus repair
alimkhann Mar 4, 2026
3ccb492
feat: complete 2.2 localization, translation cache, and generation re…
alimkhann Mar 4, 2026
5705d15
feat(i18n): expand non-english route translations
alimkhann Mar 4, 2026
0213277
fix(i18n): remove mixed-language sidebar copy
alimkhann Mar 4, 2026
fb6c672
fix(i18n): defer language swap to avoid hydration mismatch
alimkhann Mar 4, 2026
ce6d316
fix(generation): harden hydration parsing and deterministic recovery
alimkhann Mar 4, 2026
b79ad69
fix: harden progressive roadmap quality gates and diagnostics
alimkhann Mar 4, 2026
b15f8b8
fix: tighten roadmap stage validation and progressive quality recovery
alimkhann Mar 4, 2026
e5811ef
Split edge API into scoped functions with queued worker orchestration
alimkhann Mar 4, 2026
e89908d
Complete dashboard i18n wiring and regeneration moderation UX
alimkhann Mar 4, 2026
bdccbdc
Kick queued roadmap jobs from client poll loop
alimkhann Mar 4, 2026
7ebe7c1
Harden progressive generation polling against transient auth misses
alimkhann Mar 4, 2026
897fe2a
Add live token pool status card and fix stage flag visibility
alimkhann Mar 5, 2026
2a79524
Surface provider rate-limit status and hard-fail bad stage fallbacks
alimkhann Mar 5, 2026
189b3dd
Keep provider-limited worker tasks queued with longer backoff
alimkhann Mar 5, 2026
a1e19f9
Prefer flash models on free tier and remove implicit pro fallback
alimkhann Mar 5, 2026
2921b76
Harden free-tier generation flow and hide low-quality partial catalog…
alimkhann Mar 5, 2026
18df5e5
Improve queue visibility and generation preflight behavior
alimkhann Mar 5, 2026
52559e2
Tighten scaffold template detection for stage validation
alimkhann Mar 5, 2026
3e25a62
Prevent duplicate roadmap queue enqueues while active
alimkhann Mar 5, 2026
1b047a7
Improve Clerk social button contrast in auth modal
alimkhann Mar 5, 2026
009d3dc
Harden provider diagnostics and Clerk OAuth setup guidance
alimkhann Mar 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
*.cursorignore
cursor-logs.md
*.sql
!supabase/migrations/*.sql
__pypackages__/
__pycache__
.pytest_cache/
Expand Down
22 changes: 0 additions & 22 deletions TEACHING_ROADMAP_IMPLEMENTATION.md

This file was deleted.

38 changes: 0 additions & 38 deletions UI_UX_TWEAKS.md

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""add guide chat sessions table

Revision ID: 20241124_add_guide_chat_sessions
Revises: aae1346a34e5
Create Date: 2025-11-24 19:20:00.000000
"""

from __future__ import annotations

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "20241124_add_guide_chat_sessions"
down_revision = "aae1346a34e5"
branch_labels = None
depends_on = None


def upgrade():
op.create_table(
"guide_chat_sessions",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("user_id", sa.String(length=255), nullable=False),
sa.Column("repo_full_name", sa.String(length=255), nullable=False),
sa.Column("stage_id", sa.String(length=255), nullable=True),
sa.Column("messages", sa.JSON(), nullable=False, server_default="[]"),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.func.now(),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.func.now(),
onupdate=sa.func.now(),
nullable=False,
),
sa.UniqueConstraint(
"user_id",
"repo_full_name",
"stage_id",
name="uq_guide_chat_user_repo_stage",
),
)
op.create_index("ix_guide_chat_sessions_user", "guide_chat_sessions", ["user_id"])
op.create_index(
"ix_guide_chat_sessions_repo", "guide_chat_sessions", ["repo_full_name"]
)
op.create_index("ix_guide_chat_sessions_stage", "guide_chat_sessions", ["stage_id"])


def downgrade():
op.drop_index("ix_guide_chat_sessions_stage", table_name="guide_chat_sessions")
op.drop_index("ix_guide_chat_sessions_repo", table_name="guide_chat_sessions")
op.drop_index("ix_guide_chat_sessions_user", table_name="guide_chat_sessions")
op.drop_table("guide_chat_sessions")
116 changes: 107 additions & 9 deletions commitly-backend/app/api/roadmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@
from app.core.database import get_db
from app.models.roadmap import (
CatalogPage,
ChatHistoryResponse,
ChatRequest,
ChatResponse,
RatingRequest,
RatingResponse,
RoadmapRequest,
RoadmapResponse,
SaveChatRequest,
UserRepoStateResponse,
)
from app.models.roadmap import GuideChatSession
from app.services.ai.chat import GeminiChatService
from app.services.roadmap_service import RoadmapService, build_roadmap_service

Expand All @@ -41,6 +43,15 @@ def get_user_id(claims: ClerkClaims) -> str:
return user_id


def get_chat_session(
session: Session, user_id: str, repo_full_name: str, stage_id: str | None
) -> GuideChatSession | None:
query = session.query(GuideChatSession).filter_by(
user_id=user_id, repo_full_name=repo_full_name, stage_id=stage_id
)
return query.first()


@router.get("/catalog", response_model=CatalogPage)
async def list_roadmaps(
page: int = Query(1, ge=1, description="Page number"),
Expand Down Expand Up @@ -251,25 +262,112 @@ async def record_roadmap_view(
return Response(status_code=status.HTTP_204_NO_CONTENT)


@router.post("/chat", response_model=ChatResponse)
@router.post("/chat")
async def chat_with_guide(
payload: ChatRequest,
session: Session = Depends(get_db),
current_user: ClerkClaims = Depends(require_clerk_auth),
) -> ChatResponse:
# current_user: ClerkClaims = Depends(require_clerk_auth),
) -> StreamingResponse:
"""
Chat with the AI guide about the repository or a specific stage.
"""
if not settings.gemini_api_key:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Gemini API key not configured.",
)

chat_service = GeminiChatService(
session=session,
api_key=settings.gemini_api_key,
model=settings.gemini_model,
)

response = await chat_service.chat(
repo_full_name=payload.repo_full_name,
message=payload.message,
stage_id=payload.stage_id,
# If messages are provided (from useChat), use them.
# Otherwise, construct a single message list from payload.message.
messages = payload.messages
if not messages:
if not payload.message:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Either 'messages' or 'message' must be provided.",
)
messages = [{"role": "user", "content": payload.message}]

return StreamingResponse(
chat_service.chat_stream(
repo_full_name=payload.repo_full_name,
messages=messages,
stage_id=payload.stage_id,
),
media_type="text/plain",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Vercel-AI-Data-Stream": "v1",
},
)

return ChatResponse(response=response)

@router.get("/chat/history", response_model=ChatHistoryResponse)
async def get_chat_history(
repo_full_name: str,
stage_id: Optional[str] = None,
session: Session = Depends(get_db),
current_user: ClerkClaims = Depends(require_clerk_auth),
):
user_id = get_user_id(current_user)
try:
record = get_chat_session(session, user_id, repo_full_name, stage_id)
if not record:
return ChatHistoryResponse(
repo_full_name=repo_full_name, stage_id=stage_id, messages=[]
)
return ChatHistoryResponse(
repo_full_name=record.repo_full_name,
stage_id=record.stage_id,
messages=record.messages or [],
)
except Exception as exc: # pragma: no cover - defensive fallback
# If the table doesn't exist yet (migration pending), return empty history
import logging

logging.getLogger(__name__).warning(
"chat history fetch failed: %s", exc, exc_info=True
)
return ChatHistoryResponse(
repo_full_name=repo_full_name, stage_id=stage_id, messages=[]
)


@router.post("/chat/history", status_code=status.HTTP_204_NO_CONTENT)
async def save_chat_history(
payload: SaveChatRequest,
session: Session = Depends(get_db),
current_user: ClerkClaims = Depends(require_clerk_auth),
):
user_id = get_user_id(current_user)
try:
record = get_chat_session(
session, user_id, payload.repo_full_name, payload.stage_id
)

if record:
record.messages = payload.messages
else:
record = GuideChatSession(
user_id=user_id,
repo_full_name=payload.repo_full_name,
stage_id=payload.stage_id,
messages=payload.messages,
)
session.add(record)

session.commit()
except Exception as exc: # pragma: no cover
import logging

logging.getLogger(__name__).warning(
"chat history save failed: %s", exc, exc_info=True
)
return Response(status_code=status.HTTP_204_NO_CONTENT)
7 changes: 3 additions & 4 deletions commitly-backend/app/core/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from functools import lru_cache
import json
from typing import Any, List, Optional, Union

from pydantic import Field, HttpUrl, field_validator
Expand Down Expand Up @@ -44,7 +45,7 @@ class Settings(BaseSettings):
)

# GitHub ingestion
github_api_base: HttpUrl = Field(
github_api_base: HttpUrl = Field( # type: ignore
"https://api.github.com", validation_alias="GITHUB_API_BASE"
)
github_token: Optional[str] = Field(default=None, validation_alias="GITHUB_TOKEN")
Expand Down Expand Up @@ -91,8 +92,6 @@ def _coerce_list(value: Any) -> List[str]:
return []
if cleaned.startswith("["):
try:
import json

data = json.loads(cleaned)
if isinstance(data, list):
return [
Expand Down Expand Up @@ -136,7 +135,7 @@ def parse_authorized_parties(cls, value: Any) -> List[str]:
@lru_cache
def get_settings() -> Settings:
"""Return a cached instance of the application settings."""
return Settings()
return Settings() # type: ignore


settings = get_settings()
46 changes: 45 additions & 1 deletion commitly-backend/app/models/roadmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,37 @@ class RoadmapRating(Base):
)


class GuideChatSession(Base):
"""Stores the latest guide chat history per user/repo/stage."""

__tablename__ = "guide_chat_sessions"
__table_args__ = (
UniqueConstraint(
"user_id",
"repo_full_name",
"stage_id",
name="uq_guide_chat_user_repo_stage",
),
)

id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
repo_full_name: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
stage_id: Mapped[Optional[str]] = mapped_column(
String(255), nullable=True, index=True
)
messages: Mapped[list] = mapped_column(JSON, nullable=False, default=list)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)


class RoadmapViewTracker(Base):
"""Tracks views to prevent spam and implement anti-spam logic."""

Expand Down Expand Up @@ -304,10 +335,23 @@ class RatingResponse(BaseModel):


class ChatRequest(BaseModel):
message: str
message: Optional[str] = None
repo_full_name: str
stage_id: Optional[str] = None
messages: Optional[List[dict]] = None # For full chat history context


class ChatResponse(BaseModel):
response: str


class SaveChatRequest(BaseModel):
repo_full_name: str
stage_id: Optional[str] = None
messages: List[dict]


class ChatHistoryResponse(BaseModel):
repo_full_name: str
stage_id: Optional[str] = None
messages: List[dict]
Loading