SenY commited on
Commit
1d0ec46
1 Parent(s): d131a6f

Upload 2 files

Browse files
Files changed (2) hide show
  1. gemini.js +746 -0
  2. index.html +320 -116
gemini.js ADDED
@@ -0,0 +1,746 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ let lastSaveTimestamp = 0;
2
+ let controller;
3
+
4
+ function formatText() {
5
+ const textOrg = document.getElementById('novelContent1').value;
6
+ let text = textOrg.replace(/[」。)]/g, '$&\n');
7
+ while (text.includes('\n\n')) {
8
+ text = text.replace(/\n\n/g, '\n');
9
+ }
10
+ text = text.replace(/「([^」\n]*)\n([^」\n]*)」/g, '「$1$2」');
11
+ text = text.replace(/(([^)\n]*)\n([^)\n]*))/g, '($1$2)');
12
+
13
+ while (text.search(/「[^「\n]*。\n/) >= 0) {
14
+ text = text.replace(/「([^「\n]*。)\n/, '「$1');
15
+ }
16
+
17
+ text = text.replace(/\n/g, "\n\n");
18
+ text = text.replace(/\n#/g, "\n\n#");
19
+
20
+ document.getElementById('novelContent1').value = text;
21
+ }
22
+
23
+ function unmalform(text) {
24
+ let result = null;
25
+ while (!result && text) {
26
+ try {
27
+ result = decodeURI(text);
28
+ } catch (error) {
29
+ text = text.slice(0, -1);
30
+ }
31
+ }
32
+ return result || '';
33
+ }
34
+
35
+ function partialEncodeURI(text) {
36
+ if (!document.getElementById("partialEncodeToggle").checked) {
37
+ return text;
38
+ }
39
+ let length = parseInt(document.getElementById("encodeLength").value);
40
+ const chunks = [];
41
+ for (let i = 0; i < text.length; i += 1) {
42
+ chunks.push(text.slice(i, i + 1));
43
+ }
44
+ const encodedChunks = chunks.map((chunk, index) => {
45
+ if (index % length === 0) {
46
+ return encodeURI(chunk);
47
+ }
48
+ return chunk;
49
+ });
50
+ const result = encodedChunks.join('');
51
+ return result;
52
+ }
53
+
54
+ function saveToJson() {
55
+ const novelContent1 = document.getElementById('novelContent1').value;
56
+ const novelContent2 = document.getElementById('novelContent2').value;
57
+ const generatePrompt = document.getElementById('generatePrompt').value;
58
+ const nextPrompt = document.getElementById('nextPrompt').value;
59
+ const savedTitle = document.getElementById('savedTitle').value;
60
+ const jsonData = JSON.stringify({
61
+ novelContent1: novelContent1,
62
+ novelContent2: novelContent2,
63
+ generatePrompt: generatePrompt,
64
+ nextPrompt: nextPrompt,
65
+ savedTitle: savedTitle
66
+ });
67
+ const blob = new Blob([jsonData], { type: 'application/json' });
68
+ const url = URL.createObjectURL(blob);
69
+ const a = document.createElement('a');
70
+ a.href = url;
71
+ a.download = 'novel_data.json';
72
+ if (savedTitle) {
73
+ a.download = savedTitle + '.json';
74
+ }
75
+ document.body.appendChild(a);
76
+ a.click();
77
+ document.body.removeChild(a);
78
+ URL.revokeObjectURL(url);
79
+ }
80
+
81
+ function loadFromJson() {
82
+ const fileInput = document.createElement('input');
83
+ fileInput.type = 'file';
84
+ fileInput.accept = '.json';
85
+ fileInput.style.display = 'none';
86
+ document.body.appendChild(fileInput);
87
+ fileInput.addEventListener('change', function (event) {
88
+ const file = event.target.files[0];
89
+ if (file) {
90
+ const reader = new FileReader();
91
+ reader.onload = function (e) {
92
+ try {
93
+ const jsonData = JSON.parse(e.target.result);
94
+ if (jsonData.novelContent1) {
95
+ document.getElementById('novelContent1').value = jsonData.novelContent1;
96
+ }
97
+ if (jsonData.novelContent2) {
98
+ document.getElementById('novelContent2').value = jsonData.novelContent2;
99
+ }
100
+ if (jsonData.generatePrompt) {
101
+ document.getElementById('generatePrompt').value = jsonData.generatePrompt;
102
+ }
103
+ if (jsonData.nextPrompt) {
104
+ document.getElementById('nextPrompt').value = jsonData.nextPrompt;
105
+ }
106
+ if (jsonData.savedTitle) {
107
+ document.getElementById('savedTitle').value = jsonData.savedTitle;
108
+ }
109
+ alert('JSONファイルを正常に読み込みました。');
110
+ } catch (error) {
111
+ alert('無効なJSONファイルです。');
112
+ }
113
+ };
114
+ reader.readAsText(file);
115
+ }
116
+ });
117
+ fileInput.click();
118
+ }
119
+
120
+ function saveToUserStorage(force = false) {
121
+ console.debug('saveToUserStorage', force);
122
+ const currentTime = Date.now();
123
+ if (!force && currentTime - lastSaveTimestamp < 5000) {
124
+ return;
125
+ }
126
+ console.debug('セーブを実行します');
127
+
128
+ const geminiClientData = {
129
+ // メイン画面の要素(forceがfalseの場合も保存)
130
+ novelContent1: document.getElementById('novelContent1').value,
131
+ novelContent2: document.getElementById('novelContent2').value,
132
+ generatePrompt: document.getElementById('generatePrompt').value,
133
+ nextPrompt: document.getElementById('nextPrompt').value,
134
+ savedTitle: document.getElementById('savedTitle').value,
135
+ };
136
+
137
+ if (force) {
138
+ // 設定画面の要素(forceがtrueの場合のみ保存)
139
+ geminiClientData.memo = document.getElementById('memo').value;
140
+ geminiClientData.geminiApiKey = document.getElementById('geminiApiKey').value;
141
+ geminiClientData.endpointSelect = document.getElementById('endpointSelect').value;
142
+ geminiClientData.openaiEndpoint = document.getElementById('openaiEndpoint').value;
143
+ geminiClientData.openaiHeaders = document.getElementById('openaiHeaders').value;
144
+ geminiClientData.openaiJsonBody = document.getElementById('openaiJsonBody').value;
145
+ geminiClientData.characterCount = document.getElementById('characterCount').value;
146
+ geminiClientData.partialEncodeToggle = document.getElementById('partialEncodeToggle').checked;
147
+ geminiClientData.encodeLength = document.getElementById('encodeLength').value;
148
+ geminiClientData.streamToggle = document.getElementById('streamToggle').checked;
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
+ Object.keys(geminiClientData).forEach(key => {
160
+ const elem = document.getElementById(key);
161
+ if (elem) {
162
+ if (elem.type === 'checkbox') {
163
+ elem.checked = geminiClientData[key];
164
+ } else {
165
+ elem.value = geminiClientData[key];
166
+ }
167
+
168
+ // 特別な処理が必要な要素
169
+ if (key === 'characterCount' || key === 'encodeLength' || key === 'contentWidth') {
170
+ const inputElem = document.getElementById(`${key}Input`);
171
+ if (inputElem) {
172
+ inputElem.value = geminiClientData[key];
173
+ }
174
+ }
175
+ } else {
176
+ console.debug(`要素が見つかりません: ${key}`);
177
+ }
178
+ });
179
+ }
180
+ }
181
+
182
+ function createPayload() {
183
+ const novelContent1 = document.getElementById('novelContent1');
184
+ const text = novelContent1.value;
185
+ const lines = text.split('\n').filter(x => x);
186
+
187
+
188
+ let systemPrompt = `${partialEncodeURI(document.getElementById('generatePrompt').value)}`;
189
+ let messages = [
190
+ {
191
+ "role": "user",
192
+ "parts": [{ "text": "." }]
193
+ },
194
+ {
195
+ "role": "model",
196
+ "parts": [{ "text": partialEncodeURI(lines.join("\n")) }]
197
+ },
198
+ {
199
+ "role": "user",
200
+ "parts": [{ "text": `続きを書いて。${partialEncodeURI(document.getElementById('nextPrompt').value)} ${document.getElementById('characterCountInput').value}文字程度。${systemPrompt}` }]
201
+ }
202
+ ];
203
+
204
+ return {
205
+ method: 'POST',
206
+ headers: {},
207
+ body: JSON.stringify({
208
+ contents: messages,
209
+ "generationConfig": {
210
+ "temperature": 1.0,
211
+ "max_output_tokens": 4096
212
+ },
213
+ safetySettings: [
214
+ {
215
+ "category": "HARM_CATEGORY_HATE_SPEECH",
216
+ "threshold": "BLOCK_NONE"
217
+ },
218
+ {
219
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
220
+ "threshold": "BLOCK_NONE"
221
+ },
222
+ {
223
+ "category": "HARM_CATEGORY_HARASSMENT",
224
+ "threshold": "BLOCK_NONE"
225
+ },
226
+ {
227
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
228
+ "threshold": "BLOCK_NONE"
229
+ }
230
+ ]
231
+ }),
232
+ mode: 'cors'
233
+ };
234
+ }
235
+
236
+ function debugPrompt() {
237
+ console.log({
238
+ "gemini": JSON.parse(createPayload().body),
239
+ "openai": JSON.parse(createOpenAIPayload().body)
240
+ });
241
+ }
242
+
243
+ function fetchStream(ENDPOINT, payload) {
244
+ const novelContent2 = document.getElementById('novelContent2');
245
+ const requestButton = document.getElementById('requestButton');
246
+ controller = new AbortController();
247
+ const signal = controller.signal;
248
+
249
+ fetch(ENDPOINT, { ...payload, signal })
250
+ .then(response => {
251
+ if (!response.ok) {
252
+ throw new Error('ネットワークの応答が正常ではありません');
253
+ }
254
+ const reader = response.body.getReader();
255
+ const decoder = new TextDecoder();
256
+ let buffer = '';
257
+
258
+ function readStream() {
259
+ reader.read().then(({ done, value }) => {
260
+ if (done) {
261
+ console.debug('ストリームが完了しました');
262
+ document.getElementById('stopButton').classList.add('d-none');
263
+ requestButton.disabled = false;
264
+ return;
265
+ }
266
+
267
+ const chunk = decoder.decode(value, { stream: true });
268
+ buffer += chunk;
269
+ console.debug('チャンクを受信しました:', chunk);
270
+
271
+ // バッファから完全なJSONオブジェクトを抽出して処理
272
+ let startIndex = 0;
273
+ while (true) {
274
+ const endIndex = buffer.indexOf('\n', startIndex);
275
+ if (endIndex === -1) break;
276
+
277
+ const line = buffer.slice(startIndex, endIndex).trim();
278
+ startIndex = endIndex + 1;
279
+
280
+ if (line.startsWith('data: ')) {
281
+ const jsonString = line.slice(5);
282
+ if (jsonString === '[DONE]') {
283
+ console.debug('Received [DONE] signal');
284
+ break;
285
+ }
286
+ try {
287
+ const data = JSON.parse(jsonString);
288
+ console.debug('解析されたJSON:', data);
289
+ if (data.candidates && data.candidates[0].content && data.candidates[0].content.parts) {
290
+ data.candidates[0].content.parts.forEach(part => {
291
+ if (part.text) {
292
+ console.debug('出力にテキストを追加:', part.text);
293
+ novelContent2.value += part.text;
294
+ novelContent2.scrollTop = novelContent2.scrollHeight;
295
+ }
296
+ });
297
+ }
298
+ // finishReasonとblockReasonをチェック
299
+ if (data.candidates && data.candidates[0]) {
300
+ if (data.candidates[0].finishReason) {
301
+ if (data.candidates[0].finishReason === 'STOP') {
302
+ requestButton.classList.add('green-flash-bg');
303
+ setTimeout(() => {
304
+ requestButton.classList.remove('green-flash-bg');
305
+ }, 2000);
306
+ }else{
307
+ requestButton.classList.add('red-flash-bg');
308
+ setTimeout(() => {
309
+ requestButton.classList.remove('red-flash-bg');
310
+ }, 2000);
311
+ }
312
+ }
313
+ if (data.candidates[0].blockReason) {
314
+ requestButton.classList.add('red-flash-bg');
315
+ setTimeout(() => {
316
+ requestButton.classList.remove('red-flash-bg');
317
+ }, 2000);
318
+ }
319
+ }
320
+ } catch (error) {
321
+ console.error('JSONパースエラー:', error);
322
+ }
323
+ }
324
+ }
325
+
326
+ // 処理済みの部分をバッファから削除
327
+ buffer = buffer.slice(startIndex);
328
+
329
+ readStream();
330
+ }).catch(error => {
331
+ if (error.name === 'AbortError') {
332
+ console.log('フェッチがユーザーによって中止されました');
333
+ } else {
334
+ console.error('ストリーム読み取りエラー:', error);
335
+ }
336
+ document.getElementById('stopButton').classList.add('d-none');
337
+ requestButton.disabled = false;
338
+ });
339
+ }
340
+
341
+ readStream();
342
+ })
343
+ .catch(error => {
344
+ if (error.name === 'AbortError') {
345
+ console.log('フェッチがユーザーよって中止されました');
346
+ } else {
347
+ console.error('フェッチエラー:', error);
348
+ }
349
+ requestButton.disabled = false;
350
+ });
351
+ }
352
+
353
+ function fetchNonStream(ENDPOINT, payload) {
354
+ const novelContent2 = document.getElementById('novelContent2');
355
+ fetch(ENDPOINT, payload)
356
+ .then(response => response.json())
357
+ .then(data => {
358
+ if (data && data.candidates && data.candidates[0].content && data.candidates[0].content.parts && data.candidates[0].content.parts[0].text) {
359
+ novelContent2.value += data.candidates[0].content.parts[0].text;
360
+ novelContent2.scrollTop = novelContent2.scrollHeight;
361
+ }
362
+ })
363
+ .catch(error => {
364
+ console.error('エラー:', error);
365
+ })
366
+ .finally(() => {
367
+ document.getElementById('requestButton').disabled = false;
368
+ });
369
+ }
370
+
371
+ function createOpenAIPayload() {
372
+ const novelContent1 = document.getElementById('novelContent1');
373
+ const text = novelContent1.value;
374
+ const lines = text.split('\n').filter(x => x);
375
+ let lastPart = lines.pop() || '';
376
+
377
+ let messages = [
378
+ {
379
+ "content": document.getElementById('generatePrompt').value || ".",
380
+ "role": "system"
381
+ },
382
+ {
383
+ "content": ".",
384
+ "role": "user"
385
+ },
386
+ {
387
+ "content": partialEncodeURI(lines.join("\n")) || ".",
388
+ "role": "assistant"
389
+ },
390
+ {
391
+ "content": `続きを${document.getElementById('characterCountInput').value}文字程度で書いてください。${partialEncodeURI(document.getElementById('nextPrompt').value)}`,
392
+ "role": "user"
393
+ },
394
+ {
395
+ "content": lastPart,
396
+ "role": "assistant"
397
+ }
398
+ ];
399
+
400
+ let jsonBody = JSON.parse(document.getElementById('openaiJsonBody').value);
401
+ jsonBody.messages = messages;
402
+ jsonBody.stream = document.getElementById('streamToggle').checked;
403
+
404
+ return {
405
+ method: 'POST',
406
+ headers: JSON.parse(document.getElementById('openaiHeaders').value),
407
+ body: JSON.stringify(jsonBody),
408
+ mode: 'cors', // CORSモードを追加
409
+ credentials: 'same-origin' // 必要に応じて認証情報を含める
410
+ };
411
+ }
412
+
413
+ function fetchOpenAIStream(ENDPOINT, payload) {
414
+ const novelContent2 = document.getElementById('novelContent2');
415
+ const requestButton = document.getElementById('requestButton');
416
+ controller = new AbortController();
417
+ const signal = controller.signal;
418
+
419
+ fetch(ENDPOINT, {
420
+ ...payload,
421
+ signal,
422
+ mode: 'cors', // CORSモードを追加
423
+ credentials: 'same-origin' // 必要に応じて認証情報を含める
424
+ })
425
+ .then(response => {
426
+ if (!response.ok) {
427
+ throw new Error('ネットワークの応答が正常ではありません');
428
+ }
429
+ const reader = response.body.getReader();
430
+ const decoder = new TextDecoder();
431
+ let buffer = '';
432
+
433
+ function readStream() {
434
+ reader.read().then(({ done, value }) => {
435
+ if (done) {
436
+ console.debug('ストリームが完了しました');
437
+ document.getElementById('stopButton').classList.add('d-none');
438
+ requestButton.disabled = false;
439
+ return;
440
+ }
441
+
442
+ const chunk = decoder.decode(value, { stream: true });
443
+ buffer += chunk;
444
+
445
+ const lines = buffer.split('\n');
446
+ buffer = lines.pop();
447
+
448
+ lines.forEach(line => {
449
+ if (line.startsWith('data: ')) {
450
+ const jsonString = line.slice(6);
451
+ if (jsonString === '[DONE]') {
452
+ console.debug('Received [DONE] signal');
453
+ return;
454
+ }
455
+ try {
456
+ const data = JSON.parse(jsonString);
457
+ if (data.choices && data.choices[0].delta && data.choices[0].delta.content) {
458
+ novelContent2.value += data.choices[0].delta.content;
459
+ novelContent2.scrollTop = novelContent2.scrollHeight;
460
+ }
461
+ } catch (error) {
462
+ console.error('JSONパースエラー:', error);
463
+ }
464
+ }
465
+ });
466
+
467
+ readStream();
468
+ }).catch(error => {
469
+ if (error.name === 'AbortError') {
470
+ console.log('フェッチがユーザーによって中止されました');
471
+ } else {
472
+ console.error('ストリーム読み取りエラー:', error);
473
+ }
474
+ document.getElementById('stopButton').classList.add('d-none');
475
+ requestButton.disabled = false;
476
+ });
477
+ }
478
+
479
+ readStream();
480
+ })
481
+ .catch(error => {
482
+ if (error.name === 'AbortError') {
483
+ console.log('フェッチがユーザーよって中止されました');
484
+ } else {
485
+ console.error('フェッチエラー:', error);
486
+ }
487
+ requestButton.disabled = false;
488
+ });
489
+ }
490
+
491
+ function fetchOpenAINonStream(ENDPOINT, payload) {
492
+ const novelContent2 = document.getElementById('novelContent2');
493
+ fetch(ENDPOINT, {
494
+ ...payload,
495
+ mode: 'cors', // CORSモードを追加
496
+ credentials: 'same-origin' // 必要に応じて認証情報を含める
497
+ })
498
+ .then(response => response.json())
499
+ .then(data => {
500
+ if (data && data.choices && data.choices[0].message && data.choices[0].message.content) {
501
+ novelContent2.value += data.choices[0].message.content;
502
+ novelContent2.scrollTop = novelContent2.scrollHeight;
503
+ }
504
+ })
505
+ .catch(error => {
506
+ console.error('エラー:', error);
507
+ })
508
+ .finally(() => {
509
+ document.getElementById('requestButton').disabled = false;
510
+ });
511
+ }
512
+
513
+ function Request() {
514
+ const selectedEndpoint = document.getElementById('endpointSelect').value;
515
+ let ENDPOINT;
516
+ let payload;
517
+
518
+ if (selectedEndpoint.startsWith('models/gemini')) {
519
+ ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/${selectedEndpoint}:generateContent?key=` + document.getElementById('geminiApiKey').value;
520
+ payload = createPayload();
521
+ } else {
522
+ ENDPOINT = document.getElementById('openaiEndpoint').value;
523
+ payload = createOpenAIPayload();
524
+ }
525
+
526
+ document.getElementById('requestButton').disabled = true;
527
+ let stream = document.getElementById('streamToggle').checked;
528
+ document.getElementById('novelContent2').value = '';
529
+
530
+ if (stream) {
531
+ if (selectedEndpoint.startsWith('models/gemini')) {
532
+ ENDPOINT = ENDPOINT.replace(':generateContent', ':streamGenerateContent') + '&alt=sse';
533
+ fetchStream(ENDPOINT, payload);
534
+ } else {
535
+ fetchOpenAIStream(ENDPOINT, payload);
536
+ }
537
+ document.getElementById('stopButton').classList.remove('d-none');
538
+ } else {
539
+ if (selectedEndpoint.startsWith('models/gemini')) {
540
+ fetchNonStream(ENDPOINT, payload);
541
+ } else {
542
+ fetchOpenAINonStream(ENDPOINT, payload);
543
+ }
544
+ }
545
+
546
+ const outputAccordion = document.querySelector('#content2Collapse');
547
+ if (outputAccordion) {
548
+ const bsCollapse = new bootstrap.Collapse(outputAccordion, { toggle: false });
549
+ bsCollapse.show();
550
+ }
551
+ }
552
+
553
+ function stopGeneration() {
554
+ if (controller) {
555
+ controller.abort();
556
+ controller = null;
557
+ }
558
+ document.getElementById('stopButton').classList.add('d-none');
559
+ document.getElementById('requestButton').disabled = false;
560
+ }
561
+
562
+ // 新しい関数を追加
563
+ function handleKeyPress(event) {
564
+ if (event.ctrlKey && event.key === 'Enter') {
565
+ Request();
566
+ }
567
+ }
568
+
569
+ function syncInputs() {
570
+ const inputs = document.querySelectorAll('input[type="range"], input[type="number"]');
571
+ inputs.forEach(input => {
572
+ const baseId = input.id.replace('Input', '');
573
+ const pairedInput = document.getElementById(baseId + (input.type === 'range' ? 'Input' : ''));
574
+
575
+ if (pairedInput) {
576
+ input.addEventListener('input', function () {
577
+ pairedInput.value = this.value;
578
+ });
579
+ }
580
+ });
581
+ }
582
+
583
+ function openNextAccordion() {
584
+ const accordions = document.querySelectorAll('#mainAccordion .accordion-item');
585
+ let currentIndex = -1;
586
+
587
+ // 現在開いているアコーディオンのインデックスを見つける
588
+ for (let i = 0; i < accordions.length; i++) {
589
+ if (!accordions[i].querySelector('.accordion-button').classList.contains('collapsed')) {
590
+ currentIndex = i;
591
+ break;
592
+ }
593
+ }
594
+
595
+ // 次のアコーディオンを開く
596
+ if (currentIndex < accordions.length - 1) {
597
+ new bootstrap.Collapse(accordions[currentIndex].querySelector('.accordion-collapse')).hide();
598
+ new bootstrap.Collapse(accordions[currentIndex + 1].querySelector('.accordion-collapse')).show();
599
+ } else {
600
+ // もう次がない場合、ボタンを赤く点滅させる
601
+ const nextButton = document.getElementById('nextAccordion');
602
+ nextButton.classList.add('red-flash-bg');
603
+ setTimeout(() => {
604
+ nextButton.classList.remove('red-flash-bg');
605
+ }, 2000);
606
+ }
607
+ }
608
+
609
+ function openPreviousAccordion() {
610
+ const accordions = document.querySelectorAll('#mainAccordion .accordion-item');
611
+ let currentIndex = -1;
612
+
613
+ // 現在開いているアコーディオンのインデックスを見つける
614
+ for (let i = 0; i < accordions.length; i++) {
615
+ if (!accordions[i].querySelector('.accordion-button').classList.contains('collapsed')) {
616
+ currentIndex = i;
617
+ break;
618
+ }
619
+ }
620
+
621
+ // 前のアコーディオンを開く
622
+ if (currentIndex > 0) {
623
+ new bootstrap.Collapse(accordions[currentIndex].querySelector('.accordion-collapse')).hide();
624
+ new bootstrap.Collapse(accordions[currentIndex - 1].querySelector('.accordion-collapse')).show();
625
+ } else {
626
+ // もう前がない場合、ボタンを赤く点滅させる
627
+ const prevButton = document.getElementById('prevAccordion');
628
+ prevButton.classList.add('red-flash-bg');
629
+ setTimeout(() => {
630
+ prevButton.classList.remove('red-flash-bg');
631
+ }, 2000);
632
+ }
633
+ }
634
+
635
+ function moveToInput() {
636
+ const content1 = document.getElementById('novelContent1');
637
+ const content2 = document.getElementById('novelContent2');
638
+
639
+ let content1Lines = content1.value.trim().split('\n');
640
+ let content2Lines = content2.value.trim().split('\n');
641
+
642
+ // content1の最後の行とcontent2の先頭行が完全に一致する場合、content2から削除
643
+ if (content1Lines[content1Lines.length - 1] === content2Lines[0]) {
644
+ content2Lines.shift();
645
+ } else {
646
+ // 部分的な重複を検出して削除
647
+ const lastLine = content1Lines[content1Lines.length - 1];
648
+ const firstLine = content2Lines[0];
649
+ const overlapIndex = firstLine.indexOf(lastLine);
650
+ if (overlapIndex !== -1) {
651
+ content2Lines[0] = firstLine.slice(overlapIndex + lastLine.length).trim();
652
+ if (content2Lines[0] === '') {
653
+ content2Lines.shift();
654
+ }
655
+ }
656
+ }
657
+
658
+ // content2の内容をcontent1の末尾に追加
659
+ content1.value = content1Lines.join('\n') + '\n' + content2Lines.join('\n');
660
+
661
+ // content2を空にする
662
+ content2.value = '';
663
+
664
+ // content1Collapseを開く
665
+ const content1Collapse = new bootstrap.Collapse(document.getElementById('content1Collapse'), {
666
+ show: true
667
+ });
668
+ }
669
+
670
+ function updateNavbarBrand() {
671
+ const endpointSelect = document.getElementById('endpointSelect');
672
+ const navbarBrand = document.querySelector('.navbar-brand');
673
+ const googleIcon = navbarBrand.querySelector('.fa-google');
674
+ const robotIcon = navbarBrand.querySelector('.fa-robot');
675
+
676
+ if (endpointSelect.value.startsWith('models/gemini')) {
677
+ navbarBrand.style.color = '#4285F4'; // Googleブルー
678
+ googleIcon.classList.remove('d-none');
679
+ robotIcon.classList.add('d-none');
680
+ } else {
681
+ navbarBrand.style.color = '#00FF00'; // 明るい緑色
682
+ googleIcon.classList.add('d-none');
683
+ robotIcon.classList.remove('d-none');
684
+ }
685
+ }
686
+
687
+ document.addEventListener('DOMContentLoaded', function () {
688
+ // ページ読み込み時にデータを復元
689
+ loadFromUserStorage();
690
+
691
+ // メイン画面の要素のイベントリスナー
692
+ ['novelContent1', 'novelContent2', 'generatePrompt', 'nextPrompt', 'savedTitle'].forEach(id => {
693
+ document.getElementById(id).addEventListener('input', () => {
694
+ saveToUserStorage(false);
695
+ });
696
+ });
697
+
698
+ // 設定画面の要素のイベントリスナー
699
+ ['memo', 'geminiApiKey', 'endpointSelect', 'openaiEndpoint', 'openaiHeaders', 'openaiJsonBody', 'characterCount', 'encodeLength'].forEach(id => {
700
+ document.getElementById(id).addEventListener('input', () => {
701
+ saveToUserStorage(true);
702
+ });
703
+ });
704
+
705
+ ['partialEncodeToggle', 'streamToggle'].forEach(id => {
706
+ document.getElementById(id).addEventListener('change', () => {
707
+ saveToUserStorage(true);
708
+ });
709
+ });
710
+
711
+ document.getElementById('novelContent1').addEventListener('keydown', handleKeyPress);
712
+
713
+ document.querySelectorAll('[data-modal-text]').forEach(element => {
714
+ element.addEventListener('click', function () {
715
+ document.querySelectorAll(".modal-text").forEach(el => {
716
+ el.classList.add("d-none");
717
+ if (el.classList.contains(this.getAttribute('data-modal-text'))) {
718
+ el.classList.remove("d-none");
719
+ }
720
+ });
721
+ });
722
+ });
723
+
724
+ syncInputs();
725
+
726
+ // 60秒ごとに自動保存実行
727
+ setInterval(() => {
728
+ saveToUserStorage();
729
+ }, 60000);
730
+
731
+ // 基本設定のアコーディオンを開く
732
+ const basicSettingsAccordion = document.querySelector('#promptsCollapse');
733
+ if (basicSettingsAccordion) {
734
+ new bootstrap.Collapse(basicSettingsAccordion).show();
735
+ }
736
+
737
+ // ナビゲーションボタンのイベントリスナーを設定
738
+ document.getElementById('prevAccordion').addEventListener('click', openPreviousAccordion);
739
+ document.getElementById('nextAccordion').addEventListener('click', openNextAccordion);
740
+
741
+ // エンドポイント選択が変更されたときにnavbar-brandを更新
742
+ document.getElementById('endpointSelect').addEventListener('change', updateNavbarBrand);
743
+
744
+ // 初期表示時にも実行
745
+ updateNavbarBrand();
746
+ });
index.html CHANGED
@@ -9,156 +9,360 @@
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
- #novelContent1,
13
- #novelContent2 {
14
- height: 50vh;
15
  }
16
 
17
- #generatePrompt,
18
- #nextPrompt {
19
- height: 20vh;
20
  }
21
 
22
- @media (max-width: 767px) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- #novelContent1,
25
- #novelContent2,
26
- #modal-body-1 textarea {
27
- height: 90vh;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
29
  }
30
  </style>
31
  </head>
32
 
33
  <body data-bs-theme="dark">
34
- <div id="my-modal" class="modal fade" tabindex="-1" aria-labelledby="my-modalLabel" aria-hidden="true">
35
- <div class="modal-dialog modal-lg modal-dialog-centered">
36
- <div class="modal-content">
37
- <div class="bg-primary modal-header">
38
- <h5 id="modal-title-1" class="modal-title modal-text modal-text-1">API Settings</h5>
39
- <h5 id="modal-title-2" class="modal-title modal-text modal-text-2">Utility</h5>
40
- <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
41
- aria-label="Close"></button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  </div>
43
- <div id="modal-body-1" class="modal-body modal-text modal-text-1">
44
- <div class="row">
45
- <div class="col-12">
46
- <input type="text" class="form-control" id="endpoint" placeholder="Endpoint">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  </div>
48
- <div class="col-12">
49
- <textarea class="form-control" id="headers" placeholder="Headers" rows="7"></textarea>
 
 
 
 
 
 
 
 
 
 
 
 
50
  </div>
51
- <div class="col-12">
52
- <textarea class="form-control" id="jsonBody" placeholder="Body" rows="7"></textarea>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  </div>
54
  </div>
55
- </div>
56
- <div id="modal-body-2" class="modal-body modal-text modal-text-2">
57
- <div class="row">
58
- <div class="col-12 mb-3">
59
- <button id="formatTextButton" class="btn btn-primary" onclick="formatText()">
60
- <i class="fa-solid fa-align-left"></i> 改行を整理
 
 
 
 
61
  </button>
62
  </div>
63
  </div>
64
  </div>
65
- <div class="modal-footer">
66
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
67
- </div>
68
  </div>
69
  </div>
70
  </div>
71
- <div class="container-fluid">
72
- <div class="row m-0 border-start border-end border-2">
73
- <div class="col-12">
74
- <textarea class="form-control" id="generatePrompt" placeholder="システムプロンプトを入力"></textarea>
75
- </div>
76
- <div class="col-12 col-md-6">
77
- <textarea class="form-control" id="novelContent1" placeholder="ここに小説の本文を入力してください"></textarea>
78
- </div>
79
- <div class="col-12 col-md-6">
80
- <textarea class="form-control" id="novelContent2" placeholder="ここに続きが表示されます。"></textarea>
81
- </div>
82
 
83
- <div class="col-12">
84
- <textarea class="form-control" id="nextPrompt" placeholder="次の展開を指示"></textarea>
85
- </div>
86
- <div class="col-12 col-md-6 small lh-1">
87
- <div class="row">
88
- <div class="col-12 col-md-2 d-flex align-items-center mb-2 mb-md-0">
89
- <button class="btn btn-primary mx-1" data-bs-toggle="modal" data-bs-target="#my-modal"
90
- data-modal-text="modal-text-1">
91
- <i class="fa-solid fa-cog"></i>
92
- </button>
93
- <button class="btn btn-primary mx-1" data-bs-toggle="modal" data-bs-target="#my-modal"
94
- data-modal-text="modal-text-2">
95
- <i class="fa-solid fa-wrench"></i>
 
96
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  </div>
98
- <div class="col-12 col-md-4 mb-2 mb-md-0 px-5">
99
- <label for="characterCount" class="form-label mb-0 me-3">文字数</label>
100
- <input data-input-group="characterCount" type="range" class="form-range me-2"
101
- id="characterCount" min="1" max="4096" value="3000">
102
- <input data-input-group="characterCount" type="number" class="form-control me-2"
103
- id="characterCountInput" value="3000" min="1" max="4096">
104
- </div>
105
- <div class="col-12 col-md-3 mb-2 mb-md-0 px-5">
106
- <label for="encodeLength" class="form-label mb-0 me-3">エンコード頻度</label>
107
- <input data-input-group="encodeLength" type="range" class="form-range me-2" id="encodeLength"
108
- min="1" max="16" value="16">
109
- <input data-input-group="encodeLength" type="number" class="form-control me-2"
110
- id="encodeLengthInput" min="1" max="16" value="16">
111
- </div>
112
- <div class="col-12 col-md-3">
113
- <div class="form-check mb-0">
114
- <label class="form-check-label" for="partialEncodeToggle">エンコード</label>
115
- <input class="form-check-input" type="checkbox" id="partialEncodeToggle">
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  </div>
117
- <div class="form-check mb-0">
118
- <label class="form-check-label" for="streamToggle">stream</label>
119
- <input class="form-check-input" type="checkbox" id="streamToggle" checked>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  </div>
121
  </div>
122
  </div>
123
  </div>
124
- <script>
125
- const inputGroups = document.querySelectorAll('[data-input-group]');
126
- inputGroups.forEach(group => {
127
- const inputs = document.querySelectorAll(`[data-input-group="${group.dataset.inputGroup}"]`);
128
-
129
- inputs.forEach(input => {
130
- input.addEventListener('input', function () {
131
- inputs.forEach(otherInput => {
132
- if (otherInput !== input) {
133
- otherInput.value = this.value;
134
- }
135
- });
136
- });
137
- });
138
- });
139
- </script>
140
- <div class="col-12 col-md-2 d-flex align-items-center mb-2 mb-md-0">
141
- <button id="requestButton" class="btn btn-primary me-2" onclick="Request()">
142
- 続きを生成
143
- </button>
144
- <button id="stopButton" class="btn btn-danger d-none" onclick="stopGeneration()">
145
- 中止
146
- </button>
147
- </div>
148
- <div class="col-12 col-md-4 d-flex align-items-center justify-content-end">
149
- <input type="text" class="form-control me-2" id="savedTitle" placeholder="タイトル">
150
- <button id="saveButton" class="btn btn-secondary me-2" onclick="saveToJson()">
151
- 保存
152
- </button>
153
- <button id="loadButton" class="btn btn-secondary" onclick="loadFromJson()">
154
- 読込
155
- </button>
156
- </div>
157
  </div>
158
  </div>
159
- </div>
160
  <script src="https://unpkg.com/[email protected]/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
161
- <script src="script.js"></script>
162
  </body>
163
 
164
  </html>
 
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
+ #mainContent textarea.form-control {
13
+ min-height: 50vh;
 
14
  }
15
 
16
+ .accordion-button:not(.collapsed) {
17
+ background-color: #212529;
18
+ color: #fff;
19
  }
20
 
21
+ .navbar-toggler {
22
+ display: block !important;
23
+ }
24
+
25
+
26
+ @keyframes redFlashBg {
27
+
28
+ 0%,
29
+ 100% {
30
+ background-color: initial;
31
+ }
32
+
33
+ 50% {
34
+ background-color: rgba(255, 0, 0, 0.5);
35
+ }
36
+ }
37
+
38
+ @keyframes redFlashFg {
39
+
40
+ 0%,
41
+ 100% {
42
+ color: initial;
43
+ }
44
+
45
+ 50% {
46
+ color: red;
47
+ }
48
+ }
49
+
50
+ .red-flash-bg {
51
+ animation: redFlashBg 0.5s infinite;
52
+ }
53
+
54
+ .red-flash-fg {
55
+ animation: redFlashFg 0.5s infinite;
56
+ }
57
+
58
+ @keyframes greenFlashBg {
59
+
60
+ 0%,
61
+ 100% {
62
+ background-color: initial;
63
+ }
64
+
65
+ 50% {
66
+ background-color: rgba(0, 255, 0, 0.5);
67
+ }
68
+ }
69
+
70
+ @keyframes greenFlashFg {
71
 
72
+ 0%,
73
+ 100% {
74
+ color: initial;
75
+ }
76
+
77
+ 50% {
78
+ color: green;
79
+ }
80
+ }
81
+
82
+ .green-flash-bg {
83
+ animation: greenFlashBg 0.5s infinite;
84
+ }
85
+
86
+ .green-flash-fg {
87
+ animation: greenFlashFg 0.5s infinite;
88
+ }
89
+
90
+ /* 新しいスタイルを追加 */
91
+ .navbar {
92
+ position: sticky;
93
+ top: 0;
94
+ z-index: 1000;
95
+ }
96
+
97
+ /* メインコンテンツの上部にパディングを追加 */
98
+ #mainContent {
99
+ padding-top: 1rem;
100
+ }
101
+
102
+ @media (max-width: 991.98px) {
103
+ .navbar-brand {
104
+ font-size: 1rem;
105
+ }
106
+
107
+ .btn-sm {
108
+ padding: 0.25rem 0.5rem;
109
+ font-size: 0.75rem;
110
+ }
111
+ }
112
+
113
+ @media (min-width: 1400px) {
114
+ #mainContent {
115
+ max-width: 50vw;
116
  }
117
  }
118
  </style>
119
  </head>
120
 
121
  <body data-bs-theme="dark">
122
+ <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
123
+ <div class="container-fluid">
124
+ <div class="row justify-content-between align-items-center w-100">
125
+ <div class="col-auto">
126
+ <button class="navbar-toggler" type="button" data-bs-toggle="offcanvas"
127
+ data-bs-target="#settingsOffcanvas" aria-controls="settingsOffcanvas">
128
+ <span class="navbar-toggler-icon"></span>
129
+ </button>
130
+ </div>
131
+ <div class="col-auto text-center">
132
+ <a class="navbar-brand" href="#">
133
+ <i class="fa-brands fa-google"></i>
134
+ <i class="fa-solid fa-robot d-none"></i>
135
+ LLM Client
136
+ </a>
137
+ </div>
138
+ <div class="col-auto"></div>
139
+ </div>
140
+ </div>
141
+ </nav>
142
+
143
+ <div class="container-fluid mt-2">
144
+ <div class="row justify-content-center">
145
+ <div class="col-auto">
146
+ <button id="prevAccordion" class="btn btn-outline-light">
147
+ <i class="fas fa-chevron-left"></i> 前へ
148
+ </button>
149
+ </div>
150
+ <div class="col-auto">
151
+ <div class="form-check form-switch">
152
+ <input class="form-check-input" type="checkbox" id="partialEncodeToggle">
153
+ <label class="form-check-label" for="partialEncodeToggle">Encode</label>
154
  </div>
155
+ <button id="requestButton" class="btn btn-primary" onclick="Request()">
156
+ 生成
157
+ </button>
158
+ <button id="stopButton" class="btn btn-danger d-none" onclick="stopGeneration()">
159
+ 中止
160
+ </button>
161
+ </div>
162
+ <div class="col-auto">
163
+ <button id="nextAccordion" class="btn btn-outline-light">
164
+ 次へ <i class="fas fa-chevron-right"></i>
165
+ </button>
166
+ </div>
167
+ </div>
168
+ </div>
169
+
170
+ <div class="container">
171
+ <div class="row justify-content-center">
172
+ <div id="mainContent">
173
+ <div class="mt-3">
174
+ <div class="accordion" id="mainAccordion">
175
+ <div class="accordion-item">
176
+ <h2 class="accordion-header">
177
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
178
+ data-bs-target="#promptsCollapse">
179
+ 基本設定
180
+ </button>
181
+ </h2>
182
+ <div id="promptsCollapse" class="accordion-collapse collapse"
183
+ data-bs-parent="#mainAccordion">
184
+ <div class="accordion-body">
185
+ <textarea class="form-control mb-2" id="generatePrompt"
186
+ placeholder="システムプロンプトを入力"></textarea>
187
+ </div>
188
+ </div>
189
  </div>
190
+ <div class="accordion-item">
191
+ <h2 class="accordion-header">
192
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
193
+ data-bs-target="#content1Collapse">
194
+ 小説内容 (入力)
195
+ </button>
196
+ </h2>
197
+ <div id="content1Collapse" class="accordion-collapse collapse"
198
+ data-bs-parent="#mainAccordion">
199
+ <div class="accordion-body">
200
+ <textarea class="form-control mb-2" id="novelContent1"
201
+ placeholder="ここに小説の本文を入力してください"></textarea>
202
+ </div>
203
+ </div>
204
  </div>
205
+ <div class="accordion-item">
206
+ <h2 class="accordion-header">
207
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
208
+ data-bs-target="#nextPromptCollapse">
209
+ 次の展開
210
+ </button>
211
+ </h2>
212
+ <div id="nextPromptCollapse" class="accordion-collapse collapse"
213
+ data-bs-parent="#mainAccordion">
214
+ <div class="accordion-body">
215
+ <div class="d-flex justify-content-between align-items-center">
216
+ <textarea class="form-control me-2" id="nextPrompt" placeholder="次の展開を指示"
217
+ rows="1"></textarea>
218
+ </div>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ <div class="accordion-item">
223
+ <h2 class="accordion-header">
224
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
225
+ data-bs-target="#content2Collapse">
226
+ 小説内容 (出力)
227
+ </button>
228
+ </h2>
229
+ <div id="content2Collapse" class="accordion-collapse collapse"
230
+ data-bs-parent="#mainAccordion">
231
+ <div class="accordion-body">
232
+ <textarea class="form-control" id="novelContent2"
233
+ placeholder="ここに続きが表示されます。"></textarea>
234
+ <button id="moveToInputButton" class="btn btn-primary mt-2" onclick="moveToInput()">
235
+ 入力欄に追記
236
+ </button>
237
+ </div>
238
+ </div>
239
  </div>
240
  </div>
241
+
242
+ <div class="row mt-3">
243
+ <div class="col-12 d-flex justify-content-end">
244
+ <input type="text" class="form-control d-inline-block w-auto me-2" id="savedTitle"
245
+ placeholder="タイトル">
246
+ <button id="saveButton" class="btn btn-secondary me-2" onclick="saveToJson()">
247
+ 保存
248
+ </button>
249
+ <button id="loadButton" class="btn btn-secondary" onclick="loadFromJson()">
250
+ 読込
251
  </button>
252
  </div>
253
  </div>
254
  </div>
 
 
 
255
  </div>
256
  </div>
257
  </div>
 
 
 
 
 
 
 
 
 
 
 
258
 
259
+ <div class="offcanvas offcanvas-start" tabindex="-1" id="settingsOffcanvas"
260
+ aria-labelledby="settingsOffcanvasLabel">
261
+ <div class="offcanvas-header">
262
+ <h5 class="offcanvas-title" id="settingsOffcanvasLabel">設定</h5>
263
+ <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
264
+ </div>
265
+ <div class="offcanvas-body">
266
+ <div class="accordion" id="settingsAccordion">
267
+ <!-- API設定 -->
268
+ <div class="accordion-item">
269
+ <h2 class="accordion-header">
270
+ <button class="accordion-button" type="button" data-bs-toggle="collapse"
271
+ data-bs-target="#apiSettings">
272
+ API設定
273
  </button>
274
+ </h2>
275
+ <div id="apiSettings" class="accordion-collapse collapse show" data-bs-parent="#settingsAccordion">
276
+ <div class="accordion-body">
277
+ <h5>エンドポイント</h5>
278
+ <select class="form-select mb-2" id="endpointSelect">
279
+ <option value="models/gemini-1.5-pro-002">gemini-1.5-pro-002</option>
280
+ <option value="models/gemini-1.5-flash-002">gemini-1.5-flash-002</option>
281
+ <option value="models/gemini-1.5-pro-latest">gemini-1.5-pro-latest</option>
282
+ <option value="models/gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
283
+ <option value="openai">OpenAI Compatible</option>
284
+ </select>
285
+
286
+ <h5>Gemini API Key</h5>
287
+ <p class="text-muted">エンドポイントがGeminiの場合は必須</p>
288
+ <input type="text" class="form-control mb-2" id="geminiApiKey" placeholder="Gemini API Key">
289
+
290
+ <h5 class="mt-3">OpenAI Compatible設定</h5>
291
+ <p class="text-muted">エンドポイントがOpenAI Compatibleの場合は必須</p>
292
+ <input type="text" class="form-control mb-2" id="openaiEndpoint"
293
+ placeholder="OpenAI Endpoint">
294
+ <textarea class="form-control mb-2" id="openaiHeaders"
295
+ placeholder="OpenAI Headers (JSON形式)"></textarea>
296
+ <textarea class="form-control mb-2" id="openaiJsonBody"
297
+ placeholder="OpenAI Body (JSON形式)"></textarea>
298
+ </div>
299
  </div>
300
+ </div>
301
+
302
+ <!-- 生成設定 -->
303
+ <div class="accordion-item">
304
+ <h2 class="accordion-header">
305
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
306
+ data-bs-target="#generationSettings">
307
+ 生成設定
308
+ </button>
309
+ </h2>
310
+ <div id="generationSettings" class="accordion-collapse collapse"
311
+ data-bs-parent="#settingsAccordion">
312
+ <div class="accordion-body">
313
+ <div class="mb-2">
314
+ <label for="characterCount" class="form-label">文字数</label>
315
+ <input type="range" class="form-range" id="characterCount" min="64" max="8192"
316
+ value="4096" step="64">
317
+ <input type="number" class="form-control" id="characterCountInput" value="4096" min="64"
318
+ max="8192" step="64">
319
+ </div>
320
+ <div class="mb-2">
321
+ <label for="encodeLength" class="form-label">エンコード長</label>
322
+ <input type="range" class="form-range" id="encodeLength" placeholder="エンコード長" min="1"
323
+ max="16" value="4">
324
+ <input type="number" class="form-control" id="encodeLengthInput" placeholder="エンコード長"
325
+ min="1" max="16" value="4">
326
+ </div>
327
+ <div class="form-check mb-2">
328
+ <input class="form-check-input" type="checkbox" id="streamToggle" checked>
329
+ <label class="form-check-label" for="streamToggle">Stream</label>
330
+ </div>
331
  </div>
332
+ </div>
333
+ </div>
334
+
335
+ <!-- その他の設定 -->
336
+ <div class="accordion-item">
337
+ <h2 class="accordion-header">
338
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
339
+ data-bs-target="#otherSettings">
340
+ その他の設定
341
+ </button>
342
+ </h2>
343
+ <div id="otherSettings" class="accordion-collapse collapse" data-bs-parent="#settingsAccordion">
344
+ <div class="accordion-body">
345
+ <h5>メモ欄</h5>
346
+ <textarea class="form-control mb-2" id="memo" placeholder="メモ用項目。生成に一切影響しません。"
347
+ rows="5"></textarea>
348
+
349
+ <button id="formatTextButton" class="btn btn-primary mb-2" onclick="formatText()">
350
+ <i class="fa-solid fa-align-left"></i> 改行を整理
351
+ </button>
352
+
353
+ <h5 class="mt-3">デバッグ</h5>
354
+ <button id="debugButton" class="btn btn-secondary mb-2" onclick="debugPrompt()">
355
+ <i class="fa-solid fa-bug"></i> 送信するプロンプトを表示
356
+ </button>
357
  </div>
358
  </div>
359
  </div>
360
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
  </div>
362
  </div>
363
+
364
  <script src="https://unpkg.com/[email protected]/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
365
+ <script src="gemini.js"></script>
366
  </body>
367
 
368
  </html>