XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/gpt4free

Serverless webview gui

905ced06
Heiner Lohaus <hlohaus@users.noreply.github.com>
提交于

代码差异

19 个文件 +436 -404
Renamed g4f/gui/client/index.html +13 -13
@@ -1,5 +1,5 @@
1 1 <!DOCTYPE html>
2 <html lang="en">
2 <html lang="en" data-framework="javascript">
3 3
4 4 <head>
5 5 <meta charset="UTF-8">
@@ -10,14 +10,14 @@
10 10 <meta property="og:image" content="https://openai.com/content/images/2022/11/ChatGPT.jpg">
11 11 <meta property="og:description" content="A conversational AI system that listens, learns, and challenges">
12 12 <meta property="og:url" content="https://g4f.ai">
13 <link rel="stylesheet" href="/assets/css/style.css">
14 <link rel="apple-touch-icon" sizes="180x180" href="/assets/img/apple-touch-icon.png">
15 <link rel="icon" type="image/png" sizes="32x32" href="/assets/img/favicon-32x32.png">
16 <link rel="icon" type="image/png" sizes="16x16" href="/assets/img/favicon-16x16.png">
17 <link rel="manifest" href="/assets/img/site.webmanifest">
18 <script src="/assets/js/icons.js"></script>
19 <script src="/assets/js/highlightjs-copy.min.js"></script>
20 <script src="/assets/js/chat.v1.js" defer></script>
13 <link rel="stylesheet" href="/static/css/style.css">
14 <link rel="apple-touch-icon" sizes="180x180" href="/static/img/apple-touch-icon.png">
15 <link rel="icon" type="image/png" sizes="32x32" href="/static/img/favicon-32x32.png">
16 <link rel="icon" type="image/png" sizes="16x16" href="/static/img/favicon-16x16.png">
17 <link rel="manifest" href="/static/img/site.webmanifest">
18 <script src="/static/js/icons.js"></script>
19 <script src="/static/js/highlightjs-copy.min.js"></script>
20 <script src="/static/js/chat.v1.js" defer></script>
21 21 <script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script>
22 22 <link rel="stylesheet"
23 23 href="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.7.0/build/styles/base16/dracula.min.css">
@@ -38,8 +38,8 @@
38 38 </script>
39 39 <script src="https://unpkg.com/gpt-tokenizer/dist/cl100k_base.js" async></script>
40 40 <script>
41 const user_image = '<img src="/assets/img/user.png" alt="your avatar">';
42 const gpt_image = '<img src="/assets/img/gpt.png" alt="your avatar">';
41 const user_image = '<img src="/static/img/user.png" alt="your avatar">';
42 const gpt_image = '<img src="/static/img/gpt.png" alt="your avatar">';
43 43 </script>
44 44 <style>
45 45 .hljs {
@@ -74,8 +74,8 @@
74 74 background: #8b3dff;
75 75 }
76 76 </style>
77 <script src="/assets/js/highlight.min.js"></script>
78 <script>window.conversation_id = `{{chat_id}}`</script>
77 <script src="/static/js/highlight.min.js"></script>
78 <script>window.conversation_id = "{{chat_id}}"</script>
79 79 <title>g4f - gui</title>
80 80 </head>
81 81
Renamed g4f/gui/client/static/css/style.css +0 -0
此文件没有可显示的逐行差异。
Renamed g4f/gui/client/static/img/android-chrome-192x192.png +0 -0
二进制文件已变更,无法进行逐行预览。
Renamed g4f/gui/client/static/img/android-chrome-512x512.png +0 -0
二进制文件已变更,无法进行逐行预览。
Renamed g4f/gui/client/static/img/apple-touch-icon.png +0 -0
二进制文件已变更,无法进行逐行预览。
Renamed g4f/gui/client/static/img/favicon-16x16.png +0 -0
二进制文件已变更,无法进行逐行预览。
Renamed g4f/gui/client/static/img/favicon-32x32.png +0 -0
二进制文件已变更,无法进行逐行预览。
Renamed g4f/gui/client/static/img/gpt.png +0 -0
二进制文件已变更,无法进行逐行预览。
Renamed g4f/gui/client/static/img/site.webmanifest +0 -0
此文件没有可显示的逐行差异。
Renamed g4f/gui/client/static/img/user.png +0 -0
二进制文件已变更,无法进行逐行预览。
Renamed g4f/gui/client/static/js/chat.v1.js +220 -200
@@ -1,5 +1,4 @@
1 1 const colorThemes = document.querySelectorAll('[name="theme"]');
2 const markdown = window.markdownit();
3 2 const message_box = document.getElementById(`messages`);
4 3 const messageInput = document.getElementById(`message-input`);
5 4 const box_conversations = document.querySelector(`.top`);
@@ -22,8 +21,6 @@ let prompt_lock = false;
22 21
23 22 const options = ["switch", "model", "model2", "jailbreak", "patch", "provider", "history"];
24 23
25 hljs.addPlugin(new CopyButtonPlugin());
26
27 24 messageInput.addEventListener("blur", () => {
28 25 window.scrollTo(0, 0);
29 26 });
@@ -39,31 +36,29 @@ appStorage = window.localStorage || {
39 36 length: 0
40 37 }
41 38
39 const markdown = window.markdownit();
42 40 const markdown_render = (content) => {
43 41 return markdown.render(content
44 .replaceAll(/<!--.+-->/gm, "")
42 .replaceAll(/<!-- generated images start -->[\s\S]+<!-- generated images end -->/gm, "")
45 43 .replaceAll(/<img data-prompt="[^>]+">/gm, "")
46 44 )
47 45 .replaceAll("<a href=", '<a target="_blank" href=')
48 46 .replaceAll('<code>', '<code class="language-plaintext">')
49 47 }
50 48
49 hljs.addPlugin(new CopyButtonPlugin());
51 50 let typesetPromise = Promise.resolve();
52 let timeoutHighlightId;
53 51 const highlight = (container) => {
54 if (timeoutHighlightId) clearTimeout(timeoutHighlightId);
55 timeoutHighlightId = setTimeout(() => {
56 container.querySelectorAll('code:not(.hljs').forEach((el) => {
57 if (el.className != "hljs") {
58 hljs.highlightElement(el);
59 }
60 });
61 typesetPromise = typesetPromise.then(
62 () => MathJax.typesetPromise([container])
63 ).catch(
64 (err) => console.log('Typeset failed: ' + err.message)
65 );
66 }, 100);
52 container.querySelectorAll('code:not(.hljs').forEach((el) => {
53 if (el.className != "hljs") {
54 hljs.highlightElement(el);
55 }
56 });
57 typesetPromise = typesetPromise.then(
58 () => MathJax.typesetPromise([container])
59 ).catch(
60 (err) => console.log('Typeset failed: ' + err.message)
61 );
67 62 }
68 63
69 64 const register_remove_message = async () => {
@@ -99,47 +94,48 @@ const handle_ask = async () => {
99 94 window.scrollTo(0, 0);
100 95
101 96 message = messageInput.value
102 if (message.length > 0) {
103 messageInput.value = "";
104 prompt_lock = true;
105 count_input()
106 await add_conversation(window.conversation_id, message);
107 if ("text" in fileInput.dataset) {
108 message += '\n```' + fileInput.dataset.type + '\n';
109 message += fileInput.dataset.text;
110 message += '\n```'
111 }
112 let message_index = await add_message(window.conversation_id, "user", message);
113 window.token = message_id();
114
115 if (imageInput.dataset.src) URL.revokeObjectURL(imageInput.dataset.src);
116 const input = imageInput && imageInput.files.length > 0 ? imageInput : cameraInput
117 if (input.files.length > 0) imageInput.dataset.src = URL.createObjectURL(input.files[0]);
118 else delete imageInput.dataset.src
119
120 message_box.innerHTML += `
121 <div class="message" data-index="${message_index}">
122 <div class="user">
123 ${user_image}
124 <i class="fa-solid fa-xmark"></i>
125 <i class="fa-regular fa-phone-arrow-up-right"></i>
126 </div>
127 <div class="content" id="user_${token}">
128 <div class="content_inner">
129 ${markdown_render(message)}
130 ${imageInput.dataset.src
131 ? '<img src="' + imageInput.dataset.src + '" alt="Image upload">'
132 : ''
133 }
134 </div>
135 <div class="count">${count_words_and_tokens(message, get_selected_model())}</div>
97 if (message.length <= 0) {
98 return;
99 }
100 messageInput.value = "";
101 prompt_lock = true;
102 count_input()
103 await add_conversation(window.conversation_id, message);
104
105 if ("text" in fileInput.dataset) {
106 message += '\n```' + fileInput.dataset.type + '\n';
107 message += fileInput.dataset.text;
108 message += '\n```'
109 }
110 let message_index = await add_message(window.conversation_id, "user", message);
111 window.token = message_id();
112
113 if (imageInput.dataset.src) URL.revokeObjectURL(imageInput.dataset.src);
114 const input = imageInput && imageInput.files.length > 0 ? imageInput : cameraInput
115 if (input.files.length > 0) imageInput.dataset.src = URL.createObjectURL(input.files[0]);
116 else delete imageInput.dataset.src
117
118 message_box.innerHTML += `
119 <div class="message" data-index="${message_index}">
120 <div class="user">
121 ${user_image}
122 <i class="fa-solid fa-xmark"></i>
123 <i class="fa-regular fa-phone-arrow-up-right"></i>
124 </div>
125 <div class="content" id="user_${token}">
126 <div class="content_inner">
127 ${markdown_render(message)}
128 ${imageInput.dataset.src
129 ? '<img src="' + imageInput.dataset.src + '" alt="Image upload">'
130 : ''
131 }
136 132 </div>
133 <div class="count">${count_words_and_tokens(message, get_selected_model())}</div>
137 134 </div>
138 `;
139 await register_remove_message();
140 highlight(message_box);
141 await ask_gpt();
142 }
135 </div>
136 `;
137 highlight(message_box);
138 await ask_gpt();
143 139 };
144 140
145 141 const remove_cancel_button = async () => {
@@ -151,7 +147,7 @@ const remove_cancel_button = async () => {
151 147 }, 300);
152 148 };
153 149
154 const prepare_messages = (messages, filter_last_message = true) => {
150 const prepare_messages = (messages, filter_last_message=true) => {
155 151 // Removes none user messages at end
156 152 if (filter_last_message) {
157 153 let last_message;
@@ -201,17 +197,52 @@ const prepare_messages = (messages, filter_last_message = true) => {
201 197 return new_messages;
202 198 }
203 199
200 async function add_message_chunk(message) {
201 if (message.type == "provider") {
202 window.provider_result = message.provider;
203 window.content.querySelector('.provider').innerHTML = `
204 <a href="${message.provider.url}" target="_blank">
205 ${message.provider.name}
206 </a>
207 ${message.provider.model ? ' with ' + message.provider.model : ''}
208 `
209 } else if (message.type == "message") {
210 console.error(messag.message)
211 return;
212 } else if (message.type == "error") {
213 console.error(message.error);
214 window.content_inner.innerHTML += `<p><strong>An error occured:</strong> ${message.error}</p>`;
215 } else if (message.type == "content") {
216 window.text += message.content;
217 html = markdown_render(window.text);
218 let lastElement, lastIndex = null;
219 for (element of ['</p>', '</code></pre>', '</p>\n</li>\n</ol>', '</li>\n</ol>', '</li>\n</ul>']) {
220 const index = html.lastIndexOf(element)
221 if (index - element.length > lastIndex) {
222 lastElement = element;
223 lastIndex = index;
224 }
225 }
226 if (lastIndex) {
227 html = html.substring(0, lastIndex) + '<span id="cursor"></span>' + lastElement;
228 }
229 window.content_inner.innerHTML = html;
230 window.content_count.innerText = count_words_and_tokens(text, window.provider_result?.model);
231 highlight(window.content_inner);
232 }
233
234 window.scrollTo(0, 0);
235 if (message_box.scrollTop >= message_box.scrollHeight - message_box.clientHeight - 100) {
236 message_box.scrollTo({ top: message_box.scrollHeight, behavior: "auto" });
237 }
238 }
239
204 240 const ask_gpt = async () => {
205 241 regenerate.classList.add(`regenerate-hidden`);
206 242 messages = await get_messages(window.conversation_id);
207 243 total_messages = messages.length;
208
209 244 messages = prepare_messages(messages);
210 245
211 window.scrollTo(0, 0);
212 window.controller = new AbortController();
213 window.text = "";
214
215 246 stop_generating.classList.remove(`stop_generating-hidden`);
216 247
217 248 message_box.scrollTop = message_box.scrollHeight;
@@ -234,103 +265,31 @@ const ask_gpt = async () => {
234 265 </div>
235 266 </div>
236 267 `;
237 content = document.getElementById(`gpt_${window.token}`);
238 content_inner = content.querySelector('.content_inner');
239 content_count = content.querySelector('.count');
268
269 window.controller = new AbortController();
270 window.text = "";
271 window.error = null;
272 window.provider_result = null;
273
274 window.content = document.getElementById(`gpt_${window.token}`);
275 window.content_inner = content.querySelector('.content_inner');
276 window.content_count = content.querySelector('.count');
240 277
241 278 message_box.scrollTop = message_box.scrollHeight;
242 279 window.scrollTo(0, 0);
243
244 error = provider_result = null;
245 280 try {
246 let body = JSON.stringify({
281 const input = imageInput && imageInput.files.length > 0 ? imageInput : cameraInput;
282 const file = input && input.files.length > 0 ? input.files[0] : null;
283 await api("conversation", {
247 284 id: window.token,
248 285 conversation_id: window.conversation_id,
249 286 model: get_selected_model(),
250 287 jailbreak: jailbreak?.options[jailbreak.selectedIndex].value,
251 web_search: document.getElementById(`switch`).checked,
288 web_search: document.getElementById("switch").checked,
252 289 provider: providerSelect.options[providerSelect.selectedIndex].value,
253 patch_provider: document.getElementById('patch')?.checked,
290 patch_provider: document.getElementById("patch")?.checked,
254 291 messages: messages
255 });
256 const headers = {
257 accept: 'text/event-stream'
258 }
259 const input = imageInput && imageInput.files.length > 0 ? imageInput : cameraInput
260 if (input && input.files.length > 0) {
261 const formData = new FormData();
262 formData.append('image', input.files[0]);
263 formData.append('json', body);
264 body = formData;
265 } else {
266 headers['content-type'] = 'application/json';
267 }
268
269 const response = await fetch(`/backend-api/v2/conversation`, {
270 method: 'POST',
271 signal: window.controller.signal,
272 headers: headers,
273 body: body
274 });
275 const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
276 let buffer = ""
277 while (true) {
278 const { value, done } = await reader.read();
279 if (done) break;
280 for (const line of value.split("\n")) {
281 if (!line) {
282 continue;
283 }
284 let message;
285 try {
286 message = JSON.parse(buffer + line);
287 buffer = "";
288 } catch {
289 buffer += line
290 continue;
291 }
292 if (message.type == "content") {
293 text += message.content;
294 } else if (message.type == "provider") {
295 provider_result = message.provider
296 content.querySelector('.provider').innerHTML = `
297 <a href="${provider_result.url}" target="_blank">
298 ${provider_result.name}
299 </a>
300 ${provider_result.model ? ' with ' + provider_result.model : ''}
301 `
302 } else if (message.type == "error") {
303 error = message.error;
304 } else if (messag.type == "message") {
305 console.error(messag.message)
306 }
307 }
308 if (error) {
309 console.error(error);
310 content_inner.innerHTML += `<p><strong>An error occured:</strong> ${error}</p>`;
311 } else {
312 html = markdown_render(text);
313 let lastElement, lastIndex = null;
314 for (element of ['</p>', '</code></pre>', '</p>\n</li>\n</ol>', '</li>\n</ol>', '</li>\n</ul>']) {
315 const index = html.lastIndexOf(element)
316 if (index - element.length > lastIndex) {
317 lastElement = element;
318 lastIndex = index;
319 }
320 }
321 if (lastIndex) {
322 html = html.substring(0, lastIndex) + '<span id="cursor"></span>' + lastElement;
323 }
324 content_inner.innerHTML = html;
325 content_count.innerText = count_words_and_tokens(text, provider_result?.model);
326 highlight(content_inner);
327 }
328
329 window.scrollTo(0, 0);
330 if (message_box.scrollTop >= message_box.scrollHeight - message_box.clientHeight - 100) {
331 message_box.scrollTo({ top: message_box.scrollHeight, behavior: "auto" });
332 }
333 }
292 }, file);
334 293 if (!error) {
335 294 html = markdown_render(text);
336 295 content_inner.innerHTML = html;
@@ -355,7 +314,7 @@ const ask_gpt = async () => {
355 314 await add_message(window.conversation_id, "assistant", text, provider_result);
356 315 await load_conversation(window.conversation_id);
357 316 } else {
358 let cursorDiv = document.getElementById(`cursor`);
317 let cursorDiv = document.getElementById("cursor");
359 318 if (cursorDiv) cursorDiv.parentNode.removeChild(cursorDiv);
360 319 }
361 320 window.scrollTo(0, 0);
@@ -444,7 +403,7 @@ const new_conversation = async () => {
444 403 say_hello();
445 404 };
446 405
447 const load_conversation = async (conversation_id, scroll = true) => {
406 const load_conversation = async (conversation_id, scroll=true) => {
448 407 let conversation = await get_conversation(conversation_id);
449 408 let messages = conversation?.items || [];
450 409
@@ -459,7 +418,6 @@ const load_conversation = async (conversation_id, scroll = true) => {
459 418 last_model = item.provider?.model;
460 419 let next_i = parseInt(i) + 1;
461 420 let next_provider = item.provider ? item.provider : (messages.length > next_i ? messages[next_i].provider : null);
462
463 421 let provider_link = item.provider?.name ? `<a href="${item.provider.url}" target="_blank">${item.provider.name}</a>` : "";
464 422 let provider = provider_link ? `
465 423 <div class="provider">
@@ -496,7 +454,6 @@ const load_conversation = async (conversation_id, scroll = true) => {
496 454 }
497 455
498 456 message_box.innerHTML = elements;
499
500 457 register_remove_message();
501 458 highlight(message_box);
502 459
@@ -548,7 +505,9 @@ async function add_conversation(conversation_id, content) {
548 505 }
549 506
550 507 async function save_system_message() {
551 if (!window.conversation_id) return;
508 if (!window.conversation_id) {
509 return;
510 }
552 511 const conversation = await get_conversation(window.conversation_id);
553 512 conversation.system = systemPrompt?.value;
554 513 await save_conversation(window.conversation_id, conversation);
@@ -585,7 +544,6 @@ const remove_message = async (conversation_id, index) => {
585 544
586 545 const add_message = async (conversation_id, role, content, provider) => {
587 546 const conversation = await get_conversation(conversation_id);
588
589 547 conversation.items.push({
590 548 role: role,
591 549 content: content,
@@ -667,11 +625,10 @@ sidebar_button.addEventListener("click", (event) => {
667 625 sidebar.classList.add("shown");
668 626 sidebar_button.classList.add("rotated");
669 627 }
670
671 628 window.scrollTo(0, 0);
672 629 });
673 630
674 const register_settings_localstorage = async () => {
631 const register_settings_storage = async () => {
675 632 options.forEach((id) => {
676 633 element = document.getElementById(id);
677 634 if (!element) {
@@ -692,10 +649,10 @@ const register_settings_localstorage = async () => {
692 649 });
693 650 }
694 651
695 const load_settings_localstorage = async () => {
652 const load_settings_storage = async () => {
696 653 options.forEach((id) => {
697 654 element = document.getElementById(id);
698 if (!element || !(value = appStorage.getItem(element.id))) {
655 if (!element || !(value = appStorage.getItem(id))) {
699 656 return;
700 657 }
701 658 if (value) {
@@ -807,9 +764,21 @@ systemPrompt.addEventListener("blur", function() {
807 764 count_input();
808 765 });
809 766
810 window.onload = async () => {
811 setTheme();
767 window.addEventListener('load', async function() {
768 await on_load();
769 if (window.conversation_id == "{{chat_id}}") {
770 window.conversation_id = uuid();
771 } else {
772 await on_api();
773 }
774 });
812 775
776 window.addEventListener('pywebviewready', async function() {
777 await on_api();
778 });
779
780 async function on_load() {
781 setTheme();
813 782 count_input();
814 783
815 784 if (/\/chat\/.+/.test(window.location.href)) {
@@ -817,9 +786,10 @@ window.onload = async () => {
817 786 } else {
818 787 say_hello()
819 788 }
820
821 789 load_conversations();
790 }
822 791
792 async function on_api() {
823 793 messageInput.addEventListener("keydown", async (evt) => {
824 794 if (prompt_lock) return;
825 795
@@ -832,58 +802,44 @@ window.onload = async () => {
832 802 messageInput.style.height = messageInput.scrollHeight + "px";
833 803 }
834 804 });
835
836 805 sendButton.addEventListener(`click`, async () => {
837 806 console.log("clicked send");
838 807 if (prompt_lock) return;
839 808 await handle_ask();
840 809 });
841
842 810 messageInput.focus();
843 811
844 register_settings_localstorage();
845 };
812 register_settings_storage();
846 813
847 (async () => {
848 response = await fetch('/backend-api/v2/models')
849 models = await response.json()
814 versions = await api('version')
815 document.title = 'g4f - ' + versions["version"];
816 let text = "version ~ "
817 if (versions["version"] != versions["latest_version"]) {
818 let release_url = 'https://github.com/xtekky/gpt4free/releases/tag/' + versions["latest_version"];
819 let title = `New version: ${versions["latest_version"]}`;
820 text += `<a href="${release_url}" target="_blank" title="${title}">${versions["version"]} 🆕</a>`;
821 } else {
822 text += versions["version"];
823 }
824 document.getElementById("version_text").innerHTML = text
850 825
851 for (model of models) {
826 models = await api("models");
827 models.forEach((model) => {
852 828 let option = document.createElement('option');
853 829 option.value = option.text = model;
854 830 modelSelect.appendChild(option);
855 }
856
857 response = await fetch('/backend-api/v2/providers')
858 providers = await response.json()
859 select = document.getElementById('provider');
831 });
860 832
833 providers = await api('providers')
861 834 providers.forEach((provider) => {
862 835 let option = document.createElement('option');
863 836 option.value = option.text = provider;
864 select.appendChild(option);
837 providerSelect.appendChild(option);
865 838 })
866 839
867 await load_provider_models();
868
869 await load_settings_localstorage()
870 })();
871
872 (async () => {
873 response = await fetch('/backend-api/v2/version')
874 versions = await response.json()
875
876 document.title = 'g4f - gui - ' + versions["version"];
877 let text = "version ~ "
878 if (versions["version"] != versions["latest_version"]) {
879 let release_url = 'https://github.com/xtekky/gpt4free/releases/tag/' + versions["latest_version"];
880 let title = `New version: ${versions["latest_version"]}`;
881 text += `<a href="${release_url}" target="_blank" title="${title}">${versions["version"]} 🆕</a>`;
882 } else {
883 text += versions["version"];
884 }
885 document.getElementById("version_text").innerHTML = text
886 })()
840 load_provider_models();
841 load_settings_storage()
842 }
887 843
888 844 for (const el of [imageInput, cameraInput]) {
889 845 el.addEventListener('click', async () => {
@@ -899,6 +855,7 @@ fileInput.addEventListener('click', async (event) => {
899 855 fileInput.value = '';
900 856 delete fileInput.dataset.text;
901 857 });
858
902 859 fileInput.addEventListener('change', async (event) => {
903 860 if (fileInput.files.length) {
904 861 type = fileInput.files[0].type;
@@ -947,10 +904,73 @@ function get_selected_model() {
947 904 }
948 905 }
949 906
907 async function api(ressource, args=null, file=null) {
908 if (window?.pywebview) {
909 if (args) {
910 if (ressource == "models") {
911 ressource = "provider_models";
912 }
913 return pywebview.api["get_" + ressource](args);
914 }
915 return pywebview.api["get_" + ressource]();
916 }
917 if (ressource == "models" && args) {
918 ressource = `${ressource}/${args}`;
919 }
920 const url = `/backend-api/v2/${ressource}`;
921 if (ressource == "conversation") {
922 const body = JSON.stringify(args);
923 const headers = {
924 accept: 'text/event-stream'
925 }
926 if (file) {
927 const formData = new FormData();
928 formData.append('file', file);
929 formData.append('json', body);
930 body = formData;
931 } else {
932 headers['content-type'] = 'application/json';
933 }
934 response = await fetch(url, {
935 method: 'POST',
936 signal: window.controller.signal,
937 headers: headers,
938 body: body
939 });
940 return read_response(response);
941 }
942 response = await fetch(url);
943 return await response.json();
944 }
945
946 async function read_response(response) {
947 const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
948 let buffer = ""
949 while (true) {
950 const { value, done } = await reader.read();
951 if (done) {
952 break;
953 }
954 for (const line of value.split("\n")) {
955 if (!line) {
956 continue;
957 }
958 try {
959 add_message_chunk(JSON.parse(buffer + line))
960 buffer = "";
961 } catch {
962 buffer += line
963 }
964 }
965 }
966 }
967
950 968 async function load_provider_models() {
951 provider = providerSelect.options[providerSelect.selectedIndex].value;
952 response = await fetch('/backend-api/v2/models/' + provider);
953 models = await response.json();
969 const provider = providerSelect.options[providerSelect.selectedIndex].value;
970 if (!provider) {
971 return;
972 }
973 const models = await api('models', provider);
954 974 modelProvider.innerHTML = '';
955 975 if (models.length > 0) {
956 976 modelSelect.classList.add("hidden");
Renamed g4f/gui/client/static/js/highlight.min.js +0 -0
此文件没有可显示的逐行差异。
Renamed g4f/gui/client/static/js/highlightjs-copy.min.js +0 -0
此文件没有可显示的逐行差异。
Renamed g4f/gui/client/static/js/icons.js +0 -0
此文件没有可显示的逐行差异。
Added g4f/gui/server/api.py +172 -0
@@ -0,0 +1,172 @@
1 import logging
2 import json
3 from typing import Iterator
4
5 try:
6 import webview
7 except ImportError:
8 ...
9
10 from g4f import version, models
11 from g4f import get_last_provider, ChatCompletion
12 from g4f.errors import VersionNotFoundError
13 from g4f.Provider import ProviderType, __providers__, __map__
14 from g4f.providers.base_provider import ProviderModelMixin
15 from g4f.Provider.bing.create_images import patch_provider
16
17 class Api():
18 def get_models(self) -> list[str]:
19 """
20 Return a list of all models.
21
22 Fetches and returns a list of all available models in the system.
23
24 Returns:
25 List[str]: A list of model names.
26 """
27 return models._all_models
28
29 def get_provider_models(self, provider: str) -> list[dict]:
30 if provider in __map__:
31 provider: ProviderType = __map__[provider]
32 if issubclass(provider, ProviderModelMixin):
33 return [{"model": model, "default": model == provider.default_model} for model in provider.get_models()]
34 elif provider.supports_gpt_35_turbo or provider.supports_gpt_4:
35 return [
36 *([{"model": "gpt-4", "default": not provider.supports_gpt_4}] if provider.supports_gpt_4 else []),
37 *([{"model": "gpt-3.5-turbo", "default": not provider.supports_gpt_4}] if provider.supports_gpt_35_turbo else [])
38 ]
39 else:
40 return [];
41
42 def get_providers(self) -> list[str]:
43 """
44 Return a list of all working providers.
45 """
46 return [provider.__name__ for provider in __providers__ if provider.working]
47
48 def get_version(self):
49 """
50 Returns the current and latest version of the application.
51
52 Returns:
53 dict: A dictionary containing the current and latest version.
54 """
55 try:
56 current_version = version.utils.current_version
57 except VersionNotFoundError:
58 current_version = None
59 return {
60 "version": current_version,
61 "latest_version": version.utils.latest_version,
62 }
63
64 def generate_title(self):
65 """
66 Generates and returns a title based on the request data.
67
68 Returns:
69 dict: A dictionary with the generated title.
70 """
71 return {'title': ''}
72
73 def get_conversation(self, options: dict, **kwargs) -> Iterator:
74 window = webview.active_window()
75 for message in self._create_response_stream(
76 self._prepare_conversation_kwargs(options, kwargs)
77 ):
78 window.evaluate_js(f"this.add_message_chunk({json.dumps(message)})")
79
80 def _prepare_conversation_kwargs(self, json_data: dict, kwargs: dict):
81 """
82 Prepares arguments for chat completion based on the request data.
83
84 Reads the request and prepares the necessary arguments for handling
85 a chat completion request.
86
87 Returns:
88 dict: Arguments prepared for chat completion.
89 """
90 provider = json_data.get('provider', None)
91 if "image" in kwargs and provider is None:
92 provider = "Bing"
93 if provider == 'OpenaiChat':
94 kwargs['auto_continue'] = True
95
96 messages = json_data['messages']
97 if json_data.get('web_search'):
98 if provider == "Bing":
99 kwargs['web_search'] = True
100 else:
101 from .internet import get_search_message
102 messages[-1]["content"] = get_search_message(messages[-1]["content"])
103
104 model = json_data.get('model')
105 model = model if model else models.default
106 patch = patch_provider if json_data.get('patch_provider') else None
107
108 return {
109 "model": model,
110 "provider": provider,
111 "messages": messages,
112 "stream": True,
113 "ignore_stream": True,
114 "patch_provider": patch,
115 **kwargs
116 }
117
118 def _create_response_stream(self, kwargs) -> Iterator:
119 """
120 Creates and returns a streaming response for the conversation.
121
122 Args:
123 kwargs (dict): Arguments for creating the chat completion.
124
125 Yields:
126 str: JSON formatted response chunks for the stream.
127
128 Raises:
129 Exception: If an error occurs during the streaming process.
130 """
131 try:
132 first = True
133 for chunk in ChatCompletion.create(**kwargs):
134 if first:
135 first = False
136 yield self._format_json('provider', get_last_provider(True))
137 if isinstance(chunk, Exception):
138 logging.exception(chunk)
139 yield self._format_json('message', get_error_message(chunk))
140 else:
141 yield self._format_json('content', chunk)
142 except Exception as e:
143 logging.exception(e)
144 yield self._format_json('error', get_error_message(e))
145
146 def _format_json(self, response_type: str, content):
147 """
148 Formats and returns a JSON response.
149
150 Args:
151 response_type (str): The type of the response.
152 content: The content to be included in the response.
153
154 Returns:
155 str: A JSON formatted string.
156 """
157 return {
158 'type': response_type,
159 response_type: content
160 }
161
162 def get_error_message(exception: Exception) -> str:
163 """
164 Generates a formatted error message from an exception.
165
166 Args:
167 exception (Exception): The exception to format.
168
169 Returns:
170 str: A formatted error message string.
171 """
172 return f"{get_last_provider().__name__}: {type(exception).__name__}: {exception}"
Modified g4f/gui/server/app.py +3 -3
@@ -2,8 +2,8 @@ import sys, os
2 2 from flask import Flask
3 3
4 4 if getattr(sys, 'frozen', False):
5 template_folder = os.path.join(sys._MEIPASS, "client/html")
5 template_folder = os.path.join(sys._MEIPASS, "client")
6 6 else:
7 template_folder = "./../client/html"
7 template_folder = "../client"
8 8
9 app = Flask(__name__, template_folder=template_folder)
9 app = Flask(__name__, template_folder=template_folder, static_folder=f"{template_folder}/static")
Modified g4f/gui/server/backend.py +17 -157
@@ -1,16 +1,9 @@
1 import logging
2 1 import json
3 2 from flask import request, Flask
4 from typing import Generator
5 from g4f import version, models
6 from g4f import get_last_provider, ChatCompletion
7 3 from g4f.image import is_allowed_extension, to_image
8 from g4f.errors import VersionNotFoundError
9 from g4f.Provider import ProviderType, __providers__, __map__
10 from g4f.providers.base_provider import ProviderModelMixin
11 from g4f.Provider.bing.create_images import patch_provider
4 from .api import Api
12 5
13 class Backend_Api:
6 class Backend_Api(Api):
14 7 """
15 8 Handles various endpoints in a Flask application for backend operations.
16 9
@@ -59,7 +52,7 @@ class Backend_Api:
59 52 'methods': ['POST']
60 53 }
61 54 }
62
55
63 56 def handle_error(self):
64 57 """
65 58 Initialize the backend API with the given Flask application.
@@ -69,63 +62,7 @@ class Backend_Api:
69 62 """
70 63 print(request.json)
71 64 return 'ok', 200
72
73 def get_models(self):
74 """
75 Return a list of all models.
76
77 Fetches and returns a list of all available models in the system.
78
79 Returns:
80 List[str]: A list of model names.
81 """
82 return models._all_models
83
84 def get_provider_models(self, provider: str):
85 if provider in __map__:
86 provider: ProviderType = __map__[provider]
87 if issubclass(provider, ProviderModelMixin):
88 return [{"model": model, "default": model == provider.default_model} for model in provider.get_models()]
89 elif provider.supports_gpt_35_turbo or provider.supports_gpt_4:
90 return [
91 *([{"model": "gpt-3.5-turbo", "default": not provider.supports_gpt_4}] if provider.supports_gpt_35_turbo else []),
92 *([{"model": "gpt-4", "default": not provider.supports_gpt_4}] if provider.supports_gpt_4 else [])
93 ]
94 else:
95 return [];
96 return 404, "Provider not found"
97
98 def get_providers(self):
99 """
100 Return a list of all working providers.
101 """
102 return [provider.__name__ for provider in __providers__ if provider.working]
103
104 def get_version(self):
105 """
106 Returns the current and latest version of the application.
107
108 Returns:
109 dict: A dictionary containing the current and latest version.
110 """
111 try:
112 current_version = version.utils.current_version
113 except VersionNotFoundError:
114 current_version = None
115 return {
116 "version": current_version,
117 "latest_version": version.utils.latest_version,
118 }
119
120 def generate_title(self):
121 """
122 Generates and returns a title based on the request data.
123 65
124 Returns:
125 dict: A dictionary with the generated title.
126 """
127 return {'title': ''}
128
129 66 def handle_conversation(self):
130 67 """
131 68 Handles conversation requests and streams responses back.
@@ -133,26 +70,10 @@ class Backend_Api:
133 70 Returns:
134 71 Response: A Flask response object for streaming.
135 72 """
136 kwargs = self._prepare_conversation_kwargs()
137
138 return self.app.response_class(
139 self._create_response_stream(kwargs),
140 mimetype='text/event-stream'
141 )
142
143 def _prepare_conversation_kwargs(self):
144 """
145 Prepares arguments for chat completion based on the request data.
146
147 Reads the request and prepares the necessary arguments for handling
148 a chat completion request.
149
150 Returns:
151 dict: Arguments prepared for chat completion.
152 """
73
153 74 kwargs = {}
154 if "image" in request.files:
155 file = request.files['image']
75 if "file" in request.files:
76 file = request.files['file']
156 77 if file.filename != '' and is_allowed_extension(file.filename):
157 78 kwargs['image'] = to_image(file.stream, file.filename.endswith('.svg'))
158 79 kwargs['image_name'] = file.filename
@@ -160,66 +81,20 @@ class Backend_Api:
160 81 json_data = json.loads(request.form['json'])
161 82 else:
162 83 json_data = request.json
163
164 provider = json_data.get('provider', '').replace('g4f.Provider.', '')
165 provider = provider if provider and provider != "Auto" else None
166
167 if "image" in kwargs and not provider:
168 provider = "Bing"
169 if provider == 'OpenaiChat':
170 kwargs['auto_continue'] = True
171
172 messages = json_data['messages']
173 if json_data.get('web_search'):
174 if provider == "Bing":
175 kwargs['web_search'] = True
176 else:
177 # ResourceWarning: unclosed event loop
178 from .internet import get_search_message
179 messages[-1]["content"] = get_search_message(messages[-1]["content"])
180
181 model = json_data.get('model')
182 model = model if model else models.default
183 patch = patch_provider if json_data.get('patch_provider') else None
184
185 return {
186 "model": model,
187 "provider": provider,
188 "messages": messages,
189 "stream": True,
190 "ignore_stream": True,
191 "patch_provider": patch,
192 **kwargs
193 }
194 84
195 def _create_response_stream(self, kwargs) -> Generator[str, None, None]:
196 """
197 Creates and returns a streaming response for the conversation.
85 kwargs = self._prepare_conversation_kwargs(json_data, kwargs)
198 86
199 Args:
200 kwargs (dict): Arguments for creating the chat completion.
87 return self.app.response_class(
88 self._create_response_stream(kwargs),
89 mimetype='text/event-stream'
90 )
201 91
202 Yields:
203 str: JSON formatted response chunks for the stream.
92 def get_provider_models(self, provider: str):
93 models = super().get_provider_models(provider)
94 if models is None:
95 return 404, "Provider not found"
96 return models
204 97
205 Raises:
206 Exception: If an error occurs during the streaming process.
207 """
208 try:
209 first = True
210 for chunk in ChatCompletion.create(**kwargs):
211 if first:
212 first = False
213 yield self._format_json('provider', get_last_provider(True))
214 if isinstance(chunk, Exception):
215 logging.exception(chunk)
216 yield self._format_json('message', get_error_message(chunk))
217 else:
218 yield self._format_json('content', str(chunk))
219 except Exception as e:
220 logging.exception(e)
221 yield self._format_json('error', get_error_message(e))
222
223 98 def _format_json(self, response_type: str, content) -> str:
224 99 """
225 100 Formats and returns a JSON response.
@@ -231,19 +106,4 @@ class Backend_Api:
231 106 Returns:
232 107 str: A JSON formatted string.
233 108 """
234 return json.dumps({
235 'type': response_type,
236 response_type: content
237 }) + "\n"
238
239 def get_error_message(exception: Exception) -> str:
240 """
241 Generates a formatted error message from an exception.
242
243 Args:
244 exception (Exception): The exception to format.
245
246 Returns:
247 str: A formatted error message string.
248 """
249 return f"{get_last_provider().__name__}: {type(exception).__name__}: {exception}"
109 return json.dumps(super()._format_json(response_type, content)) + "\n"
Modified g4f/gui/server/website.py +4 -22
@@ -1,12 +1,5 @@
1 from flask import render_template, send_file, redirect
2 from time import time
3 from os import urandom
4 import sys, os
5
6 if getattr(sys, 'frozen', False):
7 assets_folder = os.path.join(sys._MEIPASS, "client")
8 else:
9 assets_folder = "./../client"
1 import uuid
2 from flask import render_template, redirect
10 3
11 4 class Website:
12 5 def __init__(self, app) -> None:
@@ -24,23 +17,12 @@ class Website:
24 17 'function': self._chat,
25 18 'methods': ['GET', 'POST']
26 19 },
27 '/assets/<folder>/<file>': {
28 'function': self._assets,
29 'methods': ['GET', 'POST']
30 }
31 20 }
32 21
33 22 def _chat(self, conversation_id):
34 23 if '-' not in conversation_id:
35 24 return redirect('/chat')
36
37 return render_template('index.html', chat_id = conversation_id)
25 return render_template('index.html', chat_id=conversation_id)
38 26
39 27 def _index(self):
40 return render_template('index.html', chat_id = f'{urandom(4).hex()}-{urandom(2).hex()}-{urandom(2).hex()}-{urandom(2).hex()}-{hex(int(time() * 1000))[2:]}')
41
42 def _assets(self, folder: str, file: str):
43 try:
44 return send_file(f"{assets_folder}/{folder}/{file}", as_attachment=False)
45 except:
46 return "File not found", 404
28 return render_template('index.html', chat_id=str(uuid.uuid4()))
Modified g4f/gui/webview.py +7 -9
@@ -1,34 +1,32 @@
1 1 import webview
2 from functools import partial
3 2 try:
4 3 from platformdirs import user_config_dir
5 4 has_platformdirs = True
6 5 except ImportError:
7 6 has_platformdirs = False
8 7
9 from g4f.gui import run_gui
10 8 from g4f.gui.run import gui_parser
9 from g4f.gui.server.api import Api
11 10 import g4f.version
12 11 import g4f.debug
13 12
14 13 def run_webview(
15 host: str = "0.0.0.0",
16 port: int = 8080,
17 14 debug: bool = False,
18 15 storage_path: str = None
19 16 ):
20 17 webview.create_window(
21 18 f"g4f - {g4f.version.utils.current_version}",
22 f"http://{host}:{port}/",
23 text_select=True
19 "client/index.html",
20 text_select=True,
21 js_api=Api(),
24 22 )
25 23 if has_platformdirs and storage_path is None:
26 24 storage_path = user_config_dir("g4f-webview")
27 25 webview.start(
28 partial(run_gui, host, port),
29 26 private_mode=False,
30 27 storage_path=storage_path,
31 debug=debug
28 debug=debug,
29 ssl=True
32 30 )
33 31
34 32 if __name__ == "__main__":
@@ -36,4 +34,4 @@ if __name__ == "__main__":
36 34 args = parser.parse_args()
37 35 if args.debug:
38 36 g4f.debug.logging = True
39 run_webview(args.host, args.port, args.debug)
37 run_webview(args.debug)