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) { if (!document.getElementById("partialEncodeToggle").checked) { return text; } let length = parseInt(document.getElementById("encodeLength").value); const chunks = []; for (let i = 0; i < text.length; i += 1) { chunks.push(text.slice(i, i + 1)); } const encodedChunks = chunks.map((chunk, index) => { if (index % length === 0) { return encodeURI(chunk); } return chunk; }); const result = encodedChunks.join(''); 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) { console.debug('saveToUserStorage', force); const currentTime = Date.now(); if (!force && currentTime - lastSaveTimestamp < 5000) { return; } console.debug('セーブを実行します'); const geminiClientData = { // メイン画面の要素(forceがfalseの場合も保存) novelContent1: document.getElementById('novelContent1').value, novelContent2: document.getElementById('novelContent2').value, generatePrompt: document.getElementById('generatePrompt').value, nextPrompt: document.getElementById('nextPrompt').value, savedTitle: document.getElementById('savedTitle').value, }; if (force) { // 設定画面の要素(forceがtrueの場合のみ保存) geminiClientData.memo = document.getElementById('memo').value; geminiClientData.geminiApiKey = document.getElementById('geminiApiKey').value; geminiClientData.endpointSelect = document.getElementById('endpointSelect').value; geminiClientData.openaiEndpoint = document.getElementById('openaiEndpoint').value; geminiClientData.openaiHeaders = document.getElementById('openaiHeaders').value; geminiClientData.openaiJsonBody = document.getElementById('openaiJsonBody').value; geminiClientData.characterCount = document.getElementById('characterCount').value; geminiClientData.partialEncodeToggle = document.getElementById('partialEncodeToggle').checked; geminiClientData.encodeLength = document.getElementById('encodeLength').value; geminiClientData.streamToggle = document.getElementById('streamToggle').checked; } localStorage.setItem('geminiClient', JSON.stringify(geminiClientData)); lastSaveTimestamp = currentTime; } function loadFromUserStorage() { const savedData = localStorage.getItem('geminiClient'); if (savedData) { const geminiClientData = JSON.parse(savedData); Object.keys(geminiClientData).forEach(key => { const elem = document.getElementById(key); if (elem) { if (elem.type === 'checkbox') { elem.checked = geminiClientData[key]; } else { elem.value = geminiClientData[key]; } // 特別な処理が必要な要素 if (key === 'characterCount' || key === 'encodeLength' || key === 'contentWidth') { const inputElem = document.getElementById(`${key}Input`); if (inputElem) { inputElem.value = geminiClientData[key]; } } } else { console.debug(`要素が見つかりません: ${key}`); } }); } } function createPayload() { const novelContent1 = document.getElementById('novelContent1'); const text = novelContent1.value; const lines = text.split('\n').filter(x => x); let systemPrompt = `${partialEncodeURI(document.getElementById('generatePrompt').value)}`; let messages = [ { "role": "user", "parts": [{ "text": "." }] }, { "role": "model", "parts": [{ "text": partialEncodeURI(lines.join("\n")) }] }, { "role": "user", "parts": [{ "text": `続きを書いて。${partialEncodeURI(document.getElementById('nextPrompt').value)} ${document.getElementById('characterCountInput').value}文字程度。${systemPrompt}` }] } ]; 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 debugPrompt() { console.log({ "gemini": JSON.parse(createPayload().body), "openai": JSON.parse(createOpenAIPayload().body) }); } 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('解析された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('出力にテキストを追加:', part.text); novelContent2.value += part.text; novelContent2.scrollTop = novelContent2.scrollHeight; } }); } // finishReasonとblockReasonをチェック if (data.candidates && data.candidates[0]) { if (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); } } if (data.candidates[0].blockReason) { 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 createOpenAIPayload() { const novelContent1 = document.getElementById('novelContent1'); const text = novelContent1.value; const lines = text.split('\n').filter(x => x); let lastPart = lines.pop() || ''; let messages = [ { "content": document.getElementById('generatePrompt').value || ".", "role": "system" }, { "content": ".", "role": "user" }, { "content": partialEncodeURI(lines.join("\n")) || ".", "role": "assistant" }, { "content": `続きを${document.getElementById('characterCountInput').value}文字程度で書いてください。${partialEncodeURI(document.getElementById('nextPrompt').value)}`, "role": "user" }, { "content": lastPart, "role": "assistant" } ]; let jsonBody = JSON.parse(document.getElementById('openaiJsonBody').value); jsonBody.messages = messages; jsonBody.stream = document.getElementById('streamToggle').checked; return { method: 'POST', headers: JSON.parse(document.getElementById('openaiHeaders').value), body: JSON.stringify(jsonBody), mode: 'cors', // CORSモードを追加 credentials: 'same-origin' // 必要に応じて認証情報を含める }; } function fetchOpenAIStream(ENDPOINT, payload) { const novelContent2 = document.getElementById('novelContent2'); const requestButton = document.getElementById('requestButton'); controller = new AbortController(); const signal = controller.signal; fetch(ENDPOINT, { ...payload, signal, mode: 'cors', // CORSモードを追加 credentials: 'same-origin' // 必要に応じて認証情報を含める }) .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; const lines = buffer.split('\n'); buffer = lines.pop(); lines.forEach(line => { if (line.startsWith('data: ')) { const jsonString = line.slice(6); if (jsonString === '[DONE]') { console.debug('Received [DONE] signal'); return; } try { const data = JSON.parse(jsonString); if (data.choices && data.choices[0].delta && data.choices[0].delta.content) { novelContent2.value += data.choices[0].delta.content; novelContent2.scrollTop = novelContent2.scrollHeight; } } catch (error) { console.error('JSONパースエラー:', error); } } }); 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 fetchOpenAINonStream(ENDPOINT, payload) { const novelContent2 = document.getElementById('novelContent2'); fetch(ENDPOINT, { ...payload, mode: 'cors', // CORSモードを追加 credentials: 'same-origin' // 必要に応じて認証情報を含める }) .then(response => response.json()) .then(data => { if (data && data.choices && data.choices[0].message && data.choices[0].message.content) { novelContent2.value += data.choices[0].message.content; 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; let payload; if (selectedEndpoint.startsWith('models/gemini')) { ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/${selectedEndpoint}:generateContent?key=` + document.getElementById('geminiApiKey').value; payload = createPayload(); } else { ENDPOINT = document.getElementById('openaiEndpoint').value; payload = createOpenAIPayload(); } document.getElementById('requestButton').disabled = true; let stream = document.getElementById('streamToggle').checked; document.getElementById('novelContent2').value = ''; if (stream) { if (selectedEndpoint.startsWith('models/gemini')) { ENDPOINT = ENDPOINT.replace(':generateContent', ':streamGenerateContent') + '&alt=sse'; fetchStream(ENDPOINT, payload); } else { fetchOpenAIStream(ENDPOINT, payload); } document.getElementById('stopButton').classList.remove('d-none'); } else { if (selectedEndpoint.startsWith('models/gemini')) { fetchNonStream(ENDPOINT, payload); } else { fetchOpenAINonStream(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 }); } function updateNavbarBrand() { const endpointSelect = document.getElementById('endpointSelect'); const navbarBrand = document.querySelector('.navbar-brand'); const googleIcon = navbarBrand.querySelector('.fa-google'); const robotIcon = navbarBrand.querySelector('.fa-robot'); if (endpointSelect.value.startsWith('models/gemini')) { navbarBrand.style.color = '#4285F4'; // Googleブルー googleIcon.classList.remove('d-none'); robotIcon.classList.add('d-none'); } else { navbarBrand.style.color = '#00FF00'; // 明るい緑色 googleIcon.classList.add('d-none'); robotIcon.classList.remove('d-none'); } } document.addEventListener('DOMContentLoaded', function () { // ページ読み込み時にデータを復元 loadFromUserStorage(); // メイン画面の要素のイベントリスナー ['novelContent1', 'novelContent2', 'generatePrompt', 'nextPrompt', 'savedTitle'].forEach(id => { document.getElementById(id).addEventListener('input', () => { saveToUserStorage(false); }); }); // 設定画面の要素のイベントリスナー ['memo', 'geminiApiKey', 'endpointSelect', 'openaiEndpoint', 'openaiHeaders', 'openaiJsonBody', 'characterCount', 'encodeLength'].forEach(id => { document.getElementById(id).addEventListener('input', () => { saveToUserStorage(true); }); }); ['partialEncodeToggle', 'streamToggle'].forEach(id => { document.getElementById(id).addEventListener('change', () => { 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); // エンドポイント選択が変更されたときにnavbar-brandを更新 document.getElementById('endpointSelect').addEventListener('change', updateNavbarBrand); // 初期表示時にも実行 updateNavbarBrand(); });