0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Chainlitの日本語入力で変換途中にEnterを押すとメッセージ送信されてしまう

Posted at

表題の通り、途中で送信されてしまい、ちゃんと入力ができない

そこでchainlitのコンフィグでjsを上書きする機能があるため、それを利用してEnterでのメッセージ送信を、無効化する

chainlitの実行ディレクトリで

chainlit init
# configが.chainlit/config.tomlとして作成される

.chainlit/config.tomlを編集

custom_js = "/public/custom.js"
mkdir public
vim public/custom.js

public/custom.js

// public/custom.js
// 目的:
//  - Enter(単独 / Ctrl / Cmd / Shift / Alt の組み合わせすべて)での「送信」を完全に禁止
//  - 入力欄内の Enter は送信の代わりに「改行」を挿入(contentEditable / textarea の両対応)
//  - IME合成(変換)中は何もしない
//  - React/Chainlit 側のキーハンドラより"先"で止めるため、キャプチャ段階 + stopImmediatePropagation を使用
//  - フォーム submit による送信もブロック

(() => {
  let isComposing = false;

  const isEditable = (el) => {
    if (!el || !(el instanceof Element)) return false;
    if (el.tagName === "TEXTAREA") return true;
    if (el.isContentEditable) return true;
    if (el.tagName === "INPUT") {
      const type = (el.getAttribute("type") || "text").toLowerCase();
      return ["text", "search", "url", "tel", "email", "password"].includes(type);
    }
    return false;
  };

  // contentEditable に改行を挿入
  const insertLineBreakCE = () => {
    // 1) 古典的だが堅い: execCommand('insertLineBreak') が使えるならそれを使う
    try {
      // 一部ブラウザでは queryCommandSupported は非推奨でも生きている
      if (document.queryCommandSupported && document.queryCommandSupported("insertLineBreak")) {
        document.execCommand("insertLineBreak");
        return true;
      }
    } catch (_) {}

    // 2) Range API で <br> を直接挿入
    const sel = window.getSelection?.();
    if (!sel || !sel.rangeCount) return false;
    const range = sel.getRangeAt(0);
    range.deleteContents();
    const br = document.createElement("br");
    range.insertNode(br);
    // キャレットを <br> の後ろへ移動
    range.setStartAfter(br);
    range.setEndAfter(br);
    sel.removeAllRanges();
    sel.addRange(range);
    return true;
  };

  // textarea / input に改行を挿入
  const insertNewlineInTextControl = (el) => {
    if (!("selectionStart" in el) || !("value" in el)) return false;
    const start = el.selectionStart;
    const end = el.selectionEnd;
    const v = el.value;
    const next = v.slice(0, start) + "\n" + v.slice(end);
    el.value = next;
    // キャレット位置を改行直後へ
    const pos = start + 1;
    el.selectionStart = el.selectionEnd = pos;

    // React の onChange にも伝える(必要な場合)
    const ev = new Event("input", { bubbles: true });
    el.dispatchEvent(ev);
    return true;
  };

  // Enter 系をすべて捕捉して送信を抑止し、必要なら改行を自前挿入
  const hardBlockEnter = (e) => {
    if (e.key !== "Enter") return;

    // IME合成中はノータッチ(確定 Enter をアプリ側が拾う前に止めたい場合はここを外す)
    if (e.isComposing || isComposing) return;

    const tgt = e.target;

    // ===== 送信は常に禁止 =====
    // → 既定動作とアプリのハンドラを両方止める
    //    (既定動作を許すと、アプリ側の onKeyDown で preventDefault + send が走る場合がある)
    e.preventDefault();
    // 最優先で完全停止
    if (typeof e.stopImmediatePropagation === "function") e.stopImmediatePropagation();
    e.stopPropagation();

    // ===== 入力欄内なら「改行を挿入」 =====
    if (isEditable(tgt)) {
      if (tgt.isContentEditable) {
        insertLineBreakCE();
      } else if (tgt.tagName === "TEXTAREA" || tgt.tagName === "INPUT") {
        insertNewlineInTextControl(tgt);
      }
      // ここで終了(送信はしない)
      return;
    }

    // 入力欄でなければ、何も挿入せず完全ブロック(フォーム accidental submit 対策)
  };

  // IME 状態の追跡(入力欄に付与)
  const attachCompositionHandlers = (el) => {
    el.addEventListener("compositionstart", () => { isComposing = true; }, { capture: true, passive: true });
    el.addEventListener("compositionend",   () => { isComposing = false; }, { capture: true, passive: true });
  };

  // 動的に出入りする入力欄へハンドラを付与
  const bindInputs = (root = document) => {
    const sel = [
      'div[contenteditable="true"]',
      'textarea',
      'input[type="text"]',
      'input[type="search"]',
      'input[type="url"]',
      'input[type="tel"]',
      'input[type="email"]',
      'input[type="password"]'
    ].join(',');
    root.querySelectorAll(sel).forEach(attachCompositionHandlers);
  };

  // すべての form submit を抑止(Enter submit ルートも潰す)
  const blockFormSubmit = (root = document) => {
    root.querySelectorAll("form").forEach((f) => {
      f.addEventListener("submit", (e) => {
        e.preventDefault();
        if (typeof e.stopImmediatePropagation === "function") e.stopImmediatePropagation();
        e.stopPropagation();
      }, { capture: true });
    });
  };

  // グローバルで Enter を捕捉(keydown/keypress/keyup 全段階でブロック)
  const attachGlobalKeyGuards = () => {
    ["keydown", "keypress", "keyup"].forEach((type) => {
      document.addEventListener(type, hardBlockEnter, { capture: true });
      window.addEventListener(type, hardBlockEnter,   { capture: true });
    });
  };

  // さらに保険: 以後登録される Enter 系キーハンドラを束ねて無効化
  // (もし Chainlit 側が後から document/window に capture で付けてもここで飲み込む)
  const patchAddEventListener = () => {
    const orig = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options){
      if ((type === "keydown" || type === "keypress" || type === "keyup") && typeof listener === "function") {
        const wrapped = function(ev){
          if (ev && ev.key === "Enter" && !ev.defaultPrevented) {
            // 既に我々が処理済みの Enter は defaultPrevented=true のはず。
            // そうでなくても、ここで送信系ハンドラを素通りさせない。
            return; // 送信ショートカットを無効化
          }
          return listener.call(this, ev);
        };
        // removeEventListener 対応のためにマップ(簡易)
        try { (listener.__wrappedListeners ||= new WeakMap()).set(this, wrapped); } catch(_){}
        return orig.call(this, type, wrapped, options);
      }
      return orig.call(this, type, listener, options);
    };

    const origRemove = EventTarget.prototype.removeEventListener;
    EventTarget.prototype.removeEventListener = function(type, listener, options){
      const wrapped = listener?.__wrappedListeners?.get?.(this);
      return origRemove.call(this, type, wrapped || listener, options);
    };
  };

  const init = () => {
    attachGlobalKeyGuards();
    patchAddEventListener();
    bindInputs(document);
    blockFormSubmit(document);

    // UI 再描画・画面遷移にも追従
    const mo = new MutationObserver((muts) => {
      for (const m of muts) {
        if (m.type === "childList" && m.addedNodes?.length) {
          m.addedNodes.forEach((n) => {
            if (n.nodeType === 1) {
              bindInputs(n);
              blockFormSubmit(n);
            }
          });
        }
      }
    });
    mo.observe(document.documentElement, { childList: true, subtree: true });
  };

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init, { once: true });
  } else {
    init();
  }
})();

この状態でchainlitを起動させると、enterで送信の代わりに改行が入るようになり、解決した

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?