diff --git a/.github/workflows/workflow.yaml b/.github/workflows/workflow.yaml new file mode 100644 index 0000000..54c4e3f --- /dev/null +++ b/.github/workflows/workflow.yaml @@ -0,0 +1,68 @@ +name: Build + +on: + push: + branches: + - main + pull_request: + branches: + - main + +concurrency: # Run release builds sequentially, cancel outdated PR builds + group: ci-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +permissions: # Grant write access to github.token within non-pull_request builds + contents: write + packages: write + +env: + PLATFORMS: linux/amd64,linux/arm64 + +jobs: + build: + name: Build + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + + - id: release + name: Prepare release + uses: mgoltzsche/conventional-release@v1 + with: + commit-files: chat.html + # Don't validate git commit messages prior to requiring the convention + ignore-commits-before: 5b106ddeae56211c542ef1d71415dcf4acded4f2 + + - name: Write release version into chat.html + if: steps.release.outputs.publish + run: | + sed -Ei 's/^(\s+const APP_VERSION = ).+$/\1'"'$RELEASE_VERSION';/" chat.html + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + with: + platforms: ${{ env.PLATFORMS }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log into GitHub registry + if: steps.release.outputs.publish + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Build (and push) container image + uses: docker/build-push-action@v7 + with: + context: . + push: ${{ !!steps.release.outputs.publish }} + platforms: ${{ env.PLATFORMS }} + tags: | + ghcr.io/${{ github.actor }}/statelesschatui:latest + ghcr.io/${{ github.actor }}/statelesschatui:${{ steps.release.outputs.version }} + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..eb8daa7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM nginxinc/nginx-unprivileged:1.29-alpine3.23 +COPY favicon.ico /usr/share/nginx/html/ +COPY chat.html /usr/share/nginx/html/index.html +COPY server.conf.template /etc/nginx/templates/default.conf.template +RUN rm /etc/nginx/conf.d/default.conf +ENV OPENAI_BASE_URL=http://127.0.0.1:8080/v1 +ENTRYPOINT ["/bin/sh", "-c", "export OPENAI_MODELS_BASE_URL=${OPENAI_MODELS_BASE_URL:-$OPENAI_BASE_URL}; exec /docker-entrypoint.sh nginx -g 'daemon off;'"] diff --git a/README.md b/README.md index 1d9e975..666c616 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,38 @@ python -m http.server 8000 npx http-server -p 8000 ``` +To optionally build and run the docker container to serve the file via an nginx HTTP server, run: +```bash +docker build -t statelesschatui:dev . +docker run --rm -p 8000:8000 \ + -e OPENAI_BASE_URL=http://localai:8080/v1 \ + -v "`pwd`/chat.html:/usr/share/nginx/html/index.html" \ + statelesschatui:dev +``` +Once the container started, you can browse the web UI at [http://localhost:8000](http://localhost:8000). +Instead of building the container image yourself you can also use a published release. +For more information as well as the configuration options, see the section "[Run the container](#run-the-container)". + +### Run the container + +To serve StatelessChatUI via a Linux container, run: +```sh +docker run --rm -e OPENAI_BASE_URL=http://localai:8080/v1 -p 8000:8000 ghcr.io/srware-net/statelesschatui:latest +``` +Once the container started, you can browse the web UI at [http://localhost:8000](http://localhost:8000). +To have a reproducible setup, you should replace `latest` with a concrete version ideally. + +The container can be configured using the following environment variables: +| Environment variable | Default value | Description | +| ------------------------ | --------------------------- | -------------------------------------- | +| `OPENAI_BASE_URL` | `http://127.0.0.1:8080/v1` | The base URL pointing to the OpenAI API server. | +| `OPENAI_MODELS_BASE_URL` | (same as `OPENAI_BASE_URL`) | The base URL pointing to the server serving the OpenAI models API endpoint. | + +Using the Linux container provides the following benefits compared to using the HTML file directly: +* No CORS problems due to reverse-proxied API endpoints. +* API base URL can be configured as environment variable, avoiding the need for manual configuration within the web UI. +* A separate base URL can be configured to fetch the available models, allowing to use a middleware/agent that only serves the chat completion endpoint. + ### API Configuration Set base URL and API key in the header section: diff --git a/chat.html b/chat.html index b70bd4d..40c3818 100644 --- a/chat.html +++ b/chat.html @@ -1295,6 +1295,12 @@ /* ---------- Welcome ---------- */ addMessage('system', 'Ready. CORS mode enabled. , reasoning and reasoning_content supported. Import/Export + Markdown + JSON editor + auto-scroll enabled. Load models via /v1/models.'); + + /* ---------- Derive API base URL from server & Load models ---------- */ + if (window.location.href.match(/^https?:\/\//)) { + els.apiBase.value = window.location.href.replace(/^(.+?)(\/[^\/]*)?$/, '$1')+'/v1'; + loadModels(); + } })(); diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..fc89914 Binary files /dev/null and b/favicon.ico differ diff --git a/server.conf.template b/server.conf.template new file mode 100644 index 0000000..2404a4d --- /dev/null +++ b/server.conf.template @@ -0,0 +1,42 @@ +server { + listen 8000; + server_name localhost; + + proxy_connect_timeout 5s; + proxy_read_timeout 60s; + proxy_send_timeout 10s; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Proxy ""; + proxy_http_version 1.1; + proxy_redirect off; + proxy_buffering off; + proxy_request_buffering off; + client_max_body_size 0; + + # Reverse-proxy the chat completion API endpoint + location = /v1/chat/completions { + alias /v1/chat/completions; + proxy_pass ${OPENAI_BASE_URL}/chat/completions; + } + + # Reverse-proxy the models API endpoint + location = /v1/models { + alias /v1/models; + proxy_pass ${OPENAI_MODELS_BASE_URL}/models; + } + + # Serve the UI + location / { + root /usr/share/nginx/html; + index index.html; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +}