diff --git a/wsai/dashboard.py b/wsai/dashboard.py index 61bd437..b3d0687 100644 --- a/wsai/dashboard.py +++ b/wsai/dashboard.py @@ -35,6 +35,20 @@ def _speech_text(reply: str) -> str: return reply +def _thought_summary(reply: str) -> str: + """A short '생각내용' line: the emotion/tone plan the bot chose for delivery, + derived from the [감정] tags in its reply. This is the AI's decision about + *how* to say the answer, shown between 들음 and 답변.""" + import re + from .backends.emotion import match_emotion + + tags = re.findall(r"\[([^\[\]]*)\]", reply or "") + emotions = [t.strip() for t in tags if match_emotion(t)] + if emotions: + return "감정 톤: " + " → ".join(emotions) + return "감정 태그 없음 · 기본 톤으로 답변" + + def _make_handler(dash: "Dashboard"): monitor = dash.monitor @@ -73,6 +87,13 @@ def _make_handler(dash: "Dashboard"): self._handle_voice_turn() elif path == "/api/prompt": self._handle_prompt_post() + elif path == "/api/logs/clear": + monitor.clear_events() + self._send(200, json.dumps({"ok": True}).encode(), "application/json; charset=utf-8") + elif path == "/api/logs/delete": + self._handle_log_mutate("delete") + elif path == "/api/logs/edit": + self._handle_log_mutate("edit") else: self._send(404, b"not found", "text/plain; charset=utf-8") @@ -185,6 +206,23 @@ def _make_handler(dash: "Dashboard"): }, ensure_ascii=False).encode("utf-8") self._send(200, body, "application/json; charset=utf-8") + def _handle_log_mutate(self, action: str) -> None: + """Per-line log delete/edit by event id.""" + raw = self._read_body() + try: + data = json.loads(raw.decode("utf-8")) if raw else {} + event_id = int(data.get("id")) + except (ValueError, TypeError, AttributeError): + self._send(400, json.dumps({"ok": False, "error": "id required"}).encode(), + "application/json; charset=utf-8") + return + if action == "delete": + ok = monitor.delete_event(event_id) + else: + ok = monitor.edit_event(event_id, str(data.get("message", ""))) + self._send(200 if ok else 404, + json.dumps({"ok": ok}).encode(), "application/json; charset=utf-8") + def _stream_events(self) -> None: self.send_response(200) self.send_header("Content-Type", "text/event-stream; charset=utf-8") @@ -317,10 +355,12 @@ class Dashboard: if not heard: # Nothing recognised (silence/noise): mark it as [잡음] and skip # the brain/TTS so the bot plays nothing back. + turn.thought("목소리가 아닌 잡음으로 판단 → 응답하지 않음") turn.replied("[잡음]") turn.finish() return {"heard": heard, "reply": "[잡음]", "wav": b""} reply_text = self._think(heard) + turn.thought(_thought_summary(reply_text)) turn.replied(reply_text) out_path = self._submit(self.tts.synth(_speech_text(reply_text))) with open(out_path, "rb") as f: @@ -472,6 +512,7 @@ PAGE = r""" .line{display:flex;gap:9px;margin:5px 0;align-items:flex-start} .tag{flex:0 0 42px;font-size:11px;color:var(--muted);padding-top:2px} .heard{color:var(--heard);font-weight:550} + .thought{color:var(--muted);font-size:13px} .reply{color:var(--reply);font-weight:550} .steps{margin-top:10px;border-top:1px dashed var(--line);padding-top:10px;display:flex;flex-direction:column;gap:6px} .step{display:grid;grid-template-columns:120px 1fr 66px;gap:10px;align-items:center;font-size:12.5px} @@ -525,6 +566,36 @@ PAGE = r""" background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:10px 16px; font-size:13px;box-shadow:0 10px 30px rgba(0,0,0,.4);opacity:0;transition:opacity .2s} .toast.show{opacity:1} + /* Bottom-docked VSCode-style terminal log panel */ + main{padding-bottom:46px} + .logdock{position:fixed;left:0;right:0;bottom:0;z-index:15;background:#0a0e13; + border-top:1px solid var(--line);display:flex;flex-direction:column; + max-height:45vh;box-shadow:0 -8px 24px rgba(0,0,0,.35)} + .logbar{display:flex;align-items:center;gap:8px;padding:6px 12px;border-bottom:1px solid var(--line); + background:#0d141c;flex-wrap:wrap} + .logtoggle{background:none;border:none;color:var(--fg);font-size:12.5px;cursor:pointer;font-weight:600;padding:4px 6px} + .logsearch{flex:1;min-width:120px;background:var(--panel2);border:1px solid var(--line);color:var(--fg); + border-radius:8px;padding:5px 9px;font-size:12.5px} + .logsel{background:var(--panel2);border:1px solid var(--line);color:var(--fg);border-radius:8px;padding:5px 8px;font-size:12.5px} + .logcount{color:var(--muted);font-size:11.5px} + .logbtn{padding:5px 10px;font-size:12px} + .logbody{overflow:auto;padding:8px 12px;font-family:"SFMono-Regular",Consolas,"Liberation Mono",Menlo,monospace; + font-size:12px;line-height:1.65;background:#0a0e13} + .logdock.collapsed .logbody{display:none} + .logdock.collapsed{max-height:none} + .logline{display:flex;gap:8px;align-items:baseline;padding:1px 0;border-bottom:1px solid #10171f} + .logline:hover{background:#0e151d} + .logline .lt{flex:0 0 92px;color:#5f7488} + .logline .lv{flex:0 0 46px;text-transform:uppercase;font-size:10.5px} + .logline.info .lv{color:var(--accent)} + .logline.error .lv{color:var(--err)} + .logline.warn .lv{color:var(--warn)} + .logline .lm{flex:1;color:var(--fg);white-space:pre-wrap;word-break:break-word} + .logline.error .lm{color:#ffb3bb} + .logline .lacts{opacity:0;display:flex;gap:4px} + .logline:hover .lacts{opacity:1} + .lact{background:none;border:none;color:var(--muted);cursor:pointer;font-size:12px;padding:0 3px} + .lact:hover{color:var(--fg)}
@@ -557,11 +628,22 @@ PAGE = r"""