refactor: rename prompt to tracker everywhere
- Rename prompts table to trackers
- Rename all Prompt types/functions to Tracker
- Rename prompt_id to tracker_id throughout
- Rename API endpoints /api/prompts -> /api/trackers
- Rename URL paths /dossier/{id}/prompts -> /dossier/{id}/trackers
- Rename template files and references
- Add migration script for schema changes
- Next: implement self-contained entries with metadata
This commit is contained in:
parent
9781b31c7d
commit
96fec23e22
|
|
@ -51,18 +51,18 @@ func loadLLMConfig() {
|
||||||
log.Println("Warning: Gemini API key not found.")
|
log.Println("Warning: Gemini API key not found.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize prompts directory
|
// Initialize trackers directory
|
||||||
exe, _ := os.Executable()
|
exe, _ := os.Executable()
|
||||||
promptsDir := filepath.Join(filepath.Dir(exe), "..", "api", "prompts")
|
promptsDir := filepath.Join(filepath.Dir(exe), "..", "api", "trackers")
|
||||||
if _, err := os.Stat(promptsDir); os.IsNotExist(err) {
|
if _, err := os.Stat(promptsDir); os.IsNotExist(err) {
|
||||||
promptsDir = "prompts" // Dev fallback
|
promptsDir = "trackers" // Dev fallback
|
||||||
}
|
}
|
||||||
lib.InitPrompts(promptsDir)
|
lib.InitPrompts(promptsDir)
|
||||||
log.Printf("Prompts directory set to: %s", lib.PromptsDir())
|
log.Printf("Prompts directory set to: %s", lib.TrackerPromptsDir())
|
||||||
}
|
}
|
||||||
|
|
||||||
// callLLMForPrompt is the main entry point for turning user text into a structured prompt.
|
// callLLMForTracker is the main entry point for turning user text into a structured prompt.
|
||||||
func callLLMForPrompt(userInput string, dossierID string) (*ExtractionResult, error) {
|
func callLLMForTracker(userInput string, dossierID string) (*ExtractionResult, error) {
|
||||||
triage, err := runTriage(userInput, dossierID)
|
triage, err := runTriage(userInput, dossierID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -71,7 +71,7 @@ func callLLMForPrompt(userInput string, dossierID string) (*ExtractionResult, er
|
||||||
return &ExtractionResult{Error: triage.Error}, nil
|
return &ExtractionResult{Error: triage.Error}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
existingTypes := getExistingPromptTypes(dossierID) // Assuming db is accessible in api/main
|
existingTypes := getExistingTrackerTypes(dossierID) // Assuming db is accessible in api/main
|
||||||
return runExtraction(userInput, triage.Category, triage.Language, dossierID, existingTypes)
|
return runExtraction(userInput, triage.Category, triage.Language, dossierID, existingTypes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,7 +79,7 @@ func callLLMForPrompt(userInput string, dossierID string) (*ExtractionResult, er
|
||||||
// --- Local Prompt Handling & DB Functions ---
|
// --- Local Prompt Handling & DB Functions ---
|
||||||
|
|
||||||
func loadPrompt(name string) (string, error) {
|
func loadPrompt(name string) (string, error) {
|
||||||
path := filepath.Join(lib.PromptsDir(), name+".md")
|
path := filepath.Join(lib.TrackerPromptsDir(), name+".md")
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
@ -233,10 +233,10 @@ func runExtraction(userInput, category, language, dossierID string, existingType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func getExistingPromptTypes(dossierID string) map[string][]string {
|
func getExistingTrackerTypes(dossierID string) map[string][]string {
|
||||||
result, err := lib.PromptDistinctTypes(dossierID)
|
result, err := lib.TrackerDistinctTypes(dossierID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to get existing prompt types: %v", err)
|
log.Printf("Failed to get existing tracker types: %v", err)
|
||||||
return make(map[string][]string)
|
return make(map[string][]string)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|
@ -252,6 +252,6 @@ func callSonnet(prompt string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func callSonnetWithRetry(prompt string, maxRetries int, baseDelay time.Duration) (string, error) {
|
func callSonnetWithRetry(prompt string, maxRetries int, baseDelay time.Duration) (string, error) {
|
||||||
// ... implementation remains the same, but is not called by the main prompt generation logic.
|
// ... implementation remains the same, but is not called by the main tracker generation logic.
|
||||||
return "", fmt.Errorf("callSonnet is deprecated")
|
return "", fmt.Errorf("callSonnet is deprecated")
|
||||||
}
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// PromptResponse is the API representation of a prompt, including dynamic data.
|
// PromptResponse is the API representation of a prompt, including dynamic data.
|
||||||
type PromptResponse struct {
|
type TrackerResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
|
@ -24,21 +24,21 @@ type PromptResponse struct {
|
||||||
Active bool `json:"active"`
|
Active bool `json:"active"`
|
||||||
IsDue bool `json:"is_due"`
|
IsDue bool `json:"is_due"`
|
||||||
|
|
||||||
// Last response (for pre-filling) - restored from lib.Prompt
|
// Last response (for pre-filling) - restored from lib.Tracker
|
||||||
LastResponse json.RawMessage `json:"last_response,omitempty"`
|
LastResponse json.RawMessage `json:"last_response,omitempty"`
|
||||||
LastResponseRaw string `json:"last_response_raw,omitempty"`
|
LastResponseRaw string `json:"last_response_raw,omitempty"`
|
||||||
LastResponseAt int64 `json:"last_response_at,omitempty"`
|
LastResponseAt int64 `json:"last_response_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PromptRespondRequest struct {
|
type TrackerRespondRequest struct {
|
||||||
PromptID string `json:"prompt_id"`
|
TrackerID string `json:"tracker_id"`
|
||||||
Response string `json:"response"` // JSON string
|
Response string `json:"response"` // JSON string
|
||||||
ResponseRaw string `json:"response_raw"` // what they typed
|
ResponseRaw string `json:"response_raw"` // what they typed
|
||||||
Action string `json:"action"` // "respond", "skip", "dismiss"
|
Action string `json:"action"` // "respond", "skip", "dismiss"
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/prompts?dossier=X
|
// GET /api/prompts?dossier=X
|
||||||
func handlePrompts(w http.ResponseWriter, r *http.Request) {
|
func handleTrackers(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
dossierHex := r.URL.Query().Get("dossier")
|
dossierHex := r.URL.Query().Get("dossier")
|
||||||
|
|
@ -48,13 +48,13 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
dossierID := dossierHex
|
dossierID := dossierHex
|
||||||
|
|
||||||
var prompts []*lib.Prompt
|
var trackers []*lib.Tracker
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if r.URL.Query().Get("all") == "1" {
|
if r.URL.Query().Get("all") == "1" {
|
||||||
prompts, err = lib.PromptQueryAll(dossierID)
|
trackers, err = lib.TrackerQueryAll(dossierID)
|
||||||
} else {
|
} else {
|
||||||
prompts, err = lib.PromptQueryActive(dossierID)
|
trackers, err = lib.TrackerQueryActive(dossierID)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
|
@ -63,14 +63,14 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// Ensure there's always a freeform prompt
|
// Ensure there's always a freeform prompt
|
||||||
hasFreeform := false
|
hasFreeform := false
|
||||||
for _, p := range prompts {
|
for _, p := range trackers {
|
||||||
if p.InputType == "freeform" && p.Active {
|
if p.InputType == "freeform" && p.Active {
|
||||||
hasFreeform = true
|
hasFreeform = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !hasFreeform {
|
if !hasFreeform {
|
||||||
freeform := &lib.Prompt{
|
freeform := &lib.Tracker{
|
||||||
DossierID: dossierID,
|
DossierID: dossierID,
|
||||||
Category: "note",
|
Category: "note",
|
||||||
Type: "freeform",
|
Type: "freeform",
|
||||||
|
|
@ -78,18 +78,18 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) {
|
||||||
InputType: "freeform",
|
InputType: "freeform",
|
||||||
Active: true,
|
Active: true,
|
||||||
}
|
}
|
||||||
if err := lib.PromptAdd(freeform); err == nil {
|
if err := lib.TrackerAdd(freeform); err == nil {
|
||||||
prompts = append(prompts, freeform)
|
trackers = append(trackers, freeform)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make([]PromptResponse, 0, len(prompts))
|
result := make([]TrackerResponse, 0, len(trackers))
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
for _, p := range prompts {
|
for _, p := range trackers {
|
||||||
isDue := p.NextAsk <= now || p.NextAsk == 0 || p.InputType == "freeform"
|
isDue := p.NextAsk <= now || p.NextAsk == 0 || p.InputType == "freeform"
|
||||||
|
|
||||||
pr := PromptResponse{
|
pr := TrackerResponse{
|
||||||
ID: p.PromptID,
|
ID: p.TrackerID,
|
||||||
Category: p.Category,
|
Category: p.Category,
|
||||||
Type: p.Type,
|
Type: p.Type,
|
||||||
Question: p.Question,
|
Question: p.Question,
|
||||||
|
|
@ -117,7 +117,7 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
|
||||||
// POST /api/prompts/respond
|
// POST /api/prompts/respond
|
||||||
func handlePromptRespond(w http.ResponseWriter, r *http.Request) {
|
func handleTrackerRespond(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
if r.Method != "POST" {
|
if r.Method != "POST" {
|
||||||
|
|
@ -125,32 +125,32 @@ func handlePromptRespond(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var req PromptRespondRequest
|
var req TrackerRespondRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
promptID := req.PromptID
|
trackerID := req.TrackerID
|
||||||
if promptID == "" {
|
if trackerID == "" {
|
||||||
http.Error(w, "prompt_id required", http.StatusBadRequest)
|
http.Error(w, "tracker_id required", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
var newPrompt *lib.Prompt
|
var newTracker *lib.Tracker
|
||||||
|
|
||||||
switch req.Action {
|
switch req.Action {
|
||||||
case "respond", "":
|
case "respond", "":
|
||||||
err = lib.PromptRespond(promptID, req.Response, req.ResponseRaw)
|
err = lib.TrackerRespond(trackerID, req.Response, req.ResponseRaw)
|
||||||
// Check if this is a freeform prompt - if so, generate new prompt from input
|
// Check if this is a freeform tracker - if so, generate new tracker from input
|
||||||
if err == nil && req.ResponseRaw != "" {
|
if err == nil && req.ResponseRaw != "" {
|
||||||
newPrompt, _ = tryGeneratePromptFromFreeform(promptID, req.ResponseRaw)
|
newTracker, _ = tryGenerateTrackerFromFreeform(trackerID, req.ResponseRaw)
|
||||||
}
|
}
|
||||||
case "skip":
|
case "skip":
|
||||||
err = lib.PromptSkip(promptID)
|
err = lib.TrackerSkip(trackerID)
|
||||||
case "dismiss":
|
case "dismiss":
|
||||||
err = lib.PromptDismiss(promptID)
|
err = lib.TrackerDismiss(trackerID)
|
||||||
default:
|
default:
|
||||||
http.Error(w, "invalid action", http.StatusBadRequest)
|
http.Error(w, "invalid action", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
|
@ -163,35 +163,35 @@ func handlePromptRespond(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
result := map[string]interface{}{
|
result := map[string]interface{}{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"prompt_id": req.PromptID,
|
"tracker_id": req.TrackerID,
|
||||||
"action": req.Action,
|
"action": req.Action,
|
||||||
}
|
}
|
||||||
if newPrompt != nil {
|
if newTracker != nil {
|
||||||
np := promptToAPI(newPrompt)
|
np := promptToAPI(newTracker)
|
||||||
result["new_prompt"] = np
|
result["new_prompt"] = np
|
||||||
}
|
}
|
||||||
json.NewEncoder(w).Encode(result)
|
json.NewEncoder(w).Encode(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Router for /api/prompts and /api/prompts/*
|
// Router for /api/prompts and /api/prompts/*
|
||||||
func handlePromptsRouter(w http.ResponseWriter, r *http.Request) {
|
func handleTrackersRouter(w http.ResponseWriter, r *http.Request) {
|
||||||
path := r.URL.Path
|
path := r.URL.Path
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case path == "/api/prompts" && r.Method == "GET":
|
case path == "/api/prompts" && r.Method == "GET":
|
||||||
handlePrompts(w, r)
|
handleTrackers(w, r)
|
||||||
case path == "/api/prompts" && r.Method == "POST":
|
case path == "/api/prompts" && r.Method == "POST":
|
||||||
handlePromptCreate(w, r)
|
handleTrackerCreate(w, r)
|
||||||
case path == "/api/prompts/respond":
|
case path == "/api/prompts/respond":
|
||||||
handlePromptRespond(w, r)
|
handleTrackerRespond(w, r)
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tryGeneratePromptFromFreeform checks if the prompt is freeform and generates a new prompt from user input
|
// tryGenerateTrackerFromFreeform checks if the tracker is freeform and generates a new tracker from user input
|
||||||
func tryGeneratePromptFromFreeform(promptID string, userInput string) (*lib.Prompt, string) {
|
func tryGenerateTrackerFromFreeform(trackerID string, userInput string) (*lib.Tracker, string) {
|
||||||
p, err := lib.PromptGet(promptID)
|
p, err := lib.TrackerGet(trackerID)
|
||||||
if err != nil || p == nil {
|
if err != nil || p == nil {
|
||||||
return nil, ""
|
return nil, ""
|
||||||
}
|
}
|
||||||
|
|
@ -200,9 +200,9 @@ func tryGeneratePromptFromFreeform(promptID string, userInput string) (*lib.Prom
|
||||||
return nil, ""
|
return nil, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
generated, err := callLLMForPrompt(userInput, p.DossierID)
|
generated, err := callLLMForTracker(userInput, p.DossierID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to generate prompt from freeform: %v", err)
|
log.Printf("Failed to generate tracker from freeform: %v", err)
|
||||||
return nil, ""
|
return nil, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -254,7 +254,7 @@ func tryGeneratePromptFromFreeform(promptID string, userInput string) (*lib.Prom
|
||||||
scheduleJSON, _ := json.Marshal(generated.Schedule)
|
scheduleJSON, _ := json.Marshal(generated.Schedule)
|
||||||
inputConfigJSON, _ := json.Marshal(generated.InputConfig)
|
inputConfigJSON, _ := json.Marshal(generated.InputConfig)
|
||||||
|
|
||||||
newPrompt := &lib.Prompt{
|
newTracker := &lib.Tracker{
|
||||||
DossierID: p.DossierID,
|
DossierID: p.DossierID,
|
||||||
Category: generated.Category,
|
Category: generated.Category,
|
||||||
Type: generated.Type,
|
Type: generated.Type,
|
||||||
|
|
@ -270,25 +270,25 @@ func tryGeneratePromptFromFreeform(promptID string, userInput string) (*lib.Prom
|
||||||
// NOTE: Don't set LastResponse here - backfilled entries are historical.
|
// NOTE: Don't set LastResponse here - backfilled entries are historical.
|
||||||
// LastResponse will be set when user actually submits a response for "today".
|
// LastResponse will be set when user actually submits a response for "today".
|
||||||
|
|
||||||
if err := lib.PromptAdd(newPrompt); err != nil {
|
if err := lib.TrackerAdd(newTracker); err != nil {
|
||||||
log.Printf("Failed to create prompt: %v", err)
|
log.Printf("Failed to create prompt: %v", err)
|
||||||
return nil, ""
|
return nil, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the entries we just created to link them to this prompt via SearchKey
|
// Update the entries we just created to link them to this tracker via SearchKey
|
||||||
for _, entry := range createdEntries {
|
for _, entry := range createdEntries {
|
||||||
entry.SearchKey = newPrompt.PromptID
|
entry.SearchKey = newTracker.TrackerID
|
||||||
if err := lib.EntryAdd(entry); err != nil {
|
if err := lib.EntryAdd(entry); err != nil {
|
||||||
log.Printf("Failed to update entry search_key: %v", err)
|
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))
|
log.Printf("Created tracker from freeform: %s (%s/%s) and linked %d entries", newTracker.Question, newTracker.Category, newTracker.Type, len(createdEntries))
|
||||||
return newPrompt, primaryEntryValue
|
return newTracker, primaryEntryValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// promptToAPI converts a Prompt to API response format, now much simpler
|
// promptToAPI converts a Prompt to API response format, now much simpler
|
||||||
func promptToAPI(p *lib.Prompt) map[string]interface{} {
|
func promptToAPI(p *lib.Tracker) map[string]interface{} {
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
isDue := p.NextAsk <= now || p.NextAsk == 0 || p.InputType == "freeform"
|
isDue := p.NextAsk <= now || p.NextAsk == 0 || p.InputType == "freeform"
|
||||||
|
|
||||||
|
|
@ -296,7 +296,7 @@ func promptToAPI(p *lib.Prompt) map[string]interface{} {
|
||||||
json.Unmarshal([]byte(p.InputConfig), &inputConfig)
|
json.Unmarshal([]byte(p.InputConfig), &inputConfig)
|
||||||
|
|
||||||
result := map[string]interface{}{
|
result := map[string]interface{}{
|
||||||
"id": p.PromptID,
|
"id": p.TrackerID,
|
||||||
"category": p.Category,
|
"category": p.Category,
|
||||||
"type": p.Type,
|
"type": p.Type,
|
||||||
"question": p.Question,
|
"question": p.Question,
|
||||||
|
|
@ -311,7 +311,7 @@ func promptToAPI(p *lib.Prompt) map[string]interface{} {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func handlePromptCreate(w http.ResponseWriter, r *http.Request) {
|
func handleTrackerCreate(w http.ResponseWriter, r *http.Request) {
|
||||||
// This function needs to be updated to use the new Prompt struct
|
// This function needs to be updated to use the new Prompt struct
|
||||||
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
||||||
}
|
}
|
||||||
|
|
@ -414,7 +414,7 @@ type ParseEntryResponse struct {
|
||||||
Timestamp int64 `json:"timestamp"`
|
Timestamp int64 `json:"timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ParsePromptResponse struct {
|
type ParseTrackerResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
|
@ -428,7 +428,7 @@ type ParseResponse struct {
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Entries []ParseEntryResponse `json:"entries"`
|
Entries []ParseEntryResponse `json:"entries"`
|
||||||
Prompt *ParsePromptResponse `json:"prompt,omitempty"`
|
Tracker *ParseTrackerResponse `json:"prompt,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -460,7 +460,7 @@ func v1Parse(w http.ResponseWriter, r *http.Request, dossierID string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run triage + extraction
|
// Run triage + extraction
|
||||||
generated, err := callLLMForPrompt(req.Input, dossierID)
|
generated, err := callLLMForTracker(req.Input, dossierID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v1Error(w, err.Error(), http.StatusInternalServerError)
|
v1Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
@ -505,13 +505,13 @@ func v1Parse(w http.ResponseWriter, r *http.Request, dossierID string) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create prompt if there's a follow-up question with schedule
|
// Create tracker if there's a follow-up question with schedule
|
||||||
if generated.Question != "" && len(generated.Schedule) > 0 {
|
if generated.Question != "" && len(generated.Schedule) > 0 {
|
||||||
nextAsk := calculateNextAskFromSchedule(generated.Schedule, now)
|
nextAsk := calculateNextAskFromSchedule(generated.Schedule, now)
|
||||||
scheduleJSON, _ := json.Marshal(generated.Schedule)
|
scheduleJSON, _ := json.Marshal(generated.Schedule)
|
||||||
inputConfigJSON, _ := json.Marshal(generated.InputConfig)
|
inputConfigJSON, _ := json.Marshal(generated.InputConfig)
|
||||||
|
|
||||||
prompt := &lib.Prompt{
|
tracker := &lib.Tracker{
|
||||||
DossierID: dossierID,
|
DossierID: dossierID,
|
||||||
Category: generated.Category,
|
Category: generated.Category,
|
||||||
Type: generated.Type,
|
Type: generated.Type,
|
||||||
|
|
@ -527,23 +527,23 @@ func v1Parse(w http.ResponseWriter, r *http.Request, dossierID string) {
|
||||||
// Pre-fill last response from initial extracted data
|
// Pre-fill last response from initial extracted data
|
||||||
if len(generated.Entries) > 0 && generated.Entries[0].Data != nil {
|
if len(generated.Entries) > 0 && generated.Entries[0].Data != nil {
|
||||||
initialData, _ := json.Marshal(generated.Entries[0].Data)
|
initialData, _ := json.Marshal(generated.Entries[0].Data)
|
||||||
prompt.LastResponse = string(initialData)
|
tracker.LastResponse = string(initialData)
|
||||||
prompt.LastResponseRaw = generated.Entries[0].Value
|
tracker.LastResponseRaw = generated.Entries[0].Value
|
||||||
prompt.LastResponseAt = now.Unix()
|
tracker.LastResponseAt = now.Unix()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := lib.PromptAdd(prompt); err == nil {
|
if err := lib.TrackerAdd(tracker); err == nil {
|
||||||
var inputConfig any
|
var inputConfig any
|
||||||
json.Unmarshal([]byte(prompt.InputConfig), &inputConfig)
|
json.Unmarshal([]byte(tracker.InputConfig), &inputConfig)
|
||||||
|
|
||||||
resp.Prompt = &ParsePromptResponse{
|
resp.Tracker = &ParseTrackerResponse{
|
||||||
ID: prompt.PromptID,
|
ID: tracker.TrackerID,
|
||||||
Category: prompt.Category,
|
Category: tracker.Category,
|
||||||
Type: prompt.Type,
|
Type: tracker.Type,
|
||||||
Question: prompt.Question,
|
Question: tracker.Question,
|
||||||
InputType: prompt.InputType,
|
InputType: tracker.InputType,
|
||||||
InputConfig: inputConfig,
|
InputConfig: inputConfig,
|
||||||
NextAsk: prompt.NextAsk,
|
NextAsk: tracker.NextAsk,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -564,7 +564,7 @@ func v1Prompts(w http.ResponseWriter, r *http.Request, dossierID string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
q := r.URL.Query()
|
q := r.URL.Query()
|
||||||
filter := &lib.PromptFilter{DossierID: dossierID}
|
filter := &lib.TrackerFilter{DossierID: dossierID}
|
||||||
if cat := q.Get("category"); cat != "" {
|
if cat := q.Get("category"); cat != "" {
|
||||||
filter.Category = cat
|
filter.Category = cat
|
||||||
}
|
}
|
||||||
|
|
@ -575,16 +575,16 @@ func v1Prompts(w http.ResponseWriter, r *http.Request, dossierID string) {
|
||||||
filter.ActiveOnly = true
|
filter.ActiveOnly = true
|
||||||
}
|
}
|
||||||
|
|
||||||
prompts, err := lib.PromptList(filter)
|
trackers, err := lib.TrackerList(filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v1Error(w, err.Error(), http.StatusInternalServerError)
|
v1Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []map[string]any
|
var result []map[string]any
|
||||||
for _, p := range prompts {
|
for _, p := range trackers {
|
||||||
result = append(result, map[string]any{
|
result = append(result, map[string]any{
|
||||||
"id": p.PromptID,
|
"id": p.TrackerID,
|
||||||
"category": p.Category,
|
"category": p.Category,
|
||||||
"type": p.Type,
|
"type": p.Type,
|
||||||
"question": p.Question,
|
"question": p.Question,
|
||||||
|
|
@ -766,7 +766,7 @@ func v1Router(w http.ResponseWriter, r *http.Request) {
|
||||||
v1Audit(w, r, parts[1])
|
v1Audit(w, r, parts[1])
|
||||||
|
|
||||||
// GET /dossiers/{id}/prompts
|
// GET /dossiers/{id}/prompts
|
||||||
case len(parts) == 3 && parts[0] == "dossiers" && parts[2] == "prompts" && r.Method == "GET":
|
case len(parts) == 3 && parts[0] == "dossiers" && parts[2] == "trackers" && r.Method == "GET":
|
||||||
v1Prompts(w, r, parts[1])
|
v1Prompts(w, r, parts[1])
|
||||||
|
|
||||||
// POST /dossiers/{id}/parse
|
// POST /dossiers/{id}/parse
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ type FormField struct {
|
||||||
Options []string `json:"options,omitempty"`
|
Options []string `json:"options,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScheduleSlot defines when a prompt should be shown.
|
// ScheduleSlot defines when a tracker should be shown.
|
||||||
// Supports both old format (Time string) and new format (Times []string).
|
// Supports both old format (Time string) and new format (Times []string).
|
||||||
type ScheduleSlot struct {
|
type ScheduleSlot struct {
|
||||||
Days []string `json:"days"`
|
Days []string `json:"days"`
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,8 @@ func main() {
|
||||||
http.HandleFunc("/api/access", handleAccess)
|
http.HandleFunc("/api/access", handleAccess)
|
||||||
http.HandleFunc("/api/audit", handleAudit)
|
http.HandleFunc("/api/audit", handleAudit)
|
||||||
http.HandleFunc("/api/entries", handleEntries)
|
http.HandleFunc("/api/entries", handleEntries)
|
||||||
http.HandleFunc("/api/prompts", handlePromptsRouter)
|
http.HandleFunc("/api/prompts", handleTrackersRouter)
|
||||||
http.HandleFunc("/api/prompts/", handlePromptsRouter)
|
http.HandleFunc("/api/prompts/", handleTrackersRouter)
|
||||||
// http.HandleFunc("/api/prompt/generate", handlePromptGenerate) // REMOVED: Deprecated
|
// http.HandleFunc("/api/prompt/generate", handlePromptGenerate) // REMOVED: Deprecated
|
||||||
|
|
||||||
// Add the missing freeform handler
|
// Add the missing freeform handler
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,15 @@ import (
|
||||||
|
|
||||||
var promptsDir string
|
var promptsDir string
|
||||||
|
|
||||||
// InitPrompts sets the directory where prompt files are located.
|
// InitPrompts sets the directory where tracker files are located.
|
||||||
// This must be called by the main application at startup.
|
// This must be called by the main application at startup.
|
||||||
func InitPrompts(path string) {
|
func InitPrompts(path string) {
|
||||||
promptsDir = path
|
promptsDir = path
|
||||||
}
|
}
|
||||||
|
|
||||||
// PromptsDir returns the configured prompts directory.
|
// TrackerPromptsDir returns the configured trackers directory.
|
||||||
// This is used by local prompt loading functions in consumer packages.
|
// This is used by local tracker loading functions in consumer packages.
|
||||||
func PromptsDir() string {
|
func TrackerPromptsDir() string {
|
||||||
return promptsDir
|
return promptsDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PromptAdd inserts a new prompt. Generates PromptID if empty.
|
// TrackerAdd inserts a new prompt. Generates TrackerID if empty.
|
||||||
func PromptAdd(p *Prompt) error {
|
func TrackerAdd(p *Tracker) error {
|
||||||
if p.PromptID == "" {
|
if p.TrackerID == "" {
|
||||||
p.PromptID = NewID()
|
p.TrackerID = NewID()
|
||||||
}
|
}
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
if p.CreatedAt == 0 {
|
if p.CreatedAt == 0 {
|
||||||
|
|
@ -20,31 +20,31 @@ func PromptAdd(p *Prompt) error {
|
||||||
if p.Active == false && p.Dismissed == false {
|
if p.Active == false && p.Dismissed == false {
|
||||||
p.Active = true // default to active
|
p.Active = true // default to active
|
||||||
}
|
}
|
||||||
return Save("prompts", p)
|
return Save("trackers", p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PromptModify updates an existing prompt
|
// TrackerModify updates an existing prompt
|
||||||
func PromptModify(p *Prompt) error {
|
func TrackerModify(p *Tracker) error {
|
||||||
p.UpdatedAt = time.Now().Unix()
|
p.UpdatedAt = time.Now().Unix()
|
||||||
return Save("prompts", p)
|
return Save("trackers", p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PromptDelete removes a prompt
|
// TrackerDelete removes a prompt
|
||||||
func PromptDelete(promptID string) error {
|
func TrackerDelete(trackerID string) error {
|
||||||
return Delete("prompts", "prompt_id", promptID)
|
return Delete("trackers", "tracker_id", trackerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PromptGet retrieves a single prompt by ID
|
// TrackerGet retrieves a single tracker by ID
|
||||||
func PromptGet(promptID string) (*Prompt, error) {
|
func TrackerGet(trackerID string) (*Tracker, error) {
|
||||||
p := &Prompt{}
|
p := &Tracker{}
|
||||||
return p, Load("prompts", promptID, p)
|
return p, Load("trackers", trackerID, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PromptQueryActive retrieves active prompts due for a dossier
|
// TrackerQueryActive retrieves active trackers due for a dossier
|
||||||
func PromptQueryActive(dossierID string) ([]*Prompt, error) {
|
func TrackerQueryActive(dossierID string) ([]*Tracker, error) {
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
var result []*Prompt
|
var result []*Tracker
|
||||||
err := Query(`SELECT * FROM prompts
|
err := Query(`SELECT * FROM trackers
|
||||||
WHERE dossier_id = ? AND active = 1 AND dismissed = 0
|
WHERE dossier_id = ? AND active = 1 AND dismissed = 0
|
||||||
AND (expires_at = 0 OR expires_at > ?)
|
AND (expires_at = 0 OR expires_at > ?)
|
||||||
ORDER BY
|
ORDER BY
|
||||||
|
|
@ -53,20 +53,20 @@ func PromptQueryActive(dossierID string) ([]*Prompt, error) {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// PromptQueryAll retrieves all prompts for a dossier (including inactive)
|
// TrackerQueryAll retrieves all trackers for a dossier (including inactive)
|
||||||
func PromptQueryAll(dossierID string) ([]*Prompt, error) {
|
func TrackerQueryAll(dossierID string) ([]*Tracker, error) {
|
||||||
var result []*Prompt
|
var result []*Tracker
|
||||||
err := Query(`SELECT * FROM prompts WHERE dossier_id = ? ORDER BY active DESC, time_of_day, created_at`,
|
err := Query(`SELECT * FROM trackers WHERE dossier_id = ? ORDER BY active DESC, time_of_day, created_at`,
|
||||||
[]any{dossierID}, &result)
|
[]any{dossierID}, &result)
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// PromptRespond records a response and advances next_ask
|
// TrackerRespond records a response and advances next_ask
|
||||||
func PromptRespond(promptID string, response, responseRaw string) error {
|
func TrackerRespond(trackerID string, response, responseRaw string) error {
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
|
|
||||||
// Get current prompt to calculate next_ask
|
// Get current tracker to calculate next_ask
|
||||||
p, err := PromptGet(promptID)
|
p, err := TrackerGet(trackerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -77,22 +77,22 @@ func PromptRespond(promptID string, response, responseRaw string) error {
|
||||||
p.NextAsk = calculateNextAsk(p.Frequency, p.TimeOfDay, now)
|
p.NextAsk = calculateNextAsk(p.Frequency, p.TimeOfDay, now)
|
||||||
p.UpdatedAt = now
|
p.UpdatedAt = now
|
||||||
|
|
||||||
if err := Save("prompts", p); err != nil {
|
if err := Save("trackers", p); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create entry for certain prompt types
|
// Create entry for certain tracker types
|
||||||
if err := promptCreateEntry(p, response, now); err != nil {
|
if err := trackerCreateEntry(p, response, now); err != nil {
|
||||||
// Log but don't fail the response
|
// Log but don't fail the response
|
||||||
log.Printf("Failed to create entry for prompt %s: %v", promptID, err)
|
log.Printf("Failed to create entry for tracker %s: %v", trackerID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// promptCreateEntry creates an entry from a prompt response
|
// trackerCreateEntry creates an entry from a tracker response
|
||||||
// Uses the prompt's category/type directly - no hardcoded mappings
|
// Uses the prompt's category/type directly - no hardcoded mappings
|
||||||
func promptCreateEntry(p *Prompt, response string, timestamp int64) error {
|
func trackerCreateEntry(p *Tracker, response string, timestamp int64) error {
|
||||||
// Skip freeform/note types for now
|
// Skip freeform/note types for now
|
||||||
if p.InputType == "freeform" {
|
if p.InputType == "freeform" {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -105,8 +105,8 @@ func promptCreateEntry(p *Prompt, response string, timestamp int64) error {
|
||||||
Type: p.Type,
|
Type: p.Type,
|
||||||
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","tracker_id":"%s"}`, response, p.TrackerID),
|
||||||
SearchKey: p.PromptID, // Foreign key to link entry back to its prompt
|
SearchKey: p.TrackerID, // Foreign key to link entry back to its prompt
|
||||||
}
|
}
|
||||||
return EntryAdd(e)
|
return EntryAdd(e)
|
||||||
}
|
}
|
||||||
|
|
@ -141,27 +141,27 @@ func responseToValue(response string) string {
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
// PromptDismiss marks a prompt as dismissed
|
// TrackerDismiss marks a tracker as dismissed
|
||||||
func PromptDismiss(promptID string) error {
|
func TrackerDismiss(trackerID string) error {
|
||||||
p, err := PromptGet(promptID)
|
p, err := TrackerGet(trackerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.Dismissed = true
|
p.Dismissed = true
|
||||||
p.UpdatedAt = time.Now().Unix()
|
p.UpdatedAt = time.Now().Unix()
|
||||||
return Save("prompts", p)
|
return Save("trackers", p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PromptSkip advances next_ask to tomorrow without recording a response
|
// TrackerSkip advances next_ask to tomorrow without recording a response
|
||||||
func PromptSkip(promptID string) error {
|
func TrackerSkip(trackerID string) error {
|
||||||
p, err := PromptGet(promptID)
|
p, err := TrackerGet(trackerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
p.NextAsk = now + 24*60*60
|
p.NextAsk = now + 24*60*60
|
||||||
p.UpdatedAt = now
|
p.UpdatedAt = now
|
||||||
return Save("prompts", p)
|
return Save("trackers", p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculateNextAsk determines when to ask again based on frequency
|
// calculateNextAsk determines when to ask again based on frequency
|
||||||
|
|
@ -344,8 +344,8 @@ type AuditEntry struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompt represents a scheduled question or tracker (decrypted)
|
// Prompt represents a scheduled question or tracker (decrypted)
|
||||||
type Prompt struct {
|
type Tracker struct {
|
||||||
PromptID string `db:"prompt_id,pk"`
|
TrackerID string `db:"tracker_id,pk"`
|
||||||
DossierID string `db:"dossier_id"`
|
DossierID string `db:"dossier_id"`
|
||||||
Category string `db:"category"`
|
Category string `db:"category"`
|
||||||
Type string `db:"type"`
|
Type string `db:"type"`
|
||||||
|
|
|
||||||
38
lib/v2.go
38
lib/v2.go
|
|
@ -481,7 +481,7 @@ func AuditList(f *AuditFilter) ([]*AuditEntry, error) {
|
||||||
|
|
||||||
// --- PROMPT ---
|
// --- PROMPT ---
|
||||||
|
|
||||||
type PromptFilter struct {
|
type TrackerFilter struct {
|
||||||
DossierID string
|
DossierID string
|
||||||
Category string
|
Category string
|
||||||
Type string
|
Type string
|
||||||
|
|
@ -489,27 +489,27 @@ type PromptFilter struct {
|
||||||
Limit int
|
Limit int
|
||||||
}
|
}
|
||||||
|
|
||||||
func PromptWrite(prompts ...*Prompt) error {
|
func TrackerWrite(trackers ...*Tracker) error {
|
||||||
if len(prompts) == 0 {
|
if len(trackers) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for _, p := range prompts {
|
for _, p := range trackers {
|
||||||
if p.PromptID == "" {
|
if p.TrackerID == "" {
|
||||||
p.PromptID = NewID()
|
p.TrackerID = NewID()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(prompts) == 1 {
|
if len(trackers) == 1 {
|
||||||
return Save("prompts", prompts[0])
|
return Save("trackers", trackers[0])
|
||||||
}
|
}
|
||||||
return Save("prompts", prompts)
|
return Save("trackers", trackers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func PromptRemove(ids ...string) error {
|
func TrackerRemove(ids ...string) error {
|
||||||
return deleteByIDs("prompts", "prompt_id", ids)
|
return deleteByIDs("trackers", "tracker_id", ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
func PromptList(f *PromptFilter) ([]*Prompt, error) {
|
func TrackerList(f *TrackerFilter) ([]*Tracker, error) {
|
||||||
q := "SELECT * FROM prompts WHERE 1=1"
|
q := "SELECT * FROM trackers WHERE 1=1"
|
||||||
args := []any{}
|
args := []any{}
|
||||||
|
|
||||||
if f != nil {
|
if f != nil {
|
||||||
|
|
@ -536,7 +536,7 @@ func PromptList(f *PromptFilter) ([]*Prompt, error) {
|
||||||
q += fmt.Sprintf(" LIMIT %d", f.Limit)
|
q += fmt.Sprintf(" LIMIT %d", f.Limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []*Prompt
|
var result []*Tracker
|
||||||
err := Query(q, args, &result)
|
err := Query(q, args, &result)
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
@ -747,17 +747,17 @@ func AccessListByTargetWithNames(targetID string) ([]map[string]interface{}, err
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PromptDistinctTypes returns distinct category/type pairs for a dossier's active prompts
|
// TrackerDistinctTypes returns distinct category/type pairs for a dossier's active trackers
|
||||||
func PromptDistinctTypes(dossierID string) (map[string][]string, error) {
|
func TrackerDistinctTypes(dossierID string) (map[string][]string, error) {
|
||||||
var prompts []*Prompt
|
var trackers []*Tracker
|
||||||
if err := Query("SELECT * FROM prompts WHERE dossier_id = ? AND active = 1", []any{dossierID}, &prompts); err != nil {
|
if err := Query("SELECT * FROM trackers WHERE dossier_id = ? AND active = 1", []any{dossierID}, &trackers); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract distinct category/type pairs
|
// Extract distinct category/type pairs
|
||||||
seen := make(map[string]bool)
|
seen := make(map[string]bool)
|
||||||
result := make(map[string][]string)
|
result := make(map[string][]string)
|
||||||
for _, p := range prompts {
|
for _, p := range trackers {
|
||||||
key := p.Category + "|" + p.Type
|
key := p.Category + "|" + p.Type
|
||||||
if !seen[key] && p.Category != "" && p.Type != "" {
|
if !seen[key] && p.Category != "" && p.Type != "" {
|
||||||
seen[key] = true
|
seen[key] = true
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ func initMobileAPI(mux *http.ServeMux) {
|
||||||
mux.HandleFunc("/api/v1/auth/verify", handleAPIVerify)
|
mux.HandleFunc("/api/v1/auth/verify", handleAPIVerify)
|
||||||
mux.HandleFunc("/api/v1/dashboard", handleAPIDashboard)
|
mux.HandleFunc("/api/v1/dashboard", handleAPIDashboard)
|
||||||
mux.HandleFunc("/api/v1/prompts", handleAPIPrompts)
|
mux.HandleFunc("/api/v1/prompts", handleAPIPrompts)
|
||||||
mux.HandleFunc("/api/v1/prompts/respond", handleAPIPromptRespond)
|
mux.HandleFunc("/api/v1/prompts/respond", handleAPITrackerRespond)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Auth ---
|
// --- Auth ---
|
||||||
|
|
@ -221,10 +221,10 @@ func handleAPIPrompts(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// TODO: Implement per spec
|
// TODO: Implement per spec
|
||||||
jsonOK(w, map[string]interface{}{"prompts": []interface{}{}})
|
jsonOK(w, map[string]interface{}{"trackers": []interface{}{}})
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleAPIPromptRespond(w http.ResponseWriter, r *http.Request) {
|
func handleAPITrackerRespond(w http.ResponseWriter, r *http.Request) {
|
||||||
if cors(w, r) { return }
|
if cors(w, r) { return }
|
||||||
d := getAPIAuth(r)
|
d := getAPIAuth(r)
|
||||||
if d == nil {
|
if d == nil {
|
||||||
|
|
|
||||||
|
|
@ -32,11 +32,11 @@ type DossierSection struct {
|
||||||
// Checkin-specific: show "build your profile" prompt
|
// Checkin-specific: show "build your profile" prompt
|
||||||
ShowBuildPrompt bool // true if trackable categories are empty
|
ShowBuildPrompt bool // true if trackable categories are empty
|
||||||
TrackableStats map[string]int // counts for trackable categories
|
TrackableStats map[string]int // counts for trackable categories
|
||||||
PromptButtons []PromptButton // buttons for empty trackable categories
|
TrackerButtons []TrackerButton // buttons for empty trackable categories
|
||||||
}
|
}
|
||||||
|
|
||||||
// PromptButton for the "build your profile" section
|
// TrackerButton for the "build your profile" section
|
||||||
type PromptButton struct {
|
type TrackerButton struct {
|
||||||
Label string
|
Label string
|
||||||
Icon string
|
Icon string
|
||||||
URL string
|
URL string
|
||||||
|
|
@ -150,7 +150,7 @@ func BuildDossierSections(targetID, targetHex string, target *lib.Dossier, p *li
|
||||||
section.ShowBuildPrompt = true
|
section.ShowBuildPrompt = true
|
||||||
section.Summary = T("checkin_build_profile")
|
section.Summary = T("checkin_build_profile")
|
||||||
promptsURL := fmt.Sprintf("/dossier/%s/prompts", targetHex)
|
promptsURL := fmt.Sprintf("/dossier/%s/prompts", targetHex)
|
||||||
section.PromptButtons = []PromptButton{
|
section.TrackerButtons = []TrackerButton{
|
||||||
{Label: T("btn_vitals"), URL: promptsURL + "?add=vital"},
|
{Label: T("btn_vitals"), URL: promptsURL + "?add=vital"},
|
||||||
{Label: T("btn_medications"), URL: promptsURL + "?add=medication"},
|
{Label: T("btn_medications"), URL: promptsURL + "?add=medication"},
|
||||||
{Label: T("btn_supplements"), URL: promptsURL + "?add=supplement"},
|
{Label: T("btn_supplements"), URL: promptsURL + "?add=supplement"},
|
||||||
|
|
|
||||||
|
|
@ -2005,9 +2005,9 @@ func setupMux() http.Handler {
|
||||||
} else if strings.HasSuffix(path, "/permissions") { handlePermissions(w, r)
|
} else if strings.HasSuffix(path, "/permissions") { handlePermissions(w, r)
|
||||||
} else if strings.Contains(path, "/rbac/") { handleEditRBAC(w, r)
|
} else if strings.Contains(path, "/rbac/") { handleEditRBAC(w, r)
|
||||||
} else if strings.Contains(path, "/access/") { handleEditAccess(w, r)
|
} else if strings.Contains(path, "/access/") { handleEditAccess(w, r)
|
||||||
} else if strings.HasSuffix(path, "/prompts") { handlePrompts(w, r)
|
} else if strings.HasSuffix(path, "/prompts") { handleTrackers(w, r)
|
||||||
} else if strings.Contains(path, "/prompts/card/") { handleRenderPromptCard(w, r)
|
} else if strings.Contains(path, "/prompts/card/") { handleRenderTrackerCard(w, r)
|
||||||
} else if strings.HasSuffix(path, "/prompts/respond") { handlePromptRespond(w, r)
|
} else if strings.HasSuffix(path, "/prompts/respond") { handleTrackerRespond(w, r)
|
||||||
} else if strings.HasSuffix(path, "/upload") { if r.Method == "POST" { handleUploadPost(w, r) } else { handleUploadPage(w, r) }
|
} else if strings.HasSuffix(path, "/upload") { if r.Method == "POST" { handleUploadPost(w, r) } else { handleUploadPage(w, r) }
|
||||||
} else if strings.Contains(path, "/files/") && strings.HasSuffix(path, "/delete") { handleDeleteFile(w, r)
|
} else if strings.Contains(path, "/files/") && strings.HasSuffix(path, "/delete") { handleDeleteFile(w, r)
|
||||||
} else if strings.Contains(path, "/files/") && strings.HasSuffix(path, "/update") { handleUpdateFile(w, r)
|
} else if strings.Contains(path, "/files/") && strings.HasSuffix(path, "/update") { handleUpdateFile(w, r)
|
||||||
|
|
@ -2028,7 +2028,7 @@ func setupMux() http.Handler {
|
||||||
mux.HandleFunc("/api/v1/auth/verify", handleAPIVerify)
|
mux.HandleFunc("/api/v1/auth/verify", handleAPIVerify)
|
||||||
mux.HandleFunc("/api/v1/dashboard", handleAPIDashboard)
|
mux.HandleFunc("/api/v1/dashboard", handleAPIDashboard)
|
||||||
mux.HandleFunc("/api/v1/prompts", handleAPIPrompts)
|
mux.HandleFunc("/api/v1/prompts", handleAPIPrompts)
|
||||||
mux.HandleFunc("/api/v1/prompts/respond", handleAPIPromptRespond)
|
mux.HandleFunc("/api/v1/prompts/respond", handleAPITrackerRespond)
|
||||||
|
|
||||||
mux.HandleFunc("/api", handleAPI)
|
mux.HandleFunc("/api", handleAPI)
|
||||||
mux.HandleFunc("/api/token/generate", handleAPITokenGenerate)
|
mux.HandleFunc("/api/token/generate", handleAPITokenGenerate)
|
||||||
|
|
|
||||||
|
|
@ -279,7 +279,7 @@ func handleMCPInitialize(w http.ResponseWriter, req mcpRequest) {
|
||||||
"protocolVersion": mcpProtocolVersion,
|
"protocolVersion": mcpProtocolVersion,
|
||||||
"capabilities": map[string]interface{}{
|
"capabilities": map[string]interface{}{
|
||||||
"tools": map[string]interface{}{},
|
"tools": map[string]interface{}{},
|
||||||
"prompts": map[string]interface{}{},
|
"trackers": map[string]interface{}{},
|
||||||
},
|
},
|
||||||
"serverInfo": map[string]interface{}{
|
"serverInfo": map[string]interface{}{
|
||||||
"name": mcpServerName,
|
"name": mcpServerName,
|
||||||
|
|
@ -617,7 +617,7 @@ func handleMCPPromptsList(w http.ResponseWriter, req mcpRequest) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMCPResult(w, req.ID, map[string]interface{}{"prompts": prompts})
|
sendMCPResult(w, req.ID, map[string]interface{}{"trackers": prompts})
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleMCPPromptsGet(w http.ResponseWriter, req mcpRequest, accessToken, dossierID string) {
|
func handleMCPPromptsGet(w http.ResponseWriter, req mcpRequest, accessToken, dossierID string) {
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@
|
||||||
{{else if eq .Page "styleguide"}}{{template "styleguide" .}}
|
{{else if eq .Page "styleguide"}}{{template "styleguide" .}}
|
||||||
{{else if eq .Page "pricing"}}{{template "pricing" .}}
|
{{else if eq .Page "pricing"}}{{template "pricing" .}}
|
||||||
{{else if eq .Page "faq"}}{{template "faq" .}}
|
{{else if eq .Page "faq"}}{{template "faq" .}}
|
||||||
{{else if eq .Page "prompts"}}{{template "prompts" .}}
|
{{else if eq .Page "trackers"}}{{template "trackers" .}}
|
||||||
{{else if eq .Page "permissions"}}{{template "permissions" .}}
|
{{else if eq .Page "permissions"}}{{template "permissions" .}}
|
||||||
{{else if eq .Page "edit_access"}}{{template "edit_access" .}}
|
{{else if eq .Page "edit_access"}}{{template "edit_access" .}}
|
||||||
{{else if eq .Page "edit_rbac"}}{{template "edit_rbac" .}}
|
{{else if eq .Page "edit_rbac"}}{{template "edit_rbac" .}}
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@
|
||||||
<li>In the text area, add:</li>
|
<li>In the text area, add:</li>
|
||||||
</ol>
|
</ol>
|
||||||
<div class="code-wrapper">
|
<div class="code-wrapper">
|
||||||
<pre id="custom-instructions">At the start of health-related conversations, use the family_health_context prompt from the Inou Health connector to understand what health data is available.</pre>
|
<pre id="custom-instructions">At the start of health-related conversations, use the family_health_context tracker from the Inou Health connector to understand what health data is available.</pre>
|
||||||
<button class="copy-icon" onclick="copyCode('custom-instructions', this)" title="Copy">
|
<button class="copy-icon" onclick="copyCode('custom-instructions', this)" title="Copy">
|
||||||
<svg viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||||
|
|
@ -161,7 +161,7 @@ If you get a 401 error with "token expired", ask me to visit https://inou.com/co
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="login-prompt">
|
<div class="login-tracker">
|
||||||
<a href="/start">Sign in</a> to generate your API token and get personalized setup instructions.
|
<a href="/start">Sign in</a> to generate your API token and get personalized setup instructions.
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if not (and .Dossier .Dossier.DossierID)}}
|
{{if not (and .Dossier .Dossier.DossierID)}}
|
||||||
<div class="login-prompt">
|
<div class="login-tracker">
|
||||||
<strong>Let op:</strong> <a href="/start">Log in</a> om gepersonaliseerde instructies te zien met je account-token al ingevuld.
|
<strong>Let op:</strong> <a href="/start">Log in</a> om gepersonaliseerde instructies te zien met je account-token al ingevuld.
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if not (and .Dossier .Dossier.DossierID)}}
|
{{if not (and .Dossier .Dossier.DossierID)}}
|
||||||
<div class="login-prompt">
|
<div class="login-tracker">
|
||||||
<strong>Примечание:</strong> <a href="/start">Войдите</a>, чтобы увидеть персонализированные инструкции с вашим токеном учётной записи.
|
<strong>Примечание:</strong> <a href="/start">Войдите</a>, чтобы увидеть персонализированные инструкции с вашим токеном учётной записи.
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -728,10 +728,10 @@ loadGeneticsCategories();
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .ShowBuildPrompt}}
|
{{if .ShowBuildTracker}}
|
||||||
<div class="build-profile-prompt">
|
<div class="build-profile-tracker">
|
||||||
<div class="build-profile-buttons">
|
<div class="build-profile-buttons">
|
||||||
{{range .PromptButtons}}
|
{{range .TrackerButtons}}
|
||||||
<a href="{{.URL}}" class="build-profile-btn">
|
<a href="{{.URL}}" class="build-profile-btn">
|
||||||
<span>{{.Label}}</span>
|
<span>{{.Label}}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,8 @@
|
||||||
.quick-start h3 { margin: 0 0 1rem 0; font-size: 1.1rem; }
|
.quick-start h3 { margin: 0 0 1rem 0; font-size: 1.1rem; }
|
||||||
.quick-start p { margin: 0.5rem 0; }
|
.quick-start p { margin: 0.5rem 0; }
|
||||||
|
|
||||||
.login-prompt { background: #fff3cd; border: 1px solid #ffc107; border-radius: 8px; padding: 1rem 1.5rem; margin-bottom: 1.5rem; }
|
.login-tracker { background: #fff3cd; border: 1px solid #ffc107; border-radius: 8px; padding: 1rem 1.5rem; margin-bottom: 1.5rem; }
|
||||||
.login-prompt a { color: var(--accent); font-weight: 500; }
|
.login-tracker a { color: var(--accent); font-weight: 500; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
<a href="/" class="btn btn-secondary btn-small">← Home</a>
|
<a href="/" class="btn btn-secondary btn-small">← Home</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="login-prompt">
|
<div class="login-tracker">
|
||||||
<strong>Note:</strong> <a href="/start">Sign in</a> to see personalized setup instructions with your account token pre-filled.
|
<strong>Note:</strong> <a href="/start">Sign in</a> to see personalized setup instructions with your account token pre-filled.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@
|
||||||
<div class="sg-settings-row">
|
<div class="sg-settings-row">
|
||||||
<div>
|
<div>
|
||||||
<div class="sg-settings-label">Primary AI Assistant</div>
|
<div class="sg-settings-label">Primary AI Assistant</div>
|
||||||
<div class="sg-settings-desc">Used for "Ask AI" prompts and analysis</div>
|
<div class="sg-settings-desc">Used for "Ask AI" trackers and analysis</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sg-settings-control">
|
<div class="sg-settings-control">
|
||||||
<label class="sg-llm-option selected"><input type="radio" name="llm" checked><span class="sg-llm-icon">🤖</span><span>Claude (Anthropic)</span></label>
|
<label class="sg-llm-option selected"><input type="radio" name="llm" checked><span class="sg-llm-icon">🤖</span><span>Claude (Anthropic)</span></label>
|
||||||
|
|
@ -481,14 +481,14 @@
|
||||||
<div class="sg-modal-overlay" id="gene-modal-1" onclick="if(event.target===this)this.classList.remove('show')">
|
<div class="sg-modal-overlay" id="gene-modal-1" onclick="if(event.target===this)this.classList.remove('show')">
|
||||||
<div class="sg-modal">
|
<div class="sg-modal">
|
||||||
<h3>Ask AI about CYP2C19</h3>
|
<h3>Ask AI about CYP2C19</h3>
|
||||||
<div class="sg-modal-prompt">I have a genetic variant in CYP2C19 (rs4244285) with genotype G;A.
|
<div class="sg-modal-tracker">I have a genetic variant in CYP2C19 (rs4244285) with genotype G;A.
|
||||||
|
|
||||||
This makes me an intermediate metabolizer.
|
This makes me an intermediate metabolizer.
|
||||||
|
|
||||||
What medications are affected by this? What should I discuss with my doctor?</div>
|
What medications are affected by this? What should I discuss with my doctor?</div>
|
||||||
<div class="sg-modal-actions">
|
<div class="sg-modal-actions">
|
||||||
<button class="btn btn-secondary btn-small" onclick="this.closest('.sg-modal-overlay').classList.remove('show')">Close</button>
|
<button class="btn btn-secondary btn-small" onclick="this.closest('.sg-modal-overlay').classList.remove('show')">Close</button>
|
||||||
<button class="btn btn-primary btn-small" onclick="navigator.clipboard.writeText(this.closest('.sg-modal').querySelector('.sg-modal-prompt').innerText); this.innerText='Copied!'; setTimeout(()=>this.innerText='Copy prompt', 1500)">Copy prompt</button>
|
<button class="btn btn-primary btn-small" onclick="navigator.clipboard.writeText(this.closest('.sg-modal').querySelector('.sg-modal-tracker').innerText); this.innerText='Copied!'; setTimeout(()=>this.innerText='Copy tracker', 1500)">Copy tracker</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,49 @@
|
||||||
{{/* Reusable prompt card partial - expects . to be a PromptView */}}
|
{{/* Reusable tracker card partial - expects . to be a TrackerView */}}
|
||||||
<div class="prompt-item prompt-pending" data-prompt-id="{{.ID}}">
|
<div class="tracker-item tracker-pending" data-tracker-id="{{.ID}}">
|
||||||
<a href="#" class="prompt-dismiss" onclick="showDismissConfirm(this, '{{.ID}}'); return false;" title="Don't ask again">✕</a>
|
<a href="#" class="tracker-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>
|
||||||
<form class="prompt-form" data-prompt-id="{{.ID}}">
|
<form class="tracker-form" data-tracker-id="{{.ID}}">
|
||||||
<div class="prompt-header">
|
<div class="tracker-header">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 12px;">
|
<div style="display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 12px;">
|
||||||
<div>
|
<div>
|
||||||
<span class="prompt-question">{{.Question}}</span>
|
<span class="tracker-question">{{.Question}}</span>
|
||||||
{{if .ScheduleFormatted}}
|
{{if .ScheduleFormatted}}
|
||||||
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 4px;">🔁 {{.ScheduleFormatted}}</div>
|
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 4px;">🔁 {{.ScheduleFormatted}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; gap: 12px; align-items: center;">
|
<div style="display: flex; gap: 12px; align-items: center;">
|
||||||
<span class="prompt-category">{{.Category}}</span>
|
<span class="tracker-category">{{.Category}}</span>
|
||||||
<span class="prompt-due">{{.NextAskFormatted}}</span>
|
<span class="tracker-due">{{.NextAskFormatted}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="prompt-body">
|
<div class="tracker-body">
|
||||||
{{if .Groups}}
|
{{if .Groups}}
|
||||||
{{if eq .Layout "two-column"}}
|
{{if eq .Layout "two-column"}}
|
||||||
{{/* Two-column layout - first two groups side-by-side */}}
|
{{/* Two-column layout - first two groups side-by-side */}}
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px; align-items: start;">
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px; align-items: start;">
|
||||||
{{range $i, $g := .Groups}}
|
{{range $i, $g := .Groups}}
|
||||||
{{if lt $i 2}}
|
{{if lt $i 2}}
|
||||||
<div class="prompt-group">
|
<div class="tracker-group">
|
||||||
{{if $g.Title}}<div class="prompt-group-title">{{$g.Title}}</div>{{end}}
|
{{if $g.Title}}<div class="tracker-group-title">{{$g.Title}}</div>{{end}}
|
||||||
<div class="prompt-input-row" style="flex-direction: column; align-items: flex-start; gap: 8px;">
|
<div class="tracker-input-row" style="flex-direction: column; align-items: flex-start; gap: 8px;">
|
||||||
{{range $g.Fields}}
|
{{range $g.Fields}}
|
||||||
{{if eq .Type "number"}}
|
{{if eq .Type "number"}}
|
||||||
<div style="display: flex; align-items: center; gap: 4px; width: 100%;">
|
<div style="display: flex; align-items: center; gap: 4px; width: 100%;">
|
||||||
{{if .Label}}<span class="prompt-field-label" style="min-width: 100px;">{{.Label}}:</span>{{end}}
|
{{if .Label}}<span class="tracker-field-label" style="min-width: 100px;">{{.Label}}:</span>{{end}}
|
||||||
<input type="number" name="field_{{.Key}}"
|
<input type="number" name="field_{{.Key}}"
|
||||||
{{if .Min}}min="{{.Min}}"{{end}}
|
{{if .Min}}min="{{.Min}}"{{end}}
|
||||||
{{if .Max}}max="{{.Max}}"{{end}}
|
{{if .Max}}max="{{.Max}}"{{end}}
|
||||||
{{if .Step}}step="{{.Step}}"{{end}}
|
{{if .Step}}step="{{.Step}}"{{end}}
|
||||||
{{if .Value}}value="{{.Value}}"{{end}}
|
{{if .Value}}value="{{.Value}}"{{end}}
|
||||||
class="prompt-input-number"
|
class="tracker-input-number"
|
||||||
style="max-width: 80px;"
|
style="max-width: 80px;"
|
||||||
onkeydown="if(event.key==='Enter'){event.preventDefault();saveItem(this.closest('.prompt-item'));}">
|
onkeydown="if(event.key==='Enter'){event.preventDefault();saveItem(this.closest('.tracker-item'));}">
|
||||||
{{if .Unit}}<span class="prompt-unit">{{.Unit}}</span>{{end}}
|
{{if .Unit}}<span class="tracker-unit">{{.Unit}}</span>{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
@ -55,18 +55,18 @@
|
||||||
{{/* Remaining groups below (index 2+) with save button inline */}}
|
{{/* Remaining groups below (index 2+) with save button inline */}}
|
||||||
{{range $i, $g := .Groups}}
|
{{range $i, $g := .Groups}}
|
||||||
{{if ge $i 2}}
|
{{if ge $i 2}}
|
||||||
<div class="prompt-group" style="margin-top: 16px;">
|
<div class="tracker-group" style="margin-top: 16px;">
|
||||||
{{if $g.Title}}<div class="prompt-group-title">{{$g.Title}}</div>{{end}}
|
{{if $g.Title}}<div class="tracker-group-title">{{$g.Title}}</div>{{end}}
|
||||||
{{range $g.Fields}}
|
{{range $g.Fields}}
|
||||||
{{if eq .Type "text"}}
|
{{if eq .Type "text"}}
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
<input type="text" name="field_{{.Key}}"
|
<input type="text" name="field_{{.Key}}"
|
||||||
{{if .Value}}value="{{.Value}}"{{end}}
|
{{if .Value}}value="{{.Value}}"{{end}}
|
||||||
{{if .MaxLength}}maxlength="{{.MaxLength}}"{{end}}
|
{{if .MaxLength}}maxlength="{{.MaxLength}}"{{end}}
|
||||||
class="prompt-input-text"
|
class="tracker-input-text"
|
||||||
style="flex: 1;"
|
style="flex: 1;"
|
||||||
placeholder="{{.Label}}">
|
placeholder="{{.Label}}">
|
||||||
<button type="button" class="btn-save" onclick="saveItem(this.closest('.prompt-item'))">Save</button>
|
<button type="button" class="btn-save" onclick="saveItem(this.closest('.tracker-item'))">Save</button>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
@ -82,35 +82,35 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if not $hasText}}
|
{{if not $hasText}}
|
||||||
<div style="display: flex; justify-content: flex-end; margin-top: 12px;">
|
<div style="display: flex; justify-content: flex-end; margin-top: 12px;">
|
||||||
<button type="button" class="btn-save" onclick="saveItem(this.closest('.prompt-item'))">Save</button>
|
<button type="button" class="btn-save" onclick="saveItem(this.closest('.tracker-item'))">Save</button>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{/* Regular grouped fields */}}
|
{{/* Regular grouped fields */}}
|
||||||
{{range .Groups}}
|
{{range .Groups}}
|
||||||
<div class="prompt-group">
|
<div class="tracker-group">
|
||||||
{{if .Title}}<div class="prompt-group-title">{{.Title}}</div>{{end}}
|
{{if .Title}}<div class="tracker-group-title">{{.Title}}</div>{{end}}
|
||||||
<div class="prompt-input-row" style="justify-content: flex-start; align-items: center; flex-wrap: wrap;">
|
<div class="tracker-input-row" style="justify-content: flex-start; align-items: center; flex-wrap: wrap;">
|
||||||
{{range .Fields}}
|
{{range .Fields}}
|
||||||
{{if eq .Type "number"}}
|
{{if eq .Type "number"}}
|
||||||
<div style="display: flex; align-items: center; gap: 4px; margin-right: 16px;">
|
<div style="display: flex; align-items: center; gap: 4px; margin-right: 16px;">
|
||||||
{{if .Label}}<span class="prompt-field-label">{{.Label}}:</span>{{end}}
|
{{if .Label}}<span class="tracker-field-label">{{.Label}}:</span>{{end}}
|
||||||
<input type="number" name="field_{{.Key}}"
|
<input type="number" name="field_{{.Key}}"
|
||||||
{{if .Min}}min="{{.Min}}"{{end}}
|
{{if .Min}}min="{{.Min}}"{{end}}
|
||||||
{{if .Max}}max="{{.Max}}"{{end}}
|
{{if .Max}}max="{{.Max}}"{{end}}
|
||||||
{{if .Step}}step="{{.Step}}"{{end}}
|
{{if .Step}}step="{{.Step}}"{{end}}
|
||||||
{{if .Value}}value="{{.Value}}"{{end}}
|
{{if .Value}}value="{{.Value}}"{{end}}
|
||||||
class="prompt-input-number"
|
class="tracker-input-number"
|
||||||
style="max-width: 80px;"
|
style="max-width: 80px;"
|
||||||
onkeydown="if(event.key==='Enter'){event.preventDefault();saveItem(this.closest('.prompt-item'));}">
|
onkeydown="if(event.key==='Enter'){event.preventDefault();saveItem(this.closest('.tracker-item'));}">
|
||||||
{{if .Unit}}<span class="prompt-unit">{{.Unit}}</span>{{end}}
|
{{if .Unit}}<span class="tracker-unit">{{.Unit}}</span>{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{else if eq .Type "text"}}
|
{{else if eq .Type "text"}}
|
||||||
<div style="display: flex; align-items: center; gap: 4px; width: 100%;">
|
<div style="display: flex; align-items: center; gap: 4px; width: 100%;">
|
||||||
<input type="text" name="field_{{.Key}}"
|
<input type="text" name="field_{{.Key}}"
|
||||||
{{if .Value}}value="{{.Value}}"{{end}}
|
{{if .Value}}value="{{.Value}}"{{end}}
|
||||||
{{if .MaxLength}}maxlength="{{.MaxLength}}"{{end}}
|
{{if .MaxLength}}maxlength="{{.MaxLength}}"{{end}}
|
||||||
class="prompt-input-text"
|
class="tracker-input-text"
|
||||||
style="flex: 1;"
|
style="flex: 1;"
|
||||||
placeholder="{{.Label}}">
|
placeholder="{{.Label}}">
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -120,39 +120,39 @@
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div style="display: flex; justify-content: flex-end; margin-top: 12px;">
|
<div style="display: flex; justify-content: flex-end; margin-top: 12px;">
|
||||||
<button type="button" class="btn-save" onclick="saveItem(this.closest('.prompt-item'))">Save</button>
|
<button type="button" class="btn-save" onclick="saveItem(this.closest('.tracker-item'))">Save</button>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else if .Fields}}
|
{{else if .Fields}}
|
||||||
{{/* Flat fields */}}
|
{{/* Flat fields */}}
|
||||||
<div class="prompt-input-row" style="justify-content: flex-start; align-items: center;">
|
<div class="tracker-input-row" style="justify-content: flex-start; align-items: center;">
|
||||||
{{range .Fields}}
|
{{range .Fields}}
|
||||||
{{if eq .Type "number"}}
|
{{if eq .Type "number"}}
|
||||||
{{if .Label}}<span class="prompt-field-label">{{.Label}}</span>{{end}}
|
{{if .Label}}<span class="tracker-field-label">{{.Label}}</span>{{end}}
|
||||||
<input type="number" name="field_{{.Key}}"
|
<input type="number" name="field_{{.Key}}"
|
||||||
{{if .Min}}min="{{.Min}}"{{end}}
|
{{if .Min}}min="{{.Min}}"{{end}}
|
||||||
{{if .Max}}max="{{.Max}}"{{end}}
|
{{if .Max}}max="{{.Max}}"{{end}}
|
||||||
{{if .Step}}step="{{.Step}}"{{end}}
|
{{if .Step}}step="{{.Step}}"{{end}}
|
||||||
{{if .Value}}value="{{.Value}}"{{else}}placeholder="0"{{end}}
|
{{if .Value}}value="{{.Value}}"{{else}}placeholder="0"{{end}}
|
||||||
class="prompt-input-number"
|
class="tracker-input-number"
|
||||||
style="max-width: 120px;"
|
style="max-width: 120px;"
|
||||||
onkeydown="if(event.key==='Enter'){event.preventDefault();saveItem(this.closest('.prompt-item'));}">
|
onkeydown="if(event.key==='Enter'){event.preventDefault();saveItem(this.closest('.tracker-item'));}">
|
||||||
{{if .Unit}}<span class="prompt-unit">{{.Unit}}</span>{{end}}
|
{{if .Unit}}<span class="tracker-unit">{{.Unit}}</span>{{end}}
|
||||||
{{else if eq .Type "checkbox"}}
|
{{else if eq .Type "checkbox"}}
|
||||||
<button type="button" class="prompt-btn" data-field="{{.Key}}" data-value="yes" onclick="selectYesNo(this)">Yes</button>
|
<button type="button" class="tracker-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>
|
<button type="button" class="tracker-btn" data-field="{{.Key}}" data-value="no" onclick="selectYesNo(this)">No</button>
|
||||||
<input type="hidden" name="field_{{.Key}}" value="">
|
<input type="hidden" name="field_{{.Key}}" value="">
|
||||||
{{else if eq .Type "text"}}
|
{{else if eq .Type "text"}}
|
||||||
{{if .Label}}<span class="prompt-field-label">{{.Label}}</span>{{end}}
|
{{if .Label}}<span class="tracker-field-label">{{.Label}}</span>{{end}}
|
||||||
<input type="text" name="field_{{.Key}}"
|
<input type="text" name="field_{{.Key}}"
|
||||||
{{if .Value}}value="{{.Value}}"{{end}}
|
{{if .Value}}value="{{.Value}}"{{end}}
|
||||||
{{if .MaxLength}}maxlength="{{.MaxLength}}"{{end}}
|
{{if .MaxLength}}maxlength="{{.MaxLength}}"{{end}}
|
||||||
class="prompt-input-text"
|
class="tracker-input-text"
|
||||||
placeholder="{{.Label}}"
|
placeholder="{{.Label}}"
|
||||||
onkeydown="if(event.key==='Enter'){event.preventDefault();saveItem(this.closest('.prompt-item'));}">
|
onkeydown="if(event.key==='Enter'){event.preventDefault();saveItem(this.closest('.tracker-item'));}">
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
<button type="button" class="btn-save" onclick="saveItem(this.closest('.prompt-item'))" style="margin-left: auto;">Save</button>
|
<button type="button" class="btn-save" onclick="saveItem(this.closest('.tracker-item'))" style="margin-left: auto;">Save</button>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -12,7 +12,7 @@ import (
|
||||||
"inou/lib"
|
"inou/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PromptField struct {
|
type TrackerField struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
|
@ -28,9 +28,9 @@ type PromptField struct {
|
||||||
Value string // filled from last_response
|
Value string // filled from last_response
|
||||||
}
|
}
|
||||||
|
|
||||||
type PromptGroup struct {
|
type TrackerGroup struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Fields []PromptField `json:"fields"`
|
Fields []TrackerField `json:"fields"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScheduleSlot struct {
|
type ScheduleSlot struct {
|
||||||
|
|
@ -38,13 +38,13 @@ type ScheduleSlot struct {
|
||||||
Time string `json:"time"`
|
Time string `json:"time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PromptView struct {
|
type TrackerView struct {
|
||||||
ID string
|
ID string
|
||||||
Category string
|
Category string
|
||||||
Type string
|
Type string
|
||||||
Question string
|
Question string
|
||||||
Fields []PromptField
|
Fields []TrackerField
|
||||||
Groups []PromptGroup
|
Groups []TrackerGroup
|
||||||
Layout string // "two-column" or empty
|
Layout string // "two-column" or empty
|
||||||
Schedule []ScheduleSlot
|
Schedule []ScheduleSlot
|
||||||
ScheduleFormatted string
|
ScheduleFormatted string
|
||||||
|
|
@ -67,15 +67,15 @@ type EntryView struct {
|
||||||
Value string
|
Value string
|
||||||
Question string
|
Question string
|
||||||
SourceInput string
|
SourceInput string
|
||||||
Fields []PromptField
|
Fields []TrackerField
|
||||||
Groups []PromptGroup
|
Groups []TrackerGroup
|
||||||
Layout string // "two-column" or empty
|
Layout string // "two-column" or empty
|
||||||
Timestamp int64
|
Timestamp int64
|
||||||
TimeFormatted string
|
TimeFormatted string
|
||||||
PromptID string // linked prompt for delete
|
TrackerID string // linked tracker for delete
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePrompts(w http.ResponseWriter, r *http.Request) {
|
func handleTrackers(w http.ResponseWriter, r *http.Request) {
|
||||||
p := getLoggedInDossier(r)
|
p := getLoggedInDossier(r)
|
||||||
if p == nil {
|
if p == nil {
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
|
|
@ -108,7 +108,7 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) {
|
||||||
lang := getLang(r)
|
lang := getLang(r)
|
||||||
showAll := r.URL.Query().Get("all") == "1"
|
showAll := r.URL.Query().Get("all") == "1"
|
||||||
|
|
||||||
// Fetch prompts from API
|
// Fetch trackers from API
|
||||||
url := fmt.Sprintf("http://localhost:8082/api/prompts?dossier=%s&all=1", targetHex)
|
url := fmt.Sprintf("http://localhost:8082/api/prompts?dossier=%s&all=1", targetHex)
|
||||||
if showAll {
|
if showAll {
|
||||||
url += "&all=1"
|
url += "&all=1"
|
||||||
|
|
@ -116,7 +116,7 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
resp, err := http.Get(url)
|
resp, err := http.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render(w, r, PageData{Page: "prompts", Lang: lang, Dossier: p, Error: "Failed to load prompts"})
|
render(w, r, PageData{Page: "trackers", Lang: lang, Dossier: p, Error: "Failed to load prompts"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
@ -153,9 +153,9 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to view models
|
// Convert to view models
|
||||||
var prompts []PromptView
|
var trackers []TrackerView
|
||||||
for _, ap := range apiPrompts {
|
for _, ap := range apiPrompts {
|
||||||
pv := PromptView{
|
pv := TrackerView{
|
||||||
ID: ap.ID,
|
ID: ap.ID,
|
||||||
Category: translateCategory(ap.Category),
|
Category: translateCategory(ap.Category),
|
||||||
Type: ap.Type,
|
Type: ap.Type,
|
||||||
|
|
@ -171,7 +171,7 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
if ap.NextAsk > 0 {
|
if ap.NextAsk > 0 {
|
||||||
pv.NextAskFormatted = formatDueDate(ap.NextAsk)
|
pv.NextAskFormatted = formatDueDate(ap.NextAsk)
|
||||||
// Freeform prompts (like "anything else") are never overdue
|
// Freeform trackers (like "anything else") are never overdue
|
||||||
pv.IsOverdue = ap.InputType != "freeform" && ap.NextAsk < time.Now().Unix()
|
pv.IsOverdue = ap.InputType != "freeform" && ap.NextAsk < time.Now().Unix()
|
||||||
}
|
}
|
||||||
if ap.LastResponseAt > 0 {
|
if ap.LastResponseAt > 0 {
|
||||||
|
|
@ -181,8 +181,8 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) {
|
||||||
// Parse input_config
|
// Parse input_config
|
||||||
if len(ap.InputConfig) > 0 {
|
if len(ap.InputConfig) > 0 {
|
||||||
var config struct {
|
var config struct {
|
||||||
Fields []PromptField `json:"fields"`
|
Fields []TrackerField `json:"fields"`
|
||||||
Groups []PromptGroup `json:"groups"`
|
Groups []TrackerGroup `json:"groups"`
|
||||||
Layout string `json:"layout"`
|
Layout string `json:"layout"`
|
||||||
}
|
}
|
||||||
if json.Unmarshal(ap.InputConfig, &config) == nil {
|
if json.Unmarshal(ap.InputConfig, &config) == nil {
|
||||||
|
|
@ -228,20 +228,20 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prompts = append(prompts, pv)
|
trackers = append(trackers, pv)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := PageData{
|
data := PageData{
|
||||||
Page: "prompts",
|
Page: "trackers",
|
||||||
Lang: lang,
|
Lang: lang,
|
||||||
Dossier: p,
|
Dossier: p,
|
||||||
TargetDossier: target,
|
TargetDossier: target,
|
||||||
}
|
}
|
||||||
data.T = translations[lang]
|
data.T = translations[lang]
|
||||||
|
|
||||||
// Build prompt lookup map by prompt ID
|
// Build tracker lookup map by tracker ID
|
||||||
promptMap := make(map[string]PromptView)
|
promptMap := make(map[string]TrackerView)
|
||||||
for _, p := range prompts {
|
for _, p := range trackers {
|
||||||
promptMap[p.ID] = p
|
promptMap[p.ID] = p
|
||||||
log.Printf("DEBUG: Added to promptMap: id=%s, question=%s", p.ID, p.Question)
|
log.Printf("DEBUG: Added to promptMap: id=%s, question=%s", p.ID, p.Question)
|
||||||
}
|
}
|
||||||
|
|
@ -267,21 +267,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"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Link entry to prompt via SearchKey (foreign key relationship)
|
// Link entry to tracker via SearchKey (foreign key relationship)
|
||||||
if e.SearchKey != "" {
|
if e.SearchKey != "" {
|
||||||
log.Printf("DEBUG: Looking for prompt with id=%s", e.SearchKey)
|
log.Printf("DEBUG: Looking for tracker with id=%s", e.SearchKey)
|
||||||
if prompt, ok := promptMap[e.SearchKey]; ok {
|
if prompt, ok := promptMap[e.SearchKey]; ok {
|
||||||
log.Printf("DEBUG: Found matching prompt: question=%s, fields=%d, groups=%d", prompt.Question, len(prompt.Fields), len(prompt.Groups))
|
log.Printf("DEBUG: Found matching prompt: question=%s, fields=%d, groups=%d", prompt.Question, len(prompt.Fields), len(prompt.Groups))
|
||||||
ev.Question = prompt.Question
|
ev.Question = prompt.Question
|
||||||
ev.PromptID = prompt.ID
|
ev.TrackerID = prompt.ID
|
||||||
// Copy fields, groups, and layout from prompt
|
// Copy fields, groups, and layout from prompt
|
||||||
ev.Fields = make([]PromptField, len(prompt.Fields))
|
ev.Fields = make([]TrackerField, len(prompt.Fields))
|
||||||
copy(ev.Fields, prompt.Fields)
|
copy(ev.Fields, prompt.Fields)
|
||||||
ev.Groups = make([]PromptGroup, len(prompt.Groups))
|
ev.Groups = make([]TrackerGroup, len(prompt.Groups))
|
||||||
copy(ev.Groups, prompt.Groups)
|
copy(ev.Groups, prompt.Groups)
|
||||||
ev.Layout = prompt.Layout
|
ev.Layout = prompt.Layout
|
||||||
} else {
|
} else {
|
||||||
log.Printf("DEBUG: No prompt found with id=%s", e.SearchKey)
|
log.Printf("DEBUG: No tracker found with id=%s", e.SearchKey)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("DEBUG: Entry has no search_key (may be freeform/legacy entry)")
|
log.Printf("DEBUG: Entry has no search_key (may be freeform/legacy entry)")
|
||||||
|
|
@ -319,10 +319,10 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
log.Printf("DEBUG: Built %d entry views for display", len(entries))
|
log.Printf("DEBUG: Built %d entry views for display", len(entries))
|
||||||
|
|
||||||
// Split prompts into regular and freeform
|
// Split trackers into regular and freeform
|
||||||
var allPrompts, freeformPrompts []PromptView
|
var allPrompts, freeformPrompts []TrackerView
|
||||||
dueCount := 0
|
dueCount := 0
|
||||||
for _, p := range prompts {
|
for _, p := range trackers {
|
||||||
if p.IsFreeform {
|
if p.IsFreeform {
|
||||||
freeformPrompts = append(freeformPrompts, p)
|
freeformPrompts = append(freeformPrompts, p)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -333,19 +333,19 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add prompts and entries to template
|
// Add trackers and entries to template
|
||||||
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
|
||||||
AllPrompts []PromptView
|
AllPrompts []TrackerView
|
||||||
FreeformPrompts []PromptView
|
FreeformPrompts []TrackerView
|
||||||
Entries []EntryView
|
Entries []EntryView
|
||||||
TargetHex string
|
TargetHex string
|
||||||
DueCount int
|
DueCount int
|
||||||
}{data, allPrompts, freeformPrompts, entries, targetHex, dueCount})
|
}{data, allPrompts, freeformPrompts, entries, targetHex, dueCount})
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePromptRespond(w http.ResponseWriter, r *http.Request) {
|
func handleTrackerRespond(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "POST" {
|
if r.Method != "POST" {
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
|
|
@ -365,7 +365,7 @@ func handlePromptRespond(w http.ResponseWriter, r *http.Request) {
|
||||||
targetHex := parts[2]
|
targetHex := parts[2]
|
||||||
|
|
||||||
r.ParseForm()
|
r.ParseForm()
|
||||||
promptID := r.FormValue("prompt_id")
|
trackerID := r.FormValue("tracker_id")
|
||||||
action := r.FormValue("action")
|
action := r.FormValue("action")
|
||||||
|
|
||||||
// Build response JSON from form fields
|
// Build response JSON from form fields
|
||||||
|
|
@ -389,7 +389,7 @@ func handlePromptRespond(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// Call API
|
// Call API
|
||||||
reqBody := map[string]string{
|
reqBody := map[string]string{
|
||||||
"prompt_id": promptID,
|
"tracker_id": trackerID,
|
||||||
"response": string(responseJSON),
|
"response": string(responseJSON),
|
||||||
"response_raw": responseRaw,
|
"response_raw": responseRaw,
|
||||||
"action": action,
|
"action": action,
|
||||||
|
|
@ -469,8 +469,8 @@ func formatDueDate(ts int64) string {
|
||||||
return due.Format("Jan 2, 3:04 PM")
|
return due.Format("Jan 2, 3:04 PM")
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleRenderPromptCard renders just the prompt card HTML for a given prompt
|
// handleRenderTrackerCard renders just the tracker card HTML for a given prompt
|
||||||
func handleRenderPromptCard(w http.ResponseWriter, r *http.Request) {
|
func handleRenderTrackerCard(w http.ResponseWriter, r *http.Request) {
|
||||||
p := getLoggedInDossier(r)
|
p := getLoggedInDossier(r)
|
||||||
if p == nil {
|
if p == nil {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
|
@ -478,15 +478,15 @@ func handleRenderPromptCard(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(r.URL.Path, "/")
|
parts := strings.Split(r.URL.Path, "/")
|
||||||
// Path: /dossier/{id}/prompts/card/{promptID}
|
// Path: /dossier/{id}/prompts/card/{trackerID}
|
||||||
if len(parts) < 6 {
|
if len(parts) < 6 {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
targetHex := parts[2]
|
targetHex := parts[2]
|
||||||
promptID := parts[5]
|
trackerID := parts[5]
|
||||||
|
|
||||||
// Get prompt from API
|
// Get tracker from API
|
||||||
resp, err := http.Get(fmt.Sprintf("http://localhost:8082/api/prompts?dossier=%s", targetHex))
|
resp, err := http.Get(fmt.Sprintf("http://localhost:8082/api/prompts?dossier=%s", targetHex))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Failed to fetch prompts", http.StatusInternalServerError)
|
http.Error(w, "Failed to fetch prompts", http.StatusInternalServerError)
|
||||||
|
|
@ -494,17 +494,17 @@ func handleRenderPromptCard(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
var prompts []map[string]interface{}
|
var trackers []map[string]interface{}
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&prompts); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&trackers); err != nil {
|
||||||
http.Error(w, "Failed to parse prompts", http.StatusInternalServerError)
|
http.Error(w, "Failed to parse prompts", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the specific prompt
|
// Find the specific prompt
|
||||||
var targetPrompt map[string]interface{}
|
var targetPrompt map[string]interface{}
|
||||||
for _, prompt := range prompts {
|
for _, tracker := range trackers {
|
||||||
if prompt["id"] == promptID {
|
if tracker["id"] == trackerID {
|
||||||
targetPrompt = prompt
|
targetPrompt = tracker
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -514,8 +514,8 @@ func handleRenderPromptCard(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to PromptView
|
// Convert to TrackerView
|
||||||
promptView := convertToPromptView(targetPrompt)
|
promptView := convertToTrackerView(targetPrompt)
|
||||||
|
|
||||||
// Render just the card template
|
// Render just the card template
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
|
@ -525,9 +525,9 @@ func handleRenderPromptCard(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertToPromptView converts API prompt JSON to PromptView
|
// convertToTrackerView converts API tracker JSON to TrackerView
|
||||||
func convertToPromptView(prompt map[string]interface{}) PromptView {
|
func convertToTrackerView(prompt map[string]interface{}) TrackerView {
|
||||||
view := PromptView{
|
view := TrackerView{
|
||||||
ID: prompt["id"].(string),
|
ID: prompt["id"].(string),
|
||||||
Question: getString(prompt, "question"),
|
Question: getString(prompt, "question"),
|
||||||
Category: getString(prompt, "category"),
|
Category: getString(prompt, "category"),
|
||||||
|
|
@ -565,13 +565,13 @@ func convertToPromptView(prompt map[string]interface{}) PromptView {
|
||||||
if groupsRaw, ok := config["groups"].([]interface{}); ok {
|
if groupsRaw, ok := config["groups"].([]interface{}); ok {
|
||||||
for _, g := range groupsRaw {
|
for _, g := range groupsRaw {
|
||||||
group := g.(map[string]interface{})
|
group := g.(map[string]interface{})
|
||||||
fg := PromptGroup{
|
fg := TrackerGroup{
|
||||||
Title: getString(group, "title"),
|
Title: getString(group, "title"),
|
||||||
}
|
}
|
||||||
if fieldsRaw, ok := group["fields"].([]interface{}); ok {
|
if fieldsRaw, ok := group["fields"].([]interface{}); ok {
|
||||||
for _, f := range fieldsRaw {
|
for _, f := range fieldsRaw {
|
||||||
field := f.(map[string]interface{})
|
field := f.(map[string]interface{})
|
||||||
fg.Fields = append(fg.Fields, PromptField{
|
fg.Fields = append(fg.Fields, TrackerField{
|
||||||
Key: getString(field, "key"),
|
Key: getString(field, "key"),
|
||||||
Label: getString(field, "label"),
|
Label: getString(field, "label"),
|
||||||
Type: getString(field, "type"),
|
Type: getString(field, "type"),
|
||||||
|
|
@ -587,7 +587,7 @@ func convertToPromptView(prompt map[string]interface{}) PromptView {
|
||||||
if fieldsRaw, ok := config["fields"].([]interface{}); ok {
|
if fieldsRaw, ok := config["fields"].([]interface{}); ok {
|
||||||
for _, f := range fieldsRaw {
|
for _, f := range fieldsRaw {
|
||||||
field := f.(map[string]interface{})
|
field := f.(map[string]interface{})
|
||||||
view.Fields = append(view.Fields, PromptField{
|
view.Fields = append(view.Fields, TrackerField{
|
||||||
Key: getString(field, "key"),
|
Key: getString(field, "key"),
|
||||||
Label: getString(field, "label"),
|
Label: getString(field, "label"),
|
||||||
Type: getString(field, "type"),
|
Type: getString(field, "type"),
|
||||||
Loading…
Reference in New Issue