2025-02-16 22:32:32 +08:00
|
|
|
# ui/main_window.py
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
import os
|
|
|
|
|
import threading
|
|
|
|
|
import logging
|
|
|
|
|
import traceback
|
|
|
|
|
import customtkinter as ctk
|
|
|
|
|
import tkinter as tk
|
|
|
|
|
from tkinter import filedialog, messagebox
|
|
|
|
|
|
|
|
|
|
from config_manager import load_config, save_config, test_llm_config, test_embedding_config
|
|
|
|
|
from utils import read_file, save_string_to_txt, clear_file_content
|
|
|
|
|
from tooltips import tooltips
|
|
|
|
|
|
|
|
|
|
from ui.context_menu import TextWidgetContextMenu
|
|
|
|
|
from ui.main_tab import build_main_tab, build_left_layout, build_right_layout
|
|
|
|
|
from ui.config_tab import build_config_tabview, load_config_btn, save_config_btn
|
|
|
|
|
from ui.novel_params_tab import build_novel_params_area, build_optional_buttons_area
|
|
|
|
|
from ui.generation_handlers import (
|
|
|
|
|
generate_novel_architecture_ui,
|
|
|
|
|
generate_chapter_blueprint_ui,
|
|
|
|
|
generate_chapter_draft_ui,
|
|
|
|
|
finalize_chapter_ui,
|
|
|
|
|
do_consistency_check,
|
|
|
|
|
import_knowledge_handler,
|
|
|
|
|
clear_vectorstore_handler,
|
|
|
|
|
show_plot_arcs_ui
|
|
|
|
|
)
|
|
|
|
|
from ui.setting_tab import build_setting_tab, load_novel_architecture, save_novel_architecture
|
|
|
|
|
from ui.directory_tab import build_directory_tab, load_chapter_blueprint, save_chapter_blueprint
|
|
|
|
|
from ui.character_tab import build_character_tab, load_character_state, save_character_state
|
|
|
|
|
from ui.summary_tab import build_summary_tab, load_global_summary, save_global_summary
|
|
|
|
|
from ui.chapters_tab import build_chapters_tab, refresh_chapters_list, on_chapter_selected, load_chapter_content, save_current_chapter, prev_chapter, next_chapter
|
|
|
|
|
|
|
|
|
|
class NovelGeneratorGUI:
|
|
|
|
|
"""
|
|
|
|
|
小说生成器的主GUI类,包含所有的界面布局、事件处理、与后端逻辑的交互等。
|
|
|
|
|
"""
|
|
|
|
|
def __init__(self, master):
|
|
|
|
|
self.master = master
|
|
|
|
|
self.master.title("Novel Generator GUI")
|
|
|
|
|
try:
|
|
|
|
|
if os.path.exists("icon.ico"):
|
|
|
|
|
self.master.iconbitmap("icon.ico")
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
self.master.geometry("1350x840")
|
|
|
|
|
|
|
|
|
|
# --------------- 配置文件路径 ---------------
|
|
|
|
|
self.config_file = "config.json"
|
|
|
|
|
self.loaded_config = load_config(self.config_file)
|
|
|
|
|
|
|
|
|
|
if self.loaded_config:
|
|
|
|
|
last_llm = self.loaded_config.get("last_interface_format", "OpenAI")
|
|
|
|
|
last_embedding = self.loaded_config.get("last_embedding_interface_format", "OpenAI")
|
|
|
|
|
else:
|
|
|
|
|
last_llm = "OpenAI"
|
|
|
|
|
last_embedding = "OpenAI"
|
|
|
|
|
|
|
|
|
|
if self.loaded_config and "llm_configs" in self.loaded_config and last_llm in self.loaded_config["llm_configs"]:
|
|
|
|
|
llm_conf = self.loaded_config["llm_configs"][last_llm]
|
|
|
|
|
else:
|
|
|
|
|
llm_conf = {
|
|
|
|
|
"api_key": "",
|
|
|
|
|
"base_url": "https://api.openai.com/v1",
|
|
|
|
|
"model_name": "gpt-4o-mini",
|
|
|
|
|
"temperature": 0.7,
|
|
|
|
|
"max_tokens": 8192,
|
|
|
|
|
"timeout": 600
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.loaded_config and "embedding_configs" in self.loaded_config and last_embedding in self.loaded_config["embedding_configs"]:
|
|
|
|
|
emb_conf = self.loaded_config["embedding_configs"][last_embedding]
|
|
|
|
|
else:
|
|
|
|
|
emb_conf = {
|
|
|
|
|
"api_key": "",
|
|
|
|
|
"base_url": "https://api.openai.com/v1",
|
|
|
|
|
"model_name": "text-embedding-ada-002",
|
|
|
|
|
"retrieval_k": 4
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# -- LLM通用参数 --
|
|
|
|
|
self.api_key_var = ctk.StringVar(value=llm_conf.get("api_key", ""))
|
|
|
|
|
self.base_url_var = ctk.StringVar(value=llm_conf.get("base_url", "https://api.openai.com/v1"))
|
|
|
|
|
self.interface_format_var = ctk.StringVar(value=last_llm)
|
|
|
|
|
self.model_name_var = ctk.StringVar(value=llm_conf.get("model_name", "gpt-4o-mini"))
|
|
|
|
|
self.temperature_var = ctk.DoubleVar(value=llm_conf.get("temperature", 0.7))
|
|
|
|
|
self.max_tokens_var = ctk.IntVar(value=llm_conf.get("max_tokens", 8192))
|
|
|
|
|
self.timeout_var = ctk.IntVar(value=llm_conf.get("timeout", 600))
|
|
|
|
|
|
|
|
|
|
# -- Embedding相关 --
|
|
|
|
|
self.embedding_interface_format_var = ctk.StringVar(value=last_embedding)
|
|
|
|
|
self.embedding_api_key_var = ctk.StringVar(value=emb_conf.get("api_key", ""))
|
|
|
|
|
self.embedding_url_var = ctk.StringVar(value=emb_conf.get("base_url", "https://api.openai.com/v1"))
|
|
|
|
|
self.embedding_model_name_var = ctk.StringVar(value=emb_conf.get("model_name", "text-embedding-ada-002"))
|
|
|
|
|
self.embedding_retrieval_k_var = ctk.StringVar(value=str(emb_conf.get("retrieval_k", 4)))
|
|
|
|
|
|
|
|
|
|
# -- 小说参数相关 --
|
|
|
|
|
if self.loaded_config and "other_params" in self.loaded_config:
|
|
|
|
|
op = self.loaded_config["other_params"]
|
|
|
|
|
self.topic_default = op.get("topic", "")
|
|
|
|
|
self.genre_var = ctk.StringVar(value=op.get("genre", "玄幻"))
|
|
|
|
|
self.num_chapters_var = ctk.StringVar(value=str(op.get("num_chapters", 10)))
|
|
|
|
|
self.word_number_var = ctk.StringVar(value=str(op.get("word_number", 3000)))
|
|
|
|
|
self.filepath_var = ctk.StringVar(value=op.get("filepath", ""))
|
|
|
|
|
self.chapter_num_var = ctk.StringVar(value=str(op.get("chapter_num", "1")))
|
|
|
|
|
self.characters_involved_var = ctk.StringVar(value=op.get("characters_involved", ""))
|
|
|
|
|
self.key_items_var = ctk.StringVar(value=op.get("key_items", ""))
|
|
|
|
|
self.scene_location_var = ctk.StringVar(value=op.get("scene_location", ""))
|
|
|
|
|
self.time_constraint_var = ctk.StringVar(value=op.get("time_constraint", ""))
|
|
|
|
|
self.user_guidance_default = op.get("user_guidance", "")
|
|
|
|
|
else:
|
|
|
|
|
self.topic_default = ""
|
|
|
|
|
self.genre_var = ctk.StringVar(value="玄幻")
|
|
|
|
|
self.num_chapters_var = ctk.StringVar(value="10")
|
|
|
|
|
self.word_number_var = ctk.StringVar(value="3000")
|
|
|
|
|
self.filepath_var = ctk.StringVar(value="")
|
|
|
|
|
self.chapter_num_var = ctk.StringVar(value="1")
|
|
|
|
|
self.characters_involved_var = ctk.StringVar(value="")
|
|
|
|
|
self.key_items_var = ctk.StringVar(value="")
|
|
|
|
|
self.scene_location_var = ctk.StringVar(value="")
|
|
|
|
|
self.time_constraint_var = ctk.StringVar(value="")
|
|
|
|
|
self.user_guidance_default = ""
|
|
|
|
|
|
|
|
|
|
# --------------- 整体Tab布局 ---------------
|
|
|
|
|
self.tabview = ctk.CTkTabview(self.master)
|
|
|
|
|
self.tabview.pack(fill="both", expand=True)
|
|
|
|
|
|
|
|
|
|
# 创建各个标签页
|
|
|
|
|
build_main_tab(self)
|
|
|
|
|
build_config_tabview(self)
|
|
|
|
|
build_novel_params_area(self, start_row=1)
|
|
|
|
|
build_optional_buttons_area(self, start_row=2)
|
|
|
|
|
build_setting_tab(self)
|
|
|
|
|
build_directory_tab(self)
|
|
|
|
|
build_character_tab(self)
|
|
|
|
|
build_summary_tab(self)
|
|
|
|
|
build_chapters_tab(self)
|
|
|
|
|
|
|
|
|
|
# ----------------- 通用辅助函数 -----------------
|
|
|
|
|
def show_tooltip(self, key: str):
|
|
|
|
|
info_text = tooltips.get(key, "暂无说明")
|
|
|
|
|
messagebox.showinfo("参数说明", info_text)
|
|
|
|
|
|
|
|
|
|
def safe_get_int(self, var, default=1):
|
|
|
|
|
try:
|
|
|
|
|
val_str = str(var.get()).strip()
|
|
|
|
|
return int(val_str)
|
|
|
|
|
except:
|
|
|
|
|
var.set(str(default))
|
|
|
|
|
return default
|
|
|
|
|
|
|
|
|
|
def log(self, message: str):
|
|
|
|
|
self.log_text.configure(state="normal")
|
|
|
|
|
self.log_text.insert("end", message + "\n")
|
|
|
|
|
self.log_text.see("end")
|
|
|
|
|
self.log_text.configure(state="disabled")
|
|
|
|
|
|
|
|
|
|
def safe_log(self, message: str):
|
|
|
|
|
self.master.after(0, lambda: self.log(message))
|
|
|
|
|
|
|
|
|
|
def disable_button_safe(self, btn):
|
|
|
|
|
self.master.after(0, lambda: btn.configure(state="disabled"))
|
|
|
|
|
|
|
|
|
|
def enable_button_safe(self, btn):
|
|
|
|
|
self.master.after(0, lambda: btn.configure(state="normal"))
|
|
|
|
|
|
|
|
|
|
def handle_exception(self, context: str):
|
|
|
|
|
full_message = f"{context}\n{traceback.format_exc()}"
|
|
|
|
|
logging.error(full_message)
|
|
|
|
|
self.safe_log(full_message)
|
|
|
|
|
|
|
|
|
|
def show_chapter_in_textbox(self, text: str):
|
|
|
|
|
self.chapter_result.delete("0.0", "end")
|
|
|
|
|
self.chapter_result.insert("0.0", text)
|
|
|
|
|
self.chapter_result.see("end")
|
|
|
|
|
|
|
|
|
|
def test_llm_config(self):
|
|
|
|
|
"""
|
|
|
|
|
测试当前的LLM配置是否可用
|
|
|
|
|
"""
|
|
|
|
|
interface_format = self.interface_format_var.get().strip()
|
|
|
|
|
api_key = self.api_key_var.get().strip()
|
|
|
|
|
base_url = self.base_url_var.get().strip()
|
|
|
|
|
model_name = self.model_name_var.get().strip()
|
|
|
|
|
temperature = self.temperature_var.get()
|
|
|
|
|
max_tokens = self.max_tokens_var.get()
|
|
|
|
|
timeout = self.timeout_var.get()
|
|
|
|
|
|
2025-02-17 23:09:28 +08:00
|
|
|
if interface_format == 'Ollama' and api_key == '':
|
|
|
|
|
api_key = 'ollama'
|
|
|
|
|
|
2025-02-16 22:32:32 +08:00
|
|
|
test_llm_config(
|
|
|
|
|
interface_format=interface_format,
|
|
|
|
|
api_key=api_key,
|
|
|
|
|
base_url=base_url,
|
|
|
|
|
model_name=model_name,
|
|
|
|
|
temperature=temperature,
|
|
|
|
|
max_tokens=max_tokens,
|
|
|
|
|
timeout=timeout,
|
|
|
|
|
log_func=self.safe_log,
|
|
|
|
|
handle_exception_func=self.handle_exception
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_embedding_config(self):
|
|
|
|
|
"""
|
|
|
|
|
测试当前的Embedding配置是否可用
|
|
|
|
|
"""
|
|
|
|
|
api_key = self.embedding_api_key_var.get().strip()
|
|
|
|
|
base_url = self.embedding_url_var.get().strip()
|
|
|
|
|
interface_format = self.embedding_interface_format_var.get().strip()
|
|
|
|
|
model_name = self.embedding_model_name_var.get().strip()
|
|
|
|
|
|
|
|
|
|
test_embedding_config(
|
|
|
|
|
api_key=api_key,
|
|
|
|
|
base_url=base_url,
|
|
|
|
|
interface_format=interface_format,
|
|
|
|
|
model_name=model_name,
|
|
|
|
|
log_func=self.safe_log,
|
|
|
|
|
handle_exception_func=self.handle_exception
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def browse_folder(self):
|
|
|
|
|
selected_dir = filedialog.askdirectory()
|
|
|
|
|
if selected_dir:
|
|
|
|
|
self.filepath_var.set(selected_dir)
|
|
|
|
|
|
|
|
|
|
# ----------------- 将导入的各模块函数直接赋给类方法 -----------------
|
|
|
|
|
generate_novel_architecture_ui = generate_novel_architecture_ui
|
|
|
|
|
generate_chapter_blueprint_ui = generate_chapter_blueprint_ui
|
|
|
|
|
generate_chapter_draft_ui = generate_chapter_draft_ui
|
|
|
|
|
finalize_chapter_ui = finalize_chapter_ui
|
|
|
|
|
do_consistency_check = do_consistency_check
|
|
|
|
|
import_knowledge_handler = import_knowledge_handler
|
|
|
|
|
clear_vectorstore_handler = clear_vectorstore_handler
|
|
|
|
|
show_plot_arcs_ui = show_plot_arcs_ui
|
|
|
|
|
load_config_btn = load_config_btn
|
|
|
|
|
save_config_btn = save_config_btn
|
|
|
|
|
load_novel_architecture = load_novel_architecture
|
|
|
|
|
save_novel_architecture = save_novel_architecture
|
|
|
|
|
load_chapter_blueprint = load_chapter_blueprint
|
|
|
|
|
save_chapter_blueprint = save_chapter_blueprint
|
|
|
|
|
load_character_state = load_character_state
|
|
|
|
|
save_character_state = save_character_state
|
|
|
|
|
load_global_summary = load_global_summary
|
|
|
|
|
save_global_summary = save_global_summary
|
|
|
|
|
refresh_chapters_list = refresh_chapters_list
|
|
|
|
|
on_chapter_selected = on_chapter_selected
|
|
|
|
|
save_current_chapter = save_current_chapter
|
|
|
|
|
prev_chapter = prev_chapter
|
|
|
|
|
next_chapter = next_chapter
|
|
|
|
|
test_llm_config = test_llm_config
|
|
|
|
|
test_embedding_config = test_embedding_config
|
|
|
|
|
browse_folder = browse_folder
|