年初的时候再电脑上部署了deepseek的大模型,在ollama上部署,用chatbox操作.人比较懒,操作少.等到四月份的时候再用.ollama自己可以操作大模型了,而chatbox上运作不了.也没怎么理他,到八月,莫名其妙的ollama的模型要从新下载.

细想一下,AI大模型才刚火,就像当年的QQ,微信,360小巧好用,现在,唉!特别是360.所以就像自己编一段代码用于和本地部署的大模型交互.

学Python的时间比较短,都是自学,代码都是借助大模型一点一点完善出来的.

import sys
import os
import json
import requests
import threading
import time
import subprocess
import psutil
import tempfile
import webbrowser
from datetime import datetime
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
                             QTextEdit, QLineEdit, QPushButton, QListWidget, QLabel,
                             QProgressBar, QFileDialog, QMessageBox, QComboBox, QSplitter,
                             QFrame, QListWidgetItem, QStatusBar, QGroupBox, QSlider,
                             QToolBar, QAction, QMenu, QToolButton, QSizePolicy, QDialog,
                             QDialogButtonBox, QCheckBox, QTextBrowser, QScrollArea)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSize, QUrl, QTimer
from PyQt5.QtGui import QFont, QColor, QTextCharFormat, QTextCursor, QIcon, QPalette, QDesktopServices, \
    QSyntaxHighlighter, QTextDocument
from PyQt5.QtPrintSupport import QPrintDialog, QPrinter


class CodeHighlighter(QSyntaxHighlighter):
    """简单的代码高亮器"""

    def __init__(self, parent=None):
        super(CodeHighlighter, self).__init__(parent)

        self.highlighting_rules = []

        # 关键字格式
        keyword_format = QTextCharFormat()
        keyword_format.setForeground(QColor("#0066CC"))
        keyword_format.setFontWeight(QFont.Bold)

        # 常见编程关键字
        keywords = [
            "def", "class", "return", "if", "else", "elif", "for", "while",
            "import", "from", "as", "try", "except", "finally", "with",
            "var", "let", "const", "function", "export", "import", "return",
            "public", "private", "protected", "static", "void", "int", "string"
        ]

        for word in keywords:
            pattern = r"\b" + word + r"\b"
            self.highlighting_rules.append((QtCore.QRegExp(pattern), keyword_format))

        # 字符串格式
        string_format = QTextCharFormat()
        string_format.setForeground(QColor("#008800"))
        self.highlighting_rules.append((QtCore.QRegExp("\".*\""), string_format))
        self.highlighting_rules.append((QtCore.QRegExp("'.*'"), string_format))

        # 注释格式
        comment_format = QTextCharFormat()
        comment_format.setForeground(QColor("#888888"))
        self.highlighting_rules.append((QtCore.QRegExp("#[^\n]*"), comment_format))

    def highlightBlock(self, text):
        for pattern, format in self.highlighting_rules:
            expression = QtCore.QRegExp(pattern)
            index = expression.indexIn(text)
            while index >= 0:
                length = expression.matchedLength()
                self.setFormat(index, length, format)
                index = expression.indexIn(text, index + length)


class ExportDialog(QDialog):
    """导出文件对话框"""

    def __init__(self, content, parent=None):
        super().__init__(parent)
        self.content = content
        self.initUI()

    def initUI(self):
        self.setWindowTitle("导出代码")
        self.setGeometry(100, 100, 800, 600)

        layout = QVBoxLayout(self)

        # 文件格式选择
        format_layout = QHBoxLayout()
        format_layout.addWidget(QLabel("文件格式:"))

        self.format_combo = QComboBox()
        self.format_combo.addItems(["Python (.py)", "JavaScript (.js)", "HTML (.html)",
                                    "CSS (.css)", "文本文件 (.txt)", "Markdown (.md)"])
        format_layout.addWidget(self.format_combo)

        layout.addLayout(format_layout)

        # 文件名输入
        name_layout = QHBoxLayout()
        name_layout.addWidget(QLabel("文件名:"))

        self.name_edit = QLineEdit(f"code_{datetime.now().strftime('%Y%m%d_%H%M%S')}")
        name_layout.addWidget(self.name_edit)

        layout.addLayout(name_layout)

        # 代码预览
        layout.addWidget(QLabel("代码预览:"))

        self.preview = QTextEdit()
        self.preview.setPlainText(self.content)
        self.preview.setReadOnly(True)
        layout.addWidget(self.preview)

        # 按钮
        button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        button_box.accepted.connect(self.accept)
        button_box.rejected.connect(self.reject)
        layout.addWidget(button_box)

    def get_file_info(self):
        format_map = {
            "Python (.py)": ".py",
            "JavaScript (.js)": ".js",
            "HTML (.html)": ".html",
            "CSS (.css)": ".css",
            "文本文件 (.txt)": ".txt",
            "Markdown (.md)": ".md"
        }

        ext = format_map[self.format_combo.currentText()]
        filename = self.name_edit.text()
        if not filename.endswith(ext):
            filename += ext

        return filename, ext


