diff --git a/main.go b/main.go index 8e5657e..62de05d 100644 --- a/main.go +++ b/main.go @@ -7,36 +7,6 @@ import ( "github.com/S1ro1/popcorn-cli/src/cmd" ) -func displayAsciiArt() { - art := ` - _ __ _ ______ _ -| | / / | | | ___ \ | | -| |/ / ___ _ __ _ __ ___ | | | |_/ / ___ _| |_ -| \ / _ \ '__| '_ \ / _ \| | | ___ \ / _ \| | __| -| |\ \ __/ | | | | | __/| | | |_/ /| (_) | | |_ -\_| \_/\___|_| |_| |_|\___|_/ \____/ \___/|_|\__| - - POPCORN CLI - GPU MODE - - ┌───────────────────────────────────────┐ - │ ┌─────┐ ┌─────┐ ┌─────┐ │ - │ │ooOoo│ │ooOoo│ │ooOoo│ │▒ - │ │oOOOo│ │oOOOo│ │oOOOo│ │▒ - │ │ooOoo│ │ooOoo│ │ooOoo│ ┌────────┐ │▒ - │ └─────┘ └─────┘ └─────┘ │████████│ │▒ - │ │████████│ │▒ - │ ┌────────────────────────┐ │████████│ │▒ - │ │ │ │████████│ │▒ - │ │ POPCORN GPU COMPUTE │ └────────┘ │▒ - │ │ │ │▒ - │ └────────────────────────┘ │▒ - │ │▒ - └───────────────────────────────────────┘▒ - ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ - ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ -` - fmt.Println(art) -} func main() { @@ -44,6 +14,5 @@ func main() { fmt.Println("POPCORN_API_URL is not set. Please set it to the URL of the Popcorn API.") os.Exit(1) } - displayAsciiArt() cmd.Execute() } diff --git a/src/cmd/popcorn-cli.go b/src/cmd/popcorn-cli.go index 30535f7..df50f71 100644 --- a/src/cmd/popcorn-cli.go +++ b/src/cmd/popcorn-cli.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "os" + "strings" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/spinner" @@ -11,14 +12,11 @@ import ( "github.com/S1ro1/popcorn-cli/src/models" "github.com/S1ro1/popcorn-cli/src/service" + "github.com/S1ro1/popcorn-cli/src/utils" + tea "github.com/charmbracelet/bubbletea" ) -var runnerItems = []list.Item{ - models.RunnerItem{TitleText: "Modal", DescriptionText: "Submit a solution to be evaluated on Modal runners.", Value: "modal"}, - models.RunnerItem{TitleText: "Github", DescriptionText: "Submit a solution to be evaluated on Github runners. This can take a little longer to spin up.", Value: "github"}, -} - var submissionModeItems = []list.Item{ models.SubmissionModeItem{TitleText: "Test", DescriptionText: "Test the solution and give detailed results about passed/failed tests.", Value: "test"}, models.SubmissionModeItem{TitleText: "Benchmark", DescriptionText: "Benchmark the solution, this also runs the tests and afterwards runs the benchmark, returning detailed timing results", Value: "benchmark"}, @@ -35,8 +33,6 @@ type model struct { filepath string leaderboardsList list.Model selectedLeaderboard string - runnersList list.Model - selectedRunner string gpusList list.Model selectedGpu string submissionModeList list.Model @@ -58,6 +54,17 @@ func (m model) Init() tea.Cmd { func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd + if len(m.gpusList.Items()) == 0 && m.modalState == models.ModelStateGpuSelection { + gpus, err := service.GetListItems(func() ([]models.GpuItem, error) { + return service.FetchAvailableGpus(m.selectedLeaderboard) + }) + if err != nil { + m.SetError(fmt.Sprintf("Error fetching GPUs: %s", err)) + return m, tea.Quit + } + m.gpusList = list.New(gpus, list.NewDefaultDelegate(), m.width-2, m.height-2) + m.gpusList.SetSize(m.width-2, m.height-2) + } if !m.finishedOkay { return m, tea.Quit } @@ -72,25 +79,26 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case models.ModelStateLeaderboardSelection: if i := m.leaderboardsList.SelectedItem(); i != nil { m.selectedLeaderboard = i.(models.LeaderboardItem).TitleText - m.modalState = models.ModelStateRunnerSelection - m.runnersList.SetSize(m.width-2, m.height-2) - } - case models.ModelStateRunnerSelection: - if i := m.runnersList.SelectedItem(); i != nil { - m.selectedRunner = i.(models.RunnerItem).Value - m.modalState = models.ModelStateGpuSelection - gpus, err := service.GetListItems(func() ([]models.GpuItem, error) { - return service.FetchAvailableGpus(m.selectedLeaderboard, m.selectedRunner) - }) - if err != nil { - m.SetError(fmt.Sprintf("Error fetching GPUs: %s", err)) - return m, tea.Quit + // No gpu selected in popcorn directives, fetch gpus and move to gpu selection + if m.selectedGpu == "" { + gpus, err := service.GetListItems(func() ([]models.GpuItem, error) { + return service.FetchAvailableGpus(m.selectedLeaderboard) + }) + if err != nil { + m.SetError(fmt.Sprintf("Error fetching GPUs: %s", err)) + return m, tea.Quit + } + if len(gpus) == 0 { + m.SetError("No GPUs available for this leaderboard.") + return m, tea.Quit + } + m.gpusList = list.New(gpus, list.NewDefaultDelegate(), m.width-2, m.height-2) + m.gpusList.SetSize(m.width-2, m.height-2) + m.modalState = models.ModelStateGpuSelection + } else { + m.modalState = models.ModelStateSubmissionModeSelection + m.submissionModeList.SetSize(m.width-2, m.height-2) } - if len(gpus) == 0 { - m.SetError("No GPUs available for this runner and leaderboard.") - return m, tea.Quit - } - m.gpusList = list.New(gpus, list.NewDefaultDelegate(), m.width-2, m.height-2) } case models.ModelStateGpuSelection: if i := m.gpusList.SelectedItem(); i != nil { @@ -119,8 +127,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch m.modalState { case models.ModelStateLeaderboardSelection: m.leaderboardsList.SetSize(listWidth, listHeight) - case models.ModelStateRunnerSelection: - m.runnersList.SetSize(listWidth, listHeight) case models.ModelStateGpuSelection: m.gpusList.SetSize(listWidth, listHeight) case models.ModelStateSubmissionModeSelection: @@ -131,8 +137,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch m.modalState { case models.ModelStateLeaderboardSelection: m.leaderboardsList, cmd = m.leaderboardsList.Update(msg) - case models.ModelStateRunnerSelection: - m.runnersList, cmd = m.runnersList.Update(msg) case models.ModelStateGpuSelection: m.gpusList, cmd = m.gpusList.Update(msg) case models.ModelStateSubmissionModeSelection: @@ -159,8 +163,6 @@ func (m model) View() string { switch m.modalState { case models.ModelStateLeaderboardSelection: content = m.leaderboardsList.View() - case models.ModelStateRunnerSelection: - content = m.runnersList.View() case models.ModelStateGpuSelection: content = m.gpusList.View() case models.ModelStateSubmissionModeSelection: @@ -182,12 +184,14 @@ func (m model) Submit() tea.Cmd { go func() { fileContent, err := os.ReadFile(m.filepath) if err != nil { + p.Send(models.ErrorMsg{Err: fmt.Errorf("error reading file: %s", err)}) m.SetError(fmt.Sprintf("Error reading file: %s", err)) return } - prettyResult, err := service.SubmitSolution(m.selectedLeaderboard, m.selectedRunner, m.selectedGpu, m.selectedSubmissionMode, m.filepath, fileContent) + prettyResult, err := service.SubmitSolution(m.selectedLeaderboard, m.selectedGpu, m.selectedSubmissionMode, m.filepath, fileContent) if err != nil { + p.Send(models.ErrorMsg{Err: fmt.Errorf("error submitting solution: %s", err)}) m.SetError(fmt.Sprintf("Error submitting solution: %s", err)) return } @@ -213,10 +217,34 @@ func Execute() { return } + popcornDirectives, err := utils.GetPopcornDirectives(filepath) + if err != nil { + fmt.Println("Error:", err) + var input string + fmt.Scanln(&input) + if strings.ToLower(input) != "y" { + return + } + } + + var modalState models.ModelState + if popcornDirectives.LeaderboardName != "" && len(popcornDirectives.Gpus) > 0 { + modalState = models.ModelStateSubmissionModeSelection + } else if popcornDirectives.LeaderboardName != "" { + modalState = models.ModelStateGpuSelection + } else { + modalState = models.ModelStateLeaderboardSelection + } + + var selectedGpu string + if len(popcornDirectives.Gpus) > 0 { + selectedGpu = popcornDirectives.Gpus[0] + } + leaderboardItems, err := service.GetListItems(service.FetchLeaderboards) if err != nil { fmt.Println("Error fetching leaderboards:", err) - return + } s := spinner.New() @@ -224,17 +252,18 @@ func Execute() { s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) m := model{ - filepath: filepath, - leaderboardsList: list.New(leaderboardItems, list.NewDefaultDelegate(), 0, 0), - runnersList: list.New(runnerItems, list.NewDefaultDelegate(), 0, 0), - submissionModeList: list.New(submissionModeItems, list.NewDefaultDelegate(), 0, 0), - spinner: s, - modalState: models.ModelStateLeaderboardSelection, - finishedOkay: true, - finalStatus: "", + filepath: filepath, + leaderboardsList: list.New(leaderboardItems, list.NewDefaultDelegate(), 0, 0), + submissionModeList: list.New(submissionModeItems, list.NewDefaultDelegate(), 0, 0), + gpusList: list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0), + spinner: s, + modalState: modalState, + finishedOkay: true, + finalStatus: "", + selectedLeaderboard: popcornDirectives.LeaderboardName, + selectedGpu: selectedGpu, } m.leaderboardsList.Title = "Leaderboards" - m.runnersList.Title = "Runners" p = tea.NewProgram(m) finalModel, err := p.Run() @@ -244,6 +273,7 @@ func Execute() { } m, ok := finalModel.(model) + utils.DisplayAsciiArt() if ok && m.finishedOkay { fmt.Printf("\nResult:\n\n%s\n", m.finalStatus) } else if ok && !m.finishedOkay { diff --git a/src/models/types.go b/src/models/types.go index bcf9d60..83d6469 100644 --- a/src/models/types.go +++ b/src/models/types.go @@ -1,7 +1,7 @@ package models type LeaderboardItem struct { - TitleText string + TitleText string TaskDescription string } diff --git a/src/service/api.go b/src/service/api.go index d259431..f45e137 100644 --- a/src/service/api.go +++ b/src/service/api.go @@ -41,21 +41,20 @@ func FetchLeaderboards() ([]models.LeaderboardItem, error) { return nil, err } - leaderboardNames := make([]models.LeaderboardItem, len(leaderboards)) for i, lb := range leaderboards { task := lb["task"].(map[string]interface{}) leaderboardNames[i] = models.LeaderboardItem{ - TitleText: lb["name"].(string), - TaskDescription: task["description"].(string), + TitleText: lb["name"].(string), + TaskDescription: task["description"].(string), } } return leaderboardNames, nil } -func FetchAvailableGpus(leaderboard string, runner string) ([]models.GpuItem, error) { - resp, err := http.Get(BASE_URL + "/" + leaderboard + "/" + runner + "/gpus") +func FetchAvailableGpus(leaderboard string) ([]models.GpuItem, error) { + resp, err := http.Get(BASE_URL + "/gpus/" + leaderboard) if err != nil { return nil, err } @@ -84,7 +83,7 @@ func FetchAvailableGpus(leaderboard string, runner string) ([]models.GpuItem, er return gpuItems, nil } -func SubmitSolution(leaderboard string, runner string, gpu string, submissionMode string, filename string, fileContent []byte) (string, error) { +func SubmitSolution(leaderboard string, gpu string, submissionMode string, filename string, fileContent []byte) (string, error) { body := &bytes.Buffer{} writer := multipart.NewWriter(body) @@ -101,10 +100,9 @@ func SubmitSolution(leaderboard string, runner string, gpu string, submissionMod return "", fmt.Errorf("error closing form: %s", err) } - url := fmt.Sprintf("%s/%s/%s/%s/%s", + url := fmt.Sprintf("%s/%s/%s/%s", BASE_URL, strings.ToLower(leaderboard), - strings.ToLower(runner), strings.ToLower(gpu), strings.ToLower(submissionMode)) @@ -161,4 +159,3 @@ func GetListItems[T list.Item](fetchFn func() ([]T, error)) ([]list.Item, error) return listItems, nil } - diff --git a/src/utils/utils.go b/src/utils/utils.go new file mode 100644 index 0000000..0ebcf42 --- /dev/null +++ b/src/utils/utils.go @@ -0,0 +1,82 @@ +package utils + +import ( + "fmt" + "os" + "strings" +) + +type PopcornDirectives struct { + LeaderboardName string + Gpus []string +} + +func GetPopcornDirectives(filepath string) (*PopcornDirectives, error) { + var err error = nil + content, err := os.ReadFile(filepath) + + var gpus []string = []string{} + var leaderboard_name string = "" + + if err != nil { + return nil, err + } + + lines := strings.Split(string(content), "\n") + for _, line := range lines { + if !strings.HasPrefix(line, "//") && !strings.HasPrefix(line, "#") { + continue + } + + parts := strings.Split(line, " ") + if parts[0] == "//!POPCORN" || parts[0] == "#!POPCORN" { + arg := strings.ToLower(parts[1]) + if arg == "gpu" || arg == "gpus" { + gpus = parts[2:] + } else if arg == "leaderboard" { + leaderboard_name = parts[2] + } + } + } + + if len(gpus) > 1 { + err = fmt.Errorf("multiple GPUs are not yet supported, continue with the first gpu? (%s) [y/N]", gpus[0]) + gpus = []string{gpus[0]} + } + + return &PopcornDirectives{ + LeaderboardName: leaderboard_name, + Gpus: gpus, + }, err +} + +func DisplayAsciiArt() { + art := ` + _ __ _ ______ _ +| | / / | | | ___ \ | | +| |/ / ___ _ __ _ __ ___ | | | |_/ / ___ _| |_ +| \ / _ \ '__| '_ \ / _ \| | | ___ \ / _ \| | __| +| |\ \ __/ | | | | | __/| | | |_/ /| (_) | | |_ +\_| \_/\___|_| |_| |_|\___|_/ \____/ \___/|_|\__| + + POPCORN CLI - GPU MODE + + ┌───────────────────────────────────────┐ + │ ┌─────┐ ┌─────┐ ┌─────┐ │ + │ │ooOoo│ │ooOoo│ │ooOoo│ │▒ + │ │oOOOo│ │oOOOo│ │oOOOo│ │▒ + │ │ooOoo│ │ooOoo│ │ooOoo│ ┌────────┐ │▒ + │ └─────┘ └─────┘ └─────┘ │████████│ │▒ + │ │████████│ │▒ + │ ┌────────────────────────┐ │████████│ │▒ + │ │ │ │████████│ │▒ + │ │ POPCORN GPU COMPUTE │ └────────┘ │▒ + │ │ │ │▒ + │ └────────────────────────┘ │▒ + │ │▒ + └───────────────────────────────────────┘▒ + ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +` + fmt.Println(art) +}