Python 搭建 AI API 对话机器人 UI 程序完整指南
使用 Python 和 Tkinter 框架搭建 AI 对话机器人 UI 程序的完整指南。涵盖环境准备、依赖安装、应用架构、Markdown 处理、UI 布局及通信机制。支持流式响应、代码提取、对话导出等功能。提供模型配置、参数优化及故障排查方案。包含完整可运行代码,适合开发者构建本地 AI 交互工具。

使用 Python 和 Tkinter 框架搭建 AI 对话机器人 UI 程序的完整指南。涵盖环境准备、依赖安装、应用架构、Markdown 处理、UI 布局及通信机制。支持流式响应、代码提取、对话导出等功能。提供模型配置、参数优化及故障排查方案。包含完整可运行代码,适合开发者构建本地 AI 交互工具。

在人工智能大模型时代,越来越多的开发者和企业需要集成先进的 AI 模型到自己的应用中。然而,直接调用各个模型的官方 API 存在诸多痛点:需要分别注册多个平台账户、维护不同的 API 密钥、处理各异的接口规范、承担高昂的计费成本。为了解决这些问题,API 聚合平台应运而生。本项目基于高效的 OpenAI 接口聚合管理平台,融合了丰富的免费和付费模型资源,为用户提供了一套完整的、开箱即用的 AI 对话机器人解决方案。这套解决方案采用 Python 与 Tkinter 框架开发,打造了一个功能强大、界面友好的桌面应用程序,使得普通用户无需复杂的技术背景,就能轻松体验各种最先进的 AI 模型。
这个 AI 对话机器人应用集成了众多实用功能,使其成为一个专业级的 AI 交互工具。首先,应用支持实时流式响应,用户输入的问题能够以流式形式返回 AI 的回答,提供了更加自然流畅的交互体验。其次,应用内置了完整的 Markdown 格式支持,可以正确渲染标题、加粗、斜体、代码块等各种排版元素,让 AI 的回答更加易读。第三,应用特别设计了代码提取功能,当 AI 在回答中包含代码段时,系统会自动识别并提取这些代码块到独立的选项卡中,方便用户查看和复制。此外,应用还提供了丰富的参数调整选项,包括温度、Top P 等采样参数,允许用户根据不同场景灵活调整模型的生成行为。应用还支持完整的对话导出功能,可以将整个对话历史保存为文本文件以供日后查阅,同时支持从 JSON 文件导入设置,方便用户备份和恢复自己的配置。
为了成功运行这个 AI 对话机器人应用,用户需要准备一个合适的开发环境。应用要求 Python 3.7 或以上版本,因为代码中使用了 f-string 等现代 Python 特性。在操作系统方面,由于应用使用了 Tkinter 作为 GUI 框架,理论上可以运行在 Windows、macOS 和 Linux 等多个平台上,但在实际使用中,Windows 和 macOS 用户体验通常最佳。建议用户在安装 Python 时,确保勾选了 Add Python to PATH 选项,这样可以在命令行中直接调用 Python。
项目的核心依赖包括 requests 库用于发送 HTTP 请求、json 库用于处理 JSON 数据、threading 库用于多线程操作。这些库中,requests 是唯一需要额外安装的第三方库,而 json 和 threading 已经包含在 Python 标准库中。用户可以通过以下命令安装所有必需的库:
pip install requests
对于需要使用最新版本或特定版本的 requests 库的用户,可以使用以下命令:
pip install --upgrade requests
如果用户所在的网络环境访问 PyPI 速度较慢,可以考虑使用国内镜像源,例如阿里云镜像或清华大学镜像。一个完整的带有镜像源的安装命令示例为:
pip install -i https://mirrors.aliyun.com/pypi/simple/ requests
在开始使用本应用之前,用户必须在 API 平台上注册账户并获取 API 密钥。注册过程非常直观:用户需要访问平台官网,按照页面提示填写基本信息如邮箱地址和密码,完成邮箱验证后即可注册成功。
注册完成后,用户进入令牌管理页面,根据实际需求添加令牌,将看到自己生成的 API 密钥。这个密钥是访问所有 AI 模型的凭证,用户需要妥善保管并在应用启动时输入。为了安全起见,不建议在代码中硬编码 API 密钥,而应该通过环境变量或配置文件的方式存储。本应用设计了一个自动保存机制,用户首次输入 API 密钥后,点击保存按钮,系统会将密钥保存到本地文件中,下次启动应用时会自动加载,无需重复输入。
如使用免费的,则使用免费额度;如果使用高级点的模型,也可充值后使用付费分组。
这个应用的设计采用了面向对象编程范式,由两个主要类组成:MarkdownFormatter 和 ChatBotUI。MarkdownFormatter 类专门负责处理 Markdown 格式的文本转换和渲染,它包含了两个重要的静态方法,一个是 extract_code_blocks 方法,用于从 AI 返回的文本中识别并提取代码块,另一个是 format_text 方法,用于将 Markdown 格式的文本正确地插入到 Tkinter 的 Text widget 中,并应用相应的标签样式。ChatBotUI 类是整个应用的主类,负责创建用户界面、处理用户交互、管理对话历史、与 API 通信等核心业务逻辑。这种分离的设计使得代码结构清晰,各个模块职责明确,易于维护和扩展。
Markdown 处理引擎是这个应用的一个独特之处,它能够正确地识别和渲染各种 Markdown 格式元素。extract_code_blocks 方法使用了正则表达式来识别代码块,具体使用的模式是 r'(?:python|py)?\s*\n(.*?)\n',这个模式能够匹配三个反引号包围的代码块,并且支持可选的 python 语言标识符。该方法的核心逻辑是遍历所有匹配到的代码块,逐个提取其内容,同时删除原文本中的代码块标记,返回处理后的文本和代码块列表。这样做的好处是,AI 返回的代码会被自动分离出来,可以在独立的选项卡中以高亮的代码编辑器样式显示,提升了代码的可读性。
format_text 方法的工作原理更加复杂,它逐行处理文本,首先检查每一行是否是标题(以#开头),如果是标题则根据#的数量确定标题级别(1 到 3 级),应用相应的标签样式。对于非标题行,方法会逐个字符地处理,在每个位置使用正则表达式同时检查多种格式标记:文本(加粗)、文本(斜体)、文本(代码)和 链接文本(链接)。当发现格式标记时,取出最先出现的那个,应用相应的 tag 标签,然后继续处理剩余的文本。这种逐个处理的方式虽然计算量较大,但保证了格式的正确性,避免了格式标记之间的冲突。
应用的用户界面采用了左右分栏的经典布局设计。左侧是一个统一的参数配置面板,使用了 LabelFrame 容器来组织各个参数控制元素。这个面板包含了 API 密钥管理、模型选择、系统提示词、生成参数调整等各个功能区域。API 密钥部分采用了一个 Frame 容器,在其中水平排列了一个 Entry 输入框、一个保存按钮和一个注册按钮,使得用户可以方便地输入密钥或快速跳转到注册页面。系统提示词使用了 ScrolledText 组件,这样即使提示词内容过长也能正常显示。参数调整部分(最大输出长度、温度、Top P)采用了 Spinbox 和 Scale 等专门的组件,提供了图形化的参数调整体验。
右侧是主要的聊天和代码展示区域,采用了 Notebook(选项卡)组件来实现多选项卡的界面。默认情况下,应用启动时会创建一个聊天选项卡,显示聊天内容。当 AI 返回的内容中包含代码块时,应用会动态地添加新的代码选项卡,每个代码块对应一个独立的选项卡。这样的设计使得用户可以轻松地在聊天内容和代码之间切换查看,而无需在冗长的对话历史中来回翻滚。
应用与 API 的通信采用了一个精心设计的流程。当用户点击发送按钮或按下 Ctrl+Enter 快捷键后,应用首先会验证 API 密钥是否已输入,然后检查用户是否正在等待上一条消息的回复。通过验证后,用户的消息会被添加到对话历史中,UI 中会显示用户消息,输入框会被清空。接着,应用会设置一个标志位 is_loading 为 True,禁用发送按钮,更新状态标签为正在处理...,并在后台开启一个新的工作线程来处理 API 请求,避免阻塞主 UI 线程。
在工作线程中,应用构造了一个遵循 OpenAI API 格式的请求 payload,包含了用户选择的模型、完整的消息历史(包括系统提示词)、各种采样参数。该 payload 被发送到 v1/chat/completions 端点。应用使用了 stream=True 参数来启用流式响应,这意味着 API 会立即开始返回结果,而不是等待完整的响应生成。应用通过 response.iter_lines() 方法逐行迭代处理返回的数据,每一行都是一个符合 Server-Sent Events 格式的 JSON 对象。对于包含 reasoning_content 字段的数据块,应用将其识别为模型的思考过程(某些模型如 DeepSeek 推理模型会返回此字段),并以特殊的 thinking 样式显示。对于包含 content 字段的数据块,应用将其作为最终回答,以 assistant 样式显示。
API 平台为用户提供了多个免费使用的 AI 模型,这些模型涵盖了多个领域和能力水平。目前应用代码中预配置的免费模型包括以下几个:首先是 GPT 系列模型中的 gpt-4.1-nano 和 gpt-5-nano 这两个轻量级版本,这些模型虽然参数量较小,但在基础的文本生成和问题回答方面表现可靠,适合那些对响应速度有要求或输入量较小的场景。其次是 DeepSeek 系列的 deepseek-r1-0528 和 deepseek-v3 两个模型,其中 r1 模型是推理专向模型,特别擅长数学、编程等需要逻辑推理的任务,会返回详细的思考过程;deepseek-v3 是多用途模型,在知识问答、文本生成等方面表现均衡。此外还有 deepseek-v3.1,这是 v3 的一个改进版本。最后是 gemini-2.5-flash-lite,这是 Google 最新推出的轻量化 Gemini 模型,在多模态理解和快速响应方面表现优异。这些免费模型的可用额度通常有限制,新用户一般会获得初始的免费试用额度,具体的额度和有效期需要用户在平台上查看。
使用本应用的基础流程非常简洁。首先,用户启动应用后,在左侧参数面板的 API 密钥输入框中粘贴自己的 API 密钥,然后点击保存按钮,系统会将密钥保存到本地。其次,用户在选择模型下拉框中选择想要使用的模型,比如选择 gpt-4.1-nano 进行快速体验。如果需要自定义 AI 的行为风格,可以在系统提示词的大文本框中修改系统提示,比如可以将默认的中文助手改为特定行业的专家角色。然后用户在右侧的输入框中输入想要问的问题,点击发送按钮或按下 Ctrl+Enter 快捷键,应用就会向 API 发送请求。等待几秒钟后(具体时间取决于模型复杂度和网络延迟),AI 的回答就会以流式的形式出现在上方的聊天展示区,用户可以实时看到回答的逐字生成过程。如果 AI 的回答中包含代码块,应用会自动创建新的代码选项卡,用户可以点击选项卡查看代码,右键选择复制代码将代码复制到剪贴板。
在实际使用中,为了获得更好的效果,用户应该注意以下几个最佳实践:首先,在提问时要尽可能清晰具体,提供充足的上下文信息,这样 AI 能够更准确地理解需求。其次,对于需要长篇幅回答或深度思考的问题,应该选择较高的 max_tokens 值(比如 8192 或更高),同时可以适当提高温度参数以获得更创意的回答。第三,对于对答案准确性有严格要求的场景(比如代码生成、数学计算),应该选择推理能力强的模型(如 deepseek-r1)或设置较低的温度值(0.3-0.5)以获得更一致的回答。第四,如果需要维持一个连贯的多轮对话,应该避免频繁地清空对话历史,这样 AI 可以基于之前的上下文进行更一致的回答。
虽然 API 平台提供了多个免费模型供初级用户体验,但对于需要更强大能力的用户,平台还提供了多个付费模型选项。付费模型通常分为几个层级:基础层模型包括像 gpt-5-mini 这样的快速、经济的模型,适合日常文本生成和简单问答任务,价格最低廉;中端层模型包括 gpt-4.1、claude-code 等,这些模型具有更强的理解能力和生成质量,适合需要更高准确度的任务;高端层模型包括 gpt-5.1、claude-4.5-opus、claude-4.5-sonnet 等最新发布的模型,具有最强的能力,适合复杂的推理、创意写作等高端任务;专用模型则包括 gemini-pro(Google 的旗舰模型)、llama-2 等开源模型的托管版本,这些模型在特定领域可能有独特的优势。平台通常采用按照输入 token 数和输出 token 数分别计费的模式,不同模型的价格差异较大,用户需要根据自己的预算和需求选择合适的模型。
要在应用中使用付费模型,用户需要修改 ChatBotUI 类中的 models 列表。原始的模型列表定义在__init__方法中,看起来如下:
self.models = [ "gpt-4.1-nano", "gpt-5-nano", "deepseek-r1-0528", "deepseek-v3", "deepseek-v3.1", "gemini-2.5-flash-lite" ]
要添加付费模型,用户只需要在这个列表中添加新的模型字符串。例如,如果用户想添加 OpenAI 的 GPT-4 Turbo 模型和 Claude 3.5 Sonnet 模型,修改后的列表应该如下所示:
self.models = [ "gpt-4.1-nano", "gpt-5-nano", "deepseek-r1-0528", "deepseek-v3", "deepseek-v3.1", "gemini-2.5-flash-lite", "gpt-4-turbo", "gpt-4-turbo-preview", "claude-3.5-sonnet", "claude-3-opus", "gemini-pro" ]
添加完模型后,当用户启动应用时,在选择模型下拉框中就会看到新增的模型选项。用户可以选择任何一个付费模型,然后像使用免费模型一样发送消息,应用会自动调用该模型的 API。需要注意的是,一旦选择了付费模型,每一次 API 调用都会根据平台的定价产生费用。为了避免意外的大额消费,建议用户在实验新模型时先使用较小的 max_tokens 值,并且在平台上设置每日或每月的消费上限。
在 API 平台上,各个模型通常按照功能分组进行组织。第一个分组是通用文本生成模型组,这一组包括所有的 GPT 系列模型和通用的 Claude 模型,这些模型在各种任务上都有不错的表现,适合那些没有特殊需求的一般用户。用户如果不确定选择哪个模型,可以考虑从这个分组中选择。第二个分组是推理和编程专向模型组,这一组主要包括 DeepSeek 的 r1 和 v3 系列,以及 OpenAI 的 GPT-4 系列,这些模型在代码生成、数学推理、逻辑问题解决等方面表现特别突出,适合开发人员和科研工作者。如果用户的任务涉及编写复杂代码或解决深层次的逻辑问题,应该优先选择这个分组中的模型。第三个分组是多模态模型组,包括 Google 的 Gemini 系列和其他支持图像输入的模型,虽然本应用的当前版本主要处理文本,但这些模型的文本生成能力也很强,在需要更全面理解的场景下表现更好。
对于不同场景,用户有以下建议的选择策略:如果进行日常问答和文本生成,推荐使用 gpt-4.1-nano 或 gpt-5-nano 这样的免费轻量级模型,既能保证质量又能节省成本。如果需要编写代码或解决数学问题,推荐使用 deepseek-r1-0528 或 deepseek-v3,因为这两个模型在代码质量和推理准确性上都很突出,而且 deepseek-r1-0528 会返回详细的思考过程,帮助用户理解 AI 的推理逻辑。如果追求最好的输出质量且预算充足,可以选择付费的 gpt-5.2 或 claude-4.5-sonnet,这两个模型在创意写作、深度分析等复杂任务上能力最强。如果进行快速原型设计或对延迟有严格要求,可以选择 gemini-2.5-flash-lite 或其他标注为 flash 的轻量版本,这些模型的响应速度最快。
应用中提供的温度(Temperature)和 Top P 两个采样参数是控制模型生成行为的关键因素。温度参数的取值范围是 0 到 2,它控制了模型生成结果的随机性程度。当温度设置为 0 时,模型会采用最贪心的策略,总是选择概率最高的下一个 token,生成的结果最确定、最可预测,适合对答案准确性要求很高的场景如代码生成或数据提取。当温度设置为 1 时,这是一个平衡点,模型会以正常的概率分布进行采样。当温度设置为更高的值(如 1.5 或 2)时,模型会倾向于选择概率较低但更多样化的 token,生成的结果会更富创意、更多样化,适合创意写作或头脑风暴场景。Top P 参数的取值范围是 0 到 1,它采用核采样的策略,表示只从累计概率达到 P 的最可能的 token 中进行采样。例如,当 Top P 设置为 0.9 时,模型只会在那些累计概率达到 90% 的 token 中进行选择,这样可以避免选择极低概率的离奇 token,同时保留一定的多样性。
在实际使用中,一个常用的参数组合是温度 0.7、Top P 0.9,这个组合在多数场景下提供了很好的平衡。当用户发现模型的回答显得过于保守或重复时,可以尝试提高温度值或降低 Top P 值。相反,当模型的回答显得过于随意或与问题偏离时,应该降低温度或提高 Top P 值。
系统提示词(System Prompt)是指导 AI 行为的关键指令。应用中默认的系统提示词是你是一个专业、友善且富有创意的 AI 助手。你会用中文回答用户的问题。这个提示词设置了基本的角色定义和语言偏好。但用户可以根据实际需求定制系统提示词,来引导 AI 产生特定风格或领域的回答。例如,如果用户想让 AI 扮演一个 C++ 编程专家的角色,可以将系统提示词改为你是一位资深的 C++ 工程师,拥有 20 年的软件开发经验。当用户提出关于 C++ 的问题时,你应该从实战经验出发,提供深入、专业的解答,并给出最佳实践建议。又或者,如果用户想让 AI 充当英文翻译助手,可以设置你是一位资深的翻译工作者,精通英文和中文。你的任务是准确、优雅地将用户提供的文本翻译成指定的语言,保留原文的含义和风格。这样的系统提示词。设计好的系统提示词应该包括以下几个要素:清晰的角色定义(你是什么角色)、明确的任务目标(你要做什么)、输出格式要求(如果需要)、以及任何特殊的限制或偏好。
应用实现了完整的对话历史管理功能。在后台,应用使用一个列表来存储所有的对话消息,每条消息包括 role(消息发送者的角色,可以是 user 或 assistant)和 content(消息的实际内容)两个字段。这个历史列表在内存中维护,用户与 AI 的每一条往来都会被记录。当用户清空对话时,这个列表会被重置。应用还提供了导出对话功能,用户点击这个按钮后,应用会打开一个文件保存对话框,用户可以选择保存位置和文件名。导出的文件是纯文本格式,包含了对话时间戳、使用的模型名称以及完整的对话内容,这样用户可以保存重要的对话记录供日后参考,或者用于分享讨论或学习。
应用还支持导入设置功能,这允许用户从一个 JSON 文件中加载之前保存的配置。用户可以手动编辑这样的 JSON 文件来快速设置多个配置方案。一个典型的设置 JSON 文件看起来如下:
{ "api_key": "sk-xxxxxxxxxxxx", "model": "deepseek-v3", "system_prompt": "你是一位资深的 Python 编程专家", "max_tokens": 8192, "temperature": 0.7, "top_p": 0.9 }
用户可以为不同的使用场景创建多个这样的配置文件,然后根据需要导入不同的配置,这样可以大大提高工作效率。
在使用过程中,用户可能会遇到各种问题。首先是连接错误,如果应用显示连接失败,通常意味着无法连接到 API 服务器的服务器。这可能是由于网络问题、DNS 解析失败或 API 服务器暂时不可用导致的。解决方法是检查网络连接是否正常(可以尝试用浏览器打开确认),确认 API 地址是否正确,如果还是无法连接可以尝试更换网络或使用 VPN。其次是超时错误,应用的默认超时时间是 120 秒,如果在这个时间内没有收到完整的响应,就会显示超时错误。这通常发生在 max_tokens 设置过高、网络延迟大或 API 服务器响应慢的情况下。解决方法是降低 max_tokens 值,或者选择一个更轻量化的模型,或者等待一段时间后重试。第三个常见问题是 API 错误,应用会显示具体的错误码和错误信息。最常见的是 401 错误(未授权),这表示 API 密钥无效或已过期,需要检查 API 密钥是否正确输入。还有 429 错误(请求过于频繁),这表示 API 请求频率超过了平台的限制,需要降低请求频率。
为了获得最佳的使用体验,用户应该注意以下几个优化点。首先,合理设置 max_tokens 参数,不要盲目设置为最大值。对于一般的问答任务,2048 或 4096 的 max_tokens 通常就足够了,只有在需要生成长篇幅内容时才需要设置更高的值。较低的 max_tokens 不仅会提高响应速度,还会降低 API 调用成本。其次,在切换模型时要考虑速度和质量的权衡。轻量级模型(nano 版本、flash 版本)响应速度快但生成质量可能较低,适合快速原型和实时交互。复杂模型(turbo 版本、opus 版本)生成质量高但响应较慢,适合对质量有要求的任务。第三,利用系统提示词来约束模型的行为,一个好的系统提示词可以大大提升回答的相关性和准确性,减少冗余内容,从而提高 token 利用效率和成本效益。第四,定期检查平台上的消费统计和余额,设置合理的日均或月均消费上限,避免因为过度使用导致意外的费用。
本文详细介绍了如何基于 API 平台和 Python Tkinter 框架,搭建一个功能完整、易于使用的 AI 对话机器人应用。从环境准备、基础使用、到高级功能,我们覆盖了使用者可能需要了解的各个方面。通过这个应用,用户可以轻松地体验各种免费和付费的先进 AI 模型,包括 OpenAI 的 GPT 系列、DeepSeek 的推理模型、Google 的 Gemini 模型等。应用内置的 Markdown 渲染、代码提取、流式响应等特性,使得与 AI 的交互更加高效和愉快。
展望未来,这个应用还有很多潜在的扩展方向。例如,可以添加多文件上传和处理能力,让用户可以直接在应用中分析文档、图片等多种文件格式。可以集成本地模型支持,让用户选择在本地运行轻量级模型而不仅仅依赖云端 API。可以添加插件系统,让第三方开发者为应用开发各种功能扩展。可以实现更智能的对话管理,比如自动摘要、主题提取等功能。随着 AI 技术的不断进步,API 站这样的聚合平台会集成越来越多的新模型,而这个应用框架已经为这样的扩展做好了准备。希望通过本文,开发者和用户能够充分利用这个强大的工具,探索 AI 的无限可能性。
窗体程序完整代码实现如下:
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, filedialog
import requests
import json
import threading
from datetime import datetime
import os
import re
import webbrowser
class MarkdownFormatter:
"""处理 Markdown 格式化"""
@staticmethod
def extract_code_blocks(text):
"""提取代码块,返回 (非代码文本,代码块列表)"""
code_blocks = []
# 更灵活的正则表达式,支持多种格式
pattern = r'```(?:python|py)?\s*\n(.*?)\n```'
matches = list(re.finditer(pattern, text, flags=re.DOTALL))
# 提取所有代码块
for match in matches:
code_content = match.group(1).strip()
if code_content:
code_blocks.append(code_content)
# 替换代码块为空字符串,保留其他文本
text_without_code = re.sub(pattern, '', text, flags=re.DOTALL)
return text_without_code.strip(), code_blocks
@staticmethod
def format_text(widget, text,):
"""将 Markdown 格式的文本插入到 Text widget"""
base_tag = ""
widget.config(state=tk.NORMAL)
# 分行处理
lines = text.split('\n')
for line_idx, line in enumerate(lines):
heading_match = re.(, line)
heading_match:
level = ((heading_match.group()), )
content = heading_match.group()
widget.insert(tk.END, content, )
line_idx < (lines) - :
widget.insert(tk.END, )
pos =
pos < (line):
bold_match = re.search(, line[pos:])
italic_match = re.search(, line[pos:])
code_match = re.search(, line[pos:])
link_match = re.search(, line[pos:])
matches = []
bold_match:
matches.append((, bold_match.start(), bold_match.end(), bold_match.group(), ))
italic_match:
matches.append((, italic_match.start(), italic_match.end(), italic_match.group(), ))
code_match:
matches.append((, code_match.start(), code_match.end(), code_match.group(), ))
link_match:
matches.append((, link_match.start(), link_match.end(), link_match.group(), ))
matches:
widget.insert(tk.END, line[pos:], base_tag)
matches.sort(key= x: x[])
match_type, start, end, content, marker = matches[]
start > :
widget.insert(tk.END, line[pos:pos + start], base_tag)
match_type == :
widget.insert(tk.END, content, )
:
widget.insert(tk.END, content, match_type)
pos += end
line_idx < (lines) - :
widget.insert(tk.END, )
widget.config(state=tk.DISABLED)
:
():
.root = root
.root.title()
.root.geometry()
.api_key =
.api_url =
.register_url =
.tutorial_url =
.api_key_file = os.path.join(os.getcwd(), )
.models = [
,
,
,
,
,
]
.conversation_history = []
.is_loading =
.code_tab_count =
.setup_fonts()
.load_api_key()
.create_ui()
():
:
.chinese_font = (, )
.title_font = (, , )
.large_font = (, , )
.code_font = (, )
:
:
.chinese_font = (, )
.title_font = (, , )
.large_font = (, , )
.code_font = (, )
:
.chinese_font = (, )
.title_font = (, , )
.large_font = (, , )
.code_font = (, )
():
:
os.path.exists(.api_key_file):
(.api_key_file, , encoding=) f:
api_key = f.read().strip()
api_key:
.api_key = api_key
Exception e:
()
():
:
api_key:
(.api_key_file, , encoding=) f:
f.write(api_key)
Exception e:
()
messagebox.showerror(, )
():
main_frame = ttk.Frame(.root)
main_frame.pack(fill=tk.BOTH, expand=, padx=, pady=)
left_frame = ttk.LabelFrame(main_frame, text=, padding=)
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, padx=, pady=)
left_frame.config(width=)
ttk.Label(left_frame, text=, font=.title_font).pack(anchor=tk.W, pady=(, ))
api_key_frame = ttk.Frame(left_frame)
api_key_frame.pack(anchor=tk.W, pady=(, ), fill=tk.X)
.api_key_var = tk.StringVar(value=.api_key)
.api_key_entry = ttk.Entry(api_key_frame, textvariable=.api_key_var, width=, font=.chinese_font)
.api_key_entry.pack(side=tk.LEFT, fill=tk.X, expand=, padx=(, ))
ttk.Button(api_key_frame, text=, command=.update_api_key, width=).pack(side=tk.LEFT, padx=)
ttk.Button(api_key_frame, text=, command=.open_register_page, width=).pack(side=tk.LEFT, padx=)
ttk.Separator(left_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=)
ttk.Label(left_frame, text=, font=.title_font).pack(anchor=tk.W, pady=(, ))
.model_var = tk.StringVar(value=.models[])
model_combo = ttk.Combobox(left_frame, textvariable=.model_var, values=.models, state=, width=, font=.chinese_font)
model_combo.pack(anchor=tk.W, pady=(, ), fill=tk.X)
ttk.Label(left_frame, text=, font=.title_font).pack(anchor=tk.W, pady=(, ))
.system_prompt = scrolledtext.ScrolledText(left_frame, height=, width=, font=.chinese_font, wrap=tk.WORD)
.system_prompt.pack(pady=(, ), fill=tk.BOTH, expand=)
.system_prompt.insert(tk.END, )
ttk.Label(left_frame, text=, font=.title_font).pack(anchor=tk.W, pady=(, ))
max_tokens_frame = ttk.Frame(left_frame)
max_tokens_frame.pack(anchor=tk.W, pady=(, ), fill=tk.X)
.max_tokens_var = tk.StringVar(value=)
ttk.Spinbox(max_tokens_frame, from_=, to=, textvariable=.max_tokens_var, width=, font=.chinese_font).pack(side=tk.LEFT)
ttk.Label(max_tokens_frame, text=, font=.chinese_font).pack(side=tk.LEFT, padx=)
ttk.Label(left_frame, text=, font=.title_font).pack(anchor=tk.W, pady=(, ))
temp_frame = ttk.Frame(left_frame)
temp_frame.pack(anchor=tk.W, pady=(, ), fill=tk.X)
.temperature_var = tk.StringVar(value=)
temp_scale = ttk.Scale(temp_frame, from_=, to=, orient=tk.HORIZONTAL, variable=.temperature_var, command=.update_temp_label)
temp_scale.pack(side=tk.LEFT, fill=tk.X, expand=)
.temp_label = ttk.Label(temp_frame, text=, width=, font=.chinese_font)
.temp_label.pack(side=tk.LEFT, padx=)
ttk.Label(left_frame, text=, font=.title_font).pack(anchor=tk.W, pady=(, ))
topp_frame = ttk.Frame(left_frame)
topp_frame.pack(anchor=tk.W, pady=(, ), fill=tk.X)
.top_p_var = tk.StringVar(value=)
topp_scale = ttk.Scale(topp_frame, from_=, to=, orient=tk.HORIZONTAL, variable=.top_p_var, command=.update_topp_label)
topp_scale.pack(side=tk.LEFT, fill=tk.X, expand=)
.topp_label = ttk.Label(topp_frame, text=, width=, font=.chinese_font)
.topp_label.pack(side=tk.LEFT, padx=)
button_frame = ttk.Frame(left_frame)
button_frame.pack(anchor=tk.W, pady=(, ), fill=tk.X)
ttk.Button(button_frame, text=, command=.clear_conversation).pack(fill=tk.X, pady=)
ttk.Button(button_frame, text=, command=.export_chat).pack(fill=tk.X, pady=)
ttk.Button(button_frame, text=, command=.import_settings).pack(fill=tk.X, pady=)
ttk.Button(button_frame, text=, command=.open_tutorial_page).pack(fill=tk.X, pady=)
ttk.Separator(left_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=)
ttk.Label(left_frame, text=, font=.title_font).pack(anchor=tk.W, pady=(, ))
.status_label = ttk.Label(left_frame, text=, font=.chinese_font, foreground=)
.status_label.pack(anchor=tk.W)
right_frame = ttk.LabelFrame(main_frame, text=, padding=)
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=, padx=, pady=)
.main_notebook = ttk.Notebook(right_frame)
.main_notebook.pack(fill=tk.BOTH, expand=, pady=(, ))
chat_tab = ttk.Frame(.main_notebook)
.main_notebook.add(chat_tab, text=)
.chat_display = scrolledtext.ScrolledText(chat_tab, height=, width=, font=.chinese_font, wrap=tk.WORD, state=tk.DISABLED, bg=)
.chat_display.pack(fill=tk.BOTH, expand=)
.chat_display.tag_config(, foreground=, font=(.chinese_font[], .chinese_font[], ))
.chat_display.tag_config(, foreground=, font=(.chinese_font[], .chinese_font[], ))
.chat_display.tag_config(, foreground=, font=(.chinese_font[], .chinese_font[], ))
.chat_display.tag_config(, foreground=, font=(.chinese_font[], .chinese_font[], ))
.chat_display.tag_config(, foreground=, font=(.chinese_font[], ))
.chat_display.tag_config(, font=(.chinese_font[], .chinese_font[], ))
.chat_display.tag_config(, font=(.chinese_font[], .chinese_font[], ))
.chat_display.tag_config(, foreground=, background=, font=(, ))
.chat_display.tag_config(, font=(.chinese_font[], , ), foreground=)
.chat_display.tag_config(, font=(.chinese_font[], , ), foreground=)
.chat_display.tag_config(, font=(.chinese_font[], , ), foreground=)
.chat_display.tag_config(, foreground=, underline=)
ttk.Label(right_frame, text=, font=.title_font).pack(anchor=tk.W, pady=(, ))
.input_text = tk.Text(right_frame, height=, width=, font=.chinese_font, wrap=tk.WORD)
.input_text.pack(fill=tk.BOTH, padx=, pady=(, ))
.input_text.bind(, e: .send_message())
button_frame_right = ttk.Frame(right_frame)
button_frame_right.pack(fill=tk.X, padx=, pady=)
.send_button = ttk.Button(button_frame_right, text=, command=.send_message)
.send_button.pack(side=tk.LEFT, padx=)
ttk.Button(button_frame_right, text=, command=.clear_input).pack(side=tk.LEFT, padx=)
ttk.Button(button_frame_right, text=, command=.copy_last_response).pack(side=tk.LEFT, padx=)
():
.code_tab_count +=
tab_name =
:
code_frame = ttk.Frame(.main_notebook)
.main_notebook.add(code_frame, text=tab_name)
code_display = scrolledtext.ScrolledText(code_frame, font=.code_font, wrap=tk.NONE, bg=, fg=, insertbackground=)
code_display.pack(fill=tk.BOTH, expand=, padx=, pady=)
code_display.insert(tk.END, code_content)
code_display.config(state=tk.DISABLED)
():
.root.clipboard_clear()
.root.clipboard_append(code_content)
.status_label.config(text=, foreground=)
popup_menu = tk.Menu(code_display, tearoff=)
popup_menu.add_command(label=, command=copy_code)
():
:
popup_menu.tk_popup(e.x_root, e.y_root)
:
popup_menu.grab_release()
code_display.bind(, show_popup)
Exception e:
()
():
webbrowser.(.register_url)
():
webbrowser.(.tutorial_url)
():
new_api_key = .api_key_var.get().strip()
new_api_key:
messagebox.showwarning(, )
.save_api_key(new_api_key):
.api_key = new_api_key
messagebox.showinfo(, )
.status_label.config(text=, foreground=)
():
.temp_label.config(text=)
():
.topp_label.config(text=)
():
current_api_key = .api_key_var.get().strip()
current_api_key:
messagebox.showerror(, )
.api_key = current_api_key
.is_loading:
messagebox.showwarning(, )
user_message = .input_text.get(, tk.END).strip()
user_message:
messagebox.showwarning(, )
.display_message(, user_message, )
.clear_input()
.is_loading =
.send_button.config(state=tk.DISABLED)
.status_label.config(text=, foreground=)
.root.update()
threading.Thread(target=.get_response, args=(user_message,), daemon=).start()
():
:
.conversation_history.append({
: ,
: user_message
})
headers = {
: ,
:
}
messages = []
system_prompt = .system_prompt.get(, tk.END).strip()
system_prompt:
messages.append({
: ,
: system_prompt
})
(.conversation_history) > :
messages.extend(.conversation_history[-:])
:
messages.extend(.conversation_history)
data = {
: .model_var.get(),
: messages,
: (.max_tokens_var.get()),
: (.temperature_var.get()),
: (.top_p_var.get()),
:
}
response = requests.post(.api_url, headers=headers, json=data, timeout=, stream=)
response.status_code != :
error_msg =
:
error_json = response.json()
error_msg += json.dumps(error_json, ensure_ascii=, indent=)
:
error_msg += response.text
.display_message(, error_msg, )
.status_label.config(text=, foreground=)
timestamp = datetime.now().strftime()
thinking_shown =
content_started =
update_count =
assistant_message =
reasoning_content =
.chat_display.config(state=tk.NORMAL)
.chat_display.insert(tk.END, , )
line response.iter_lines():
line:
line = line.decode() (line, ) line
line = line.strip()
line == line == :
line.startswith():
data_str = line[:]
line.startswith():
data_str = line
:
:
chunk = json.loads(data_str)
chunk:
error_msg = chunk.get(, {}).get(, )
.chat_display.insert(tk.END, , )
choices = chunk.get(, [])
choices (choices) == :
delta = choices[].get(, {})
reasoning = delta.get()
reasoning:
thinking_shown:
.chat_display.insert(tk.END, , )
thinking_shown =
reasoning_content += reasoning
.chat_display.insert(tk.END, reasoning)
content = delta.get()
content:
content_started:
thinking_shown:
.chat_display.insert(tk.END, , )
:
.chat_display.insert(tk.END, , )
content_started =
assistant_message += content
.chat_display.insert(tk.END, content)
update_count +=
update_count % == :
.chat_display.see(tk.END)
.root.update()
json.JSONDecodeError:
.chat_display.insert(tk.END, )
.chat_display.see(tk.END)
.chat_display.config(state=tk.DISABLED)
.root.update()
assistant_message:
text_without_code, code_blocks = MarkdownFormatter.extract_code_blocks(assistant_message)
.chat_display.config(state=tk.NORMAL)
:
thinking_shown:
search_text =
:
search_text =
pos = .chat_display.search(search_text, , nocase=)
pos:
pos_line, pos_col = pos.split()
pos_line = ((pos_line) + )
start_pos =
.chat_display.delete(start_pos, tk.END)
text_without_code:
MarkdownFormatter.format_text(.chat_display, text_without_code)
.chat_display.insert(tk.END, )
Exception e:
()
.chat_display.config(state=tk.DISABLED)
code_blocks:
code_block code_blocks:
.add_code_tab(code_block)
.conversation_history.append({
: ,
: assistant_message
})
.status_label.config(text=, foreground=)
:
.display_message(, , )
.status_label.config(text=, foreground=)
requests.exceptions.Timeout:
.display_message(, , )
.status_label.config(text=, foreground=)
requests.exceptions.ConnectionError e:
.display_message(, , )
.status_label.config(text=, foreground=)
Exception e:
.display_message(, , )
.status_label.config(text=, foreground=)
:
.is_loading =
.send_button.config(state=tk.NORMAL)
.status_label.cget() == :
.status_label.config(text=, foreground=)
():
.chat_display.config(state=tk.NORMAL)
timestamp = datetime.now().strftime()
.chat_display.insert(tk.END, , )
.chat_display.insert(tk.END, , tag)
tag [, ]:
.chat_display.insert(tk.END, )
:
MarkdownFormatter.format_text(.chat_display, message)
.chat_display.insert(tk.END, )
.chat_display.config(state=tk.DISABLED)
.chat_display.see(tk.END)
():
messagebox.askyesno(, ):
.conversation_history = []
.chat_display.config(state=tk.NORMAL)
.chat_display.delete(, tk.END)
.chat_display.config(state=tk.DISABLED)
.status_label.config(text=, foreground=)
.main_notebook.index() > :
.main_notebook.forget()
.code_tab_count =
():
.input_text.delete(, tk.END)
():
.conversation_history:
i ((.conversation_history) - , -, -):
.conversation_history[i][] == :
message = .conversation_history[i][]
.root.clipboard_clear()
.root.clipboard_append(message)
.status_label.config(text=, foreground=)
messagebox.showinfo(, )
():
.conversation_history:
messagebox.showwarning(, )
file_path = filedialog.asksaveasfilename(defaultextension=, filetypes=[(, ), (, )])
file_path:
:
(file_path, , encoding=) f:
f.write()
f.write()
f.write( * + )
msg .conversation_history:
sender = msg[] ==
f.write()
messagebox.showinfo(, )
Exception e:
messagebox.showerror(, )
():
file_path = filedialog.askopenfilename(filetypes=[(, ), (, )])
file_path:
:
(file_path, , encoding=) f:
settings = json.load(f)
settings:
.api_key_var.(settings[])
settings:
.system_prompt.delete(, tk.END)
.system_prompt.insert(tk.END, settings[])
settings:
.max_tokens_var.((settings[]))
settings:
.temperature_var.((settings[]))
settings:
.top_p_var.((settings[]))
settings settings[] .models:
.model_var.(settings[])
messagebox.showinfo(, )
Exception e:
messagebox.showerror(, )
__name__ == :
root = tk.Tk()
app = ChatBotUI(root)
root.mainloop()

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online