Skip to content

Instantly share code, notes, and snippets.

@DaikiSuganuma
Last active December 25, 2025 04:59
Show Gist options
  • Select an option

  • Save DaikiSuganuma/c2f6e22fb401a12a1a4e0606274c2366 to your computer and use it in GitHub Desktop.

Select an option

Save DaikiSuganuma/c2f6e22fb401a12a1a4e0606274c2366 to your computer and use it in GitHub Desktop.
[セミナー時にリアルタイムでコメントを見える化するシステムをAIで作る](https://blog.dksg.jp/2025/12/ai.html)
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
/* 全体の基本スタイル */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 0;
padding: 0;
background: #f0f2f5;
padding-bottom: 90px; /* 入力エリアの高さに合わせて余白を広げました */
}
.container { max-width: 800px; margin: 0 auto; padding: 20px; }
/* コメントリストのスタイル */
#comment-list { list-style: none; padding: 0; }
.comment-card {
background: white; padding: 15px; margin-bottom: 10px;
border-radius: 12px; box-shadow: 0 1px 2px rgba(0,0,0,0.1);
animation: slideIn 0.3s ease;
}
.time { font-size: 0.75em; color: #888; margin-bottom: 4px; }
/* ★変更: white-space: pre-wrap によって、改行コードを画面上の改行として表示します */
.text {
font-size: 1.1em; color: #333; line-height: 1.4;
word-wrap: break-word;
white-space: pre-wrap;
}
@keyframes slideIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }
.no-comment { text-align: center; color: #666; margin-top: 20px; }
/* 下部固定の入力エリア */
.input-area {
position: fixed; bottom: 0; left: 0; width: 100%;
background: #fff; border-top: 1px solid #ddd;
padding: 10px 15px; box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
display: flex; gap: 10px; box-sizing: border-box;
z-index: 100;
align-items: center; /* 垂直方向の中央揃え */
}
/* ★変更: textarea用のスタイル */
.input-box {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 10px; /* 角丸を少し控えめに */
font-size: 16px;
outline: none;
transition: border 0.2s;
resize: none; /* ユーザーによるサイズ変更を禁止(高さ固定のため) */
height: 50px; /* 高さを固定(約2行分) */
font-family: inherit;
}
.input-box:focus { border-color: #1a73e8; }
.send-btn {
background: #1a73e8; color: white; border: none; padding: 0 20px;
border-radius: 20px; font-weight: bold; cursor: pointer;
font-size: 14px; white-space: nowrap;
height: 40px; /* ボタンの高さ */
}
.send-btn:disabled { background: #ccc; cursor: not-allowed; }
</style>
</head>
<body>
<!-- コメント表示エリア -->
<div class="container">
<div id="comment-list">
<div class="no-comment" id="loading-msg">読み込み中...</div>
</div>
</div>
<!-- 入力フォームエリア -->
<div class="input-area">
<!-- ★変更: textareaに変更し、maxlengthを設定 -->
<textarea id="comment-input" class="input-box" placeholder="コメントを入力...(140文字まで)" maxlength="140" autocomplete="off"></textarea>
<button id="send-btn" class="send-btn" onclick="submitComment()">送信</button>
</div>
<script>
var currentLastRow = 1;
var isFetching = false;
var isSubmitting = false;
loadNewData();
setInterval(loadNewData, 3000);
// ★変更: Enterキーでの送信機能を削除しました。
// textareaでEnterキーを押すと、通常の「改行」として動作します。
// 送信するには画面上の「送信」ボタンを押してください。
/**
* コメント送信処理関数
*/
function submitComment() {
if (isSubmitting) return;
var input = document.getElementById('comment-input');
var btn = document.getElementById('send-btn');
var text = input.value;
if (!text.trim()) return;
// --- ロック開始 ---
isSubmitting = true;
btn.disabled = true;
btn.innerText = "送信中...";
google.script.run
.withSuccessHandler(function() {
input.value = "";
btn.disabled = false;
btn.innerText = "送信";
isSubmitting = false;
loadNewData();
})
.withFailureHandler(function(err) {
alert("送信に失敗しました");
btn.disabled = false;
btn.innerText = "送信";
isSubmitting = false;
})
.postComment(text);
}
/**
* 新しいデータを取得して表示する関数
*/
function loadNewData() {
if (isFetching) return;
isFetching = true;
google.script.run
.withSuccessHandler(function(response) {
updateList(response);
isFetching = false;
})
.withFailureHandler(function(err) {
console.error(err);
isFetching = false;
})
.getNewData(currentLastRow);
}
/**
* 画面更新関数
*/
function updateList(response) {
if (!response || !response.data || response.data.length === 0) {
if (currentLastRow === 1 && document.getElementById('loading-msg')) {
document.getElementById('loading-msg').innerText = "まだコメントはありません";
}
return;
}
currentLastRow = response.lastRow;
var list = document.getElementById('comment-list');
var msg = document.getElementById('loading-msg');
if (msg) msg.remove();
var newItems = response.data;
for (var i = 0; i < newItems.length; i++) {
var timeStr = escapeHtml(newItems[i][0]);
var textStr = escapeHtml(newItems[i][1]);
var div = document.createElement('div');
div.className = 'comment-card';
// CSSで white-space: pre-wrap を指定しているため、
// textStr の中に含まれる改行コード(\n)はそのまま表示されます。
div.innerHTML = '<div class="time">' + timeStr + '</div>' +
'<div class="text">' + textStr + '</div>';
list.prepend(div);
}
}
function escapeHtml(str) {
if(typeof str !== 'string') return str;
return str.replace(/[&<>"']/g, function(m) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[m];
});
}
</script>
</body>
</html>
/**
* Webアプリへのアクセス時に最初に実行される関数
* Index.htmlを表示用テンプレートとして読み込みます。
*/
function doGet() {
return HtmlService.createTemplateFromFile('Index').evaluate()
.setTitle('リアルタイムコメント') // ブラウザのタブ名を設定
.addMetaTag('viewport', 'width=device-width, initial-scale=1, maximum-scale=1'); // スマホ表示対応
}
/**
* 新しいコメントデータのみを取得する関数
* @param {number} clientLastRow - クライアント(画面側)が現在保持している最後の行番号
* @return {object} data: 新しい行の配列, lastRow: 現在の最新行番号
*/
function getNewData(clientLastRow) {
try {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
var sheetLastRow = sheet.getLastRow();
// シートの行数がクライアントの保持行数以下なら、新しいデータはないので空を返す
// (データ通信量を削減するための重要な判定処理)
if (sheetLastRow <= clientLastRow) {
return {
data: [],
lastRow: clientLastRow
};
}
// 差分(新しいデータ)の行数を計算
var numNewRows = sheetLastRow - clientLastRow;
// 新しい範囲を指定してデータを取得 (開始行, 列, 行数, 列数)
var range = sheet.getRange(clientLastRow + 1, 1, numNewRows, 2);
// 日付などを自動変換させず、見た目通りの「文字列」として取得してエラーを防ぐ
var values = range.getDisplayValues();
return {
data: values, // 新しいデータの配列
lastRow: sheetLastRow // 現在の最新行数を返す
};
} catch (e) {
Logger.log(e); // エラーログを残す
return { data: [], lastRow: clientLastRow };
}
}
/**
* ユーザーからのコメントをスプレッドシートに保存する関数
* @param {string} text - 投稿されたコメント本文
*/
function postComment(text) {
// 空文字やスペースのみの場合は無視する
if (!text || text.toString().trim() === "") return;
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
// A列に現在日時、B列にコメントを追加
// appendRowを使うことで、既存データの一番下に追記される
sheet.appendRow([new Date(), text]);
return true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment