SenY commited on
Commit
d131a6f
1 Parent(s): 19c1163

Upload 2 files

Browse files
Files changed (2) hide show
  1. index.html +164 -19
  2. script.js +310 -0
index.html CHANGED
@@ -1,19 +1,164 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ #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>
script.js ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ let lastSaveTimestamp = 0;
2
+
3
+ function formatText(){
4
+ const textOrg = document.getElementById('novelContent1').value;
5
+ let text = textOrg.replace(/[」。)]/g, '$&\n');
6
+ while (text.includes('\n\n')) {
7
+ text = text.replace(/\n\n/g, '\n');
8
+ }
9
+ text = text.replace(/「([^」\n]*)\n([^」\n]*)」/g, '「$1$2」');
10
+ text = text.replace(/(([^)\n]*)\n([^)\n]*))/g, '($1$2)');
11
+
12
+ while (text.search(/「[^「\n]*。\n/) >= 0) {
13
+ text = text.replace(/「([^「\n]*。)\n/, '「$1');
14
+ }
15
+
16
+ text = text.replace(/\n/g, "\n\n");
17
+ text = text.replace(/\n#/g, "\n\n#");
18
+
19
+ document.getElementById('novelContent1').value = text;
20
+ }
21
+
22
+ function unmalform(text) {
23
+ let result = null;
24
+ while (!result && text) {
25
+ try {
26
+ result = decodeURI(text);
27
+ } catch (error) {
28
+ text = text.slice(0, -1);
29
+ }
30
+ }
31
+ return result || '';
32
+ }
33
+
34
+ function partialEncodeURI(text) {
35
+ if (!document.getElementById("partialEncodeToggle").checked) {
36
+ return text;
37
+ }
38
+ let length = document.getElementById("encodeLength").value;
39
+ const chunks = [];
40
+ for (let i = 0; i < text.length; i += 1) {
41
+ chunks.push(text.slice(i, i + 1));
42
+ }
43
+ const encodedChunks = chunks.map((chunk, index) => {
44
+ if (index % length === 0) {
45
+ return encodeURI(chunk);
46
+ }
47
+ return chunk;
48
+ });
49
+ return encodedChunks.join('');
50
+ }
51
+
52
+ function saveToJson() {
53
+ const novelContent1 = document.getElementById('novelContent1').value;
54
+ const novelContent2 = document.getElementById('novelContent2').value;
55
+ const generatePrompt = document.getElementById('generatePrompt').value;
56
+ const nextPrompt = document.getElementById('nextPrompt').value;
57
+ const savedTitle = document.getElementById('savedTitle').value;
58
+ const jsonData = JSON.stringify({
59
+ novelContent1: novelContent1,
60
+ novelContent2: novelContent2,
61
+ generatePrompt: generatePrompt,
62
+ nextPrompt: nextPrompt,
63
+ savedTitle: savedTitle
64
+ });
65
+ const blob = new Blob([jsonData], { type: 'application/json' });
66
+ const url = URL.createObjectURL(blob);
67
+ const a = document.createElement('a');
68
+ a.href = url;
69
+ a.download = 'novel_data.json';
70
+ if (savedTitle) {
71
+ a.download = savedTitle + '.json';
72
+ }
73
+ document.body.appendChild(a);
74
+ a.click();
75
+ document.body.removeChild(a);
76
+ URL.revokeObjectURL(url);
77
+ }
78
+
79
+ function loadFromJson() {
80
+ const fileInput = document.createElement('input');
81
+ fileInput.type = 'file';
82
+ fileInput.accept = '.json';
83
+ fileInput.style.display = 'none';
84
+ document.body.appendChild(fileInput);
85
+ fileInput.addEventListener('change', function (event) {
86
+ const file = event.target.files[0];
87
+ if (file) {
88
+ const reader = new FileReader();
89
+ reader.onload = function (e) {
90
+ try {
91
+ const jsonData = JSON.parse(e.target.result);
92
+ if (jsonData.novelContent1) {
93
+ document.getElementById('novelContent1').value = jsonData.novelContent1;
94
+ }
95
+ if (jsonData.novelContent2) {
96
+ document.getElementById('novelContent2').value = jsonData.novelContent2;
97
+ }
98
+ if (jsonData.generatePrompt) {
99
+ document.getElementById('generatePrompt').value = jsonData.generatePrompt;
100
+ }
101
+ if (jsonData.nextPrompt) {
102
+ document.getElementById('nextPrompt').value = jsonData.nextPrompt;
103
+ }
104
+ if (jsonData.savedTitle) {
105
+ document.getElementById('savedTitle').value = jsonData.savedTitle;
106
+ }
107
+ alert('JSONファイルを正常に読み込みました。');
108
+ } catch (error) {
109
+ alert('無効なJSONファイルです。');
110
+ }
111
+ };
112
+ reader.readAsText(file);
113
+ }
114
+ });
115
+ fileInput.click();
116
+ }
117
+
118
+ function saveToUserStorage(force = false) {
119
+ const currentTime = Date.now();
120
+ if (!force && currentTime - lastSaveTimestamp < 5000) {
121
+ return;
122
+ }
123
+ const content = document.getElementById('novelContent1').value;
124
+ const prompt = document.getElementById('generatePrompt').value;
125
+ const nextPrompt = document.getElementById('nextPrompt').value;
126
+ const savedTitle = document.getElementById('savedTitle').value;
127
+ localStorage.setItem('savedNovelContent', content);
128
+ localStorage.setItem('savedGeneratePrompt', prompt);
129
+ localStorage.setItem('savedNextPrompt', nextPrompt);
130
+ ['endpoint', 'headers', 'jsonBody'].forEach(id => {
131
+ localStorage.setItem(`saved${id}`, document.getElementById(id).value);
132
+ });
133
+ if (savedTitle) {
134
+ localStorage.setItem('savedTitle', savedTitle);
135
+ }
136
+ lastSaveTimestamp = currentTime;
137
+ }
138
+
139
+ // 60秒ごとに自動保存を実行
140
+ setInterval(() => {
141
+ saveToUserStorage();
142
+ }, 60000);
143
+ const savedContent = localStorage.getItem('savedNovelContent');
144
+ const savedPrompt = localStorage.getItem('savedGeneratePrompt');
145
+ const savedNextPrompt = localStorage.getItem('savedNextPrompt');
146
+ const savedTitle = localStorage.getItem('savedTitle');
147
+ if (savedContent) {
148
+ document.getElementById('novelContent1').value = savedContent;
149
+ }
150
+ if (savedPrompt) {
151
+ document.getElementById('generatePrompt').value = savedPrompt;
152
+ }
153
+ if (savedNextPrompt) {
154
+ document.getElementById('nextPrompt').value = savedNextPrompt;
155
+ }
156
+ if (savedTitle) {
157
+ document.getElementById('savedTitle').value = savedTitle;
158
+ }
159
+ ['endpoint', 'headers', 'jsonBody'].forEach(id => {
160
+ document.getElementById(id).value = localStorage.getItem(`saved${id}`);
161
+ });
162
+
163
+
164
+ ['novelContent1', 'generatePrompt', 'nextPrompt'].forEach(id => {
165
+ document.getElementById(id).addEventListener('input', saveToUserStorage);
166
+ });
167
+ ['endpoint', 'headers', 'jsonBody'].forEach(id => {
168
+ document.getElementById(id).addEventListener('input', () => {
169
+ saveToUserStorage(true);
170
+ });
171
+ });
172
+
173
+ document.querySelectorAll('[data-modal-text]').forEach(element => {
174
+ element.addEventListener('click', function() {
175
+ document.querySelectorAll(".modal-text").forEach(el => {
176
+ el.classList.add("d-none");
177
+ if(el.classList.contains(this.getAttribute('data-modal-text'))) {
178
+ el.classList.remove("d-none");
179
+ }
180
+ });
181
+
182
+
183
+ });
184
+ });
185
+
186
+
187
+ let controller;
188
+ function Request() {
189
+ let ENDPOINT;
190
+ let HEADERS;
191
+ let jsonBody;
192
+ try {
193
+ ENDPOINT = document.getElementById("endpoint").value;
194
+ HEADERS = JSON.parse(document.getElementById("headers").value);
195
+ jsonBody = JSON.parse(document.getElementById("jsonBody").value);
196
+ if (!ENDPOINT) {
197
+ throw new Error("エンドポイントが設定されていません。");
198
+ }
199
+ } catch (e) {
200
+ console.error(e);
201
+ document.querySelector(".modal-header").classList.add("bg-danger");
202
+ document.querySelector('[data-modal-text="modal-text-1"]').click();
203
+ return;
204
+ }
205
+ document.querySelector(".modal-header").classList.remove("bg-danger");
206
+
207
+
208
+ const novelContent1 = document.getElementById('novelContent1');
209
+ const novelContent2 = document.getElementById('novelContent2');
210
+ const text = novelContent1.value;
211
+ const lines = text.split('\n').filter(x => x);
212
+ let lastPart = lines.pop() || '';
213
+ let removedPart;
214
+ let messages = [
215
+ {
216
+ "content": document.getElementById('generatePrompt').value || ".",
217
+ "role": "system"
218
+ },
219
+ {
220
+ "content": ".",
221
+ "role": "user"
222
+ },
223
+ {
224
+ "content": partialEncodeURI(lines.join("\n")) || ".",
225
+ "role": "assistant"
226
+ }
227
+ ];
228
+ messages = messages.concat([
229
+ {
230
+ "content": `続きを${document.getElementById('characterCountInput').value}文字程度で書いてください。${partialEncodeURI(nextPrompt.value)}`,
231
+ "role": "user"
232
+ },
233
+ {
234
+ "content": lastPart,
235
+ "role": "assistant"
236
+ }
237
+ ]);
238
+ document.getElementById('requestButton').disabled = true;
239
+ let stream = document.getElementById('streamToggle').checked;
240
+ jsonBody.messages = messages;
241
+ jsonBody.stream = stream;
242
+ let payload = {
243
+ method: 'POST',
244
+ headers: HEADERS,
245
+ body: JSON.stringify(jsonBody)
246
+ }
247
+ if (stream === true) {
248
+ controller = new AbortController();
249
+ payload.signal = controller.signal;
250
+ }
251
+ console.debug(messages, JSON.stringify(messages).length);
252
+ fetch(ENDPOINT, payload)
253
+ .then(response => {
254
+ if (stream === true) {
255
+ const reader = response.body.getReader();
256
+ const decoder = new TextDecoder();
257
+ let buffer = '';
258
+ function readStream() {
259
+ reader.read().then(({ done, value }) => {
260
+ if (done) {
261
+ document.getElementById('stopButton').classList.add('d-none');
262
+ return;
263
+ }
264
+ buffer += decoder.decode(value, { stream: true });
265
+ const lines = buffer.split('\n');
266
+ buffer = lines.pop();
267
+ lines.forEach(line => {
268
+ if (line.startsWith('data: ')) {
269
+ const data = JSON.parse(line.slice(6));
270
+ if (data.choices && data.choices[0].delta.content) {
271
+ novelContent2.value += data.choices[0].delta.content;
272
+ novelContent2.scrollTop = novelContent2.scrollHeight;
273
+ }
274
+ }
275
+ });
276
+ readStream();
277
+ });
278
+ }
279
+ readStream();
280
+ } else {
281
+ return response.json();
282
+ }
283
+ })
284
+ .then(data => {
285
+ if (data) {
286
+ novelContent2.value += data.choices[0].message.content;
287
+ }
288
+ })
289
+ .catch(error => {
290
+ if (error.name === 'AbortError') {
291
+ console.debug('生成が中止されました');
292
+ } else {
293
+ console.error('エラー:', error);
294
+ }
295
+ })
296
+ .finally(() => {
297
+ document.getElementById('requestButton').disabled = false;
298
+ });
299
+ if (stream === true) {
300
+ document.getElementById('stopButton').classList.remove('d-none');
301
+ }
302
+ novelContent2.value = '';
303
+ }
304
+ function stopGeneration() {
305
+ if (controller) {
306
+ controller.abort();
307
+ controller = null;
308
+ }
309
+ document.getElementById('stopButton').classList.add('d-none');
310
+ }