inou/lib/prompt.go

184 lines
4.8 KiB
Go

package lib
import (
"encoding/json"
"fmt"
"log"
"time"
)
// PromptAdd inserts a new prompt. Generates PromptID if empty.
func PromptAdd(p *Prompt) error {
if p.PromptID == "" {
p.PromptID = NewID()
}
now := time.Now().Unix()
if p.CreatedAt == 0 {
p.CreatedAt = now
}
p.UpdatedAt = now
if p.Active == false && p.Dismissed == false {
p.Active = true // default to active
}
return Save("prompts", p)
}
// PromptModify updates an existing prompt
func PromptModify(p *Prompt) error {
p.UpdatedAt = time.Now().Unix()
return Save("prompts", p)
}
// PromptDelete removes a prompt
func PromptDelete(promptID string) error {
return Delete("prompts", "prompt_id", promptID)
}
// PromptGet retrieves a single prompt by ID
func PromptGet(promptID string) (*Prompt, error) {
p := &Prompt{}
return p, Load("prompts", promptID, p)
}
// PromptQueryActive retrieves active prompts due for a dossier
func PromptQueryActive(dossierID string) ([]*Prompt, error) {
now := time.Now().Unix()
var result []*Prompt
err := Query(`SELECT * FROM prompts
WHERE dossier_id = ? AND active = 1 AND dismissed = 0
AND (expires_at = 0 OR expires_at > ?)
ORDER BY
CASE WHEN next_ask <= ? OR next_ask IS NULL OR input_type = 'freeform' THEN 0 ELSE 1 END,
next_ask, time_of_day`, []any{dossierID, now, now}, &result)
return result, err
}
// PromptQueryAll retrieves all prompts for a dossier (including inactive)
func PromptQueryAll(dossierID string) ([]*Prompt, error) {
var result []*Prompt
err := Query(`SELECT * FROM prompts WHERE dossier_id = ? ORDER BY active DESC, time_of_day, created_at`,
[]any{dossierID}, &result)
return result, err
}
// PromptRespond records a response and advances next_ask
func PromptRespond(promptID string, response, responseRaw string) error {
now := time.Now().Unix()
// Get current prompt to calculate next_ask
p, err := PromptGet(promptID)
if err != nil {
return err
}
p.LastResponse = response
p.LastResponseRaw = responseRaw
p.LastResponseAt = now
p.NextAsk = calculateNextAsk(p.Frequency, p.TimeOfDay, now)
p.UpdatedAt = now
if err := Save("prompts", p); err != nil {
return err
}
// Create entry for certain prompt types
if err := promptCreateEntry(p, response, now); err != nil {
// Log but don't fail the response
log.Printf("Failed to create entry for prompt %s: %v", promptID, err)
}
return nil
}
// promptCreateEntry creates an entry from a prompt response
// Uses the prompt's category/type directly - no hardcoded mappings
func promptCreateEntry(p *Prompt, response string, timestamp int64) error {
// Skip freeform/note types for now
if p.InputType == "freeform" {
return nil
}
// Entry inherits category/type from prompt
e := &Entry{
DossierID: p.DossierID,
Category: CategoryFromString[p.Category], // Prompt still uses string, convert here
Type: p.Type,
Value: responseToValue(response),
Timestamp: timestamp,
Data: fmt.Sprintf(`{"response":%s,"source":"prompt","prompt_id":"%s"}`, response, p.PromptID),
}
return EntryAdd(e)
}
// responseToValue converts JSON response to a human-readable value string
func responseToValue(response string) string {
var resp map[string]interface{}
if err := json.Unmarshal([]byte(response), &resp); err != nil {
return response // fallback to raw
}
// Single value
if v, ok := resp["value"]; ok {
return fmt.Sprintf("%v", v)
}
// Blood pressure style: systolic/diastolic
if sys, ok := resp["systolic"]; ok {
if dia, ok := resp["diastolic"]; ok {
return fmt.Sprintf("%v/%v", sys, dia)
}
}
// Fallback: join all values
var parts []string
for _, v := range resp {
parts = append(parts, fmt.Sprintf("%v", v))
}
if len(parts) > 0 {
return fmt.Sprintf("%v", parts[0]) // just first for now
}
return response
}
// PromptDismiss marks a prompt as dismissed
func PromptDismiss(promptID string) error {
p, err := PromptGet(promptID)
if err != nil {
return err
}
p.Dismissed = true
p.UpdatedAt = time.Now().Unix()
return Save("prompts", p)
}
// PromptSkip advances next_ask to tomorrow without recording a response
func PromptSkip(promptID string) error {
p, err := PromptGet(promptID)
if err != nil {
return err
}
now := time.Now().Unix()
p.NextAsk = now + 24*60*60
p.UpdatedAt = now
return Save("prompts", p)
}
// calculateNextAsk determines when to ask again based on frequency
func calculateNextAsk(frequency, timeOfDay string, now int64) int64 {
switch frequency {
case "once":
return 0 // never ask again (will be filtered by expires_at or dismissed)
case "daily":
return now + 24*60*60
case "twice_daily":
return now + 12*60*60
case "weekly":
return now + 7*24*60*60
case "until_resolved":
return now + 24*60*60 // ask daily until dismissed
default:
// Handle "weekly:mon,wed,fri" or other patterns later
return now + 24*60*60
}
}