refactor: prompts UI and LLM API cleanup
This commit is contained in:
parent
37b7602027
commit
3014f21d72
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue