智能Chat!By:PySide6

目录

前言

一、展示

软件主界面展示

基础对话展示

基于文档对话展示

AI绘画展示(文生图)

二、项目细节(技术分享)

布局及其结构

自定义零部件(开箱即用)

 数据存储

总结


前言

大家好呀,最近整了一款基于pyside6+qfluentwidgets框架进行开发的软件

该软件支持:

  1. 上下文对话
  2. 基于文档对话
  3. AI绘画

本篇全是干货满满,望大家耐心阅读。

软件源码获取链接智能AI对话源码


一、展示

  • 软件主界面展示

该界面采用比较淳朴简约的风格规划而成,对话记录以不同分栏进行分块保存,可自由切换模式(对话模式/绘画模式),同时可以自由管理对话比如删除某段对话记录或者全部删除 ,同时支持会话导出

  • 基础对话展示

(1)背景介绍

对话模式内置大模型的现在使用的是通义千问qwen1.5-110b-chat大模型

模型特性:Qwen1.5-110B与其他Qwen1.5模型相似,采用了相同的Transformer解码器架构。它包含了分组查询注意力(GQA),在模型推理时更加高效。该模型支持32K tokens的上下文长度,同时它仍然是多语言的,支持英、中、法、西、德、俄、日、韩、越、阿等多种语言。

(2)对话功能亮点介绍

  1. 支持自由暂停对话功能,使其对话更加灵活。
  2. 支持流式输出,让用户使用起来更加丝滑增加用户的使用体验。
  3. 支持快捷键一键发送,省去用户点击的繁琐操作同时增加用户体验感。
  4. 支持对输出文档进行美观处理,分点清晰。
  5. 支持上下文联动对话。
  • 基于文档对话展示

①拖拽后及时检测

 ②页面加载过渡页

③文档可选操作 

 ④效果演示

 (1)文档对话亮点介绍

  1. 支持拖拽式上传文档,软件可自动识别有效的文件,同时也增加了用户的使用的便捷性。
  2. 使用过渡页动画效果增加用户体验感。
  3. 可支持自由筛选加载后的文档进行对话,使其更加灵活
  • AI绘画展示(文生图)

①绘画效果展示

②过渡动画(该动画效果为动态图片不太好演示)

(1)背景介绍

该ai绘画对接的是国内midjourney镜像接口生成的图片细节拉满,绘画效果比大多数国内已有的模型要好

(2)AI绘画亮点介绍

  1. 动态动画过渡增加用户体验感
  2. 支持图片的放大功能
  3. 图片双击即可查看效果

二、项目细节(技术分享)

  • 布局及其结构

布局:该软件采用先采用QT设计师+自己定义的零部件功能组件,纯手搓位置不太好找准,先用软件把整体的布局框架搭好再放入自己的零部件

项目结构:


  • 自定义零部件(开箱即用)

问题输入框组件(继承QPlainTextEdit)

功能:

  1. 可以根据用户输入的内容自动调整高度
  2. 可增加输入框的限高操作比如输入到一定高度就高度不变
  3. 快捷键检测
  4. 拖拽文件检测
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QDragEnterEvent, QDropEvent
from PySide6.QtWidgets import QPlainTextEdit
"""
model_type :是否进行限高
pre_height :输入框初始高度
"""

