SenY commited on
Commit
30642fd
1 Parent(s): 1d0ec46

Upload 2 files

Browse files
Files changed (2) hide show
  1. gemini.js +410 -42
  2. index.html +72 -37
gemini.js CHANGED
@@ -1,5 +1,7 @@
1
  let lastSaveTimestamp = 0;
2
  let controller;
 
 
3
 
4
  function formatText() {
5
  const textOrg = document.getElementById('novelContent1').value;
@@ -32,6 +34,27 @@ function unmalform(text) {
32
  return result || '';
33
  }
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  function partialEncodeURI(text) {
36
  if (!document.getElementById("partialEncodeToggle").checked) {
37
  return text;
@@ -106,7 +129,7 @@ function loadFromJson() {
106
  if (jsonData.savedTitle) {
107
  document.getElementById('savedTitle').value = jsonData.savedTitle;
108
  }
109
- alert('JSONファイルを正常に読み込みました。');
110
  } catch (error) {
111
  alert('無効なJSONファイルです。');
112
  }
@@ -118,36 +141,24 @@ function loadFromJson() {
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
  }
@@ -164,7 +175,7 @@ function loadFromUserStorage() {
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`);
@@ -179,13 +190,54 @@ function loadFromUserStorage() {
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",
@@ -197,7 +249,7 @@ function createPayload() {
197
  },
198
  {
199
  "role": "user",
200
- "parts": [{ "text": `続きを書いて。${partialEncodeURI(document.getElementById('nextPrompt').value)} ${document.getElementById('characterCountInput').value}文字程度。${systemPrompt}` }]
201
  }
202
  ];
203
 
@@ -266,7 +318,7 @@ function fetchStream(ENDPOINT, payload) {
266
 
267
  const chunk = decoder.decode(value, { stream: true });
268
  buffer += chunk;
269
- console.debug('チャンクを受信しました:', chunk);
270
 
271
  // バッファから完全なJSONオブジェクトを抽出して処理
272
  let startIndex = 0;
@@ -301,9 +353,9 @@ function fetchStream(ENDPOINT, payload) {
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');
@@ -510,22 +562,114 @@ function fetchOpenAINonStream(ENDPOINT, payload) {
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')) {
@@ -542,12 +686,6 @@ function Request() {
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() {
@@ -684,14 +822,235 @@ function updateNavbarBrand() {
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
 
@@ -699,12 +1058,16 @@ document.addEventListener('DOMContentLoaded', function () {
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
 
@@ -726,6 +1089,8 @@ document.addEventListener('DOMContentLoaded', function () {
726
  // 60秒ごとに自動保存実行
727
  setInterval(() => {
728
  saveToUserStorage();
 
 
729
  }, 60000);
730
 
731
  // 基本設定のアコーディオンを開く
@@ -743,4 +1108,7 @@ document.addEventListener('DOMContentLoaded', function () {
743
 
744
  // 初期表示時にも実行
745
  updateNavbarBrand();
 
 
 
746
  });
 
1
  let lastSaveTimestamp = 0;
2
  let controller;
3
+ let lastTokenUpdateTimestamp = 0;
4
+ let summeries = {};
5
 
6
  function formatText() {
7
  const textOrg = document.getElementById('novelContent1').value;
 
34
  return result || '';
35
  }
36
 
37
+ async function summerize(text) {
38
+ const ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-002:generateContent?key=${document.getElementById('geminiApiKey').value}`;
39
+ const prompt = `以下の文章を240文字程度に要約してください:\n\n${text}`;
40
+ const payload = {
41
+ method: 'POST',
42
+ headers: {},
43
+ body: JSON.stringify({
44
+ contents: [{ parts: [{ text: prompt }] }],
45
+ generationConfig: { temperature: 0.7, max_output_tokens: 256 }
46
+ })
47
+ };
48
+ try {
49
+ const response = await fetch(ENDPOINT, payload);
50
+ const data = await response.json();
51
+ return data.candidates[0].content.parts[0].text;
52
+ } catch (error) {
53
+ console.error('要約エラー:', error);
54
+ return '';
55
+ }
56
+ }
57
+
58
  function partialEncodeURI(text) {
59
  if (!document.getElementById("partialEncodeToggle").checked) {
60
  return text;
 
129
  if (jsonData.savedTitle) {
130
  document.getElementById('savedTitle').value = jsonData.savedTitle;
131
  }
132
+ alert('JSONファイルを正常読み込みました');
133
  } catch (error) {
134
  alert('無効なJSONファイルです。');
135
  }
 
141
  }
142
 
143
  function saveToUserStorage(force = false) {
 
144
  const currentTime = Date.now();
145
+ if (currentTime - lastSaveTimestamp < 5000 && !force) {
146
+ console.debug('セーブをスキップします');
147
  return;
148
  }
149
  console.debug('セーブを実行します');
150
 
151
+ // 既存のデータを取得
152
+ const geminiClientData = JSON.parse(localStorage.getItem('geminiClient') || '{}');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
+ const newData = {};
155
+ Array.from(document.querySelectorAll("input[id], textarea[id], select[id]")).forEach(el => {
156
+ if (el.id) {
157
+ newData[el.id] = el.type === 'checkbox' ? el.checked : el.value;
158
+ }
159
+ });
160
+ Object.assign(geminiClientData, newData);
161
+ console.log(geminiClientData);
162
  localStorage.setItem('geminiClient', JSON.stringify(geminiClientData));
163
  lastSaveTimestamp = currentTime;
164
  }
 
175
  } else {
176
  elem.value = geminiClientData[key];
177
  }
178
+
179
  // 特別な処理が必要な要素
180
  if (key === 'characterCount' || key === 'encodeLength' || key === 'contentWidth') {
181
  const inputElem = document.getElementById(`${key}Input`);
 
190
  }
191
  }
192
 
193
+ function createSummarizedText() {
194
+ const indexOffcanvasBody = document.querySelector('#indexOffcanvas .offcanvas-body');
195
+ const rootUl = indexOffcanvasBody.querySelector('ul.list-unstyled');
196
+ let summarizedText = '';
197
+
198
+ function processUl(ul, level = 0) {
199
+ const items = ul.children;
200
+ for (let item of items) {
201
+ const a = item.querySelector(':scope > a');
202
+ if (a) {
203
+ summarizedText += '#'.repeat(level + 1) + ' ' + a.textContent + '\n';
204
+ }
205
+
206
+ const contentItem = item.querySelector(':scope > ul > li');
207
+ if (contentItem) {
208
+ const fullText = contentItem.querySelector('.full-text');
209
+ const summaryText = contentItem.querySelector('.summery-text');
210
+ if (summaryText && summaryText.value.trim()) {
211
+ summarizedText += summaryText.value + '\n\n';
212
+ } else if (fullText) {
213
+ summarizedText += fullText.value + '\n\n';
214
+ }
215
+ }
216
+
217
+ const subUl = item.querySelector(':scope > ul');
218
+ if (subUl) {
219
+ processUl(subUl, level + 1);
220
+ }
221
+ }
222
+ }
223
+
224
+ if (rootUl) {
225
+ processUl(rootUl);
226
+ }
227
+
228
+ return summarizedText.trim();
229
+ }
230
+
231
  function createPayload() {
232
  const novelContent1 = document.getElementById('novelContent1');
233
+ let text = novelContent1.value;
234
+ if (document.getElementById('summerizedPromptToggle').checked) {
235
+ text = createSummarizedText();
236
+ }
237
  const lines = text.split('\n').filter(x => x);
238
 
 
239
  let systemPrompt = `${partialEncodeURI(document.getElementById('generatePrompt').value)}`;
240
+ let prompt = `続きを書いて。${partialEncodeURI(document.getElementById('nextPrompt').value)} ${document.getElementById('characterCountInput').value}文字程度。${systemPrompt}`;
241
  let messages = [
242
  {
243
  "role": "user",
 
249
  },
250
  {
251
  "role": "user",
252
+ "parts": [{ "text": prompt }]
253
  }
254
  ];
255
 
 
318
 
319
  const chunk = decoder.decode(value, { stream: true });
320
  buffer += chunk;
321
+ console.debug('チャンクを受信しまし:', chunk);
322
 
323
  // バッファから完全なJSONオブジェクトを抽出して処理
324
  let startIndex = 0;
 
353
  if (data.candidates[0].finishReason === 'STOP') {
354
  requestButton.classList.add('green-flash-bg');
355
  setTimeout(() => {
356
+ requestButton.classList.remove('green-flash-bg');
357
  }, 2000);
358
+ } else {
359
  requestButton.classList.add('red-flash-bg');
360
  setTimeout(() => {
361
  requestButton.classList.remove('red-flash-bg');
 
562
  });
563
  }
564
 
565
+ async function tokenCount() {
566
  const selectedEndpoint = document.getElementById('endpointSelect').value;
567
+ let payload = createPayload();
568
+ payload.body = {
569
+ "contents": JSON.parse(payload.body).contents
570
+ };
571
+ payload.body = JSON.stringify(payload.body);
572
+ const ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/${selectedEndpoint}:countTokens?key=` + document.getElementById('geminiApiKey').value;
573
+ try {
574
+ const response = await fetch(ENDPOINT, payload);
575
+ const data = await response.json();
576
+ return data.totalTokens;
577
+ } catch (error) {
578
+ console.error('エラー:', error);
579
+ return null;
580
+ }
581
+ }
582
+
583
+ async function createDraft() {
584
+ const ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-002:generateContent?key=` + document.getElementById('geminiApiKey').value;
585
+ let payload = createPayload();
586
+ const response = await fetch(ENDPOINT, payload);
587
+ const data = await response.json();
588
+ const text = data.candidates[0].content.parts[0].text;
589
+ return text
590
+ }
591
+
592
+
593
+ async function Request() {
594
+ let selectedEndpoint = document.getElementById('endpointSelect').value;
595
+ const requestButton = document.getElementById('requestButton');
596
+ requestButton.disabled = true;
597
+ document.getElementById('novelContent2').value = '';
598
+ const outputAccordion = document.querySelector('#content2Collapse');
599
+ if (outputAccordion) {
600
+ const bsCollapse = new bootstrap.Collapse(outputAccordion, { toggle: false });
601
+ bsCollapse.show();
602
+ }
603
+
604
  let ENDPOINT;
605
  let payload;
606
 
607
  if (selectedEndpoint.startsWith('models/gemini')) {
608
  ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/${selectedEndpoint}:generateContent?key=` + document.getElementById('geminiApiKey').value;
609
  payload = createPayload();
610
+ } else if (selectedEndpoint === 'restart') {
611
+ ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro-002:generateContent?key=` + document.getElementById('geminiApiKey').value;
612
+ document.getElementById('novelContent2').value = '(下書き中)';
613
+ try {
614
+ draft = await createDraft();
615
+ } catch (error) {
616
+ console.error('エラー:', error);
617
+ requestButton.disabled = false;
618
+ requestButton.classList.add('green-flash-bg');
619
+ setTimeout(() => {
620
+ requestButton.classList.remove('green-flash-bg');
621
+ }, 2000);
622
+ return;
623
+ }
624
+ //document.getElementById('novelContent2').value = `# 下書き\n${draft}\n\n# リライト\n`;
625
+ document.getElementById('novelContent2').value = "";
626
+ const prompt = `以下の文章を、要件を満たす形に直したものを返信してください。\n返信内容は直した後のテキストのみになるようにし他の内容が混ざらないようにしてください。\n\n# 要件\n- 元の文章や会話の言い回しは不自然な日本語になっている部分もかなり多いので、根本的に書き替えること。ただし、登場人物の一人称や他者への呼称は変更しないこと。\n- 物語の大筋やキャラクターの性格は保ったまま、文章は全く別物に書き換える必要がある。元のテキストそのものはなるべく流用しないで新規に書くよう心掛ける。\n- 随所にレトリックを駆使しつつも凝り過ぎて読み辛くなることのないシンプルな美文になることを意識する。\n- 登場人物の会話を重視し、文脈を読み取り元のキャラクター設定に合った台詞回しを保ちつつ、より生き生きとした魅力的な人物像に仕上がるようにする。\n- 細かい動作や心理描写のディテールを重視し、よりリアルな描写になるようにする。\n- 文章の終わりに「。」をつける、字下げをするなど、一般的な小説のフォーマットに従う書き方にする。\n\n# 文章\n${draft}`;
627
+ payload = {
628
+ method: 'POST',
629
+ headers: {},
630
+ body: JSON.stringify({
631
+ contents: [
632
+ {
633
+ "parts": [
634
+ {
635
+ "text": prompt
636
+ }
637
+ ],
638
+ "role": "user"
639
+ }
640
+ ],
641
+ "generationConfig": {
642
+ "temperature": 1.0,
643
+ "max_output_tokens": 4096
644
+ },
645
+ safetySettings: [
646
+ {
647
+ "category": "HARM_CATEGORY_HATE_SPEECH",
648
+ "threshold": "BLOCK_NONE"
649
+ },
650
+ {
651
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
652
+ "threshold": "BLOCK_NONE"
653
+ },
654
+ {
655
+ "category": "HARM_CATEGORY_HARASSMENT",
656
+ "threshold": "BLOCK_NONE"
657
+ },
658
+ {
659
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
660
+ "threshold": "BLOCK_NONE"
661
+ }
662
+ ]
663
+ }),
664
+ mode: 'cors'
665
+ };
666
+ selectedEndpoint = 'models/gemini-1.5-pro-002';
667
  } else {
668
  ENDPOINT = document.getElementById('openaiEndpoint').value;
669
  payload = createOpenAIPayload();
670
  }
671
 
 
672
  let stream = document.getElementById('streamToggle').checked;
 
673
 
674
  if (stream) {
675
  if (selectedEndpoint.startsWith('models/gemini')) {
 
686
  fetchOpenAINonStream(ENDPOINT, payload);
687
  }
688
  }
 
 
 
 
 
 
689
  }
690
 
691
  function stopGeneration() {
 
822
  }
823
  }
824
 
825
+ async function updateTokenCount(force = false) {
826
+ const currentTime = Date.now();
827
+ if (currentTime - lastTokenUpdateTimestamp < 60000 && !force) {
828
+ console.debug('トークン数更新をスキップします');
829
+ return;
830
+ }
831
+ console.debug('トークン数更新を実行します');
832
+
833
+ const count = await tokenCount();
834
+ const indexOffcanvasLabel = document.getElementById('indexOffcanvasLabel');
835
+ indexOffcanvasLabel.textContent = `目次 (${count}トークン)`;
836
+ lastTokenUpdateTimestamp = currentTime;
837
+ }
838
+
839
+ function generateIndexMenu() {
840
+ const content = document.getElementById('novelContent1').value;
841
+ const tokens = marked.lexer(content);
842
+ const indexOffcanvasBody = document.querySelector('#indexOffcanvas .offcanvas-body');
843
+
844
+ indexOffcanvasBody.innerHTML = '';
845
+
846
+ const rootUl = document.createElement('ul');
847
+ rootUl.className = 'list-unstyled';
848
+
849
+ let stack = [{ ul: rootUl, level: 0 }];
850
+ let lastHeading = null;
851
+ let contentBuffer = '';
852
+
853
+ tokens.forEach((token, index) => {
854
+ if (token.type === 'heading') {
855
+ if (lastHeading && contentBuffer.trim()) {
856
+ addTextarea(lastHeading.querySelector('ul'), contentBuffer.trim());
857
+ }
858
+ contentBuffer = '';
859
+
860
+ while (stack.length > 1 && stack[stack.length - 1].level >= token.depth) {
861
+ stack.pop();
862
+ }
863
+
864
+ const li = document.createElement('li');
865
+ const toggleBtn = document.createElement('button');
866
+ toggleBtn.className = 'btn btn-sm btn-outline-secondary me-2 toggle-btn';
867
+ const icon = document.createElement('i');
868
+ icon.className = 'fas fa-plus'; // Font Awesomeのプラスアイコン
869
+ toggleBtn.appendChild(icon);
870
+ toggleBtn.onclick = () => toggleSubMenu(li);
871
+
872
+ const a = document.createElement('a');
873
+ a.href = '#';
874
+ a.textContent = token.text;
875
+ a.onclick = (e) => {
876
+ e.preventDefault();
877
+ scrollToHeading(token.text);
878
+ };
879
+
880
+ li.appendChild(toggleBtn);
881
+ li.appendChild(a);
882
+
883
+ const subUl = document.createElement('ul');
884
+ subUl.className = 'list-unstyled ms-3 d-none';
885
+ li.appendChild(subUl);
886
+
887
+ stack[stack.length - 1].ul.appendChild(li);
888
+
889
+ if (token.depth > stack[stack.length - 1].level) {
890
+ stack.push({ ul: subUl, level: token.depth });
891
+ }
892
+
893
+ lastHeading = li;
894
+ } else if (token.type === 'text' || token.type === 'paragraph') {
895
+ contentBuffer += token.text + '\n';
896
+ }
897
+ });
898
+
899
+ if (lastHeading && contentBuffer.trim()) {
900
+ addTextarea(lastHeading.querySelector('ul'), contentBuffer.trim());
901
+ }
902
+
903
+ if (rootUl.children.length > 0) {
904
+ indexOffcanvasBody.appendChild(rootUl);
905
+ } else {
906
+ indexOffcanvasBody.textContent = '目次がありません';
907
+ }
908
+
909
+ updateAllAccordionHeaderCounts();
910
+ updateTokenCount(true); // トークン数を強制更新
911
+ }
912
+
913
+ function toggleSubMenu(li) {
914
+ const subUl = li.querySelector('ul');
915
+ const toggleBtn = li.querySelector('.toggle-btn');
916
+ const icon = toggleBtn.querySelector('i');
917
+ subUl.classList.toggle('d-none');
918
+ icon.className = subUl.classList.contains('d-none') ? 'fas fa-plus' : 'fas fa-minus';
919
+ }
920
+
921
+ function addTextarea(ul, content) {
922
+ const li = document.createElement('li');
923
+
924
+ // テキストエリアの作成
925
+ const textarea = document.createElement('textarea');
926
+ textarea.readOnly = true;
927
+ textarea.className = 'form-control mt-2 full-text';
928
+ textarea.value = content;
929
+ textarea.rows = 3;
930
+
931
+ // 要約用のテキストエリアの作成
932
+ const summaryInput = document.createElement('textarea');
933
+ summaryInput.className = 'form-control mt-2 summery-text';
934
+ summaryInput.placeholder = '要約';
935
+ summaryInput.rows = 3;
936
+ if(summeries[content]) {
937
+ summaryInput.value = summeries[content];
938
+ }
939
+
940
+ // ボタン用のコンテナ作成
941
+ const buttonContainer = document.createElement('div');
942
+ buttonContainer.className = 'mt-2';
943
+
944
+ // 要約取得ボタンの作成
945
+ const summaryButton = document.createElement('button');
946
+ summaryButton.textContent = '要約を取得';
947
+ summaryButton.className = 'btn btn-secondary me-2';
948
+ summaryButton.onclick = async () => {
949
+ summaryButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Loading...';
950
+ summaryButton.disabled = true;
951
+ try {
952
+ const summary = await summerize(content);
953
+ summaryInput.value = summary;
954
+ summeries[content] = summary;
955
+ updateTokenCount(true);
956
+ } finally {
957
+ summaryButton.innerHTML = '要約を取得';
958
+ summaryButton.disabled = false;
959
+ }
960
+ };
961
+
962
+ // 要約削除ボタンの作成
963
+ const deleteSummaryButton = document.createElement('button');
964
+ deleteSummaryButton.textContent = '要約を削除';
965
+ deleteSummaryButton.className = 'btn btn-danger';
966
+ deleteSummaryButton.onclick = () => {
967
+ summaryInput.value = '';
968
+ delete summeries[content];
969
+ updateTokenCount(true);
970
+ };
971
+
972
+ // ボタンをコンテナに追加
973
+ buttonContainer.appendChild(summaryButton);
974
+ buttonContainer.appendChild(deleteSummaryButton);
975
+
976
+ // 要素の追加
977
+ li.appendChild(textarea);
978
+ li.appendChild(summaryInput);
979
+ li.appendChild(buttonContainer);
980
+ ul.appendChild(li);
981
+ }
982
+
983
+ function scrollToHeading(headingText) {
984
+ const content = document.getElementById('novelContent1');
985
+ const lines = content.value.split('\n');
986
+ let position = 0;
987
+
988
+ for (let i = 0; i < lines.length; i++) {
989
+ if (lines[i].trim().startsWith('#') && lines[i].includes(headingText)) {
990
+ // アコーディオンを開く
991
+ openAccordionContainingPosition(position);
992
+
993
+ content.focus();
994
+ content.setSelectionRange(position, position);
995
+ content.scrollTop = content.scrollHeight * (position / content.value.length);
996
+ break;
997
+ }
998
+ position += lines[i].length + 1; // +1 for newline character
999
+ }
1000
+
1001
+ // Offcanvasを閉じる
1002
+ const indexOffcanvas = bootstrap.Offcanvas.getInstance(document.getElementById('indexOffcanvas'));
1003
+ }
1004
+
1005
+ function openAccordionContainingPosition(position) {
1006
+ const content = document.getElementById('novelContent1');
1007
+ const accordionItems = document.querySelectorAll('#mainAccordion .accordion-item');
1008
+ let currentPosition = 0;
1009
+
1010
+ for (let i = 0; i < accordionItems.length; i++) {
1011
+ const textArea = accordionItems[i].querySelector('textarea');
1012
+ if (textArea && textArea.id === 'novelContent1') {
1013
+ if (position >= currentPosition && position < currentPosition + textArea.value.length) {
1014
+ // このアコーディオンアイテムに目的の位置が含まれている
1015
+ const collapseElement = accordionItems[i].querySelector('.accordion-collapse');
1016
+ const bsCollapse = new bootstrap.Collapse(collapseElement, { toggle: false });
1017
+ bsCollapse.show();
1018
+ break;
1019
+ }
1020
+ currentPosition += textArea.value.length;
1021
+ }
1022
+ }
1023
+ }
1024
+
1025
+ function updateAccordionHeaderCount(accordionId) {
1026
+ const accordionItem = document.getElementById(accordionId).closest('.accordion-item');
1027
+ if (!accordionItem) return;
1028
+
1029
+ const textarea = accordionItem.querySelector('.accordion-body textarea');
1030
+ const header = accordionItem.querySelector('.accordion-header button');
1031
+
1032
+ if (textarea && header) {
1033
+ const charCount = textarea.value.length;
1034
+ const originalText = header.textContent.split('(')[0].trim();
1035
+ header.textContent = `${originalText} (${charCount}文字)`;
1036
+ }
1037
+ }
1038
+
1039
+ function updateAllAccordionHeaderCounts() {
1040
+ const accordionIds = ['promptsCollapse', 'content1Collapse', 'nextPromptCollapse', 'content2Collapse'];
1041
+ accordionIds.forEach(updateAccordionHeaderCount);
1042
+ }
1043
+
1044
  document.addEventListener('DOMContentLoaded', function () {
1045
  // ページ読み込み時にデータを復元
1046
  loadFromUserStorage();
1047
 
1048
+ // メイン画面の要素のイベントリスナー。inputイベントが発生する頻度が非常に高いのでこちらの発動は60秒に1回に制限する
1049
  ['novelContent1', 'novelContent2', 'generatePrompt', 'nextPrompt', 'savedTitle'].forEach(id => {
1050
  document.getElementById(id).addEventListener('input', () => {
1051
  saveToUserStorage(false);
1052
+ generateIndexMenu();
1053
+ updateTokenCount(); // 60秒に1回の制限が適用される
1054
  });
1055
  });
1056
 
 
1058
  ['memo', 'geminiApiKey', 'endpointSelect', 'openaiEndpoint', 'openaiHeaders', 'openaiJsonBody', 'characterCount', 'encodeLength'].forEach(id => {
1059
  document.getElementById(id).addEventListener('input', () => {
1060
  saveToUserStorage(true);
1061
+ generateIndexMenu();
1062
+ updateTokenCount(); // 60秒に1回の制限が適用される
1063
  });
1064
  });
1065
 
1066
  ['partialEncodeToggle', 'streamToggle'].forEach(id => {
1067
  document.getElementById(id).addEventListener('change', () => {
1068
  saveToUserStorage(true);
1069
+ generateIndexMenu();
1070
+ updateTokenCount(); // 60秒に1回の制限が適用される
1071
  });
1072
  });
1073
 
 
1089
  // 60秒ごとに自動保存実行
1090
  setInterval(() => {
1091
  saveToUserStorage();
1092
+ generateIndexMenu();
1093
+ updateTokenCount(true); // 強制的に更新
1094
  }, 60000);
1095
 
1096
  // 基本設定のアコーディオンを開く
 
1108
 
1109
  // 初期表示時にも実行
1110
  updateNavbarBrand();
1111
+ generateIndexMenu();
1112
+ updateAllAccordionHeaderCounts();
1113
+ updateTokenCount(true); // 強制的に更新
1114
  });
index.html CHANGED
@@ -10,7 +10,8 @@
10
  crossorigin="anonymous">
11
  <style>
12
  #mainContent textarea.form-control {
13
- min-height: 50vh;
 
14
  }
