Skip to content

feat(integrations): add Streamlit UI wrapper for the humanizer skill#102

Open
Rich627 wants to merge 6 commits intoblader:mainfrom
Rich627:feat/streamlit-ui
Open

feat(integrations): add Streamlit UI wrapper for the humanizer skill#102
Rich627 wants to merge 6 commits intoblader:mainfrom
Rich627:feat/streamlit-ui

Conversation

@Rich627
Copy link
Copy Markdown

@Rich627 Rich627 commented Apr 24, 2026

Summary

Adds a minimal Streamlit UI at integrations/streamlit/ that wraps the humanizer skill. Paste text → click Humanize → get the cleaned rewrite back with a native copy button, without the skill's default draft / audit / summary sections.

This is the first non-skill code in the repo. Everything is scoped under integrations/streamlit/ so the skill itself is untouched. If you'd prefer to keep this repo pure-skill, I'm happy to move the wrapper out to a separate repo and link from the root README instead — just say the word.

What the wrapper does

  • Shells out to the host's claude CLI (claude -p --no-session-persistence …), so it reuses the user's existing Claude Code OAuth login — no API key required. ANTHROPIC_API_KEY is honored as a fallback.
  • Wraps the user's text in a prompt that invokes the humanizer skill but adds strict output constraints so the returned text is just the rewritten prose — no "Draft rewrite" / "What makes this AI" / "Summary of changes" sections.
  • Defensive post-process regex (_PREAMBLE_RE + _TRAILING_RE) strips any leftover preamble or skill-trailer block if the model ever disobeys. The trailing regex is anchored to the skill's actual bolded headers (e.g. **Summary of changes:**, **Changes made:**) so legitimate prose that merely starts with "Notes" or "Summary" is preserved.
  • Renders the result via st.code(wrap_lines=True) so there's a native copy-to-clipboard icon.

Ways to run

  • Local: cd integrations/streamlit && ./run.sh — creates a venv, installs streamlit, opens http://localhost:8501.
  • Docker: cd integrations/streamlit && docker compose up --build. The compose file mounts ~/.claude/ into the container (RW, since macOS may need claude /login inside the container), runs Streamlit as a non-root user (uid 1000), and binds port 127.0.0.1:8501 so the unauthenticated UI isn't exposed to the LAN by default.

Files added

```
integrations/streamlit/
├── app.py # Streamlit UI + subprocess call to claude -p
├── run.sh # Local launcher (venv + streamlit)
├── Dockerfile # python + node + claude CLI + streamlit (non-root user)
├── docker-compose.yml # Mounts ~/.claude, loopback-bound :8501
├── .dockerignore
├── requirements.txt # streamlit>=1.56
└── README.md # Lifecycle / auth / stop / output / security docs
```

Notes for review

  • Each click spawns a fresh claude -p --no-session-persistence subprocess — no state survives across clicks, nothing is written to the session store.
  • Clicking Humanize again kills any still-running subprocess first.
  • On Streamlit shutdown (Ctrl+C / docker compose down), an atexit hook (registered exactly once via st.cache_resource, not per click) kills any in-flight subprocess.
  • README has a Security considerations section covering the ~/.claude trust boundary, the non-root container user, the loopback port bind, and the single-user scope.

Test plan

  • Local: cd integrations/streamlit && ./run.sh, paste AI-sounding text, click Humanize, confirm cleaned output appears with a copy icon.
  • Local: confirm Ctrl+C in the terminal cleanly stops Streamlit and kills any in-flight claude subprocess.
  • Docker (Linux): docker compose up --build, same flow. (not tested locally — no Linux host available; Dockerfile is identical to the macOS path so this is expected to work, but flagging for reviewer verification.)
  • Docker (macOS): after docker compose up -d --build, run docker compose exec humanizer claude /login once, then humanize.
  • API-key path: ANTHROPIC_API_KEY=sk-ant-… ./run.sh, confirm no OAuth prompt. (not tested — no spare API key on hand; the code path is a single os.environ pass-through to the claude subprocess, which is the documented override.)
  • Regression guard: paste prose beginning with words like "Notes" / "Summary" / "Changes" and confirm it is preserved intact by the cleanup regex.

