feat: cooking assistant can propose adding items to a shopping list
Second tool alongside createRecipe, same safety shape: addToShoppingList's execute only validates/echoes input, no DB write. The proposal card lets the user pick an existing list (fetched lazily) or name a new one, then confirms through the exact two-call flow AddToShoppingListButton already uses — POST /api/v1/shopping-lists (if new) then POST .../items — no new persistence code path. generateMealPlan intentionally not built as a tool: the existing /api/v1/ai/meal-plan/generate endpoint generates and writes in one step with a week/preferences input, not a specific plan payload, so it has no "save this exact draft" entry point to confirm against without a larger refactor. Documented as a known gap rather than forcing a weaker pattern. v0.46.0
This commit is contained in:
@@ -12,6 +12,7 @@ import ReactMarkdown from "react-markdown";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChatHistorySearch } from "./chat-history-search";
|
||||
import { ConversationMenu, type Conversation } from "./conversation-menu";
|
||||
import { ShoppingListProposalCard, type ShoppingListProposal } from "./shopping-list-proposal-card";
|
||||
|
||||
type RecipeProposal = {
|
||||
title: string;
|
||||
@@ -25,11 +26,15 @@ type RecipeProposal = {
|
||||
steps: Array<{ instruction: string }>;
|
||||
};
|
||||
|
||||
type ProposalStatus = "pending" | "creating" | "created" | "discarded";
|
||||
|
||||
type Message = {
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
proposedRecipe?: RecipeProposal;
|
||||
proposalStatus?: "pending" | "creating" | "created" | "discarded";
|
||||
proposalStatus?: ProposalStatus;
|
||||
proposedShoppingList?: ShoppingListProposal;
|
||||
shoppingProposalStatus?: ProposalStatus;
|
||||
};
|
||||
|
||||
type HistoryEntry = { role: "user" | "assistant"; content: string };
|
||||
@@ -119,7 +124,12 @@ export function CookingAssistantPanel() {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ question, conversationId: conversationId ?? undefined }),
|
||||
});
|
||||
const data = await res.json() as { answer?: string; error?: string; proposedRecipe?: RecipeProposal };
|
||||
const data = await res.json() as {
|
||||
answer?: string;
|
||||
error?: string;
|
||||
proposedRecipe?: RecipeProposal;
|
||||
proposedShoppingList?: ShoppingListProposal;
|
||||
};
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
@@ -127,6 +137,8 @@ export function CookingAssistantPanel() {
|
||||
content: data.answer ?? t("sorry"),
|
||||
proposedRecipe: data.proposedRecipe,
|
||||
proposalStatus: data.proposedRecipe ? "pending" : undefined,
|
||||
proposedShoppingList: data.proposedShoppingList,
|
||||
shoppingProposalStatus: data.proposedShoppingList ? "pending" : undefined,
|
||||
},
|
||||
]);
|
||||
} catch {
|
||||
@@ -182,6 +194,10 @@ export function CookingAssistantPanel() {
|
||||
setMessages((prev) => prev.map((m, i) => (i === index ? { ...m, proposalStatus: "discarded" } : m)));
|
||||
}
|
||||
|
||||
function handleShoppingProposalStatusChange(index: number, status: ProposalStatus) {
|
||||
setMessages((prev) => prev.map((m, i) => (i === index ? { ...m, shoppingProposalStatus: status } : m)));
|
||||
}
|
||||
|
||||
const suggestions = [
|
||||
t("suggestion1"),
|
||||
t("suggestion2"),
|
||||
@@ -331,6 +347,14 @@ export function CookingAssistantPanel() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{msg.proposedShoppingList && msg.shoppingProposalStatus && (
|
||||
<ShoppingListProposalCard
|
||||
proposal={msg.proposedShoppingList}
|
||||
status={msg.shoppingProposalStatus}
|
||||
onStatusChange={(status) => handleShoppingProposalStatusChange(i, status)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user