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

XFEstudio/gpt4free

Use custom user data dir for each provider Reuse cookies and access token in Copilot Send in the gui messages to multiple providers at once Add GUI documenation

6a624acf
Heiner Lohaus <hlohaus@users.noreply.github.com>
提交于

代码差异

15 个文件 +481 -123
Modified README.md +16 -6
@@ -132,17 +132,27 @@ To ensure the seamless operation of our application, please follow the instructi
132 132
133 133 By following these steps, you should be able to successfully install and run the application on your Windows system. If you encounter any issues during the installation process, please refer to our Issue Tracker or try to get contact over Discord for assistance.
134 134
135 Run the **Webview UI** on other Platforms:
135 ---
136
137 ### Learn More About the GUI
138
139 For detailed instructions on how to set up, configure, and use the GPT4Free GUI, refer to the **GUI Documentation**:
140
141 - [GUI Documentation](docs/gui.md)
136 142
137 - [/docs/webview](docs/webview.md)
143 This guide includes step-by-step details on provider selection, managing conversations, using advanced features like speech recognition, and more.
138 144
139 ##### Use your smartphone:
145 ---
146
147 ### Use Your Smartphone
140 148
141 Run the Web UI on Your Smartphone:
149 Run the Web UI on your smartphone for easy access on the go. Check out the dedicated guide to learn how to set up and use the GUI on your mobile device:
142 150
143 - [/docs/guides/phone](docs/guides/phone.md)
151 - [Run on Smartphone Guide](docs/guides/phone.md)
152
153 ---
144 154
145 #### Use python
155 ### Use python
146 156
147 157 ##### Prerequisites:
148 158
Added docs/gui.md +147 -0
@@ -0,0 +1,147 @@
1 # G4F - GUI Documentation
2
3 ## Overview
4 The G4F GUI is a self-contained, user-friendly interface designed for interacting with multiple AI models from various providers. It allows users to generate text, code, and images effortlessly. Advanced features such as speech recognition, file uploads, conversation backup/restore, and more are included. Both the backend and frontend are fully integrated into the GUI, making setup simple and seamless.
5
6 ## Features
7
8 ### 1. **Multiple Providers and Models**
9 - **Provider/Model Selection via Dropdown:** Use the select box to choose a specific **provider/model combination**.
10 - **Pinning Provider/Model Combinations:** After selecting a provider and model from the dropdown, click the **pin button** to add the combination to the pinned list.
11 - **Remove Pinned Combinations:** Each pinned provider/model combination is displayed as a button. Clicking on the button removes it from the pinned list.
12 - **Send Requests to Multiple Providers:** You can pin multiple provider/model combinations and send requests to all of them simultaneously, enabling fast and comprehensive content generation.
13
14 ### 2. **Text, Code, and Image Generation**
15 - **Text and Code Generation:** Enter prompts to generate text or code outputs.
16 - **Image Generation:** Provide text prompts to generate images, which are shown as thumbnails. Clicking on a thumbnail opens the image in a lightbox view.
17
18 ### 3. **Gallery Functionality**
19 - **Image Thumbnails:** Generated images appear as small thumbnails within the conversation.
20 - **Lightbox View:** Clicking a thumbnail opens the image in full size, along with the prompt used to generate it.
21 - **Automatic Image Download:** Enable automatic downloading of generated images through the settings.
22
23 ### 4. **Conversation Management**
24 - **Message Reuse:** While messages can't be edited, you can copy and reuse them.
25 - **Message Deletion:** Conversations can be deleted for a cleaner workspace.
26 - **Conversation List:** The left sidebar displays a list of active and past conversations for easy navigation.
27 - **Change Conversation Title:** By clicking the three dots next to a conversation title, you can either delete or change its title.
28 - **Backup and Restore Conversations:** Backup and restore all conversations and messages as a JSON file (accessible via the settings).
29
30 ### 5. **Speech Recognition and Synthesis**
31 - **Speech Input:** Use speech recognition to input prompts by speaking instead of typing.
32 - **Speech Output (Text-to-Speech):** The generated text can be read aloud using speech synthesis.
33 - **Custom Language Settings:** Configure the language used for speech recognition to match your preference.
34
35 ### 6. **File Uploads**
36 - **Image Uploads:** Upload images that will be appended to your message and sent to the AI provider.
37 - **Text File Uploads:** Upload text files, and their contents will be added to the message to provide more detailed input to the AI.
38
39 ### 7. **Web Access and Settings**
40 - **DuckDuckGo Web Access:** Enable web access through DuckDuckGo for privacy-focused browsing.
41 - **Theme Toggle:** Switch between **dark mode** and **light mode** in the settings.
42 - **Provider Visibility:** Hide unused providers in the settings using toggle buttons.
43 - **Log Access:** View application logs, including error messages and debug logs, through the settings.
44
45 ### 8. **Authentication**
46 - **Basic Authentication:** Set a password for Basic Authentication using the `--g4f-api-key` argument when starting the web server.
47
48 ## Installation
49
50 You can install the G4F GUI either as a full stack or in a lightweight version:
51
52 1. **Full Stack Installation** (includes all packages, including browser support and drivers):
53 ```bash
54 pip install -U g4f[all]
55 ```
56
57 2. **Slim Installation** (does not include browser drivers, suitable for headless environments):
58 ```bash
59 pip install -U g4f[slim]
60 ```
61
62 - **Full Stack Installation:** This installs all necessary dependencies, including browser support for web-based interactions.
63 - **Slim Installation:** This version is lighter, with no browser support, ideal for environments where browser interactions are not required.
64
65 ## Setup
66
67 ### Setting the Environment Variable
68
69 It is **recommended** to set a `G4F_API_KEY` environment variable for authentication. You can do this as follows:
70
71 On **Linux/macOS**:
72 ```bash
73 export G4F_API_KEY="your-api-key-here"
74 ```
75
76 On **Windows**:
77 ```bash
78 set G4F_API_KEY="your-api-key-here"
79 ```
80
81 ### Start the GUI and Backend
82
83 Run the following command to start both the GUI and backend services based on the G4F client:
84
85 ```bash
86 python -m g4f --debug --port 8080 --g4f-api-key $G4F_API_KEY
87 ```
88
89 This starts the GUI at `http://localhost:8080` with all necessary backend components running seamlessly.
90
91 ### Access the GUI
92
93 Once the server is running, open your browser and navigate to:
94
95 ```
96 http://localhost:8080/chat/
97 ```
98
99 ## Using the Interface
100
101 1. **Select and Manage Providers/Models:**
102 - Use the **select box** to view the list of available providers and models.
103 - Select a **provider/model combination** from the dropdown.
104 - Click the **pin button** to add the combination to your pinned list.
105 - To **unpin** a combination, click the corresponding button in the pinned list.
106
107 2. **Input a Prompt:**
108 - Enter your prompt manually or use **speech recognition** to dictate it.
109 - You can also upload **images** or **text files** to be included in the prompt.
110
111 3. **Generate Content:**
112 - Click the "Generate" button to produce the content.
113 - The AI will generate text, code, or images depending on the prompt.
114
115 4. **View and Interact with Results:**
116 - **For Text/Code:** The generated content will appear in the conversation window.
117 - **For Images:** Generated images will be shown as thumbnails. Click on them to view in full size.
118
119 5. **Backup and Restore Conversations:**
120 - Backup all your conversations as a **JSON file** and restore them at any time via the settings.
121
122 6. **Manage Conversations:**
123 - Delete or rename any conversation by clicking the three dots next to the conversation title.
124
125 ### Gallery Functionality
126
127 - **Image Thumbnails:** All generated images are shown as thumbnails within the conversation window.
128 - **Lightbox View:** Clicking a thumbnail opens the image in a larger view along with the associated prompt.
129 - **Automatic Image Download:** Enable automatic downloading of generated images in the settings.
130
131 ## Settings Configuration
132
133 1. **API Key:** Set your API key when starting the server by defining the `G4F_API_KEY` environment variable.
134 2. **Provider Visibility:** Hide unused providers through the settings.
135 3. **Theme:** Toggle between **dark mode** and **light mode**. Disabling dark mode switches to a white theme.
136 4. **DuckDuckGo Access:** Enable DuckDuckGo for privacy-focused web browsing.
137 5. **Speech Recognition Language:** Set your preferred language for speech recognition.
138 6. **Log Access:** View logs, including error and debug messages, from the settings menu.
139 7. **Automatic Image Download:** Enable this feature to automatically download generated images.
140
141 ## Known Issues
142
143 - **Gallery Loading:** Large images may take time to load depending on system performance.
144 - **Speech Recognition Accuracy:** Accuracy may vary depending on microphone quality or speech clarity.
145 - **Provider Downtime:** Some AI providers may experience downtime or disruptions.
146
147 [Return to Home](/)
Modified g4f/Provider/Copilot.py +19 -19
@@ -29,13 +29,9 @@ from .. import debug
29 29
30 30 class Conversation(BaseConversation):
31 31 conversation_id: str
32 cookie_jar: CookieJar
33 access_token: str
34 32
35 def __init__(self, conversation_id: str, cookie_jar: CookieJar, access_token: str = None):
33 def __init__(self, conversation_id: str):
36 34 self.conversation_id = conversation_id
37 self.cookie_jar = cookie_jar
38 self.access_token = access_token
39 35
40 36 class Copilot(AbstractProvider, ProviderModelMixin):
41 37 label = "Microsoft Copilot"
@@ -50,6 +46,9 @@ class Copilot(AbstractProvider, ProviderModelMixin):
50 46
51 47 websocket_url = "wss://copilot.microsoft.com/c/api/chat?api-version=2"
52 48 conversation_url = f"{url}/c/api/conversations"
49
50 _access_token: str = None
51 _cookies: CookieJar = None
53 52
54 53 @classmethod
55 54 def create_completion(
@@ -69,42 +68,43 @@ class Copilot(AbstractProvider, ProviderModelMixin):
69 68 raise MissingRequirementsError('Install or update "curl_cffi" package | pip install -U curl_cffi')
70 69
71 70 websocket_url = cls.websocket_url
72 access_token = None
73 71 headers = None
74 cookies = conversation.cookie_jar if conversation is not None else None
75 72 if cls.needs_auth or image is not None:
76 if conversation is None or conversation.access_token is None:
73 if cls._access_token is None:
77 74 try:
78 access_token, cookies = readHAR(cls.url)
75 cls._access_token, cls._cookies = readHAR(cls.url)
79 76 except NoValidHarFileError as h:
80 77 debug.log(f"Copilot: {h}")
81 78 try:
82 79 get_running_loop(check_nested=True)
83 access_token, cookies = asyncio.run(get_access_token_and_cookies(cls.url, proxy))
80 cls._access_token, cls._cookies = asyncio.run(get_access_token_and_cookies(cls.url, proxy))
84 81 except MissingRequirementsError:
85 82 raise h
86 else:
87 access_token = conversation.access_token
88 debug.log(f"Copilot: Access token: {access_token[:7]}...{access_token[-5:]}")
89 websocket_url = f"{websocket_url}&accessToken={quote(access_token)}"
90 headers = {"authorization": f"Bearer {access_token}"}
83 debug.log(f"Copilot: Access token: {cls._access_token[:7]}...{cls._access_token[-5:]}")
84 websocket_url = f"{websocket_url}&accessToken={quote(cls._access_token)}"
85 headers = {"authorization": f"Bearer {cls._access_token}"}
91 86
92 87 with Session(
93 88 timeout=timeout,
94 89 proxy=proxy,
95 90 impersonate="chrome",
96 91 headers=headers,
97 cookies=cookies,
92 cookies=cls._cookies,
98 93 ) as session:
94 if cls._access_token is not None:
95 cls._cookies = session.cookies.jar
99 96 response = session.get("https://copilot.microsoft.com/c/api/user")
100 97 raise_for_status(response)
101 debug.log(f"Copilot: User: {response.json().get('firstName', 'null')}")
98 user = response.json().get('firstName')
99 if user is None:
100 cls._access_token = None
101 debug.log(f"Copilot: User: {user or 'null'}")
102 102 if conversation is None:
103 103 response = session.post(cls.conversation_url)
104 104 raise_for_status(response)
105 105 conversation_id = response.json().get("id")
106 106 if return_conversation:
107 yield Conversation(conversation_id, session.cookies.jar, access_token)
107 yield Conversation(conversation_id)
108 108 prompt = format_prompt(messages)
109 109 debug.log(f"Copilot: Created conversation: {conversation_id}")
110 110 else:
@@ -162,7 +162,7 @@ class Copilot(AbstractProvider, ProviderModelMixin):
162 162 raise RuntimeError(f"Invalid response: {last_msg}")
163 163
164 164 async def get_access_token_and_cookies(url: str, proxy: str = None, target: str = "ChatAI",):
165 browser = await get_nodriver(proxy=proxy)
165 browser = await get_nodriver(proxy=proxy, user_data_dir="copilot")
166 166 page = await browser.get(url)
167 167 access_token = None
168 168 while access_token is None:
Modified g4f/Provider/needs_auth/BingCreateImages.py +2 -2
@@ -9,11 +9,11 @@ from ..bing.create_images import create_images, create_session
9 9
10 10 class BingCreateImages(AsyncGeneratorProvider, ProviderModelMixin):
11 11 label = "Microsoft Designer in Bing"
12 parent = "Bing"
13 12 url = "https://www.bing.com/images/create"
14 13 working = True
15 14 needs_auth = True
16 image_models = ["dall-e"]
15 image_models = ["dall-e-3"]
16 models = image_models
17 17
18 18 def __init__(self, cookies: Cookies = None, proxy: str = None, api_key: str = None) -> None:
19 19 if api_key is not None:
Modified g4f/Provider/needs_auth/Gemini.py +1 -1
@@ -69,7 +69,7 @@ class Gemini(AsyncGeneratorProvider):
69 69 if debug.logging:
70 70 print("Skip nodriver login in Gemini provider")
71 71 return
72 browser = await get_nodriver(proxy=proxy)
72 browser = await get_nodriver(proxy=proxy, user_data_dir="gemini")
73 73 login_url = os.environ.get("G4F_LOGIN_URL")
74 74 if login_url:
75 75 yield f"Please login: [Google Gemini]({login_url})\n\n"
Modified g4f/Provider/needs_auth/MicrosoftDesigner.py +1 -1
@@ -142,7 +142,7 @@ def readHAR(url: str) -> tuple[str, str]:
142 142 return api_key, user_agent
143 143
144 144 async def get_access_token_and_user_agent(url: str, proxy: str = None):
145 browser = await get_nodriver(proxy=proxy)
145 browser = await get_nodriver(proxy=proxy, user_data_dir="designer")
146 146 page = await browser.get(url)
147 147 user_agent = await page.evaluate("navigator.userAgent")
148 148 access_token = None
Modified g4f/Provider/needs_auth/OpenaiChat.py +1 -1
@@ -510,7 +510,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
510 510
511 511 @classmethod
512 512 async def nodriver_auth(cls, proxy: str = None):
513 browser = await get_nodriver(proxy=proxy)
513 browser = await get_nodriver(proxy=proxy, user_data_dir="chatgpt")
514 514 page = browser.main_tab
515 515 def on_request(event: nodriver.cdp.network.RequestWillBeSent):
516 516 if event.request.url == start_url or event.request.url.startswith(conversation_url):
Modified g4f/gui/client/index.html +35 -9
@@ -20,6 +20,7 @@
20 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" href="/static/css/dracula.min.css">
23 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe/dist/photoswipe.css">
23 24 <script>
24 25 MathJax = {
25 26 chtml: {
@@ -37,9 +38,34 @@
37 38 </script>
38 39 <script src="https://cdn.jsdelivr.net/npm/gpt-tokenizer/dist/cl100k_base.js" async></script>
39 40 <script src="/static/js/text_to_speech/index.js" async></script>
40 <!--
41 <script src="/static/js/whisper-web/index.js" async></script>
42 -->
41 <script type="module" async>
42 import PhotoSwipeLightbox from 'https://unpkg.com/photoswipe/dist/photoswipe-lightbox.esm.js';
43 const lightbox = new PhotoSwipeLightbox({
44 gallery: '#messages',
45 children: 'a:has(img)',
46 showHideAnimationType: 'none',
47 pswpModule: () => import('https://unpkg.com/photoswipe'),
48 });
49 lightbox.on('uiRegister', function() {
50 lightbox.pswp.ui.registerElement({
51 name: 'custom-caption',
52 order: 9,
53 isButton: false,
54 appendTo: 'root',
55 html: 'Caption text',
56 onInit: (el, pswp) => {
57 lightbox.pswp.on('change', () => {
58 const currSlideElement = lightbox.pswp.currSlide.data.element;
59 let captionHTML = '';
60 if (currSlideElement) {
61 el.innerHTML = currSlideElement.querySelector('img').getAttribute('alt');
62 }
63 });
64 }
65 });
66 });
67 lightbox.init();
68 </script>
43 69 <script>
44 70 const user_image = '<img src="/static/img/user.png" alt="your avatar">';
45 71 const gpt_image = '<img src="/static/img/gpt.png" alt="your avatar">';
@@ -261,16 +287,16 @@
261 287 <option value="">Provider: Auto</option>
262 288 <option value="OpenaiChat">OpenAI ChatGPT</option>
263 289 <option value="Copilot">Microsoft Copilot</option>
264 <option value="ChatGpt">ChatGpt</option>
265 <option value="Gemini">Gemini</option>
266 <option value="MetaAI">Meta AI</option>
267 <option value="DeepInfraChat">DeepInfraChat</option>
268 <option value="Blackbox">Blackbox</option>
290 <option value="Gemini">Google Gemini</option>
269 291 <option value="DDG">DuckDuckGo</option>
270 <option value="Pizzagpt">Pizzagpt</option>
271 292 <option disabled="disabled">----</option>
272 293 </select>
273 294 </div>
295 <div class="field">
296 <button id="pin">
297 <i class="fa-solid fa-thumbtack"></i>
298 </button>
299 </div>
274 300 </div>
275 301 </div>
276 302 <div class="log hidden"></div>
Modified g4f/gui/client/static/css/style.css +73 -7
@@ -63,6 +63,7 @@
63 63 --conversations-hover: #c7a2ff4d;
64 64 --scrollbar: var(--colour-3);
65 65 --scrollbar-thumb: var(--blur-bg);
66 --button-hover: var(--colour-5);
66 67 }
67 68
68 69 :root {
@@ -533,7 +534,7 @@ body.white .gradient{
533 534
534 535 .stop_generating, .toolbar .regenerate {
535 536 position: absolute;
536 z-index: 1000000;
537 z-index: 100000;
537 538 top: 0;
538 539 right: 0;
539 540 }
@@ -729,13 +730,8 @@ label[for="camera"] {
729 730
730 731
731 732 select {
732 -webkit-border-radius: 8px;
733 -moz-border-radius: 8px;
734 733 border-radius: 8px;
735
736 -webkit-backdrop-filter: blur(20px);
737 734 backdrop-filter: blur(20px);
738
739 735 cursor: pointer;
740 736 background-color: var(--colour-1);
741 737 border: 1px solid var(--blur-border);
@@ -745,11 +741,47 @@ select {
745 741 overflow: hidden;
746 742 outline: none;
747 743 padding: 8px 16px;
748
749 744 appearance: none;
750 745 width: 160px;
751 746 }
752 747
748 .buttons button {
749 border-radius: 8px;
750 backdrop-filter: blur(20px);
751 cursor: pointer;
752 background-color: var(--colour-1);
753 border: 1px solid var(--blur-border);
754 color: var(--colour-3);
755 padding: 8px;
756 }
757
758 .buttons button.pinned span {
759 max-width: 160px;
760 overflow: hidden;
761 text-wrap: nowrap;
762 margin-right: 16px;
763 display: block;
764 text-overflow: ellipsis;
765 }
766
767 .buttons button.pinned i {
768 position: absolute;
769 top: 10px;
770 right: 6px;
771 }
772
773 select:hover,
774 .buttons button:hover,
775 .stop_generating button:hover,
776 .toolbar .regenerate button:hover,
777 #send-button:hover {
778 background-color: var(--button-hover);
779 }
780
781 #provider option:disabled[value] {
782 display: none;
783 }
784
753 785 #systemPrompt, .settings textarea {
754 786 font-size: 15px;
755 787 width: 100%;
@@ -761,6 +793,39 @@ select {
761 793 resize: vertical;
762 794 }
763 795
796 .pswp {
797 --pswp-placeholder-bg: #000 !important;
798 }
799 .pswp img {
800 object-fit: contain;
801 }
802 .pswp__img--placeholder--blank{
803 display: none !important;
804 }
805 .pswp__custom-caption {
806 opacity: 0 !important;
807 background: rgba(0, 0, 0, 0.3);
808 font-size: 16px;
809 color: #fff;
810 width: calc(100% - 32px);
811 max-width: 400px;
812 padding: 2px 8px;
813 border-radius: 4px;
814 position: absolute;
815 left: 50%;
816 bottom: 16px;
817 transform: translateX(-50%);
818 max-height: 100px;
819 overflow: auto;
820 }
821 .pswp__custom-caption:hover {
822 opacity: 1 !important;
823 }
824 .pswp__custom-caption a {
825 color: #fff;
826 text-decoration: underline;
827 }
828
764 829 .slide-systemPrompt {
765 830 position: absolute;
766 831 top: 0;
@@ -1112,6 +1177,7 @@ ul {
1112 1177 --colour-3: #212529;
1113 1178 --scrollbar: var(--colour-1);
1114 1179 --scrollbar-thumb: var(--gradient);
1180 --button-hover: var(--colour-4);
1115 1181 }
1116 1182
1117 1183 .white .message .assistant .fa-xmark {
Modified g4f/gui/client/static/js/chat.v1.js +127 -29
@@ -3,7 +3,7 @@ const message_box = document.getElementById(`messages`);
3 3 const messageInput = document.getElementById(`message-input`);
4 4 const box_conversations = document.querySelector(`.top`);
5 5 const stop_generating = document.querySelector(`.stop_generating`);
6 const regenerate = document.querySelector(`.regenerate`);
6 const regenerate_button = document.querySelector(`.regenerate`);
7 7 const sidebar = document.querySelector(".conversations");
8 8 const sidebar_button = document.querySelector(".mobile-sidebar");
9 9 const sendButton = document.getElementById("send-button");
@@ -21,7 +21,7 @@ const chat = document.querySelector(".conversation");
21 21 const album = document.querySelector(".images");
22 22 const log_storage = document.querySelector(".log");
23 23
24 const optionElements = document.querySelectorAll(".settings input, .settings textarea, #model, #model2, #provider")
24 const optionElementsSelector = ".settings input, .settings textarea, #model, #model2, #provider";
25 25
26 26 let provider_storage = {};
27 27 let message_storage = {};
@@ -364,7 +364,7 @@ const handle_ask = async () => {
364 364 }
365 365 </div>
366 366 <div class="count">
367 ${count_words_and_tokens(message, get_selected_model())}
367 ${count_words_and_tokens(message, get_selected_model()?.value)}
368 368 <i class="fa-solid fa-volume-high"></i>
369 369 <i class="fa-regular fa-clipboard"></i>
370 370 <a><i class="fa-brands fa-whatsapp"></i></a>
@@ -375,7 +375,19 @@ const handle_ask = async () => {
375 375 </div>
376 376 `;
377 377 highlight(message_box);
378 await ask_gpt(message_id);
378
379 const all_pinned = document.querySelectorAll(".buttons button.pinned")
380 if (all_pinned.length > 0) {
381 all_pinned.forEach((el, idx) => ask_gpt(
382 idx == 0 ? message_id : get_message_id(),
383 -1,
384 idx != 0,
385 el.dataset.provider,
386 el.dataset.model
387 ));
388 } else {
389 await ask_gpt(message_id);
390 }
379 391 };
380 392
381 393 async function safe_remove_cancel_button() {
@@ -387,16 +399,21 @@ async function safe_remove_cancel_button() {
387 399 stop_generating.classList.add("stop_generating-hidden");
388 400 }
389 401
390 regenerate.addEventListener("click", async () => {
391 regenerate.classList.add("regenerate-hidden");
392 setTimeout(()=>regenerate.classList.remove("regenerate-hidden"), 3000);
393 await hide_message(window.conversation_id);
394 await ask_gpt(get_message_id());
402 regenerate_button.addEventListener("click", async () => {
403 regenerate_button.classList.add("regenerate-hidden");
404 setTimeout(()=>regenerate_button.classList.remove("regenerate-hidden"), 3000);
405 const all_pinned = document.querySelectorAll(".buttons button.pinned")
406 if (all_pinned.length > 0) {
407 all_pinned.forEach((el) => ask_gpt(get_message_id(), -1, true, el.dataset.provider, el.dataset.model));
408 } else {
409 await hide_message(window.conversation_id);
410 await ask_gpt(get_message_id());
411 }
395 412 });
396 413
397 414 stop_generating.addEventListener("click", async () => {
398 415 stop_generating.classList.add("stop_generating-hidden");
399 regenerate.classList.remove("regenerate-hidden");
416 regenerate_button.classList.remove("regenerate-hidden");
400 417 let key;
401 418 for (key in controller_storage) {
402 419 if (!controller_storage[key].signal.aborted) {
@@ -538,7 +555,11 @@ imageInput?.addEventListener("click", (e) => {
538 555 }
539 556 });
540 557
541 const ask_gpt = async (message_id, message_index = -1) => {
558 const ask_gpt = async (message_id, message_index = -1, regenerate = false, provider = null, model = null) => {
559 if (!model && !provider) {
560 model = get_selected_model()?.value || null;
561 provider = providerSelect.options[providerSelect.selectedIndex].value;
562 }
542 563 let messages = await get_messages(window.conversation_id);
543 564 messages = prepare_messages(messages, message_index);
544 565 message_storage[message_id] = "";
@@ -553,7 +574,7 @@ const ask_gpt = async (message_id, message_index = -1) => {
553 574
554 575 const message_el = document.createElement("div");
555 576 message_el.classList.add("message");
556 if (message_index != -1) {
577 if (message_index != -1 || regenerate) {
557 578 message_el.classList.add("regenerate");
558 579 }
559 580 message_el.innerHTML += `
@@ -593,14 +614,13 @@ const ask_gpt = async (message_id, message_index = -1) => {
593 614 try {
594 615 const input = imageInput && imageInput.files.length > 0 ? imageInput : cameraInput;
595 616 const file = input && input.files.length > 0 ? input.files[0] : null;
596 const provider = providerSelect.options[providerSelect.selectedIndex].value;
597 617 const auto_continue = document.getElementById("auto_continue")?.checked;
598 618 const download_images = document.getElementById("download_images")?.checked;
599 619 let api_key = get_api_key_by_provider(provider);
600 620 await api("conversation", {
601 621 id: message_id,
602 622 conversation_id: window.conversation_id,
603 model: get_selected_model(),
623 model: model,
604 624 web_search: document.getElementById("switch").checked,
605 625 provider: provider,
606 626 messages: messages,
@@ -632,7 +652,8 @@ const ask_gpt = async (message_id, message_index = -1) => {
632 652 message_storage[message_id],
633 653 message_provider,
634 654 message_index,
635 synthesize_storage[message_id]
655 synthesize_storage[message_id],
656 regenerate
636 657 );
637 658 await safe_load_conversation(window.conversation_id, message_index == -1);
638 659 } else {
@@ -645,7 +666,7 @@ const ask_gpt = async (message_id, message_index = -1) => {
645 666 await safe_remove_cancel_button();
646 667 await register_message_buttons();
647 668 await load_conversations();
648 regenerate.classList.remove("regenerate-hidden");
669 regenerate_button.classList.remove("regenerate-hidden");
649 670 };
650 671
651 672 async function scroll_to_bottom() {
@@ -848,7 +869,7 @@ const load_conversation = async (conversation_id, scroll=true) => {
848 869 message_box.innerHTML = elements;
849 870 register_message_buttons();
850 871 highlight(message_box);
851 regenerate.classList.remove("regenerate-hidden");
872 regenerate_button.classList.remove("regenerate-hidden");
852 873
853 874 if (scroll) {
854 875 message_box.scrollTo({ top: message_box.scrollHeight, behavior: "smooth" });
@@ -960,7 +981,8 @@ const add_message = async (
960 981 conversation_id, role, content,
961 982 provider = null,
962 983 message_index = -1,
963 synthesize_data = null
984 synthesize_data = null,
985 regenerate = false
964 986 ) => {
965 987 const conversation = await get_conversation(conversation_id);
966 988 if (!conversation) return;
@@ -972,6 +994,9 @@ const add_message = async (
972 994 if (synthesize_data) {
973 995 new_message.synthesize = synthesize_data;
974 996 }
997 if (regenerate) {
998 new_message.regenerate = true;
999 }
975 1000 if (message_index == -1) {
976 1001 conversation.items.push(new_message);
977 1002 } else {
@@ -1118,6 +1143,7 @@ function open_album() {
1118 1143 }
1119 1144
1120 1145 const register_settings_storage = async () => {
1146 const optionElements = document.querySelectorAll(optionElementsSelector);
1121 1147 optionElements.forEach((element) => {
1122 1148 if (element.type == "textarea") {
1123 1149 element.addEventListener('input', async (event) => {
@@ -1145,6 +1171,7 @@ const register_settings_storage = async () => {
1145 1171 }
1146 1172
1147 1173 const load_settings_storage = async () => {
1174 const optionElements = document.querySelectorAll(optionElementsSelector);
1148 1175 optionElements.forEach((element) => {
1149 1176 if (!(value = appStorage.getItem(element.id))) {
1150 1177 return;
@@ -1226,7 +1253,7 @@ const count_input = async () => {
1226 1253 if (timeoutId) clearTimeout(timeoutId);
1227 1254 timeoutId = setTimeout(() => {
1228 1255 if (countFocus.value) {
1229 inputCount.innerText = count_words_and_tokens(countFocus.value, get_selected_model());
1256 inputCount.innerText = count_words_and_tokens(countFocus.value, get_selected_model()?.value);
1230 1257 } else {
1231 1258 inputCount.innerText = "";
1232 1259 }
@@ -1267,6 +1294,24 @@ async function on_load() {
1267 1294 load_conversations();
1268 1295 }
1269 1296
1297 const load_provider_option = (input, provider_name) => {
1298 if (input.checked) {
1299 providerSelect.querySelectorAll(`option[value="${provider_name}"]`).forEach(
1300 (el) => el.removeAttribute("disabled")
1301 );
1302 providerSelect.querySelectorAll(`option[data-parent="${provider_name}"]`).forEach(
1303 (el) => el.removeAttribute("disabled")
1304 );
1305 } else {
1306 providerSelect.querySelectorAll(`option[value="${provider_name}"]`).forEach(
1307 (el) => el.setAttribute("disabled", "disabled")
1308 );
1309 providerSelect.querySelectorAll(`option[data-parent="${provider_name}"]`).forEach(
1310 (el) => el.setAttribute("disabled", "disabled")
1311 );
1312 }
1313 };
1314
1270 1315 async function on_api() {
1271 1316 let prompt_lock = false;
1272 1317 messageInput.addEventListener("keydown", async (evt) => {
@@ -1292,22 +1337,42 @@ async function on_api() {
1292 1337 await handle_ask();
1293 1338 });
1294 1339 messageInput.focus();
1295
1296 register_settings_storage();
1297
1340 let provider_options = [];
1298 1341 try {
1299 1342 models = await api("models");
1300 1343 models.forEach((model) => {
1301 1344 let option = document.createElement("option");
1302 option.value = option.text = model;
1345 option.value = option.text = option.dataset.label = model;
1303 1346 modelSelect.appendChild(option);
1304 1347 });
1305 1348 providers = await api("providers")
1306 Object.entries(providers).forEach(([provider, label]) => {
1349 providers.sort((a, b) => a.label.localeCompare(b.label));
1350 providers.forEach((provider) => {
1307 1351 let option = document.createElement("option");
1308 option.value = provider;
1309 option.text = label;
1352 option.value = provider.name;
1353 option.dataset.label = provider.label;
1354 option.text = provider.label
1355 + (provider.vision ? " (Image Upload)" : "")
1356 + (provider.image ? " (Image Generation)" : "")
1357 + (provider.webdriver ? " (Webdriver)" : "")
1358 + (provider.auth ? " (Auth)" : "");
1359 if (provider.parent)
1360 option.dataset.parent = provider.parent;
1310 1361 providerSelect.appendChild(option);
1362
1363 if (!provider.parent) {
1364 option = document.createElement("div");
1365 option.classList.add("field");
1366 option.innerHTML = `
1367 <div class="field">
1368 <span class="label">Enable ${provider.label}</span>
1369 <input id="Provider${provider.name}" type="checkbox" name="Provider${provider.name}" checked="">
1370 <label for="Provider${provider.name}" class="toogle" title="Remove provider from dropdown"></label>
1371 </div>`;
1372 option.querySelector("input").addEventListener("change", (event) => load_provider_option(event.target, provider.name));
1373 settings.querySelector(".paper").appendChild(option);
1374 provider_options[provider.name] = option;
1375 }
1311 1376 });
1312 1377 await load_provider_models(appStorage.getItem("provider"));
1313 1378 } catch (e) {
@@ -1316,8 +1381,11 @@ async function on_api() {
1316 1381 document.location.href = `/chat/error`;
1317 1382 }
1318 1383 }
1319
1384 register_settings_storage();
1320 1385 await load_settings_storage()
1386 Object.entries(provider_options).forEach(
1387 ([provider_name, option]) => load_provider_option(option.querySelector("input"), provider_name)
1388 );
1321 1389
1322 1390 const hide_systemPrompt = document.getElementById("hide-systemPrompt")
1323 1391 const slide_systemPrompt_icon = document.querySelector(".slide-systemPrompt i");
@@ -1455,9 +1523,12 @@ systemPrompt?.addEventListener("input", async () => {
1455 1523
1456 1524 function get_selected_model() {
1457 1525 if (modelProvider.selectedIndex >= 0) {
1458 return modelProvider.options[modelProvider.selectedIndex].value;
1526 return modelProvider.options[modelProvider.selectedIndex];
1459 1527 } else if (modelSelect.selectedIndex >= 0) {
1460 return modelSelect.options[modelSelect.selectedIndex].value;
1528 model = modelSelect.options[modelSelect.selectedIndex];
1529 if (model.value) {
1530 return model;
1531 }
1461 1532 }
1462 1533 }
1463 1534
@@ -1554,6 +1625,7 @@ async function load_provider_models(providerIndex=null) {
1554 1625 models.forEach((model) => {
1555 1626 let option = document.createElement('option');
1556 1627 option.value = model.model;
1628 option.dataset.label = model.model;
1557 1629 option.text = `${model.model}${model.image ? " (Image Generation)" : ""}${model.vision ? " (Image Upload)" : ""}`;
1558 1630 option.selected = model.default;
1559 1631 modelProvider.appendChild(option);
@@ -1564,6 +1636,32 @@ async function load_provider_models(providerIndex=null) {
1564 1636 }
1565 1637 };
1566 1638 providerSelect.addEventListener("change", () => load_provider_models());
1639 document.getElementById("pin").addEventListener("click", async () => {
1640 const pin_container = document.getElementById("pin").parentElement;
1641 let selected_provider = providerSelect.options[providerSelect.selectedIndex];
1642 selected_provider = selected_provider.value ? selected_provider : null;
1643 const selected_model = get_selected_model();
1644 if (selected_provider || selected_model) {
1645 const pinned = document.createElement("button");
1646 pinned.classList.add("pinned");
1647 if (selected_provider) pinned.dataset.provider = selected_provider.value;
1648 if (selected_model) pinned.dataset.model = selected_model.value;
1649 pinned.innerHTML = `
1650 <span>
1651 ${selected_provider ? selected_provider.dataset.label || selected_provider.text : ""}
1652 ${selected_provider && selected_model ? "/" : ""}
1653 ${selected_model ? selected_model.dataset.label || selected_model.text : ""}
1654 </span>
1655 <i class="fa-regular fa-circle-xmark"></i>`;
1656 pinned.addEventListener("click", () => pin_container.removeChild(pinned));
1657 let all_pinned = pin_container.querySelectorAll(".pinned");
1658 while (all_pinned.length > 4) {
1659 pin_container.removeChild(all_pinned[0])
1660 all_pinned = pin_container.querySelectorAll(".pinned");
1661 }
1662 pin_container.appendChild(pinned);
1663 }
1664 });
1567 1665
1568 1666 function save_storage() {
1569 1667 let filename = `chat ${new Date().toLocaleString()}.json`.replaceAll(":", "-");
Modified g4f/gui/server/api.py +46 -44
@@ -8,11 +8,12 @@ from flask import send_from_directory
8 8 from inspect import signature
9 9
10 10 from g4f import version, models
11 from g4f import get_last_provider, ChatCompletion
11 from g4f import get_last_provider, ChatCompletion, get_model_and_provider
12 12 from g4f.errors import VersionNotFoundError
13 13 from g4f.image import ImagePreview, ImageResponse, copy_images, ensure_images_dir, images_dir
14 14 from g4f.Provider import ProviderType, __providers__, __map__
15 15 from g4f.providers.base_provider import ProviderModelMixin
16 from g4f.providers.retry_provider import BaseRetryProvider
16 17 from g4f.providers.response import BaseConversation, FinishReason, SynthesizeData
17 18 from g4f.client.service import convert_to_provider
18 19 from g4f import debug
@@ -47,15 +48,15 @@ class Api:
47 48
48 49 @staticmethod
49 50 def get_providers() -> dict[str, str]:
50 return {
51 provider.__name__: (provider.label if hasattr(provider, "label") else provider.__name__)
52 + (" (Image Generation)" if getattr(provider, "image_models", None) else "")
53 + (" (Image Upload)" if getattr(provider, "default_vision_model", None) else "")
54 + (" (WebDriver)" if "webdriver" in provider.get_parameters() else "")
55 + (" (Auth)" if provider.needs_auth else "")
56 for provider in __providers__
57 if provider.working
58 }
51 return [{
52 "name": provider.__name__,
53 "label": provider.label if hasattr(provider, "label") else provider.__name__,
54 "parent": getattr(provider, "parent", None),
55 "image": getattr(provider, "image_models", None) is not None,
56 "vision": getattr(provider, "default_vision_model", None) is not None,
57 "webdriver": "webdriver" in provider.get_parameters(),
58 "auth": provider.needs_auth,
59 } for provider in __providers__ if provider.working]
59 60
60 61 @staticmethod
61 62 def get_version() -> dict:
@@ -115,43 +116,44 @@ class Api:
115 116 debug.log_handler = log_handler
116 117 proxy = os.environ.get("G4F_PROXY")
117 118 try:
118 result = ChatCompletion.create(**kwargs)
119 model, provider = get_model_and_provider(
120 kwargs.get("model"), kwargs.get("provider"),
121 stream=True,
122 ignore_stream=True
123 )
124 result = ChatCompletion.create(**{**kwargs, "model": model, "provider": provider})
119 125 first = True
120 if isinstance(result, ImageResponse):
126 for chunk in result:
121 127 if first:
122 128 first = False
123 yield self._format_json("provider", get_last_provider(True))
124 yield self._format_json("content", str(result))
125 else:
126 for chunk in result:
127 if first:
128 first = False
129 yield self._format_json("provider", get_last_provider(True))
130 if isinstance(chunk, BaseConversation):
131 if provider:
132 if provider not in conversations:
133 conversations[provider] = {}
134 conversations[provider][conversation_id] = chunk
135 yield self._format_json("conversation", conversation_id)
136 elif isinstance(chunk, Exception):
137 logger.exception(chunk)
138 yield self._format_json("message", get_error_message(chunk))
139 elif isinstance(chunk, ImagePreview):
140 yield self._format_json("preview", chunk.to_string())
141 elif isinstance(chunk, ImageResponse):
142 images = chunk
143 if download_images:
144 images = asyncio.run(copy_images(chunk.get_list(), chunk.get("cookies"), proxy))
145 images = ImageResponse(images, chunk.alt)
146 yield self._format_json("content", str(images))
147 elif isinstance(chunk, SynthesizeData):
148 yield self._format_json("synthesize", chunk.to_json())
149 elif not isinstance(chunk, FinishReason):
150 yield self._format_json("content", str(chunk))
151 if debug.logs:
152 for log in debug.logs:
153 yield self._format_json("log", str(log))
154 debug.logs = []
129 if isinstance(provider, BaseRetryProvider):
130 provider = provider.last_provider
131 yield self._format_json("provider", {**provider.get_dict(), "model": model})
132 if isinstance(chunk, BaseConversation):
133 if provider:
134 if provider not in conversations:
135 conversations[provider] = {}
136 conversations[provider][conversation_id] = chunk
137 yield self._format_json("conversation", conversation_id)
138 elif isinstance(chunk, Exception):
139 logger.exception(chunk)
140 yield self._format_json("message", get_error_message(chunk))
141 elif isinstance(chunk, ImagePreview):
142 yield self._format_json("preview", chunk.to_string())
143 elif isinstance(chunk, ImageResponse):
144 images = chunk
145 if download_images:
146 images = asyncio.run(copy_images(chunk.get_list(), chunk.get("cookies"), proxy))
147 images = ImageResponse(images, chunk.alt)
148 yield self._format_json("content", str(images))
149 elif isinstance(chunk, SynthesizeData):
150 yield self._format_json("synthesize", chunk.to_json())
151 elif not isinstance(chunk, FinishReason):
152 yield self._format_json("content", str(chunk))
153 if debug.logs:
154 for log in debug.logs:
155 yield self._format_json("log", str(log))
156 debug.logs = []
155 157 except Exception as e:
156 158 logger.exception(e)
157 159 yield self._format_json('error', get_error_message(e))
Modified g4f/gui/server/backend.py +7 -1
@@ -55,6 +55,12 @@ class Backend_Api(Api):
55 55 return jsonify(response)
56 56 return response
57 57
58 def jsonify_providers(**kwargs):
59 response = self.get_providers(**kwargs)
60 if isinstance(response, list):
61 return jsonify(response)
62 return response
63
58 64 self.routes = {
59 65 '/backend-api/v2/models': {
60 66 'function': jsonify_models,
@@ -65,7 +71,7 @@ class Backend_Api(Api):
65 71 'methods': ['GET']
66 72 },
67 73 '/backend-api/v2/providers': {
68 'function': self.get_providers,
74 'function': jsonify_providers,
69 75 'methods': ['GET']
70 76 },
71 77 '/backend-api/v2/version': {
Modified g4f/image.py +3 -0
Modified g4f/providers/types.py +1 -1
Modified g4f/requests/__init__.py +2 -2