Refactor UI rendering and add support for web UI

This commit is contained in:
Max 2024-02-15 10:10:33 +08:00
parent 14a97da330
commit 52a008bbcb
5 changed files with 157 additions and 74 deletions

View file

@ -49,6 +49,47 @@ func Close(id string) {
contexts.Delete(id)
}
// Resume the context by id
func Resume(id string, args ...any) any {
ctx := Open(id)
v, err := ctx.resume(args...)
if err != nil {
exception.New("pipe: %s %s", 500, ctx.Name, err).Throw()
}
return v
}
// resume the context by id
func (ctx *Context) resume(args ...any) (any, error) {
if ctx.current == nil {
return nil, ctx.Errorf("pipe %s has no nodes", ctx.Name)
}
node := ctx.current
output, err := ctx.parseNodeOutput(node, args)
if err != nil {
return nil, node.Errorf(ctx, err.Error())
}
// Next node
next, eof, err := ctx.next()
if err != nil {
return nil, err
}
// End of the pipe
if eof {
defer Close(ctx.id)
output, err := ctx.parseOutput()
if err != nil {
return nil, err
}
return output, nil
}
return ctx.exec(next, anyToInput(output))
}
// Run the pipe
func (ctx *Context) Run(args ...any) any {
v, err := ctx.Exec(args...)
@ -58,53 +99,6 @@ func (ctx *Context) Run(args ...any) any {
return v
}
// ID the context id
func (ctx *Context) ID() string {
return ctx.id
}
// Next the next node
func (ctx *Context) Next() (*Node, bool, error) {
if ctx.current == nil {
return nil, true, nil
}
// if the goto is not empty, then goto the node
if ctx.current.Goto != "" {
data := ctx.data(ctx.current)
next, err := data.replaceString(ctx.current.Goto)
if err != nil {
return nil, false, err
}
if next == "EOF" {
return nil, true, nil
}
var has = false
ctx.current, has = ctx.mapping[next]
if !has {
return nil, false, ctx.Errorf("node %s not found", next)
}
return ctx.current, false, nil
}
// continue to the next node
next := ctx.current.index + 1
if next >= len(ctx.Nodes) {
return nil, true, nil
}
ctx.current = &ctx.Nodes[next]
return ctx.current, false, nil
}
// IsEOF check if the error is EOF
func IsEOF(err error) bool {
return err != nil && err.Error() == "EOF"
}
// Exec this is the entry point of the pipe
func (ctx *Context) Exec(args ...any) (any, error) {
if ctx.current == nil {
@ -150,17 +144,23 @@ func (ctx *Context) exec(node *Node, input Input) (output any, err error) {
}
case "user-input":
out, err = node.Render(ctx, input)
var pause bool = false
out, pause, err = node.Render(ctx, input)
if err != nil {
return nil, err
}
// Pause the pipe waiting for user input
if pause {
return out, nil
}
default:
return nil, node.Errorf(ctx, "type '%s' not support", node.Type)
}
// Execute the next node
next, eof, err := ctx.Next()
next, eof, err := ctx.next()
if err != nil {
return nil, err
}
@ -180,6 +180,43 @@ func (ctx *Context) exec(node *Node, input Input) (output any, err error) {
return ctx.exec(next, anyToInput(out))
}
// Next the next node
func (ctx *Context) next() (*Node, bool, error) {
if ctx.current == nil {
return nil, true, nil
}
// if the goto is not empty, then goto the node
if ctx.current.Goto != "" {
data := ctx.data(ctx.current)
next, err := data.replaceString(ctx.current.Goto)
if err != nil {
return nil, false, err
}
if next == "EOF" {
return nil, true, nil
}
var has = false
ctx.current, has = ctx.mapping[next]
if !has {
return nil, false, ctx.Errorf("node %s not found", next)
}
return ctx.current, false, nil
}
// continue to the next node
next := ctx.current.index + 1
if next >= len(ctx.Nodes) {
return nil, true, nil
}
ctx.current = &ctx.Nodes[next]
return ctx.current, false, nil
}
// ParseNodeInput parse the node input
func (ctx *Context) parseNodeInput(node *Node, input Input) (Input, error) {
ctx.in[node] = input

View file

@ -209,18 +209,33 @@ func (node *Node) aiMergeHistory(ctx *Context, prompts []Prompt) []Prompt {
}
// Render Execute the user input
func (node *Node) Render(ctx *Context, input Input) (any, error) {
func (node *Node) Render(ctx *Context, input Input) (any, bool, error) {
switch node.UI {
case "cli":
return node.renderCli(ctx, input)
output, err := node.renderCli(ctx, input)
if err != nil {
return nil, false, err
}
return output, false, nil
case "web":
default:
input, err := ctx.parseNodeInput(node, input)
if err != nil {
return nil, true, err
}
return ResumeContext{
ID: ctx.id,
Input: input,
Node: node,
Data: ctx.data(node),
Type: node.Type,
UI: node.UI,
}, true, nil
}
return nil, fmt.Errorf("pipe: %s %s", ctx.Name, "node type error")
}
func (node *Node) renderCli(ctx *Context, input Input) (any, error) {
@ -255,12 +270,12 @@ func (node *Node) renderCli(ctx *Context, input Input) (any, error) {
lines, err := cli.New(option).Render(input)
if err != nil {
return nil, err
return nil, node.Errorf(ctx, err.Error())
}
output, err := ctx.parseNodeOutput(node, lines)
if err != nil {
return nil, err
return nil, node.Errorf(ctx, err.Error())
}
return output, nil
}

View file

@ -2,7 +2,6 @@ package pipe
import (
"context"
"fmt"
"os"
"testing"
"time"
@ -15,10 +14,10 @@ import (
"github.com/yaoapp/yao/test"
)
func TestRun(t *testing.T) {
func TestRunCli(t *testing.T) {
prepare(t)
defer test.Clean()
translator, err := Get("translator")
translator, err := Get("cli.translator")
if err != nil {
t.Fatal(err)
}
@ -31,7 +30,8 @@ func TestRun(t *testing.T) {
With(context).
WithGlobal(map[string]interface{}{"foo": "bar"}).
WithSid(sid)
defer Close(ctx.ID())
defer Close(ctx.id)
output, err := ctx.Exec(map[string]interface{}{"placeholder": "translate\nhello world"})
if err != nil {
t.Fatal(err)
@ -43,16 +43,49 @@ func TestRun(t *testing.T) {
assert.True(t, res.Has("output"))
assert.True(t, res.Has("sid"))
assert.True(t, res.Has("switch"))
assert.Equal(t, "bar", res.Get("global.foo"))
assert.Equal(t, "translate\nhello world", res.Get("input[0].placeholder"))
assert.Len(t, res.Get("switch"), 2)
}
func TestRunWeb(t *testing.T) {
prepare(t)
defer test.Clean()
translator, err := Get("web.translator")
if err != nil {
t.Fatal(err)
}
sid := session.ID()
context, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctx := translator.
Create().
With(context).
WithGlobal(map[string]interface{}{"foo": "bar"}).
WithSid(sid)
defer Close(ctx.id)
web := ctx.Run("hello web world")
resume := web.(ResumeContext)
assert.Equal(t, Input{"hello web world"}, resume.Input)
output := Resume(resume.ID, "translate", "hello web world")
res := any.Of(output).Map().MapStrAny.Dot()
assert.True(t, res.Has("global"))
assert.True(t, res.Has("input"))
assert.True(t, res.Has("output"))
assert.True(t, res.Has("sid"))
assert.True(t, res.Has("switch"))
assert.Equal(t, "bar", res.Get("global.foo"))
assert.Equal(t, "hello web world", res.Get("input[0]"))
assert.Len(t, res.Get("switch"), 2)
}
func prepare(t *testing.T) {
test.Prepare(t, config.Conf)
mirror := os.Getenv("TEST_MOAPI_MIRROR")
fmt.Println(mirror)
secret := os.Getenv("TEST_MOAPI_SECRET")
share.App = share.AppInfo{
Moapi: share.Moapi{Channel: "stable", Mirrors: []string{mirror}, Secret: secret},

View file

@ -77,6 +77,16 @@ type Args []any
// Data data for the template
type Data map[string]interface{}
// ResumeContext the resume context
type ResumeContext struct {
ID string `json:"__id"`
Type string `json:"__type"`
UI string `json:"__ui"`
Input Input `json:"input"`
Node *Node `json:"node"`
Data Data `json:"data"`
}
// AutoFill the autofill
type AutoFill struct {
Value any `json:"value"`

View file

@ -1,12 +0,0 @@
package web
// Web the web UI
type Web struct{}
// Option the web option
type Option struct{}
// Render the Web UI
func (web *Web) Render(args []any, option Option) error {
return nil
}