LLMClient / gemini.js
SenY's picture
Upload 2 files
1d0ec46 verified
raw
history blame
29.3 kB
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();
});