Add chat-ui-htmx module: streaming chat page on Spring AI 2.0.2 with htmx and its SSE extension (vendored), MessageChatMemoryAdvisor keyed by the HTTP session, a Stop button, and the traps measured: EventSource strips one leading space per data line, model text must be HTML-escaped, a cancelled stream left the upstream connection open in 7 of 8 runs until an OkHttp interceptor calls Call.cancel (8 of 8 closed after), and the SDK push source ignores back-pressure (limitRate changes only the demand; a bounded buffer works). The model is a stub OpenAI-compatible SSE server written for the module; no real provider was run.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b6Kg9RWSTJzyz6KP9X
This commit is contained in:
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Drives the real page in headless Chromium (Playwright for Python) against the running app + stub.
|
||||
Prints a transcript to stdout. Needs: pip install playwright && playwright install chromium."""
|
||||
import sys, time, urllib.request
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
APP = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:38180"
|
||||
STUB = sys.argv[2] if len(sys.argv) > 2 else "http://localhost:38181"
|
||||
|
||||
def stub(path):
|
||||
return urllib.request.urlopen(STUB + path, data=b"" if "config" in path else None).read().decode().strip()
|
||||
|
||||
def ask(page, text):
|
||||
n = page.locator(".msg.bot").count()
|
||||
page.fill("input[name=message]", text)
|
||||
page.click("form button")
|
||||
page.wait_for_selector(f".msg.bot >> nth={n}")
|
||||
return int(page.locator(".msg.bot span[id^=out-]").last.get_attribute("id")[4:])
|
||||
|
||||
def wait_done(page, n, timeout=20000):
|
||||
page.wait_for_function(f"!document.querySelector('#stop-{n}')", timeout=timeout)
|
||||
|
||||
def bot_text(page, n):
|
||||
return page.locator(f"#out-{n}").inner_text()
|
||||
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch()
|
||||
ctx = b.new_context()
|
||||
page = ctx.new_page()
|
||||
dialogs = []
|
||||
page.on("dialog", lambda d: (dialogs.append(d.message), d.dismiss()))
|
||||
page.goto(APP + "/")
|
||||
print("== turn 1 (session A)")
|
||||
n = ask(page, "My name is Asha and I like tea"); wait_done(page, n)
|
||||
print("A1:", repr(bot_text(page, n)))
|
||||
print("== turn 2 (session A, same cookie)")
|
||||
n = ask(page, "What did I just tell you?"); wait_done(page, n)
|
||||
print("A2:", repr(bot_text(page, n)))
|
||||
print("cookies:", [c["name"] for c in ctx.cookies()])
|
||||
print("== turn 1 (session B, fresh browser context, no cookie)")
|
||||
ctx2 = b.new_context(); page2 = ctx2.new_page(); page2.goto(APP + "/")
|
||||
n = ask(page2, "What did I just tell you?"); wait_done(page2, n)
|
||||
print("B1:", repr(bot_text(page2, n)))
|
||||
print("== New conversation button in session A, then ask again")
|
||||
page.click("text=New conversation"); time.sleep(0.5)
|
||||
n = ask(page, "Do you remember me?"); wait_done(page, n)
|
||||
print("A3:", repr(bot_text(page, n)))
|
||||
print("== model output containing markup")
|
||||
n = ask(page, "html please"); wait_done(page, n)
|
||||
print("rendered text:", repr(bot_text(page, n)))
|
||||
print("<b> elements created inside the answer:", page.locator(f"#out-{n} b").count())
|
||||
print("<img> elements created inside the answer:", page.locator(f"#out-{n} img").count())
|
||||
print("alert dialogs fired:", len(dialogs))
|
||||
print("== stop button")
|
||||
stub("/stub/config?delayMs=50")
|
||||
n = ask(page, "count 200")
|
||||
page.wait_for_function(f"document.querySelector('#out-{n}').innerText.length > 30")
|
||||
before = bot_text(page, n)
|
||||
t0 = time.time(); page.click(f"#stop-{n}")
|
||||
page.wait_for_function(f"!document.querySelector('#stop-{n}')")
|
||||
time.sleep(1.5)
|
||||
after = bot_text(page, n)
|
||||
print("text at click :", repr(before))
|
||||
print("text 1.5 s on :", repr(after))
|
||||
print("grew after stop:", len(after.replace(' [stopped]','')) > len(before.replace(' [stopped]','')) + 6)
|
||||
stub("/stub/config?delayMs=20")
|
||||
b.close()
|
||||
Reference in New Issue
Block a user