let lastSaveTimestamp = 0; let controller; function formatText() { const textOrg = document.getElementById('novelContent1').value; let text = textOrg.replace(/[」。)]/g, '$&\n'); while (text.includes('\n\n')) { text = text.replace(/\n\n/g, '\n'); } text = text.replace(/「([^」\n]*)\n([^」\n]*)」/g, '「$1$2」'); text = text.replace(/(([^)\n]*)\n([^)\n]*))/g, '($1$2)'); while (text.search(/「[^「\n]*。\n/) >= 0) { text = text.replace(/「([^「\n]*。)\n/, '「$1'); } text = text.replace(/\n/g, "\n\n"); text = text.replace(/\n#/g, "\n\n#"); document.getElementById('novelContent1').value = text; } function unmalform(text) { let result = null; while (!result && text) { try { result = decodeURI(text); } catch (error) { text = text.slice(0, -1); } } return result || ''; } function partialEncodeURI(text) { console.log('partialEncodeURI called'); console.log('partialEncodeToggle checked:', document.getElementById("partialEncodeToggle").checked); if (!document.getElementById("partialEncodeToggle").checked) { console.log('Partial encode is disabled, returning original text'); return text; } let length = parseInt(document.getElementById("encodeLength").value); console.log('Encode length:', length); const chunks = []; for (let i = 0; i < text.length; i += 1) { chunks.push(text.slice(i, i + 1)); } console.log('Original text:', text); const encodedChunks = chunks.map((chunk, index) => { if (index % length === 0) { console.log('Encoding chunk at index', index, ':', chunk); return encodeURI(chunk); } return chunk; }); const result = encodedChunks.join(''); console.log('Encoded text:', result); return result; } function saveToJson() { const novelContent1 = document.getElementById('novelContent1').value; const novelContent2 = document.getElementById('novelContent2').value; const generatePrompt = document.getElementById('generatePrompt').value; const nextPrompt = document.getElementById('nextPrompt').value; const savedTitle = document.getElementById('savedTitle').value; const jsonData = JSON.stringify({ novelContent1: novelContent1, novelContent2: novelContent2, generatePrompt: generatePrompt, nextPrompt: nextPrompt, savedTitle: savedTitle }); const blob = new Blob([jsonData], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'novel_data.json'; if (savedTitle) { a.download = savedTitle + '.json'; } document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function loadFromJson() { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.json'; fileInput.style.display = 'none'; document.body.appendChild(fileInput); fileInput.addEventListener('change', function (event) { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = function (e) { try { const jsonData = JSON.parse(e.target.result); if (jsonData.novelContent1) { document.getElementById('novelContent1').value = jsonData.novelContent1; } if (jsonData.novelContent2) { document.getElementById('novelContent2').value = jsonData.novelContent2; } if (jsonData.generatePrompt) { document.getElementById('generatePrompt').value = jsonData.generatePrompt; } if (jsonData.nextPrompt) { document.getElementById('nextPrompt').value = jsonData.nextPrompt; } if (jsonData.savedTitle) { document.getElementById('savedTitle').value = jsonData.savedTitle; } alert('JSONファイルを正常に読み込みました。'); } catch (error) { alert('無効なJSONファイルです。'); } }; reader.readAsText(file); } }); fileInput.click(); } function saveToUserStorage(force = false) { const currentTime = Date.now(); if (!force && currentTime - lastSaveTimestamp < 5000) { return; } const geminiClientData = { novelContent: document.getElementById('novelContent1').value, novelContent2: document.getElementById('novelContent2').value, generatePrompt: document.getElementById('generatePrompt').value, nextPrompt: document.getElementById('nextPrompt').value, savedTitle: document.getElementById('savedTitle').value, memo: document.getElementById('memo').value, geminiApiKey: document.getElementById('geminiApiKey').value, selectedEndpoint: document.getElementById('endpointSelect').value }; localStorage.setItem('geminiClient', JSON.stringify(geminiClientData)); lastSaveTimestamp = currentTime; } function loadFromUserStorage() { const savedData = localStorage.getItem('geminiClient'); if (savedData) { const geminiClientData = JSON.parse(savedData); document.getElementById('novelContent1').value = geminiClientData.novelContent || ''; document.getElementById('novelContent2').value = geminiClientData.novelContent2 || ''; document.getElementById('generatePrompt').value = geminiClientData.generatePrompt || ''; document.getElementById('nextPrompt').value = geminiClientData.nextPrompt || ''; document.getElementById('savedTitle').value = geminiClientData.savedTitle || ''; document.getElementById('memo').value = geminiClientData.memo || ''; document.getElementById('geminiApiKey').value = geminiClientData.geminiApiKey || ''; document.getElementById('endpointSelect').value = geminiClientData.selectedEndpoint || 'models/gemini-1.5-pro-002'; } } function createPayload() { const novelContent1 = document.getElementById('novelContent1'); const text = novelContent1.value; const lines = text.split('\n').filter(x => x); let lastPart = lines.pop() || ''; let systemPrompt = `${partialEncodeURI(document.getElementById('generatePrompt').value)}`; let messages = [ { "role": "user", "parts": [{ "text": systemPrompt || "." }] } ]; if (lines.length > 0) { messages.push( { "role": "model", "parts": [{ "text": partialEncodeURI(lines.join("\n")) }] }, { "role": "user", "parts": [{ "text": "続きを書いて。" }] } ); } messages.push( { "role": "model", "parts": [{ "text": lastPart }] }, { "role": "user", "parts": [{ "text": `続きを書いて。${partialEncodeURI(document.getElementById('nextPrompt').value)} ${document.getElementById('characterCountInput').value}文字程度。` }] } ); return { method: 'POST', headers: {}, body: JSON.stringify({ contents: messages, "generationConfig": { "temperature": 1.0, "max_output_tokens": 4096 }, safetySettings: [ { "category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE" }, { "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE" }, { "category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE" }, { "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE" } ] }), mode: 'cors' }; } function fetchStream(ENDPOINT, payload) { const novelContent2 = document.getElementById('novelContent2'); const requestButton = document.getElementById('requestButton'); controller = new AbortController(); const signal = controller.signal; fetch(ENDPOINT, { ...payload, signal }) .then(response => { if (!response.ok) { throw new Error('ネットワークの応答が正常ではありません'); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; function readStream() { reader.read().then(({ done, value }) => { if (done) { console.debug('ストリームが完了しました'); document.getElementById('stopButton').classList.add('d-none'); requestButton.disabled = false; return; } const chunk = decoder.decode(value, { stream: true }); buffer += chunk; console.debug('チャンクを受信しました:', chunk); // バッファから完全なJSONオブジェクトを抽出して処理 let startIndex = 0; while (true) { const endIndex = buffer.indexOf('\n', startIndex); if (endIndex === -1) break; const line = buffer.slice(startIndex, endIndex).trim(); startIndex = endIndex + 1; if (line.startsWith('data: ')) { const jsonString = line.slice(5); if (jsonString === '[DONE]') { console.debug('Received [DONE] signal'); break; } try { const data = JSON.parse(jsonString); console.debug('Parsed JSON:', data); if (data.candidates && data.candidates[0].content && data.candidates[0].content.parts) { data.candidates[0].content.parts.forEach(part => { if (part.text) { console.debug('Adding text to output:', part.text); novelContent2.value += part.text; novelContent2.scrollTop = novelContent2.scrollHeight; } }); } // finishReasonをチェック if (data.candidates && data.candidates[0].finishReason) { if (data.candidates[0].finishReason === 'STOP') { requestButton.classList.add('green-flash-bg'); setTimeout(() => { requestButton.classList.remove('green-flash-bg'); }, 2000); } else { requestButton.classList.add('red-flash-bg'); setTimeout(() => { requestButton.classList.remove('red-flash-bg'); }, 2000); } } } catch (error) { console.error('JSONパースエラー:', error); } } } // 処理済みの部分をバッファから削除 buffer = buffer.slice(startIndex); readStream(); }).catch(error => { if (error.name === 'AbortError') { console.log('フェッチがユーザーによって中止されました'); } else { console.error('ストリーム読み取りエラー:', error); } document.getElementById('stopButton').classList.add('d-none'); requestButton.disabled = false; }); } readStream(); }) .catch(error => { if (error.name === 'AbortError') { console.log('フェッチがユーザーよって中止されました'); } else { console.error('フェッチエラー:', error); } requestButton.disabled = false; }); } function fetchNonStream(ENDPOINT, payload) { const novelContent2 = document.getElementById('novelContent2'); fetch(ENDPOINT, payload) .then(response => response.json()) .then(data => { if (data && data.candidates && data.candidates[0].content && data.candidates[0].content.parts && data.candidates[0].content.parts[0].text) { novelContent2.value += data.candidates[0].content.parts[0].text; novelContent2.scrollTop = novelContent2.scrollHeight; } }) .catch(error => { console.error('エラー:', error); }) .finally(() => { document.getElementById('requestButton').disabled = false; }); } function Request() { const selectedEndpoint = document.getElementById('endpointSelect').value; let ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/${selectedEndpoint}:generateContent?key=` + document.getElementById('geminiApiKey').value; try { if (!document.getElementById('geminiApiKey').value) { throw new Error("Gemini APIキーが設定されていません。"); } } catch (e) { console.error(e); document.getElementById("geminiApiKey").classList.add("bg-danger"); document.querySelector('[data-bs-toggle="offcanvas"]').click(); return; } document.getElementById("geminiApiKey").classList.remove("bg-danger"); document.getElementById('requestButton').disabled = true; let stream = document.getElementById('streamToggle').checked; document.getElementById('novelContent2').value = ''; // 新しい生成を開始する前に内容をクリア const payload = createPayload(); if (stream) { // ここでエンドポイントを正しく変更 ENDPOINT = ENDPOINT.replace(':generateContent', ':streamGenerateContent') + '&alt=sse'; console.debug('Stream ENDPOINT:', ENDPOINT); // デバッグ用出力を追加 fetchStream(ENDPOINT, payload); document.getElementById('stopButton').classList.remove('d-none'); } else { fetchNonStream(ENDPOINT, payload); } // 出力のアコーディオンを開く const outputAccordion = document.querySelector('#content2Collapse'); if (outputAccordion) { const bsCollapse = new bootstrap.Collapse(outputAccordion, { toggle: false }); bsCollapse.show(); } } function stopGeneration() { if (controller) { controller.abort(); controller = null; } document.getElementById('stopButton').classList.add('d-none'); document.getElementById('requestButton').disabled = false; } // 新しい関数を追加 function handleKeyPress(event) { if (event.ctrlKey && event.key === 'Enter') { Request(); } } function syncInputs() { const inputs = document.querySelectorAll('input[type="range"], input[type="number"]'); inputs.forEach(input => { const baseId = input.id.replace('Input', ''); const pairedInput = document.getElementById(baseId + (input.type === 'range' ? 'Input' : '')); if (pairedInput) { input.addEventListener('input', function () { pairedInput.value = this.value; }); } }); } function openNextAccordion() { const accordions = document.querySelectorAll('#mainAccordion .accordion-item'); let currentIndex = -1; // 現在開いているアコーディオンのインデックスを見つける for (let i = 0; i < accordions.length; i++) { if (!accordions[i].querySelector('.accordion-button').classList.contains('collapsed')) { currentIndex = i; break; } } // 次のアコーディオンを開く if (currentIndex < accordions.length - 1) { new bootstrap.Collapse(accordions[currentIndex].querySelector('.accordion-collapse')).hide(); new bootstrap.Collapse(accordions[currentIndex + 1].querySelector('.accordion-collapse')).show(); } else { // もう次がない場合、ボタンを赤く点滅させる const nextButton = document.getElementById('nextAccordion'); nextButton.classList.add('red-flash-bg'); setTimeout(() => { nextButton.classList.remove('red-flash-bg'); }, 2000); } } function openPreviousAccordion() { const accordions = document.querySelectorAll('#mainAccordion .accordion-item'); let currentIndex = -1; // 現在開いているアコーディオンのインデックスを見つける for (let i = 0; i < accordions.length; i++) { if (!accordions[i].querySelector('.accordion-button').classList.contains('collapsed')) { currentIndex = i; break; } } // 前のアコーディオンを開く if (currentIndex > 0) { new bootstrap.Collapse(accordions[currentIndex].querySelector('.accordion-collapse')).hide(); new bootstrap.Collapse(accordions[currentIndex - 1].querySelector('.accordion-collapse')).show(); } else { // もう前がない場合、ボタンを赤く点滅させる const prevButton = document.getElementById('prevAccordion'); prevButton.classList.add('red-flash-bg'); setTimeout(() => { prevButton.classList.remove('red-flash-bg'); }, 2000); } } function moveToInput() { const content1 = document.getElementById('novelContent1'); const content2 = document.getElementById('novelContent2'); let content1Lines = content1.value.trim().split('\n'); let content2Lines = content2.value.trim().split('\n'); // content1の最後の行とcontent2の先頭行が完全に一致する場合、content2から削除 if (content1Lines[content1Lines.length - 1] === content2Lines[0]) { content2Lines.shift(); } else { // 部分的な重複を検出して削除 const lastLine = content1Lines[content1Lines.length - 1]; const firstLine = content2Lines[0]; const overlapIndex = firstLine.indexOf(lastLine); if (overlapIndex !== -1) { content2Lines[0] = firstLine.slice(overlapIndex + lastLine.length).trim(); if (content2Lines[0] === '') { content2Lines.shift(); } } } // content2の内容をcontent1の末尾に追加 content1.value = content1Lines.join('\n') + '\n' + content2Lines.join('\n'); // content2を空にする content2.value = ''; // content1Collapseを開く const content1Collapse = new bootstrap.Collapse(document.getElementById('content1Collapse'), { show: true }); } document.addEventListener('DOMContentLoaded', function () { // ページ読み込み時にデータを復元 loadFromUserStorage(); // イベントリスナーの設定 ['novelContent1', 'novelContent2', 'generatePrompt', 'nextPrompt', 'memo', 'geminiApiKey', 'endpointSelect'].forEach(id => { document.getElementById(id).addEventListener('input', () => { saveToUserStorage(true); }); }); document.getElementById('novelContent1').addEventListener('keydown', handleKeyPress); document.querySelectorAll('[data-modal-text]').forEach(element => { element.addEventListener('click', function () { document.querySelectorAll(".modal-text").forEach(el => { el.classList.add("d-none"); if (el.classList.contains(this.getAttribute('data-modal-text'))) { el.classList.remove("d-none"); } }); }); }); syncInputs(); // 60秒ごとに自動保存実行 setInterval(() => { saveToUserStorage(); }, 60000); // 基本設定のアコーディオンを開く const basicSettingsAccordion = document.querySelector('#promptsCollapse'); if (basicSettingsAccordion) { new bootstrap.Collapse(basicSettingsAccordion).show(); } // ナビゲーションボタンのイベントリスナーを設定 document.getElementById('prevAccordion').addEventListener('click', openPreviousAccordion); document.getElementById('nextAccordion').addEventListener('click', openNextAccordion); });