15
 
16
  .accordion-button:not(.collapsed) {
@@ -110,63 +111,72 @@
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">
@@ -280,6 +290,7 @@
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
 
@@ -324,7 +335,15 @@
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>
@@ -352,8 +371,12 @@
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>
@@ -361,7 +384,19 @@
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
 
 
10
  crossorigin="anonymous">
11
  <style>
12
  #mainContent textarea.form-control {
13
+ min-height: 40vh;
14
+ font-size: 1.2rem;
15
  }
16
 
17
  .accordion-button:not(.collapsed) {
 
111
  }
112
  }
113
 
114
+ @media (min-width: 1400px) {}
115
+
116
+ /* 目次のスタイル */
117
+ #indexOffcanvas .offcanvas-body ul {
118
+ padding-left: 0;
119
+ }
120
+
121
+ #indexOffcanvas .offcanvas-body li {
122
+ margin-bottom: 0.5rem;
123
+ }
124
+
125
+ #indexOffcanvas .offcanvas-body a {
126
+ text-decoration: none;
127
+ color: inherit;
128
+ }
129
+
130
+ #indexOffcanvas .offcanvas-body .toggle-btn {
131
+ padding: 0.1rem 0.3rem;
132
+ font-size: 0.8rem;
133
  }
134
  </style>
