<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Random Mix Generator v2.3</title>
<style>
/* ベーススタイル */
body {
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Hiragino Kaku Gothic ProN", "Hiragino Sans", Arial, sans-serif;
padding: 20px;
background-color: #f4f6f9;
color: #333;
height: 100vh;
box-sizing: border-box;
overflow: hidden;
}
/* メインコンテナ(左右均等分割) */
.dashboard-container {
display: grid;
grid-template-columns: 1fr 1fr; /* 1:1 分割 */
gap: 20px;
max-width: 1400px;
margin: 0 auto;
height: calc(100vh - 40px);
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
overflow: hidden;
}
/* 共通パネルスタイル */
.panel {
padding: 20px;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 左パネル(設定エリア) */
.left-panel {
background-color: #fafbfc;
border-right: 1px solid #e1e4e8;
gap: 10px;
}
/* 3列レイアウト用のヘッダーとエリア */
.columns-header {
display: flex;
gap: 10px;
font-weight: bold;
color: #555;
text-align: center;
}
.columns-header div {
flex: 1;
padding: 5px;
background: #eef2f7;
border-radius: 4px;
font-size: 0.9em;
}
.columns-container {
display: flex;
gap: 10px;
flex-grow: 1;
min-height: 0;
}
.text-column {
flex: 1;
display: flex;
flex-direction: column;
}
/* テキストエリア */
textarea.word-list {
flex-grow: 1;
width: 100%;
resize: none;
border: 1px solid #ddd;
border-radius: 6px;
padding: 8px;
font-size: 0.9em;
line-height: 1.4;
box-sizing: border-box;
background-color: #fff;
}
textarea.word-list:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102,126,234,0.1);
}
/* 保存ボタン */
.save-btn {
width: 100%;
padding: 10px;
font-size: 0.95em;
font-weight: bold;
color: #fff;
background-color: #6c757d;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s;
}
.save-btn:hover { background-color: #5a6268; }
.save-btn.saved { background-color: #28a745; }
/* 右パネル(結果エリア) */
.right-panel {
background-color: #fff;
gap: 10px;
}
/* 共通指示入力欄 */
.common-input {
width: 100%;
padding: 10px;
font-size: 1em;
border: 1px solid #007bff;
border-radius: 6px;
box-sizing: border-box;
background-color: #f0f7ff;
color: #333;
}
.common-input:focus {
outline: none;
border-color: #0056b3;
box-shadow: 0 0 0 2px rgba(0,123,255,0.2);
}
/* コントロールバー(数値入力と生成ボタン) */
.control-bar {
display: flex;
gap: 10px;
align-items: center;
}
.count-input {
width: 60px;
padding: 8px;
font-size: 1em;
border: 1px solid #ddd;
border-radius: 6px;
text-align: center;
}
/* 生成ボタン */
.generate-btn {
flex-grow: 1;
padding: 8px;
font-size: 1em;
font-weight: bold;
color: #fff;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
box-shadow: 0 2px 5px rgba(118, 75, 162, 0.3);
border: none;
border-radius: 6px;
cursor: pointer;
transition: opacity 0.2s, transform 0.1s;
}
.generate-btn:hover { opacity: 0.95; }
.generate-btn:active { transform: translateY(1px); }
/* 移動ボタン */
.move-btn {
padding: 8px 12px;
font-size: 1.1em;
font-weight: bold;
color: #fff;
background-color: #17a2b8;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s;
}
.move-btn:hover { background-color: #138496; }
/* 一括解除ボタン */
.unlock-all-btn {
padding: 8px 12px;
font-size: 0.9em;
font-weight: bold;
color: #555;
background-color: #e2e6ea;
border: 1px solid #dae0e5;
border-radius: 6px;
cursor: pointer;
white-space: nowrap;
}
.unlock-all-btn:hover { background-color: #dbe0e5; }
/* 結果リストエリア */
.results-container {
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 6px;
overflow-y: auto;
padding-bottom: 5px;
padding-right: 5px;
border-top: 1px solid #eee; /* 区切り線 */
padding-top: 10px;
}
/* 1行ごとの結果行 */
.result-row {
display: flex;
gap: 8px;
align-items: center;
background: #fff; /* 背景色明示 */
}
/* チェックボックス */
.row-checkbox {
width: 18px;
height: 18px;
cursor: pointer;
flex-shrink: 0;
}
.result-number {
font-size: 0.8em;
color: #999;
width: 25px;
text-align: right;
flex-shrink: 0;
}
.result-input {
flex-grow: 1;
padding: 6px 10px;
font-size: 0.95em;
border: 1px solid #eee;
border-radius: 4px;
color: #333;
background-color: #fdfdfd;
transition: background-color 0.2s;
}
.result-input:focus {
outline: none; border-color: #667eea;
}
/* ロック時のスタイル */
.result-input.locked {
background-color: #fff9db; /* 薄い黄色 */
color: #665c26;
border-color: #e6dbb9;
}
/* ボタン類 */
.action-btn {
padding: 0 10px;
height: 32px;
font-size: 0.8em;
font-weight: bold;
border: none;
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
transition: background-color 0.2s;
flex-shrink: 0;
}
/* コピーボタン */
.copy-btn {
color: #fff;
background-color: #28a745;
width: 70px;
}
.copy-btn:hover { background-color: #218838; }
.copy-btn.copied { background-color: #6c757d; }
/* 固定(ロック)ボタン */
.lock-btn {
width: 40px;
font-size: 1.2em; /* アイコン用 */
background-color: #e2e6ea;
color: #555;
padding: 0; /* 中央寄せのため */
}
.lock-btn:hover { background-color: #d3d9df; }
.lock-btn.is-locked {
background-color: #ffc107; /* アクティブな黄色 */
color: #333;
}
.lock-btn.is-locked:hover { background-color: #e0a800; }
/* ダウンロードエリア */
.download-area {
border-top: 1px solid #e1e4e8;
padding-top: 10px;
margin-top: auto;
}
.download-btn {
width: 100%;
padding: 12px;
font-size: 1em;
font-weight: bold;
color: #007bff;
background-color: #fff;
border: 2px solid #007bff;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.download-btn:hover {
background-color: #007bff;
color: #fff;
}
</style>
</head>
<body>
<div class="dashboard-container">
<div class="panel left-panel">
<div class="columns-header">
<div>修飾語</div>
<div>主語</div>
<div>時間</div>
</div>
<div class="columns-container">
<div class="text-column">
<textarea id="input_mod" class="word-list" placeholder="例:
霧の深い
渓谷に囲まれた"></textarea>
</div>
<div class="text-column">
<textarea id="input_subj" class="word-list" placeholder="例:
近未来都市
古代都市"></textarea>
</div>
<div class="text-column">
<textarea id="input_time" class="word-list" placeholder="例:
夕方
朝方"></textarea>
</div>
</div>
<button id="saveBtn" class="save-btn" onclick="saveSettings()">設定を保存</button>
</div>
<div class="panel right-panel">
<input type="text" id="common_inst" class="common-input" placeholder="共通の指示を入力...">
<div class="control-bar">
<input type="number" id="gen_count" class="count-input" value="10" min="1" max="500">
<span style="font-size:0.9em; color:#666;">個</span>
<button class="generate-btn" onclick="generateBatch()">✨ 一括生成</button>
<button class="move-btn" onclick="moveCheckedItems('up')" title="チェックした項目を上に移動">↑</button>
<button class="move-btn" onclick="moveCheckedItems('down')" title="チェックした項目を下に移動">↓</button>
<button class="unlock-all-btn" onclick="unlockAll()">🔓 全解除</button>
</div>
<div class="results-container" id="resultsList">
</div>
<div class="download-area">
<button class="download-btn" onclick="downloadFile()">⬇️ リストをテキスト保存 (gemini-360data-list.txt)</button>
</div>
</div>
</div>
<script>
// ---------------------------------------------------------
// ■ 初期設定とデータ管理
// ---------------------------------------------------------
// デフォルト値
const defaultData = {
common: "360度パノラマ画像を作って。 Deep focus, 高精細で頼む。",
mod: "霧の深い\n霧のある\n霧のたなびく\n渓谷に囲まれた\n滝に囲まれた\n廃墟となった\n水没した\n空中に浮かぶ\n氷に覆われた\n砂嵐が吹き荒れる\n静寂に包まれた\nディストピアな\n古代の神話のような\n清涼感のある爽やかな風が吹く\n神秘的な",
subj: "近未来都市\n古代都市\n宇宙ステーション\n神殿\n巨大樹の村\n海底神殿\nサイバーパンクな路地裏\n要塞\n空中庭園\n神殿\nスペースコロニー\n中世の城のある町\n工業地帯\n渓谷\nユグドラシルの根本\n雲海\n大都市\n高層建築の谷間\n巨大地下空洞\n浮遊島\n海岸\nエネルギーシールドに守られたドーム\n巨大ステーション\n大迷宮の巨大空間\n化石化した超巨大生物の肋骨の町\n高層ビル群\n海上都市\n魔王城\n巨大クジラの背の上\n巨大亀の背の上\n城下町\n浮遊大陸\n岩礁地帯\n草原\n大量の古代の塔\n祭壇\n雲海に浮かぶ城",
time: "夕方\n朝方\n真夜中\n正午\n黄昏時\n夜明け前"
};
// ページ読み込み時
window.onload = function() {
loadSettings();
document.getElementById('gen_count').value = 10;
generateBatch();
};
// 設定を読み込んで各エリアにセット
function loadSettings() {
const savedCommon = localStorage.getItem('prompt_common');
const savedMod = localStorage.getItem('prompt_mod');
const savedSubj = localStorage.getItem('prompt_subj');
const savedTime = localStorage.getItem('prompt_time');
document.getElementById('common_inst').value = savedCommon !== null ? savedCommon : defaultData.common;
document.getElementById('input_mod').value = savedMod !== null ? savedMod : defaultData.mod;
document.getElementById('input_subj').value = savedSubj !== null ? savedSubj : defaultData.subj;
document.getElementById('input_time').value = savedTime !== null ? savedTime : defaultData.time;
}
function saveSettings() {
const commonVal = document.getElementById('common_inst').value;
const modVal = document.getElementById('input_mod').value;
const subjVal = document.getElementById('input_subj').value;
const timeVal = document.getElementById('input_time').value;
localStorage.setItem('prompt_common', commonVal);
localStorage.setItem('prompt_mod', modVal);
localStorage.setItem('prompt_subj', subjVal);
localStorage.setItem('prompt_time', timeVal);
const btn = document.getElementById('saveBtn');
const originalText = btn.innerText;
btn.innerText = "全設定を保存しました!";
btn.classList.add("saved");
setTimeout(() => {
btn.innerText = originalText;
btn.classList.remove("saved");
}, 1500);
}
// ---------------------------------------------------------
// ■ 生成ロジック
// ---------------------------------------------------------
// 単発のランダムワード生成関数
function getRandomTextFromLists(modList, subjList, timeList) {
const pick = (arr) => {
if (arr.length === 0) return "";
return arr[Math.floor(Math.random() * arr.length)];
};
const m = pick(modList);
const s = pick(subjList);
const t = pick(timeList);
let text = m + s;
if (t) {
if (text) text += ", " + t;
else text = t;
}
return text;
}
function generateBatch() {
const container = document.getElementById('resultsList');
const countInput = document.getElementById('gen_count');
let count = parseInt(countInput.value, 10);
// バリデーション
if (isNaN(count) || count < 1) count = 1;
if (count > 1000) count = 1000;
countInput.value = count;
const getList = (id) => {
const val = document.getElementById(id).value;
return val.split('\n').map(s => s.trim()).filter(s => s !== "");
};
const modList = getList('input_mod');
const subjList = getList('input_subj');
const timeList = getList('input_time');
// 既存の行を取得(Live NodeList)
const existingRows = container.children;
// 指定回数ループして要素を作成・更新
for (let i = 0; i < count; i++) {
// 既存の行があるかチェック
if (i < existingRows.length) {
// 既存行がある場合:ロック状態を確認
const row = existingRows[i];
const input = row.querySelector('.result-input');
// ロックされていなければ内容を更新
if (!input.classList.contains('locked')) {
input.value = getRandomTextFromLists(modList, subjList, timeList);
}
// チェックボックスの存在確認と追加(古いバージョンからの以降用)
if (!row.querySelector('.row-checkbox')) {
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.className = 'row-checkbox';
row.prepend(cb);
}
} else {
// 新規作成の場合
const text = getRandomTextFromLists(modList, subjList, timeList);
const row = document.createElement('div');
row.className = 'result-row';
// チェックボックスを追加したHTML構造
row.innerHTML = `
<input type="checkbox" class="row-checkbox">
<span class="result-number"></span>
<input type="text" class="result-input" value="${text}">
<button class="action-btn copy-btn" onclick="">コピー</button>
<button class="action-btn lock-btn" onclick="" title="固定する">🔓</button>
`;
container.appendChild(row);
}
}
// 数が減った場合、余剰分を削除
while (container.children.length > count) {
container.removeChild(container.lastChild);
}
// 全体の番号とIDを振り直す(一括処理)
updateRowIndices();
}
// ---------------------------------------------------------
// ■ 上下移動・並べ替え機能
// ---------------------------------------------------------
function moveCheckedItems(direction) {
const container = document.getElementById('resultsList');
const rows = Array.from(container.children); // 配列化して操作しやすくする
if (rows.length < 2) return; // 1個しかなければ移動不要
if (direction === 'up') {
// 上へ移動: 上から順に走査
for (let i = 1; i < rows.length; i++) {
const currentRow = rows[i];
const prevRow = rows[i - 1];
const cb = currentRow.querySelector('.row-checkbox');
// チェックされていて、かつ上の行が存在する場合、DOM上で入れ替える
// (上の行がチェックされていない場合のみ入れ替えるロジックも一般的だが、
// ここではチェック群をまとめて動かすため単純なinsertBeforeを使用)
if (cb.checked) {
container.insertBefore(currentRow, prevRow);
}
}
} else if (direction === 'down') {
// 下へ移動: 下から順に走査
for (let i = rows.length - 2; i >= 0; i--) {
const currentRow = rows[i];
const nextRow = rows[i + 1]; // DOM上では次の要素
const cb = currentRow.querySelector('.row-checkbox');
if (cb.checked) {
// nextRowのさらに次(nextSibling)の前に挿入=nextRowの後ろに移動
container.insertBefore(currentRow, nextRow.nextSibling);
}
}
}
// 移動が終わったら番号とIDを正しく振り直す
updateRowIndices();
}
// 行番号、ID、onClickイベントを一括でリフレッシュする関数
function updateRowIndices() {
const container = document.getElementById('resultsList');
const rows = container.children;
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
// 番号更新
row.querySelector('.result-number').innerText = (i + 1) + ".";
// IDとイベント更新
const input = row.querySelector('.result-input');
const copyBtn = row.querySelector('.copy-btn');
const lockBtn = row.querySelector('.lock-btn');
input.id = `result_${i}`;
copyBtn.id = `btn_${i}`;
copyBtn.setAttribute('onclick', `copyText(${i})`);
lockBtn.id = `lock_${i}`;
lockBtn.setAttribute('onclick', `toggleLock(${i})`);
}
}
// ---------------------------------------------------------
// ■ ロック機能
// ---------------------------------------------------------
function toggleLock(index) {
const input = document.getElementById(`result_${index}`);
const lockBtn = document.getElementById(`lock_${index}`);
if (!input || !lockBtn) return;
// クラスのトグル
input.classList.toggle('locked');
lockBtn.classList.toggle('is-locked');
// アイコンの切り替え
if (input.classList.contains('locked')) {
lockBtn.innerText = "🔒";
lockBtn.title = "固定解除";
} else {
lockBtn.innerText = "🔓";
lockBtn.title = "固定する";
}
}
function unlockAll() {
const lockedInputs = document.querySelectorAll('.result-input.locked');
const lockedBtns = document.querySelectorAll('.lock-btn.is-locked');
lockedInputs.forEach(el => el.classList.remove('locked'));
lockedBtns.forEach(btn => {
btn.classList.remove('is-locked');
btn.innerText = "🔓";
btn.title = "固定する";
});
}
// ---------------------------------------------------------
// ■ コピー機能
// ---------------------------------------------------------
function copyText(index) {
const commonText = document.getElementById('common_inst').value.trim();
const inputEl = document.getElementById(`result_${index}`);
if (!inputEl) return;
const resultText = inputEl.value.trim();
if (!resultText) return;
let finalText = "";
if (commonText) {
finalText = commonText + " " + resultText;
} else {
finalText = resultText;
}
navigator.clipboard.writeText(finalText).then(() => {
const btnEl = document.getElementById(`btn_${index}`);
btnEl.innerText = "OK!";
btnEl.classList.add("copied");
setTimeout(() => {
btnEl.innerText = "コピー";
btnEl.classList.remove("copied");
}, 1500);
});
}
// ---------------------------------------------------------
// ■ ファイル保存機能 (ダウンロード)
// ---------------------------------------------------------
function downloadFile() {
const commonText = document.getElementById('common_inst').value.trim();
const inputs = document.querySelectorAll('.result-input');
let fileContent = "";
inputs.forEach((input) => {
const val = input.value.trim();
if (val) {
if (commonText) {
fileContent += commonText + " " + val + "\n";
} else {
fileContent += val + "\n";
}
}
});
if (!fileContent) {
alert("保存するデータがありません。");
return;
}
const blob = new Blob([fileContent], { type: "text/plain" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "gemini-360data-list.txt";
link.click();
URL.revokeObjectURL(link.href);
}
</script>
</body>
</html>
使用変数
| blob | |
| btn | |
| btnEl | |
| cb | |
| charset | |
| class | |
| className | |
| commonText | |
| commonVal | |
| container | |
| copyBtn | |
| copyText -------( Function ) | |
| count | |
| countInput | |
| currentRow | |
| defaultData | |
| direction | |
| download | |
| downloadFile -------( Function ) | |
| el | |
| existingRows | |
| fileContent | |
| finalText | |
| generateBatch -------( Function ) | |
| getList | |
| getRandomTextFromLists -------( Function ) | |
| href | |
| i | |
| id | |
| innerHTML | |
| innerText | |
| input | |
| inputEl | |
| inputs | |
| lang | |
| length | |
| link | |
| loadSettings -------( Function ) | |
| lockBtn | |
| lockedBtns | |
| lockedInputs | |
| m | |
| max | |
| min | |
| modList | |
| modVal | |
| moveCheckedItems -------( Function ) | |
| nextRow | |
| onclick | |
| onload | |
| originalText | |
| pick | |
| placeholder | |
| prevRow | |
| resultText | |
| row | |
| rows | |
| s | |
| savedCommon | |
| savedMod | |
| savedSubj | |
| savedTime | |
| saveSettings -------( Function ) | |
| style | |
| subjList | |
| subjVal | |
| t | |
| text | |
| timeList | |
| timeVal | |
| title | |
| toggleLock -------( Function ) | |
| type | |
| unlockAll -------( Function ) | |
| updateRowIndices -------( Function ) | |
| val | |
| value |