hisaruki commited on
Commit
b0e88e3
·
1 Parent(s): 45da44d

Restartモード実装

Browse files
Files changed (1) hide show
  1. gemini.js +240 -43
gemini.js CHANGED
@@ -1,6 +1,5 @@
1
  let lastSaveTimestamp = 0;
2
  let controller;
3
- let isLoading = false;
4
 
5
  function formatText() {
6
  const textOrg = document.getElementById('novelContent1').value;
@@ -107,7 +106,7 @@ function loadFromJson() {
107
  if (jsonData.savedTitle) {
108
  document.getElementById('savedTitle').value = jsonData.savedTitle;
109
  }
110
- alert('JSONファイルを正常に読み込みました。');
111
  } catch (error) {
112
  alert('無効なJSONファイルです。');
113
  }
@@ -119,43 +118,29 @@ function loadFromJson() {
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);
@@ -167,7 +152,7 @@ function loadFromUserStorage() {
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`);
@@ -180,7 +165,6 @@ function loadFromUserStorage() {
180
  }
181
  });
182
  }
183
- isLoading = false;
184
  }
185
 
186
  function createPayload() {
@@ -270,7 +254,7 @@ function fetchStream(ENDPOINT, payload) {
270
 
271
  const chunk = decoder.decode(value, { stream: true });
272
  buffer += chunk;
273
- console.debug('チャンクを受信しました:', chunk);
274
 
275
  // バッファから完全なJSONオブジェクトを抽出して処理
276
  let startIndex = 0;
@@ -305,9 +289,9 @@ function fetchStream(ENDPOINT, payload) {
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');
@@ -514,22 +498,103 @@ function fetchOpenAINonStream(ENDPOINT, payload) {
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')) {
@@ -546,12 +611,6 @@ function Request() {
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 });
553
- bsCollapse.show();
554
- }
555
  }
556
 
557
  function stopGeneration() {
@@ -688,14 +747,148 @@ function updateNavbarBrand() {
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
 
@@ -703,12 +896,14 @@ document.addEventListener('DOMContentLoaded', function () {
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
 
@@ -730,6 +925,7 @@ document.addEventListener('DOMContentLoaded', function () {
730
  // 60秒ごとに自動保存実行
731
  setInterval(() => {
732
  saveToUserStorage();
 
733
  }, 60000);
734
 
735
  // 基本設定のアコーディオンを開く
@@ -747,4 +943,5 @@ document.addEventListener('DOMContentLoaded', function () {
747
 
748
  // 初期表示時にも実行
749
  updateNavbarBrand();
 
750
  });
 
1
  let lastSaveTimestamp = 0;
2
  let controller;
 
3
 
4
  function formatText() {
5
  const textOrg = document.getElementById('novelContent1').value;
 
106
  if (jsonData.savedTitle) {
107
  document.getElementById('savedTitle').value = jsonData.savedTitle;
108
  }
109
+ alert('JSONファイルを正常読み込みました。');
110
  } catch (error) {
111
  alert('無効なJSONファイルです。');
112
  }
 
118
  }
119
 
120
  function saveToUserStorage(force = false) {
 
 
121
  const currentTime = Date.now();
122
+ if (currentTime - lastSaveTimestamp < 5000 && !force) {
123
+ console.debug('セーブをスキップします');
124
  return;
125
  }
126
  console.debug('セーブを実行します');
127
 
128
+ // 既存のデータを取得
129
+ const geminiClientData = JSON.parse(localStorage.getItem('geminiClient') || '{}');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
+ const newData = {};
132
+ Array.from(document.querySelectorAll("input[id], textarea[id], select[id]")).forEach(el => {
133
+ if (el.id) {
134
+ newData[el.id] = el.type === 'checkbox' ? el.checked : el.value;
135
+ }
136
+ });
137
+ Object.assign(geminiClientData, newData);
138
+ console.log(geminiClientData);
139
  localStorage.setItem('geminiClient', JSON.stringify(geminiClientData));
140
  lastSaveTimestamp = currentTime;
141
  }
142
 
143
  function loadFromUserStorage() {
 
144
  const savedData = localStorage.getItem('geminiClient');
145
  if (savedData) {
146
  const geminiClientData = JSON.parse(savedData);
 
152
  } else {
153
  elem.value = geminiClientData[key];
154
  }
155
+
156
  // 特別な処理が必要な要素
157
  if (key === 'characterCount' || key === 'encodeLength' || key === 'contentWidth') {
158
  const inputElem = document.getElementById(`${key}Input`);
 
165
  }
166
  });
167
  }
 
168
  }
169
 
170
  function createPayload() {
 
254
 
255
  const chunk = decoder.decode(value, { stream: true });
256
  buffer += chunk;
257
+ console.debug('チャンクを受信しまし:', chunk);
258
 
259
  // バッファから完全なJSONオブジェクトを抽出して処理
260
  let startIndex = 0;
 
289
  if (data.candidates[0].finishReason === 'STOP') {
290
  requestButton.classList.add('green-flash-bg');
291
  setTimeout(() => {
292
+ requestButton.classList.remove('green-flash-bg');
293
  }, 2000);
294
+ } else {
295
  requestButton.classList.add('red-flash-bg');
296
  setTimeout(() => {
297
  requestButton.classList.remove('red-flash-bg');
 
498
  });
499
  }
500
 
501
+ function tokenCount() {
502
  const selectedEndpoint = document.getElementById('endpointSelect').value;
503
+ let payload = createPayload();
504
+ payload.body = {
505
+ "contents": JSON.parse(payload.body).contents
506
+ };
507
+ payload.body = JSON.stringify(payload.body);
508
+ const ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/${selectedEndpoint}:countTokens?key=` + document.getElementById('geminiApiKey').value;
509
+ fetch(ENDPOINT, payload)
510
+ .then(response => response.json())
511
+ .then(data => {
512
+ console.log(data);
513
+ })
514
+ .catch(error => {
515
+ console.error('エラー:', error);
516
+ });
517
+ }
518
+
519
+ async function createDraft() {
520
+ const ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-002:generateContent?key=` + document.getElementById('geminiApiKey').value;
521
+ let payload = createPayload();
522
+ const response = await fetch(ENDPOINT, payload);
523
+ const data = await response.json();
524
+ const text = data.candidates[0].content.parts[0].text;
525
+ return text
526
+ }
527
+
528
+
529
+ async function Request() {
530
+ let selectedEndpoint = document.getElementById('endpointSelect').value;
531
+
532
+ document.getElementById('requestButton').disabled = true;
533
+ document.getElementById('novelContent2').value = '';
534
+ const outputAccordion = document.querySelector('#content2Collapse');
535
+ if (outputAccordion) {
536
+ const bsCollapse = new bootstrap.Collapse(outputAccordion, { toggle: false });
537
+ bsCollapse.show();
538
+ }
539
+
540
  let ENDPOINT;
541
  let payload;
542
 
543
  if (selectedEndpoint.startsWith('models/gemini')) {
544
  ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/${selectedEndpoint}:generateContent?key=` + document.getElementById('geminiApiKey').value;
545
  payload = createPayload();
546
+ } else if (selectedEndpoint === 'restart') {
547
+ ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro-002:generateContent?key=` + document.getElementById('geminiApiKey').value;
548
+ document.getElementById('novelContent2').value = '(下書き中)';
549
+ draft = await createDraft();
550
+ document.getElementById('novelContent2').value = `# 下書き\n${draft}\n\n# リライト\n`;
551
+ const prompt = `以下の文章を、要件を満たす形に直したものを返信してください。\n返信内容は直した後のテキストのみになるようにし他の内容が混ざらないようにしてください。\n\n# 要件\n- 元の文章や会話の言い回しは不自然な日本語になっている部分もかなり多いので、根本的に書き替えること。ただし、登場人物の一人称や他者への呼称は変更しないこと。\n- 物語の大筋やキャラクターの性格は保ったまま、文章は全く別物に書き換える必要がある。元のテキストそのものはなるべく流用しないで新規に書くよう心掛ける。\n- 随所にレトリックを駆使しつつも凝り過ぎて読み辛くなることのないシンプルな美文になることを意識する。\n- 登場人物の会話を重視し、文脈を読み取り元のキャラクター設定に合った台詞回しを保ちつつ、より生き生きとした魅力的な人物像に仕上がるようにする。\n- 細かい動作や心理描写のディテールを重視し、よりリアルな描写になるようにする。\n- 文章の終わりに「。」をつける、字下げをするなど、一般的な小説のフォーマットに従う書き方にする。\n\n# 文章\n${draft}`;
552
+ payload = {
553
+ method: 'POST',
554
+ headers: {},
555
+ body: JSON.stringify({
556
+ contents: [
557
+ {
558
+ "parts": [
559
+ {
560
+ "text": prompt
561
+ }
562
+ ],
563
+ "role": "user"
564
+ }
565
+ ],
566
+ "generationConfig": {
567
+ "temperature": 1.0,
568
+ "max_output_tokens": 4096
569
+ },
570
+ safetySettings: [
571
+ {
572
+ "category": "HARM_CATEGORY_HATE_SPEECH",
573
+ "threshold": "BLOCK_NONE"
574
+ },
575
+ {
576
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
577
+ "threshold": "BLOCK_NONE"
578
+ },
579
+ {
580
+ "category": "HARM_CATEGORY_HARASSMENT",
581
+ "threshold": "BLOCK_NONE"
582
+ },
583
+ {
584
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
585
+ "threshold": "BLOCK_NONE"
586
+ }
587
+ ]
588
+ }),
589
+ mode: 'cors'
590
+ };
591
+ selectedEndpoint = 'models/gemini-1.5-pro-002';
592
  } else {
593
  ENDPOINT = document.getElementById('openaiEndpoint').value;
594
  payload = createOpenAIPayload();
595
  }
596
 
 
597
  let stream = document.getElementById('streamToggle').checked;
 
598
 
599
  if (stream) {
600
  if (selectedEndpoint.startsWith('models/gemini')) {
 
611
  fetchOpenAINonStream(ENDPOINT, payload);
612
  }
613
  }
 
 
 
 
 
 
614
  }