135
  </head>
136
 
137
  <body data-bs-theme="dark">
138
+ <div class="container-fluid">
139
+ <div class="row">
140
+ <div class="col-12 text-center">
141
+ <a class="navbar-brand" href="#">
142
+ <i class="fa-brands fa-google"></i>
143
+ <i class="fa-solid fa-robot d-none"></i>
144
+ LLM Client
145
+ </a>
 
 
 
 
 
 
 
 
 
146
  </div>
147
  </div>
148
+ <div class="row w-100">
149
+ <div class="col-3 d-flex justify-content-end">
150
+ <button class="navbar-toggler" type="button" data-bs-toggle="offcanvas"
151
+ data-bs-target="#settingsOffcanvas" aria-controls="settingsOffcanvas">
152
+ <i class="fas fa-list"></i>
153
+ </button>
154
+ </div>
155
+ <div class="col-6 d-flex justify-content-center">
156
  <button id="prevAccordion" class="btn btn-outline-light">
157
  <i class="fas fa-chevron-left"></i> 前へ
158
  </button>
 
 
 
 
 
 
159
  <button id="requestButton" class="btn btn-primary" onclick="Request()">
160
  生成
161
  </button>
162
  <button id="stopButton" class="btn btn-danger d-none" onclick="stopGeneration()">
