SOY NV AI commited on
Commit ยท
46ab602
1
Parent(s): f5d168b
Add WebNovel story/trace views, style refinement logic, and admin menu updates
Browse files- app/agent/agent.py +18 -4
- app/agent/deps.py +1 -0
- app/database.py +26 -0
- app/routers/creation.py +160 -28
- app/routes.py +186 -1
- app/services/style_analysis_service.py +165 -0
- force_update_menu.py +1 -0
- migrate_add_last_status.py +42 -0
- templates/admin_stories_list.html +59 -0
- templates/admin_styles.html +421 -0
- templates/admin_traces_list.html +59 -0
- templates/admin_webtoon_milestone_producer_manager.html +1 -0
- templates/analysis/webnovel_style.html +382 -0
- templates/novel_dashboard.html +8 -5
- templates/novel_story_view.html +76 -0
- templates/novel_trace_view.html +144 -0
- templates/novel_workspace.html +269 -33
app/agent/agent.py
CHANGED
|
@@ -60,9 +60,8 @@ novel_agent = Agent(
|
|
| 60 |
"1. 'narrative' ํ๋์๋ ๋
์๊ฐ ๋ชฐ์
ํ ์ ์๋ ์์ค ๋ณธ๋ฌธ์ ์์ฑํ์ธ์. ๋ณธ๋ฌธ ๋ด์ ์ํ์ฐฝ์ด๋ ํต๊ณ ์์น๋ฅผ ํฌํจํ์ง ๋ง์ธ์.\n"
|
| 61 |
"2. 'status' ํ๋์๋ ์์ฌ ์ ๊ฐ์ ๋ง์ถฐ ์
๋ฐ์ดํธ๋ ์บ๋ฆญํฐ์ ๊ฒ์์ ์ํ ์ ๋ณด๋ฅผ ๊ตฌ์กฐํํ์ฌ ์
๋ ฅํ์ธ์.\n"
|
| 62 |
"3. 'hidden_thoughts' ํ๋์๋ ๋ค์ ์ ๊ฐ๋ฅผ ์ํ ๋ณต์ ์ด๋ ์์ด์ ํธ์ ์๋๋ฅผ ๊ธฐ๋กํ์ธ์.\n"
|
| 63 |
-
"4. 'image_description' ํ๋์๋ ํ์ฌ ์บ๋ฆญํฐ์ ์ด๋ฆ, ์ธ๋ชจ, ๋ณต์ฅ, ์์น, ์ํฉ, ๋ถ์๊ธฐ ๋ฑ์ ๋ถ์ํ์ฌ ์ด๋ฏธ์ง ์์ฑ์ ์ ํฉํ ์์ด ์ค๋ช
์ ์์ฑํ์ธ์.
|
| 64 |
-
"
|
| 65 |
-
"๋ฐ๋์ ํ์ฌ status์ ์บ๋ฆญํฐ ์ ๋ณด(name, location, affiliation ๋ฑ)์ narrative ๋ด์ฉ์ ๊ธฐ๋ฐ์ผ๋ก ์ ํํ๊ฒ ์์ฑํ์ธ์."
|
| 66 |
)
|
| 67 |
)
|
| 68 |
|
|
@@ -98,6 +97,20 @@ async def dynamic_system_prompt(ctx: RunContext[NovelWriterDeps]) -> str:
|
|
| 98 |
|
| 99 |
facts_str = "\n- ".join(facts) if facts else "์์ง ์ ์ฅ๋ ์ ์ ์ค์ ์ด ์์ต๋๋ค."
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
# ์ด๊ธฐ ์ค์ ์น์
๊ตฌ์ฑ
|
| 102 |
initial_settings_section = ""
|
| 103 |
if initial_settings:
|
|
@@ -115,7 +128,8 @@ async def dynamic_system_prompt(ctx: RunContext[NovelWriterDeps]) -> str:
|
|
| 115 |
|
| 116 |
prompt = f"""
|
| 117 |
[ํ์ฌ ํ๋ก์ ํธ: {project.title}]
|
| 118 |
-
๋ชจ๋: {project.mode.value}
|
|
|
|
| 119 |
{initial_settings_section}
|
| 120 |
[์ ์ ์ธ๊ณ ์ค์ ๋ฐ ์ฌ์ค๊ด๊ณ (Mem0/RAG)]
|
| 121 |
- {facts_str}
|
|
|
|
| 60 |
"1. 'narrative' ํ๋์๋ ๋
์๊ฐ ๋ชฐ์
ํ ์ ์๋ ์์ค ๋ณธ๋ฌธ์ ์์ฑํ์ธ์. ๋ณธ๋ฌธ ๋ด์ ์ํ์ฐฝ์ด๋ ํต๊ณ ์์น๋ฅผ ํฌํจํ์ง ๋ง์ธ์.\n"
|
| 61 |
"2. 'status' ํ๋์๋ ์์ฌ ์ ๊ฐ์ ๋ง์ถฐ ์
๋ฐ์ดํธ๋ ์บ๋ฆญํฐ์ ๊ฒ์์ ์ํ ์ ๋ณด๋ฅผ ๊ตฌ์กฐํํ์ฌ ์
๋ ฅํ์ธ์.\n"
|
| 62 |
"3. 'hidden_thoughts' ํ๋์๋ ๋ค์ ์ ๊ฐ๋ฅผ ์ํ ๋ณต์ ์ด๋ ์์ด์ ํธ์ ์๋๋ฅผ ๊ธฐ๋กํ์ธ์.\n"
|
| 63 |
+
"4. 'image_description' ํ๋์๋ ํ์ฌ ์บ๋ฆญํฐ์ ์ด๋ฆ, ์ธ๋ชจ, ๋ณต์ฅ, ์์น, ์ํฉ, ๋ถ์๊ธฐ ๋ฑ์ ๋ถ์ํ์ฌ ์ด๋ฏธ์ง ์์ฑ์ ์ ํฉํ ์์ด ์ค๋ช
์ ์์ฑํ์ธ์.\n"
|
| 64 |
+
"5. ๋ง์ฝ 'Style Bible'์ด ์ ๊ณต๋ ๊ฒฝ์ฐ, ๋ฐ๋์ [๊ตฌ์ -> ๋ฌธ์ฒด ๋ณํ -> ์ต์ข
๊ฒํ ] ๋จ๊ณ๋ฅผ ๊ฑฐ์ณ ํด๋น ๋ฌธ์ฒด๋ฅผ ์๋ฒฝํ ๋ฐ์ํ ๊ฒฐ๊ณผ๋ฌผ๋ง 'narrative'์ ๋ด์ผ์ธ์. ๋จ์ํ ๋ด์ฉ์ ์์ฝํ๋ ๊ฒ์ด ์๋๋ผ, ๋ฌธ์ฒด์ ํน์ง(์ด๋ฏธ, ๋งํฌ, ๋ฌ์ฌ ๋ฐฉ์ ๋ฑ)์ ์ด๋ ค ํ๋ถํ๊ฒ ์์ ํด์ผ ํฉ๋๋ค."
|
|
|
|
| 65 |
)
|
| 66 |
)
|
| 67 |
|
|
|
|
| 97 |
|
| 98 |
facts_str = "\n- ".join(facts) if facts else "์์ง ์ ์ฅ๋ ์ ์ ์ค์ ์ด ์์ต๋๋ค."
|
| 99 |
|
| 100 |
+
# ๋ฌธ์ฒด ์ค์ ์น์
๊ตฌ์ฑ ๋ฐ ๋จ๊ณ ์ง์
|
| 101 |
+
style_section = ""
|
| 102 |
+
if ctx.deps.style_content:
|
| 103 |
+
style_section = f"""
|
| 104 |
+
[์ ์ฉํ ๋ฌธ์ฒด ๋ฐ ์์ ์ง์นจ (Style Bible)]
|
| 105 |
+
{ctx.deps.style_content}
|
| 106 |
+
|
| 107 |
+
[๋ฌธ์ฒด ์ ์ฉ ์๋ฌด ์ฌํญ]
|
| 108 |
+
ํ์ฌ ์ฌ์ฉ์๊ฐ ํน์ '๋ฌธ์ฒด(Style Bible)'๋ฅผ ์ง์ ํ์ต๋๋ค. ๋น์ ์ ๋ฐ๋์ ์ ์ง์นจ์ ๋ฐ๋ผ ๋ต๋ณ์ ์์ฑํด์ผ ํฉ๋๋ค.
|
| 109 |
+
1. ๋งํฌ์ ์ด๋ฏธ๊ฐ ์ง์นจ๊ณผ ์ผ์นํ๋์ง ํ์ธํ์ธ์.
|
| 110 |
+
2. ๋ฌ์ฌ ๋ฐฉ์(๋น์ , ๋ฌธ์ฅ์ ๊ธธ์ด, ๋ถ์๊ธฐ)์ด ์ง์นจ๊ณผ ๋ถํฉํ๋์ง ํ์ธํ์ธ์.
|
| 111 |
+
3. ๋ง์ฝ ๋น์ ์ด 'Style Bible์ด ์ ๊ณต๋์ง ์์๋ค'๊ณ ๋ต๋ณํ๋ค๋ฉด ๊ทธ๊ฒ์ ์ค๋ต์
๋๋ค. ์ ์น์
์ ๋ถ๋ช
ํ ์ง์นจ์ด ์กด์ฌํฉ๋๋ค.
|
| 112 |
+
"""
|
| 113 |
+
|
| 114 |
# ์ด๊ธฐ ์ค์ ์น์
๊ตฌ์ฑ
|
| 115 |
initial_settings_section = ""
|
| 116 |
if initial_settings:
|
|
|
|
| 128 |
|
| 129 |
prompt = f"""
|
| 130 |
[ํ์ฌ ํ๋ก์ ํธ: {project.title}]
|
| 131 |
+
๋ชจ๋: {project.mode.value if hasattr(project.mode, 'value') else project.mode}
|
| 132 |
+
{style_section}
|
| 133 |
{initial_settings_section}
|
| 134 |
[์ ์ ์ธ๊ณ ์ค์ ๋ฐ ์ฌ์ค๊ด๊ณ (Mem0/RAG)]
|
| 135 |
- {facts_str}
|
app/agent/deps.py
CHANGED
|
@@ -10,4 +10,5 @@ class NovelWriterDeps:
|
|
| 10 |
mem0_client: Any # Mem0 client instance
|
| 11 |
zep_client: Any # Zep client instance
|
| 12 |
rag_service: Any # Interface for existing VectorDB/GraphRAG
|
|
|
|
| 13 |
|
|
|
|
| 10 |
mem0_client: Any # Mem0 client instance
|
| 11 |
zep_client: Any # Zep client instance
|
| 12 |
rag_service: Any # Interface for existing VectorDB/GraphRAG
|
| 13 |
+
style_content: str = None # ์ถ๊ฐ: ์ ์ฉํ ๋ฌธ์ฒด(Style Bible) ๋ด์ฉ
|
| 14 |
|
app/database.py
CHANGED
|
@@ -86,6 +86,7 @@ class NovelProject(db.Model):
|
|
| 86 |
custom_system_prompt = db.Column(db.Text, nullable=True) # ์๊ฐ ์ ์ฉ ์์คํ
์ง์นจ (์ปค์คํ
ํ๋กฌํํธ)
|
| 87 |
viewer_enabled = db.Column(db.Boolean, default=False, nullable=False) # ๋ทฐ์ด ํ์ฑํ ์ฌ๋ถ
|
| 88 |
viewer_selected_message_ids = db.Column(db.Text, nullable=True) # ์ ํ๋ ๋ฉ์์ง ID ๋ชฉ๋ก (JSON ๋ฐฐ์ด)
|
|
|
|
| 89 |
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
| 90 |
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
| 91 |
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
@@ -105,6 +106,7 @@ class NovelProject(db.Model):
|
|
| 105 |
'custom_system_prompt': self.custom_system_prompt,
|
| 106 |
'viewer_enabled': self.viewer_enabled,
|
| 107 |
'viewer_selected_message_ids': self.viewer_selected_message_ids,
|
|
|
|
| 108 |
'user_id': self.user_id,
|
| 109 |
'created_at': self.created_at.isoformat() if self.created_at else None,
|
| 110 |
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
|
@@ -470,6 +472,30 @@ class ChatbotPrompt(db.Model):
|
|
| 470 |
|
| 471 |
|
| 472 |
# -----------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
# ์นํฐ ํ๋ก์ ํธ ์ผ์ (์์
์
๋ก๋)
|
| 474 |
# -----------------------------
|
| 475 |
|
|
|
|
| 86 |
custom_system_prompt = db.Column(db.Text, nullable=True) # ์๊ฐ ์ ์ฉ ์์คํ
์ง์นจ (์ปค์คํ
ํ๋กฌํํธ)
|
| 87 |
viewer_enabled = db.Column(db.Boolean, default=False, nullable=False) # ๋ทฐ์ด ํ์ฑํ ์ฌ๋ถ
|
| 88 |
viewer_selected_message_ids = db.Column(db.Text, nullable=True) # ์ ํ๋ ๋ฉ์์ง ID ๋ชฉ๋ก (JSON ๋ฐฐ์ด)
|
| 89 |
+
last_status = db.Column(db.Text, nullable=True) # AI๊ฐ ํ๋จํ ๋ง์ง๋ง ์บ๋ฆญํฐ/์ธ๊ณ๊ด ์ํ (JSON)
|
| 90 |
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
| 91 |
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
| 92 |
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
|
|
| 106 |
'custom_system_prompt': self.custom_system_prompt,
|
| 107 |
'viewer_enabled': self.viewer_enabled,
|
| 108 |
'viewer_selected_message_ids': self.viewer_selected_message_ids,
|
| 109 |
+
'last_status': self.last_status,
|
| 110 |
'user_id': self.user_id,
|
| 111 |
'created_at': self.created_at.isoformat() if self.created_at else None,
|
| 112 |
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
|
|
|
| 472 |
|
| 473 |
|
| 474 |
# -----------------------------
|
| 475 |
+
class StyleAnalysis(db.Model):
|
| 476 |
+
"""์น์์ค ๋ฌธ์ฒด ๋ถ์ ๊ฒฐ๊ณผ (Style Bible)"""
|
| 477 |
+
__tablename__ = 'style_analysis'
|
| 478 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 479 |
+
file_id = db.Column(db.Integer, db.ForeignKey('uploaded_file.id'), nullable=False, unique=True)
|
| 480 |
+
content = db.Column(db.Text, nullable=False) # ๋ถ์ ๊ฒฐ๊ณผ (JSON ๊ถ์ฅ, ์ฌ๊ธฐ์ Text)
|
| 481 |
+
model_name = db.Column(db.String(100), nullable=True)
|
| 482 |
+
is_public = db.Column(db.Boolean, default=False, nullable=False) # ๊ณต๊ฐ ์ฌ๋ถ ์ถ๊ฐ
|
| 483 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
| 484 |
+
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
| 485 |
+
|
| 486 |
+
file = db.relationship('UploadedFile', backref=db.backref('style_analysis', uselist=False, cascade='all, delete-orphan'))
|
| 487 |
+
|
| 488 |
+
def to_dict(self):
|
| 489 |
+
return {
|
| 490 |
+
'id': self.id,
|
| 491 |
+
'file_id': self.file_id,
|
| 492 |
+
'content': self.content,
|
| 493 |
+
'model_name': self.model_name,
|
| 494 |
+
'is_public': self.is_public,
|
| 495 |
+
'created_at': self.created_at.isoformat() if self.created_at else None,
|
| 496 |
+
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
# ์นํฐ ํ๋ก์ ํธ ์ผ์ (์์
์
๋ก๋)
|
| 500 |
# -----------------------------
|
| 501 |
|
app/routers/creation.py
CHANGED
|
@@ -7,7 +7,7 @@ import asyncio
|
|
| 7 |
import re
|
| 8 |
import json
|
| 9 |
from sqlalchemy import case
|
| 10 |
-
from app.database import db, NovelProject, UploadedFile, ParentChunk, DocumentChunk, GraphEntity, GraphRelationship, GraphEvent, NovelProjectFact
|
| 11 |
from app.routes import inject_admin_menu as shared_inject_admin_menu
|
| 12 |
from app.models.project_config import ProjectConfig, ProjectMode
|
| 13 |
from app.agent.agent import novel_agent
|
|
@@ -46,7 +46,7 @@ async def generate_image_from_description(description: str) -> Optional[str]:
|
|
| 46 |
def inject_admin_menu():
|
| 47 |
return shared_inject_admin_menu()
|
| 48 |
|
| 49 |
-
def get_deps(project_id: str, user_id: int) -> NovelWriterDeps:
|
| 50 |
project_db = NovelProject.query.filter_by(project_id=project_id, user_id=user_id).first()
|
| 51 |
if not project_db:
|
| 52 |
return None
|
|
@@ -66,7 +66,8 @@ def get_deps(project_id: str, user_id: int) -> NovelWriterDeps:
|
|
| 66 |
current_project=project_config,
|
| 67 |
mem0_client=Mem0Service(api_key="mem0_key"),
|
| 68 |
zep_client=ZepService(api_key="zep_key", api_url="http://zep:8000"),
|
| 69 |
-
rag_service=RAGService()
|
|
|
|
| 70 |
)
|
| 71 |
|
| 72 |
@creation_bp.route('/webnovel', methods=['GET'])
|
|
@@ -82,7 +83,14 @@ def workspace(project_id: str):
|
|
| 82 |
project = NovelProject.query.filter_by(project_id=project_id, user_id=current_user.id).first()
|
| 83 |
if not project:
|
| 84 |
return "Project not found", 404
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
def run_async(coro):
|
| 88 |
"""์ค๋ ๋ ์์ ํ๊ฒ ๋น๋๊ธฐ ํจ์๋ฅผ ์คํํ๋ ๋์ฐ๋ฏธ"""
|
|
@@ -135,6 +143,7 @@ def analysis(project_id: str):
|
|
| 135 |
if file_ids:
|
| 136 |
# ์ฐ์ ์์: ์ค์ ํ์ผ(virtual) -> ์ฐธ์กฐ ํ์ผ
|
| 137 |
if settings_file:
|
|
|
|
| 138 |
parent_chunk = ParentChunk.query.filter(ParentChunk.file_id.in_(file_ids)).order_by(
|
| 139 |
case((ParentChunk.file_id == settings_file.id, 0), else_=1)
|
| 140 |
).first()
|
|
@@ -621,21 +630,36 @@ def get_system_prompt(project_id: str):
|
|
| 621 |
def chat_with_agent(project_id: str):
|
| 622 |
"""์์ด์ ํธ์ ์ฑํ
(์งํ ๋์)์ ์งํํฉ๋๋ค."""
|
| 623 |
try:
|
| 624 |
-
from app.database import ChatSession, ChatMessage
|
| 625 |
from pydantic_ai.messages import ModelRequest, ModelResponse, UserPromptPart, TextPart
|
| 626 |
from datetime import datetime
|
| 627 |
|
| 628 |
data = request.get_json()
|
| 629 |
message = data.get('message')
|
| 630 |
model_name = data.get('model') # ํด๋ผ์ด์ธํธ์์ ์ ๋ฌํ ๋ชจ๋ธ๋ช
|
|
|
|
| 631 |
|
| 632 |
-
current_app.logger.info(f"[WebNovel Chat] Project: {project_id}, Message: {message}, Model: {model_name}")
|
| 633 |
|
| 634 |
project = NovelProject.query.filter_by(project_id=project_id, user_id=current_user.id).first()
|
| 635 |
if not project:
|
| 636 |
return jsonify({"error": "Project not found"}), 404
|
| 637 |
|
| 638 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
|
| 640 |
# 1. ๊ธฐ์กด ์ธ์
์ฐพ๊ธฐ ๋๋ ์์ฑ
|
| 641 |
session = ChatSession.query.filter_by(novel_project_id=project.id, user_id=current_user.id).first()
|
|
@@ -701,41 +725,119 @@ def chat_with_agent(project_id: str):
|
|
| 701 |
agent_model = agent_model_input
|
| 702 |
|
| 703 |
# 4. PydanticAI ์์ด์ ํธ ์คํ (history ์ ๋ฌ)
|
| 704 |
-
async def
|
| 705 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
|
| 707 |
-
|
| 708 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 709 |
# 5. AI ์๋ต ๋ฐ์ดํฐ ๋ฐ ํ ํฐ ์ฌ์ฉ๋ ์ถ์ถ
|
| 710 |
-
#
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 724 |
|
| 725 |
# ์ธ์
์
๋ฐ์ดํธ ์๊ฐ ๊ฐฑ์
|
| 726 |
session.updated_at = datetime.utcnow()
|
| 727 |
db.session.commit()
|
| 728 |
|
| 729 |
# ์ด๋ฏธ์ง ์ค๋ช
์์ ์ด๋ฏธ์ง URL ์์ฑ (๋๋ ์ด๋ฏธ์ง ์์ฑ ์๋น์ค ํธ์ถ)
|
| 730 |
-
image_url = run_async(generate_image_from_description(gen_result.image_description)) if gen_result.image_description else None
|
| 731 |
|
| 732 |
return jsonify({
|
| 733 |
"reply": reply_text,
|
| 734 |
"status": status_info,
|
| 735 |
-
"hidden_thoughts": gen_result.hidden_thoughts,
|
| 736 |
-
"image_description": gen_result.image_description,
|
| 737 |
"image_url": image_url,
|
| 738 |
-
"message_id": ai_msg.id,
|
|
|
|
| 739 |
"usage": {
|
| 740 |
"request_tokens": usage.request_tokens or usage.input_tokens,
|
| 741 |
"response_tokens": usage.response_tokens or usage.output_tokens,
|
|
@@ -880,3 +982,33 @@ def public_viewer(project_id: str):
|
|
| 880 |
import traceback
|
| 881 |
current_app.logger.error(f"[Public Viewer Error] {str(e)}\n{traceback.format_exc()}")
|
| 882 |
return f"์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}", 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
import re
|
| 8 |
import json
|
| 9 |
from sqlalchemy import case
|
| 10 |
+
from app.database import db, NovelProject, UploadedFile, ParentChunk, DocumentChunk, GraphEntity, GraphRelationship, GraphEvent, NovelProjectFact, StyleAnalysis
|
| 11 |
from app.routes import inject_admin_menu as shared_inject_admin_menu
|
| 12 |
from app.models.project_config import ProjectConfig, ProjectMode
|
| 13 |
from app.agent.agent import novel_agent
|
|
|
|
| 46 |
def inject_admin_menu():
|
| 47 |
return shared_inject_admin_menu()
|
| 48 |
|
| 49 |
+
def get_deps(project_id: str, user_id: int, style_content: str = None) -> NovelWriterDeps:
|
| 50 |
project_db = NovelProject.query.filter_by(project_id=project_id, user_id=user_id).first()
|
| 51 |
if not project_db:
|
| 52 |
return None
|
|
|
|
| 66 |
current_project=project_config,
|
| 67 |
mem0_client=Mem0Service(api_key="mem0_key"),
|
| 68 |
zep_client=ZepService(api_key="zep_key", api_url="http://zep:8000"),
|
| 69 |
+
rag_service=RAGService(),
|
| 70 |
+
style_content=style_content
|
| 71 |
)
|
| 72 |
|
| 73 |
@creation_bp.route('/webnovel', methods=['GET'])
|
|
|
|
| 83 |
project = NovelProject.query.filter_by(project_id=project_id, user_id=current_user.id).first()
|
| 84 |
if not project:
|
| 85 |
return "Project not found", 404
|
| 86 |
+
|
| 87 |
+
# ๊ณต๊ฐ๋ ๋ฌธ์ฒด ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
|
| 88 |
+
public_styles = StyleAnalysis.query.filter_by(is_public=True).order_by(StyleAnalysis.created_at.desc()).all()
|
| 89 |
+
current_app.logger.info(f"[Workspace] Found {len(public_styles)} public styles")
|
| 90 |
+
for s in public_styles:
|
| 91 |
+
current_app.logger.info(f" - Style ID: {s.id}, Public: {s.is_public}")
|
| 92 |
+
|
| 93 |
+
return render_template('novel_workspace.html', project=project, public_styles=public_styles)
|
| 94 |
|
| 95 |
def run_async(coro):
|
| 96 |
"""์ค๋ ๋ ์์ ํ๊ฒ ๋น๋๊ธฐ ํจ์๋ฅผ ์คํํ๋ ๋์ฐ๋ฏธ"""
|
|
|
|
| 143 |
if file_ids:
|
| 144 |
# ์ฐ์ ์์: ์ค์ ํ์ผ(virtual) -> ์ฐธ์กฐ ํ์ผ
|
| 145 |
if settings_file:
|
| 146 |
+
print(f"DEBUG: Running updated creation.py at line 138")
|
| 147 |
parent_chunk = ParentChunk.query.filter(ParentChunk.file_id.in_(file_ids)).order_by(
|
| 148 |
case((ParentChunk.file_id == settings_file.id, 0), else_=1)
|
| 149 |
).first()
|
|
|
|
| 630 |
def chat_with_agent(project_id: str):
|
| 631 |
"""์์ด์ ํธ์ ์ฑํ
(์งํ ๋์)์ ์งํํฉ๋๋ค."""
|
| 632 |
try:
|
| 633 |
+
from app.database import ChatSession, ChatMessage, StyleAnalysis
|
| 634 |
from pydantic_ai.messages import ModelRequest, ModelResponse, UserPromptPart, TextPart
|
| 635 |
from datetime import datetime
|
| 636 |
|
| 637 |
data = request.get_json()
|
| 638 |
message = data.get('message')
|
| 639 |
model_name = data.get('model') # ํด๋ผ์ด์ธํธ์์ ์ ๋ฌํ ๋ชจ๋ธ๋ช
|
| 640 |
+
style_id = data.get('style_id') # ํด๋ผ์ด์ธํธ์์ ์ ๋ฌํ ๋ฌธ์ฒด ID
|
| 641 |
|
| 642 |
+
current_app.logger.info(f"[WebNovel Chat] Project: {project_id}, Message: {message}, Model: {model_name}, Style: {style_id}")
|
| 643 |
|
| 644 |
project = NovelProject.query.filter_by(project_id=project_id, user_id=current_user.id).first()
|
| 645 |
if not project:
|
| 646 |
return jsonify({"error": "Project not found"}), 404
|
| 647 |
|
| 648 |
+
# ๋ฌธ์ฒด ๋ด์ฉ ๊ฐ์ ธ์ค๊ธฐ
|
| 649 |
+
style_content = None
|
| 650 |
+
if style_id:
|
| 651 |
+
try:
|
| 652 |
+
style_id_int = int(style_id)
|
| 653 |
+
style = StyleAnalysis.query.get(style_id_int)
|
| 654 |
+
if style:
|
| 655 |
+
style_content = style.content
|
| 656 |
+
current_app.logger.info(f"[Style Refinement] Style content found for ID {style_id_int} (length: {len(style_content)})")
|
| 657 |
+
else:
|
| 658 |
+
current_app.logger.warning(f"[Style Refinement] Style ID {style_id_int} not found in DB")
|
| 659 |
+
except (ValueError, TypeError) as e:
|
| 660 |
+
current_app.logger.error(f"[Style Refinement] Invalid style_id format: {style_id} - {str(e)}")
|
| 661 |
+
|
| 662 |
+
deps = get_deps(project_id, current_user.id, style_content=style_content)
|
| 663 |
|
| 664 |
# 1. ๊ธฐ์กด ์ธ์
์ฐพ๊ธฐ ๋๋ ์์ฑ
|
| 665 |
session = ChatSession.query.filter_by(novel_project_id=project.id, user_id=current_user.id).first()
|
|
|
|
| 725 |
agent_model = agent_model_input
|
| 726 |
|
| 727 |
# 4. PydanticAI ์์ด์ ํธ ์คํ (history ์ ๋ฌ)
|
| 728 |
+
async def _run_agent_with_refinement():
|
| 729 |
+
# 1๋จ๊ณ: ๊ธฐ๋ณธ ๋ต๋ณ ์์ฑ
|
| 730 |
+
res1 = await novel_agent.run(message, deps=deps, model=agent_model, message_history=history)
|
| 731 |
+
|
| 732 |
+
if style_content:
|
| 733 |
+
# 2๋จ๊ณ: ๋ฌธ์ฒด ์์ ๋จ๊ณ ์ถ๊ฐ (Self-Refinement)
|
| 734 |
+
current_app.logger.info(f"[Style Refinement] Refining initial response using style_id: {style_id}")
|
| 735 |
+
|
| 736 |
+
# ์ฒซ ๋ฒ์งธ ์์ฑ ๊ฒฐ๊ณผ(์ด์)๋ฅผ ํฌํจํ ์์ ํ์คํ ๋ฆฌ ๊ตฌ์ฑ
|
| 737 |
+
from pydantic_ai.messages import ModelRequest, ModelResponse, UserPromptPart, TextPart
|
| 738 |
+
refine_history = list(history)
|
| 739 |
+
refine_history.append(ModelRequest(parts=[UserPromptPart(content=message)]))
|
| 740 |
+
refine_history.append(ModelResponse(parts=[TextPart(content=res1.output.narrative)]))
|
| 741 |
+
|
| 742 |
+
# ๋ฌธ์ฒด ์์ ์ ์ํ ๋ช
์์ ์์ฒญ
|
| 743 |
+
refine_query = (
|
| 744 |
+
f"### [์ ์ฉํ ๋ฌธ์ฒด ๋ฐ ์์ ์ง์นจ (Style Bible)]\n{style_content}\n\n"
|
| 745 |
+
"์ 'Style Bible'์ ์๋ฒฝํ๊ฒ ๋ฐ์ํ์ฌ ์์ ๋ต๋ณ ๋ด์ฉ์ '์ ๋ฉด ์์ 'ํด ์ฃผ์ธ์.\n"
|
| 746 |
+
"๋จ์ํ ๊ต์ ์ด ์๋๋ผ, Style Bible์์ ๋ถ์๋ ๋งํฌ, ์ด๋ฏธ, ํํ์ ๊ฐ๋, ์์ ๊ธฐ๋ฒ ๋ฑ์ ์ฌ์ฉํ์ฌ ์์์ ๋๋์ด ๊ฐํ๊ฒ ๋๋๋ก ์ฌ์์ฑํด์ผ ํฉ๋๋ค.\n"
|
| 747 |
+
"์ต์ข
์ ์ผ๋ก ๋ฌธ์ฒด๊ฐ ์ ์ฉ๋ ๊ฒฐ๊ณผ๋ฌผ๋ง 'narrative' ํ๋์ ๋ด์ ์๋ตํ์ธ์."
|
| 748 |
+
)
|
| 749 |
+
|
| 750 |
+
# ๋ ๋ฒ์งธ ํจ์ค ์คํ (์์ ๋จ๊ณ)
|
| 751 |
+
res2 = await novel_agent.run(refine_query, deps=deps, model=agent_model, message_history=refine_history)
|
| 752 |
+
|
| 753 |
+
return res1, res2
|
| 754 |
+
|
| 755 |
+
return res1, None
|
| 756 |
|
| 757 |
+
result_initial, result_final = run_async(_run_agent_with_refinement())
|
| 758 |
|
| 759 |
+
# ์ค์ ์ฌ์ฉ๋ ๋ชจ๋ธ๋ช
์ถ์ถ
|
| 760 |
+
actual_model_display = str(agent_model)
|
| 761 |
+
if hasattr(agent_model, 'model_name'):
|
| 762 |
+
actual_model_display = agent_model.model_name
|
| 763 |
+
elif isinstance(agent_model, str):
|
| 764 |
+
actual_model_display = agent_model
|
| 765 |
+
|
| 766 |
# 5. AI ์๋ต ๋ฐ์ดํฐ ๋ฐ ํ ํฐ ์ฌ์ฉ๋ ์ถ์ถ
|
| 767 |
+
# ๊ฒฐ๊ณผ๊ฐ ํ๋๋ง ์๋ ๊ฒฝ์ฐ(style ๋ฏธ์ ์ฉ)
|
| 768 |
+
if result_final is None:
|
| 769 |
+
gen_result = result_initial.output
|
| 770 |
+
usage = result_initial.usage()
|
| 771 |
+
|
| 772 |
+
reply_text = clean_system_comments(gen_result.narrative)
|
| 773 |
+
status_info = gen_result.status.model_dump()
|
| 774 |
+
|
| 775 |
+
# AI ์๋ต ์ ์ฅ
|
| 776 |
+
ai_msg = ChatMessage(
|
| 777 |
+
session_id=session.id,
|
| 778 |
+
role='ai',
|
| 779 |
+
content=reply_text,
|
| 780 |
+
model_name=actual_model_display,
|
| 781 |
+
input_tokens=usage.request_tokens or usage.input_tokens,
|
| 782 |
+
output_tokens=usage.response_tokens or usage.output_tokens,
|
| 783 |
+
usage_type='user'
|
| 784 |
+
)
|
| 785 |
+
db.session.add(ai_msg)
|
| 786 |
+
else:
|
| 787 |
+
# ๊ฒฐ๊ณผ๊ฐ ๋ ๊ฐ ์๋ ๊ฒฝ์ฐ(style ์ ์ฉ)
|
| 788 |
+
gen_result_initial = result_initial.output
|
| 789 |
+
usage_initial = result_initial.usage()
|
| 790 |
+
|
| 791 |
+
gen_result_final = result_final.output
|
| 792 |
+
usage_final = result_final.usage()
|
| 793 |
+
|
| 794 |
+
reply_text = clean_system_comments(gen_result_final.narrative)
|
| 795 |
+
status_info = gen_result_final.status.model_dump()
|
| 796 |
+
|
| 797 |
+
# 1. ์ด์ ์ ์ฅ (system ํ์
์ผ๋ก)
|
| 798 |
+
draft_msg = ChatMessage(
|
| 799 |
+
session_id=session.id,
|
| 800 |
+
role='ai',
|
| 801 |
+
content=clean_system_comments(gen_result_initial.narrative),
|
| 802 |
+
model_name=actual_model_display,
|
| 803 |
+
input_tokens=usage_initial.request_tokens or usage_initial.input_tokens,
|
| 804 |
+
output_tokens=usage_initial.response_tokens or usage_initial.output_tokens,
|
| 805 |
+
usage_type='system' # ์ด์์ ์์คํ
์ฉ์ผ๋ก ํ์
|
| 806 |
+
)
|
| 807 |
+
db.session.add(draft_msg)
|
| 808 |
+
|
| 809 |
+
# 2. ์ต์ข
๋ณธ ์ ์ฅ (user ํ์
์ผ๋ก)
|
| 810 |
+
ai_msg = ChatMessage(
|
| 811 |
+
session_id=session.id,
|
| 812 |
+
role='ai',
|
| 813 |
+
content=reply_text,
|
| 814 |
+
model_name=actual_model_display,
|
| 815 |
+
input_tokens=usage_final.request_tokens or usage_final.input_tokens,
|
| 816 |
+
output_tokens=usage_final.response_tokens or usage_final.output_tokens,
|
| 817 |
+
usage_type='user'
|
| 818 |
+
)
|
| 819 |
+
db.session.add(ai_msg)
|
| 820 |
+
|
| 821 |
+
usage = usage_final # ์ต์ข
์ฌ์ฉ๋์ผ๋ก ์ค์ (์๋ต์ฉ)
|
| 822 |
+
|
| 823 |
+
# ํ๋ก์ ํธ์ ๋ง์ง๋ง ์ํ ์
๋ฐ์ดํธ
|
| 824 |
+
project.last_status = json.dumps(status_info)
|
| 825 |
|
| 826 |
# ์ธ์
์
๋ฐ์ดํธ ์๊ฐ ๊ฐฑ์
|
| 827 |
session.updated_at = datetime.utcnow()
|
| 828 |
db.session.commit()
|
| 829 |
|
| 830 |
# ์ด๋ฏธ์ง ์ค๋ช
์์ ์ด๋ฏธ์ง URL ์์ฑ (๋๋ ์ด๋ฏธ์ง ์์ฑ ์๋น์ค ํธ์ถ)
|
| 831 |
+
image_url = run_async(generate_image_from_description(gen_result_final.image_description if result_final else gen_result.image_description)) if (result_final and gen_result_final.image_description) or (not result_final and gen_result.image_description) else None
|
| 832 |
|
| 833 |
return jsonify({
|
| 834 |
"reply": reply_text,
|
| 835 |
"status": status_info,
|
| 836 |
+
"hidden_thoughts": gen_result_final.hidden_thoughts if result_final else gen_result.hidden_thoughts,
|
| 837 |
+
"image_description": gen_result_final.image_description if result_final else gen_result.image_description,
|
| 838 |
"image_url": image_url,
|
| 839 |
+
"message_id": ai_msg.id,
|
| 840 |
+
"style_applied": True if style_content else False,
|
| 841 |
"usage": {
|
| 842 |
"request_tokens": usage.request_tokens or usage.input_tokens,
|
| 843 |
"response_tokens": usage.response_tokens or usage.output_tokens,
|
|
|
|
| 982 |
import traceback
|
| 983 |
current_app.logger.error(f"[Public Viewer Error] {str(e)}\n{traceback.format_exc()}")
|
| 984 |
return f"์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}", 500
|
| 985 |
+
|
| 986 |
+
@creation_bp.route('/story/<project_id>', methods=['GET'])
|
| 987 |
+
@login_required
|
| 988 |
+
def story_view(project_id: str):
|
| 989 |
+
"""์น์์ค ๋ณธ๋ฌธ๋ง ํ์ธํ๋ ํ์ด์ง"""
|
| 990 |
+
from app.database import ChatSession, ChatMessage
|
| 991 |
+
project = NovelProject.query.filter_by(project_id=project_id, user_id=current_user.id).first_or_404()
|
| 992 |
+
session = ChatSession.query.filter_by(novel_project_id=project.id).first()
|
| 993 |
+
|
| 994 |
+
messages = []
|
| 995 |
+
if session:
|
| 996 |
+
# AI๊ฐ ์์ฑํ ๋ณธ๋ฌธ(ai role)๋ง ๊ฐ์ ธ์ด
|
| 997 |
+
messages = ChatMessage.query.filter_by(session_id=session.id, role='ai').order_by(ChatMessage.created_at.asc()).all()
|
| 998 |
+
|
| 999 |
+
return render_template('novel_story_view.html', project=project, messages=messages)
|
| 1000 |
+
|
| 1001 |
+
@creation_bp.route('/trace/<project_id>', methods=['GET'])
|
| 1002 |
+
@login_required
|
| 1003 |
+
def trace_view(project_id: str):
|
| 1004 |
+
"""์น์์ค ์์ฑ์ ๋ชจ๋ ๊ณผ์ (์ถ๋ก , ์ํ ๋ฑ)์ ํ์ธํ๋ ํ์ด์ง"""
|
| 1005 |
+
from app.database import ChatSession, ChatMessage
|
| 1006 |
+
project = NovelProject.query.filter_by(project_id=project_id, user_id=current_user.id).first_or_404()
|
| 1007 |
+
session = ChatSession.query.filter_by(novel_project_id=project.id).first()
|
| 1008 |
+
|
| 1009 |
+
messages = []
|
| 1010 |
+
if session:
|
| 1011 |
+
# ๋ชจ๋ ๋ฉ์์ง(user, ai)๋ฅผ ๊ฐ์ ธ์ด
|
| 1012 |
+
messages = ChatMessage.query.filter_by(session_id=session.id).order_by(ChatMessage.created_at.asc()).all()
|
| 1013 |
+
|
| 1014 |
+
return render_template('novel_trace_view.html', project=project, messages=messages)
|
app/routes.py
CHANGED
|
@@ -14,13 +14,14 @@ from app.database import (
|
|
| 14 |
WebtoonWBSAnalysis, WebtoonWBSJob,
|
| 15 |
WebtoonProjectDuration, WebtoonEpisodeDuration, WebtoonDurationJob,
|
| 16 |
WebtoonStageKeyMapping, WebtoonMilestone, WebtoonMilestoneManager, WebtoonMilestoneProducer,
|
| 17 |
-
Notice, NoticeRecipient, NotionScheduleCache, PhotoAlbum, Photo
|
| 18 |
)
|
| 19 |
from PIL import Image
|
| 20 |
from app.core.config import Config
|
| 21 |
import io
|
| 22 |
from app.vector_db import get_vector_db
|
| 23 |
from app.gemini_client import get_gemini_client
|
|
|
|
| 24 |
import requests
|
| 25 |
import os
|
| 26 |
from datetime import datetime, timedelta
|
|
@@ -72,8 +73,11 @@ def get_default_admin_menu():
|
|
| 72 |
{"label": "๋ฉ๋ด ๊ด๋ฆฌ", "endpoint": "main.admin_menu", "roles": ["admin"]},
|
| 73 |
{"label": "ํ์ผ ๊ด๋ฆฌ", "endpoint": "main.admin_files"},
|
| 74 |
{"label": "๋ฉ์์ง ํ์ธ", "endpoint": "main.admin_messages"},
|
|
|
|
|
|
|
| 75 |
{"label": "์ ํธ", "endpoint": "main.admin_utils"},
|
| 76 |
{"label": "์ฌ์ง์ฒฉ ๊ด๋ฆฌ", "endpoint": "main.admin_photo_album"},
|
|
|
|
| 77 |
],
|
| 78 |
},
|
| 79 |
{
|
|
@@ -82,6 +86,7 @@ def get_default_admin_menu():
|
|
| 82 |
"items": [
|
| 83 |
{"label": "AI ์ํ ๊ฐ๋ฐ ์ด์์คํดํธ", "endpoint": "main.index"},
|
| 84 |
{"label": "์์ ์ ๋ณด", "endpoint": "main.webnovels"},
|
|
|
|
| 85 |
{"label": "ํ๊ทธ ์ ๋ณด", "endpoint": "main.user_tags"},
|
| 86 |
{"label": "ํ๋กฌํํธ ์ ๋ณด", "endpoint": "main.user_chatbot_prompts"},
|
| 87 |
],
|
|
@@ -201,6 +206,20 @@ def check_db_schema():
|
|
| 201 |
with db.engine.connect() as conn:
|
| 202 |
conn.execute(text("ALTER TABLE photo_album ADD COLUMN representative_photo_id INTEGER REFERENCES photo(id)"))
|
| 203 |
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
except Exception as e:
|
| 205 |
print(f"Schema check error: {e}")
|
| 206 |
|
|
@@ -283,6 +302,7 @@ def get_user_menu():
|
|
| 283 |
"items": [
|
| 284 |
{"label": "AI ์ํ ๊ฐ๋ฐ ์ด์์คํดํธ", "endpoint": "main.index"},
|
| 285 |
{"label": "์์ ์ ๋ณด", "endpoint": "main.webnovels"},
|
|
|
|
| 286 |
{"label": "ํ๊ทธ ์ ๋ณด", "endpoint": "main.user_tags"},
|
| 287 |
{"label": "ํ๋กฌํํธ ์ ๋ณด", "endpoint": "main.user_chatbot_prompts"},
|
| 288 |
],
|
|
@@ -2896,6 +2916,50 @@ def admin_messages():
|
|
| 2896 |
"""๊ด๋ฆฌ์ ๋ฉ์์ง ํ์ธ ํ์ด์ง"""
|
| 2897 |
return render_template('admin_messages.html')
|
| 2898 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2899 |
@main_bp.route('/admin/webtoon/personal-management')
|
| 2900 |
@login_required
|
| 2901 |
def webtoon_personal_management():
|
|
@@ -3025,6 +3089,65 @@ def admin_webnovels():
|
|
| 3025 |
"""์น์์ค ๊ด๋ฆฌ ํ์ด์ง"""
|
| 3026 |
return render_template('admin_webnovels.html')
|
| 3027 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3028 |
|
| 3029 |
@main_bp.route('/admin/webtoons/project-upload')
|
| 3030 |
@admin_required
|
|
@@ -6227,6 +6350,68 @@ def admin_photo_album():
|
|
| 6227 |
albums = PhotoAlbum.query.order_by(PhotoAlbum.created_at.desc()).all()
|
| 6228 |
return render_template('admin_photo_album.html', albums=albums)
|
| 6229 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6230 |
@main_bp.route('/admin/photo-album/create', methods=['POST'])
|
| 6231 |
@admin_required
|
| 6232 |
def admin_photo_album_create():
|
|
|
|
| 14 |
WebtoonWBSAnalysis, WebtoonWBSJob,
|
| 15 |
WebtoonProjectDuration, WebtoonEpisodeDuration, WebtoonDurationJob,
|
| 16 |
WebtoonStageKeyMapping, WebtoonMilestone, WebtoonMilestoneManager, WebtoonMilestoneProducer,
|
| 17 |
+
Notice, NoticeRecipient, NotionScheduleCache, PhotoAlbum, Photo, StyleAnalysis
|
| 18 |
)
|
| 19 |
from PIL import Image
|
| 20 |
from app.core.config import Config
|
| 21 |
import io
|
| 22 |
from app.vector_db import get_vector_db
|
| 23 |
from app.gemini_client import get_gemini_client
|
| 24 |
+
from app.services.style_analysis_service import StyleAnalysisService
|
| 25 |
import requests
|
| 26 |
import os
|
| 27 |
from datetime import datetime, timedelta
|
|
|
|
| 73 |
{"label": "๋ฉ๋ด ๊ด๋ฆฌ", "endpoint": "main.admin_menu", "roles": ["admin"]},
|
| 74 |
{"label": "ํ์ผ ๊ด๋ฆฌ", "endpoint": "main.admin_files"},
|
| 75 |
{"label": "๋ฉ์์ง ํ์ธ", "endpoint": "main.admin_messages"},
|
| 76 |
+
{"label": "์์ค ๋ณธ๋ฌธ ๋ณด๊ธฐ", "endpoint": "main.admin_stories"},
|
| 77 |
+
{"label": "์์ฑ ๊ณผ์ ์ถ์ ", "endpoint": "main.admin_traces"},
|
| 78 |
{"label": "์ ํธ", "endpoint": "main.admin_utils"},
|
| 79 |
{"label": "์ฌ์ง์ฒฉ ๊ด๋ฆฌ", "endpoint": "main.admin_photo_album"},
|
| 80 |
+
{"label": "๋ฌธ์ฒด ๋ชฉ๋ก", "endpoint": "main.admin_styles"},
|
| 81 |
],
|
| 82 |
},
|
| 83 |
{
|
|
|
|
| 86 |
"items": [
|
| 87 |
{"label": "AI ์ํ ๊ฐ๋ฐ ์ด์์คํดํธ", "endpoint": "main.index"},
|
| 88 |
{"label": "์์ ์ ๋ณด", "endpoint": "main.webnovels"},
|
| 89 |
+
{"label": "์น์์ค ๋ฌธ์ฒด ๋ถ์", "endpoint": "main.webnovel_style_analysis"},
|
| 90 |
{"label": "ํ๊ทธ ์ ๋ณด", "endpoint": "main.user_tags"},
|
| 91 |
{"label": "ํ๋กฌํํธ ์ ๋ณด", "endpoint": "main.user_chatbot_prompts"},
|
| 92 |
],
|
|
|
|
| 206 |
with db.engine.connect() as conn:
|
| 207 |
conn.execute(text("ALTER TABLE photo_album ADD COLUMN representative_photo_id INTEGER REFERENCES photo(id)"))
|
| 208 |
conn.commit()
|
| 209 |
+
|
| 210 |
+
# StyleAnalysis ํ
์ด๋ธ
|
| 211 |
+
if 'style_analysis' not in inspector.get_table_names():
|
| 212 |
+
print("Creating 'style_analysis' table...")
|
| 213 |
+
StyleAnalysis.__table__.create(db.engine)
|
| 214 |
+
|
| 215 |
+
# StyleAnalysis ์ปฌ๋ผ ์ถ๊ฐ (is_public)
|
| 216 |
+
columns = [c['name'] for c in inspector.get_columns('style_analysis')]
|
| 217 |
+
if 'is_public' not in columns:
|
| 218 |
+
print("Adding 'is_public' column to style_analysis table...")
|
| 219 |
+
with db.engine.connect() as conn:
|
| 220 |
+
conn.execute(text("ALTER TABLE style_analysis ADD COLUMN is_public BOOLEAN DEFAULT FALSE NOT NULL"))
|
| 221 |
+
conn.commit()
|
| 222 |
+
|
| 223 |
except Exception as e:
|
| 224 |
print(f"Schema check error: {e}")
|
| 225 |
|
|
|
|
| 302 |
"items": [
|
| 303 |
{"label": "AI ์ํ ๊ฐ๋ฐ ์ด์์คํดํธ", "endpoint": "main.index"},
|
| 304 |
{"label": "์์ ์ ๋ณด", "endpoint": "main.webnovels"},
|
| 305 |
+
{"label": "์น์์ค ๋ฌธ์ฒด ๋ถ์", "endpoint": "main.webnovel_style_analysis"},
|
| 306 |
{"label": "ํ๊ทธ ์ ๋ณด", "endpoint": "main.user_tags"},
|
| 307 |
{"label": "ํ๋กฌํํธ ์ ๋ณด", "endpoint": "main.user_chatbot_prompts"},
|
| 308 |
],
|
|
|
|
| 2916 |
"""๊ด๋ฆฌ์ ๋ฉ์์ง ํ์ธ ํ์ด์ง"""
|
| 2917 |
return render_template('admin_messages.html')
|
| 2918 |
|
| 2919 |
+
@main_bp.route('/admin/stories')
|
| 2920 |
+
@main_bp.route('/admin/stories/<project_id>')
|
| 2921 |
+
@admin_required
|
| 2922 |
+
def admin_stories(project_id=None):
|
| 2923 |
+
"""๊ด๋ฆฌ์์ฉ ์น์์ค ๋ณธ๋ฌธ ํ์ธ"""
|
| 2924 |
+
from app.database import NovelProject, ChatSession, ChatMessage
|
| 2925 |
+
|
| 2926 |
+
if project_id is None:
|
| 2927 |
+
# ํ๋ก์ ํธ ๋ชฉ๋ก ํ์
|
| 2928 |
+
projects = NovelProject.query.order_by(NovelProject.created_at.desc()).all()
|
| 2929 |
+
return render_template('admin_stories_list.html', projects=projects)
|
| 2930 |
+
|
| 2931 |
+
# ํน์ ํ๋ก์ ํธ ๋ณธ๋ฌธ ํ์
|
| 2932 |
+
project = NovelProject.query.filter_by(project_id=project_id).first_or_404()
|
| 2933 |
+
session = ChatSession.query.filter_by(novel_project_id=project.id).first()
|
| 2934 |
+
|
| 2935 |
+
messages = []
|
| 2936 |
+
if session:
|
| 2937 |
+
messages = ChatMessage.query.filter_by(session_id=session.id, role='ai').order_by(ChatMessage.created_at.asc()).all()
|
| 2938 |
+
|
| 2939 |
+
return render_template('novel_story_view.html', project=project, messages=messages, is_admin=True)
|
| 2940 |
+
|
| 2941 |
+
@main_bp.route('/admin/traces')
|
| 2942 |
+
@main_bp.route('/admin/traces/<project_id>')
|
| 2943 |
+
@admin_required
|
| 2944 |
+
def admin_traces(project_id=None):
|
| 2945 |
+
"""๊ด๋ฆฌ์์ฉ ์น์์ค ์์ฑ ๊ณผ์ ์ถ์ """
|
| 2946 |
+
from app.database import NovelProject, ChatSession, ChatMessage
|
| 2947 |
+
|
| 2948 |
+
if project_id is None:
|
| 2949 |
+
# ํ๋ก์ ํธ ๋ชฉ๋ก ํ์
|
| 2950 |
+
projects = NovelProject.query.order_by(NovelProject.created_at.desc()).all()
|
| 2951 |
+
return render_template('admin_traces_list.html', projects=projects)
|
| 2952 |
+
|
| 2953 |
+
# ํน์ ํ๋ก์ ํธ ์ถ์ ์ ๋ณด ํ์
|
| 2954 |
+
project = NovelProject.query.filter_by(project_id=project_id).first_or_404()
|
| 2955 |
+
session = ChatSession.query.filter_by(novel_project_id=project.id).first()
|
| 2956 |
+
|
| 2957 |
+
messages = []
|
| 2958 |
+
if session:
|
| 2959 |
+
messages = ChatMessage.query.filter_by(session_id=session.id).order_by(ChatMessage.created_at.asc()).all()
|
| 2960 |
+
|
| 2961 |
+
return render_template('novel_trace_view.html', project=project, messages=messages, is_admin=True)
|
| 2962 |
+
|
| 2963 |
@main_bp.route('/admin/webtoon/personal-management')
|
| 2964 |
@login_required
|
| 2965 |
def webtoon_personal_management():
|
|
|
|
| 3089 |
"""์น์์ค ๊ด๋ฆฌ ํ์ด์ง"""
|
| 3090 |
return render_template('admin_webnovels.html')
|
| 3091 |
|
| 3092 |
+
@main_bp.route('/analysis/webnovel_style')
|
| 3093 |
+
@login_required
|
| 3094 |
+
def webnovel_style_analysis():
|
| 3095 |
+
"""์น์์ค ๋ฌธ์ฒด ๋ถ์ ํ์ด์ง"""
|
| 3096 |
+
# ์
๋ก๋๋ ์น์์ค ๋ชฉ๋ก ์กฐํ
|
| 3097 |
+
files = UploadedFile.query.order_by(UploadedFile.uploaded_at.desc()).all()
|
| 3098 |
+
|
| 3099 |
+
# ์ฌ์ฉ ๊ฐ๋ฅํ AI ๋ชจ๋ธ ๋ชฉ๋ก ์กฐํ
|
| 3100 |
+
gemini = get_gemini_client()
|
| 3101 |
+
models = gemini.get_available_models()
|
| 3102 |
+
|
| 3103 |
+
return render_template('analysis/webnovel_style.html', files=files, models=models)
|
| 3104 |
+
|
| 3105 |
+
@main_bp.route('/api/analysis/style', methods=['POST'])
|
| 3106 |
+
@login_required
|
| 3107 |
+
def api_analyze_webnovel_style():
|
| 3108 |
+
"""์น์์ค ๋ฌธ์ฒด ๋ถ์ ์์ฒญ ์ฒ๋ฆฌ"""
|
| 3109 |
+
try:
|
| 3110 |
+
data = request.get_json()
|
| 3111 |
+
file_id = data.get('file_id')
|
| 3112 |
+
model_name = data.get('model_name')
|
| 3113 |
+
analysis_type = data.get('type') # 'all' (default), 'bible', 'summary', 'vector'
|
| 3114 |
+
|
| 3115 |
+
if not file_id:
|
| 3116 |
+
return jsonify({'error': 'ํ์ผ ID๊ฐ ํ์ํฉ๋๋ค.'}), 400
|
| 3117 |
+
|
| 3118 |
+
service = StyleAnalysisService()
|
| 3119 |
+
|
| 3120 |
+
if analysis_type and analysis_type != 'all':
|
| 3121 |
+
# ๋ถ๋ถ ๋ถ์ ์คํ
|
| 3122 |
+
result = service.analyze_part(file_id, model_name, analysis_type)
|
| 3123 |
+
return jsonify({'success': True, 'type': analysis_type, 'result': result})
|
| 3124 |
+
else:
|
| 3125 |
+
# ์ ์ฒด ๋ถ์ ์คํ (๊ธฐ์กด ํธํ์ฑ)
|
| 3126 |
+
result = service.analyze_style(file_id, model_name)
|
| 3127 |
+
return jsonify({'success': True, 'type': 'all', 'result': result})
|
| 3128 |
+
|
| 3129 |
+
except Exception as e:
|
| 3130 |
+
return jsonify({'success': False, 'error': str(e)}), 500
|
| 3131 |
+
|
| 3132 |
+
@main_bp.route('/api/analysis/style/save', methods=['POST'])
|
| 3133 |
+
@login_required
|
| 3134 |
+
def api_save_webnovel_style():
|
| 3135 |
+
"""์น์์ค ๋ฌธ์ฒด ๋ถ์ ๊ฒฐ๊ณผ ์ ์ฅ (Track 1)"""
|
| 3136 |
+
try:
|
| 3137 |
+
data = request.get_json()
|
| 3138 |
+
file_id = data.get('file_id')
|
| 3139 |
+
content = data.get('content')
|
| 3140 |
+
|
| 3141 |
+
if not file_id or not content:
|
| 3142 |
+
return jsonify({'error': 'ํ์ ๋ฐ์ดํฐ ๋๋ฝ'}), 400
|
| 3143 |
+
|
| 3144 |
+
service = StyleAnalysisService()
|
| 3145 |
+
service.save_style_bible(file_id, content)
|
| 3146 |
+
|
| 3147 |
+
return jsonify({'success': True})
|
| 3148 |
+
except Exception as e:
|
| 3149 |
+
return jsonify({'success': False, 'error': str(e)}), 500
|
| 3150 |
+
|
| 3151 |
|
| 3152 |
@main_bp.route('/admin/webtoons/project-upload')
|
| 3153 |
@admin_required
|
|
|
|
| 6350 |
albums = PhotoAlbum.query.order_by(PhotoAlbum.created_at.desc()).all()
|
| 6351 |
return render_template('admin_photo_album.html', albums=albums)
|
| 6352 |
|
| 6353 |
+
@main_bp.route('/admin/styles')
|
| 6354 |
+
@admin_required
|
| 6355 |
+
def admin_styles():
|
| 6356 |
+
"""๋ฌธ์ฒด ๋ชฉ๋ก ๊ด๋ฆฌ ํ์ด์ง"""
|
| 6357 |
+
styles = StyleAnalysis.query.order_by(StyleAnalysis.created_at.desc()).all()
|
| 6358 |
+
return render_template('admin_styles.html', styles=styles)
|
| 6359 |
+
|
| 6360 |
+
@main_bp.route('/api/admin/styles/<int:style_id>/visibility', methods=['POST'])
|
| 6361 |
+
@admin_required
|
| 6362 |
+
def toggle_style_visibility(style_id):
|
| 6363 |
+
"""๋ฌธ์ฒด ๊ณต๊ฐ/๋น๊ณต๊ฐ ํ ๊ธ"""
|
| 6364 |
+
style = StyleAnalysis.query.get_or_404(style_id)
|
| 6365 |
+
data = request.get_json()
|
| 6366 |
+
|
| 6367 |
+
style.is_public = data.get('is_public', False)
|
| 6368 |
+
db.session.commit()
|
| 6369 |
+
|
| 6370 |
+
return jsonify({'success': True, 'is_public': style.is_public})
|
| 6371 |
+
|
| 6372 |
+
@main_bp.route('/api/admin/styles/<int:style_id>', methods=['DELETE'])
|
| 6373 |
+
@admin_required
|
| 6374 |
+
def delete_style(style_id):
|
| 6375 |
+
"""๋ฌธ์ฒด ์ญ์ """
|
| 6376 |
+
style = StyleAnalysis.query.get_or_404(style_id)
|
| 6377 |
+
db.session.delete(style)
|
| 6378 |
+
db.session.commit()
|
| 6379 |
+
return jsonify({'success': True})
|
| 6380 |
+
|
| 6381 |
+
@main_bp.route('/api/admin/styles/<int:style_id>', methods=['GET'])
|
| 6382 |
+
@admin_required
|
| 6383 |
+
def get_style_detail(style_id):
|
| 6384 |
+
"""๋ฌธ์ฒด ์์ธ ์กฐํ"""
|
| 6385 |
+
style = StyleAnalysis.query.get_or_404(style_id)
|
| 6386 |
+
return jsonify({
|
| 6387 |
+
'success': True,
|
| 6388 |
+
'style': {
|
| 6389 |
+
'id': style.id,
|
| 6390 |
+
'filename': style.file.original_filename if style.file else 'ํ์ผ ์ญ์ ๋จ',
|
| 6391 |
+
'content': style.content,
|
| 6392 |
+
'model_name': style.model_name,
|
| 6393 |
+
'is_public': style.is_public,
|
| 6394 |
+
'created_at': style.created_at.strftime('%Y-%m-%d %H:%M')
|
| 6395 |
+
}
|
| 6396 |
+
})
|
| 6397 |
+
|
| 6398 |
+
@main_bp.route('/api/admin/styles/<int:style_id>/edit', methods=['POST'])
|
| 6399 |
+
@admin_required
|
| 6400 |
+
def edit_style(style_id):
|
| 6401 |
+
"""๋ฌธ์ฒด ์์ """
|
| 6402 |
+
style = StyleAnalysis.query.get_or_404(style_id)
|
| 6403 |
+
data = request.get_json()
|
| 6404 |
+
|
| 6405 |
+
if 'content' in data:
|
| 6406 |
+
style.content = data['content']
|
| 6407 |
+
if 'model_name' in data:
|
| 6408 |
+
style.model_name = data['model_name']
|
| 6409 |
+
if 'is_public' in data:
|
| 6410 |
+
style.is_public = data['is_public']
|
| 6411 |
+
|
| 6412 |
+
db.session.commit()
|
| 6413 |
+
return jsonify({'success': True})
|
| 6414 |
+
|
| 6415 |
@main_bp.route('/admin/photo-album/create', methods=['POST'])
|
| 6416 |
@admin_required
|
| 6417 |
def admin_photo_album_create():
|
app/services/style_analysis_service.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import logging
|
| 4 |
+
from app.database import db, UploadedFile, StyleAnalysis, ParentChunk, DocumentChunk
|
| 5 |
+
from app.gemini_client import get_gemini_client
|
| 6 |
+
from app.vector_db import get_vector_db
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
+
class StyleAnalysisService:
|
| 11 |
+
def __init__(self):
|
| 12 |
+
self.gemini = get_gemini_client()
|
| 13 |
+
self.vector_db = get_vector_db()
|
| 14 |
+
|
| 15 |
+
def analyze_style(self, file_id: int, model_name: str = None):
|
| 16 |
+
"""
|
| 17 |
+
[Legacy] ์ ์ฒด ๋ถ์ ํ๋ฒ์ ์คํ
|
| 18 |
+
"""
|
| 19 |
+
results = {}
|
| 20 |
+
results["style_bible"] = self.analyze_part(file_id, model_name, "bible")
|
| 21 |
+
results["summary"] = self.analyze_part(file_id, model_name, "summary")
|
| 22 |
+
results["vector_db_status"] = self.analyze_part(file_id, model_name, "vector")
|
| 23 |
+
return results
|
| 24 |
+
|
| 25 |
+
def analyze_part(self, file_id: int, model_name: str, part: str):
|
| 26 |
+
"""
|
| 27 |
+
๋ถ๋ถ ๋ถ์ ์คํ
|
| 28 |
+
part: 'bible', 'summary', 'vector'
|
| 29 |
+
"""
|
| 30 |
+
file_record = UploadedFile.query.get(file_id)
|
| 31 |
+
if not file_record:
|
| 32 |
+
raise ValueError(f"File {file_id} not found")
|
| 33 |
+
|
| 34 |
+
text_content = self._read_file_content(file_record.file_path)
|
| 35 |
+
# ํ
์คํธ๊ฐ ์์ด๋ DB ์กฐํ๋ ์๋ํด์ผ ํจ (์ด๋ฏธ ์ ์ฅ๋ ๊ฒฝ์ฐ)
|
| 36 |
+
|
| 37 |
+
if part == "bible":
|
| 38 |
+
return self._generate_style_bible(file_record, text_content, model_name)
|
| 39 |
+
elif part == "summary":
|
| 40 |
+
return self._generate_summary(file_record, text_content, model_name)
|
| 41 |
+
elif part == "vector":
|
| 42 |
+
return self._process_embeddings(file_record, text_content)
|
| 43 |
+
else:
|
| 44 |
+
raise ValueError(f"Unknown analysis part: {part}")
|
| 45 |
+
|
| 46 |
+
def save_style_bible(self, file_id: int, content: str):
|
| 47 |
+
"""Style Bible ์๋ ์ ์ฅ/์์ """
|
| 48 |
+
analysis = StyleAnalysis.query.filter_by(file_id=file_id).first()
|
| 49 |
+
if analysis:
|
| 50 |
+
analysis.content = content
|
| 51 |
+
analysis.updated_at = db.func.now()
|
| 52 |
+
else:
|
| 53 |
+
analysis = StyleAnalysis(file_id=file_id, content=content)
|
| 54 |
+
db.session.add(analysis)
|
| 55 |
+
|
| 56 |
+
db.session.commit()
|
| 57 |
+
return True
|
| 58 |
+
|
| 59 |
+
def _read_file_content(self, file_path: str) -> str:
|
| 60 |
+
"""ํ์ผ ์ฝ๊ธฐ ๋ฐ ์ ์ฒ๋ฆฌ"""
|
| 61 |
+
try:
|
| 62 |
+
if not os.path.exists(file_path):
|
| 63 |
+
file_path = os.path.join(os.getcwd(), file_path)
|
| 64 |
+
|
| 65 |
+
if not os.path.exists(file_path):
|
| 66 |
+
return ""
|
| 67 |
+
|
| 68 |
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
| 69 |
+
return f.read()
|
| 70 |
+
except Exception as e:
|
| 71 |
+
logger.error(f"Error reading file {file_path}: {e}")
|
| 72 |
+
return ""
|
| 73 |
+
|
| 74 |
+
def _generate_style_bible(self, file_record: UploadedFile, text: str, model_name: str) -> dict:
|
| 75 |
+
"""Track 1: ๋ฌธ์ฒด ๋ถ์๊ฐ (DB ์กฐํ ์ฐ์ )"""
|
| 76 |
+
# 1. DB ์กฐํ
|
| 77 |
+
existing = StyleAnalysis.query.filter_by(file_id=file_record.id).first()
|
| 78 |
+
if existing and existing.content:
|
| 79 |
+
return existing.content
|
| 80 |
+
|
| 81 |
+
# 2. ํ
์คํธ ์์ผ๋ฉด ๋ถ์ ๋ถ๊ฐ
|
| 82 |
+
if not text:
|
| 83 |
+
return "์๋ณธ ํ์ผ ํ
์คํธ๋ฅผ ์ฝ์ ์ ์์ด ๋ถ์ํ ์ ์์ต๋๋ค."
|
| 84 |
+
|
| 85 |
+
# 3. LLM ๋ถ์
|
| 86 |
+
sample_text = text[:10000]
|
| 87 |
+
prompt = f"""
|
| 88 |
+
๋ค์ ์น์์ค ํ
์คํธ๋ฅผ ๋ถ์ํ์ฌ 'Style Bible'์ ์์ฑํด์ค.
|
| 89 |
+
๋ค์ ํญ๋ชฉ๋ค์ ํฌํจํด์ผ ํด:
|
| 90 |
+
1. ์๊ฐ ํค์ค๋งค๋ (๋ถ์๊ธฐ, ์ด์กฐ)
|
| 91 |
+
2. ๋ฌธ์ฅ ํธํก ๊ธธ์ด (๋จ๋ฌธ/์ฅ๋ฌธ ๋น์จ, ๋ฆฌ๋ฌ๊ฐ)
|
| 92 |
+
3. ์์ฃผ ์ฐ๋ ์ด๋ฏธ๋ ๋ฌธ์ฒด์ ํน์ง
|
| 93 |
+
4. ๋ฌ์ฌ ๋ ๋ํ์ ๋น์จ (๋๋ต์ ์ธ ํผ์ผํธ์ ํน์ง)
|
| 94 |
+
5. ์ฃผ์ ํค์๋๋ ๋ฐ๋ณต๋๋ ํํ
|
| 95 |
+
|
| 96 |
+
ํ
์คํธ:
|
| 97 |
+
{sample_text}
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
response = self.gemini.generate_response(prompt, model_name=model_name)
|
| 101 |
+
if response.get('error'):
|
| 102 |
+
raise Exception(response['error'])
|
| 103 |
+
|
| 104 |
+
result_text = response.get('response', '')
|
| 105 |
+
|
| 106 |
+
# 4. DB ์ ์ฅ
|
| 107 |
+
try:
|
| 108 |
+
new_analysis = StyleAnalysis(
|
| 109 |
+
file_id=file_record.id,
|
| 110 |
+
content=result_text,
|
| 111 |
+
model_name=model_name
|
| 112 |
+
)
|
| 113 |
+
db.session.add(new_analysis)
|
| 114 |
+
db.session.commit()
|
| 115 |
+
except Exception as e:
|
| 116 |
+
logger.error(f"Failed to save Style Bible: {e}")
|
| 117 |
+
db.session.rollback()
|
| 118 |
+
|
| 119 |
+
return result_text
|
| 120 |
+
|
| 121 |
+
def _generate_summary(self, file_record: UploadedFile, text: str, model_name: str) -> str:
|
| 122 |
+
"""Track 3: ์ค๊ฑฐ๋ฆฌ ์์ฝ๊ฐ (๊ธฐ์กด ParentChunk ์กฐํ)"""
|
| 123 |
+
# 1. DB ์กฐํ (ParentChunk)
|
| 124 |
+
parent_chunk = ParentChunk.query.filter_by(file_id=file_record.id).first()
|
| 125 |
+
if parent_chunk:
|
| 126 |
+
# ๊ธฐ์กด ์ ๋ณด ์กฐํฉํ์ฌ ๋ฐํ
|
| 127 |
+
summary_parts = []
|
| 128 |
+
if parent_chunk.world_view: summary_parts.append(f"[์ธ๊ณ๊ด]\n{parent_chunk.world_view}")
|
| 129 |
+
if parent_chunk.characters: summary_parts.append(f"[์ธ๋ฌผ]\n{parent_chunk.characters}")
|
| 130 |
+
if parent_chunk.story: summary_parts.append(f"[์คํ ๋ฆฌ]\n{parent_chunk.story}")
|
| 131 |
+
if parent_chunk.episodes: summary_parts.append(f"[์ํผ์๋]\n{parent_chunk.episodes}")
|
| 132 |
+
|
| 133 |
+
if summary_parts:
|
| 134 |
+
return "\n\n".join(summary_parts)
|
| 135 |
+
|
| 136 |
+
# 2. ํ
์คํธ ์์ผ๋ฉด ๋ถ์ ๋ถ๊ฐ
|
| 137 |
+
if not text:
|
| 138 |
+
return "์๋ณธ ํ์ผ ํ
์คํธ๋ฅผ ์ฝ์ ์ ์์ด ์์ฝํ ์ ์์ต๋๋ค."
|
| 139 |
+
|
| 140 |
+
# 3. LLM ๋ถ์ (๊ธฐ์กด ๋ฐ์ดํฐ๊ฐ ์์ผ๋ฉด ์๋ก ์์ฑํ๋, ParentChunk์๋ ๋ฃ์ง ์๊ณ ๋จ์ ํ
์คํธ ๋ฐํ)
|
| 141 |
+
# (์ํ๋ค๋ฉด ParentChunk์ ์ ์ฅํ๋ ๋ก์ง์ ์ถ๊ฐํ ์ ์์)
|
| 142 |
+
sample_text = text[:15000]
|
| 143 |
+
prompt = f"""
|
| 144 |
+
๋ค์ ์น์์ค์ ์ด๋ฐ๋ถ ๋ด์ฉ์ ๋ฐํ์ผ๋ก ์ ์ฒด์ ์ธ ์์ฌ ํ๋ฆ๊ณผ ์ค๊ฑฐ๋ฆฌ๋ฅผ ์์ฝํด์ค.
|
| 145 |
+
์ฃผ์ ๋ฑ์ฅ์ธ๋ฌผ๊ณผ ๊ทธ๋ค์ ๊ด๊ณ, ๊ทธ๋ฆฌ๊ณ ์์๋๋ ์ฌ๊ฑด์ ์ค์ฌ์ผ๋ก ์ ๋ฆฌํด์ค.
|
| 146 |
+
|
| 147 |
+
ํ
์คํธ:
|
| 148 |
+
{sample_text}
|
| 149 |
+
"""
|
| 150 |
+
|
| 151 |
+
response = self.gemini.generate_response(prompt, model_name=model_name)
|
| 152 |
+
if response.get('error'):
|
| 153 |
+
raise Exception(response['error'])
|
| 154 |
+
|
| 155 |
+
return response.get('response', '')
|
| 156 |
+
|
| 157 |
+
def _process_embeddings(self, file_record: UploadedFile, text: str):
|
| 158 |
+
"""Track 2: Chunking & Embedding -> Vector DB (๊ธฐ์กด ๋ฐ์ดํฐ ํ์ธ)"""
|
| 159 |
+
# 1. ์ฒญํฌ ๊ฐ์ ํ์ธ
|
| 160 |
+
chunk_count = DocumentChunk.query.filter_by(file_id=file_record.id).count()
|
| 161 |
+
|
| 162 |
+
if chunk_count > 0:
|
| 163 |
+
return f"โ
์ด๋ฏธ ๊ตฌ์ถ๋จ ({chunk_count}๊ฐ์ ์ฒญํฌ)"
|
| 164 |
+
|
| 165 |
+
return "โ ๏ธ ๊ตฌ์ถ๋์ง ์์ (์๋ ์ฒ๋ฆฌ ๋๊ธฐ ์ค)"
|
force_update_menu.py
CHANGED
|
@@ -55,5 +55,6 @@ if __name__ == "__main__":
|
|
| 55 |
|
| 56 |
|
| 57 |
|
|
|
|
| 58 |
|
| 59 |
|
|
|
|
| 55 |
|
| 56 |
|
| 57 |
|
| 58 |
+
|
| 59 |
|
| 60 |
|
migrate_add_last_status.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
import os
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ฒฝ๋ก
|
| 6 |
+
db_path = Path(__file__).parent / 'instance' / 'finance_analysis.db'
|
| 7 |
+
|
| 8 |
+
def migrate_database():
|
| 9 |
+
"""novel_project ํ
์ด๋ธ์ last_status ์ปฌ๋ผ ์ถ๊ฐ"""
|
| 10 |
+
if not db_path.exists():
|
| 11 |
+
print(f"๋ฐ์ดํฐ๋ฒ ์ด์ค ํ์ผ์ด ์์ต๋๋ค: {db_path}")
|
| 12 |
+
return
|
| 13 |
+
|
| 14 |
+
try:
|
| 15 |
+
conn = sqlite3.connect(str(db_path))
|
| 16 |
+
cursor = conn.cursor()
|
| 17 |
+
|
| 18 |
+
# novel_project ํ
์ด๋ธ์ last_status ์ปฌ๋ผ์ด ์๋์ง ํ์ธ
|
| 19 |
+
cursor.execute("PRAGMA table_info(novel_project)")
|
| 20 |
+
columns = [column[1] for column in cursor.fetchall()]
|
| 21 |
+
|
| 22 |
+
if 'last_status' in columns:
|
| 23 |
+
print("last_status ์ปฌ๋ผ์ด ์ด๋ฏธ ์กด์ฌํฉ๋๋ค.")
|
| 24 |
+
conn.close()
|
| 25 |
+
return
|
| 26 |
+
|
| 27 |
+
# last_status ์ปฌ๋ผ ์ถ๊ฐ
|
| 28 |
+
print("last_status ์ปฌ๋ผ์ ์ถ๊ฐํ๋ ์ค...")
|
| 29 |
+
cursor.execute("ALTER TABLE novel_project ADD COLUMN last_status TEXT")
|
| 30 |
+
conn.commit()
|
| 31 |
+
print("last_status ์ปฌ๋ผ์ด ์ฑ๊ณต์ ์ผ๋ก ์ถ๊ฐ๋์์ต๋๋ค.")
|
| 32 |
+
|
| 33 |
+
conn.close()
|
| 34 |
+
print("๋ง์ด๊ทธ๋ ์ด์
์ด ์๋ฃ๋์์ต๋๋ค.")
|
| 35 |
+
|
| 36 |
+
except Exception as e:
|
| 37 |
+
print(f"์ค๋ฅ ๋ฐ์: {e}")
|
| 38 |
+
raise
|
| 39 |
+
|
| 40 |
+
if __name__ == '__main__':
|
| 41 |
+
migrate_database()
|
| 42 |
+
|
templates/admin_stories_list.html
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "creation_base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}์์ค ๋ณธ๋ฌธ ๋ชฉ๋ก - ๊ด๋ฆฌ์{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="container py-5">
|
| 7 |
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
| 8 |
+
<h2><i class="fas fa-book me-2"></i> ์์ค ๋ณธ๋ฌธ ๋ชฉ๋ก (๊ด๋ฆฌ์)</h2>
|
| 9 |
+
<a href="/" class="btn btn-outline-secondary">ํ์ผ๋ก</a>
|
| 10 |
+
</div>
|
| 11 |
+
|
| 12 |
+
<div class="card shadow-sm">
|
| 13 |
+
<div class="card-body">
|
| 14 |
+
<div class="table-responsive">
|
| 15 |
+
<table class="table table-hover align-middle">
|
| 16 |
+
<thead class="table-light">
|
| 17 |
+
<tr>
|
| 18 |
+
<th>ID</th>
|
| 19 |
+
<th>ํ๋ก์ ํธ ์ ๋ชฉ</th>
|
| 20 |
+
<th>์์ฑ์</th>
|
| 21 |
+
<th>์์ฑ์ผ</th>
|
| 22 |
+
<th>์ํ</th>
|
| 23 |
+
<th>์ก์
</th>
|
| 24 |
+
</tr>
|
| 25 |
+
</thead>
|
| 26 |
+
<tbody>
|
| 27 |
+
{% for p in projects %}
|
| 28 |
+
<tr>
|
| 29 |
+
<td><code>{{ p.project_id }}</code></td>
|
| 30 |
+
<td><strong>{{ p.title }}</strong></td>
|
| 31 |
+
<td>{{ p.user.username if p.user else 'Unknown' }}</td>
|
| 32 |
+
<td>{{ p.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
| 33 |
+
<td>
|
| 34 |
+
{% if p.last_status %}
|
| 35 |
+
<span class="badge bg-success">์งํ๋จ</span>
|
| 36 |
+
{% else %}
|
| 37 |
+
<span class="badge bg-secondary">๋๊ธฐ์ค</span>
|
| 38 |
+
{% endif %}
|
| 39 |
+
</td>
|
| 40 |
+
<td>
|
| 41 |
+
<a href="/admin/stories/{{ p.project_id }}" class="btn btn-sm btn-primary">
|
| 42 |
+
<i class="fas fa-eye me-1"></i> ๋ณธ๋ฌธ ๋ณด๊ธฐ
|
| 43 |
+
</a>
|
| 44 |
+
</td>
|
| 45 |
+
</tr>
|
| 46 |
+
{% endfor %}
|
| 47 |
+
{% if not projects %}
|
| 48 |
+
<tr>
|
| 49 |
+
<td colspan="6" class="text-center py-4 text-muted">๋ฑ๋ก๋ ํ๋ก์ ํธ๊ฐ ์์ต๋๋ค.</td>
|
| 50 |
+
</tr>
|
| 51 |
+
{% endif %}
|
| 52 |
+
</tbody>
|
| 53 |
+
</table>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
{% endblock %}
|
| 59 |
+
|
templates/admin_styles.html
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="ko">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>๋ฌธ์ฒด ๋ชฉ๋ก ๊ด๋ฆฌ - SOYMEDIA</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
| 8 |
+
<style>
|
| 9 |
+
:root {
|
| 10 |
+
--bg-primary: #ffffff;
|
| 11 |
+
--bg-secondary: #f8f9fa;
|
| 12 |
+
--text-primary: #202124;
|
| 13 |
+
--text-secondary: #5f6368;
|
| 14 |
+
--accent: #1a73e8;
|
| 15 |
+
--border: #dadce0;
|
| 16 |
+
--success: #1e8e3e;
|
| 17 |
+
--danger: #d93025;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
body {
|
| 21 |
+
font-family: 'Inter', sans-serif;
|
| 22 |
+
background: var(--bg-secondary);
|
| 23 |
+
color: var(--text-primary);
|
| 24 |
+
margin: 0;
|
| 25 |
+
padding: 20px;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
.container {
|
| 29 |
+
max-width: 1200px;
|
| 30 |
+
margin: 0 auto;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
.header {
|
| 34 |
+
background: var(--bg-primary);
|
| 35 |
+
padding: 20px;
|
| 36 |
+
border-radius: 8px;
|
| 37 |
+
margin-bottom: 20px;
|
| 38 |
+
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
| 39 |
+
display: flex;
|
| 40 |
+
justify-content: space-between;
|
| 41 |
+
align-items: center;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.header h1 {
|
| 45 |
+
margin: 0;
|
| 46 |
+
font-size: 24px;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.card {
|
| 50 |
+
background: var(--bg-primary);
|
| 51 |
+
border-radius: 8px;
|
| 52 |
+
padding: 20px;
|
| 53 |
+
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
table {
|
| 57 |
+
width: 100%;
|
| 58 |
+
border-collapse: collapse;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
th, td {
|
| 62 |
+
text-align: left;
|
| 63 |
+
padding: 12px;
|
| 64 |
+
border-bottom: 1px solid var(--border);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
th {
|
| 68 |
+
background: var(--bg-secondary);
|
| 69 |
+
font-weight: 600;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.btn {
|
| 73 |
+
padding: 6px 12px;
|
| 74 |
+
border: none;
|
| 75 |
+
border-radius: 4px;
|
| 76 |
+
cursor: pointer;
|
| 77 |
+
font-size: 13px;
|
| 78 |
+
font-weight: 500;
|
| 79 |
+
transition: background 0.2s;
|
| 80 |
+
text-decoration: none;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.btn-primary {
|
| 84 |
+
background: var(--accent);
|
| 85 |
+
color: white;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.btn-danger {
|
| 89 |
+
background: var(--danger);
|
| 90 |
+
color: white;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.btn-outline {
|
| 94 |
+
background: transparent;
|
| 95 |
+
border: 1px solid var(--border);
|
| 96 |
+
color: var(--text-primary);
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.btn-outline.active {
|
| 100 |
+
background: var(--accent);
|
| 101 |
+
color: white;
|
| 102 |
+
border-color: var(--accent);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.switch {
|
| 106 |
+
position: relative;
|
| 107 |
+
display: inline-block;
|
| 108 |
+
width: 34px;
|
| 109 |
+
height: 20px;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.switch input {
|
| 113 |
+
opacity: 0;
|
| 114 |
+
width: 0;
|
| 115 |
+
height: 0;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.slider {
|
| 119 |
+
position: absolute;
|
| 120 |
+
cursor: pointer;
|
| 121 |
+
top: 0;
|
| 122 |
+
left: 0;
|
| 123 |
+
right: 0;
|
| 124 |
+
bottom: 0;
|
| 125 |
+
background-color: #ccc;
|
| 126 |
+
transition: .4s;
|
| 127 |
+
border-radius: 20px;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.slider:before {
|
| 131 |
+
position: absolute;
|
| 132 |
+
content: "";
|
| 133 |
+
height: 14px;
|
| 134 |
+
width: 14px;
|
| 135 |
+
left: 3px;
|
| 136 |
+
bottom: 3px;
|
| 137 |
+
background-color: white;
|
| 138 |
+
transition: .4s;
|
| 139 |
+
border-radius: 50%;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
input:checked + .slider {
|
| 143 |
+
background-color: var(--accent);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
input:checked + .slider:before {
|
| 147 |
+
transform: translateX(14px);
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
/* Modal Styles */
|
| 151 |
+
.modal {
|
| 152 |
+
display: none;
|
| 153 |
+
position: fixed;
|
| 154 |
+
z-index: 1000;
|
| 155 |
+
left: 0;
|
| 156 |
+
top: 0;
|
| 157 |
+
width: 100%;
|
| 158 |
+
height: 100%;
|
| 159 |
+
background-color: rgba(0,0,0,0.5);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.modal-content {
|
| 163 |
+
background-color: var(--bg-primary);
|
| 164 |
+
margin: 5% auto;
|
| 165 |
+
padding: 24px;
|
| 166 |
+
border-radius: 8px;
|
| 167 |
+
width: 80%;
|
| 168 |
+
max-width: 800px;
|
| 169 |
+
max-height: 80vh;
|
| 170 |
+
overflow-y: auto;
|
| 171 |
+
position: relative;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.close {
|
| 175 |
+
position: absolute;
|
| 176 |
+
right: 20px;
|
| 177 |
+
top: 15px;
|
| 178 |
+
font-size: 28px;
|
| 179 |
+
font-weight: bold;
|
| 180 |
+
cursor: pointer;
|
| 181 |
+
color: var(--text-secondary);
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.form-group {
|
| 185 |
+
margin-bottom: 20px;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.form-group label {
|
| 189 |
+
display: block;
|
| 190 |
+
margin-bottom: 8px;
|
| 191 |
+
font-weight: 600;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.form-control {
|
| 195 |
+
width: 100%;
|
| 196 |
+
padding: 10px;
|
| 197 |
+
border: 1px solid var(--border);
|
| 198 |
+
border-radius: 4px;
|
| 199 |
+
box-sizing: border-box;
|
| 200 |
+
font-family: inherit;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
textarea.form-control {
|
| 204 |
+
min-height: 300px;
|
| 205 |
+
resize: vertical;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.modal-footer {
|
| 209 |
+
margin-top: 24px;
|
| 210 |
+
display: flex;
|
| 211 |
+
justify-content: flex-end;
|
| 212 |
+
gap: 12px;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.style-link {
|
| 216 |
+
color: var(--accent);
|
| 217 |
+
text-decoration: none;
|
| 218 |
+
cursor: pointer;
|
| 219 |
+
font-weight: 500;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.style-link:hover {
|
| 223 |
+
text-decoration: underline;
|
| 224 |
+
}
|
| 225 |
+
</style>
|
| 226 |
+
</head>
|
| 227 |
+
<body>
|
| 228 |
+
<div class="container">
|
| 229 |
+
<div class="header">
|
| 230 |
+
<h1>๋ฌธ์ฒด ๋ชฉ๋ก ๊ด๋ฆฌ</h1>
|
| 231 |
+
<div>
|
| 232 |
+
<a href="/admin" class="btn btn-outline">๊ด๋ฆฌ์ ํ</a>
|
| 233 |
+
</div>
|
| 234 |
+
</div>
|
| 235 |
+
|
| 236 |
+
<div class="card">
|
| 237 |
+
<table>
|
| 238 |
+
<thead>
|
| 239 |
+
<tr>
|
| 240 |
+
<th>ID</th>
|
| 241 |
+
<th>ํ์ผ๋ช
</th>
|
| 242 |
+
<th>๋ชจ๋ธ</th>
|
| 243 |
+
<th>์์ฑ์ผ</th>
|
| 244 |
+
<th>๊ณต๊ฐ ์ฌ๋ถ</th>
|
| 245 |
+
<th>๊ด๋ฆฌ</th>
|
| 246 |
+
</tr>
|
| 247 |
+
</thead>
|
| 248 |
+
<tbody>
|
| 249 |
+
{% for style in styles %}
|
| 250 |
+
<tr id="row-{{ style.id }}">
|
| 251 |
+
<td>{{ style.id }}</td>
|
| 252 |
+
<td>
|
| 253 |
+
<span class="style-link" onclick="openStyleModal({{ style.id }})">
|
| 254 |
+
{{ style.file.original_filename if style.file else 'ํ์ผ ์ญ์ ๋จ' }}
|
| 255 |
+
</span>
|
| 256 |
+
</td>
|
| 257 |
+
<td>{{ style.model_name or '-' }}</td>
|
| 258 |
+
<td>{{ style.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
| 259 |
+
<td>
|
| 260 |
+
<label class="switch">
|
| 261 |
+
<input type="checkbox" onchange="toggleVisibility({{ style.id }}, this)" {% if style.is_public %}checked{% endif %}>
|
| 262 |
+
<span class="slider"></span>
|
| 263 |
+
</label>
|
| 264 |
+
<span id="status-{{ style.id }}" style="margin-left: 8px; font-size: 13px;">
|
| 265 |
+
{{ '๊ณต๊ฐ' if style.is_public else '๋ฏธ๊ณต๊ฐ' }}
|
| 266 |
+
</span>
|
| 267 |
+
</td>
|
| 268 |
+
<td>
|
| 269 |
+
<button class="btn btn-danger" onclick="deleteStyle({{ style.id }})">์ญ์ </button>
|
| 270 |
+
</td>
|
| 271 |
+
</tr>
|
| 272 |
+
{% else %}
|
| 273 |
+
<tr>
|
| 274 |
+
<td colspan="6" style="text-align: center; padding: 30px; color: var(--text-secondary);">
|
| 275 |
+
์์ฑ๋ ๋ฌธ์ฒด ๋ถ์ ๊ฒฐ๊ณผ๊ฐ ์์ต๋๋ค.
|
| 276 |
+
</td>
|
| 277 |
+
</tr>
|
| 278 |
+
{% endfor %}
|
| 279 |
+
</tbody>
|
| 280 |
+
</table>
|
| 281 |
+
</div>
|
| 282 |
+
</div>
|
| 283 |
+
|
| 284 |
+
<!-- Edit Modal -->
|
| 285 |
+
<div id="styleModal" class="modal">
|
| 286 |
+
<div class="modal-content">
|
| 287 |
+
<span class="close" onclick="closeModal()">×</span>
|
| 288 |
+
<h2 id="modalTitle">๋ฌธ์ฒด ์์ธ ์ ๋ณด</h2>
|
| 289 |
+
<form id="editForm">
|
| 290 |
+
<input type="hidden" id="editStyleId">
|
| 291 |
+
<div class="form-group">
|
| 292 |
+
<label for="editFilename">ํ์ผ๋ช
</label>
|
| 293 |
+
<input type="text" id="editFilename" class="form-control" readonly>
|
| 294 |
+
</div>
|
| 295 |
+
<div class="form-group">
|
| 296 |
+
<label for="editModelName">๋ชจ๋ธ๋ช
</label>
|
| 297 |
+
<input type="text" id="editModelName" class="form-control">
|
| 298 |
+
</div>
|
| 299 |
+
<div class="form-group">
|
| 300 |
+
<label for="editContent">๋ถ์ ๋ด์ฉ</label>
|
| 301 |
+
<textarea id="editContent" class="form-control"></textarea>
|
| 302 |
+
</div>
|
| 303 |
+
<div class="modal-footer">
|
| 304 |
+
<button type="button" class="btn btn-outline" onclick="closeModal()">๋ซ๊ธฐ</button>
|
| 305 |
+
<button type="submit" class="btn btn-primary">์ ์ฅํ๊ธฐ</button>
|
| 306 |
+
</div>
|
| 307 |
+
</form>
|
| 308 |
+
</div>
|
| 309 |
+
</div>
|
| 310 |
+
|
| 311 |
+
<script>
|
| 312 |
+
const modal = document.getElementById('styleModal');
|
| 313 |
+
const editForm = document.getElementById('editForm');
|
| 314 |
+
|
| 315 |
+
async function openStyleModal(id) {
|
| 316 |
+
try {
|
| 317 |
+
const response = await fetch(`/api/admin/styles/${id}`);
|
| 318 |
+
const data = await response.json();
|
| 319 |
+
|
| 320 |
+
if (data.success) {
|
| 321 |
+
document.getElementById('editStyleId').value = data.style.id;
|
| 322 |
+
document.getElementById('editFilename').value = data.style.filename;
|
| 323 |
+
document.getElementById('editModelName').value = data.style.model_name || '';
|
| 324 |
+
document.getElementById('editContent').value = data.style.content;
|
| 325 |
+
|
| 326 |
+
modal.style.display = 'block';
|
| 327 |
+
} else {
|
| 328 |
+
alert('์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค์ง ๋ชปํ์ต๋๋ค.');
|
| 329 |
+
}
|
| 330 |
+
} catch (e) {
|
| 331 |
+
console.error(e);
|
| 332 |
+
alert('์ค๋ฅ ๋ฐ์');
|
| 333 |
+
}
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
function closeModal() {
|
| 337 |
+
modal.style.display = 'none';
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
window.onclick = function(event) {
|
| 341 |
+
if (event.target == modal) {
|
| 342 |
+
closeModal();
|
| 343 |
+
}
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
editForm.onsubmit = async function(e) {
|
| 347 |
+
e.preventDefault();
|
| 348 |
+
const id = document.getElementById('editStyleId').value;
|
| 349 |
+
const content = document.getElementById('editContent').value;
|
| 350 |
+
const modelName = document.getElementById('editModelName').value;
|
| 351 |
+
|
| 352 |
+
try {
|
| 353 |
+
const response = await fetch(`/api/admin/styles/${id}/edit`, {
|
| 354 |
+
method: 'POST',
|
| 355 |
+
headers: { 'Content-Type': 'application/json' },
|
| 356 |
+
body: JSON.stringify({
|
| 357 |
+
content: content,
|
| 358 |
+
model_name: modelName
|
| 359 |
+
})
|
| 360 |
+
});
|
| 361 |
+
|
| 362 |
+
const result = await response.json();
|
| 363 |
+
if (result.success) {
|
| 364 |
+
alert('์ ์ฅ๋์์ต๋๋ค.');
|
| 365 |
+
location.reload(); // ๋ฆฌ์คํธ ๊ฐฑ์ ์ ์ํด ๋ฆฌ๋ก๋
|
| 366 |
+
} else {
|
| 367 |
+
alert('์ ์ฅ ์คํจ');
|
| 368 |
+
}
|
| 369 |
+
} catch (e) {
|
| 370 |
+
console.error(e);
|
| 371 |
+
alert('์ค๋ฅ ๋ฐ์');
|
| 372 |
+
}
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
async function toggleVisibility(id, checkbox) {
|
| 376 |
+
const isPublic = checkbox.checked;
|
| 377 |
+
const statusSpan = document.getElementById(`status-${id}`);
|
| 378 |
+
|
| 379 |
+
try {
|
| 380 |
+
const response = await fetch(`/api/admin/styles/${id}/visibility`, {
|
| 381 |
+
method: 'POST',
|
| 382 |
+
headers: { 'Content-Type': 'application/json' },
|
| 383 |
+
body: JSON.stringify({ is_public: isPublic })
|
| 384 |
+
});
|
| 385 |
+
|
| 386 |
+
const result = await response.json();
|
| 387 |
+
if (result.success) {
|
| 388 |
+
statusSpan.textContent = isPublic ? '๊ณต๊ฐ' : '๋ฏธ๊ณต๊ฐ';
|
| 389 |
+
} else {
|
| 390 |
+
alert('๋ณ๊ฒฝ ์คํจ');
|
| 391 |
+
checkbox.checked = !isPublic; // ๋๋๋ฆฌ๊ธฐ
|
| 392 |
+
}
|
| 393 |
+
} catch (e) {
|
| 394 |
+
console.error(e);
|
| 395 |
+
alert('์ค๋ฅ ๋ฐ์');
|
| 396 |
+
checkbox.checked = !isPublic;
|
| 397 |
+
}
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
async function deleteStyle(id) {
|
| 401 |
+
if (!confirm('์ ๋ง ์ญ์ ํ์๊ฒ ์ต๋๊น? ๋ณต๊ตฌํ ์ ์์ต๋๋ค.')) return;
|
| 402 |
+
|
| 403 |
+
try {
|
| 404 |
+
const response = await fetch(`/api/admin/styles/${id}`, {
|
| 405 |
+
method: 'DELETE'
|
| 406 |
+
});
|
| 407 |
+
|
| 408 |
+
if (response.ok) {
|
| 409 |
+
document.getElementById(`row-${id}`).remove();
|
| 410 |
+
} else {
|
| 411 |
+
alert('์ญ์ ์คํจ');
|
| 412 |
+
}
|
| 413 |
+
} catch (e) {
|
| 414 |
+
console.error(e);
|
| 415 |
+
alert('์ค๋ฅ ๋ฐ์');
|
| 416 |
+
}
|
| 417 |
+
}
|
| 418 |
+
</script>
|
| 419 |
+
</body>
|
| 420 |
+
</html>
|
| 421 |
+
|
templates/admin_traces_list.html
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "creation_base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}์์ฑ ๊ณผ์ ์ถ์ ๋ชฉ๋ก - ๊ด๋ฆฌ์{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="container py-5">
|
| 7 |
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
| 8 |
+
<h2><i class="fas fa-history me-2"></i> ์์ฑ ๊ณผ์ ์ถ์ ๋ชฉ๋ก (๊ด๋ฆฌ์)</h2>
|
| 9 |
+
<a href="/" class="btn btn-outline-secondary">ํ์ผ๋ก</a>
|
| 10 |
+
</div>
|
| 11 |
+
|
| 12 |
+
<div class="card shadow-sm">
|
| 13 |
+
<div class="card-body">
|
| 14 |
+
<div class="table-responsive">
|
| 15 |
+
<table class="table table-hover align-middle">
|
| 16 |
+
<thead class="table-light">
|
| 17 |
+
<tr>
|
| 18 |
+
<th>ID</th>
|
| 19 |
+
<th>ํ๋ก์ ํธ ์ ๋ชฉ</th>
|
| 20 |
+
<th>์์ฑ์</th>
|
| 21 |
+
<th>์์ฑ์ผ</th>
|
| 22 |
+
<th>์งํ ๋ก๊ทธ</th>
|
| 23 |
+
<th>์ก์
</th>
|
| 24 |
+
</tr>
|
| 25 |
+
</thead>
|
| 26 |
+
<tbody>
|
| 27 |
+
{% for p in projects %}
|
| 28 |
+
<tr>
|
| 29 |
+
<td><code>{{ p.project_id }}</code></td>
|
| 30 |
+
<td><strong>{{ p.title }}</strong></td>
|
| 31 |
+
<td>{{ p.user.username if p.user else 'Unknown' }}</td>
|
| 32 |
+
<td>{{ p.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
| 33 |
+
<td>
|
| 34 |
+
{% if p.last_status %}
|
| 35 |
+
<small class="text-muted">{{ p.last_status[:50] }}...</small>
|
| 36 |
+
{% else %}
|
| 37 |
+
-
|
| 38 |
+
{% endif %}
|
| 39 |
+
</td>
|
| 40 |
+
<td>
|
| 41 |
+
<a href="/admin/traces/{{ p.project_id }}" class="btn btn-sm btn-warning">
|
| 42 |
+
<i class="fas fa-search me-1"></i> ๊ณผ์ ์ถ์
|
| 43 |
+
</a>
|
| 44 |
+
</td>
|
| 45 |
+
</tr>
|
| 46 |
+
{% endfor %}
|
| 47 |
+
{% if not projects %}
|
| 48 |
+
<tr>
|
| 49 |
+
<td colspan="6" class="text-center py-4 text-muted">๋ฑ๋ก๋ ํ๋ก์ ํธ๊ฐ ์์ต๋๋ค.</td>
|
| 50 |
+
</tr>
|
| 51 |
+
{% endif %}
|
| 52 |
+
</tbody>
|
| 53 |
+
</table>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
{% endblock %}
|
| 59 |
+
|
templates/admin_webtoon_milestone_producer_manager.html
CHANGED
|
@@ -351,3 +351,4 @@
|
|
| 351 |
|
| 352 |
|
| 353 |
|
|
|
|
|
|
| 351 |
|
| 352 |
|
| 353 |
|
| 354 |
+
|
templates/analysis/webnovel_style.html
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="ko">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>์น์์ค ๋ฌธ์ฒด ๋ถ์ - SOYMEDIA</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
| 8 |
+
<style>
|
| 9 |
+
:root {
|
| 10 |
+
--bg-primary: #ffffff;
|
| 11 |
+
--bg-secondary: #f8f9fa;
|
| 12 |
+
--text-primary: #202124;
|
| 13 |
+
--text-secondary: #5f6368;
|
| 14 |
+
--accent: #1a73e8;
|
| 15 |
+
--border: #dadce0;
|
| 16 |
+
--success: #1e8e3e;
|
| 17 |
+
--warning: #f9ab00;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
body {
|
| 21 |
+
font-family: 'Inter', sans-serif;
|
| 22 |
+
background: var(--bg-secondary);
|
| 23 |
+
color: var(--text-primary);
|
| 24 |
+
margin: 0;
|
| 25 |
+
padding: 20px;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
.container {
|
| 29 |
+
max-width: 1200px;
|
| 30 |
+
margin: 0 auto;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
.header {
|
| 34 |
+
background: var(--bg-primary);
|
| 35 |
+
padding: 20px;
|
| 36 |
+
border-radius: 8px;
|
| 37 |
+
margin-bottom: 20px;
|
| 38 |
+
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
| 39 |
+
display: flex;
|
| 40 |
+
justify-content: space-between;
|
| 41 |
+
align-items: center;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.header h1 {
|
| 45 |
+
margin: 0;
|
| 46 |
+
font-size: 24px;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.card {
|
| 50 |
+
background: var(--bg-primary);
|
| 51 |
+
border-radius: 8px;
|
| 52 |
+
padding: 20px;
|
| 53 |
+
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
| 54 |
+
margin-bottom: 20px;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
table {
|
| 58 |
+
width: 100%;
|
| 59 |
+
border-collapse: collapse;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
th, td {
|
| 63 |
+
text-align: left;
|
| 64 |
+
padding: 12px;
|
| 65 |
+
border-bottom: 1px solid var(--border);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
th {
|
| 69 |
+
background: var(--bg-secondary);
|
| 70 |
+
font-weight: 600;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.btn {
|
| 74 |
+
padding: 8px 16px;
|
| 75 |
+
border: none;
|
| 76 |
+
border-radius: 4px;
|
| 77 |
+
cursor: pointer;
|
| 78 |
+
font-size: 14px;
|
| 79 |
+
font-weight: 500;
|
| 80 |
+
transition: background 0.2s;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.btn-primary {
|
| 84 |
+
background: var(--accent);
|
| 85 |
+
color: white;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.btn-primary:hover {
|
| 89 |
+
background: #1557b0;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.btn-secondary {
|
| 93 |
+
background: #f1f3f4;
|
| 94 |
+
color: var(--text-primary);
|
| 95 |
+
border: 1px solid var(--border);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.form-group {
|
| 99 |
+
margin-bottom: 15px;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
select {
|
| 103 |
+
padding: 8px;
|
| 104 |
+
border-radius: 4px;
|
| 105 |
+
border: 1px solid var(--border);
|
| 106 |
+
width: 200px;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.result-area {
|
| 110 |
+
margin-top: 20px;
|
| 111 |
+
padding: 20px;
|
| 112 |
+
background: #f1f3f4;
|
| 113 |
+
border-radius: 8px;
|
| 114 |
+
display: none;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.log-area {
|
| 118 |
+
font-family: monospace;
|
| 119 |
+
background: #202124;
|
| 120 |
+
color: #e8eaed;
|
| 121 |
+
padding: 15px;
|
| 122 |
+
border-radius: 4px;
|
| 123 |
+
margin-bottom: 20px;
|
| 124 |
+
max-height: 200px;
|
| 125 |
+
overflow-y: auto;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.log-item {
|
| 129 |
+
margin-bottom: 5px;
|
| 130 |
+
display: flex;
|
| 131 |
+
align-items: center;
|
| 132 |
+
gap: 8px;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.status-icon {
|
| 136 |
+
width: 16px;
|
| 137 |
+
height: 16px;
|
| 138 |
+
display: inline-block;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.status-pending::before { content: "โช"; }
|
| 142 |
+
.status-loading::before { content: "โณ"; }
|
| 143 |
+
.status-success::before { content: "โ
"; }
|
| 144 |
+
.status-error::before { content: "โ"; }
|
| 145 |
+
|
| 146 |
+
/* Mermaid Diagram Style */
|
| 147 |
+
.diagram-container {
|
| 148 |
+
margin-bottom: 30px;
|
| 149 |
+
overflow-x: auto;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
/* Editable Content */
|
| 153 |
+
.editable-content {
|
| 154 |
+
background: white;
|
| 155 |
+
padding: 15px;
|
| 156 |
+
border-radius: 4px;
|
| 157 |
+
margin-bottom: 15px;
|
| 158 |
+
border: 1px solid var(--border);
|
| 159 |
+
min-height: 200px;
|
| 160 |
+
white-space: pre-wrap;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.editable-content:focus {
|
| 164 |
+
outline: 2px solid var(--accent);
|
| 165 |
+
}
|
| 166 |
+
</style>
|
| 167 |
+
<!-- Mermaid.js for diagram -->
|
| 168 |
+
<script type="module">
|
| 169 |
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
|
| 170 |
+
mermaid.initialize({ startOnLoad: true });
|
| 171 |
+
</script>
|
| 172 |
+
</head>
|
| 173 |
+
<body>
|
| 174 |
+
<div class="container">
|
| 175 |
+
<div class="header">
|
| 176 |
+
<h1>์น์์ค ๋ฌธ์ฒด ๋ถ์</h1>
|
| 177 |
+
<div>
|
| 178 |
+
<a href="/" class="btn">ํ์ผ๋ก</a>
|
| 179 |
+
</div>
|
| 180 |
+
</div>
|
| 181 |
+
|
| 182 |
+
<!-- Workflow Diagram -->
|
| 183 |
+
<div class="card diagram-container">
|
| 184 |
+
<h3>๋ถ์ ์ํฌํ๋ก์ฐ</h3>
|
| 185 |
+
<pre class="mermaid">
|
| 186 |
+
graph LR
|
| 187 |
+
A[๐ ์น์์ค ์๋ฌธ] --> B(ํ
์คํธ ์ ์ฒ๋ฆฌ)
|
| 188 |
+
subgraph "Track 1: ๋ฌธ์ฒด ๋ถ์"
|
| 189 |
+
B --> C1{๐ค LLM: ๋ฌธ์ฒด ๋ถ์๊ฐ}
|
| 190 |
+
C1 --> D1[๐ Style Bible]
|
| 191 |
+
end
|
| 192 |
+
subgraph "Track 2: ์ ์ฅ"
|
| 193 |
+
B --> C2(Chunking & Embedding)
|
| 194 |
+
C2 --> D2[(๐๏ธ Vector DB)]
|
| 195 |
+
end
|
| 196 |
+
subgraph "Track 3: ์์ฝ"
|
| 197 |
+
B --> C3{๐ค LLM: ์์ฝ๊ฐ}
|
| 198 |
+
C3 --> D3[๐๏ธ ์ค๊ฑฐ๋ฆฌ DB]
|
| 199 |
+
end
|
| 200 |
+
</pre>
|
| 201 |
+
<p style="font-size: 0.9em; color: var(--text-secondary);">* Track 2(์ ์ฅ)์ Track 3(์์ฝ)์ <a href="/admin/webnovels">์น์์ค ๊ด๋ฆฌ</a> ๋ฉ๋ด์ ๊ธฐ์กด ๋ฐ์ดํฐ๋ฅผ ์ฐธ์กฐํฉ๋๋ค.</p>
|
| 202 |
+
</div>
|
| 203 |
+
|
| 204 |
+
<div class="card">
|
| 205 |
+
<h3>๋ถ์ ์ค์ </h3>
|
| 206 |
+
<div class="form-group">
|
| 207 |
+
<label for="aiModel">์ฌ์ฉํ AI ๋ชจ๋ธ:</label>
|
| 208 |
+
<select id="aiModel">
|
| 209 |
+
{% for model in models %}
|
| 210 |
+
<option value="{{ model }}" {% if 'pro' in model %}selected{% endif %}>{{ model }}</option>
|
| 211 |
+
{% endfor %}
|
| 212 |
+
</select>
|
| 213 |
+
</div>
|
| 214 |
+
</div>
|
| 215 |
+
|
| 216 |
+
<div class="card">
|
| 217 |
+
<h3>์
๋ก๋๋ ์น์์ค ๋ชฉ๋ก</h3>
|
| 218 |
+
<table>
|
| 219 |
+
<thead>
|
| 220 |
+
<tr>
|
| 221 |
+
<th>ID</th>
|
| 222 |
+
<th>์ ๋ชฉ</th>
|
| 223 |
+
<th>ํ์ผ๋ช
</th>
|
| 224 |
+
<th>์
๋ก๋ ์ผ์</th>
|
| 225 |
+
<th>์์
</th>
|
| 226 |
+
</tr>
|
| 227 |
+
</thead>
|
| 228 |
+
<tbody>
|
| 229 |
+
{% for file in files %}
|
| 230 |
+
<tr>
|
| 231 |
+
<td>{{ file.id }}</td>
|
| 232 |
+
<td>{{ file.filename }}</td>
|
| 233 |
+
<td>{{ file.original_filename }}</td>
|
| 234 |
+
<td>{{ file.uploaded_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
| 235 |
+
<td>
|
| 236 |
+
<button class="btn btn-primary" onclick="startAnalysis({{ file.id }})">๋ถ์ ํํฉ ํ์ธ / ์คํ</button>
|
| 237 |
+
</td>
|
| 238 |
+
</tr>
|
| 239 |
+
{% endfor %}
|
| 240 |
+
</tbody>
|
| 241 |
+
</table>
|
| 242 |
+
</div>
|
| 243 |
+
|
| 244 |
+
<div id="analysisResult" class="card result-area">
|
| 245 |
+
<h3>๋ถ์ ๊ฒฐ๊ณผ ๋ฐ ๊ด๋ฆฌ</h3>
|
| 246 |
+
<div id="logArea" class="log-area">
|
| 247 |
+
<div id="logBible" class="log-item status-pending">Track 1: Style Bible ์ํ ํ์ธ ์ค...</div>
|
| 248 |
+
<div id="logSummary" class="log-item status-pending">Track 3: ์ค๊ฑฐ๋ฆฌ ์์ฝ ๋ฐ์ดํฐ ํ์ธ ์ค...</div>
|
| 249 |
+
<div id="logVector" class="log-item status-pending">Track 2: Vector DB ๊ตฌ์ถ ์ํ ํ์ธ ์ค...</div>
|
| 250 |
+
</div>
|
| 251 |
+
|
| 252 |
+
<div id="resultContent"></div>
|
| 253 |
+
</div>
|
| 254 |
+
</div>
|
| 255 |
+
|
| 256 |
+
<script>
|
| 257 |
+
async function callApi(fileId, modelName, type) {
|
| 258 |
+
const response = await fetch('/api/analysis/style', {
|
| 259 |
+
method: 'POST',
|
| 260 |
+
headers: { 'Content-Type': 'application/json' },
|
| 261 |
+
body: JSON.stringify({
|
| 262 |
+
file_id: fileId,
|
| 263 |
+
model_name: modelName,
|
| 264 |
+
type: type
|
| 265 |
+
})
|
| 266 |
+
});
|
| 267 |
+
return await response.json();
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
async function saveStyleBible(fileId) {
|
| 271 |
+
const content = document.getElementById('bibleContent').value;
|
| 272 |
+
if(!content) {
|
| 273 |
+
alert('์ ์ฅํ ๋ด์ฉ์ด ์์ต๋๋ค.');
|
| 274 |
+
return;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
if(!confirm('Style Bible ๋ด์ฉ์ ์ ์ฅํ์๊ฒ ์ต๋๊น?')) return;
|
| 278 |
+
|
| 279 |
+
try {
|
| 280 |
+
const response = await fetch('/api/analysis/style/save', {
|
| 281 |
+
method: 'POST',
|
| 282 |
+
headers: { 'Content-Type': 'application/json' },
|
| 283 |
+
body: JSON.stringify({
|
| 284 |
+
file_id: fileId,
|
| 285 |
+
content: content
|
| 286 |
+
})
|
| 287 |
+
});
|
| 288 |
+
const result = await response.json();
|
| 289 |
+
if(result.success) {
|
| 290 |
+
alert('์ ์ฅ๋์์ต๋๋ค.');
|
| 291 |
+
} else {
|
| 292 |
+
alert('์ ์ฅ ์คํจ: ' + result.error);
|
| 293 |
+
}
|
| 294 |
+
} catch(e) {
|
| 295 |
+
alert('์ค๋ฅ ๋ฐ์: ' + e.message);
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
function updateLog(elementId, status, text) {
|
| 300 |
+
const el = document.getElementById(elementId);
|
| 301 |
+
el.className = `log-item status-${status}`;
|
| 302 |
+
el.textContent = text;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
async function startAnalysis(fileId) {
|
| 306 |
+
const modelName = document.getElementById('aiModel').value;
|
| 307 |
+
const resultArea = document.getElementById('analysisResult');
|
| 308 |
+
const content = document.getElementById('resultContent');
|
| 309 |
+
|
| 310 |
+
resultArea.style.display = 'block';
|
| 311 |
+
content.innerHTML = ''; // ์ด๊ธฐํ
|
| 312 |
+
|
| 313 |
+
// ์ํ ์ด๊ธฐํ
|
| 314 |
+
updateLog('logBible', 'pending', 'Track 1: Style Bible ๋ฐ์ดํฐ ์กฐํ ์ค...');
|
| 315 |
+
updateLog('logSummary', 'pending', 'Track 3: ์ค๊ฑฐ๋ฆฌ ์์ฝ ๋ฐ์ดํฐ ์กฐํ ์ค...');
|
| 316 |
+
updateLog('logVector', 'pending', 'Track 2: Vector DB ์ํ ์กฐํ ์ค...');
|
| 317 |
+
|
| 318 |
+
try {
|
| 319 |
+
// 1. Style Bible (Track 1)
|
| 320 |
+
updateLog('logBible', 'loading', 'Track 1: Style Bible ์ฒ๋ฆฌ ์ค... (DB์กฐํ or LLM์์ฑ)');
|
| 321 |
+
const bibleRes = await callApi(fileId, modelName, 'bible');
|
| 322 |
+
|
| 323 |
+
let bibleHtml = '';
|
| 324 |
+
if (bibleRes.success) {
|
| 325 |
+
updateLog('logBible', 'success', 'Track 1: Style Bible ์ค๋น ์๋ฃ');
|
| 326 |
+
bibleHtml = `
|
| 327 |
+
<h4>[Track 1] Style Bible (ํธ์ง ๊ฐ๋ฅ)</h4>
|
| 328 |
+
<div style="margin-bottom: 10px;">
|
| 329 |
+
<textarea id="bibleContent" class="editable-content" style="width:100%; height:300px;">${bibleRes.result}</textarea>
|
| 330 |
+
<button class="btn btn-primary" onclick="saveStyleBible(${fileId})">์ ์ฅํ๊ธฐ</button>
|
| 331 |
+
</div>
|
| 332 |
+
`;
|
| 333 |
+
} else {
|
| 334 |
+
updateLog('logBible', 'error', `Track 1 ์คํจ: ${bibleRes.error}`);
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
// 2. Summary (Track 3)
|
| 338 |
+
updateLog('logSummary', 'loading', 'Track 3: ์ค๊ฑฐ๋ฆฌ ์์ฝ ์กฐํ ์ค...');
|
| 339 |
+
const summaryRes = await callApi(fileId, modelName, 'summary');
|
| 340 |
+
|
| 341 |
+
let summaryHtml = '';
|
| 342 |
+
if (summaryRes.success) {
|
| 343 |
+
updateLog('logSummary', 'success', 'Track 3: ์ค๊ฑฐ๋ฆฌ ์์ฝ ํ์ธ๋จ');
|
| 344 |
+
summaryHtml = `
|
| 345 |
+
<h4>[Track 3] ์ค๊ฑฐ๋ฆฌ ์์ฝ (์ฐธ์กฐ)</h4>
|
| 346 |
+
<div style="background: white; padding: 15px; border-radius: 4px; margin-bottom: 15px; max-height: 200px; overflow-y: auto;">
|
| 347 |
+
${summaryRes.result.replace(/\n/g, '<br>')}
|
| 348 |
+
</div>
|
| 349 |
+
<p style="font-size:0.9em; color:#666;">* ์์ ์ <a href="/admin/webnovels">์น์์ค ๊ด๋ฆฌ</a>์์ ๊ฐ๋ฅํฉ๋๋ค.</p>
|
| 350 |
+
`;
|
| 351 |
+
} else {
|
| 352 |
+
updateLog('logSummary', 'error', `Track 3 ์คํจ: ${summaryRes.error}`);
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
// 3. Vector DB (Track 2)
|
| 356 |
+
updateLog('logVector', 'loading', 'Track 2: Vector DB ์ํ ํ์ธ ์ค...');
|
| 357 |
+
const vectorRes = await callApi(fileId, modelName, 'vector');
|
| 358 |
+
|
| 359 |
+
let vectorHtml = '';
|
| 360 |
+
if (vectorRes.success) {
|
| 361 |
+
updateLog('logVector', 'success', `Track 2: ${vectorRes.result}`);
|
| 362 |
+
vectorHtml = `
|
| 363 |
+
<h4>[Track 2] Vector DB ์ํ</h4>
|
| 364 |
+
<div style="background: white; padding: 15px; border-radius: 4px;">
|
| 365 |
+
${vectorRes.result}
|
| 366 |
+
</div>
|
| 367 |
+
`;
|
| 368 |
+
} else {
|
| 369 |
+
updateLog('logVector', 'error', `Track 2 ์คํจ: ${vectorRes.error}`);
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
// ๊ฒฐ๊ณผ ๋ ๋๋ง (์์๋๋ก)
|
| 373 |
+
content.innerHTML = bibleHtml + summaryHtml + vectorHtml;
|
| 374 |
+
|
| 375 |
+
} catch (error) {
|
| 376 |
+
console.error(error);
|
| 377 |
+
alert('์์
์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: ' + error.message);
|
| 378 |
+
}
|
| 379 |
+
}
|
| 380 |
+
</script>
|
| 381 |
+
</body>
|
| 382 |
+
</html>
|
templates/novel_dashboard.html
CHANGED
|
@@ -156,15 +156,18 @@ async function loadProjects() {
|
|
| 156 |
</div>
|
| 157 |
<p class="card-text text-muted small mb-0">ID: ${p.project_id.substring(0, 8)}...</p>
|
| 158 |
</div>
|
| 159 |
-
<div class="card-footer bg-white border-top-0 p-4 pt-0 d-flex gap-2">
|
| 160 |
-
<a href="/creation/workspace/${p.project_id}" class="btn btn-
|
| 161 |
<i class="fas fa-pen-nib me-1"></i> ์งํ ์์
|
| 162 |
</a>
|
| 163 |
-
<a href="/creation/
|
| 164 |
-
<i class="fas fa-
|
|
|
|
|
|
|
|
|
|
| 165 |
</a>
|
| 166 |
<button class="btn btn-outline-danger px-3" onclick="deleteProject('${p.project_id}', '${p.title.replace(/'/g, "\\'")}')" title="ํ๋ก์ ํธ ์ญ์ ">
|
| 167 |
-
<i class="fas fa-trash-alt"></i>
|
| 168 |
</button>
|
| 169 |
</div>
|
| 170 |
</div>
|
|
|
|
| 156 |
</div>
|
| 157 |
<p class="card-text text-muted small mb-0">ID: ${p.project_id.substring(0, 8)}...</p>
|
| 158 |
</div>
|
| 159 |
+
<div class="card-footer bg-white border-top-0 p-4 pt-0 d-flex flex-wrap gap-2">
|
| 160 |
+
<a href="/creation/workspace/${p.project_id}" class="btn btn-primary fw-medium flex-fill">
|
| 161 |
<i class="fas fa-pen-nib me-1"></i> ์งํ ์์
|
| 162 |
</a>
|
| 163 |
+
<a href="/creation/trace/${p.project_id}" class="btn btn-outline-warning fw-medium flex-fill" title="์์ฑ ๊ณผ์ ํ์ธ">
|
| 164 |
+
<i class="fas fa-history me-1"></i> ๊ณผ์ ์ถ์
|
| 165 |
+
</a>
|
| 166 |
+
<a href="/creation/analysis/${p.project_id}" class="btn btn-outline-info fw-medium flex-fill" title="RAG/๋ฉ๋ชจ๋ฆฌ ๋ถ์">
|
| 167 |
+
<i class="fas fa-chart-line me-1"></i> ๋ถ์
|
| 168 |
</a>
|
| 169 |
<button class="btn btn-outline-danger px-3" onclick="deleteProject('${p.project_id}', '${p.title.replace(/'/g, "\\'")}')" title="ํ๋ก์ ํธ ์ญ์ ">
|
| 170 |
+
<i class="fas fa-trash-alt"></i>
|
| 171 |
</button>
|
| 172 |
</div>
|
| 173 |
</div>
|
templates/novel_story_view.html
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "creation_base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}์์ค ๋ณธ๋ฌธ ๋ณด๊ธฐ - {{ project.title }}{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block extra_css %}
|
| 6 |
+
<style>
|
| 7 |
+
.story-container {
|
| 8 |
+
max-width: 800px;
|
| 9 |
+
margin: 40px auto;
|
| 10 |
+
background: white;
|
| 11 |
+
padding: 60px;
|
| 12 |
+
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
| 13 |
+
border-radius: 8px;
|
| 14 |
+
min-height: 100vh;
|
| 15 |
+
}
|
| 16 |
+
.story-title {
|
| 17 |
+
text-align: center;
|
| 18 |
+
margin-bottom: 50px;
|
| 19 |
+
font-size: 32px;
|
| 20 |
+
font-weight: 700;
|
| 21 |
+
color: #1a1a1a;
|
| 22 |
+
}
|
| 23 |
+
.story-content {
|
| 24 |
+
font-size: 18px;
|
| 25 |
+
line-height: 1.8;
|
| 26 |
+
color: #333;
|
| 27 |
+
white-space: pre-wrap;
|
| 28 |
+
}
|
| 29 |
+
.story-meta {
|
| 30 |
+
text-align: center;
|
| 31 |
+
color: #888;
|
| 32 |
+
margin-bottom: 20px;
|
| 33 |
+
font-size: 14px;
|
| 34 |
+
}
|
| 35 |
+
.back-btn {
|
| 36 |
+
position: fixed;
|
| 37 |
+
top: 20px;
|
| 38 |
+
left: 20px;
|
| 39 |
+
z-index: 100;
|
| 40 |
+
}
|
| 41 |
+
@media (max-width: 768px) {
|
| 42 |
+
.story-container {
|
| 43 |
+
padding: 30px 20px;
|
| 44 |
+
margin: 0;
|
| 45 |
+
border-radius: 0;
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
</style>
|
| 49 |
+
{% endblock %}
|
| 50 |
+
|
| 51 |
+
{% block content %}
|
| 52 |
+
<div class="back-btn">
|
| 53 |
+
{% if is_admin %}
|
| 54 |
+
<a href="/admin/stories" class="btn btn-outline-secondary">
|
| 55 |
+
<i class="fas fa-arrow-left me-2"></i> ๋ชฉ๋ก์ผ๋ก ๋์๊ฐ๊ธฐ
|
| 56 |
+
</a>
|
| 57 |
+
{% else %}
|
| 58 |
+
<a href="/creation/workspace/{{ project.project_id }}" class="btn btn-outline-secondary">
|
| 59 |
+
<i class="fas fa-arrow-left me-2"></i> ์์
๊ณต๊ฐ์ผ๋ก ๋์๊ฐ๊ธฐ
|
| 60 |
+
</a>
|
| 61 |
+
{% endif %}
|
| 62 |
+
</div>
|
| 63 |
+
|
| 64 |
+
<div class="story-container">
|
| 65 |
+
<div class="story-meta">SOY NV AI - Web Novel Export</div>
|
| 66 |
+
<h1 class="story-title">{{ project.title }}</h1>
|
| 67 |
+
|
| 68 |
+
<div class="story-content">
|
| 69 |
+
{% for msg in messages %}
|
| 70 |
+
{{ msg.content }}
|
| 71 |
+
<hr style="margin: 40px 0; border: none; border-top: 1px dashed #eee;">
|
| 72 |
+
{% endfor %}
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
{% endblock %}
|
| 76 |
+
|
templates/novel_trace_view.html
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "creation_base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}์์ฑ ๊ณผ์ ํ์ธ - {{ project.title }}{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block extra_css %}
|
| 6 |
+
<style>
|
| 7 |
+
.trace-container {
|
| 8 |
+
max-width: 1000px;
|
| 9 |
+
margin: 40px auto;
|
| 10 |
+
padding: 20px;
|
| 11 |
+
}
|
| 12 |
+
.message-card {
|
| 13 |
+
background: white;
|
| 14 |
+
border-radius: 12px;
|
| 15 |
+
box-shadow: 0 2px 12px rgba(0,0,0,0.05);
|
| 16 |
+
margin-bottom: 24px;
|
| 17 |
+
overflow: hidden;
|
| 18 |
+
border: 1px solid #eee;
|
| 19 |
+
}
|
| 20 |
+
.message-header {
|
| 21 |
+
padding: 12px 20px;
|
| 22 |
+
background: #f8f9fa;
|
| 23 |
+
border-bottom: 1px solid #eee;
|
| 24 |
+
display: flex;
|
| 25 |
+
justify-content: space-between;
|
| 26 |
+
align-items: center;
|
| 27 |
+
}
|
| 28 |
+
.role-badge {
|
| 29 |
+
font-size: 12px;
|
| 30 |
+
font-weight: 600;
|
| 31 |
+
padding: 4px 10px;
|
| 32 |
+
border-radius: 20px;
|
| 33 |
+
text-transform: uppercase;
|
| 34 |
+
}
|
| 35 |
+
.role-user { background: #e0f2fe; color: #0369a1; }
|
| 36 |
+
.role-ai { background: #f0fdf4; color: #15803d; }
|
| 37 |
+
.role-ai-system { background: #fff7ed; color: #9a3412; border: 1px dashed #fdba74; }
|
| 38 |
+
|
| 39 |
+
.message-card.system-message {
|
| 40 |
+
opacity: 0.85;
|
| 41 |
+
border-left: 4px solid #f97316;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.message-body {
|
| 45 |
+
padding: 20px;
|
| 46 |
+
}
|
| 47 |
+
.content-section {
|
| 48 |
+
margin-bottom: 20px;
|
| 49 |
+
}
|
| 50 |
+
.content-label {
|
| 51 |
+
font-size: 11px;
|
| 52 |
+
font-weight: 700;
|
| 53 |
+
color: #94a3b8;
|
| 54 |
+
text-transform: uppercase;
|
| 55 |
+
letter-spacing: 0.05em;
|
| 56 |
+
margin-bottom: 8px;
|
| 57 |
+
display: block;
|
| 58 |
+
}
|
| 59 |
+
.content-text {
|
| 60 |
+
font-size: 15px;
|
| 61 |
+
line-height: 1.6;
|
| 62 |
+
color: #334155;
|
| 63 |
+
white-space: pre-wrap;
|
| 64 |
+
}
|
| 65 |
+
.meta-grid {
|
| 66 |
+
display: grid;
|
| 67 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 68 |
+
gap: 16px;
|
| 69 |
+
padding: 15px 20px;
|
| 70 |
+
background: #fafafa;
|
| 71 |
+
border-top: 1px solid #f0f0f0;
|
| 72 |
+
}
|
| 73 |
+
.meta-item {
|
| 74 |
+
font-size: 13px;
|
| 75 |
+
}
|
| 76 |
+
.meta-label { color: #64748b; font-weight: 500; }
|
| 77 |
+
.meta-value { color: #1e293b; font-weight: 600; }
|
| 78 |
+
|
| 79 |
+
.back-btn {
|
| 80 |
+
margin-bottom: 30px;
|
| 81 |
+
}
|
| 82 |
+
</style>
|
| 83 |
+
{% endblock %}
|
| 84 |
+
|
| 85 |
+
{% block content %}
|
| 86 |
+
<div class="trace-container">
|
| 87 |
+
<div class="back-btn">
|
| 88 |
+
{% if is_admin %}
|
| 89 |
+
<a href="/admin/traces" class="btn btn-outline-secondary">
|
| 90 |
+
<i class="fas fa-arrow-left me-2"></i> ๋ชฉ๋ก์ผ๋ก ๋์๊ฐ๊ธฐ
|
| 91 |
+
</a>
|
| 92 |
+
{% else %}
|
| 93 |
+
<a href="/creation/workspace/{{ project.project_id }}" class="btn btn-outline-secondary">
|
| 94 |
+
<i class="fas fa-arrow-left me-2"></i> ์์
๊ณต๊ฐ์ผ๋ก ๋์๊ฐ๊ธฐ
|
| 95 |
+
</a>
|
| 96 |
+
{% endif %}
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
+
<h2 class="mb-4">์์ฑ ๊ณผ์ ์ถ์ : {{ project.title }}</h2>
|
| 100 |
+
|
| 101 |
+
{% if not messages %}
|
| 102 |
+
<div class="alert alert-info">์์ง ์์ฑ๋ ๋ฉ์์ง๊ฐ ์์ต๋๋ค.</div>
|
| 103 |
+
{% endif %}
|
| 104 |
+
|
| 105 |
+
{% for msg in messages %}
|
| 106 |
+
<div class="message-card {% if msg.usage_type == 'system' %}system-message{% endif %}">
|
| 107 |
+
<div class="message-header">
|
| 108 |
+
<div>
|
| 109 |
+
<span class="role-badge role-{{ msg.role }}{% if msg.usage_type == 'system' %}-system{% endif %}">
|
| 110 |
+
{{ msg.role }}{% if msg.usage_type == 'system' %} (๋ฌธ์ฒด ์ ์ฉ ์ ์ด์){% endif %}
|
| 111 |
+
</span>
|
| 112 |
+
{% if msg.usage_type == 'system' %}
|
| 113 |
+
<span class="ms-2 badge bg-warning text-dark" style="font-size: 10px;">INTERNAL DRAFT</span>
|
| 114 |
+
{% endif %}
|
| 115 |
+
</div>
|
| 116 |
+
<span class="text-muted small">{{ msg.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</span>
|
| 117 |
+
</div>
|
| 118 |
+
<div class="message-body">
|
| 119 |
+
<div class="content-section">
|
| 120 |
+
<span class="content-label">{% if msg.usage_type == 'system' %}์ด์ ๋ด์ฉ (Before Style Refinement){% else %}๋ณธ๋ฌธ ๋ด์ฉ{% endif %}</span>
|
| 121 |
+
<div class="content-text">{{ msg.content }}</div>
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
{% if msg.role == 'ai' %}
|
| 125 |
+
<div class="meta-grid">
|
| 126 |
+
<div class="meta-item">
|
| 127 |
+
<span class="meta-label">๋ชจ๋ธ:</span>
|
| 128 |
+
<span class="meta-value">{{ msg.model_name or 'N/A' }}</span>
|
| 129 |
+
</div>
|
| 130 |
+
<div class="meta-item">
|
| 131 |
+
<span class="meta-label">์
๋ ฅ ํ ํฐ:</span>
|
| 132 |
+
<span class="meta-value">{{ msg.input_tokens if msg.input_tokens is not none else '-' }}</span>
|
| 133 |
+
</div>
|
| 134 |
+
<div class="meta-item">
|
| 135 |
+
<span class="meta-label">์ถ๋ ฅ ํ ํฐ:</span>
|
| 136 |
+
<span class="meta-value">{{ msg.output_tokens if msg.output_tokens is not none else '-' }}</span>
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
{% endif %}
|
| 140 |
+
</div>
|
| 141 |
+
{% endfor %}
|
| 142 |
+
</div>
|
| 143 |
+
{% endblock %}
|
| 144 |
+
|
templates/novel_workspace.html
CHANGED
|
@@ -257,26 +257,66 @@
|
|
| 257 |
}
|
| 258 |
|
| 259 |
.message-thoughts {
|
| 260 |
-
margin-top:
|
| 261 |
-
padding:
|
| 262 |
-
background-color: rgba(30, 41, 59, 0.
|
| 263 |
-
border: 1px solid rgba(51, 65, 85, 0.
|
| 264 |
border-radius: 12px;
|
| 265 |
font-size: 13px;
|
| 266 |
color: var(--text-muted);
|
| 267 |
-
font-style: italic;
|
| 268 |
}
|
| 269 |
|
| 270 |
.message-thoughts-header {
|
| 271 |
display: flex;
|
| 272 |
align-items: center;
|
| 273 |
-
|
| 274 |
-
margin-bottom:
|
| 275 |
-
font-size:
|
| 276 |
font-weight: 700;
|
| 277 |
text-transform: uppercase;
|
| 278 |
-
letter-spacing: 0.
|
| 279 |
-
color: var(--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
}
|
| 281 |
|
| 282 |
.message-image {
|
|
@@ -401,39 +441,42 @@
|
|
| 401 |
background-color: rgba(30, 41, 59, 0.4);
|
| 402 |
border: 1px solid rgba(51, 65, 85, 0.5);
|
| 403 |
border-radius: 16px;
|
| 404 |
-
padding:
|
|
|
|
| 405 |
}
|
| 406 |
|
| 407 |
.user-profile {
|
| 408 |
display: flex;
|
| 409 |
align-items: center;
|
| 410 |
-
gap:
|
| 411 |
-
margin-bottom:
|
| 412 |
}
|
| 413 |
|
| 414 |
.user-avatar {
|
| 415 |
-
width:
|
| 416 |
-
height:
|
| 417 |
-
background-color:
|
| 418 |
-
border-radius:
|
| 419 |
display: flex;
|
| 420 |
align-items: center;
|
| 421 |
justify-content: center;
|
| 422 |
-
color: var(--
|
| 423 |
-
border: 1px solid
|
|
|
|
| 424 |
}
|
| 425 |
|
| 426 |
.user-info-name {
|
| 427 |
-
font-size:
|
| 428 |
font-weight: 700;
|
| 429 |
color: var(--text-main);
|
|
|
|
| 430 |
}
|
| 431 |
|
| 432 |
.user-info-role {
|
| 433 |
-
font-size:
|
| 434 |
color: var(--text-muted);
|
| 435 |
-
|
| 436 |
-
|
| 437 |
}
|
| 438 |
|
| 439 |
.status-items {
|
|
@@ -441,20 +484,61 @@
|
|
| 441 |
padding-top: 12px;
|
| 442 |
display: flex;
|
| 443 |
flex-direction: column;
|
| 444 |
-
gap:
|
| 445 |
}
|
| 446 |
|
| 447 |
.status-item {
|
| 448 |
display: flex;
|
| 449 |
justify-content: space-between;
|
|
|
|
| 450 |
font-size: 11px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
}
|
| 452 |
|
| 453 |
.status-item-label {
|
| 454 |
color: var(--text-muted);
|
| 455 |
display: flex;
|
| 456 |
align-items: center;
|
| 457 |
-
gap:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
}
|
| 459 |
|
| 460 |
.objective-card {
|
|
@@ -549,12 +633,24 @@
|
|
| 549 |
<i class="fas fa-robot"></i>
|
| 550 |
</div>
|
| 551 |
<div>
|
| 552 |
-
<h1 class="chat-header-title">{{ project.title }}</h1>
|
| 553 |
<p class="chat-header-subtitle">{{ project.mode }} MODE</p>
|
| 554 |
</div>
|
| 555 |
</div>
|
| 556 |
|
| 557 |
<div style="display: flex; align-items: center; gap: 16px;">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 558 |
<div style="display: flex; align-items: center; gap: 8px; padding: 6px 12px; background-color: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2); border-radius: 8px;">
|
| 559 |
<label style="display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--text-muted); cursor: pointer;">
|
| 560 |
<input type="checkbox" id="viewer-toggle" style="cursor: pointer;">
|
|
@@ -564,6 +660,15 @@
|
|
| 564 |
<a id="viewer-link" href="#" target="_blank" style="display: none; padding: 6px 12px; background-color: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.2); border-radius: 8px; color: #22c55e; text-decoration: none; font-size: 11px; font-weight: 600;">
|
| 565 |
<i class="fas fa-external-link-alt"></i> ๋ทฐ์ด ์ด๊ธฐ
|
| 566 |
</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 567 |
<select id="model-selector" class="model-select">
|
| 568 |
<!-- Models will be loaded here -->
|
| 569 |
</select>
|
|
@@ -618,6 +723,14 @@
|
|
| 618 |
<span class="status-item-label"><i class="fas fa-clock"></i> Time</span>
|
| 619 |
<span id="status-time" class="status-item-value">D+0000 | 00:00</span>
|
| 620 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
</div>
|
| 622 |
</div>
|
| 623 |
</div>
|
|
@@ -659,6 +772,7 @@
|
|
| 659 |
const chatMessages = document.getElementById('chat-messages');
|
| 660 |
const chatMessagesInner = document.getElementById('chat-messages-inner');
|
| 661 |
const modelSelector = document.getElementById('model-selector');
|
|
|
|
| 662 |
const toggleSidebarBtn = document.getElementById('toggle-sidebar');
|
| 663 |
const sidebar = document.getElementById('sidebar');
|
| 664 |
|
|
@@ -689,6 +803,9 @@
|
|
| 689 |
const defaultModel = data.models.find(m => m.name.includes('gemini')) || data.models[0];
|
| 690 |
modelSelector.value = defaultModel.name;
|
| 691 |
}
|
|
|
|
|
|
|
|
|
|
| 692 |
}
|
| 693 |
} catch (e) { console.error('Failed to load models:', e); }
|
| 694 |
}
|
|
@@ -697,6 +814,10 @@
|
|
| 697 |
localStorage.setItem('lastWriterModel', e.target.value);
|
| 698 |
});
|
| 699 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 700 |
// --- Markdown Rendering ---
|
| 701 |
function renderMarkdown(text) {
|
| 702 |
if (!text) return '';
|
|
@@ -743,10 +864,20 @@
|
|
| 743 |
|
| 744 |
if (role === 'ai' && extra.thoughts) {
|
| 745 |
const renderedThoughts = renderMarkdown(extra.thoughts);
|
|
|
|
| 746 |
html += `
|
| 747 |
<div class="message-thoughts">
|
| 748 |
-
<div class="message-thoughts-header">
|
|
|
|
|
|
|
|
|
|
| 749 |
<div class="markdown-content">${renderedThoughts}</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 750 |
</div>`;
|
| 751 |
}
|
| 752 |
|
|
@@ -836,18 +967,83 @@
|
|
| 836 |
indicator.id = 'typing-indicator';
|
| 837 |
indicator.className = 'message message-ai';
|
| 838 |
indicator.innerHTML = `
|
| 839 |
-
<div class="message-bubble">
|
| 840 |
-
<div
|
| 841 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 842 |
</div>
|
| 843 |
</div>`;
|
| 844 |
chatMessagesInner.appendChild(indicator);
|
| 845 |
scrollToBottom();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 846 |
}
|
| 847 |
|
| 848 |
function hideTypingIndicator() {
|
| 849 |
const indicator = document.getElementById('typing-indicator');
|
| 850 |
-
if (indicator)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 851 |
}
|
| 852 |
|
| 853 |
// --- Chat History ---
|
|
@@ -873,6 +1069,29 @@
|
|
| 873 |
document.getElementById('status-location').textContent = status.location || 'Unknown';
|
| 874 |
document.getElementById('status-time').textContent = status.current_time || 'D+0000 | 00:00';
|
| 875 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 876 |
const objectiveSection = document.getElementById('objective-section');
|
| 877 |
if (status.quest_status) {
|
| 878 |
objectiveSection.style.display = 'block';
|
|
@@ -926,7 +1145,11 @@
|
|
| 926 |
const response = await fetch(`/creation/api/chat/${projectId}`, {
|
| 927 |
method: 'POST',
|
| 928 |
headers: { 'Content-Type': 'application/json' },
|
| 929 |
-
body: JSON.stringify({
|
|
|
|
|
|
|
|
|
|
|
|
|
| 930 |
});
|
| 931 |
const data = await response.json();
|
| 932 |
|
|
@@ -938,7 +1161,8 @@
|
|
| 938 |
const aiMessageId = data.message_id || null;
|
| 939 |
const messageEl = addMessage('ai', data.reply, {
|
| 940 |
thoughts: data.hidden_thoughts,
|
| 941 |
-
imageUrl: data.image_url
|
|
|
|
| 942 |
}, aiMessageId);
|
| 943 |
|
| 944 |
// messageId๊ฐ ๋์ค์ ์
๋ฐ์ดํธ๋๋ ๊ฒฝ์ฐ๋ฅผ ๋๋นํ์ฌ ์
๋ฐ์ดํธ
|
|
@@ -1152,6 +1376,18 @@
|
|
| 1152 |
loadModels();
|
| 1153 |
loadHistory();
|
| 1154 |
loadViewerSettings();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1155 |
});
|
| 1156 |
</script>
|
| 1157 |
{% endblock %}
|
|
|
|
| 257 |
}
|
| 258 |
|
| 259 |
.message-thoughts {
|
| 260 |
+
margin-top: 12px;
|
| 261 |
+
padding: 12px 16px;
|
| 262 |
+
background-color: rgba(30, 41, 59, 0.2);
|
| 263 |
+
border: 1px solid rgba(51, 65, 85, 0.3);
|
| 264 |
border-radius: 12px;
|
| 265 |
font-size: 13px;
|
| 266 |
color: var(--text-muted);
|
|
|
|
| 267 |
}
|
| 268 |
|
| 269 |
.message-thoughts-header {
|
| 270 |
display: flex;
|
| 271 |
align-items: center;
|
| 272 |
+
justify-content: space-between;
|
| 273 |
+
margin-bottom: 8px;
|
| 274 |
+
font-size: 11px;
|
| 275 |
font-weight: 700;
|
| 276 |
text-transform: uppercase;
|
| 277 |
+
letter-spacing: 0.05em;
|
| 278 |
+
color: var(--accent);
|
| 279 |
+
cursor: pointer;
|
| 280 |
+
user-select: none;
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.thought-steps {
|
| 284 |
+
display: flex;
|
| 285 |
+
flex-direction: column;
|
| 286 |
+
gap: 6px;
|
| 287 |
+
margin-top: 8px;
|
| 288 |
+
padding-top: 8px;
|
| 289 |
+
border-top: 1px solid rgba(51, 65, 85, 0.2);
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
.thought-step {
|
| 293 |
+
display: flex;
|
| 294 |
+
align-items: center;
|
| 295 |
+
gap: 8px;
|
| 296 |
+
font-size: 11px;
|
| 297 |
+
color: rgba(148, 163, 184, 0.8);
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
.thought-step i {
|
| 301 |
+
font-size: 10px;
|
| 302 |
+
color: #22c55e;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
/* Progress Indicator CSS */
|
| 306 |
+
.progress-container {
|
| 307 |
+
width: 100%;
|
| 308 |
+
background-color: rgba(30, 41, 59, 0.5);
|
| 309 |
+
border-radius: 4px;
|
| 310 |
+
height: 4px;
|
| 311 |
+
margin: 8px 0;
|
| 312 |
+
overflow: hidden;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
.progress-bar {
|
| 316 |
+
height: 100%;
|
| 317 |
+
background: linear-gradient(90deg, var(--accent), #818cf8);
|
| 318 |
+
width: 0%;
|
| 319 |
+
transition: width 0.5s ease;
|
| 320 |
}
|
| 321 |
|
| 322 |
.message-image {
|
|
|
|
| 441 |
background-color: rgba(30, 41, 59, 0.4);
|
| 442 |
border: 1px solid rgba(51, 65, 85, 0.5);
|
| 443 |
border-radius: 16px;
|
| 444 |
+
padding: 20px;
|
| 445 |
+
box-shadow: inset 0 1px 1px rgba(255,255,255,0.05);
|
| 446 |
}
|
| 447 |
|
| 448 |
.user-profile {
|
| 449 |
display: flex;
|
| 450 |
align-items: center;
|
| 451 |
+
gap: 16px;
|
| 452 |
+
margin-bottom: 20px;
|
| 453 |
}
|
| 454 |
|
| 455 |
.user-avatar {
|
| 456 |
+
width: 48px;
|
| 457 |
+
height: 48px;
|
| 458 |
+
background-color: rgba(59, 130, 246, 0.1);
|
| 459 |
+
border-radius: 12px;
|
| 460 |
display: flex;
|
| 461 |
align-items: center;
|
| 462 |
justify-content: center;
|
| 463 |
+
color: var(--accent);
|
| 464 |
+
border: 1px solid rgba(59, 130, 246, 0.2);
|
| 465 |
+
font-size: 18px;
|
| 466 |
}
|
| 467 |
|
| 468 |
.user-info-name {
|
| 469 |
+
font-size: 15px;
|
| 470 |
font-weight: 700;
|
| 471 |
color: var(--text-main);
|
| 472 |
+
letter-spacing: -0.02em;
|
| 473 |
}
|
| 474 |
|
| 475 |
.user-info-role {
|
| 476 |
+
font-size: 11px;
|
| 477 |
color: var(--text-muted);
|
| 478 |
+
font-weight: 500;
|
| 479 |
+
margin-top: 2px;
|
| 480 |
}
|
| 481 |
|
| 482 |
.status-items {
|
|
|
|
| 484 |
padding-top: 12px;
|
| 485 |
display: flex;
|
| 486 |
flex-direction: column;
|
| 487 |
+
gap: 2px;
|
| 488 |
}
|
| 489 |
|
| 490 |
.status-item {
|
| 491 |
display: flex;
|
| 492 |
justify-content: space-between;
|
| 493 |
+
align-items: flex-start;
|
| 494 |
font-size: 11px;
|
| 495 |
+
gap: 12px;
|
| 496 |
+
padding: 8px 0;
|
| 497 |
+
border-bottom: 1px solid rgba(51, 65, 85, 0.1);
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
.status-item:last-child {
|
| 501 |
+
border-bottom: none;
|
| 502 |
}
|
| 503 |
|
| 504 |
.status-item-label {
|
| 505 |
color: var(--text-muted);
|
| 506 |
display: flex;
|
| 507 |
align-items: center;
|
| 508 |
+
gap: 10px;
|
| 509 |
+
white-space: nowrap;
|
| 510 |
+
flex-shrink: 0;
|
| 511 |
+
padding-top: 2px;
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
.status-item-label i {
|
| 515 |
+
width: 14px;
|
| 516 |
+
text-align: center;
|
| 517 |
+
color: var(--accent);
|
| 518 |
+
opacity: 0.7;
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
.status-item-value {
|
| 522 |
+
text-align: right;
|
| 523 |
+
color: var(--text-main);
|
| 524 |
+
font-weight: 500;
|
| 525 |
+
line-height: 1.5;
|
| 526 |
+
word-break: keep-all;
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
/* Stats ์ ์ฉ ์คํ์ผ - ๋ด์ฉ์ด ๊ธธ ์ ์์ผ๋ฏ๋ก ์ค๋ฐ๊ฟ ํ์ฉ */
|
| 530 |
+
#status-stats-row {
|
| 531 |
+
flex-direction: column;
|
| 532 |
+
align-items: stretch;
|
| 533 |
+
gap: 6px;
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
#status-stats-row .status-item-value {
|
| 537 |
+
text-align: left;
|
| 538 |
+
padding-left: 24px;
|
| 539 |
+
font-size: 10.5px;
|
| 540 |
+
color: rgba(241, 245, 249, 0.8);
|
| 541 |
+
line-height: 1.6;
|
| 542 |
}
|
| 543 |
|
| 544 |
.objective-card {
|
|
|
|
| 633 |
<i class="fas fa-robot"></i>
|
| 634 |
</div>
|
| 635 |
<div>
|
| 636 |
+
<h1 class="chat-header-title">{{ project.title }} ({{ public_styles|length }})</h1>
|
| 637 |
<p class="chat-header-subtitle">{{ project.mode }} MODE</p>
|
| 638 |
</div>
|
| 639 |
</div>
|
| 640 |
|
| 641 |
<div style="display: flex; align-items: center; gap: 16px;">
|
| 642 |
+
<div class="dropdown">
|
| 643 |
+
<button class="btn btn-outline-light btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false" style="font-size: 11px; background: rgba(255,255,255,0.05); border: 1px solid var(--border-color); color: var(--text-muted);">
|
| 644 |
+
<i class="fas fa-eye me-1"></i> ๋ณด๊ธฐ ๋ชจ๋
|
| 645 |
+
</button>
|
| 646 |
+
<ul class="dropdown-menu dropdown-menu-dark shadow" style="background-color: var(--bg-message-ai); border: 1px solid var(--border-color); font-size: 13px;">
|
| 647 |
+
<li><a class="dropdown-item py-2" href="/creation/story/{{ project.project_id }}"><i class="fas fa-book me-2 text-success"></i> ์์ค ๋ณธ๋ฌธ๋ง ๋ณด๊ธฐ</a></li>
|
| 648 |
+
<li><a class="dropdown-item py-2" href="/creation/trace/{{ project.project_id }}"><i class="fas fa-history me-2 text-warning"></i> ์์ฑ ๊ณผ์ ์ถ์ ํ๊ธฐ</a></li>
|
| 649 |
+
<li><hr class="dropdown-divider" style="border-color: var(--border-color);"></li>
|
| 650 |
+
<li><a class="dropdown-item py-2" href="/creation/analysis/{{ project.project_id }}"><i class="fas fa-chart-line me-2 text-info"></i> ๋ถ์ ๋ฆฌํฌํธ ํ์ธ</a></li>
|
| 651 |
+
</ul>
|
| 652 |
+
</div>
|
| 653 |
+
|
| 654 |
<div style="display: flex; align-items: center; gap: 8px; padding: 6px 12px; background-color: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2); border-radius: 8px;">
|
| 655 |
<label style="display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--text-muted); cursor: pointer;">
|
| 656 |
<input type="checkbox" id="viewer-toggle" style="cursor: pointer;">
|
|
|
|
| 660 |
<a id="viewer-link" href="#" target="_blank" style="display: none; padding: 6px 12px; background-color: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.2); border-radius: 8px; color: #22c55e; text-decoration: none; font-size: 11px; font-weight: 600;">
|
| 661 |
<i class="fas fa-external-link-alt"></i> ๋ทฐ์ด ์ด๊ธฐ
|
| 662 |
</a>
|
| 663 |
+
<select id="style-selector" class="model-select">
|
| 664 |
+
<option value="">๋ฌธ์ฒด ์ ํ (๊ธฐ๋ณธ)</option>
|
| 665 |
+
{% if not public_styles %}
|
| 666 |
+
<option value="" disabled>๊ณต๊ฐ๋ ๋ฌธ์ฒด๊ฐ ์์ต๋๋ค</option>
|
| 667 |
+
{% endif %}
|
| 668 |
+
{% for style in public_styles %}
|
| 669 |
+
<option value="{{ style.id }}">{{ style.file.original_filename if style.file else '๋ฌธ์ฒด #' ~ style.id }}</option>
|
| 670 |
+
{% endfor %}
|
| 671 |
+
</select>
|
| 672 |
<select id="model-selector" class="model-select">
|
| 673 |
<!-- Models will be loaded here -->
|
| 674 |
</select>
|
|
|
|
| 723 |
<span class="status-item-label"><i class="fas fa-clock"></i> Time</span>
|
| 724 |
<span id="status-time" class="status-item-value">D+0000 | 00:00</span>
|
| 725 |
</div>
|
| 726 |
+
<div class="status-item" id="status-hp-row" style="display: none;">
|
| 727 |
+
<span class="status-item-label"><i class="fas fa-heart"></i> HP</span>
|
| 728 |
+
<span id="status-hp" class="status-item-value">-</span>
|
| 729 |
+
</div>
|
| 730 |
+
<div class="status-item" id="status-stats-row" style="display: none;">
|
| 731 |
+
<span class="status-item-label"><i class="fas fa-chart-line"></i> Stats</span>
|
| 732 |
+
<span id="status-stats" class="status-item-value">-</span>
|
| 733 |
+
</div>
|
| 734 |
</div>
|
| 735 |
</div>
|
| 736 |
</div>
|
|
|
|
| 772 |
const chatMessages = document.getElementById('chat-messages');
|
| 773 |
const chatMessagesInner = document.getElementById('chat-messages-inner');
|
| 774 |
const modelSelector = document.getElementById('model-selector');
|
| 775 |
+
const styleSelector = document.getElementById('style-selector');
|
| 776 |
const toggleSidebarBtn = document.getElementById('toggle-sidebar');
|
| 777 |
const sidebar = document.getElementById('sidebar');
|
| 778 |
|
|
|
|
| 803 |
const defaultModel = data.models.find(m => m.name.includes('gemini')) || data.models[0];
|
| 804 |
modelSelector.value = defaultModel.name;
|
| 805 |
}
|
| 806 |
+
|
| 807 |
+
const savedStyle = localStorage.getItem('lastWriterStyle');
|
| 808 |
+
if (savedStyle) styleSelector.value = savedStyle;
|
| 809 |
}
|
| 810 |
} catch (e) { console.error('Failed to load models:', e); }
|
| 811 |
}
|
|
|
|
| 814 |
localStorage.setItem('lastWriterModel', e.target.value);
|
| 815 |
});
|
| 816 |
|
| 817 |
+
styleSelector.addEventListener('change', (e) => {
|
| 818 |
+
localStorage.setItem('lastWriterStyle', e.target.value);
|
| 819 |
+
});
|
| 820 |
+
|
| 821 |
// --- Markdown Rendering ---
|
| 822 |
function renderMarkdown(text) {
|
| 823 |
if (!text) return '';
|
|
|
|
| 864 |
|
| 865 |
if (role === 'ai' && extra.thoughts) {
|
| 866 |
const renderedThoughts = renderMarkdown(extra.thoughts);
|
| 867 |
+
const hasStyle = extra.styleApplied ? '<div class="thought-step"><i class="fas fa-magic"></i> <span>Style Bible ๋ฌธ์ฒด ๋ณํ ์๋ฃ</span></div>' : '';
|
| 868 |
html += `
|
| 869 |
<div class="message-thoughts">
|
| 870 |
+
<div class="message-thoughts-header">
|
| 871 |
+
<span><i class="fas fa-brain"></i> AI ์ถ๋ก ๊ณผ์ </span>
|
| 872 |
+
<i class="fas fa-chevron-down"></i>
|
| 873 |
+
</div>
|
| 874 |
<div class="markdown-content">${renderedThoughts}</div>
|
| 875 |
+
<div class="thought-steps">
|
| 876 |
+
<div class="thought-step"><i class="fas fa-check"></i> <span>์ธ๊ณ๊ด ๋ฐ ๋งฅ๋ฝ ๋ถ์</span></div>
|
| 877 |
+
<div class="thought-step"><i class="fas fa-check"></i> <span>์ฅ๋ฉด ์ ๊ฐ ๊ตฌ์</span></div>
|
| 878 |
+
${hasStyle}
|
| 879 |
+
<div class="thought-step"><i class="fas fa-check"></i> <span>์ต์ข
ํ
์คํธ ์์ฑ</span></div>
|
| 880 |
+
</div>
|
| 881 |
</div>`;
|
| 882 |
}
|
| 883 |
|
|
|
|
| 967 |
indicator.id = 'typing-indicator';
|
| 968 |
indicator.className = 'message message-ai';
|
| 969 |
indicator.innerHTML = `
|
| 970 |
+
<div class="message-bubble" style="min-width: 280px; background-color: rgba(30, 41, 59, 0.4); border: 1px solid var(--border-color);">
|
| 971 |
+
<div style="display: flex; flex-direction: column; gap: 12px; padding: 4px;">
|
| 972 |
+
<div style="display: flex; align-items: center; justify-content: space-between;">
|
| 973 |
+
<div style="display: flex; align-items: center; gap: 8px;">
|
| 974 |
+
<i class="fas fa-robot fa-spin" style="color: var(--accent); font-size: 14px;"></i>
|
| 975 |
+
<span id="typing-status-text" style="font-size: 13px; font-weight: 600; color: var(--text-main);">์์ด์ ํธ ๊ฐ๋ ์ค...</span>
|
| 976 |
+
</div>
|
| 977 |
+
<span id="typing-percent" style="font-size: 11px; color: var(--accent); font-weight: 700;">0%</span>
|
| 978 |
+
</div>
|
| 979 |
+
|
| 980 |
+
<div class="progress-container">
|
| 981 |
+
<div id="typing-progress-bar" class="progress-bar"></div>
|
| 982 |
+
</div>
|
| 983 |
+
|
| 984 |
+
<div id="typing-log" style="display: flex; flex-direction: column; gap: 6px;">
|
| 985 |
+
<div class="thought-step" style="opacity: 1;"><i class="fas fa-check"></i> <span>์ฌ์ฉ์ ์์ฒญ ์์ </span></div>
|
| 986 |
+
</div>
|
| 987 |
</div>
|
| 988 |
</div>`;
|
| 989 |
chatMessagesInner.appendChild(indicator);
|
| 990 |
scrollToBottom();
|
| 991 |
+
|
| 992 |
+
// ์๋ฎฌ๋ ์ด์
๋ ์งํ ๋ก๊ทธ ์
๋ฐ์ดํธ
|
| 993 |
+
const steps = [
|
| 994 |
+
{ text: "์ธ๊ณ๊ด ๋ฐ ์บ๋ฆญํฐ ์ค์ ๋ถ์ ์ค...", p: 15 },
|
| 995 |
+
{ text: "์์ฌ์ ์ ๊ฐ ๋งฅ๋ฝ(Zep) ๋ก๋ ์๋ฃ", p: 30 },
|
| 996 |
+
{ text: "๋ค์ ์ฅ๋ฉด ์ ๊ฐ ๊ตฌ์ ์ค...", p: 45 },
|
| 997 |
+
{ text: "์ด์ ์์ฑ์ ์ํ AI ์ถ๋ก ์์", p: 60 },
|
| 998 |
+
{ text: "Style Bible ๋ฌธ์ฒด ์ ์ฉ ๋ฐ ๋ณํ ์ค...", p: 80 },
|
| 999 |
+
{ text: "์ต์ข
๊ฒฐ๊ณผ๋ฌผ ๊ฒํ ๋ฐ ๋ค๋ฌ๊ธฐ", p: 95 }
|
| 1000 |
+
];
|
| 1001 |
+
|
| 1002 |
+
let currentStep = 0;
|
| 1003 |
+
const logInterval = setInterval(() => {
|
| 1004 |
+
const statusText = document.getElementById('typing-status-text');
|
| 1005 |
+
const progressBar = document.getElementById('typing-progress-bar');
|
| 1006 |
+
const percentText = document.getElementById('typing-percent');
|
| 1007 |
+
const logContainer = document.getElementById('typing-log');
|
| 1008 |
+
|
| 1009 |
+
if (!statusText || currentStep >= steps.length) {
|
| 1010 |
+
clearInterval(logInterval);
|
| 1011 |
+
return;
|
| 1012 |
+
}
|
| 1013 |
+
|
| 1014 |
+
const step = steps[currentStep];
|
| 1015 |
+
statusText.textContent = step.text;
|
| 1016 |
+
progressBar.style.width = `${step.p}%`;
|
| 1017 |
+
percentText.textContent = `${step.p}%`;
|
| 1018 |
+
|
| 1019 |
+
const logEntry = document.createElement('div');
|
| 1020 |
+
logEntry.className = 'thought-step';
|
| 1021 |
+
logEntry.innerHTML = `<i class="fas fa-check"></i> <span>${step.text}</span>`;
|
| 1022 |
+
logContainer.appendChild(logEntry);
|
| 1023 |
+
|
| 1024 |
+
// ๋ก๊ทธ๊ฐ ๋ง์์ง๋ฉด ์ค๋๋ ๊ฒ ํฌ๋ช
๋ ์กฐ์
|
| 1025 |
+
const children = logContainer.children;
|
| 1026 |
+
if (children.length > 3) {
|
| 1027 |
+
for(let i = 0; i < children.length - 3; i++) {
|
| 1028 |
+
children[i].style.opacity = "0.4";
|
| 1029 |
+
}
|
| 1030 |
+
}
|
| 1031 |
+
|
| 1032 |
+
currentStep++;
|
| 1033 |
+
scrollToBottom();
|
| 1034 |
+
}, 2500);
|
| 1035 |
+
|
| 1036 |
+
indicator.dataset.intervalId = logInterval;
|
| 1037 |
}
|
| 1038 |
|
| 1039 |
function hideTypingIndicator() {
|
| 1040 |
const indicator = document.getElementById('typing-indicator');
|
| 1041 |
+
if (indicator) {
|
| 1042 |
+
if (indicator.dataset.intervalId) {
|
| 1043 |
+
clearInterval(indicator.dataset.intervalId);
|
| 1044 |
+
}
|
| 1045 |
+
indicator.remove();
|
| 1046 |
+
}
|
| 1047 |
}
|
| 1048 |
|
| 1049 |
// --- Chat History ---
|
|
|
|
| 1069 |
document.getElementById('status-location').textContent = status.location || 'Unknown';
|
| 1070 |
document.getElementById('status-time').textContent = status.current_time || 'D+0000 | 00:00';
|
| 1071 |
|
| 1072 |
+
// ์บ๋ฆญํฐ ์ด๋ฆ ๋ฐ ์์ ์
๋ฐ์ดํธ
|
| 1073 |
+
if (status.name) {
|
| 1074 |
+
document.querySelector('.user-info-name').textContent = status.name;
|
| 1075 |
+
}
|
| 1076 |
+
if (status.affiliation) {
|
| 1077 |
+
document.querySelector('.user-info-role').textContent = status.affiliation;
|
| 1078 |
+
}
|
| 1079 |
+
|
| 1080 |
+
// HP ๋ฐ ๋ฅ๋ ฅ์น ์
๋ฐ์ดํธ
|
| 1081 |
+
if (status.hp_status) {
|
| 1082 |
+
document.getElementById('status-hp-row').style.display = 'flex';
|
| 1083 |
+
document.getElementById('status-hp').textContent = status.hp_status;
|
| 1084 |
+
} else {
|
| 1085 |
+
document.getElementById('status-hp-row').style.display = 'none';
|
| 1086 |
+
}
|
| 1087 |
+
|
| 1088 |
+
if (status.stats) {
|
| 1089 |
+
document.getElementById('status-stats-row').style.display = 'flex';
|
| 1090 |
+
document.getElementById('status-stats').textContent = status.stats;
|
| 1091 |
+
} else {
|
| 1092 |
+
document.getElementById('status-stats-row').style.display = 'none';
|
| 1093 |
+
}
|
| 1094 |
+
|
| 1095 |
const objectiveSection = document.getElementById('objective-section');
|
| 1096 |
if (status.quest_status) {
|
| 1097 |
objectiveSection.style.display = 'block';
|
|
|
|
| 1145 |
const response = await fetch(`/creation/api/chat/${projectId}`, {
|
| 1146 |
method: 'POST',
|
| 1147 |
headers: { 'Content-Type': 'application/json' },
|
| 1148 |
+
body: JSON.stringify({
|
| 1149 |
+
message,
|
| 1150 |
+
model: modelSelector.value,
|
| 1151 |
+
style_id: styleSelector.value
|
| 1152 |
+
})
|
| 1153 |
});
|
| 1154 |
const data = await response.json();
|
| 1155 |
|
|
|
|
| 1161 |
const aiMessageId = data.message_id || null;
|
| 1162 |
const messageEl = addMessage('ai', data.reply, {
|
| 1163 |
thoughts: data.hidden_thoughts,
|
| 1164 |
+
imageUrl: data.image_url,
|
| 1165 |
+
styleApplied: data.style_applied
|
| 1166 |
}, aiMessageId);
|
| 1167 |
|
| 1168 |
// messageId๊ฐ ๋์ค์ ์
๋ฐ์ดํธ๋๋ ๊ฒฝ์ฐ๋ฅผ ๋๋นํ์ฌ ์
๋ฐ์ดํธ
|
|
|
|
| 1376 |
loadModels();
|
| 1377 |
loadHistory();
|
| 1378 |
loadViewerSettings();
|
| 1379 |
+
|
| 1380 |
+
// ๊ธฐ์กด ์ํ ๋ฐ์ดํฐ ๋ก๋
|
| 1381 |
+
{% if project.last_status %}
|
| 1382 |
+
try {
|
| 1383 |
+
const initialStatus = JSON.parse('{{ project.last_status | safe }}');
|
| 1384 |
+
if (initialStatus) {
|
| 1385 |
+
updateStatus(initialStatus);
|
| 1386 |
+
}
|
| 1387 |
+
} catch (e) {
|
| 1388 |
+
console.error('Failed to parse initial status:', e);
|
| 1389 |
+
}
|
| 1390 |
+
{% endif %}
|
| 1391 |
});
|
| 1392 |
</script>
|
| 1393 |
{% endblock %}
|