diff options
Diffstat (limited to '')
-rw-r--r-- | g4f/gui/client/index.html | 4 | ||||
-rw-r--r-- | g4f/gui/client/static/css/style.css | 5 | ||||
-rw-r--r-- | g4f/gui/client/static/js/chat.v1.js | 36 | ||||
-rw-r--r-- | g4f/gui/client/static/js/highlightjs-copy.min.js | 55 | ||||
-rw-r--r-- | g4f/gui/server/api.py | 127 | ||||
-rw-r--r-- | g4f/gui/server/internet.py | 2 |
6 files changed, 154 insertions, 75 deletions
diff --git a/g4f/gui/client/index.html b/g4f/gui/client/index.html index a2f883d9..1a660062 100644 --- a/g4f/gui/client/index.html +++ b/g4f/gui/client/index.html @@ -229,8 +229,8 @@ <option value="">Model: Default</option> <option value="gpt-4">gpt-4</option> <option value="gpt-3.5-turbo">gpt-3.5-turbo</option> - <option value="llama2-70b">llama2-70b</option> - <option value="llama3-70b-instruct">llama3-70b-instruct</option> + <option value="llama-3-70b-chat">llama-3-70b-chat</option> + <option value="llama-3.1-70b">llama-3.1-70b</option> <option value="gemini-pro">gemini-pro</option> <option value="">----</option> </select> diff --git a/g4f/gui/client/static/css/style.css b/g4f/gui/client/static/css/style.css index f3a4708d..441e2042 100644 --- a/g4f/gui/client/static/css/style.css +++ b/g4f/gui/client/static/css/style.css @@ -87,12 +87,9 @@ body { } body { - padding: 10px; background: var(--colour-1); color: var(--colour-3); height: 100vh; - max-width: 1600px; - margin: auto; } .row { @@ -1146,4 +1143,4 @@ a:-webkit-any-link { .message.regenerate { opacity: 1; } -}
\ No newline at end of file +} diff --git a/g4f/gui/client/static/js/chat.v1.js b/g4f/gui/client/static/js/chat.v1.js index 9790b261..d5a1ffaa 100644 --- a/g4f/gui/client/static/js/chat.v1.js +++ b/g4f/gui/client/static/js/chat.v1.js @@ -57,6 +57,25 @@ function filter_message(text) { ) } +function fallback_clipboard (text) { + var textBox = document.createElement("textarea"); + textBox.value = text; + textBox.style.top = "0"; + textBox.style.left = "0"; + textBox.style.position = "fixed"; + document.body.appendChild(textBox); + textBox.focus(); + textBox.select(); + try { + var success = document.execCommand('copy'); + var msg = success ? 'succeeded' : 'failed'; + console.log('Clipboard Fallback: Copying text command ' + msg); + } catch (e) { + console.error('Clipboard Fallback: Unable to copy', e); + } + document.body.removeChild(textBox); +} + hljs.addPlugin(new CopyButtonPlugin()); let typesetPromise = Promise.resolve(); const highlight = (container) => { @@ -88,18 +107,31 @@ const register_message_buttons = async () => { }) } }); + document.querySelectorAll(".message .fa-clipboard").forEach(async (el) => { if (!("click" in el.dataset)) { el.dataset.click = "true"; el.addEventListener("click", async () => { const message_el = el.parentElement.parentElement.parentElement; const copyText = await get_message(window.conversation_id, message_el.dataset.index); - navigator.clipboard.writeText(copyText); + + try { + if (!navigator.clipboard) { + throw new Error("navigator.clipboard: Clipboard API unavailable."); + } + await navigator.clipboard.writeText(copyText); + } catch (e) { + console.error(e); + console.error("Clipboard API writeText() failed! Fallback to document.exec(\"copy\")..."); + fallback_clipboard(copyText); + } + el.classList.add("clicked"); setTimeout(() => el.classList.remove("clicked"), 1000); }) } }); + document.querySelectorAll(".message .fa-volume-high").forEach(async (el) => { if (!("click" in el.dataset)) { el.dataset.click = "true"; @@ -1424,4 +1456,4 @@ if (SpeechRecognition) { recognition.start(); } }); -}
\ No newline at end of file +} diff --git a/g4f/gui/client/static/js/highlightjs-copy.min.js b/g4f/gui/client/static/js/highlightjs-copy.min.js index ac11d33e..cd8ae957 100644 --- a/g4f/gui/client/static/js/highlightjs-copy.min.js +++ b/g4f/gui/client/static/js/highlightjs-copy.min.js @@ -1 +1,54 @@ -class CopyButtonPlugin{constructor(options={}){self.hook=options.hook;self.callback=options.callback}"after:highlightElement"({el,text}){let button=Object.assign(document.createElement("button"),{innerHTML:"Copy",className:"hljs-copy-button"});button.dataset.copied=false;el.parentElement.classList.add("hljs-copy-wrapper");el.parentElement.appendChild(button);el.parentElement.style.setProperty("--hljs-theme-background",window.getComputedStyle(el).backgroundColor);button.onclick=function(){if(!navigator.clipboard)return;let newText=text;if(hook&&typeof hook==="function"){newText=hook(text,el)||text}navigator.clipboard.writeText(newText).then(function(){button.innerHTML="Copied!";button.dataset.copied=true;let alert=Object.assign(document.createElement("div"),{role:"status",className:"hljs-copy-alert",innerHTML:"Copied to clipboard"});el.parentElement.appendChild(alert);setTimeout(()=>{button.innerHTML="Copy";button.dataset.copied=false;el.parentElement.removeChild(alert);alert=null},2e3)}).then(function(){if(typeof callback==="function")return callback(newText,el)})}}}
\ No newline at end of file +class CopyButtonPlugin { + constructor(options = {}) { + self.hook = options.hook; + self.callback = options.callback + } + "after:highlightElement"({ + el, + text + }) { + let button = Object.assign(document.createElement("button"), { + innerHTML: "Copy", + className: "hljs-copy-button" + }); + button.dataset.copied = false; + el.parentElement.classList.add("hljs-copy-wrapper"); + el.parentElement.appendChild(button); + el.parentElement.style.setProperty("--hljs-theme-background", window.getComputedStyle(el).backgroundColor); + button.onclick = async () => { + let newText = text; + if (hook && typeof hook === "function") { + newText = hook(text, el) || text + } + try { + if (!navigator.clipboard) { + throw new Error("navigator.clipboard: Clipboard API unavailable."); + } + await navigator.clipboard.writeText(newText); + } catch (e) { + console.error(e); + console.error("Clipboard API writeText() failed! Fallback to document.exec(\"copy\")..."); + fallback_clipboard(newText); + } + button.innerHTML = "Copied!"; + button.dataset.copied = true; + let alert = Object.assign(document.createElement("div"), { + role: "status", + className: "hljs-copy-alert", + innerHTML: "Copied to clipboard" + }); + el.parentElement.appendChild(alert); + setTimeout(() => { + button.innerHTML = "Copy"; + button.dataset.copied = false; + el.parentElement.removeChild(alert); + alert = null + }, 2e3) + } + + + if (typeof callback === "function") return callback(newText, el); + + } + +} diff --git a/g4f/gui/server/api.py b/g4f/gui/server/api.py index 3da0fe17..57f3eaa1 100644 --- a/g4f/gui/server/api.py +++ b/g4f/gui/server/api.py @@ -23,8 +23,8 @@ from g4f.providers.conversation import BaseConversation conversations: dict[dict[str, BaseConversation]] = {} images_dir = "./generated_images" -class Api(): +class Api: @staticmethod def get_models() -> list[str]: """ @@ -42,14 +42,11 @@ class Api(): if provider in __map__: provider: ProviderType = __map__[provider] if issubclass(provider, ProviderModelMixin): - return [{"model": model, "default": model == provider.default_model} for model in provider.get_models()] - elif provider.supports_gpt_35_turbo or provider.supports_gpt_4: return [ - *([{"model": "gpt-4", "default": not provider.supports_gpt_4}] if provider.supports_gpt_4 else []), - *([{"model": "gpt-3.5-turbo", "default": not provider.supports_gpt_4}] if provider.supports_gpt_35_turbo else []) + {"model": model, "default": model == provider.default_model} + for model in provider.get_models() ] - else: - return []; + return [] @staticmethod def get_image_models() -> list[dict]: @@ -71,7 +68,7 @@ class Api(): "image_model": model, "vision_model": parent.default_vision_model if hasattr(parent, "default_vision_model") else None }) - index.append(parent.__name__) + index.append(parent.__name__) elif hasattr(provider, "default_vision_model") and provider.__name__ not in index: image_models.append({ "provider": provider.__name__, @@ -89,15 +86,13 @@ class Api(): Return a list of all working providers. """ return { - provider.__name__: (provider.label - if hasattr(provider, "label") - else provider.__name__) + - (" (WebDriver)" - if "webdriver" in provider.get_parameters() - else "") + - (" (Auth)" - if provider.needs_auth - else "") + provider.__name__: ( + provider.label if hasattr(provider, "label") else provider.__name__ + ) + ( + " (WebDriver)" if "webdriver" in provider.get_parameters() else "" + ) + ( + " (Auth)" if provider.needs_auth else "" + ) for provider in __providers__ if provider.working } @@ -131,7 +126,7 @@ class Api(): Returns: dict: Arguments prepared for chat completion. - """ + """ model = json_data.get('model') or models.default provider = json_data.get('provider') messages = json_data['messages'] @@ -160,61 +155,62 @@ class Api(): } def _create_response_stream(self, kwargs: dict, conversation_id: str, provider: str) -> Iterator: - """ - Creates and returns a streaming response for the conversation. - - Args: - kwargs (dict): Arguments for creating the chat completion. - - Yields: - str: JSON formatted response chunks for the stream. - - Raises: - Exception: If an error occurs during the streaming process. - """ try: + result = ChatCompletion.create(**kwargs) first = True - for chunk in ChatCompletion.create(**kwargs): + if isinstance(result, ImageResponse): + # Якщо результат є ImageResponse, обробляємо його як одиночний елемент if first: first = False yield self._format_json("provider", get_last_provider(True)) - if isinstance(chunk, BaseConversation): - if provider not in conversations: - conversations[provider] = {} - conversations[provider][conversation_id] = chunk - yield self._format_json("conversation", conversation_id) - elif isinstance(chunk, Exception): - logging.exception(chunk) - yield self._format_json("message", get_error_message(chunk)) - elif isinstance(chunk, ImagePreview): - yield self._format_json("preview", chunk.to_string()) - elif isinstance(chunk, ImageResponse): - async def copy_images(images: list[str], cookies: Optional[Cookies] = None): - async with ClientSession( - connector=get_connector(None, os.environ.get("G4F_PROXY")), - cookies=cookies - ) as session: - async def copy_image(image): - async with session.get(image) as response: - target = os.path.join(images_dir, f"{int(time.time())}_{str(uuid.uuid4())}") - with open(target, "wb") as f: - async for chunk in response.content.iter_any(): - f.write(chunk) - with open(target, "rb") as f: - extension = is_accepted_format(f.read(12)).split("/")[-1] - extension = "jpg" if extension == "jpeg" else extension - new_target = f"{target}.{extension}" - os.rename(target, new_target) - return f"/images/{os.path.basename(new_target)}" - return await asyncio.gather(*[copy_image(image) for image in images]) - images = asyncio.run(copy_images(chunk.get_list(), chunk.options.get("cookies"))) - yield self._format_json("content", str(ImageResponse(images, chunk.alt))) - elif not isinstance(chunk, FinishReason): - yield self._format_json("content", str(chunk)) + yield self._format_json("content", str(result)) + else: + # Якщо результат є ітерабельним, обробляємо його як раніше + for chunk in result: + if first: + first = False + yield self._format_json("provider", get_last_provider(True)) + if isinstance(chunk, BaseConversation): + if provider not in conversations: + conversations[provider] = {} + conversations[provider][conversation_id] = chunk + yield self._format_json("conversation", conversation_id) + elif isinstance(chunk, Exception): + logging.exception(chunk) + yield self._format_json("message", get_error_message(chunk)) + elif isinstance(chunk, ImagePreview): + yield self._format_json("preview", chunk.to_string()) + elif isinstance(chunk, ImageResponse): + # Обробка ImageResponse + images = asyncio.run(self._copy_images(chunk.get_list(), chunk.options.get("cookies"))) + yield self._format_json("content", str(ImageResponse(images, chunk.alt))) + elif not isinstance(chunk, FinishReason): + yield self._format_json("content", str(chunk)) except Exception as e: logging.exception(e) yield self._format_json('error', get_error_message(e)) + # Додайте цей метод до класу Api + async def _copy_images(self, images: list[str], cookies: Optional[Cookies] = None): + async with ClientSession( + connector=get_connector(None, os.environ.get("G4F_PROXY")), + cookies=cookies + ) as session: + async def copy_image(image): + async with session.get(image) as response: + target = os.path.join(images_dir, f"{int(time.time())}_{str(uuid.uuid4())}") + with open(target, "wb") as f: + async for chunk in response.content.iter_any(): + f.write(chunk) + with open(target, "rb") as f: + extension = is_accepted_format(f.read(12)).split("/")[-1] + extension = "jpg" if extension == "jpeg" else extension + new_target = f"{target}.{extension}" + os.rename(target, new_target) + return f"/images/{os.path.basename(new_target)}" + + return await asyncio.gather(*[copy_image(image) for image in images]) + def _format_json(self, response_type: str, content): """ Formats and returns a JSON response. @@ -231,6 +227,7 @@ class Api(): response_type: content } + def get_error_message(exception: Exception) -> str: """ Generates a formatted error message from an exception. @@ -245,4 +242,4 @@ def get_error_message(exception: Exception) -> str: provider = get_last_provider() if provider is None: return message - return f"{provider.__name__}: {message}"
\ No newline at end of file + return f"{provider.__name__}: {message}" diff --git a/g4f/gui/server/internet.py b/g4f/gui/server/internet.py index a1fafa7d..78bea0ca 100644 --- a/g4f/gui/server/internet.py +++ b/g4f/gui/server/internet.py @@ -101,7 +101,7 @@ async def search(query: str, n_results: int = 5, max_words: int = 2500, add_text raise MissingRequirementsError('Install "duckduckgo-search" and "beautifulsoup4" package') async with AsyncDDGS() as ddgs: results = [] - for result in await ddgs.text( + for result in await ddgs.atext( query, region="wt-wt", safesearch="moderate", |