feat: 实现AI穿搭方案智能分组功能
- 修改提示词让AI直接返回分组结构 - 添加GroupingStrategy和OutfitQualityScore数据结构 - 支持按风格、场合、季节等维度智能分组 - 为每个方案添加质量评分系统 - 前端支持分组展示和获取更多同类方案 - 保持向后兼容性 主要变更: - 后端: 更新提示词和解析逻辑支持分组JSON结构 - 前端: OutfitRecommendationList支持分组显示 - 类型: 新增分组相关TypeScript接口 - 功能: 每个分组支持'获取更多'按钮扩展方案
This commit is contained in:
@@ -1,6 +1,47 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use chrono::{DateTime, Utc};
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ColorInfo {
|
pub struct ColorInfo {
|
||||||
@@ -77,6 +118,8 @@ pub struct OutfitRecommendation {
|
|||||||
pub styling_tips: Vec<String>,
|
pub styling_tips: Vec<String>,
|
||||||
/// 创建时间
|
/// 创建时间
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
|
/// 方案质量评分
|
||||||
|
pub quality_score: OutfitQualityScore,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 穿搭方案生成请求
|
/// 穿搭方案生成请求
|
||||||
@@ -99,14 +142,21 @@ pub struct OutfitRecommendationRequest {
|
|||||||
/// 穿搭方案生成响应
|
/// 穿搭方案生成响应
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct OutfitRecommendationResponse {
|
pub struct OutfitRecommendationResponse {
|
||||||
/// 生成的穿搭方案列表
|
/// 分组策略
|
||||||
pub recommendations: Vec<OutfitRecommendation>,
|
pub grouping_strategy: GroupingStrategy,
|
||||||
|
/// 生成的穿搭方案分组列表
|
||||||
|
pub groups: Vec<OutfitRecommendationGroup>,
|
||||||
/// 生成时间 (毫秒)
|
/// 生成时间 (毫秒)
|
||||||
pub generation_time_ms: u64,
|
pub generation_time_ms: u64,
|
||||||
/// 生成时间戳
|
/// 生成时间戳
|
||||||
pub generated_at: DateTime<Utc>,
|
pub generated_at: DateTime<Utc>,
|
||||||
/// 使用的提示词 (调试用)
|
/// 使用的提示词 (调试用)
|
||||||
pub prompt_used: Option<String>,
|
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::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::{
|
use crate::data::models::outfit_recommendation::{
|
||||||
OutfitRecommendation, OutfitRecommendationRequest, OutfitRecommendationResponse,
|
OutfitRecommendation, OutfitRecommendationRequest, OutfitRecommendationResponse,
|
||||||
@@ -1738,17 +1745,27 @@ impl GeminiService {
|
|||||||
let raw_response = self.parse_gemini_response_content(&result)?;
|
let raw_response = self.parse_gemini_response_content(&result)?;
|
||||||
|
|
||||||
// 解析穿搭方案
|
// 解析穿搭方案
|
||||||
match self.parse_outfit_recommendations(&raw_response, request) {
|
match self.parse_outfit_recommendation_groups(&raw_response, request) {
|
||||||
Ok(recommendations) => {
|
Ok((grouping_strategy, groups)) => {
|
||||||
let generation_time = start_time.elapsed().as_millis() as u64;
|
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 {
|
return Ok(OutfitRecommendationResponse {
|
||||||
recommendations,
|
grouping_strategy,
|
||||||
|
groups,
|
||||||
generation_time_ms: generation_time,
|
generation_time_ms: generation_time,
|
||||||
generated_at: chrono::Utc::now(),
|
generated_at: chrono::Utc::now(),
|
||||||
prompt_used: Some(prompt),
|
prompt_used: Some(prompt),
|
||||||
|
recommendations: all_recommendations,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(parse_error) => {
|
Err(parse_error) => {
|
||||||
@@ -1816,11 +1833,22 @@ impl GeminiService {
|
|||||||
prompt.push_str(&format!("
|
prompt.push_str(&format!("
|
||||||
|
|
||||||
## 输出要求
|
## 输出要求
|
||||||
请生成 {} 个不同风格的穿搭方案,以JSON格式返回,结构如下:
|
请根据用户需求智能分析,将 {} 个穿搭方案按照最合适的维度进行分组(如风格、场合、季节、色彩等),以JSON格式返回分组结构:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{{
|
{{
|
||||||
\"recommendations\": [
|
\"grouping_strategy\": {{
|
||||||
|
\"primary_dimension\": \"Style|Occasion|Season|Color|Budget|Complexity\",
|
||||||
|
\"reasoning\": \"选择此分组方式的原因说明\"
|
||||||
|
}},
|
||||||
|
\"groups\": [
|
||||||
|
{{
|
||||||
|
\"group\": \"分组名称\",
|
||||||
|
\"description\": \"分组描述\",
|
||||||
|
\"group_id\": \"group_001\",
|
||||||
|
\"style_keywords\": [\"关键词1\", \"关键词2\"],
|
||||||
|
\"can_load_more\": true,
|
||||||
|
\"children\": [
|
||||||
{{
|
{{
|
||||||
\"id\": \"outfit_001\",
|
\"id\": \"outfit_001\",
|
||||||
\"title\": \"方案标题\",
|
\"title\": \"方案标题\",
|
||||||
@@ -1829,6 +1857,13 @@ impl GeminiService {
|
|||||||
\"style_tags\": [\"风格标签1\", \"风格标签2\"],
|
\"style_tags\": [\"风格标签1\", \"风格标签2\"],
|
||||||
\"occasions\": [\"适合场合1\", \"适合场合2\"],
|
\"occasions\": [\"适合场合1\", \"适合场合2\"],
|
||||||
\"seasons\": [\"适合季节\"],
|
\"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
|
||||||
|
}},
|
||||||
\"items\": [
|
\"items\": [
|
||||||
{{
|
{{
|
||||||
\"category\": \"上装\",
|
\"category\": \"上装\",
|
||||||
@@ -1870,20 +1905,149 @@ impl GeminiService {
|
|||||||
}}
|
}}
|
||||||
]
|
]
|
||||||
}}
|
}}
|
||||||
|
]
|
||||||
|
}}
|
||||||
```
|
```
|
||||||
|
|
||||||
请确保:
|
请确保:
|
||||||
1. 每个方案都有独特的风格定位
|
1. **智能分组**: 根据用户查询内容选择最合适的分组维度(风格/场合/季节/色彩等)
|
||||||
2. 色彩搭配符合当前流行趋势
|
2. **分组均衡**: 每个分组包含2-4个方案,分组数量控制在2-4个
|
||||||
3. 场景推荐具有强烈的视觉冲击力
|
3. **质量评分**: 为每个方案提供准确的质量评分(0-1范围)
|
||||||
4. TikTok优化建议实用且具体
|
4. **风格差异**: 同组内方案风格相近,不同组间有明显差异
|
||||||
5. 返回的是有效的JSON格式
|
5. **关键词提取**: 为每个分组提供3-5个核心风格关键词
|
||||||
|
6. **可扩展性**: 每个分组都支持后续获取更多同类方案
|
||||||
|
7. **TikTok优化**: 所有建议都要考虑短视频平台特性
|
||||||
|
8. **JSON格式**: 返回严格有效的JSON格式
|
||||||
", request.count));
|
", request.count));
|
||||||
|
|
||||||
prompt
|
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>> {
|
fn parse_outfit_recommendations(&self, raw_response: &str, request: &OutfitRecommendationRequest) -> Result<Vec<OutfitRecommendation>> {
|
||||||
println!("🔍 开始解析穿搭方案响应...");
|
println!("🔍 开始解析穿搭方案响应...");
|
||||||
println!("📄 原始响应: {}", raw_response);
|
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())
|
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
|
||||||
.unwrap_or_default();
|
.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 {
|
Ok(OutfitRecommendation {
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
@@ -2037,6 +2232,7 @@ impl GeminiService {
|
|||||||
tiktok_tips,
|
tiktok_tips,
|
||||||
styling_tips,
|
styling_tips,
|
||||||
created_at: chrono::Utc::now(),
|
created_at: chrono::Utc::now(),
|
||||||
|
quality_score,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { EmptyState } from '../EmptyState';
|
|||||||
* 遵循设计系统规范,提供统一的列表展示界面
|
* 遵循设计系统规范,提供统一的列表展示界面
|
||||||
*/
|
*/
|
||||||
export const OutfitRecommendationList: React.FC<OutfitRecommendationListProps> = ({
|
export const OutfitRecommendationList: React.FC<OutfitRecommendationListProps> = ({
|
||||||
|
groups,
|
||||||
|
groupingStrategy,
|
||||||
recommendations,
|
recommendations,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
error,
|
error,
|
||||||
@@ -22,6 +24,7 @@ export const OutfitRecommendationList: React.FC<OutfitRecommendationListProps> =
|
|||||||
onSceneSearch,
|
onSceneSearch,
|
||||||
onMaterialSearch,
|
onMaterialSearch,
|
||||||
onRegenerate,
|
onRegenerate,
|
||||||
|
onLoadMoreForGroup,
|
||||||
className = '',
|
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 (
|
return (
|
||||||
<div className={`space-y-6 ${className}`}>
|
<div className={`space-y-6 ${className}`}>
|
||||||
@@ -145,8 +155,16 @@ export const OutfitRecommendationList: React.FC<OutfitRecommendationListProps> =
|
|||||||
AI穿搭推荐
|
AI穿搭推荐
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-medium-emphasis">
|
<p className="text-sm text-medium-emphasis">
|
||||||
共为您生成 {recommendations.length} 个个性化穿搭方案
|
{hasGroups
|
||||||
|
? `共为您生成 ${groups.length} 个分组,${totalRecommendations} 个个性化穿搭方案`
|
||||||
|
: `共为您生成 ${totalRecommendations} 个个性化穿搭方案`
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
|
{groupingStrategy && (
|
||||||
|
<p className="text-xs text-low-emphasis mt-1">
|
||||||
|
{groupingStrategy.reasoning}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -161,9 +179,68 @@ export const OutfitRecommendationList: React.FC<OutfitRecommendationListProps> =
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{recommendations.map((recommendation, index) => (
|
{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
|
<OutfitRecommendationCard
|
||||||
key={recommendation.id || index}
|
key={recommendation.id || index}
|
||||||
recommendation={recommendation}
|
recommendation={recommendation}
|
||||||
@@ -176,6 +253,7 @@ export const OutfitRecommendationList: React.FC<OutfitRecommendationListProps> =
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 底部提示 */}
|
{/* 底部提示 */}
|
||||||
<div className="card p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200">
|
<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
|
Info
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import OutfitRecommendationService from '../../services/outfitRecommendationService';
|
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 OutfitRecommendationList from '../../components/outfit/OutfitRecommendationList';
|
||||||
import { CustomSelect } from '../../components/CustomSelect';
|
import { CustomSelect } from '../../components/CustomSelect';
|
||||||
import { MaterialSearchPanel, MaterialDetailModal } from '../../components/material';
|
import { MaterialSearchPanel, MaterialDetailModal } from '../../components/material';
|
||||||
@@ -28,6 +35,9 @@ const OutfitRecommendationTool: React.FC = () => {
|
|||||||
const [colorPreferences, setColorPreferences] = useState<string[]>([]);
|
const [colorPreferences, setColorPreferences] = useState<string[]>([]);
|
||||||
const [count, setCount] = useState(3);
|
const [count, setCount] = useState(3);
|
||||||
|
|
||||||
|
// 分组相关状态
|
||||||
|
const [groups, setGroups] = useState<OutfitRecommendationGroup[]>([]);
|
||||||
|
const [groupingStrategy, setGroupingStrategy] = useState<GroupingStrategy | null>(null);
|
||||||
const [recommendations, setRecommendations] = useState<OutfitRecommendation[]>([]);
|
const [recommendations, setRecommendations] = useState<OutfitRecommendation[]>([]);
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -61,7 +71,18 @@ const OutfitRecommendationTool: React.FC = () => {
|
|||||||
count,
|
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) {
|
} catch (err) {
|
||||||
console.error('穿搭方案生成失败:', err);
|
console.error('穿搭方案生成失败:', err);
|
||||||
setError(err instanceof Error ? err.message : '穿搭方案生成失败');
|
setError(err instanceof Error ? err.message : '穿搭方案生成失败');
|
||||||
@@ -75,6 +96,65 @@ const OutfitRecommendationTool: React.FC = () => {
|
|||||||
handleGenerate();
|
handleGenerate();
|
||||||
}, [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(() => {
|
const handleClear = useCallback(() => {
|
||||||
setQuery('');
|
setQuery('');
|
||||||
@@ -83,6 +163,8 @@ const OutfitRecommendationTool: React.FC = () => {
|
|||||||
setSeason('');
|
setSeason('');
|
||||||
setColorPreferences([]);
|
setColorPreferences([]);
|
||||||
setCount(3);
|
setCount(3);
|
||||||
|
setGroups([]);
|
||||||
|
setGroupingStrategy(null);
|
||||||
setRecommendations([]);
|
setRecommendations([]);
|
||||||
setError(null);
|
setError(null);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -362,11 +444,14 @@ const OutfitRecommendationTool: React.FC = () => {
|
|||||||
{/* 右侧结果区域 */}
|
{/* 右侧结果区域 */}
|
||||||
<div className="lg:col-span-3">
|
<div className="lg:col-span-3">
|
||||||
<OutfitRecommendationList
|
<OutfitRecommendationList
|
||||||
|
groups={groups}
|
||||||
|
groupingStrategy={groupingStrategy || undefined}
|
||||||
recommendations={recommendations}
|
recommendations={recommendations}
|
||||||
isLoading={isGenerating}
|
isLoading={isGenerating}
|
||||||
error={error || undefined}
|
error={error || undefined}
|
||||||
onRegenerate={handleRegenerate}
|
onRegenerate={handleRegenerate}
|
||||||
onMaterialSearch={handleMaterialSearch}
|
onMaterialSearch={handleMaterialSearch}
|
||||||
|
onLoadMoreForGroup={handleLoadMoreForGroup}
|
||||||
className="min-h-[600px]"
|
className="min-h-[600px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,31 @@
|
|||||||
* 遵循 Tauri 开发规范的类型安全设计
|
* 遵循 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 {
|
export interface ColorInfo {
|
||||||
/** 色彩名称 */
|
/** 色彩名称 */
|
||||||
@@ -75,6 +100,8 @@ export interface OutfitRecommendation {
|
|||||||
styling_tips: string[];
|
styling_tips: string[];
|
||||||
/** 创建时间 */
|
/** 创建时间 */
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
/** 方案质量评分 */
|
||||||
|
quality_score: OutfitQualityScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 穿搭方案生成请求
|
// 穿搭方案生成请求
|
||||||
@@ -95,14 +122,20 @@ export interface OutfitRecommendationRequest {
|
|||||||
|
|
||||||
// 穿搭方案生成响应
|
// 穿搭方案生成响应
|
||||||
export interface OutfitRecommendationResponse {
|
export interface OutfitRecommendationResponse {
|
||||||
/** 生成的穿搭方案列表 */
|
/** 分组策略 */
|
||||||
recommendations: OutfitRecommendation[];
|
grouping_strategy: GroupingStrategy;
|
||||||
|
/** 生成的穿搭方案分组列表 */
|
||||||
|
groups: OutfitRecommendationGroup[];
|
||||||
/** 生成时间 (毫秒) */
|
/** 生成时间 (毫秒) */
|
||||||
generation_time_ms: number;
|
generation_time_ms: number;
|
||||||
/** 生成时间戳 */
|
/** 生成时间戳 */
|
||||||
generated_at: string;
|
generated_at: string;
|
||||||
/** 使用的提示词 (调试用) */
|
/** 使用的提示词 (调试用) */
|
||||||
prompt_used?: string;
|
prompt_used?: string;
|
||||||
|
|
||||||
|
// 保持向后兼容性
|
||||||
|
/** 生成的穿搭方案列表 (向后兼容) */
|
||||||
|
recommendations?: OutfitRecommendation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 场景检索请求 (用于方案详情到场景检索的集成)
|
// 场景检索请求 (用于方案详情到场景检索的集成)
|
||||||
@@ -151,8 +184,12 @@ export interface OutfitRecommendationCardProps {
|
|||||||
|
|
||||||
// 穿搭方案列表组件属性
|
// 穿搭方案列表组件属性
|
||||||
export interface OutfitRecommendationListProps {
|
export interface OutfitRecommendationListProps {
|
||||||
/** 穿搭方案列表 */
|
/** 穿搭方案分组列表 */
|
||||||
recommendations: OutfitRecommendation[];
|
groups?: OutfitRecommendationGroup[];
|
||||||
|
/** 分组策略 */
|
||||||
|
groupingStrategy?: GroupingStrategy;
|
||||||
|
/** 穿搭方案列表 (向后兼容) */
|
||||||
|
recommendations?: OutfitRecommendation[];
|
||||||
/** 是否正在加载 */
|
/** 是否正在加载 */
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
/** 错误信息 */
|
/** 错误信息 */
|
||||||
@@ -165,6 +202,8 @@ export interface OutfitRecommendationListProps {
|
|||||||
onMaterialSearch?: (recommendation: OutfitRecommendation) => void;
|
onMaterialSearch?: (recommendation: OutfitRecommendation) => void;
|
||||||
/** 重新生成事件 */
|
/** 重新生成事件 */
|
||||||
onRegenerate?: () => void;
|
onRegenerate?: () => void;
|
||||||
|
/** 获取更多同类方案事件 */
|
||||||
|
onLoadMoreForGroup?: (groupId: string, styleKeywords: string[]) => void;
|
||||||
/** 自定义样式类名 */
|
/** 自定义样式类名 */
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user