hisaruki commited on
Commit
45da44d
·
1 Parent(s): c8e4e29
Files changed (2) hide show
  1. gemini.html +0 -220
  2. gemini.js +277 -83
gemini.html DELETED
@@ -1,220 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="ja">
3
-
4
- <head>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>LLM Client</title>
8
- <link href="https://unpkg.com/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
9
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
10
- crossorigin="anonymous">
11
- <style>
12
- textarea.form-control {
13
- min-height: 50vh;
14
- }
15
- .accordion-button:not(.collapsed) {
16
- background-color: #212529;
17
- color: #fff;
18
- }
19
- .navbar-toggler {
20
- display: block !important;
21
- }
22
- @keyframes redFlashBg {
23
- 0%, 100% { background-color: initial; }
24
- 50% { background-color: rgba(255, 0, 0, 0.5); }
25
- }
26
-
27
- @keyframes redFlashFg {
28
- 0%, 100% { color: initial; }
29
- 50% { color: red; }
30
- }
31
-
32
- .red-flash-bg {
33
- animation: redFlashBg 0.5s infinite;
34
- }
35
-
36
- .red-flash-fg {
37
- animation: redFlashFg 0.5s infinite;
38
- }
39
-
40
- @keyframes greenFlashBg {
41
- 0%, 100% { background-color: initial; }
42
- 50% { background-color: rgba(0, 255, 0, 0.5); }
43
- }
44
-
45
- @keyframes greenFlashFg {
46
- 0%, 100% { color: initial; }
47
- 50% { color: green; }
48
- }
49
-
50
- .green-flash-bg {
51
- animation: greenFlashBg 0.5s infinite;
52
- }
53
-
54
- .green-flash-fg {
55
- animation: greenFlashFg 0.5s infinite;
56
- }
57
-
58
- /* 新しいスタイルを追加 */
59
- .navbar {
60
- position: sticky;
61
- top: 0;
62
- z-index: 1000;
63
- }
64
-
65
- /* メインコンテンツの上部にパディングを追加 */
66
- #mainContent {
67
- padding-top: 1rem;
68
- }
69
- </style>
70
- </head>
71
-
72
- <body data-bs-theme="dark">
73
- <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
74
- <div class="container">
75
- <div class="row justify-content-center w-100">
76
- <div class="col-lg-7 d-flex justify-content-between align-items-center">
77
- <button class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#settingsOffcanvas" aria-controls="settingsOffcanvas">
78
- <span class="navbar-toggler-icon"></span>
79
- </button>
80
- <button id="prevAccordion" class="btn btn-sm btn-outline-light me-2">
81
- <i class="fas fa-chevron-left"></i> 前へ
82
- </button>
83
- <button id="nextAccordion" class="btn btn-sm btn-outline-light me-2">
84
- 次へ <i class="fas fa-chevron-right"></i>
85
- </button>
86
- <div class="input-group" style="max-width: 5rem;">
87
- <input type="range" class="form-range" id="encodeLength" placeholder="エンコード長" min="1" max="16" value="4">
88
- <input type="number" class="form-control" id="encodeLengthInput" placeholder="エンコード長" min="1" max="16" value="4">
89
- </div>
90
- <div class="form-check form-switch">
91
- <input class="form-check-input" type="checkbox" id="partialEncodeToggle">
92
- <label class="form-check-label" for="partialEncodeToggle">部分エンコード</label>
93
- </div>
94
- <button id="requestButton" class="btn btn-sm btn-primary" onclick="Request()">
95
- 続きを生成
96
- </button>
97
- <button id="stopButton" class="btn btn-sm btn-danger d-none" onclick="stopGeneration()">
98
- 中止
99
- </button>
100
- <a class="navbar-brand" href="#">LLM Client</a>
101
- </div>
102
- </div>
103
- </div>
104
- </nav>
105
-
106
- <div class="container">
107
- <div class="row justify-content-center">
108
- <div id="mainContent" class="col-lg-7">
109
- <div class="mt-3">
110
- <div class="accordion" id="mainAccordion">
111
- <div class="accordion-item">
112
- <h2 class="accordion-header">
113
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#promptsCollapse">
114
- 基本設定
115
- </button>
116
- </h2>
117
- <div id="promptsCollapse" class="accordion-collapse collapse" data-bs-parent="#mainAccordion">
118
- <div class="accordion-body">
119
- <textarea class="form-control mb-2" id="generatePrompt" placeholder="システムプロンプトを入力"></textarea>
120
- </div>
121
- </div>
122
- </div>
123
- <div class="accordion-item">
124
- <h2 class="accordion-header">
125
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#content1Collapse">
126
- 小説内容 (入力)
127
- </button>
128
- </h2>
129
- <div id="content1Collapse" class="accordion-collapse collapse" data-bs-parent="#mainAccordion">
130
- <div class="accordion-body">
131
- <textarea class="form-control mb-2" id="novelContent1" placeholder="ここに小説の本文を入力してください"></textarea>
132
- </div>
133
- </div>
134
- </div>
135
- <div class="accordion-item">
136
- <h2 class="accordion-header">
137
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#nextPromptCollapse">
138
- 次の展開
139
- </button>
140
- </h2>
141
- <div id="nextPromptCollapse" class="accordion-collapse collapse" data-bs-parent="#mainAccordion">
142
- <div class="accordion-body">
143
- <div class="d-flex justify-content-between align-items-center">
144
- <textarea class="form-control me-2" id="nextPrompt" placeholder="次の展開を指示" rows="1"></textarea>
145
- </div>
146
- </div>
147
- </div>
148
- </div>
149
- <div class="accordion-item">
150
- <h2 class="accordion-header">
151
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#content2Collapse">
152
- 小説内容 (出力)
153
- </button>
154
- </h2>
155
- <div id="content2Collapse" class="accordion-collapse collapse" data-bs-parent="#mainAccordion">
156
- <div class="accordion-body">
157
- <textarea class="form-control" id="novelContent2" placeholder="ここに続きが表示されます。"></textarea>
158
- <button id="moveToInputButton" class="btn btn-primary mt-2" onclick="moveToInput()">
159
- 入力欄へ移動
160
- </button>
161
- </div>
162
- </div>
163
- </div>
164
- </div>
165
-
166
- <div class="row mt-3">
167
- <div class="col-12 d-flex justify-content-end">
168
- <input type="text" class="form-control d-inline-block w-auto me-2" id="savedTitle" placeholder="タイトル">
169
- <button id="saveButton" class="btn btn-secondary me-2" onclick="saveToJson()">
170
- 保存
171
- </button>
172
- <button id="loadButton" class="btn btn-secondary" onclick="loadFromJson()">
173
- 読込
174
- </button>
175
- </div>
176
- </div>
177
- </div>
178
- </div>
179
- </div>
180
- </div>
181
-
182
- <div class="offcanvas offcanvas-start" tabindex="-1" id="settingsOffcanvas" aria-labelledby="settingsOffcanvasLabel">
183
- <div class="offcanvas-header">
184
- <h5 class="offcanvas-title" id="settingsOffcanvasLabel">設定</h5>
185
- <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
186
- </div>
187
- <div class="offcanvas-body">
188
- <h5>Gemini API Key</h5>
189
- <input type="text" class="form-control mb-2" id="geminiApiKey" placeholder="Gemini API Key">
190
-
191
- <h5>エンドポイント</h5>
192
- <select class="form-select mb-2" id="endpointSelect">
193
- <option value="models/gemini-1.5-pro-002">gemini-1.5-pro-002</option>
194
- <option value="models/gemini-1.5-flash-002">gemini-1.5-flash-002</option>
195
- <option value="models/gemini-1.5-pro-latest">gemini-1.5-pro-latest</option>
196
- <option value="models/gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
197
- </select>
198
-
199
- <div class="mb-2">
200
- <label for="characterCount" class="form-label">文字数</label>
201
- <input type="range" class="form-range" id="characterCount" min="64" max="8192" value="4096" step="64">
202
- <input type="number" class="form-control" id="characterCountInput" value="4096" min="64" max="8192" step="64">
203
- </div>
204
- <div class="form-check mb-2">
205
- <input class="form-check-input" type="checkbox" id="streamToggle" checked>
206
- <label class="form-check-label" for="streamToggle">Stream</label>
207
- </div>
208
- <button id="formatTextButton" class="btn btn-primary mb-2" onclick="formatText()">
209
- <i class="fa-solid fa-align-left"></i> 改行を整理
210
- </button>
211
- <h5 class="mt-4">メモ</h5>
212
- <textarea class="form-control" id="memo" placeholder="メモを記録する項目" rows="10"></textarea>
213
- </div>
214
- </div>
215
-
216
- <script src="https://unpkg.com/[email protected]/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
217
- <script src="gemini.js"></script>
218
- </body>
219
-
220
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
gemini.js CHANGED
@@ -1,5 +1,6 @@
1
  let lastSaveTimestamp = 0;
2
  let controller;
 
3
 
4
  function formatText() {
5
  const textOrg = document.getElementById('novelContent1').value;
@@ -33,35 +34,21 @@ function unmalform(text) {
33
  }
34
 
35
  function partialEncodeURI(text) {
36
- console.log('partialEncodeURI called');
37
- console.log('partialEncodeToggle checked:', document.getElementById("partialEncodeToggle").checked);
38
-
39
  if (!document.getElementById("partialEncodeToggle").checked) {
40
- console.log('Partial encode is disabled, returning original text');
41
  return text;
42
  }
43
-
44
  let length = parseInt(document.getElementById("encodeLength").value);
45
- console.log('Encode length:', length);
46
-
47
  const chunks = [];
48
  for (let i = 0; i < text.length; i += 1) {
49
  chunks.push(text.slice(i, i + 1));
50
  }
51
-
52
- console.log('Original text:', text);
53
-
54
  const encodedChunks = chunks.map((chunk, index) => {
55
  if (index % length === 0) {
56
- console.log('Encoding chunk at index', index, ':', chunk);
57
  return encodeURI(chunk);
58
  }
59
  return chunk;
60
  });
61
-
62
  const result = encodedChunks.join('');
63
- console.log('Encoded text:', result);
64
-
65
  return result;
66
  }
67
 
@@ -132,79 +119,91 @@ function loadFromJson() {
132
  }
133
 
134
  function saveToUserStorage(force = false) {
 
 
135
  const currentTime = Date.now();
136
  if (!force && currentTime - lastSaveTimestamp < 5000) {
137
  return;
138
  }
 
139
 
140
  const geminiClientData = {
141
- novelContent: document.getElementById('novelContent1').value,
 
142
  novelContent2: document.getElementById('novelContent2').value,
143
  generatePrompt: document.getElementById('generatePrompt').value,
144
  nextPrompt: document.getElementById('nextPrompt').value,
145
  savedTitle: document.getElementById('savedTitle').value,
146
- memo: document.getElementById('memo').value,
147
- geminiApiKey: document.getElementById('geminiApiKey').value,
148
- selectedEndpoint: document.getElementById('endpointSelect').value
149
  };
150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  localStorage.setItem('geminiClient', JSON.stringify(geminiClientData));
152
  lastSaveTimestamp = currentTime;
153
  }
154
 
155
  function loadFromUserStorage() {
 
156
  const savedData = localStorage.getItem('geminiClient');
157
  if (savedData) {
158
  const geminiClientData = JSON.parse(savedData);
159
- document.getElementById('novelContent1').value = geminiClientData.novelContent || '';
160
- document.getElementById('novelContent2').value = geminiClientData.novelContent2 || '';
161
- document.getElementById('generatePrompt').value = geminiClientData.generatePrompt || '';
162
- document.getElementById('nextPrompt').value = geminiClientData.nextPrompt || '';
163
- document.getElementById('savedTitle').value = geminiClientData.savedTitle || '';
164
- document.getElementById('memo').value = geminiClientData.memo || '';
165
- document.getElementById('geminiApiKey').value = geminiClientData.geminiApiKey || '';
166
- document.getElementById('endpointSelect').value = geminiClientData.selectedEndpoint || 'models/gemini-1.5-pro-002';
 
 
 
 
 
 
 
 
 
 
 
 
167
  }
 
168
  }
169
 
170
  function createPayload() {
171
  const novelContent1 = document.getElementById('novelContent1');
172
  const text = novelContent1.value;
173
  const lines = text.split('\n').filter(x => x);
174
- let lastPart = lines.pop() || '';
175
 
176
 
177
  let systemPrompt = `${partialEncodeURI(document.getElementById('generatePrompt').value)}`;
178
  let messages = [
179
  {
180
  "role": "user",
181
- "parts": [{ "text": systemPrompt || "." }]
182
- }
183
- ];
184
-
185
- if (lines.length > 0) {
186
- messages.push(
187
- {
188
- "role": "model",
189
- "parts": [{ "text": partialEncodeURI(lines.join("\n")) }]
190
- },
191
- {
192
- "role": "user",
193
- "parts": [{ "text": "続きを書いて。" }]
194
- }
195
- );
196
- }
197
-
198
- messages.push(
199
  {
200
  "role": "model",
201
- "parts": [{ "text": lastPart }]
202
  },
203
  {
204
  "role": "user",
205
- "parts": [{ "text": `続きを書いて。${partialEncodeURI(document.getElementById('nextPrompt').value)} ${document.getElementById('characterCountInput').value}文字程度。` }]
206
  }
207
- );
208
 
209
  return {
210
  method: 'POST',
@@ -238,6 +237,13 @@ function createPayload() {
238
  };
239
  }
240
 
 
 
 
 
 
 
 
241
  function fetchStream(ENDPOINT, payload) {
242
  const novelContent2 = document.getElementById('novelContent2');
243
  const requestButton = document.getElementById('requestButton');
@@ -283,24 +289,32 @@ function fetchStream(ENDPOINT, payload) {
283
  }
284
  try {
285
  const data = JSON.parse(jsonString);
286
- console.debug('Parsed JSON:', data);
287
  if (data.candidates && data.candidates[0].content && data.candidates[0].content.parts) {
288
  data.candidates[0].content.parts.forEach(part => {
289
  if (part.text) {
290
- console.debug('Adding text to output:', part.text);
291
  novelContent2.value += part.text;
292
  novelContent2.scrollTop = novelContent2.scrollHeight;
293
  }
294
  });
295
  }
296
- // finishReasonをチェック
297
- if (data.candidates && data.candidates[0].finishReason) {
298
- if (data.candidates[0].finishReason === 'STOP') {
299
- requestButton.classList.add('green-flash-bg');
300
- setTimeout(() => {
 
301
  requestButton.classList.remove('green-flash-bg');
302
- }, 2000);
303
- } else {
 
 
 
 
 
 
 
304
  requestButton.classList.add('red-flash-bg');
305
  setTimeout(() => {
306
  requestButton.classList.remove('red-flash-bg');
@@ -358,38 +372,181 @@ function fetchNonStream(ENDPOINT, payload) {
358
  });
359
  }
360
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
  function Request() {
362
  const selectedEndpoint = document.getElementById('endpointSelect').value;
363
- let ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/${selectedEndpoint}:generateContent?key=` + document.getElementById('geminiApiKey').value;
364
- try {
365
- if (!document.getElementById('geminiApiKey').value) {
366
- throw new Error("Gemini APIキーが設定されていません。");
367
- }
368
- } catch (e) {
369
- console.error(e);
370
- document.getElementById("geminiApiKey").classList.add("bg-danger");
371
- document.querySelector('[data-bs-toggle="offcanvas"]').click();
372
- return;
373
  }
374
- document.getElementById("geminiApiKey").classList.remove("bg-danger");
375
 
376
  document.getElementById('requestButton').disabled = true;
377
  let stream = document.getElementById('streamToggle').checked;
378
- document.getElementById('novelContent2').value = ''; // 新しい生成を開始する前に内容をクリア
379
-
380
- const payload = createPayload();
381
 
382
  if (stream) {
383
- // ここでエンドポイントを正しく変更
384
- ENDPOINT = ENDPOINT.replace(':generateContent', ':streamGenerateContent') + '&alt=sse';
385
- console.debug('Stream ENDPOINT:', ENDPOINT); // デバッグ用出力を追加
386
- fetchStream(ENDPOINT, payload);
 
 
387
  document.getElementById('stopButton').classList.remove('d-none');
388
  } else {
389
- fetchNonStream(ENDPOINT, payload);
 
 
 
 
390
  }
391
 
392
- // 出力のアコーディオンを開く
393
  const outputAccordion = document.querySelector('#content2Collapse');
394
  if (outputAccordion) {
395
  const bsCollapse = new bootstrap.Collapse(outputAccordion, { toggle: false });
@@ -430,7 +587,7 @@ function syncInputs() {
430
  function openNextAccordion() {
431
  const accordions = document.querySelectorAll('#mainAccordion .accordion-item');
432
  let currentIndex = -1;
433
-
434
  // 現在開いているアコーディオンのインデックスを見つける
435
  for (let i = 0; i < accordions.length; i++) {
436
  if (!accordions[i].querySelector('.accordion-button').classList.contains('collapsed')) {
@@ -438,7 +595,7 @@ function openNextAccordion() {
438
  break;
439
  }
440
  }
441
-
442
  // 次のアコーディオンを開く
443
  if (currentIndex < accordions.length - 1) {
444
  new bootstrap.Collapse(accordions[currentIndex].querySelector('.accordion-collapse')).hide();
@@ -456,7 +613,7 @@ function openNextAccordion() {
456
  function openPreviousAccordion() {
457
  const accordions = document.querySelectorAll('#mainAccordion .accordion-item');
458
  let currentIndex = -1;
459
-
460
  // 現在開いているアコーディオンのインデックスを見つける
461
  for (let i = 0; i < accordions.length; i++) {
462
  if (!accordions[i].querySelector('.accordion-button').classList.contains('collapsed')) {
@@ -464,7 +621,7 @@ function openPreviousAccordion() {
464
  break;
465
  }
466
  }
467
-
468
  // 前のアコーディオンを開く
469
  if (currentIndex > 0) {
470
  new bootstrap.Collapse(accordions[currentIndex].querySelector('.accordion-collapse')).hide();
@@ -514,16 +671,47 @@ function moveToInput() {
514
  });
515
  }
516
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
  document.addEventListener('DOMContentLoaded', function () {
518
  // ページ読み込み時にデータを復元
519
  loadFromUserStorage();
520
 
521
- // イベントリスナーの設定
522
- ['novelContent1', 'novelContent2', 'generatePrompt', 'nextPrompt', 'memo', 'geminiApiKey', 'endpointSelect'].forEach(id => {
 
 
 
 
 
 
 
523
  document.getElementById(id).addEventListener('input', () => {
524
  saveToUserStorage(true);
525
  });
526
  });
 
 
 
 
 
 
 
527
  document.getElementById('novelContent1').addEventListener('keydown', handleKeyPress);
528
 
529
  document.querySelectorAll('[data-modal-text]').forEach(element => {
@@ -553,4 +741,10 @@ document.addEventListener('DOMContentLoaded', function () {
553
  // ナビゲーションボタンのイベントリスナーを設定
554
  document.getElementById('prevAccordion').addEventListener('click', openPreviousAccordion);
555
  document.getElementById('nextAccordion').addEventListener('click', openNextAccordion);
 
 
 
 
 
 
556
  });
 
1
  let lastSaveTimestamp = 0;
2
  let controller;
3
+ let isLoading = false;
4
 
5
  function formatText() {
6
  const textOrg = document.getElementById('novelContent1').value;
 
34
  }
35
 
36
  function partialEncodeURI(text) {
 
 
 
37
  if (!document.getElementById("partialEncodeToggle").checked) {
 
38
  return text;
39
  }
 
40
  let length = parseInt(document.getElementById("encodeLength").value);
 
 
41
  const chunks = [];
42
  for (let i = 0; i < text.length; i += 1) {
43
  chunks.push(text.slice(i, i + 1));
44
  }
 
 
 
45
  const encodedChunks = chunks.map((chunk, index) => {
46
  if (index % length === 0) {
 
47
  return encodeURI(chunk);
48
  }
49
  return chunk;
50
  });
 
51
  const result = encodedChunks.join('');
 
 
52
  return result;
53
  }
54
 
 
119
  }
120
 
121
  function saveToUserStorage(force = false) {
122
+ if (isLoading) return;
123
+ console.debug('saveToUserStorage', force);
124
  const currentTime = Date.now();
125
  if (!force && currentTime - lastSaveTimestamp < 5000) {
126
  return;
127
  }
128
+ console.debug('セーブを実行します');
129
 
130
  const geminiClientData = {
131
+ // メイン画面の要素(forceがfalseの場合も保存)
132
+ novelContent1: document.getElementById('novelContent1').value,
133
  novelContent2: document.getElementById('novelContent2').value,
134
  generatePrompt: document.getElementById('generatePrompt').value,
135
  nextPrompt: document.getElementById('nextPrompt').value,
136
  savedTitle: document.getElementById('savedTitle').value,
 
 
 
137
  };
138
 
139
+ if (force) {
140
+ // 設定画面の要素(forceがtrueの場合のみ保存)
141
+ geminiClientData.memo = document.getElementById('memo').value;
142
+ geminiClientData.geminiApiKey = document.getElementById('geminiApiKey').value;
143
+ geminiClientData.endpointSelect = document.getElementById('endpointSelect').value;
144
+ geminiClientData.openaiEndpoint = document.getElementById('openaiEndpoint').value;
145
+ geminiClientData.openaiHeaders = document.getElementById('openaiHeaders').value;
146
+ geminiClientData.openaiJsonBody = document.getElementById('openaiJsonBody').value;
147
+ geminiClientData.characterCount = document.getElementById('characterCount').value;
148
+ geminiClientData.partialEncodeToggle = document.getElementById('partialEncodeToggle').checked;
149
+ geminiClientData.encodeLength = document.getElementById('encodeLength').value;
150
+ geminiClientData.streamToggle = document.getElementById('streamToggle').checked;
151
+ }
152
+
153
  localStorage.setItem('geminiClient', JSON.stringify(geminiClientData));
154
  lastSaveTimestamp = currentTime;
155
  }
156
 
157
  function loadFromUserStorage() {
158
+ isLoading = true;
159
  const savedData = localStorage.getItem('geminiClient');
160
  if (savedData) {
161
  const geminiClientData = JSON.parse(savedData);
162
+ Object.keys(geminiClientData).forEach(key => {
163
+ const elem = document.getElementById(key);
164
+ if (elem) {
165
+ if (elem.type === 'checkbox') {
166
+ elem.checked = geminiClientData[key];
167
+ } else {
168
+ elem.value = geminiClientData[key];
169
+ }
170
+
171
+ // 特別な処理が必要な要素
172
+ if (key === 'characterCount' || key === 'encodeLength' || key === 'contentWidth') {
173
+ const inputElem = document.getElementById(`${key}Input`);
174
+ if (inputElem) {
175
+ inputElem.value = geminiClientData[key];
176
+ }
177
+ }
178
+ } else {
179
+ console.debug(`要素が見つかりません: ${key}`);
180
+ }
181
+ });
182
  }
183
+ isLoading = false;
184
  }
185
 
186
  function createPayload() {
187
  const novelContent1 = document.getElementById('novelContent1');
188
  const text = novelContent1.value;
189
  const lines = text.split('\n').filter(x => x);
 
190
 
191
 
192
  let systemPrompt = `${partialEncodeURI(document.getElementById('generatePrompt').value)}`;
193
  let messages = [
194
  {
195
  "role": "user",
196
+ "parts": [{ "text": "." }]
197
+ },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  {
199
  "role": "model",
200
+ "parts": [{ "text": partialEncodeURI(lines.join("\n")) }]
201
  },
202
  {
203
  "role": "user",
204
+ "parts": [{ "text": `続きを書いて。${partialEncodeURI(document.getElementById('nextPrompt').value)} ${document.getElementById('characterCountInput').value}文字程度。${systemPrompt}` }]
205
  }
206
+ ];
207
 
208
  return {
209
  method: 'POST',
 
237
  };
238
  }
239
 
240
+ function debugPrompt() {
241
+ console.log({
242
+ "gemini": JSON.parse(createPayload().body),
243
+ "openai": JSON.parse(createOpenAIPayload().body)
244
+ });
245
+ }
246
+
247
  function fetchStream(ENDPOINT, payload) {
248
  const novelContent2 = document.getElementById('novelContent2');
249
  const requestButton = document.getElementById('requestButton');
 
289
  }
290
  try {
291
  const data = JSON.parse(jsonString);
292
+ console.debug('解析されたJSON:', data);
293
  if (data.candidates && data.candidates[0].content && data.candidates[0].content.parts) {
294
  data.candidates[0].content.parts.forEach(part => {
295
  if (part.text) {
296
+ console.debug('出力にテキストを追加:', part.text);
297
  novelContent2.value += part.text;
298
  novelContent2.scrollTop = novelContent2.scrollHeight;
299
  }
300
  });
301
  }
302
+ // finishReasonとblockReasonをチェック
303
+ if (data.candidates && data.candidates[0]) {
304
+ if (data.candidates[0].finishReason) {
305
+ if (data.candidates[0].finishReason === 'STOP') {
306
+ requestButton.classList.add('green-flash-bg');
307
+ setTimeout(() => {
308
  requestButton.classList.remove('green-flash-bg');
309
+ }, 2000);
310
+ }else{
311
+ requestButton.classList.add('red-flash-bg');
312
+ setTimeout(() => {
313
+ requestButton.classList.remove('red-flash-bg');
314
+ }, 2000);
315
+ }
316
+ }
317
+ if (data.candidates[0].blockReason) {
318
  requestButton.classList.add('red-flash-bg');
319
  setTimeout(() => {
320
  requestButton.classList.remove('red-flash-bg');
 
372
  });
373
  }
374
 
375
+ function createOpenAIPayload() {
376
+ const novelContent1 = document.getElementById('novelContent1');
377
+ const text = novelContent1.value;
378
+ const lines = text.split('\n').filter(x => x);
379
+ let lastPart = lines.pop() || '';
380
+
381
+ let messages = [
382
+ {
383
+ "content": document.getElementById('generatePrompt').value || ".",
384
+ "role": "system"
385
+ },
386
+ {
387
+ "content": ".",
388
+ "role": "user"
389
+ },
390
+ {
391
+ "content": partialEncodeURI(lines.join("\n")) || ".",
392
+ "role": "assistant"
393
+ },
394
+ {
395
+ "content": `続きを${document.getElementById('characterCountInput').value}文字程度で書いてください。${partialEncodeURI(document.getElementById('nextPrompt').value)}`,
396
+ "role": "user"
397
+ },
398
+ {
399
+ "content": lastPart,
400
+ "role": "assistant"
401
+ }
402
+ ];
403
+
404
+ let jsonBody = JSON.parse(document.getElementById('openaiJsonBody').value);
405
+ jsonBody.messages = messages;
406
+ jsonBody.stream = document.getElementById('streamToggle').checked;
407
+
408
+ return {
409
+ method: 'POST',
410
+ headers: JSON.parse(document.getElementById('openaiHeaders').value),
411
+ body: JSON.stringify(jsonBody),
412
+ mode: 'cors', // CORSモードを追加
413
+ credentials: 'same-origin' // 必要に応じて認証情報を含める
414
+ };
415
+ }
416
+
417
+ function fetchOpenAIStream(ENDPOINT, payload) {
418
+ const novelContent2 = document.getElementById('novelContent2');
419
+ const requestButton = document.getElementById('requestButton');
420
+ controller = new AbortController();
421
+ const signal = controller.signal;
422
+
423
+ fetch(ENDPOINT, {
424
+ ...payload,
425
+ signal,
426
+ mode: 'cors', // CORSモードを追加
427
+ credentials: 'same-origin' // 必要に応じて認証情報を含める
428
+ })
429
+ .then(response => {
430
+ if (!response.ok) {
431
+ throw new Error('ネットワークの応答が正常ではありません');
432
+ }
433
+ const reader = response.body.getReader();
434
+ const decoder = new TextDecoder();
435
+ let buffer = '';
436
+
437
+ function readStream() {
438
+ reader.read().then(({ done, value }) => {
439
+ if (done) {
440
+ console.debug('ストリームが完了しました');
441
+ document.getElementById('stopButton').classList.add('d-none');
442
+ requestButton.disabled = false;
443
+ return;
444
+ }
445
+
446
+ const chunk = decoder.decode(value, { stream: true });
447
+ buffer += chunk;
448
+
449
+ const lines = buffer.split('\n');
450
+ buffer = lines.pop();
451
+
452
+ lines.forEach(line => {
453
+ if (line.startsWith('data: ')) {
454
+ const jsonString = line.slice(6);
455
+ if (jsonString === '[DONE]') {
456
+ console.debug('Received [DONE] signal');
457
+ return;
458
+ }
459
+ try {
460
+ const data = JSON.parse(jsonString);
461
+ if (data.choices && data.choices[0].delta && data.choices[0].delta.content) {
462
+ novelContent2.value += data.choices[0].delta.content;
463
+ novelContent2.scrollTop = novelContent2.scrollHeight;
464
+ }
465
+ } catch (error) {
466
+ console.error('JSONパースエラー:', error);
467
+ }
468
+ }
469
+ });
470
+
471
+ readStream();
472
+ }).catch(error => {
473
+ if (error.name === 'AbortError') {
474
+ console.log('フェッチがユーザーによって中止されました');
475
+ } else {
476
+ console.error('ストリーム読み取りエラー:', error);
477
+ }
478
+ document.getElementById('stopButton').classList.add('d-none');
479
+ requestButton.disabled = false;
480
+ });
481
+ }
482
+
483
+ readStream();
484
+ })
485
+ .catch(error => {
486
+ if (error.name === 'AbortError') {
487
+ console.log('フェッチがユーザーよって中止されました');
488
+ } else {
489
+ console.error('フェッチエラー:', error);
490
+ }
491
+ requestButton.disabled = false;
492
+ });
493
+ }
494
+
495
+ function fetchOpenAINonStream(ENDPOINT, payload) {
496
+ const novelContent2 = document.getElementById('novelContent2');
497
+ fetch(ENDPOINT, {
498
+ ...payload,
499
+ mode: 'cors', // CORSモードを追加
500
+ credentials: 'same-origin' // 必要に応じて認証情報を含める
501
+ })
502
+ .then(response => response.json())
503
+ .then(data => {
504
+ if (data && data.choices && data.choices[0].message && data.choices[0].message.content) {
505
+ novelContent2.value += data.choices[0].message.content;
506
+ novelContent2.scrollTop = novelContent2.scrollHeight;
507
+ }
508
+ })
509
+ .catch(error => {
510
+ console.error('エラー:', error);
511
+ })
512
+ .finally(() => {
513
+ document.getElementById('requestButton').disabled = false;
514
+ });
515
+ }
516
+
517
  function Request() {
518
  const selectedEndpoint = document.getElementById('endpointSelect').value;
519
+ let ENDPOINT;
520
+ let payload;
521
+
522
+ if (selectedEndpoint.startsWith('models/gemini')) {
523
+ ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/${selectedEndpoint}:generateContent?key=` + document.getElementById('geminiApiKey').value;
524
+ payload = createPayload();
525
+ } else {
526
+ ENDPOINT = document.getElementById('openaiEndpoint').value;
527
+ payload = createOpenAIPayload();
 
528
  }
 
529
 
530
  document.getElementById('requestButton').disabled = true;
531
  let stream = document.getElementById('streamToggle').checked;
532
+ document.getElementById('novelContent2').value = '';
 
 
533
 
534
  if (stream) {
535
+ if (selectedEndpoint.startsWith('models/gemini')) {
536
+ ENDPOINT = ENDPOINT.replace(':generateContent', ':streamGenerateContent') + '&alt=sse';
537
+ fetchStream(ENDPOINT, payload);
538
+ } else {
539
+ fetchOpenAIStream(ENDPOINT, payload);
540
+ }
541
  document.getElementById('stopButton').classList.remove('d-none');
542
  } else {
543
+ if (selectedEndpoint.startsWith('models/gemini')) {
544
+ fetchNonStream(ENDPOINT, payload);
545
+ } else {
546
+ fetchOpenAINonStream(ENDPOINT, payload);
547
+ }
548
  }
549
 
 
550
  const outputAccordion = document.querySelector('#content2Collapse');
551
  if (outputAccordion) {
552
  const bsCollapse = new bootstrap.Collapse(outputAccordion, { toggle: false });
 
587
  function openNextAccordion() {
588
  const accordions = document.querySelectorAll('#mainAccordion .accordion-item');
589
  let currentIndex = -1;
590
+
591
  // 現在開いているアコーディオンのインデックスを見つける
592
  for (let i = 0; i < accordions.length; i++) {
593
  if (!accordions[i].querySelector('.accordion-button').classList.contains('collapsed')) {
 
595
  break;
596
  }
597
  }
598
+
599
  // 次のアコーディオンを開く
600
  if (currentIndex < accordions.length - 1) {
601
  new bootstrap.Collapse(accordions[currentIndex].querySelector('.accordion-collapse')).hide();
 
613
  function openPreviousAccordion() {
614
  const accordions = document.querySelectorAll('#mainAccordion .accordion-item');
615
  let currentIndex = -1;
616
+
617
  // 現在開いているアコーディオンのインデックスを見つける
618
  for (let i = 0; i < accordions.length; i++) {
619
  if (!accordions[i].querySelector('.accordion-button').classList.contains('collapsed')) {
 
621
  break;
622
  }
623
  }
624
+
625
  // 前のアコーディオンを開く
626
  if (currentIndex > 0) {
627
  new bootstrap.Collapse(accordions[currentIndex].querySelector('.accordion-collapse')).hide();
 
671
  });
672
  }
673
 
674
+ function updateNavbarBrand() {
675
+ const endpointSelect = document.getElementById('endpointSelect');
676
+ const navbarBrand = document.querySelector('.navbar-brand');
677
+ const googleIcon = navbarBrand.querySelector('.fa-google');
678
+ const robotIcon = navbarBrand.querySelector('.fa-robot');
679
+
680
+ if (endpointSelect.value.startsWith('models/gemini')) {
681
+ navbarBrand.style.color = '#4285F4'; // Googleブルー
682
+ googleIcon.classList.remove('d-none');
683
+ robotIcon.classList.add('d-none');
684
+ } else {
685
+ navbarBrand.style.color = '#00FF00'; // 明るい緑色
686
+ googleIcon.classList.add('d-none');
687
+ robotIcon.classList.remove('d-none');
688
+ }
689
+ }
690
+
691
  document.addEventListener('DOMContentLoaded', function () {
692
  // ページ読み込み時にデータを復元
693
  loadFromUserStorage();
694
 
695
+ // メイン画面の要素のイベントリスナー
696
+ ['novelContent1', 'novelContent2', 'generatePrompt', 'nextPrompt', 'savedTitle'].forEach(id => {
697
+ document.getElementById(id).addEventListener('input', () => {
698
+ saveToUserStorage(false);
699
+ });
700
+ });
701
+
702
+ // 設定画面の要素のイベントリスナー
703
+ ['memo', 'geminiApiKey', 'endpointSelect', 'openaiEndpoint', 'openaiHeaders', 'openaiJsonBody', 'characterCount', 'encodeLength'].forEach(id => {
704
  document.getElementById(id).addEventListener('input', () => {
705
  saveToUserStorage(true);
706
  });
707
  });
708
+
709
+ ['partialEncodeToggle', 'streamToggle'].forEach(id => {
710
+ document.getElementById(id).addEventListener('change', () => {
711
+ saveToUserStorage(true);
712
+ });
713
+ });
714
+
715
  document.getElementById('novelContent1').addEventListener('keydown', handleKeyPress);
716
 
717
  document.querySelectorAll('[data-modal-text]').forEach(element => {
 
741
  // ナビゲーションボタンのイベントリスナーを設定
742
  document.getElementById('prevAccordion').addEventListener('click', openPreviousAccordion);
743
  document.getElementById('nextAccordion').addEventListener('click', openNextAccordion);
744
+
745
+ // エンドポイント選択が変更されたときにnavbar-brandを更新
746
+ document.getElementById('endpointSelect').addEventListener('change', updateNavbarBrand);
747
+
748
+ // 初期表示時にも実行
749
+ updateNavbarBrand();
750
  });