diff --git a/.github/workflows/deploy-chat-backend.yml b/.github/workflows/deploy-chat-backend.yml new file mode 100644 index 0000000..b39d29b --- /dev/null +++ b/.github/workflows/deploy-chat-backend.yml @@ -0,0 +1,72 @@ +name: deploy-chat-backend +run-name: Deploy chat backend to Cloud Run + +on: + push: + branches: + - main + paths: + - 'chat-backend/**' + workflow_dispatch: + +env: + PROJECT_ID: chipflow-platform + REGION: us-central1 + SERVICE_NAME: chipflow-docs-chat + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # Required for Workload Identity Federation + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Authenticate to Google Cloud + id: auth + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} + + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v2 + + - name: Configure Docker for GCR + run: gcloud auth configure-docker --quiet + + - name: Build and push container + working-directory: chat-backend + run: | + docker build -t gcr.io/$PROJECT_ID/$SERVICE_NAME:${{ github.sha }} \ + -t gcr.io/$PROJECT_ID/$SERVICE_NAME:latest . + docker push gcr.io/$PROJECT_ID/$SERVICE_NAME:${{ github.sha }} + docker push gcr.io/$PROJECT_ID/$SERVICE_NAME:latest + + - name: Deploy to Cloud Run + run: | + gcloud run deploy $SERVICE_NAME \ + --image gcr.io/$PROJECT_ID/$SERVICE_NAME:${{ github.sha }} \ + --region $REGION \ + --platform managed \ + --allow-unauthenticated \ + --memory 1Gi \ + --cpu 1 \ + --min-instances 0 \ + --max-instances 3 \ + --set-env-vars "DOCS_URL=https://docs.chipflow.io/llms-full.txt,GCP_PROJECT=$PROJECT_ID,GCP_LOCATION=$REGION" + + - name: Show service URL + run: | + URL=$(gcloud run services describe $SERVICE_NAME --region $REGION --format='value(status.url)') + echo "## Deployed to Cloud Run" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Service URL: $URL" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Update \`chat-widget.js\` with:" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`javascript" >> $GITHUB_STEP_SUMMARY + echo "apiUrl: '${URL}/api/chat'" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index e572595..71d4991 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ docs/source/* !docs/source/support.rst !docs/source/platform-api.rst !docs/source/tutorial-intro-chipflow-platform.rst +!docs/source/_static/ # Misc log diff --git a/chat-backend/Dockerfile b/chat-backend/Dockerfile new file mode 100644 index 0000000..e8af082 --- /dev/null +++ b/chat-backend/Dockerfile @@ -0,0 +1,27 @@ +# Use official Python runtime as base image +FROM python:3.12-slim + +# Set working directory +WORKDIR /app + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PORT=8080 + +# Install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY main.py . + +# Create non-root user for security +RUN useradd --create-home appuser && chown -R appuser:appuser /app +USER appuser + +# Expose port +EXPOSE 8080 + +# Run the application +CMD ["python", "main.py"] diff --git a/chat-backend/README.md b/chat-backend/README.md new file mode 100644 index 0000000..c6e730e --- /dev/null +++ b/chat-backend/README.md @@ -0,0 +1,180 @@ +# ChipFlow Docs Chat Backend + +A FastAPI backend that provides AI-powered Q&A for ChipFlow documentation using Vertex AI. + +## Architecture + +- **FastAPI** - Web framework with async support +- **Vertex AI** - Embeddings (text-embedding-005) and LLM (Gemini 1.5 Flash) +- **In-memory RAG** - Simple vector search using numpy +- **Cloud Run** - Serverless deployment + +## How it Works + +1. On startup, fetches `llms-full.txt` from the docs site +2. Chunks the documentation into overlapping segments +3. Generates embeddings for each chunk using Vertex AI +4. When a question arrives: + - Generates query embedding + - Finds most similar chunks via cosine similarity + - Sends relevant context + question to Gemini + - Returns the response + +## Local Development + +### Prerequisites + +- Python 3.12+ +- Google Cloud SDK with authentication configured +- Access to a GCP project with Vertex AI enabled + +### Setup + +```bash +cd chat-backend + +# Create virtual environment +python -m venv venv +source venv/bin/activate + +# Install dependencies +pip install -r requirements.txt + +# Set environment variables +export GCP_PROJECT=your-project-id +export GCP_LOCATION=us-central1 +export DOCS_URL=https://docs.chipflow.io/llms-full.txt + +# Run locally +python main.py +``` + +The server will start at http://localhost:8080 + +### Test the API + +```bash +# Health check +curl http://localhost:8080/health + +# Ask a question +curl -X POST http://localhost:8080/api/chat \ + -H "Content-Type: application/json" \ + -d '{"question": "What is Amaranth?"}' +``` + +## Deployment to Cloud Run + +### Prerequisites + +1. GCP project with billing enabled +2. Enable required APIs: + ```bash + gcloud services enable \ + cloudbuild.googleapis.com \ + run.googleapis.com \ + aiplatform.googleapis.com \ + containerregistry.googleapis.com + ``` + +3. Grant Cloud Run service account Vertex AI access: + ```bash + PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)') + gcloud projects add-iam-policy-binding $PROJECT_ID \ + --member="serviceAccount:$PROJECT_NUMBER-compute@developer.gserviceaccount.com" \ + --role="roles/aiplatform.user" + ``` + +### Deploy with Cloud Build + +```bash +cd chat-backend + +# Deploy using Cloud Build +gcloud builds submit --config cloudbuild.yaml + +# Or manually build and deploy +gcloud builds submit --tag gcr.io/$PROJECT_ID/chipflow-docs-chat +gcloud run deploy chipflow-docs-chat \ + --image gcr.io/$PROJECT_ID/chipflow-docs-chat \ + --region us-central1 \ + --platform managed \ + --allow-unauthenticated \ + --memory 1Gi \ + --set-env-vars "DOCS_URL=https://docs.chipflow.io/llms-full.txt,GCP_PROJECT=$PROJECT_ID,GCP_LOCATION=us-central1" +``` + +### After Deployment + +1. Get the Cloud Run URL: + ```bash + gcloud run services describe chipflow-docs-chat --region us-central1 --format='value(status.url)' + ``` + +2. Update the chat widget in `docs/source/_static/js/chat-widget.js`: + ```javascript + const CONFIG = { + apiUrl: 'https://chipflow-docs-chat-xxxxx.a.run.app/api/chat', + // ... + }; + ``` + +## Configuration + +Environment variables: + +| Variable | Default | Description | +|----------|---------|-------------| +| `DOCS_URL` | `https://docs.chipflow.io/llms-full.txt` | URL to fetch documentation | +| `GCP_PROJECT` | `chipflow-docs` | Google Cloud project ID | +| `GCP_LOCATION` | `us-central1` | Vertex AI region | +| `PORT` | `8080` | Server port | + +## Cost Estimation + +Based on ~50 users with infrequent queries (~100 queries/day): + +- **Vertex AI Embeddings**: ~$0.01/1000 queries +- **Gemini 1.5 Flash**: ~$0.075/1M input tokens, $0.30/1M output tokens +- **Cloud Run**: Pay-per-use, scales to zero when idle + +Estimated monthly cost: **$5-20** (well under $100 budget) + +## API Reference + +### `GET /health` + +Health check endpoint. + +**Response:** +```json +{ + "status": "healthy", + "initialized": true, + "chunks": 150 +} +``` + +### `POST /api/chat` + +Ask a question about the documentation. + +**Request:** +```json +{ + "question": "How do I create an Amaranth module?", + "conversation_history": [ + {"role": "user", "content": "previous question"}, + {"role": "assistant", "content": "previous answer"} + ], + "page": "/amaranth/guide/basics.html" +} +``` + +**Response:** +```json +{ + "answer": "To create an Amaranth module...", + "sources": ["Getting Started", "Module Basics"] +} +``` diff --git a/chat-backend/cloudbuild.yaml b/chat-backend/cloudbuild.yaml new file mode 100644 index 0000000..a6a55ff --- /dev/null +++ b/chat-backend/cloudbuild.yaml @@ -0,0 +1,54 @@ +# Cloud Build configuration for deploying the chat backend to Cloud Run +steps: + # Build the container image + - name: 'gcr.io/cloud-builders/docker' + args: + - 'build' + - '-t' + - 'gcr.io/$PROJECT_ID/chipflow-docs-chat:$COMMIT_SHA' + - '-t' + - 'gcr.io/$PROJECT_ID/chipflow-docs-chat:latest' + - '.' + + # Push the container image to Container Registry + - name: 'gcr.io/cloud-builders/docker' + args: + - 'push' + - 'gcr.io/$PROJECT_ID/chipflow-docs-chat:$COMMIT_SHA' + + - name: 'gcr.io/cloud-builders/docker' + args: + - 'push' + - 'gcr.io/$PROJECT_ID/chipflow-docs-chat:latest' + + # Deploy to Cloud Run + - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk' + entrypoint: gcloud + args: + - 'run' + - 'deploy' + - 'chipflow-docs-chat' + - '--image' + - 'gcr.io/$PROJECT_ID/chipflow-docs-chat:$COMMIT_SHA' + - '--region' + - 'us-central1' + - '--platform' + - 'managed' + - '--allow-unauthenticated' + - '--memory' + - '1Gi' + - '--cpu' + - '1' + - '--min-instances' + - '0' + - '--max-instances' + - '3' + - '--set-env-vars' + - 'DOCS_URL=https://docs.chipflow.io/llms-full.txt,GCP_PROJECT=$PROJECT_ID,GCP_LOCATION=us-central1' + +images: + - 'gcr.io/$PROJECT_ID/chipflow-docs-chat:$COMMIT_SHA' + - 'gcr.io/$PROJECT_ID/chipflow-docs-chat:latest' + +options: + logging: CLOUD_LOGGING_ONLY diff --git a/chat-backend/main.py b/chat-backend/main.py new file mode 100644 index 0000000..d654ff6 --- /dev/null +++ b/chat-backend/main.py @@ -0,0 +1,285 @@ +""" +ChipFlow Documentation Chat API + +A FastAPI backend that provides AI-powered Q&A for ChipFlow documentation. +Uses Vertex AI for embeddings and LLM responses with simple in-memory RAG. +""" +import os +import json +import logging +from typing import Optional +from contextlib import asynccontextmanager + +import numpy as np +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +import httpx + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Configuration +DOCS_URL = os.getenv("DOCS_URL", "https://docs.chipflow.io/llms-full.txt") +GCP_PROJECT = os.getenv("GCP_PROJECT", "chipflow-docs") +GCP_LOCATION = os.getenv("GCP_LOCATION", "us-central1") +EMBEDDING_MODEL = "text-embedding-005" +LLM_MODEL = "gemini-1.5-flash-002" + +# Allowed origins for CORS +ALLOWED_ORIGINS = [ + "https://docs.chipflow.io", + "https://chipflow-docs.docs.chipflow-infra.com", + "http://localhost:8000", + "http://127.0.0.1:8000", +] + + +class ChatRequest(BaseModel): + question: str + conversation_history: list = [] + page: Optional[str] = None + + +class ChatResponse(BaseModel): + answer: str + sources: list = [] + + +class DocumentStore: + """Simple in-memory document store with vector search.""" + + def __init__(self): + self.chunks: list[dict] = [] + self.embeddings: Optional[np.ndarray] = None + self.initialized = False + + async def initialize(self, docs_url: str): + """Load and process documentation.""" + logger.info(f"Fetching documentation from {docs_url}") + + async with httpx.AsyncClient(timeout=60.0) as client: + response = await client.get(docs_url) + response.raise_for_status() + content = response.text + + # Split into chunks (by section headers or fixed size) + self.chunks = self._chunk_content(content) + logger.info(f"Created {len(self.chunks)} chunks") + + # Generate embeddings + self.embeddings = await self._generate_embeddings([c["text"] for c in self.chunks]) + self.initialized = True + logger.info("Document store initialized") + + def _chunk_content(self, content: str, chunk_size: int = 1500, overlap: int = 200) -> list[dict]: + """Split content into overlapping chunks.""" + chunks = [] + lines = content.split('\n') + current_chunk = [] + current_size = 0 + current_title = "Documentation" + + for line in lines: + # Track section headers + if line.startswith('# '): + current_title = line[2:].strip() + elif line.startswith('## '): + current_title = line[3:].strip() + + current_chunk.append(line) + current_size += len(line) + 1 + + if current_size >= chunk_size: + chunk_text = '\n'.join(current_chunk) + chunks.append({ + "text": chunk_text, + "title": current_title, + }) + + # Keep overlap + overlap_lines = [] + overlap_size = 0 + for l in reversed(current_chunk): + if overlap_size + len(l) > overlap: + break + overlap_lines.insert(0, l) + overlap_size += len(l) + 1 + + current_chunk = overlap_lines + current_size = overlap_size + + # Add remaining content + if current_chunk: + chunks.append({ + "text": '\n'.join(current_chunk), + "title": current_title, + }) + + return chunks + + async def _generate_embeddings(self, texts: list[str]) -> np.ndarray: + """Generate embeddings using Vertex AI.""" + from google.cloud import aiplatform + from vertexai.language_models import TextEmbeddingModel + + aiplatform.init(project=GCP_PROJECT, location=GCP_LOCATION) + model = TextEmbeddingModel.from_pretrained(EMBEDDING_MODEL) + + # Process in batches + all_embeddings = [] + batch_size = 5 + + for i in range(0, len(texts), batch_size): + batch = texts[i:i + batch_size] + embeddings = model.get_embeddings(batch) + all_embeddings.extend([e.values for e in embeddings]) + + return np.array(all_embeddings) + + async def search(self, query: str, top_k: int = 5) -> list[dict]: + """Search for relevant chunks.""" + if not self.initialized: + raise RuntimeError("Document store not initialized") + + # Generate query embedding + from google.cloud import aiplatform + from vertexai.language_models import TextEmbeddingModel + + aiplatform.init(project=GCP_PROJECT, location=GCP_LOCATION) + model = TextEmbeddingModel.from_pretrained(EMBEDDING_MODEL) + + query_embedding = model.get_embeddings([query])[0].values + query_vec = np.array(query_embedding) + + # Cosine similarity + similarities = np.dot(self.embeddings, query_vec) / ( + np.linalg.norm(self.embeddings, axis=1) * np.linalg.norm(query_vec) + ) + + # Get top-k + top_indices = np.argsort(similarities)[-top_k:][::-1] + + results = [] + for idx in top_indices: + results.append({ + "text": self.chunks[idx]["text"], + "title": self.chunks[idx]["title"], + "score": float(similarities[idx]), + }) + + return results + + +# Global document store +doc_store = DocumentStore() + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Initialize document store on startup.""" + try: + await doc_store.initialize(DOCS_URL) + except Exception as e: + logger.error(f"Failed to initialize document store: {e}") + # Continue without initialization - will fail gracefully on requests + yield + + +app = FastAPI( + title="ChipFlow Docs Chat API", + description="AI-powered Q&A for ChipFlow documentation", + version="0.1.0", + lifespan=lifespan, +) + +# CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=ALLOWED_ORIGINS, + allow_credentials=True, + allow_methods=["POST", "OPTIONS"], + allow_headers=["*"], +) + + +@app.get("/health") +async def health(): + """Health check endpoint.""" + return { + "status": "healthy", + "initialized": doc_store.initialized, + "chunks": len(doc_store.chunks) if doc_store.initialized else 0, + } + + +@app.post("/api/chat", response_model=ChatResponse) +async def chat(request: ChatRequest): + """Answer questions about ChipFlow documentation.""" + if not doc_store.initialized: + raise HTTPException( + status_code=503, + detail="Service initializing, please try again in a moment" + ) + + try: + # Search for relevant context + results = await doc_store.search(request.question, top_k=5) + + # Build context + context_parts = [] + sources = [] + for r in results: + if r["score"] > 0.5: # Only include relevant results + context_parts.append(f"### {r['title']}\n{r['text']}") + if r["title"] not in sources: + sources.append(r["title"]) + + context = "\n\n---\n\n".join(context_parts) + + # Build conversation history + history_text = "" + if request.conversation_history: + for msg in request.conversation_history[-4:]: # Last 4 messages + role = "User" if msg.get("role") == "user" else "Assistant" + history_text += f"{role}: {msg.get('content', '')}\n" + + # Generate response using Vertex AI + from google.cloud import aiplatform + import vertexai + from vertexai.generative_models import GenerativeModel + + vertexai.init(project=GCP_PROJECT, location=GCP_LOCATION) + model = GenerativeModel(LLM_MODEL) + + prompt = f"""You are a helpful assistant for ChipFlow documentation. Answer the user's question based on the provided context from the documentation. + +Guidelines: +- Be concise and accurate +- If the context doesn't contain relevant information, say so +- Reference specific documentation sections when helpful +- Use code examples from the context when relevant + +Context from ChipFlow documentation: +{context} + +{f"Previous conversation:{chr(10)}{history_text}" if history_text else ""} + +User question: {request.question} + +Answer:""" + + response = model.generate_content(prompt) + answer = response.text.strip() + + return ChatResponse(answer=answer, sources=sources) + + except Exception as e: + logger.error(f"Chat error: {e}") + raise HTTPException(status_code=500, detail="Failed to generate response") + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", "8080"))) diff --git a/chat-backend/requirements.txt b/chat-backend/requirements.txt new file mode 100644 index 0000000..1c63abf --- /dev/null +++ b/chat-backend/requirements.txt @@ -0,0 +1,7 @@ +fastapi>=0.109.0 +uvicorn[standard]>=0.27.0 +httpx>=0.26.0 +numpy>=1.26.0 +pydantic>=2.5.0 +google-cloud-aiplatform>=1.38.0 +vertexai>=1.38.0 diff --git a/chat-backend/setup-gcp.sh b/chat-backend/setup-gcp.sh new file mode 100755 index 0000000..2e2a52f --- /dev/null +++ b/chat-backend/setup-gcp.sh @@ -0,0 +1,131 @@ +#!/bin/bash +# Idempotent GCP setup script for ChipFlow Docs Chat backend +# Usage: ./setup-gcp.sh [PROJECT_ID] [GITHUB_REPO] +# +# Example: ./setup-gcp.sh chipflow-platform ChipFlow/chipflow-docs + +set -e + +PROJECT_ID="${1:-chipflow-platform}" +GITHUB_REPO="${2:-ChipFlow/chipflow-docs}" +REGION="us-central1" +SERVICE_NAME="chipflow-docs-chat" +POOL_NAME="github-actions-pool" +PROVIDER_NAME="github-actions-provider" +SA_NAME="github-actions-deployer" + +echo "==> Setting up GCP project: $PROJECT_ID" +echo "==> GitHub repo: $GITHUB_REPO" + +# Set the project +gcloud config set project "$PROJECT_ID" + +# Enable required APIs (idempotent - no error if already enabled) +echo "" +echo "==> Enabling required APIs..." +gcloud services enable \ + cloudbuild.googleapis.com \ + run.googleapis.com \ + aiplatform.googleapis.com \ + artifactregistry.googleapis.com \ + iam.googleapis.com \ + iamcredentials.googleapis.com \ + sts.googleapis.com \ + --quiet + +# Get project number +PROJECT_NUMBER=$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)') +COMPUTE_SA="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" +DEPLOYER_SA="${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" + +# Create service account for GitHub Actions (if not exists) +echo "" +echo "==> Creating service account for GitHub Actions..." +if ! gcloud iam service-accounts describe "$DEPLOYER_SA" --quiet 2>/dev/null; then + gcloud iam service-accounts create "$SA_NAME" \ + --display-name="GitHub Actions Deployer" \ + --quiet +fi + +# Grant required roles to the deployer service account +echo "" +echo "==> Granting roles to deployer service account..." +for role in "roles/run.admin" "roles/storage.admin" "roles/aiplatform.user"; do + gcloud projects add-iam-policy-binding "$PROJECT_ID" \ + --member="serviceAccount:$DEPLOYER_SA" \ + --role="$role" \ + --condition=None \ + --quiet 2>/dev/null || true +done + +# Allow deployer to act as compute service account +gcloud iam service-accounts add-iam-policy-binding "$COMPUTE_SA" \ + --member="serviceAccount:$DEPLOYER_SA" \ + --role="roles/iam.serviceAccountUser" \ + --quiet 2>/dev/null || true + +# Grant Vertex AI access to Cloud Run's compute service account +echo "" +echo "==> Granting Vertex AI access to compute service account..." +gcloud projects add-iam-policy-binding "$PROJECT_ID" \ + --member="serviceAccount:$COMPUTE_SA" \ + --role="roles/aiplatform.user" \ + --condition=None \ + --quiet 2>/dev/null || true + +# Set up Workload Identity Federation for GitHub Actions +echo "" +echo "==> Setting up Workload Identity Federation..." + +# Create workload identity pool (if not exists) +if ! gcloud iam workload-identity-pools describe "$POOL_NAME" --location=global --quiet 2>/dev/null; then + echo " Creating workload identity pool..." + gcloud iam workload-identity-pools create "$POOL_NAME" \ + --location="global" \ + --display-name="GitHub Actions Pool" \ + --quiet +fi + +# Create OIDC provider (if not exists) +if ! gcloud iam workload-identity-pools providers describe "$PROVIDER_NAME" \ + --workload-identity-pool="$POOL_NAME" --location=global --quiet 2>/dev/null; then + echo " Creating OIDC provider..." + gcloud iam workload-identity-pools providers create-oidc "$PROVIDER_NAME" \ + --location="global" \ + --workload-identity-pool="$POOL_NAME" \ + --display-name="GitHub Actions Provider" \ + --issuer-uri="https://token.actions.githubusercontent.com" \ + --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" \ + --attribute-condition="assertion.repository=='$GITHUB_REPO'" \ + --quiet +fi + +# Allow GitHub Actions to impersonate the service account +echo "" +echo "==> Granting workload identity user to GitHub repo..." +gcloud iam service-accounts add-iam-policy-binding "$DEPLOYER_SA" \ + --role="roles/iam.workloadIdentityUser" \ + --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_NAME}/attribute.repository/${GITHUB_REPO}" \ + --quiet 2>/dev/null || true + +# Get the workload identity provider resource name +WIF_PROVIDER="projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_NAME}/providers/${PROVIDER_NAME}" + +# Set GitHub secrets +echo "" +echo "==> Setting GitHub secrets..." +echo "$WIF_PROVIDER" | gh secret set GCP_WORKLOAD_IDENTITY_PROVIDER --repo="$GITHUB_REPO" +echo "$DEPLOYER_SA" | gh secret set GCP_SERVICE_ACCOUNT --repo="$GITHUB_REPO" + +echo "" +echo "==========================================" +echo "Setup complete!" +echo "==========================================" +echo "" +echo "GitHub secrets have been set:" +echo " - GCP_WORKLOAD_IDENTITY_PROVIDER" +echo " - GCP_SERVICE_ACCOUNT" +echo "" +echo "Trigger deployment by:" +echo " - Pushing to main with changes in chat-backend/" +echo " - Or manually: gh workflow run deploy-chat-backend --repo $GITHUB_REPO" diff --git a/docs/source/_static/js/chat-widget.js b/docs/source/_static/js/chat-widget.js new file mode 100644 index 0000000..2378b78 --- /dev/null +++ b/docs/source/_static/js/chat-widget.js @@ -0,0 +1,386 @@ +/** + * ChipFlow Documentation AI Chat Widget + * + * A floating chat widget that allows users to ask questions about + * ChipFlow documentation using an AI-powered backend. + */ +(function() { + 'use strict'; + + // Configuration - update this when deploying the backend + const CONFIG = { + apiUrl: 'https://chipflow-docs-chat-PLACEHOLDER.a.run.app/api/chat', + projectName: 'ChipFlow', + placeholder: 'Ask about ChipFlow docs...', + welcomeMessage: 'Hi! I can help answer questions about ChipFlow documentation. What would you like to know?' + }; + + // Don't initialize if already loaded + if (window.chipflowChatLoaded) return; + window.chipflowChatLoaded = true; + + // Create styles + const styles = document.createElement('style'); + styles.textContent = ` + #cf-chat-btn { + position: fixed; + bottom: 24px; + right: 24px; + z-index: 9999; + padding: 14px 20px; + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + color: white; + border: none; + border-radius: 28px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + font-family: system-ui, -apple-system, sans-serif; + box-shadow: 0 4px 14px rgba(99, 102, 241, 0.4); + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 8px; + } + #cf-chat-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(99, 102, 241, 0.5); + } + #cf-chat-btn svg { + width: 18px; + height: 18px; + } + #cf-chat-modal { + display: none; + position: fixed; + bottom: 90px; + right: 24px; + width: 400px; + max-width: calc(100vw - 48px); + height: 520px; + max-height: calc(100vh - 120px); + background: var(--color-background-primary, #ffffff); + border-radius: 16px; + box-shadow: 0 8px 40px rgba(0, 0, 0, 0.15); + z-index: 9999; + flex-direction: column; + overflow: hidden; + font-family: system-ui, -apple-system, sans-serif; + } + #cf-chat-modal.open { + display: flex; + } + .cf-chat-header { + padding: 16px 20px; + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + color: white; + display: flex; + justify-content: space-between; + align-items: center; + } + .cf-chat-header h3 { + margin: 0; + font-size: 15px; + font-weight: 600; + } + .cf-chat-close { + background: rgba(255,255,255,0.2); + border: none; + color: white; + width: 28px; + height: 28px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s; + } + .cf-chat-close:hover { + background: rgba(255,255,255,0.3); + } + .cf-chat-messages { + flex: 1; + overflow-y: auto; + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; + } + .cf-msg { + max-width: 85%; + padding: 10px 14px; + border-radius: 12px; + font-size: 14px; + line-height: 1.5; + word-wrap: break-word; + } + .cf-msg-user { + align-self: flex-end; + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + color: white; + border-bottom-right-radius: 4px; + } + .cf-msg-assistant { + align-self: flex-start; + background: var(--color-background-secondary, #f3f4f6); + color: var(--color-foreground-primary, #1f2937); + border-bottom-left-radius: 4px; + } + .cf-msg-assistant a { + color: #6366f1; + } + .cf-msg-error { + background: #fef2f2; + color: #991b1b; + } + .cf-msg-loading { + display: flex; + gap: 4px; + padding: 12px 16px; + } + .cf-msg-loading span { + width: 8px; + height: 8px; + background: #9ca3af; + border-radius: 50%; + animation: cf-bounce 1.4s infinite ease-in-out both; + } + .cf-msg-loading span:nth-child(1) { animation-delay: -0.32s; } + .cf-msg-loading span:nth-child(2) { animation-delay: -0.16s; } + @keyframes cf-bounce { + 0%, 80%, 100% { transform: scale(0); } + 40% { transform: scale(1); } + } + .cf-chat-input-area { + padding: 12px 16px; + border-top: 1px solid var(--color-background-border, #e5e7eb); + display: flex; + gap: 8px; + } + .cf-chat-input { + flex: 1; + padding: 10px 14px; + border: 1px solid var(--color-background-border, #d1d5db); + border-radius: 8px; + font-size: 14px; + outline: none; + background: var(--color-background-primary, #ffffff); + color: var(--color-foreground-primary, #1f2937); + } + .cf-chat-input:focus { + border-color: #6366f1; + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); + } + .cf-chat-send { + padding: 10px 16px; + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: opacity 0.2s; + } + .cf-chat-send:hover { + opacity: 0.9; + } + .cf-chat-send:disabled { + opacity: 0.5; + cursor: not-allowed; + } + .cf-powered-by { + padding: 8px 16px; + text-align: center; + font-size: 11px; + color: var(--color-foreground-muted, #6b7280); + border-top: 1px solid var(--color-background-border, #e5e7eb); + } + .cf-powered-by a { + color: #6366f1; + text-decoration: none; + } + `; + document.head.appendChild(styles); + + // Create chat button + const chatBtn = document.createElement('button'); + chatBtn.id = 'cf-chat-btn'; + chatBtn.innerHTML = ` + + Ask AI + `; + + // Create chat modal + const chatModal = document.createElement('div'); + chatModal.id = 'cf-chat-modal'; + chatModal.innerHTML = ` +