From 714a4c5cc1db5fdff804242c0e49348abf6c3554 Mon Sep 17 00:00:00 2001 From: stewartshea Date: Wed, 4 Mar 2026 07:31:48 -0500 Subject: [PATCH 1/5] Add Intake Wizard and API Integration - Included the Intake Wizard route in the frontend application, allowing users to access the new feature via the `/intake` path. - Updated the Header component to include a navigation button for the Intake Wizard. - Implemented the Intake API in the services, defining interfaces and methods for platform retrieval, search functionality, and submission of design specifications. - Enhanced the backend by integrating the Intake router, ensuring the new functionality is accessible through the API. --- cc-registry-v2/backend/app/main.py | 3 +- cc-registry-v2/backend/app/routers/intake.py | 426 ++++++++++++ cc-registry-v2/frontend/src/App.tsx | 2 + .../frontend/src/components/Header.tsx | 12 + .../frontend/src/pages/IntakeWizard.tsx | 625 ++++++++++++++++++ cc-registry-v2/frontend/src/services/api.ts | 71 ++ 6 files changed, 1138 insertions(+), 1 deletion(-) create mode 100644 cc-registry-v2/backend/app/routers/intake.py create mode 100644 cc-registry-v2/frontend/src/pages/IntakeWizard.tsx diff --git a/cc-registry-v2/backend/app/main.py b/cc-registry-v2/backend/app/main.py index dcd7f24c1177..2a0c2b88fcd8 100644 --- a/cc-registry-v2/backend/app/main.py +++ b/cc-registry-v2/backend/app/main.py @@ -99,7 +99,7 @@ async def health_check(): } # Include routers -from app.routers import admin, tasks, raw_data, admin_crud, task_execution_admin, versions, task_management, admin_inventory, helm_charts, mcp_chat, chat_debug, github_issues, schedule_config, analytics, vector_search +from app.routers import admin, tasks, raw_data, admin_crud, task_execution_admin, versions, task_management, admin_inventory, helm_charts, mcp_chat, chat_debug, github_issues, schedule_config, analytics, vector_search, intake app.include_router(admin.router) app.include_router(tasks.router) app.include_router(raw_data.router) @@ -115,6 +115,7 @@ async def health_check(): app.include_router(github_issues.router, prefix="/api/v1") app.include_router(analytics.router) app.include_router(vector_search.router) +app.include_router(intake.router) @app.get("/api/v1/registry/collections") async def list_collections(): diff --git a/cc-registry-v2/backend/app/routers/intake.py b/cc-registry-v2/backend/app/routers/intake.py new file mode 100644 index 000000000000..a8bde6e57619 --- /dev/null +++ b/cc-registry-v2/backend/app/routers/intake.py @@ -0,0 +1,426 @@ +""" +CodeBundle Intake Wizard Router + +Guides users through a conversational flow to define CodeBundle requirements, +searches existing coverage via the MCP server, and files structured issues +to the codebundle-farm repository. +""" +import logging +from typing import Dict, List, Optional, Any +from datetime import datetime, timezone +from fastapi import APIRouter, HTTPException, status +from pydantic import BaseModel +import requests + +from app.core.config import settings +from app.services.mcp_client import get_mcp_client, MCPError + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/api/v1/intake", tags=["intake"]) + +CODEBUNDLE_FARM_REPO = "stewartshea/codebundle-farm" + +PLATFORMS = [ + "Kubernetes", "AWS", "Azure", "GCP", "Linux", + "Database", "Terraform", "Docker", "GitHub", "Other", +] + + +# ============================================================================= +# Request/Response Models +# ============================================================================= + +class SearchRequest(BaseModel): + """Step 1–2: User describes their need and we search for existing coverage.""" + description: str + platform: Optional[str] = None + + +class SearchMatch(BaseModel): + display_name: str + slug: str + collection_slug: str + platform: str + description: str + tasks: List[str] = [] + tags: List[str] = [] + relevance_score: float = 0.0 + source_url: str = "" + + +class ExistingRequest(BaseModel): + number: int + title: str + url: str + created_at: str + + +class SearchResponse(BaseModel): + matches: List[SearchMatch] + existing_requests: List[ExistingRequest] + suggested_platform: str + query_used: str + + +class DesignSpecDraft(BaseModel): + """Step 4–5: The structured Design Spec generated from user answers.""" + codebundle_name: str + target_collection: str = "rw-cli-codecollection" + platform: str + purpose: str + tasks: List[Dict[str, str]] + resource_types: List[str] = [] + env_vars: List[Dict[str, str]] = [] + secrets: List[Dict[str, str]] = [] + tools_required: List[str] = [] + related_bundles: List[str] = [] + user_description: str + coverage_notes: str = "" + + +class SubmitRequest(BaseModel): + """Step 6: Submit the Design Spec as a GitHub Issue.""" + design_spec: DesignSpecDraft + contact_email: Optional[str] = None + + +class SubmitResponse(BaseModel): + issue_url: str + issue_number: int + message: str + + +# ============================================================================= +# Endpoints +# ============================================================================= + +@router.get("/platforms") +async def get_platforms(): + """Return the list of supported platforms for the wizard.""" + return {"platforms": PLATFORMS} + + +@router.post("/search", response_model=SearchResponse) +async def search_existing_coverage(req: SearchRequest): + """ + Search for existing CodeBundles and open requests that match the + user's description. Called during wizard steps 1–3. + """ + mcp = get_mcp_client() + matches: List[SearchMatch] = [] + existing_requests: List[ExistingRequest] = [] + suggested_platform = req.platform or "" + + # Search existing CodeBundles via MCP + try: + if await mcp.is_available(): + args: Dict[str, Any] = {"query": req.description, "max_results": 8} + if req.platform: + args["platform"] = req.platform + + result = await mcp.call_tool("find_codebundle", args) + if result and result.get("success"): + parsed = _parse_codebundle_results(result.get("result", "")) + matches = parsed + + # Check for open requests + search_term = req.platform or req.description.split()[0] if req.description else "" + if search_term: + req_result = await mcp.call_tool("check_existing_requests", {"search_term": search_term}) + if req_result and req_result.get("success"): + existing_requests = _parse_existing_requests(req_result.get("result", "")) + except MCPError as e: + logger.warning(f"MCP search failed, continuing without results: {e}") + except Exception as e: + logger.warning(f"Unexpected error during MCP search: {e}") + + # Infer platform from description if not provided + if not suggested_platform: + suggested_platform = _infer_platform(req.description) + + return SearchResponse( + matches=matches, + existing_requests=existing_requests, + suggested_platform=suggested_platform, + query_used=req.description, + ) + + +@router.post("/generate-spec", response_model=DesignSpecDraft) +async def generate_design_spec( + description: str, + platform: str, + tasks_description: str, + resource_types: str = "", + tools: str = "", +): + """ + Generate a draft Design Spec from user-provided information. + This is a helper that pre-fills the spec structure; the frontend + lets the user refine it before submission. + """ + task_list = [t.strip() for t in tasks_description.split("\n") if t.strip()] + tasks = [{"name": _slugify_task(t), "checks": t} for t in task_list] + resources = [r.strip() for r in resource_types.split(",") if r.strip()] + tool_list = [t.strip() for t in tools.split(",") if t.strip()] + + name = _generate_bundle_name(platform, description) + + return DesignSpecDraft( + codebundle_name=name, + platform=platform, + purpose=description, + tasks=tasks, + resource_types=resources, + tools_required=tool_list, + user_description=description, + ) + + +@router.post("/submit", response_model=SubmitResponse) +async def submit_intake(req: SubmitRequest): + """ + Create a GitHub Issue in codebundle-farm with the Design Spec + and label it for the Architect pipeline. + """ + if settings.GITHUB_TOKEN == "your_github_token_here": + raise HTTPException( + status_code=status.HTTP_501_NOT_IMPLEMENTED, + detail="GitHub integration not configured. Set GITHUB_TOKEN.", + ) + + spec = req.design_spec + title = f"[intake] {spec.codebundle_name}: {spec.purpose[:80]}" + + body = _build_issue_body(spec, req.contact_email) + + labels = [ + "intake", + "needs-architect", + f"platform:{spec.platform.lower()}", + ] + + try: + api_url = f"https://api.github.com/repos/{CODEBUNDLE_FARM_REPO}/issues" + headers = { + "Authorization": f"token {settings.GITHUB_TOKEN}", + "Accept": "application/vnd.github.v3+json", + } + issue_data = {"title": title, "body": body, "labels": labels} + + response = requests.post(api_url, json=issue_data, headers=headers) + + if response.status_code == 201: + info = response.json() + return SubmitResponse( + issue_url=info["html_url"], + issue_number=info["number"], + message=f"Created issue #{info['number']} in codebundle-farm", + ) + else: + logger.error(f"GitHub API error: {response.status_code} - {response.text}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"GitHub API error: {response.text}", + ) + except requests.RequestException as e: + logger.error(f"Error creating GitHub issue: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error communicating with GitHub: {e}", + ) + + +# ============================================================================= +# Helpers +# ============================================================================= + +def _build_issue_body(spec: DesignSpecDraft, contact_email: Optional[str]) -> str: + """Build the GitHub Issue body with the Design Spec in YAML.""" + tasks_yaml = "" + for t in spec.tasks: + tasks_yaml += f' - name: "{t.get("name", "")}"\n' + tasks_yaml += f' checks: "{t.get("checks", "")}"\n' + + env_yaml = "" + for v in spec.env_vars: + env_yaml += f' - name: "{v.get("name", "")}"\n' + env_yaml += f' description: "{v.get("description", "")}"\n' + env_yaml += f' example: "{v.get("example", "")}"\n' + + secrets_yaml = "" + for s in spec.secrets: + secrets_yaml += f' - name: "{s.get("name", "")}"\n' + secrets_yaml += f' description: "{s.get("description", "")}"\n' + + parts = [ + "## Original Request", + "", + f"> {spec.user_description}", + "", + ] + + if spec.coverage_notes: + parts.extend([ + "## Existing Coverage Notes", + "", + spec.coverage_notes, + "", + ]) + + parts.extend([ + "## Design Spec (draft)", + "", + "```yaml", + f"codebundle_name: {spec.codebundle_name}", + f"target_collection: {spec.target_collection}", + f"platform: {spec.platform}", + f'purpose: "{spec.purpose}"', + "", + "tasks:", + tasks_yaml.rstrip(), + "", + "resource_types:", + ]) + for r in spec.resource_types: + parts.append(f" - {r}") + + if spec.env_vars: + parts.extend(["", "env_vars:", env_yaml.rstrip()]) + if spec.secrets: + parts.extend(["", "secrets:", secrets_yaml.rstrip()]) + if spec.tools_required: + parts.append("") + parts.append("tools_required:") + for t in spec.tools_required: + parts.append(f" - {t}") + if spec.related_bundles: + parts.append("") + parts.append("related_bundles:") + for b in spec.related_bundles: + parts.append(f" - {b}") + + parts.extend(["```", ""]) + + if contact_email: + parts.append(f"**Contact**: {contact_email}") + parts.append("") + + parts.extend([ + "---", + f"*Created via the CodeCollection Registry intake wizard at {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}.*", + ]) + + return "\n".join(parts) + + +def _infer_platform(description: str) -> str: + """Best-effort platform detection from free text.""" + d = description.lower() + mapping = [ + (["kubernetes", "k8s", "kubectl", "pod", "deployment", "namespace", "helm"], "Kubernetes"), + (["aws", "amazon", "s3", "ec2", "lambda", "cloudwatch", "iam"], "AWS"), + (["azure", "az ", "aks", "app service", "resource group"], "Azure"), + (["gcp", "google cloud", "gke", "bigquery", "pubsub"], "GCP"), + (["terraform", "tfstate", "hcl"], "Terraform"), + (["docker", "container", "dockerfile"], "Docker"), + (["github", "gh ", "actions", "repository"], "GitHub"), + (["postgres", "mysql", "redis", "database", "sql"], "Database"), + (["linux", "ssh", "systemd", "cron"], "Linux"), + ] + for keywords, platform in mapping: + if any(kw in d for kw in keywords): + return platform + return "" + + +def _generate_bundle_name(platform: str, description: str) -> str: + """Generate a bundle name slug from platform and description.""" + prefix = platform.lower().replace(" ", "-") if platform else "generic" + words = description.lower().split()[:4] + slug = "-".join(w for w in words if len(w) > 2 and w.isalnum()) + if not slug: + slug = "healthcheck" + return f"{prefix}-{slug}" + + +def _slugify_task(description: str) -> str: + """Turn a task description into a Robot Framework task name.""" + words = description.strip().split()[:8] + return " ".join(w.capitalize() for w in words) + + +def _parse_codebundle_results(markdown: str) -> List[SearchMatch]: + """Parse the markdown output from MCP find_codebundle into structured matches.""" + matches = [] + current: Dict[str, Any] = {} + + for line in markdown.split("\n"): + line = line.strip() + if line.startswith("## ") and "**" in line: + if current.get("display_name"): + matches.append(SearchMatch(**current)) + name = line.split("**")[1] if "**" in line else line[3:] + current = { + "display_name": name.strip(), + "slug": "", + "collection_slug": "", + "platform": "", + "description": "", + "tasks": [], + "tags": [], + "relevance_score": 0.0, + "source_url": "", + } + elif line.startswith("**Collection:**"): + current["collection_slug"] = line.split("**Collection:**")[1].strip() + elif line.startswith("**Platform:**"): + current["platform"] = line.split("**Platform:**")[1].strip() + elif line.startswith("**Description:**"): + current["description"] = line.split("**Description:**")[1].strip() + elif line.startswith("**Relevance:**"): + try: + score_str = line.split("**Relevance:**")[1].strip().rstrip("%") + current["relevance_score"] = float(score_str) / 100 + except (ValueError, IndexError): + pass + elif line.startswith("**Tags:**"): + tags_str = line.split("**Tags:**")[1].strip() + current["tags"] = [t.strip() for t in tags_str.split(",") if t.strip()] + elif line.startswith("**Source:**"): + # Extract slug from source link + if "/codebundles/" in line: + slug_part = line.split("/codebundles/")[-1].rstrip(")") + current["slug"] = slug_part + current["source_url"] = line.split("(")[-1].rstrip(")") if "(" in line else "" + elif line.startswith("- ") and current.get("display_name"): + task = line[2:].strip() + if task and "tasks" in current: + current["tasks"].append(task) + + if current.get("display_name"): + matches.append(SearchMatch(**current)) + + return matches + + +def _parse_existing_requests(markdown: str) -> List[ExistingRequest]: + """Parse the markdown output from MCP check_existing_requests.""" + requests_list = [] + for line in markdown.split("\n"): + line = line.strip() + if line.startswith("- **#"): + try: + number = int(line.split("**#")[1].split("**")[0]) + title = line.split("[")[1].split("]")[0] if "[" in line else "" + url = line.split("(")[1].split(")")[0] if "(" in line else "" + created = "" + if "Created:" in line: + created = line.split("Created:")[1].strip() + requests_list.append(ExistingRequest( + number=number, title=title, url=url, created_at=created, + )) + except (IndexError, ValueError): + continue + return requests_list diff --git a/cc-registry-v2/frontend/src/App.tsx b/cc-registry-v2/frontend/src/App.tsx index 5777e1d3e8e5..7a93231db9f9 100644 --- a/cc-registry-v2/frontend/src/App.tsx +++ b/cc-registry-v2/frontend/src/App.tsx @@ -17,6 +17,7 @@ import Login from './pages/Login'; import ConfigBuilder from './pages/ConfigBuilder'; import Chat from './pages/Chat'; import ChatDebug from './pages/ChatDebug'; +import IntakeWizard from './pages/IntakeWizard'; import Footer from './components/Footer'; import { CartProvider } from './contexts/CartContext'; import { AuthProvider } from './contexts/AuthContext'; @@ -38,6 +39,7 @@ function AppContent() { } /> } /> } /> + } /> } /> } /> diff --git a/cc-registry-v2/frontend/src/components/Header.tsx b/cc-registry-v2/frontend/src/components/Header.tsx index 6548f574b1c0..290b03769294 100644 --- a/cc-registry-v2/frontend/src/components/Header.tsx +++ b/cc-registry-v2/frontend/src/components/Header.tsx @@ -228,6 +228,18 @@ const Header: React.FC = () => { Registry Chat + + + + + )} + + {/* ─── Step 1: Search Results ─── */} + {activeStep === 1 && ( + + {matches.length > 0 && ( + + + + Existing CodeBundles ({matches.length}) + + setMatchesExpanded(!matchesExpanded)}> + {matchesExpanded ? : } + + + + We found these existing bundles that may address your need. + If one solves your problem, you're all set! + + + + {matches.map((m, i) => ( + + + + + {m.display_name} + + {m.relevance_score > 0 && ( + 0.7 ? 'success' : 'default'} + /> + )} + + + {m.description} + + + {m.platform && } + + {m.tags.slice(0, 4).map((t) => ( + + ))} + + {m.tasks.length > 0 && ( + + Tasks: {m.tasks.slice(0, 5).join(', ')} + {m.tasks.length > 5 && ` (+${m.tasks.length - 5} more)`} + + )} + + + ))} + + + )} + + {existingRequests.length > 0 && ( + + + Open Requests ({existingRequests.length}) + + + These open issues may be requesting the same thing. Consider + commenting on one instead of filing a duplicate. + + + {existingRequests.map((r) => ( + + + + #{r.number} — {r.title} + + } + secondary={r.created_at ? `Created ${r.created_at}` : undefined} + /> + + ))} + + + )} + + {matches.length === 0 && existingRequests.length === 0 && ( + + No existing coverage found + This looks like a new need. Let's define the requirements so the + team can build it. + + )} + + + + + + + )} + + {/* ─── Step 2: Specify ─── */} + {activeStep === 2 && ( + + + Define the CodeBundle + + + + setBundleName(e.target.value)} + helperText="Convention: {platform}-{resource}-{purpose}" + /> + setPlatform(v)} + sx={{ minWidth: 200 }} + renderInput={(params) => } + /> + + + setPurpose(e.target.value)} + sx={{ mb: 3 }} + /> + + {/* Tasks */} + + Tasks (what should this bundle check or do?) + + {tasks.map((t, i) => ( + + updateTask(i, 'name', e.target.value)} + sx={{ flex: 1 }} + /> + updateTask(i, 'checks', e.target.value)} + sx={{ flex: 2 }} + /> + {tasks.length > 1 && ( + removeTask(i)}> + )} + + ))} + + + + + setResourceTypes(e.target.value)} + helperText="Comma-separated Kubernetes/cloud resource types" + sx={{ mb: 3 }} + /> + + setToolsRequired(e.target.value)} + helperText="Comma-separated" + sx={{ mb: 3 }} + /> + + {/* Env Vars */} + + Environment Variables (user-configurable inputs) + + {envVars.map((v, i) => ( + + updateEnvVar(i, 'name', e.target.value)} sx={{ flex: 1 }} /> + updateEnvVar(i, 'description', e.target.value)} sx={{ flex: 2 }} /> + updateEnvVar(i, 'example', e.target.value)} sx={{ flex: 1 }} /> + removeEnvVar(i)}> + + ))} + + + {/* Secrets */} + + Secrets (credentials needed) + + {secrets.map((s, i) => ( + + updateSecret(i, 'name', e.target.value)} sx={{ flex: 1 }} /> + updateSecret(i, 'description', e.target.value)} sx={{ flex: 2 }} /> + removeSecret(i)}> + + ))} + + + + + setTargetCollection(e.target.value)} + helperText="Which CodeCollection should this bundle live in?" + sx={{ mb: 3 }} + /> + + + + + + + )} + + {/* ─── Step 3: Review & Submit ─── */} + {activeStep === 3 && ( + + {submitResult ? ( + + + + Request Submitted + + + Issue #{submitResult.number} has been created in codebundle-farm. + The team will review your Design Spec and begin implementation. + + + + + ) : ( + + + Review Your Request + + + + + CodeBundle Name + {bundleName} + + Platform + {platform || 'Not specified'} + + Purpose + {purpose} + + Tasks + + {tasks.filter((t) => t.name || t.checks).map((t, i) => ( + + + + ))} + + + {resourceTypes && ( + <> + Resource Types + + {resourceTypes.split(',').filter(Boolean).map((r) => ( + + ))} + + + )} + + {envVars.filter((v) => v.name).length > 0 && ( + <> + Environment Variables + {envVars.filter((v) => v.name).map((v, i) => ( + + {v.name} — {v.description} {v.example && `(e.g., ${v.example})`} + + ))} + + )} + + {secrets.filter((s) => s.name).length > 0 && ( + <> + Secrets + {secrets.filter((s) => s.name).map((s, i) => ( + + {s.name} — {s.description} + + ))} + + )} + + Target Collection + {targetCollection} + + + + setContactEmail(e.target.value)} + helperText="If you'd like to be notified when the bundle is ready" + sx={{ mb: 3 }} + /> + + + + + + + )} + + )} + + ); +} diff --git a/cc-registry-v2/frontend/src/services/api.ts b/cc-registry-v2/frontend/src/services/api.ts index 773bd7f56646..6b824f24793c 100644 --- a/cc-registry-v2/frontend/src/services/api.ts +++ b/cc-registry-v2/frontend/src/services/api.ts @@ -1031,6 +1031,77 @@ export const chatApi = { } }; +// ============================================================================= +// Intake Wizard API +// ============================================================================= + +export interface IntakeSearchMatch { + display_name: string; + slug: string; + collection_slug: string; + platform: string; + description: string; + tasks: string[]; + tags: string[]; + relevance_score: number; + source_url: string; +} + +export interface IntakeExistingRequest { + number: number; + title: string; + url: string; + created_at: string; +} + +export interface IntakeSearchResponse { + matches: IntakeSearchMatch[]; + existing_requests: IntakeExistingRequest[]; + suggested_platform: string; + query_used: string; +} + +export interface IntakeDesignSpec { + codebundle_name: string; + target_collection: string; + platform: string; + purpose: string; + tasks: Array<{ name: string; checks: string }>; + resource_types: string[]; + env_vars: Array<{ name: string; description: string; example: string }>; + secrets: Array<{ name: string; description: string }>; + tools_required: string[]; + related_bundles: string[]; + user_description: string; + coverage_notes: string; +} + +export interface IntakeSubmitResponse { + issue_url: string; + issue_number: number; + message: string; +} + +export const intakeApi = { + async getPlatforms(): Promise<{ platforms: string[] }> { + const response = await api.get('/intake/platforms'); + return response.data; + }, + + async search(description: string, platform?: string): Promise { + const response = await api.post('/intake/search', { description, platform }); + return response.data; + }, + + async submit(designSpec: IntakeDesignSpec, contactEmail?: string): Promise { + const response = await api.post('/intake/submit', { + design_spec: designSpec, + contact_email: contactEmail, + }); + return response.data; + }, +}; + export const githubApi = { // Get issue template for a query async getIssueTemplate(userQuery: string): Promise { From e0419d759be733e533a5871a2216830e8a8361ae Mon Sep 17 00:00:00 2001 From: stewartshea Date: Thu, 5 Mar 2026 17:20:50 -0500 Subject: [PATCH 2/5] Refactor Intake Submission and Chat Integration - Updated the SubmitRequest model to include title, description, and additional context for improved intake submissions. - Enhanced the submit_intake function to generate GitHub issues with minimal required fields and include search results for better context. - Introduced a new chat feature in the Header component, allowing users to access the Registry Chat easily. - Simplified the IntakeWizard component by removing unnecessary steps and integrating direct navigation from the chat to the intake process. - Refactored the API service to accommodate the new submission structure, ensuring seamless communication with the backend. --- cc-registry-v2/backend/app/routers/intake.py | 109 ++- .../frontend/src/components/Header.tsx | 35 +- cc-registry-v2/frontend/src/pages/Chat.tsx | 124 +-- .../frontend/src/pages/IntakeWizard.tsx | 785 ++++++------------ cc-registry-v2/frontend/src/services/api.ts | 28 +- 5 files changed, 367 insertions(+), 714 deletions(-) diff --git a/cc-registry-v2/backend/app/routers/intake.py b/cc-registry-v2/backend/app/routers/intake.py index a8bde6e57619..6b3b26f137bf 100644 --- a/cc-registry-v2/backend/app/routers/intake.py +++ b/cc-registry-v2/backend/app/routers/intake.py @@ -64,12 +64,12 @@ class SearchResponse(BaseModel): class DesignSpecDraft(BaseModel): - """Step 4–5: The structured Design Spec generated from user answers.""" + """Legacy: structured Design Spec. Kept for backwards compat; minimal intake uses SubmitRequest.""" codebundle_name: str target_collection: str = "rw-cli-codecollection" - platform: str + platform: str = "" purpose: str - tasks: List[Dict[str, str]] + tasks: List[Dict[str, str]] = [] resource_types: List[str] = [] env_vars: List[Dict[str, str]] = [] secrets: List[Dict[str, str]] = [] @@ -80,9 +80,14 @@ class DesignSpecDraft(BaseModel): class SubmitRequest(BaseModel): - """Step 6: Submit the Design Spec as a GitHub Issue.""" - design_spec: DesignSpecDraft + """Minimal intake: title + description required. Search results included for the designer.""" + title: str + description: str + extra_context: Optional[str] = None contact_email: Optional[str] = None + contact_ok: Optional[bool] = False + matches: List[SearchMatch] = [] + existing_requests: List[ExistingRequest] = [] class SubmitResponse(BaseModel): @@ -181,8 +186,9 @@ async def generate_design_spec( @router.post("/submit", response_model=SubmitResponse) async def submit_intake(req: SubmitRequest): """ - Create a GitHub Issue in codebundle-farm with the Design Spec - and label it for the Architect pipeline. + Create a GitHub Issue in codebundle-farm. Requires only title + description. + Search results (matches, existing_requests) are included in the issue body + so the designer can see existing coverage and avoid duplication. """ if settings.GITHUB_TOKEN == "your_github_token_here": raise HTTPException( @@ -190,16 +196,12 @@ async def submit_intake(req: SubmitRequest): detail="GitHub integration not configured. Set GITHUB_TOKEN.", ) - spec = req.design_spec - title = f"[intake] {spec.codebundle_name}: {spec.purpose[:80]}" - - body = _build_issue_body(spec, req.contact_email) - - labels = [ - "intake", - "needs-architect", - f"platform:{spec.platform.lower()}", - ] + title = f"[intake] {req.title[:100]}" + body = _build_minimal_issue_body(req) + platform = _infer_platform(req.description) + labels = ["intake", "needs-architect"] + if platform: + labels.append(f"platform:{platform.lower()}") try: api_url = f"https://api.github.com/repos/{CODEBUNDLE_FARM_REPO}/issues" @@ -236,6 +238,79 @@ async def submit_intake(req: SubmitRequest): # Helpers # ============================================================================= + +def _build_minimal_issue_body(req: SubmitRequest) -> str: + """Build issue body with original request + full existing coverage details for the designer.""" + parts = [ + "## Request", + "", + f"**Title:** {req.title}", + "", + "**Description:**", + "", + f"> {req.description}", + "", + ] + + if req.extra_context: + parts.extend([ + "**Additional context:**", + "", + req.extra_context, + "", + ]) + + # Existing CodeBundles found by search — full details for the designer + if req.matches: + parts.extend([ + "---", + "## Existing Coverage (from registry search)", + "", + "The following CodeBundles may overlap with this request. Designer: consider reusing, extending, or differentiating.", + "", + ]) + for i, m in enumerate(req.matches, 1): + score_str = f" ({int(m.relevance_score * 100)}% match)" if m.relevance_score > 0 else "" + parts.append(f"### {i}. {m.display_name}{score_str}") + parts.append("") + parts.append(f"- **Collection:** `{m.collection_slug}`") + parts.append(f"- **Platform:** {m.platform or '—'}") + parts.append(f"- **Description:** {m.description[:500]}{'…' if len(m.description) > 500 else ''}") + if m.tasks: + parts.append(f"- **Tasks:** {', '.join(m.tasks[:8])}{'…' if len(m.tasks) > 8 else ''}") + if m.tags: + parts.append(f"- **Tags:** {', '.join(m.tags[:10])}") + if m.source_url: + parts.append(f"- **Link:** {m.source_url}") + parts.append("") + + # Existing open requests — designer can consolidate + if req.existing_requests: + parts.extend([ + "---", + "## Open Requests (may overlap)", + "", + "Consider commenting on an existing issue instead of duplicating work.", + "", + ]) + for r in req.existing_requests: + parts.append(f"- [#{r.number} {r.title}]({r.url})") + parts.append("") + + if req.contact_email: + parts.append(f"**Contact:** {req.contact_email}") + parts.append("") + if req.contact_ok: + parts.append("**Contact OK:** Yes, please reach out.") + parts.append("") + + parts.extend([ + "---", + f"*Created via CodeCollection Registry intake at {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}.*", + ]) + return "\n".join(parts) + + def _build_issue_body(spec: DesignSpecDraft, contact_email: Optional[str]) -> str: """Build the GitHub Issue body with the Design Spec in YAML.""" tasks_yaml = "" diff --git a/cc-registry-v2/frontend/src/components/Header.tsx b/cc-registry-v2/frontend/src/components/Header.tsx index 290b03769294..8bef9b33e560 100644 --- a/cc-registry-v2/frontend/src/components/Header.tsx +++ b/cc-registry-v2/frontend/src/components/Header.tsx @@ -9,6 +9,7 @@ import { Menu, MenuItem, Divider, + Tooltip, } from '@mui/material'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import { @@ -17,6 +18,7 @@ import { MoreVert as MoreVertIcon, DarkMode as DarkModeIcon, LightMode as LightModeIcon, + SmartToy as SmartToyIcon, } from '@mui/icons-material'; import { useCart } from '../contexts/CartContext'; import { useAuth } from '../contexts/AuthContext'; @@ -124,6 +126,27 @@ const Header: React.FC = () => { CodeCollection Registry + + + + + + @@ -216,18 +239,6 @@ const Header: React.FC = () => { - - - - - ); }; diff --git a/cc-registry-v2/frontend/src/pages/IntakeWizard.tsx b/cc-registry-v2/frontend/src/pages/IntakeWizard.tsx index 803384c5e115..08611da262e5 100644 --- a/cc-registry-v2/frontend/src/pages/IntakeWizard.tsx +++ b/cc-registry-v2/frontend/src/pages/IntakeWizard.tsx @@ -1,179 +1,134 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; import { - Box, Container, Typography, Stepper, Step, StepLabel, - TextField, Button, Paper, Chip, Card, CardContent, - CircularProgress, Alert, AlertTitle, IconButton, - Autocomplete, List, ListItem, ListItemText, ListItemIcon, - Divider, Link, Collapse, + Box, Container, Typography, TextField, Button, Paper, + CircularProgress, Alert, Collapse, FormControlLabel, Checkbox, + ToggleButton, ToggleButtonGroup, } from '@mui/material'; import { - Search as SearchIcon, - Add as AddIcon, - Delete as DeleteIcon, CheckCircle as CheckIcon, - Warning as WarningIcon, OpenInNew as ExternalLinkIcon, ExpandMore as ExpandMoreIcon, ExpandLess as ExpandLessIcon, RocketLaunch as RocketIcon, + Chat as ChatIcon, + Tune as TuneIcon, } from '@mui/icons-material'; -import { - intakeApi, - IntakeSearchMatch, - IntakeExistingRequest, - IntakeDesignSpec, -} from '../services/api'; - -const STEPS = ['Describe', 'Search Results', 'Specify', 'Review & Submit']; - -interface TaskEntry { - name: string; - checks: string; -} - -interface EnvVarEntry { - name: string; - description: string; - example: string; -} +import { intakeApi, IntakeSearchMatch, IntakeExistingRequest } from '../services/api'; -interface SecretEntry { - name: string; - description: string; -} +type Mode = 'simple' | 'explicit'; export default function IntakeWizard() { - const [activeStep, setActiveStep] = useState(0); + const location = useLocation(); + const [mode, setMode] = useState('simple'); - // Step 1: Describe - const [description, setDescription] = useState(''); + // Core answers (question-driven) + const [problemDescription, setProblemDescription] = useState(''); const [platform, setPlatform] = useState(''); - const [platforms, setPlatforms] = useState([]); - - // Step 2: Search results - const [searching, setSearching] = useState(false); - const [matches, setMatches] = useState([]); - const [existingRequests, setExistingRequests] = useState([]); - const [suggestedPlatform, setSuggestedPlatform] = useState(''); - const [matchesExpanded, setMatchesExpanded] = useState(true); + const [healthyLooksLike, setHealthyLooksLike] = useState(''); + const [anythingElse, setAnythingElse] = useState(''); - // Step 3: Specify - const [bundleName, setBundleName] = useState(''); - const [purpose, setPurpose] = useState(''); - const [tasks, setTasks] = useState([{ name: '', checks: '' }]); - const [resourceTypes, setResourceTypes] = useState(''); - const [envVars, setEnvVars] = useState([]); - const [secrets, setSecrets] = useState([]); - const [toolsRequired, setToolsRequired] = useState(''); - const [targetCollection, setTargetCollection] = useState('rw-cli-codecollection'); + // Explicit mode only + const [explicitTasks, setExplicitTasks] = useState(''); + const [explicitVariables, setExplicitVariables] = useState(''); - // Step 4: Submit + // Meta const [contactEmail, setContactEmail] = useState(''); - const [coverageNotes, setCoverageNotes] = useState(''); + const [contactOk, setContactOk] = useState(false); + const [showContact, setShowContact] = useState(false); + const [submitting, setSubmitting] = useState(false); const [submitResult, setSubmitResult] = useState<{ url: string; number: number } | null>(null); const [error, setError] = useState(''); + // Pre-fill from Chat "Request CodeBundle" or other entry points useEffect(() => { - intakeApi.getPlatforms().then((res) => setPlatforms(res.platforms)).catch(() => {}); - }, []); - - // Step 1 → 2: Search - const handleSearch = useCallback(async () => { - setSearching(true); - setError(''); - try { - const res = await intakeApi.search(description, platform || undefined); - setMatches(res.matches); - setExistingRequests(res.existing_requests); - if (res.suggested_platform && !platform) { - setSuggestedPlatform(res.suggested_platform); - setPlatform(res.suggested_platform); - } - setPurpose(description); - setActiveStep(1); - } catch (err: any) { - setError(err?.response?.data?.detail || 'Search failed. You can skip to specify details manually.'); - setActiveStep(1); - } finally { - setSearching(false); + const state = location.state as { initialQuery?: string } | null; + if (state?.initialQuery?.trim()) { + setProblemDescription(state.initialQuery.trim()); } - }, [description, platform]); + }, [location.state]); - // Step 2 → 3: Nothing found, proceed to specify - const handleProceedToSpecify = () => { - const notes = matches.length > 0 - ? `Searched and found ${matches.length} partially related bundle(s), but user indicated none fully solve the need.` - : 'No existing CodeBundles found for this request.'; - setCoverageNotes(notes); - - if (!bundleName) { - const prefix = platform ? platform.toLowerCase().replace(/\s+/g, '-') : 'generic'; - const words = description.toLowerCase().split(/\s+/).filter((w) => w.length > 2).slice(0, 3); - setBundleName(`${prefix}-${words.join('-') || 'healthcheck'}`); - } - setActiveStep(2); + const buildTitle = () => { + const firstLine = problemDescription.split('\n')[0].trim(); + return firstLine.length > 60 ? firstLine.slice(0, 57) + '...' : firstLine || 'CodeBundle request'; }; - // Step 3 → 4: Move to review - const handleReview = () => { - setActiveStep(3); + const buildDescription = () => { + const parts: string[] = [problemDescription]; + if (platform.trim()) parts.push(`\n**Platform:** ${platform.trim()}`); + if (healthyLooksLike.trim()) parts.push(`\n**What healthy looks like:** ${healthyLooksLike.trim()}`); + if (anythingElse.trim()) parts.push(`\n**Additional context:** ${anythingElse.trim()}`); + if (mode === 'explicit' && (explicitTasks.trim() || explicitVariables.trim())) { + if (explicitTasks.trim()) parts.push(`\n**Suggested tasks:**\n${explicitTasks.trim()}`); + if (explicitVariables.trim()) parts.push(`\n**Variables/config:**\n${explicitVariables.trim()}`); + } + return parts.join('\n'); }; - // Step 4: Submit const handleSubmit = async () => { setSubmitting(true); setError(''); try { - const spec: IntakeDesignSpec = { - codebundle_name: bundleName, - target_collection: targetCollection, - platform, - purpose, - tasks: tasks.filter((t) => t.name || t.checks), - resource_types: resourceTypes.split(',').map((s) => s.trim()).filter(Boolean), - env_vars: envVars.filter((v) => v.name), - secrets: secrets.filter((s) => s.name), - tools_required: toolsRequired.split(',').map((s) => s.trim()).filter(Boolean), - related_bundles: matches.slice(0, 3).map((m) => `${m.collection_slug}/${m.slug}`), - user_description: description, - coverage_notes: coverageNotes, - }; - const res = await intakeApi.submit(spec, contactEmail || undefined); + const description = buildDescription(); + let matches: IntakeSearchMatch[] = []; + let existingRequests: IntakeExistingRequest[] = []; + try { + const searchRes = await intakeApi.search(description); + matches = searchRes.matches; + existingRequests = searchRes.existing_requests; + } catch { + // Search failed — continue + } + + const res = await intakeApi.submit({ + title: buildTitle(), + description, + extra_context: undefined, + contact_email: contactEmail.trim() || undefined, + contact_ok: contactOk, + matches, + existing_requests: existingRequests, + }); setSubmitResult({ url: res.issue_url, number: res.issue_number }); } catch (err: any) { - setError(err?.response?.data?.detail || 'Failed to create issue. Please try again.'); + setError(err?.response?.data?.detail || 'Failed to create request. Please try again.'); } finally { setSubmitting(false); } }; - // Task list helpers - const addTask = () => setTasks([...tasks, { name: '', checks: '' }]); - const removeTask = (i: number) => setTasks(tasks.filter((_, idx) => idx !== i)); - const updateTask = (i: number, field: keyof TaskEntry, value: string) => { - const updated = [...tasks]; - updated[i] = { ...updated[i], [field]: value }; - setTasks(updated); - }; - - // Env var helpers - const addEnvVar = () => setEnvVars([...envVars, { name: '', description: '', example: '' }]); - const removeEnvVar = (i: number) => setEnvVars(envVars.filter((_, idx) => idx !== i)); - const updateEnvVar = (i: number, field: keyof EnvVarEntry, value: string) => { - const updated = [...envVars]; - updated[i] = { ...updated[i], [field]: value }; - setEnvVars(updated); - }; + const canSubmit = problemDescription.trim().length > 0; - // Secret helpers - const addSecret = () => setSecrets([...secrets, { name: '', description: '' }]); - const removeSecret = (i: number) => setSecrets(secrets.filter((_, idx) => idx !== i)); - const updateSecret = (i: number, field: keyof SecretEntry, value: string) => { - const updated = [...secrets]; - updated[i] = { ...updated[i], [field]: value }; - setSecrets(updated); - }; + if (submitResult) { + return ( + + + + + Request Submitted + + + Issue #{submitResult.number} has been created. The designer will review + your request and any existing coverage we found. + + + + + + ); + } return ( @@ -182,444 +137,174 @@ export default function IntakeWizard() { Request a CodeBundle - Describe what you need automated, and we'll check existing coverage - before creating a structured request. + Describe the problem you're solving. The designer will figure out the rest. - - {STEPS.map((label) => ( - - {label} - - ))} - - {error && ( - setError('')}> + setError('')}> {error} )} - {/* ─── Step 0: Describe ─── */} - {activeStep === 0 && ( - - - What do you need automated? + + {/* Mode toggle */} + + v && setMode(v)} + size="small" + > + + + Simple + + + + Explicit + + + + {mode === 'simple' + ? 'Describe naturally — the designer has autonomy' + : 'Specify tasks and variables if you know them'} - - Describe the infrastructure task, health check, or troubleshooting - scenario you need. Be as specific as possible — include the platform, - resource types, and what "healthy" looks like. - - - setDescription(e.target.value)} - sx={{ mb: 3 }} - /> - - setPlatform(v)} - renderInput={(params) => ( - - )} - sx={{ mb: 3 }} - /> - - - - - - )} - - {/* ─── Step 1: Search Results ─── */} - {activeStep === 1 && ( - - {matches.length > 0 && ( - - - - Existing CodeBundles ({matches.length}) - - setMatchesExpanded(!matchesExpanded)}> - {matchesExpanded ? : } - - - - We found these existing bundles that may address your need. - If one solves your problem, you're all set! - - - - {matches.map((m, i) => ( - - - - - {m.display_name} - - {m.relevance_score > 0 && ( - 0.7 ? 'success' : 'default'} - /> - )} - - - {m.description} - - - {m.platform && } - - {m.tags.slice(0, 4).map((t) => ( - - ))} - - {m.tasks.length > 0 && ( - - Tasks: {m.tasks.slice(0, 5).join(', ')} - {m.tasks.length > 5 && ` (+${m.tasks.length - 5} more)`} - - )} - - - ))} - - - )} - - {existingRequests.length > 0 && ( - - - Open Requests ({existingRequests.length}) - - - These open issues may be requesting the same thing. Consider - commenting on one instead of filing a duplicate. - - - {existingRequests.map((r) => ( - - - - #{r.number} — {r.title} - - } - secondary={r.created_at ? `Created ${r.created_at}` : undefined} - /> - - ))} - - - )} - - {matches.length === 0 && existingRequests.length === 0 && ( - - No existing coverage found - This looks like a new need. Let's define the requirements so the - team can build it. - - )} - - - - - - )} - - {/* ─── Step 2: Specify ─── */} - {activeStep === 2 && ( - - - Define the CodeBundle - - + {/* Question 1: Core */} + + What problem are you trying to solve? + + + Describe the infrastructure task, health check, or troubleshooting scenario in your own words. + + setProblemDescription(e.target.value)} + sx={{ mb: 3 }} + /> + + {/* Question 2: Platform */} + + What platform or infrastructure does this involve? + + + Optional — we can often infer this from your description. + + setPlatform(e.target.value)} + sx={{ mb: 3 }} + /> + + {/* Question 3: Healthy */} + + What does "healthy" or "working" look like? How would you know something is wrong? + + + Optional — helps the designer scope the checks. + + setHealthyLooksLike(e.target.value)} + sx={{ mb: 3 }} + /> + + {/* Question 4: Anything else */} + + Anything else the designer should know? + + setAnythingElse(e.target.value)} + sx={{ mb: 3 }} + /> + + {/* Explicit mode: tasks and variables */} + + + + Only fill these if you already know the structure. Otherwise leave blank. + setBundleName(e.target.value)} - helperText="Convention: {platform}-{resource}-{purpose}" + multiline + minRows={2} + label="Suggested tasks (one per line)" + placeholder="Check for failed CronJobs\nCheck for suspended CronJobs\n..." + value={explicitTasks} + onChange={(e) => setExplicitTasks(e.target.value)} + sx={{ mb: 2 }} /> - setPlatform(v)} - sx={{ minWidth: 200 }} - renderInput={(params) => } + setExplicitVariables(e.target.value)} /> - - setPurpose(e.target.value)} - sx={{ mb: 3 }} - /> - - {/* Tasks */} - - Tasks (what should this bundle check or do?) - - {tasks.map((t, i) => ( - - updateTask(i, 'name', e.target.value)} - sx={{ flex: 1 }} - /> - updateTask(i, 'checks', e.target.value)} - sx={{ flex: 2 }} - /> - {tasks.length > 1 && ( - removeTask(i)}> - )} - - ))} - - - - - setResourceTypes(e.target.value)} - helperText="Comma-separated Kubernetes/cloud resource types" - sx={{ mb: 3 }} - /> - - setToolsRequired(e.target.value)} - helperText="Comma-separated" - sx={{ mb: 3 }} - /> - - {/* Env Vars */} - - Environment Variables (user-configurable inputs) - - {envVars.map((v, i) => ( - - updateEnvVar(i, 'name', e.target.value)} sx={{ flex: 1 }} /> - updateEnvVar(i, 'description', e.target.value)} sx={{ flex: 2 }} /> - updateEnvVar(i, 'example', e.target.value)} sx={{ flex: 1 }} /> - removeEnvVar(i)}> - - ))} - - - {/* Secrets */} - - Secrets (credentials needed) - - {secrets.map((s, i) => ( - - updateSecret(i, 'name', e.target.value)} sx={{ flex: 1 }} /> - updateSecret(i, 'description', e.target.value)} sx={{ flex: 2 }} /> - removeSecret(i)}> - - ))} - - - - - setTargetCollection(e.target.value)} - helperText="Which CodeCollection should this bundle live in?" - sx={{ mb: 3 }} - /> - - - - - - - )} - - {/* ─── Step 3: Review & Submit ─── */} - {activeStep === 3 && ( - - {submitResult ? ( - - - - Request Submitted - - - Issue #{submitResult.number} has been created in codebundle-farm. - The team will review your Design Spec and begin implementation. - - - - - ) : ( - - - Review Your Request - - - - - CodeBundle Name - {bundleName} - - Platform - {platform || 'Not specified'} - - Purpose - {purpose} - - Tasks - - {tasks.filter((t) => t.name || t.checks).map((t, i) => ( - - - - ))} - - - {resourceTypes && ( - <> - Resource Types - - {resourceTypes.split(',').filter(Boolean).map((r) => ( - - ))} - - - )} - - {envVars.filter((v) => v.name).length > 0 && ( - <> - Environment Variables - {envVars.filter((v) => v.name).map((v, i) => ( - - {v.name} — {v.description} {v.example && `(e.g., ${v.example})`} - - ))} - - )} - - {secrets.filter((s) => s.name).length > 0 && ( - <> - Secrets - {secrets.filter((s) => s.name).map((s, i) => ( - - {s.name} — {s.description} - - ))} - - )} - - Target Collection - {targetCollection} - - - + + setContactEmail(e.target.value)} - helperText="If you'd like to be notified when the bundle is ready" - sx={{ mb: 3 }} + sx={{ mb: 1 }} /> - - - - - - - )} + setContactOk(e.target.checked)} />} + label="It's OK to reach out for clarification" + /> + + - )} + + + + We search existing CodeBundles first, then create your request with the results attached for the designer. + + ); } diff --git a/cc-registry-v2/frontend/src/services/api.ts b/cc-registry-v2/frontend/src/services/api.ts index 6b824f24793c..39b8fa85f2ac 100644 --- a/cc-registry-v2/frontend/src/services/api.ts +++ b/cc-registry-v2/frontend/src/services/api.ts @@ -1061,19 +1061,14 @@ export interface IntakeSearchResponse { query_used: string; } -export interface IntakeDesignSpec { - codebundle_name: string; - target_collection: string; - platform: string; - purpose: string; - tasks: Array<{ name: string; checks: string }>; - resource_types: string[]; - env_vars: Array<{ name: string; description: string; example: string }>; - secrets: Array<{ name: string; description: string }>; - tools_required: string[]; - related_bundles: string[]; - user_description: string; - coverage_notes: string; +export interface IntakeSubmitRequest { + title: string; + description: string; + extra_context?: string; + contact_email?: string; + contact_ok?: boolean; + matches: IntakeSearchMatch[]; + existing_requests: IntakeExistingRequest[]; } export interface IntakeSubmitResponse { @@ -1093,11 +1088,8 @@ export const intakeApi = { return response.data; }, - async submit(designSpec: IntakeDesignSpec, contactEmail?: string): Promise { - const response = await api.post('/intake/submit', { - design_spec: designSpec, - contact_email: contactEmail, - }); + async submit(payload: IntakeSubmitRequest): Promise { + const response = await api.post('/intake/submit', payload); return response.data; }, }; From 334471fcd5c5617a172561c4979ed17fdb4bd9d3 Mon Sep 17 00:00:00 2001 From: stewartshea Date: Thu, 5 Mar 2026 17:45:34 -0500 Subject: [PATCH 3/5] Update Header Component to Enhance Chat Feature - Replaced the SmartToy icon with ChatBubbleOutline for better representation of the chat feature. - Refactored the chat access point in the Header component from an IconButton to a Button with a tooltip, improving usability and accessibility. - Adjusted styling for the chat button to enhance visibility and user experience when navigating to the chat interface. --- .../frontend/src/components/Header.tsx | 40 ++++++++----------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/cc-registry-v2/frontend/src/components/Header.tsx b/cc-registry-v2/frontend/src/components/Header.tsx index 8bef9b33e560..9e62ce0249e7 100644 --- a/cc-registry-v2/frontend/src/components/Header.tsx +++ b/cc-registry-v2/frontend/src/components/Header.tsx @@ -18,7 +18,7 @@ import { MoreVert as MoreVertIcon, DarkMode as DarkModeIcon, LightMode as LightModeIcon, - SmartToy as SmartToyIcon, + ChatBubbleOutline as ChatIcon, } from '@mui/icons-material'; import { useCart } from '../contexts/CartContext'; import { useAuth } from '../contexts/AuthContext'; @@ -126,30 +126,24 @@ const Header: React.FC = () => { CodeCollection Registry - - - - - - - + + + + + {/* Browse Dropdown */}