615
 
616
  function stopGeneration() {
 
747
  }
748
  }
749
 
750
+ function generateIndexMenu() {
751
+ const content = document.getElementById('novelContent1').value;
752
+ const tokens = marked.lexer(content);
753
+ const indexOffcanvasBody = document.querySelector('#indexOffcanvas .offcanvas-body');
754
+
755
+ indexOffcanvasBody.innerHTML = '';
756
+
757
+ const rootUl = document.createElement('ul');
758
+ rootUl.className = 'list-unstyled';
759
+
760
+ let stack = [{ ul: rootUl, level: 0 }];
761
+ let lastHeading = null;
762
+ let contentBuffer = '';
763
+
764
+ tokens.forEach((token, index) => {
765
+ if (token.type === 'heading') {
766
+ if (lastHeading && contentBuffer.trim()) {
767
+ addTextarea(lastHeading.querySelector('ul'), contentBuffer.trim());
768
+ }
769
+ contentBuffer = '';
770
+
771
+ while (stack.length > 1 && stack[stack.length - 1].level >= token.depth) {
772
+ stack.pop();
773
+ }
774
+
775
+ const li = document.createElement('li');
776
+ const toggleBtn = document.createElement('button');
777
+ toggleBtn.className = 'btn btn-sm btn-outline-secondary me-2 toggle-btn';
778
+ const icon = document.createElement('i');
779
+ icon.className = 'fas fa-plus'; // Font Awesomeのプラスアイコン
780
+ toggleBtn.appendChild(icon);
781
+ toggleBtn.onclick = () => toggleSubMenu(li);
782
+
783
+ const a = document.createElement('a');
784
+ a.href = '#';
785
+ a.textContent = token.text;
786
+ a.onclick = (e) => {
787
+ e.preventDefault();
788
+ scrollToHeading(token.text);
789
+ };
790
+
791
+ li.appendChild(toggleBtn);
792
+ li.appendChild(a);
793
+
794
+ const subUl = document.createElement('ul');
795
+ subUl.className = 'list-unstyled ms-3 d-none';
796
+ li.appendChild(subUl);
797
+
798
+ stack[stack.length - 1].ul.appendChild(li);
799
+
800
+ if (token.depth > stack[stack.length - 1].level) {
801
+ stack.push({ ul: subUl, level: token.depth });
802
+ }
803
+
804
+ lastHeading = li;
805
+ } else if (token.type === 'text' || token.type === 'paragraph') {
806
+ contentBuffer += token.text + '\n';
807
+ }
808
+ });
809
+
810
+ if (lastHeading && contentBuffer.trim()) {
811
+ addTextarea(lastHeading.querySelector('ul'), contentBuffer.trim());
812
+ }
813
+
814
+ if (rootUl.children.length > 0) {
815
+ indexOffcanvasBody.appendChild(rootUl);
816
+ } else {
817
+ indexOffcanvasBody.textContent = '目次がありません';
818
+ }
819
+ }
820
+
821
+ function toggleSubMenu(li) {
822
+ const subUl = li.querySelector('ul');
823
+ const toggleBtn = li.querySelector('.toggle-btn');
824
+ const icon = toggleBtn.querySelector('i');
825
+ subUl.classList.toggle('d-none');
826
+ icon.className = subUl.classList.contains('d-none') ? 'fas fa-plus' : 'fas fa-minus';
827
+ }
828
+
829
+ function addTextarea(ul, content) {
830
+ const li = document.createElement('li');
831
+ const textarea = document.createElement('textarea');
832
+ textarea.readOnly = true;
833
+ textarea.className = 'form-control mt-2';
834
+ textarea.value = content;
835
+ textarea.rows = 3;
836
+ textarea.readOnly = true;
837
+ li.appendChild(textarea);
838
+ ul.appendChild(li);
839
+ }
840
+
841
+ function scrollToHeading(headingText) {
842
+ const content = document.getElementById('novelContent1');
843
+ const lines = content.value.split('\n');
844
+ let position = 0;
845
+
846
+ for (let i = 0; i < lines.length; i++) {
847
+ if (lines[i].trim().startsWith('#') && lines[i].includes(headingText)) {
848
+ // アコーディオンを開く
849
+ openAccordionContainingPosition(position);
850
+
851
+ content.focus();
852
+ content.setSelectionRange(position, position);
853
+ content.scrollTop = content.scrollHeight * (position / content.value.length);
854
+ break;
855
+ }
856
+ position += lines[i].length + 1; // +1 for newline character
857
+ }
858
+
859
+ // Offcanvasを閉じる
860
+ const indexOffcanvas = bootstrap.Offcanvas.getInstance(document.getElementById('indexOffcanvas'));
861
+ }
862
+
863
+ function openAccordionContainingPosition(position) {
864
+ const content = document.getElementById('novelContent1');
865
+ const accordionItems = document.querySelectorAll('#mainAccordion .accordion-item');
866
+ let currentPosition = 0;
867
+
868
+ for (let i = 0; i < accordionItems.length; i++) {
869
+ const textArea = accordionItems[i].querySelector('textarea');
870
+ if (textArea && textArea.id === 'novelContent1') {
871
+ if (position >= currentPosition && position < currentPosition + textArea.value.length) {
872
+ // このアコーディオンアイテムに目的の位置が含まれている
873
+ const collapseElement = accordionItems[i].querySelector('.accordion-collapse');
874
+ const bsCollapse = new bootstrap.Collapse(collapseElement, { toggle: false });
875
+ bsCollapse.show();
876
+ break;
877
+ }
878
+ currentPosition += textArea.value.length;
879
+ }
880
+ }
881
+ }
882
+
883
  document.addEventListener('DOMContentLoaded', function () {
884
  // ページ読み込み時にデータを復元
885
  loadFromUserStorage();
886
 
887
+ // メイン画面の要素のイベントリスナー。inputイベントが発生する頻度が非常に高いのでこちらの発動は5秒に1回に制限する
888
  ['novelContent1', 'novelContent2', 'generatePrompt', 'nextPrompt', 'savedTitle'].forEach(id => {
889
  document.getElementById(id).addEventListener('input', () => {
890
  saveToUserStorage(false);
891
+ generateIndexMenu();
892
  });
893
  });
894
 
 
896
  ['memo', 'geminiApiKey', 'endpointSelect', 'openaiEndpoint', 'openaiHeaders', 'openaiJsonBody', 'characterCount', 'encodeLength'].forEach(id => {
897
  document.getElementById(id).addEventListener('input', () => {
898
  saveToUserStorage(true);
899
+ generateIndexMenu();
900
  });
901
  });
902
 
903
  ['partialEncodeToggle', 'streamToggle'].forEach(id => {
904
  document.getElementById(id).addEventListener('change', () => {
905
  saveToUserStorage(true);
906
+ generateIndexMenu();
907
  });
908
  });
909
 
 
925
  // 60秒ごとに自動保存実行
926
  setInterval(() => {
927
  saveToUserStorage();
928
+ generateIndexMenu();
929
  }, 60000);
930
 
931
  // 基本設定のアコーディオンを開く
 
943
 
944
  // 初期表示時にも実行
945
  updateNavbarBrand();
946
+ generateIndexMenu();
947
  });