diff --git a/general/ai/cli.go b/general/ai/cli.go index 979fadac1..3aaa16d50 100644 --- a/general/ai/cli.go +++ b/general/ai/cli.go @@ -6,12 +6,14 @@ import ( "encoding/json" "errors" "fmt" + "github.com/jedib0t/go-pretty/v6/table" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli/utils/cliutils" "github.com/jfrog/jfrog-client-go/artifactory/services/utils" "github.com/jfrog/jfrog-client-go/http/httpclient" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/log" + "github.com/manifoldco/promptui" "github.com/urfave/cli" "io" "net/http" @@ -22,13 +24,16 @@ import ( type ApiCommand string const ( - cliAiAskApiPath = "https://cli-ai-app.jfrog.info/api/ask" - apiHeader = "X-JFrog-CLI-AI" + cliAiAppApiUrl = "https://cli-ai-app.jfrog.info/api/" + askRateLimitHeader = "X-JFrog-CLI-AI" ) -type QuestionBody struct { - Question string `json:"question"` -} +type ApiType string + +const ( + ask ApiType = "ask" + feedback ApiType = "feedback" +) func HowCmd(c *cli.Context) error { if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { @@ -37,7 +42,7 @@ func HowCmd(c *cli.Context) error { if c.NArg() > 0 { return cliutils.WrongNumberOfArgumentsHandler(c) } - log.Output(coreutils.PrintTitle("This AI-based interface converts your natural language inputs into fully functional JFrog CLI commands.\n" + + log.Output(coreutils.PrintLink("This AI-based interface converts your natural language inputs into fully functional JFrog CLI commands.\n" + "NOTE: This is an experimental version and it supports mostly Artifactory and Xray commands.\n")) for { @@ -53,18 +58,84 @@ func HowCmd(c *cli.Context) error { break } } - fmt.Print("\nšŸ¤– Generated command:\n ") + fmt.Print("\nšŸ¤– Generated command:\n") llmAnswer, err := askQuestion(question) if err != nil { return err } - log.Output(coreutils.PrintLink(llmAnswer)) + // Print the generated command within a styled table frame. + // TODO: Use this from jfrog-cli-core + PrintMessageInsideFrame(coreutils.PrintBoldTitle(llmAnswer), " ") + + log.Output() + if err = sendFeedback(); err != nil { + return err + } + log.Output("\n" + coreutils.PrintComment("-------------------") + "\n") } } +func PrintMessageInsideFrame(message, marginLeft string) { + tableWriter := table.NewWriter() + tableWriter.SetOutputMirror(os.Stdout) + if log.IsStdOutTerminal() { + tableWriter.SetStyle(table.StyleLight) + } + // Set padding left for the whole frame (for example, " "). + tableWriter.Style().Box.Left = marginLeft + tableWriter.Style().Box.Left + tableWriter.Style().Box.TopLeft = marginLeft + tableWriter.Style().Box.TopLeft + tableWriter.Style().Box.BottomLeft = marginLeft + tableWriter.Style().Box.BottomLeft + // Remove emojis from non-supported terminals + tableWriter.AppendRow(table.Row{message}) + tableWriter.Render() +} + +type questionBody struct { + Question string `json:"question"` +} + func askQuestion(question string) (response string, err error) { - contentBytes, err := json.Marshal(QuestionBody{Question: question}) + return sendRestAPI(ask, questionBody{Question: question}) +} + +type feedbackBody struct { + IsGoodResponse bool `json:"is_good_response"` +} + +func sendFeedback() (err error) { + isGoodResponse, err := getUserFeedback() + if err != nil { + return err + } + _, err = sendRestAPI(feedback, feedbackBody{IsGoodResponse: isGoodResponse}) + return err +} + +func getUserFeedback() (bool, error) { + // Customize the template to place the options on the same line as the question + templates := &promptui.SelectTemplates{ + Label: "{{ . }}", + Active: "šŸ‘‰ {{ . | cyan }}", + Inactive: " {{ . }}", + } + + prompt := promptui.Select{ + Label: "ā­ Rate this response:", + Items: []string{"šŸ‘ Good response!", "šŸ‘Ž Could be better..."}, + Templates: templates, + HideHelp: true, + HideSelected: true, + } + selected, _, err := prompt.Run() + if err != nil { + return false, err + } + return selected == 0, nil +} + +func sendRestAPI(apiType ApiType, content interface{}) (response string, err error) { + contentBytes, err := json.Marshal(content) if errorutils.CheckError(err) != nil { return } @@ -72,12 +143,14 @@ func askQuestion(question string) (response string, err error) { if errorutils.CheckError(err) != nil { return } - req, err := http.NewRequest(http.MethodPost, cliAiAskApiPath, bytes.NewBuffer(contentBytes)) + req, err := http.NewRequest(http.MethodPost, cliAiAppApiUrl+string(apiType), bytes.NewBuffer(contentBytes)) if errorutils.CheckError(err) != nil { return } req.Header.Set("Content-Type", "application/json") - req.Header.Set(apiHeader, "true") + if apiType == ask { + req.Header.Set(askRateLimitHeader, "true") + } log.Debug(fmt.Sprintf("Sending HTTP %s request to: %s", req.Method, req.URL)) resp, err := client.GetClient().Do(req) if err != nil { @@ -100,6 +173,12 @@ func askQuestion(question string) (response string, err error) { } return } + + if apiType == feedback { + // If the API is feedback, no response is expected + return + } + defer func() { if resp.Body != nil { err = errors.Join(err, errorutils.CheckError(resp.Body.Close())) diff --git a/go.mod b/go.mod index f97e42df8..e53d9f70b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/jfrog/jfrog-cli -go 1.22.3 +go 1.22.6 replace ( // Should not be updated to 0.2.6 due to a bug (https://github.com/jfrog/jfrog-cli-core/pull/372) @@ -13,7 +13,7 @@ replace ( require ( github.com/agnivade/levenshtein v1.1.1 github.com/buger/jsonparser v1.1.1 - github.com/docker/docker v27.1.2+incompatible + github.com/docker/docker v27.2.0+incompatible github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 github.com/jfrog/archiver/v3 v3.6.1 github.com/jfrog/build-info-go v1.9.35 @@ -24,6 +24,7 @@ require ( github.com/jfrog/jfrog-cli-security v1.8.0 github.com/jfrog/jfrog-client-go v1.46.1 github.com/jszwec/csvutil v1.10.0 + github.com/manifoldco/promptui v0.9.0 github.com/stretchr/testify v1.9.0 github.com/testcontainers/testcontainers-go v0.33.0 github.com/urfave/cli v1.22.15 @@ -59,7 +60,6 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/fatih/color v1.17.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/forPelevin/gomoji v1.2.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect @@ -94,7 +94,6 @@ require ( github.com/ktrysmt/go-bitbucket v0.9.73 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect diff --git a/go.sum b/go.sum index 6c23b575e..1cadf735a 100644 --- a/go.sum +++ b/go.sum @@ -707,8 +707,8 @@ github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+ github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v27.1.2+incompatible h1:AhGzR1xaQIy53qCkxARaFluI00WPGtXn0AJuoQsVYTY= -github.com/docker/docker v27.1.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= +github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -737,8 +737,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=