feat: 实现AI穿搭方案智能分组功能
- 修改提示词让AI直接返回分组结构 - 添加GroupingStrategy和OutfitQualityScore数据结构 - 支持按风格、场合、季节等维度智能分组 - 为每个方案添加质量评分系统 - 前端支持分组展示和获取更多同类方案 - 保持向后兼容性 主要变更: - 后端: 更新提示词和解析逻辑支持分组JSON结构 - 前端: OutfitRecommendationList支持分组显示 - 类型: 新增分组相关TypeScript接口 - 功能: 每个分组支持'获取更多'按钮扩展方案
This commit is contained in:
parent
9a764d60dc
commit
d33f7fbc7f
|
|
@ -1,6 +1,47 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
/// 分组策略
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GroupingStrategy {
|
||||
/// 主要分组维度
|
||||
pub primary_dimension: String,
|
||||
/// 分组原因说明
|
||||
pub reasoning: String,
|
||||
}
|
||||
|
||||
/// 方案质量评分
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OutfitQualityScore {
|
||||
/// AI对该方案的信心度 (0-1)
|
||||
pub ai_confidence_score: f32,
|
||||
/// TikTok趋势匹配度 (0-1)
|
||||
pub trend_score: f32,
|
||||
/// 搭配versatility评分 (0-1)
|
||||
pub versatility_score: f32,
|
||||
/// 搭配难度等级
|
||||
pub difficulty_level: String,
|
||||
/// 整体推荐度 (0-1)
|
||||
pub overall_recommendation_score: f32,
|
||||
}
|
||||
|
||||
/// 穿搭方案分组
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OutfitRecommendationGroup {
|
||||
/// 分组名称
|
||||
pub group: String,
|
||||
/// 分组描述
|
||||
pub description: String,
|
||||
/// 分组唯一标识符
|
||||
pub group_id: String,
|
||||
/// 该分组的风格关键词
|
||||
pub style_keywords: Vec<String>,
|
||||
/// 是否可以加载更多方案
|
||||
pub can_load_more: bool,
|
||||
/// 分组下的方案列表
|
||||
pub children: Vec<OutfitRecommendation>,
|
||||
}
|
||||
|
||||
/// 色彩信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ColorInfo {
|
||||
|
|
@ -77,6 +118,8 @@ pub struct OutfitRecommendation {
|
|||
pub styling_tips: Vec<String>,
|
||||
/// 创建时间
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// 方案质量评分
|
||||
pub quality_score: OutfitQualityScore,
|
||||
}
|
||||
|
||||
/// 穿搭方案生成请求
|
||||
|
|
@ -99,14 +142,21 @@ pub struct OutfitRecommendationRequest {
|
|||
/// 穿搭方案生成响应
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OutfitRecommendationResponse {
|
||||
/// 生成的穿搭方案列表
|
||||
pub recommendations: Vec<OutfitRecommendation>,
|
||||
/// 分组策略
|
||||
pub grouping_strategy: GroupingStrategy,
|
||||
/// 生成的穿搭方案分组列表
|
||||
pub groups: Vec<OutfitRecommendationGroup>,
|
||||
/// 生成时间 (毫秒)
|
||||
pub generation_time_ms: u64,
|
||||
/// 生成时间戳
|
||||
pub generated_at: DateTime<Utc>,
|
||||
/// 使用的提示词 (调试用)
|
||||
pub prompt_used: Option<String>,
|
||||
|
||||
// 保持向后兼容性
|
||||
/// 生成的穿搭方案列表 (向后兼容)
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub recommendations: Vec<OutfitRecommendation>,
|
||||
}
|
||||
|
||||
/// 场景检索请求 (用于方案详情到场景检索的集成)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,13 @@ use crate::data::models::conversation::{
|
|||
};
|
||||
use crate::data::repositories::conversation_repository::ConversationRepository;
|
||||
|
||||
// 导入穿搭推荐相关模块
|
||||
use crate::data::models::outfit_recommendation::{
|
||||
OutfitRecommendation, OutfitRecommendationRequest, OutfitRecommendationResponse,
|
||||
OutfitRecommendationGroup, GroupingStrategy, OutfitQualityScore,
|
||||
ColorInfo, OutfitItem, SceneRecommendation
|
||||
};
|
||||
|
||||
// 导入穿搭方案推荐相关模块
|
||||
use crate::data::models::outfit_recommendation::{
|
||||
OutfitRecommendation, OutfitRecommendationRequest, OutfitRecommendationResponse,
|
||||
|
|
@ -1738,17 +1745,27 @@ impl GeminiService {
|
|||
let raw_response = self.parse_gemini_response_content(&result)?;
|
||||
|
||||
// 解析穿搭方案
|
||||
match self.parse_outfit_recommendations(&raw_response, request) {
|
||||
Ok(recommendations) => {
|
||||
match self.parse_outfit_recommendation_groups(&raw_response, request) {
|
||||
Ok((grouping_strategy, groups)) => {
|
||||
let generation_time = start_time.elapsed().as_millis() as u64;
|
||||
|
||||
println!("✅ 穿搭方案生成完成,共生成 {} 个方案", recommendations.len());
|
||||
// 计算总方案数
|
||||
let total_recommendations: usize = groups.iter().map(|g| g.children.len()).sum();
|
||||
|
||||
println!("✅ 穿搭方案生成完成,共生成 {} 个分组,{} 个方案", groups.len(), total_recommendations);
|
||||
|
||||
// 为向后兼容,将所有方案平铺到recommendations字段
|
||||
let all_recommendations: Vec<OutfitRecommendation> = groups.iter()
|
||||
.flat_map(|group| group.children.clone())
|
||||
.collect();
|
||||
|
||||
return Ok(OutfitRecommendationResponse {
|
||||
recommendations,
|
||||
grouping_strategy,
|
||||
groups,
|
||||
generation_time_ms: generation_time,
|
||||
generated_at: chrono::Utc::now(),
|
||||
prompt_used: Some(prompt),
|
||||
recommendations: all_recommendations,
|
||||
});
|
||||
}
|
||||
Err(parse_error) => {
|
||||
|
|
@ -1816,74 +1833,221 @@ impl GeminiService {
|
|||
prompt.push_str(&format!("
|
||||
|
||||
## 输出要求
|
||||
请生成 {} 个不同风格的穿搭方案,以JSON格式返回,结构如下:
|
||||
请根据用户需求智能分析,将 {} 个穿搭方案按照最合适的维度进行分组(如风格、场合、季节、色彩等),以JSON格式返回分组结构:
|
||||
|
||||
```json
|
||||
{{
|
||||
\"recommendations\": [
|
||||
\"grouping_strategy\": {{
|
||||
\"primary_dimension\": \"Style|Occasion|Season|Color|Budget|Complexity\",
|
||||
\"reasoning\": \"选择此分组方式的原因说明\"
|
||||
}},
|
||||
\"groups\": [
|
||||
{{
|
||||
\"id\": \"outfit_001\",
|
||||
\"title\": \"方案标题\",
|
||||
\"description\": \"详细的穿搭描述,突出亮点和特色\",
|
||||
\"overall_style\": \"整体风格\",
|
||||
\"style_tags\": [\"风格标签1\", \"风格标签2\"],
|
||||
\"occasions\": [\"适合场合1\", \"适合场合2\"],
|
||||
\"seasons\": [\"适合季节\"],
|
||||
\"items\": [
|
||||
\"group\": \"分组名称\",
|
||||
\"description\": \"分组描述\",
|
||||
\"group_id\": \"group_001\",
|
||||
\"style_keywords\": [\"关键词1\", \"关键词2\"],
|
||||
\"can_load_more\": true,
|
||||
\"children\": [
|
||||
{{
|
||||
\"category\": \"上装\",
|
||||
\"description\": \"具体单品描述\",
|
||||
\"primary_color\": {{
|
||||
\"name\": \"颜色名称\",
|
||||
\"hex\": \"#FFFFFF\",
|
||||
\"hsv\": [0.0, 0.0, 1.0]
|
||||
\"id\": \"outfit_001\",
|
||||
\"title\": \"方案标题\",
|
||||
\"description\": \"详细的穿搭描述,突出亮点和特色\",
|
||||
\"overall_style\": \"整体风格\",
|
||||
\"style_tags\": [\"风格标签1\", \"风格标签2\"],
|
||||
\"occasions\": [\"适合场合1\", \"适合场合2\"],
|
||||
\"seasons\": [\"适合季节\"],
|
||||
\"quality_score\": {{
|
||||
\"ai_confidence_score\": 0.9,
|
||||
\"trend_score\": 0.8,
|
||||
\"versatility_score\": 0.7,
|
||||
\"difficulty_level\": \"Beginner|Intermediate|Advanced\",
|
||||
\"overall_recommendation_score\": 0.85
|
||||
}},
|
||||
\"secondary_color\": {{
|
||||
\"name\": \"次要颜色\",
|
||||
\"hex\": \"#000000\",
|
||||
\"hsv\": [0.0, 0.0, 0.0]
|
||||
}},
|
||||
\"material\": \"材质\",
|
||||
\"style_tags\": [\"单品风格标签\"]
|
||||
\"items\": [
|
||||
{{
|
||||
\"category\": \"上装\",
|
||||
\"description\": \"具体单品描述\",
|
||||
\"primary_color\": {{
|
||||
\"name\": \"颜色名称\",
|
||||
\"hex\": \"#FFFFFF\",
|
||||
\"hsv\": [0.0, 0.0, 1.0]
|
||||
}},
|
||||
\"secondary_color\": {{
|
||||
\"name\": \"次要颜色\",
|
||||
\"hex\": \"#000000\",
|
||||
\"hsv\": [0.0, 0.0, 0.0]
|
||||
}},
|
||||
\"material\": \"材质\",
|
||||
\"style_tags\": [\"单品风格标签\"]
|
||||
}}
|
||||
],
|
||||
\"color_theme\": \"色彩搭配主题\",
|
||||
\"primary_colors\": [
|
||||
{{
|
||||
\"name\": \"主色调名称\",
|
||||
\"hex\": \"#FFFFFF\",
|
||||
\"hsv\": [0.0, 0.0, 1.0]
|
||||
}}
|
||||
],
|
||||
\"scene_recommendations\": [
|
||||
{{
|
||||
\"name\": \"推荐场景名称\",
|
||||
\"description\": \"场景详细描述\",
|
||||
\"scene_type\": \"室内或室外或特殊\",
|
||||
\"time_of_day\": [\"时间段\"],
|
||||
\"lighting\": \"光线条件描述\",
|
||||
\"photography_tips\": [\"拍摄建议1\", \"拍摄建议2\"]
|
||||
}}
|
||||
],
|
||||
\"tiktok_tips\": [\"TikTok优化建议1\", \"TikTok优化建议2\"],
|
||||
\"styling_tips\": [\"搭配要点1\", \"搭配要点2\"]
|
||||
}}
|
||||
],
|
||||
\"color_theme\": \"色彩搭配主题\",
|
||||
\"primary_colors\": [
|
||||
{{
|
||||
\"name\": \"主色调名称\",
|
||||
\"hex\": \"#FFFFFF\",
|
||||
\"hsv\": [0.0, 0.0, 1.0]
|
||||
}}
|
||||
],
|
||||
\"scene_recommendations\": [
|
||||
{{
|
||||
\"name\": \"推荐场景名称\",
|
||||
\"description\": \"场景详细描述\",
|
||||
\"scene_type\": \"室内或室外或特殊\",
|
||||
\"time_of_day\": [\"时间段\"],
|
||||
\"lighting\": \"光线条件描述\",
|
||||
\"photography_tips\": [\"拍摄建议1\", \"拍摄建议2\"]
|
||||
}}
|
||||
],
|
||||
\"tiktok_tips\": [\"TikTok优化建议1\", \"TikTok优化建议2\"],
|
||||
\"styling_tips\": [\"搭配要点1\", \"搭配要点2\"]
|
||||
]
|
||||
}}
|
||||
]
|
||||
}}
|
||||
```
|
||||
|
||||
请确保:
|
||||
1. 每个方案都有独特的风格定位
|
||||
2. 色彩搭配符合当前流行趋势
|
||||
3. 场景推荐具有强烈的视觉冲击力
|
||||
4. TikTok优化建议实用且具体
|
||||
5. 返回的是有效的JSON格式
|
||||
1. **智能分组**: 根据用户查询内容选择最合适的分组维度(风格/场合/季节/色彩等)
|
||||
2. **分组均衡**: 每个分组包含2-4个方案,分组数量控制在2-4个
|
||||
3. **质量评分**: 为每个方案提供准确的质量评分(0-1范围)
|
||||
4. **风格差异**: 同组内方案风格相近,不同组间有明显差异
|
||||
5. **关键词提取**: 为每个分组提供3-5个核心风格关键词
|
||||
6. **可扩展性**: 每个分组都支持后续获取更多同类方案
|
||||
7. **TikTok优化**: 所有建议都要考虑短视频平台特性
|
||||
8. **JSON格式**: 返回严格有效的JSON格式
|
||||
", request.count));
|
||||
|
||||
prompt
|
||||
}
|
||||
|
||||
/// 解析穿搭方案推荐响应
|
||||
/// 解析穿搭方案分组响应
|
||||
fn parse_outfit_recommendation_groups(&self, raw_response: &str, _request: &OutfitRecommendationRequest) -> Result<(GroupingStrategy, Vec<OutfitRecommendationGroup>)> {
|
||||
println!("🔍 开始解析穿搭方案分组响应...");
|
||||
println!("📄 原始响应: {}", raw_response);
|
||||
|
||||
// 尝试提取JSON部分
|
||||
let json_str = self.extract_json_from_response(raw_response)?;
|
||||
|
||||
// 使用容错JSON解析器
|
||||
let config = ParserConfig {
|
||||
max_text_length: 1024 * 1024,
|
||||
enable_comments: true,
|
||||
enable_unquoted_keys: true,
|
||||
enable_trailing_commas: true,
|
||||
timeout_ms: 30000,
|
||||
recovery_strategies: vec![
|
||||
RecoveryStrategy::StandardJson,
|
||||
RecoveryStrategy::ManualFix,
|
||||
RecoveryStrategy::RegexExtract,
|
||||
RecoveryStrategy::PartialParse,
|
||||
],
|
||||
};
|
||||
|
||||
let mut parser = TolerantJsonParser::new(Some(config))?;
|
||||
|
||||
match parser.parse(&json_str) {
|
||||
Ok((parsed, _stats)) => {
|
||||
// 解析分组策略
|
||||
let grouping_strategy = if let Some(strategy_obj) = parsed.get("grouping_strategy") {
|
||||
GroupingStrategy {
|
||||
primary_dimension: strategy_obj.get("primary_dimension")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("Style")
|
||||
.to_string(),
|
||||
reasoning: strategy_obj.get("reasoning")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("基于用户查询自动选择分组策略")
|
||||
.to_string(),
|
||||
}
|
||||
} else {
|
||||
GroupingStrategy {
|
||||
primary_dimension: "Style".to_string(),
|
||||
reasoning: "默认按风格分组".to_string(),
|
||||
}
|
||||
};
|
||||
|
||||
// 解析分组
|
||||
let mut groups = Vec::new();
|
||||
if let Some(groups_array) = parsed.get("groups").and_then(|v| v.as_array()) {
|
||||
for (group_index, group_value) in groups_array.iter().enumerate() {
|
||||
match self.parse_single_outfit_group(group_value, group_index) {
|
||||
Ok(group) => groups.push(group),
|
||||
Err(e) => {
|
||||
println!("⚠️ 解析第{}个分组失败: {}", group_index + 1, e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if groups.is_empty() {
|
||||
return Err(anyhow!("未能解析出任何有效的穿搭方案分组"));
|
||||
}
|
||||
|
||||
println!("✅ 成功解析 {} 个分组", groups.len());
|
||||
Ok((grouping_strategy, groups))
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ JSON解析失败: {}", e);
|
||||
Err(anyhow!("JSON解析失败: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析单个穿搭方案分组
|
||||
fn parse_single_outfit_group(&self, value: &serde_json::Value, index: usize) -> Result<OutfitRecommendationGroup> {
|
||||
let group = value.get("group")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or(&format!("分组{}", index + 1))
|
||||
.to_string();
|
||||
|
||||
let description = value.get("description")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("精选穿搭方案")
|
||||
.to_string();
|
||||
|
||||
let group_id = value.get("group_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or(&format!("group_{:03}", index + 1))
|
||||
.to_string();
|
||||
|
||||
let style_keywords = value.get("style_keywords")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
let can_load_more = value.get("can_load_more")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(true);
|
||||
|
||||
// 解析子方案
|
||||
let mut children = Vec::new();
|
||||
if let Some(children_array) = value.get("children").and_then(|v| v.as_array()) {
|
||||
for (child_index, child_value) in children_array.iter().enumerate() {
|
||||
match self.parse_single_outfit_recommendation(child_value, child_index) {
|
||||
Ok(recommendation) => children.push(recommendation),
|
||||
Err(e) => {
|
||||
println!("⚠️ 解析分组{}中第{}个方案失败: {}", group, child_index + 1, e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(OutfitRecommendationGroup {
|
||||
group,
|
||||
description,
|
||||
group_id,
|
||||
style_keywords,
|
||||
can_load_more,
|
||||
children,
|
||||
})
|
||||
}
|
||||
|
||||
/// 解析穿搭方案推荐响应 (向后兼容)
|
||||
fn parse_outfit_recommendations(&self, raw_response: &str, request: &OutfitRecommendationRequest) -> Result<Vec<OutfitRecommendation>> {
|
||||
println!("🔍 开始解析穿搭方案响应...");
|
||||
println!("📄 原始响应: {}", raw_response);
|
||||
|
|
@ -2022,6 +2186,37 @@ impl GeminiService {
|
|||
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
// 解析质量评分
|
||||
let quality_score = if let Some(score_obj) = value.get("quality_score") {
|
||||
OutfitQualityScore {
|
||||
ai_confidence_score: score_obj.get("ai_confidence_score")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.8) as f32,
|
||||
trend_score: score_obj.get("trend_score")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.7) as f32,
|
||||
versatility_score: score_obj.get("versatility_score")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.6) as f32,
|
||||
difficulty_level: score_obj.get("difficulty_level")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("Intermediate")
|
||||
.to_string(),
|
||||
overall_recommendation_score: score_obj.get("overall_recommendation_score")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.7) as f32,
|
||||
}
|
||||
} else {
|
||||
// 默认质量评分
|
||||
OutfitQualityScore {
|
||||
ai_confidence_score: 0.8,
|
||||
trend_score: 0.7,
|
||||
versatility_score: 0.6,
|
||||
difficulty_level: "Intermediate".to_string(),
|
||||
overall_recommendation_score: 0.7,
|
||||
}
|
||||
};
|
||||
|
||||
Ok(OutfitRecommendation {
|
||||
id,
|
||||
title,
|
||||
|
|
@ -2037,6 +2232,7 @@ impl GeminiService {
|
|||
tiktok_tips,
|
||||
styling_tips,
|
||||
created_at: chrono::Utc::now(),
|
||||
quality_score,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import { EmptyState } from '../EmptyState';
|
|||
* 遵循设计系统规范,提供统一的列表展示界面
|
||||
*/
|
||||
export const OutfitRecommendationList: React.FC<OutfitRecommendationListProps> = ({
|
||||
groups,
|
||||
groupingStrategy,
|
||||
recommendations,
|
||||
isLoading = false,
|
||||
error,
|
||||
|
|
@ -22,6 +24,7 @@ export const OutfitRecommendationList: React.FC<OutfitRecommendationListProps> =
|
|||
onSceneSearch,
|
||||
onMaterialSearch,
|
||||
onRegenerate,
|
||||
onLoadMoreForGroup,
|
||||
className = '',
|
||||
}) => {
|
||||
// 加载状态
|
||||
|
|
@ -131,6 +134,13 @@ export const OutfitRecommendationList: React.FC<OutfitRecommendationListProps> =
|
|||
);
|
||||
}
|
||||
|
||||
// 确定显示模式:分组模式 vs 传统模式
|
||||
const hasGroups = groups && groups.length > 0;
|
||||
const displayRecommendations = recommendations || [];
|
||||
const totalRecommendations = hasGroups
|
||||
? groups.reduce((sum, group) => sum + group.children.length, 0)
|
||||
: displayRecommendations.length;
|
||||
|
||||
// 正常状态 - 显示推荐列表
|
||||
return (
|
||||
<div className={`space-y-6 ${className}`}>
|
||||
|
|
@ -145,8 +155,16 @@ export const OutfitRecommendationList: React.FC<OutfitRecommendationListProps> =
|
|||
AI穿搭推荐
|
||||
</h3>
|
||||
<p className="text-sm text-medium-emphasis">
|
||||
共为您生成 {recommendations.length} 个个性化穿搭方案
|
||||
{hasGroups
|
||||
? `共为您生成 ${groups.length} 个分组,${totalRecommendations} 个个性化穿搭方案`
|
||||
: `共为您生成 ${totalRecommendations} 个个性化穿搭方案`
|
||||
}
|
||||
</p>
|
||||
{groupingStrategy && (
|
||||
<p className="text-xs text-low-emphasis mt-1">
|
||||
{groupingStrategy.reasoning}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -161,21 +179,81 @@ export const OutfitRecommendationList: React.FC<OutfitRecommendationListProps> =
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* 推荐卡片网格 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{recommendations.map((recommendation, index) => (
|
||||
<OutfitRecommendationCard
|
||||
key={recommendation.id || index}
|
||||
recommendation={recommendation}
|
||||
onSelect={onRecommendationSelect}
|
||||
onSceneSearch={onSceneSearch}
|
||||
onMaterialSearch={onMaterialSearch}
|
||||
showDetails={true}
|
||||
compact={false}
|
||||
className="animate-fade-in-up"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* 分组模式显示 */}
|
||||
{hasGroups ? (
|
||||
<div className="space-y-8">
|
||||
{groups.map((group, groupIndex) => (
|
||||
<div key={group.group_id || groupIndex} className="space-y-4">
|
||||
{/* 分组标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold text-high-emphasis">
|
||||
{group.group}
|
||||
</h4>
|
||||
<p className="text-sm text-medium-emphasis">
|
||||
{group.description}
|
||||
</p>
|
||||
{group.style_keywords.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{group.style_keywords.map((keyword, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 text-xs bg-primary-50 text-primary-600 rounded-full"
|
||||
>
|
||||
{keyword}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 获取更多按钮 */}
|
||||
{group.can_load_more && onLoadMoreForGroup && (
|
||||
<button
|
||||
onClick={() => onLoadMoreForGroup(group.group_id, group.style_keywords)}
|
||||
className="flex items-center gap-2 px-3 py-2 text-sm text-primary-600 hover:text-primary-700 hover:bg-primary-50 rounded-lg transition-all duration-200"
|
||||
>
|
||||
<ShoppingBag className="w-4 h-4" />
|
||||
<span>获取更多</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 分组内的方案卡片 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{group.children.map((recommendation, index) => (
|
||||
<OutfitRecommendationCard
|
||||
key={recommendation.id || `${groupIndex}-${index}`}
|
||||
recommendation={recommendation}
|
||||
onSelect={onRecommendationSelect}
|
||||
onSceneSearch={onSceneSearch}
|
||||
onMaterialSearch={onMaterialSearch}
|
||||
showDetails={true}
|
||||
compact={false}
|
||||
className="animate-fade-in-up"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
/* 传统模式显示 */
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{displayRecommendations.map((recommendation, index) => (
|
||||
<OutfitRecommendationCard
|
||||
key={recommendation.id || index}
|
||||
recommendation={recommendation}
|
||||
onSelect={onRecommendationSelect}
|
||||
onSceneSearch={onSceneSearch}
|
||||
onMaterialSearch={onMaterialSearch}
|
||||
showDetails={true}
|
||||
compact={false}
|
||||
className="animate-fade-in-up"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 底部提示 */}
|
||||
<div className="card p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200">
|
||||
|
|
|
|||
|
|
@ -10,7 +10,14 @@ import {
|
|||
Info
|
||||
} from 'lucide-react';
|
||||
import OutfitRecommendationService from '../../services/outfitRecommendationService';
|
||||
import { OutfitRecommendation, STYLE_OPTIONS, OCCASION_OPTIONS, SEASON_OPTIONS } from '../../types/outfitRecommendation';
|
||||
import {
|
||||
OutfitRecommendation,
|
||||
OutfitRecommendationGroup,
|
||||
GroupingStrategy,
|
||||
STYLE_OPTIONS,
|
||||
OCCASION_OPTIONS,
|
||||
SEASON_OPTIONS
|
||||
} from '../../types/outfitRecommendation';
|
||||
import OutfitRecommendationList from '../../components/outfit/OutfitRecommendationList';
|
||||
import { CustomSelect } from '../../components/CustomSelect';
|
||||
import { MaterialSearchPanel, MaterialDetailModal } from '../../components/material';
|
||||
|
|
@ -28,6 +35,9 @@ const OutfitRecommendationTool: React.FC = () => {
|
|||
const [colorPreferences, setColorPreferences] = useState<string[]>([]);
|
||||
const [count, setCount] = useState(3);
|
||||
|
||||
// 分组相关状态
|
||||
const [groups, setGroups] = useState<OutfitRecommendationGroup[]>([]);
|
||||
const [groupingStrategy, setGroupingStrategy] = useState<GroupingStrategy | null>(null);
|
||||
const [recommendations, setRecommendations] = useState<OutfitRecommendation[]>([]);
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
@ -61,7 +71,18 @@ const OutfitRecommendationTool: React.FC = () => {
|
|||
count,
|
||||
});
|
||||
|
||||
setRecommendations(response.recommendations);
|
||||
// 更新分组和方案数据
|
||||
if (response.groups && response.groups.length > 0) {
|
||||
setGroups(response.groups);
|
||||
setGroupingStrategy(response.grouping_strategy);
|
||||
// 为向后兼容,也设置recommendations
|
||||
setRecommendations(response.recommendations || []);
|
||||
} else {
|
||||
// 向后兼容模式
|
||||
setGroups([]);
|
||||
setGroupingStrategy(null);
|
||||
setRecommendations(response.recommendations || []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('穿搭方案生成失败:', err);
|
||||
setError(err instanceof Error ? err.message : '穿搭方案生成失败');
|
||||
|
|
@ -75,6 +96,65 @@ const OutfitRecommendationTool: React.FC = () => {
|
|||
handleGenerate();
|
||||
}, [handleGenerate]);
|
||||
|
||||
// 获取更多同类方案
|
||||
const handleLoadMoreForGroup = useCallback(async (groupId: string, styleKeywords: string[]) => {
|
||||
try {
|
||||
console.log(`🎨 获取分组 ${groupId} 的更多方案,关键词:`, styleKeywords);
|
||||
|
||||
// 构建增强的查询
|
||||
const enhancedQuery = `${query} ${styleKeywords.join(' ')}`;
|
||||
|
||||
const response = await OutfitRecommendationService.generateRecommendations({
|
||||
query: enhancedQuery,
|
||||
target_style: targetStyle || undefined,
|
||||
occasions: occasions.length > 0 ? occasions : undefined,
|
||||
season: season || undefined,
|
||||
color_preferences: colorPreferences.length > 0 ? colorPreferences : undefined,
|
||||
count: 3, // 每次获取3个新方案
|
||||
});
|
||||
|
||||
// 找到对应的分组并添加新方案
|
||||
if (response.groups && response.groups.length > 0) {
|
||||
// 如果返回了分组,合并到现有分组中
|
||||
setGroups(prevGroups => {
|
||||
return prevGroups.map(group => {
|
||||
if (group.group_id === groupId) {
|
||||
// 找到匹配的新分组并合并方案
|
||||
const newGroup = response.groups.find(g =>
|
||||
g.style_keywords.some(keyword =>
|
||||
styleKeywords.includes(keyword)
|
||||
)
|
||||
);
|
||||
if (newGroup) {
|
||||
return {
|
||||
...group,
|
||||
children: [...group.children, ...newGroup.children]
|
||||
};
|
||||
}
|
||||
}
|
||||
return group;
|
||||
});
|
||||
});
|
||||
} else if (response.recommendations) {
|
||||
// 向后兼容:直接添加到对应分组
|
||||
setGroups(prevGroups => {
|
||||
return prevGroups.map(group => {
|
||||
if (group.group_id === groupId) {
|
||||
return {
|
||||
...group,
|
||||
children: [...group.children, ...response.recommendations]
|
||||
};
|
||||
}
|
||||
return group;
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取更多方案失败:', err);
|
||||
setError(err instanceof Error ? err.message : '获取更多方案失败');
|
||||
}
|
||||
}, [query, targetStyle, occasions, season, colorPreferences]);
|
||||
|
||||
// 清空表单
|
||||
const handleClear = useCallback(() => {
|
||||
setQuery('');
|
||||
|
|
@ -83,6 +163,8 @@ const OutfitRecommendationTool: React.FC = () => {
|
|||
setSeason('');
|
||||
setColorPreferences([]);
|
||||
setCount(3);
|
||||
setGroups([]);
|
||||
setGroupingStrategy(null);
|
||||
setRecommendations([]);
|
||||
setError(null);
|
||||
}, []);
|
||||
|
|
@ -362,11 +444,14 @@ const OutfitRecommendationTool: React.FC = () => {
|
|||
{/* 右侧结果区域 */}
|
||||
<div className="lg:col-span-3">
|
||||
<OutfitRecommendationList
|
||||
groups={groups}
|
||||
groupingStrategy={groupingStrategy || undefined}
|
||||
recommendations={recommendations}
|
||||
isLoading={isGenerating}
|
||||
error={error || undefined}
|
||||
onRegenerate={handleRegenerate}
|
||||
onMaterialSearch={handleMaterialSearch}
|
||||
onLoadMoreForGroup={handleLoadMoreForGroup}
|
||||
className="min-h-[600px]"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,31 @@
|
|||
* 遵循 Tauri 开发规范的类型安全设计
|
||||
*/
|
||||
|
||||
// 分组策略
|
||||
export interface GroupingStrategy {
|
||||
primary_dimension: string;
|
||||
reasoning: string;
|
||||
}
|
||||
|
||||
// 方案质量评分
|
||||
export interface OutfitQualityScore {
|
||||
ai_confidence_score: number;
|
||||
trend_score: number;
|
||||
versatility_score: number;
|
||||
difficulty_level: string;
|
||||
overall_recommendation_score: number;
|
||||
}
|
||||
|
||||
// 穿搭方案分组
|
||||
export interface OutfitRecommendationGroup {
|
||||
group: string;
|
||||
description: string;
|
||||
group_id: string;
|
||||
style_keywords: string[];
|
||||
can_load_more: boolean;
|
||||
children: OutfitRecommendation[];
|
||||
}
|
||||
|
||||
// 色彩信息
|
||||
export interface ColorInfo {
|
||||
/** 色彩名称 */
|
||||
|
|
@ -75,6 +100,8 @@ export interface OutfitRecommendation {
|
|||
styling_tips: string[];
|
||||
/** 创建时间 */
|
||||
created_at: string;
|
||||
/** 方案质量评分 */
|
||||
quality_score: OutfitQualityScore;
|
||||
}
|
||||
|
||||
// 穿搭方案生成请求
|
||||
|
|
@ -95,14 +122,20 @@ export interface OutfitRecommendationRequest {
|
|||
|
||||
// 穿搭方案生成响应
|
||||
export interface OutfitRecommendationResponse {
|
||||
/** 生成的穿搭方案列表 */
|
||||
recommendations: OutfitRecommendation[];
|
||||
/** 分组策略 */
|
||||
grouping_strategy: GroupingStrategy;
|
||||
/** 生成的穿搭方案分组列表 */
|
||||
groups: OutfitRecommendationGroup[];
|
||||
/** 生成时间 (毫秒) */
|
||||
generation_time_ms: number;
|
||||
/** 生成时间戳 */
|
||||
generated_at: string;
|
||||
/** 使用的提示词 (调试用) */
|
||||
prompt_used?: string;
|
||||
|
||||
// 保持向后兼容性
|
||||
/** 生成的穿搭方案列表 (向后兼容) */
|
||||
recommendations?: OutfitRecommendation[];
|
||||
}
|
||||
|
||||
// 场景检索请求 (用于方案详情到场景检索的集成)
|
||||
|
|
@ -151,8 +184,12 @@ export interface OutfitRecommendationCardProps {
|
|||
|
||||
// 穿搭方案列表组件属性
|
||||
export interface OutfitRecommendationListProps {
|
||||
/** 穿搭方案列表 */
|
||||
recommendations: OutfitRecommendation[];
|
||||
/** 穿搭方案分组列表 */
|
||||
groups?: OutfitRecommendationGroup[];
|
||||
/** 分组策略 */
|
||||
groupingStrategy?: GroupingStrategy;
|
||||
/** 穿搭方案列表 (向后兼容) */
|
||||
recommendations?: OutfitRecommendation[];
|
||||
/** 是否正在加载 */
|
||||
isLoading?: boolean;
|
||||
/** 错误信息 */
|
||||
|
|
@ -165,6 +202,8 @@ export interface OutfitRecommendationListProps {
|
|||
onMaterialSearch?: (recommendation: OutfitRecommendation) => void;
|
||||
/** 重新生成事件 */
|
||||
onRegenerate?: () => void;
|
||||
/** 获取更多同类方案事件 */
|
||||
onLoadMoreForGroup?: (groupId: string, styleKeywords: string[]) => void;
|
||||
/** 自定义样式类名 */
|
||||
className?: string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue