summaryrefslogtreecommitdiffstats
path: root/g4f/gui
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--g4f/gui/client/index.html51
-rw-r--r--g4f/gui/client/static/css/style.css5
-rw-r--r--g4f/gui/client/static/js/chat.v1.js52
-rw-r--r--g4f/gui/client/static/js/highlightjs-copy.min.js55
-rw-r--r--g4f/gui/server/api.py182
-rw-r--r--g4f/gui/server/internet.py2
-rw-r--r--g4f/gui/server/website.py6
7 files changed, 202 insertions, 151 deletions
diff --git a/g4f/gui/client/index.html b/g4f/gui/client/index.html
index 1a660062..7e8ef09c 100644
--- a/g4f/gui/client/index.html
+++ b/g4f/gui/client/index.html
@@ -224,28 +224,35 @@
</div>
</div>
<div class="buttons">
- <div class="field">
- <select name="model" id="model">
- <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="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>
- <select name="model2" id="model2" class="hidden"></select>
- </div>
- <div class="field">
- <select name="provider" id="provider">
- <option value="">Provider: Auto</option>
- <option value="Bing">Bing</option>
- <option value="OpenaiChat">OpenAI ChatGPT</option>
- <option value="Gemini">Gemini</option>
- <option value="Liaobots">Liaobots</option>
- <option value="MetaAI">Meta AI</option>
- <option value="You">You</option>
- <option value="">----</option>
+ <div class="field">
+ <select name="model" id="model">
+ <option value="">Model: Default</option>
+ <option value="gpt-4">gpt-4</option>
+ <option value="gpt-4o">gpt-4o</option>
+ <option value="gpt-4o-mini">gpt-4o-mini</option>
+ <option value="llama-3.1-70b">llama-3.1-70b</option>
+ <option value="llama-3.1-70b">llama-3.1-405b</option>
+ <option value="llama-3.1-70b">mixtral-8x7b</option>
+ <option value="gemini-pro">gemini-pro</option>
+ <option value="gemini-flash">gemini-flash</option>
+ <option value="claude-3-haiku">claude-3-haiku</option>
+ <option value="claude-3.5-sonnet">claude-3.5-sonnet</option>
+ <option value="">----</option>
+ </select>
+ <select name="model2" id="model2" class="hidden"></select>
+ </div>
+ <div class="field">
+ <select name="provider" id="provider">
+ <option value="">Provider: Auto</option>
+ <option value="OpenaiChat">OpenAI ChatGPT</option>
+ <option value="Gemini">Gemini</option>
+ <option value="MetaAI">Meta AI</option>
+ <option value="DeepInfraChat">DeepInfraChat</option>
+ <option value="Blackbox">Blackbox</option>
+ <option value="HuggingChat">HuggingChat</option>
+ <option value="DDG">DDG</option>
+ <option value="Pizzagpt">Pizzagpt</option>
+ <option value="">----</option>
</select>
</div>
</div>
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..42ddb129 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";
@@ -306,6 +338,14 @@ const prepare_messages = (messages, message_index = -1) => {
messages = messages.filter((_, index) => message_index >= index);
}
+ let new_messages = [];
+ if (systemPrompt?.value) {
+ new_messages.push({
+ "role": "system",
+ "content": systemPrompt.value
+ });
+ }
+
// Remove history, if it's selected
if (document.getElementById('history')?.checked) {
if (message_index == null) {
@@ -315,13 +355,6 @@ const prepare_messages = (messages, message_index = -1) => {
}
}
- let new_messages = [];
- if (systemPrompt?.value) {
- new_messages.push({
- "role": "system",
- "content": systemPrompt.value
- });
- }
messages.forEach((new_message) => {
// Include only not regenerated messages
if (new_message && !new_message.regenerate) {
@@ -334,6 +367,7 @@ const prepare_messages = (messages, message_index = -1) => {
return new_messages;
}
+
async function add_message_chunk(message) {
if (message.type == "conversation") {
console.info("Conversation used:", message.conversation)
@@ -1424,4 +1458,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..7aac650a 100644
--- a/g4f/gui/server/api.py
+++ b/g4f/gui/server/api.py
@@ -2,12 +2,11 @@ from __future__ import annotations
import logging
import os
-import os.path
import uuid
import asyncio
import time
from aiohttp import ClientSession
-from typing import Iterator, Optional
+from typing import Iterator, Optional, AsyncIterator, Union
from flask import send_from_directory
from g4f import version, models
@@ -20,21 +19,20 @@ from g4f.Provider import ProviderType, __providers__, __map__
from g4f.providers.base_provider import ProviderModelMixin, FinishReason
from g4f.providers.conversation import BaseConversation
-conversations: dict[dict[str, BaseConversation]] = {}
+# Define the directory for generated images
images_dir = "./generated_images"
-class Api():
+# Function to ensure the images directory exists
+def ensure_images_dir():
+ if not os.path.exists(images_dir):
+ os.makedirs(images_dir)
- @staticmethod
- def get_models() -> list[str]:
- """
- Return a list of all models.
+conversations: dict[dict[str, BaseConversation]] = {}
- Fetches and returns a list of all available models in the system.
- Returns:
- List[str]: A list of model names.
- """
+class Api:
+ @staticmethod
+ def get_models() -> list[str]:
return models._all_models
@staticmethod
@@ -42,14 +40,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 +66,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__,
@@ -85,31 +80,20 @@ class Api():
@staticmethod
def get_providers() -> list[str]:
- """
- 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
}
@staticmethod
def get_version():
- """
- Returns the current and latest version of the application.
-
- Returns:
- dict: A dictionary containing the current and latest version.
- """
try:
current_version = version.utils.current_version
except VersionNotFoundError:
@@ -120,18 +104,10 @@ class Api():
}
def serve_images(self, name):
+ ensure_images_dir()
return send_from_directory(os.path.abspath(images_dir), name)
def _prepare_conversation_kwargs(self, json_data: dict, kwargs: dict):
- """
- Prepares arguments for chat completion based on the request data.
-
- Reads the request and prepares the necessary arguments for handling
- a chat completion request.
-
- Returns:
- dict: Arguments prepared for chat completion.
- """
model = json_data.get('model') or models.default
provider = json_data.get('provider')
messages = json_data['messages']
@@ -139,7 +115,7 @@ class Api():
if api_key is not None:
kwargs["api_key"] = api_key
if json_data.get('web_search'):
- if provider in ("Bing", "HuggingChat"):
+ if provider:
kwargs['web_search'] = True
else:
from .internet import get_search_message
@@ -160,89 +136,69 @@ 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):
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):
+ 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))
- def _format_json(self, response_type: str, content):
- """
- Formats and returns a JSON response.
-
- Args:
- response_type (str): The type of the response.
- content: The content to be included in the response.
+ async def _copy_images(self, images: list[str], cookies: Optional[Cookies] = None):
+ ensure_images_dir()
+ 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])
- Returns:
- str: A JSON formatted string.
- """
+ def _format_json(self, response_type: str, content):
return {
'type': response_type,
response_type: content
}
-def get_error_message(exception: Exception) -> str:
- """
- Generates a formatted error message from an exception.
- Args:
- exception (Exception): The exception to format.
-
- Returns:
- str: A formatted error message string.
- """
+def get_error_message(exception: Exception) -> str:
message = f"{type(exception).__name__}: {exception}"
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",
diff --git a/g4f/gui/server/website.py b/g4f/gui/server/website.py
index 5e633674..3cabcdf3 100644
--- a/g4f/gui/server/website.py
+++ b/g4f/gui/server/website.py
@@ -27,6 +27,10 @@ class Website:
'function': redirect_home,
'methods': ['GET', 'POST']
},
+ '/images/': {
+ 'function': redirect_home,
+ 'methods': ['GET', 'POST']
+ },
}
def _chat(self, conversation_id):
@@ -35,4 +39,4 @@ class Website:
return render_template('index.html', chat_id=conversation_id)
def _index(self):
- return render_template('index.html', chat_id=str(uuid.uuid4())) \ No newline at end of file
+ return render_template('index.html', chat_id=str(uuid.uuid4()))