mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
31 Commits
fix-pr-557
...
openhands-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
faa988084c | ||
|
|
a2e9e206e8 | ||
|
|
7ae1f768fc | ||
|
|
e692e06acf | ||
|
|
6f80dc5eee | ||
|
|
aa6070624b | ||
|
|
b6c8aa27fa | ||
|
|
3e9ba40a92 | ||
|
|
ab0eabd88c | ||
|
|
4de0a27ed2 | ||
|
|
c37e865c56 | ||
|
|
6523fcae6b | ||
|
|
d7a3ec69d9 | ||
|
|
bb578a2e9d | ||
|
|
d8b33c4e78 | ||
|
|
0e8e3c87f3 | ||
|
|
5e8c20f406 | ||
|
|
4dd40049ab | ||
|
|
c7a8dcf079 | ||
|
|
2869d646b6 | ||
|
|
0e4e1b3316 | ||
|
|
37363a0a8d | ||
|
|
14dc04474c | ||
|
|
5502a9c448 | ||
|
|
e718089802 | ||
|
|
95f7a6a4dc | ||
|
|
7318c22928 | ||
|
|
78aef2b150 | ||
|
|
194181a420 | ||
|
|
2933f07440 | ||
|
|
037457dec9 |
66
.github/scripts/check_version_consistency.py
vendored
Executable file
66
.github/scripts/check_version_consistency.py
vendored
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from typing import Set, Tuple
|
||||
|
||||
|
||||
def find_version_references(directory: str) -> Tuple[Set[str], Set[str]]:
|
||||
openhands_versions = set()
|
||||
runtime_versions = set()
|
||||
|
||||
version_pattern_openhands = re.compile(r'openhands:(\d{1})\.(\d{2})')
|
||||
version_pattern_runtime = re.compile(r'runtime:(\d{1})\.(\d{2})')
|
||||
|
||||
for root, _, files in os.walk(directory):
|
||||
# Skip .git directory
|
||||
if '.git' in root:
|
||||
continue
|
||||
|
||||
for file in files:
|
||||
if file.endswith(
|
||||
('.md', '.yml', '.yaml', '.txt', '.html', '.py', '.js', '.ts')
|
||||
):
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Find all openhands version references
|
||||
matches = version_pattern_openhands.findall(content)
|
||||
openhands_versions.update(matches)
|
||||
|
||||
# Find all runtime version references
|
||||
matches = version_pattern_runtime.findall(content)
|
||||
runtime_versions.update(matches)
|
||||
except Exception as e:
|
||||
print(f'Error reading {file_path}: {e}', file=sys.stderr)
|
||||
|
||||
return openhands_versions, runtime_versions
|
||||
|
||||
|
||||
def main():
|
||||
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
openhands_versions, runtime_versions = find_version_references(repo_root)
|
||||
|
||||
exit_code = 0
|
||||
|
||||
if len(openhands_versions) > 1:
|
||||
print('Error: Multiple openhands versions found:', file=sys.stderr)
|
||||
print('Found versions:', sorted(openhands_versions), file=sys.stderr)
|
||||
exit_code = 1
|
||||
elif len(openhands_versions) == 0:
|
||||
print('Warning: No openhands version references found', file=sys.stderr)
|
||||
|
||||
if len(runtime_versions) > 1:
|
||||
print('Error: Multiple runtime versions found:', file=sys.stderr)
|
||||
print('Found versions:', sorted(runtime_versions), file=sys.stderr)
|
||||
exit_code = 1
|
||||
elif len(runtime_versions) == 0:
|
||||
print('Warning: No runtime version references found', file=sys.stderr)
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
2
.github/workflows/fe-unit-tests.yml
vendored
2
.github/workflows/fe-unit-tests.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install dependencies
|
||||
working-directory: ./frontend
|
||||
run: npm ci --legacy-peer-deps
|
||||
run: npm ci
|
||||
- name: Run TypeScript compilation
|
||||
working-directory: ./frontend
|
||||
run: npm run make-i18n && tsc
|
||||
|
||||
13
.github/workflows/lint.yml
vendored
13
.github/workflows/lint.yml
vendored
@@ -53,3 +53,16 @@ jobs:
|
||||
run: pip install pre-commit==3.7.0
|
||||
- name: Run pre-commit hooks
|
||||
run: pre-commit run --files openhands/**/* evaluation/**/* tests/**/* --show-diff-on-failure --config ./dev_config/python/.pre-commit-config.yaml
|
||||
|
||||
# Check version consistency across documentation
|
||||
check-version-consistency:
|
||||
name: Check version consistency
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
- name: Run version consistency check
|
||||
run: .github/scripts/check_version_consistency.py
|
||||
|
||||
13
.github/workflows/openhands-resolver.yml
vendored
13
.github/workflows/openhands-resolver.yml
vendored
@@ -185,12 +185,17 @@ jobs:
|
||||
|
||||
- name: Install OpenHands
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
COMMENT_BODY: ${{ github.event.comment.body || '' }}
|
||||
REVIEW_BODY: ${{ github.event.review.body || '' }}
|
||||
LABEL_NAME: ${{ github.event.label.name || '' }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
with:
|
||||
script: |
|
||||
const commentBody = `${{ github.event.comment.body || '' }}`.trim();
|
||||
const reviewBody = `${{ github.event.review.body || '' }}`.trim();
|
||||
const labelName = `${{ github.event.label.name || '' }}`.trim();
|
||||
const eventName = `${{ github.event_name }}`.trim();
|
||||
const commentBody = process.env.COMMENT_BODY.trim();
|
||||
const reviewBody = process.env.REVIEW_BODY.trim();
|
||||
const labelName = process.env.LABEL_NAME.trim();
|
||||
const eventName = process.env.EVENT_NAME.trim();
|
||||
|
||||
// Check conditions
|
||||
const isExperimentalLabel = labelName === "fix-me-experimental";
|
||||
|
||||
@@ -8,7 +8,7 @@ Otherwise, you can clone the OpenHands project directly.
|
||||
* Linux, Mac OS, or [WSL on Windows](https://learn.microsoft.com/en-us/windows/wsl/install) [Ubuntu <= 22.04]
|
||||
* [Docker](https://docs.docker.com/engine/install/) (For those on MacOS, make sure to allow the default Docker socket to be used from advanced settings!)
|
||||
* [Python](https://www.python.org/downloads/) = 3.12
|
||||
* [NodeJS](https://nodejs.org/en/download/package-manager) >= 18.17.1
|
||||
* [NodeJS](https://nodejs.org/en/download/package-manager) >= 20.x
|
||||
* [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer) >= 1.8
|
||||
* OS-specific dependencies:
|
||||
- Ubuntu: build-essential => `sudo apt-get install build-essential`
|
||||
|
||||
4
Makefile
4
Makefile
@@ -81,10 +81,10 @@ check-nodejs:
|
||||
@if command -v node > /dev/null; then \
|
||||
NODE_VERSION=$(shell node --version | sed -E 's/v//g'); \
|
||||
IFS='.' read -r -a NODE_VERSION_ARRAY <<< "$$NODE_VERSION"; \
|
||||
if [ "$${NODE_VERSION_ARRAY[0]}" -gt 18 ] || ([ "$${NODE_VERSION_ARRAY[0]}" -eq 18 ] && [ "$${NODE_VERSION_ARRAY[1]}" -gt 17 ]) || ([ "$${NODE_VERSION_ARRAY[0]}" -eq 18 ] && [ "$${NODE_VERSION_ARRAY[1]}" -eq 17 ] && [ "$${NODE_VERSION_ARRAY[2]}" -ge 1 ]); then \
|
||||
if [ "$${NODE_VERSION_ARRAY[0]}" -ge 20 ]; then \
|
||||
echo "$(BLUE)Node.js $$NODE_VERSION is already installed.$(RESET)"; \
|
||||
else \
|
||||
echo "$(RED)Node.js 18.17.1 or later is required. Please install Node.js 18.17.1 or later to continue.$(RESET)"; \
|
||||
echo "$(RED)Node.js 20.x or later is required. Please install Node.js 20.x or later to continue.$(RESET)"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
else \
|
||||
|
||||
@@ -52,7 +52,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.16-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -61,7 +61,7 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.16 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.17 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.16-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -56,6 +56,6 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.16 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.17 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
||||
```
|
||||
|
||||
@@ -13,16 +13,16 @@
|
||||
La façon la plus simple d'exécuter OpenHands est avec Docker.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.16-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.16-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.16
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.17
|
||||
```
|
||||
|
||||
Vous pouvez également exécuter OpenHands en mode [headless scriptable](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), en tant que [CLI interactive](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), ou en utilisant l'[Action GitHub OpenHands](https://docs.all-hands.dev/modules/usage/how-to/github-action).
|
||||
|
||||
@@ -13,7 +13,7 @@ C'est le Runtime par défaut qui est utilisé lorsque vous démarrez OpenHands.
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.16-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -50,7 +50,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.16-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -59,7 +59,7 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.16 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.17 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.16-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -57,6 +57,6 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.16 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.17 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
||||
```
|
||||
|
||||
@@ -11,16 +11,16 @@
|
||||
在 Docker 中运行 OpenHands 是最简单的方式。
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.16-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.16-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.16
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.17
|
||||
```
|
||||
|
||||
你也可以在可脚本化的[无头模式](https://docs.all-hands.dev/modules/usage/how-to/headless-mode)下运行 OpenHands,作为[交互式 CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),或使用 [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action)。
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.16-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -12,7 +12,7 @@ To run OpenHands in headless mode with Python,
|
||||
and then run:
|
||||
|
||||
```bash
|
||||
poetry run python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
poetry run python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
||||
```
|
||||
|
||||
You'll need to be sure to set your model, API key, and other settings via environment variables
|
||||
|
||||
@@ -63,7 +63,7 @@ def get_config(
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='eventstream',
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
|
||||
@@ -43,7 +43,7 @@ def get_config(
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime=os.environ.get('RUNTIME', 'eventstream'),
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-slim',
|
||||
|
||||
@@ -50,7 +50,7 @@ def get_config(
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime=os.environ.get('RUNTIME', 'eventstream'),
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.11-bookworm',
|
||||
|
||||
@@ -61,7 +61,7 @@ def get_config(
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='eventstream',
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image=BIOCODER_BENCH_CONTAINER_IMAGE,
|
||||
|
||||
@@ -74,7 +74,7 @@ def get_config(
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='eventstream',
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
|
||||
@@ -39,7 +39,7 @@ def get_config(
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='eventstream',
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
|
||||
@@ -124,7 +124,7 @@ def get_config(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
max_iterations=metadata.max_iterations,
|
||||
runtime=os.environ.get('RUNTIME', 'eventstream'),
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image=base_container_image,
|
||||
enable_auto_lint=True,
|
||||
|
||||
@@ -65,7 +65,7 @@ def get_config(
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='eventstream',
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
|
||||
@@ -50,7 +50,7 @@ def get_config(
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='eventstream',
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
|
||||
@@ -43,7 +43,7 @@ def get_config(
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='eventstream',
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
|
||||
@@ -64,7 +64,7 @@ def get_config(
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='eventstream',
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
|
||||
@@ -85,7 +85,7 @@ def get_config(
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='eventstream',
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
|
||||
@@ -48,7 +48,7 @@ def get_config(
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='eventstream',
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='xingyaoww/od-eval-logic-reasoning:v1.0',
|
||||
|
||||
@@ -58,7 +58,7 @@ def get_config(
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime=os.environ.get('RUNTIME', 'eventstream'),
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='xingyaoww/od-eval-miniwob:v1.0',
|
||||
|
||||
@@ -106,7 +106,7 @@ def get_config(
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='eventstream',
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='xingyaoww/od-eval-mint:v1.0',
|
||||
|
||||
@@ -80,7 +80,7 @@ def get_config(
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='eventstream',
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='public.ecr.aws/i5g0m1f6/ml-bench',
|
||||
|
||||
@@ -62,7 +62,7 @@ def get_config(
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime=os.environ.get('RUNTIME', 'eventstream'),
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
max_budget_per_task=4,
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
|
||||
@@ -76,7 +76,7 @@ def get_config(instance: pd.Series) -> AppConfig:
|
||||
)
|
||||
config = AppConfig(
|
||||
run_as_openhands=False,
|
||||
runtime=os.environ.get('RUNTIME', 'eventstream'),
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image=base_container_image,
|
||||
use_host_network=False,
|
||||
|
||||
@@ -121,7 +121,7 @@ def get_config(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
max_iterations=metadata.max_iterations,
|
||||
runtime=os.environ.get('RUNTIME', 'eventstream'),
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image=base_container_image,
|
||||
enable_auto_lint=True,
|
||||
|
||||
@@ -44,7 +44,7 @@ def get_config(
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='eventstream',
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
|
||||
@@ -53,7 +53,7 @@ def get_config(
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='eventstream',
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
|
||||
@@ -42,7 +42,7 @@ def get_config(
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime=os.environ.get('RUNTIME', 'eventstream'),
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
# use default base_container_image
|
||||
|
||||
47
frontend/__tests__/api/github.test.ts
Normal file
47
frontend/__tests__/api/github.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { retrieveLatestGitHubCommit } from "../../src/api/github";
|
||||
|
||||
describe("retrieveLatestGitHubCommit", () => {
|
||||
const { githubGetMock } = vi.hoisted(() => ({
|
||||
githubGetMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../src/api/github-axios-instance", () => ({
|
||||
github: {
|
||||
get: githubGetMock,
|
||||
},
|
||||
}));
|
||||
|
||||
it("should return the latest commit when repository has commits", async () => {
|
||||
const mockCommit = {
|
||||
sha: "123abc",
|
||||
commit: {
|
||||
message: "Initial commit",
|
||||
},
|
||||
};
|
||||
|
||||
githubGetMock.mockResolvedValueOnce({
|
||||
data: [mockCommit],
|
||||
});
|
||||
|
||||
const result = await retrieveLatestGitHubCommit("user/repo");
|
||||
expect(result).toEqual(mockCommit);
|
||||
});
|
||||
|
||||
it("should return null when repository is empty", async () => {
|
||||
const error = new Error("Repository is empty");
|
||||
(error as any).response = { status: 409 };
|
||||
githubGetMock.mockRejectedValueOnce(error);
|
||||
|
||||
const result = await retrieveLatestGitHubCommit("user/empty-repo");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should throw error for other error cases", async () => {
|
||||
const error = new Error("Network error");
|
||||
(error as any).response = { status: 500 };
|
||||
githubGetMock.mockRejectedValueOnce(error);
|
||||
|
||||
await expect(retrieveLatestGitHubCommit("user/repo")).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -1,46 +1,24 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, it, expect, test, beforeEach } from "vitest";
|
||||
import { describe, it, expect, test } from "vitest";
|
||||
import { ChatMessage } from "#/components/features/chat/chat-message";
|
||||
import { Provider } from "react-redux";
|
||||
import configureStore from "redux-mock-store";
|
||||
|
||||
const mockStore = configureStore([]);
|
||||
|
||||
describe("ChatMessage", () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore({
|
||||
speech: {
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const renderWithRedux = (component) => {
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
{component}
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
it("should render a user message", () => {
|
||||
renderWithRedux(<ChatMessage type="user" message="Hello, World!" />);
|
||||
render(<ChatMessage type="user" message="Hello, World!" />);
|
||||
expect(screen.getByTestId("user-message")).toBeInTheDocument();
|
||||
expect(screen.getByText("Hello, World!")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render an assistant message", () => {
|
||||
renderWithRedux(<ChatMessage type="assistant" message="Hello, World!" />);
|
||||
render(<ChatMessage type="assistant" message="Hello, World!" />);
|
||||
expect(screen.getByTestId("assistant-message")).toBeInTheDocument();
|
||||
expect(screen.getByText("Hello, World!")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.skip("should support code syntax highlighting", () => {
|
||||
const code = "```js\nconsole.log('Hello, World!')\n```";
|
||||
renderWithRedux(<ChatMessage type="user" message={code} />);
|
||||
render(<ChatMessage type="user" message={code} />);
|
||||
|
||||
// SyntaxHighlighter breaks the code blocks into "tokens"
|
||||
expect(screen.getByText("console")).toBeInTheDocument();
|
||||
@@ -50,7 +28,7 @@ describe("ChatMessage", () => {
|
||||
|
||||
it("should render the copy to clipboard button when the user hovers over the message", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithRedux(<ChatMessage type="user" message="Hello, World!" />);
|
||||
render(<ChatMessage type="user" message="Hello, World!" />);
|
||||
const message = screen.getByText("Hello, World!");
|
||||
|
||||
expect(screen.getByTestId("copy-to-clipboard")).not.toBeVisible();
|
||||
@@ -62,7 +40,7 @@ describe("ChatMessage", () => {
|
||||
|
||||
it("should copy content to clipboard", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithRedux(<ChatMessage type="user" message="Hello, World!" />);
|
||||
render(<ChatMessage type="user" message="Hello, World!" />);
|
||||
const copyToClipboardButton = screen.getByTestId("copy-to-clipboard");
|
||||
|
||||
await user.click(copyToClipboardButton);
|
||||
@@ -76,7 +54,7 @@ describe("ChatMessage", () => {
|
||||
function Component() {
|
||||
return <div data-testid="custom-component">Custom Component</div>;
|
||||
}
|
||||
renderWithRedux(
|
||||
render(
|
||||
<ChatMessage type="user" message="Hello, World">
|
||||
<Component />
|
||||
</ChatMessage>,
|
||||
@@ -85,7 +63,7 @@ describe("ChatMessage", () => {
|
||||
});
|
||||
|
||||
it("should apply correct styles to inline code", () => {
|
||||
renderWithRedux(
|
||||
render(
|
||||
<ChatMessage
|
||||
type="assistant"
|
||||
message="Here is some `inline code` text"
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, act, waitFor } from "@testing-library/react";
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { ChatMessage } from "#/components/features/chat/chat-message";
|
||||
import { Provider } from "react-redux";
|
||||
import configureStore from "redux-mock-store";
|
||||
|
||||
const mockStore = configureStore([]);
|
||||
|
||||
// Mock the Web Speech API
|
||||
const mockSpeechSynthesis = {
|
||||
cancel: vi.fn(),
|
||||
speak: vi.fn(),
|
||||
getVoices: vi.fn().mockReturnValue([
|
||||
{
|
||||
name: "Google US English",
|
||||
lang: "en-US",
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
const mockUtterance = {
|
||||
voice: null,
|
||||
rate: 1,
|
||||
pitch: 1,
|
||||
volume: 1,
|
||||
};
|
||||
|
||||
// @ts-ignore - partial implementation
|
||||
global.SpeechSynthesisUtterance = vi.fn().mockImplementation(() => mockUtterance);
|
||||
// @ts-ignore - partial implementation
|
||||
global.speechSynthesis = mockSpeechSynthesis;
|
||||
|
||||
describe("ChatMessage with speech", () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const renderWithRedux = (component, speechEnabled = false) => {
|
||||
store = mockStore({
|
||||
speech: {
|
||||
enabled: speechEnabled,
|
||||
},
|
||||
});
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
{component}
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
it("speaks assistant messages when speech is enabled", async () => {
|
||||
await act(async () => {
|
||||
renderWithRedux(<ChatMessage type="assistant" message="Hello, world!" />, true);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSpeechSynthesis.cancel).toHaveBeenCalled();
|
||||
expect(mockSpeechSynthesis.speak).toHaveBeenCalled();
|
||||
expect(global.SpeechSynthesisUtterance).toHaveBeenCalledWith("Hello, world!");
|
||||
});
|
||||
});
|
||||
|
||||
it("does not speak user messages", async () => {
|
||||
await act(async () => {
|
||||
renderWithRedux(<ChatMessage type="user" message="Hello, world!" />, true);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSpeechSynthesis.speak).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("does not speak when speech is disabled", async () => {
|
||||
await act(async () => {
|
||||
renderWithRedux(<ChatMessage type="assistant" message="Hello, world!" />, false);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSpeechSynthesis.speak).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("removes markdown formatting before speaking", async () => {
|
||||
await act(async () => {
|
||||
renderWithRedux(<ChatMessage type="assistant" message="**Hello** *world* `code`" />, true);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(global.SpeechSynthesisUtterance).toHaveBeenCalledWith("**Hello** *world* `code`");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -28,8 +28,8 @@ describe("AccountSettingsContextMenu", () => {
|
||||
expect(
|
||||
screen.getByTestId("account-settings-context-menu"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Account Settings")).toBeInTheDocument();
|
||||
expect(screen.getByText("Logout")).toBeInTheDocument();
|
||||
expect(screen.getByText("ACCOUNT_SETTINGS$SETTINGS")).toBeInTheDocument();
|
||||
expect(screen.getByText("ACCOUNT_SETTINGS$LOGOUT")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onClickAccountSettings when the account settings option is clicked", async () => {
|
||||
@@ -42,7 +42,7 @@ describe("AccountSettingsContextMenu", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const accountSettingsOption = screen.getByText("Account Settings");
|
||||
const accountSettingsOption = screen.getByText("ACCOUNT_SETTINGS$SETTINGS");
|
||||
await user.click(accountSettingsOption);
|
||||
|
||||
expect(onClickAccountSettingsMock).toHaveBeenCalledOnce();
|
||||
@@ -58,7 +58,7 @@ describe("AccountSettingsContextMenu", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const logoutOption = screen.getByText("Logout");
|
||||
const logoutOption = screen.getByText("ACCOUNT_SETTINGS$LOGOUT");
|
||||
await user.click(logoutOption);
|
||||
|
||||
expect(onLogoutMock).toHaveBeenCalledOnce();
|
||||
@@ -74,7 +74,7 @@ describe("AccountSettingsContextMenu", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const logoutOption = screen.getByText("Logout");
|
||||
const logoutOption = screen.getByText("ACCOUNT_SETTINGS$LOGOUT");
|
||||
await user.click(logoutOption);
|
||||
|
||||
expect(onLogoutMock).not.toHaveBeenCalled();
|
||||
@@ -90,7 +90,7 @@ describe("AccountSettingsContextMenu", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const accountSettingsButton = screen.getByText("Account Settings");
|
||||
const accountSettingsButton = screen.getByText("ACCOUNT_SETTINGS$SETTINGS");
|
||||
await user.click(accountSettingsButton);
|
||||
await user.click(document.body);
|
||||
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, test, vi } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { formatTimeDelta } from "#/utils/format-time-delta";
|
||||
import { ConversationCard } from "#/components/features/conversation-panel/conversation-card";
|
||||
|
||||
describe("ConversationCard", () => {
|
||||
const onClick = vi.fn();
|
||||
const onDelete = vi.fn();
|
||||
const onChangeTitle = vi.fn();
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render the conversation card", () => {
|
||||
render(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
onClick={onClick}
|
||||
onChangeTitle={onChangeTitle}
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
const expectedDate = `${formatTimeDelta(new Date("2021-10-01T12:00:00Z"))} ago`;
|
||||
|
||||
const card = screen.getByTestId("conversation-card");
|
||||
const title = within(card).getByTestId("conversation-card-title");
|
||||
|
||||
expect(title).toHaveValue("Conversation 1");
|
||||
within(card).getByText(expectedDate);
|
||||
});
|
||||
|
||||
it("should render the repo if available", () => {
|
||||
const { rerender } = render(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
onClick={onClick}
|
||||
onChangeTitle={onChangeTitle}
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId("conversation-card-repo"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
onClick={onClick}
|
||||
onChangeTitle={onChangeTitle}
|
||||
name="Conversation 1"
|
||||
repo="org/repo"
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
screen.getByTestId("conversation-card-repo");
|
||||
});
|
||||
|
||||
it("should call onClick when the card is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
onClick={onClick}
|
||||
onChangeTitle={onChangeTitle}
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
const card = screen.getByTestId("conversation-card");
|
||||
await user.click(card);
|
||||
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should toggle a context menu when clicking the ellipsis button", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
onClick={onClick}
|
||||
onChangeTitle={onChangeTitle}
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("context-menu")).not.toBeInTheDocument();
|
||||
|
||||
const ellipsisButton = screen.getByTestId("ellipsis-button");
|
||||
await user.click(ellipsisButton);
|
||||
|
||||
screen.getByTestId("context-menu");
|
||||
|
||||
await user.click(ellipsisButton);
|
||||
|
||||
expect(screen.queryByTestId("context-menu")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onDelete when the delete button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ConversationCard
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
const ellipsisButton = screen.getByTestId("ellipsis-button");
|
||||
await user.click(ellipsisButton);
|
||||
|
||||
const menu = screen.getByTestId("context-menu");
|
||||
const deleteButton = within(menu).getByTestId("delete-button");
|
||||
|
||||
await user.click(deleteButton);
|
||||
|
||||
expect(onDelete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("clicking the repo should not trigger the onClick handler", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ConversationCard
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
name="Conversation 1"
|
||||
repo="org/repo"
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
const repo = screen.getByTestId("conversation-card-repo");
|
||||
await user.click(repo);
|
||||
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("conversation title should call onChangeTitle when changed and blurred", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ConversationCard
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
onChangeTitle={onChangeTitle}
|
||||
/>,
|
||||
);
|
||||
|
||||
const title = screen.getByTestId("conversation-card-title");
|
||||
|
||||
await user.clear(title);
|
||||
await user.type(title, "New Conversation Name ");
|
||||
await user.tab();
|
||||
|
||||
expect(onChangeTitle).toHaveBeenCalledWith("New Conversation Name");
|
||||
expect(title).toHaveValue("New Conversation Name");
|
||||
});
|
||||
|
||||
it("should reset title and not call onChangeTitle when the title is empty", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ConversationCard
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
const title = screen.getByTestId("conversation-card-title");
|
||||
|
||||
await user.clear(title);
|
||||
await user.tab();
|
||||
|
||||
expect(onChangeTitle).not.toHaveBeenCalled();
|
||||
expect(title).toHaveValue("Conversation 1");
|
||||
});
|
||||
|
||||
test("clicking the title should not trigger the onClick handler", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ConversationCard
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
const title = screen.getByTestId("conversation-card-title");
|
||||
await user.click(title);
|
||||
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("clicking the delete button should not trigger the onClick handler", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ConversationCard
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
const ellipsisButton = screen.getByTestId("ellipsis-button");
|
||||
await user.click(ellipsisButton);
|
||||
|
||||
const menu = screen.getByTestId("context-menu");
|
||||
const deleteButton = within(menu).getByTestId("delete-button");
|
||||
|
||||
await user.click(deleteButton);
|
||||
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("state indicator", () => {
|
||||
it("should render the 'cold' indicator by default", () => {
|
||||
render(
|
||||
<ConversationCard
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
screen.getByTestId("cold-indicator");
|
||||
});
|
||||
|
||||
it("should render the other indicators when provided", () => {
|
||||
render(
|
||||
<ConversationCard
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
state="warm"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("cold-indicator")).not.toBeInTheDocument();
|
||||
screen.getByTestId("warm-indicator");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,267 @@
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
QueryClientProvider,
|
||||
QueryClient,
|
||||
QueryClientConfig,
|
||||
} from "@tanstack/react-query";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { ConversationPanel } from "#/components/features/conversation-panel/conversation-panel";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
|
||||
describe("ConversationPanel", () => {
|
||||
const onCloseMock = vi.fn();
|
||||
|
||||
const renderConversationPanel = (config?: QueryClientConfig) =>
|
||||
render(<ConversationPanel onClose={onCloseMock} />, {
|
||||
wrapper: ({ children }) => (
|
||||
<AuthProvider>
|
||||
<QueryClientProvider client={new QueryClient(config)}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
),
|
||||
});
|
||||
|
||||
const { endSessionMock } = vi.hoisted(() => ({
|
||||
endSessionMock: vi.fn(),
|
||||
}));
|
||||
|
||||
beforeAll(() => {
|
||||
vi.mock("react-router", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("react-router")>()),
|
||||
Link: ({ children }: React.PropsWithChildren) => children,
|
||||
useNavigate: vi.fn(() => vi.fn()),
|
||||
useLocation: vi.fn(() => ({ pathname: "/conversation" })),
|
||||
useParams: vi.fn(() => ({ conversationId: "2" })),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/use-end-session", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("#/hooks/use-end-session")>()),
|
||||
useEndSession: vi.fn(() => endSessionMock),
|
||||
}));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should render the conversations", async () => {
|
||||
renderConversationPanel();
|
||||
const cards = await screen.findAllByTestId("conversation-card");
|
||||
|
||||
expect(cards).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("should display an empty state when there are no conversations", async () => {
|
||||
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
|
||||
getUserConversationsSpy.mockResolvedValue([]);
|
||||
|
||||
renderConversationPanel();
|
||||
|
||||
const emptyState = await screen.findByText("No conversations found");
|
||||
expect(emptyState).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle an error when fetching conversations", async () => {
|
||||
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
|
||||
getUserConversationsSpy.mockRejectedValue(
|
||||
new Error("Failed to fetch conversations"),
|
||||
);
|
||||
|
||||
renderConversationPanel({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const error = await screen.findByText("Failed to fetch conversations");
|
||||
expect(error).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should cancel deleting a conversation", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderConversationPanel();
|
||||
|
||||
let cards = await screen.findAllByTestId("conversation-card");
|
||||
expect(
|
||||
within(cards[0]).queryByTestId("delete-button"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
|
||||
await user.click(ellipsisButton);
|
||||
const deleteButton = screen.getByTestId("delete-button");
|
||||
|
||||
// Click the first delete button
|
||||
await user.click(deleteButton);
|
||||
|
||||
// Cancel the deletion
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
await user.click(cancelButton);
|
||||
|
||||
expect(screen.queryByText("Cancel")).not.toBeInTheDocument();
|
||||
|
||||
// Ensure the conversation is not deleted
|
||||
cards = await screen.findAllByTestId("conversation-card");
|
||||
expect(cards).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("should call endSession after deleting a conversation that is the current session", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderConversationPanel();
|
||||
|
||||
let cards = await screen.findAllByTestId("conversation-card");
|
||||
const ellipsisButton = within(cards[1]).getByTestId("ellipsis-button");
|
||||
await user.click(ellipsisButton);
|
||||
const deleteButton = screen.getByTestId("delete-button");
|
||||
|
||||
// Click the second delete button
|
||||
await user.click(deleteButton);
|
||||
|
||||
// Confirm the deletion
|
||||
const confirmButton = screen.getByText("Confirm");
|
||||
await user.click(confirmButton);
|
||||
|
||||
expect(screen.queryByText("Confirm")).not.toBeInTheDocument();
|
||||
|
||||
// Ensure the conversation is deleted
|
||||
cards = await screen.findAllByTestId("conversation-card");
|
||||
expect(cards).toHaveLength(2);
|
||||
|
||||
expect(endSessionMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should delete a conversation", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderConversationPanel();
|
||||
|
||||
let cards = await screen.findAllByTestId("conversation-card");
|
||||
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
|
||||
await user.click(ellipsisButton);
|
||||
const deleteButton = screen.getByTestId("delete-button");
|
||||
|
||||
// Click the first delete button
|
||||
await user.click(deleteButton);
|
||||
|
||||
// Confirm the deletion
|
||||
const confirmButton = screen.getByText("Confirm");
|
||||
await user.click(confirmButton);
|
||||
|
||||
expect(screen.queryByText("Confirm")).not.toBeInTheDocument();
|
||||
|
||||
// Ensure the conversation is deleted
|
||||
cards = await screen.findAllByTestId("conversation-card");
|
||||
expect(cards).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should rename a conversation", async () => {
|
||||
const updateUserConversationSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
"updateUserConversation",
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
renderConversationPanel();
|
||||
const cards = await screen.findAllByTestId("conversation-card");
|
||||
const title = within(cards[0]).getByTestId("conversation-card-title");
|
||||
|
||||
await user.clear(title);
|
||||
await user.type(title, "Conversation 1 Renamed");
|
||||
await user.tab();
|
||||
|
||||
// Ensure the conversation is renamed
|
||||
expect(updateUserConversationSpy).toHaveBeenCalledWith("3", {
|
||||
name: "Conversation 1 Renamed",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not rename a conversation when the name is unchanged", async () => {
|
||||
const updateUserConversationSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
"updateUserConversation",
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
renderConversationPanel();
|
||||
const cards = await screen.findAllByTestId("conversation-card");
|
||||
const title = within(cards[0]).getByTestId("conversation-card-title");
|
||||
|
||||
await user.click(title);
|
||||
await user.tab();
|
||||
|
||||
// Ensure the conversation is not renamed
|
||||
expect(updateUserConversationSpy).not.toHaveBeenCalled();
|
||||
|
||||
await user.type(title, "Conversation 1");
|
||||
await user.click(title);
|
||||
await user.tab();
|
||||
|
||||
expect(updateUserConversationSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
await user.click(title);
|
||||
await user.tab();
|
||||
|
||||
expect(updateUserConversationSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call onClose after clicking a card", async () => {
|
||||
renderConversationPanel();
|
||||
const cards = await screen.findAllByTestId("conversation-card");
|
||||
const firstCard = cards[0];
|
||||
|
||||
await userEvent.click(firstCard);
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
describe("New Conversation Button", () => {
|
||||
it("should display a confirmation modal when clicking", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderConversationPanel();
|
||||
|
||||
expect(
|
||||
screen.queryByTestId("confirm-new-conversation-modal"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const newProjectButton = screen.getByTestId("new-conversation-button");
|
||||
await user.click(newProjectButton);
|
||||
|
||||
const modal = screen.getByTestId("confirm-new-conversation-modal");
|
||||
expect(modal).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call endSession and close panel after confirming", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderConversationPanel();
|
||||
|
||||
const newProjectButton = screen.getByTestId("new-conversation-button");
|
||||
await user.click(newProjectButton);
|
||||
|
||||
const confirmButton = screen.getByText("Confirm");
|
||||
await user.click(confirmButton);
|
||||
|
||||
expect(endSessionMock).toHaveBeenCalledOnce();
|
||||
expect(onCloseMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should close the modal when cancelling", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderConversationPanel();
|
||||
|
||||
const newProjectButton = screen.getByTestId("new-conversation-button");
|
||||
await user.click(newProjectButton);
|
||||
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
await user.click(cancelButton);
|
||||
|
||||
expect(endSessionMock).not.toHaveBeenCalled();
|
||||
expect(
|
||||
screen.queryByTestId("confirm-new-conversation-modal"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { Sidebar } from "#/components/features/sidebar/sidebar";
|
||||
import { MULTI_CONVO_UI_IS_ENABLED } from "#/utils/constants";
|
||||
|
||||
const renderSidebar = () => {
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
path: "/conversation/:conversationId",
|
||||
Component: Sidebar,
|
||||
},
|
||||
]);
|
||||
|
||||
renderWithProviders(<RouterStub initialEntries={["/conversation/123"]} />);
|
||||
};
|
||||
|
||||
describe("Sidebar", () => {
|
||||
it.skipIf(!MULTI_CONVO_UI_IS_ENABLED)(
|
||||
"should have the conversation panel open by default",
|
||||
() => {
|
||||
renderSidebar();
|
||||
expect(screen.getByTestId("conversation-panel")).toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!MULTI_CONVO_UI_IS_ENABLED)(
|
||||
"should toggle the conversation panel",
|
||||
async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSidebar();
|
||||
|
||||
const projectPanelButton = screen.getByTestId(
|
||||
"toggle-conversation-panel",
|
||||
);
|
||||
|
||||
await user.click(projectPanelButton);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId("conversation-panel"),
|
||||
).not.toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -52,14 +52,10 @@ describe("BaseModal", () => {
|
||||
expect(screen.getByText("Save")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.click(screen.getByText("Save"));
|
||||
});
|
||||
await userEvent.click(screen.getByText("Save"));
|
||||
expect(onPrimaryClickMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.click(screen.getByText("Cancel"));
|
||||
});
|
||||
await userEvent.click(screen.getByText("Cancel"));
|
||||
expect(onSecondaryClickMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -80,9 +76,7 @@ describe("BaseModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.click(screen.getByText("Save"));
|
||||
});
|
||||
await userEvent.click(screen.getByText("Save"));
|
||||
expect(onOpenChangeMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { Provider } from "react-redux";
|
||||
import configureStore from "redux-mock-store";
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { ToggleSpeechButton } from "#/components/shared/buttons/toggle-speech-button";
|
||||
import { toggleSpeech } from "#/state/speech-slice";
|
||||
|
||||
const mockStore = configureStore([]);
|
||||
|
||||
describe("ToggleSpeechButton", () => {
|
||||
let store: any;
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore({
|
||||
speech: {
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
store.dispatch = vi.fn();
|
||||
});
|
||||
|
||||
it("renders correctly when disabled", () => {
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<ToggleSpeechButton />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toHaveAttribute("title", "Enable speech");
|
||||
});
|
||||
|
||||
it("renders correctly when enabled", () => {
|
||||
store = mockStore({
|
||||
speech: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<ToggleSpeechButton />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toHaveAttribute("title", "Disable speech");
|
||||
});
|
||||
|
||||
it("dispatches toggle action when clicked", () => {
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<ToggleSpeechButton />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(toggleSpeech());
|
||||
});
|
||||
});
|
||||
@@ -58,7 +58,7 @@ describe("UserActions", () => {
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
|
||||
const accountSettingsOption = screen.getByText("Account Settings");
|
||||
const accountSettingsOption = screen.getByText("ACCOUNT_SETTINGS$SETTINGS");
|
||||
await user.click(accountSettingsOption);
|
||||
|
||||
expect(onClickAccountSettingsMock).toHaveBeenCalledOnce();
|
||||
@@ -79,7 +79,7 @@ describe("UserActions", () => {
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
|
||||
const logoutOption = screen.getByText("Logout");
|
||||
const logoutOption = screen.getByText("ACCOUNT_SETTINGS$LOGOUT");
|
||||
await user.click(logoutOption);
|
||||
|
||||
expect(onLogoutMock).toHaveBeenCalledOnce();
|
||||
@@ -99,7 +99,7 @@ describe("UserActions", () => {
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
|
||||
const logoutOption = screen.getByText("Logout");
|
||||
const logoutOption = screen.getByText("ACCOUNT_SETTINGS$LOGOUT");
|
||||
await user.click(logoutOption);
|
||||
|
||||
expect(onLogoutMock).not.toHaveBeenCalled();
|
||||
|
||||
83
frontend/__tests__/routes/_oh.app.test.tsx
Normal file
83
frontend/__tests__/routes/_oh.app.test.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import toast from "react-hot-toast";
|
||||
import App from "#/routes/_oh.app/route";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { MULTI_CONVO_UI_IS_ENABLED } from "#/utils/constants";
|
||||
|
||||
describe("App", () => {
|
||||
const RouteStub = createRoutesStub([
|
||||
{ Component: App, path: "/conversation/:conversationId" },
|
||||
]);
|
||||
|
||||
const { endSessionMock } = vi.hoisted(() => ({
|
||||
endSessionMock: vi.fn(),
|
||||
}));
|
||||
|
||||
beforeAll(() => {
|
||||
vi.mock("#/hooks/use-end-session", () => ({
|
||||
useEndSession: vi.fn(() => endSessionMock),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/use-terminal", () => ({
|
||||
useTerminal: vi.fn(),
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render", async () => {
|
||||
renderWithProviders(<RouteStub initialEntries={["/conversation/123"]} />);
|
||||
await screen.findByTestId("app-route");
|
||||
});
|
||||
|
||||
it.skipIf(!MULTI_CONVO_UI_IS_ENABLED)(
|
||||
"should call endSession if the user does not have permission to view conversation",
|
||||
async () => {
|
||||
const errorToastSpy = vi.spyOn(toast, "error");
|
||||
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
|
||||
|
||||
getConversationSpy.mockResolvedValue(null);
|
||||
renderWithProviders(
|
||||
<RouteStub initialEntries={["/conversation/9999"]} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(endSessionMock).toHaveBeenCalledOnce();
|
||||
expect(errorToastSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("should not call endSession if the user has permission", async () => {
|
||||
const errorToastSpy = vi.spyOn(toast, "error");
|
||||
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
|
||||
|
||||
getConversationSpy.mockResolvedValue({
|
||||
conversation_id: "9999",
|
||||
lastUpdated: "",
|
||||
name: "",
|
||||
repo: "",
|
||||
state: "cold",
|
||||
});
|
||||
const { rerender } = renderWithProviders(
|
||||
<RouteStub initialEntries={["/conversation/9999"]} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(endSessionMock).not.toHaveBeenCalled();
|
||||
expect(errorToastSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
rerender(<RouteStub initialEntries={["/conversation"]} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(endSessionMock).not.toHaveBeenCalled();
|
||||
expect(errorToastSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { speechSlice, toggleSpeech } from "#/state/speech-slice";
|
||||
|
||||
// Mock window.speechSynthesis
|
||||
const mockSpeechSynthesis = {
|
||||
cancel: () => {},
|
||||
};
|
||||
|
||||
Object.defineProperty(window, 'speechSynthesis', {
|
||||
value: mockSpeechSynthesis,
|
||||
writable: true
|
||||
});
|
||||
|
||||
describe("speechSlice", () => {
|
||||
const initialState = {
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
it("should handle initial state", () => {
|
||||
expect(speechSlice.reducer(undefined, { type: "unknown" })).toEqual({
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle toggleSpeech", () => {
|
||||
const actual = speechSlice.reducer(initialState, toggleSpeech());
|
||||
expect(actual.enabled).toEqual(true);
|
||||
|
||||
const actual2 = speechSlice.reducer(actual, toggleSpeech());
|
||||
expect(actual2.enabled).toEqual(false);
|
||||
});
|
||||
});
|
||||
380
frontend/package-lock.json
generated
380
frontend/package-lock.json
generated
@@ -14,7 +14,7 @@
|
||||
"@react-router/serve": "^7.1.1",
|
||||
"@react-types/shared": "^3.25.0",
|
||||
"@reduxjs/toolkit": "^2.5.0",
|
||||
"@tanstack/react-query": "^5.62.10",
|
||||
"@tanstack/react-query": "^5.62.11",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
@@ -27,12 +27,12 @@
|
||||
"isbot": "^5.1.19",
|
||||
"jose": "^5.9.4",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.203.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"posthog-js": "^1.203.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-i18next": "^15.2.0",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-redux": "^9.2.0",
|
||||
@@ -43,17 +43,17 @@
|
||||
"sirv-cli": "^3.0.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"vite": "^5.4.9",
|
||||
"vite": "^5.4.11",
|
||||
"web-vitals": "^3.5.2",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mswjs/socket.io-binding": "^0.1.1",
|
||||
"@playwright/test": "^1.49.1",
|
||||
"@react-router/dev": "^7.1.1",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tanstack/eslint-plugin-query": "^5.62.9",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/jest-dom": "^6.6.1",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/node": "^22.10.2",
|
||||
@@ -61,8 +61,6 @@
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/redux-mock-store": "^1.5.0",
|
||||
"@types/testing-library__react": "^10.0.1",
|
||||
"@types/ws": "^8.5.12",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
@@ -78,16 +76,14 @@
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-react": "^7.37.3",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"framer-motion": "^11.15.0",
|
||||
"husky": "^9.1.6",
|
||||
"jsdom": "^25.0.1",
|
||||
"lint-staged": "^15.2.11",
|
||||
"lint-staged": "^15.3.0",
|
||||
"msw": "^2.6.6",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.4.2",
|
||||
"redux-mock-store": "^1.5.5",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.5.3",
|
||||
"typescript": "^5.6.3",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^1.6.0"
|
||||
@@ -1533,36 +1529,6 @@
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/types": {
|
||||
"version": "25.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz",
|
||||
"integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/istanbul-lib-coverage": "^2.0.0",
|
||||
"@types/istanbul-reports": "^1.1.1",
|
||||
"@types/yargs": "^15.0.0",
|
||||
"chalk": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/types/node_modules/chalk": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
|
||||
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||
@@ -1661,6 +1627,21 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@mswjs/socket.io-binding": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@mswjs/socket.io-binding/-/socket.io-binding-0.1.1.tgz",
|
||||
"integrity": "sha512-mtFDHC5XMeti43toe3HBynD4uBxvUA2GfJVC6TDfhOQlH+G2hf5znNTSa75A30XdWL0P6aNqUKpcNo6L0Wop+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mswjs/interceptors": "^0.37.1",
|
||||
"engine.io-parser": "^5.2.3",
|
||||
"socket.io-parser": "^4.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mswjs/interceptors": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@nextui-org/accordion": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@nextui-org/accordion/-/accordion-2.2.6.tgz",
|
||||
@@ -2276,6 +2257,23 @@
|
||||
"react-dom": ">=18 || >=19.0.0-rc.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nextui-org/listbox/node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.10.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.9.tgz",
|
||||
"integrity": "sha512-OXO2uBjFqA4Ibr2O3y0YMnkrRWGVNqcvHQXmGvMu6IK8chZl3PrDxFXdGZ2iZkSrKh3/qUYoFqYe+Rx23RoU0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.10.9"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nextui-org/menu": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@nextui-org/menu/-/menu-2.2.8.tgz",
|
||||
@@ -2614,6 +2612,23 @@
|
||||
"react-dom": ">=18 || >=19.0.0-rc.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nextui-org/select/node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.10.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.9.tgz",
|
||||
"integrity": "sha512-OXO2uBjFqA4Ibr2O3y0YMnkrRWGVNqcvHQXmGvMu6IK8chZl3PrDxFXdGZ2iZkSrKh3/qUYoFqYe+Rx23RoU0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.10.9"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nextui-org/shared-icons": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@nextui-org/shared-icons/-/shared-icons-2.1.1.tgz",
|
||||
@@ -5371,23 +5386,6 @@
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.10.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.9.tgz",
|
||||
"integrity": "sha512-OXO2uBjFqA4Ibr2O3y0YMnkrRWGVNqcvHQXmGvMu6IK8chZl3PrDxFXdGZ2iZkSrKh3/qUYoFqYe+Rx23RoU0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.10.9"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.10.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.9.tgz",
|
||||
@@ -5404,6 +5402,7 @@
|
||||
"integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@@ -5507,7 +5506,8 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
@@ -5589,34 +5589,6 @@
|
||||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
||||
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-report": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
|
||||
"integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/istanbul-lib-coverage": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/istanbul-reports": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz",
|
||||
"integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/istanbul-lib-coverage": "*",
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/json5": {
|
||||
"version": "0.0.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||
@@ -5703,26 +5675,6 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/redux-mock-store": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.5.0.tgz",
|
||||
"integrity": "sha512-jcscBazm6j05Hs6xYCca6psTUBbFT2wqMxT7wZEHAYFxHB/I8jYk7d5msrHUlDiSL02HdTqTmkK2oIV8i3C8DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"redux": "^4.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/redux-mock-store/node_modules/redux": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/statuses": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz",
|
||||
@@ -5730,74 +5682,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/testing-library__dom": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/testing-library__dom/-/testing-library__dom-7.0.2.tgz",
|
||||
"integrity": "sha512-8yu1gSwUEAwzg2OlPNbGq+ixhmSviGurBu1+ivxRKq1eRcwdjkmlwtPvr9VhuxTq2fNHBWN2po6Iem3Xt5A6rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pretty-format": "^25.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/testing-library__dom/node_modules/pretty-format": {
|
||||
"version": "25.5.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz",
|
||||
"integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/types": "^25.5.0",
|
||||
"ansi-regex": "^5.0.0",
|
||||
"ansi-styles": "^4.0.0",
|
||||
"react-is": "^16.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/testing-library__dom/node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/testing-library__react": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/testing-library__react/-/testing-library__react-10.0.1.tgz",
|
||||
"integrity": "sha512-RbDwmActAckbujLZeVO/daSfdL1pnjVqas25UueOkAY5r7vriavWf0Zqg7ghXMHa8ycD/kLkv8QOj31LmSYwww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react-dom": "*",
|
||||
"@types/testing-library__dom": "*",
|
||||
"pretty-format": "^25.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/testing-library__react/node_modules/pretty-format": {
|
||||
"version": "25.5.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz",
|
||||
"integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/types": "^25.5.0",
|
||||
"ansi-regex": "^5.0.0",
|
||||
"ansi-styles": "^4.0.0",
|
||||
"react-is": "^16.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/testing-library__react/node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/tough-cookie": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
|
||||
@@ -5827,23 +5711,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "15.0.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz",
|
||||
"integrity": "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/yargs-parser": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yargs-parser": {
|
||||
"version": "21.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
|
||||
"integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "7.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
|
||||
@@ -8088,7 +7955,8 @@
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/dot-case": {
|
||||
"version": "3.0.4",
|
||||
@@ -8282,9 +8150,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-abstract": {
|
||||
"version": "1.23.7",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.7.tgz",
|
||||
"integrity": "sha512-OygGC8kIcDhXX+6yAZRGLqwi2CmEXCbLQixeGUgYeR+Qwlppqmo7DIDr8XibtEBZp+fJcoYpoatp5qwLMEdcqQ==",
|
||||
"version": "1.23.8",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.8.tgz",
|
||||
"integrity": "sha512-lfab8IzDn6EpI1ibZakcgS6WsfEBiB+43cuJo+wgylx1xKXf+Sp+YR3vFuQwC/u3sxYwV8Cxe3B0DpVUu/WiJQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -8323,8 +8191,10 @@
|
||||
"object-inspect": "^1.13.3",
|
||||
"object-keys": "^1.1.1",
|
||||
"object.assign": "^4.1.7",
|
||||
"own-keys": "^1.0.0",
|
||||
"regexp.prototype.flags": "^1.5.3",
|
||||
"safe-array-concat": "^1.1.3",
|
||||
"safe-push-apply": "^1.0.0",
|
||||
"safe-regex-test": "^1.1.0",
|
||||
"string.prototype.trim": "^1.2.10",
|
||||
"string.prototype.trimend": "^1.0.9",
|
||||
@@ -9571,6 +9441,7 @@
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.15.0.tgz",
|
||||
"integrity": "sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"motion-dom": "^11.14.3",
|
||||
"motion-utils": "^11.14.3",
|
||||
@@ -11701,6 +11572,7 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
@@ -11758,6 +11630,7 @@
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
@@ -12898,13 +12771,15 @@
|
||||
"version": "11.14.3",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz",
|
||||
"integrity": "sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "11.14.3",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.14.3.tgz",
|
||||
"integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
@@ -13423,6 +13298,24 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/own-keys": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
||||
"integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"object-keys": "^1.1.1",
|
||||
"safe-push-apply": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
@@ -13989,6 +13882,7 @@
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
@@ -14004,6 +13898,7 @@
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -14219,28 +14114,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
|
||||
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
|
||||
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
"scheduler": "^0.25.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1"
|
||||
"react": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-highlight": {
|
||||
@@ -14269,9 +14160,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "15.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.2.0.tgz",
|
||||
"integrity": "sha512-iJNc8111EaDtVTVMKigvBtPHyrJV+KblWG73cUxqp+WmJCcwkzhWNFXmkAD5pwP2Z4woeDj/oXDdbjDsb3Gutg==",
|
||||
"version": "15.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.0.tgz",
|
||||
"integrity": "sha512-Py6UkX3zV08RTvL6ZANRoBh9sL/ne6rQq79XlkHEdd82cZr2H9usbWpUNVadJntIZP2pu3M2rL1CN+5rQYfYFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.0",
|
||||
@@ -14304,7 +14195,8 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-markdown": {
|
||||
"version": "9.0.1",
|
||||
@@ -14497,19 +14389,6 @@
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/redux-mock-store": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.5.tgz",
|
||||
"integrity": "sha512-YxX+ofKUTQkZE4HbhYG4kKGr7oCTJfB0GLy7bSeqx86GLpGirrbUWstMnqXkqHNaQpcnbMGbof2dYs5KsPE6Zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash.isplainobject": "^4.0.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"redux": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||
@@ -15052,6 +14931,30 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-push-apply": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
|
||||
"integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"isarray": "^2.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-push-apply/node_modules/isarray": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
|
||||
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-regex-test": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
|
||||
@@ -15090,13 +14993,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
|
||||
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scroll-into-view-if-needed": {
|
||||
"version": "3.0.10",
|
||||
@@ -16621,9 +16521,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.5.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
|
||||
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
|
||||
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"@react-router/serve": "^7.1.1",
|
||||
"@react-types/shared": "^3.25.0",
|
||||
"@reduxjs/toolkit": "^2.5.0",
|
||||
"@tanstack/react-query": "^5.62.10",
|
||||
"@tanstack/react-query": "^5.62.11",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
@@ -26,12 +26,12 @@
|
||||
"isbot": "^5.1.19",
|
||||
"jose": "^5.9.4",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.203.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"posthog-js": "^1.203.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-i18next": "^15.2.0",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-redux": "^9.2.0",
|
||||
@@ -42,7 +42,7 @@
|
||||
"sirv-cli": "^3.0.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"vite": "^5.4.9",
|
||||
"vite": "^5.4.11",
|
||||
"web-vitals": "^3.5.2",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
@@ -75,12 +75,12 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mswjs/socket.io-binding": "^0.1.1",
|
||||
"@playwright/test": "^1.49.1",
|
||||
"@react-router/dev": "^7.1.1",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tanstack/eslint-plugin-query": "^5.62.9",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/jest-dom": "^6.6.1",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/node": "^22.10.2",
|
||||
@@ -88,8 +88,6 @@
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/redux-mock-store": "^1.5.0",
|
||||
"@types/testing-library__react": "^10.0.1",
|
||||
"@types/ws": "^8.5.12",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
@@ -105,33 +103,21 @@
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-react": "^7.37.3",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"framer-motion": "^11.15.0",
|
||||
"husky": "^9.1.6",
|
||||
"jsdom": "^25.0.1",
|
||||
"lint-staged": "^15.2.11",
|
||||
"lint-staged": "^15.3.0",
|
||||
"msw": "^2.6.6",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.4.2",
|
||||
"redux-mock-store": "^1.5.5",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.5.3",
|
||||
"typescript": "^5.6.3",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
"packageManager": "npm@10.5.0",
|
||||
"overrides": {
|
||||
"@tanstack/react-virtual": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"react-textarea-autosize": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.11.0"
|
||||
"node": "18.20.1"
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": [
|
||||
|
||||
@@ -106,15 +106,28 @@ export const retrieveGitHubUser = async () => {
|
||||
|
||||
export const retrieveLatestGitHubCommit = async (
|
||||
repository: string,
|
||||
): Promise<GitHubCommit> => {
|
||||
const response = await github.get<GitHubCommit[]>(
|
||||
`/repos/${repository}/commits`,
|
||||
{
|
||||
params: {
|
||||
per_page: 1,
|
||||
): Promise<GitHubCommit | null> => {
|
||||
try {
|
||||
const response = await github.get<GitHubCommit[]>(
|
||||
`/repos/${repository}/commits`,
|
||||
{
|
||||
params: {
|
||||
per_page: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return response.data[0];
|
||||
);
|
||||
return response.data[0] || null;
|
||||
} catch (error) {
|
||||
if (!error || typeof error !== "object") {
|
||||
throw new Error("Unknown error occurred");
|
||||
}
|
||||
const axiosError = error as { response?: { status: number } };
|
||||
if (axiosError.response?.status === 409) {
|
||||
// Repository is empty, no commits yet
|
||||
return null;
|
||||
}
|
||||
throw new Error(
|
||||
error instanceof Error ? error.message : "Unknown error occurred",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,8 +8,10 @@ import {
|
||||
GetConfigResponse,
|
||||
GetVSCodeUrlResponse,
|
||||
AuthenticateResponse,
|
||||
Conversation,
|
||||
} from "./open-hands.types";
|
||||
import { openHands } from "./open-hands-axios";
|
||||
import { ApiSettings } from "#/services/settings";
|
||||
|
||||
class OpenHands {
|
||||
/**
|
||||
@@ -219,6 +221,52 @@ class OpenHands {
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getUserConversations(): Promise<Conversation[]> {
|
||||
const { data } = await openHands.get<Conversation[]>("/api/conversations");
|
||||
return data;
|
||||
}
|
||||
|
||||
static async deleteUserConversation(conversationId: string): Promise<void> {
|
||||
await openHands.delete(`/api/conversations/${conversationId}`);
|
||||
}
|
||||
|
||||
static async updateUserConversation(
|
||||
conversationId: string,
|
||||
conversation: Partial<Omit<Conversation, "id">>,
|
||||
): Promise<void> {
|
||||
await openHands.put(`/api/conversations/${conversationId}`, conversation);
|
||||
}
|
||||
|
||||
static async createConversation(
|
||||
githubToken?: string,
|
||||
selectedRepository?: string,
|
||||
): Promise<Conversation> {
|
||||
const body = {
|
||||
github_token: githubToken,
|
||||
selected_repository: selectedRepository,
|
||||
};
|
||||
|
||||
const { data } = await openHands.post<Conversation>(
|
||||
"/api/conversations",
|
||||
body,
|
||||
);
|
||||
|
||||
// TODO: remove this once we have a multi-conversation UI
|
||||
localStorage.setItem("latest_conversation_id", data.conversation_id);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getConversation(
|
||||
conversationId: string,
|
||||
): Promise<Conversation | null> {
|
||||
const { data } = await openHands.get<Conversation | null>(
|
||||
`/api/conversations/${conversationId}`,
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static async searchEvents(
|
||||
conversationId: string,
|
||||
params: {
|
||||
@@ -248,20 +296,22 @@ class OpenHands {
|
||||
return data;
|
||||
}
|
||||
|
||||
static async newConversation(params: {
|
||||
githubToken?: string;
|
||||
selectedRepository?: string;
|
||||
}): Promise<{ conversation_id: string }> {
|
||||
const { data } = await openHands.post<{
|
||||
conversation_id: string;
|
||||
}>("/api/conversations", {
|
||||
github_token: params.githubToken,
|
||||
selected_repository: params.selectedRepository,
|
||||
});
|
||||
// TODO: remove this once we have a multi-conversation UI
|
||||
localStorage.setItem("latest_conversation_id", data.conversation_id);
|
||||
/**
|
||||
* Get the settings from the server or use the default settings if not found
|
||||
*/
|
||||
static async getSettings(): Promise<ApiSettings> {
|
||||
const { data } = await openHands.get<ApiSettings>("/api/settings");
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the settings to the server. Only valid settings are saved.
|
||||
* @param settings - the settings to save
|
||||
*/
|
||||
static async saveSettings(settings: Partial<ApiSettings>): Promise<boolean> {
|
||||
const data = await openHands.post("/api/settings", settings);
|
||||
return data.status === 200;
|
||||
}
|
||||
}
|
||||
|
||||
export default OpenHands;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ProjectState } from "#/components/features/conversation-panel/conversation-state-indicator";
|
||||
|
||||
export interface ErrorResponse {
|
||||
error: string;
|
||||
}
|
||||
@@ -57,3 +59,11 @@ export interface AuthenticateResponse {
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface Conversation {
|
||||
conversation_id: string;
|
||||
name: string;
|
||||
repo: string | null;
|
||||
lastUpdated: string;
|
||||
state: ProjectState;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import React from "react";
|
||||
import Markdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { useSelector } from "react-redux";
|
||||
import { code } from "../markdown/code";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { ul, ol } from "../markdown/list";
|
||||
import { CopyToClipboardButton } from "#/components/shared/buttons/copy-to-clipboard-button";
|
||||
import { anchor } from "../markdown/anchor";
|
||||
import { RootState } from "#/state/store";
|
||||
|
||||
interface ChatMessageProps {
|
||||
type: "user" | "assistant";
|
||||
@@ -21,7 +19,6 @@ export function ChatMessage({
|
||||
}: React.PropsWithChildren<ChatMessageProps>) {
|
||||
const [isHovering, setIsHovering] = React.useState(false);
|
||||
const [isCopy, setIsCopy] = React.useState(false);
|
||||
const speechEnabled = useSelector((state: RootState) => state.speech.enabled);
|
||||
|
||||
const handleCopyToClipboard = async () => {
|
||||
await navigator.clipboard.writeText(message);
|
||||
@@ -42,14 +39,6 @@ export function ChatMessage({
|
||||
};
|
||||
}, [isCopy]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (speechEnabled && type === "assistant") {
|
||||
const utterance = new SpeechSynthesisUtterance(message);
|
||||
speechSynthesis.cancel(); // Cancel any ongoing speech
|
||||
speechSynthesis.speak(utterance);
|
||||
}
|
||||
}, [message, type, speechEnabled]);
|
||||
|
||||
return (
|
||||
<article
|
||||
data-testid={`${type}-message`}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ContextMenu } from "./context-menu";
|
||||
import { ContextMenuListItem } from "./context-menu-list-item";
|
||||
import { ContextMenuSeparator } from "./context-menu-separator";
|
||||
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface AccountSettingsContextMenuProps {
|
||||
onClickAccountSettings: () => void;
|
||||
@@ -17,6 +19,7 @@ export function AccountSettingsContextMenu({
|
||||
isLoggedIn,
|
||||
}: AccountSettingsContextMenuProps) {
|
||||
const ref = useClickOutsideElement<HTMLUListElement>(onClose);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
@@ -25,11 +28,11 @@ export function AccountSettingsContextMenu({
|
||||
className="absolute left-full -top-1 z-10"
|
||||
>
|
||||
<ContextMenuListItem onClick={onClickAccountSettings}>
|
||||
Account Settings
|
||||
{t(I18nKey.ACCOUNT_SETTINGS$SETTINGS)}
|
||||
</ContextMenuListItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuListItem onClick={onLogout} isDisabled={!isLoggedIn}>
|
||||
Logout
|
||||
{t(I18nKey.ACCOUNT_SETTINGS$LOGOUT)}
|
||||
</ContextMenuListItem>
|
||||
</ContextMenu>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface ContextMenuListItemProps {
|
||||
onClick: () => void;
|
||||
testId?: string;
|
||||
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export function ContextMenuListItem({
|
||||
children,
|
||||
testId,
|
||||
onClick,
|
||||
isDisabled,
|
||||
}: React.PropsWithChildren<ContextMenuListItemProps>) {
|
||||
return (
|
||||
<button
|
||||
data-testid="context-menu-list-item"
|
||||
data-testid={testId || "context-menu-list-item"}
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={isDisabled}
|
||||
|
||||
@@ -2,13 +2,19 @@ import React from "react";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface ContextMenuProps {
|
||||
ref?: React.RefObject<HTMLUListElement | null>;
|
||||
testId?: string;
|
||||
children: React.ReactNode;
|
||||
className?: React.HTMLAttributes<HTMLUListElement>["className"];
|
||||
}
|
||||
|
||||
export const ContextMenu = React.forwardRef<HTMLUListElement, ContextMenuProps>(
|
||||
({ testId, children, className }, ref) => (
|
||||
export function ContextMenu({
|
||||
testId,
|
||||
children,
|
||||
className,
|
||||
ref,
|
||||
}: ContextMenuProps) {
|
||||
return (
|
||||
<ul
|
||||
data-testid={testId}
|
||||
ref={ref}
|
||||
@@ -16,7 +22,5 @@ export const ContextMenu = React.forwardRef<HTMLUListElement, ContextMenuProps>(
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
);
|
||||
|
||||
ContextMenu.displayName = "ContextMenu";
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ModalButton } from "#/components/shared/buttons/modal-button";
|
||||
import {
|
||||
BaseModalDescription,
|
||||
BaseModalTitle,
|
||||
} from "#/components/shared/modals/confirmation-modals/base-modal";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
|
||||
interface ConfirmDeleteModalProps {
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function ConfirmDeleteModal({
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: ConfirmDeleteModalProps) {
|
||||
return (
|
||||
<ModalBackdrop>
|
||||
<ModalBody className="items-start">
|
||||
<div className="flex flex-col gap-2">
|
||||
<BaseModalTitle title="Are you sure you want to delete this project?" />
|
||||
<BaseModalDescription description="All data associated with this project will be lost." />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<ModalButton
|
||||
onClick={onConfirm}
|
||||
className="bg-[#4465DB]"
|
||||
text="Confirm"
|
||||
/>
|
||||
<ModalButton onClick={onCancel} className="bg-danger" text="Cancel" />
|
||||
</div>
|
||||
</ModalBody>
|
||||
</ModalBackdrop>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import React from "react";
|
||||
import { formatTimeDelta } from "#/utils/format-time-delta";
|
||||
import { ConversationRepoLink } from "./conversation-repo-link";
|
||||
import {
|
||||
ProjectState,
|
||||
ConversationStateIndicator,
|
||||
} from "./conversation-state-indicator";
|
||||
import { ContextMenu } from "../context-menu/context-menu";
|
||||
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
|
||||
import { EllipsisButton } from "./ellipsis-button";
|
||||
|
||||
interface ProjectCardProps {
|
||||
onClick: () => void;
|
||||
onDelete: () => void;
|
||||
onChangeTitle: (title: string) => void;
|
||||
name: string;
|
||||
repo: string | null;
|
||||
lastUpdated: string; // ISO 8601
|
||||
state?: ProjectState;
|
||||
}
|
||||
|
||||
export function ConversationCard({
|
||||
onClick,
|
||||
onDelete,
|
||||
onChangeTitle,
|
||||
name,
|
||||
repo,
|
||||
lastUpdated,
|
||||
state = "cold",
|
||||
}: ProjectCardProps) {
|
||||
const [contextMenuVisible, setContextMenuVisible] = React.useState(false);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleBlur = () => {
|
||||
if (inputRef.current?.value) {
|
||||
const trimmed = inputRef.current.value.trim();
|
||||
onChangeTitle(trimmed);
|
||||
inputRef.current!.value = trimmed;
|
||||
} else {
|
||||
// reset the value if it's empty
|
||||
inputRef.current!.value = name;
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputClick = (event: React.MouseEvent<HTMLInputElement>) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const handleDelete = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
onDelete();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="conversation-card"
|
||||
onClick={onClick}
|
||||
className="h-[100px] w-full px-[18px] py-4 border-b border-neutral-600"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<input
|
||||
ref={inputRef}
|
||||
data-testid="conversation-card-title"
|
||||
onClick={handleInputClick}
|
||||
onBlur={handleBlur}
|
||||
type="text"
|
||||
defaultValue={name}
|
||||
className="text-sm leading-6 font-semibold bg-transparent"
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-2 relative">
|
||||
<ConversationStateIndicator state={state} />
|
||||
<EllipsisButton
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setContextMenuVisible((prev) => !prev);
|
||||
}}
|
||||
/>
|
||||
{contextMenuVisible && (
|
||||
<ContextMenu testId="context-menu" className="absolute left-full">
|
||||
<ContextMenuListItem
|
||||
testId="delete-button"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete
|
||||
</ContextMenuListItem>
|
||||
</ContextMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{repo && (
|
||||
<ConversationRepoLink
|
||||
repo={repo}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
<p className="text-xs text-neutral-400">
|
||||
<time>{formatTimeDelta(new Date(lastUpdated))} ago</time>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import React from "react";
|
||||
import { useLocation, useNavigate, useParams } from "react-router";
|
||||
import { ConversationCard } from "./conversation-card";
|
||||
import { useUserConversations } from "#/hooks/query/use-user-conversations";
|
||||
import { useDeleteConversation } from "#/hooks/mutation/use-delete-conversation";
|
||||
import { ConfirmDeleteModal } from "./confirm-delete-modal";
|
||||
import { NewConversationButton } from "./new-conversation-button";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import { useUpdateConversation } from "#/hooks/mutation/use-update-conversation";
|
||||
import { useEndSession } from "#/hooks/use-end-session";
|
||||
import { ExitConversationModal } from "./exit-conversation-modal";
|
||||
|
||||
interface ConversationPanelProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ConversationPanel({ onClose }: ConversationPanelProps) {
|
||||
const { conversationId: cid } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const endSession = useEndSession();
|
||||
|
||||
const [confirmDeleteModalVisible, setConfirmDeleteModalVisible] =
|
||||
React.useState(false);
|
||||
const [
|
||||
confirmExitConversationModalVisible,
|
||||
setConfirmExitConversationModalVisible,
|
||||
] = React.useState(false);
|
||||
const [selectedConversationId, setSelectedConversationId] = React.useState<
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
const { data: conversations, isFetching, error } = useUserConversations();
|
||||
|
||||
const { mutate: deleteConversation } = useDeleteConversation();
|
||||
const { mutate: updateConversation } = useUpdateConversation();
|
||||
|
||||
const handleDeleteProject = (conversationId: string) => {
|
||||
setConfirmDeleteModalVisible(true);
|
||||
setSelectedConversationId(conversationId);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = () => {
|
||||
if (selectedConversationId) {
|
||||
deleteConversation({ conversationId: selectedConversationId });
|
||||
setConfirmDeleteModalVisible(false);
|
||||
|
||||
if (cid === selectedConversationId) {
|
||||
endSession();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangeTitle = (
|
||||
conversationId: string,
|
||||
oldTitle: string,
|
||||
newTitle: string,
|
||||
) => {
|
||||
if (oldTitle !== newTitle)
|
||||
updateConversation({
|
||||
id: conversationId,
|
||||
conversation: { name: newTitle },
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickCard = (conversationId: string) => {
|
||||
navigate(`/conversations/${conversationId}`);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="conversation-panel"
|
||||
className="w-[350px] h-full border border-neutral-700 bg-neutral-800 rounded-xl"
|
||||
>
|
||||
<div className="pt-4 px-4 flex items-center justify-between">
|
||||
{location.pathname.startsWith("/conversation") && (
|
||||
<NewConversationButton
|
||||
onClick={() => setConfirmExitConversationModalVisible(true)}
|
||||
/>
|
||||
)}
|
||||
{isFetching && <LoadingSpinner size="small" />}
|
||||
</div>
|
||||
{error && (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<p className="text-danger">{error.message}</p>
|
||||
</div>
|
||||
)}
|
||||
{conversations?.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<p className="text-neutral-400">No conversations found</p>
|
||||
</div>
|
||||
)}
|
||||
{conversations?.map((project) => (
|
||||
<ConversationCard
|
||||
key={project.conversation_id}
|
||||
onClick={() => handleClickCard(project.conversation_id)}
|
||||
onDelete={() => handleDeleteProject(project.conversation_id)}
|
||||
onChangeTitle={(title) =>
|
||||
handleChangeTitle(project.conversation_id, project.name, title)
|
||||
}
|
||||
name={project.name}
|
||||
repo={project.repo}
|
||||
lastUpdated={project.lastUpdated}
|
||||
state={project.state}
|
||||
/>
|
||||
))}
|
||||
|
||||
{confirmDeleteModalVisible && (
|
||||
<ConfirmDeleteModal
|
||||
onConfirm={handleConfirmDelete}
|
||||
onCancel={() => setConfirmDeleteModalVisible(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{confirmExitConversationModalVisible && (
|
||||
<ExitConversationModal
|
||||
onConfirm={() => {
|
||||
endSession();
|
||||
onClose();
|
||||
}}
|
||||
onClose={() => setConfirmExitConversationModalVisible(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
interface ConversationRepoLinkProps {
|
||||
repo: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
|
||||
}
|
||||
|
||||
export function ConversationRepoLink({
|
||||
repo,
|
||||
onClick,
|
||||
}: ConversationRepoLinkProps) {
|
||||
return (
|
||||
<a
|
||||
data-testid="conversation-card-repo"
|
||||
href={`https://github.com/${repo}`}
|
||||
target="_blank noopener noreferrer"
|
||||
onClick={onClick}
|
||||
className="text-xs text-neutral-400 hover:text-neutral-200"
|
||||
>
|
||||
{repo}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import ColdIcon from "./state-indicators/cold.svg?react";
|
||||
import CoolingIcon from "./state-indicators/cooling.svg?react";
|
||||
import FinishedIcon from "./state-indicators/finished.svg?react";
|
||||
import RunningIcon from "./state-indicators/running.svg?react";
|
||||
import WaitingIcon from "./state-indicators/waiting.svg?react";
|
||||
import WarmIcon from "./state-indicators/warm.svg?react";
|
||||
|
||||
type SVGIcon = React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
|
||||
export type ProjectState =
|
||||
| "cold"
|
||||
| "cooling"
|
||||
| "finished"
|
||||
| "running"
|
||||
| "waiting"
|
||||
| "warm";
|
||||
|
||||
const INDICATORS: Record<ProjectState, SVGIcon> = {
|
||||
cold: ColdIcon,
|
||||
cooling: CoolingIcon,
|
||||
finished: FinishedIcon,
|
||||
running: RunningIcon,
|
||||
waiting: WaitingIcon,
|
||||
warm: WarmIcon,
|
||||
};
|
||||
|
||||
interface ConversationStateIndicatorProps {
|
||||
state: ProjectState;
|
||||
}
|
||||
|
||||
export function ConversationStateIndicator({
|
||||
state,
|
||||
}: ConversationStateIndicatorProps) {
|
||||
const StateIcon = INDICATORS[state];
|
||||
|
||||
return (
|
||||
<div data-testid={`${state}-indicator`}>
|
||||
<StateIcon />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { FaEllipsisV } from "react-icons/fa";
|
||||
|
||||
interface EllipsisButtonProps {
|
||||
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
|
||||
export function EllipsisButton({ onClick }: EllipsisButtonProps) {
|
||||
return (
|
||||
<button data-testid="ellipsis-button" type="button" onClick={onClick}>
|
||||
<FaEllipsisV fill="#a3a3a3" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { ModalButton } from "#/components/shared/buttons/modal-button";
|
||||
import { BaseModalTitle } from "#/components/shared/modals/confirmation-modals/base-modal";
|
||||
|
||||
interface ExitConversationModalProps {
|
||||
onConfirm: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ExitConversationModal({
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: ExitConversationModalProps) {
|
||||
return (
|
||||
<ModalBackdrop>
|
||||
<ModalBody testID="confirm-new-conversation-modal">
|
||||
<BaseModalTitle title="Creating a new conversation will replace your active conversation" />
|
||||
<div className="flex w-full gap-2">
|
||||
<ModalButton
|
||||
text="Confirm"
|
||||
onClick={onConfirm}
|
||||
className="bg-[#C63143] flex-1"
|
||||
/>
|
||||
<ModalButton
|
||||
text="Cancel"
|
||||
onClick={onClose}
|
||||
className="bg-neutral-700 flex-1"
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</ModalBackdrop>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
interface NewConversationButtonProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function NewConversationButton({ onClick }: NewConversationButtonProps) {
|
||||
return (
|
||||
<button
|
||||
data-testid="new-conversation-button"
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="font-bold bg-[#4465DB] px-2 py-1 rounded"
|
||||
>
|
||||
+ New Project
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.87012 2C9.87012 1.44772 9.4224 1 8.87012 1C8.31783 1 7.87012 1.44772 7.87012 2V8C7.87012 8.55228 8.31783 9 8.87012 9C9.4224 9 9.87012 8.55228 9.87012 8V2Z" fill="#A7A9AC"/>
|
||||
<path d="M10.8698 2.42001V2.56001C10.8698 2.93001 11.0698 3.28001 11.4098 3.43001C13.6798 4.43001 15.2198 6.80001 14.9698 9.48001C14.6998 12.47 12.0998 14.87 9.08979 14.92C5.73979 14.97 2.98979 12.26 2.98979 8.92001C2.98979 6.57001 4.34979 4.54001 6.30979 3.56001C6.63979 3.40001 6.85979 3.08001 6.85979 2.72001V2.55001C6.85979 1.86001 6.13979 1.43001 5.50979 1.73001C2.43979 3.20001 0.449793 6.62001 1.13979 10.41C1.70979 13.57 4.23979 16.14 7.38979 16.76C12.5098 17.76 16.9998 13.86 16.9998 8.92001C16.9998 5.61001 14.9898 2.78001 12.1198 1.56001C11.5298 1.31001 10.8698 1.78001 10.8698 2.42001Z" fill="#A7A9AC"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 904 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.87012 2.02002C9.87012 1.46773 9.4224 1.02002 8.87012 1.02002C8.31783 1.02002 7.87012 1.46773 7.87012 2.02002V8.02002C7.87012 8.5723 8.31783 9.02002 8.87012 9.02002C9.4224 9.02002 9.87012 8.5723 9.87012 8.02002V2.02002Z" fill="#EFC818"/>
|
||||
<path d="M10.8698 2.44003V2.58003C10.8698 2.95003 11.0698 3.30003 11.4098 3.45003C13.6798 4.45003 15.2198 6.82003 14.9698 9.50003C14.6998 12.49 12.0998 14.89 9.08979 14.94C5.73979 14.99 2.98979 12.28 2.98979 8.94003C2.98979 6.59003 4.34979 4.56003 6.30979 3.58003C6.63979 3.42003 6.85979 3.10003 6.85979 2.74003V2.57003C6.85979 1.88003 6.13979 1.45003 5.50979 1.75003C2.43979 3.23003 0.449793 6.64003 1.13979 10.43C1.70979 13.59 4.23979 16.16 7.38979 16.78C12.5098 17.78 16.9998 13.88 16.9998 8.94003C16.9998 5.63003 14.9898 2.80003 12.1198 1.58003C11.5298 1.33003 10.8698 1.80003 10.8698 2.44003Z" fill="#EFC818"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 968 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 16.8599C13.4183 16.8599 17 13.2781 17 8.85986C17 4.44159 13.4183 0.859863 9 0.859863C4.58172 0.859863 1 4.44159 1 8.85986C1 13.2781 4.58172 16.8599 9 16.8599Z" fill="#779FD4"/>
|
||||
<path d="M4.61035 8.43014L7.86035 12.0301L13.3904 6.64014" stroke="#231F20" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 433 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.04004 3.10986C12.06 3.10986 14.57 5.34986 14.98 8.25986C15.05 8.74986 15.47 9.10986 15.96 9.10986C16.57 9.10986 17.04 8.56986 16.96 7.96986C16.41 4.08986 13.07 1.10986 9.04004 1.10986C4.62004 1.10986 1.04004 4.68986 1.04004 9.10986C1.04004 13.1399 4.02004 16.4799 7.90004 17.0299C8.50004 17.1199 9.04004 16.6399 9.04004 16.0299C9.04004 15.5399 8.68004 15.1199 8.19004 15.0499C5.28004 14.6399 3.04004 12.1299 3.04004 9.10986C3.04004 5.79986 5.73004 3.10986 9.04004 3.10986Z" fill="#60BB46"/>
|
||||
<path d="M12.3504 9.11L7.40039 6.25V11.96L12.3504 9.11Z" fill="#60BB46"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 680 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.76039 6.99002C8.478 6.99002 9.87039 5.59763 9.87039 3.88002C9.87039 2.16241 8.478 0.77002 6.76039 0.77002C5.04279 0.77002 3.65039 2.16241 3.65039 3.88002C3.65039 5.59763 5.04279 6.99002 6.76039 6.99002Z" fill="#FFE165"/>
|
||||
<path d="M1.0802 17.0799C1.0802 17.0799 0.610196 11.5499 3.0102 9.67992C4.7902 8.29992 7.3302 9.44992 9.7802 7.95992C11.5802 6.86992 13.6102 4.10992 14.5202 2.49992C14.9302 1.77992 15.9102 1.62992 16.6102 2.05992C17.3802 2.51992 17.6102 3.53992 17.1102 4.28992C16.2302 5.58992 14.1802 8.85992 13.1202 10.3699C10.7602 13.7599 11.4302 17.0799 11.4302 17.0799H1.0702H1.0802Z" fill="#FFE165"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 726 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.87012 2.08984C9.87012 1.53756 9.4224 1.08984 8.87012 1.08984C8.31783 1.08984 7.87012 1.53756 7.87012 2.08984V8.08984C7.87012 8.64213 8.31783 9.08984 8.87012 9.08984C9.4224 9.08984 9.87012 8.64213 9.87012 8.08984V2.08984Z" fill="#60BB46"/>
|
||||
<path d="M10.8702 2.50988V2.64988C10.8702 3.01988 11.0702 3.36988 11.4102 3.51988C13.6802 4.51988 15.2202 6.88988 14.9702 9.56988C14.7002 12.5599 12.1002 14.9599 9.09021 15.0099C5.74021 15.0599 2.99021 12.3499 2.99021 9.00988C2.99021 6.65988 4.35021 4.62988 6.31021 3.64988C6.64021 3.48988 6.86021 3.16988 6.86021 2.80988V2.63988C6.86021 1.94988 6.14021 1.51988 5.51021 1.81988C2.42021 3.30988 0.430214 6.71988 1.12021 10.5199C1.69021 13.6799 4.22021 16.2499 7.37021 16.8699C12.4902 17.8699 16.9802 13.9699 16.9802 9.02988C16.9802 5.71988 14.9702 2.88988 12.1002 1.66988C11.5102 1.41988 10.8502 1.88988 10.8502 2.52988L10.8702 2.50988Z" fill="#60BB46"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1008 B |
@@ -31,17 +31,6 @@ export function GitHubRepositoriesSuggestionBox({
|
||||
}
|
||||
};
|
||||
|
||||
if (isGitHubErrorReponse(repositories)) {
|
||||
return (
|
||||
<SuggestionBox
|
||||
title="Error Fetching Repositories"
|
||||
content={
|
||||
<p className="text-danger text-center">{repositories.message}</p>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const isLoggedIn = !!user && !isGitHubErrorReponse(user);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import EllipsisH from "#/icons/ellipsis-h.svg?react";
|
||||
import { ProjectMenuCardContextMenu } from "./project.menu-card-context-menu";
|
||||
import { ProjectMenuDetailsPlaceholder } from "./project-menu-details-placeholder";
|
||||
@@ -7,6 +8,7 @@ import { ProjectMenuDetails } from "./project-menu-details";
|
||||
import { ConnectToGitHubModal } from "#/components/shared/modals/connect-to-github-modal";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { DownloadModal } from "#/components/shared/download-modal";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface ProjectMenuCardProps {
|
||||
isConnectedToGitHub: boolean;
|
||||
@@ -21,6 +23,8 @@ export function ProjectMenuCard({
|
||||
isConnectedToGitHub,
|
||||
githubData,
|
||||
}: ProjectMenuCardProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [contextMenuIsOpen, setContextMenuIsOpen] = React.useState(false);
|
||||
const [connectToGitHubModalOpen, setConnectToGitHubModalOpen] =
|
||||
React.useState(false);
|
||||
@@ -71,7 +75,7 @@ export function ProjectMenuCard({
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleMenuVisibility}
|
||||
aria-label="Open project menu"
|
||||
aria-label={t(I18nKey.PROJECT_MENU_CARD$OPEN)}
|
||||
>
|
||||
<EllipsisH width={36} height={36} />
|
||||
</button>
|
||||
|
||||
@@ -30,7 +30,9 @@ export function ProjectMenuDetailsPlaceholder({
|
||||
"hover:underline hover:underline-offset-2",
|
||||
)}
|
||||
>
|
||||
{!isConnectedToGitHub ? "Connect to GitHub" : "Connected"}
|
||||
{!isConnectedToGitHub
|
||||
? t(I18nKey.PROJECT_MENU_DETAILS_PLACEHOLDER$CONNECT_TO_GITHUB)
|
||||
: t(I18nKey.PROJECT_MENU_DETAILS_PLACEHOLDER$CONNECTED)}
|
||||
<CloudConnection width={12} height={12} />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { useLocation } from "react-router";
|
||||
import FolderIcon from "#/icons/docs.svg?react";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { useSettings } from "#/context/settings-context";
|
||||
import { useGitHubUser } from "#/hooks/query/use-github-user";
|
||||
import { useIsAuthed } from "#/hooks/query/use-is-authed";
|
||||
import { UserActions } from "./user-actions";
|
||||
@@ -13,21 +13,28 @@ import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import { AccountSettingsModal } from "#/components/shared/modals/account-settings/account-settings-modal";
|
||||
import { ExitProjectConfirmationModal } from "#/components/shared/modals/exit-project-confirmation-modal";
|
||||
import { SettingsModal } from "#/components/shared/modals/settings/settings-modal";
|
||||
import { useSettingsUpToDate } from "#/context/settings-up-to-date-context";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { ConversationPanel } from "../conversation-panel/conversation-panel";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { MULTI_CONVO_UI_IS_ENABLED } from "#/utils/constants";
|
||||
|
||||
export function Sidebar() {
|
||||
const location = useLocation();
|
||||
|
||||
const user = useGitHubUser();
|
||||
const { data: isAuthed } = useIsAuthed();
|
||||
|
||||
const { logout } = useAuth();
|
||||
const { settingsAreUpToDate } = useSettings();
|
||||
const { data: settings, isError: settingsIsError } = useSettings();
|
||||
const { isUpToDate: settingsAreUpToDate } = useSettingsUpToDate();
|
||||
|
||||
const [accountSettingsModalOpen, setAccountSettingsModalOpen] =
|
||||
React.useState(false);
|
||||
const [settingsModalIsOpen, setSettingsModalIsOpen] = React.useState(false);
|
||||
const [startNewProjectModalIsOpen, setStartNewProjectModalIsOpen] =
|
||||
React.useState(false);
|
||||
const [conversationPanelIsOpen, setConversationPanelIsOpen] = React.useState(
|
||||
MULTI_CONVO_UI_IS_ENABLED,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
// If the github token is invalid, open the account settings modal again
|
||||
@@ -54,7 +61,7 @@ export function Sidebar() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside className="h-[40px] md:h-auto px-1 flex flex-row md:flex-col gap-1">
|
||||
<aside className="h-[40px] md:h-auto px-1 flex flex-row md:flex-col gap-1 relative">
|
||||
<nav className="flex flex-row md:flex-col items-center gap-[18px]">
|
||||
<div className="w-[34px] h-[34px] flex items-center justify-center">
|
||||
<AllHandsLogoButton onClick={handleClickLogo} />
|
||||
@@ -70,18 +77,45 @@ export function Sidebar() {
|
||||
/>
|
||||
)}
|
||||
<SettingsButton onClick={() => setSettingsModalIsOpen(true)} />
|
||||
{MULTI_CONVO_UI_IS_ENABLED && (
|
||||
<button
|
||||
data-testid="toggle-conversation-panel"
|
||||
type="button"
|
||||
onClick={() => setConversationPanelIsOpen((prev) => !prev)}
|
||||
className={cn(
|
||||
conversationPanelIsOpen ? "border-b-2 border-[#FFE165]" : "",
|
||||
)}
|
||||
>
|
||||
<FolderIcon width={28} height={28} />
|
||||
</button>
|
||||
)}
|
||||
<DocsButton />
|
||||
<ExitProjectButton
|
||||
onClick={() => setStartNewProjectModalIsOpen(true)}
|
||||
/>
|
||||
</nav>
|
||||
|
||||
{conversationPanelIsOpen && (
|
||||
<div
|
||||
className="absolute h-full left-[calc(100%+12px)] top-0 z-20" // 12px padding (sidebar parent)
|
||||
>
|
||||
<ConversationPanel
|
||||
onClose={() => setConversationPanelIsOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
{accountSettingsModalOpen && (
|
||||
<AccountSettingsModal onClose={handleAccountSettingsModalClose} />
|
||||
)}
|
||||
{showSettingsModal && (
|
||||
<SettingsModal onClose={() => setSettingsModalIsOpen(false)} />
|
||||
)}
|
||||
{settingsIsError ||
|
||||
(showSettingsModal && (
|
||||
<SettingsModal
|
||||
settings={settings}
|
||||
onClose={() => setSettingsModalIsOpen(false)}
|
||||
/>
|
||||
))}
|
||||
{startNewProjectModalIsOpen && (
|
||||
<ExitProjectConfirmationModal
|
||||
onClose={() => setStartNewProjectModalIsOpen(false)}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Tooltip } from "@nextui-org/react";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import DefaultUserAvatar from "#/icons/default-user.svg?react";
|
||||
import { cn } from "#/utils/utils";
|
||||
@@ -10,7 +11,7 @@ interface UserAvatarProps {
|
||||
}
|
||||
|
||||
export function UserAvatar({ onClick, avatarUrl, isLoading }: UserAvatarProps) {
|
||||
return (
|
||||
const buttonContent = (
|
||||
<button
|
||||
data-testid="user-avatar"
|
||||
type="button"
|
||||
@@ -31,4 +32,10 @@ export function UserAvatar({ onClick, avatarUrl, isLoading }: UserAvatarProps) {
|
||||
{isLoading && <LoadingSpinner size="small" />}
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip content="Account settings" closeDelay={100}>
|
||||
{buttonContent}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
191
frontend/src/components/layout/resizable-panel.tsx
Normal file
191
frontend/src/components/layout/resizable-panel.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import React, { CSSProperties, JSX, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
VscChevronDown,
|
||||
VscChevronLeft,
|
||||
VscChevronRight,
|
||||
VscChevronUp,
|
||||
} from "react-icons/vsc";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { IconButton } from "../shared/buttons/icon-button";
|
||||
|
||||
export enum Orientation {
|
||||
HORIZONTAL = "horizontal",
|
||||
VERTICAL = "vertical",
|
||||
}
|
||||
|
||||
enum Collapse {
|
||||
COLLAPSED = "collapsed",
|
||||
SPLIT = "split",
|
||||
FILLED = "filled",
|
||||
}
|
||||
|
||||
type ResizablePanelProps = {
|
||||
firstChild: React.ReactNode;
|
||||
firstClassName: string | undefined;
|
||||
secondChild: React.ReactNode;
|
||||
secondClassName: string | undefined;
|
||||
className: string | undefined;
|
||||
orientation: Orientation;
|
||||
initialSize: number;
|
||||
};
|
||||
|
||||
export function ResizablePanel({
|
||||
firstChild,
|
||||
firstClassName,
|
||||
secondChild,
|
||||
secondClassName,
|
||||
className,
|
||||
orientation,
|
||||
initialSize,
|
||||
}: ResizablePanelProps): JSX.Element {
|
||||
const [firstSize, setFirstSize] = useState<number>(initialSize);
|
||||
const [dividerPosition, setDividerPosition] = useState<number | null>(null);
|
||||
const firstRef = useRef<HTMLDivElement>(null);
|
||||
const secondRef = useRef<HTMLDivElement>(null);
|
||||
const [collapse, setCollapse] = useState<Collapse>(Collapse.SPLIT);
|
||||
const isHorizontal = orientation === Orientation.HORIZONTAL;
|
||||
|
||||
useEffect(() => {
|
||||
if (dividerPosition == null || !firstRef.current) {
|
||||
return undefined;
|
||||
}
|
||||
const getFirstSizeFromEvent = (e: MouseEvent) => {
|
||||
const position = isHorizontal ? e.clientX : e.clientY;
|
||||
return firstSize + position - dividerPosition;
|
||||
};
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const newFirstSize = `${getFirstSizeFromEvent(e)}px`;
|
||||
const { current } = firstRef;
|
||||
if (current) {
|
||||
if (isHorizontal) {
|
||||
current.style.width = newFirstSize;
|
||||
current.style.minWidth = newFirstSize;
|
||||
} else {
|
||||
current.style.height = newFirstSize;
|
||||
current.style.minHeight = newFirstSize;
|
||||
}
|
||||
}
|
||||
};
|
||||
const onMouseUp = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
if (firstRef.current) {
|
||||
firstRef.current.style.transition = "";
|
||||
}
|
||||
if (secondRef.current) {
|
||||
secondRef.current.style.transition = "";
|
||||
}
|
||||
setFirstSize(getFirstSizeFromEvent(e));
|
||||
setDividerPosition(null);
|
||||
document.removeEventListener("mousemove", onMouseMove);
|
||||
document.removeEventListener("mouseup", onMouseUp);
|
||||
};
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", onMouseMove);
|
||||
document.removeEventListener("mouseup", onMouseUp);
|
||||
};
|
||||
}, [dividerPosition, firstSize, orientation]);
|
||||
|
||||
const onMouseDown = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
if (firstRef.current) {
|
||||
firstRef.current.style.transition = "none";
|
||||
}
|
||||
if (secondRef.current) {
|
||||
secondRef.current.style.transition = "none";
|
||||
}
|
||||
const position = isHorizontal ? e.clientX : e.clientY;
|
||||
setDividerPosition(position);
|
||||
};
|
||||
|
||||
const getStyleForFirst = () => {
|
||||
const style: CSSProperties = { overflow: "hidden" };
|
||||
if (collapse === Collapse.COLLAPSED) {
|
||||
style.opacity = 0;
|
||||
style.width = 0;
|
||||
style.minWidth = 0;
|
||||
style.height = 0;
|
||||
style.minHeight = 0;
|
||||
} else if (collapse === Collapse.SPLIT) {
|
||||
const firstSizePx = `${firstSize}px`;
|
||||
if (isHorizontal) {
|
||||
style.width = firstSizePx;
|
||||
style.minWidth = firstSizePx;
|
||||
} else {
|
||||
style.height = firstSizePx;
|
||||
style.minHeight = firstSizePx;
|
||||
}
|
||||
} else {
|
||||
style.flexGrow = 1;
|
||||
}
|
||||
return style;
|
||||
};
|
||||
|
||||
const getStyleForSecond = () => {
|
||||
const style: CSSProperties = { overflow: "hidden" };
|
||||
if (collapse === Collapse.FILLED) {
|
||||
style.opacity = 0;
|
||||
style.width = 0;
|
||||
style.minWidth = 0;
|
||||
style.height = 0;
|
||||
style.minHeight = 0;
|
||||
} else if (collapse === Collapse.SPLIT) {
|
||||
style.flexGrow = 1;
|
||||
} else {
|
||||
style.flexGrow = 1;
|
||||
}
|
||||
return style;
|
||||
};
|
||||
|
||||
const onCollapse = () => {
|
||||
if (collapse === Collapse.SPLIT) {
|
||||
setCollapse(Collapse.COLLAPSED);
|
||||
} else {
|
||||
setCollapse(Collapse.SPLIT);
|
||||
}
|
||||
};
|
||||
|
||||
const onExpand = () => {
|
||||
if (collapse === Collapse.SPLIT) {
|
||||
setCollapse(Collapse.FILLED);
|
||||
} else {
|
||||
setCollapse(Collapse.SPLIT);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={twMerge("flex", !isHorizontal && "flex-col", className)}>
|
||||
<div
|
||||
ref={firstRef}
|
||||
className={twMerge(firstClassName, "transition-all ease-soft-spring")}
|
||||
style={getStyleForFirst()}
|
||||
>
|
||||
{firstChild}
|
||||
</div>
|
||||
<div
|
||||
className={`${isHorizontal ? "cursor-ew-resize w-3 flex-col" : "cursor-ns-resize h-3 flex-row"} shrink-0 flex justify-center items-center`}
|
||||
onMouseDown={collapse === Collapse.SPLIT ? onMouseDown : undefined}
|
||||
>
|
||||
<IconButton
|
||||
icon={isHorizontal ? <VscChevronLeft /> : <VscChevronUp />}
|
||||
ariaLabel="Collapse"
|
||||
onClick={onCollapse}
|
||||
/>
|
||||
<IconButton
|
||||
icon={isHorizontal ? <VscChevronRight /> : <VscChevronDown />}
|
||||
ariaLabel="Expand"
|
||||
onClick={onExpand}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={secondRef}
|
||||
className={twMerge(secondClassName, "transition-all ease-soft-spring")}
|
||||
style={getStyleForSecond()}
|
||||
>
|
||||
{secondChild}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
|
||||
import { TooltipButton } from "./tooltip-button";
|
||||
|
||||
interface AllHandsLogoButtonProps {
|
||||
onClick: () => void;
|
||||
@@ -6,8 +7,12 @@ interface AllHandsLogoButtonProps {
|
||||
|
||||
export function AllHandsLogoButton({ onClick }: AllHandsLogoButtonProps) {
|
||||
return (
|
||||
<button type="button" aria-label="All Hands Logo" onClick={onClick}>
|
||||
<TooltipButton
|
||||
tooltip="All Hands AI"
|
||||
ariaLabel="All Hands Logo"
|
||||
onClick={onClick}
|
||||
>
|
||||
<AllHandsLogo width={34} height={23} />
|
||||
</button>
|
||||
</TooltipButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import DocsIcon from "#/icons/docs.svg?react";
|
||||
import { TooltipButton } from "./tooltip-button";
|
||||
|
||||
export function DocsButton() {
|
||||
return (
|
||||
<a
|
||||
<TooltipButton
|
||||
tooltip="Documentation"
|
||||
ariaLabel="Documentation"
|
||||
href="https://docs.all-hands.dev"
|
||||
aria-label="Documentation"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="w-8 h-8 rounded-full hover:opacity-80 flex items-center justify-center"
|
||||
>
|
||||
<DocsIcon width={28} height={28} />
|
||||
</a>
|
||||
</TooltipButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import NewProjectIcon from "#/icons/new-project.svg?react";
|
||||
import { TooltipButton } from "./tooltip-button";
|
||||
|
||||
interface ExitProjectButtonProps {
|
||||
onClick: () => void;
|
||||
@@ -6,13 +7,13 @@ interface ExitProjectButtonProps {
|
||||
|
||||
export function ExitProjectButton({ onClick }: ExitProjectButtonProps) {
|
||||
return (
|
||||
<button
|
||||
data-testid="new-project-button"
|
||||
type="button"
|
||||
aria-label="Start new project"
|
||||
<TooltipButton
|
||||
tooltip="Start new project"
|
||||
ariaLabel="Start new project"
|
||||
onClick={onClick}
|
||||
testId="new-project-button"
|
||||
>
|
||||
<NewProjectIcon width={28} height={28} />
|
||||
</button>
|
||||
</TooltipButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Button } from "@nextui-org/react";
|
||||
import React, { MouseEventHandler, ReactElement } from "react";
|
||||
import React, { ReactElement } from "react";
|
||||
|
||||
export interface IconButtonProps {
|
||||
icon: ReactElement;
|
||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||
onClick: () => void;
|
||||
ariaLabel: string;
|
||||
testId?: string;
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export function IconButton({
|
||||
<Button
|
||||
type="button"
|
||||
variant="flat"
|
||||
onClick={onClick}
|
||||
onPress={onClick}
|
||||
className="cursor-pointer text-[12px] bg-transparent aspect-square px-0 min-w-[20px] h-[20px]"
|
||||
aria-label={ariaLabel}
|
||||
data-testid={testId}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import CogTooth from "#/assets/cog-tooth";
|
||||
import { TooltipButton } from "./tooltip-button";
|
||||
|
||||
interface SettingsButtonProps {
|
||||
onClick: () => void;
|
||||
@@ -6,13 +7,13 @@ interface SettingsButtonProps {
|
||||
|
||||
export function SettingsButton({ onClick }: SettingsButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Settings"
|
||||
className="w-8 h-8 rounded-full hover:opacity-80 flex items-center justify-center"
|
||||
<TooltipButton
|
||||
testId="settings-button"
|
||||
tooltip="Settings"
|
||||
ariaLabel="Settings"
|
||||
onClick={onClick}
|
||||
>
|
||||
<CogTooth />
|
||||
</button>
|
||||
</TooltipButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { toggleSpeech } from "#/state/speech-slice";
|
||||
|
||||
export function ToggleSpeechButton() {
|
||||
const dispatch = useDispatch();
|
||||
const enabled = useSelector((state: RootState) => state.speech.enabled);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => dispatch(toggleSpeech())}
|
||||
className="button-base p-1 hover:bg-neutral-500"
|
||||
title={enabled ? "Disable speech" : "Enable speech"}
|
||||
>
|
||||
{/* Speaker icon - filled when enabled, outline when disabled */}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill={enabled ? "currentColor" : "none"}
|
||||
stroke="currentColor"
|
||||
width={15}
|
||||
height={15}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
52
frontend/src/components/shared/buttons/tooltip-button.tsx
Normal file
52
frontend/src/components/shared/buttons/tooltip-button.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Tooltip } from "@nextui-org/react";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface TooltipButtonProps {
|
||||
children: ReactNode;
|
||||
tooltip: string;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
ariaLabel: string;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
export function TooltipButton({
|
||||
children,
|
||||
tooltip,
|
||||
onClick,
|
||||
href,
|
||||
ariaLabel,
|
||||
testId,
|
||||
}: TooltipButtonProps) {
|
||||
const buttonContent = (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={ariaLabel}
|
||||
data-testid={testId}
|
||||
onClick={onClick}
|
||||
className="w-8 h-8 rounded-full hover:opacity-80 flex items-center justify-center"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
const content = href ? (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="w-8 h-8 rounded-full hover:opacity-80 flex items-center justify-center"
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
) : (
|
||||
buttonContent
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip content={tooltip} closeDelay={100}>
|
||||
{content}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import toast, { Toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface ErrorToastProps {
|
||||
id: Toast["id"];
|
||||
@@ -6,6 +8,8 @@ interface ErrorToastProps {
|
||||
}
|
||||
|
||||
export function ErrorToast({ id, error }: ErrorToastProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between w-full h-full">
|
||||
<span>{error}</span>
|
||||
@@ -14,7 +18,7 @@ export function ErrorToast({ id, error }: ErrorToastProps) {
|
||||
onClick={() => toast.dismiss(id)}
|
||||
className="bg-neutral-500 px-1 rounded h-full"
|
||||
>
|
||||
Close
|
||||
{t(I18nKey.ERROR_TOAST$CLOSE_BUTTON_LABEL)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
import { Input } from "@nextui-org/react";
|
||||
import { Input, Tooltip } from "@nextui-org/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaCheckCircle, FaExclamationCircle } from "react-icons/fa";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface APIKeyInputProps {
|
||||
isDisabled: boolean;
|
||||
defaultValue: string;
|
||||
isSet: boolean;
|
||||
}
|
||||
|
||||
export function APIKeyInput({ isDisabled, defaultValue }: APIKeyInputProps) {
|
||||
export function APIKeyInput({ isDisabled, isSet }: APIKeyInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<fieldset data-testid="api-key-input" className="flex flex-col gap-2">
|
||||
<label htmlFor="api-key" className="font-[500] text-[#A3A3A3] text-xs">
|
||||
{t(I18nKey.SETTINGS_FORM$API_KEY_LABEL)}
|
||||
</label>
|
||||
<Tooltip content={isSet ? "API Key is set" : "API Key is not set"}>
|
||||
<label
|
||||
htmlFor="api-key"
|
||||
className="font-[500] text-[#A3A3A3] text-xs flex items-center gap-1 self-start"
|
||||
>
|
||||
{isSet && <FaCheckCircle className="text-[#00D1B2] inline-block" />}
|
||||
{!isSet && (
|
||||
<FaExclamationCircle className="text-[#FF3860] inline-block" />
|
||||
)}
|
||||
{t(I18nKey.SETTINGS_FORM$API_KEY_LABEL)}
|
||||
</label>
|
||||
</Tooltip>
|
||||
<Input
|
||||
isDisabled={isDisabled}
|
||||
id="api-key"
|
||||
name="api-key"
|
||||
aria-label="API Key"
|
||||
type="password"
|
||||
defaultValue={defaultValue}
|
||||
defaultValue=""
|
||||
classNames={{
|
||||
inputWrapper: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]",
|
||||
}}
|
||||
|
||||
@@ -8,12 +8,12 @@ import { ModalBody } from "../modal-body";
|
||||
import { AvailableLanguages } from "#/i18n";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { useSettings } from "#/context/settings-context";
|
||||
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
|
||||
import { ModalButton } from "../../buttons/modal-button";
|
||||
import { CustomInput } from "../../custom-input";
|
||||
import { FormFieldset } from "../../form-fieldset";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
|
||||
|
||||
interface AccountSettingsFormProps {
|
||||
onClose: () => void;
|
||||
@@ -30,7 +30,7 @@ export function AccountSettingsForm({
|
||||
}: AccountSettingsFormProps) {
|
||||
const { gitHubToken, setGitHubToken, logout } = useAuth();
|
||||
const { data: config } = useConfig();
|
||||
const { saveSettings } = useSettings();
|
||||
const { mutate: saveSettings } = useSaveSettings();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSettings } from "#/context/settings-context";
|
||||
import { useGitHubUser } from "#/hooks/query/use-github-user";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { ModalBackdrop } from "../modal-backdrop";
|
||||
import { AccountSettingsForm } from "./account-settings-form";
|
||||
|
||||
@@ -9,7 +9,7 @@ interface AccountSettingsModalProps {
|
||||
|
||||
export function AccountSettingsModal({ onClose }: AccountSettingsModalProps) {
|
||||
const user = useGitHubUser();
|
||||
const { settings } = useSettings();
|
||||
const { data: settings } = useSettings();
|
||||
|
||||
// FIXME: Bad practice to use localStorage directly
|
||||
const analyticsConsent = localStorage.getItem("analytics-consent");
|
||||
|
||||
@@ -23,7 +23,7 @@ export function FooterContent({ actions, closeModal }: FooterContentProps) {
|
||||
key={label}
|
||||
type="button"
|
||||
isDisabled={isDisabled}
|
||||
onClick={() => {
|
||||
onPress={() => {
|
||||
action();
|
||||
if (closeAfterAction) closeModal();
|
||||
}}
|
||||
|
||||
@@ -35,14 +35,20 @@ export function BaseModalDescription({
|
||||
}
|
||||
|
||||
interface BaseModalProps {
|
||||
testId?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
buttons: ButtonConfig[];
|
||||
}
|
||||
|
||||
export function BaseModal({ title, description, buttons }: BaseModalProps) {
|
||||
export function BaseModal({
|
||||
testId,
|
||||
title,
|
||||
description,
|
||||
buttons,
|
||||
}: BaseModalProps) {
|
||||
return (
|
||||
<ModalBody>
|
||||
<ModalBody testID={testId}>
|
||||
<div className="flex flex-col gap-2 self-start">
|
||||
<BaseModalTitle title={title} />
|
||||
<BaseModalDescription description={description} />
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { BaseModal } from "./base-modal";
|
||||
|
||||
interface DangerModalProps {
|
||||
testId?: string;
|
||||
|
||||
title: string;
|
||||
description: string;
|
||||
|
||||
@@ -10,9 +12,15 @@ interface DangerModalProps {
|
||||
};
|
||||
}
|
||||
|
||||
export function DangerModal({ title, description, buttons }: DangerModalProps) {
|
||||
export function DangerModal({
|
||||
testId,
|
||||
title,
|
||||
description,
|
||||
buttons,
|
||||
}: DangerModalProps) {
|
||||
return (
|
||||
<BaseModal
|
||||
testId={testId}
|
||||
title={title}
|
||||
description={description}
|
||||
buttons={[
|
||||
|
||||
@@ -127,7 +127,7 @@ function SecurityInvariant() {
|
||||
<>
|
||||
<div className="flex justify-between items-center border-b border-neutral-600 mb-4 p-4">
|
||||
<h2 className="text-2xl">{t(I18nKey.INVARIANT$LOG_LABEL)}</h2>
|
||||
<Button onClick={() => exportTraces()} className="bg-neutral-700">
|
||||
<Button onPress={() => exportTraces()} className="bg-neutral-700">
|
||||
{t(I18nKey.INVARIANT$EXPORT_TRACE_LABEL)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -162,7 +162,7 @@ function SecurityInvariant() {
|
||||
<h2 className="text-2xl">{t(I18nKey.INVARIANT$POLICY_LABEL)}</h2>
|
||||
<Button
|
||||
className="bg-neutral-700"
|
||||
onClick={() => updatePolicy({ policy })}
|
||||
onPress={() => updatePolicy({ policy })}
|
||||
>
|
||||
{t(I18nKey.INVARIANT$UPDATE_POLICY_LABEL)}
|
||||
</Button>
|
||||
@@ -184,7 +184,7 @@ function SecurityInvariant() {
|
||||
<h2 className="text-2xl">{t(I18nKey.INVARIANT$SETTINGS_LABEL)}</h2>
|
||||
<Button
|
||||
className="bg-neutral-700"
|
||||
onClick={() => updateRiskSeverity({ riskSeverity: selectedRisk })}
|
||||
onPress={() => updateRiskSeverity({ riskSeverity: selectedRisk })}
|
||||
>
|
||||
{t(I18nKey.INVARIANT$UPDATE_SETTINGS_LABEL)}
|
||||
</Button>
|
||||
|
||||
@@ -68,6 +68,7 @@ export function ModelSelector({
|
||||
LLM Provider
|
||||
</label>
|
||||
<Autocomplete
|
||||
data-testid="llm-provider"
|
||||
isRequired
|
||||
isVirtualized={false}
|
||||
name="llm-provider"
|
||||
@@ -91,7 +92,11 @@ export function ModelSelector({
|
||||
{Object.keys(models)
|
||||
.filter((provider) => VERIFIED_PROVIDERS.includes(provider))
|
||||
.map((provider) => (
|
||||
<AutocompleteItem key={provider} value={provider}>
|
||||
<AutocompleteItem
|
||||
data-testid={`provider-item-${provider}`}
|
||||
key={provider}
|
||||
value={provider}
|
||||
>
|
||||
{mapProvider(provider)}
|
||||
</AutocompleteItem>
|
||||
))}
|
||||
@@ -113,6 +118,7 @@ export function ModelSelector({
|
||||
LLM Model
|
||||
</label>
|
||||
<Autocomplete
|
||||
data-testid="llm-model"
|
||||
isRequired
|
||||
isVirtualized={false}
|
||||
name="llm-model"
|
||||
@@ -144,7 +150,11 @@ export function ModelSelector({
|
||||
{models[selectedProvider || ""]?.models
|
||||
.filter((model) => !VERIFIED_MODELS.includes(model))
|
||||
.map((model) => (
|
||||
<AutocompleteItem key={model} value={model}>
|
||||
<AutocompleteItem
|
||||
data-testid={`model-item-${model}`}
|
||||
key={model}
|
||||
value={model}
|
||||
>
|
||||
{model}
|
||||
</AutocompleteItem>
|
||||
))}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { DangerModal } from "../confirmation-modals/danger-modal";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { extractSettings, saveSettingsView } from "#/utils/settings-utils";
|
||||
import { useEndSession } from "#/hooks/use-end-session";
|
||||
import { useSettings } from "#/context/settings-context";
|
||||
import { ModalButton } from "../../buttons/modal-button";
|
||||
import { AdvancedOptionSwitch } from "../../inputs/advanced-option-switch";
|
||||
import { AgentInput } from "../../inputs/agent-input";
|
||||
@@ -20,6 +19,7 @@ import { CustomModelInput } from "../../inputs/custom-model-input";
|
||||
import { SecurityAnalyzerInput } from "../../inputs/security-analyzers-input";
|
||||
import { ModalBackdrop } from "../modal-backdrop";
|
||||
import { ModelSelector } from "./model-selector";
|
||||
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
|
||||
|
||||
interface SettingsFormProps {
|
||||
disabled?: boolean;
|
||||
@@ -38,7 +38,7 @@ export function SettingsForm({
|
||||
securityAnalyzers,
|
||||
onClose,
|
||||
}: SettingsFormProps) {
|
||||
const { saveSettings } = useSettings();
|
||||
const { mutateAsync: saveSettings } = useSaveSettings();
|
||||
const endSession = useEndSession();
|
||||
|
||||
const location = useLocation();
|
||||
@@ -82,7 +82,6 @@ export function SettingsForm({
|
||||
const resetOngoingSession = () => {
|
||||
if (location.pathname.startsWith("/conversations/")) {
|
||||
endSession();
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -92,7 +91,7 @@ export function SettingsForm({
|
||||
const newSettings = extractSettings(formData);
|
||||
|
||||
saveSettingsView(isUsingAdvancedOptions ? "advanced" : "basic");
|
||||
await saveSettings(newSettings);
|
||||
await saveSettings(newSettings, { onSuccess: onClose });
|
||||
resetOngoingSession();
|
||||
|
||||
posthog.capture("settings_saved", {
|
||||
@@ -102,11 +101,9 @@ export function SettingsForm({
|
||||
};
|
||||
|
||||
const handleConfirmResetSettings = async () => {
|
||||
await saveSettings(getDefaultSettings());
|
||||
await saveSettings(getDefaultSettings(), { onSuccess: onClose });
|
||||
resetOngoingSession();
|
||||
posthog.capture("settings_reset");
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleConfirmEndSession = () => {
|
||||
@@ -122,7 +119,6 @@ export function SettingsForm({
|
||||
setConfirmEndSessionModalOpen(true);
|
||||
} else {
|
||||
handleFormSubmission(formData);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -165,7 +161,7 @@ export function SettingsForm({
|
||||
|
||||
<APIKeyInput
|
||||
isDisabled={!!disabled}
|
||||
defaultValue={settings.LLM_API_KEY || ""}
|
||||
isSet={settings.LLM_API_KEY === "SET"}
|
||||
/>
|
||||
|
||||
{showAdvancedOptions && (
|
||||
@@ -221,6 +217,7 @@ export function SettingsForm({
|
||||
{confirmResetDefaultsModalOpen && (
|
||||
<ModalBackdrop>
|
||||
<DangerModal
|
||||
testId="reset-defaults-modal"
|
||||
title={t(I18nKey.SETTINGS_FORM$ARE_YOU_SURE_LABEL)}
|
||||
description={t(
|
||||
I18nKey.SETTINGS_FORM$ALL_INFORMATION_WILL_BE_DELETED_MESSAGE,
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { useSettings } from "#/context/settings-context";
|
||||
import { useAIConfigOptions } from "#/hooks/query/use-ai-config-options";
|
||||
import { Settings } from "#/services/settings";
|
||||
import { LoadingSpinner } from "../../loading-spinner";
|
||||
import { ModalBackdrop } from "../modal-backdrop";
|
||||
import { SettingsForm } from "./settings-form";
|
||||
|
||||
interface SettingsModalProps {
|
||||
settings: Settings;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
const { settings } = useSettings();
|
||||
export function SettingsModal({ onClose, settings }: SettingsModalProps) {
|
||||
const aiConfigOptions = useAIConfigOptions();
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import React from "react";
|
||||
import { useNavigate, useNavigation } from "react-router";
|
||||
import { useNavigation } from "react-router";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import posthog from "posthog-js";
|
||||
import { RootState } from "#/store";
|
||||
import {
|
||||
addFile,
|
||||
removeFile,
|
||||
setInitialQuery,
|
||||
} from "#/state/initial-query-slice";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
import { addFile, removeFile } from "#/state/initial-query-slice";
|
||||
import { SuggestionBubble } from "#/components/features/suggestions/suggestion-bubble";
|
||||
import { SUGGESTIONS } from "#/utils/suggestions";
|
||||
import { convertImageToBase64 } from "#/utils/convert-image-to-base-64";
|
||||
@@ -21,6 +12,7 @@ import { cn } from "#/utils/utils";
|
||||
import { AttachImageLabel } from "../features/images/attach-image-label";
|
||||
import { ImageCarousel } from "../features/images/image-carousel";
|
||||
import { UploadImageInput } from "../features/images/upload-image-input";
|
||||
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
|
||||
import { LoadingSpinner } from "./loading-spinner";
|
||||
|
||||
interface TaskFormProps {
|
||||
@@ -30,8 +22,6 @@ interface TaskFormProps {
|
||||
export function TaskForm({ ref }: TaskFormProps) {
|
||||
const dispatch = useDispatch();
|
||||
const navigation = useNavigation();
|
||||
const navigate = useNavigate();
|
||||
const { gitHubToken } = useAuth();
|
||||
|
||||
const { selectedRepository, files } = useSelector(
|
||||
(state: RootState) => state.initialQuery,
|
||||
@@ -42,24 +32,7 @@ export function TaskForm({ ref }: TaskFormProps) {
|
||||
getRandomKey(SUGGESTIONS["non-repo"]),
|
||||
);
|
||||
const [inputIsFocused, setInputIsFocused] = React.useState(false);
|
||||
const newConversationMutation = useMutation({
|
||||
mutationFn: (variables: { q?: string }) => {
|
||||
if (variables.q) dispatch(setInitialQuery(variables.q));
|
||||
return OpenHands.newConversation({
|
||||
githubToken: gitHubToken || undefined,
|
||||
selectedRepository: selectedRepository || undefined,
|
||||
});
|
||||
},
|
||||
onSuccess: ({ conversation_id: conversationId }, { q }) => {
|
||||
posthog.capture("initial_query_submitted", {
|
||||
entry_point: "task_form",
|
||||
query_character_length: q?.length,
|
||||
has_repository: !!selectedRepository,
|
||||
has_files: files.length > 0,
|
||||
});
|
||||
navigate(`/conversations/${conversationId}`);
|
||||
},
|
||||
});
|
||||
const { mutate: createConversation, isPending } = useCreateConversation();
|
||||
|
||||
const onRefreshSuggestion = () => {
|
||||
const suggestions = SUGGESTIONS["non-repo"];
|
||||
@@ -90,9 +63,7 @@ export function TaskForm({ ref }: TaskFormProps) {
|
||||
const formData = new FormData(event.currentTarget);
|
||||
|
||||
const q = formData.get("q")?.toString();
|
||||
if (q?.trim()) {
|
||||
newConversationMutation.mutate({ q });
|
||||
}
|
||||
createConversation({ q });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -114,7 +85,7 @@ export function TaskForm({ ref }: TaskFormProps) {
|
||||
"hover:border-neutral-500 focus-within:border-neutral-500",
|
||||
)}
|
||||
>
|
||||
{newConversationMutation.isPending ? (
|
||||
{isPending ? (
|
||||
<div className="flex justify-center py-[17px]">
|
||||
<LoadingSpinner size="small" />
|
||||
</div>
|
||||
|
||||
@@ -102,7 +102,7 @@ function AuthProvider({ children }: React.PropsWithChildren) {
|
||||
[gitHubTokenState],
|
||||
);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
return <AuthContext value={value}>{children}</AuthContext>;
|
||||
}
|
||||
|
||||
function useAuth() {
|
||||
|
||||
@@ -24,11 +24,7 @@ export function ConversationProvider({
|
||||
|
||||
const value = useMemo(() => ({ conversationId }), [conversationId]);
|
||||
|
||||
return (
|
||||
<ConversationContext.Provider value={value}>
|
||||
{children}
|
||||
</ConversationContext.Provider>
|
||||
);
|
||||
return <ConversationContext value={value}>{children}</ConversationContext>;
|
||||
}
|
||||
|
||||
export function useConversation() {
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
getSettings,
|
||||
Settings,
|
||||
saveSettings,
|
||||
settingsAreUpToDate as checkIfSettingsAreUpToDate,
|
||||
DEFAULT_SETTINGS,
|
||||
} from "#/services/settings";
|
||||
|
||||
interface SettingsContextType {
|
||||
settings: Settings;
|
||||
settingsAreUpToDate: boolean;
|
||||
saveSettings: (settings: Partial<Settings>) => void;
|
||||
}
|
||||
|
||||
const SettingsContext = React.createContext<SettingsContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const SETTINGS_QUERY_KEY = ["settings"];
|
||||
|
||||
function SettingsProvider({ children }: React.PropsWithChildren) {
|
||||
const { data: settings } = useQuery({
|
||||
queryKey: SETTINGS_QUERY_KEY,
|
||||
queryFn: getSettings,
|
||||
initialData: DEFAULT_SETTINGS,
|
||||
});
|
||||
|
||||
const [settingsAreUpToDate, setSettingsAreUpToDate] = React.useState(
|
||||
checkIfSettingsAreUpToDate(),
|
||||
);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleSaveSettings = async (newSettings: Partial<Settings>) => {
|
||||
await saveSettings(newSettings);
|
||||
queryClient.invalidateQueries({ queryKey: SETTINGS_QUERY_KEY });
|
||||
setSettingsAreUpToDate(checkIfSettingsAreUpToDate());
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (settings?.LLM_API_KEY) {
|
||||
posthog.capture("user_activated");
|
||||
}
|
||||
}, [settings?.LLM_API_KEY]);
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
settings,
|
||||
settingsAreUpToDate,
|
||||
saveSettings: handleSaveSettings,
|
||||
}),
|
||||
[settings, settingsAreUpToDate],
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingsContext.Provider value={value}>
|
||||
{children}
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function useSettings() {
|
||||
const context = React.useContext(SettingsContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useSettings must be used within a SettingsProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export { SettingsProvider, useSettings };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user