class NetworkSettingsDialog(QDialog):
    """网络设置对话框"""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.initUI()

    def initUI(self):
        self.setWindowTitle("网络设置")
        self.setGeometry(100, 100, 500, 300)

        layout = QVBoxLayout(self)

        # 网络访问权限
        self.network_access = QCheckBox("允许模型访问网络查找信息")
        self.network_access.setChecked(True)
        layout.addWidget(self.network_access)

        # 代理设置
        proxy_group = QGroupBox("代理设置 (可选)")
        proxy_layout = QVBoxLayout(proxy_group)

        http_layout = QHBoxLayout()
        http_layout.addWidget(QLabel("HTTP代理:"))
        self.http_proxy = QLineEdit()
        http_layout.addWidget(self.http_proxy)
        proxy_layout.addLayout(http_layout)

        https_layout = QHBoxLayout()
        https_layout.addWidget(QLabel("HTTPS代理:"))
        self.https_proxy = QLineEdit()
        https_layout.addWidget(self.https_proxy)
        proxy_layout.addLayout(https_layout)

        layout.addWidget(proxy_group)

        # 说明文本
        info = QTextBrowser()
        info.setPlainText("注意:\n"
                          "- 启用网络访问后,模型可以查找最新信息,但响应速度可能会变慢\n"
                          "- 如果网络连接失败,模型会使用其内部知识回答问题\n"
                          "- 代理设置适用于需要科学上网的环境")
        info.setMaximumHeight(100)
        layout.addWidget(info)

        # 按钮
        button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        button_box.accepted.connect(self.accept)
        button_box.rejected.connect(self.reject)
        layout.addWidget(button_box)


class ModelLoaderThread(QThread):
    """模型加载线程"""
    progress_signal = pyqtSignal(int)
    finished_signal = pyqtSignal(str)
    error_signal = pyqtSignal(str)

    def __init__(self, model_name, ollama_host):
        super().__init__()
        self.model_name = model_name
        self.ollama_host = ollama_host

    def run(self):
        try:
            # 先检查模型是否已存在
            url = f"{self.ollama_host}/api/tags"
            response = requests.get(url, timeout=10)
            if response.status_code == 200:
                models = response.json().get('models', [])
                if any(model['name'] == self.model_name for model in models):
                    self.finished_signal.emit(f"模型 '{self.model_name}' 已加载!")
                    return

            # 拉取模型
            url = f"{self.ollama_host}/api/pull"
            payload = {"name": self.model_name}
            response = requests.post(url, json=payload, stream=True, timeout=300)

            if response.status_code == 200:
                total_size = 0
                downloaded = 0

                for line in response.iter_lines():
                    if line:
                        try:
                            data = json.loads(line.decode('utf-8'))
                            if 'total' in data:
                                total_size = data['total']
                            if 'completed' in data:
                                downloaded = data['completed']
                                if total_size > 0:
                                    progress = int((downloaded / total_size) * 100)
                                    self.progress_signal.emit(progress)
                        except:
                            continue

                self.finished_signal.emit(f"模型 '{self.model_name}' 加载成功!")
            else:
                self.error_signal.emit(f"模型加载失败: HTTP {response.status_code}")

        except Exception as e:
            self.error_signal.emit(f"加载错误: {str(e)}")


class ChatThread(QThread):
    """聊天线程"""
    response_signal = pyqtSignal(str, bool)  # 内容, 是否结束
    error_signal = pyqtSignal(str)
    stop_signal = pyqtSignal()

    def __init__(self, model_name, messages, ollama_host, attached_file_content=None, allow_network=False):
        super().__init__()
        self.model_name = model_name
        self.messages = messages
        self.ollama_host = ollama_host
        self.attached_file_content = attached_file_content
        self.allow_network = allow_network
        self._is_running = True

    def stop(self):
        """停止生成"""
        self._is_running = False
        self.stop_signal.emit()

    def run(self):
        try:
            # 如果有附件内容,添加到消息中
            if self.attached_file_content:
                # 添加到用户消息中而不是系统消息
                self.messages[-1][
                    'content'] = f"文件内容:\n{self.attached_file_content}\n\n问题:{self.messages[-1]['content']}"

            # 添加网络访问权限提示
            if self.allow_network:
                system_message = {
                    "role": "system",
                    "content": "你被允许访问网络来查找最新信息。如果用户询问需要最新数据的问题,请使用网络搜索功能。如果网络不可用或搜索失败,请明确告知用户并基于你的知识回答问题。"
                }
                self.messages.insert(0, system_message)

            # 准备API请求 - 使用正确的Ollama API格式
            url = f"{self.ollama_host}/api/chat"
            payload = {
                "model": self.model_name,
                "messages": self.messages,
                "stream": True
            }

            # 发送请求
            response = requests.post(url, json=payload, stream=True, timeout=120)

            if response.status_code != 200:
                error_msg = f"API请求失败: HTTP {response.status_code}"
                try:
                    error_detail = response.json()
                    error_msg += f"\n详情: {error_detail}"
                except:
                    pass
                self.error_signal.emit(error_msg)
                return

            full_response = ""
            for line in response.iter_lines():
                if not self._is_running:
                    break

                if line:
                    try:
                        chunk = json.loads(line.decode('utf-8'))
                        if chunk.get('done', False):
                            self.response_signal.emit("", True)  # 结束信号
                            break
                        if 'message' in chunk and 'content' in chunk['message']:
                            content = chunk['message']['content']
                            full_response += content
                            self.response_signal.emit(content, False)
                    except json.JSONDecodeError as e:
                        self.error_signal.emit(f"JSON解析错误: {str(e)}")
                        continue

        except Exception as e:
            self.error_signal.emit(f"聊天错误: {str(e)}")


