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