163
  中止
164
  </button>
 
 
165
  <button id="nextAccordion" class="btn btn-outline-light">
166
  次へ <i class="fas fa-chevron-right"></i>
167
  </button>
168
  </div>
169
+ <div class="col-3 d-flex justify-content-start">
170
+ <button class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#indexOffcanvas"
171
+ aria-controls="indexOffcanvas">
172
+ <i class="fas fa-book"></i>
173
+ </button>
174
+ </div>
175
  </div>
176
  </div>
177
 
178
+
179
+
180
  <div class="container">
181
  <div class="row justify-content-center">
182
  <div id="mainContent">
 
290
  <option value="models/gemini-1.5-flash-002">gemini-1.5-flash-002</option>
291
  <option value="models/gemini-1.5-pro-latest">gemini-1.5-pro-latest</option>
292
  <option value="models/gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
293
+ <option value="restart">Restart</option>
294
  <option value="openai">OpenAI Compatible</option>
295
  </select>
296
 
 
335
  <input type="number" class="form-control" id="encodeLengthInput" placeholder="エンコード長"
336
  min="1" max="16" value="4">
337
  </div>
338
+ <div class="form-check mb-2 form-switch">
339
+ <input class="form-check-input" type="checkbox" id="summerizedPromptToggle">
340
+ <label class="form-check-label" for="summerizedPromptToggle">Summerize</label>
341
+ </div>
342
+ <div class="form-check mb-2 form-switch">
343
+ <input class="form-check-input" type="checkbox" id="partialEncodeToggle">
344
+ <label class="form-check-label" for="partialEncodeToggle">Encode</label>
345
+ </div>
346
+ <div class="form-check mb-2 form-switch">
347
  <input class="form-check-input" type="checkbox" id="streamToggle" checked>
