Commit 841414df authored by Mahmoud Aglan's avatar Mahmoud Aglan

test

parent 37b9873f
...@@ -53,6 +53,20 @@ def _run_migrations(): ...@@ -53,6 +53,20 @@ def _run_migrations():
if table_name not in existing_tables: if table_name not in existing_tables:
print(f" Creating {table_name} table") print(f" Creating {table_name} table")
if "linked_repos" in existing_tables:
lr_columns = {c["name"] for c in inspector.get_columns("linked_repos")}
with engine.connect() as conn:
if "architecture_map" not in lr_columns:
conn.execute(text("ALTER TABLE linked_repos ADD COLUMN architecture_map TEXT"))
print(" Added linked_repos.architecture_map column")
if "map_status" not in lr_columns:
conn.execute(text("ALTER TABLE linked_repos ADD COLUMN map_status VARCHAR(20) DEFAULT 'none'"))
print(" Added linked_repos.map_status column")
if "map_generated_at" not in lr_columns:
conn.execute(text("ALTER TABLE linked_repos ADD COLUMN map_generated_at DATETIME"))
print(" Added linked_repos.map_generated_at column")
conn.commit()
except Exception as e: except Exception as e:
print(f" Migration note: {e}") print(f" Migration note: {e}")
......
...@@ -151,6 +151,9 @@ class LinkedRepo(Base): ...@@ -151,6 +151,9 @@ class LinkedRepo(Base):
default_branch = Column(String(100), default="main") default_branch = Column(String(100), default="main")
web_url = Column(String(500), default="") web_url = Column(String(500), default="")
description = Column(Text, default="") description = Column(Text, default="")
architecture_map = Column(Text, nullable=True)
map_status = Column(String(20), default="none")
map_generated_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime, default=datetime.utcnow)
actions = relationship("PendingAction", back_populates="repo", cascade="all,delete-orphan") actions = relationship("PendingAction", back_populates="repo", cascade="all,delete-orphan")
......
...@@ -3,6 +3,7 @@ GitLab CE integration routes — superadmin only. ...@@ -3,6 +3,7 @@ GitLab CE integration routes — superadmin only.
Son of Anton v4.0.0 Son of Anton v4.0.0
""" """
import asyncio
import json import json
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
...@@ -14,7 +15,7 @@ from sqlalchemy.orm import Session ...@@ -14,7 +15,7 @@ from sqlalchemy.orm import Session
from backend.database import get_db from backend.database import get_db
from backend.models import User, GitLabSettings, LinkedRepo, PendingAction from backend.models import User, GitLabSettings, LinkedRepo, PendingAction
from backend.auth import require_superadmin from backend.auth import require_superadmin
from backend.services import gitlab_service from backend.services import gitlab_service, code_analyzer
router = APIRouter() router = APIRouter()
...@@ -181,10 +182,18 @@ async def link_repo(body: LinkRepoBody, admin: User = Depends(require_superadmin ...@@ -181,10 +182,18 @@ async def link_repo(body: LinkRepoBody, admin: User = Depends(require_superadmin
default_branch=project.get("default_branch", "main"), default_branch=project.get("default_branch", "main"),
web_url=project.get("web_url", ""), web_url=project.get("web_url", ""),
description=project.get("description", ""), description=project.get("description", ""),
map_status="analyzing",
) )
db.add(repo) db.add(repo)
db.commit() db.commit()
db.refresh(repo) db.refresh(repo)
# Start background analysis
asyncio.create_task(_analyze_repo_background(
repo.id, s.gitlab_url, s.gitlab_token,
project["id"], project.get("default_branch", "main"),
))
return _repo_dict(repo) return _repo_dict(repo)
...@@ -196,6 +205,84 @@ def unlink_repo(repo_id: str, admin: User = Depends(require_superadmin), db: Ses ...@@ -196,6 +205,84 @@ def unlink_repo(repo_id: str, admin: User = Depends(require_superadmin), db: Ses
return {"ok": True} return {"ok": True}
# ═══════════════════════════════════════════════════
# Architecture Map
# ═══════════════════════════════════════════════════
async def _analyze_repo_background(repo_id: str, gitlab_url: str, gitlab_token: str, project_id: int, branch: str):
"""Background task: load all files and generate architecture map."""
from backend.database import SessionLocal as BgSession
db = BgSession()
try:
repo = db.query(LinkedRepo).filter(LinkedRepo.id == repo_id).first()
if not repo:
return
repo.map_status = "analyzing"
db.commit()
result = await gitlab_service.load_project_files(
gitlab_url, gitlab_token, project_id, ref=branch,
)
files = result.get("files", [])
if not files:
repo.map_status = "failed"
repo.architecture_map = "[No files could be loaded for analysis]"
db.commit()
return
architecture_map = code_analyzer.analyze_codebase(files)
repo.architecture_map = architecture_map
repo.map_status = "ready"
repo.map_generated_at = datetime.utcnow()
db.commit()
print(f" ✅ Architecture map generated for {repo.name} ({len(architecture_map)} chars)")
except Exception as e:
try:
repo = db.query(LinkedRepo).filter(LinkedRepo.id == repo_id).first()
if repo:
repo.map_status = "failed"
repo.architecture_map = f"[Analysis failed: {str(e)[:200]}]"
db.commit()
except Exception:
pass
print(f" ❌ Architecture analysis failed for repo {repo_id}: {e}")
finally:
db.close()
@router.post("/repos/{repo_id}/analyze")
async def reanalyze_repo(repo_id: str, admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
"""Re-generate the architecture map for a linked repo."""
s = _get_settings(db)
repo = _get_repo(repo_id, db)
repo.map_status = "analyzing"
db.commit()
asyncio.create_task(_analyze_repo_background(
repo.id, s.gitlab_url, s.gitlab_token,
repo.gitlab_project_id, repo.default_branch,
))
return {"ok": True, "status": "analyzing"}
@router.get("/repos/{repo_id}/map")
def get_repo_map(repo_id: str, admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
"""Get the architecture map for a linked repo."""
repo = _get_repo(repo_id, db)
return {
"map_status": repo.map_status or "none",
"map_generated_at": str(repo.map_generated_at) if repo.map_generated_at else None,
"architecture_map": repo.architecture_map or "",
"map_size": len(repo.architecture_map or ""),
}
# ═══════════════════════════════════════════════════ # ═══════════════════════════════════════════════════
# Repository Operations # Repository Operations
# ═══════════════════════════════════════════════════ # ═══════════════════════════════════════════════════
...@@ -300,23 +387,6 @@ async def create_mr(repo_id: str, body: MergeRequestBody, admin: User = Depends( ...@@ -300,23 +387,6 @@ async def create_mr(repo_id: str, body: MergeRequestBody, admin: User = Depends(
raise HTTPException(e.status_code, e.detail) raise HTTPException(e.status_code, e.detail)
@router.get("/repos/{repo_id}/analyze")
async def analyze_project(
repo_id: str,
ref: Optional[str] = Query(None),
admin: User = Depends(require_superadmin),
db: Session = Depends(get_db),
):
s = _get_settings(db)
repo = _get_repo(repo_id, db)
branch = ref or repo.default_branch
try:
result = await gitlab_service.load_project_files(s.gitlab_url, s.gitlab_token, repo.gitlab_project_id, ref=branch)
return result
except gitlab_service.GitLabError as e:
raise HTTPException(e.status_code, e.detail)
# ═══════════════════════════════════════════════════ # ═══════════════════════════════════════════════════
# Pending Actions # Pending Actions
# ═══════════════════════════════════════════════════ # ═══════════════════════════════════════════════════
...@@ -419,6 +489,8 @@ def _repo_dict(r: LinkedRepo) -> dict: ...@@ -419,6 +489,8 @@ def _repo_dict(r: LinkedRepo) -> dict:
"default_branch": r.default_branch, "default_branch": r.default_branch,
"web_url": r.web_url, "web_url": r.web_url,
"description": r.description, "description": r.description,
"map_status": r.map_status or "none",
"map_generated_at": str(r.map_generated_at) if r.map_generated_at else None,
"created_at": str(r.created_at), "created_at": str(r.created_at),
} }
......
This diff is collapsed.
""" """
Background generation manager — v4.1.0 Background generation manager — v4.1.0
Smart codebase loading for massive repos + persistent file context. Smart codebase loading for massive repos + persistent file context + architecture mindmap.
""" """
import asyncio import asyncio
...@@ -19,11 +19,9 @@ from backend.services import bedrock_service, memory_service, rag_service, attac ...@@ -19,11 +19,9 @@ from backend.services import bedrock_service, memory_service, rag_service, attac
# Caches # Caches
# ═══════════════════════════════════════════════════ # ═══════════════════════════════════════════════════
# Tree cache: repo_id:branch → (timestamp, tree_list)
_tree_cache: dict[str, tuple[float, list[dict]]] = {} _tree_cache: dict[str, tuple[float, list[dict]]] = {}
TREE_CACHE_TTL = 600 # 10 minutes TREE_CACHE_TTL = 600
# Tracks which files have been discussed per chat
_chat_file_history: dict[str, set[str]] = {} _chat_file_history: dict[str, set[str]] = {}
...@@ -103,7 +101,6 @@ class GenerationManager: ...@@ -103,7 +101,6 @@ class GenerationManager:
await asyncio.sleep(0.02) await asyncio.sleep(0.02)
def invalidate_repo_cache(self, repo_id: str): def invalidate_repo_cache(self, repo_id: str):
"""Call after a commit to force-refresh on next message."""
keys_to_remove = [k for k in _tree_cache if k.startswith(f"{repo_id}:")] keys_to_remove = [k for k in _tree_cache if k.startswith(f"{repo_id}:")]
for k in keys_to_remove: for k in keys_to_remove:
_tree_cache.pop(k, None) _tree_cache.pop(k, None)
...@@ -115,13 +112,6 @@ class GenerationManager: ...@@ -115,13 +112,6 @@ class GenerationManager:
async def _build_repo_context( async def _build_repo_context(
self, db, chat, user_query: str self, db, chat, user_query: str
) -> Optional[str]: ) -> Optional[str]:
"""
Build repo context using smart file selection.
For ANY size codebase:
1. Full file tree (paths only) — always included
2. Priority files (configs, entry points) — always loaded
3. Query-relevant files — loaded based on what user asked
"""
if not chat.linked_repo_id: if not chat.linked_repo_id:
return None return None
...@@ -138,7 +128,6 @@ class GenerationManager: ...@@ -138,7 +128,6 @@ class GenerationManager:
branch = repo.default_branch branch = repo.default_branch
try: try:
# 1. Get tree (cached)
tree = _get_tree_cache(repo.id, branch) tree = _get_tree_cache(repo.id, branch)
if tree is None: if tree is None:
tree = await gitlab_service.get_tree( tree = await gitlab_service.get_tree(
...@@ -147,10 +136,8 @@ class GenerationManager: ...@@ -147,10 +136,8 @@ class GenerationManager:
) )
_set_tree_cache(repo.id, branch, tree) _set_tree_cache(repo.id, branch, tree)
# 2. Get previously discussed files for this chat
prev_files = _chat_file_history.get(chat.id, set()) prev_files = _chat_file_history.get(chat.id, set())
# 3. Smart-load files
result = await gitlab_service.load_smart_files( result = await gitlab_service.load_smart_files(
gl_url, gl_token, repo.gitlab_project_id, gl_url, gl_token, repo.gitlab_project_id,
ref=branch, tree=tree, ref=branch, tree=tree,
...@@ -158,7 +145,6 @@ class GenerationManager: ...@@ -158,7 +145,6 @@ class GenerationManager:
previous_files=prev_files, previous_files=prev_files,
) )
# 4. Track loaded files for future messages
loaded_paths = set() loaded_paths = set()
for f in result["priority_files"]: for f in result["priority_files"]:
loaded_paths.add(f["path"]) loaded_paths.add(f["path"])
...@@ -168,11 +154,9 @@ class GenerationManager: ...@@ -168,11 +154,9 @@ class GenerationManager:
_chat_file_history[chat.id] = set() _chat_file_history[chat.id] = set()
_chat_file_history[chat.id].update(loaded_paths) _chat_file_history[chat.id].update(loaded_paths)
# 5. Format the context return self._format_smart_context(result, tree, repo, db)
return self._format_smart_context(result, tree, repo)
except Exception as e: except Exception as e:
# Fallback: just the tree
try: try:
tree = await gitlab_service.get_tree( tree = await gitlab_service.get_tree(
gl_url, gl_token, repo.gitlab_project_id, ref=branch, gl_url, gl_token, repo.gitlab_project_id, ref=branch,
...@@ -182,16 +166,10 @@ class GenerationManager: ...@@ -182,16 +166,10 @@ class GenerationManager:
return f"[Repository: {repo.name} — error: {str(e)[:200]}]" return f"[Repository: {repo.name} — error: {str(e)[:200]}]"
def _format_smart_context( def _format_smart_context(
self, result: dict, tree: list[dict], repo self, result: dict, tree: list[dict], repo, db
) -> str: ) -> str:
"""Format loaded files into prompt context.""" files_in_tree = sorted([i["path"] for i in tree if i["type"] == "blob"])
# File tree dirs_in_tree = sorted([i["path"] for i in tree if i["type"] == "tree"])
files_in_tree = sorted(
[i["path"] for i in tree if i["type"] == "blob"]
)
dirs_in_tree = sorted(
[i["path"] for i in tree if i["type"] == "tree"]
)
lines = [ lines = [
f"Repository: {repo.name}", f"Repository: {repo.name}",
...@@ -200,44 +178,45 @@ class GenerationManager: ...@@ -200,44 +178,45 @@ class GenerationManager:
f"Total files: {len(files_in_tree)} | Directories: {len(dirs_in_tree)}", f"Total files: {len(files_in_tree)} | Directories: {len(dirs_in_tree)}",
f"Files loaded into context: {result['files_loaded']}", f"Files loaded into context: {result['files_loaded']}",
f"Characters loaded: {result['total_characters']:,}", f"Characters loaded: {result['total_characters']:,}",
"",
"═" * 60,
"COMPLETE FILE TREE (all paths):",
"═" * 60,
] ]
# Architecture map
if repo.architecture_map and repo.map_status == "ready":
lines.append("")
lines.append(repo.architecture_map)
lines.append("")
# File tree
lines.append("═" * 60)
lines.append("COMPLETE FILE TREE:")
lines.append("═" * 60)
for fp in files_in_tree: for fp in files_in_tree:
lines.append(f" {fp}") lines.append(f" {fp}")
# File contents
lines.append("") lines.append("")
lines.append("═" * 60) lines.append("═" * 60)
lines.append("LOADED FILE CONTENTS:") lines.append("LOADED FILE CONTENTS:")
lines.append("═" * 60) lines.append("═" * 60)
# Priority files
if result["priority_files"]: if result["priority_files"]:
lines.append("") lines.append("\n── Config & Entry Point Files ──")
lines.append("── Config & Entry Point Files ──")
for f in result["priority_files"]: for f in result["priority_files"]:
lines.append(f"\n━━━ {f['path']} ━━━") lines.append(f"\n━━━ {f['path']} ━━━")
lines.append(f["content"]) lines.append(f["content"])
lines.append(f"━━━ end {f['path']} ━━━") lines.append(f"━━━ end {f['path']} ━━━")
# Query-relevant files
if result["query_files"]: if result["query_files"]:
lines.append("") lines.append("\n── Files Relevant to Current Question ──")
lines.append("── Files Relevant to Current Question ──")
for f in result["query_files"]: for f in result["query_files"]:
lines.append(f"\n━━━ {f['path']} ━━━") lines.append(f"\n━━━ {f['path']} ━━━")
lines.append(f["content"]) lines.append(f["content"])
lines.append(f"━━━ end {f['path']} ━━━") lines.append(f"━━━ end {f['path']} ━━━")
# Note about unloaded files
unloaded = len(files_in_tree) - result["files_loaded"] unloaded = len(files_in_tree) - result["files_loaded"]
if unloaded > 0: if unloaded > 0:
lines.append("") lines.append(f"\nNOTE: {unloaded} additional files exist but are not loaded.")
lines.append(f"NOTE: {unloaded} additional files exist in the repository.") lines.append("Mention specific file names to have them loaded in the next message.")
lines.append("If you need to see a specific file, ask the user to mention it by name.")
lines.append("You can see ALL file paths in the tree above.")
return "\n".join(lines) return "\n".join(lines)
...@@ -266,7 +245,6 @@ class GenerationManager: ...@@ -266,7 +245,6 @@ class GenerationManager:
db_user = db.query(User).filter(User.id == user_id).first() db_user = db.query(User).filter(User.id == user_id).first()
# Quota reset
now = datetime.utcnow() now = datetime.utcnow()
if db_user.quota_reset_date and now >= db_user.quota_reset_date: if db_user.quota_reset_date and now >= db_user.quota_reset_date:
db_user.tokens_used_this_month = 0 db_user.tokens_used_this_month = 0
...@@ -280,7 +258,6 @@ class GenerationManager: ...@@ -280,7 +258,6 @@ class GenerationManager:
state.events.append({"type": "error", "message": "Monthly token quota exceeded."}) state.events.append({"type": "error", "message": "Monthly token quota exceeded."})
return return
# Process attachments
attachments = [] attachments = []
if attachment_ids: if attachment_ids:
attachments = ( attachments = (
...@@ -305,7 +282,6 @@ class GenerationManager: ...@@ -305,7 +282,6 @@ class GenerationManager:
if attachments: if attachments:
db.commit() db.commit()
# RAG
kb_id = knowledge_base_id or chat.knowledge_base_id kb_id = knowledge_base_id or chat.knowledge_base_id
rag_context = None rag_context = None
if kb_id: if kb_id:
...@@ -314,29 +290,22 @@ class GenerationManager: ...@@ -314,29 +290,22 @@ class GenerationManager:
except Exception: except Exception:
pass pass
# ── SMART REPO CONTEXT (query-aware file loading) ──
repo_context = await self._build_repo_context(db, chat, content) repo_context = await self._build_repo_context(db, chat, content)
# ── PERSISTENT ATTACHMENT CONTEXT ──
attachment_context = memory_service.gather_attachment_context(chat_id, db) attachment_context = memory_service.gather_attachment_context(chat_id, db)
# Build system prompt
system_prompt = build_full_prompt( system_prompt = build_full_prompt(
rag_context=rag_context, rag_context=rag_context,
repo_context=repo_context, repo_context=repo_context,
attachment_context=attachment_context, attachment_context=attachment_context,
) )
# Build conversation messages
messages = memory_service.build_messages(chat, db) messages = memory_service.build_messages(chat, db)
# Inject multimodal content blocks for current attachments
if attachments and messages and messages[-1]["role"] == "user": if attachments and messages and messages[-1]["role"] == "user":
content_blocks = attachment_service.build_claude_content_blocks(attachments) content_blocks = attachment_service.build_claude_content_blocks(attachments)
content_blocks.append({"type": "text", "text": content}) content_blocks.append({"type": "text", "text": content})
messages[-1]["content"] = content_blocks messages[-1]["content"] = content_blocks
# Thinking config
effective_max = max_tokens effective_max = max_tokens
thinking_config = None thinking_config = None
if reasoning_budget > 0: if reasoning_budget > 0:
...@@ -387,7 +356,6 @@ class GenerationManager: ...@@ -387,7 +356,6 @@ class GenerationManager:
usage = event.get("usage", {}) usage = event.get("usage", {})
output_tokens = usage.get("output_tokens", 0) output_tokens = usage.get("output_tokens", 0)
# Save assistant message
assistant_msg = Message( assistant_msg = Message(
chat_id=chat_id, role="assistant", content=full_text, chat_id=chat_id, role="assistant", content=full_text,
thinking_content=full_thinking or None, thinking_content=full_thinking or None,
...@@ -404,7 +372,6 @@ class GenerationManager: ...@@ -404,7 +372,6 @@ class GenerationManager:
state.message_id = assistant_msg.id state.message_id = assistant_msg.id
# Auto-title
msg_count = db.query(Message).filter(Message.chat_id == chat_id).count() msg_count = db.query(Message).filter(Message.chat_id == chat_id).count()
if msg_count <= 2 and chat.title == "New Chat": if msg_count <= 2 and chat.title == "New Chat":
try: try:
......
...@@ -176,6 +176,8 @@ export const gitlabCommitSingle = (token, repoId, data) => request("POST", `/git ...@@ -176,6 +176,8 @@ export const gitlabCommitSingle = (token, repoId, data) => request("POST", `/git
export const gitlabCreateMR = (token, repoId, data) => request("POST", `/gitlab/repos/${repoId}/merge-request`, token, data); export const gitlabCreateMR = (token, repoId, data) => request("POST", `/gitlab/repos/${repoId}/merge-request`, token, data);
export const gitlabAnalyzeProject = (token, repoId, ref) => export const gitlabAnalyzeProject = (token, repoId, ref) =>
request("GET", `/gitlab/repos/${repoId}/analyze?ref=${encodeURIComponent(ref || "")}`, token); request("GET", `/gitlab/repos/${repoId}/analyze?ref=${encodeURIComponent(ref || "")}`, token);
export const gitlabReanalyzeRepo = (token, repoId) => request("POST", `/gitlab/repos/${repoId}/analyze`, token);
export const gitlabGetRepoMap = (token, repoId) => request("GET", `/gitlab/repos/${repoId}/map`, token);
export const gitlabListActions = (token, status) => request("GET", `/gitlab/actions?status=${status || "pending"}`, token); export const gitlabListActions = (token, status) => request("GET", `/gitlab/actions?status=${status || "pending"}`, token);
export const gitlabCreateAction = (token, data) => request("POST", "/gitlab/actions", token, data); export const gitlabCreateAction = (token, data) => request("POST", "/gitlab/actions", token, data);
export const gitlabApproveAction = (token, actionId) => request("POST", `/gitlab/actions/${actionId}/approve`, token); export const gitlabApproveAction = (token, actionId) => request("POST", `/gitlab/actions/${actionId}/approve`, token);
......
import React, { useState, useEffect, useRef, useCallback, useMemo } from "react"; import React, { useState, useEffect, useRef, useCallback } from "react";
import { useApp } from "../store"; import { useApp } from "../store";
import { import {
getMessages, downloadZip, listKnowledgeBases, updateChat, getMessages, downloadZip, listKnowledgeBases, updateChat,
...@@ -108,9 +108,6 @@ export default function ChatView({ chatId }) { ...@@ -108,9 +108,6 @@ export default function ChatView({ chatId }) {
linked_repo_id: selectedRepoId || "", linked_repo_id: selectedRepoId || "",
}); });
// ── THIS IS THE FIX ──
// Build the full linked_repo object from the local repos list
// so the UI immediately sees the repo banner, commit buttons, etc.
const repoObj = selectedRepoId const repoObj = selectedRepoId
? repos.find(r => r.id === selectedRepoId) || null ? repos.find(r => r.id === selectedRepoId) || null
: null; : null;
...@@ -124,7 +121,7 @@ export default function ChatView({ chatId }) { ...@@ -124,7 +121,7 @@ export default function ChatView({ chatId }) {
reasoning_budget: reasoningBudget, reasoning_budget: reasoningBudget,
knowledge_base_id: selectedKbId, knowledge_base_id: selectedKbId,
linked_repo_id: selectedRepoId, linked_repo_id: selectedRepoId,
linked_repo: repoObj, // ← was missing linked_repo: repoObj,
}, },
}); });
} catch { } } catch { }
...@@ -164,7 +161,6 @@ export default function ChatView({ chatId }) { ...@@ -164,7 +161,6 @@ export default function ChatView({ chatId }) {
if (!msg) return; if (!msg) return;
try { try {
await gitlabCommitSingle(state.token, linkedRepo.id, { branch, file_path: filePath, content: code, commit_message: msg, action }); await gitlabCommitSingle(state.token, linkedRepo.id, { branch, file_path: filePath, content: code, commit_message: msg, action });
// Refresh repo cache so AI sees updated code
try { await refreshRepoContext(state.token, chatId); } catch { } try { await refreshRepoContext(state.token, chatId); } catch { }
} catch (e) { alert(`❌ ${e.message}`); throw e; } } catch (e) { alert(`❌ ${e.message}`); throw e; }
}, [linkedRepo, state.token, chatId]); }, [linkedRepo, state.token, chatId]);
...@@ -185,14 +181,31 @@ export default function ChatView({ chatId }) { ...@@ -185,14 +181,31 @@ export default function ChatView({ chatId }) {
{/* Repo banner */} {/* Repo banner */}
{linkedRepo && ( {linkedRepo && (
<div className="px-3 py-1.5 bg-orange-500/10 border-b border-orange-500/20 flex items-center gap-2 text-xs"> <div className="px-3 py-1.5 bg-orange-500/10 border-b border-orange-500/20 flex items-center gap-2 text-xs flex-wrap">
<GitBranch size={12} className="text-orange-400" /> <GitBranch size={12} className="text-orange-400" />
<span className="text-orange-300 font-medium">{linkedRepo.name}</span> <span className="text-orange-300 font-medium">{linkedRepo.name}</span>
<span className="text-orange-300/60">({linkedRepo.default_branch})</span> <span className="text-orange-300/60">({linkedRepo.default_branch})</span>
<span className="text-orange-300/40">Full codebase loaded</span> {linkedRepo.map_status === "ready" && (
<button onClick={handleRefreshRepo} disabled={refreshingRepo} className="ml-auto text-orange-300/60 hover:text-orange-300 transition" title="Refresh repo context"> <span className="text-green-400/80 flex items-center gap-1">
<RefreshCw size={12} className={refreshingRepo ? "animate-spin" : ""} /> <span className="w-1.5 h-1.5 bg-green-400 rounded-full" /> Mindmap ready
</button> </span>
)}
{linkedRepo.map_status === "analyzing" && (
<span className="text-amber-400/80 flex items-center gap-1">
<Loader2 size={10} className="animate-spin" /> Analyzing…
</span>
)}
{linkedRepo.map_status === "failed" && (
<span className="text-red-400/80">Map failed</span>
)}
{(!linkedRepo.map_status || linkedRepo.map_status === "none") && (
<span className="text-orange-300/40">No mindmap</span>
)}
<div className="ml-auto flex items-center gap-2">
<button onClick={handleRefreshRepo} disabled={refreshingRepo} className="text-orange-300/60 hover:text-orange-300 transition" title="Refresh repo cache">
<RefreshCw size={12} className={refreshingRepo ? "animate-spin" : ""} />
</button>
</div>
</div> </div>
)} )}
...@@ -246,9 +259,9 @@ export default function ChatView({ chatId }) { ...@@ -246,9 +259,9 @@ export default function ChatView({ chatId }) {
<label className="text-xs text-anton-muted mb-1 flex items-center gap-1"><GitBranch size={12} className="text-orange-400" /> Repository (AI sees all files)</label> <label className="text-xs text-anton-muted mb-1 flex items-center gap-1"><GitBranch size={12} className="text-orange-400" /> Repository (AI sees all files)</label>
<select value={selectedRepoId || ""} onChange={e => setSelectedRepoId(e.target.value || null)} className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white focus:outline-none focus:border-orange-400"> <select value={selectedRepoId || ""} onChange={e => setSelectedRepoId(e.target.value || null)} className="w-full bg-anton-bg border border-anton-border rounded-lg px-3 py-2.5 text-white focus:outline-none focus:border-orange-400">
<option value="">None</option> <option value="">None</option>
{repos.map(r => <option key={r.id} value={r.id}>🔀 {r.name} ({r.default_branch})</option>)} {repos.map(r => <option key={r.id} value={r.id}>🔀 {r.name} ({r.default_branch}){r.map_status === "ready" ? " ✅" : r.map_status === "analyzing" ? " ⏳" : ""}</option>)}
</select> </select>
<p className="text-[9px] text-orange-400/60 mt-1">When linked, AI loads the full codebase into context.</p> <p className="text-[9px] text-orange-400/60 mt-1">When linked, AI loads the full codebase + architecture mindmap into context.</p>
</div> </div>
)} )}
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment