feat: 为 VEO3 场景写作工具添加保存场景文件功能
- 添加保存场景按钮,位于发送按钮右侧,使用 Save 图标 - 集成容错 JSON 解析器提取最近一次对话中的场景 JSON - 支持选择保存目录并自动生成带时间戳的文件名 - 添加成功/错误提示,提供良好的用户反馈 - 实现完整的场景文件保存流程,支持 JSON 格式输出 - 功能逻辑与角色生成页面保持一致,提供统一的用户体验
This commit is contained in:
parent
e2a1f43e85
commit
8afd39b056
|
|
@ -800,7 +800,8 @@ pub fn run() {
|
|||
commands::veo3_actor_define_commands::veo3_scene_writer_get_conversation_history,
|
||||
commands::veo3_actor_define_commands::veo3_scene_writer_clear_conversation,
|
||||
commands::veo3_actor_define_commands::veo3_scene_writer_remove_session,
|
||||
commands::veo3_actor_define_commands::veo3_scene_writer_get_active_sessions
|
||||
commands::veo3_actor_define_commands::veo3_scene_writer_get_active_sessions,
|
||||
commands::veo3_actor_define_commands::veo3_scene_writer_create_scene_file
|
||||
])
|
||||
.setup(|app| {
|
||||
// 初始化日志系统
|
||||
|
|
|
|||
|
|
@ -470,6 +470,131 @@ pub async fn veo3_scene_writer_get_active_sessions(
|
|||
Ok(sessions.keys().cloned().collect())
|
||||
}
|
||||
|
||||
/// 创建场景文件请求结构
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateSceneFileRequest {
|
||||
pub session_id: String,
|
||||
pub file_path: String,
|
||||
pub file_name: Option<String>,
|
||||
}
|
||||
|
||||
/// 创建场景文件响应结构
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CreateSceneFileResponse {
|
||||
pub success: bool,
|
||||
pub file_path: Option<String>,
|
||||
pub extracted_json: Option<Value>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// 创建场景文件 - 从最近一次对话中提取JSON并保存
|
||||
#[tauri::command]
|
||||
pub async fn veo3_scene_writer_create_scene_file(
|
||||
request: CreateSceneFileRequest,
|
||||
session_manager: State<'_, Veo3SceneWriterSessionManager>,
|
||||
) -> Result<CreateSceneFileResponse, String> {
|
||||
// 获取最近一次助手回复
|
||||
let last_assistant_message = {
|
||||
let sessions = session_manager.lock().await;
|
||||
if let Some(scene_writer) = sessions.get(&request.session_id) {
|
||||
let history = scene_writer.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(CreateSceneFileResponse {
|
||||
success: false,
|
||||
file_path: None,
|
||||
extracted_json: None,
|
||||
error: Some("未找到助手回复消息".to_string()),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 使用容错JSON解析器提取JSON
|
||||
let mut parser = match TolerantJsonParser::new(None) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return Ok(CreateSceneFileResponse {
|
||||
success: false,
|
||||
file_path: None,
|
||||
extracted_json: None,
|
||||
error: Some(format!("创建JSON解析器失败: {}", e)),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let json_data = match parser.parse(&assistant_message) {
|
||||
Ok((data, _statistics)) => data,
|
||||
Err(e) => {
|
||||
return Ok(CreateSceneFileResponse {
|
||||
success: false,
|
||||
file_path: None,
|
||||
extracted_json: None,
|
||||
error: Some(format!("JSON解析失败: {}", e)),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 生成文件名
|
||||
let file_name = request.file_name.unwrap_or_else(|| {
|
||||
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
|
||||
format!("veo3_scene_{}.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(CreateSceneFileResponse {
|
||||
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(CreateSceneFileResponse {
|
||||
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(CreateSceneFileResponse {
|
||||
success: true,
|
||||
file_path: Some(full_path.to_string_lossy().to_string()),
|
||||
extracted_json: Some(json_data),
|
||||
error: None,
|
||||
}),
|
||||
Err(e) => Ok(CreateSceneFileResponse {
|
||||
success: false,
|
||||
file_path: None,
|
||||
extracted_json: Some(json_data),
|
||||
error: Some(format!("文件保存失败: {}", e)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -12,12 +12,14 @@ import {
|
|||
Copy,
|
||||
CheckCircle,
|
||||
Clapperboard,
|
||||
FolderOpen
|
||||
FolderOpen,
|
||||
Save
|
||||
} from 'lucide-react';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import {
|
||||
veo3SceneWriterService,
|
||||
Veo3SceneWriterResponse
|
||||
import {
|
||||
veo3SceneWriterService,
|
||||
Veo3SceneWriterResponse,
|
||||
CreateSceneFileResponse
|
||||
} from '../../services/veo3SceneWriterService';
|
||||
|
||||
/**
|
||||
|
|
@ -44,6 +46,8 @@ export const Veo3SceneWriterTool: React.FC = () => {
|
|||
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
|
||||
const [serviceAvailable, setServiceAvailable] = useState<boolean | null>(null);
|
||||
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
|
||||
const [isCreatingScene, setIsCreatingScene] = useState(false);
|
||||
const [createSceneSuccess, setCreateSceneSuccess] = useState<string | null>(null);
|
||||
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -218,6 +222,41 @@ export const Veo3SceneWriterTool: React.FC = () => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
// 选择保存目录并创建场景文件
|
||||
const handleCreateSceneFile = useCallback(async () => {
|
||||
if (isCreatingScene) return;
|
||||
|
||||
try {
|
||||
// 选择保存目录
|
||||
const selectedDir = await open({
|
||||
directory: true,
|
||||
title: '选择场景文件保存目录'
|
||||
});
|
||||
|
||||
if (!selectedDir || typeof selectedDir !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCreatingScene(true);
|
||||
setError(null);
|
||||
|
||||
// 调用创建场景文件服务
|
||||
const result = await veo3SceneWriterService.createSceneFile(selectedDir);
|
||||
|
||||
if (result.success && result.file_path) {
|
||||
setCreateSceneSuccess(result.file_path);
|
||||
setTimeout(() => setCreateSceneSuccess(null), 5000);
|
||||
} else {
|
||||
setError(result.error || '创建场景文件失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建场景文件失败:', error);
|
||||
setError(error instanceof Error ? error.message : '创建场景文件失败');
|
||||
} finally {
|
||||
setIsCreatingScene(false);
|
||||
}
|
||||
}, [isCreatingScene]);
|
||||
|
||||
// 如果服务不可用,显示错误状态
|
||||
if (serviceAvailable === false) {
|
||||
return (
|
||||
|
|
@ -385,6 +424,23 @@ export const Veo3SceneWriterTool: React.FC = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 成功提示 */}
|
||||
{createSceneSuccess && (
|
||||
<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">{createSceneSuccess}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setCreateSceneSuccess(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">
|
||||
{/* 选中的文件显示 */}
|
||||
|
|
@ -455,6 +511,20 @@ export const Veo3SceneWriterTool: React.FC = () => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleCreateSceneFile}
|
||||
disabled={isCreatingScene || 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并保存为场景文件"
|
||||
>
|
||||
{isCreatingScene ? (
|
||||
<div className="animate-spin w-5 h-5 border-2 border-white border-t-transparent rounded-full"></div>
|
||||
) : (
|
||||
<Save className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleSendMessage}
|
||||
disabled={(!input.trim() && selectedFiles.length === 0) || isLoading}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,25 @@ export interface Veo3SceneWriterSession {
|
|||
last_updated: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建场景文件请求接口
|
||||
*/
|
||||
export interface CreateSceneFileRequest {
|
||||
session_id: string;
|
||||
file_path: string;
|
||||
file_name?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建场景文件响应接口
|
||||
*/
|
||||
export interface CreateSceneFileResponse {
|
||||
success: boolean;
|
||||
file_path?: string;
|
||||
extracted_json?: any;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* VEO3 场景写作服务类
|
||||
* 封装与 Rust 后端 veo3-scene-writer crate 的通信逻辑
|
||||
|
|
@ -154,6 +173,32 @@ export class Veo3SceneWriterService {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建场景文件 - 从最近一次对话中提取JSON并保存
|
||||
*/
|
||||
public async createSceneFile(
|
||||
filePath: string,
|
||||
fileName?: string
|
||||
): Promise<CreateSceneFileResponse> {
|
||||
try {
|
||||
const response = await invoke<CreateSceneFileResponse>('veo3_scene_writer_create_scene_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 getVeo3SceneWriterConversationHistory = (): Promise<string[]> => {
|
|||
export const clearVeo3SceneWriterConversation = (): Promise<boolean> => {
|
||||
return veo3SceneWriterService.clearConversation();
|
||||
};
|
||||
|
||||
/**
|
||||
* 便捷函数:创建场景文件
|
||||
*/
|
||||
export const createVeo3SceneFile = (
|
||||
filePath: string,
|
||||
fileName?: string
|
||||
): Promise<CreateSceneFileResponse> => {
|
||||
return veo3SceneWriterService.createSceneFile(filePath, fileName);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue