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.match(r'^(#{1,6})\s+(.+)$', line)
if heading_match:
level = min(len(heading_match.group(1)), 3)
content = heading_match.group(2)
widget.insert(tk.END, content, f"heading{level}")
if line_idx < len(lines) - 1:
widget.insert(tk.END, '\n')
continue
pos = 0
while pos < len(line):
bold_match = re.search(r'\*(.+?)\*', line[pos:])
italic_match = re.search(r'(?<!\*)\*([^*]+?)\*(?!\*)', line[pos:])
code_match = re.search(r'`([^`]+?)`', line[pos:])
link_match = re.search(r'\[([^\]]+)\]\(([^\)]+)\)', line[pos:])
matches = []
if bold_match:
matches.append(('bold', bold_match.start(), bold_match.end(), bold_match.group(1), '**'))
if italic_match:
matches.append(('italic', italic_match.start(), italic_match.end(), italic_match.group(1), '*'))
if code_match:
matches.append(('code', code_match.start(), code_match.end(), code_match.group(1), '`'))
if link_match:
matches.append(('link', link_match.start(), link_match.end(), link_match.group(1), 'link'))
if not matches:
widget.insert(tk.END, line[pos:], base_tag)
break
matches.sort(key=lambda x: x[1])
match_type, start, end, content, marker = matches[0]
if start > 0:
widget.insert(tk.END, line[pos:pos + start], base_tag)
if match_type == 'link':
widget.insert(tk.END, content, 'link')
else:
widget.insert(tk.END, content, match_type)
pos += end
if line_idx < len(lines) - 1:
widget.insert(tk.END, '\n')
widget.config(state=tk.DISABLED)
class ChatBotUI:
def __init__(self, root):
self.root = root
self.root.title("AI 聊天机器人")
self.root.geometry("1600x800")
self.api_key = ""
self.api_url = "https://api.aigc.bar/v1/chat/completions"
self.register_url = "https://api.aigc.bar/register"
self.tutorial_url = ""
self.api_key_file = os.path.join(os.getcwd(), "api_key.txt")
self.models = [
"gpt-4.1-nano",
"gpt-5-nano",
"deepseek-r1-0528",
"deepseek-v3",
"deepseek-v3.1",
"gemini-2.5-flash-lite"
]
self.conversation_history = []
self.is_loading = False
self.code_tab_count = 0
self.setup_fonts()
self.load_api_key()
self.create_ui()
def setup_fonts(self):
"""设置中文字体"""
try:
self.chinese_font = ("微软雅黑", 10)
self.title_font = ("微软雅黑", 11, "bold")
self.large_font = ("微软雅黑", 12, "bold")
self.code_font = ("Courier New", 10)
except:
try:
self.chinese_font = ("WenQuanYi Micro Hei", 10)
self.title_font = ("WenQuanYi Micro Hei", 11, "bold")
self.large_font = ("WenQuanYi Micro Hei", 12, "bold")
self.code_font = ("Courier New", 10)
except:
self.chinese_font = ("Helvetica", 10)
self.title_font = ("Helvetica", 11, "bold")
self.large_font = ("Helvetica", 12, "bold")
self.code_font = ("Courier New", 10)
def load_api_key(self):
"""从文件加载 API 密钥"""
try:
if os.path.exists(self.api_key_file):
with open(self.api_key_file, 'r', encoding='utf-8') as f:
api_key = f.read().strip()
if api_key:
self.api_key = api_key
except Exception as e:
print(f"加载 API 密钥失败:{e}")
def save_api_key(self, api_key):
"""保存 API 密钥到文件"""
try:
if api_key:
with open(self.api_key_file, 'w', encoding='utf-8') as f:
f.write(api_key)
return True
except Exception as e:
print(f"保存 API 密钥失败:{e}")
messagebox.showerror("错误", f"保存 API 密钥失败:{str(e)}")
return False
def create_ui(self):
"""创建用户界面"""
main_frame = ttk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=8)
left_frame = ttk.LabelFrame(main_frame, text="参数配置", padding=12)
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, padx=5, pady=5)
left_frame.config(width=300)
ttk.Label(left_frame, text="API 密钥:", font=self.title_font).pack(anchor=tk.W, pady=(5, 2))
api_key_frame = ttk.Frame(left_frame)
api_key_frame.pack(anchor=tk.W, pady=(0, 5), fill=tk.X)
self.api_key_var = tk.StringVar(value=self.api_key)
self.api_key_entry = ttk.Entry(api_key_frame, textvariable=self.api_key_var, width=20, font=self.chinese_font)
self.api_key_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 2))
ttk.Button(api_key_frame, text="保存", command=self.update_api_key, width=6).pack(side=tk.LEFT, padx=2)
ttk.Button(api_key_frame, text="注册", command=self.open_register_page, width=6).pack(side=tk.LEFT, padx=2)
ttk.Separator(left_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10)
ttk.Label(left_frame, text="选择模型:", font=self.title_font).pack(anchor=tk.W, pady=(5, 2))
self.model_var = tk.StringVar(value=self.models[0])
model_combo = ttk.Combobox(left_frame, textvariable=self.model_var, values=self.models, state="readonly", width=25, font=self.chinese_font)
model_combo.pack(anchor=tk.W, pady=(0, 10), fill=tk.X)
ttk.Label(left_frame, text="系统提示词 (System Prompt):", font=self.title_font).pack(anchor=tk.W, pady=(5, 2))
self.system_prompt = scrolledtext.ScrolledText(left_frame, height=7, width=30, font=self.chinese_font, wrap=tk.WORD)
self.system_prompt.pack(pady=(0, 10), fill=tk.BOTH, expand=True)
self.system_prompt.insert(tk.END, "你是一个专业、友善且富有创意的 AI 助手。\n你会用中文回答用户的问题。")
ttk.Label(left_frame, text="最大输出长度 (tokens):", font=self.title_font).pack(anchor=tk.W, pady=(5, 2))
max_tokens_frame = ttk.Frame(left_frame)
max_tokens_frame.pack(anchor=tk.W, pady=(0, 10), fill=tk.X)
self.max_tokens_var = tk.StringVar(value="16384")
ttk.Spinbox(max_tokens_frame, from_=100, to=16384, textvariable=self.max_tokens_var, width=10, font=self.chinese_font).pack(side=tk.LEFT)
ttk.Label(max_tokens_frame, text="(100-16384)", font=self.chinese_font).pack(side=tk.LEFT, padx=5)
ttk.Label(left_frame, text="温度 (Temperature):", font=self.title_font).pack(anchor=tk.W, pady=(5, 2))
temp_frame = ttk.Frame(left_frame)
temp_frame.pack(anchor=tk.W, pady=(0, 10), fill=tk.X)
self.temperature_var = tk.StringVar(value="0.7")
temp_scale = ttk.Scale(temp_frame, from_=0, to=2, orient=tk.HORIZONTAL, variable=self.temperature_var, command=self.update_temp_label)
temp_scale.pack(side=tk.LEFT, fill=tk.X, expand=True)
self.temp_label = ttk.Label(temp_frame, text="0.7", width=5, font=self.chinese_font)
self.temp_label.pack(side=tk.LEFT, padx=5)
ttk.Label(left_frame, text="Top P:", font=self.title_font).pack(anchor=tk.W, pady=(5, 2))
topp_frame = ttk.Frame(left_frame)
topp_frame.pack(anchor=tk.W, pady=(0, 15), fill=tk.X)
self.top_p_var = tk.StringVar(value="0.9")
topp_scale = ttk.Scale(topp_frame, from_=0, to=1, orient=tk.HORIZONTAL, variable=self.top_p_var, command=self.update_topp_label)
topp_scale.pack(side=tk.LEFT, fill=tk.X, expand=True)
self.topp_label = ttk.Label(topp_frame, text="0.9", width=5, font=self.chinese_font)
self.topp_label.pack(side=tk.LEFT, padx=5)
button_frame = ttk.Frame(left_frame)
button_frame.pack(anchor=tk.W, pady=(10, 0), fill=tk.X)
ttk.Button(button_frame, text="清空对话", command=self.clear_conversation).pack(fill=tk.X, pady=2)
ttk.Button(button_frame, text="导出对话", command=self.export_chat).pack(fill=tk.X, pady=2)
ttk.Button(button_frame, text="导入设置", command=self.import_settings).pack(fill=tk.X, pady=2)
ttk.Button(button_frame, text="使用教程", command=self.open_tutorial_page).pack(fill=tk.X, pady=2)
ttk.Separator(left_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10)
ttk.Label(left_frame, text="状态:", font=self.title_font).pack(anchor=tk.W, pady=(5, 2))
self.status_label = ttk.Label(left_frame, text="就绪", font=self.chinese_font, foreground="green")
self.status_label.pack(anchor=tk.W)
right_frame = ttk.LabelFrame(main_frame, text="聊天与代码", padding=10)
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5)
self.main_notebook = ttk.Notebook(right_frame)
self.main_notebook.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
chat_tab = ttk.Frame(self.main_notebook)
self.main_notebook.add(chat_tab, text="聊天")
self.chat_display = scrolledtext.ScrolledText(chat_tab, height=25, width=70, font=self.chinese_font, wrap=tk.WORD, state=tk.DISABLED, bg="#f5f5f5")
self.chat_display.pack(fill=tk.BOTH, expand=True)
self.chat_display.tag_config("user", foreground="#0066cc", font=(self.chinese_font[0], self.chinese_font[1], "bold"))
self.chat_display.tag_config("assistant", foreground="#009900", font=(self.chinese_font[0], self.chinese_font[1], "bold"))
self.chat_display.tag_config("thinking", foreground="#FF9500", font=(self.chinese_font[0], self.chinese_font[1], "bold"))
self.chat_display.tag_config("error", foreground="#cc0000", font=(self.chinese_font[0], self.chinese_font[1], "bold"))
self.chat_display.tag_config("timestamp", foreground="#999999", font=(self.chinese_font[0], 9))
self.chat_display.tag_config("bold", font=(self.chinese_font[0], self.chinese_font[1], "bold"))
self.chat_display.tag_config("italic", font=(self.chinese_font[0], self.chinese_font[1], "italic"))
self.chat_display.tag_config("code", foreground="#d63384", background="#f8f9fa", font=("Courier", 9))
self.chat_display.tag_config("heading1", font=(self.chinese_font[0], 14, "bold"), foreground="#000080")
self.chat_display.tag_config("heading2", font=(self.chinese_font[0], 12, "bold"), foreground="#000080")
self.chat_display.tag_config("heading3", font=(self.chinese_font[0], 11, "bold"), foreground="#000080")
self.chat_display.tag_config("link", foreground="#0066cc", underline=True)
ttk.Label(right_frame, text="输入消息 (Ctrl+Enter 快速发送):", font=self.title_font).pack(anchor=tk.W, pady=(5, 2))
self.input_text = tk.Text(right_frame, height=5, width=70, font=self.chinese_font, wrap=tk.WORD)
self.input_text.pack(fill=tk.BOTH, padx=0, pady=(0, 8))
self.input_text.bind("<Control-Return>", lambda e: self.send_message())
button_frame_right = ttk.Frame(right_frame)
button_frame_right.pack(fill=tk.X, padx=0, pady=0)
self.send_button = ttk.Button(button_frame_right, text="发送", command=self.send_message)
self.send_button.pack(side=tk.LEFT, padx=3)
ttk.Button(button_frame_right, text="清空输入", command=self.clear_input).pack(side=tk.LEFT, padx=3)
ttk.Button(button_frame_right, text="复制最后回复", command=self.copy_last_response).pack(side=tk.LEFT, padx=3)
def add_code_tab(self, code_content):
"""添加代码选项卡"""
self.code_tab_count += 1
tab_name = f"代码 {self.code_tab_count}"
try:
code_frame = ttk.Frame(self.main_notebook)
self.main_notebook.add(code_frame, text=tab_name)
code_display = scrolledtext.ScrolledText(code_frame, font=self.code_font, wrap=tk.NONE, bg="#2b2b2b", fg="#f8f8f2", insertbackground="white")
code_display.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
code_display.insert(tk.END, code_content)
code_display.config(state=tk.DISABLED)
def copy_code():
self.root.clipboard_clear()
self.root.clipboard_append(code_content)
self.status_label.config(text="代码已复制到剪贴板", foreground="blue")
popup_menu = tk.Menu(code_display, tearoff=0)
popup_menu.add_command(label="复制代码", command=copy_code)
def show_popup(e):
try:
popup_menu.tk_popup(e.x_root, e.y_root)
finally:
popup_menu.grab_release()
code_display.bind("<Button-3>", show_popup)
except Exception as e:
print(f"添加代码选项卡出错:{e}")
def open_register_page(self):
"""打开注册页面"""
webbrowser.open(self.register_url)
def open_tutorial_page(self):
"""打开使用教程页面"""
webbrowser.open(self.tutorial_url)
def update_api_key(self):
"""更新并保存 API 密钥"""
new_api_key = self.api_key_var.get().strip()
if not new_api_key:
messagebox.showwarning("提示", "请输入 API 密钥")
return
if self.save_api_key(new_api_key):
self.api_key = new_api_key
messagebox.showinfo("成功", "API 密钥已保存")
self.status_label.config(text="API 密钥已保存", foreground="blue")
def update_temp_label(self, value):
"""更新温度标签"""
self.temp_label.config(text=f"{float(value):.2f}")
def update_topp_label(self, value):
"""更新 Top P 标签"""
self.topp_label.config(text=f"{float(value):.2f}")
def send_message(self):
"""发送消息"""
current_api_key = self.api_key_var.get().strip()
if not current_api_key:
messagebox.showerror("错误", "请先填写 API 密钥或点击注册按钮获取密钥")
return
self.api_key = current_api_key
if self.is_loading:
messagebox.showwarning("提示", "正在等待上一条消息的回复,请稍候...")
return
user_message = self.input_text.get("1.0", tk.END).strip()
if not user_message:
messagebox.showwarning("提示", "请输入消息")
return
self.display_message("你", user_message, "user")
self.clear_input()
self.is_loading = True
self.send_button.config(state=tk.DISABLED)
self.status_label.config(text="正在处理...", foreground="orange")
self.root.update()
threading.Thread(target=self.get_response, args=(user_message,), daemon=True).start()
def get_response(self, user_message):
"""获取 AI 响应(在工作线程中执行)"""
try:
self.conversation_history.append({
"role": "user",
"content": user_message
})
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
messages = []
system_prompt = self.system_prompt.get("1.0", tk.END).strip()
if system_prompt:
messages.append({
"role": "system",
"content": system_prompt
})
if len(self.conversation_history) > 1:
messages.extend(self.conversation_history[-2:])
else:
messages.extend(self.conversation_history)
data = {
"model": self.model_var.get(),
"messages": messages,
"max_tokens": int(self.max_tokens_var.get()),
"temperature": float(self.temperature_var.get()),
"top_p": float(self.top_p_var.get()),
"stream": True
}
response = requests.post(self.api_url, headers=headers, json=data, timeout=120, stream=True)
if response.status_code != 200:
error_msg = f"API 错误 {response.status_code}\n"
try:
error_json = response.json()
error_msg += json.dumps(error_json, ensure_ascii=False, indent=2)
except:
error_msg += response.text
self.display_message("错误", error_msg, "error")
self.status_label.config(text="错误", foreground="red")
return
timestamp = datetime.now().strftime("%H:%M:%S")
thinking_shown = False
content_started = False
update_count = 0
assistant_message = ""
reasoning_content = ""
self.chat_display.config(state=tk.NORMAL)
self.chat_display.insert(tk.END, f"[{timestamp}] ", "timestamp")
for line in response.iter_lines():
if not line:
continue
line = line.decode('utf-8') if isinstance(line, bytes) else line
line = line.strip()
if line == "[DONE]" or line == "data: [DONE]":
break
if line.startswith("data: "):
data_str = line[6:]
elif line.startswith("{"):
data_str = line
else:
continue
try:
chunk = json.loads(data_str)
if "error" in chunk:
error_msg = chunk.get("error", {}).get("message", "未知错误")
self.chat_display.insert(tk.END, f"流式错误:{error_msg}\n", "error")
break
choices = chunk.get("choices", [])
if not choices or len(choices) == 0:
continue
delta = choices[0].get("delta", {})
reasoning = delta.get("reasoning_content")
if reasoning:
if not thinking_shown:
self.chat_display.insert(tk.END, "思考过程:\n", "thinking")
thinking_shown = True
reasoning_content += reasoning
self.chat_display.insert(tk.END, reasoning)
content = delta.get("content")
if content:
if not content_started:
if thinking_shown:
self.chat_display.insert(tk.END, "\n\n最终回复:\n", "assistant")
else:
self.chat_display.insert(tk.END, "AI:\n", "assistant")
content_started = True
assistant_message += content
self.chat_display.insert(tk.END, content)
update_count += 1
if update_count % 5 == 0:
self.chat_display.see(tk.END)
self.root.update()
except json.JSONDecodeError:
continue
self.chat_display.insert(tk.END, "\n\n")
self.chat_display.see(tk.END)
self.chat_display.config(state=tk.DISABLED)
self.root.update()
if assistant_message:
text_without_code, code_blocks = MarkdownFormatter.extract_code_blocks(assistant_message)
self.chat_display.config(state=tk.NORMAL)
try:
if thinking_shown:
search_text = "最终回复:\n"
else:
search_text = "AI:\n"
pos = self.chat_display.search(search_text, "1.0", nocase=True)
if pos:
pos_line, pos_col = pos.split('.')
pos_line = str(int(pos_line) + 1)
start_pos = f"{pos_line}.0"
self.chat_display.delete(start_pos, tk.END)
if text_without_code:
MarkdownFormatter.format_text(self.chat_display, text_without_code)
self.chat_display.insert(tk.END, "\n\n")
except Exception as e:
print(f"格式化错误:{e}")
self.chat_display.config(state=tk.DISABLED)
if code_blocks:
for code_block in code_blocks:
self.add_code_tab(code_block)
self.conversation_history.append({
"role": "assistant",
"content": assistant_message
})
self.status_label.config(text="就绪", foreground="green")
else:
self.display_message("错误", "未收到有效的 AI 响应", "error")
self.status_label.config(text="无响应", foreground="red")
except requests.exceptions.Timeout:
self.display_message("错误", "请求超时(120 秒)。请检查网络连接或尝试更小的输出长度。", "error")
self.status_label.config(text="超时错误", foreground="red")
except requests.exceptions.ConnectionError as e:
self.display_message("错误", f"连接失败:{str(e)}\n请检查 API 地址和网络连接。", "error")
self.status_label.config(text="连接错误", foreground="red")
except Exception as e:
self.display_message("错误", f"发生错误:{str(e)}", "error")
self.status_label.config(text="未知错误", foreground="red")
finally:
self.is_loading = False
self.send_button.config(state=tk.NORMAL)
if self.status_label.cget("text") == "正在处理...":
self.status_label.config(text="就绪", foreground="green")
def display_message(self, sender, message, tag="user"):
"""显示消息"""
self.chat_display.config(state=tk.NORMAL)
timestamp = datetime.now().strftime("%H:%M:%S")
self.chat_display.insert(tk.END, f"[{timestamp}] ", "timestamp")
self.chat_display.insert(tk.END, f"{sender}:\n", tag)
if tag in ["user", "error"]:
self.chat_display.insert(tk.END, f"{message}\n\n")
else:
MarkdownFormatter.format_text(self.chat_display, message)
self.chat_display.insert(tk.END, "\n\n")
self.chat_display.config(state=tk.DISABLED)
self.chat_display.see(tk.END)
def clear_conversation(self):
"""清空对话历史"""
if messagebox.askyesno("确认", "确定要清空所有对话历史吗?"):
self.conversation_history = []
self.chat_display.config(state=tk.NORMAL)
self.chat_display.delete("1.0", tk.END)
self.chat_display.config(state=tk.DISABLED)
self.status_label.config(text="对话已清空", foreground="blue")
while self.main_notebook.index("end") > 1:
self.main_notebook.forget(1)
self.code_tab_count = 0
def clear_input(self):
"""清空输入框"""
self.input_text.delete("1.0", tk.END)
def copy_last_response(self):
"""复制最后一条 AI 回复"""
if self.conversation_history:
for i in range(len(self.conversation_history) - 1, -1, -1):
if self.conversation_history[i]["role"] == "assistant":
message = self.conversation_history[i]["content"]
self.root.clipboard_clear()
self.root.clipboard_append(message)
self.status_label.config(text="已复制到剪贴板", foreground="blue")
return
messagebox.showinfo("提示", "没有可复制的 AI 回复")
def export_chat(self):
"""导出对话为文本文件"""
if not self.conversation_history:
messagebox.showwarning("提示", "没有对话可导出")
return
file_path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")])
if file_path:
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(f"对话导出 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"模型:{self.model_var.get()}\n")
f.write("=" * 60 + "\n\n")
for msg in self.conversation_history:
sender = "用户" if msg["role"] == "user" else "AI"
f.write(f"{sender}:\n{msg['content']}\n\n")
messagebox.showinfo("成功", f"对话已导出到:{file_path}")
except Exception as e:
messagebox.showerror("错误", f"导出失败:{str(e)}")
def import_settings(self):
"""从 JSON 文件导入设置"""
file_path = filedialog.askopenfilename(filetypes=[("JSON 文件", "*.json"), ("所有文件", "*.*")])
if file_path:
try:
with open(file_path, 'r', encoding='utf-8') as f:
settings = json.load(f)
if "api_key" in settings:
self.api_key_var.set(settings["api_key"])
if "system_prompt" in settings:
self.system_prompt.delete("1.0", tk.END)
self.system_prompt.insert(tk.END, settings["system_prompt"])
if "max_tokens" in settings:
self.max_tokens_var.set(str(settings["max_tokens"]))
if "temperature" in settings:
self.temperature_var.set(str(settings["temperature"]))
if "top_p" in settings:
self.top_p_var.set(str(settings["top_p"]))
if "model" in settings and settings["model"] in self.models:
self.model_var.set(settings["model"])
messagebox.showinfo("成功", "设置导入完成")
except Exception as e:
messagebox.showerror("错误", f"导入失败:{str(e)}")
if __name__ == "__main__":
root = tk.Tk()
app = ChatBotUI(root)
root.mainloop()