表題の通り、途中で送信されてしまい、ちゃんと入力ができない
そこで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で送信の代わりに改行が入るようになり、解決した