diff --git a/api/api_llm.go b/api/api_llm.go index 8cb2b5f..6b1c478 100644 --- a/api/api_llm.go +++ b/api/api_llm.go @@ -147,35 +147,41 @@ func runExtraction(userInput, category, language string, existingTypes map[strin return nil, err } + log.Printf("Gemini raw response for %s: %s", category, respText) + + // First try to parse as singular "entry" (what the prompts actually generate) + var singleEntryResult struct { + Question string `json:"question"` + Category string `json:"category"` + Type string `json:"type"` + InputType string `json:"input_type"` + InputConfig InputConfig `json:"input_config"` + Schedule []ScheduleSlot `json:"schedule"` + Entry *EntryData `json:"entry,omitempty"` + Error string `json:"error,omitempty"` + } + if err := json.Unmarshal([]byte(respText), &singleEntryResult); err == nil { + result := ExtractionResult{ + Question: singleEntryResult.Question, + Category: singleEntryResult.Category, + Type: singleEntryResult.Type, + InputType: singleEntryResult.InputType, + InputConfig: singleEntryResult.InputConfig, + Schedule: singleEntryResult.Schedule, + Error: singleEntryResult.Error, + } + if singleEntryResult.Entry != nil { + result.Entries = []*EntryData{singleEntryResult.Entry} + } + if result.Category == "" { + result.Category = category + } + return &result, nil + } + + // Fallback: try plural "entries" format var result ExtractionResult if err := json.Unmarshal([]byte(respText), &result); err != nil { - // Fallback for single entry to maintain compatibility with older prompts - var singleEntryResult struct { - Question string `json:"question"` - Category string `json:"category"` - Type string `json:"type"` - InputType string `json:"input_type"` - InputConfig InputConfig `json:"input_config"` - Schedule []ScheduleSlot `json:"schedule"` - Entry *EntryData `json:"entry,omitempty"` - Error string `json:"error,omitempty"` - } - if err2 := json.Unmarshal([]byte(respText), &singleEntryResult); err2 == nil { - result.Question = singleEntryResult.Question - result.Category = singleEntryResult.Category - result.Type = singleEntryResult.Type - result.InputType = singleEntryResult.InputType - result.InputConfig = singleEntryResult.InputConfig - result.Schedule = singleEntryResult.Schedule - result.Error = singleEntryResult.Error - if singleEntryResult.Entry != nil { - result.Entries = []*EntryData{singleEntryResult.Entry} - } - if result.Category == "" { - result.Category = category - } - return &result, nil - } return nil, fmt.Errorf("failed to parse extraction JSON: %v (raw: %s)", err, respText) } diff --git a/api/api_prompts.go b/api/api_prompts.go index 075cead..7dc9500 100644 --- a/api/api_prompts.go +++ b/api/api_prompts.go @@ -206,8 +206,11 @@ func tryGeneratePromptFromFreeform(promptID string, userInput string) (*lib.Prom return nil, "" } - // Create entries from the LLM result + log.Printf("LLM generated: category=%s, type=%s, entries=%d", generated.Category, generated.Type, len(generated.Entries)) + + // Create entries from the LLM result (keep track of entry IDs) var primaryEntryValue string + var createdEntries []*lib.Entry if len(generated.Entries) > 0 { primaryEntryValue = generated.Entries[0].Value for _, entryData := range generated.Entries { @@ -222,6 +225,8 @@ func tryGeneratePromptFromFreeform(promptID string, userInput string) (*lib.Prom } if err := lib.EntryAdd(entry); err != nil { log.Printf("Failed to create entry from freeform: %v", err) + } else { + createdEntries = append(createdEntries, entry) } } } @@ -263,7 +268,15 @@ func tryGeneratePromptFromFreeform(promptID string, userInput string) (*lib.Prom return nil, "" } - log.Printf("Created prompt from freeform: %s (%s/%s)", newPrompt.Question, newPrompt.Category, newPrompt.Type) + // Update the entries we just created to link them to this prompt via SearchKey + for _, entry := range createdEntries { + entry.SearchKey = newPrompt.PromptID + if err := lib.EntryAdd(entry); err != nil { + log.Printf("Failed to update entry search_key: %v", err) + } + } + + log.Printf("Created prompt from freeform: %s (%s/%s) and linked %d entries", newPrompt.Question, newPrompt.Category, newPrompt.Type, len(createdEntries)) return newPrompt, primaryEntryValue } diff --git a/lib/prompt.go b/lib/prompt.go index b497f3b..e4f6906 100644 --- a/lib/prompt.go +++ b/lib/prompt.go @@ -106,6 +106,7 @@ func promptCreateEntry(p *Prompt, response string, timestamp int64) error { Value: responseToValue(response), Timestamp: timestamp, Data: fmt.Sprintf(`{"response":%s,"source":"prompt","prompt_id":"%s"}`, response, p.PromptID), + SearchKey: p.PromptID, // Foreign key to link entry back to its prompt } return EntryAdd(e) } diff --git a/portal/prompts.go b/portal/prompts.go index a88ebe7..08ac747 100644 --- a/portal/prompts.go +++ b/portal/prompts.go @@ -124,13 +124,13 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) { // Helper to translate category name translateCategory := func(cat string) string { switch cat { - case "supplement": return T(lang, "section_supplements") - case "medication": return T(lang, "section_medications") - case "vital": return T(lang, "section_vitals") - case "exercise": return T(lang, "section_exercise") - case "symptom": return T(lang, "section_symptoms") - case "nutrition": return T(lang, "section_nutrition") - case "note": return T(lang, "section_notes") + case "supplement": return "Supplement" + case "medication": return "Medication" + case "vital": return "Vital" + case "exercise": return "Exercise" + case "symptom": return "Symptom" + case "nutrition": return "Nutrition" + case "note": return "Note" default: return cat } } @@ -199,22 +199,25 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) { } data.T = translations[lang] - // Build prompt lookup map by category/type + // Build prompt lookup map by prompt ID promptMap := make(map[string]PromptView) for _, p := range prompts { - key := p.Category + "/" + p.Type - promptMap[key] = p + promptMap[p.ID] = p + log.Printf("DEBUG: Added to promptMap: id=%s, question=%s", p.ID, p.Question) } // Fetch today's entries now := time.Now() startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Unix() endOfDay := startOfDay + 86400 + log.Printf("DEBUG: Fetching entries for today: start=%d end=%d now=%d", startOfDay, endOfDay, now.Unix()) rawEntries, _ := lib.EntryQueryByDate(targetID, startOfDay, endOfDay) + log.Printf("DEBUG: Found %d entries for today", len(rawEntries)) var entries []EntryView for _, e := range rawEntries { catName := lib.CategoryName(e.Category) + log.Printf("DEBUG: Processing entry: cat=%d (%s), type=%s, value=%s, search_key=%s", e.Category, catName, e.Type, e.Value, e.SearchKey) ev := EntryView{ ID: formatHexID(e.EntryID), Category: lib.CategoryTranslate(e.Category, lang), @@ -224,14 +227,21 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) { TimeFormatted: time.Unix(e.Timestamp, 0).Format("Jan 2, 3:04 PM"), } - // Get layout from matching prompt (uses internal category name) - key := catName + "/" + e.Type - if prompt, ok := promptMap[key]; ok { - ev.Question = prompt.Question - ev.PromptID = prompt.ID - // Copy fields from prompt - ev.Fields = make([]PromptField, len(prompt.Fields)) - copy(ev.Fields, prompt.Fields) + // Link entry to prompt via SearchKey (foreign key relationship) + if e.SearchKey != "" { + log.Printf("DEBUG: Looking for prompt with id=%s", e.SearchKey) + if prompt, ok := promptMap[e.SearchKey]; ok { + log.Printf("DEBUG: Found matching prompt: question=%s, fields=%d", prompt.Question, len(prompt.Fields)) + ev.Question = prompt.Question + ev.PromptID = prompt.ID + // Copy fields from prompt + ev.Fields = make([]PromptField, len(prompt.Fields)) + copy(ev.Fields, prompt.Fields) + } else { + log.Printf("DEBUG: No prompt found with id=%s", e.SearchKey) + } + } else { + log.Printf("DEBUG: Entry has no search_key (may be freeform/legacy entry)") } // Extract source_input and values from Data JSON @@ -255,22 +265,19 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) { } entries = append(entries, ev) } + log.Printf("DEBUG: Built %d entry views for display", len(entries)) - // Split prompts into due and upcoming - var duePrompts, upcomingPrompts []PromptView - for _, p := range prompts { - if p.IsDue || p.IsFreeform { - duePrompts = append(duePrompts, p) - } else { - upcomingPrompts = append(upcomingPrompts, p) - } - } - - // Count due items (excluding freeform) + // Split prompts into regular and freeform + var allPrompts, freeformPrompts []PromptView dueCount := 0 - for _, p := range duePrompts { - if !p.IsFreeform { - dueCount++ + for _, p := range prompts { + if p.IsFreeform { + freeformPrompts = append(freeformPrompts, p) + } else { + allPrompts = append(allPrompts, p) + if p.IsDue { + dueCount++ + } } } @@ -278,12 +285,12 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") templates.ExecuteTemplate(w, "base.tmpl", struct { PageData - DuePrompts []PromptView - UpcomingPrompts []PromptView - Entries []EntryView - TargetHex string - DueCount int - }{data, duePrompts, upcomingPrompts, entries, targetHex, dueCount}) + AllPrompts []PromptView + FreeformPrompts []PromptView + Entries []EntryView + TargetHex string + DueCount int + }{data, allPrompts, freeformPrompts, entries, targetHex, dueCount}) } func handlePromptRespond(w http.ResponseWriter, r *http.Request) { diff --git a/portal/templates/prompts.tmpl b/portal/templates/prompts.tmpl index 53a25a1..f15a42f 100644 --- a/portal/templates/prompts.tmpl +++ b/portal/templates/prompts.tmpl @@ -3,19 +3,18 @@
Track daily measurements and observations
- ++ Enter what you want to track (e.g., "I take vitamin D every morning" or "walked 30 minutes today"). + The system will learn patterns and create tracking prompts. +
{{if .Error}}✨ No tracking prompts yet
@@ -230,12 +167,6 @@ padding: 16px 20px; border-bottom: 1px solid var(--border); } -.prompt-section-bar { - width: 4px; - height: 36px; - background: var(--accent); - border-radius: 2px; -} .prompt-section-title { font-size: 0.8rem; font-weight: 600; @@ -304,10 +235,7 @@ color: var(--text-muted); } .prompt-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 12px; + margin-bottom: 8px; } .prompt-category { display: inline-block; @@ -572,9 +500,6 @@ background: #fafafa; border-left: 3px solid #64748b; } -.prompt-preview { - opacity: 0.5; -} .prompt-preview input[disabled] { cursor: not-allowed; background: #f9fafb; @@ -967,6 +892,11 @@ async function saveItem(item) { // For freeform prompts, just clear and show notification if (item.classList.contains('prompt-freeform')) { form.querySelector('textarea').value = ''; + if (saveBtn) { + saveBtn.classList.remove('saving'); + saveBtn.textContent = originalText; + saveBtn.disabled = false; + } showNotification('✓ Saved', 'success'); // If LLM generated a new prompt, add it as a pending card