2024-01-01 13:31:34 +08:00
|
|
|
from typing import List, Optional, Dict, Tuple
|
2023-12-31 22:39:45 +08:00
|
|
|
from .iobase import pipe_interaction
|
|
|
|
from abc import ABC, abstractmethod
|
2024-01-01 13:31:34 +08:00
|
|
|
from uuid import uuid4
|
2023-12-31 22:39:45 +08:00
|
|
|
|
|
|
|
|
|
|
|
class Widget(ABC):
|
|
|
|
"""
|
|
|
|
Abstract base class for widgets
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self._rendered = False
|
2024-01-01 13:31:34 +08:00
|
|
|
# Prefix for IDs/keys in the widget
|
|
|
|
self._id_prefix = self.gen_id_prefix()
|
2023-12-31 22:39:45 +08:00
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def _in_chatmark(self) -> str:
|
|
|
|
"""
|
|
|
|
Generate ChatMark syntax for the widget
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def _parse_response(self, response: Dict) -> None:
|
|
|
|
"""
|
|
|
|
Parse ChatMark response from user input
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
def render(self) -> None:
|
|
|
|
"""
|
|
|
|
Render the widget to receive user input
|
|
|
|
"""
|
|
|
|
if self._rendered:
|
|
|
|
# already rendered once
|
|
|
|
# not sure if the constraint is necessary
|
|
|
|
# could be removed if re-rendering is needed
|
|
|
|
raise RuntimeError("Widget can only be rendered once")
|
|
|
|
|
|
|
|
self._rendered = True
|
|
|
|
|
|
|
|
lines = [
|
|
|
|
"```chatmark",
|
|
|
|
self._in_chatmark(),
|
|
|
|
"```",
|
|
|
|
]
|
|
|
|
|
|
|
|
chatmark = "\n".join(lines)
|
|
|
|
response = pipe_interaction(chatmark)
|
|
|
|
self._parse_response(response)
|
|
|
|
|
2024-01-01 13:31:34 +08:00
|
|
|
@staticmethod
|
|
|
|
def gen_id_prefix() -> str:
|
|
|
|
return uuid4().hex
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def gen_id(id_prefix: str, index: int) -> str:
|
|
|
|
return f"{id_prefix}_{index}"
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def parse_id(a_id: str) -> Tuple[Optional[str], Optional[int]]:
|
|
|
|
try:
|
|
|
|
id_prefix, index = a_id.split("_")
|
|
|
|
return id_prefix, int(index)
|
|
|
|
except Exception:
|
|
|
|
return None, None
|
|
|
|
|
2023-12-31 22:39:45 +08:00
|
|
|
|
|
|
|
class Checkbox(Widget):
|
|
|
|
"""
|
2024-01-01 13:31:34 +08:00
|
|
|
ChatMark syntax:
|
2023-12-31 22:39:45 +08:00
|
|
|
```chatmark
|
|
|
|
Which files would you like to commit? I've suggested a few.
|
|
|
|
> [x](file1) devchat/engine/prompter.py
|
|
|
|
> [x](file2) devchat/prompt.py
|
|
|
|
> [](file3) tests/test_cli_prompt.py
|
|
|
|
```
|
|
|
|
|
|
|
|
Response:
|
|
|
|
```yaml
|
|
|
|
file1: checked
|
|
|
|
file3: checked
|
|
|
|
```
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
options: List[str],
|
|
|
|
check_states: Optional[List[bool]] = None,
|
|
|
|
title: Optional[str] = None,
|
|
|
|
):
|
|
|
|
"""
|
|
|
|
options: options to be selected
|
|
|
|
check_states: initial check states of options, default to all False
|
|
|
|
title: title of the widget
|
|
|
|
"""
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
if check_states is not None:
|
|
|
|
assert len(options) == len(check_states)
|
|
|
|
else:
|
|
|
|
check_states = [False for _ in options]
|
|
|
|
|
|
|
|
self._options = options
|
|
|
|
self._states = check_states
|
|
|
|
self._title = title
|
|
|
|
|
|
|
|
self._selections: Optional[List[int]] = None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def selections(self) -> Optional[List[int]]:
|
|
|
|
"""
|
|
|
|
Get the indices of selected options
|
|
|
|
"""
|
|
|
|
return self._selections
|
|
|
|
|
|
|
|
@property
|
|
|
|
def options(self) -> List[str]:
|
|
|
|
"""
|
|
|
|
Get the options
|
|
|
|
"""
|
|
|
|
return self._options
|
|
|
|
|
|
|
|
def _in_chatmark(self) -> str:
|
|
|
|
"""
|
|
|
|
Generate ChatMark syntax for checkbox options
|
2024-01-01 13:31:34 +08:00
|
|
|
Use the index of option to generate id/key
|
2023-12-31 22:39:45 +08:00
|
|
|
"""
|
|
|
|
lines = []
|
|
|
|
|
|
|
|
if self._title:
|
|
|
|
lines.append(self._title)
|
|
|
|
|
|
|
|
for idx, (option, state) in enumerate(zip(self._options, self._states)):
|
|
|
|
mark = "[x]" if state else "[]"
|
2024-01-01 13:31:34 +08:00
|
|
|
key = self.gen_id(self._id_prefix, idx)
|
|
|
|
lines.append(f"> {mark}({key}) {option}")
|
2023-12-31 22:39:45 +08:00
|
|
|
|
|
|
|
text = "\n".join(lines)
|
|
|
|
return text
|
|
|
|
|
|
|
|
def _parse_response(self, response: Dict):
|
|
|
|
selections = []
|
|
|
|
for key, value in response.items():
|
2024-01-01 13:31:34 +08:00
|
|
|
prefix, index = self.parse_id(key)
|
|
|
|
# check if the prefix is the same as the widget's
|
|
|
|
if prefix != self._id_prefix:
|
|
|
|
continue
|
|
|
|
|
2023-12-31 22:39:45 +08:00
|
|
|
if value == "checked":
|
2024-01-01 13:31:34 +08:00
|
|
|
selections.append(index)
|
|
|
|
|
2023-12-31 22:39:45 +08:00
|
|
|
self._selections = selections
|
|
|
|
|
|
|
|
|
|
|
|
class TextEditor(Widget):
|
|
|
|
"""
|
2024-01-01 13:31:34 +08:00
|
|
|
ChatMark syntax:
|
2023-12-31 22:39:45 +08:00
|
|
|
```chatmark
|
|
|
|
I've drafted a commit message for you as below. Feel free to modify it.
|
|
|
|
|
|
|
|
> | (ID)
|
|
|
|
> fix: prevent racing of requests
|
|
|
|
>
|
|
|
|
> Introduce a request id and a reference to latest request. Dismiss
|
|
|
|
> incoming responses other than from latest request.
|
|
|
|
>
|
|
|
|
> Reviewed-by: Z
|
|
|
|
> Refs: #123
|
|
|
|
```
|
|
|
|
|
|
|
|
Response:
|
|
|
|
```yaml
|
|
|
|
ID: |
|
|
|
|
fix: prevent racing of requests
|
|
|
|
|
|
|
|
Introduce a request ID and a reference to latest request. Dismiss
|
|
|
|
incoming responses other than from latest request.
|
|
|
|
|
|
|
|
Reviewed-by: Z
|
|
|
|
Refs: #123
|
|
|
|
```
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, text: str, title: Optional[str] = None):
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
self._title = title
|
|
|
|
self._text = text
|
|
|
|
|
2024-01-01 13:31:34 +08:00
|
|
|
self._editor_key = self.gen_id(self._id_prefix, 0)
|
2023-12-31 22:39:45 +08:00
|
|
|
self._new_text: Optional[str] = None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def new_text(self):
|
|
|
|
return self._new_text
|
|
|
|
|
2024-01-01 13:31:34 +08:00
|
|
|
def _in_chatmark(self) -> str:
|
2023-12-31 22:39:45 +08:00
|
|
|
"""
|
|
|
|
Generate ChatMark syntax for text editor
|
2024-01-01 13:31:34 +08:00
|
|
|
Use _editor_key as id
|
2023-12-31 22:39:45 +08:00
|
|
|
"""
|
|
|
|
lines = self._text.split("\n")
|
|
|
|
new_lines = []
|
|
|
|
|
|
|
|
if self._title:
|
|
|
|
new_lines.append(self._title)
|
2024-01-01 13:31:34 +08:00
|
|
|
|
|
|
|
new_lines.append(f"> | ({self._editor_key})")
|
2023-12-31 22:39:45 +08:00
|
|
|
new_lines.extend([f"> {line}" for line in lines])
|
|
|
|
|
|
|
|
text = "\n".join(new_lines)
|
|
|
|
return text
|
|
|
|
|
|
|
|
def _parse_response(self, response: Dict):
|
2024-01-01 13:31:34 +08:00
|
|
|
self._new_text = response.get(self._editor_key, None)
|
2023-12-31 22:39:45 +08:00
|
|
|
|
|
|
|
|
|
|
|
class Radio(Widget):
|
|
|
|
"""
|
2024-01-01 13:31:34 +08:00
|
|
|
ChatMark syntax:
|
2023-12-31 22:39:45 +08:00
|
|
|
```chatmark
|
|
|
|
How would you like to make the change?
|
|
|
|
> - (insert) Insert the new code.
|
|
|
|
> - (new) Put the code in a new file.
|
|
|
|
> - (replace) Replace the current code.
|
|
|
|
```
|
|
|
|
|
|
|
|
Reponse:
|
|
|
|
```yaml
|
|
|
|
replace: checked
|
|
|
|
```
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
options: List[str],
|
2024-01-02 11:50:17 +08:00
|
|
|
# TODO: implement default_selected after the design is ready
|
2023-12-31 22:39:45 +08:00
|
|
|
# default_selected: Optional[int] = None,
|
|
|
|
title: Optional[str] = None,
|
|
|
|
) -> None:
|
|
|
|
"""
|
|
|
|
options: options to be selected
|
|
|
|
default_selected: index of the option to be selected by default, default to None
|
|
|
|
title: title of the widget
|
|
|
|
"""
|
2024-01-02 11:50:17 +08:00
|
|
|
# TODO: implement default_selected after the design is ready
|
2023-12-31 22:39:45 +08:00
|
|
|
# if default_selected is not None:
|
|
|
|
# assert 0 <= default_selected < len(options)
|
|
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
self._options = options
|
|
|
|
self._title = title
|
|
|
|
|
|
|
|
self._selection: Optional[int] = None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def options(self) -> List[str]:
|
|
|
|
"""
|
|
|
|
Return the options
|
|
|
|
"""
|
|
|
|
return self._options
|
|
|
|
|
|
|
|
@property
|
|
|
|
def selection(self) -> Optional[int]:
|
|
|
|
"""
|
|
|
|
Return the index of the selected option
|
|
|
|
"""
|
|
|
|
return self._selection
|
|
|
|
|
|
|
|
def _in_chatmark(self) -> str:
|
|
|
|
"""
|
|
|
|
Generate ChatMark syntax for options
|
2024-01-01 13:31:34 +08:00
|
|
|
Use the index of option to generate id/key
|
2023-12-31 22:39:45 +08:00
|
|
|
"""
|
|
|
|
lines = []
|
|
|
|
|
|
|
|
if self._title:
|
|
|
|
lines.append(self._title)
|
|
|
|
|
|
|
|
for idx, option in enumerate(self._options):
|
2024-01-01 13:31:34 +08:00
|
|
|
key = self.gen_id(self._id_prefix, idx)
|
|
|
|
lines.append(f"> - ({key}) {option}")
|
2023-12-31 22:39:45 +08:00
|
|
|
|
|
|
|
text = "\n".join(lines)
|
|
|
|
return text
|
|
|
|
|
|
|
|
def _parse_response(self, response: Dict):
|
|
|
|
selected = None
|
|
|
|
for key, value in response.items():
|
2024-01-01 13:31:34 +08:00
|
|
|
prefix, idx = self.parse_id(key)
|
|
|
|
# check if the prefix is the same as the widget's
|
|
|
|
if prefix != self._id_prefix:
|
|
|
|
continue
|
|
|
|
|
2023-12-31 22:39:45 +08:00
|
|
|
if value == "checked":
|
2024-01-01 13:31:34 +08:00
|
|
|
selected = idx
|
2023-12-31 22:39:45 +08:00
|
|
|
break
|
|
|
|
|
|
|
|
self._selection = selected
|
|
|
|
|
|
|
|
|
|
|
|
class Button(Widget):
|
|
|
|
"""
|
|
|
|
ChatMark syntax:
|
|
|
|
```chatmark
|
|
|
|
Would you like to pay $0.02 for this LLM query?
|
|
|
|
> (Confirm) Yes, go ahead!
|
|
|
|
> (Cancel) No, let's skip this.
|
|
|
|
```
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
Confirm: clicked
|
|
|
|
```
|
|
|
|
|
|
|
|
# NOTE: almost the same as Radio essentially
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
buttons: List[str],
|
|
|
|
title: Optional[str] = None,
|
|
|
|
) -> None:
|
|
|
|
"""
|
|
|
|
buttons: button names to show
|
|
|
|
title: title of the widget
|
|
|
|
"""
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
self._buttons = buttons
|
|
|
|
self._title = title
|
|
|
|
|
|
|
|
self._clicked: Optional[int] = None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def clicked(self) -> Optional[int]:
|
|
|
|
"""
|
|
|
|
Return the index of the clicked button
|
|
|
|
"""
|
|
|
|
return self._clicked
|
|
|
|
|
|
|
|
@property
|
|
|
|
def buttons(self) -> List[str]:
|
|
|
|
"""
|
|
|
|
Return the buttons
|
|
|
|
"""
|
|
|
|
return self._buttons
|
|
|
|
|
|
|
|
def _in_chatmark(self) -> str:
|
|
|
|
"""
|
|
|
|
Generate ChatMark syntax for options
|
2024-01-01 13:31:34 +08:00
|
|
|
Use the index of button to generate id/key
|
2023-12-31 22:39:45 +08:00
|
|
|
"""
|
|
|
|
lines = []
|
|
|
|
|
|
|
|
if self._title:
|
|
|
|
lines.append(self._title)
|
|
|
|
|
|
|
|
for idx, button in enumerate(self._buttons):
|
2024-01-01 13:31:34 +08:00
|
|
|
key = self.gen_id(self._id_prefix, idx)
|
|
|
|
lines.append(f"> ({key}) {button}")
|
2023-12-31 22:39:45 +08:00
|
|
|
|
|
|
|
text = "\n".join(lines)
|
|
|
|
return text
|
|
|
|
|
|
|
|
def _parse_response(self, response: Dict[str, str]):
|
|
|
|
clicked = None
|
|
|
|
for key, value in response.items():
|
2024-01-01 13:31:34 +08:00
|
|
|
prefix, idx = self.parse_id(key)
|
|
|
|
# check if the prefix is the same as the widget's
|
|
|
|
if prefix != self._id_prefix:
|
|
|
|
continue
|
|
|
|
|
2023-12-31 22:39:45 +08:00
|
|
|
if value == "clicked":
|
2024-01-01 13:31:34 +08:00
|
|
|
clicked = idx
|
2023-12-31 22:39:45 +08:00
|
|
|
break
|
|
|
|
self._clicked = clicked
|