feat: 改进标签系统 — 新增品类/费用/强度/预约标签,timeSlot 参与选活动
- IdeaCategory 扩展 7→9,新增 experience(体验)和 nature(自然) - 替换 outdoor boolean 为 costLevel/intensity/needsBooking 三个高价值字段 - AI 标注 prompt 同步更新,行程规划新增强度交替、费用平衡、预约提醒原则 - selectIdeasForSlots 重写为四优先级:timeSlot+category > category > timeSlot > 任意 - 前端想法卡片展示费用/强度/预约标签
This commit is contained in:
@@ -101,7 +101,9 @@ model BlindBoxIdea {
|
|||||||
category String?
|
category String?
|
||||||
timeSlot String?
|
timeSlot String?
|
||||||
estimatedMinutes Int?
|
estimatedMinutes Int?
|
||||||
outdoor Boolean?
|
costLevel String?
|
||||||
|
intensity String?
|
||||||
|
needsBooking Boolean?
|
||||||
searchQuery String?
|
searchQuery String?
|
||||||
searchType String?
|
searchType String?
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,9 @@ export const TEST_BLINDBOX_IDEA = {
|
|||||||
category: "outdoor",
|
category: "outdoor",
|
||||||
timeSlot: "morning",
|
timeSlot: "morning",
|
||||||
estimatedMinutes: 120,
|
estimatedMinutes: 120,
|
||||||
outdoor: true,
|
costLevel: "free",
|
||||||
|
intensity: "active",
|
||||||
|
needsBooking: false,
|
||||||
searchQuery: "公园",
|
searchQuery: "公园",
|
||||||
searchType: "category",
|
searchType: "category",
|
||||||
drawnById: null,
|
drawnById: null,
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ vi.mock("@/lib/ai", () => ({
|
|||||||
category: "outdoor",
|
category: "outdoor",
|
||||||
timeSlot: "morning",
|
timeSlot: "morning",
|
||||||
estimatedMinutes: 120,
|
estimatedMinutes: 120,
|
||||||
outdoor: true,
|
costLevel: "free",
|
||||||
|
intensity: "active",
|
||||||
|
needsBooking: false,
|
||||||
searchQuery: "公园",
|
searchQuery: "公园",
|
||||||
searchType: "category",
|
searchType: "category",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ export const POST = apiHandler(async (req) => {
|
|||||||
category: tags.category,
|
category: tags.category,
|
||||||
timeSlot: tags.timeSlot,
|
timeSlot: tags.timeSlot,
|
||||||
estimatedMinutes: tags.estimatedMinutes,
|
estimatedMinutes: tags.estimatedMinutes,
|
||||||
outdoor: tags.outdoor,
|
costLevel: tags.costLevel,
|
||||||
|
intensity: tags.intensity,
|
||||||
|
needsBooking: tags.needsBooking,
|
||||||
searchQuery: tags.searchQuery,
|
searchQuery: tags.searchQuery,
|
||||||
searchType: tags.searchType,
|
searchType: tags.searchType,
|
||||||
},
|
},
|
||||||
@@ -65,7 +67,9 @@ export const GET = apiHandler(async (req) => {
|
|||||||
category: true,
|
category: true,
|
||||||
timeSlot: true,
|
timeSlot: true,
|
||||||
estimatedMinutes: true,
|
estimatedMinutes: true,
|
||||||
outdoor: true,
|
costLevel: true,
|
||||||
|
intensity: true,
|
||||||
|
needsBooking: true,
|
||||||
searchQuery: true,
|
searchQuery: true,
|
||||||
searchType: true,
|
searchType: true,
|
||||||
},
|
},
|
||||||
@@ -109,7 +113,9 @@ export const PUT = apiHandler(async (req) => {
|
|||||||
category: tags.category,
|
category: tags.category,
|
||||||
timeSlot: tags.timeSlot,
|
timeSlot: tags.timeSlot,
|
||||||
estimatedMinutes: tags.estimatedMinutes,
|
estimatedMinutes: tags.estimatedMinutes,
|
||||||
outdoor: tags.outdoor,
|
costLevel: tags.costLevel,
|
||||||
|
intensity: tags.intensity,
|
||||||
|
needsBooking: tags.needsBooking,
|
||||||
searchQuery: tags.searchQuery,
|
searchQuery: tags.searchQuery,
|
||||||
searchType: tags.searchType,
|
searchType: tags.searchType,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -448,7 +448,9 @@ export default function BlindboxRoomPage() {
|
|||||||
category: data.tags.category,
|
category: data.tags.category,
|
||||||
timeSlot: data.tags.timeSlot,
|
timeSlot: data.tags.timeSlot,
|
||||||
estimatedMinutes: data.tags.estimatedMinutes,
|
estimatedMinutes: data.tags.estimatedMinutes,
|
||||||
outdoor: data.tags.outdoor,
|
costLevel: data.tags.costLevel,
|
||||||
|
intensity: data.tags.intensity,
|
||||||
|
needsBooking: data.tags.needsBooking,
|
||||||
searchQuery: data.tags.searchQuery,
|
searchQuery: data.tags.searchQuery,
|
||||||
searchType: data.tags.searchType,
|
searchType: data.tags.searchType,
|
||||||
},
|
},
|
||||||
@@ -490,7 +492,9 @@ export default function BlindboxRoomPage() {
|
|||||||
category: data.tags.category,
|
category: data.tags.category,
|
||||||
timeSlot: data.tags.timeSlot,
|
timeSlot: data.tags.timeSlot,
|
||||||
estimatedMinutes: data.tags.estimatedMinutes,
|
estimatedMinutes: data.tags.estimatedMinutes,
|
||||||
outdoor: data.tags.outdoor,
|
costLevel: data.tags.costLevel,
|
||||||
|
intensity: data.tags.intensity,
|
||||||
|
needsBooking: data.tags.needsBooking,
|
||||||
searchQuery: data.tags.searchQuery,
|
searchQuery: data.tags.searchQuery,
|
||||||
searchType: data.tags.searchType,
|
searchType: data.tags.searchType,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
Dumbbell,
|
Dumbbell,
|
||||||
Landmark,
|
Landmark,
|
||||||
Coffee,
|
Coffee,
|
||||||
|
Palette,
|
||||||
|
Mountain,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { IdeaCategory } from "@/types";
|
import type { IdeaCategory } from "@/types";
|
||||||
|
|
||||||
@@ -26,7 +28,9 @@ export interface MyIdea {
|
|||||||
category?: string | null;
|
category?: string | null;
|
||||||
timeSlot?: string | null;
|
timeSlot?: string | null;
|
||||||
estimatedMinutes?: number | null;
|
estimatedMinutes?: number | null;
|
||||||
outdoor?: boolean | null;
|
costLevel?: string | null;
|
||||||
|
intensity?: string | null;
|
||||||
|
needsBooking?: boolean | null;
|
||||||
searchQuery?: string | null;
|
searchQuery?: string | null;
|
||||||
searchType?: string | null;
|
searchType?: string | null;
|
||||||
}
|
}
|
||||||
@@ -42,6 +46,8 @@ const CATEGORY_CONFIG: Record<
|
|||||||
sports: { icon: Dumbbell, color: "text-amber-400", label: "运动" },
|
sports: { icon: Dumbbell, color: "text-amber-400", label: "运动" },
|
||||||
culture: { icon: Landmark, color: "text-violet-400", label: "文化" },
|
culture: { icon: Landmark, color: "text-violet-400", label: "文化" },
|
||||||
relaxation: { icon: Coffee, color: "text-teal-400", label: "休闲" },
|
relaxation: { icon: Coffee, color: "text-teal-400", label: "休闲" },
|
||||||
|
experience: { icon: Palette, color: "text-rose-400", label: "体验" },
|
||||||
|
nature: { icon: Mountain, color: "text-lime-400", label: "自然" },
|
||||||
};
|
};
|
||||||
|
|
||||||
function CategoryBadge({ category }: { category?: string | null }) {
|
function CategoryBadge({ category }: { category?: string | null }) {
|
||||||
@@ -49,7 +55,7 @@ function CategoryBadge({ category }: { category?: string | null }) {
|
|||||||
const cfg = CATEGORY_CONFIG[category as IdeaCategory];
|
const cfg = CATEGORY_CONFIG[category as IdeaCategory];
|
||||||
if (!cfg) return <span className="text-sm">💡</span>;
|
if (!cfg) return <span className="text-sm">💡</span>;
|
||||||
const Icon = cfg.icon;
|
const Icon = cfg.icon;
|
||||||
return <Icon size={14} className={`shrink-0 ${cfg.color}`} />;
|
return <Icon size={14} className={`mt-0.5 shrink-0 ${cfg.color}`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DurationLabel({ minutes }: { minutes?: number | null }) {
|
function DurationLabel({ minutes }: { minutes?: number | null }) {
|
||||||
@@ -62,6 +68,39 @@ function DurationLabel({ minutes }: { minutes?: number | null }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const COST_LABELS: Record<string, string> = {
|
||||||
|
free: "免费",
|
||||||
|
budget: "实惠",
|
||||||
|
moderate: "适中",
|
||||||
|
premium: "高端",
|
||||||
|
};
|
||||||
|
|
||||||
|
const INTENSITY_LABELS: Record<string, string> = {
|
||||||
|
chill: "轻松",
|
||||||
|
moderate: "适度",
|
||||||
|
active: "活跃",
|
||||||
|
};
|
||||||
|
|
||||||
|
function TagPills({ idea }: { idea: MyIdea }) {
|
||||||
|
const pills: string[] = [];
|
||||||
|
if (idea.costLevel && COST_LABELS[idea.costLevel]) pills.push(COST_LABELS[idea.costLevel]);
|
||||||
|
if (idea.intensity && INTENSITY_LABELS[idea.intensity]) pills.push(INTENSITY_LABELS[idea.intensity]);
|
||||||
|
if (idea.needsBooking) pills.push("需预约");
|
||||||
|
|
||||||
|
if (pills.length === 0 && !idea.estimatedMinutes) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-1 flex flex-wrap items-center gap-1">
|
||||||
|
{pills.map((label) => (
|
||||||
|
<span key={label} className="rounded-md bg-elevated px-1.5 py-0.5 text-[10px] font-medium text-dim">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
<DurationLabel minutes={idea.estimatedMinutes} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function MyIdeaItem({
|
function MyIdeaItem({
|
||||||
idea,
|
idea,
|
||||||
onEdit,
|
onEdit,
|
||||||
@@ -86,7 +125,7 @@ function MyIdeaItem({
|
|||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
layout
|
layout
|
||||||
className="flex items-center gap-2 rounded-xl bg-surface/60 px-3 py-2.5 ring-1 ring-border/80"
|
className="flex items-start gap-2 rounded-xl bg-surface/60 px-3 py-2.5 ring-1 ring-border/80"
|
||||||
initial={{ opacity: 0, y: -8 }}
|
initial={{ opacity: 0, y: -8 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, x: -20 }}
|
exit={{ opacity: 0, x: -20 }}
|
||||||
@@ -119,8 +158,10 @@ function MyIdeaItem({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<CategoryBadge category={idea.category} />
|
<CategoryBadge category={idea.category} />
|
||||||
<p className="min-w-0 flex-1 truncate text-sm text-secondary">{idea.content}</p>
|
<div className="min-w-0 flex-1">
|
||||||
<DurationLabel minutes={idea.estimatedMinutes} />
|
<p className="truncate text-sm text-secondary">{idea.content}</p>
|
||||||
|
<TagPills idea={idea} />
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setEditing(true)}
|
onClick={() => setEditing(true)}
|
||||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-muted transition-colors active:bg-elevated active:text-purple-400"
|
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-muted transition-colors active:bg-elevated active:text-purple-400"
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ interface BlindboxPlanProps {
|
|||||||
function guessCategory(activity: string): string | null {
|
function guessCategory(activity: string): string | null {
|
||||||
const lower = activity.toLowerCase();
|
const lower = activity.toLowerCase();
|
||||||
if (/吃|餐|饭|火锅|烧烤|面|菜|厨|食/.test(lower)) return "dining";
|
if (/吃|餐|饭|火锅|烧烤|面|菜|厨|食/.test(lower)) return "dining";
|
||||||
if (/公园|山|湖|海|户外|骑|徒步|露营/.test(lower)) return "outdoor";
|
if (/手作|工坊|烘焙|插花|陶艺|DIY|体验/.test(lower)) return "experience";
|
||||||
|
if (/露营|徒步|赶海|农场|自然|野|营地/.test(lower)) return "nature";
|
||||||
|
if (/公园|山|湖|海|户外|骑/.test(lower)) return "outdoor";
|
||||||
if (/电影|KTV|密室|游戏|桌游|剧/.test(lower)) return "entertainment";
|
if (/电影|KTV|密室|游戏|桌游|剧/.test(lower)) return "entertainment";
|
||||||
if (/逛街|购物|商场|买/.test(lower)) return "shopping";
|
if (/逛街|购物|商场|买/.test(lower)) return "shopping";
|
||||||
if (/运动|健身|球|跑|游泳|瑜伽/.test(lower)) return "sports";
|
if (/运动|健身|球|跑|游泳|瑜伽/.test(lower)) return "sports";
|
||||||
|
|||||||
+22
-6
@@ -11,11 +11,15 @@ const TAG_SYSTEM_PROMPT = `你是一个周末活动分析助手。用户会输
|
|||||||
|
|
||||||
返回字段:
|
返回字段:
|
||||||
- category: 活动品类,必须是以下之一:
|
- category: 活动品类,必须是以下之一:
|
||||||
"dining"(餐饮美食)| "outdoor"(户外活动)| "entertainment"(娱乐休闲,如电影、KTV、密室)| "shopping"(购物逛街)| "sports"(运动健身)| "culture"(文化艺术,如博物馆、展览)| "relaxation"(放松休息,如SPA、咖啡、下午茶)
|
"dining"(餐饮美食)| "outdoor"(户外活动)| "entertainment"(娱乐休闲,如电影、KTV、密室)| "shopping"(购物逛街)| "sports"(运动健身)| "culture"(文化艺术,如博物馆、展览)| "relaxation"(放松休息,如SPA、咖啡、下午茶)| "experience"(体验活动,如手作、烘焙、插花、DIY)| "nature"(自然户外,如露营、徒步、赶海)
|
||||||
- timeSlot: 最适合的时间段,必须是以下之一:
|
- timeSlot: 最适合的时间段,必须是以下之一:
|
||||||
"morning"(上午)| "afternoon"(下午)| "evening"(晚上)| "flexible"(任意时间都可以)| "all_day"(需要一整天)
|
"morning"(上午)| "afternoon"(下午)| "evening"(晚上)| "flexible"(任意时间都可以)| "all_day"(需要一整天)
|
||||||
- estimatedMinutes: 预估活动时长(整数,单位分钟)
|
- estimatedMinutes: 预估活动时长(整数,单位分钟)
|
||||||
- outdoor: 是否户外活动(布尔值)
|
- costLevel: 费用等级,必须是以下之一:
|
||||||
|
"free"(免费)| "budget"(人均<50元)| "moderate"(人均50-200元)| "premium"(人均>200元)
|
||||||
|
- intensity: 体力强度,必须是以下之一:
|
||||||
|
"chill"(轻松休闲)| "moderate"(适度活动)| "active"(需要体力)
|
||||||
|
- needsBooking: 是否需要提前预约/购票(布尔值)
|
||||||
- searchQuery: 在地图服务上搜索的关键词(品牌名、地名或品类名)
|
- searchQuery: 在地图服务上搜索的关键词(品牌名、地名或品类名)
|
||||||
- searchType: 搜索策略,必须是以下之一:
|
- searchType: 搜索策略,必须是以下之一:
|
||||||
"brand"(连锁品牌,有多个分店)| "place"(唯一地点,如某个公园)| "category"(模糊品类,搜附近匹配的)
|
"brand"(连锁品牌,有多个分店)| "place"(唯一地点,如某个公园)| "category"(模糊品类,搜附近匹配的)
|
||||||
@@ -29,6 +33,9 @@ const SCHEDULE_SYSTEM_PROMPT = `你是一个周末行程规划师。根据用户
|
|||||||
2. 尊重活动的时间偏好(公园上午、正餐在饭点、电影灵活)
|
2. 尊重活动的时间偏好(公园上午、正餐在饭点、电影灵活)
|
||||||
3. 活动之间留出合理的交通时间(15-30分钟)
|
3. 活动之间留出合理的交通时间(15-30分钟)
|
||||||
4. 如果有"category"类型的活动,选择离其他已确定地点最近的候选
|
4. 如果有"category"类型的活动,选择离其他已确定地点最近的候选
|
||||||
|
5. 高低强度活动交替安排,避免连续高体力活动
|
||||||
|
6. 费用平衡,避免连续安排 premium 级别活动
|
||||||
|
7. 需要预约(needsBooking)的活动,在 reason 中提醒用户提前预约
|
||||||
|
|
||||||
返回 JSON 格式:
|
返回 JSON 格式:
|
||||||
{
|
{
|
||||||
@@ -57,6 +64,9 @@ export interface ScheduleContext {
|
|||||||
estimatedMinutes: number;
|
estimatedMinutes: number;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
searchType: string;
|
searchType: string;
|
||||||
|
costLevel?: string;
|
||||||
|
intensity?: string;
|
||||||
|
needsBooking?: boolean;
|
||||||
}[];
|
}[];
|
||||||
candidates: Record<
|
candidates: Record<
|
||||||
string,
|
string,
|
||||||
@@ -85,16 +95,20 @@ export async function tagIdea(content: string): Promise<IdeaTags | null> {
|
|||||||
|
|
||||||
const parsed = JSON.parse(text);
|
const parsed = JSON.parse(text);
|
||||||
|
|
||||||
const validCategories = ["dining", "outdoor", "entertainment", "shopping", "sports", "culture", "relaxation"];
|
const validCategories = ["dining", "outdoor", "entertainment", "shopping", "sports", "culture", "relaxation", "experience", "nature"];
|
||||||
const validTimeSlots = ["morning", "afternoon", "evening", "flexible", "all_day"];
|
const validTimeSlots = ["morning", "afternoon", "evening", "flexible", "all_day"];
|
||||||
const validSearchTypes = ["brand", "place", "category"];
|
const validSearchTypes = ["brand", "place", "category"];
|
||||||
|
const validCostLevels = ["free", "budget", "moderate", "premium"];
|
||||||
|
const validIntensities = ["chill", "moderate", "active"];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!validCategories.includes(parsed.category) ||
|
!validCategories.includes(parsed.category) ||
|
||||||
!validTimeSlots.includes(parsed.timeSlot) ||
|
!validTimeSlots.includes(parsed.timeSlot) ||
|
||||||
!validSearchTypes.includes(parsed.searchType) ||
|
!validSearchTypes.includes(parsed.searchType) ||
|
||||||
typeof parsed.estimatedMinutes !== "number" ||
|
typeof parsed.estimatedMinutes !== "number" ||
|
||||||
typeof parsed.outdoor !== "boolean" ||
|
!validCostLevels.includes(parsed.costLevel) ||
|
||||||
|
!validIntensities.includes(parsed.intensity) ||
|
||||||
|
typeof parsed.needsBooking !== "boolean" ||
|
||||||
typeof parsed.searchQuery !== "string"
|
typeof parsed.searchQuery !== "string"
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
@@ -104,7 +118,9 @@ export async function tagIdea(content: string): Promise<IdeaTags | null> {
|
|||||||
category: parsed.category,
|
category: parsed.category,
|
||||||
timeSlot: parsed.timeSlot,
|
timeSlot: parsed.timeSlot,
|
||||||
estimatedMinutes: parsed.estimatedMinutes,
|
estimatedMinutes: parsed.estimatedMinutes,
|
||||||
outdoor: parsed.outdoor,
|
costLevel: parsed.costLevel,
|
||||||
|
intensity: parsed.intensity,
|
||||||
|
needsBooking: parsed.needsBooking,
|
||||||
searchQuery: parsed.searchQuery,
|
searchQuery: parsed.searchQuery,
|
||||||
searchType: parsed.searchType,
|
searchType: parsed.searchType,
|
||||||
};
|
};
|
||||||
@@ -162,7 +178,7 @@ export async function generateSchedule(
|
|||||||
用户出发位置:纬度 ${ctx.userLocation.lat},经度 ${ctx.userLocation.lng}
|
用户出发位置:纬度 ${ctx.userLocation.lat},经度 ${ctx.userLocation.lng}
|
||||||
|
|
||||||
活动列表:
|
活动列表:
|
||||||
${ctx.ideas.map((idea, i) => `${i + 1}. "${idea.content}"(品类:${idea.category},偏好时间:${idea.timeSlot},预估${idea.estimatedMinutes}分钟)`).join("\n")}
|
${ctx.ideas.map((idea, i) => `${i + 1}. "${idea.content}"(品类:${idea.category},偏好时间:${idea.timeSlot},预估${idea.estimatedMinutes}分钟${idea.costLevel ? `,费用:${idea.costLevel}` : ""}${idea.intensity ? `,强度:${idea.intensity}` : ""}${idea.needsBooking ? ",需预约" : ""})`).join("\n")}
|
||||||
|
|
||||||
各活动的候选地点:
|
各活动的候选地点:
|
||||||
${Object.entries(ctx.candidates)
|
${Object.entries(ctx.candidates)
|
||||||
|
|||||||
+56
-21
@@ -21,24 +21,34 @@ interface TaggedIdea {
|
|||||||
estimatedMinutes: number;
|
estimatedMinutes: number;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
searchType: string;
|
searchType: string;
|
||||||
|
costLevel: string | null;
|
||||||
|
intensity: string | null;
|
||||||
|
needsBooking: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SLOT_CATEGORY_MAP: Record<string, string[]> = {
|
const SLOT_CATEGORY_MAP: Record<string, string[]> = {
|
||||||
morning: ["outdoor", "sports", "culture"],
|
morning: ["outdoor", "nature", "sports", "culture"],
|
||||||
lunch: ["dining"],
|
lunch: ["dining"],
|
||||||
afternoon: ["entertainment", "shopping", "relaxation", "outdoor", "culture"],
|
afternoon: ["entertainment", "shopping", "relaxation", "outdoor", "culture", "experience"],
|
||||||
dinner: ["dining"],
|
dinner: ["dining"],
|
||||||
evening: ["entertainment", "relaxation"],
|
evening: ["entertainment", "relaxation", "experience"],
|
||||||
};
|
};
|
||||||
|
|
||||||
function selectIdeasForSlots(ideas: TaggedIdea[], availableHours: number): TaggedIdea[] {
|
const SLOT_TIME_MAP: Record<string, string[]> = {
|
||||||
const byCategory = new Map<string, TaggedIdea[]>();
|
morning: ["morning", "flexible"],
|
||||||
for (const idea of ideas) {
|
lunch: ["flexible"],
|
||||||
const list = byCategory.get(idea.category) || [];
|
afternoon: ["afternoon", "flexible"],
|
||||||
list.push(idea);
|
dinner: ["evening", "flexible"],
|
||||||
byCategory.set(idea.category, list);
|
evening: ["evening", "flexible"],
|
||||||
}
|
};
|
||||||
|
|
||||||
|
function matchesSlot(idea: TaggedIdea, slot: string): boolean {
|
||||||
|
if (idea.timeSlot === "all_day") return true;
|
||||||
|
const validTimes = SLOT_TIME_MAP[slot];
|
||||||
|
return validTimes ? validTimes.includes(idea.timeSlot) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectIdeasForSlots(ideas: TaggedIdea[], availableHours: number): TaggedIdea[] {
|
||||||
const slots: string[] = [];
|
const slots: string[] = [];
|
||||||
if (availableHours >= 10) {
|
if (availableHours >= 10) {
|
||||||
slots.push("morning", "lunch", "afternoon", "dinner", "evening");
|
slots.push("morning", "lunch", "afternoon", "dinner", "evening");
|
||||||
@@ -53,22 +63,41 @@ function selectIdeasForSlots(ideas: TaggedIdea[], availableHours: number): Tagge
|
|||||||
const selected: TaggedIdea[] = [];
|
const selected: TaggedIdea[] = [];
|
||||||
const usedIds = new Set<string>();
|
const usedIds = new Set<string>();
|
||||||
|
|
||||||
|
function pickRandom(pool: TaggedIdea[]): TaggedIdea | null {
|
||||||
|
if (pool.length === 0) return null;
|
||||||
|
return pool[Math.floor(Math.random() * pool.length)];
|
||||||
|
}
|
||||||
|
|
||||||
for (const slot of slots) {
|
for (const slot of slots) {
|
||||||
|
const remaining = ideas.filter((i) => !usedIds.has(i.id));
|
||||||
const preferredCategories = SLOT_CATEGORY_MAP[slot] || [];
|
const preferredCategories = SLOT_CATEGORY_MAP[slot] || [];
|
||||||
let picked: TaggedIdea | null = null;
|
|
||||||
for (const cat of preferredCategories) {
|
// Priority 1: timeSlot + category both match
|
||||||
const pool = (byCategory.get(cat) || []).filter((i) => !usedIds.has(i.id));
|
const p1 = remaining.filter(
|
||||||
if (pool.length > 0) {
|
(i) => matchesSlot(i, slot) && preferredCategories.includes(i.category),
|
||||||
picked = pool[Math.floor(Math.random() * pool.length)];
|
);
|
||||||
break;
|
let picked = pickRandom(p1);
|
||||||
}
|
|
||||||
}
|
// Priority 2: category match only (existing logic)
|
||||||
if (!picked) {
|
if (!picked) {
|
||||||
const remaining = ideas.filter((i) => !usedIds.has(i.id));
|
for (const cat of preferredCategories) {
|
||||||
if (remaining.length > 0) {
|
const pool = remaining.filter((i) => i.category === cat);
|
||||||
picked = remaining[Math.floor(Math.random() * remaining.length)];
|
picked = pickRandom(pool);
|
||||||
|
if (picked) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Priority 3: timeSlot match only
|
||||||
|
if (!picked) {
|
||||||
|
const p3 = remaining.filter((i) => matchesSlot(i, slot));
|
||||||
|
picked = pickRandom(p3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 4: any remaining
|
||||||
|
if (!picked) {
|
||||||
|
picked = pickRandom(remaining);
|
||||||
|
}
|
||||||
|
|
||||||
if (picked) {
|
if (picked) {
|
||||||
selected.push(picked);
|
selected.push(picked);
|
||||||
usedIds.add(picked.id);
|
usedIds.add(picked.id);
|
||||||
@@ -170,6 +199,9 @@ export async function runPlanGeneration(
|
|||||||
estimatedMinutes: true,
|
estimatedMinutes: true,
|
||||||
searchQuery: true,
|
searchQuery: true,
|
||||||
searchType: true,
|
searchType: true,
|
||||||
|
costLevel: true,
|
||||||
|
intensity: true,
|
||||||
|
needsBooking: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -272,6 +304,9 @@ export async function runPlanGeneration(
|
|||||||
estimatedMinutes: i.estimatedMinutes,
|
estimatedMinutes: i.estimatedMinutes,
|
||||||
searchQuery: i.searchQuery,
|
searchQuery: i.searchQuery,
|
||||||
searchType: i.searchType,
|
searchType: i.searchType,
|
||||||
|
costLevel: i.costLevel ?? undefined,
|
||||||
|
intensity: i.intensity ?? undefined,
|
||||||
|
needsBooking: i.needsBooking ?? undefined,
|
||||||
})),
|
})),
|
||||||
candidates,
|
candidates,
|
||||||
userLocation: { lat: room.lat!, lng: room.lng! },
|
userLocation: { lat: room.lat!, lng: room.lng! },
|
||||||
|
|||||||
+9
-2
@@ -77,7 +77,12 @@ export type IdeaCategory =
|
|||||||
| "shopping"
|
| "shopping"
|
||||||
| "sports"
|
| "sports"
|
||||||
| "culture"
|
| "culture"
|
||||||
| "relaxation";
|
| "relaxation"
|
||||||
|
| "experience"
|
||||||
|
| "nature";
|
||||||
|
|
||||||
|
export type IdeaCostLevel = "free" | "budget" | "moderate" | "premium";
|
||||||
|
export type IdeaIntensity = "chill" | "moderate" | "active";
|
||||||
|
|
||||||
export type IdeaTimeSlot =
|
export type IdeaTimeSlot =
|
||||||
| "morning"
|
| "morning"
|
||||||
@@ -92,7 +97,9 @@ export interface IdeaTags {
|
|||||||
category: IdeaCategory;
|
category: IdeaCategory;
|
||||||
timeSlot: IdeaTimeSlot;
|
timeSlot: IdeaTimeSlot;
|
||||||
estimatedMinutes: number;
|
estimatedMinutes: number;
|
||||||
outdoor: boolean;
|
costLevel: IdeaCostLevel;
|
||||||
|
intensity: IdeaIntensity;
|
||||||
|
needsBooking: boolean;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
searchType: IdeaSearchType;
|
searchType: IdeaSearchType;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user