class OllamaGUI(QMainWindow):
    def __init__(self):
        super().__init__()
        self.available_models = []
        self.loaded_models = []
        self.attached_file_path = None
        self.attached_file_content = None
        self.current_model = None
        self.ollama_host = "http://localhost:11434"  # 默认Ollama地址
        self.font_size = 14  # 默认字体大小
        self.current_response_content = ""  # 存储当前响应内容
        self.allow_network_access = True  # 默认允许网络访问
        self.is_generating = False  # 是否正在生成响应
        self.chat_thread = None  # 聊天线程
        self.initUI()
        self.check_ollama_connection()

    def initUI(self):
        self.setWindowTitle('Ollama 大模型交互界面 - 专业版')
        self.setGeometry(100, 100, 1400, 900)

        # 设置主窗口部件
        central_widget = QWidget()
        self.setCentralWidget(central_widget)

        # 主布局
        main_layout = QHBoxLayout(central_widget)

        # 创建工具栏
        self.create_toolbar()

        # 左侧面板
        left_panel = QFrame()
        left_panel.setFrameShape(QFrame.StyledPanel)
        left_panel.setFixedWidth(350)
        left_layout = QVBoxLayout(left_panel)

        # 连接设置组
        connection_group = QGroupBox("连接设置")
        connection_group.setStyleSheet("""
            QGroupBox {
                font-weight: bold;
                font-size: 14px;
                color: #2E86AB;
                border: 2px solid #2E86AB;
                border-radius: 8px;
                margin-top: 10px;
                padding-top: 15px;
            }
            QGroupBox::title {
                subcontrol-origin: margin;
                left: 10px;
                padding: 0 5px 0 5px;
            }
        """)
        connection_layout = QVBoxLayout(connection_group)

        host_layout = QHBoxLayout()
        host_layout.addWidget(QLabel("Ollama地址:"))
        self.host_input = QLineEdit(self.ollama_host)
        host_layout.addWidget(self.host_input)
        connection_layout.addLayout(host_layout)

        test_btn = QPushButton("测试连接")
        test_btn.clicked.connect(self.check_ollama_connection)
        test_btn.setStyleSheet("""
            QPushButton {
                background-color: #2E86AB;
                color: white;
                border: 2px solid #1B5E84;
                border-radius: 6px;
                padding: 8px;
                font-weight: bold;
            }
            QPushButton:hover {
                background-color: #3B99D9;
            }
            QPushButton:pressed {
                background-color: #1B5E84;
            }
        """)
        connection_layout.addWidget(test_btn)

        left_layout.addWidget(connection_group)

        # 模型管理组
        model_group = QGroupBox("模型管理")
        model_group.setStyleSheet(connection_group.styleSheet())
        model_layout = QVBoxLayout(model_group)

        # 模型列表
        self.model_list = QListWidget()
        self.model_list.itemClicked.connect(self.on_model_selected)
        model_layout.addWidget(QLabel("可用模型:"))
        model_layout.addWidget(self.model_list)

        # 按钮布局
        btn_layout = QHBoxLayout()
        refresh_btn = QPushButton("刷新列表")
        refresh_btn.clicked.connect(self.check_local_models)
        refresh_btn.setStyleSheet("""
            QPushButton {
                background-color: #A23B72;
                color: white;
                border: 2px solid #7D2C5A;
                border-radius: 6px;
                padding: 6px;
            }
            QPushButton:hover { background-color: #C34F8C; }
        """)
        btn_layout.addWidget(refresh_btn)

        delete_btn = QPushButton("删除模型")
        delete_btn.clicked.connect(self.delete_selected_model)
        delete_btn.setStyleSheet("""
            QPushButton {
                background-color: #F18F01;
                color: white;
                border: 2px solid #C57601;
                border-radius: 6px;
                padding: 6px;
            }
            QPushButton:hover { background-color: #FFA726; }
        """)
        btn_layout.addWidget(delete_btn)

        model_layout.addLayout(btn_layout)
        left_layout.addWidget(model_group)

        # 模型加载组
        load_group = QGroupBox("模型加载")
        load_group.setStyleSheet(connection_group.styleSheet())
        load_layout = QVBoxLayout(load_group)

        # 系统信息
        sys_info = self.get_system_info()
        sys_label = QLabel(f"系统资源: {sys_info}")
        sys_label.setWordWrap(True)
        sys_label.setStyleSheet(
            "color: #6C757D; font-size: 12px; background-color: #F8F9FA; padding: 5px; border-radius: 4px;")
        load_layout.addWidget(sys_label)

        self.model_combo = QComboBox()
        load_layout.addWidget(QLabel("选择模型加载:"))
        load_layout.addWidget(self.model_combo)

        # 进度条
        self.progress_bar = QProgressBar()
        self.progress_bar.setVisible(False)
        load_layout.addWidget(self.progress_bar)

        # 加载按钮
        self.load_btn = QPushButton("加载模型")
        self.load_btn.clicked.connect(self.load_model)
        self.load_btn.setStyleSheet(test_btn.styleSheet())
        load_layout.addWidget(self.load_btn)

        left_layout.addWidget(load_group)

        # 附件组
        attach_group = QGroupBox("文件附件")
        attach_group.setStyleSheet(connection_group.styleSheet())
        attach_layout = QVBoxLayout(attach_group)

        self.attach_btn = QPushButton("选择文件")
        self.attach_btn.clicked.connect(self.attach_file)
        self.attach_btn.setStyleSheet("""
            QPushButton {
                background-color: #C73E1D;
                color: white;
                border: 2px solid #9B3017;
                border-radius: 6px;
                padding: 8px;
            }
            QPushButton:hover { background-color: #E04A23; }
        """)
        attach_layout.addWidget(self.attach_btn)

        self.attach_label = QLabel("未选择文件")
        self.attach_label.setWordWrap(True)
        self.attach_label.setStyleSheet(
            "color: #6C757D; font-size: 12px; background-color: #F8F9FA; padding: 8px; border-radius: 4px; border: 1px dashed #DEE2E6;")
        attach_layout.addWidget(self.attach_label)

        left_layout.addWidget(attach_group)
        left_layout.addStretch()

        # 右侧面板
        right_panel = QFrame()
        right_panel.setFrameShape(QFrame.StyledPanel)
        right_layout = QVBoxLayout(right_panel)

        # 当前模型显示
        self.current_model_label = QLabel("当前模型: 未选择")
        self.current_model_label.setStyleSheet("""
            QLabel {
                background-color: #2E86AB;
                color: white;
                padding: 10px;
                border-radius: 6px;
                font-weight: bold;
                font-size: 14px;
            }
        """)
        right_layout.addWidget(self.current_model_label)

        # 聊天显示区域
        self.chat_display = QTextEdit()
        self.chat_display.setReadOnly(True)
        self.chat_display.setStyleSheet(f"""
            QTextEdit {{
                background-color: #FFFFFF;
                border: 2px solid #DEE2E6;
                border-radius: 8px;
                padding: 15px;
                font-size: {self.font_size}px;
                font-family: 'Microsoft YaHei', sans-serif;
            }}
        """)
        right_layout.addWidget(self.chat_display)

        # 输入区域
        input_layout = QHBoxLayout()
        self.input_field = QLineEdit()
        self.input_field.setPlaceholderText("输入您的问题...")
        self.input_field.returnPressed.connect(self.send_message)
        self.input_field.setStyleSheet("""
            QLineEdit {
                border: 2px solid #DEE2E6;
                border-radius: 6px;
                padding: 12px;
                font-size: 14px;
                font-family: 'Microsoft YaHei', sans-serif;
            }
            QLineEdit:focus {
                border-color: #2E86AB;
            }
        """)
        input_layout.addWidget(self.input_field)

        self.send_btn = QPushButton("发送")
        self.send_btn.clicked.connect(self.send_message)
        self.send_btn.setStyleSheet(test_btn.styleSheet())
        self.send_btn.setFixedWidth(80)
        input_layout.addWidget(self.send_btn)

        self.stop_btn = QPushButton("停止")
        self.stop_btn.clicked.connect(self.stop_generation)
        self.stop_btn.setStyleSheet("""
            QPushButton {
                background-color: #C73E1D;
                color: white;
                border: 2px solid #9B3017;
                border-radius: 6px;
                padding: 12px;
                font-weight: bold;
            }
            QPushButton:hover { background-color: #E04A23; }
            QPushButton:disabled { background-color: #CCCCCC; }
        """)
        self.stop_btn.setFixedWidth(80)
        self.stop_btn.setEnabled(False)
        input_layout.addWidget(self.stop_btn)

        right_layout.addLayout(input_layout)

        # 将左右面板添加到主布局
        main_layout.addWidget(left_panel)
        main_layout.addWidget(right_panel)

        # 设置状态栏
        self.status_bar = QStatusBar()
        self.setStatusBar(self.status_bar)

        # 设置应用程序样式
        self.setStyleSheet("""
            QMainWindow {
                background-color: #F5F7FA;
                font-family: 'Microsoft YaHei', sans-serif;
            }
            QGroupBox {
                font-weight: bold;
                color: #2E86AB;
            }
            QLabel {
                color: #343A40;
                font-family: 'Microsoft YaHei', sans-serif;
            }
            QPushButton {
                font-family: 'Microsoft YaHei', sans-serif;
                font-weight: bold;
            }
            QListWidget {
                border: 2px solid #DEE2E6;
                border-radius: 6px;
                background-color: #FFFFFF;
                font-family: 'Microsoft YaHei', sans-serif;
            }
            QListWidget::item {
                padding: 8px;
                border-bottom: 1px solid #E9ECEF;
            }
            QListWidget::item:selected {
                background-color: #2E86AB;
                color: white;
            }
            QComboBox {
                border: 2px solid #DEE2E6;
                border-radius: 6px;
                padding: 8px;
                font-family: 'Microsoft YaHei', sans-serif;
            }
            QComboBox:focus {
                border-color: #2E86AB;
            }
            QProgressBar {
                border: 2px solid #DEE2E6;
                border-radius: 6px;
                text-align: center;
                font-family: 'Microsoft YaHei', sans-serif;
            }
            QProgressBar::chunk {
                background-color: #2E86AB;
                border-radius: 4px;
            }
            QMessageBox {
                background-color: #FFFFFF;
                font-family: 'Microsoft YaHei', sans-serif;
            }
            QMessageBox QLabel {
                font-family: 'Microsoft YaHei', sans-serif;
            }
        """)

        # 初始化状态
        self.update_status("就绪")

    def create_toolbar(self):
        """创建工具栏"""
        toolbar = QToolBar("主工具栏")
        toolbar.setIconSize(QSize(16, 16))
        self.addToolBar(toolbar)

        # 字体大小调节
        toolbar.addWidget(QLabel("字体大小:"))

        self.font_size_slider = QSlider(Qt.Horizontal)
        self.font_size_slider.setMinimum(10)
        self.font_size_slider.setMaximum(24)
        self.font_size_slider.setValue(self.font_size)
        self.font_size_slider.setFixedWidth(100)
        self.font_size_slider.valueChanged.connect(self.change_font_size)
        toolbar.addWidget(self.font_size_slider)

        toolbar.addWidget(QLabel(f"{self.font_size}px"))

        # 添加分隔符
        toolbar.addSeparator()

        # 网络设置按钮
        network_action = QAction("网络设置", self)
        network_action.triggered.connect(self.show_network_settings)
        toolbar.addAction(network_action)

        # 复制按钮
        copy_action = QAction("复制代码", self)
        copy_action.setShortcut("Ctrl+C")
        copy_action.triggered.connect(self.copy_code_blocks)
        toolbar.addAction(copy_action)

        # 导出按钮
        export_action = QAction("导出代码", self)
        export_action.setShortcut("Ctrl+E")
        export_action.triggered.connect(self.export_code)
        toolbar.addAction(export_action)

        # 保存按钮
        save_action = QAction("保存聊天", self)
        save_action.setShortcut("Ctrl+S")
        save_action.triggered.connect(self.save_chat)
        toolbar.addAction(save_action)

        # 清除按钮
        clear_action = QAction("清除聊天", self)
        clear_action.triggered.connect(self.clear_chat)
        toolbar.addAction(clear_action)

    def show_network_settings(self):
        """显示网络设置对话框"""
        dialog = NetworkSettingsDialog(self)
        dialog.network_access.setChecked(self.allow_network_access)

        if dialog.exec_() == QDialog.Accepted:
            self.allow_network_access = dialog.network_access.isChecked()
            status = "启用" if self.allow_network_access else "禁用"
            self.update_status(f"网络访问已{status}")

    def change_font_size(self, size):
        """改变字体大小"""
        self.font_size = size
        self.chat_display.setStyleSheet(f"""
            QTextEdit {{
                background-color: #FFFFFF;
                border: 2px solid #DEE2E6;
                border-radius: 8px;
                padding: 15px;
                font-size: {self.font_size}px;
                font-family: 'Microsoft YaHei', sans-serif;
            }}
        """)

    def extract_code_blocks(self, text):
        """从文本中提取所有代码块"""
        code_blocks = []
        lines = text.split('\n')
        in_code_block = False
        current_block = []
        code_language = ""

        for line in lines:
            if line.strip().startswith('```'):
                if in_code_block:
                    # 结束代码块
                    if current_block:
                        code_blocks.append({
                            'language': code_language,
                            'code': '\n'.join(current_block)
                        })
                    current_block = []
                    in_code_block = False
                else:
                    # 开始代码块
                    in_code_block = True
                    code_language = line.strip()[3:].strip()  # 提取语言标识
            elif in_code_block:
                current_block.append(line)

        return code_blocks

    def copy_code_blocks(self):
        """复制所有代码块到剪贴板"""
        if not self.current_response_content:
            QMessageBox.information(self, "提示", "没有可复制的代码")
            return

        code_blocks = self.extract_code_blocks(self.current_response_content)
        if not code_blocks:
            QMessageBox.information(self, "提示", "没有检测到代码块")
            return

        # 将所有代码块合并
        all_code = ""
        for i, block in enumerate(code_blocks, 1):
            if len(code_blocks) > 1:
                all_code += f"# 代码块 {i} ({block['language'] or '无指定语言'})\n"
            all_code += block['code'] + "\n\n"

        # 复制到剪贴板
        clipboard = QApplication.clipboard()
        clipboard.setText(all_code.strip())

        self.update_status("已复制所有代码块")
        QMessageBox.information(self, "成功", "所有代码块已复制到剪贴板")

    def export_code(self):
        """导出代码到文件"""
        if not self.current_response_content:
            QMessageBox.information(self, "提示", "没有可导出的代码")
            return

        code_blocks = self.extract_code_blocks(self.current_response_content)
        if not code_blocks:
            QMessageBox.information(self, "提示", "没有检测到代码块")
            return

        # 如果有多个代码块,让用户选择
        if len(code_blocks) > 1:
            selection_dialog = QDialog(self)
            selection_dialog.setWindowTitle("选择代码块")
            selection_dialog.setModal(True)
            layout = QVBoxLayout(selection_dialog)

            layout.addWidget(QLabel("检测到多个代码块,请选择要导出的代码块:"))

            list_widget = QListWidget()
            for i, block in enumerate(code_blocks, 1):
                lang = block['language'] or '无指定语言'
                preview = block['code'][:50].replace('\n', ' ') + "..." if len(block['code']) > 50 else block['code']
                list_widget.addItem(f"代码块 {i} ({lang}): {preview}")

            layout.addWidget(list_widget)

            button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
            button_box.accepted.connect(selection_dialog.accept)
            button_box.rejected.connect(selection_dialog.reject)
            layout.addWidget(button_box)

            if selection_dialog.exec_() != QDialog.Accepted or not list_widget.currentRow():
                return

            selected_block = code_blocks[list_widget.currentRow()]
            content = selected_block['code']
        else:
            content = code_blocks[0]['code']

        # 显示导出对话框
        dialog = ExportDialog(content, self)
        if dialog.exec_() == QDialog.Accepted:
            filename, ext = dialog.get_file_info()

            # 创建桌面文件夹
            desktop = os.path.join(os.path.expanduser("~"), "Desktop")
            ollama_folder = os.path.join(desktop, "Ollama_Exports")

            if not os.path.exists(ollama_folder):
                os.makedirs(ollama_folder)

            file_path = os.path.join(ollama_folder, filename)

            try:
                with open(file_path, 'w', encoding='utf-8') as f:
                    f.write(content)

                self.update_status(f"代码已导出到: {file_path}")
                QMessageBox.information(self, "成功", f"代码已导出到:\n{file_path}")

                # 询问是否打开文件所在文件夹
                reply = QMessageBox.question(self, "打开文件夹",
                                             "是否打开文件所在文件夹?",
                                             QMessageBox.Yes | QMessageBox.No)
                if reply == QMessageBox.Yes:
                    QDesktopServices.openUrl(QUrl.fromLocalFile(ollama_folder))

            except Exception as e:
                QMessageBox.critical(self, "错误", f"导出文件失败: {str(e)}")

    def save_chat(self):
        """保存聊天记录"""
        file_path, _ = QFileDialog.getSaveFileName(
            self, "保存聊天记录", f"chat_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt",
            "文本文件 (*.txt);;Markdown文件 (*.md);;所有文件 (*)"
        )

        if file_path:
            try:
                with open(file_path, 'w', encoding='utf-8') as file:
                    file.write(self.chat_display.toPlainText())
                self.update_status(f"聊天记录已保存到: {file_path}")
                QMessageBox.information(self, "成功", f"聊天记录已保存到:\n{file_path}")
            except Exception as e:
                QMessageBox.critical(self, "错误", f"保存文件失败: {str(e)}")

    def clear_chat(self):
        """清除聊天记录"""
        reply = QMessageBox.question(self, "确认清除",
                                     "确定要清除所有聊天记录吗?",
                                     QMessageBox.Yes | QMessageBox.No,
                                     QMessageBox.No)

        if reply == QMessageBox.Yes:
            self.chat_display.clear()
            self.current_response_content = ""
            self.update_status("聊天记录已清除")

    def get_system_info(self):
        """获取系统资源信息"""
        try:
            memory = psutil.virtual_memory()
            total_memory = memory.total / (1024 ** 3)  # GB
            available_memory = memory.available / (1024 ** 3)  # GB

            cpu_count = psutil.cpu_count()
            cpu_percent = psutil.cpu_percent()

            return f"CPU: {cpu_count}核 {cpu_percent}%使用 | 内存: {available_memory:.1f}GB可用/{total_memory:.1f}GB"
        except:
            return "无法获取系统信息"

    def check_ollama_connection(self):
        """检查Ollama连接"""
        self.ollama_host = self.host_input.text().strip()
        if not self.ollama_host:
            self.ollama_host = "http://localhost:11434"
            self.host_input.setText(self.ollama_host)

        self.update_status("正在测试连接...")
        try:
            url = f"{self.ollama_host}/api/tags"
            response = requests.get(url, timeout=10)
            if response.status_code == 200:
                self.update_status("连接成功!")
                self.check_local_models()
                QMessageBox.information(self, "成功", "成功连接到Ollama服务!")
            else:
                self.update_status(f"连接失败: HTTP {response.status_code}")
                QMessageBox.warning(self, "连接失败",
                                    f"无法连接到Ollama服务 (HTTP {response.status_code})\n\n"
                                    f"请确保:\n"
                                    f"1. Ollama已安装并在运行中\n"
                                    f"2. 服务地址正确\n"
                                    f"3. 端口11434可访问")
        except Exception as e:
            self.update_status(f"连接错误: {str(e)}")
            QMessageBox.critical(self, "连接错误",
                                 f"无法连接到Ollama服务: {str(e)}\n\n"
                                 f"请确保:\n"
                                 f"1. Ollama已安装并在运行中\n"
                                 f"2. 服务地址正确\n"
                                 f"3. 端口11434可访问")

    def check_local_models(self):
        """检查本地可用的模型"""
        try:
            self.update_status("正在检查本地模型...")
            url = f"{self.ollama_host}/api/tags"
            response = requests.get(url, timeout=10)

            if response.status_code == 200:
                data = response.json()
                self.available_models = [model['name'] for model in data.get('models', [])]

                self.model_list.clear()
                for model in self.available_models:
                    item = QListWidgetItem(model)
                    self.model_list.addItem(item)

                self.model_combo.clear()
                self.model_combo.addItems(self.available_models)

                self.update_status(f"找到 {len(self.available_models)} 个模型")
            else:
                self.update_status(f"获取模型失败: HTTP {response.status_code}")
                QMessageBox.warning(self, "错误", f"获取模型列表失败: HTTP {response.status_code}")

        except Exception as e:
            self.update_status(f"检查模型错误: {str(e)}")
            QMessageBox.critical(self, "错误", f"无法获取模型列表: {str(e)}")

    def on_model_selected(self, item):
        """当选择模型时"""
        self.current_model = item.text()
        self.current_model_label.setText(f"当前模型: {self.current_model}")
        self.update_status(f"已选择模型: {self.current_model}")

    def delete_selected_model(self):
        """删除选中的模型"""
        current_item = self.model_list.currentItem()
        if not current_item:
            QMessageBox.warning(self, "警告", "请先选择一个模型")
            return

        model_name = current_item.text()
        reply = QMessageBox.question(self, "确认删除",
                                     f"确定要删除模型 '{model_name}' 吗?\n\n此操作不可撤销!",
                                     QMessageBox.Yes | QMessageBox.No,
                                     QMessageBox.No)

        if reply == QMessageBox.Yes:
            try:
                url = f"{self.ollama_host}/api/delete"
                payload = {"name": model_name}
                response = requests.delete(url, json=payload, timeout=30)

                if response.status_code == 200:
                    self.update_status(f"已删除模型: {model_name}")
                    QMessageBox.information(self, "成功", f"模型 {model_name} 已删除")
                    self.check_local_models()  # 刷新列表
                else:
                    QMessageBox.critical(self, "错误", f"删除模型失败: HTTP {response.status_code}")
            except Exception as e:
                QMessageBox.critical(self, "错误", f"删除模型失败: {str(e)}")

    def load_model(self):
        """加载选中的模型"""
        model_name = self.model_combo.currentText()
        if not model_name:
            QMessageBox.warning(self, "警告", "请先选择一个模型")
            return

        self.update_status(f"正在加载模型: {model_name}")
        self.progress_bar.setVisible(True)
        self.progress_bar.setValue(0)
        self.load_btn.setEnabled(False)

        # 启动加载线程
        self.loader_thread = ModelLoaderThread(model_name, self.ollama_host)
        self.loader_thread.progress_signal.connect(self.progress_bar.setValue)
        self.loader_thread.finished_signal.connect(self.on_model_loaded)
        self.loader_thread.error_signal.connect(self.on_model_load_error)
        self.loader_thread.start()

    def on_model_loaded(self, message):
        """模型加载完成"""
        self.progress_bar.setVisible(False)
        self.load_btn.setEnabled(True)
        self.update_status(message)
        self.current_model = self.model_combo.currentText()
        self.current_model_label.setText(f"当前模型: {self.current_model}")
        QMessageBox.information(self, "成功", message)
        self.check_local_models()  # 刷新模型列表

    def on_model_load_error(self, error_message):
        """模型加载错误"""
        self.progress_bar.setVisible(False)
        self.load_btn.setEnabled(True)
        self.update_status(error_message)
        QMessageBox.critical(self, "错误", error_message)

    def attach_file(self):
        """附加文件"""
        file_path, _ = QFileDialog.getOpenFileName(
            self, "选择文件", "",
            "文本文件 (*.txt *.py *.js *.java *.c *.cpp *.h *.html *.css *.json *.xml);;所有文件 (*)"
        )

        if file_path:
            self.attached_file_path = file_path
            filename = os.path.basename(file_path)
            self.attach_label.setText(f"已选择: {filename}")

            # 读取文件内容
            try:
                with open(file_path, 'r', encoding='utf-8') as file:
                    self.attached_file_content = file.read()
                self.update_status(f"已附加文件: {filename}")
            except UnicodeDecodeError:
                try:
                    # 尝试其他编码
                    with open(file_path, 'r', encoding='gbk') as file:
                        self.attached_file_content = file.read()
                    self.update_status(f"已附加文件: {filename} (GBK编码)")
                except:
                    # 如果是二进制文件,只记录路径
                    self.attached_file_content = f"二进制文件: {file_path}"
                    self.update_status(f"已附加二进制文件: {filename}")
            except Exception as e:
                QMessageBox.critical(self, "错误", f"读取文件失败: {str(e)}")
                self.attached_file_content = None

    def send_message(self):
        """发送消息"""
        message = self.input_field.text().strip()
        if not message:
            return

        if not self.current_model:
            QMessageBox.warning(self, "警告", "请先选择一个模型")
            return

        # 显示用户消息
        self.display_message("用户", message)
        self.input_field.clear()

        # 准备消息历史
        messages = [{"role": "user", "content": message}]

        # 启动聊天线程
        self.chat_thread = ChatThread(
            self.current_model,
            messages,
            self.ollama_host,
            self.attached_file_content,
            self.allow_network_access
        )
        self.chat_thread.response_signal.connect(self.display_model_response)
        self.chat_thread.error_signal.connect(self.on_chat_error)
        self.chat_thread.stop_signal.connect(self.on_generation_stopped)
        self.chat_thread.start()

        # 更新UI状态
        self.is_generating = True
        self.send_btn.setEnabled(False)
        self.stop_btn.setEnabled(True)
        self.update_status(f"正在与 {self.current_model} 交互...")

    def stop_generation(self):
        """停止生成响应"""
        if self.is_generating and self.chat_thread:
            self.chat_thread.stop()
            self.update_status("正在停止生成...")

    def on_generation_stopped(self):
        """生成已停止"""
        self.is_generating = False
        self.send_btn.setEnabled(True)
        self.stop_btn.setEnabled(False)
        self.update_status("生成已停止")

    def display_message(self, sender, message):
        """显示消息"""
        timestamp = datetime.now().strftime("%H:%M:%S")

        if sender == "用户":
            # 用户消息样式
            html = f"""
            <div style='margin: 10px 0;'>
                <div style='color: #2E86AB; font-weight: bold; font-size: 12px;'>
                    [{timestamp}] {sender}
                </div>
                <div style='background-color: #E3F2FD; padding: 12px; border-radius: 10px; 
                          border: 2px solid #BBDEFB; margin: 5px 0; font-size: {self.font_size}px;'>
                    {message.replace('\n', '<br>')}
                </div>
            </div>
            """
        else:
            # 模型消息样式
            html = f"""
            <div style='margin: 10px 0;'>
                <div style='color: #C73E1D; font-weight: bold; font-size: 12px;'>
                    [{timestamp}] {sender}
                </div>
                <div style='background-color: #FFEBEE; padding: 12px; border-radius: 10px; 
                          border: 2px solid #FFCDD2; margin: 5px 0; font-size: {self.font_size}px;'>
                    {message.replace('\n', '<br>')}
                </div>
            </div>
            """

        # 保存当前光标位置
        cursor = self.chat_display.textCursor()
        cursor.movePosition(QTextCursor.End)

        # 插入HTML
        self.chat_display.textCursor().insertHtml(html)

        # 滚动到底部
        self.chat_display.moveCursor(QTextCursor.End)

    def display_model_response(self, content, is_end):
        """显示模型响应"""
        if is_end:
            self.update_status("响应完成")
            self.is_generating = False
            self.send_btn.setEnabled(True)
            self.stop_btn.setEnabled(False)

            # 添加复制和导出按钮
            self.add_action_buttons()
            return

        # 获取当前光标
        cursor = self.chat_display.textCursor()
        cursor.movePosition(QTextCursor.End)

        # 如果是响应开始,添加模型消息头部
        if not hasattr(self, 'response_started'):
            self.response_started = True
            self.current_response_content = ""  # 重置当前响应内容
            timestamp = datetime.now().strftime("%H:%M:%S")
            html = f"""
            <div style='margin: 10px 0;'>
                <div style='color: #C73E1D; font-weight: bold; font-size: 12px;'>
                    [{timestamp}] 模型
                </div>
                <div style='background-color: #FFEBEE; padding: 12px; border-radius: 10px; 
                          border: 2px solid #FFCDD2; margin: 5px 0; font-size: {self.font_size}px;'>
            """
            cursor.insertHtml(html)

        # 插入内容
        cursor.insertText(content)
        self.current_response_content += content  # 保存响应内容

        # 滚动到底部
        self.chat_display.moveCursor(QTextCursor.End)

    def add_action_buttons(self):
        """在消息末尾添加操作按钮"""
        cursor = self.chat_display.textCursor()
        cursor.movePosition(QTextCursor.End)

        # 添加操作按钮的HTML
        buttons_html = f"""
        <div style='margin: 15px 0; padding: 10px; background-color: #F8F9FA; border-radius: 8px; border: 1px solid #DEE2E6;'>
            <div style='font-weight: bold; margin-bottom: 8px; color: #6C757D;'>代码操作:</div>
            <button style='background-color: #2E86AB; color: white; border: none; 
                     padding: 8px 15px; border-radius: 4px; cursor: pointer; font-size: 12px; margin-right: 8px;'
                     onclick='copyAllCode()'>
                复制所有代码
            </button>
            <button style='background-color: #C73E1D; color: white; border: none; 
                     padding: 8px 15px; border-radius: 4px; cursor: pointer; font-size: 12px;'
                     onclick='exportCode()'>
                导出为文件
            </button>
        </div>
        </div>
        """

        cursor.insertHtml(buttons_html)

        # 滚动到底部
        self.chat_display.moveCursor(QTextCursor.End)

        # 清除响应开始标志
        if hasattr(self, 'response_started'):
            del self.response_started

    def on_chat_error(self, error_message):
        """聊天错误处理"""
        if hasattr(self, 'response_started'):
            del self.response_started

        self.is_generating = False
        self.send_btn.setEnabled(True)
        self.stop_btn.setEnabled(False)

        self.update_status(error_message)

        # 检查网络错误
        if "网络" in error_message or "连接" in error_message:
            QMessageBox.warning(self, "网络错误",
                                f"{error_message}\n\n"
                                "请检查:\n"
                                "1. 网络连接是否正常\n"
                                "2. 代理设置是否正确\n"
                                "3. 防火墙是否阻止了连接")
        else:
            QMessageBox.critical(self, "错误", error_message)

    def update_status(self, message):
        """更新状态栏"""
        self.status_bar.showMessage(f"状态: {message}")


def main():
    app = QApplication(sys.argv)

    # 设置应用程序字体
    font = QFont("Microsoft YaHei", 10)
    app.setFont(font)

    # 设置应用程序样式
    app.setStyle("Fusion")

    window = OllamaGUI()
    window.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()

代码需要的依赖库

# PyQt5 - GUI框架
pip install PyQt5

# requests - HTTP请求库
pip install requests

# psutil - 系统资源监控
pip install psutil

可选依赖库

# 如果您需要打印功能(虽然代码中有QPrintDialog导入)
pip install PyQt5-sip  # 通常已包含在PyQt5中

# 如果您想使用其他Qt样式
pip install qdarkstyle  # 暗色主题

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