feat: 为 VEO3 场景写作工具添加保存场景文件功能

- 添加保存场景按钮,位于发送按钮右侧,使用 Save 图标
- 集成容错 JSON 解析器提取最近一次对话中的场景 JSON
- 支持选择保存目录并自动生成带时间戳的文件名
- 添加成功/错误提示,提供良好的用户反馈
- 实现完整的场景文件保存流程,支持 JSON 格式输出
- 功能逻辑与角色生成页面保持一致,提供统一的用户体验
This commit is contained in:
imeepos 2025-08-18 10:22:44 +08:00
parent e2a1f43e85
commit 8afd39b056
4 changed files with 256 additions and 5 deletions

View File

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

View File

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

View File

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

View File

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