···1010)
11111212// Test that the HTML contains Save and Dismiss buttons for recipes
1313-func TestFormatChatHTML_ContainsSaveAndDismissButtons(t *testing.T) {
1313+func TestFormatShoppingListHTML_ContainsSaveAndDismissButtons(t *testing.T) {
1414 // Create a shopping list with multiple recipes
1515 multiRecipeList := ai.ShoppingList{
1616 Recipes: []ai.Recipe{
···4040 loc := locations.Location{ID: "L1", Name: "Store", Address: "1 Main St"}
4141 p := DefaultParams(&loc, time.Now())
4242 w := httptest.NewRecorder()
4343- FormatChatHTML(p, multiRecipeList, w)
4343+ FormatShoppingListHTML(p, multiRecipeList, w)
4444 html := w.Body.String()
45454646 // Verify HTML is valid
+2
internal/recipes/generator.go
···9696 // should never happen? How do you get save on first generte?
9797 // shoppingList.Recipes = append(shoppingList.Recipes, p.Saved...)
98989999+ //TODO this does not get saved in params and thus must be loaded from html
100100+ // could update params after first generation or pregenerate before we save params.
99101 p.ConversationID = shoppingList.ConversationID
100102 slog.InfoContext(ctx, "generated chat", "location", p.String(), "duration", time.Since(start), "hash", hash)
101103 return shoppingList, nil
+25-2
internal/recipes/html.go
···1111 "strings"
1212)
13131414-// FormatChatHTML renders the raw AI chat (JSON or free-form text) for a location.
1515-func FormatChatHTML(p *generatorParams, l ai.ShoppingList, writer http.ResponseWriter) {
1414+// FormatShoppingListHTML renders the multi-recipe shopping list view.
1515+func FormatShoppingListHTML(p *generatorParams, l ai.ShoppingList, writer http.ResponseWriter) {
1616 // TODO just put params into shopping list and pass that up?
1717 data := struct {
1818 Location locations.Location
···3434 ShoppingList: shoppingListForDisplay(l.Recipes),
3535 ConversationID: l.ConversationID,
3636 Style: seasons.GetCurrentStyle(),
3737+ }
3838+3939+ if err := templates.ShoppingList.Execute(writer, data); err != nil {
4040+ http.Error(writer, "shopping list template error: "+err.Error(), http.StatusInternalServerError)
4141+ }
4242+}
4343+4444+// FormatRecipeHTML renders a single recipe view.
4545+func FormatRecipeHTML(p *generatorParams, recipe ai.Recipe, writer http.ResponseWriter) {
4646+ data := struct {
4747+ Location locations.Location
4848+ Date string
4949+ ClarityScript template.HTML
5050+ Recipe ai.Recipe
5151+ OriginHash string
5252+ Style seasons.Style
5353+ }{
5454+ Location: *p.Location,
5555+ Date: p.Date.Format("2006-01-02"),
5656+ ClarityScript: templates.ClarityScript(),
5757+ Recipe: recipe,
5858+ OriginHash: recipe.OriginHash,
5959+ Style: seasons.GetCurrentStyle(),
3760 }
38613962 if err := templates.Recipe.Execute(writer, data); err != nil {
+33-9
internal/recipes/html_test.go
···4343 },
4444}
45454646-func TestFormatChatHTML_ValidHTML(t *testing.T) {
4646+func TestFormatShoppingListHTML_ValidHTML(t *testing.T) {
4747 loc := locations.Location{ID: "L1", Name: "Store", Address: "1 Main St"}
4848 p := DefaultParams(&loc, time.Now())
4949 w := httptest.NewRecorder()
5050- FormatChatHTML(p, list, w)
5050+ FormatShoppingListHTML(p, list, w)
5151 html := w.Body.String()
5252 if w.Code != http.StatusOK {
5353 t.Error("Want ok statuscode")
···5959 loc := locations.Location{ID: "L1", Name: "Store", Address: "1 Main St"}
6060 p := DefaultParams(&loc, time.Now())
6161 w := httptest.NewRecorder()
6262- FormatChatHTML(p, list, w)
6262+ FormatShoppingListHTML(p, list, w)
6363 html := w.Body.String()
64646565 isValidHTML(t, html)
···6868 }
6969}
70707171-func TestFormatChatHTML_IncludesClarityScript(t *testing.T) {
7171+func TestFormatShoppingListHTML_IncludesClarityScript(t *testing.T) {
7272 loc := locations.Location{ID: "L1", Name: "Store", Address: "1 Main St"}
7373 p := DefaultParams(&loc, time.Now())
7474 templates.SetClarity("test456")
7575 w := httptest.NewRecorder()
7676- FormatChatHTML(p, list, w)
7676+ FormatShoppingListHTML(p, list, w)
7777 if !bytes.Contains(w.Body.Bytes(), []byte("www.clarity.ms/tag/")) {
7878 t.Error("HTML should contain Clarity script URL")
7979 }
···8383 }
8484}
85858686-func TestFormatChatHTML_NoClarityWhenEmpty(t *testing.T) {
8686+func TestFormatShoppingListHTML_NoClarityWhenEmpty(t *testing.T) {
8787 loc := locations.Location{ID: "L1", Name: "Store", Address: "1 Main St"}
8888 p := DefaultParams(&loc, time.Now())
8989 templates.SetClarity("")
9090 w := httptest.NewRecorder()
9191- FormatChatHTML(p, list, w)
9191+ FormatShoppingListHTML(p, list, w)
9292 if bytes.Contains(w.Body.Bytes(), []byte("clarity.ms")) {
9393 t.Error("HTML should not contain Clarity script when project ID is empty")
9494 }
9595}
96969797-func TestFormatChatHTML_HomePageLink(t *testing.T) {
9797+func TestFormatShoppingListHTML_HomePageLink(t *testing.T) {
9898 loc := locations.Location{ID: "L1", Name: "Store", Address: "1 Main St"}
9999 p := DefaultParams(&loc, time.Now())
100100 w := httptest.NewRecorder()
101101- FormatChatHTML(p, list, w)
101101+ FormatShoppingListHTML(p, list, w)
102102 html := w.Body.String()
103103104104 // Verify "Careme Recipes" is a link to home page
···109109 t.Error("HTML should contain 'Careme Recipes' as a link")
110110 }
111111}
112112+113113+func TestFormatRecipeHTML_NoFinalizeOrRegenerate(t *testing.T) {
114114+ loc := locations.Location{ID: "L1", Name: "Store", Address: "1 Main St"}
115115+ p := DefaultParams(&loc, time.Now())
116116+ p.ConversationID = "convo123"
117117+ w := httptest.NewRecorder()
118118+ FormatRecipeHTML(p, list.Recipes[0], w)
119119+ html := w.Body.String()
120120+121121+ isValidHTML(t, html)
122122+123123+ if strings.Contains(html, "Finalize") {
124124+ t.Error("recipe HTML should not contain Finalize button")
125125+ }
126126+ if strings.Contains(html, "Regenerate") {
127127+ t.Error("recipe HTML should not contain Regenerate button")
128128+ }
129129+ if strings.Contains(html, `name="saved"`) || strings.Contains(html, `name="dismissed"`) {
130130+ t.Error("recipe HTML should not contain save/dismiss inputs")
131131+ }
132132+ if strings.Contains(html, `name="question"`) {
133133+ t.Error("recipe HTML should not contain question input")
134134+ }
135135+}
+19-18
internal/recipes/server.go
···7676 return
7777 }
78787979- p := DefaultParams(&locations.Location{
8080- ID: "",
8181- Name: "Unknown Location",
8282- }, time.Now())
8383- if recipe.OriginHash != "" {
8484- loadedp, err := loadParamsFromHash(ctx, recipe.OriginHash, s.cache)
8585- if err != nil {
8686- slog.ErrorContext(ctx, "failed to load params for hash", "hash", recipe.OriginHash, "error", err)
8787- // http.Error(w, "recipe not found or expired", http.StatusNotFound)
8888- // return
8989- } else {
9090- p = loadedp
9191- }
7979+ if recipe.OriginHash == "" {
8080+ slog.WarnContext(ctx, "recipe missing origin hash Probably and old recipe", "hash", hash)
8181+ p := DefaultParams(&locations.Location{
8282+ ID: "",
8383+ Name: "Unknown Location",
8484+ }, time.Now())
8585+ FormatRecipeHTML(p, *recipe, w)
8686+ return
9287 }
93889494- list := ai.ShoppingList{
9595- Recipes: []ai.Recipe{*recipe},
8989+ p, err := loadParamsFromHash(ctx, recipe.OriginHash, s.cache)
9090+ if err != nil {
9191+ slog.ErrorContext(ctx, "failed to load params for hash", "hash", recipe.OriginHash, "error", err)
9292+ http.Error(w, "recipe not found or expired", http.StatusNotFound)
9393+ return
9694 }
97959696+ // TODO this p is mising converastion id. See todo in generate recipes we can pregenerate it or update it after generation.
9797+9898+ // TODO: Add questions or regneration to signle recipes
9999+98100 slog.InfoContext(ctx, "serving shared recipe by hash", "hash", hash)
9999- FormatChatHTML(p, list, w)
101101+ FormatRecipeHTML(p, *recipe, w)
100102}
101103102104const (
···153155 return
154156 }
155157156156-157158 p, err := loadParamsFromHash(ctx, hashParam, s.cache)
158159 if err != nil {
159160 slog.ErrorContext(ctx, "failed to load params for hash", "hash", hashParam, "error", err)
···164165 FormatMail(p, *slist, w)
165166 return
166167 }
167167- FormatChatHTML(p, *slist, w)
168168+ FormatShoppingListHTML(p, *slist, w)
168169 return
169170 }
170171