diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a4c9117 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: debug-statements + - id: check-ast + + - repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black + language_version: python3 + args: [--line-length=100] diff --git a/llama_assistant/global_hotkey.py b/llama_assistant/global_hotkey.py index 707e8ed..3c97a98 100644 --- a/llama_assistant/global_hotkey.py +++ b/llama_assistant/global_hotkey.py @@ -7,9 +7,7 @@ class GlobalHotkey(QObject): def __init__(self, hotkey): super().__init__() - self.hotkey = keyboard.HotKey( - keyboard.HotKey.parse(hotkey), self.on_activate - ) + self.hotkey = keyboard.HotKey(keyboard.HotKey.parse(hotkey), self.on_activate) self.listener = keyboard.Listener( on_press=self.for_canonical(self.hotkey.press), on_release=self.for_canonical(self.hotkey.release), diff --git a/llama_assistant/icons.py b/llama_assistant/icons.py new file mode 100644 index 0000000..17fa01b --- /dev/null +++ b/llama_assistant/icons.py @@ -0,0 +1,32 @@ +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QIcon +from PyQt6.QtSvg import QSvgRenderer +from PyQt6.QtCore import QByteArray +from PyQt6.QtGui import QPixmap, QPainter + +# Updated SVG icons with white fill and stroke +copy_icon_svg = """ + + + + +""" + +clear_icon_svg = """ + + + + + +""" + + +def create_icon_from_svg(svg_string): + svg_bytes = QByteArray(svg_string.encode("utf-8")) + renderer = QSvgRenderer(svg_bytes) + pixmap = QPixmap(24, 24) # Size of the icon + pixmap.fill(Qt.GlobalColor.transparent) + painter = QPainter(pixmap) + renderer.render(painter) + painter.end() + return QIcon(pixmap) diff --git a/llama_assistant/llama_assistant.py b/llama_assistant/llama_assistant.py index 80ce1d9..6ca83e9 100644 --- a/llama_assistant/llama_assistant.py +++ b/llama_assistant/llama_assistant.py @@ -44,6 +44,11 @@ from llama_assistant.speech_recognition import SpeechRecognitionThread from llama_assistant.utils import image_to_base64_data_uri from llama_assistant.model_handler import handler as model_handler +from llama_assistant.icons import ( + create_icon_from_svg, + copy_icon_svg, + clear_icon_svg, +) class LlamaAssistant(QMainWindow): @@ -106,10 +111,7 @@ def save_settings(self): def init_ui(self): self.setWindowTitle("AI Assistant") self.setFixedSize(600, 200) - self.setWindowFlags( - Qt.WindowType.FramelessWindowHint - | Qt.WindowType.WindowStaysOnTopHint - ) + self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) central_widget = QWidget(self) @@ -147,9 +149,7 @@ def init_ui(self): top_layout.addWidget(self.input_field) # Load the mic icon from resources - with resources.path( - "llama_assistant.resources", "mic_icon.png" - ) as path: + with resources.path("llama_assistant.resources", "mic_icon.png") as path: mic_icon = QIcon(str(path)) self.mic_button = QPushButton(self) @@ -206,12 +206,33 @@ def init_ui(self): # Add new buttons to layout result_layout = QHBoxLayout() result_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) + + # Create and set up the Copy Result button self.copy_button = QPushButton("Copy Result", self) + self.copy_button.setIcon(create_icon_from_svg(copy_icon_svg)) + self.copy_button.setIconSize(QSize(18, 18)) + self.copy_button.setStyleSheet( + """ + QPushButton { padding-left: 4px; padding-right: 8px; } + QPushButton::icon { margin-right: 4px; } + """ + ) self.copy_button.clicked.connect(self.copy_result) self.copy_button.hide() + + # Create and set up the Clear button self.clear_button = QPushButton("Clear", self) + self.clear_button.setIcon(create_icon_from_svg(clear_icon_svg)) + self.clear_button.setIconSize(QSize(18, 18)) + self.clear_button.setStyleSheet( + """ + QPushButton { padding-left: 4px; padding-right: 8px; } + QPushButton::icon { margin-right: 4px; } + """ + ) self.clear_button.clicked.connect(self.clear_chat) self.clear_button.hide() + result_layout.addWidget(self.copy_button) result_layout.addWidget(self.clear_button) @@ -231,9 +252,7 @@ def init_ui(self): # Create a scroll area for the chat box self.scroll_area = QScrollArea(self) self.scroll_area.setWidgetResizable(True) - self.scroll_area.setHorizontalScrollBarPolicy( - Qt.ScrollBarPolicy.ScrollBarAlwaysOff - ) + self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.scroll_area.setStyleSheet( """ QScrollArea { @@ -285,7 +304,6 @@ def on_task_button_clicked(self): def update_styles(self): opacity = self.settings.get("transparency", 90) / 100 base_style = f""" - background-color: rgba{QColor(self.settings["color"]).getRgb()[:3] + (opacity,)}; border: none; border-radius: 20px; color: white; @@ -295,16 +313,23 @@ def update_styles(self): self.input_field.setStyleSheet( f""" QPlainTextEdit {{ + background-color: rgba{QColor(self.settings["color"]).getRgb()[:3] + (opacity,)}; {base_style} }} """ ) - self.chat_box.setStyleSheet(f"QTextBrowser {{ {base_style} }}") + self.chat_box.setStyleSheet( + f"""QTextBrowser {{ {base_style} + background-color: rgba{QColor(self.settings["color"]).lighter(120).getRgb()[:3] + (opacity,)}; + border-radius: 5px; + }}""" + ) button_style = f""" QPushButton {{ {base_style} padding: 2.5px 5px; border-radius: 5px; + background-color: rgba{QColor(self.settings["color"]).getRgb()[:3] + (opacity,)}; }} QPushButton:hover {{ background-color: rgba{QColor(self.settings["color"]).lighter(120).getRgb()[:3] + (opacity,)}; @@ -324,9 +349,10 @@ def update_styles(self): {base_style} padding: 2.5px 5px; border-radius: 5px; + background-color: rgba{QColor(self.settings["color"]).lighter(120).getRgb()[:3] + (opacity,)}; }} QPushButton:hover {{ - background-color: rgba(200, 200, 200, 0.8); + background-color: rgba{QColor(self.settings["color"]).lighter(150).getRgb()[:3] + (opacity,)}; }} """ for button in [self.copy_button, self.clear_button]: @@ -382,9 +408,7 @@ def toggle_visibility(self): def on_submit(self): message = self.input_field.toPlainText() self.input_field.clear() - self.loading_animation.move( - self.width() // 2 - 25, self.height() // 2 - 25 - ) + self.loading_animation.move(self.width() // 2 - 25, self.height() // 2 - 25) self.loading_animation.start_animation() if self.dropped_image: @@ -412,9 +436,7 @@ def process_text(self, message, task="chat"): self.last_response = response self.chat_box.append(f"You: {message}") - self.chat_box.append( - f"AI ({task}): {markdown.markdown(response)}" - ) + self.chat_box.append(f"AI ({task}): {markdown.markdown(response)}") self.loading_animation.stop_animation() self.show_chat_box() @@ -425,9 +447,7 @@ def process_image_with_prompt(self, image_path, prompt): self.chat_box.append(f"You: [Uploaded an image: {image_path}]") self.chat_box.append(f"You: {prompt}") self.chat_box.append( - f"AI: {markdown.markdown(response)}" - if response - else "No response" + f"AI: {markdown.markdown(response)}" if response else "No response" ) self.loading_animation.stop_animation() self.show_chat_box() @@ -438,9 +458,7 @@ def show_chat_box(self): self.copy_button.show() self.clear_button.show() self.setFixedHeight(600) # Increase this value if needed - self.chat_box.verticalScrollBar().setValue( - self.chat_box.verticalScrollBar().maximum() - ) + self.chat_box.verticalScrollBar().setValue(self.chat_box.verticalScrollBar().maximum()) def copy_result(self): self.hide() @@ -451,6 +469,7 @@ def copy_result(self): def clear_chat(self): self.chat_box.clear() self.last_response = "" + self.scroll_area.hide() def dragEnterEvent(self, event: QDragEnterEvent): if event.mimeData().hasUrls(): @@ -461,13 +480,9 @@ def dragEnterEvent(self, event: QDragEnterEvent): def dropEvent(self, event: QDropEvent): files = [u.toLocalFile() for u in event.mimeData().urls()] for file_path in files: - if file_path.lower().endswith( - (".png", ".jpg", ".jpeg", ".gif", ".bmp") - ): + if file_path.lower().endswith((".png", ".jpg", ".jpeg", ".gif", ".bmp")): self.dropped_image = file_path - self.input_field.setPlaceholderText( - "Enter a prompt for the image..." - ) + self.input_field.setPlaceholderText("Enter a prompt for the image...") self.show_image_thumbnail(file_path) break @@ -531,9 +546,7 @@ def show_image_thumbnail(self, image_path): # Add new image to layout self.image_layout.addWidget(self.image_label) - self.setFixedHeight( - self.height() + 110 - ) # Increase height to accommodate larger image + self.setFixedHeight(self.height() + 110) # Increase height to accommodate larger image def remove_image_thumbnail(self): if self.image_label: @@ -541,9 +554,7 @@ def remove_image_thumbnail(self): self.image_label = None self.dropped_image = None self.input_field.setPlaceholderText("Ask me anything...") - self.setFixedHeight( - self.height() - 110 - ) # Decrease height after removing image + self.setFixedHeight(self.height() - 110) # Decrease height after removing image def mousePressEvent(self, event): self.oldPos = event.globalPosition().toPoint() diff --git a/llama_assistant/loading_animation.py b/llama_assistant/loading_animation.py index 5f93a1f..6832fa8 100644 --- a/llama_assistant/loading_animation.py +++ b/llama_assistant/loading_animation.py @@ -56,9 +56,7 @@ def paintEvent(self, event): painter.setBrush(color) painter.setPen(Qt.PenStyle.NoPen) - painter.drawEllipse( - QPointF(x, y), self.dot_radius, self.dot_radius - ) + painter.drawEllipse(QPointF(x, y), self.dot_radius, self.dot_radius) @property def rotation(self): diff --git a/llama_assistant/model_handler.py b/llama_assistant/model_handler.py index 475615b..757009d 100644 --- a/llama_assistant/model_handler.py +++ b/llama_assistant/model_handler.py @@ -39,16 +39,12 @@ def add_supported_model(self, model: Model): self.supported_models.append(model) def remove_supported_model(self, model_id: str): - self.supported_models = [ - m for m in self.supported_models if m.model_id != model_id - ] + self.supported_models = [m for m in self.supported_models if m.model_id != model_id] if model_id in self.loaded_models: self.unload_model(model_id) def load_model(self, model_id: str) -> Optional[Dict]: - model = next( - (m for m in self.supported_models if m.model_id == model_id), None - ) + model = next((m for m in self.supported_models if m.model_id == model_id), None) if not model: print(f"Model with ID {model_id} not found.") return None @@ -123,9 +119,7 @@ def chat_completion( ] ) else: - response = model.create_chat_completion( - messages=[{"role": "user", "content": message}] - ) + response = model.create_chat_completion(messages=[{"role": "user", "content": message}]) return response["choices"][0]["message"]["content"] @@ -181,9 +175,7 @@ def _schedule_unload(self, model_id: str): # Use image model image_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg" - result = handler.chat_completion( - "moondream", "What's in this image?", image=image_url - ) + result = handler.chat_completion("moondream", "What's in this image?", image=image_url) print(result) # Use local model diff --git a/llama_assistant/setting_dialog.py b/llama_assistant/setting_dialog.py index e351680..2220ffd 100644 --- a/llama_assistant/setting_dialog.py +++ b/llama_assistant/setting_dialog.py @@ -40,9 +40,7 @@ def __init__(self, parent=None): self.ai_model_combo.addItems(["Llama 1B + Moondream2"]) self.layout.addRow("AI Model:", self.ai_model_combo) - self.label = QLabel( - "Note: Changing AI model will be supported in the future." - ) + self.label = QLabel("Note: Changing AI model will be supported in the future.") self.layout.addRow(self.label) self.save_button = QPushButton("Save") @@ -66,13 +64,9 @@ def load_settings(self): if settings_file.exists(): with open(settings_file, "r") as f: settings = json.load(f) - self.shortcut_recorder.setText( - settings.get("shortcut", "++") - ) + self.shortcut_recorder.setText(settings.get("shortcut", "++")) self.color = QColor(settings.get("color", "#1E1E1E")) - self.transparency_slider.setValue( - int(settings.get("transparency", 90)) - ) + self.transparency_slider.setValue(int(settings.get("transparency", 90))) # self.ai_model_combo.setCurrentText( # settings.get("ai_model", "Llama 1B") # ) # TODO: Implement this feature diff --git a/llama_assistant/speech_recognition.py b/llama_assistant/speech_recognition.py index 4573ece..2a0c0c0 100644 --- a/llama_assistant/speech_recognition.py +++ b/llama_assistant/speech_recognition.py @@ -17,9 +17,7 @@ def run(self): self.recognizer.adjust_for_ambient_noise(source) while not self.stop_listening: try: - audio = self.recognizer.listen( - source, timeout=1, phrase_time_limit=10 - ) + audio = self.recognizer.listen(source, timeout=1, phrase_time_limit=10) text = self.recognizer.recognize_google(audio) self.finished.emit(text) except sr.WaitTimeoutError: diff --git a/pyproject.toml b/pyproject.toml index 13c8d6f..0e87b97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,4 +51,47 @@ include = ["llama_assistant*"] exclude = ["tests*"] [tool.setuptools.package-data] -"llama_assistant.resources" = ["*.png"] \ No newline at end of file +"llama_assistant.resources" = ["*.png"] + + +[tool.black] +line-length = 100 +target-version = ['py37'] +include = '\.pyi?$' +extend-exclude = ''' +/( + # directories + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | build + | dist +)/ +''' + +[tool.pylint.master] +ignore-patterns = ["test_.*?py"] + +[tool.pylint.format] +max-line-length = 100 + +[tool.pylint.messages_control] +disable = [ + "C0114", # missing-module-docstring + "C0116", # missing-function-docstring + "C0103", # invalid-name + "W0611", # unused-import + "W0612", # unused-variable + "W0613", # unused-argument + "W0621", # redefined-outer-name + "W0622", # redefined-builtin + "W0703", # broad-except + "R0801", # duplicate-code + "R0902", # too-many-instance-attributes + "R0903", # too-few-public-methods + "R0904", # too-many-public-methods + "R0913", # too-many-arguments +]