feat: 为 VEO3 角色定义工具添加创建角色文件功能

- 添加创建角色文件按钮,位于发送按钮右侧
- 集成 parse_json_tolerant 命令提取最近一次对话中的 JSON
- 支持选择保存目录并自动生成带时间戳的文件名
- 添加成功/错误提示,提供良好的用户反馈
- 实现完整的角色档案保存流程,支持 JSON 格式输出
- 优化用户体验,按钮状态和加载动画
This commit is contained in:
imeepos 2025-08-15 18:38:55 +08:00
parent c7268ba5b1
commit d86c1d23fb
4 changed files with 269 additions and 5 deletions

View File

@ -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| {
// 初始化日志系统

View File

@ -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::*;

View File

@ -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}

View File

@ -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);
};