348
  <label class="form-check-label" for="streamToggle">Stream</label>
349
  </div>
 
371
 
372
  <h5 class="mt-3">デバッグ</h5>
373
  <button id="debugButton" class="btn btn-secondary mb-2" onclick="debugPrompt()">
374
+ <i class="fa-solid fa-bug"></i> payload
375
+ </button>
376
+ <button id="summaryButton" class="btn btn-secondary mb-2" onclick="console.log(createSummarizedText())">
377
+ <i class="fa-solid fa-bug"></i> 要約
378
  </button>
379
+
380
  </div>
381
  </div>
382
  </div>
 
384
  </div>
385
  </div>
386
 
387
+ <!-- 新しい右側のOffCanvas -->
388
+ <div class="offcanvas offcanvas-end" tabindex="-1" id="indexOffcanvas" aria-labelledby="indexOffcanvasLabel">
389
+ <div class="offcanvas-header">
390
+ <h5 class="offcanvas-title" id="indexOffcanvasLabel">目次</h5>
391
+ <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
392
+ </div>
393
+ <div class="offcanvas-body">
394
+ <!-- ここに目次の内容を追加します -->
395
+ </div>
396
+ </div>
397
+
398
  <script src="https://unpkg.com/[email protected]/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
399
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
400
  <script src="gemini.js"></script>
401
  </body>
402