refactor: Restructure project into core, services, ui, and utils directories, moving existing modules and adding new UI tabs.

This commit is contained in:
tinix-ai
2026-03-17 14:54:16 +07:00
parent 6699c516cd
commit a40d8ef4a6
28 changed files with 1970 additions and 953 deletions
View File
+362
View File
@@ -0,0 +1,362 @@
import gradio as gr
from datetime import datetime
from locales.i18n import t
from services.project_manager import ProjectManager, list_project_titles
from services.novel_generator import Chapter
from core.state import app_state
import logging
logger = logging.getLogger(__name__)
def build_continue_tab():
with gr.Tab(t("tabs.continue_tab")):
gr.Markdown(f"### {t('continue_tab.header')}")
with gr.Accordion("📂 1. Quản lý dự án", open=True):
with gr.Row():
with gr.Column(scale=4):
project_choices = list_project_titles()
continue_project_selector = gr.Dropdown(
choices=project_choices,
label=t("continue_tab.select_project"),
interactive=True
)
with gr.Column(scale=1, min_width=100):
refresh_continue_btn = gr.Button(t("continue_tab.load_btn"), size="lg")
with gr.Accordion("⚙️ 2. Thông tin & Cài đặt", open=False):
with gr.Row():
with gr.Column(scale=1):
continue_project_info = gr.Markdown(t("continue_tab.no_project_loaded"))
with gr.Column(scale=2):
continue_outline_display = gr.Textbox(
label=t("continue_tab.outline_label"),
interactive=False, lines=6, max_lines=12,
value="Chưa tải dự án..."
)
with gr.Row():
with gr.Column(scale=1):
memory_type = gr.Radio(label="Cách nhớ ngữ cảnh", choices=["Toàn văn", "Tóm tắt"], value="Toàn văn")
with gr.Column(scale=1):
memory_chapters = gr.Number(label="Số chương ghi nhớ", value=3, minimum=1, maximum=20, step=1)
with gr.Row():
continue_target_words = gr.Number(label=t("rewrite.target_words"), value=3000, minimum=100, maximum=50000, step=100)
continue_custom_prompt = gr.Textbox(label=t("create.custom_prompt_label"), placeholder=t("create.custom_prompt_placeholder"), scale=2)
with gr.Accordion("🚀 3. Sáng tác tiếp", open=False):
with gr.Row():
continue_chapter_num = gr.Number(label="Chương số", value=1, minimum=1, scale=1)
continue_chapter_title = gr.Textbox(label="Tiêu đề chương", placeholder="Nhập tiêu đề chương...", scale=2)
continue_chapter_desc = gr.Textbox(
label="Nội dung/Dàn ý chương này",
lines=3, placeholder="Nhập mô tả nội dung chương..."
)
with gr.Row():
continue_generate_btn = gr.Button(t("continue_tab.continue_gen_btn"), variant="primary", size="lg", scale=1)
continue_auto_btn = gr.Button("Tự động viết nốt các chương còn lại", variant="primary", size="lg", scale=2)
continue_stop_btn = gr.Button("Dừng tự động", variant="stop", size="lg", scale=1)
with gr.Row():
with gr.Column(scale=1):
continue_status = gr.Textbox(label=t("continue_tab.gen_status"), interactive=False, lines=15)
with gr.Column(scale=2):
continue_chapter_selector = gr.Dropdown(label="Danh sách chương đã tạo", choices=[], interactive=True)
continue_content_display = gr.Textbox(label="Nội dung chương", lines=15, interactive=False)
def on_refresh_continue(current_title):
titles = list_project_titles()
if current_title in titles:
info, next_ch, outline_text, chapter_choices = on_continue_project_select(current_title)
return gr.update(choices=titles, value=current_title), info, next_ch, outline_text, gr.update(choices=chapter_choices, value=chapter_choices[-1] if chapter_choices else None)
else:
return gr.update(choices=titles, value=None), t("continue_tab.no_project_loaded"), 1, "Chưa tải dự án...", gr.update(choices=[], value=None)
def on_continue_project_select(project_title):
if not project_title:
return t("continue_tab.no_project_loaded"), 1, "Chưa tải dự án...", []
try:
project_data = ProjectManager.get_project_by_title(project_title)
if not project_data:
return f"{t('continue_tab.project_not_found')}", 1, "Chưa tải dự án..."
project_id = project_data.get("id")
project, msg = ProjectManager.load_project(project_id)
if project:
app_state.current_project = project
completed = project.get_completed_count()
total_chapters = len(project.chapters)
next_ch = completed + 1
percent = f"{(completed / total_chapters * 100):.1f}" if total_chapters > 0 else "0.0"
# Handle None values safely
char_str = project.character_setting or ""
world_str = project.world_setting or ""
plot_str = project.plot_idea or ""
char_fmt = char_str[:100] + "..." if len(char_str) > 100 else char_str
world_fmt = world_str[:100] + "..." if len(world_str) > 100 else world_str
plot_fmt = plot_str[:100] + "..." if len(plot_str) > 100 else plot_str
# Logic fallback
info_template = t("continue_tab.info_template")
if isinstance(info_template, str) and '{' in info_template:
info = info_template.format(
title=project.title,
genre=project.genre,
completed=completed,
total=total_chapters,
words=project.get_total_words(),
percent=percent,
char=char_fmt,
world=world_fmt,
plot=plot_fmt
)
else:
info = f"### 📖 {project.title}\n**Thể loại**: {project.genre}\n**Tiến độ**: {percent}% ({completed}/{total_chapters} chương)\n**Tổng số từ**: {project.get_total_words()}\n💡 Chương tiếp theo: {next_ch}"
# Format the outline list
outline_lines = []
for ch in project.chapters:
status = "" if getattr(ch, 'content', None) else ""
outline_lines.append(f"{status} Chương {ch.num}: {ch.title} - {ch.desc}")
outline_text = "\n".join(outline_lines)
if not outline_text:
outline_text = "Dự án này chưa có dàn ý chi tiết."
# Generate chapter choices
chapter_choices = [f"Chương {ch.num}: {ch.title}" for ch in project.chapters if getattr(ch, 'content', None)]
return info, next_ch, outline_text, chapter_choices
return f"{msg}", 1, "Chưa tải dự án...", []
except Exception as e:
return f"{str(e)}", 1, "Lỗi khi tải dàn ý", []
def on_continue_chapter_select(chapter_title):
if not chapter_title or not app_state.current_project:
return ""
for ch in app_state.current_project.chapters:
if f"Chương {ch.num}: {ch.title}" == chapter_title:
return ch.content or ""
return ""
def on_continue_generate(project_title, ch_num, ch_title, ch_desc, target_words, custom_prompt, mem_type, mem_chaps):
yield "⏳ Đang chuẩn bị dữ liệu...", "", gr.update(interactive=False), gr.update(), gr.update()
if not app_state.current_project:
yield f"{t('continue_tab.no_project_selected')}", "", gr.update(interactive=True), gr.update(), gr.update()
return
gen = app_state.get_generator()
project = app_state.current_project
# Check if the requested chapter exists in the outline
if not any(int(ch.num) == int(ch_num) for ch in project.chapters):
yield f"❌ Lỗi: Chương {int(ch_num)} không có trong dàn ý. Tab này chỉ hỗ trợ hoàn thành nốt nội dung dàn ý đã có, không viết thêm chương mới.", "", gr.update(interactive=True), gr.update(), gr.update()
return
# Get completed past chapters mapped linearly
all_past_chapters = [ch for ch in project.chapters if ch.num < int(ch_num) and getattr(ch, 'content', None)]
mem_ch = int(mem_chaps) if mem_chaps else 3
past_chapters = all_past_chapters[-mem_ch:] if len(all_past_chapters) > mem_ch else all_past_chapters
prev_content = ""
context_summary = ""
if mem_type == "Toàn văn":
prev_texts = [c.content for c in past_chapters]
prev_content = "\n\n".join(prev_texts)
prev_content = prev_content[-4000:]
else:
summaries = []
for c in past_chapters:
if not hasattr(c, 'summary') or not c.summary:
yield f"⏳ Đang tạo tóm tắt ngữ cảnh cho chương {c.num}...", ""
summ, _ = gen.generate_chapter_summary(c.content, c.title)
c.summary = summ
if c.summary:
summaries.append(f"Chương {c.num} - {c.title}: {c.summary}")
if summaries:
context_summary = "\n".join(summaries)
if all_past_chapters:
prev_content = all_past_chapters[-1].content[-1500:]
yield f"⏳ Đang sinh Chương {int(ch_num)}...", "", gr.update(interactive=False), gr.update(), gr.update()
content, msg = gen.generate_chapter(
chapter_num=int(ch_num), chapter_title=ch_title,
chapter_desc=ch_desc, novel_title=project.title,
character_setting=project.character_setting,
world_setting=project.world_setting,
plot_idea=project.plot_idea, genre=project.genre,
sub_genres=project.sub_genres,
previous_content=prev_content, context_summary=context_summary, custom_prompt=custom_prompt
)
if content:
new_ch = Chapter(
num=int(ch_num), title=ch_title, desc=ch_desc,
content=content, word_count=len(content),
generated_at=datetime.now().isoformat()
)
found = False
for i, ch in enumerate(project.chapters):
if ch.num == int(ch_num):
project.chapters[i] = new_ch
found = True
break
if not found:
project.chapters.append(new_ch)
project.chapters.sort(key=lambda x: x.num)
ProjectManager.save_project(project)
# Format the outline list
outline_lines = []
for ch in project.chapters:
status = "" if getattr(ch, 'content', None) else ""
outline_lines.append(f"{status} Chương {ch.num}: {ch.title} - {ch.desc}")
outline_text = "\n".join(outline_lines)
# Update choices
chapter_choices = [f"Chương {ch.num}: {ch.title}" for ch in project.chapters if getattr(ch, 'content', None)]
selected_choice = f"Chương {int(ch_num)}: {ch_title}"
yield f"✅ Chương {int(ch_num)} đã sinh ({len(content)} từ)", content, gr.update(interactive=True), gr.update(value=outline_text), gr.update(choices=chapter_choices, value=selected_choice)
else:
yield f"{msg}", "", gr.update(interactive=True), gr.update(), gr.update()
def on_continue_auto_generate(project_title, target_words, custom_prompt, mem_type, mem_chaps, progress=gr.Progress()):
yield "⏳ Đang chuẩn bị dữ liệu...", "", gr.update(interactive=False), gr.update(interactive=False), gr.update(), gr.update()
if not app_state.current_project:
yield f"{t('continue_tab.no_project_selected')}", "", gr.update(interactive=True), gr.update(interactive=True), gr.update(), gr.update()
return
gen = app_state.get_generator()
project = app_state.current_project
app_state.is_generating = True
app_state.stop_requested = False
blank_chapters = [ch for ch in project.chapters if not getattr(ch, 'content', None)]
if not blank_chapters:
yield "✅ Đã viết xong tất cả các chương trong dàn ý!", "", gr.update(interactive=True), gr.update(interactive=True), gr.update(), gr.update()
return
results = [f"📋 Phát hiện {len(blank_chapters)} chương chưa viết. Bắt đầu tự động tạo..."]
yield "\n".join(results), "", gr.update(interactive=False), gr.update(interactive=False), gr.update(), gr.update()
last_content = ""
for i, ch in enumerate(blank_chapters):
if app_state.stop_requested:
results.append("\n⚠️ Đã dừng sinh tự động!")
yield "\n".join(results), last_content, gr.update(interactive=True), gr.update(interactive=True), gr.update(), gr.update()
break
results.append(f"\n✍️ Đang sinh Chương {ch.num}: {ch.title}...")
progress((i + 1) / len(blank_chapters))
yield "\n".join(results), last_content, gr.update(interactive=False), gr.update(interactive=False), gr.update(), gr.update()
all_past_chapters = [past for past in project.chapters if past.num < ch.num and getattr(past, 'content', None)]
mem_ch = int(mem_chaps) if mem_chaps else 3
past_chapters = all_past_chapters[-mem_ch:] if len(all_past_chapters) > mem_ch else all_past_chapters
prev_content = ""
context_summary = ""
if mem_type == "Toàn văn":
prev_texts = [c.content for c in past_chapters]
prev_content = "\n\n".join(prev_texts)
prev_content = prev_content[-4000:]
else:
summaries = []
for c in past_chapters:
if not hasattr(c, 'summary') or not c.summary:
summ, _ = gen.generate_chapter_summary(c.content, c.title)
c.summary = summ
if c.summary:
summaries.append(f"Chương {c.num} - {c.title}: {c.summary}")
if summaries:
context_summary = "\n".join(summaries)
if all_past_chapters:
prev_content = all_past_chapters[-1].content[-1500:]
content, msg = gen.generate_chapter(
chapter_num=int(ch.num), chapter_title=ch.title,
chapter_desc=ch.desc, novel_title=project.title,
character_setting=project.character_setting,
world_setting=project.world_setting,
plot_idea=project.plot_idea, genre=project.genre,
sub_genres=project.sub_genres,
previous_content=prev_content, context_summary=context_summary, custom_prompt=custom_prompt
)
if content:
ch.content = content
ch.word_count = len(content)
ch.generated_at = datetime.now().isoformat()
ProjectManager.save_project(project)
last_content = content
# Format the outline list
outline_lines = []
for pr_ch in project.chapters:
status = "" if getattr(pr_ch, 'content', None) else ""
outline_lines.append(f"{status} Chương {pr_ch.num}: {pr_ch.title} - {pr_ch.desc}")
outline_text = "\n".join(outline_lines)
# Update choices
chapter_choices = [f"Chương {ch.num}: {ch.title}" for ch in project.chapters if getattr(ch, 'content', None)]
selected_choice = f"Chương {ch.num}: {ch.title}"
results.append(f"✅ Chương {ch.num} hoàn tất ({len(content)} từ)")
yield "\n".join(results), content, gr.update(interactive=False), gr.update(interactive=False), gr.update(value=outline_text), gr.update(choices=chapter_choices, value=selected_choice)
else:
results.append(f"❌ Lỗi ở chương {ch.num}: {msg}")
app_state.stop_requested = True
yield "\n".join(results), last_content, gr.update(interactive=True), gr.update(interactive=True), gr.update(), gr.update()
break
app_state.is_generating = False
results.append("\n🎉 Hoàn thành chuỗi viết tự động!")
yield "\n".join(results), last_content, gr.update(interactive=True), gr.update(interactive=True), gr.update(), gr.update()
def on_continue_stop():
app_state.stop_requested = True
return "⏸️ Đang dừng..."
refresh_continue_btn.click(
fn=on_refresh_continue,
inputs=[continue_project_selector],
outputs=[continue_project_selector, continue_project_info, continue_chapter_num, continue_outline_display, continue_chapter_selector]
)
continue_project_selector.change(
fn=on_continue_project_select,
inputs=[continue_project_selector],
outputs=[continue_project_info, continue_chapter_num, continue_outline_display, continue_chapter_selector]
)
continue_chapter_selector.change(
fn=on_continue_chapter_select,
inputs=[continue_chapter_selector],
outputs=[continue_content_display]
)
continue_generate_btn.click(
fn=on_continue_generate,
inputs=[continue_project_selector, continue_chapter_num, continue_chapter_title,
continue_chapter_desc, continue_target_words, continue_custom_prompt, memory_type, memory_chapters],
outputs=[continue_status, continue_content_display, continue_generate_btn, continue_outline_display, continue_chapter_selector]
)
continue_auto_btn.click(
fn=on_continue_auto_generate,
inputs=[continue_project_selector, continue_target_words, continue_custom_prompt, memory_type, memory_chapters],
outputs=[continue_status, continue_content_display, continue_auto_btn, continue_generate_btn, continue_outline_display, continue_chapter_selector],
show_progress="full"
)
continue_stop_btn.click(
fn=on_continue_stop,
outputs=[continue_status]
)
+400
View File
@@ -0,0 +1,400 @@
import gradio as gr
from datetime import datetime
import json
import re
import traceback
from locales.i18n import t
from services.novel_generator import OutlineParser
from services.project_manager import ProjectManager
from services.genre_manager import GenreManager
from services.sub_genre_manager import SubGenreManager
from core.state import app_state
import logging
logger = logging.getLogger(__name__)
def build_create_tab():
"""Xây dựng giao diện Tab Sáng tác từ đầu"""
with gr.Tab(t("tabs.create")):
gr.Markdown(f"### 🪄 {t('create.header')}")
with gr.Accordion("📌 1. Thông tin cơ bản", open=True):
with gr.Row():
with gr.Column(scale=1):
genre_choices = GenreManager.get_genre_names()
genre_dropdown = gr.Dropdown(
choices=genre_choices,
value=genre_choices[0] if genre_choices else None,
label=t("create.genre_label"),
interactive=True
)
sub_genre_choices = SubGenreManager.get_sub_genre_names()
sub_genre_dropdown = gr.Dropdown(
choices=sub_genre_choices,
label=t("create.sub_genres_label"),
multiselect=True,
interactive=True
)
with gr.Column(scale=2):
title_input = gr.Textbox(
label=t("create.novel_title"),
placeholder=t("create.novel_title_default"),
interactive=True
)
with gr.Group():
suggested_titles_radio = gr.Radio(
label=t("create.suggested_titles_radio") if t("create.suggested_titles_radio") != "create.suggested_titles_radio" else "Gợi ý tên truyện",
choices=[],
visible=False,
interactive=True
)
with gr.Row():
suggest_title_prompt = gr.Textbox(
show_label=False,
placeholder=t("create.custom_prompt_placeholder"),
lines=1, interactive=True, scale=4
)
suggest_title_btn = gr.Button(t("create.suggest_title_btn"), variant="secondary", size="sm", scale=1)
suggest_title_status = gr.Textbox(show_label=False, interactive=False, visible=False)
with gr.Accordion("🎭 2. Thiết lập chi tiết", open=False):
with gr.Row():
with gr.Column():
character_input = gr.Textbox(
label=t("create.char_setting"),
placeholder=t("create.char_setting_placeholder"),
lines=5, interactive=False
)
with gr.Group():
with gr.Row():
num_main_chars = gr.Number(label=t("create.num_main_chars"), value=2, minimum=1, maximum=10, step=1, scale=1)
num_sub_chars = gr.Number(label=t("create.num_sub_chars"), value=3, minimum=0, maximum=20, step=1, scale=1)
with gr.Row():
suggest_char_prompt = gr.Textbox(show_label=False, placeholder=t("create.custom_prompt_placeholder"), lines=1, interactive=False, scale=4)
suggest_char_btn = gr.Button(t("create.suggest_btn"), variant="secondary", size="sm", scale=1, interactive=False)
suggest_char_status = gr.Textbox(show_label=False, interactive=False, visible=False)
with gr.Column():
world_input = gr.Textbox(
label=t("create.world_setting"),
placeholder=t("create.world_setting_placeholder"),
lines=5, interactive=False
)
with gr.Group():
with gr.Row():
suggest_world_prompt = gr.Textbox(show_label=False, placeholder=t("create.custom_prompt_placeholder"), lines=1, interactive=False, scale=4)
suggest_world_btn = gr.Button(t("create.suggest_btn"), variant="secondary", size="sm", scale=1, interactive=False)
suggest_world_status = gr.Textbox(show_label=False, interactive=False, visible=False)
plot_input = gr.Textbox(
label=t("create.plot_idea"),
placeholder=t("create.plot_idea_placeholder"),
lines=3, interactive=False
)
with gr.Group():
with gr.Row():
suggest_plot_prompt = gr.Textbox(show_label=False, placeholder=t("create.custom_prompt_placeholder"), lines=1, interactive=False, scale=4)
suggest_plot_btn = gr.Button(t("create.suggest_plot_btn"), variant="secondary", size="sm", scale=1, interactive=False)
suggest_plot_status = gr.Textbox(show_label=False, interactive=False, visible=False)
with gr.Accordion("📝 3. Dàn ý truyện", open=False):
with gr.Row():
total_chapters = gr.Number(
label=t("create.chapter_count"), value=20, minimum=1, maximum=200, step=1, scale=1, interactive=False
)
generate_outline_btn = gr.Button(t("create.gen_outline_btn"), variant="primary", size="lg", scale=3, interactive=False)
outline_output = gr.Textbox(label=t("create.outline_display"), lines=15, interactive=True)
outline_status = gr.Textbox(label=t("create.gen_status"), interactive=False)
with gr.Accordion("🚀 4. Sáng tác tự động", open=False):
with gr.Row():
with gr.Column(scale=1):
memory_type = gr.Radio(label="Cách nhớ ngữ cảnh", choices=["Toàn văn", "Tóm tắt"], value="Toàn văn")
with gr.Column(scale=1):
memory_chapters = gr.Number(label="Số chương ghi nhớ", value=3, minimum=1, maximum=20, step=1)
with gr.Row():
auto_generate_btn = gr.Button(t("create.start_gen_btn"), variant="primary", size="lg", interactive=False)
stop_btn = gr.Button(t("create.pause_gen_btn"), variant="stop", size="lg", interactive=False)
with gr.Row():
with gr.Column(scale=1):
generation_progress = gr.Textbox(label=t("create.gen_status"), lines=15, interactive=False)
with gr.Column(scale=2):
chapter_selector = gr.Dropdown(label="Danh sách chương đã tạo", choices=[], interactive=True)
chapter_content_display = gr.Textbox(label="Nội dung chương", lines=15, interactive=False)
def on_suggest_title(genre, sub_genres, custom_prompt):
yield gr.Radio(visible=False), gr.update(value="⏳ Đang gọi AI xử lý... Vui lòng chờ.", visible=True), gr.update(interactive=False)
try:
gen = app_state.get_generator()
content, msg = gen.suggest_title(genre, sub_genres, custom_prompt)
if content:
try:
match = re.search(r'\[\s*\{[\s\S]*\}\s*\]', content)
if match:
data = {"suggestions": json.loads(match.group(0))}
else:
clean_content = re.sub(r'```(?:json)?\s*', '', content)
clean_content = re.sub(r'```\s*', '', clean_content)
data = json.loads(clean_content)
if "suggestions" in data:
choices = [f"{item.get('title', '')} - {item.get('description', '')}" for item in data["suggestions"]]
yield gr.Radio(choices=choices, visible=True), gr.update(visible=False), gr.update(interactive=True)
return
except Exception as e:
logger.error(f"Failed to parse title JSON: {e}")
yield gr.Radio(choices=[content, f"Parse error: {str(e)}"], visible=True), gr.update(visible=False), gr.update(interactive=True)
return
yield gr.Radio(choices=[msg], visible=True), gr.update(visible=False), gr.update(interactive=True)
except Exception as e:
traceback.print_exc()
yield gr.Radio(choices=[f"Lỗi: {str(e)}"], visible=True), gr.update(value=f"❌ Lỗi: {str(e)}", visible=True), gr.update(interactive=True)
def on_title_select(selected):
if selected and " - " in selected:
return selected.split(" - ", 1)[0]
return selected
def on_suggest_char(title, genre, sub_genres, custom_prompt, num_main, num_sub):
yield gr.update(), gr.update(value="⏳ Đang gọi AI xử lý... Vui lòng chờ.", visible=True), gr.update(interactive=False)
try:
gen = app_state.get_generator()
nm = int(num_main) if num_main else 2
ns = int(num_sub) if num_sub else 3
content, msg = gen.suggest_content("char", title, genre, sub_genres, "", "", custom_prompt, nm, ns)
yield gr.update(value=content if content else msg), gr.update(visible=False), gr.update(interactive=True)
except Exception as e:
traceback.print_exc()
yield gr.update(), gr.update(value=f"❌ Lỗi: {str(e)}", visible=True), gr.update(interactive=True)
def on_suggest_world(title, genre, sub_genres, custom_prompt):
yield gr.update(), gr.update(value="⏳ Đang gọi AI xử lý... Vui lòng chờ.", visible=True), gr.update(interactive=False)
try:
gen = app_state.get_generator()
content, msg = gen.suggest_content("world", title, genre, sub_genres, "", "", custom_prompt)
yield gr.update(value=content if content else msg), gr.update(visible=False), gr.update(interactive=True)
except Exception as e:
traceback.print_exc()
yield gr.update(), gr.update(value=f"❌ Lỗi: {str(e)}", visible=True), gr.update(interactive=True)
def on_suggest_plot(title, genre, sub_genres, char_setting, world_setting, custom_prompt):
yield gr.update(), gr.update(value="⏳ Đang gọi AI xử lý... Vui lòng chờ.", visible=True), gr.update(interactive=False)
try:
gen = app_state.get_generator()
content, msg = gen.suggest_content("plot", title, genre, sub_genres, char_setting, world_setting, custom_prompt)
yield gr.update(value=content if content else msg), gr.update(visible=False), gr.update(interactive=True)
except Exception as e:
traceback.print_exc()
yield gr.update(), gr.update(value=f"❌ Lỗi: {str(e)}", visible=True), gr.update(interactive=True)
def on_generate_outline(title, genre, sub_genres, num_chapters, char_setting, world_setting, plot_idea, progress=gr.Progress()):
progress(0.1, desc="Đang gọi AI...")
gen = app_state.get_generator()
content, msg = gen.generate_outline(
title, genre, sub_genres or [],
int(num_chapters), char_setting, world_setting, plot_idea
)
return content, msg
def on_auto_generate(title, genre, sub_genres, char_setting, world_setting, plot_idea, outline_text, mem_type, mem_chaps, progress=gr.Progress()):
"""Tự động tạo toàn bộ tiểu thuyết"""
gen = app_state.get_generator()
chapters, parse_msg = OutlineParser.parse(outline_text)
if not chapters:
yield f"{parse_msg}", gr.update()
return
project, create_msg = ProjectManager.create_project(
title, genre, sub_genres or [],
char_setting, world_setting, plot_idea
)
if not project:
yield f"{create_msg}", gr.update()
return
project.chapters = chapters
app_state.current_project = project
app_state.is_generating = True
app_state.stop_requested = False
# Save immediately to ensure chapter titles are stored even if generation is stopped early
ProjectManager.save_project(project)
results = [f"📋 Đã phân tích {len(chapters)} chương", f"💾 {create_msg}"]
yield "\n".join(results), gr.update(choices=[], value=None)
generated_chapters = []
for i, chapter in enumerate(chapters):
if app_state.stop_requested:
results.append("\n⚠️ Đã dừng sinh!")
yield "\n".join(results), gr.update(choices=generated_chapters)
break
results.append(f"\n✍️ Đang sinh Chương {chapter.num}: {chapter.title}...")
progress((i + 1) / len(chapters))
yield "\n".join(results), gr.update(choices=generated_chapters)
mem_ch = int(mem_chaps) if mem_chaps else 3
start_idx = max(0, i - mem_ch)
past_chapters = chapters[start_idx:i]
prev_content = ""
context_summary = ""
if mem_type == "Toàn văn":
prev_texts = [c.content for c in past_chapters if hasattr(c, 'content') and c.content]
prev_content = "\n\n".join(prev_texts)
prev_content = prev_content[-4000:]
else:
summaries = []
for c in past_chapters:
if not hasattr(c, 'content') or not c.content:
continue
if not hasattr(c, 'summary') or not c.summary:
summ, _ = gen.generate_chapter_summary(c.content, c.title)
c.summary = summ
if c.summary:
summaries.append(f"Chương {c.num} - {c.title}: {c.summary}")
if summaries:
context_summary = "\n".join(summaries)
if i > 0 and chapters[i-1].content:
prev_content = chapters[i-1].content[-1500:]
content, msg = gen.generate_chapter(
chapter_num=chapter.num, chapter_title=chapter.title,
chapter_desc=chapter.desc, novel_title=title,
character_setting=char_setting, world_setting=world_setting,
plot_idea=plot_idea, genre=genre, sub_genres=sub_genres,
previous_content=prev_content, context_summary=context_summary
)
if content:
chapter.content = content
chapter.word_count = len(content)
chapter.generated_at = datetime.now().isoformat()
results.append(f"✅ Chương {chapter.num}: {len(content)} từ")
ProjectManager.save_project(project)
chapter_name = f"Chương {chapter.num}: {chapter.title}"
generated_chapters.append(chapter_name)
yield "\n".join(results), gr.update(choices=generated_chapters, value=chapter_name)
else:
results.append(f"❌ Chương {chapter.num}: {msg}")
yield "\n".join(results), gr.update(choices=generated_chapters)
app_state.is_generating = False
total_words = sum(ch.word_count for ch in chapters if ch.content)
results.append(f"\n🎉 Hoàn thành! Tổng: {total_words} từ")
yield "\n".join(results), gr.update(choices=generated_chapters)
def on_chapter_select(chapter_title):
if not app_state.current_project or not chapter_title:
return ""
for ch in app_state.current_project.chapters:
if f"Chương {ch.num}: {ch.title}" == chapter_title:
return ch.content or ""
return ""
def on_stop():
app_state.stop_requested = True
return "⏸️ Đang dừng..."
# Bind events
suggest_title_btn.click(
fn=on_suggest_title,
inputs=[genre_dropdown, sub_genre_dropdown, suggest_title_prompt],
outputs=[suggested_titles_radio, suggest_title_status, suggest_title_btn]
)
suggested_titles_radio.change(
fn=on_title_select,
inputs=[suggested_titles_radio],
outputs=[title_input]
)
suggest_char_btn.click(
fn=on_suggest_char,
inputs=[title_input, genre_dropdown, sub_genre_dropdown, suggest_char_prompt, num_main_chars, num_sub_chars],
outputs=[character_input, suggest_char_status, suggest_char_btn]
)
suggest_world_btn.click(
fn=on_suggest_world,
inputs=[title_input, genre_dropdown, sub_genre_dropdown, suggest_world_prompt],
outputs=[world_input, suggest_world_status, suggest_world_btn]
)
suggest_plot_btn.click(
fn=on_suggest_plot,
inputs=[title_input, genre_dropdown, sub_genre_dropdown, character_input, world_input, suggest_plot_prompt],
outputs=[plot_input, suggest_plot_status, suggest_plot_btn]
)
generate_outline_btn.click(
fn=on_generate_outline,
inputs=[title_input, genre_dropdown, sub_genre_dropdown, total_chapters, character_input, world_input, plot_input],
outputs=[outline_output, outline_status],
show_progress="full"
)
auto_generate_btn.click(
fn=on_auto_generate,
inputs=[title_input, genre_dropdown, sub_genre_dropdown, character_input, world_input, plot_input, outline_output, memory_type, memory_chapters],
outputs=[generation_progress, chapter_selector]
)
chapter_selector.change(
fn=on_chapter_select,
inputs=[chapter_selector],
outputs=[chapter_content_display]
)
stop_btn.click(fn=on_stop, outputs=[generation_progress])
# Flow Enforcement Events
title_input.change(
fn=lambda t: [
gr.update(interactive=bool(t)),
gr.update(interactive=bool(t)),
gr.update(interactive=bool(t))
],
inputs=[title_input],
outputs=[character_input, suggest_char_prompt, suggest_char_btn]
)
character_input.change(
fn=lambda c: [
gr.update(interactive=bool(c)),
gr.update(interactive=bool(c)),
gr.update(interactive=bool(c))
],
inputs=[character_input],
outputs=[world_input, suggest_world_prompt, suggest_world_btn]
)
world_input.change(
fn=lambda w: [
gr.update(interactive=bool(w)),
gr.update(interactive=bool(w)),
gr.update(interactive=bool(w))
],
inputs=[world_input],
outputs=[plot_input, suggest_plot_prompt, suggest_plot_btn]
)
plot_input.change(
fn=lambda p: [
gr.update(interactive=bool(p)),
gr.update(interactive=bool(p))
],
inputs=[plot_input],
outputs=[total_chapters, generate_outline_btn]
)
outline_output.change(
fn=lambda o: [
gr.update(interactive=bool(o)),
gr.update(interactive=bool(o))
],
inputs=[outline_output],
outputs=[auto_generate_btn, stop_btn]
)
+92
View File
@@ -0,0 +1,92 @@
import gradio as gr
from locales.i18n import t
from services.project_manager import ProjectManager, list_project_titles
from utils.exporter import export_to_docx, export_to_txt, export_to_markdown, export_to_html
import logging
logger = logging.getLogger(__name__)
def build_export_tab():
with gr.Tab(t("tabs.export")):
gr.Markdown(f"### {t('export.header')}")
export_project_choices = list_project_titles()
export_project_selector = gr.Dropdown(
choices=export_project_choices,
label=t("projects.select_project"),
interactive=True
)
refresh_export_btn = gr.Button(t("projects.refresh_btn"), size="sm")
export_format = gr.Radio(
choices=[
t("create.export_format_word"),
t("create.export_format_txt"),
t("create.export_format_md"),
t("create.export_format_html")
],
value=t("create.export_format_txt"),
label=t("projects.export_format"),
interactive=True
)
export_btn = gr.Button(t("projects.export_btn"), variant="primary", size="lg")
export_status = gr.Textbox(label=t("projects.export_status"), interactive=False)
export_download = gr.File(label=t("projects.download_file"), interactive=False)
def on_refresh_export():
titles = list_project_titles()
return gr.update(choices=titles, value=None)
def on_export(project_title, format_type):
if not project_title:
return f"{t('projects.select_project_first')}", None
try:
project_data = ProjectManager.get_project_by_title(project_title)
if not project_data:
return f"{t('projects.project_not_found')}", None
project_id = project_data.get("id")
project, msg = ProjectManager.load_project(project_id)
if not project:
return f"{msg}", None
full_text = f"# {project.title}\n\n"
for ch in project.chapters:
if ch.content:
full_text += f"## Chương {ch.num}: {ch.title}\n\n"
full_text += ch.content + "\n\n"
if len(full_text.strip()) < 50:
return f"{t('ui.no_content_export')}", None
# Map format
format_map = {
t("create.export_format_word"): "docx",
t("create.export_format_txt"): "txt",
t("create.export_format_md"): "md",
t("create.export_format_html"): "html"
}
fmt = format_map.get(format_type, "txt")
if fmt == "docx":
filepath, exp_msg = export_to_docx(full_text, project.title)
elif fmt == "txt":
filepath, exp_msg = export_to_txt(full_text, project.title)
elif fmt == "md":
filepath, exp_msg = export_to_markdown(full_text, project.title)
elif fmt == "html":
filepath, exp_msg = export_to_html(full_text, project.title)
else:
return f"{t('ui.unsupported_format', format=fmt)}", None
if filepath:
return f"{exp_msg}", filepath
return f"{exp_msg}", None
except Exception as e:
logger.error(f"Export failed: {e}", exc_info=True)
return f"{t('ui.export_failed', error=str(e))}", None
refresh_export_btn.click(fn=on_refresh_export, outputs=[export_project_selector])
export_btn.click(fn=on_export, inputs=[export_project_selector, export_format], outputs=[export_status, export_download])
+61
View File
@@ -0,0 +1,61 @@
import gradio as gr
from locales.i18n import t
from utils.file_parser import parse_novel_file
from core.state import app_state
import logging
logger = logging.getLogger(__name__)
def build_polish_tab():
with gr.Tab(t("tabs.polish")):
gr.Markdown(f"### {t('polish.header')}")
polish_file_input = gr.File(label=t("polish.upload_file"), file_types=[".txt", ".docx", ".md", ".pdf"])
polish_input = gr.Textbox(
label=t("polish.original_text"),
lines=10, placeholder="Dán nội dung cần đánh bóng..."
)
polish_custom_req = gr.Textbox(
label=t("polish.custom_req"),
placeholder=t("polish.custom_req_placeholder"),
lines=2
)
with gr.Row():
polish_btn = gr.Button(t("polish.start_polish"), variant="primary")
polish_suggest_btn = gr.Button(t("polish.polish_suggest_btn"), variant="secondary")
polish_status = gr.Textbox(label=t("polish.polish_status"), interactive=False)
polish_output = gr.Textbox(label=t("polish.polished_text"), lines=15, interactive=True)
def on_polish_file_upload(file):
if file is None:
return ""
try:
content = parse_novel_file(file.name)
return content
except Exception as e:
return f"{str(e)}"
def on_polish(text, custom_req):
yield "⏳ Đang gọi AI xử lý... Vui lòng chờ.", gr.update(), gr.update(interactive=False)
gen = app_state.get_generator()
content, msg = gen.polish_text(text, custom_requirements=custom_req)
if content:
yield f"{msg}", content, gr.update(interactive=True)
else:
yield f"{msg}", gr.update(), gr.update(interactive=True)
def on_polish_suggest(text, custom_req):
yield "⏳ Đang gọi AI xử lý... Vui lòng chờ.", gr.update(), gr.update(interactive=False)
gen = app_state.get_generator()
content, msg = gen.polish_and_suggest(text, custom_requirements=custom_req)
if content:
yield f"{msg}", content, gr.update(interactive=True)
else:
yield f"{msg}", gr.update(), gr.update(interactive=True)
polish_file_input.change(fn=on_polish_file_upload, inputs=[polish_file_input], outputs=[polish_input])
polish_btn.click(fn=on_polish, inputs=[polish_input, polish_custom_req], outputs=[polish_status, polish_output, polish_btn])
polish_suggest_btn.click(fn=on_polish_suggest, inputs=[polish_input, polish_custom_req], outputs=[polish_status, polish_output, polish_suggest_btn])
+69
View File
@@ -0,0 +1,69 @@
import gradio as gr
from locales.i18n import t
from services.project_manager import ProjectManager, list_project_titles
import logging
logger = logging.getLogger(__name__)
def build_projects_tab():
with gr.Tab(t("tabs.projects")):
gr.Markdown(f"### {t('projects.header')}")
projects_table = gr.Dataframe(
headers=["ID", t("ui.col_project_name"), t("ui.col_type"), t("ui.col_created_at"), t("ui.col_chapters")],
interactive=False
)
with gr.Row():
refresh_projects_btn = gr.Button(t("projects.refresh_btn"))
delete_project_btn = gr.Button(t("projects.delete_btn"), variant="stop")
gr.Markdown(f"#### {t('projects.delete_header')}")
delete_project_selector = gr.Dropdown(
choices=list_project_titles(),
label=t("projects.delete_select_project"),
interactive=True
)
project_manage_status = gr.Textbox(label=t("projects.status_label"), interactive=False)
def on_refresh_projects():
try:
projects = ProjectManager.list_projects()
table_data = []
for p in projects:
table_data.append([
p.get("id", ""),
p.get("title", ""),
p.get("genre", ""),
p.get("created_at", "")[:10] if p.get("created_at") else "",
f"{p.get('completed_chapters', 0)}/{p.get('chapter_count', 0)}"
])
titles = [p.get("title", "") for p in projects]
return table_data, gr.update(choices=titles, value=None)
except Exception as e:
logger.error(f"Refresh projects failed: {e}")
return [], gr.update()
def on_delete_project(project_title):
if not project_title or not project_title.strip():
return f"{t('projects.select_project_first')}", gr.update(), gr.update()
try:
project_data = ProjectManager.get_project_by_title(project_title)
if not project_data:
return f"{t('projects.project_not_found')}", gr.update(), gr.update()
project_id = project_data.get("id")
success, msg = ProjectManager.delete_project(project_id)
if success:
new_table, new_dropdown = on_refresh_projects()
return f"{t('projects.delete_success')}", new_table, new_dropdown
return f"{t('projects.delete_failed')}: {msg}", gr.update(), gr.update()
except Exception as e:
return f"{str(e)}", gr.update(), gr.update()
refresh_projects_btn.click(fn=on_refresh_projects, outputs=[projects_table, delete_project_selector])
delete_project_btn.click(
fn=on_delete_project,
inputs=[delete_project_selector],
outputs=[project_manage_status, projects_table, delete_project_selector]
)
+52
View File
@@ -0,0 +1,52 @@
import gradio as gr
from locales.i18n import t
from services.novel_generator import get_preset_templates
from utils.file_parser import parse_novel_file
from core.state import app_state
import logging
logger = logging.getLogger(__name__)
def build_rewrite_tab():
with gr.Tab(t("tabs.rewrite")):
gr.Markdown(f"### {t('rewrite.header')}")
rewrite_file_input = gr.File(label=t("rewrite.upload_file"), file_types=[".txt", ".docx", ".md", ".pdf"])
rewrite_input = gr.Textbox(
label=t("polish.original_text"),
lines=10, placeholder="Dán nội dung cần viết lại..."
)
preset_choices = list(get_preset_templates().keys())
rewrite_style = gr.Dropdown(
choices=preset_choices,
label=t("rewrite.preset_style"),
value=preset_choices[0] if preset_choices else None,
interactive=True
)
rewrite_btn = gr.Button(t("rewrite.start_rewrite"), variant="primary")
rewrite_status = gr.Textbox(label=t("rewrite.parse_status"), interactive=False)
rewrite_output = gr.Textbox(label=t("rewrite.full_rewritten"), lines=15, interactive=True)
def on_file_upload(file):
if file is None:
return ""
try:
content = parse_novel_file(file.name)
return content
except Exception as e:
return f"{str(e)}"
def on_rewrite(text, style):
yield "⏳ Đang gọi AI xử lý... Vui lòng chờ.", gr.update(), gr.update(interactive=False)
instructions = get_preset_templates().get(style, "")
gen = app_state.get_generator()
content, msg = gen.rewrite_paragraph(text, instructions)
if content:
yield f"{msg}", content, gr.update(interactive=True)
else:
yield f"{msg}", gr.update(), gr.update(interactive=True)
rewrite_file_input.change(fn=on_file_upload, inputs=[rewrite_file_input], outputs=[rewrite_input])
rewrite_btn.click(fn=on_rewrite, inputs=[rewrite_input, rewrite_style], outputs=[rewrite_status, rewrite_output, rewrite_btn])
+298
View File
@@ -0,0 +1,298 @@
import gradio as gr
from locales.i18n import t
from core.config import get_config, API_PROVIDERS
from core.config_api import ConfigAPIManager
from services.api_client import get_api_client, reinit_api_client
from services.novel_generator import get_cache_size, list_generation_caches, clear_generation_cache
from services.genre_manager import GenreManager
from services.sub_genre_manager import SubGenreManager
from core.state import app_state
def build_settings_tab():
with gr.Tab(t("tabs.settings")):
gr.Markdown(f"### {t('settings.header')}")
with gr.Tabs():
# Sub-tab: Quản lý giao diện API
with gr.Tab(t("settings.tab_backends")):
gr.Markdown(f"#### {t('settings.backends_header')}")
backends_display = gr.Markdown("...")
def get_backends_display():
try:
result = ConfigAPIManager.list_backends()
if result["success"] and result["data"]:
lines = [f"### 📋 {len(result['data'])} backend(s)\n"]
for b in result["data"]:
enabled = "" if b.get("enabled") else ""
default = "" if b.get("is_default") else ""
lines.append(f"{enabled} **{b['name']}**{default}")
lines.append(f" - Model: `{b.get('model', '')}`")
lines.append(f" - URL: `{b.get('base_url', '')[:50]}...`")
lines.append(f" - Timeout: {b.get('timeout', 30)}s")
lines.append("")
return "\n".join(lines)
return t("app.no_backends_warning")
except Exception as e:
return f"{str(e)}"
gr.Markdown("---")
gr.Markdown(f"#### {t('settings.add_backend_header')}")
provider_names = [API_PROVIDERS[k]["name"] for k in API_PROVIDERS]
with gr.Row():
api_provider_dropdown = gr.Dropdown(
choices=provider_names,
label=t("settings.provider_label"),
info=t("settings.provider_info"),
interactive=True
)
with gr.Row():
api_name_input = gr.Textbox(label=t("settings.backend_name"), placeholder=t("settings.backend_name_placeholder"))
api_type_dropdown = gr.Dropdown(
choices=ConfigAPIManager.get_backend_types(),
value="openai", label=t("settings.backend_type"), interactive=True
)
with gr.Row():
api_url_input = gr.Textbox(label=t("settings.base_url"), placeholder=t("settings.base_url_placeholder"))
api_key_input = gr.Textbox(label=t("settings.api_key"), placeholder=t("settings.api_key_placeholder"), type="password")
with gr.Row():
api_model_input = gr.Textbox(label=t("settings.model_name"), placeholder=t("settings.model_name_placeholder"))
api_timeout_input = gr.Slider(minimum=5, maximum=600, value=120, step=5, label=t("settings.timeout"))
with gr.Row():
api_save_btn = gr.Button(t("settings.add_btn"), variant="primary")
api_refresh_btn = gr.Button(t("settings.refresh_list"), size="sm")
api_status = gr.Textbox(label=t("settings.operation_result"), interactive=False)
gr.Markdown("---")
gr.Markdown(f"#### {t('settings.test_manage_header')}")
with gr.Row():
test_name_input = gr.Textbox(label=t("settings.test_backend_name"), placeholder=t("settings.test_backend_placeholder"))
api_test_btn = gr.Button(t("settings.test_btn"), variant="secondary")
with gr.Row():
delete_name_input = gr.Textbox(label=t("settings.delete_backend_name"), placeholder=t("settings.delete_backend_placeholder"))
api_delete_btn = gr.Button(t("settings.delete_btn"), variant="stop")
test_result = gr.Textbox(label=t("settings.test_result"), interactive=False)
def on_provider_select(provider_name):
for key, info in API_PROVIDERS.items():
if info["name"] == provider_name:
return info.get("base_url", ""), info.get("default_model", ""), provider_name
return "", "", provider_name
def on_api_test(name):
yield "⏳ Đang kiểm tra kết nối API... Vui lòng chờ."
result = ConfigAPIManager.test_backend(name)
yield result["message"]
def on_api_save(name, btype, url, key, model, timeout):
result = ConfigAPIManager.add_backend(name, btype, url, key, model, int(timeout))
reinit_api_client()
app_state.generator = None
return result["message"], get_backends_display()
def on_api_delete(name):
result = ConfigAPIManager.delete_backend(name)
reinit_api_client()
app_state.generator = None
return result["message"], get_backends_display()
api_provider_dropdown.change(
fn=on_provider_select,
inputs=[api_provider_dropdown],
outputs=[api_url_input, api_model_input, api_name_input]
)
api_save_btn.click(
fn=on_api_save,
inputs=[api_name_input, api_type_dropdown, api_url_input, api_key_input, api_model_input, api_timeout_input],
outputs=[api_status, backends_display]
)
api_test_btn.click(fn=on_api_test, inputs=[test_name_input], outputs=[test_result])
api_delete_btn.click(fn=on_api_delete, inputs=[delete_name_input], outputs=[api_status, backends_display])
api_refresh_btn.click(fn=lambda: get_backends_display(), outputs=[backends_display])
# Sub-tab: Tham số sinh
with gr.Tab(t("settings.tab_params")):
gr.Markdown(f"#### {t('settings.params_header')}")
config = get_config()
gen_config = config.generation
param_temperature = gr.Slider(minimum=0.1, maximum=2.0, value=gen_config.temperature, step=0.1, label=t("settings.temperature_label"), info=t("settings.temperature_info"))
param_top_p = gr.Slider(minimum=0.1, maximum=1.0, value=gen_config.top_p, step=0.05, label="Top P", info=t("settings.top_p_info"))
param_max_tokens = gr.Slider(minimum=100, maximum=100000, value=gen_config.max_tokens, step=100, label="Max Tokens", info=t("settings.max_tokens_info"))
param_chapter_words = gr.Slider(minimum=500, maximum=65536, value=gen_config.chapter_target_words, step=500, label=t("settings.chapter_target_words"))
save_params_btn = gr.Button(t("settings.save_params_btn"), variant="primary")
params_status = gr.Textbox(label=t("settings.save_status"), interactive=False)
def on_save_params(temp, top_p, max_tokens, chapter_words):
cfg = get_config()
success, msg = cfg.update_generation_config(
temperature=temp, top_p=top_p,
max_tokens=int(max_tokens),
chapter_target_words=int(chapter_words)
)
if success:
app_state.generator = None
return f"{msg}"
return f"{msg}"
save_params_btn.click(
fn=on_save_params,
inputs=[param_temperature, param_top_p, param_max_tokens, param_chapter_words],
outputs=[params_status]
)
# Sub-tab: Bộ nhớ đệm
with gr.Tab(t("settings.tab_cache")):
gr.Markdown(f"#### {t('settings.cache_header')}")
cache_info_display = gr.Markdown(t("ui.no_cache"))
def get_cache_info():
try:
api_client = get_api_client()
stats = api_client.get_cache_stats()
gen_caches = list_generation_caches()
gen_size_val = get_cache_size()
return f"### Thống kê bộ nhớ đệm\n- **API Cache**: {stats['total_entries']}/{stats['max_size']} ({stats['usage_rate']:.1f}%)\n- **Generation Cache**: {len(gen_caches)} files ({gen_size_val / 1024:.1f} KB)"
except Exception as e:
return f"{str(e)}"
with gr.Row():
refresh_cache_btn = gr.Button(t("settings.refresh_cache"), size="sm")
clear_all_cache_btn = gr.Button(t("settings.clear_all"), variant="stop")
cache_op_status = gr.Textbox(label=t("settings.cache_op_status"), interactive=False)
def on_clear_all_cache():
try:
api_client = get_api_client()
api_client.clear_cache()
clear_generation_cache()
return "✅ Đã xóa tất cả bộ nhớ đệm", get_cache_info()
except Exception as e:
return f"{str(e)}", get_cache_info()
refresh_cache_btn.click(fn=get_cache_info, outputs=[cache_info_display])
clear_all_cache_btn.click(fn=on_clear_all_cache, outputs=[cache_op_status, cache_info_display])
# Sub-tab: Quản lý thể loại
with gr.Tab(t("settings.tab_genre")):
gr.Markdown(f"#### {t('settings.genre_desc')}")
genre_select = gr.Dropdown(
choices=GenreManager.get_genre_names(),
label=t("settings.genre_select"),
interactive=True
)
genre_name_input = gr.Textbox(label=t("settings.genre_name"), placeholder=t("settings.genre_name_placeholder"))
genre_desc_input = gr.Textbox(label=t("settings.genre_description"), placeholder=t("settings.genre_description_placeholder"), lines=3)
with gr.Row():
genre_add_btn = gr.Button(t("settings.genre_add_btn"), variant="primary")
genre_update_btn = gr.Button(t("settings.genre_update_btn"), variant="secondary")
genre_delete_btn = gr.Button(t("settings.genre_delete_btn"), variant="stop")
genre_op_status = gr.Textbox(label=t("settings.genre_op_status"), interactive=False)
def on_genre_select(name):
if name:
desc = GenreManager.get_genre_description(name)
return name, desc or ""
return "", ""
def on_genre_add(name, desc):
if not name.strip():
return t("settings.genre_err_name_empty"), gr.update()
success = GenreManager.add_genre(name.strip(), desc.strip())
if success:
return t("settings.genre_add_success"), gr.update(choices=GenreManager.get_genre_names())
return t("settings.genre_err_exists"), gr.update()
def on_genre_update(old_name, new_name, desc):
if not old_name:
return t("settings.genre_err_none_selected"), gr.update()
success = GenreManager.update_genre(old_name, new_name.strip(), desc.strip())
if success:
return t("settings.genre_update_success"), gr.update(choices=GenreManager.get_genre_names())
return t("settings.genre_err_update"), gr.update()
def on_genre_delete(name):
if not name:
return t("settings.genre_err_none_selected"), gr.update()
success = GenreManager.delete_genre(name)
if success:
return t("settings.genre_delete_success"), gr.update(choices=GenreManager.get_genre_names())
return t("settings.genre_err_delete"), gr.update()
genre_select.change(fn=on_genre_select, inputs=[genre_select], outputs=[genre_name_input, genre_desc_input])
genre_add_btn.click(fn=on_genre_add, inputs=[genre_name_input, genre_desc_input], outputs=[genre_op_status, genre_select])
genre_update_btn.click(fn=on_genre_update, inputs=[genre_select, genre_name_input, genre_desc_input], outputs=[genre_op_status, genre_select])
genre_delete_btn.click(fn=on_genre_delete, inputs=[genre_select], outputs=[genre_op_status, genre_select])
# Sub-tab: Quản lý chủ đề con
with gr.Tab(t("settings.tab_sub_genre")):
gr.Markdown(f"#### {t('settings.sub_genre_desc')}")
sub_genre_select = gr.Dropdown(
choices=SubGenreManager.get_sub_genre_names(),
label=t("settings.sub_genre_select"),
interactive=True
)
sub_genre_name_input = gr.Textbox(label=t("settings.sub_genre_name"), placeholder=t("settings.sub_genre_name_placeholder"))
sub_genre_desc_input = gr.Textbox(label=t("settings.sub_genre_description"), placeholder=t("settings.sub_genre_description_placeholder"), lines=3)
with gr.Row():
sub_genre_add_btn = gr.Button(t("settings.sub_genre_add_btn"), variant="primary")
sub_genre_update_btn = gr.Button(t("settings.sub_genre_update_btn"), variant="secondary")
sub_genre_delete_btn = gr.Button(t("settings.sub_genre_delete_btn"), variant="stop")
sub_genre_op_status = gr.Textbox(label=t("settings.sub_genre_op_status"), interactive=False)
def on_sub_genre_select(name):
if name:
desc = SubGenreManager.get_sub_genre_description(name)
return name, desc or ""
return "", ""
def on_sub_genre_add(name, desc):
if not name.strip():
return t("settings.sub_genre_err_name_empty"), gr.update()
success = SubGenreManager.add_sub_genre(name.strip(), desc.strip())
if success:
return t("settings.sub_genre_add_success"), gr.update(choices=SubGenreManager.get_sub_genre_names())
return t("settings.sub_genre_err_exists"), gr.update()
def on_sub_genre_update(old_name, new_name, desc):
if not old_name:
return t("settings.sub_genre_err_none_selected"), gr.update()
success = SubGenreManager.update_sub_genre(old_name, new_name.strip(), desc.strip())
if success:
return t("settings.sub_genre_update_success"), gr.update(choices=SubGenreManager.get_sub_genre_names())
return t("settings.sub_genre_err_update"), gr.update()
def on_sub_genre_delete(name):
if not name:
return t("settings.sub_genre_err_none_selected"), gr.update()
success = SubGenreManager.delete_sub_genre(name)
if success:
return t("settings.sub_genre_delete_success"), gr.update(choices=SubGenreManager.get_sub_genre_names())
return t("settings.sub_genre_err_delete"), gr.update()
sub_genre_select.change(fn=on_sub_genre_select, inputs=[sub_genre_select], outputs=[sub_genre_name_input, sub_genre_desc_input])
sub_genre_add_btn.click(fn=on_sub_genre_add, inputs=[sub_genre_name_input, sub_genre_desc_input], outputs=[sub_genre_op_status, sub_genre_select])
sub_genre_update_btn.click(fn=on_sub_genre_update, inputs=[sub_genre_select, sub_genre_name_input, sub_genre_desc_input], outputs=[sub_genre_op_status, sub_genre_select])
sub_genre_delete_btn.click(fn=on_sub_genre_delete, inputs=[sub_genre_select], outputs=[sub_genre_op_status, sub_genre_select])