update:1.支持手动创建角色 组织
This commit is contained in:
@@ -3,13 +3,16 @@ from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
import json
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from app.database import get_db
|
||||
from app.utils.sse_response import SSEResponse, create_sse_response
|
||||
from app.models.character import Character
|
||||
from app.models.project import Project
|
||||
from app.models.generation_history import GenerationHistory
|
||||
from app.models.relationship import CharacterRelationship, Organization, OrganizationMember, RelationshipType
|
||||
from app.schemas.character import (
|
||||
CharacterCreate,
|
||||
CharacterUpdate,
|
||||
CharacterResponse,
|
||||
CharacterListResponse,
|
||||
@@ -276,6 +279,79 @@ async def delete_character(
|
||||
return {"message": "角色删除成功"}
|
||||
|
||||
|
||||
@router.post("", response_model=CharacterResponse, summary="手动创建角色")
|
||||
async def create_character(
|
||||
character_data: CharacterCreate,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
手动创建角色或组织
|
||||
|
||||
- 可以创建普通角色(is_organization=False)
|
||||
- 也可以创建组织(is_organization=True)
|
||||
- 如果创建组织且提供了组织额外字段,会自动创建Organization详情记录
|
||||
"""
|
||||
# 验证用户权限
|
||||
user_id = getattr(request.state, 'user_id', None)
|
||||
await verify_project_access(character_data.project_id, user_id, db)
|
||||
|
||||
try:
|
||||
# 创建角色
|
||||
character = Character(
|
||||
project_id=character_data.project_id,
|
||||
name=character_data.name,
|
||||
age=character_data.age,
|
||||
gender=character_data.gender,
|
||||
is_organization=character_data.is_organization,
|
||||
role_type=character_data.role_type or "supporting",
|
||||
personality=character_data.personality,
|
||||
background=character_data.background,
|
||||
appearance=character_data.appearance,
|
||||
relationships=character_data.relationships,
|
||||
organization_type=character_data.organization_type,
|
||||
organization_purpose=character_data.organization_purpose,
|
||||
organization_members=character_data.organization_members,
|
||||
traits=character_data.traits,
|
||||
avatar_url=character_data.avatar_url
|
||||
)
|
||||
db.add(character)
|
||||
await db.flush() # 获取character.id
|
||||
|
||||
logger.info(f"✅ 手动创建角色成功:{character.name} (ID: {character.id}, 是否组织: {character.is_organization})")
|
||||
|
||||
# 如果是组织,且提供了组织额外字段,自动创建Organization详情记录
|
||||
if character.is_organization and (
|
||||
character_data.power_level is not None or
|
||||
character_data.location or
|
||||
character_data.motto or
|
||||
character_data.color
|
||||
):
|
||||
organization = Organization(
|
||||
character_id=character.id,
|
||||
project_id=character_data.project_id,
|
||||
member_count=0,
|
||||
power_level=character_data.power_level or 50,
|
||||
location=character_data.location,
|
||||
motto=character_data.motto,
|
||||
color=character_data.color
|
||||
)
|
||||
db.add(organization)
|
||||
await db.flush()
|
||||
logger.info(f"✅ 自动创建组织详情:{character.name} (Org ID: {organization.id})")
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(character)
|
||||
|
||||
logger.info(f"🎉 成功手动创建角色/组织: {character.name}")
|
||||
|
||||
return character
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"手动创建角色失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"创建角色失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/generate", response_model=CharacterResponse, summary="AI生成角色")
|
||||
async def generate_character(
|
||||
request: CharacterGenerateRequest,
|
||||
@@ -649,4 +725,216 @@ async def generate_character(
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成角色失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"生成角色失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"生成角色失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/generate-stream", summary="AI生成角色(流式)")
|
||||
async def generate_character_stream(
|
||||
request: CharacterGenerateRequest,
|
||||
http_request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user_ai_service: AIService = Depends(get_user_ai_service)
|
||||
):
|
||||
"""
|
||||
使用AI生成角色卡(支持SSE流式进度显示)
|
||||
|
||||
通过Server-Sent Events返回实时进度信息
|
||||
"""
|
||||
async def generate() -> AsyncGenerator[str, None]:
|
||||
try:
|
||||
# 验证用户权限和项目是否存在
|
||||
user_id = getattr(http_request.state, 'user_id', None)
|
||||
project = await verify_project_access(request.project_id, user_id, db)
|
||||
|
||||
yield await SSEResponse.send_progress("开始生成角色...", 0)
|
||||
|
||||
# 获取已存在的角色列表
|
||||
yield await SSEResponse.send_progress("获取项目上下文...", 10)
|
||||
|
||||
existing_chars_result = await db.execute(
|
||||
select(Character)
|
||||
.where(Character.project_id == request.project_id)
|
||||
.order_by(Character.created_at.desc())
|
||||
)
|
||||
existing_characters = existing_chars_result.scalars().all()
|
||||
|
||||
# 构建现有角色信息摘要
|
||||
existing_chars_info = ""
|
||||
character_list = []
|
||||
organization_list = []
|
||||
|
||||
if existing_characters:
|
||||
for c in existing_characters[:10]:
|
||||
if c.is_organization:
|
||||
organization_list.append(f"- {c.name} [{c.organization_type or '组织'}]")
|
||||
else:
|
||||
character_list.append(f"- {c.name}({c.role_type or '未知'})")
|
||||
|
||||
if character_list:
|
||||
existing_chars_info += "\n已有角色:\n" + "\n".join(character_list)
|
||||
if organization_list:
|
||||
existing_chars_info += "\n\n已有组织:\n" + "\n".join(organization_list)
|
||||
|
||||
# 构建项目上下文
|
||||
project_context = f"""
|
||||
项目信息:
|
||||
- 书名:{project.title}
|
||||
- 主题:{project.theme or '未设定'}
|
||||
- 类型:{project.genre or '未设定'}
|
||||
- 时间背景:{project.world_time_period or '未设定'}
|
||||
- 地理位置:{project.world_location or '未设定'}
|
||||
- 氛围基调:{project.world_atmosphere or '未设定'}
|
||||
- 世界规则:{project.world_rules or '未设定'}
|
||||
{existing_chars_info}
|
||||
"""
|
||||
|
||||
user_input = f"""
|
||||
用户要求:
|
||||
- 角色名称:{request.name or '请AI生成'}
|
||||
- 角色定位:{request.role_type or 'supporting'}
|
||||
- 背景设定:{request.background or '无特殊要求'}
|
||||
- 其他要求:{request.requirements or '无'}
|
||||
"""
|
||||
|
||||
yield await SSEResponse.send_progress("构建AI提示词...", 20)
|
||||
|
||||
prompt = prompt_service.get_single_character_prompt(
|
||||
project_context=project_context,
|
||||
user_input=user_input
|
||||
)
|
||||
|
||||
yield await SSEResponse.send_progress("调用AI服务生成角色...", 30)
|
||||
logger.info(f"🎯 开始为项目 {request.project_id} 生成角色(SSE流式)")
|
||||
|
||||
try:
|
||||
result = await user_ai_service.generate_text_with_mcp(
|
||||
prompt=prompt,
|
||||
user_id=user_id,
|
||||
db_session=db,
|
||||
enable_mcp=True,
|
||||
max_tool_rounds=2,
|
||||
tool_choice="auto",
|
||||
provider=None,
|
||||
model=None
|
||||
)
|
||||
|
||||
if isinstance(result, dict):
|
||||
ai_response = result.get('content', '')
|
||||
else:
|
||||
ai_response = result
|
||||
|
||||
except Exception as ai_error:
|
||||
logger.error(f"❌ AI服务调用异常:{str(ai_error)}")
|
||||
yield await SSEResponse.send_error(f"AI服务调用失败:{str(ai_error)}")
|
||||
return
|
||||
|
||||
if not ai_response or not ai_response.strip():
|
||||
yield await SSEResponse.send_error("AI服务返回空响应")
|
||||
return
|
||||
|
||||
yield await SSEResponse.send_progress("解析AI响应...", 60)
|
||||
|
||||
# 清理AI响应
|
||||
cleaned_response = ai_response.strip()
|
||||
if cleaned_response.startswith("```json"):
|
||||
cleaned_response = cleaned_response[7:]
|
||||
if cleaned_response.startswith("```"):
|
||||
cleaned_response = cleaned_response[3:]
|
||||
if cleaned_response.endswith("```"):
|
||||
cleaned_response = cleaned_response[:-3]
|
||||
cleaned_response = cleaned_response.strip()
|
||||
|
||||
try:
|
||||
character_data = json.loads(cleaned_response)
|
||||
except json.JSONDecodeError as e:
|
||||
yield await SSEResponse.send_error(f"AI返回的内容无法解析为JSON:{str(e)}")
|
||||
return
|
||||
|
||||
yield await SSEResponse.send_progress("创建角色记录...", 75)
|
||||
|
||||
# 转换traits
|
||||
traits_json = json.dumps(character_data.get("traits", []), ensure_ascii=False) if character_data.get("traits") else None
|
||||
is_organization = character_data.get("is_organization", False)
|
||||
|
||||
# 创建角色
|
||||
character = Character(
|
||||
project_id=request.project_id,
|
||||
name=character_data.get("name", request.name or "未命名角色"),
|
||||
age=str(character_data.get("age", "")),
|
||||
gender=character_data.get("gender"),
|
||||
is_organization=is_organization,
|
||||
role_type=request.role_type or "supporting",
|
||||
personality=character_data.get("personality", ""),
|
||||
background=character_data.get("background", ""),
|
||||
appearance=character_data.get("appearance", ""),
|
||||
relationships=character_data.get("relationships_text", character_data.get("relationships", "")),
|
||||
organization_type=character_data.get("organization_type") if is_organization else None,
|
||||
organization_purpose=character_data.get("organization_purpose") if is_organization else None,
|
||||
organization_members=json.dumps(character_data.get("organization_members", []), ensure_ascii=False) if is_organization else None,
|
||||
traits=traits_json
|
||||
)
|
||||
db.add(character)
|
||||
await db.flush()
|
||||
|
||||
logger.info(f"✅ 角色创建成功:{character.name} (ID: {character.id})")
|
||||
|
||||
# 如果是组织,创建Organization详情
|
||||
if is_organization:
|
||||
yield await SSEResponse.send_progress("创建组织详情...", 85)
|
||||
|
||||
org_check = await db.execute(
|
||||
select(Organization).where(Organization.character_id == character.id)
|
||||
)
|
||||
existing_org = org_check.scalar_one_or_none()
|
||||
|
||||
if not existing_org:
|
||||
organization = Organization(
|
||||
character_id=character.id,
|
||||
project_id=request.project_id,
|
||||
member_count=0,
|
||||
power_level=character_data.get("power_level", 50),
|
||||
location=character_data.get("location"),
|
||||
motto=character_data.get("motto"),
|
||||
color=character_data.get("color")
|
||||
)
|
||||
db.add(organization)
|
||||
await db.flush()
|
||||
|
||||
yield await SSEResponse.send_progress("保存生成历史...", 95)
|
||||
|
||||
# 记录生成历史
|
||||
history = GenerationHistory(
|
||||
project_id=request.project_id,
|
||||
prompt=prompt,
|
||||
generated_content=json.dumps(result, ensure_ascii=False) if isinstance(result, dict) else ai_response,
|
||||
model=user_ai_service.default_model
|
||||
)
|
||||
db.add(history)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(character)
|
||||
|
||||
logger.info(f"🎉 成功生成角色: {character.name}")
|
||||
|
||||
yield await SSEResponse.send_progress("角色生成完成!", 100, "success")
|
||||
|
||||
# 发送结果数据
|
||||
yield await SSEResponse.send_result({
|
||||
"character": {
|
||||
"id": character.id,
|
||||
"name": character.name,
|
||||
"role_type": character.role_type,
|
||||
"is_organization": character.is_organization
|
||||
}
|
||||
})
|
||||
|
||||
yield await SSEResponse.send_done()
|
||||
|
||||
except HTTPException as he:
|
||||
logger.error(f"HTTP异常: {he.detail}")
|
||||
yield await SSEResponse.send_error(he.detail, he.status_code)
|
||||
except Exception as e:
|
||||
logger.error(f"生成角色失败: {str(e)}")
|
||||
yield await SSEResponse.send_error(f"生成角色失败: {str(e)}")
|
||||
|
||||
return create_sse_response(generate())
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, AsyncGenerator
|
||||
from pydantic import BaseModel, Field
|
||||
import json
|
||||
|
||||
from app.database import get_db
|
||||
from app.utils.sse_response import SSEResponse, create_sse_response
|
||||
from app.models.relationship import Organization, OrganizationMember
|
||||
from app.models.character import Character
|
||||
from app.models.project import Project
|
||||
@@ -129,7 +130,7 @@ async def get_organization(
|
||||
return org
|
||||
|
||||
|
||||
@router.post("/", response_model=OrganizationResponse, summary="创建组织")
|
||||
@router.post("", response_model=OrganizationResponse, summary="创建组织")
|
||||
async def create_organization(
|
||||
organization: OrganizationCreate,
|
||||
request: Request,
|
||||
@@ -616,3 +617,195 @@ async def generate_organization(
|
||||
except Exception as e:
|
||||
logger.error(f"生成组织失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"生成组织失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/generate-stream", summary="AI生成组织(流式)")
|
||||
async def generate_organization_stream(
|
||||
gen_request: OrganizationGenerateRequest,
|
||||
http_request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user_ai_service: AIService = Depends(get_user_ai_service)
|
||||
):
|
||||
"""
|
||||
使用AI生成组织设定(支持SSE流式进度显示)
|
||||
|
||||
通过Server-Sent Events返回实时进度信息
|
||||
"""
|
||||
async def generate() -> AsyncGenerator[str, None]:
|
||||
try:
|
||||
# 验证用户权限和项目是否存在
|
||||
user_id = getattr(http_request.state, 'user_id', None)
|
||||
project = await verify_project_access(gen_request.project_id, user_id, db)
|
||||
|
||||
yield await SSEResponse.send_progress("开始生成组织...", 0)
|
||||
|
||||
# 获取已存在的角色和组织列表
|
||||
yield await SSEResponse.send_progress("获取项目上下文...", 10)
|
||||
|
||||
existing_chars_result = await db.execute(
|
||||
select(Character)
|
||||
.where(Character.project_id == gen_request.project_id)
|
||||
.order_by(Character.created_at.desc())
|
||||
)
|
||||
existing_characters = existing_chars_result.scalars().all()
|
||||
|
||||
# 构建现有角色和组织信息摘要
|
||||
existing_info = ""
|
||||
character_list = []
|
||||
organization_list = []
|
||||
|
||||
if existing_characters:
|
||||
for c in existing_characters[:10]:
|
||||
if c.is_organization:
|
||||
organization_list.append(f"- {c.name} [{c.organization_type or '组织'}]")
|
||||
else:
|
||||
character_list.append(f"- {c.name}({c.role_type or '未知'})")
|
||||
|
||||
if character_list:
|
||||
existing_info += "\n已有角色:\n" + "\n".join(character_list)
|
||||
if organization_list:
|
||||
existing_info += "\n\n已有组织:\n" + "\n".join(organization_list)
|
||||
|
||||
# 构建项目上下文
|
||||
project_context = f"""
|
||||
项目信息:
|
||||
- 书名:{project.title}
|
||||
- 主题:{project.theme or '未设定'}
|
||||
- 类型:{project.genre or '未设定'}
|
||||
- 时间背景:{project.world_time_period or '未设定'}
|
||||
- 地理位置:{project.world_location or '未设定'}
|
||||
- 氛围基调:{project.world_atmosphere or '未设定'}
|
||||
- 世界规则:{project.world_rules or '未设定'}
|
||||
{existing_info}
|
||||
"""
|
||||
|
||||
user_input = f"""
|
||||
用户要求:
|
||||
- 组织名称:{gen_request.name or '请AI生成'}
|
||||
- 组织类型:{gen_request.organization_type or '请AI根据世界观决定'}
|
||||
- 背景设定:{gen_request.background or '无特殊要求'}
|
||||
- 其他要求:{gen_request.requirements or '无'}
|
||||
"""
|
||||
|
||||
yield await SSEResponse.send_progress("构建AI提示词...", 20)
|
||||
|
||||
prompt = prompt_service.get_single_organization_prompt(
|
||||
project_context=project_context,
|
||||
user_input=user_input
|
||||
)
|
||||
|
||||
yield await SSEResponse.send_progress("调用AI服务生成组织...", 30)
|
||||
logger.info(f"🎯 开始为项目 {gen_request.project_id} 生成组织(SSE流式)")
|
||||
|
||||
try:
|
||||
ai_response = await user_ai_service.generate_text(prompt=prompt)
|
||||
ai_content = ai_response.get("content", "") if isinstance(ai_response, dict) else str(ai_response)
|
||||
except Exception as ai_error:
|
||||
logger.error(f"❌ AI服务调用异常:{str(ai_error)}")
|
||||
yield await SSEResponse.send_error(f"AI服务调用失败:{str(ai_error)}")
|
||||
return
|
||||
|
||||
if not ai_content or not ai_content.strip():
|
||||
yield await SSEResponse.send_error("AI服务返回空响应")
|
||||
return
|
||||
|
||||
yield await SSEResponse.send_progress("解析AI响应...", 60)
|
||||
|
||||
# 清理AI响应
|
||||
cleaned_response = ai_content.strip()
|
||||
if cleaned_response.startswith("```json"):
|
||||
cleaned_response = cleaned_response[7:]
|
||||
if cleaned_response.startswith("```"):
|
||||
cleaned_response = cleaned_response[3:]
|
||||
if cleaned_response.endswith("```"):
|
||||
cleaned_response = cleaned_response[:-3]
|
||||
cleaned_response = cleaned_response.strip()
|
||||
|
||||
try:
|
||||
organization_data = json.loads(cleaned_response)
|
||||
except json.JSONDecodeError as e:
|
||||
yield await SSEResponse.send_error(f"AI返回的内容无法解析为JSON:{str(e)}")
|
||||
return
|
||||
|
||||
yield await SSEResponse.send_progress("创建组织记录...", 75)
|
||||
|
||||
# 创建角色记录(组织也是角色的一种)
|
||||
character = Character(
|
||||
project_id=gen_request.project_id,
|
||||
name=organization_data.get("name", gen_request.name or "未命名组织"),
|
||||
is_organization=True,
|
||||
role_type="supporting",
|
||||
personality=organization_data.get("personality", ""),
|
||||
background=organization_data.get("background", ""),
|
||||
appearance=organization_data.get("appearance", ""),
|
||||
organization_type=organization_data.get("organization_type"),
|
||||
organization_purpose=organization_data.get("organization_purpose"),
|
||||
organization_members=json.dumps(
|
||||
organization_data.get("organization_members", []),
|
||||
ensure_ascii=False
|
||||
),
|
||||
traits=json.dumps(
|
||||
organization_data.get("traits", []),
|
||||
ensure_ascii=False
|
||||
)
|
||||
)
|
||||
db.add(character)
|
||||
await db.flush()
|
||||
|
||||
logger.info(f"✅ 组织角色创建成功:{character.name} (ID: {character.id})")
|
||||
|
||||
yield await SSEResponse.send_progress("创建组织详情...", 85)
|
||||
|
||||
# 自动创建Organization详情记录
|
||||
organization = Organization(
|
||||
character_id=character.id,
|
||||
project_id=gen_request.project_id,
|
||||
member_count=0,
|
||||
power_level=organization_data.get("power_level", 50),
|
||||
location=organization_data.get("location"),
|
||||
motto=organization_data.get("motto"),
|
||||
color=organization_data.get("color")
|
||||
)
|
||||
db.add(organization)
|
||||
await db.flush()
|
||||
|
||||
logger.info(f"✅ 组织详情创建成功:{character.name} (Org ID: {organization.id})")
|
||||
|
||||
yield await SSEResponse.send_progress("保存生成历史...", 95)
|
||||
|
||||
# 记录生成历史
|
||||
history = GenerationHistory(
|
||||
project_id=gen_request.project_id,
|
||||
prompt=prompt,
|
||||
generated_content=ai_content,
|
||||
model=user_ai_service.default_model
|
||||
)
|
||||
db.add(history)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(character)
|
||||
|
||||
logger.info(f"🎉 成功生成组织: {character.name}")
|
||||
|
||||
yield await SSEResponse.send_progress("组织生成完成!", 100, "success")
|
||||
|
||||
# 发送结果数据
|
||||
yield await SSEResponse.send_result({
|
||||
"character": {
|
||||
"id": character.id,
|
||||
"name": character.name,
|
||||
"organization_type": character.organization_type,
|
||||
"is_organization": character.is_organization
|
||||
}
|
||||
})
|
||||
|
||||
yield await SSEResponse.send_done()
|
||||
|
||||
except HTTPException as he:
|
||||
logger.error(f"HTTP异常: {he.detail}")
|
||||
yield await SSEResponse.send_error(he.detail, he.status_code)
|
||||
except Exception as e:
|
||||
logger.error(f"生成组织失败: {str(e)}")
|
||||
yield await SSEResponse.send_error(f"生成组织失败: {str(e)}")
|
||||
|
||||
return create_sse_response(generate())
|
||||
|
||||
@@ -21,6 +21,31 @@ class CharacterBase(BaseModel):
|
||||
traits: Optional[str] = Field(None, description="特征标签(JSON)")
|
||||
|
||||
|
||||
class CharacterCreate(BaseModel):
|
||||
"""手动创建角色的请求模型"""
|
||||
project_id: str = Field(..., description="项目ID")
|
||||
name: str = Field(..., description="角色/组织姓名")
|
||||
age: Optional[str] = Field(None, description="年龄")
|
||||
gender: Optional[str] = Field(None, description="性别")
|
||||
is_organization: bool = Field(False, description="是否为组织")
|
||||
role_type: Optional[str] = Field("supporting", description="角色类型:protagonist/supporting/antagonist")
|
||||
personality: Optional[str] = Field(None, description="性格特点/组织特性")
|
||||
background: Optional[str] = Field(None, description="背景故事")
|
||||
appearance: Optional[str] = Field(None, description="外貌特征")
|
||||
relationships: Optional[str] = Field(None, description="人际关系(JSON)")
|
||||
organization_type: Optional[str] = Field(None, description="组织类型")
|
||||
organization_purpose: Optional[str] = Field(None, description="组织目的")
|
||||
organization_members: Optional[str] = Field(None, description="组织成员(JSON)")
|
||||
traits: Optional[str] = Field(None, description="特征标签(JSON)")
|
||||
avatar_url: Optional[str] = Field(None, description="头像URL")
|
||||
|
||||
# 组织额外字段
|
||||
power_level: Optional[int] = Field(None, description="组织势力等级(0-100)")
|
||||
location: Optional[str] = Field(None, description="组织所在地")
|
||||
motto: Optional[str] = Field(None, description="组织格言/口号")
|
||||
color: Optional[str] = Field(None, description="组织代表颜色")
|
||||
|
||||
|
||||
class CharacterUpdate(BaseModel):
|
||||
"""更新角色的请求模型"""
|
||||
name: Optional[str] = None
|
||||
|
||||
@@ -7,6 +7,7 @@ import { projectApi, writingStyleApi } from '../services/api';
|
||||
import type { Chapter, ChapterUpdate, ApiError, WritingStyle, AnalysisTask } from '../types';
|
||||
import { cardStyles } from '../components/CardStyles';
|
||||
import ChapterAnalysis from '../components/ChapterAnalysis';
|
||||
import { SSELoadingOverlay } from '../components/SSELoadingOverlay';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
@@ -30,6 +31,10 @@ export default function Chapters() {
|
||||
const [analysisTasksMap, setAnalysisTasksMap] = useState<Record<string, AnalysisTask>>({});
|
||||
const pollingIntervalsRef = useRef<Record<string, number>>({});
|
||||
|
||||
// 单章节生成进度状态
|
||||
const [singleChapterProgress, setSingleChapterProgress] = useState(0);
|
||||
const [singleChapterProgressMessage, setSingleChapterProgressMessage] = useState('');
|
||||
|
||||
// 批量生成相关状态
|
||||
const [batchGenerateVisible, setBatchGenerateVisible] = useState(false);
|
||||
const [batchGenerating, setBatchGenerating] = useState(false);
|
||||
@@ -301,17 +306,29 @@ export default function Chapters() {
|
||||
try {
|
||||
setIsContinuing(true);
|
||||
setIsGenerating(true);
|
||||
setSingleChapterProgress(0);
|
||||
setSingleChapterProgressMessage('准备开始生成...');
|
||||
|
||||
const result = await generateChapterContentStream(editingId, (content) => {
|
||||
editorForm.setFieldsValue({ content });
|
||||
|
||||
if (contentTextAreaRef.current) {
|
||||
const textArea = contentTextAreaRef.current.resizableTextArea?.textArea;
|
||||
if (textArea) {
|
||||
textArea.scrollTop = textArea.scrollHeight;
|
||||
const result = await generateChapterContentStream(
|
||||
editingId,
|
||||
(content) => {
|
||||
editorForm.setFieldsValue({ content });
|
||||
|
||||
if (contentTextAreaRef.current) {
|
||||
const textArea = contentTextAreaRef.current.resizableTextArea?.textArea;
|
||||
if (textArea) {
|
||||
textArea.scrollTop = textArea.scrollHeight;
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedStyleId,
|
||||
targetWordCount,
|
||||
(progressMsg, progressValue) => {
|
||||
// 进度回调
|
||||
setSingleChapterProgress(progressValue);
|
||||
setSingleChapterProgressMessage(progressMsg);
|
||||
}
|
||||
}, selectedStyleId, targetWordCount);
|
||||
);
|
||||
|
||||
message.success('AI创作成功,正在分析章节内容...');
|
||||
|
||||
@@ -338,6 +355,8 @@ export default function Chapters() {
|
||||
} finally {
|
||||
setIsContinuing(false);
|
||||
setIsGenerating(false);
|
||||
setSingleChapterProgress(0);
|
||||
setSingleChapterProgressMessage('');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1378,6 +1397,13 @@ export default function Chapters() {
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* 单章节生成进度显示 */}
|
||||
<SSELoadingOverlay
|
||||
loading={isGenerating}
|
||||
progress={singleChapterProgress}
|
||||
message={singleChapterProgressMessage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button, Modal, Form, Input, Select, message, Row, Col, Empty, Tabs, Divider, Typography, Space } from 'antd';
|
||||
import { ThunderboltOutlined, UserOutlined, TeamOutlined } from '@ant-design/icons';
|
||||
import { Button, Modal, Form, Input, Select, message, Row, Col, Empty, Tabs, Divider, Typography, Space, InputNumber } from 'antd';
|
||||
import { ThunderboltOutlined, UserOutlined, TeamOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { useCharacterSync } from '../store/hooks';
|
||||
import { characterGridConfig } from '../components/CardStyles';
|
||||
import { CharacterCard } from '../components/CharacterCard';
|
||||
import { SSELoadingOverlay } from '../components/SSELoadingOverlay';
|
||||
import type { Character, CharacterUpdate } from '../types';
|
||||
import { characterApi } from '../services/api';
|
||||
import { SSEPostClient } from '../utils/sseClient';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
@@ -15,16 +17,21 @@ const { TextArea } = Input;
|
||||
export default function Characters() {
|
||||
const { currentProject, characters } = useStore();
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [progressMessage, setProgressMessage] = useState('');
|
||||
const [activeTab, setActiveTab] = useState<'all' | 'character' | 'organization'>('all');
|
||||
const [generateForm] = Form.useForm();
|
||||
const [generateOrgForm] = Form.useForm();
|
||||
const [createForm] = Form.useForm();
|
||||
const [editForm] = Form.useForm();
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [createType, setCreateType] = useState<'character' | 'organization'>('character');
|
||||
const [editingCharacter, setEditingCharacter] = useState<Character | null>(null);
|
||||
|
||||
const {
|
||||
refreshCharacters,
|
||||
deleteCharacter,
|
||||
generateCharacter
|
||||
deleteCharacter
|
||||
} = useCharacterSync();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -48,18 +55,140 @@ export default function Characters() {
|
||||
const handleGenerate = async (values: { name?: string; role_type: string; background?: string }) => {
|
||||
try {
|
||||
setIsGenerating(true);
|
||||
await generateCharacter({
|
||||
project_id: currentProject.id,
|
||||
name: values.name,
|
||||
role_type: values.role_type,
|
||||
background: values.background,
|
||||
});
|
||||
setProgress(0);
|
||||
setProgressMessage('准备生成角色...');
|
||||
|
||||
const client = new SSEPostClient(
|
||||
'/api/characters/generate-stream',
|
||||
{
|
||||
project_id: currentProject.id,
|
||||
name: values.name,
|
||||
role_type: values.role_type,
|
||||
background: values.background,
|
||||
},
|
||||
{
|
||||
onProgress: (msg, prog) => {
|
||||
setProgress(prog);
|
||||
setProgressMessage(msg);
|
||||
},
|
||||
onResult: (data) => {
|
||||
console.log('角色生成完成:', data);
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error(`生成失败: ${error}`);
|
||||
},
|
||||
onComplete: () => {
|
||||
setProgress(100);
|
||||
setProgressMessage('生成完成!');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await client.connect();
|
||||
message.success('AI生成角色成功');
|
||||
Modal.destroyAll();
|
||||
} catch {
|
||||
message.error('AI生成失败');
|
||||
await refreshCharacters();
|
||||
} catch (error: any) {
|
||||
message.error(error.message || 'AI生成失败');
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
setTimeout(() => {
|
||||
setIsGenerating(false);
|
||||
setProgress(0);
|
||||
setProgressMessage('');
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerateOrganization = async (values: {
|
||||
name?: string;
|
||||
organization_type?: string;
|
||||
background?: string;
|
||||
requirements?: string;
|
||||
}) => {
|
||||
try {
|
||||
setIsGenerating(true);
|
||||
setProgress(0);
|
||||
setProgressMessage('准备生成组织...');
|
||||
|
||||
const client = new SSEPostClient(
|
||||
'/api/organizations/generate-stream',
|
||||
{
|
||||
project_id: currentProject.id,
|
||||
name: values.name,
|
||||
organization_type: values.organization_type,
|
||||
background: values.background,
|
||||
requirements: values.requirements,
|
||||
},
|
||||
{
|
||||
onProgress: (msg, prog) => {
|
||||
setProgress(prog);
|
||||
setProgressMessage(msg);
|
||||
},
|
||||
onResult: (data) => {
|
||||
console.log('组织生成完成:', data);
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error(`生成失败: ${error}`);
|
||||
},
|
||||
onComplete: () => {
|
||||
setProgress(100);
|
||||
setProgressMessage('生成完成!');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await client.connect();
|
||||
message.success('AI生成组织成功');
|
||||
Modal.destroyAll();
|
||||
await refreshCharacters();
|
||||
} catch (error: any) {
|
||||
message.error(error.message || 'AI生成失败');
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
setIsGenerating(false);
|
||||
setProgress(0);
|
||||
setProgressMessage('');
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateCharacter = async (values: any) => {
|
||||
try {
|
||||
const createData: any = {
|
||||
project_id: currentProject.id,
|
||||
name: values.name,
|
||||
is_organization: createType === 'organization',
|
||||
};
|
||||
|
||||
if (createType === 'character') {
|
||||
// 角色字段
|
||||
createData.age = values.age;
|
||||
createData.gender = values.gender;
|
||||
createData.role_type = values.role_type || 'supporting';
|
||||
createData.personality = values.personality;
|
||||
createData.appearance = values.appearance;
|
||||
createData.relationships = values.relationships;
|
||||
createData.background = values.background;
|
||||
} else {
|
||||
// 组织字段
|
||||
createData.organization_type = values.organization_type;
|
||||
createData.organization_purpose = values.organization_purpose;
|
||||
createData.organization_members = values.organization_members;
|
||||
createData.background = values.background;
|
||||
createData.power_level = values.power_level;
|
||||
createData.location = values.location;
|
||||
createData.motto = values.motto;
|
||||
createData.color = values.color;
|
||||
createData.role_type = 'supporting'; // 组织默认为配角
|
||||
}
|
||||
|
||||
await characterApi.createCharacter(createData);
|
||||
message.success(`${createType === 'character' ? '角色' : '组织'}创建成功`);
|
||||
setIsCreateModalOpen(false);
|
||||
createForm.resetFields();
|
||||
await refreshCharacters();
|
||||
} catch {
|
||||
message.error('创建失败');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -126,6 +255,42 @@ export default function Characters() {
|
||||
});
|
||||
};
|
||||
|
||||
const showGenerateOrgModal = () => {
|
||||
Modal.confirm({
|
||||
title: 'AI生成组织',
|
||||
width: 600,
|
||||
centered: true,
|
||||
content: (
|
||||
<Form form={generateOrgForm} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item
|
||||
label="组织名称"
|
||||
name="name"
|
||||
>
|
||||
<Input placeholder="如:天剑门、黑龙会(可选,AI会自动生成)" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="组织类型"
|
||||
name="organization_type"
|
||||
>
|
||||
<Input placeholder="如:门派、帮派、公司、学院(可选,AI会根据世界观生成)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="背景设定" name="background">
|
||||
<TextArea rows={3} placeholder="简要描述组织的背景和环境..." />
|
||||
</Form.Item>
|
||||
<Form.Item label="其他要求" name="requirements">
|
||||
<TextArea rows={2} placeholder="其他特殊要求..." />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
),
|
||||
okText: '生成',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
const values = await generateOrgForm.validateFields();
|
||||
await handleGenerateOrganization(values);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const characterList = characters.filter(c => !c.is_organization);
|
||||
const organizationList = characters.filter(c => c.is_organization);
|
||||
|
||||
@@ -156,15 +321,48 @@ export default function Characters() {
|
||||
alignItems: isMobile ? 'stretch' : 'center'
|
||||
}}>
|
||||
<h2 style={{ margin: 0, fontSize: isMobile ? 18 : 24 }}>角色与组织管理</h2>
|
||||
<Button
|
||||
type="dashed"
|
||||
icon={<ThunderboltOutlined />}
|
||||
onClick={showGenerateModal}
|
||||
loading={isGenerating}
|
||||
block={isMobile}
|
||||
>
|
||||
AI生成角色
|
||||
</Button>
|
||||
<Space wrap>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
setCreateType('character');
|
||||
setIsCreateModalOpen(true);
|
||||
}}
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
>
|
||||
创建角色
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
setCreateType('organization');
|
||||
setIsCreateModalOpen(true);
|
||||
}}
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
>
|
||||
创建组织
|
||||
</Button>
|
||||
<Button
|
||||
type="dashed"
|
||||
icon={<ThunderboltOutlined />}
|
||||
onClick={showGenerateModal}
|
||||
loading={isGenerating}
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
>
|
||||
AI生成角色
|
||||
</Button>
|
||||
<Button
|
||||
type="dashed"
|
||||
icon={<ThunderboltOutlined />}
|
||||
onClick={showGenerateOrgModal}
|
||||
loading={isGenerating}
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
>
|
||||
AI生成组织
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{characters.length > 0 && (
|
||||
@@ -465,6 +663,162 @@ export default function Characters() {
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 手动创建角色/组织模态框 */}
|
||||
<Modal
|
||||
title={createType === 'character' ? '创建角色' : '创建组织'}
|
||||
open={isCreateModalOpen}
|
||||
onCancel={() => {
|
||||
setIsCreateModalOpen(false);
|
||||
createForm.resetFields();
|
||||
}}
|
||||
footer={null}
|
||||
centered={!isMobile}
|
||||
width={isMobile ? '100%' : 600}
|
||||
style={isMobile ? { top: 0, paddingBottom: 0, maxWidth: '100vw' } : undefined}
|
||||
styles={isMobile ? { body: { maxHeight: 'calc(100vh - 110px)', overflowY: 'auto' } } : undefined}
|
||||
>
|
||||
<Form form={createForm} layout="vertical" onFinish={handleCreateCharacter}>
|
||||
<Row gutter={16}>
|
||||
<Col span={createType === 'organization' ? 24 : 12}>
|
||||
<Form.Item
|
||||
label={createType === 'character' ? '角色名称' : '组织名称'}
|
||||
name="name"
|
||||
rules={[{ required: true, message: `请输入${createType === 'character' ? '角色' : '组织'}名称` }]}
|
||||
>
|
||||
<Input placeholder={`输入${createType === 'character' ? '角色' : '组织'}名称`} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
{createType === 'character' && (
|
||||
<Col span={12}>
|
||||
<Form.Item label="角色定位" name="role_type" initialValue="supporting">
|
||||
<Select>
|
||||
<Select.Option value="protagonist">主角</Select.Option>
|
||||
<Select.Option value="supporting">配角</Select.Option>
|
||||
<Select.Option value="antagonist">反派</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
{createType === 'character' ? (
|
||||
<>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="年龄" name="age">
|
||||
<Input placeholder="如:25、30岁" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="性别" name="gender">
|
||||
<Select placeholder="选择性别">
|
||||
<Select.Option value="男">男</Select.Option>
|
||||
<Select.Option value="女">女</Select.Option>
|
||||
<Select.Option value="其他">其他</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item label="性格特点" name="personality">
|
||||
<TextArea rows={2} placeholder="描述角色的性格特点..." />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="外貌描写" name="appearance">
|
||||
<TextArea rows={2} placeholder="描述角色的外貌特征..." />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="人际关系" name="relationships">
|
||||
<TextArea rows={2} placeholder="描述角色与其他角色的关系..." />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="角色背景" name="background">
|
||||
<TextArea rows={3} placeholder="描述角色的背景故事..." />
|
||||
</Form.Item>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="组织类型"
|
||||
name="organization_type"
|
||||
rules={[{ required: true, message: '请输入组织类型' }]}
|
||||
>
|
||||
<Input placeholder="如:帮派、公司、门派、学院" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="势力等级"
|
||||
name="power_level"
|
||||
initialValue={50}
|
||||
tooltip="0-100的数值,表示组织的影响力"
|
||||
>
|
||||
<InputNumber min={0} max={100} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
label="组织目的"
|
||||
name="organization_purpose"
|
||||
rules={[{ required: true, message: '请输入组织目的' }]}
|
||||
>
|
||||
<TextArea rows={2} placeholder="描述组织的宗旨和目标..." />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="主要成员" name="organization_members">
|
||||
<Input placeholder="如:张三、李四、王五" />
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="所在地" name="location">
|
||||
<Input placeholder="组织的主要活动区域或总部位置" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="代表颜色" name="color">
|
||||
<Input placeholder="如:深红色、金色、黑色等" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item label="格言/口号" name="motto">
|
||||
<Input placeholder="组织的宗旨、格言或口号" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="组织背景" name="background">
|
||||
<TextArea rows={3} placeholder="描述组织的背景故事..." />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form.Item>
|
||||
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
||||
<Button onClick={() => {
|
||||
setIsCreateModalOpen(false);
|
||||
createForm.resetFields();
|
||||
}}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
创建
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* SSE进度显示 */}
|
||||
<SSELoadingOverlay
|
||||
loading={isGenerating}
|
||||
progress={progress}
|
||||
message={progressMessage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Card, Table, Tag, Button, Space, message, Modal, Form, Select, InputNumber, Input, Descriptions } from 'antd';
|
||||
import { PlusOutlined, TeamOutlined, UserOutlined, EditOutlined, DeleteOutlined, ThunderboltOutlined } from '@ant-design/icons';
|
||||
import { PlusOutlined, TeamOutlined, UserOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import axios from 'axios';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface Organization {
|
||||
id: string;
|
||||
character_id: string;
|
||||
@@ -48,10 +46,8 @@ export default function Organizations() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isAddMemberModalOpen, setIsAddMemberModalOpen] = useState(false);
|
||||
const [isEditOrgModalOpen, setIsEditOrgModalOpen] = useState(false);
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [editOrgForm] = Form.useForm();
|
||||
const [generateForm] = Form.useForm();
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -151,68 +147,6 @@ export default function Organizations() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleGenerateOrganization = async (values: {
|
||||
name?: string;
|
||||
organization_type?: string;
|
||||
background?: string;
|
||||
requirements?: string;
|
||||
}) => {
|
||||
try {
|
||||
setIsGenerating(true);
|
||||
await axios.post('/api/organizations/generate', {
|
||||
project_id: projectId,
|
||||
name: values.name,
|
||||
organization_type: values.organization_type,
|
||||
background: values.background,
|
||||
requirements: values.requirements,
|
||||
});
|
||||
message.success('AI生成组织成功');
|
||||
Modal.destroyAll();
|
||||
generateForm.resetFields();
|
||||
loadOrganizations();
|
||||
} catch (error: any) {
|
||||
message.error(error.response?.data?.detail || 'AI生成失败');
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const showGenerateModal = () => {
|
||||
Modal.confirm({
|
||||
title: 'AI生成组织',
|
||||
width: 600,
|
||||
centered: !isMobile,
|
||||
content: (
|
||||
<Form form={generateForm} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item
|
||||
label="组织名称"
|
||||
name="name"
|
||||
>
|
||||
<Input placeholder="如:天剑门、黑龙会(可选,AI会自动生成)" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="组织类型"
|
||||
name="organization_type"
|
||||
>
|
||||
<Input placeholder="如:门派、帮派、公司、学院(可选,AI会根据世界观生成)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="背景设定" name="background">
|
||||
<TextArea rows={3} placeholder="简要描述组织的背景和环境..." />
|
||||
</Form.Item>
|
||||
<Form.Item label="其他要求" name="requirements">
|
||||
<TextArea rows={2} placeholder="其他特殊要求..." />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
),
|
||||
okText: '生成',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
const values = await generateForm.validateFields();
|
||||
await handleGenerateOrganization(values);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
active: 'green',
|
||||
@@ -332,17 +266,6 @@ export default function Organizations() {
|
||||
{!isMobile && <Tag color="blue">{currentProject?.title}</Tag>}
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Button
|
||||
type="dashed"
|
||||
icon={<ThunderboltOutlined />}
|
||||
onClick={showGenerateModal}
|
||||
loading={isGenerating}
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
>
|
||||
AI生成组织
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div style={{
|
||||
display: isMobile ? 'flex' : 'grid',
|
||||
|
||||
@@ -303,6 +303,29 @@ export const characterApi = {
|
||||
|
||||
getCharacter: (id: string) => api.get<unknown, Character>(`/characters/${id}`),
|
||||
|
||||
createCharacter: (data: {
|
||||
project_id: string;
|
||||
name: string;
|
||||
age?: string;
|
||||
gender?: string;
|
||||
is_organization?: boolean;
|
||||
role_type?: string;
|
||||
personality?: string;
|
||||
background?: string;
|
||||
appearance?: string;
|
||||
relationships?: string;
|
||||
organization_type?: string;
|
||||
organization_purpose?: string;
|
||||
organization_members?: string;
|
||||
traits?: string;
|
||||
avatar_url?: string;
|
||||
power_level?: number;
|
||||
location?: string;
|
||||
motto?: string;
|
||||
color?: string;
|
||||
}) =>
|
||||
api.post<unknown, Character>('/characters', data),
|
||||
|
||||
updateCharacter: (id: string, data: CharacterUpdate) =>
|
||||
api.put<unknown, Character>(`/characters/${id}`, data),
|
||||
|
||||
|
||||
@@ -303,7 +303,8 @@ export function useChapterSync() {
|
||||
chapterId: string,
|
||||
onProgress?: (content: string) => void,
|
||||
styleId?: number,
|
||||
targetWordCount?: number
|
||||
targetWordCount?: number,
|
||||
onProgressUpdate?: (message: string, progress: number) => void
|
||||
) => {
|
||||
try {
|
||||
// 使用fetch处理流式响应
|
||||
@@ -356,7 +357,20 @@ export function useChapterSync() {
|
||||
if (dataMatch) {
|
||||
const message = JSON.parse(dataMatch[1]);
|
||||
|
||||
if (message.type === 'content' && message.content) {
|
||||
if (message.type === 'start') {
|
||||
// 开始生成
|
||||
if (onProgressUpdate) {
|
||||
onProgressUpdate(message.message || '开始生成...', 0);
|
||||
}
|
||||
} else if (message.type === 'progress') {
|
||||
// 进度更新
|
||||
if (onProgressUpdate) {
|
||||
onProgressUpdate(
|
||||
message.message || '生成中...',
|
||||
message.progress || 0
|
||||
);
|
||||
}
|
||||
} else if (message.type === 'content' && message.content) {
|
||||
fullContent += message.content;
|
||||
if (onProgress) {
|
||||
onProgress(fullContent);
|
||||
@@ -366,8 +380,17 @@ export function useChapterSync() {
|
||||
} else if (message.type === 'done') {
|
||||
// 生成完成,保存分析任务ID
|
||||
analysisTaskId = message.analysis_task_id;
|
||||
if (onProgressUpdate) {
|
||||
onProgressUpdate('生成完成', 100);
|
||||
}
|
||||
// 生成完成,刷新章节数据
|
||||
await refreshChapters();
|
||||
} else if (message.type === 'analysis_started') {
|
||||
// 分析已开始
|
||||
analysisTaskId = message.task_id;
|
||||
if (onProgressUpdate) {
|
||||
onProgressUpdate('章节分析已开始...', 100);
|
||||
}
|
||||
} else if (message.type === 'analysis_queued') {
|
||||
// 分析任务已加入队列
|
||||
analysisTaskId = message.task_id;
|
||||
|
||||
Reference in New Issue
Block a user