diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d23d790 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,88 @@ +name: Build and Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + goos: [windows, linux] + goarch: [amd64, arm64] + fail-fast: false + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24.5' + + - name: Generate Windows resources + if: matrix.goos == 'windows' + run: | + go install github.com/tc-hib/go-winres@latest + go-winres simply \ + --manifest cli \ + --product-name whiskr \ + --original-filename whiskr.exe \ + --icon static/favicon.ico \ + --copyright "(c) 2025 coalaura" \ + --file-description "AI story writing tool" \ + --file-version "${{ github.ref_name }}" \ + --arch "${{ matrix.goarch }}" + + - name: Build ${{ matrix.goos }}_${{ matrix.goarch }} + shell: bash + run: | + mkdir -p build + [[ "${{ matrix.goos }}" == "windows" ]] && EXT=".exe" || EXT="" + + GOOS=${{ matrix.goos }} \ + GOARCH=${{ matrix.goarch }} \ + CGO_ENABLED=0 \ + go build \ + -trimpath \ + -buildvcs=false \ + -ldflags "-s -w -X 'main.Version=${{ github.ref_name }}'" \ + -o "build/whiskr${EXT}" . + + cp -r static build/static + cp .example.env build/.env + tar -czvf build/whiskr_${{ github.ref_name }}_${{ matrix.goos }}_${{ matrix.goarch }}.tar.gz -C build "whiskr${EXT}" static + rm -rf build/static build/.env "build/whiskr${EXT}" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: whiskr_${{ github.ref_name }}_${{ matrix.goos }}_${{ matrix.goarch }}.tar.gz + path: build/* + + release: + needs: build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download all build artifacts + uses: actions/download-artifact@v4 + with: + path: ./build + + - name: Create GitHub release + uses: softprops/action-gh-release@v2 + with: + files: ./build/** + name: "Release ${{ github.ref_name }}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 2e27b05..e76e02b 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ whiskr is a private, self-hosted web chat interface for interacting with AI mode ## TODO +- Statistics for messages (tps, token count, etc.) - Retry button for assistant messages - Import and export of chats - Image and file attachments diff --git a/main.go b/main.go index 8643a62..34b18cf 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,8 @@ import ( "github.com/go-chi/chi/v5/middleware" ) +const Version = "dev" + var log = logger.New().DetectTerminal().WithOptions(logger.Options{ NoLevel: true, }) @@ -27,8 +29,11 @@ func main() { fs := http.FileServer(http.Dir("./static")) r.Handle("/*", cache(http.StripPrefix("/", fs))) - r.Get("/-/models", func(w http.ResponseWriter, r *http.Request) { - RespondJson(w, http.StatusOK, models) + r.Get("/-/data", func(w http.ResponseWriter, r *http.Request) { + RespondJson(w, http.StatusOK, map[string]any{ + "version": Version, + "models": models, + }) }) r.Post("/-/chat", HandleChat) diff --git a/static/css/chat.css b/static/css/chat.css index 0505456..36426f2 100644 --- a/static/css/chat.css +++ b/static/css/chat.css @@ -68,6 +68,29 @@ body { overflow: hidden; } +#version { + position: absolute; + font-size: 12px; + font-style: italic; + top: 3px; + right: 6px; + color: #a5adcb; +} + +#version a { + color: #a5adcb; + text-decoration: none; +} + +body.loading #version { + font-size: 0; + animation: rotating 1.2s linear infinite; + background-image: url(icons/spinner.svg); + width: 16px; + height: 16px; + top: 6px; +} + #page { display: flex; flex-direction: column; @@ -419,6 +442,7 @@ select { margin-right: 4px; } +body.loading #version, .reasoning .toggle::before, .reasoning .toggle::after, #bottom, diff --git a/static/index.html b/static/index.html index f6ba374..66b8c49 100644 --- a/static/index.html +++ b/static/index.html @@ -15,7 +15,9 @@ whiskr - + +
+
diff --git a/static/js/chat.js b/static/js/chat.js index 5a7e3fe..2e23f36 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -1,5 +1,6 @@ (() => { - const $messages = document.getElementById("messages"), + const $version = document.getElementById("version"), + $messages = document.getElementById("messages"), $chat = document.getElementById("chat"), $message = document.getElementById("message"), $bottom = document.getElementById("bottom"), @@ -509,18 +510,22 @@ } } - async function loadModels() { - const modelList = await json("/-/models"); + async function loadData() { + const data = await json("/-/data"); - if (!modelList) { - alert("Failed to load models."); + if (!data) { + alert("Failed to load data."); - return []; + return false; } + // render version + $version.innerHTML = `whiskr ${data.version}`; + + // render models $model.innerHTML = ""; - for (const model of modelList) { + for (const model of data.models) { const el = document.createElement("option"); el.value = model.id; @@ -536,7 +541,7 @@ dropdown($model, 4); - return modelList; + return data; } function restore(modelList) { @@ -841,5 +846,9 @@ dropdown($prompt); dropdown($reasoningEffort); - loadModels().then(restore); + loadData().then((data) => { + restore(data?.models || []); + + document.body.classList.remove("loading"); + }); })(); diff --git a/static/js/markdown.js b/static/js/markdown.js index 1a31ebe..10b27d0 100644 --- a/static/js/markdown.js +++ b/static/js/markdown.js @@ -35,6 +35,10 @@ return `
${header}${code.text}
`; }, + + link(link) { + return `${escapeHtml(link.text || link.href)}`; + }, }, });