|
|
|
@@ -0,0 +1,143 @@
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"log"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/kami/maven/internal/router"
|
|
|
|
|
"github.com/kami/maven/internal/store"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Standing lists on the voice path (Vikunja #453).
|
|
|
|
|
//
|
|
|
|
|
// Three halves, mirroring what task capture already does: an add that runs at
|
|
|
|
|
// the top of actionNote, a read-back query source, and a crossing-off that runs
|
|
|
|
|
// on the same note path because "всё купил" is note-shaped.
|
|
|
|
|
//
|
|
|
|
|
// These read h.dataStore rather than the CoreAPI. A list is local to the core
|
|
|
|
|
// and nothing outside it writes one: the web UI has no list page, no reach
|
|
|
|
|
// files groceries, and the digestion worker does not read the table. When
|
|
|
|
|
// something outside mavend needs to add to a list, the ipc seam is what it
|
|
|
|
|
// grows through — the intake rules that CaptureTaskReq documents are about
|
|
|
|
|
// shared intake, and there is none here yet.
|
|
|
|
|
//
|
|
|
|
|
// Nothing here speaks unprompted. A list is answered when asked about.
|
|
|
|
|
|
|
|
|
|
// captureListFromNote claims the turn when the utterance adds to, clears, or
|
|
|
|
|
// crosses one item off a list. ("", false) hands the turn back to the note path.
|
|
|
|
|
func (h *reactiveHandler) captureListFromNote(ctx context.Context, dec router.Decision) (string, bool) {
|
|
|
|
|
if h.dataStore == nil {
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
// Clearing is read before removing on purpose: "всё купил" and "купил
|
|
|
|
|
// молоко" start with the same word, and only the second one names an item.
|
|
|
|
|
if list, ok := router.ParseListClear(dec.Utterance); ok {
|
|
|
|
|
n, err := h.dataStore.ClearList(ctx, list, h.now())
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("voice: clear list: %v", err)
|
|
|
|
|
return "не получилось обновить список.", true
|
|
|
|
|
}
|
|
|
|
|
if n == 0 {
|
|
|
|
|
return "в списке и так ничего не было.", true
|
|
|
|
|
}
|
|
|
|
|
return "вычеркнула всё, список пустой.", true
|
|
|
|
|
}
|
|
|
|
|
if cap, ok := router.ParseListRemove(dec.Utterance); ok {
|
|
|
|
|
if reply, ok := h.removeListItem(ctx, cap); ok {
|
|
|
|
|
return reply, true
|
|
|
|
|
}
|
|
|
|
|
// Nothing on the list by that name. "купил новый ноутбук" is a note and
|
|
|
|
|
// must stay one, so the turn goes back rather than claiming a removal
|
|
|
|
|
// that removed nothing.
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
cap, ok := router.ParseListCapture(dec.Utterance)
|
|
|
|
|
if !ok {
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
res, err := h.dataStore.AddListItem(ctx, store.ListItem{
|
|
|
|
|
List: cap.List,
|
|
|
|
|
Item: cap.Item,
|
|
|
|
|
Source: "tap:voice",
|
|
|
|
|
CreatedTs: h.now(),
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("voice: add list item: %v", err)
|
|
|
|
|
return "не получилось добавить в список.", true
|
|
|
|
|
}
|
|
|
|
|
if !res.Created {
|
|
|
|
|
return cap.Item + " уже в списке.", true
|
|
|
|
|
}
|
|
|
|
|
return "добавила в список: " + cap.Item + ".", true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// removeListItem crosses one named item off. It reports false when the list
|
|
|
|
|
// holds nothing by that name, which is what keeps the marker words from
|
|
|
|
|
// swallowing ordinary notes.
|
|
|
|
|
func (h *reactiveHandler) removeListItem(ctx context.Context, cap router.ListCapture) (string, bool) {
|
|
|
|
|
items, err := h.dataStore.ListItems(ctx, cap.List, "")
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("voice: list items: %v", err)
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
want := store.NormalizeTaskText(cap.Item)
|
|
|
|
|
for _, li := range items {
|
|
|
|
|
if store.NormalizeTaskText(li.Item) != want {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if err := h.dataStore.SetListItemStatus(ctx, li.ID, store.ListItemDone, h.now()); err != nil {
|
|
|
|
|
log.Printf("voice: cross off list item: %v", err)
|
|
|
|
|
return "не получилось обновить список.", true
|
|
|
|
|
}
|
|
|
|
|
return "вычеркнула: " + li.Item + ".", true
|
|
|
|
|
}
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// queryList — "что в списке покупок?", "что мне купить?".
|
|
|
|
|
//
|
|
|
|
|
// A query source, so it sits in querySources and either claims the turn or
|
|
|
|
|
// passes it on. It is before the recall sources for the reason every specific
|
|
|
|
|
// source is: the notes pass would otherwise answer a list question with
|
|
|
|
|
// whatever note is nearest.
|
|
|
|
|
func (h *reactiveHandler) queryList(ctx context.Context, t *queryTurn) (string, bool) {
|
|
|
|
|
list, ok := router.ParseListQuery(t.dec.Utterance)
|
|
|
|
|
if !ok || h.dataStore == nil {
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
items, err := h.dataStore.ListItems(ctx, list, "")
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("voice: list items: %v", err)
|
|
|
|
|
return "не получилось посмотреть список.", true
|
|
|
|
|
}
|
|
|
|
|
return formatListRU(list, items), true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// formatListRU reads a list aloud. One sentence, comma-separated, because a
|
|
|
|
|
// shopping list is heard in a shop and a numbered recital is unusable there.
|
|
|
|
|
func formatListRU(list string, items []store.ListItem) string {
|
|
|
|
|
name := "списке " + listGenitive(list)
|
|
|
|
|
if len(items) == 0 {
|
|
|
|
|
return "в " + name + " пусто."
|
|
|
|
|
}
|
|
|
|
|
names := make([]string, 0, len(items))
|
|
|
|
|
for _, li := range items {
|
|
|
|
|
names = append(names, li.Item)
|
|
|
|
|
}
|
|
|
|
|
return "в " + name + ": " + strings.Join(names, ", ") + "."
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// listGenitive puts a list tag into the case "список <…>" needs. Russian
|
|
|
|
|
// declines the noun and she must not say "в списке покупки".
|
|
|
|
|
func listGenitive(list string) string {
|
|
|
|
|
switch list {
|
|
|
|
|
case "покупки":
|
|
|
|
|
return "покупок"
|
|
|
|
|
case "аптека":
|
|
|
|
|
return "аптеки"
|
|
|
|
|
case "хозяйство":
|
|
|
|
|
return "хозяйства"
|
|
|
|
|
}
|
|
|
|
|
return list
|
|
|
|
|
}
|