|
let lastSaveTimestamp = 0; |
|
let controller; |
|
let lastTokenUpdateTimestamp = 0; |
|
let summeries = {}; |
|
|
|
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 || ''; |
|
} |
|
|
|
async function summerize(text) { |
|
const ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-002:generateContent?key=${document.getElementById('geminiApiKey').value}`; |
|
const prompt = `以下の文章を240文字程度に要約してください:\n\n${text}`; |
|
const payload = { |
|
method: 'POST', |
|
headers: {}, |
|
body: JSON.stringify({ |
|
contents: [{ parts: [{ text: prompt }] }], |
|
generationConfig: { temperature: 0.7, max_output_tokens: 256 } |
|
}) |
|
}; |
|
try { |
|
const response = await fetch(ENDPOINT, payload); |
|
const data = await response.json(); |
|
return data.candidates[0].content.parts[0].text; |
|
} catch (error) { |
|
console.error('要約エラー:', error); |
|
return ''; |
|
} |
|
} |
|
|
|
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) { |
|
const currentTime = Date.now(); |
|
if (currentTime - lastSaveTimestamp < 5000 && !force) { |
|
console.debug('セーブをスキップします'); |
|
return; |
|
} |
|
console.debug('セーブを実行します'); |
|
|
|
|
|
const geminiClientData = JSON.parse(localStorage.getItem('geminiClient') || '{}'); |
|
|
|
const newData = {}; |
|
Array.from(document.querySelectorAll("input[id], textarea[id], select[id]")).forEach(el => { |
|
if (el.id) { |
|
newData[el.id] = el.type === 'checkbox' ? el.checked : el.value; |
|
} |
|
}); |
|
Object.assign(geminiClientData, newData); |
|
console.log(geminiClientData); |
|
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 createSummarizedText() { |
|
const indexOffcanvasBody = document.querySelector('#indexOffcanvas .offcanvas-body'); |
|
const rootUl = indexOffcanvasBody.querySelector('ul.list-unstyled'); |
|
let summarizedText = ''; |
|
|
|
function processUl(ul, level = 0) { |
|
const items = ul.children; |
|
for (let item of items) { |
|
const a = item.querySelector(':scope > a'); |
|
if (a) { |
|
summarizedText += '#'.repeat(level + 1) + ' ' + a.textContent + '\n'; |
|
} |
|
|
|
const contentItem = item.querySelector(':scope > ul > li'); |
|
if (contentItem) { |
|
const fullText = contentItem.querySelector('.full-text'); |
|
const summaryText = contentItem.querySelector('.summery-text'); |
|
if (summaryText && summaryText.value.trim()) { |
|
summarizedText += summaryText.value + '\n\n'; |
|
} else if (fullText) { |
|
summarizedText += fullText.value + '\n\n'; |
|
} |
|
} |
|
|
|
const subUl = item.querySelector(':scope > ul'); |
|
if (subUl) { |
|
processUl(subUl, level + 1); |
|
} |
|
} |
|
} |
|
|
|
if (rootUl) { |
|
processUl(rootUl); |
|
} |
|
if (summarizedText) { |
|
return summarizedText.trim(); |
|
} else { |
|
return document.getElementById('novelContent1').value; |
|
} |
|
} |
|
|
|
function createPayload() { |
|
const novelContent1 = document.getElementById('novelContent1'); |
|
let text = novelContent1.value; |
|
if (document.getElementById('summerizedPromptToggle').checked) { |
|
text = createSummarizedText(); |
|
} |
|
const lines = text.split('\n').filter(x => x); |
|
|
|
let systemPrompt = `${partialEncodeURI(document.getElementById('generatePrompt').value)}`; |
|
let prompt = `続きを書いて。${partialEncodeURI(document.getElementById('nextPrompt').value)} ${document.getElementById('characterCountInput').value}文字程度。${systemPrompt}`; |
|
let messages = [ |
|
{ |
|
"role": "user", |
|
"parts": [{ "text": "." }] |
|
}, |
|
{ |
|
"role": "model", |
|
"parts": [{ "text": partialEncodeURI(lines.join("\n")) }] |
|
}, |
|
{ |
|
"role": "user", |
|
"parts": [{ "text": prompt }] |
|
} |
|
]; |
|
|
|
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 updateRequestButtonState(state, flashClass = null) { |
|
const requestButton = document.getElementById('requestButton'); |
|
const stopButton = document.getElementById('stopButton'); |
|
|
|
switch (state) { |
|
case 'generating': |
|
requestButton.disabled = true; |
|
stopButton.classList.remove('d-none'); |
|
break; |
|
case 'idle': |
|
requestButton.disabled = false; |
|
stopButton.classList.add('d-none'); |
|
break; |
|
case 'error': |
|
requestButton.disabled = false; |
|
stopButton.classList.add('d-none'); |
|
break; |
|
} |
|
|
|
if (flashClass) { |
|
requestButton.classList.add(flashClass); |
|
setTimeout(() => { |
|
requestButton.classList.remove(flashClass); |
|
}, 2000); |
|
} |
|
} |
|
|
|
function fetchStream(ENDPOINT, payload) { |
|
const novelContent2 = document.getElementById('novelContent2'); |
|
updateRequestButtonState('generating'); |
|
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); |
|
|
|
|
|
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; |
|
} |
|
}); |
|
} |
|
|
|
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('フェッチがユーザーによって中止されました'); |
|
updateRequestButtonState('idle'); |
|
} else { |
|
console.error('ストリーム読み取りエラー:', error); |
|
updateRequestButtonState('error', 'red-flash-bg'); |
|
} |
|
}); |
|
} |
|
|
|
readStream(); |
|
}) |
|
.catch(error => { |
|
if (error.name === 'AbortError') { |
|
console.log('フェッチがユーザーよって中止されました'); |
|
updateRequestButtonState('idle'); |
|
} else { |
|
console.error('フェッチエラー:', error); |
|
updateRequestButtonState('error', 'red-flash-bg'); |
|
} |
|
}); |
|
} |
|
|
|
async function fetchNonStream(ENDPOINT, payload) { |
|
const novelContent2 = document.getElementById('novelContent2'); |
|
updateRequestButtonState('generating'); |
|
try { |
|
const response = await fetch(ENDPOINT, payload); |
|
const data = await response.json(); |
|
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; |
|
updateRequestButtonState('idle', 'green-flash-bg'); |
|
} else { |
|
throw new Error('予期しないレスポンス形式'); |
|
} |
|
} catch (error) { |
|
console.error('エラー:', error); |
|
updateRequestButtonState('error', 'red-flash-bg'); |
|
} |
|
} |
|
|
|
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', |
|
credentials: 'same-origin' |
|
}; |
|
} |
|
|
|
function fetchOpenAIStream(ENDPOINT, payload) { |
|
const novelContent2 = document.getElementById('novelContent2'); |
|
updateRequestButtonState('generating'); |
|
controller = new AbortController(); |
|
const signal = controller.signal; |
|
|
|
fetch(ENDPOINT, { |
|
...payload, |
|
signal, |
|
mode: '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('フェッチがユーザーによって中止されました'); |
|
updateRequestButtonState('idle'); |
|
} else { |
|
console.error('ストリーム読み取りエラー:', error); |
|
updateRequestButtonState('error', 'red-flash-bg'); |
|
} |
|
}); |
|
} |
|
|
|
readStream(); |
|
}) |
|
.catch(error => { |
|
if (error.name === 'AbortError') { |
|
console.log('フェッチがユーザーよって中止されました'); |
|
updateRequestButtonState('idle'); |
|
} else { |
|
console.error('フェッチエラー:', error); |
|
updateRequestButtonState('error', 'red-flash-bg'); |
|
} |
|
}); |
|
} |
|
|
|
async function fetchOpenAINonStream(ENDPOINT, payload) { |
|
const novelContent2 = document.getElementById('novelContent2'); |
|
updateRequestButtonState('generating'); |
|
try { |
|
const response = await fetch(ENDPOINT, { |
|
...payload, |
|
mode: 'cors', |
|
credentials: 'same-origin' |
|
}); |
|
const data = await response.json(); |
|
if (data && data.choices && data.choices[0].message && data.choices[0].message.content) { |
|
novelContent2.value += data.choices[0].message.content; |
|
novelContent2.scrollTop = novelContent2.scrollHeight; |
|
updateRequestButtonState('idle', 'green-flash-bg'); |
|
} else { |
|
throw new Error('予期しないレスポンス形式'); |
|
} |
|
} catch (error) { |
|
console.error('エラー:', error); |
|
updateRequestButtonState('error', 'red-flash-bg'); |
|
} |
|
} |
|
|
|
async function tokenCount() { |
|
const selectedEndpoint = document.getElementById('endpointSelect').value; |
|
let payload = createPayload(); |
|
payload.body = { |
|
"contents": JSON.parse(payload.body).contents |
|
}; |
|
payload.body = JSON.stringify(payload.body); |
|
const ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/${selectedEndpoint}:countTokens?key=` + document.getElementById('geminiApiKey').value; |
|
try { |
|
const response = await fetch(ENDPOINT, payload); |
|
const data = await response.json(); |
|
return data.totalTokens; |
|
} catch (error) { |
|
console.error('エラー:', error); |
|
return null; |
|
} |
|
} |
|
|
|
async function createDraft() { |
|
const ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-002:generateContent?key=` + document.getElementById('geminiApiKey').value; |
|
let payload = createPayload(); |
|
const response = await fetch(ENDPOINT, payload); |
|
const data = await response.json(); |
|
const text = data.candidates[0].content.parts[0].text; |
|
return text |
|
} |
|
|
|
|
|
async function Request() { |
|
let selectedEndpoint = document.getElementById('endpointSelect').value; |
|
const requestButton = document.getElementById('requestButton'); |
|
requestButton.disabled = true; |
|
document.getElementById('novelContent2').value = ''; |
|
const outputAccordion = document.querySelector('#content2Collapse'); |
|
if (outputAccordion) { |
|
const bsCollapse = new bootstrap.Collapse(outputAccordion, { toggle: false }); |
|
bsCollapse.show(); |
|
} |
|
|
|
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 if (selectedEndpoint === 'restart') { |
|
ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro-002:generateContent?key=` + document.getElementById('geminiApiKey').value; |
|
document.getElementById('novelContent2').value = '(下書き中)'; |
|
try { |
|
draft = await createDraft(); |
|
} catch (error) { |
|
console.error('エラー:', error); |
|
requestButton.disabled = false; |
|
requestButton.classList.add('green-flash-bg'); |
|
setTimeout(() => { |
|
requestButton.classList.remove('green-flash-bg'); |
|
}, 2000); |
|
return; |
|
} |
|
|
|
document.getElementById('novelContent2').value = ""; |
|
const prompt = `以下の文章を、要件を満たす形に直したものを返信してください。\n返信内容は直した後のテキストのみになるようにし他の内容が混ざらないようにしてください。\n\n# 要件\n- 元の文章や会話の言い回しは不自然な日本語になっている部分もかなり多いので、根本的に書き替えること。ただし、登場人物の一人称や他者への呼称は変更しないこと。\n- 物語の大筋やキャラクターの性格は保ったまま、文章は全く別物に書き換える必要がある。元のテキストそのものはなるべく流用しないで新規に書くよう心掛ける。\n- 随所にレトリックを駆使しつつも凝り過ぎて読み辛くなることのないシンプルな美文になることを意識する。\n- 登場人物の会話を重視し、文脈を読み取り元のキャラクター設定に合った台詞回しを保ちつつ、より生き生きとした魅力的な人物像に仕上がるようにする。\n- 細かい動作や心理描写のディテールを重視し、よりリアルな描写になるようにする。\n- 文章の終わりに「。」をつける、字下げをするなど、一般的な小説のフォーマットに従う書き方にする。\n\n# 文章\n${draft}`; |
|
payload = { |
|
method: 'POST', |
|
headers: {}, |
|
body: JSON.stringify({ |
|
contents: [ |
|
{ |
|
"parts": [ |
|
{ |
|
"text": prompt |
|
} |
|
], |
|
"role": "user" |
|
} |
|
], |
|
"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' |
|
}; |
|
selectedEndpoint = 'models/gemini-1.5-pro-002'; |
|
} else { |
|
ENDPOINT = document.getElementById('openaiEndpoint').value; |
|
payload = createOpenAIPayload(); |
|
} |
|
|
|
let stream = document.getElementById('streamToggle').checked; |
|
|
|
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); |
|
} |
|
} |
|
} |
|
|
|
function stopGeneration() { |
|
if (controller) { |
|
controller.abort(); |
|
controller = null; |
|
} |
|
updateRequestButtonState('idle'); |
|
} |
|
|
|
|
|
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'); |
|
|
|
|
|
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(); |
|
} |
|
} |
|
} |
|
|
|
|
|
content1.value = content1Lines.join('\n') + '\n' + content2Lines.join('\n'); |
|
|
|
|
|
content2.value = ''; |
|
|
|
|
|
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'; |
|
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'); |
|
} |
|
} |
|
|
|
async function updateTokenCount(force = false) { |
|
const currentTime = Date.now(); |
|
if (currentTime - lastTokenUpdateTimestamp < 60000 && !force) { |
|
console.debug('トークン数更新をスキップします'); |
|
return; |
|
} |
|
console.debug('トークン数更新を実行します'); |
|
|
|
const count = await tokenCount(); |
|
const indexOffcanvasLabel = document.getElementById('indexOffcanvasLabel'); |
|
indexOffcanvasLabel.textContent = `目次 (${count}トークン)`; |
|
lastTokenUpdateTimestamp = currentTime; |
|
} |
|
|
|
function generateIndexMenu() { |
|
const content = document.getElementById('novelContent1').value; |
|
const tokens = marked.lexer(content); |
|
const indexOffcanvasBody = document.querySelector('#indexOffcanvas .offcanvas-body'); |
|
|
|
indexOffcanvasBody.innerHTML = ''; |
|
|
|
const rootUl = document.createElement('ul'); |
|
rootUl.className = 'list-unstyled'; |
|
|
|
let stack = [{ ul: rootUl, level: 0 }]; |
|
let lastHeading = null; |
|
let contentBuffer = ''; |
|
|
|
tokens.forEach((token, index) => { |
|
if (token.type === 'heading') { |
|
if (lastHeading && contentBuffer.trim()) { |
|
addTextarea(lastHeading.querySelector('ul'), contentBuffer.trim()); |
|
} |
|
contentBuffer = ''; |
|
|
|
while (stack.length > 1 && stack[stack.length - 1].level >= token.depth) { |
|
stack.pop(); |
|
} |
|
|
|
const li = document.createElement('li'); |
|
const toggleBtn = document.createElement('button'); |
|
toggleBtn.className = 'btn btn-sm btn-outline-secondary me-2 toggle-btn'; |
|
const icon = document.createElement('i'); |
|
icon.className = 'fas fa-plus'; |
|
toggleBtn.appendChild(icon); |
|
toggleBtn.onclick = () => toggleSubMenu(li); |
|
|
|
const a = document.createElement('a'); |
|
a.href = '#'; |
|
a.textContent = token.text; |
|
a.onclick = (e) => { |
|
e.preventDefault(); |
|
scrollToHeading(token.text); |
|
}; |
|
|
|
li.appendChild(toggleBtn); |
|
li.appendChild(a); |
|
|
|
const subUl = document.createElement('ul'); |
|
subUl.className = 'list-unstyled ms-3 d-none'; |
|
li.appendChild(subUl); |
|
|
|
stack[stack.length - 1].ul.appendChild(li); |
|
|
|
if (token.depth > stack[stack.length - 1].level) { |
|
stack.push({ ul: subUl, level: token.depth }); |
|
} |
|
|
|
lastHeading = li; |
|
} else if (token.type === 'text' || token.type === 'paragraph') { |
|
contentBuffer += token.text + '\n'; |
|
} |
|
}); |
|
|
|
if (lastHeading && contentBuffer.trim()) { |
|
addTextarea(lastHeading.querySelector('ul'), contentBuffer.trim()); |
|
} |
|
|
|
if (rootUl.children.length > 0) { |
|
indexOffcanvasBody.appendChild(rootUl); |
|
} else { |
|
indexOffcanvasBody.textContent = '目次がありません'; |
|
} |
|
|
|
updateAllAccordionHeaderCounts(); |
|
updateTokenCount(false); |
|
} |
|
|
|
function toggleSubMenu(li) { |
|
const subUl = li.querySelector('ul'); |
|
const toggleBtn = li.querySelector('.toggle-btn'); |
|
const icon = toggleBtn.querySelector('i'); |
|
subUl.classList.toggle('d-none'); |
|
icon.className = subUl.classList.contains('d-none') ? 'fas fa-plus' : 'fas fa-minus'; |
|
} |
|
|
|
function addTextarea(ul, content) { |
|
const li = document.createElement('li'); |
|
|
|
|
|
const textarea = document.createElement('textarea'); |
|
textarea.readOnly = true; |
|
textarea.className = 'form-control mt-2 full-text'; |
|
textarea.value = content; |
|
textarea.rows = 3; |
|
|
|
|
|
const summaryInput = document.createElement('textarea'); |
|
summaryInput.className = 'form-control mt-2 summery-text'; |
|
summaryInput.placeholder = '要約'; |
|
summaryInput.rows = 3; |
|
if(summeries[content]) { |
|
summaryInput.value = summeries[content]; |
|
} |
|
|
|
|
|
const buttonContainer = document.createElement('div'); |
|
buttonContainer.className = 'mt-2'; |
|
|
|
|
|
const summaryButton = document.createElement('button'); |
|
summaryButton.textContent = '要約を取得'; |
|
summaryButton.className = 'btn btn-secondary me-2'; |
|
summaryButton.onclick = async () => { |
|
summaryButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Loading...'; |
|
summaryButton.disabled = true; |
|
try { |
|
const summary = await summerize(content); |
|
summaryInput.value = summary; |
|
summeries[content] = summary; |
|
updateTokenCount(true); |
|
} finally { |
|
summaryButton.innerHTML = '要約を取得'; |
|
summaryButton.disabled = false; |
|
} |
|
}; |
|
|
|
|
|
const deleteSummaryButton = document.createElement('button'); |
|
deleteSummaryButton.textContent = '要約を削除'; |
|
deleteSummaryButton.className = 'btn btn-danger'; |
|
deleteSummaryButton.onclick = () => { |
|
summaryInput.value = ''; |
|
delete summeries[content]; |
|
updateTokenCount(true); |
|
}; |
|
|
|
|
|
buttonContainer.appendChild(summaryButton); |
|
buttonContainer.appendChild(deleteSummaryButton); |
|
|
|
|
|
li.appendChild(textarea); |
|
li.appendChild(summaryInput); |
|
li.appendChild(buttonContainer); |
|
ul.appendChild(li); |
|
} |
|
|
|
function scrollToHeading(headingText) { |
|
const content = document.getElementById('novelContent1'); |
|
const lines = content.value.split('\n'); |
|
let position = 0; |
|
|
|
for (let i = 0; i < lines.length; i++) { |
|
if (lines[i].trim().startsWith('#') && lines[i].includes(headingText)) { |
|
|
|
openAccordionContainingPosition(position); |
|
|
|
content.focus(); |
|
content.setSelectionRange(position, position); |
|
content.scrollTop = content.scrollHeight * (position / content.value.length); |
|
break; |
|
} |
|
position += lines[i].length + 1; |
|
} |
|
|
|
|
|
const indexOffcanvas = bootstrap.Offcanvas.getInstance(document.getElementById('indexOffcanvas')); |
|
} |
|
|
|
function openAccordionContainingPosition(position) { |
|
const content = document.getElementById('novelContent1'); |
|
const accordionItems = document.querySelectorAll('#mainAccordion .accordion-item'); |
|
let currentPosition = 0; |
|
|
|
for (let i = 0; i < accordionItems.length; i++) { |
|
const textArea = accordionItems[i].querySelector('textarea'); |
|
if (textArea && textArea.id === 'novelContent1') { |
|
if (position >= currentPosition && position < currentPosition + textArea.value.length) { |
|
|
|
const collapseElement = accordionItems[i].querySelector('.accordion-collapse'); |
|
const bsCollapse = new bootstrap.Collapse(collapseElement, { toggle: false }); |
|
bsCollapse.show(); |
|
break; |
|
} |
|
currentPosition += textArea.value.length; |
|
} |
|
} |
|
} |
|
|
|
function updateAccordionHeaderCount(accordionId) { |
|
const accordionItem = document.getElementById(accordionId).closest('.accordion-item'); |
|
if (!accordionItem) return; |
|
|
|
const textarea = accordionItem.querySelector('.accordion-body textarea'); |
|
const header = accordionItem.querySelector('.accordion-header button'); |
|
|
|
if (textarea && header) { |
|
const charCount = textarea.value.length; |
|
const originalText = header.textContent.split('(')[0].trim(); |
|
header.textContent = `${originalText} (${charCount}文字)`; |
|
} |
|
} |
|
|
|
function updateAllAccordionHeaderCounts() { |
|
const accordionIds = ['promptsCollapse', 'content1Collapse', 'nextPromptCollapse', 'content2Collapse']; |
|
accordionIds.forEach(updateAccordionHeaderCount); |
|
} |
|
|
|
document.addEventListener('DOMContentLoaded', function () { |
|
|
|
loadFromUserStorage(); |
|
|
|
|
|
['novelContent1', 'novelContent2', 'generatePrompt', 'nextPrompt', 'savedTitle'].forEach(id => { |
|
document.getElementById(id).addEventListener('input', () => { |
|
saveToUserStorage(false); |
|
generateIndexMenu(); |
|
}); |
|
}); |
|
|
|
|
|
['memo', 'geminiApiKey', 'endpointSelect', 'openaiEndpoint', 'openaiHeaders', 'openaiJsonBody', 'characterCount', 'encodeLength'].forEach(id => { |
|
document.getElementById(id).addEventListener('input', () => { |
|
saveToUserStorage(true); |
|
generateIndexMenu(); |
|
}); |
|
}); |
|
|
|
['partialEncodeToggle', 'streamToggle'].forEach(id => { |
|
document.getElementById(id).addEventListener('change', () => { |
|
saveToUserStorage(true); |
|
generateIndexMenu(); |
|
}); |
|
}); |
|
|
|
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(); |
|
|
|
|
|
setInterval(() => { |
|
saveToUserStorage(); |
|
generateIndexMenu(); |
|
updateTokenCount(true); |
|
}, 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); |
|
|
|
|
|
document.getElementById('endpointSelect').addEventListener('change', updateNavbarBrand); |
|
|
|
|
|
updateNavbarBrand(); |
|
generateIndexMenu(); |
|
updateAllAccordionHeaderCounts(); |
|
updateTokenCount(true); |
|
}); |