🤖 Generated with Claude Code

Rich627 and others added 6 commits April 23, 2026 17:21
Paste text → click Humanize → get the final rewritten prose back.
The wrapper shells out to the local `claude` CLI with a prompt that invokes
the humanizer skill but suppresses its usual draft / audit / summary output,
leaving only the rewritten text for copy-paste workflows.

Two ways to run:
- Local: ./run.sh (venv + streamlit)
- Docker: docker compose up --build (mounts ~/.claude for skill + auth)

No API key required — uses the host's existing Claude Code login.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
README now explains:
- exactly when the `claude` subprocess starts/stops (per-click one-shot)
- OAuth (`claude /login`) vs ANTHROPIC_API_KEY, including the macOS Docker
  keychain caveat and how to work around it
- what guarantees the output stays free of draft/audit/summary noise

app.py adds a `_clean_output()` pass that strips leaked preambles and
trailing "Summary of changes" blocks, and replaces the streamed view with
a copy-friendly textarea once generation finishes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Output now renders via st.code (wrap_lines=True) so it has a native
  copy-to-clipboard icon in the top-right corner.
- Add a visible "Running humanizer skill…" status message + spinner so
  users know work is in progress instead of only seeing the tiny Stop
  button in the Streamlit header.
- Streaming response is preserved — text appears live as it arrives,
  then gets replaced by the copy-ready code block on completion.
- README's Lifecycle section now spells out exactly how to stop:
  Ctrl+C for local, docker compose down / stop / kill for container,
  and notes that closing the browser tab alone doesn't stop Streamlit
  or the subprocess (which will self-terminate within seconds anyway).
- Bump streamlit minimum to 1.56 (wrap_lines support + general latest).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundles the code-review + security-review fixes with a couple of
usability nits reported during testing.

Correctness:
- Tighten `_TRAILING_RE` so it only strips the humanizer skill's actual
  trailer blocks (bolded "Summary of changes" / "Changes made" /
  "What makes … AI …" / "Draft rewrite" / "Now make it not …", with or
  without a leading `---` separator). Previous pattern ate any paragraph
  whose first word was "Notes" / "Summary" / "Changes" — a real problem
  for a humanizer whose job is preserving user prose.
- Add `--no-session-persistence` to the `claude -p` call so no session
  file is written to disk, guaranteeing each click is fully stateless
  with zero possibility of one run's context leaking into the next.

Resource hygiene:
- Register atexit exactly once via `@st.cache_resource` instead of per
  click. Previous code appended a new atexit hook to every Humanize
  press for the life of the Streamlit process.

Docker hardening:
- Dockerfile now creates an unprivileged `app` user (uid 1000), chowns
  /app, and runs Streamlit + claude CLI as that user. Previously ran
  as root, so a dependency bug could write host files as uid 0 via
  the ~/.claude mount.
- docker-compose.yml binds the published port to 127.0.0.1 only so the
  unauthenticated UI isn't exposed to the LAN by default, and adjusts
  the mount target + HOME to the new non-root home.

UX:
- Status message no longer wraps "claude -p" in backticks (rendered as
  tiny inline code and broke mid-sentence in the info block). Plain
  English now.
- README: new Security considerations section covering the port bind,
  the ~/.claude mount trust boundary, the non-root user, and the
  single-user scope. Lifecycle section mentions --no-session-persistence
  explicitly so users don't worry about state leaking between clicks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `claude -p` text mode doesn't actually deliver token-by-token
streaming through the PIPE in a way Streamlit's write_stream can
render incrementally, so the "streaming" UX was a lie. Swap in a
plain `proc.communicate()` inside st.spinner, render once on
completion via st.code (which still has the native copy button).

Also update README — lifecycle table + Output section no longer claim
text "streams in live".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The app supports both OAuth login and ANTHROPIC_API_KEY paths, so
phrasing the caption as "No API key required" understates the API-key
path and can mislead users who already have a key set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant