From ea368477c4367bcc86e588f6a7f51281689ea766 Mon Sep 17 00:00:00 2001 From: NikkeDoy Date: Tue, 19 May 2026 02:34:44 +0300 Subject: [PATCH] :bug: | Fix message input --- __pycache__/cors_proxy.cpython-314.pyc | Bin 0 -> 8031 bytes cors_proxy.py | 144 +++++++++++++++++++++++++ plugins/unsloth-chat/index.jsx | 72 ++++++++++--- 3 files changed, 202 insertions(+), 14 deletions(-) create mode 100644 __pycache__/cors_proxy.cpython-314.pyc create mode 100755 cors_proxy.py diff --git a/__pycache__/cors_proxy.cpython-314.pyc b/__pycache__/cors_proxy.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5142190bd3fb27e914558457d225f9955c750c91 GIT binary patch literal 8031 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 b>ev=3#PG}MJnj28hWo3DFKEOVZwUVjL#U0r literal 0 HcmV?d00001 diff --git a/cors_proxy.py b/cors_proxy.py new file mode 100755 index 0000000..67ebdcc --- /dev/null +++ b/cors_proxy.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +""" +CORS proxy for Unsloth Studio API. + +Usage: + python cors_proxy.py # proxies to http://127.0.0.1:8888 on port 8080 + python cors_proxy.py --target 8000 # proxies to http://127.0.0.1:8000 + python cors_proxy.py --listen 9090 # listens on port 9090 + python cors_proxy.py --target 10.0.0.5:8000 --listen 9000 +""" + +import argparse +import json +from http.server import HTTPServer, BaseHTTPRequestHandler +from urllib.request import Request, urlopen, URLError + +CORS_HEADERS = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "POST, OPTIONS, GET", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + "Access-Control-Max-Age": "86400", +} + + +class ProxyHandler(BaseHTTPRequestHandler): + target_host = "127.0.0.1" + target_port = 8888 + + def _target_url(self): + return f"http://{self.target_host}:{self.target_port}{self.path}" + + def _set_cors(self, status=200): + self.send_response(status) + for k, v in CORS_HEADERS.items(): + self.send_header(k, v) + + def do_OPTIONS(self): + self._set_cors(204) + self.end_headers() + + def do_POST(self): + content_length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(content_length) + + target = self._target_url() + req = Request(target, data=body, method="POST") + + for h in ("Content-Type", "Authorization"): + val = self.headers.get(h) + if val: + req.add_header(h, val) + + try: + resp = urlopen(req, timeout=60) + resp_body = resp.read() + status = resp.status + except URLError as e: + detail = f"Upstream server at {self.target_host}:{self.target_port} is unreachable." + if hasattr(e, "reason"): + detail += f" Reason: {e.reason}" + self._set_cors(502) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps({ + "error": {"message": detail, "type": "proxy_error"} + }).encode()) + return + except Exception as e: + self._set_cors(502) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps({ + "error": {"message": f"Proxy error: {e}", "type": "proxy_error"} + }).encode()) + return + + self._set_cors(status) + ctype = resp.headers.get("Content-Type", "application/json") + self.send_header("Content-Type", ctype) + self.end_headers() + self.wfile.write(resp_body) + + def log_message(self, fmt, *args): + method = self.command + path = self.path + print(f" [{method}] {path} -> {self._target_url()}") + + def do_GET(self): + if self.path == "/health": + self._set_cors(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps({ + "status": "ok", + "proxy_to": f"{self.target_host}:{self.target_port}", + }).encode()) + return + self._set_cors(405) + self.end_headers() + + +def main(): + parser = argparse.ArgumentParser(description="CORS proxy for Unsloth Studio") + parser.add_argument( + "--target", default="127.0.0.1:8888", + help="Unsloth Studio address (default: 127.0.0.1:8888)", + ) + parser.add_argument( + "--listen", default=8080, type=int, + help="Port to listen on (default: 8080)", + ) + args = parser.parse_args() + + host, port_str = args.target, "8888" + if ":" in args.target: + host, port_str = args.target.rsplit(":", 1) + try: + port = int(port_str) + except ValueError: + print(f"Invalid target port: {port_str}") + sys.exit(1) + + ProxyHandler.target_host = host + ProxyHandler.target_port = port + + server = HTTPServer(("0.0.0.0", args.listen), ProxyHandler) + + print(f" Unsloth Studio CORS Proxy") + print(f" ─────────────────────────") + print(f" Listening on: http://127.0.0.1:{args.listen}") + print(f" Forwarding to: http://{host}:{port}") + print(f" Plugin API URL: http://127.0.0.1:{args.listen}") + print(f" Health check: http://127.0.0.1:{args.listen}/health") + print() + + try: + server.serve_forever() + except KeyboardInterrupt: + print("\nShutting down.") + server.server_close() + + +if __name__ == "__main__": + main() diff --git a/plugins/unsloth-chat/index.jsx b/plugins/unsloth-chat/index.jsx index 0d38a5e..a739ae4 100644 --- a/plugins/unsloth-chat/index.jsx +++ b/plugins/unsloth-chat/index.jsx @@ -67,31 +67,75 @@ scoped.onDispose(() => removeCss()); // ── Helper: find and fill the message input ───────────────────── function setMessageInput(text) { - // Find the Discord chat textarea - const textarea = document.querySelector( - 'main [class*="channelTextArea"] textarea, ' + - 'main [class*="channelTextArea"] [role="textbox"], ' + - '[class*="chat"] [class*="channelTextArea"] textarea, ' + - '[class*="chat"] [class*="channelTextArea"] [role="textbox"]' - ); + // Broad selectors for Discord's chat input (textarea or contenteditable div) + const selectors = [ + // Modern Discord: Slate-based contenteditable div + '[class*="channelTextArea"] div[role="textbox"][contenteditable="true"]', + '[class*="channelTextArea"] [data-slate-editor="true"]', + // Classic Discord: textarea inside channelTextArea + '[class*="channelTextArea"] textarea', + // Fallback: role="textbox" anywhere inside channelTextArea + '[class*="channelTextArea"] [role="textbox"]', + // Even broader fallbacks (no main/chat prefix) + 'main [class*="channelTextArea"] textarea', + 'main [class*="channelTextArea"] [role="textbox"]', + '[class*="chat"] [class*="channelTextArea"] textarea', + '[class*="chat"] [class*="channelTextArea"] [role="textbox"]', + ]; + + let textarea = null; + for (const sel of selectors) { + textarea = document.querySelector(sel); + if (textarea) break; + } + if (!textarea) return false; - // The native textarea (or contenteditable div) inside - const nativeInput = textarea.tagName === "TEXTAREA" - ? textarea - : textarea.querySelector("textarea"); + // Determine the native input element + let nativeInput; + const isContentEditable = + textarea.tagName !== "TEXTAREA" && + (textarea.isContentEditable || textarea.getAttribute("contenteditable") === "true"); + + if (textarea.tagName === "TEXTAREA") { + // Classic Discord: direct textarea element + nativeInput = textarea; + } else if (isContentEditable) { + // Modern Discord: Slate/contenteditable div - use it directly + nativeInput = textarea; + } else { + // Look for a nested textarea (older layout) + nativeInput = textarea.querySelector("textarea"); + } if (!nativeInput) return false; - // Set the native value + // Set the value nativeInput.focus(); - nativeInput.value = text; + + if (nativeInput.tagName === "TEXTAREA") { + // Classic textarea input + nativeInput.value = text; + } else { + // ContentEditable div (Slate editor / modern Discord) + // Clear existing content and insert new text + nativeInput.textContent = ""; + nativeInput.focus(); + + // Use execCommand as fallback for Slate-based editors + try { + document.execCommand("insertText", false, text); + } catch (_) { + // Fallback: set textContent directly + nativeInput.textContent = text; + } + } // Dispatch an input event so React picks up the change const nativeInputEv = new Event("input", { bubbles: true, cancelable: true }); nativeInput.dispatchEvent(nativeInputEv); - // Also dispatch on the outer element if it's a contenteditable + // Also dispatch on the outer element if it differs and is contenteditable if (textarea !== nativeInput && textarea.isContentEditable) { textarea.textContent = text; textarea.dispatchEvent(new Event("input", { bubbles: true }));