feat(products): improve shopping suggestion decision support
This commit is contained in:
parent
5d9d18bd05
commit
bb5d402c15
8 changed files with 352 additions and 142 deletions
|
|
@ -284,14 +284,21 @@ export interface BatchSuggestion {
|
|||
|
||||
// ─── Shopping suggestion types ───────────────────────────────────────────────
|
||||
|
||||
export type ShoppingPriority = 'high' | 'medium' | 'low';
|
||||
|
||||
export interface ProductSuggestion {
|
||||
category: string;
|
||||
product_type: string;
|
||||
priority: ShoppingPriority;
|
||||
key_ingredients: string[];
|
||||
target_concerns: string[];
|
||||
why_needed: string;
|
||||
recommended_time: string;
|
||||
frequency: string;
|
||||
short_reason: string;
|
||||
reason_to_buy_now: string;
|
||||
reason_not_needed_if_budget_tight?: string;
|
||||
fit_with_current_routine: string;
|
||||
usage_cautions: string[];
|
||||
}
|
||||
|
||||
export interface ShoppingSuggestionResponse {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ export const actions: Actions = {
|
|||
return {
|
||||
suggestions: data.suggestions,
|
||||
reasoning: data.reasoning,
|
||||
validation_warnings: data.validation_warnings,
|
||||
auto_fixes_applied: data.auto_fixes_applied,
|
||||
response_metadata: data.response_metadata,
|
||||
};
|
||||
} catch (e) {
|
||||
return fail(500, { error: String(e) });
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { resolve } from '$app/paths';
|
||||
import type { ProductSuggestion, ResponseMetadata } from '$lib/types';
|
||||
import type { ProductSuggestion, ResponseMetadata, ShoppingPriority } from '$lib/types';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
|
|
@ -22,6 +22,28 @@
|
|||
let autoFixes = $state<string[] | undefined>(undefined);
|
||||
let responseMetadata = $state<ResponseMetadata | undefined>(undefined);
|
||||
|
||||
function priorityLabel(priority: ShoppingPriority) {
|
||||
switch (priority) {
|
||||
case 'high':
|
||||
return m.products_suggestPriorityHigh();
|
||||
case 'medium':
|
||||
return m.products_suggestPriorityMedium();
|
||||
default:
|
||||
return m.products_suggestPriorityLow();
|
||||
}
|
||||
}
|
||||
|
||||
function priorityClasses(priority: ShoppingPriority) {
|
||||
switch (priority) {
|
||||
case 'high':
|
||||
return 'border-transparent bg-[color:var(--page-accent)] text-white';
|
||||
case 'medium':
|
||||
return 'border-[color:var(--page-accent)]/20 bg-[var(--page-accent-soft)] text-foreground';
|
||||
default:
|
||||
return 'border-border bg-secondary text-secondary-foreground';
|
||||
}
|
||||
}
|
||||
|
||||
function enhanceForm() {
|
||||
loading = true;
|
||||
errorMsg = null;
|
||||
|
|
@ -104,37 +126,93 @@
|
|||
<div class="space-y-4 reveal-3">
|
||||
<h3 class="text-lg font-semibold">{m["products_suggestResults"]()}</h3>
|
||||
{#each suggestions as s (s.product_type)}
|
||||
<Card>
|
||||
<Card class="border-border/80 bg-card/95 shadow-sm">
|
||||
<CardContent class="pt-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<h4 class="font-medium">{s.product_type}</h4>
|
||||
<Badge variant="secondary" class="shrink-0">{s.category}</Badge>
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="space-y-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Badge class={priorityClasses(s.priority)}>{priorityLabel(s.priority)}</Badge>
|
||||
<Badge variant="secondary" class="shrink-0">{s.category}</Badge>
|
||||
</div>
|
||||
<h4 class="font-medium tracking-[0.01em]">{s.product_type}</h4>
|
||||
<p class="text-sm text-foreground/80">{s.short_reason}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 text-xs text-muted-foreground sm:justify-end">
|
||||
<span>{m["products_suggestTime"]()}: {s.recommended_time.toUpperCase()}</span>
|
||||
<span>{m["products_suggestFrequency"]()}: {s.frequency}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<div class="rounded-xl border border-border/70 bg-muted/20 p-3">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">
|
||||
{m.products_suggestBuyNow()}
|
||||
</p>
|
||||
<p class="mt-2 text-sm leading-6 text-foreground/85">{s.reason_to_buy_now}</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-border/70 bg-muted/20 p-3">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">
|
||||
{m.products_suggestRoutineFit()}
|
||||
</p>
|
||||
<p class="mt-2 text-sm leading-6 text-foreground/85">{s.fit_with_current_routine}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if s.reason_not_needed_if_budget_tight}
|
||||
<div class="rounded-xl border border-dashed border-border/80 bg-background p-3">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">
|
||||
{m.products_suggestBudgetSkip()}
|
||||
</p>
|
||||
<p class="mt-2 text-sm leading-6 text-foreground/80">
|
||||
{s.reason_not_needed_if_budget_tight}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if s.key_ingredients.length > 0}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each s.key_ingredients as ing (ing)}
|
||||
<Badge variant="outline" class="text-xs">{ing}</Badge>
|
||||
{/each}
|
||||
<div class="space-y-2">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">
|
||||
{m.products_suggestKeyIngredients()}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{#each s.key_ingredients as ing (ing)}
|
||||
<Badge variant="outline" class="text-[11px] normal-case tracking-normal">{ing}</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if s.target_concerns.length > 0}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each s.target_concerns as concern (concern)}
|
||||
<Badge class="text-xs">{concern.replace(/_/g, ' ')}</Badge>
|
||||
{/each}
|
||||
<div class="space-y-2">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">
|
||||
{m.products_suggestTargets()}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{#each s.target_concerns as concern (concern)}
|
||||
<Badge class="text-[11px] normal-case tracking-normal">{concern.replace(/_/g, ' ')}</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<p class="text-sm text-muted-foreground">{s.why_needed}</p>
|
||||
|
||||
<div class="flex gap-4 text-xs text-muted-foreground">
|
||||
<span>{m["products_suggestTime"]()}: {s.recommended_time.toUpperCase()}</span>
|
||||
<span>{m["products_suggestFrequency"]()}: {s.frequency}</span>
|
||||
{#if s.usage_cautions.length > 0}
|
||||
<div class="space-y-2">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">
|
||||
{m.products_suggestCautions()}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{#each s.usage_cautions as caution (caution)}
|
||||
<Badge variant="outline" class="border-amber-200 bg-amber-50 text-[11px] normal-case tracking-normal text-amber-900">
|
||||
{caution}
|
||||
</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/each}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue