From 86684f7c5ecf4d643cdf58ae6131902b0ca8533b Mon Sep 17 00:00:00 2001 From: Guillermo Kardolus Date: Mon, 14 Oct 2024 04:33:57 +0200 Subject: [PATCH] feat: add multiline mode support --- README.md | 1 + cmd/chatgpt/main.go | 88 +++++++++++++++++++++++++++++++++------------ types/config.go | 1 + 3 files changed, 67 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 1984a59..0c3d8f6 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,7 @@ environment variables, a config.yaml file, and default values, in that respectiv | `track_token_usage` | If set to true, displays the total token usage after each query in --query mode, helping you monitor API usage. | `false` | | `debug` | If set to true, prints the raw request and response data during API calls, useful for debugging. | `false` | | `skip_tls_verify` | If set to true, skips TLS certificate verification, allowing insecure HTTPS requests. | `false` | +| `multiline` | If set to true, enables multiline input mode in interactive sessions. | `false` | ### LLM-Specific Configuration diff --git a/cmd/chatgpt/main.go b/cmd/chatgpt/main.go index 804b7b9..beafba6 100644 --- a/cmd/chatgpt/main.go +++ b/cmd/chatgpt/main.go @@ -60,17 +60,18 @@ var configMetadata = []ConfigMetadata{ {"models_path", "set-models-path", "/v1/models", "Set the models API endpoint"}, {"auth_header", "set-auth-header", "Authorization", "Set the authorization header"}, {"auth_token_prefix", "set-auth-token-prefix", "Bearer ", "Set the authorization token prefix"}, - {"command_prompt", "set-command-prompt", "[%datetime] [Q%counter] [%usage]", "Set the command prompt format"}, - {"output_prompt", "set-output-prompt", "", "Set the output prompt format"}, + {"command_prompt", "set-command-prompt", "[%datetime] [Q%counter] [%usage]", "Set the command prompt format for interactive mode"}, + {"output_prompt", "set-output-prompt", "", "Set the output prompt format for interactive mode"}, {"temperature", "set-temperature", 1.0, "Set the sampling temperature"}, {"top_p", "set-top-p", 1.0, "Set the top-p value for nucleus sampling"}, {"frequency_penalty", "set-frequency-penalty", 0.0, "Set the frequency penalty"}, {"presence_penalty", "set-presence-penalty", 0.0, "Set the presence penalty"}, {"omit_history", "set-omit-history", false, "Omit history in the conversation"}, - {"auto_create_new_thread", "set-auto-create-new-thread", true, "Automatically create a new thread for each session"}, + {"auto_create_new_thread", "set-auto-create-new-thread", true, "Create a new thread for each interactive session"}, {"track_token_usage", "set-track-token-usage", true, "Track token usage"}, {"skip_tls_verify", "set-skip-tls-verify", false, "Skip TLS certificate verification"}, {"debug", "set-debug", false, "Enable debug mode"}, + {"multiline", "set-multiline", false, "Enables multiline mode while in interactive mode"}, {"name", "set-name", "openai", "The prefix for environment variable overrides"}, } @@ -244,7 +245,7 @@ func run(cmd *cobra.Command, args []string) error { } if interactiveMode { - fmt.Printf("Entering interactive mode. Using thread ‘%s’. Type ‘clear’ to clear the screen, ‘exit’ to quit, or press Ctrl+C.\n\n", hs.GetThread()) + fmt.Printf("Entering interactive mode. Using thread '%s'. Type 'clear' to clear the screen, 'exit' to quit, or press Ctrl+C.\n\n", hs.GetThread()) rl, err := readline.New("") if err != nil { return err @@ -259,30 +260,16 @@ func run(cmd *cobra.Command, args []string) error { for { rl.SetPrompt(commandPrompt(qNum, usage)) - line, err := rl.Readline() - if errors.Is(err, readline.ErrInterrupt) || err == io.EOF { + input, err := readInput(rl, cfg.Multiline) + if err == io.EOF { fmt.Println("Bye!") - break - } - - if line == "clear" { - ansiClearScreenCode := "\033[H\033[2J" - fmt.Print(ansiClearScreenCode) - continue - } - - if line == "exit" || line == "/q" { - fmt.Println("Bye!") - if queryMode { - fmt.Printf("Total tokens used: %d\n", usage) - } - break + return nil } fmtOutputPrompt := utils.FormatPrompt(client.Config.OutputPrompt, qNum, usage, time.Now()) if queryMode { - result, qUsage, err := client.Query(line) + result, qUsage, err := client.Query(input) if err != nil { fmt.Println("Error:", err) } else { @@ -292,7 +279,7 @@ func run(cmd *cobra.Command, args []string) error { } } else { fmt.Print(fmtOutputPrompt) - if err := client.Stream(line); err != nil { + if err := client.Stream(input); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) } else { fmt.Println() @@ -370,6 +357,60 @@ func readConfigWithComments(configPath string) (*yaml.Node, error) { return &rootNode, nil } +func readInput(rl *readline.Instance, multiline bool) (string, error) { + var lines []string + + if multiline { + fmt.Println("Multiline mode enabled. Type 'EOF' on a new line to submit your query.") + } + + // Custom keybinding to handle backspace in multiline mode + rl.Config.SetListener(func(line []rune, pos int, key rune) ([]rune, int, bool) { + // Check if backspace is pressed and if multiline mode is enabled + if multiline && key == readline.CharBackspace && pos == 0 && len(lines) > 0 { + fmt.Print("\033[A") // Move cursor up one line + + // Print the last line without clearing + lastLine := lines[len(lines)-1] + fmt.Print(lastLine) + + // Remove the last line from the slice + lines = lines[:len(lines)-1] + + // Set the cursor at the end of the previous line + return []rune(lastLine), len(lastLine), true + } + return line, pos, false // Default behavior for other keys + }) + + for { + line, err := rl.Readline() + if errors.Is(err, readline.ErrInterrupt) || err == io.EOF { + return "", io.EOF + } + + switch line { + case "clear": + fmt.Print("\033[H\033[2J") // ANSI escape code to clear the screen + continue + case "exit", "/q": + return "", io.EOF + } + + if multiline { + if line == "EOF" { + break + } + lines = append(lines, line) + } else { + return line, nil + } + } + + // Join and return all accumulated lines as a single string + return strings.Join(lines, "\n"), nil +} + func updateConfig(node *yaml.Node, changes map[string]interface{}) error { // If the node is not a document or has no content, create an empty mapping node. if node.Kind != yaml.DocumentNode || len(node.Content) == 0 { @@ -657,6 +698,7 @@ func createConfigFromViper() types.Config { TrackTokenUsage: viper.GetBool("track_token_usage"), SkipTLSVerify: viper.GetBool("skip_tls_verify"), Debug: viper.GetBool("debug"), + Multiline: viper.GetBool("multiline"), } } diff --git a/types/config.go b/types/config.go index 9688efe..61e132f 100644 --- a/types/config.go +++ b/types/config.go @@ -24,4 +24,5 @@ type Config struct { TrackTokenUsage bool `yaml:"track_token_usage"` SkipTLSVerify bool `yaml:"skip_tls_verify"` Debug bool `yaml:"debug"` + Multiline bool `yaml:"multiline"` }