class MyTextEdit(QPlainTextEdit):
    enterPressed = Signal()
    File_input = Signal(list)

    def __init__(self, model_type,pre_height, parent=None):
        super(MyTextEdit, self).__init__(parent)
        self.now_lines = 1
        self.per_lines = 1
        self.height = pre_height  # 记录框中的高度随时改变初始值为pre_height
        self.pre_height = pre_height  # 初始高度不变
        self.block = 19
        self.model_type = model_type
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)  # Disable the scrollbar

        # Connect the contentsChanged signal to the adjustHeight method
        self.document().contentsChanged.connect(self.adjustHeight)

    def adjustHeight(self):
        #得出现在的行数
        self.now_lines= self.document().documentLayout().documentSize().height()
        # print(self.now_lines)

        if self.now_lines == 1:
            self.per_lines = self.now_lines
            self.height = self.pre_height
            self.blockSignals(True)
            self.setFixedHeight(self.height)
            self.blockSignals(False)

        # 文本增加的时候
        if self.now_lines > self.per_lines:
            # 加上额外的高度以适应边距和行高
            new_height = int(self.height + self.block*(self.now_lines-self.per_lines))
            # print(new_height)
            if new_height>=(self.pre_height+7*self.block) and self.model_type==0:
                # print(new_height)
                self.per_lines = 8
                self.height = self.pre_height+7*self.block
                self.blockSignals(True)
                self.setFixedHeight(self.height)
                self.blockSignals(False)
                return
            self.per_lines = self.now_lines
            self.height = new_height
            self.blockSignals(True)
            self.setFixedHeight(new_height)
            self.blockSignals(False)

        # 文本减少的时候
        elif self.now_lines < self.per_lines:
            # 加上额外的高度以适应边距和行高
            new_height = int(self.height -self.block*(self.per_lines-self.now_lines))
            self.per_lines = self.now_lines
            self.height = new_height
            self.blockSignals(True)
            self.setFixedHeight(new_height)
            self.blockSignals(False)

    def resizeEvent(self, event):
        self.adjustHeight()
        super(MyTextEdit, self).resizeEvent(event)

    def keyPressEvent(self, event):
        # 检查是否按下了 Enter 键
        if event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter:
            # 检查 Shift 键是否被按下
            if event.modifiers() == Qt.ShiftModifier:
                # Shift+Enter 被按下,插入换行
                self.insertPlainText("\n")
            else:
                # 仅 Enter 被按下,发出信号
                self.enterPressed.emit()
        else:
            super(MyTextEdit, self).keyPressEvent(event)

    def dragEnterEvent(self, event: QDragEnterEvent):
        # 判断拖拽的类型是否是文件(包含图片)
        if event.mimeData().hasUrls():
            event.acceptProposedAction()
        else:
            event.ignore()

    def dropEvent(self, event: QDropEvent):
        # 获取拖拽的数据
        if event.mimeData().hasUrls():
            word_list = []
            for url in event.mimeData().urls():
                # 将文件路径转换为本地路径
                file_path = url.toLocalFile()
                if '.docx' in file_path:
                    word_list.append(file_path)

            event.acceptProposedAction()
            self.File_input.emit(word_list)
        else:
            event.ignore()

对话显示框显示组件 (继承QTextEdit)

功能:

  1. 自由可支持富文本/markdown渲染或加载图片等等
from PySide6.QtCore import Qt, QTimer
from PySide6.QtWidgets import QTextEdit
from PySide6.QtGui import QTextCursor

class viewEdit(QTextEdit):
    def __init__(self, pre_height, parent=None):
        super().__init__(parent)
        self.now_lines = 1
        self.per_lines = 1
        self.height = pre_height  # 当前高度
        self.pre_height = pre_height  # 初始高度
        self.block = 16
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)  # 禁用滚动条

        # 连接contentsChanged信号到adjustHeight方法
        self.document().contentsChanged.connect(self.adjustHeight)

        # 初始化一个定时器,用于延时调整高度
        self.height_adjust_timer = QTimer(self)
        self.height_adjust_timer.setSingleShot(True)
        self.height_adjust_timer.timeout.connect(self.adjustHeight)

    def adjustHeight(self):
        self.now_lines = self.get_line_count()
        if self.now_lines == 1:
            self.per_lines = self.now_lines
            self.height = self.pre_height
            self.blockSignals(True)
            self.setFixedHeight(self.height)
            self.blockSignals(False)

        elif self.now_lines > self.per_lines:
            new_height = int(self.height + self.block * (self.now_lines - self.per_lines))
            self.per_lines = self.now_lines
            self.height = new_height
            self.blockSignals(True)
            self.setFixedHeight(new_height)
            self.blockSignals(False)

        elif self.now_lines < self.per_lines:
            new_height = int(self.height - self.block * (self.per_lines - self.now_lines))
            self.per_lines = self.now_lines
            self.height = new_height
            self.blockSignals(True)
            self.setFixedHeight(new_height)
            self.blockSignals(False)

    def resizeEvent(self, event):
        self.adjustHeight()
        super(viewEdit, self).resizeEvent(event)

    def insertPlainText(self, text):
        # 步骤1: 插入文本前的逻辑(例如验证、修改)
        if not text:
            return  # 没有文本要插入,不做任何操作

        # 步骤2: 调用父类的insertPlainText方法插入文本
        super(viewEdit, self).insertPlainText(text)

        # 启动定时器来延时调整高度
        self.height_adjust_timer.start(50)  # 调整时间为50毫秒后

    def setMarkdown(self, text):
        # 步骤1: 插入文本前的逻辑(例如验证、修改)
        if not text:
            return  # 没有文本要插入,不做任何操作

        # 步骤2: 调用父类的insertPlainText方法插入文本
        super(viewEdit, self).setMarkdown(text)

        # 启动定时器来延时调整高度
        self.height_adjust_timer.start(10)  # 调整时间为10毫秒后


    def get_line_count(self):
        cursor = QTextCursor(self.document())
        cursor.movePosition(QTextCursor.Start)
        line_count = 0
        while True:
            line_count += 1
            current_position = cursor.position()
            cursor.movePosition(QTextCursor.Down)
            if cursor.position() == current_position:
                break
        return line_count

# if __name__ == "__main__":
#     app = QApplication(sys.argv)
#     ex = viewEdit(pre_height = 57)
#     ex.show()
#     sys.exit(app.exec_())

拖拽提示框(小复杂)

步骤

  1. 基本框组件构造
  2. 每一行文件部件构造
  3. 滚动区域拖拽事件触发

效果:

功能:

  1. 可观的显示文件的大小及其名称
  2. 如果文档数量一定超出内置滚动条

1)主体框架

class fileMessageBox(MessageBoxBase):
    """ Custom message box """
    def __init__(self, parent=None):
        super().__init__(parent)
        self.titleLabel = SubtitleLabel("筛选出的有效资料库")
        self.scrollArea = QScrollArea()
        self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)  # Disable the scrollbar
        self.scrollArea.setWidgetResizable(True)
        # 创建容器widget并设置布局
        self.container_widget = QWidget()
        self.container_layout = QVBoxLayout(self.container_widget)
        self.container_layout.setAlignment(Qt.AlignTop)  # 确保从顶部开始添加
        self.scrollArea.setWidget(self.container_widget)
        # # 将组件添加到布局中
        self.viewLayout.addWidget(self.titleLabel)
        self.viewLayout.addWidget(self.scrollArea)
        # 设置对话框的最小宽度
        self.widget.setMinimumWidth(320)
        self.widget.setMinimumHeight(120)

2)单行文件组件构成代码(图标+文件名+文件大小)

from PySide6.QtCore import Qt
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QLabel, QSpacerItem, QSizePolicy

class FileLayout(QWidget):
    def __init__(self,message_type):
        super().__init__()
        self.name = None
        self.size = None
        self.message_type = message_type
        self.initUI()

    def initUI(self):
        # 主水平布局
        layout = QHBoxLayout(self)
        # 左侧头像部分
        box1 = QVBoxLayout()
        icon = QLabel()
        if self.message_type == 'file':
            pixmap = QPixmap(":/images/file.png").scaled(35, 35, Qt.KeepAspectRatio, Qt.SmoothTransformation)
        else:
            pixmap = QPixmap(":/images/photo.png").scaled(35, 35, Qt.KeepAspectRatio, Qt.SmoothTransformation)
        icon.setPixmap(pixmap)
        box1.addWidget(icon)
        box1.addStretch()

        # 左侧消息+时间部分
        box2 = QVBoxLayout()
        # 创建一个水平布局,用于放置label
        hbox = QHBoxLayout()
        # 创建一个水平弹簧,将label推到最右边 时间+弹簧布局
        spacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        if self.message_type == 'file':
            self.name = QLabel("文件")
        else:
            self.name = QLabel("图片")
        hbox.addWidget(self.name)
        hbox.addItem(spacer)
        hbox.setContentsMargins(0, 0, 0, 0)  # 去掉间距

        # 创建一个水平布局,用于放置message
        hbox2 = QHBoxLayout()
        # 创建一个水平弹簧,将label推到最右边 时间+弹簧布局
        spacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        self.size = QLabel("100kb")
        hbox2.addWidget(self.size)
        hbox2.addItem(spacer)
        hbox2.setContentsMargins(0, 0, 0, 0)  # 去掉间距

        # 将上下添加到第二部分布局
        box2.addLayout(hbox)
        box2.addLayout(hbox2)

        # 将子布局添加到主布局中
        layout.addLayout(box1)  # 将头像部分放在左侧
        layout.addLayout(box2)  # 将消息部分放在右侧

        # 设置QSizePolicy以避免被压缩
        self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)

