fix: tự động chạy ai
This commit is contained in:
@@ -78,7 +78,7 @@ def create_main_ui():
|
|||||||
from ui.projects_tab import build_projects_tab
|
from ui.projects_tab import build_projects_tab
|
||||||
from ui.settings_tab import build_settings_tab
|
from ui.settings_tab import build_settings_tab
|
||||||
|
|
||||||
build_create_tab()
|
build_create_tab(app)
|
||||||
build_continue_tab()
|
build_continue_tab()
|
||||||
build_rewrite_tab()
|
build_rewrite_tab()
|
||||||
build_polish_tab()
|
build_polish_tab()
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"settings_password": "TiniX@123"
|
"settings_password": ""
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -433,11 +433,14 @@ class ConfigManager:
|
|||||||
success, msg = self.save()
|
success, msg = self.save()
|
||||||
return success, msg if not success else t("config.backend_add_success")
|
return success, msg if not success else t("config.backend_add_success")
|
||||||
|
|
||||||
def update_backend(self, name: str, **kwargs) -> tuple[bool, str]:
|
def update_backend(self, name: str, new_name: str = None, **kwargs) -> tuple[bool, str]:
|
||||||
"""Cập nhật cấu hình phụ trợ"""
|
"""Cập nhật cấu hình phụ trợ"""
|
||||||
for backend in self.backends:
|
for backend in self.backends:
|
||||||
if backend.name == name:
|
if backend.name == name:
|
||||||
for key, value in kwargs.items():
|
updates = dict(kwargs)
|
||||||
|
if new_name is not None:
|
||||||
|
updates["name"] = new_name
|
||||||
|
for key, value in updates.items():
|
||||||
if hasattr(backend, key):
|
if hasattr(backend, key):
|
||||||
setattr(backend, key, value)
|
setattr(backend, key, value)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -101,11 +101,11 @@ class ConfigAPIManager:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_backend(name: str, **kwargs) -> Dict[str, Any]:
|
def update_backend(name: str, new_name: str = None, **kwargs) -> Dict[str, Any]:
|
||||||
"""Cập nhật cấu hình backend"""
|
"""Cập nhật cấu hình backend"""
|
||||||
try:
|
try:
|
||||||
config = get_config()
|
config = get_config()
|
||||||
success, msg = config.update_backend(name, **kwargs)
|
success, msg = config.update_backend(name, new_name=new_name, **kwargs)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
logger.info(f"Backend updated: {name}")
|
logger.info(f"Backend updated: {name}")
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{"genre": null, "sub_genres": null, "style": null, "title": "", "suggest_title_prompt": "", "num_main": 2, "num_sub": 3, "character": "", "suggest_char_prompt": null, "world": "", "suggest_world_prompt": null, "plot": "", "suggest_plot_prompt": null, "custom_outline": null, "total_chapters": 20, "memory_type": "Toàn văn", "memory_chapters": 3, "use_reflection": false, "outline": null}
|
||||||
Binary file not shown.
Binary file not shown.
+7
-5
@@ -9,12 +9,14 @@ services:
|
|||||||
container_name: ai-novel-generator
|
container_name: ai-novel-generator
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "7860:7860" # Gradio Web UI
|
||||||
|
- "8000:8000" # FastAPI
|
||||||
environment:
|
environment:
|
||||||
# Cấu hình cơ bản
|
# Cấu hình cơ bản
|
||||||
- MODE=production
|
- MODE=production
|
||||||
- HOST=0.0.0.0
|
- HOST=0.0.0.0
|
||||||
- PORT=8000
|
- PORT=8000
|
||||||
|
- NOVEL_TOOL_PORT=7860
|
||||||
- CONCURRENCY=4
|
- CONCURRENCY=4
|
||||||
|
|
||||||
# Cấu hình log
|
# Cấu hình log
|
||||||
@@ -81,7 +83,7 @@ services:
|
|||||||
container_name: ai-novel-redis
|
container_name: ai-novel-redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "6379:6379"
|
- "6380:6379"
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
command: redis-server --appendonly yes
|
command: redis-server --appendonly yes
|
||||||
@@ -99,8 +101,8 @@ services:
|
|||||||
container_name: ai-novel-nginx
|
container_name: ai-novel-nginx
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "9980:80"
|
||||||
- "443:443"
|
- "9443:443"
|
||||||
volumes:
|
volumes:
|
||||||
- ./nginx.conf:/etc/nginx/nginx.conf
|
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||||
- ./ssl:/etc/nginx/ssl
|
- ./ssl:/etc/nginx/ssl
|
||||||
@@ -136,4 +138,4 @@ volumes:
|
|||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
name: ai-novel-network
|
name: ai-novel-network
|
||||||
|
|||||||
Generated
-12
@@ -73,7 +73,6 @@
|
|||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -1796,7 +1795,6 @@
|
|||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -1856,7 +1854,6 @@
|
|||||||
"integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==",
|
"integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.58.0",
|
"@typescript-eslint/scope-manager": "8.58.0",
|
||||||
"@typescript-eslint/types": "8.58.0",
|
"@typescript-eslint/types": "8.58.0",
|
||||||
@@ -2382,7 +2379,6 @@
|
|||||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -2726,7 +2722,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.10.12",
|
"baseline-browser-mapping": "^2.10.12",
|
||||||
"caniuse-lite": "^1.0.30001782",
|
"caniuse-lite": "^1.0.30001782",
|
||||||
@@ -3304,7 +3299,6 @@
|
|||||||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -3490,7 +3484,6 @@
|
|||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
@@ -5715,7 +5708,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -5725,7 +5717,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -6427,7 +6418,6 @@
|
|||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -6590,7 +6580,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -6866,7 +6855,6 @@
|
|||||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,163 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
|
|
||||||
|
const DRAFT_KEY = "tinix_create_draft";
|
||||||
|
|
||||||
|
const DEFAULT_FORM = {
|
||||||
|
title: "",
|
||||||
|
genre: "",
|
||||||
|
sub_genres: [] as string[],
|
||||||
|
style: "",
|
||||||
|
character_setting: "",
|
||||||
|
world_setting: "",
|
||||||
|
plot_idea: "",
|
||||||
|
custom_outline_prompt: "",
|
||||||
|
total_chapters: 20
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadDraft(): { form: typeof DEFAULT_FORM; outline: string } {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(DRAFT_KEY);
|
||||||
|
if (raw) {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return {
|
||||||
|
form: { ...DEFAULT_FORM, ...(parsed.form || {}) },
|
||||||
|
outline: parsed.outline || ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore corrupted draft
|
||||||
|
}
|
||||||
|
return { form: { ...DEFAULT_FORM }, outline: "" };
|
||||||
|
}
|
||||||
|
|
||||||
export default function CreatePage() {
|
export default function CreatePage() {
|
||||||
const [genres, setGenres] = useState<any[]>([]);
|
const initial = useRef(loadDraft()).current;
|
||||||
|
|
||||||
|
const [genres, setGenres] = useState<string[]>([]);
|
||||||
|
const [subGenres, setSubGenres] = useState<string[]>([]);
|
||||||
|
const [styles, setStyles] = useState<string[]>([]);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState(initial.form);
|
||||||
title: "",
|
const [outline, setOutline] = useState<string>(initial.outline);
|
||||||
genre: "",
|
|
||||||
character_setting: "",
|
|
||||||
world_setting: "",
|
|
||||||
plot_idea: "",
|
|
||||||
total_chapters: 20
|
|
||||||
});
|
|
||||||
const [outline, setOutline] = useState<string>("");
|
|
||||||
const [statusMsg, setStatusMsg] = useState<string>("");
|
const [statusMsg, setStatusMsg] = useState<string>("");
|
||||||
|
|
||||||
|
// Per-field AI suggestion UI state
|
||||||
|
const [suggestPrompt, setSuggestPrompt] = useState({
|
||||||
|
title: "",
|
||||||
|
char: "",
|
||||||
|
world: "",
|
||||||
|
plot: ""
|
||||||
|
});
|
||||||
|
const [suggesting, setSuggesting] = useState<{ [k: string]: boolean }>({});
|
||||||
|
|
||||||
|
// Persist to localStorage on every change (fixes reload content loss)
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem(DRAFT_KEY, JSON.stringify({ form: formData, outline }));
|
||||||
|
}, [formData, outline]);
|
||||||
|
|
||||||
|
// Fetch genres / styles on mount (do NOT overwrite persisted selections)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fetch genres on mount
|
|
||||||
api.getGenres().then((data) => {
|
api.getGenres().then((data) => {
|
||||||
setGenres(data || []);
|
const names = (data || []).map((g: any) => g.name ?? g);
|
||||||
if (data && data.length > 0) {
|
setGenres(names);
|
||||||
setFormData((prev) => ({ ...prev, genre: data[0].name }));
|
if (!formData.genre && names.length > 0) {
|
||||||
|
setFormData((prev) => ({ ...prev, genre: names[0] }));
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch((err) => console.error("Error fetching genres:", err));
|
||||||
console.error("Error fetching genres:", err);
|
|
||||||
});
|
api.getStyles().then((data) => {
|
||||||
|
const names = (data || []).map((s: any) => s.name ?? s);
|
||||||
|
setStyles(names);
|
||||||
|
if (!formData.style && names.length > 0) {
|
||||||
|
setFormData((prev) => ({ ...prev, style: names[0] }));
|
||||||
|
}
|
||||||
|
}).catch((err) => console.error("Error fetching styles:", err));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Load sub-genres when genre changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!formData.genre) return;
|
||||||
|
api.getSubGenres(formData.genre).then((data) => {
|
||||||
|
const names = (data || []).map((s: any) => s.name ?? s);
|
||||||
|
setSubGenres(names);
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
sub_genres: prev.sub_genres.filter((sg) => names.includes(sg))
|
||||||
|
}));
|
||||||
|
}).catch(() => setSubGenres([]));
|
||||||
|
}, [formData.genre]);
|
||||||
|
|
||||||
|
const toggleSubGenre = (sg: string) => {
|
||||||
|
setFormData((prev) => {
|
||||||
|
const has = prev.sub_genres.includes(sg);
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
sub_genres: has
|
||||||
|
? prev.sub_genres.filter((x) => x !== sg)
|
||||||
|
: [...prev.sub_genres, sg]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSuggest = async (
|
||||||
|
type: 'title' | 'char' | 'world' | 'plot'
|
||||||
|
) => {
|
||||||
|
setSuggesting((s) => ({ ...s, [type]: true }));
|
||||||
|
setStatusMsg("");
|
||||||
|
try {
|
||||||
|
const res = await api.suggest({
|
||||||
|
type,
|
||||||
|
genre: formData.genre,
|
||||||
|
sub_genres: formData.sub_genres,
|
||||||
|
title: formData.title,
|
||||||
|
character_setting: formData.character_setting,
|
||||||
|
world_setting: formData.world_setting,
|
||||||
|
custom_prompt: suggestPrompt[type],
|
||||||
|
num_main_chars: 2,
|
||||||
|
num_sub_chars: 3
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = res.content || "";
|
||||||
|
if (type === 'title') {
|
||||||
|
// suggestion returns a list of titles -> pick the first
|
||||||
|
const first = content.split("\n").map((s: string) => s.trim()).filter(Boolean)[0] || content;
|
||||||
|
setFormData((prev) => ({ ...prev, title: first }));
|
||||||
|
} else if (type === 'char') {
|
||||||
|
setFormData((prev) => ({ ...prev, character_setting: content }));
|
||||||
|
} else if (type === 'world') {
|
||||||
|
setFormData((prev) => ({ ...prev, world_setting: content }));
|
||||||
|
} else if (type === 'plot') {
|
||||||
|
setFormData((prev) => ({ ...prev, plot_idea: content }));
|
||||||
|
}
|
||||||
|
setStatusMsg(res.message || "Gợi ý đã được điền.");
|
||||||
|
} catch (err: any) {
|
||||||
|
setStatusMsg("Lỗi gợi ý: " + err.message);
|
||||||
|
} finally {
|
||||||
|
setSuggesting((s) => ({ ...s, [type]: false }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleGenerateOutline = async () => {
|
const handleGenerateOutline = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setStatusMsg("Generating outline. Please wait...");
|
setStatusMsg("Generating outline. Please wait...");
|
||||||
setOutline("");
|
setOutline("");
|
||||||
|
|
||||||
const res = await api.generateOutline({
|
const res = await api.generateOutline({
|
||||||
title: formData.title,
|
title: formData.title,
|
||||||
genre: formData.genre,
|
genre: formData.genre,
|
||||||
sub_genres: [],
|
sub_genres: formData.sub_genres,
|
||||||
total_chapters: formData.total_chapters,
|
total_chapters: formData.total_chapters,
|
||||||
character_setting: formData.character_setting,
|
character_setting: formData.character_setting,
|
||||||
world_setting: formData.world_setting,
|
world_setting: formData.world_setting,
|
||||||
plot_idea: formData.plot_idea
|
plot_idea: formData.plot_idea,
|
||||||
|
custom_outline_prompt: formData.custom_outline_prompt
|
||||||
});
|
});
|
||||||
|
|
||||||
setOutline(res.content || "");
|
setOutline(res.content || "");
|
||||||
setStatusMsg(res.message || "Success");
|
setStatusMsg(res.message || "Success");
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -54,70 +167,158 @@ export default function CreatePage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClearDraft = () => {
|
||||||
|
localStorage.removeItem(DRAFT_KEY);
|
||||||
|
setFormData({ ...DEFAULT_FORM, genre: formData.genre, style: formData.style });
|
||||||
|
setOutline("");
|
||||||
|
setSuggestPrompt({ title: "", char: "", world: "", plot: "" });
|
||||||
|
setStatusMsg("");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-1 p-8">
|
<div className="flex flex-col flex-1 p-8">
|
||||||
<div className="mb-8">
|
<div className="mb-8 flex items-start justify-between">
|
||||||
<h1 className="text-3xl font-bold text-foreground">Create Story</h1>
|
<div>
|
||||||
<p className="text-zinc-500 mt-2">Design your novel settings and generate an outline.</p>
|
<h1 className="text-3xl font-bold text-foreground">Create Story</h1>
|
||||||
|
<p className="text-zinc-500 mt-2">Design your novel settings and generate an outline.</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleClearDraft}
|
||||||
|
className="text-xs px-3 py-2 rounded-md border border-border-glass text-zinc-400 hover:text-white hover:border-brand-primary transition-colors"
|
||||||
|
>
|
||||||
|
Clear draft
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
{/* Form Column */}
|
{/* Form Column */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium text-zinc-300">Novel Title</label>
|
|
||||||
<input
|
|
||||||
className="w-full bg-background-card border border-border-glass rounded-md p-2 text-sm focus:border-brand-primary outline-none text-white"
|
|
||||||
value={formData.title}
|
|
||||||
onChange={(e) => setFormData({...formData, title: e.target.value})}
|
|
||||||
placeholder="Enter your novel title..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-zinc-300">Genre</label>
|
<label className="text-sm font-medium text-zinc-300">Genre</label>
|
||||||
<select
|
<select
|
||||||
className="w-full bg-background-card border border-border-glass rounded-md p-2 text-sm focus:border-brand-primary outline-none text-white"
|
className="w-full bg-background-card border border-border-glass rounded-md p-2 text-sm focus:border-brand-primary outline-none text-white"
|
||||||
value={formData.genre}
|
value={formData.genre}
|
||||||
onChange={(e) => setFormData({...formData, genre: e.target.value})}
|
onChange={(e) => setFormData({ ...formData, genre: e.target.value })}
|
||||||
>
|
>
|
||||||
{genres.map(g => (
|
{genres.length === 0 && <option value="">-- no genres --</option>}
|
||||||
<option key={g.name} value={g.name}>{g.name}</option>
|
{genres.map((g) => (
|
||||||
|
<option key={g} value={g}>{g}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{subGenres.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-zinc-300">Sub-genres</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{subGenres.map((sg) => {
|
||||||
|
const active = formData.sub_genres.includes(sg);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={sg}
|
||||||
|
onClick={() => toggleSubGenre(sg)}
|
||||||
|
className={`px-3 py-1 rounded-full text-xs border transition-colors ${
|
||||||
|
active
|
||||||
|
? "bg-brand-primary text-black border-brand-primary"
|
||||||
|
: "border-border-glass text-zinc-400 hover:text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{sg}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-zinc-300">Character Settings</label>
|
<label className="text-sm font-medium text-zinc-300">Writing Style</label>
|
||||||
<textarea
|
<select
|
||||||
className="w-full bg-background-card border border-border-glass rounded-md p-2 text-sm min-h-[100px] focus:border-brand-primary outline-none text-white"
|
className="w-full bg-background-card border border-border-glass rounded-md p-2 text-sm focus:border-brand-primary outline-none text-white"
|
||||||
value={formData.character_setting}
|
value={formData.style}
|
||||||
onChange={(e) => setFormData({...formData, character_setting: e.target.value})}
|
onChange={(e) => setFormData({ ...formData, style: e.target.value })}
|
||||||
placeholder="Describe main characters, personalities..."
|
>
|
||||||
|
{styles.length === 0 && <option value="">-- no styles --</option>}
|
||||||
|
{styles.map((s) => (
|
||||||
|
<option key={s} value={s}>{s}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AiField
|
||||||
|
label="Novel Title"
|
||||||
|
placeholder="Enter your novel title..."
|
||||||
|
value={formData.title}
|
||||||
|
onChange={(v) => setFormData({ ...formData, title: v })}
|
||||||
|
promptValue={suggestPrompt.title}
|
||||||
|
onPromptChange={(v) => setSuggestPrompt((p) => ({ ...p, title: v }))}
|
||||||
|
onSuggest={() => handleSuggest("title")}
|
||||||
|
suggesting={suggesting.title}
|
||||||
|
promptPlaceholder="Optional prompt for AI title suggestion..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AiField
|
||||||
|
label="Character Settings"
|
||||||
|
placeholder="Describe main characters, personalities..."
|
||||||
|
value={formData.character_setting}
|
||||||
|
onChange={(v) => setFormData({ ...formData, character_setting: v })}
|
||||||
|
promptValue={suggestPrompt.char}
|
||||||
|
onPromptChange={(v) => setSuggestPrompt((p) => ({ ...p, char: v }))}
|
||||||
|
onSuggest={() => handleSuggest("char")}
|
||||||
|
suggesting={suggesting.char}
|
||||||
|
promptPlaceholder="Optional prompt for AI character suggestion..."
|
||||||
|
textarea
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AiField
|
||||||
|
label="World Setting"
|
||||||
|
placeholder="Describe the world, magic system, laws..."
|
||||||
|
value={formData.world_setting}
|
||||||
|
onChange={(v) => setFormData({ ...formData, world_setting: v })}
|
||||||
|
promptValue={suggestPrompt.world}
|
||||||
|
onPromptChange={(v) => setSuggestPrompt((p) => ({ ...p, world: v }))}
|
||||||
|
onSuggest={() => handleSuggest("world")}
|
||||||
|
suggesting={suggesting.world}
|
||||||
|
promptPlaceholder="Optional prompt for AI world suggestion..."
|
||||||
|
textarea
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AiField
|
||||||
|
label="Plot Idea"
|
||||||
|
placeholder="Main conflict, goals, and ending idea..."
|
||||||
|
value={formData.plot_idea}
|
||||||
|
onChange={(v) => setFormData({ ...formData, plot_idea: v })}
|
||||||
|
promptValue={suggestPrompt.plot}
|
||||||
|
onPromptChange={(v) => setSuggestPrompt((p) => ({ ...p, plot: v }))}
|
||||||
|
onSuggest={() => handleSuggest("plot")}
|
||||||
|
suggesting={suggesting.plot}
|
||||||
|
promptPlaceholder="Optional prompt for AI plot suggestion..."
|
||||||
|
textarea
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-zinc-300">Custom Outline Prompt (optional)</label>
|
||||||
|
<textarea
|
||||||
|
className="w-full bg-background-card border border-border-glass rounded-md p-2 text-sm min-h-[80px] focus:border-brand-primary outline-none text-white"
|
||||||
|
value={formData.custom_outline_prompt}
|
||||||
|
onChange={(e) => setFormData({ ...formData, custom_outline_prompt: e.target.value })}
|
||||||
|
placeholder="Extra instructions for the outline..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-zinc-300">World Setting</label>
|
<label className="text-sm font-medium text-zinc-300">Total Chapters</label>
|
||||||
<textarea
|
<input
|
||||||
className="w-full bg-background-card border border-border-glass rounded-md p-2 text-sm min-h-[100px] focus:border-brand-primary outline-none text-white"
|
type="number"
|
||||||
value={formData.world_setting}
|
min={1}
|
||||||
onChange={(e) => setFormData({...formData, world_setting: e.target.value})}
|
max={200}
|
||||||
placeholder="Describe the world, magic system, laws..."
|
className="w-32 bg-background-card border border-border-glass rounded-md p-2 text-sm focus:border-brand-primary outline-none text-white"
|
||||||
|
value={formData.total_chapters}
|
||||||
|
onChange={(e) => setFormData({ ...formData, total_chapters: Number(e.target.value) })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<button
|
||||||
<label className="text-sm font-medium text-zinc-300">Plot Idea</label>
|
|
||||||
<textarea
|
|
||||||
className="w-full bg-background-card border border-border-glass rounded-md p-2 text-sm min-h-[100px] focus:border-brand-primary outline-none text-white"
|
|
||||||
value={formData.plot_idea}
|
|
||||||
onChange={(e) => setFormData({...formData, plot_idea: e.target.value})}
|
|
||||||
placeholder="Main conflict, goals, and ending idea..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
disabled={loading || !formData.title || !formData.genre}
|
disabled={loading || !formData.title || !formData.genre}
|
||||||
onClick={handleGenerateOutline}
|
onClick={handleGenerateOutline}
|
||||||
className="w-full py-3 bg-brand-primary text-black font-semibold rounded-md hover:bg-brand-secondary transition-colors disabled:opacity-50"
|
className="w-full py-3 bg-brand-primary text-black font-semibold rounded-md hover:bg-brand-secondary transition-colors disabled:opacity-50"
|
||||||
@@ -129,14 +330,14 @@ export default function CreatePage() {
|
|||||||
{/* Outline / Results Column */}
|
{/* Outline / Results Column */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h2 className="text-xl font-semibold text-foreground">Generated Outline</h2>
|
<h2 className="text-xl font-semibold text-foreground">Generated Outline</h2>
|
||||||
|
|
||||||
{statusMsg && (
|
{statusMsg && (
|
||||||
<div className="p-3 bg-blue-900/30 border border-blue-500/30 text-blue-300 rounded-md text-sm">
|
<div className="p-3 bg-blue-900/30 border border-blue-500/30 text-blue-300 rounded-md text-sm">
|
||||||
{statusMsg}
|
{statusMsg}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
className="w-full bg-black/40 border border-border-glass rounded-md p-4 text-sm h-[600px] font-mono text-zinc-300 focus:border-brand-primary outline-none resize-none"
|
className="w-full bg-black/40 border border-border-glass rounded-md p-4 text-sm h-[600px] font-mono text-zinc-300 focus:border-brand-primary outline-none resize-none"
|
||||||
value={outline}
|
value={outline}
|
||||||
readOnly
|
readOnly
|
||||||
@@ -147,3 +348,57 @@ export default function CreatePage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AiFieldProps {
|
||||||
|
label: string;
|
||||||
|
placeholder: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
promptValue: string;
|
||||||
|
onPromptChange: (v: string) => void;
|
||||||
|
onSuggest: () => void;
|
||||||
|
suggesting: boolean;
|
||||||
|
promptPlaceholder: string;
|
||||||
|
textarea?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AiField({
|
||||||
|
label, placeholder, value, onChange,
|
||||||
|
promptValue, onPromptChange, onSuggest, suggesting, promptPlaceholder, textarea
|
||||||
|
}: AiFieldProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-zinc-300">{label}</label>
|
||||||
|
{textarea ? (
|
||||||
|
<textarea
|
||||||
|
className="w-full bg-background-card border border-border-glass rounded-md p-2 text-sm min-h-[100px] focus:border-brand-primary outline-none text-white"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
className="w-full bg-background-card border border-border-glass rounded-md p-2 text-sm focus:border-brand-primary outline-none text-white"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<input
|
||||||
|
className="flex-1 bg-background-card border border-border-glass rounded-md p-2 text-xs focus:border-brand-primary outline-none text-zinc-300"
|
||||||
|
value={promptValue}
|
||||||
|
onChange={(e) => onPromptChange(e.target.value)}
|
||||||
|
placeholder={promptPlaceholder}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={onSuggest}
|
||||||
|
disabled={suggesting}
|
||||||
|
className="px-3 py-2 text-xs rounded-md bg-brand-primary text-black font-medium hover:bg-brand-secondary transition-colors disabled:opacity-50 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{suggesting ? "AI..." : "AI gợi ý"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,18 @@ export interface OutlineReq {
|
|||||||
custom_outline_prompt?: string;
|
custom_outline_prompt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SuggestionReq {
|
||||||
|
type: 'title' | 'char' | 'world' | 'plot';
|
||||||
|
genre: string;
|
||||||
|
sub_genres?: string[];
|
||||||
|
title?: string;
|
||||||
|
character_setting?: string;
|
||||||
|
world_setting?: string;
|
||||||
|
custom_prompt?: string;
|
||||||
|
num_main_chars?: number;
|
||||||
|
num_sub_chars?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
// Check health
|
// Check health
|
||||||
async checkHealth() {
|
async checkHealth() {
|
||||||
@@ -48,7 +60,30 @@ export const api = {
|
|||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
// Generate outline
|
// Get sub-genres for a genre
|
||||||
|
async getSubGenres(genre: string) {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/sub-genres/by-genre/${encodeURIComponent(genre)}`);
|
||||||
|
if (!res.ok) throw new Error('Failed to fetch sub-genres');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get writing styles
|
||||||
|
async getStyles() {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/styles`);
|
||||||
|
if (!res.ok) throw new Error('Failed to fetch styles');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get AI suggestion (title / char / world / plot)
|
||||||
|
async suggest(data: SuggestionReq) {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/suggest`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Suggestion failed');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
async generateOutline(data: OutlineReq) {
|
async generateOutline(data: OutlineReq) {
|
||||||
const res = await fetch(`${API_BASE_URL}/generate-outline`, {
|
const res = await fetch(`${API_BASE_URL}/generate-outline`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
+1
-1
@@ -599,7 +599,7 @@ async def config_add_backend(req: BackendReq):
|
|||||||
@app.put("/config/backends/{name}")
|
@app.put("/config/backends/{name}")
|
||||||
async def config_update_backend(name: str, req: BackendReq):
|
async def config_update_backend(name: str, req: BackendReq):
|
||||||
result = ConfigAPIManager.update_backend(
|
result = ConfigAPIManager.update_backend(
|
||||||
name, name=req.name, type=req.type, base_url=req.base_url,
|
name, new_name=req.name, type=req.type, base_url=req.base_url,
|
||||||
api_key=req.api_key, model=req.model, timeout=req.timeout
|
api_key=req.api_key, model=req.model, timeout=req.timeout
|
||||||
)
|
)
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
server {
|
||||||
|
listen 8000;
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
+139
-68
@@ -3,6 +3,7 @@ from datetime import datetime
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
|
import os
|
||||||
|
|
||||||
from locales.i18n import t
|
from locales.i18n import t
|
||||||
from services.novel_generator import OutlineParser
|
from services.novel_generator import OutlineParser
|
||||||
@@ -15,7 +16,48 @@ import logging
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def build_create_tab():
|
# ==================== DRAFT PERSISTENCE ====================
|
||||||
|
DRAFT_PATH = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(__file__)), "data", "create_draft.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
DRAFT_FIELDS = [
|
||||||
|
"genre", "sub_genres", "style", "title", "suggest_title_prompt",
|
||||||
|
"num_main", "num_sub", "character", "suggest_char_prompt",
|
||||||
|
"world", "suggest_world_prompt", "plot", "suggest_plot_prompt",
|
||||||
|
"custom_outline", "total_chapters", "memory_type", "memory_chapters",
|
||||||
|
"use_reflection", "outline",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _save_create_draft(d: dict):
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(DRAFT_PATH), exist_ok=True)
|
||||||
|
with open(DRAFT_PATH, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(d, f, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Save create draft failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_create_draft() -> dict:
|
||||||
|
try:
|
||||||
|
if os.path.exists(DRAFT_PATH):
|
||||||
|
with open(DRAFT_PATH, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Load create draft failed: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_create_draft():
|
||||||
|
try:
|
||||||
|
if os.path.exists(DRAFT_PATH):
|
||||||
|
os.remove(DRAFT_PATH)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Clear create draft failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def build_create_tab(demo=None):
|
||||||
"""Xây dựng giao diện Tab Sáng tác từ đầu"""
|
"""Xây dựng giao diện Tab Sáng tác từ đầu"""
|
||||||
with gr.Tab(t("tabs.create")):
|
with gr.Tab(t("tabs.create")):
|
||||||
gr.Markdown(f"### 🪄 {t('create.header')}")
|
gr.Markdown(f"### 🪄 {t('create.header')}")
|
||||||
@@ -74,51 +116,51 @@ def build_create_tab():
|
|||||||
character_input = gr.Textbox(
|
character_input = gr.Textbox(
|
||||||
label=t("create.char_setting"),
|
label=t("create.char_setting"),
|
||||||
placeholder=t("create.char_setting_placeholder"),
|
placeholder=t("create.char_setting_placeholder"),
|
||||||
lines=5, interactive=False
|
lines=5, interactive=True
|
||||||
)
|
)
|
||||||
with gr.Group():
|
with gr.Group():
|
||||||
with gr.Row():
|
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_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)
|
num_sub_chars = gr.Number(label=t("create.num_sub_chars"), value=3, minimum=0, maximum=20, step=1, scale=1)
|
||||||
with gr.Row():
|
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_prompt = gr.Textbox(show_label=False, placeholder=t("create.custom_prompt_placeholder"), lines=1, interactive=True, scale=4)
|
||||||
suggest_char_btn = gr.Button(t("create.suggest_btn"), variant="secondary", size="sm", scale=1, interactive=False)
|
suggest_char_btn = gr.Button(t("create.suggest_btn"), variant="secondary", size="sm", scale=1, interactive=True)
|
||||||
suggest_char_status = gr.Textbox(show_label=False, interactive=False, visible=False)
|
suggest_char_status = gr.Textbox(show_label=False, interactive=False, visible=False)
|
||||||
|
|
||||||
with gr.Column():
|
with gr.Column():
|
||||||
world_input = gr.Textbox(
|
world_input = gr.Textbox(
|
||||||
label=t("create.world_setting"),
|
label=t("create.world_setting"),
|
||||||
placeholder=t("create.world_setting_placeholder"),
|
placeholder=t("create.world_setting_placeholder"),
|
||||||
lines=5, interactive=False
|
lines=5, interactive=True
|
||||||
)
|
)
|
||||||
with gr.Group():
|
with gr.Group():
|
||||||
with gr.Row():
|
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_prompt = gr.Textbox(show_label=False, placeholder=t("create.custom_prompt_placeholder"), lines=1, interactive=True, scale=4)
|
||||||
suggest_world_btn = gr.Button(t("create.suggest_btn"), variant="secondary", size="sm", scale=1, interactive=False)
|
suggest_world_btn = gr.Button(t("create.suggest_btn"), variant="secondary", size="sm", scale=1, interactive=True)
|
||||||
suggest_world_status = gr.Textbox(show_label=False, interactive=False, visible=False)
|
suggest_world_status = gr.Textbox(show_label=False, interactive=False, visible=False)
|
||||||
|
|
||||||
plot_input = gr.Textbox(
|
plot_input = gr.Textbox(
|
||||||
label=t("create.plot_idea"),
|
label=t("create.plot_idea"),
|
||||||
placeholder=t("create.plot_idea_placeholder"),
|
placeholder=t("create.plot_idea_placeholder"),
|
||||||
lines=3, interactive=False
|
lines=3, interactive=True
|
||||||
)
|
)
|
||||||
with gr.Group():
|
with gr.Group():
|
||||||
with gr.Row():
|
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_prompt = gr.Textbox(show_label=False, placeholder=t("create.custom_prompt_placeholder"), lines=1, interactive=True, scale=4)
|
||||||
suggest_plot_btn = gr.Button(t("create.suggest_plot_btn"), variant="secondary", size="sm", scale=1, interactive=False)
|
suggest_plot_btn = gr.Button(t("create.suggest_plot_btn"), variant="secondary", size="sm", scale=1, interactive=True)
|
||||||
suggest_plot_status = gr.Textbox(show_label=False, interactive=False, visible=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.Accordion("📝 3. Dàn ý truyện", open=False):
|
||||||
custom_outline_prompt = gr.Textbox(
|
custom_outline_prompt = gr.Textbox(
|
||||||
label="Yêu cầu riêng/Chỉ dẫn bổ sung cho Dàn ý (Tùy chọn)",
|
label="Yêu cầu riêng/Chỉ dẫn bổ sung cho Dàn ý (Tùy chọn)",
|
||||||
placeholder="VD: Không có nữ chính, kết cục mở, nvc là người tu ma nhưng tính cách hài hước...",
|
placeholder="VD: Không có nữ chính, kết cục mở, nvc là người tu ma nhưng tính cách hài hước...",
|
||||||
lines=2, interactive=False
|
lines=2, interactive=True
|
||||||
)
|
)
|
||||||
with gr.Row():
|
with gr.Row():
|
||||||
total_chapters = gr.Number(
|
total_chapters = gr.Number(
|
||||||
label=t("create.chapter_count"), value=20, minimum=1, maximum=200, step=1, scale=1, interactive=False
|
label=t("create.chapter_count"), value=20, minimum=1, maximum=200, step=1, scale=1, interactive=True
|
||||||
)
|
)
|
||||||
generate_outline_btn = gr.Button(t("create.gen_outline_btn"), variant="primary", size="lg", scale=3, interactive=False)
|
generate_outline_btn = gr.Button(t("create.gen_outline_btn"), variant="primary", size="lg", scale=3, interactive=True)
|
||||||
|
|
||||||
outline_output = gr.Textbox(label=t("create.outline_display"), lines=15, interactive=True)
|
outline_output = gr.Textbox(label=t("create.outline_display"), lines=15, interactive=True)
|
||||||
outline_status = gr.Textbox(label=t("create.gen_status"), interactive=False)
|
outline_status = gr.Textbox(label=t("create.gen_status"), interactive=False)
|
||||||
@@ -132,8 +174,8 @@ def build_create_tab():
|
|||||||
with gr.Column(scale=1):
|
with gr.Column(scale=1):
|
||||||
use_reflection_checkbox = gr.Checkbox(label="Bật chế độ Tự kiểm duyệt (Self-Reflection)", value=False, info="AI tự đọc lại và sửa lỗi nháp. Tốn gấp đôi thời gian & Token.")
|
use_reflection_checkbox = gr.Checkbox(label="Bật chế độ Tự kiểm duyệt (Self-Reflection)", value=False, info="AI tự đọc lại và sửa lỗi nháp. Tốn gấp đôi thời gian & Token.")
|
||||||
with gr.Row():
|
with gr.Row():
|
||||||
auto_generate_btn = gr.Button(t("create.start_gen_btn"), variant="primary", size="lg", interactive=False)
|
auto_generate_btn = gr.Button(t("create.start_gen_btn"), variant="primary", size="lg", interactive=True)
|
||||||
stop_btn = gr.Button(t("create.pause_gen_btn"), variant="stop", size="lg", interactive=False)
|
stop_btn = gr.Button(t("create.pause_gen_btn"), variant="stop", size="lg", interactive=True)
|
||||||
|
|
||||||
with gr.Row():
|
with gr.Row():
|
||||||
with gr.Column(scale=1):
|
with gr.Column(scale=1):
|
||||||
@@ -182,7 +224,10 @@ def build_create_tab():
|
|||||||
nm = int(num_main) if num_main else 2
|
nm = int(num_main) if num_main else 2
|
||||||
ns = int(num_sub) if num_sub else 3
|
ns = int(num_sub) if num_sub else 3
|
||||||
content, msg = gen.suggest_content("char", title, genre, sub_genres, "", "", custom_prompt, nm, ns)
|
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)
|
if content:
|
||||||
|
yield gr.update(value=content), gr.update(visible=False), gr.update(interactive=True)
|
||||||
|
else:
|
||||||
|
yield gr.update(), gr.update(value=f"❌ {msg}", visible=True), gr.update(interactive=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
yield gr.update(), gr.update(value=f"❌ Lỗi: {str(e)}", visible=True), gr.update(interactive=True)
|
yield gr.update(), gr.update(value=f"❌ Lỗi: {str(e)}", visible=True), gr.update(interactive=True)
|
||||||
@@ -192,7 +237,10 @@ def build_create_tab():
|
|||||||
try:
|
try:
|
||||||
gen = app_state.get_generator()
|
gen = app_state.get_generator()
|
||||||
content, msg = gen.suggest_content("world", title, genre, sub_genres, "", "", custom_prompt)
|
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)
|
if content:
|
||||||
|
yield gr.update(value=content), gr.update(visible=False), gr.update(interactive=True)
|
||||||
|
else:
|
||||||
|
yield gr.update(), gr.update(value=f"❌ {msg}", visible=True), gr.update(interactive=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
yield gr.update(), gr.update(value=f"❌ Lỗi: {str(e)}", visible=True), gr.update(interactive=True)
|
yield gr.update(), gr.update(value=f"❌ Lỗi: {str(e)}", visible=True), gr.update(interactive=True)
|
||||||
@@ -202,7 +250,10 @@ def build_create_tab():
|
|||||||
try:
|
try:
|
||||||
gen = app_state.get_generator()
|
gen = app_state.get_generator()
|
||||||
content, msg = gen.suggest_content("plot", title, genre, sub_genres, char_setting, world_setting, custom_prompt)
|
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)
|
if content:
|
||||||
|
yield gr.update(value=content), gr.update(visible=False), gr.update(interactive=True)
|
||||||
|
else:
|
||||||
|
yield gr.update(), gr.update(value=f"❌ {msg}", visible=True), gr.update(interactive=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
yield gr.update(), gr.update(value=f"❌ Lỗi: {str(e)}", visible=True), gr.update(interactive=True)
|
yield gr.update(), gr.update(value=f"❌ Lỗi: {str(e)}", visible=True), gr.update(interactive=True)
|
||||||
@@ -218,6 +269,7 @@ def build_create_tab():
|
|||||||
|
|
||||||
def on_auto_generate(title, genre, sub_genres, char_setting, world_setting, plot_idea, outline_text, mem_type, mem_chaps, use_reflection, selected_style, progress=gr.Progress()):
|
def on_auto_generate(title, genre, sub_genres, char_setting, world_setting, plot_idea, outline_text, mem_type, mem_chaps, use_reflection, selected_style, progress=gr.Progress()):
|
||||||
"""Tự động tạo toàn bộ tiểu thuyết"""
|
"""Tự động tạo toàn bộ tiểu thuyết"""
|
||||||
|
_clear_create_draft()
|
||||||
gen = app_state.get_generator()
|
gen = app_state.get_generator()
|
||||||
|
|
||||||
# Cập nhật style vào config hiện tại
|
# Cập nhật style vào config hiện tại
|
||||||
@@ -327,6 +379,75 @@ def build_create_tab():
|
|||||||
app_state.stop_requested = True
|
app_state.stop_requested = True
|
||||||
return "⏸️ Đang dừng..."
|
return "⏸️ Đang dừng..."
|
||||||
|
|
||||||
|
# ==================== DRAFT PERSISTENCE ====================
|
||||||
|
draft_inputs = [
|
||||||
|
genre_dropdown, sub_genre_dropdown, create_style_dropdown,
|
||||||
|
title_input, suggest_title_prompt, num_main_chars, num_sub_chars,
|
||||||
|
character_input, suggest_char_prompt, world_input,
|
||||||
|
suggest_world_prompt, plot_input, suggest_plot_prompt,
|
||||||
|
custom_outline_prompt, total_chapters, memory_type,
|
||||||
|
memory_chapters, use_reflection_checkbox, outline_output,
|
||||||
|
]
|
||||||
|
|
||||||
|
def on_draft_change(*values):
|
||||||
|
d = dict(zip(DRAFT_FIELDS, values))
|
||||||
|
_save_create_draft(d)
|
||||||
|
return
|
||||||
|
|
||||||
|
for _comp in draft_inputs:
|
||||||
|
_comp.change(fn=on_draft_change, inputs=draft_inputs, outputs=[])
|
||||||
|
|
||||||
|
def on_load_draft():
|
||||||
|
d = _load_create_draft()
|
||||||
|
title = d.get("title", "")
|
||||||
|
character = d.get("character", "")
|
||||||
|
world = d.get("world", "")
|
||||||
|
plot = d.get("plot", "")
|
||||||
|
outline = d.get("outline", "")
|
||||||
|
return (
|
||||||
|
d.get("genre"),
|
||||||
|
d.get("sub_genres"),
|
||||||
|
d.get("style"),
|
||||||
|
title,
|
||||||
|
d.get("suggest_title_prompt"),
|
||||||
|
d.get("num_main", 2),
|
||||||
|
d.get("num_sub", 3),
|
||||||
|
d.get("character", ""),
|
||||||
|
gr.update(interactive=True),
|
||||||
|
gr.update(interactive=True),
|
||||||
|
d.get("world", ""),
|
||||||
|
gr.update(interactive=True),
|
||||||
|
gr.update(interactive=True),
|
||||||
|
d.get("plot", ""),
|
||||||
|
gr.update(interactive=True),
|
||||||
|
gr.update(interactive=True),
|
||||||
|
d.get("custom_outline", ""),
|
||||||
|
d.get("total_chapters", 20),
|
||||||
|
gr.update(interactive=True),
|
||||||
|
d.get("outline", ""),
|
||||||
|
gr.update(interactive=True),
|
||||||
|
gr.update(interactive=True),
|
||||||
|
d.get("memory_type", "Toàn văn"),
|
||||||
|
d.get("memory_chapters", 3),
|
||||||
|
bool(d.get("use_reflection", False)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if demo is not None:
|
||||||
|
demo.load(
|
||||||
|
fn=on_load_draft,
|
||||||
|
inputs=None,
|
||||||
|
outputs=[
|
||||||
|
genre_dropdown, sub_genre_dropdown, create_style_dropdown,
|
||||||
|
title_input, suggest_title_prompt, num_main_chars, num_sub_chars,
|
||||||
|
character_input, suggest_char_prompt, suggest_char_btn,
|
||||||
|
world_input, suggest_world_prompt, suggest_world_btn,
|
||||||
|
plot_input, suggest_plot_prompt, suggest_plot_btn,
|
||||||
|
custom_outline_prompt, total_chapters, generate_outline_btn,
|
||||||
|
outline_output, auto_generate_btn, stop_btn,
|
||||||
|
memory_type, memory_chapters, use_reflection_checkbox,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
# Bind events
|
# Bind events
|
||||||
suggest_title_btn.click(
|
suggest_title_btn.click(
|
||||||
fn=on_suggest_title,
|
fn=on_suggest_title,
|
||||||
@@ -370,53 +491,3 @@ def build_create_tab():
|
|||||||
outputs=[chapter_content_display]
|
outputs=[chapter_content_display]
|
||||||
)
|
)
|
||||||
stop_btn.click(fn=on_stop, outputs=[generation_progress])
|
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)),
|
|
||||||
gr.update(interactive=bool(p))
|
|
||||||
],
|
|
||||||
inputs=[plot_input],
|
|
||||||
outputs=[total_chapters, custom_outline_prompt, 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]
|
|
||||||
)
|
|
||||||
|
|||||||
+6
-6
@@ -150,12 +150,12 @@ def build_settings_tab():
|
|||||||
# Handle updating the existing backend with new details
|
# Handle updating the existing backend with new details
|
||||||
# If name was changed, it requires updating that as well but the update_backend method modifies existing object's properties in place based on its previous name
|
# If name was changed, it requires updating that as well but the update_backend method modifies existing object's properties in place based on its previous name
|
||||||
result = ConfigAPIManager.update_backend(
|
result = ConfigAPIManager.update_backend(
|
||||||
old_name,
|
old_name,
|
||||||
name=new_name.strip(),
|
new_name=new_name.strip(),
|
||||||
type=btype,
|
type=btype,
|
||||||
base_url=url,
|
base_url=url,
|
||||||
api_key=key,
|
api_key=key,
|
||||||
model=model,
|
model=model,
|
||||||
timeout=int(timeout)
|
timeout=int(timeout)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user