refactor: prompts UI and LLM API cleanup

This commit is contained in:
James 2026-02-08 08:30:27 -05:00
parent 37b7602027
commit 3014f21d72
5 changed files with 176 additions and 219 deletions

View File

@ -147,35 +147,41 @@ func runExtraction(userInput, category, language string, existingTypes map[strin
return nil, err 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 var result ExtractionResult
if err := json.Unmarshal([]byte(respText), &result); err != nil { 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) return nil, fmt.Errorf("failed to parse extraction JSON: %v (raw: %s)", err, respText)
} }

View File

@ -206,8 +206,11 @@ func tryGeneratePromptFromFreeform(promptID string, userInput string) (*lib.Prom
return nil, "" 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 primaryEntryValue string
var createdEntries []*lib.Entry
if len(generated.Entries) > 0 { if len(generated.Entries) > 0 {
primaryEntryValue = generated.Entries[0].Value primaryEntryValue = generated.Entries[0].Value
for _, entryData := range generated.Entries { for _, entryData := range generated.Entries {
@ -222,6 +225,8 @@ func tryGeneratePromptFromFreeform(promptID string, userInput string) (*lib.Prom
} }
if err := lib.EntryAdd(entry); err != nil { if err := lib.EntryAdd(entry); err != nil {
log.Printf("Failed to create entry from freeform: %v", err) 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, "" 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 return newPrompt, primaryEntryValue
} }

View File

@ -106,6 +106,7 @@ func promptCreateEntry(p *Prompt, response string, timestamp int64) error {
Value: responseToValue(response), Value: responseToValue(response),
Timestamp: timestamp, Timestamp: timestamp,
Data: fmt.Sprintf(`{"response":%s,"source":"prompt","prompt_id":"%s"}`, response, p.PromptID), 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) return EntryAdd(e)
} }

View File

@ -124,13 +124,13 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) {
// Helper to translate category name // Helper to translate category name
translateCategory := func(cat string) string { translateCategory := func(cat string) string {
switch cat { switch cat {
case "supplement": return T(lang, "section_supplements") case "supplement": return "Supplement"
case "medication": return T(lang, "section_medications") case "medication": return "Medication"
case "vital": return T(lang, "section_vitals") case "vital": return "Vital"
case "exercise": return T(lang, "section_exercise") case "exercise": return "Exercise"
case "symptom": return T(lang, "section_symptoms") case "symptom": return "Symptom"
case "nutrition": return T(lang, "section_nutrition") case "nutrition": return "Nutrition"
case "note": return T(lang, "section_notes") case "note": return "Note"
default: return cat default: return cat
} }
} }
@ -199,22 +199,25 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) {
} }
data.T = translations[lang] data.T = translations[lang]
// Build prompt lookup map by category/type // Build prompt lookup map by prompt ID
promptMap := make(map[string]PromptView) promptMap := make(map[string]PromptView)
for _, p := range prompts { for _, p := range prompts {
key := p.Category + "/" + p.Type promptMap[p.ID] = p
promptMap[key] = p log.Printf("DEBUG: Added to promptMap: id=%s, question=%s", p.ID, p.Question)
} }
// Fetch today's entries // Fetch today's entries
now := time.Now() now := time.Now()
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Unix() startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Unix()
endOfDay := startOfDay + 86400 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) rawEntries, _ := lib.EntryQueryByDate(targetID, startOfDay, endOfDay)
log.Printf("DEBUG: Found %d entries for today", len(rawEntries))
var entries []EntryView var entries []EntryView
for _, e := range rawEntries { for _, e := range rawEntries {
catName := lib.CategoryName(e.Category) 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{ ev := EntryView{
ID: formatHexID(e.EntryID), ID: formatHexID(e.EntryID),
Category: lib.CategoryTranslate(e.Category, lang), 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"), TimeFormatted: time.Unix(e.Timestamp, 0).Format("Jan 2, 3:04 PM"),
} }
// Get layout from matching prompt (uses internal category name) // Link entry to prompt via SearchKey (foreign key relationship)
key := catName + "/" + e.Type if e.SearchKey != "" {
if prompt, ok := promptMap[key]; ok { log.Printf("DEBUG: Looking for prompt with id=%s", e.SearchKey)
ev.Question = prompt.Question if prompt, ok := promptMap[e.SearchKey]; ok {
ev.PromptID = prompt.ID log.Printf("DEBUG: Found matching prompt: question=%s, fields=%d", prompt.Question, len(prompt.Fields))
// Copy fields from prompt ev.Question = prompt.Question
ev.Fields = make([]PromptField, len(prompt.Fields)) ev.PromptID = prompt.ID
copy(ev.Fields, prompt.Fields) // 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 // Extract source_input and values from Data JSON
@ -255,22 +265,19 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) {
} }
entries = append(entries, ev) entries = append(entries, ev)
} }
log.Printf("DEBUG: Built %d entry views for display", len(entries))
// Split prompts into due and upcoming // Split prompts into regular and freeform
var duePrompts, upcomingPrompts []PromptView var allPrompts, freeformPrompts []PromptView
for _, p := range prompts {
if p.IsDue || p.IsFreeform {
duePrompts = append(duePrompts, p)
} else {
upcomingPrompts = append(upcomingPrompts, p)
}
}
// Count due items (excluding freeform)
dueCount := 0 dueCount := 0
for _, p := range duePrompts { for _, p := range prompts {
if !p.IsFreeform { if p.IsFreeform {
dueCount++ 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") w.Header().Set("Content-Type", "text/html; charset=utf-8")
templates.ExecuteTemplate(w, "base.tmpl", struct { templates.ExecuteTemplate(w, "base.tmpl", struct {
PageData PageData
DuePrompts []PromptView AllPrompts []PromptView
UpcomingPrompts []PromptView FreeformPrompts []PromptView
Entries []EntryView Entries []EntryView
TargetHex string TargetHex string
DueCount int DueCount int
}{data, duePrompts, upcomingPrompts, entries, targetHex, dueCount}) }{data, allPrompts, freeformPrompts, entries, targetHex, dueCount})
} }
func handlePromptRespond(w http.ResponseWriter, r *http.Request) { func handlePromptRespond(w http.ResponseWriter, r *http.Request) {

View File

@ -3,19 +3,18 @@
<h1 style="font-size: 2.5rem; font-weight: 700;">{{if .TargetDossier}}{{.TargetDossier.Name}}'s {{end}}Daily Check-in</h1> <h1 style="font-size: 2.5rem; font-weight: 700;">{{if .TargetDossier}}{{.TargetDossier.Name}}'s {{end}}Daily Check-in</h1>
<p class="intro" style="font-size: 1.15rem; font-weight: 300; line-height: 1.8; margin-bottom: 8px;">Track daily measurements and observations</p> <p class="intro" style="font-size: 1.15rem; font-weight: 300; line-height: 1.8; margin-bottom: 8px;">Track daily measurements and observations</p>
<div class="help-banner" style="background: #fef3c7; border-left: 4px solid #f59e0b; padding: 12px 16px; border-radius: 6px; margin-bottom: 24px; font-size: 0.9rem; line-height: 1.6;"> <p style="color: #64748b; margin-bottom: 32px; line-height: 1.6;">
<strong>💡 How it works:</strong> Enter what you want to track (e.g., "I take vitamin D every morning" or "walked 30 minutes today"). 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 offer to create daily reminders with pre-filled values for fast tracking. The system will learn patterns and create tracking prompts.
</div> </p>
{{if .Error}}<div class="msg msg-error">{{.Error}}</div>{{end}} {{if .Error}}<div class="msg msg-error">{{.Error}}</div>{{end}}
{{if .Success}}<div class="msg msg-success">{{.Success}}</div>{{end}} {{if .Success}}<div class="msg msg-success">{{.Success}}</div>{{end}}
{{if or .DuePrompts .UpcomingPrompts .Entries}} {{if or .AllPrompts .Entries .FreeformPrompts}}
<div class="data-card"> <div class="data-card">
<!-- Section header --> <!-- Section header -->
<div class="prompt-section-header"> <div class="prompt-section-header">
<div class="prompt-section-bar"></div>
<div class="prompt-section-info"> <div class="prompt-section-info">
<div class="prompt-section-title">TODAY</div> <div class="prompt-section-title">TODAY</div>
<div class="prompt-section-subtitle">{{.DueCount}} items due</div> <div class="prompt-section-subtitle">{{.DueCount}} items due</div>
@ -23,99 +22,18 @@
</div> </div>
<div class="prompt-list"> <div class="prompt-list">
{{/* 1. FREEFORM CARD - Always visible */}} {{/* 1. FILLED CARDS - Entries from today (SHOW FIRST) */}}
<div style="padding: 12px 20px; background: #f8fafc; border-bottom: 1px solid var(--border);">
<span style="font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b;">Add New Entry</span>
</div>
{{range .DuePrompts}}
{{if .IsFreeform}}
<div class="prompt-item prompt-freeform" data-prompt-id="{{.ID}}">
<form class="prompt-form" data-prompt-id="{{.ID}}">
<div class="prompt-header">
<span class="prompt-question">{{.Question}}</span>
<span class="prompt-due">optional</span>
</div>
<div class="prompt-body">
<div style="display: flex; gap: 12px; align-items: flex-end;">
<textarea name="response_raw" class="prompt-textarea" rows="3" style="flex: 1;"
placeholder="Type what you want to track... (e.g., 'I take vitamin D every morning' or 'walked 30 minutes today')"
onkeydown="if(event.key==='Enter' && (event.metaKey || event.ctrlKey)){event.preventDefault();saveItem(this.closest('.prompt-item'));}"></textarea>
<button type="button" class="btn btn-primary" onclick="saveItem(this.closest('.prompt-item'))" style="align-self: flex-end;">Save</button>
</div>
<div class="prompt-hint" style="margin-top: 8px; text-align: right;">
<span id="kbd-hint">or press Ctrl+Enter</span>
</div>
</div>
</form>
</div>
{{end}}
{{end}}
{{/* 2. PENDING CARDS - Due but not filled yet */}}
<div style="padding: 12px 20px; background: #f8fafc; border-bottom: 1px solid var(--border); margin-top: 24px;">
<span style="font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b;">Due Now</span>
</div>
{{range .DuePrompts}}
{{if not .IsFreeform}}
{{if not .HasResponse}}
<div class="prompt-item prompt-pending" data-prompt-id="{{.ID}}">
<a href="#" class="prompt-dismiss" onclick="showDismissConfirm(this, '{{.ID}}'); return false;" title="Don't ask again">✕</a>
<div class="dismiss-confirm">
<span>Stop tracking?</span>
<a href="#" onclick="confirmDismiss('{{.ID}}'); return false;">Yes</a>
<a href="#" onclick="hideDismissConfirm(this); return false;">No</a>
</div>
<form class="prompt-form" data-prompt-id="{{.ID}}">
<div class="prompt-header">
<span class="prompt-category">{{.Category}}</span>
<span class="prompt-question">{{.Question}}</span>
<span class="prompt-due">{{.NextAskFormatted}}</span>
</div>
<div class="prompt-body">
{{if .Fields}}
{{if eq (len .Fields) 1}}
{{with index .Fields 0}}
{{if eq .Type "number"}}
<div class="prompt-input-row">
<input type="number" name="field_{{.Key}}"
{{if .Min}}min="{{.Min}}"{{end}}
{{if .Max}}max="{{.Max}}"{{end}}
{{if .Step}}step="{{.Step}}"{{end}}
class="prompt-input-number"
onkeydown="if(event.key==='Enter'){event.preventDefault();saveItem(this.closest('.prompt-item'));}">
{{if .Unit}}<span class="prompt-unit">{{.Unit}}</span>{{end}}
<button type="button" class="btn-save" onclick="saveItem(this.closest('.prompt-item'))">Save</button>
</div>
{{else if eq .Type "checkbox"}}
<div class="prompt-buttons">
<button type="button" class="prompt-btn" data-field="{{.Key}}" data-value="yes" onclick="selectYesNo(this)">Yes</button>
<button type="button" class="prompt-btn" data-field="{{.Key}}" data-value="no" onclick="selectYesNo(this)">No</button>
<input type="hidden" name="field_{{.Key}}" value="">
</div>
{{end}}
{{end}}
{{end}}
{{end}}
</div>
</form>
</div>
{{end}}
{{end}}
{{end}}
{{/* 3. FILLED CARDS - Entries from today */}}
{{if .Entries}}
<div style="padding: 12px 20px; background: #f8fafc; border-bottom: 1px solid var(--border); margin-top: 24px;">
<span style="font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b;">Completed Today</span>
</div>
{{end}}
{{range .Entries}} {{range .Entries}}
<div class="prompt-item prompt-filled" data-entry-id="{{.ID}}"> <div class="prompt-item prompt-filled" data-entry-id="{{.ID}}">
<a href="#" class="prompt-dismiss" onclick="deleteEntry('{{.ID}}'); return false;" title="Delete">✕</a> <a href="#" class="prompt-dismiss" onclick="deleteEntry('{{.ID}}'); return false;" title="Delete">✕</a>
<div class="prompt-header"> <div class="prompt-header">
<span class="prompt-category">{{.Category}}</span> <div style="display: flex; justify-content: space-between; align-items: baseline;">
<span class="prompt-question">{{.Question}}</span> <span class="prompt-question">{{.Question}}</span>
<span class="prompt-saved-time">{{.TimeFormatted}}</span> <span class="prompt-saved-time">{{.TimeFormatted}}</span>
</div>
<div style="margin-top: 4px;">
<span class="prompt-category">{{.Category}}</span>
</div>
</div> </div>
<div class="prompt-body entry-readonly"> <div class="prompt-body entry-readonly">
{{range .Fields}} {{range .Fields}}
@ -138,65 +56,84 @@
{{if .SourceInput}}<div class="prompt-source">Created from: "{{.SourceInput}}"</div>{{end}} {{if .SourceInput}}<div class="prompt-source">Created from: "{{.SourceInput}}"</div>{{end}}
</div> </div>
{{end}} {{end}}
</div>
</div>
{{if .UpcomingPrompts}} {{/* 2. ALL PROMPTS - Unfilled prompts (due + upcoming) */}}
<div class="data-card" style="margin-top: 24px;"> {{range .AllPrompts}}
<div class="prompt-section-header"> {{if not .HasResponse}}
<div class="prompt-section-bar" style="background: #94a3b8;"></div> <div class="prompt-item prompt-pending" data-prompt-id="{{.ID}}">
<div class="prompt-section-info">
<div class="prompt-section-title">UPCOMING</div>
<div class="prompt-section-subtitle">{{len .UpcomingPrompts}} scheduled</div>
</div>
</div>
<div class="prompt-list">
{{range .UpcomingPrompts}}
{{$prompt := .}}
<div class="prompt-item prompt-item-future" data-prompt-id="{{.ID}}">
<a href="#" class="prompt-dismiss" onclick="showDismissConfirm(this, '{{.ID}}'); return false;" title="Don't ask again">✕</a> <a href="#" class="prompt-dismiss" onclick="showDismissConfirm(this, '{{.ID}}'); return false;" title="Don't ask again">✕</a>
<div class="dismiss-confirm"> <div class="dismiss-confirm">
<span>Stop tracking?</span> <span>Stop tracking?</span>
<a href="#" onclick="confirmDismiss('{{.ID}}'); return false;">Yes</a> <a href="#" onclick="confirmDismiss('{{.ID}}'); return false;">Yes</a>
<a href="#" onclick="hideDismissConfirm(this); return false;">No</a> <a href="#" onclick="hideDismissConfirm(this); return false;">No</a>
</div> </div>
<div class="prompt-header"> <form class="prompt-form" data-prompt-id="{{.ID}}">
<span class="prompt-category">{{.Category}}</span> <div class="prompt-header">
<span class="prompt-question">{{.Question}}</span> <div style="display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 12px;">
<span class="prompt-due">{{.NextAskFormatted}}</span> <span class="prompt-question">{{.Question}}</span>
</div> <div style="display: flex; gap: 12px; align-items: center;">
{{/* Show preview of what the input will look like */}} <span class="prompt-category">{{.Category}}</span>
{{if .Fields}} <span class="prompt-due">{{.NextAskFormatted}}</span>
<div class="prompt-body prompt-preview">
{{if eq (len .Fields) 1}}
{{with index .Fields 0}}
{{if eq .Type "number"}}
<div class="prompt-input-row">
<input type="number" disabled placeholder="Amount" class="prompt-input-number">
{{if .Unit}}<span class="prompt-unit">{{.Unit}}</span>{{end}}
</div> </div>
{{else if eq .Type "checkbox"}} </div>
<label class="prompt-checkbox"> </div>
<input type="checkbox" disabled> <div class="prompt-body">
<span class="prompt-checkbox-box"></span> {{if .Fields}}
<span class="prompt-checkbox-label">Yes</span> {{if eq (len .Fields) 1}}
</label> {{with index .Fields 0}}
{{if eq .Type "number"}}
<div class="prompt-input-row" style="justify-content: flex-start;">
<input type="number" name="field_{{.Key}}"
{{if .Min}}min="{{.Min}}"{{end}}
{{if .Max}}max="{{.Max}}"{{end}}
{{if .Step}}step="{{.Step}}"{{end}}
{{if .Value}}value="{{.Value}}"{{end}}
class="prompt-input-number"
style="max-width: 120px;"
onkeydown="if(event.key==='Enter'){event.preventDefault();saveItem(this.closest('.prompt-item'));}">
{{if .Unit}}<span class="prompt-unit">{{.Unit}}</span>{{end}}
<button type="button" class="btn-save" onclick="saveItem(this.closest('.prompt-item'))" style="margin-left: auto;">Save</button>
</div>
{{else if eq .Type "checkbox"}}
<div class="prompt-buttons">
<button type="button" class="prompt-btn" data-field="{{.Key}}" data-value="yes" onclick="selectYesNo(this)">Yes</button>
<button type="button" class="prompt-btn" data-field="{{.Key}}" data-value="no" onclick="selectYesNo(this)">No</button>
<input type="hidden" name="field_{{.Key}}" value="">
</div>
{{end}}
{{end}} {{end}}
{{end}} {{end}}
{{end}} {{end}}
</div> </div>
{{end}} </form>
{{if .HasResponse}} </div>
<div class="prompt-saved-footer"> {{end}}
<span class="prompt-saved-time">Last: {{if .LastResponseFormatted}}{{.LastResponseFormatted}}{{end}}</span> {{end}}
<span class="prompt-saved-value">{{.LastResponseRaw}}</span>
</div> {{/* 3. FREEFORM CARD - Always last */}}
{{end}} {{range .FreeformPrompts}}
<div class="prompt-item prompt-freeform" data-prompt-id="{{.ID}}">
<form class="prompt-form" data-prompt-id="{{.ID}}">
<div class="prompt-header">
<span class="prompt-question">{{.Question}}</span>
<span class="prompt-due">optional</span>
</div>
<div class="prompt-body">
<div style="display: flex; gap: 12px; align-items: flex-end;">
<textarea name="response_raw" class="prompt-textarea" rows="3" style="flex: 1;"
placeholder="Type what you want to track... (e.g., 'I take vitamin D every morning' or 'walked 30 minutes today')"
onkeydown="if(event.key==='Enter' && (event.metaKey || event.ctrlKey)){event.preventDefault();saveItem(this.closest('.prompt-item'));}"></textarea>
<button type="button" class="btn btn-primary" onclick="saveItem(this.closest('.prompt-item'))" style="align-self: flex-end;">Save</button>
</div>
<div class="prompt-hint" style="margin-top: 8px; text-align: right;">
<span id="kbd-hint">or press Ctrl+Enter</span>
</div>
</div>
</form>
</div> </div>
{{end}} {{end}}
</div> </div>
</div> </div>
{{end}}
{{else}} {{else}}
<div class="empty-state" style="padding: 48px 24px; text-align: center;"> <div class="empty-state" style="padding: 48px 24px; text-align: center;">
<p style="font-size: 1.1rem; margin-bottom: 12px;">✨ No tracking prompts yet</p> <p style="font-size: 1.1rem; margin-bottom: 12px;">✨ No tracking prompts yet</p>
@ -230,12 +167,6 @@
padding: 16px 20px; padding: 16px 20px;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
.prompt-section-bar {
width: 4px;
height: 36px;
background: var(--accent);
border-radius: 2px;
}
.prompt-section-title { .prompt-section-title {
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 600; font-weight: 600;
@ -304,10 +235,7 @@
color: var(--text-muted); color: var(--text-muted);
} }
.prompt-header { .prompt-header {
display: flex; margin-bottom: 8px;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
} }
.prompt-category { .prompt-category {
display: inline-block; display: inline-block;
@ -572,9 +500,6 @@
background: #fafafa; background: #fafafa;
border-left: 3px solid #64748b; border-left: 3px solid #64748b;
} }
.prompt-preview {
opacity: 0.5;
}
.prompt-preview input[disabled] { .prompt-preview input[disabled] {
cursor: not-allowed; cursor: not-allowed;
background: #f9fafb; background: #f9fafb;
@ -967,6 +892,11 @@ async function saveItem(item) {
// For freeform prompts, just clear and show notification // For freeform prompts, just clear and show notification
if (item.classList.contains('prompt-freeform')) { if (item.classList.contains('prompt-freeform')) {
form.querySelector('textarea').value = ''; form.querySelector('textarea').value = '';
if (saveBtn) {
saveBtn.classList.remove('saving');
saveBtn.textContent = originalText;
saveBtn.disabled = false;
}
showNotification('✓ Saved', 'success'); showNotification('✓ Saved', 'success');
// If LLM generated a new prompt, add it as a pending card // If LLM generated a new prompt, add it as a pending card