3)滚动区域重写

from PySide6 import QtWidgets,QtGui
from PySide6.QtCore import Signal


class CustomScrollArea(QtWidgets.QScrollArea):
    word_input = Signal(list)

    def __init__(self, parent=None):
        super(CustomScrollArea, self).__init__(parent)
        # 设置为接受拖放事件
        self.setAcceptDrops(True)

    def dragEnterEvent(self, event: QtGui.QDragEnterEvent):
        # 判断拖拽的类型是否是文件(包含图片)
        if event.mimeData().hasUrls():
            event.acceptProposedAction()
        else:
            event.ignore()

    def dropEvent(self, event: QtGui.QDropEvent):
        # 获取拖拽的数据
        if event.mimeData().hasUrls():
            word_list = []
            for url in event.mimeData().urls():
                # 将文件路径转换为本地路径
                file_path = url.toLocalFile()
                # 打印出文件路径
                # print(f"拖拽的文件路径: {file_path}")
                if '.docx' in file_path:
                    word_list.append(file_path)

            event.acceptProposedAction()
            self.word_input.emit(word_list)
        else:
            event.ignore()

4)具体使用方法(给滚动区域绑定拖入的信号然后动态加载零部件放到滚动区域里面) 

# 滚动区域设置文件拖入的信号绑定事件
self.scrollArea.word_input.connect(lambda message: self.analyze_word(message))

w = fileMessageBox(self)
if len(message)>0:
    for i in range(len(message)):
        file = FileLayout(message_type="file")
        name = message[i].split("/")[-1]
        file.name.setText(str(name))
        size = os.path.getsize(message[i])
        file_size_kb = round(size / 1024.0, 2)  # 转换为KB并且保留两位小数
        file.size.setText(f"{str(file_size_kb)} KB")
        w.container_layout.addWidget(file)
    w.yesButton.setText('😎 加载')
    w.cancelButton.setText('🤔 下次一定')
    w.titleLabel.setText(f"筛选出{len(message)}个有效资料库")
    w.show()
    if w.exec():
        pass

  •  数据存储

可视化预览

[
    [
        {
          "type": 1,
          "time": "2024-08-20 17:27:45",
          "user": "Epic beauty, cinematic light contrast, Oriental mythological style, ultra HD wallpaper, super detail, masterpieces,BreakIn the quiet (Moon Palace:1.2),(Chang 'e: 1.5) , (wearing neon clothes: 1.5), fairy temperament, her skin (ice muscle jade skin: 1.4), her eyes revealed a hint (sadness and pain: 1.4), (her hand gently caress the windowsill), as if thinking about their past and future,Breaka candle shines on (mica screen: 1.4), forming (deep shadow: 1.2). The mica on the screen is delicate and transparent (candlelight casts a soft and warm light and shadow through the texture of the mica: 1.3). The whole room is surrounded by this dim light, creating a quiet and mysterious atmosphere,epic beauty(Outside the window: 1.4), (long river) flowing slowly, faint ripples on the water surface. As the night faded, so did the stars in the sky. In the dawn's light, the stars sank into the long river, as if a sleeping gem sank to the bottom of the water,",
          "image_path": "Datas\\painting_images\\1724146125.png",
          "jobId": "ec9cafb1-e3ba-4f6d-bff7-e0b2b7084ed6"
        },
        {
            "type": 2,
            "time": "2024-08-20 17:29:38",
            "image_path": "Datas\\painting_images\\1724146184.png"
        }
    ],
    [
        {
            "type": 0,
            "time": "2024-08-20 19:09:09",
            "user": "你好啊",
            "system": "你好!有什么可以帮助你的吗?"
        }
    ]
]

因为ai对话要支持会话保存功能那数据存储这块就比较讲究了

  1. type显示模式(0->正常问答显示,1->创建绘画后的显示,2->经过放大后的显示)方便后续加载对话的记录

正常问答显示

创建绘画后的显示

 经过放大后的显示


总结

希望本次分享可以对大家有所帮助

如果觉得本篇还不错的话,给我点个赞呗

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值