diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5540135 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,58 @@ +# ============================================ +# Chat Switchboard v0.5 - Unified Dockerfile +# ============================================ +# Stage 1: Build Go backend +# Stage 2: Download JS vendor libs (marked, DOMPurify) +# Stage 3: Production image (nginx + backend) +# +# Vendor libs are baked in during build so the +# app works in disconnected (SCIF) environments +# with no CDN access. +# ============================================ + +# ── Stage 1: Go backend build ──────────────── +FROM golang:1.22-bookworm AS backend + +WORKDIR /app +COPY server/go.mod server/go.sum* ./ +COPY server/ . +RUN go mod download && go mod tidy && go mod verify +RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /bin/switchboard . + +# ── Stage 2: Vendor JS libs ───────────────── +FROM node:20-alpine AS vendor + +WORKDIR /vendor +RUN npm pack marked@16.3.0 && \ + tar xzf marked-16.3.0.tgz && \ + cp package/lib/marked.umd.js marked.min.js && \ + rm -rf package marked-16.3.0.tgz + +RUN npm pack dompurify@3.2.4 && \ + tar xzf dompurify-3.2.4.tgz && \ + cp package/dist/purify.min.js purify.min.js && \ + rm -rf package dompurify-3.2.4.tgz + +# ── Stage 3: Production ───────────────────── +FROM nginx:1-alpine + +RUN apk add --no-cache bash + +# Go backend binary +COPY --from=backend /bin/switchboard /usr/local/bin/switchboard + +# Frontend static files +COPY src/ /usr/share/nginx/html/ + +# Vendor libs (overwrite any existing copies in src/vendor/) +COPY --from=vendor /vendor/marked.min.js /usr/share/nginx/html/vendor/marked.min.js +COPY --from=vendor /vendor/purify.min.js /usr/share/nginx/html/vendor/purify.min.js + +# nginx config +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Entrypoint: start Go backend, then nginx +COPY docker-entrypoint.sh /docker-entrypoint.d/90-start-backend.sh +RUN chmod +x /docker-entrypoint.d/90-start-backend.sh + +EXPOSE 80 diff --git a/VERSION b/VERSION index 8f0916f..4b9fcbe 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.0 +0.5.1 diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..f905b01 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# ============================================ +# Chat Switchboard - Backend Launcher +# ============================================ +# Runs as an nginx entrypoint.d hook. +# Starts the Go backend in the background +# before nginx begins accepting connections. +# ============================================ + +set -e + +echo "🔀 Starting Chat Switchboard backend..." + +# Launch Go backend in background +/usr/local/bin/switchboard & +BACKEND_PID=$! + +# Wait for backend to be ready (max 10s) +for i in $(seq 1 20); do + if wget -q --spider http://localhost:${PORT:-8080}/health 2>/dev/null; then + echo "✅ Backend ready (PID ${BACKEND_PID})" + exit 0 + fi + sleep 0.5 +done + +echo "❌ Backend failed to start within 10s" +exit 1 diff --git a/src/css/styles.css b/src/css/styles.css index 1456415..7c543f1 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -78,16 +78,71 @@ a { color: var(--accent); } } .message:hover .message-copy { opacity: 0.7; } .message-text { font-size: 0.9rem; line-height: 1.6; word-wrap: break-word; } + +/* ── Markdown Rendered Content ────────────── */ + +.message-text p { margin: 0.4em 0; } +.message-text p:first-child { margin-top: 0; } +.message-text p:last-child { margin-bottom: 0; } + .message-text pre { background: var(--bg-tertiary); padding: 0.75rem; border-radius: var(--radius); overflow-x: auto; margin: 0.5rem 0; position: relative; font-size: 0.8rem; } -.message-text code { font-family: var(--mono); font-size: 0.85em; } -.message-text pre code { font-size: inherit; } +.message-text code { font-family: var(--mono); font-size: 0.85em; background: var(--bg-tertiary); padding: 0.15em 0.35em; border-radius: 3px; } +.message-text pre code { font-size: inherit; background: none; padding: 0; } .copy-code-btn { position: absolute; top: 4px; right: 4px; background: var(--bg-secondary); border: 1px solid var(--border); color: var(--text-secondary); border-radius: 4px; - padding: 2px 8px; font-size: 0.7rem; cursor: pointer; + padding: 2px 8px; font-size: 0.7rem; cursor: pointer; opacity: 0; transition: opacity 0.15s; +} +.message-text pre:hover .copy-code-btn { opacity: 1; } +.copy-code-btn:hover { background: var(--border); color: var(--text); } + +.message-text ul, .message-text ol { margin: 0.4em 0; padding-left: 1.5em; } +.message-text li { margin: 0.2em 0; } +.message-text li > p { margin: 0.2em 0; } + +.message-text blockquote { + border-left: 3px solid var(--accent); padding: 0.25em 0.75em; margin: 0.4em 0; + color: var(--text-secondary); background: var(--bg-tertiary); border-radius: 0 4px 4px 0; +} + +.message-text a { color: var(--accent); text-decoration: none; } +.message-text a:hover { text-decoration: underline; } + +.message-text hr { border: none; border-top: 1px solid var(--border); margin: 0.75em 0; } + +.message-text table { border-collapse: collapse; width: 100%; margin: 0.5em 0; font-size: 0.95em; } +.message-text th, .message-text td { border: 1px solid var(--border); padding: 0.35em 0.6em; text-align: left; } +.message-text th { background: var(--bg-tertiary); font-weight: 600; } + +.message-text img { max-width: 100%; border-radius: 4px; } + +.message-text h1, .message-text h2, .message-text h3, +.message-text h4, .message-text h5, .message-text h6 { + margin: 0.75em 0 0.35em; font-weight: 600; +} +.message-text h1 { font-size: 1.3em; } +.message-text h2 { font-size: 1.15em; } +.message-text h3 { font-size: 1.05em; } + +/* ── Thinking Blocks ─────────────────────── */ + +.thinking-block { + border: 1px solid rgba(108, 159, 255, 0.15); border-radius: var(--radius); + margin: 0.5rem 0; background: rgba(108, 159, 255, 0.04); +} +.thinking-block summary { + padding: 0.4rem 0.75rem; cursor: pointer; font-size: 0.8rem; + color: var(--text-secondary); user-select: none; +} +.thinking-block summary:hover { color: var(--text); } +.thinking-block[open] summary { border-bottom: 1px solid rgba(108, 159, 255, 0.1); } +.thinking-content { + padding: 0.5rem 0.75rem; font-size: 0.8rem; color: var(--text-secondary); + max-height: 300px; overflow-y: auto; line-height: 1.5; + background: rgba(108, 159, 255, 0.02); } .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-secondary); } @@ -96,20 +151,6 @@ a { color: var(--accent); } .error-hint { color: var(--danger); font-size: 0.8rem; padding: 0.5rem; } .loading { color: var(--text-secondary); font-size: 0.8rem; padding: 0.5rem; } -/* ── Thinking Blocks ─────────────────────── */ - -.thinking-block { - border: 1px solid var(--border); border-radius: var(--radius); - margin: 0.5rem 0; background: rgba(108, 159, 255, 0.05); -} -.thinking-block summary { - padding: 0.4rem 0.75rem; cursor: pointer; font-size: 0.8rem; color: var(--text-secondary); -} -.thinking-content { - padding: 0.5rem 0.75rem; font-size: 0.8rem; color: var(--text-secondary); - border-top: 1px solid var(--border); max-height: 300px; overflow-y: auto; -} - /* ── Input Area ──────────────────────────── */ .input-area { diff --git a/src/index.html b/src/index.html index 7797d06..eadceb6 100644 --- a/src/index.html +++ b/src/index.html @@ -7,7 +7,7 @@ - +
@@ -297,9 +297,17 @@ - - - - + + + + + + + +