feat: 为 VEO3 角色定义工具添加创建角色文件功能
- 添加创建角色文件按钮,位于发送按钮右侧 - 集成 parse_json_tolerant 命令提取最近一次对话中的 JSON - 支持选择保存目录并自动生成带时间戳的文件名 - 添加成功/错误提示,提供良好的用户反馈 - 实现完整的角色档案保存流程,支持 JSON 格式输出 - 优化用户体验,按钮状态和加载动画
This commit is contained in:
parent
c7268ba5b1
commit
d86c1d23fb
|
|
@ -789,7 +789,8 @@ pub fn run() {
|
|||
commands::veo3_actor_define_commands::veo3_actor_define_get_conversation_history,
|
||||
commands::veo3_actor_define_commands::veo3_actor_define_clear_conversation,
|
||||
commands::veo3_actor_define_commands::veo3_actor_define_remove_session,
|
||||
commands::veo3_actor_define_commands::veo3_actor_define_get_active_sessions
|
||||
commands::veo3_actor_define_commands::veo3_actor_define_get_active_sessions,
|
||||
commands::veo3_actor_define_commands::veo3_actor_define_create_actor_file
|
||||
])
|
||||
.setup(|app| {
|
||||
// 初始化日志系统
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use tokio::sync::Mutex;
|
||||
use tauri::State;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use chrono;
|
||||
use veo3_scene_writer::Veo3ActorDefine;
|
||||
use crate::presentation::commands::tolerant_json_commands::{ParseJsonRequest, JsonParserState};
|
||||
|
||||
/// VEO3 角色定义响应结构
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
|
@ -176,6 +180,139 @@ pub async fn veo3_actor_define_get_active_sessions(
|
|||
Ok(sessions.keys().cloned().collect())
|
||||
}
|
||||
|
||||
/// 创建角色文件请求结构
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateActorFileRequest {
|
||||
pub session_id: String,
|
||||
pub file_path: String,
|
||||
pub file_name: Option<String>,
|
||||
}
|
||||
|
||||
/// 创建角色文件响应结构
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CreateActorFileResponse {
|
||||
pub success: bool,
|
||||
pub file_path: Option<String>,
|
||||
pub extracted_json: Option<Value>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// 创建角色文件 - 从最近一次对话中提取JSON并保存
|
||||
#[tauri::command]
|
||||
pub async fn veo3_actor_define_create_actor_file(
|
||||
request: CreateActorFileRequest,
|
||||
session_manager: State<'_, Veo3ActorDefineSessionManager>,
|
||||
json_parser_state: State<'_, JsonParserState>,
|
||||
) -> Result<CreateActorFileResponse, String> {
|
||||
// 获取最近一次助手回复
|
||||
let last_assistant_message = {
|
||||
let sessions = session_manager.lock().await;
|
||||
if let Some(actor_define) = sessions.get(&request.session_id) {
|
||||
let history = actor_define.get_conversation_history();
|
||||
|
||||
// 查找最后一条助手消息
|
||||
history.iter()
|
||||
.rev()
|
||||
.find(|msg| msg.starts_with("助手: "))
|
||||
.map(|msg| msg.strip_prefix("助手: ").unwrap_or(msg).to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let assistant_message = match last_assistant_message {
|
||||
Some(msg) => msg,
|
||||
None => {
|
||||
return Ok(CreateActorFileResponse {
|
||||
success: false,
|
||||
file_path: None,
|
||||
extracted_json: None,
|
||||
error: Some("未找到助手回复消息".to_string()),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 使用容错JSON解析器提取JSON
|
||||
let parse_request = ParseJsonRequest {
|
||||
text: assistant_message,
|
||||
config: None,
|
||||
};
|
||||
|
||||
let parse_response = match crate::presentation::commands::tolerant_json_commands::parse_json_tolerant(
|
||||
parse_request,
|
||||
json_parser_state,
|
||||
).await {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
return Ok(CreateActorFileResponse {
|
||||
success: false,
|
||||
file_path: None,
|
||||
extracted_json: None,
|
||||
error: Some(format!("JSON解析失败: {}", e)),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if !parse_response.success || parse_response.data.is_none() {
|
||||
return Ok(CreateActorFileResponse {
|
||||
success: false,
|
||||
file_path: None,
|
||||
extracted_json: None,
|
||||
error: Some(parse_response.error.unwrap_or_else(|| "JSON解析失败".to_string())),
|
||||
});
|
||||
}
|
||||
|
||||
let json_data = parse_response.data.unwrap();
|
||||
|
||||
// 生成文件名
|
||||
let file_name = request.file_name.unwrap_or_else(|| {
|
||||
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
|
||||
format!("veo3_actor_{}.json", timestamp)
|
||||
});
|
||||
|
||||
// 确保文件路径存在
|
||||
let full_path = Path::new(&request.file_path).join(&file_name);
|
||||
|
||||
if let Some(parent_dir) = full_path.parent() {
|
||||
if let Err(e) = std::fs::create_dir_all(parent_dir) {
|
||||
return Ok(CreateActorFileResponse {
|
||||
success: false,
|
||||
file_path: None,
|
||||
extracted_json: Some(json_data),
|
||||
error: Some(format!("创建目录失败: {}", e)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 保存JSON文件
|
||||
let json_string = match serde_json::to_string_pretty(&json_data) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Ok(CreateActorFileResponse {
|
||||
success: false,
|
||||
file_path: None,
|
||||
extracted_json: Some(json_data),
|
||||
error: Some(format!("JSON序列化失败: {}", e)),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
match std::fs::write(&full_path, json_string) {
|
||||
Ok(_) => Ok(CreateActorFileResponse {
|
||||
success: true,
|
||||
file_path: Some(full_path.to_string_lossy().to_string()),
|
||||
extracted_json: Some(json_data),
|
||||
error: None,
|
||||
}),
|
||||
Err(e) => Ok(CreateActorFileResponse {
|
||||
success: false,
|
||||
file_path: None,
|
||||
extracted_json: Some(json_data),
|
||||
error: Some(format!("文件保存失败: {}", e)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -11,12 +11,15 @@ import {
|
|||
RefreshCw,
|
||||
Download,
|
||||
Copy,
|
||||
CheckCircle
|
||||
CheckCircle,
|
||||
UserPlus,
|
||||
FolderOpen
|
||||
} from 'lucide-react';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import {
|
||||
veo3ActorDefineService,
|
||||
Veo3ActorDefineResponse
|
||||
import {
|
||||
veo3ActorDefineService,
|
||||
Veo3ActorDefineResponse,
|
||||
CreateActorFileResponse
|
||||
} from '../../services/veo3ActorDefineService';
|
||||
|
||||
/**
|
||||
|
|
@ -43,6 +46,8 @@ export const Veo3ActorDefineTool: React.FC = () => {
|
|||
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
|
||||
const [serviceAvailable, setServiceAvailable] = useState<boolean | null>(null);
|
||||
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
|
||||
const [isCreatingActor, setIsCreatingActor] = useState(false);
|
||||
const [createActorSuccess, setCreateActorSuccess] = useState<string | null>(null);
|
||||
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -213,6 +218,41 @@ export const Veo3ActorDefineTool: React.FC = () => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
// 选择保存目录并创建角色文件
|
||||
const handleCreateActorFile = useCallback(async () => {
|
||||
if (isCreatingActor) return;
|
||||
|
||||
try {
|
||||
// 选择保存目录
|
||||
const selectedDir = await open({
|
||||
directory: true,
|
||||
title: '选择角色文件保存目录'
|
||||
});
|
||||
|
||||
if (!selectedDir || typeof selectedDir !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCreatingActor(true);
|
||||
setError(null);
|
||||
|
||||
// 调用创建角色文件服务
|
||||
const result = await veo3ActorDefineService.createActorFile(selectedDir);
|
||||
|
||||
if (result.success && result.file_path) {
|
||||
setCreateActorSuccess(result.file_path);
|
||||
setTimeout(() => setCreateActorSuccess(null), 5000);
|
||||
} else {
|
||||
setError(result.error || '创建角色文件失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建角色文件失败:', error);
|
||||
setError(error instanceof Error ? error.message : '创建角色文件失败');
|
||||
} finally {
|
||||
setIsCreatingActor(false);
|
||||
}
|
||||
}, [isCreatingActor]);
|
||||
|
||||
// 如果服务不可用,显示错误状态
|
||||
if (serviceAvailable === false) {
|
||||
return (
|
||||
|
|
@ -380,6 +420,23 @@ export const Veo3ActorDefineTool: React.FC = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 成功提示 */}
|
||||
{createActorSuccess && (
|
||||
<div className="mx-4 mb-2 p-3 bg-green-50 border border-green-200 rounded-lg flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<span className="text-green-700 text-sm">角色文件创建成功!</span>
|
||||
<div className="text-xs text-green-600 mt-1 break-all">{createActorSuccess}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setCreateActorSuccess(null)}
|
||||
className="ml-auto text-green-600 hover:text-green-800 text-sm font-medium"
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 输入区域 */}
|
||||
<div className="flex-shrink-0 bg-white border-t border-gray-200">
|
||||
{/* 选中的文件显示 */}
|
||||
|
|
@ -450,6 +507,20 @@ export const Veo3ActorDefineTool: React.FC = () => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleCreateActorFile}
|
||||
disabled={isCreatingActor || messages.length === 0}
|
||||
className="px-4 bg-green-500 text-white rounded-lg hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 disabled:bg-gray-300 disabled:cursor-not-allowed transition-all duration-200 flex items-center justify-center flex-shrink-0"
|
||||
style={{ height: '48px', minHeight: '48px' }}
|
||||
title="从最近一次对话中提取JSON并保存为角色文件"
|
||||
>
|
||||
{isCreatingActor ? (
|
||||
<div className="animate-spin w-5 h-5 border-2 border-white border-t-transparent rounded-full"></div>
|
||||
) : (
|
||||
<UserPlus className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleSendMessage}
|
||||
disabled={(!input.trim() && selectedFiles.length === 0) || isLoading}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,25 @@ export interface Veo3ActorDefineSession {
|
|||
last_updated: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建角色文件请求接口
|
||||
*/
|
||||
export interface CreateActorFileRequest {
|
||||
session_id: string;
|
||||
file_path: string;
|
||||
file_name?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建角色文件响应接口
|
||||
*/
|
||||
export interface CreateActorFileResponse {
|
||||
success: boolean;
|
||||
file_path?: string;
|
||||
extracted_json?: any;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* VEO3 角色定义服务类
|
||||
* 封装与 Rust 后端 veo3-scene-writer crate 的通信逻辑
|
||||
|
|
@ -154,6 +173,32 @@ export class Veo3ActorDefineService {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建角色文件 - 从最近一次对话中提取JSON并保存
|
||||
*/
|
||||
public async createActorFile(
|
||||
filePath: string,
|
||||
fileName?: string
|
||||
): Promise<CreateActorFileResponse> {
|
||||
try {
|
||||
const response = await invoke<CreateActorFileResponse>('veo3_actor_define_create_actor_file', {
|
||||
request: {
|
||||
session_id: this.sessionId,
|
||||
file_path: filePath,
|
||||
file_name: fileName
|
||||
}
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('创建角色文件失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '未知错误'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -191,3 +236,13 @@ export const getVeo3ActorDefineConversationHistory = (): Promise<string[]> => {
|
|||
export const clearVeo3ActorDefineConversation = (): Promise<boolean> => {
|
||||
return veo3ActorDefineService.clearConversation();
|
||||
};
|
||||
|
||||
/**
|
||||
* 便捷函数:创建角色文件
|
||||
*/
|
||||
export const createVeo3ActorFile = (
|
||||
filePath: string,
|
||||
fileName?: string
|
||||
): Promise<CreateActorFileResponse> => {
|
||||
return veo3ActorDefineService.createActorFile(filePath, fileName);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue