From 5921da9ed75b6e470c56df416a6215a80d8af6f0 Mon Sep 17 00:00:00 2001 From: David Gamba Date: Thu, 5 Dec 2024 23:43:34 -0700 Subject: [PATCH] chatgpt: refactor UI to use bubbletea library --- chatgpt/go.mod | 42 +++++-- chatgpt/go.sum | 102 +++++++++++++--- chatgpt/main.go | 35 +----- chatgpt/query.go | 100 ++++++++++++++++ chatgpt/styles.go | 18 +++ chatgpt/ui.go | 298 ++++++++++++++++++++++++++++++++++++++++++++++ chatgpt/view.go | 70 +++++++++++ 7 files changed, 607 insertions(+), 58 deletions(-) create mode 100644 chatgpt/query.go create mode 100644 chatgpt/styles.go create mode 100644 chatgpt/ui.go create mode 100644 chatgpt/view.go diff --git a/chatgpt/go.mod b/chatgpt/go.mod index ff80f81..cf4ccff 100644 --- a/chatgpt/go.mod +++ b/chatgpt/go.mod @@ -1,17 +1,45 @@ module github.com/DavidGamba/dgtools/chatgpt -go 1.20 +go 1.23 require ( - github.com/DavidGamba/dgtools/run v0.7.0 - github.com/DavidGamba/go-getoptions v0.27.0 + github.com/DavidGamba/dgtools/run v0.9.0 + github.com/DavidGamba/go-getoptions v0.31.0 + github.com/charmbracelet/bubbles v0.20.0 + github.com/charmbracelet/bubbletea v1.2.4 + github.com/charmbracelet/glamour v0.8.0 + github.com/charmbracelet/lipgloss v1.0.0 github.com/chzyer/readline v1.5.1 - github.com/fatih/color v1.15.0 - github.com/sashabaranov/go-openai v1.9.3 + github.com/fatih/color v1.18.0 + github.com/sashabaranov/go-openai v1.36.0 ) require ( + github.com/alecthomas/chroma/v2 v2.14.0 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/charmbracelet/x/ansi v0.5.2 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/dlclark/regexp2 v1.11.4 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.18 // indirect - golang.org/x/sys v0.8.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/yuin/goldmark v1.7.8 // indirect + github.com/yuin/goldmark-emoji v1.0.4 // indirect + golang.org/x/net v0.32.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect + golang.org/x/text v0.21.0 // indirect ) diff --git a/chatgpt/go.sum b/chatgpt/go.sum index 4ee46e4..6111553 100644 --- a/chatgpt/go.sum +++ b/chatgpt/go.sum @@ -1,29 +1,97 @@ -github.com/DavidGamba/dgtools/run v0.7.0 h1:ENTskNQvBM1/n/b42PxqJktU9EzQVqPZRGUBtVdEVdE= -github.com/DavidGamba/dgtools/run v0.7.0/go.mod h1:3P1fMJupTWqsiE8IXsXrk2HtgkZBTCFRLbaTjRlmDe0= -github.com/DavidGamba/go-getoptions v0.27.0 h1:hldKJSwO9SwvR+z9pe6ojhEcYECrRiO/bar9B7MnBKA= -github.com/DavidGamba/go-getoptions v0.27.0/go.mod h1:qLaLSYeQ8sUVOfKuu5JT5qKKS3OCwyhkYSJnoG+ggmo= +github.com/DavidGamba/dgtools/run v0.9.0 h1:Hg0v4ExUMd6Vzf9x9Bqr2yxreZtZpqlcAi8tI86QtIM= +github.com/DavidGamba/dgtools/run v0.9.0/go.mod h1:GVGYL0p5hdBaQ9uIAslXh1g1TTfr0igMSDVTwhhy9q4= +github.com/DavidGamba/go-getoptions v0.31.0 h1:RvVrggcstVLZiT6BEHlHcXhPXFSCL2fwhxXz4j4xOjo= +github.com/DavidGamba/go-getoptions v0.31.0/go.mod h1:zE97E3PR9P3BI/HKyNYgdMlYxodcuiC6W68KIgeYT84= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= +github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= +github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= +github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= +github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs= +github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw= +github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= +github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= +github.com/charmbracelet/x/ansi v0.5.2 h1:dEa1x2qdOZXD/6439s+wF7xjV+kZLu/iN00GuXXrU9E= +github.com/charmbracelet/x/ansi v0.5.2/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/sashabaranov/go-openai v1.5.7 h1:8DGgRG+P7yWixte5j720y6yiXgY3Hlgcd0gcpHdltfo= -github.com/sashabaranov/go-openai v1.5.7/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= -github.com/sashabaranov/go-openai v1.9.3 h1:uNak3Rn5pPsKRs9bdT7RqRZEyej/zdZOEI2/8wvrFtM= -github.com/sashabaranov/go-openai v1.9.3/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/sashabaranov/go-openai v1.36.0 h1:fcSrn8uGuorzPWCBp8L0aCR95Zjb/Dd+ZSML0YZy9EI= +github.com/sashabaranov/go-openai v1.36.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= +github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-emoji v1.0.4 h1:vCwMkPZSNefSUnOW2ZKRUjBSD5Ok3W78IXhGxxAEF90= +github.com/yuin/goldmark-emoji v1.0.4/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= diff --git a/chatgpt/main.go b/chatgpt/main.go index ec4fe87..441656a 100644 --- a/chatgpt/main.go +++ b/chatgpt/main.go @@ -56,7 +56,7 @@ func program(args []string) int { opt.SetUnknownMode(getoptions.Pass) opt.Bool("quiet", false, opt.GetEnv("QUIET")) opt.String("config-file", "", opt.GetEnv("CHATGPT_CONFIG_FILE")) - opt.SetCommandFn(Run) + opt.SetCommandFn(UIRun) opt.HelpCommand("help", opt.Alias("?")) remaining, err := opt.Parse(args[1:]) if err != nil { @@ -226,39 +226,6 @@ func printMessageHistoryContext(messageHistory *[]openai.ChatCompletionMessage) return nil } -func chat(ctx context.Context, messageHistory *[]openai.ChatCompletionMessage, message string) error { - // Post message to OpenAI - client := openai.NewClient(os.Getenv("OPENAI_API_KEY")) - - // Add new user message - *messageHistory = append(*messageHistory, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleUser, - Content: message, - }) - - resp, err := client.CreateChatCompletion( - ctx, - openai.ChatCompletionRequest{ - Model: openai.GPT3Dot5Turbo, - Messages: *messageHistory, - }, - ) - - if err != nil { - return fmt.Errorf("ChatCompletion error: %v", err) - } - - content := resp.Choices[0].Message.Content - // 🤖 ⏩ - fmt.Println("🤖 " + content) - *messageHistory = append(*messageHistory, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleAssistant, - Content: content, - }) - - return nil -} - func image(ctx context.Context, messageHistory *[]openai.ChatCompletionMessage, message string, size string) error { // Post message to OpenAI client := openai.NewClient(os.Getenv("OPENAI_API_KEY")) diff --git a/chatgpt/query.go b/chatgpt/query.go new file mode 100644 index 0000000..5dd14f4 --- /dev/null +++ b/chatgpt/query.go @@ -0,0 +1,100 @@ +package main + +import ( + "context" + "fmt" + "os" + + tea "github.com/charmbracelet/bubbletea" + "github.com/sashabaranov/go-openai" +) + +type queryMsg struct { + content string + err error +} + +func (t *thread) sendQueryMsg(ctx context.Context, message string) tea.Cmd { + return func() tea.Msg { + content, err := t.query(ctx, message) + return queryMsg{content, err} + } +} + +type thread struct { + client *openai.Client + messageHistory *[]openai.ChatCompletionMessage +} + +func NewThread() *thread { + return &thread{ + client: openai.NewClient(os.Getenv("OPENAI_API_KEY")), + messageHistory: &[]openai.ChatCompletionMessage{}, + } +} + +func (t *thread) reset() { + t.messageHistory = &[]openai.ChatCompletionMessage{} +} + +func (t *thread) query(ctx context.Context, message string) (string, error) { + // Add new user message + *t.messageHistory = append(*t.messageHistory, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleUser, + Content: message, + }) + + resp, err := t.client.CreateChatCompletion( + ctx, + openai.ChatCompletionRequest{ + Model: openai.GPT4, + Messages: *t.messageHistory, + }, + ) + + if err != nil { + return "", fmt.Errorf("ChatCompletion error: %v", err) + } + + content := resp.Choices[0].Message.Content + + *t.messageHistory = append(*t.messageHistory, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleAssistant, + Content: content, + }) + + return content, nil +} + +func chat(ctx context.Context, messageHistory *[]openai.ChatCompletionMessage, message string) error { + // Post message to OpenAI + client := openai.NewClient(os.Getenv("OPENAI_API_KEY")) + + // Add new user message + *messageHistory = append(*messageHistory, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleUser, + Content: message, + }) + + resp, err := client.CreateChatCompletion( + ctx, + openai.ChatCompletionRequest{ + Model: openai.GPT3Dot5Turbo, + Messages: *messageHistory, + }, + ) + + if err != nil { + return fmt.Errorf("ChatCompletion error: %v", err) + } + + content := resp.Choices[0].Message.Content + // 🤖 ⏩ + fmt.Println("🤖 " + content) + *messageHistory = append(*messageHistory, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleAssistant, + Content: content, + }) + + return nil +} diff --git a/chatgpt/styles.go b/chatgpt/styles.go new file mode 100644 index 0000000..fc70fb6 --- /dev/null +++ b/chatgpt/styles.go @@ -0,0 +1,18 @@ +package main + +import ( + "github.com/charmbracelet/lipgloss" +) + +var ( + normalModeStyle = lipgloss.NewStyle(). + Background(lipgloss.Color("#FADA7A")). + Foreground(lipgloss.Color("0")). + Bold(true) + textModeStyle = lipgloss.NewStyle(). + Background(lipgloss.Color("57")). + Foreground(lipgloss.Color("230")). + Bold(true) + spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("69")) + senderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("5")) +) diff --git a/chatgpt/ui.go b/chatgpt/ui.go new file mode 100644 index 0000000..44d1c0c --- /dev/null +++ b/chatgpt/ui.go @@ -0,0 +1,298 @@ +package main + +import ( + "context" + "strings" + "time" + + "github.com/DavidGamba/go-getoptions" + "github.com/charmbracelet/bubbles/cursor" + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/spinner" + "github.com/charmbracelet/bubbles/stopwatch" + "github.com/charmbracelet/bubbles/textarea" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/glamour" + "github.com/charmbracelet/lipgloss" +) + +func UIRun(ctx context.Context, opt *getoptions.GetOpt, args []string) error { + p := tea.NewProgram(newModel(), tea.WithAltScreen()) + if _, err := p.Run(); err != nil { + return err + } + return nil +} + +/* +This example assumes an existing understanding of commands and messages. If you +haven't already read our tutorials on the basics of Bubble Tea and working +with commands, we recommend reading those first. + +Find them at: +https://github.com/charmbracelet/bubbletea/tree/master/tutorials/commands +https://github.com/charmbracelet/bubbletea/tree/master/tutorials/basics +*/ + +// current typing mode +type currentMode uint + +const ( + textMode currentMode = iota + normalMode +) + +var ( + width = 200 +) + +type model struct { + mode currentMode + viewReady bool + waiting bool + showRaw bool + + messages []string + rawMessages []string + + keymap keymap + + // Models + stopwatch stopwatch.Model + spinner spinner.Model + viewport viewport.Model + textarea textarea.Model + help help.Model + + threads *thread + + quitting bool + + err error +} + +type keymap struct { + start key.Binding + stop key.Binding + reset key.Binding + showRaw key.Binding + tab key.Binding + esc key.Binding + insert key.Binding + submit key.Binding + quit key.Binding + + j key.Binding + k key.Binding +} + +func newModel() model { + m := model{} + m.mode = textMode + + m.threads = NewThread() + + // Initialize models + m.stopwatch = stopwatch.NewWithInterval(time.Millisecond) + m.spinner = spinner.New() + m.spinner.Spinner = spinner.Globe + m.spinner.Style = spinnerStyle + // m.spinner.Spinner = spinner.Points + + ta := textarea.New() + ta.Placeholder = "Send a message..." + ta.Focus() + + ta.Prompt = "┃ " + ta.CharLimit = width + + ta.SetHeight(3) + + // Remove cursor line styling + // ta.FocusedStyle.CursorLine = lipgloss.NewStyle() + + ta.ShowLineNumbers = false + + ta.KeyMap.InsertNewline.SetEnabled(true) + + m.textarea = ta + + m.keymap = keymap{ + reset: key.NewBinding( + key.WithKeys("ctrl+l"), + key.WithHelp("ctrl+l", "reset"), + ), + showRaw: key.NewBinding( + key.WithKeys("r"), + key.WithHelp("r", "show raw"), + ), + tab: key.NewBinding( + key.WithKeys("tab"), + key.WithHelp("tab", "switch view"), + ), + esc: key.NewBinding( + key.WithKeys("esc"), + key.WithHelp("esc", "Change to normal mode"), + ), + insert: key.NewBinding( + key.WithKeys("i"), + key.WithHelp("i", "Change to insert mode"), + ), + submit: key.NewBinding( + key.WithKeys("ctrl+s"), + key.WithHelp("ctrl+s", "submit"), + ), + k: key.NewBinding( + key.WithKeys("k", "up"), + key.WithHelp("k / ↑", "Up"), + ), + j: key.NewBinding( + key.WithKeys("j", "down"), + key.WithHelp("j / ↓", "Down"), + ), + quit: key.NewBinding( + key.WithKeys("ctrl+c", "q"), + key.WithHelp("q", "quit"), + ), + } + m.keymap.stop.SetEnabled(false) + m.help = help.New() + m.err = nil + return m +} + +func (m model) Init() tea.Cmd { + // init the submodels + return tea.Batch(m.spinner.Tick, textarea.Blink) +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + var cmds []tea.Cmd + + switch msg := msg.(type) { + + case tea.WindowSizeMsg: + headerHeight := lipgloss.Height(m.headerView()) + footerHeight := lipgloss.Height(m.footerView()) + promptHeight := lipgloss.Height(m.promptView()) + verticalMarginHeight := headerHeight + footerHeight + promptHeight + + if !m.viewReady { + // Since this program is using the full size of the viewport we + // need to wait until we've received the window dimensions before + // we can initialize the viewport. The initial dimensions come in + // quickly, though asynchronously, which is why we wait for them + // here. + m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight) + m.viewport.YPosition = headerHeight + m.viewReady = true + } + m.textarea.SetWidth(msg.Width) + m.viewport.Width = msg.Width + m.viewport.Height = msg.Height - verticalMarginHeight + + return m, nil + case cursor.BlinkMsg: + // Textarea should also process cursor blinks. + var cmd tea.Cmd + m.textarea, cmd = m.textarea.Update(msg) + return m, cmd + case tea.KeyMsg: + switch m.mode { + case textMode: + switch { + case key.Matches(msg, m.keymap.esc): + m.mode = normalMode + return m, nil + case key.Matches(msg, m.keymap.submit): + v := m.textarea.Value() + + if v == "" { + // Don't send empty messages. + return m, nil + } + m.messages = append(m.messages, senderStyle.Render("You: ")+v) + m.rawMessages = append(m.rawMessages, v) + m.viewport.SetContent(strings.Join(m.messages, "\n")) + m.textarea.Reset() + m.waiting = true + m.viewport.GotoBottom() + + cmds = append(cmds, m.threads.sendQueryMsg(context.Background(), v)) + cmds = append(cmds, m.stopwatch.Reset(), m.stopwatch.Start()) + default: + // Send all other keypresses to the textarea. + var cmd tea.Cmd + m.textarea, cmd = m.textarea.Update(msg) + return m, cmd + } + case normalMode: + switch { + case key.Matches(msg, m.keymap.quit): + m.quitting = true + return m, tea.Quit + case key.Matches(msg, m.keymap.insert): + m.mode = textMode + return m, nil + case key.Matches(msg, m.keymap.j): + m.viewport.LineDown(1) + case key.Matches(msg, m.keymap.k): + m.viewport.LineUp(1) + case key.Matches(msg, m.keymap.showRaw): + m.showRaw = !m.showRaw + if m.showRaw { + m.viewport.SetContent(strings.Join(m.rawMessages, "\n")) + } else { + m.viewport.SetContent(strings.Join(m.messages, "\n")) + } + case key.Matches(msg, m.keymap.reset): + m.textarea.Reset() + m.viewport.SetContent("") + m.messages = []string{} + m.rawMessages = []string{} + case key.Matches(msg, m.keymap.start, m.keymap.stop): + m.keymap.stop.SetEnabled(!m.stopwatch.Running()) + m.keymap.start.SetEnabled(m.stopwatch.Running()) + cmds = append(cmds, m.stopwatch.Toggle()) + // cmds = append(cmds, m.stopwatch.Reset()) + } + } + case spinner.TickMsg: + m.spinner, cmd = m.spinner.Update(msg) + cmds = append(cmds, cmd) + + case queryMsg: + if msg.err != nil { + m.messages = append(m.messages, msg.err.Error()) + } else { + md, err := glamour.Render(msg.content, "dark") + if err != nil { + m.messages = append(m.messages, err.Error()) + m.messages = append(m.messages, msg.content) + m.rawMessages = append(m.rawMessages, err.Error()) + m.rawMessages = append(m.rawMessages, msg.content) + } + m.rawMessages = append(m.rawMessages, msg.content) + m.messages = append(m.messages, md) + } + m.viewport.SetContent(strings.Join(m.messages, "\n")) + m.viewport.GotoBottom() + m.waiting = false + + cmds = append(cmds, m.stopwatch.Stop()) + } + + m.spinner, cmd = m.spinner.Update(msg) + cmds = append(cmds, cmd) + + m.stopwatch, cmd = m.stopwatch.Update(msg) + cmds = append(cmds, cmd) + + m.viewport, cmd = m.viewport.Update(msg) + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} diff --git a/chatgpt/view.go b/chatgpt/view.go new file mode 100644 index 0000000..8f696ff --- /dev/null +++ b/chatgpt/view.go @@ -0,0 +1,70 @@ +package main + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/key" +) + +func (m model) View() string { + return fmt.Sprintf("%s\n%s\n%s\n%s", m.headerView(), m.promptView(), m.viewportView(), m.footerView()) +} + +func (m model) headerView() string { + var s string + switch m.mode { + case textMode: + s += textModeStyle.Width(m.viewport.Width).Render("Text Mode") + s += "\n" + textModeStyle.Render(m.helpView()) + case normalMode: + s += normalModeStyle.Width(m.viewport.Width).Render("Normal Mode") + s += "\n" + normalModeStyle.Render(m.helpView()) + } + if m.waiting { + s += "\n" + m.spinner.View() + " " + m.stopwatch.View() + } else { + if len(m.messages) > 0 { + s += "\n" + " " + m.stopwatch.View() + } else { + s += "\n" + } + } + return s +} + +func (m model) promptView() string { + return m.textarea.View() +} + +func (m model) viewportView() string { + return m.viewport.View() +} + +func (m model) footerView() string { + var s string + switch m.mode { + case textMode: + s += textModeStyle.Width(m.viewport.Width).Render("") + case normalMode: + s += normalModeStyle.Width(m.viewport.Width).Render("") + } + return s +} + +func (m model) helpView() string { + switch m.mode { + case textMode: + return m.help.ShortHelpView([]key.Binding{ + m.keymap.esc, + m.keymap.submit, + }) + case normalMode: + return m.help.ShortHelpView([]key.Binding{ + m.keymap.insert, + m.keymap.showRaw, + m.keymap.reset, + m.keymap.quit, + }) + } + return "" +}