mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1385d92447 | |||
| 74f5b24144 | |||
| e8d51a0878 | |||
| c25a96825c | |||
| 16137942eb | |||
| cf07b151dc | |||
| ea3787c2ba | |||
| 956d278138 | |||
| fa186fc433 | |||
| 4ef9c72da1 | |||
| c5245a622d | |||
| 9b1aaa53fe | |||
| 4deffa3907 | |||
| a47c6f3ed1 | |||
| 90ece3f8e1 | |||
| a948b0fef3 | |||
| 52848cd3db | |||
| 62f015370a | |||
| 7109b057b6 | |||
| e694fc2d58 | |||
| 50baf3fd18 | |||
| dc37f039bf | |||
| 91fff77ffe | |||
| db01cd39d5 | |||
| 00865fbb8a | |||
| 356cd9ff9f | |||
| 78d82408b7 | |||
| 3c4ebc3d8a | |||
| 964478c22f | |||
| 00c449d447 | |||
| bfd75a1355 | |||
| dc91cb263b | |||
| fa559ace86 | |||
| 1fd26d196a | |||
| 5de62d85fd | |||
| 5d749aeba7 | |||
| 693c72d670 | |||
| 62557d44f2 | |||
| 89f8e162da | |||
| b0a9938e6c | |||
| 039fe295a4 | |||
| 8f3ff1210e | |||
| 5e1e685493 | |||
| e9f2b72ea5 | |||
| 986b90be0a | |||
| bf9f2aa7a5 | |||
| b3bd3924a0 | |||
| 0de50153a0 | |||
| a04024a239 | |||
| 1e509a70d4 | |||
| 300a59853b | |||
| 2514b200c5 | |||
| 52d881c98d | |||
| 6ac23aea80 | |||
| 0412949018 | |||
| 5b5adc5c7b | |||
| 0b40f6fac8 | |||
| 44d488b718 | |||
| 4f9120ffc6 | |||
| 50426edaa1 | |||
| a792f84a83 | |||
| cd9d96766c | |||
| 14564b25d6 | |||
| 0637b5b912 | |||
| 20bf48b693 | |||
| 5b3270be2d | |||
| ae43744052 |
@@ -179,7 +179,7 @@ jobs:
|
||||
|
||||
echo "MAX_ITERATIONS=${{ inputs.max_iterations || 50 }}" >> $GITHUB_ENV
|
||||
echo "SANDBOX_ENV_GITHUB_TOKEN=${{ secrets.PAT_TOKEN || github.token }}" >> $GITHUB_ENV
|
||||
echo "SANDBOX_ENV_BASE_CONTAINER_IMAGE=${{ inputs.base_container_image }}" >> $GITHUB_ENV
|
||||
echo "SANDBOX_BASE_CONTAINER_IMAGE=${{ inputs.base_container_image }}" >> $GITHUB_ENV
|
||||
|
||||
# Set branch variables
|
||||
echo "TARGET_BRANCH=${{ inputs.target_branch || 'main' }}" >> $GITHUB_ENV
|
||||
|
||||
+1
-1
@@ -118,7 +118,7 @@ poetry run pytest ./tests/unit/test_*.py
|
||||
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker container image by
|
||||
setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
|
||||
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.33-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.34-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ ifeq ($(INSTALL_DOCKER),)
|
||||
@$(MAKE) -s check-docker
|
||||
endif
|
||||
@$(MAKE) -s check-poetry
|
||||
@$(MAKE) -s check-tmux
|
||||
@echo "$(GREEN)Dependencies checked successfully.$(RESET)"
|
||||
|
||||
check-system:
|
||||
@@ -101,6 +102,18 @@ check-docker:
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
check-tmux:
|
||||
@echo "$(YELLOW)Checking tmux installation...$(RESET)"
|
||||
@if command -v tmux > /dev/null; then \
|
||||
echo "$(BLUE)$(shell tmux -V) is already installed.$(RESET)"; \
|
||||
else \
|
||||
echo "$(YELLOW)╔════════════════════════════════════════════════════════════════════════════╗$(RESET)"; \
|
||||
echo "$(YELLOW)║ OPTIONAL: tmux is not installed. ║$(RESET)"; \
|
||||
echo "$(YELLOW)║ Some advanced terminal features may not work without tmux. ║$(RESET)"; \
|
||||
echo "$(YELLOW)║ You can install it if needed, but it's not required for development. ║$(RESET)"; \
|
||||
echo "$(YELLOW)╚════════════════════════════════════════════════════════════════════════════╝$(RESET)"; \
|
||||
fi
|
||||
|
||||
check-poetry:
|
||||
@echo "$(YELLOW)Checking Poetry installation...$(RESET)"
|
||||
@if command -v poetry > /dev/null; then \
|
||||
|
||||
@@ -52,17 +52,17 @@ system requirements and more information.
|
||||
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.33
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.34
|
||||
```
|
||||
|
||||
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
|
||||
|
||||
@@ -221,9 +221,22 @@ enable_browsing = true
|
||||
# Whether the LLM draft editor is enabled
|
||||
enable_llm_editor = false
|
||||
|
||||
# Whether the standard editor tool (str_replace_editor) is enabled
|
||||
# Only has an effect if enable_llm_editor is False
|
||||
enable_editor = true
|
||||
|
||||
# Whether the IPython tool is enabled
|
||||
enable_jupyter = true
|
||||
|
||||
# Whether the command tool is enabled
|
||||
enable_cmd = true
|
||||
|
||||
# Whether the think tool is enabled
|
||||
enable_think = true
|
||||
|
||||
# Whether the finish tool is enabled
|
||||
enable_finish = true
|
||||
|
||||
# LLM config group to use
|
||||
#llm_config = 'your-llm-config-group'
|
||||
|
||||
|
||||
@@ -61,8 +61,8 @@ RUN add-apt-repository ppa:deadsnakes/ppa \
|
||||
&& apt-get install -y python3.12 python3.12-venv python3.12-dev python3-pip \
|
||||
&& ln -s /usr/bin/python3.12 /usr/bin/python
|
||||
|
||||
# NodeJS >= 18.17.1
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||
# NodeJS >= 22.x
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
|
||||
# Poetry >= 1.8
|
||||
@@ -108,7 +108,7 @@ WORKDIR /app
|
||||
|
||||
# cache build dependencies
|
||||
RUN \
|
||||
--mount=type=bind,source=./,target=/app/ \
|
||||
--mount=type=bind,source=./,target=/app/,rw \
|
||||
<<EOF
|
||||
#!/bin/bash
|
||||
make -s clean
|
||||
|
||||
@@ -11,7 +11,7 @@ services:
|
||||
- BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
|
||||
- SANDBOX_API_HOSTNAME=host.docker.internal
|
||||
#
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.33-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.34-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ services:
|
||||
image: openhands:latest
|
||||
container_name: openhands-app-${DATE:-}
|
||||
environment:
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik}
|
||||
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of openhands-state for this user
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
# Production
|
||||
/build
|
||||
/static/swagger-ui
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
|
||||
@@ -36,6 +36,7 @@ const config: Config = {
|
||||
mermaid: true,
|
||||
},
|
||||
themes: ['@docusaurus/theme-mermaid'],
|
||||
plugins: [],
|
||||
presets: [
|
||||
[
|
||||
'classic',
|
||||
@@ -75,6 +76,11 @@ const config: Config = {
|
||||
position: 'left',
|
||||
label: 'User Guides',
|
||||
},
|
||||
{
|
||||
href: 'https://docs.all-hands.dev/swagger-ui/', // FIXME: this should be a relative path, but docusarus steals the click
|
||||
label: 'API',
|
||||
position: 'left',
|
||||
},
|
||||
{
|
||||
type: 'localeDropdown',
|
||||
position: 'left',
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const swaggerUiDist = require('swagger-ui-dist');
|
||||
|
||||
/**
|
||||
* This script manually sets up Swagger UI for the Docusaurus documentation.
|
||||
*
|
||||
* Why we need this approach:
|
||||
* 1. Docusaurus doesn't have a built-in way to integrate Swagger UI
|
||||
* 2. We need to copy the necessary files from swagger-ui-dist to our static directory
|
||||
* 3. We need to create a custom index.html file that points to our OpenAPI spec
|
||||
* 4. This approach allows us to customize the Swagger UI to match our documentation style
|
||||
*/
|
||||
|
||||
// Get the absolute path to the swagger-ui-dist package
|
||||
const swaggerUiDistPath = swaggerUiDist.getAbsoluteFSPath();
|
||||
|
||||
// Create the target directory if it doesn't exist
|
||||
const targetDir = path.join(__dirname, 'static', 'swagger-ui');
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Copy all files from swagger-ui-dist to our target directory
|
||||
const files = fs.readdirSync(swaggerUiDistPath);
|
||||
files.forEach(file => {
|
||||
const sourcePath = path.join(swaggerUiDistPath, file);
|
||||
const targetPath = path.join(targetDir, file);
|
||||
|
||||
// Skip directories and non-essential files
|
||||
if (fs.statSync(sourcePath).isDirectory() ||
|
||||
file === 'package.json' ||
|
||||
file === 'README.md' ||
|
||||
file.endsWith('.map')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.copyFileSync(sourcePath, targetPath);
|
||||
});
|
||||
|
||||
// Create a custom index.html file that points to our OpenAPI spec
|
||||
const indexHtml = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OpenHands API Documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
|
||||
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
|
||||
<style>
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
|
||||
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
|
||||
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
// Begin Swagger UI call region
|
||||
const ui = SwaggerUIBundle({
|
||||
url: "/openapi.json",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "StandaloneLayout"
|
||||
});
|
||||
// End Swagger UI call region
|
||||
window.ui = ui;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(targetDir, 'index.html'), indexHtml);
|
||||
|
||||
console.log('Swagger UI files generated successfully in static/swagger-ui/');
|
||||
@@ -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.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-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.33 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
|
||||
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.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-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.33 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
|
||||
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.33-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-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.33
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.34
|
||||
```
|
||||
|
||||
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.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -34,7 +34,7 @@ Docker で OpenHands を CLI モードで実行するには:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -44,7 +44,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--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.33 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ DockerでOpenHandsをヘッドレスモードで実行するには:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -42,7 +42,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--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.33 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ nikolaik の `SANDBOX_RUNTIME_CONTAINER_IMAGE` は、ランタイムサーバー
|
||||
|
||||
```bash
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||
@@ -82,5 +82,5 @@ docker network create openhands-network
|
||||
# 分離されたネットワークで OpenHands を実行
|
||||
docker run # ... \
|
||||
--network openhands-network \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.33
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.34
|
||||
```
|
||||
|
||||
@@ -35,7 +35,7 @@ Para executar o OpenHands no modo CLI com Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -45,7 +45,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--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.33 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
+2
-2
@@ -32,7 +32,7 @@ Para executar o OpenHands no modo Headless com Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -43,7 +43,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--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.33 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
|
||||
python -m openhands.core.main -t "escreva um script bash que imprima oi"
|
||||
```
|
||||
|
||||
|
||||
@@ -58,17 +58,17 @@
|
||||
A maneira mais fácil de executar o OpenHands é no Docker.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.33
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.34
|
||||
```
|
||||
|
||||
Você encontrará o OpenHands em execução em http://localhost:3000!
|
||||
|
||||
@@ -13,7 +13,7 @@ Este é o Runtime padrão que é usado quando você inicia o OpenHands. Você po
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-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.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-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.33 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
+2
-2
@@ -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.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-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.33 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
|
||||
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.33-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-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.33
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.34
|
||||
```
|
||||
|
||||
你也可以在可脚本化的[无头模式](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.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -8,18 +8,22 @@ OpenHands Cloud can be accessed at https://app.all-hands.dev/.
|
||||
|
||||
## Getting Started
|
||||
|
||||
After visiting OpenHands Cloud, you will be asked to connect with your GitHub account:
|
||||
1. After reading and accepting the terms of service, click `Connect to GitHub`.
|
||||
After visiting OpenHands Cloud, you will be asked to connect with your GitHub or GitLab account:
|
||||
|
||||
1. After reading and accepting the terms of service, click `Log in with GitHub` or `Log in with GitLab`.
|
||||
2. Review the permissions requested by OpenHands and then click `Authorize OpenHands AI`.
|
||||
- OpenHands will require some permissions from your GitHub account. To read more about these permissions,
|
||||
you can click the `Learn more` link on the GitHub authorize page.
|
||||
- OpenHands will require some permissions from your GitHub or GitLab account. To read more about these permissions:
|
||||
- GitHub: You can click the `Learn more` link on the GitHub authorize page.
|
||||
- GitLab: You can expand each permission request on the GitLab authorize page.
|
||||
|
||||
## Repository Access
|
||||
|
||||
### Adding Repository Access
|
||||
### GitHub
|
||||
|
||||
#### Adding Repository Access
|
||||
|
||||
You can grant OpenHands specific repository access:
|
||||
1. Click the `Select a Git project` dropdown, select `Add more repositories...`.
|
||||
1. Click `Add GitHub repos` on the Home page.
|
||||
2. Select the organization, then choose the specific repositories to grant OpenHands access to.
|
||||
<details>
|
||||
<summary>Permission Details for Repository Access</summary>
|
||||
@@ -42,11 +46,15 @@ You can grant OpenHands specific repository access:
|
||||
|
||||
3. Click on `Install & Authorize`.
|
||||
|
||||
### Modifying Repository Access
|
||||
#### Modifying Repository Access
|
||||
|
||||
You can modify repository access at any time by:
|
||||
* Using the same `Select a Git project > Add more repositories` workflow, or
|
||||
* Visiting the Settings page and selecting `Configure GitHub Repositories` under the `GitHub Settings` section.
|
||||
You can modify GitHub repository access at any time by:
|
||||
* Using the same `Add GitHub repos` workflow, or
|
||||
* Visiting the Settings page and selecting `Configure GitHub Repositories` under the `Git Settings` section.
|
||||
|
||||
### GitLab
|
||||
|
||||
When using your GitLab account, OpenHands will automatically have access to your repositories.
|
||||
|
||||
## Conversation Persistence
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ To run OpenHands in CLI mode with Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -45,7 +45,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--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.33 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ To run OpenHands in Headless mode with Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -43,7 +43,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--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.33 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
|
||||
@@ -58,17 +58,17 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
|
||||
The easiest way to run OpenHands is in Docker.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.33
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.34
|
||||
```
|
||||
|
||||
You'll find OpenHands running at http://localhost:3000!
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
- Displays the conversation between the user and OpenHands.
|
||||
- OpenHands explains its actions in this panel.
|
||||
|
||||
### Changes
|
||||
- Shows the file changes performed by OpenHands.
|
||||
|
||||
### Workspace
|
||||
- Browse project files and directories.
|
||||
- Use the `Open in VS Code` option to:
|
||||
@@ -20,7 +23,7 @@
|
||||
- Particularly handy when using OpenHands to perform data visualization tasks.
|
||||
|
||||
### App
|
||||
- Shows the web server when OpenHands runs an application.
|
||||
- Displays the web server when OpenHands runs an application.
|
||||
- Users can interact with the running application.
|
||||
|
||||
### Browser
|
||||
|
||||
@@ -15,7 +15,7 @@ It is highly recommended that you use GPUs to serve local models for optimal exp
|
||||
For example, to download [OpenHands LM 32B v0.1](https://huggingface.co/all-hands/openhands-lm-32b-v0.1):
|
||||
|
||||
```bash
|
||||
huggingface-cli download all-hands/openhands-lm-32b-v0.1 --local-dir my_folder/openhands-lm-32b-v0.1
|
||||
huggingface-cli download all-hands/openhands-lm-32b-v0.1 --local-dir all-hands/openhands-lm-32b-v0.1
|
||||
```
|
||||
|
||||
## Create an OpenAI-Compatible Endpoint With a Model Serving Framework
|
||||
@@ -27,7 +27,7 @@ huggingface-cli download all-hands/openhands-lm-32b-v0.1 --local-dir my_folder/o
|
||||
|
||||
```bash
|
||||
SGLANG_ALLOW_OVERWRITE_LONGER_CONTEXT_LEN=1 python3 -m sglang.launch_server \
|
||||
--model my_folder/openhands-lm-32b-v0.1 \
|
||||
--model all-hands/openhands-lm-32b-v0.1 \
|
||||
--served-model-name openhands-lm-32b-v0.1 \
|
||||
--port 8000 \
|
||||
--tp 2 --dp 1 \
|
||||
@@ -41,7 +41,7 @@ SGLANG_ALLOW_OVERWRITE_LONGER_CONTEXT_LEN=1 python3 -m sglang.launch_server \
|
||||
- Example launch command for OpenHands LM 32B (with at least 2 GPUs):
|
||||
|
||||
```bash
|
||||
vllm serve my_folder/openhands-lm-32b-v0.1 \
|
||||
vllm serve all-hands/openhands-lm-32b-v0.1 \
|
||||
--host 0.0.0.0 --port 8000 \
|
||||
--api-key mykey \
|
||||
--tensor-parallel-size 2 \
|
||||
@@ -67,7 +67,7 @@ Ensure `config.toml` exists by running `make setup-config` which will create one
|
||||
workspace_base="/path/to/your/workspace"
|
||||
|
||||
[llm]
|
||||
embedding_model="local"
|
||||
model="openhands-lm-32b-v0.1"
|
||||
ollama_base_url="http://localhost:8000"
|
||||
```
|
||||
|
||||
|
||||
Generated
+414
-19
@@ -24,6 +24,8 @@
|
||||
"@docusaurus/module-type-aliases": "^3.5.1",
|
||||
"@docusaurus/tsconfig": "^3.7.0",
|
||||
"@docusaurus/types": "^3.5.1",
|
||||
"swagger-cli": "^4.0.4",
|
||||
"swagger-ui-dist": "^5.21.0",
|
||||
"typescript": "~5.8.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -273,6 +275,273 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/openapi-schemas": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
|
||||
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-cli/-/swagger-cli-4.0.4.tgz",
|
||||
"integrity": "sha512-hdDT3B6GLVovCsRZYDi3+wMcB1HfetTU20l2DC8zD3iFRNMC6QNAZG5fo/6PYeHWBEv7ri4MvnlKodhNB0nt7g==",
|
||||
"deprecated": "This package has been abandoned. Please switch to using the actively maintained @redocly/cli",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.0.1",
|
||||
"chalk": "^4.1.0",
|
||||
"js-yaml": "^3.14.0",
|
||||
"yargs": "^15.4.1"
|
||||
},
|
||||
"bin": {
|
||||
"swagger-cli": "bin/swagger-cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-methods": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
|
||||
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-parser": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.1.tgz",
|
||||
"integrity": "sha512-u/kozRnsPO/x8QtKYJOqoGtC4kH6yg1lfYkB9Au0WhYB0FNLpyFusttQtvhlwjtG3rOwiRz4D8DnnXa8iEpIKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@apidevtools/json-schema-ref-parser": "11.7.2",
|
||||
"@apidevtools/openapi-schemas": "^2.1.0",
|
||||
"@apidevtools/swagger-methods": "^3.0.2",
|
||||
"@jsdevtools/ono": "^7.1.3",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-draft-04": "^1.0.0",
|
||||
"call-me-maybe": "^1.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openapi-types": ">=7"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-parser/node_modules/@apidevtools/json-schema-ref-parser": {
|
||||
"version": "11.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz",
|
||||
"integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jsdevtools/ono": "^7.1.3",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"js-yaml": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/philsturgeon"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.26.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
|
||||
@@ -3835,6 +4104,13 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@jsdevtools/ono": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
|
||||
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@leichtgewicht/ip-codec": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
|
||||
@@ -3970,6 +4246,14 @@
|
||||
"integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@scarf/scarf": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@sideway/address": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
|
||||
@@ -4967,6 +5251,21 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-draft-04": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
|
||||
"integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"ajv": "^8.5.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ajv": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
||||
@@ -5549,6 +5848,13 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/call-me-maybe": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
|
||||
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -7192,6 +7498,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decode-named-character-reference": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz",
|
||||
@@ -8600,6 +8916,16 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
|
||||
@@ -13110,15 +13436,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
@@ -13427,6 +13754,14 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/openapi-types": {
|
||||
"version": "12.1.3",
|
||||
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
|
||||
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/opener": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
|
||||
@@ -13809,9 +14144,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.38",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
|
||||
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
|
||||
"version": "8.4.49",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
|
||||
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -13826,10 +14161,11 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.2.0"
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
@@ -15367,6 +15703,15 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/pupa": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pupa/-/pupa-3.1.0.tgz",
|
||||
@@ -16205,6 +16550,16 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
@@ -16222,6 +16577,13 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
@@ -16702,6 +17064,13 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
@@ -16980,9 +17349,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -17347,6 +17717,32 @@
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
|
||||
},
|
||||
"node_modules/swagger-cli": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/swagger-cli/-/swagger-cli-4.0.4.tgz",
|
||||
"integrity": "sha512-Cp8YYuLny3RJFQ4CvOBTaqmOOgYsem52dPx1xM5S4EUWFblIh2Q8atppMZvXKUr1e9xH5RwipYpmdUzdPcxWcA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-cli": "4.0.4"
|
||||
},
|
||||
"bin": {
|
||||
"swagger-cli": "swagger-cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-ui-dist": {
|
||||
"version": "5.21.0",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.21.0.tgz",
|
||||
"integrity": "sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scarf/scarf": "=1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
||||
@@ -17949,14 +18345,6 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js/node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/url-loader": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz",
|
||||
@@ -18610,6 +18998,13 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/widest-line": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz",
|
||||
|
||||
+9
-4
@@ -4,16 +4,18 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"build": "docusaurus build",
|
||||
"start": "node generate-swagger-ui.js && docusaurus start",
|
||||
"build": "node generate-swagger-ui.js && docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
"serve": "docusaurus serve",
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids",
|
||||
"typecheck": "tsc"
|
||||
"typecheck": "tsc",
|
||||
"generate-swagger-ui": "node generate-swagger-ui.js"
|
||||
},
|
||||
"// Note": "The OpenAPI spec is stored in docs/static/openapi.json so it's accessible at /openapi.json in the deployed site",
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^3.7.0",
|
||||
"@docusaurus/plugin-content-pages": "^3.7.0",
|
||||
@@ -31,6 +33,8 @@
|
||||
"@docusaurus/module-type-aliases": "^3.5.1",
|
||||
"@docusaurus/tsconfig": "^3.7.0",
|
||||
"@docusaurus/types": "^3.5.1",
|
||||
"swagger-cli": "^4.0.4",
|
||||
"swagger-ui-dist": "^5.21.0",
|
||||
"typescript": "~5.8.3"
|
||||
},
|
||||
"browserslist": {
|
||||
@@ -47,5 +51,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
+1
-1
@@ -268,4 +268,4 @@ const sidebars: SidebarsConfig = {
|
||||
],
|
||||
};
|
||||
|
||||
export default sidebars;
|
||||
export default sidebars;
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
# Static Files for OpenHands Documentation
|
||||
|
||||
This directory contains static files that are copied directly to the build output of the Docusaurus documentation.
|
||||
|
||||
## OpenAPI Specification
|
||||
|
||||
The `openapi.json` file in this directory is the OpenAPI specification for the OpenHands API. It is copied to the build output and is accessible at `/openapi.json` in the deployed site.
|
||||
|
||||
This file is used by the Swagger UI interface, which is accessible at `/swagger-ui/` in the deployed site.
|
||||
|
||||
## Why is the OpenAPI spec in the static directory?
|
||||
|
||||
The OpenAPI specification is placed in the static directory so that it's accessible at a predictable URL in the deployed site. This allows the Swagger UI to reference it directly.
|
||||
|
||||
We only need one copy of the OpenAPI spec file, which is this one in the static directory.
|
||||
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 120 KiB |
Vendored
+2085
File diff suppressed because it is too large
Load Diff
+450
-178
File diff suppressed because it is too large
Load Diff
@@ -65,7 +65,7 @@ def main(
|
||||
ci_mode = pred['metadata']['details'].get('mode', '') == 'swt-ci'
|
||||
try:
|
||||
git_diff = remove_setup_files(git_diff, pred['instance'], ci_mode)
|
||||
except:
|
||||
except: # noqa: E722
|
||||
_LOGGER.warning(
|
||||
'Warning: Invalid git diff found for instance %s',
|
||||
pred['instance_id'],
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
# Run frontend checks
|
||||
echo "Running frontend checks..."
|
||||
cd frontend
|
||||
npm run check-unlocalized-strings
|
||||
npx lint-staged
|
||||
|
||||
# Run backend pre-commit
|
||||
echo "Running backend pre-commit..."
|
||||
cd ..
|
||||
pre-commit run --files openhands/**/* evaluation/**/* tests/**/* --show-diff-on-failure --config ./dev_config/python/.pre-commit-config.yaml
|
||||
|
||||
@@ -45,7 +45,15 @@ describe("Empty state", () => {
|
||||
it("should render suggestions if empty", () => {
|
||||
const { store } = renderWithProviders(<ChatInterface />, {
|
||||
preloadedState: {
|
||||
chat: { messages: [] },
|
||||
chat: {
|
||||
messages: [],
|
||||
systemMessage: {
|
||||
content: "",
|
||||
tools: [],
|
||||
openhands_version: null,
|
||||
agent_class: null
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -68,7 +76,15 @@ describe("Empty state", () => {
|
||||
it("should render the default suggestions", () => {
|
||||
renderWithProviders(<ChatInterface />, {
|
||||
preloadedState: {
|
||||
chat: { messages: [] },
|
||||
chat: {
|
||||
messages: [],
|
||||
systemMessage: {
|
||||
content: "",
|
||||
tools: [],
|
||||
openhands_version: null,
|
||||
agent_class: null
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -98,7 +114,15 @@ describe("Empty state", () => {
|
||||
const user = userEvent.setup();
|
||||
const { store } = renderWithProviders(<ChatInterface />, {
|
||||
preloadedState: {
|
||||
chat: { messages: [] },
|
||||
chat: {
|
||||
messages: [],
|
||||
systemMessage: {
|
||||
content: "",
|
||||
tools: [],
|
||||
openhands_version: null,
|
||||
agent_class: null
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -127,7 +151,15 @@ describe("Empty state", () => {
|
||||
const user = userEvent.setup();
|
||||
const { rerender } = renderWithProviders(<ChatInterface />, {
|
||||
preloadedState: {
|
||||
chat: { messages: [] },
|
||||
chat: {
|
||||
messages: [],
|
||||
systemMessage: {
|
||||
content: "",
|
||||
tools: [],
|
||||
openhands_version: null,
|
||||
agent_class: null
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -95,6 +95,23 @@ describe("ExpandableMessage", () => {
|
||||
expect(screen.queryByTestId("status-icon")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render with neutral border and no icon for action messages with undefined success (timeout case)", () => {
|
||||
renderWithProviders(
|
||||
<ExpandableMessage
|
||||
id="OBSERVATION_MESSAGE$RUN"
|
||||
message="Command timed out"
|
||||
type="action"
|
||||
success={undefined}
|
||||
/>,
|
||||
);
|
||||
const element = screen.getByText("OBSERVATION_MESSAGE$RUN");
|
||||
const container = element.closest(
|
||||
"div.flex.gap-2.items-center.justify-start",
|
||||
);
|
||||
expect(container).toHaveClass("border-neutral-300");
|
||||
expect(screen.queryByTestId("status-icon")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render the out of credits message when the user is out of credits", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
// @ts-expect-error - We only care about the APP_MODE and FEATURE_FLAGS fields
|
||||
|
||||
@@ -56,12 +56,16 @@ describe("GitRepositorySelector", () => {
|
||||
full_name: "test/repo1",
|
||||
git_provider: "github" as Provider,
|
||||
stargazers_count: 100,
|
||||
is_public: true,
|
||||
pushed_at: "2023-01-01T00:00:00Z",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
full_name: "test/repo2",
|
||||
git_provider: "github" as Provider,
|
||||
stargazers_count: 200,
|
||||
is_public: true,
|
||||
pushed_at: "2023-01-02T00:00:00Z",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { Provider } from "react-redux";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { setupStore } from "test-utils";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { HomeHeader } from "#/components/features/home/home-header";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
const renderHomeHeader = () => {
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
Component: HomeHeader,
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
Component: () => <div data-testid="conversation-screen" />,
|
||||
path: "/conversations/:conversationId",
|
||||
},
|
||||
]);
|
||||
|
||||
return render(<RouterStub />, {
|
||||
wrapper: ({ children }) => (
|
||||
<Provider store={setupStore()}>
|
||||
<AuthProvider initialProvidersAreSet>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
</Provider>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
describe("HomeHeader", () => {
|
||||
it("should create an empty conversation and redirect when pressing the launch from scratch button", async () => {
|
||||
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
|
||||
|
||||
renderHomeHeader();
|
||||
|
||||
const launchButton = screen.getByRole("button", {
|
||||
name: /launch from scratch/i,
|
||||
});
|
||||
await userEvent.click(launchButton);
|
||||
|
||||
expect(createConversationSpy).toHaveBeenCalledExactlyOnceWith(
|
||||
undefined,
|
||||
undefined,
|
||||
[],
|
||||
undefined,
|
||||
);
|
||||
|
||||
// expect to be redirected to /conversations/:conversationId
|
||||
await screen.findByTestId("conversation-screen");
|
||||
});
|
||||
|
||||
it("should change the launch button text to 'Loading...' when creating a conversation", async () => {
|
||||
renderHomeHeader();
|
||||
|
||||
const launchButton = screen.getByRole("button", {
|
||||
name: /launch from scratch/i,
|
||||
});
|
||||
await userEvent.click(launchButton);
|
||||
|
||||
expect(launchButton).toHaveTextContent(/Loading/i);
|
||||
expect(launchButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,229 @@
|
||||
import { render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
||||
import { setupStore } from "test-utils";
|
||||
import { Provider } from "react-redux";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import * as GitService from "#/api/git";
|
||||
import { RepoConnector } from "#/components/features/home/repo-connector";
|
||||
|
||||
const renderRepoConnector = (initialProvidersAreSet = true) => {
|
||||
const mockRepoSelection = vi.fn();
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
Component: () => <RepoConnector onRepoSelection={mockRepoSelection} />,
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
Component: () => <div data-testid="conversation-screen" />,
|
||||
path: "/conversations/:conversationId",
|
||||
},
|
||||
{
|
||||
Component: () => <div data-testid="settings-screen" />,
|
||||
path: "/settings",
|
||||
},
|
||||
]);
|
||||
|
||||
return render(<RouterStub />, {
|
||||
wrapper: ({ children }) => (
|
||||
<Provider store={setupStore()}>
|
||||
<AuthProvider initialProvidersAreSet={initialProvidersAreSet}>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
</Provider>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const MOCK_RESPOSITORIES: GitRepository[] = [
|
||||
{
|
||||
id: 1,
|
||||
full_name: "rbren/polaris",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
full_name: "All-Hands-AI/OpenHands",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
},
|
||||
];
|
||||
|
||||
describe("RepoConnector", () => {
|
||||
it("should render the repository connector section", () => {
|
||||
renderRepoConnector();
|
||||
screen.getByTestId("repo-connector");
|
||||
});
|
||||
|
||||
it("should render the available repositories in the dropdown", async () => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
GitService,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
|
||||
renderRepoConnector();
|
||||
|
||||
// Wait for the loading state to be replaced with the dropdown
|
||||
const dropdown = await waitFor(() => screen.getByTestId("repo-dropdown"));
|
||||
await userEvent.click(dropdown);
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText("rbren/polaris");
|
||||
screen.getByText("All-Hands-AI/OpenHands");
|
||||
});
|
||||
});
|
||||
|
||||
it("should only enable the launch button if a repo is selected", async () => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
GitService,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
|
||||
renderRepoConnector();
|
||||
|
||||
const launchButton = screen.getByTestId("repo-launch-button");
|
||||
expect(launchButton).toBeDisabled();
|
||||
|
||||
// Wait for the loading state to be replaced with the dropdown
|
||||
const dropdown = await waitFor(() => screen.getByTestId("repo-dropdown"));
|
||||
await userEvent.click(dropdown);
|
||||
await userEvent.click(screen.getByText("rbren/polaris"));
|
||||
|
||||
expect(launchButton).toBeEnabled();
|
||||
});
|
||||
|
||||
it("should render the 'add git(hub|lab) repos' links if saas mode", async () => {
|
||||
const getConfiSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
// @ts-expect-error - only return the APP_MODE
|
||||
getConfiSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
});
|
||||
|
||||
renderRepoConnector();
|
||||
|
||||
await screen.findByText("Add GitHub repos");
|
||||
});
|
||||
|
||||
it("should not render the 'add git(hub|lab) repos' links if oss mode", async () => {
|
||||
const getConfiSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
// @ts-expect-error - only return the APP_MODE
|
||||
getConfiSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
});
|
||||
|
||||
renderRepoConnector();
|
||||
|
||||
expect(screen.queryByText("Add GitHub repos")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Add GitLab repos")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should create a conversation and redirect with the selected repo when pressing the launch button", async () => {
|
||||
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
GitService,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
|
||||
renderRepoConnector();
|
||||
|
||||
const repoConnector = screen.getByTestId("repo-connector");
|
||||
const launchButton =
|
||||
within(repoConnector).getByTestId("repo-launch-button");
|
||||
await userEvent.click(launchButton);
|
||||
|
||||
// repo not selected yet
|
||||
expect(createConversationSpy).not.toHaveBeenCalled();
|
||||
|
||||
// select a repository from the dropdown
|
||||
const dropdown = await waitFor(() =>
|
||||
within(repoConnector).getByTestId("repo-dropdown")
|
||||
);
|
||||
await userEvent.click(dropdown);
|
||||
|
||||
const repoOption = screen.getByText("rbren/polaris");
|
||||
await userEvent.click(repoOption);
|
||||
await userEvent.click(launchButton);
|
||||
|
||||
expect(createConversationSpy).toHaveBeenCalledExactlyOnceWith(
|
||||
{
|
||||
full_name: "rbren/polaris",
|
||||
git_provider: "github",
|
||||
id: 1,
|
||||
is_public: true,
|
||||
},
|
||||
undefined,
|
||||
[],
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("should change the launch button text to 'Loading...' when creating a conversation", async () => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
GitService,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
|
||||
renderRepoConnector();
|
||||
|
||||
const launchButton = screen.getByTestId("repo-launch-button");
|
||||
|
||||
// Wait for the loading state to be replaced with the dropdown
|
||||
const dropdown = await waitFor(() => screen.getByTestId("repo-dropdown"));
|
||||
await userEvent.click(dropdown);
|
||||
await userEvent.click(screen.getByText("rbren/polaris"));
|
||||
|
||||
await userEvent.click(launchButton);
|
||||
expect(launchButton).toBeDisabled();
|
||||
expect(launchButton).toHaveTextContent(/Loading/i);
|
||||
});
|
||||
|
||||
it("should not display a button to settings if the user is signed in with their git provider", async () => {
|
||||
renderRepoConnector(true);
|
||||
expect(
|
||||
screen.queryByTestId("navigate-to-settings-button"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display a button to settings if the user needs to sign in with their git provider", async () => {
|
||||
renderRepoConnector(false);
|
||||
|
||||
const goToSettingsButton = await screen.findByTestId(
|
||||
"navigate-to-settings-button",
|
||||
);
|
||||
const dropdown = screen.queryByTestId("repo-dropdown");
|
||||
const launchButton = screen.queryByTestId("repo-launch-button");
|
||||
const providerLinks = screen.queryAllByText(/add git(hub|lab) repos/i);
|
||||
|
||||
expect(dropdown).not.toBeInTheDocument();
|
||||
expect(launchButton).not.toBeInTheDocument();
|
||||
expect(providerLinks.length).toBe(0);
|
||||
|
||||
expect(goToSettingsButton).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(goToSettingsButton);
|
||||
await screen.findByTestId("settings-screen");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,206 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { Provider } from "react-redux";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { setupStore } from "test-utils";
|
||||
import { SuggestedTask } from "#/components/features/home/tasks/task.types";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { TaskCard } from "#/components/features/home/tasks/task-card";
|
||||
import * as GitService from "#/api/git";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import {
|
||||
getFailingChecksPrompt,
|
||||
getMergeConflictPrompt,
|
||||
getOpenIssuePrompt,
|
||||
getUnresolvedCommentsPrompt,
|
||||
} from "#/components/features/home/tasks/get-prompt-for-query";
|
||||
|
||||
const MOCK_TASK_1: SuggestedTask = {
|
||||
issue_number: 123,
|
||||
repo: "repo1",
|
||||
title: "Task 1",
|
||||
task_type: "MERGE_CONFLICTS",
|
||||
git_provider: "github",
|
||||
};
|
||||
|
||||
const MOCK_TASK_2: SuggestedTask = {
|
||||
issue_number: 456,
|
||||
repo: "repo2",
|
||||
title: "Task 2",
|
||||
task_type: "FAILING_CHECKS",
|
||||
git_provider: "github",
|
||||
};
|
||||
|
||||
const MOCK_TASK_3: SuggestedTask = {
|
||||
issue_number: 789,
|
||||
repo: "repo3",
|
||||
title: "Task 3",
|
||||
task_type: "UNRESOLVED_COMMENTS",
|
||||
git_provider: "gitlab",
|
||||
};
|
||||
|
||||
const MOCK_TASK_4: SuggestedTask = {
|
||||
issue_number: 101112,
|
||||
repo: "repo4",
|
||||
title: "Task 4",
|
||||
task_type: "OPEN_ISSUE",
|
||||
git_provider: "gitlab",
|
||||
};
|
||||
|
||||
const MOCK_RESPOSITORIES: GitRepository[] = [
|
||||
{ id: 1, full_name: "repo1", git_provider: "github", is_public: true },
|
||||
{ id: 2, full_name: "repo2", git_provider: "github", is_public: true },
|
||||
{ id: 3, full_name: "repo3", git_provider: "gitlab", is_public: true },
|
||||
{ id: 4, full_name: "repo4", git_provider: "gitlab", is_public: true },
|
||||
];
|
||||
|
||||
const renderTaskCard = (task = MOCK_TASK_1) => {
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
Component: () => <TaskCard task={task} />,
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
Component: () => <div data-testid="conversation-screen" />,
|
||||
path: "/conversations/:conversationId",
|
||||
},
|
||||
]);
|
||||
|
||||
return render(<RouterStub />, {
|
||||
wrapper: ({ children }) => (
|
||||
<Provider store={setupStore()}>
|
||||
<AuthProvider initialProvidersAreSet>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
</Provider>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
describe("TaskCard", () => {
|
||||
it("format the issue id", async () => {
|
||||
renderTaskCard();
|
||||
|
||||
const taskId = screen.getByTestId("task-id");
|
||||
expect(taskId).toHaveTextContent(/#123/i);
|
||||
});
|
||||
|
||||
it("should call createConversation when clicking the launch button", async () => {
|
||||
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
|
||||
|
||||
renderTaskCard();
|
||||
|
||||
const launchButton = screen.getByTestId("task-launch-button");
|
||||
await userEvent.click(launchButton);
|
||||
|
||||
expect(createConversationSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("creating conversation prompts", () => {
|
||||
beforeEach(() => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
GitService,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("should call create conversation with the merge conflict prompt", async () => {
|
||||
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
|
||||
|
||||
renderTaskCard(MOCK_TASK_1);
|
||||
|
||||
const launchButton = screen.getByTestId("task-launch-button");
|
||||
await userEvent.click(launchButton);
|
||||
|
||||
expect(createConversationSpy).toHaveBeenCalledWith(
|
||||
MOCK_RESPOSITORIES[0],
|
||||
getMergeConflictPrompt(
|
||||
MOCK_TASK_1.git_provider,
|
||||
MOCK_TASK_1.issue_number,
|
||||
MOCK_TASK_1.repo,
|
||||
),
|
||||
[],
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("should call create conversation with the failing checks prompt", async () => {
|
||||
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
|
||||
|
||||
renderTaskCard(MOCK_TASK_2);
|
||||
|
||||
const launchButton = screen.getByTestId("task-launch-button");
|
||||
await userEvent.click(launchButton);
|
||||
|
||||
expect(createConversationSpy).toHaveBeenCalledWith(
|
||||
MOCK_RESPOSITORIES[1],
|
||||
getFailingChecksPrompt(
|
||||
MOCK_TASK_2.git_provider,
|
||||
MOCK_TASK_2.issue_number,
|
||||
MOCK_TASK_2.repo,
|
||||
),
|
||||
[],
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("should call create conversation with the unresolved comments prompt", async () => {
|
||||
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
|
||||
|
||||
renderTaskCard(MOCK_TASK_3);
|
||||
|
||||
const launchButton = screen.getByTestId("task-launch-button");
|
||||
await userEvent.click(launchButton);
|
||||
|
||||
expect(createConversationSpy).toHaveBeenCalledWith(
|
||||
MOCK_RESPOSITORIES[2],
|
||||
getUnresolvedCommentsPrompt(
|
||||
MOCK_TASK_3.git_provider,
|
||||
MOCK_TASK_3.issue_number,
|
||||
MOCK_TASK_3.repo,
|
||||
),
|
||||
[],
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("should call create conversation with the open issue prompt", async () => {
|
||||
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
|
||||
|
||||
renderTaskCard(MOCK_TASK_4);
|
||||
|
||||
const launchButton = screen.getByTestId("task-launch-button");
|
||||
await userEvent.click(launchButton);
|
||||
|
||||
expect(createConversationSpy).toHaveBeenCalledWith(
|
||||
MOCK_RESPOSITORIES[3],
|
||||
getOpenIssuePrompt(
|
||||
MOCK_TASK_4.git_provider,
|
||||
MOCK_TASK_4.issue_number,
|
||||
MOCK_TASK_4.repo,
|
||||
),
|
||||
[],
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should disable the launch button and update text content when creating a conversation", async () => {
|
||||
renderTaskCard();
|
||||
|
||||
const launchButton = screen.getByTestId("task-launch-button");
|
||||
await userEvent.click(launchButton);
|
||||
|
||||
expect(launchButton).toHaveTextContent(/Loading/i);
|
||||
expect(launchButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Provider } from "react-redux";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { setupStore } from "test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { TaskSuggestions } from "#/components/features/home/tasks/task-suggestions";
|
||||
import { SuggestionsService } from "#/api/suggestions-service/suggestions-service.api";
|
||||
import { MOCK_TASKS } from "#/mocks/task-suggestions-handlers";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
|
||||
const renderTaskSuggestions = (initialProvidersAreSet = true) => {
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
Component: TaskSuggestions,
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
Component: () => <div data-testid="conversation-screen" />,
|
||||
path: "/conversations/:conversationId",
|
||||
},
|
||||
{
|
||||
Component: () => <div data-testid="settings-screen" />,
|
||||
path: "/settings",
|
||||
},
|
||||
]);
|
||||
|
||||
return render(<RouterStub />, {
|
||||
wrapper: ({ children }) => (
|
||||
<Provider store={setupStore()}>
|
||||
<AuthProvider initialProvidersAreSet={initialProvidersAreSet}>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
</Provider>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
describe("TaskSuggestions", () => {
|
||||
const getSuggestedTasksSpy = vi.spyOn(
|
||||
SuggestionsService,
|
||||
"getSuggestedTasks",
|
||||
);
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render the task suggestions section", () => {
|
||||
renderTaskSuggestions();
|
||||
screen.getByTestId("task-suggestions");
|
||||
});
|
||||
|
||||
it("should render an empty message if there are no tasks", async () => {
|
||||
getSuggestedTasksSpy.mockResolvedValue([]);
|
||||
renderTaskSuggestions();
|
||||
await screen.findByText(/No tasks available/i);
|
||||
});
|
||||
|
||||
it("should render the task groups with the correct titles", async () => {
|
||||
getSuggestedTasksSpy.mockResolvedValue(MOCK_TASKS);
|
||||
renderTaskSuggestions();
|
||||
|
||||
await waitFor(() => {
|
||||
MOCK_TASKS.forEach((taskGroup) => {
|
||||
screen.getByText(taskGroup.title);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the task cards with the correct task details", async () => {
|
||||
getSuggestedTasksSpy.mockResolvedValue(MOCK_TASKS);
|
||||
renderTaskSuggestions();
|
||||
|
||||
await waitFor(() => {
|
||||
MOCK_TASKS.forEach((task) => {
|
||||
screen.getByText(task.title);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should render skeletons when loading", async () => {
|
||||
getSuggestedTasksSpy.mockResolvedValue(MOCK_TASKS);
|
||||
renderTaskSuggestions();
|
||||
|
||||
const skeletons = screen.getAllByTestId("task-group-skeleton");
|
||||
expect(skeletons.length).toBeGreaterThan(0);
|
||||
|
||||
await waitFor(() => {
|
||||
MOCK_TASKS.forEach((taskGroup) => {
|
||||
screen.getByText(taskGroup.title);
|
||||
});
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId("task-group-skeleton")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display a button to settings if the user needs to sign in with their git provider", async () => {
|
||||
renderTaskSuggestions(false);
|
||||
|
||||
expect(getSuggestedTasksSpy).not.toHaveBeenCalled();
|
||||
const goToSettingsButton = await screen.findByTestId(
|
||||
"navigate-to-settings-button",
|
||||
);
|
||||
expect(goToSettingsButton).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(goToSettingsButton);
|
||||
await screen.findByTestId("settings-screen");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,370 @@
|
||||
import { render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { Provider } from "react-redux";
|
||||
import { setupStore } from "test-utils";
|
||||
import { AxiosError } from "axios";
|
||||
import HomeScreen from "#/routes/home";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import * as GitService from "#/api/git";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import MainApp from "#/routes/root-layout";
|
||||
|
||||
const createAxiosNotFoundErrorObject = () =>
|
||||
new AxiosError(
|
||||
"Request failed with status code 404",
|
||||
"ERR_BAD_REQUEST",
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
status: 404,
|
||||
statusText: "Not Found",
|
||||
data: { message: "Settings not found" },
|
||||
headers: {},
|
||||
// @ts-expect-error - we only need the response object for this test
|
||||
config: {},
|
||||
},
|
||||
);
|
||||
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
Component: MainApp,
|
||||
path: "/",
|
||||
children: [
|
||||
{
|
||||
Component: HomeScreen,
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
Component: () => <div data-testid="conversation-screen" />,
|
||||
path: "/conversations/:conversationId",
|
||||
},
|
||||
{
|
||||
Component: () => <div data-testid="settings-screen" />,
|
||||
path: "/settings",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const renderHomeScreen = (initialProvidersAreSet = true) =>
|
||||
render(<RouterStub />, {
|
||||
wrapper: ({ children }) => (
|
||||
<Provider store={setupStore()}>
|
||||
<AuthProvider initialProvidersAreSet={initialProvidersAreSet}>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
</Provider>
|
||||
),
|
||||
});
|
||||
|
||||
const MOCK_RESPOSITORIES: GitRepository[] = [
|
||||
{
|
||||
id: 1,
|
||||
full_name: "octocat/hello-world",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
full_name: "octocat/earth",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
},
|
||||
];
|
||||
|
||||
describe("HomeScreen", () => {
|
||||
it("should render", () => {
|
||||
renderHomeScreen();
|
||||
screen.getByTestId("home-screen");
|
||||
});
|
||||
|
||||
it("should render the repository connector and suggested tasks sections", async () => {
|
||||
renderHomeScreen();
|
||||
|
||||
screen.getByTestId("repo-connector");
|
||||
screen.getByTestId("task-suggestions");
|
||||
});
|
||||
|
||||
it("should filter the suggested tasks based on the selected repository", async () => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
GitService,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
|
||||
renderHomeScreen();
|
||||
|
||||
const taskSuggestions = screen.getByTestId("task-suggestions");
|
||||
|
||||
// Initially, all tasks should be visible
|
||||
await waitFor(() => {
|
||||
within(taskSuggestions).getByText("octocat/hello-world");
|
||||
within(taskSuggestions).getByText("octocat/earth");
|
||||
});
|
||||
|
||||
// Select a repository from the dropdown
|
||||
const repoConnector = screen.getByTestId("repo-connector");
|
||||
|
||||
const dropdown = within(repoConnector).getByTestId("repo-dropdown");
|
||||
await userEvent.click(dropdown);
|
||||
|
||||
const repoOption = screen.getAllByText("octocat/hello-world")[1];
|
||||
await userEvent.click(repoOption);
|
||||
|
||||
// After selecting a repository, only tasks related to that repository should be visible
|
||||
await waitFor(() => {
|
||||
within(taskSuggestions).getByText("octocat/hello-world");
|
||||
expect(
|
||||
within(taskSuggestions).queryByText("octocat/earth"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should reset the filtered tasks when the selected repository is cleared", async () => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
GitService,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
|
||||
renderHomeScreen();
|
||||
|
||||
const taskSuggestions = screen.getByTestId("task-suggestions");
|
||||
|
||||
// Initially, all tasks should be visible
|
||||
await waitFor(() => {
|
||||
within(taskSuggestions).getByText("octocat/hello-world");
|
||||
within(taskSuggestions).getByText("octocat/earth");
|
||||
});
|
||||
|
||||
// Select a repository from the dropdown
|
||||
const repoConnector = screen.getByTestId("repo-connector");
|
||||
|
||||
const dropdown = within(repoConnector).getByTestId("repo-dropdown");
|
||||
await userEvent.click(dropdown);
|
||||
|
||||
const repoOption = screen.getAllByText("octocat/hello-world")[1];
|
||||
await userEvent.click(repoOption);
|
||||
|
||||
// After selecting a repository, only tasks related to that repository should be visible
|
||||
await waitFor(() => {
|
||||
within(taskSuggestions).getByText("octocat/hello-world");
|
||||
expect(
|
||||
within(taskSuggestions).queryByText("octocat/earth"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Clear the selected repository
|
||||
await userEvent.clear(dropdown);
|
||||
|
||||
// All tasks should be visible again
|
||||
await waitFor(() => {
|
||||
within(taskSuggestions).getByText("octocat/hello-world");
|
||||
within(taskSuggestions).getByText("octocat/earth");
|
||||
});
|
||||
});
|
||||
|
||||
describe("launch buttons", () => {
|
||||
const setupLaunchButtons = async () => {
|
||||
let headerLaunchButton = screen.getByTestId("header-launch-button");
|
||||
let repoLaunchButton = screen.getByTestId("repo-launch-button");
|
||||
let tasksLaunchButtons =
|
||||
await screen.findAllByTestId("task-launch-button");
|
||||
|
||||
// Select a repository from the dropdown to enable the repo launch button
|
||||
const repoConnector = screen.getByTestId("repo-connector");
|
||||
const dropdown = within(repoConnector).getByTestId("repo-dropdown");
|
||||
await userEvent.click(dropdown);
|
||||
const repoOption = screen.getAllByText("octocat/hello-world")[1];
|
||||
await userEvent.click(repoOption);
|
||||
|
||||
expect(headerLaunchButton).not.toBeDisabled();
|
||||
expect(repoLaunchButton).not.toBeDisabled();
|
||||
tasksLaunchButtons.forEach((button) => {
|
||||
expect(button).not.toBeDisabled();
|
||||
});
|
||||
|
||||
headerLaunchButton = screen.getByTestId("header-launch-button");
|
||||
repoLaunchButton = screen.getByTestId("repo-launch-button");
|
||||
tasksLaunchButtons = await screen.findAllByTestId("task-launch-button");
|
||||
|
||||
return {
|
||||
headerLaunchButton,
|
||||
repoLaunchButton,
|
||||
tasksLaunchButtons,
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
GitService,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("should disable the other launch buttons when the header launch button is clicked", async () => {
|
||||
renderHomeScreen();
|
||||
const { headerLaunchButton, repoLaunchButton } =
|
||||
await setupLaunchButtons();
|
||||
|
||||
const tasksLaunchButtonsAfter =
|
||||
await screen.findAllByTestId("task-launch-button");
|
||||
|
||||
// All other buttons should be disabled when the header button is clicked
|
||||
await userEvent.click(headerLaunchButton);
|
||||
|
||||
expect(headerLaunchButton).toBeDisabled();
|
||||
expect(repoLaunchButton).toBeDisabled();
|
||||
tasksLaunchButtonsAfter.forEach((button) => {
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should disable the other launch buttons when the repo launch button is clicked", async () => {
|
||||
renderHomeScreen();
|
||||
const { headerLaunchButton, repoLaunchButton } =
|
||||
await setupLaunchButtons();
|
||||
|
||||
const tasksLaunchButtonsAfter =
|
||||
await screen.findAllByTestId("task-launch-button");
|
||||
|
||||
// All other buttons should be disabled when the repo button is clicked
|
||||
await userEvent.click(repoLaunchButton);
|
||||
|
||||
expect(headerLaunchButton).toBeDisabled();
|
||||
expect(repoLaunchButton).toBeDisabled();
|
||||
tasksLaunchButtonsAfter.forEach((button) => {
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should disable the other launch buttons when any task launch button is clicked", async () => {
|
||||
renderHomeScreen();
|
||||
const { headerLaunchButton, repoLaunchButton, tasksLaunchButtons } =
|
||||
await setupLaunchButtons();
|
||||
|
||||
const tasksLaunchButtonsAfter =
|
||||
await screen.findAllByTestId("task-launch-button");
|
||||
|
||||
// All other buttons should be disabled when the task button is clicked
|
||||
await userEvent.click(tasksLaunchButtons[0]);
|
||||
|
||||
expect(headerLaunchButton).toBeDisabled();
|
||||
expect(repoLaunchButton).toBeDisabled();
|
||||
tasksLaunchButtonsAfter.forEach((button) => {
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should hide the suggested tasks section if not authed with git(hub|lab)", async () => {
|
||||
renderHomeScreen(false);
|
||||
|
||||
const taskSuggestions = screen.queryByTestId("task-suggestions");
|
||||
const repoConnector = screen.getByTestId("repo-connector");
|
||||
|
||||
expect(taskSuggestions).not.toBeInTheDocument();
|
||||
expect(repoConnector).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Settings 404", () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
|
||||
it("should open the settings modal if GET /settings fails with a 404", async () => {
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
getSettingsSpy.mockRejectedValue(error);
|
||||
|
||||
renderHomeScreen();
|
||||
|
||||
const settingsModal = await screen.findByTestId("ai-config-modal");
|
||||
expect(settingsModal).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should navigate to the settings screen when clicking the advanced settings button", async () => {
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
getSettingsSpy.mockRejectedValue(error);
|
||||
|
||||
const user = userEvent.setup();
|
||||
renderHomeScreen();
|
||||
|
||||
const settingsScreen = screen.queryByTestId("settings-screen");
|
||||
expect(settingsScreen).not.toBeInTheDocument();
|
||||
|
||||
const settingsModal = await screen.findByTestId("ai-config-modal");
|
||||
expect(settingsModal).toBeInTheDocument();
|
||||
|
||||
const advancedSettingsButton = await screen.findByTestId(
|
||||
"advanced-settings-link",
|
||||
);
|
||||
await user.click(advancedSettingsButton);
|
||||
|
||||
const settingsScreenAfter = await screen.findByTestId("settings-screen");
|
||||
expect(settingsScreenAfter).toBeInTheDocument();
|
||||
|
||||
const settingsModalAfter = screen.queryByTestId("ai-config-modal");
|
||||
expect(settingsModalAfter).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not open the settings modal if GET /settings fails but is SaaS mode", async () => {
|
||||
// @ts-expect-error - we only need APP_MODE for this test
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
getSettingsSpy.mockRejectedValue(error);
|
||||
|
||||
renderHomeScreen();
|
||||
|
||||
// small hack to wait for the modal to not appear
|
||||
await expect(
|
||||
screen.findByTestId("ai-config-modal", {}, { timeout: 1000 }),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Setup Payment modal", () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
|
||||
it("should only render if SaaS mode and is new user", async () => {
|
||||
// @ts-expect-error - we only need the APP_MODE for this test
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: true,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
getSettingsSpy.mockRejectedValue(error);
|
||||
|
||||
renderHomeScreen();
|
||||
|
||||
const setupPaymentModal = await screen.findByTestId(
|
||||
"proceed-to-stripe-button",
|
||||
);
|
||||
expect(setupPaymentModal).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,177 +0,0 @@
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { AxiosError } from "axios";
|
||||
import MainApp from "#/routes/root-layout";
|
||||
import SettingsScreen from "#/routes/settings";
|
||||
import Home from "#/routes/home";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
const createAxiosNotFoundErrorObject = () =>
|
||||
new AxiosError(
|
||||
"Request failed with status code 404",
|
||||
"ERR_BAD_REQUEST",
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
status: 404,
|
||||
statusText: "Not Found",
|
||||
data: { message: "Settings not found" },
|
||||
headers: {},
|
||||
// @ts-expect-error - we only need the response object for this test
|
||||
config: {},
|
||||
},
|
||||
);
|
||||
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
// layout route
|
||||
Component: MainApp,
|
||||
path: "/",
|
||||
children: [
|
||||
{
|
||||
// home route
|
||||
Component: Home,
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
Component: SettingsScreen,
|
||||
path: "/settings",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Home Screen", () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
|
||||
it("should render the home screen", () => {
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
});
|
||||
|
||||
it("should navigate to the settings screen when the settings button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
|
||||
const settingsButton = await screen.findByTestId("settings-button");
|
||||
await user.click(settingsButton);
|
||||
|
||||
const settingsScreen = await screen.findByTestId("settings-screen");
|
||||
expect(settingsScreen).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should navigate to the settings when pressing 'Connect to GitHub' if the user isn't authenticated", async () => {
|
||||
// @ts-expect-error - we only need APP_MODE for this test
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
|
||||
const connectToGitHubButton =
|
||||
await screen.findByTestId("connect-to-github");
|
||||
await user.click(connectToGitHubButton);
|
||||
|
||||
const settingsScreen = await screen.findByTestId("settings-screen");
|
||||
expect(settingsScreen).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Settings 404", () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
|
||||
it("should open the settings modal if GET /settings fails with a 404", async () => {
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
getSettingsSpy.mockRejectedValue(error);
|
||||
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
|
||||
const settingsModal = await screen.findByTestId("ai-config-modal");
|
||||
expect(settingsModal).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should navigate to the settings screen when clicking the advanced settings button", async () => {
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
getSettingsSpy.mockRejectedValue(error);
|
||||
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
|
||||
const settingsScreen = screen.queryByTestId("settings-screen");
|
||||
expect(settingsScreen).not.toBeInTheDocument();
|
||||
|
||||
const settingsModal = await screen.findByTestId("ai-config-modal");
|
||||
expect(settingsModal).toBeInTheDocument();
|
||||
|
||||
const advancedSettingsButton = await screen.findByTestId(
|
||||
"advanced-settings-link",
|
||||
);
|
||||
await user.click(advancedSettingsButton);
|
||||
|
||||
const settingsScreenAfter = await screen.findByTestId("settings-screen");
|
||||
expect(settingsScreenAfter).toBeInTheDocument();
|
||||
|
||||
const settingsModalAfter = screen.queryByTestId("ai-config-modal");
|
||||
expect(settingsModalAfter).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not open the settings modal if GET /settings fails but is SaaS mode", async () => {
|
||||
// @ts-expect-error - we only need APP_MODE for this test
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
getSettingsSpy.mockRejectedValue(error);
|
||||
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
|
||||
// small hack to wait for the modal to not appear
|
||||
await expect(
|
||||
screen.findByTestId("ai-config-modal", {}, { timeout: 1000 }),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Setup Payment modal", () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should only render if SaaS mode and is new user", async () => {
|
||||
// @ts-expect-error - we only need the APP_MODE for this test
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: true,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
getSettingsSpy.mockRejectedValue(error);
|
||||
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
|
||||
const setupPaymentModal = await screen.findByTestId(
|
||||
"proceed-to-stripe-button",
|
||||
);
|
||||
expect(setupPaymentModal).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -43,10 +43,12 @@ describe("Settings Billing", () => {
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
const navbar = screen.queryByTestId("settings-navbar");
|
||||
expect(navbar).not.toBeInTheDocument();
|
||||
});
|
||||
// Wait for the settings screen to be rendered
|
||||
await screen.findByTestId("settings-screen");
|
||||
|
||||
// Then check that the navbar is not present
|
||||
const navbar = screen.queryByTestId("settings-navbar");
|
||||
expect(navbar).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render the navbar if SaaS mode", async () => {
|
||||
|
||||
@@ -25,7 +25,6 @@ const mock_provider_tokens_are_set: Record<Provider, boolean> = {
|
||||
describe("Settings Screen", () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const resetSettingsSpy = vi.spyOn(OpenHands, "resetSettings");
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
|
||||
const { handleLogoutMock } = vi.hoisted(() => ({
|
||||
@@ -67,7 +66,6 @@ describe("Settings Screen", () => {
|
||||
// Use queryAllByText to handle multiple elements with the same text
|
||||
expect(screen.queryAllByText("SETTINGS$LLM_SETTINGS")).not.toHaveLength(0);
|
||||
screen.getByText("ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS");
|
||||
screen.getByText("BUTTON$RESET_TO_DEFAULTS");
|
||||
screen.getByText("BUTTON$SAVE");
|
||||
});
|
||||
});
|
||||
@@ -542,54 +540,6 @@ describe("Settings Screen", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("resetting settings with no changes but having advanced enabled should hide the advanced items", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await toggleAdvancedSettings(user);
|
||||
|
||||
const resetButton = screen.getByText("BUTTON$RESET_TO_DEFAULTS");
|
||||
await user.click(resetButton);
|
||||
|
||||
// show modal
|
||||
const modal = await screen.findByTestId("reset-modal");
|
||||
expect(modal).toBeInTheDocument();
|
||||
|
||||
// Mock the settings that will be returned after reset
|
||||
// This should be the default settings with no advanced settings enabled
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_base_url: "",
|
||||
confirmation_mode: false,
|
||||
security_analyzer: "",
|
||||
});
|
||||
|
||||
// confirm reset
|
||||
const confirmButton = within(modal).getByText("Reset");
|
||||
await user.click(confirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByTestId("llm-custom-model-input"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("base-url-input"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("agent-input")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("security-analyzer-input"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("enable-confirmation-mode-switch"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should save if only confirmation mode is enabled", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsScreen();
|
||||
@@ -762,81 +712,6 @@ describe("Settings Screen", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should reset the settings when the 'Reset to defaults' button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
const languageInput = await screen.findByTestId("language-input");
|
||||
await user.click(languageInput);
|
||||
|
||||
const norskOption = await screen.findByText("Norsk");
|
||||
await user.click(norskOption);
|
||||
|
||||
expect(languageInput).toHaveValue("Norsk");
|
||||
|
||||
const resetButton = screen.getByText("BUTTON$RESET_TO_DEFAULTS");
|
||||
await user.click(resetButton);
|
||||
|
||||
expect(saveSettingsSpy).not.toHaveBeenCalled();
|
||||
|
||||
// show modal
|
||||
const modal = await screen.findByTestId("reset-modal");
|
||||
expect(modal).toBeInTheDocument();
|
||||
|
||||
// confirm reset
|
||||
const confirmButton = within(modal).getByText("Reset");
|
||||
await user.click(confirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(resetSettingsSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Mock the settings response after reset
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_base_url: "",
|
||||
confirmation_mode: false,
|
||||
security_analyzer: "",
|
||||
});
|
||||
|
||||
// Wait for the mutation to complete and the modal to be removed
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("reset-modal")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("llm-custom-model-input"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("base-url-input")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("agent-input")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("security-analyzer-input"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("enable-confirmation-mode-switch"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should cancel the reset when the 'Cancel' button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
const resetButton = await screen.findByText("BUTTON$RESET_TO_DEFAULTS");
|
||||
await user.click(resetButton);
|
||||
|
||||
const modal = await screen.findByTestId("reset-modal");
|
||||
expect(modal).toBeInTheDocument();
|
||||
|
||||
const cancelButton = within(modal).getByText("Cancel");
|
||||
await user.click(cancelButton);
|
||||
|
||||
expect(saveSettingsSpy).not.toHaveBeenCalled();
|
||||
expect(screen.queryByTestId("reset-modal")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call handleCaptureConsent with true if the save is successful", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleCaptureConsentSpy = vi.spyOn(
|
||||
@@ -1044,18 +919,5 @@ describe("Settings Screen", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should not submit the unwanted fields when resetting", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsScreen();
|
||||
|
||||
const resetButton = await screen.findByText("BUTTON$RESET_TO_DEFAULTS");
|
||||
await user.click(resetButton);
|
||||
|
||||
const modal = await screen.findByTestId("reset-modal");
|
||||
const confirmButton = within(modal).getByText("Reset");
|
||||
await user.click(confirmButton);
|
||||
expect(saveSettingsSpy).not.toHaveBeenCalled();
|
||||
expect(resetSettingsSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
SuggestedTask,
|
||||
SuggestedTaskGroup,
|
||||
} from "#/components/features/home/tasks/task.types";
|
||||
import { groupSuggestedTasks } from "#/utils/group-suggested-tasks";
|
||||
|
||||
const rawTasks: SuggestedTask[] = [
|
||||
{
|
||||
issue_number: 1,
|
||||
repo: "repo1",
|
||||
title: "Task 1",
|
||||
task_type: "MERGE_CONFLICTS",
|
||||
git_provider: "github",
|
||||
},
|
||||
{
|
||||
issue_number: 2,
|
||||
repo: "repo1",
|
||||
title: "Task 2",
|
||||
task_type: "FAILING_CHECKS",
|
||||
git_provider: "github",
|
||||
},
|
||||
{
|
||||
issue_number: 3,
|
||||
repo: "repo2",
|
||||
title: "Task 3",
|
||||
task_type: "UNRESOLVED_COMMENTS",
|
||||
git_provider: "github",
|
||||
},
|
||||
{
|
||||
issue_number: 4,
|
||||
repo: "repo2",
|
||||
title: "Task 4",
|
||||
task_type: "OPEN_ISSUE",
|
||||
git_provider: "github",
|
||||
},
|
||||
{
|
||||
issue_number: 5,
|
||||
repo: "repo3",
|
||||
title: "Task 5",
|
||||
task_type: "FAILING_CHECKS",
|
||||
git_provider: "github",
|
||||
},
|
||||
];
|
||||
|
||||
const groupedTasks: SuggestedTaskGroup[] = [
|
||||
{
|
||||
title: "repo1",
|
||||
tasks: [
|
||||
{
|
||||
issue_number: 1,
|
||||
repo: "repo1",
|
||||
title: "Task 1",
|
||||
task_type: "MERGE_CONFLICTS",
|
||||
git_provider: "github",
|
||||
},
|
||||
{
|
||||
issue_number: 2,
|
||||
repo: "repo1",
|
||||
title: "Task 2",
|
||||
task_type: "FAILING_CHECKS",
|
||||
git_provider: "github",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "repo2",
|
||||
tasks: [
|
||||
{
|
||||
issue_number: 3,
|
||||
repo: "repo2",
|
||||
title: "Task 3",
|
||||
task_type: "UNRESOLVED_COMMENTS",
|
||||
git_provider: "github",
|
||||
},
|
||||
{
|
||||
issue_number: 4,
|
||||
repo: "repo2",
|
||||
title: "Task 4",
|
||||
task_type: "OPEN_ISSUE",
|
||||
git_provider: "github",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "repo3",
|
||||
tasks: [
|
||||
{
|
||||
issue_number: 5,
|
||||
repo: "repo3",
|
||||
title: "Task 5",
|
||||
task_type: "FAILING_CHECKS",
|
||||
git_provider: "github",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
test("groupSuggestedTasks", () => {
|
||||
expect(groupSuggestedTasks(rawTasks)).toEqual(groupedTasks);
|
||||
});
|
||||
Generated
+403
-292
File diff suppressed because it is too large
Load Diff
+14
-12
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.33.0",
|
||||
"version": "0.34.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
@@ -8,13 +8,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroui/react": "2.7.6",
|
||||
"@microlink/react-json-view": "^1.26.1",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@react-router/node": "^7.5.1",
|
||||
"@react-router/serve": "^7.5.1",
|
||||
"@react-router/node": "^7.5.2",
|
||||
"@react-router/serve": "^7.5.2",
|
||||
"@react-types/shared": "^3.29.0",
|
||||
"@reduxjs/toolkit": "^2.7.0",
|
||||
"@stripe/react-stripe-js": "^3.6.0",
|
||||
"@stripe/stripe-js": "^7.1.0",
|
||||
"@stripe/stripe-js": "^7.2.0",
|
||||
"@tanstack/react-query": "^5.74.4",
|
||||
"@vitejs/plugin-react": "^4.4.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
@@ -22,30 +23,31 @@
|
||||
"axios": "^1.8.4",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.7.4",
|
||||
"i18next": "^25.0.0",
|
||||
"framer-motion": "^12.9.1",
|
||||
"i18next": "^25.0.1",
|
||||
"i18next-browser-languagedetector": "^8.0.5",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.25",
|
||||
"jose": "^6.0.10",
|
||||
"lucide-react": "^0.503.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.236.2",
|
||||
"posthog-js": "^1.236.6",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-hot-toast": "^2.5.1",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-i18next": "^15.5.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^7.5.1",
|
||||
"react-router": "^7.5.2",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"react-textarea-autosize": "^8.5.9",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sirv-cli": "^3.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"vite": "^6.3.2",
|
||||
"vite": "^6.3.3",
|
||||
"web-vitals": "^3.5.2",
|
||||
"ws": "^8.18.1"
|
||||
},
|
||||
@@ -80,7 +82,7 @@
|
||||
"@babel/types": "^7.27.0",
|
||||
"@mswjs/socket.io-binding": "^0.1.1",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@react-router/dev": "^7.5.1",
|
||||
"@react-router/dev": "^7.5.2",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/eslint-plugin-query": "^5.73.3",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
@@ -95,7 +97,7 @@
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"@vitest/coverage-v8": "^3.1.2",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.57.0",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* - Please do NOT serve this file on production.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = '2.7.3'
|
||||
const PACKAGE_VERSION = '2.7.5'
|
||||
const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
|
||||
@@ -105,6 +105,7 @@ function isRawTranslationKey(str) {
|
||||
// Specific technical strings that should be excluded from localization
|
||||
const EXCLUDED_TECHNICAL_STRINGS = [
|
||||
"openid email profile", // OAuth scope string - not user-facing
|
||||
"OPEN_ISSUE", // Task type identifier, not a UI string
|
||||
];
|
||||
|
||||
function isExcludedTechnicalString(str) {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { openHands } from "./open-hands-axios";
|
||||
|
||||
export interface ApiKey {
|
||||
id: string;
|
||||
name: string;
|
||||
prefix: string;
|
||||
created_at: string;
|
||||
last_used_at: string | null;
|
||||
}
|
||||
|
||||
export interface CreateApiKeyResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
key: string; // Full key, only returned once upon creation
|
||||
prefix: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
class ApiKeysClient {
|
||||
/**
|
||||
* Get all API keys for the current user
|
||||
*/
|
||||
static async getApiKeys(): Promise<ApiKey[]> {
|
||||
const { data } = await openHands.get<unknown>("/api/keys");
|
||||
// Ensure we always return an array, even if the API returns something else
|
||||
return Array.isArray(data) ? (data as ApiKey[]) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new API key
|
||||
* @param name - A descriptive name for the API key
|
||||
*/
|
||||
static async createApiKey(name: string): Promise<CreateApiKeyResponse> {
|
||||
const { data } = await openHands.post<CreateApiKeyResponse>("/api/keys", {
|
||||
name,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an API key
|
||||
* @param id - The ID of the API key to delete
|
||||
*/
|
||||
static async deleteApiKey(id: string): Promise<void> {
|
||||
await openHands.delete(`/api/keys/${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default ApiKeysClient;
|
||||
@@ -199,14 +199,6 @@ class OpenHands {
|
||||
return data.status === 200;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset user settings in server
|
||||
*/
|
||||
static async resetSettings(): Promise<boolean> {
|
||||
const response = await openHands.post("/api/reset-settings");
|
||||
return response.status === 200;
|
||||
}
|
||||
|
||||
static async createCheckoutSession(amount: number): Promise<string> {
|
||||
const { data } = await openHands.post(
|
||||
"/api/billing/create-checkout-session",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { SuggestedTask } from "#/components/features/home/tasks/task.types";
|
||||
import { openHands } from "../open-hands-axios";
|
||||
|
||||
export class SuggestionsService {
|
||||
static async getSuggestedTasks(): Promise<SuggestedTask[]> {
|
||||
const { data } = await openHands.get("/api/user/suggested-tasks");
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,13 @@
|
||||
fill="black" />
|
||||
<path
|
||||
d="M38.7381 10.5084C38.5759 10.5084 38.4106 10.4788 38.2545 10.4076C37.6821 10.1526 37.4312 9.49736 37.6944 8.94289C38.5453 7.1431 39.791 5.48266 41.2938 4.14245C41.7559 3.73031 42.4782 3.75699 42.9037 4.20768C43.3291 4.65541 43.3016 5.35516 42.8363 5.76731C41.5539 6.91182 40.4919 8.32912 39.7634 9.86502C39.5737 10.2653 39.1666 10.5055 38.7381 10.5084Z"
|
||||
fill="white" />
|
||||
fill="black" />
|
||||
<path
|
||||
d="M34.898 9.87074C34.3073 9.87667 33.8023 9.43784 33.7533 8.85669C33.536 6.25633 33.5268 3.62039 33.7319 1.02003C33.7808 0.412188 34.3287 -0.0414663 34.9531 0.00300963C35.5805 0.0504507 36.0488 0.578232 36.0029 1.18607C35.807 3.67079 35.8162 6.1911 36.0243 8.67582C36.0763 9.28366 35.6081 9.81737 34.9806 9.86481C34.9531 9.86481 34.9255 9.86778 34.898 9.86778V9.87074Z"
|
||||
fill="white" />
|
||||
fill="black" />
|
||||
<path
|
||||
d="M30.976 10.5558C30.4649 10.5618 29.9935 10.2267 29.8619 9.7256C29.3783 7.88726 28.4632 6.14084 27.2175 4.67906C26.8165 4.20762 26.8869 3.51379 27.3705 3.12537C27.8572 2.73695 28.5734 2.80514 28.9743 3.27362C30.4312 4.98743 31.5024 7.03036 32.0656 9.18003C32.2217 9.77008 31.8514 10.372 31.2423 10.5232C31.1505 10.5469 31.0617 10.5558 30.9699 10.5588L30.976 10.5558Z"
|
||||
fill="white" />
|
||||
fill="black" />
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
+10
@@ -8,6 +8,7 @@ interface ConversationCardContextMenuProps {
|
||||
onDelete?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onEdit?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onDisplayCost?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onShowAgentTools?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onDownloadViaVSCode?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
position?: "top" | "bottom";
|
||||
}
|
||||
@@ -17,6 +18,7 @@ export function ConversationCardContextMenu({
|
||||
onDelete,
|
||||
onEdit,
|
||||
onDisplayCost,
|
||||
onShowAgentTools,
|
||||
onDownloadViaVSCode,
|
||||
position = "bottom",
|
||||
}: ConversationCardContextMenuProps) {
|
||||
@@ -58,6 +60,14 @@ export function ConversationCardContextMenu({
|
||||
Display Cost
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
{onShowAgentTools && (
|
||||
<ContextMenuListItem
|
||||
testId="show-agent-tools-button"
|
||||
onClick={onShowAgentTools}
|
||||
>
|
||||
Show Agent Tools & Metadata
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,10 +10,12 @@ import {
|
||||
} from "./conversation-state-indicator";
|
||||
import { EllipsisButton } from "./ellipsis-button";
|
||||
import { ConversationCardContextMenu } from "./conversation-card-context-menu";
|
||||
import { SystemMessageModal } from "./system-message-modal";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { BaseModal } from "../../shared/modals/base-modal/base-modal";
|
||||
import { RootState } from "#/store";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { selectSystemMessage } from "#/state/chat-slice";
|
||||
|
||||
interface ConversationCardProps {
|
||||
onClick?: () => void;
|
||||
@@ -52,10 +54,12 @@ export function ConversationCard({
|
||||
const [contextMenuVisible, setContextMenuVisible] = React.useState(false);
|
||||
const [titleMode, setTitleMode] = React.useState<"view" | "edit">("view");
|
||||
const [metricsModalVisible, setMetricsModalVisible] = React.useState(false);
|
||||
const [systemModalVisible, setSystemModalVisible] = React.useState(false);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
// Subscribe to metrics data from Redux store
|
||||
const metrics = useSelector((state: RootState) => state.metrics);
|
||||
const systemMessage = useSelector(selectSystemMessage);
|
||||
|
||||
const handleBlur = () => {
|
||||
if (inputRef.current?.value) {
|
||||
@@ -129,6 +133,11 @@ export function ConversationCard({
|
||||
setMetricsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleShowAgentTools = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
setSystemModalVisible(true);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (titleMode === "edit") {
|
||||
inputRef.current?.focus();
|
||||
@@ -207,6 +216,11 @@ export function ConversationCard({
|
||||
: undefined
|
||||
}
|
||||
onDisplayCost={showOptions ? handleDisplayCost : undefined}
|
||||
onShowAgentTools={
|
||||
showOptions && systemMessage
|
||||
? handleShowAgentTools
|
||||
: undefined
|
||||
}
|
||||
position={variant === "compact" ? "top" : "bottom"}
|
||||
/>
|
||||
)}
|
||||
@@ -315,6 +329,12 @@ export function ConversationCard({
|
||||
)}
|
||||
</div>
|
||||
</BaseModal>
|
||||
|
||||
<SystemMessageModal
|
||||
isOpen={systemModalVisible}
|
||||
onClose={() => setSystemModalVisible(false)}
|
||||
systemMessage={systemMessage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
import ReactJsonView from "@microlink/react-json-view";
|
||||
import { 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";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
// Custom JSON viewer theme that matches our application theme
|
||||
const jsonViewTheme = {
|
||||
base00: "transparent", // background
|
||||
base01: "#2d2d2d", // lighter background
|
||||
base02: "#4e4e4e", // selection background
|
||||
base03: "#6c6c6c", // comments, invisibles
|
||||
base04: "#969896", // dark foreground
|
||||
base05: "#d9d9d9", // default foreground
|
||||
base06: "#e8e8e8", // light foreground
|
||||
base07: "#ffffff", // light background
|
||||
base08: "#ff5370", // variables, red
|
||||
base09: "#f78c6c", // integers, orange
|
||||
base0A: "#ffcb6b", // booleans, yellow
|
||||
base0B: "#c3e88d", // strings, green
|
||||
base0C: "#89ddff", // support, cyan
|
||||
base0D: "#82aaff", // functions, blue
|
||||
base0E: "#c792ea", // keywords, purple
|
||||
base0F: "#ff5370", // deprecated, red
|
||||
};
|
||||
|
||||
interface SystemMessageModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
systemMessage: {
|
||||
content: string;
|
||||
tools: Array<Record<string, unknown>> | null;
|
||||
openhands_version: string | null;
|
||||
agent_class: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface FunctionData {
|
||||
name?: string;
|
||||
description?: string;
|
||||
parameters?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface ToolData {
|
||||
type?: string;
|
||||
function?: FunctionData;
|
||||
name?: string;
|
||||
description?: string;
|
||||
parameters?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function SystemMessageModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
systemMessage,
|
||||
}: SystemMessageModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<"system" | "tools">("system");
|
||||
const [expandedTools, setExpandedTools] = useState<Record<number, boolean>>(
|
||||
{},
|
||||
);
|
||||
|
||||
if (!systemMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const toggleTool = (index: number) => {
|
||||
setExpandedTools((prev) => ({
|
||||
...prev,
|
||||
[index]: !prev[index],
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
isOpen && (
|
||||
<ModalBackdrop onClose={onClose}>
|
||||
<ModalBody
|
||||
width="medium"
|
||||
className="max-h-[80vh] flex flex-col items-start"
|
||||
>
|
||||
<div className="flex flex-col gap-6 w-full">
|
||||
<BaseModalTitle title={t("SYSTEM_MESSAGE_MODAL$TITLE")} />
|
||||
<div className="flex flex-col gap-2">
|
||||
{systemMessage.agent_class && (
|
||||
<div className="text-sm">
|
||||
<span className="font-semibold text-gray-300">
|
||||
{t("SYSTEM_MESSAGE_MODAL$AGENT_CLASS")}
|
||||
</span>{" "}
|
||||
<span className="font-medium text-gray-100">
|
||||
{systemMessage.agent_class}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{systemMessage.openhands_version && (
|
||||
<div className="text-sm">
|
||||
<span className="font-semibold text-gray-300">
|
||||
{t("SYSTEM_MESSAGE_MODAL$OPENHANDS_VERSION")}
|
||||
</span>{" "}
|
||||
<span className="text-gray-100">
|
||||
{systemMessage.openhands_version}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<div className="flex border-b mb-2">
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"px-4 py-2 font-medium border-b-2 transition-colors",
|
||||
activeTab === "system"
|
||||
? "border-primary text-gray-100"
|
||||
: "border-transparent hover:text-gray-700 dark:hover:text-gray-300",
|
||||
)}
|
||||
onClick={() => setActiveTab("system")}
|
||||
>
|
||||
{t("SYSTEM_MESSAGE_MODAL$SYSTEM_MESSAGE_TAB")}
|
||||
</button>
|
||||
{systemMessage.tools && systemMessage.tools.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"px-4 py-2 font-medium border-b-2 transition-colors",
|
||||
activeTab === "tools"
|
||||
? "border-primary text-gray-100"
|
||||
: "border-transparent hover:text-gray-700 dark:hover:text-gray-300",
|
||||
)}
|
||||
onClick={() => setActiveTab("tools")}
|
||||
>
|
||||
{t("SYSTEM_MESSAGE_MODAL$TOOLS_TAB")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="h-[60vh] overflow-auto rounded-md">
|
||||
{activeTab === "system" && (
|
||||
<div className="p-4 whitespace-pre-wrap font-mono text-sm leading-relaxed text-gray-300 shadow-inner">
|
||||
{systemMessage.content}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "tools" &&
|
||||
systemMessage.tools &&
|
||||
systemMessage.tools.length > 0 && (
|
||||
<div className="p-2 space-y-3">
|
||||
{systemMessage.tools.map((tool, index) => {
|
||||
// Extract function data from the nested structure
|
||||
const toolData = tool as ToolData;
|
||||
const functionData = toolData.function || toolData;
|
||||
const name =
|
||||
functionData.name ||
|
||||
(toolData.type === "function" &&
|
||||
toolData.function?.name) ||
|
||||
"";
|
||||
const description =
|
||||
functionData.description ||
|
||||
(toolData.type === "function" &&
|
||||
toolData.function?.description) ||
|
||||
"";
|
||||
const parameters =
|
||||
functionData.parameters ||
|
||||
(toolData.type === "function" &&
|
||||
toolData.function?.parameters) ||
|
||||
null;
|
||||
|
||||
const isExpanded = expandedTools[index] || false;
|
||||
|
||||
return (
|
||||
<div key={index} className="rounded-md overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleTool(index)}
|
||||
className="w-full py-3 px-2 text-left flex items-center justify-between hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<h3 className="font-bold text-gray-100">
|
||||
{String(name)}
|
||||
</h3>
|
||||
</div>
|
||||
<span className="text-gray-300">
|
||||
{isExpanded ? (
|
||||
<ChevronDown size={18} />
|
||||
) : (
|
||||
<ChevronRight size={18} />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="px-2 pb-3 pt-1">
|
||||
<div className="mt-2 mb-3">
|
||||
<p className="text-sm whitespace-pre-wrap text-gray-300 leading-relaxed">
|
||||
{String(description)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Parameters section */}
|
||||
{parameters && (
|
||||
<div className="mt-2">
|
||||
<h4 className="text-sm font-semibold text-gray-300">
|
||||
{t("SYSTEM_MESSAGE_MODAL$PARAMETERS")}
|
||||
</h4>
|
||||
<div className="text-sm mt-2 p-3 bg-gray-900 rounded-md overflow-auto text-gray-300 max-h-[400px] shadow-inner">
|
||||
<ReactJsonView
|
||||
src={parameters}
|
||||
theme={jsonViewTheme}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "tools" &&
|
||||
(!systemMessage.tools || systemMessage.tools.length === 0) && (
|
||||
<div className="flex items-center justify-center h-full p-4">
|
||||
<p className="text-gray-400">
|
||||
{t("SYSTEM_MESSAGE_MODAL$NO_TOOLS")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</ModalBackdrop>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Link } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BrandButton } from "#/components/features/settings/brand-button";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
|
||||
export function ConnectToProviderMessage() {
|
||||
const { isLoading } = useSettings();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<p>{t("HOME$CONNECT_PROVIDER_MESSAGE")}</p>
|
||||
<Link data-testid="navigate-to-settings-button" to="/settings">
|
||||
<BrandButton type="button" variant="primary" isDisabled={isLoading}>
|
||||
{!isLoading && t("SETTINGS$TITLE")}
|
||||
{isLoading && t("HOME$LOADING")}
|
||||
</BrandButton>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
|
||||
import { useIsCreatingConversation } from "#/hooks/use-is-creating-conversation";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import AllHandsLogo from "#/assets/branding/all-hands-logo-spark.svg?react";
|
||||
|
||||
export function HomeHeader() {
|
||||
const {
|
||||
mutate: createConversation,
|
||||
isPending,
|
||||
isSuccess,
|
||||
} = useCreateConversation();
|
||||
const isCreatingConversationElsewhere = useIsCreatingConversation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// We check for isSuccess because the app might require time to render
|
||||
// into the new conversation screen after the conversation is created.
|
||||
const isCreatingConversation =
|
||||
isPending || isSuccess || isCreatingConversationElsewhere;
|
||||
|
||||
return (
|
||||
<header className="flex flex-col gap-5">
|
||||
<AllHandsLogo />
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="heading">{t("HOME$LETS_START_BUILDING")}</h1>
|
||||
<BrandButton
|
||||
testId="header-launch-button"
|
||||
variant="primary"
|
||||
type="button"
|
||||
onClick={() => createConversation({})}
|
||||
isDisabled={isCreatingConversation}
|
||||
>
|
||||
{!isCreatingConversation && "Launch from Scratch"}
|
||||
{isCreatingConversation && t("HOME$LOADING")}
|
||||
</BrandButton>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm max-w-[424px]">
|
||||
{t("HOME$OPENHANDS_DESCRIPTION")}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
{t("HOME$NOT_SURE_HOW_TO_START")}{" "}
|
||||
<a
|
||||
href="https://docs.all-hands.dev/modules/usage/getting-started"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline underline-offset-2"
|
||||
>
|
||||
Read this
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ConnectToProviderMessage } from "./connect-to-provider-message";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { RepositorySelectionForm } from "./repo-selection-form";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { RepoProviderLinks } from "./repo-provider-links";
|
||||
|
||||
interface RepoConnectorProps {
|
||||
onRepoSelection: (repoTitle: string | null) => void;
|
||||
}
|
||||
|
||||
export function RepoConnector({ onRepoSelection }: RepoConnectorProps) {
|
||||
const { providersAreSet } = useAuth();
|
||||
const { data: config } = useConfig();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isSaaS = config?.APP_MODE === "saas";
|
||||
|
||||
return (
|
||||
<section
|
||||
data-testid="repo-connector"
|
||||
className="w-full flex flex-col gap-6"
|
||||
>
|
||||
<h2 className="heading">{t("HOME$CONNECT_TO_REPOSITORY")}</h2>
|
||||
|
||||
{!providersAreSet && <ConnectToProviderMessage />}
|
||||
{providersAreSet && (
|
||||
<RepositorySelectionForm onRepoSelection={onRepoSelection} />
|
||||
)}
|
||||
|
||||
{isSaaS && providersAreSet && <RepoProviderLinks />}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
|
||||
export function RepoProviderLinks() {
|
||||
const { data: config } = useConfig();
|
||||
|
||||
const githubHref = config
|
||||
? `https://github.com/apps/${config.APP_SLUG}/installations/new`
|
||||
: "";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col text-sm underline underline-offset-2 text-content-2 gap-4 w-fit">
|
||||
<a href={githubHref} target="_blank" rel="noopener noreferrer">
|
||||
Add GitHub repos
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, test, expect, vi, beforeEach } from "vitest";
|
||||
import { RepositorySelectionForm } from "./repo-selection-form";
|
||||
|
||||
// Create mock functions
|
||||
const mockUseUserRepositories = vi.fn();
|
||||
const mockUseCreateConversation = vi.fn();
|
||||
const mockUseIsCreatingConversation = vi.fn();
|
||||
const mockUseTranslation = vi.fn();
|
||||
const mockUseAuth = vi.fn();
|
||||
|
||||
// Setup default mock returns
|
||||
mockUseUserRepositories.mockReturnValue({
|
||||
data: { pages: [{ data: [] }] },
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
mockUseCreateConversation.mockReturnValue({
|
||||
mutate: vi.fn(),
|
||||
isPending: false,
|
||||
isSuccess: false,
|
||||
});
|
||||
|
||||
mockUseIsCreatingConversation.mockReturnValue(false);
|
||||
|
||||
mockUseTranslation.mockReturnValue({ t: (key: string) => key });
|
||||
|
||||
mockUseAuth.mockReturnValue({
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
providersAreSet: true,
|
||||
user: {
|
||||
id: 1,
|
||||
login: "testuser",
|
||||
avatar_url: "https://example.com/avatar.png",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
company: "Test Company",
|
||||
},
|
||||
login: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
});
|
||||
|
||||
// Mock the modules
|
||||
vi.mock("#/hooks/query/use-user-repositories", () => ({
|
||||
useUserRepositories: () => mockUseUserRepositories(),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/mutation/use-create-conversation", () => ({
|
||||
useCreateConversation: () => mockUseCreateConversation(),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/use-is-creating-conversation", () => ({
|
||||
useIsCreatingConversation: () => mockUseIsCreatingConversation(),
|
||||
}));
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => mockUseTranslation(),
|
||||
}));
|
||||
|
||||
vi.mock("#/context/auth-context", () => ({
|
||||
useAuth: () => mockUseAuth(),
|
||||
}));
|
||||
|
||||
describe("RepositorySelectionForm", () => {
|
||||
const mockOnRepoSelection = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("shows loading indicator when repositories are being fetched", () => {
|
||||
// Setup loading state
|
||||
mockUseUserRepositories.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
render(<RepositorySelectionForm onRepoSelection={mockOnRepoSelection} />);
|
||||
|
||||
// Check if loading indicator is displayed
|
||||
expect(screen.getByTestId("repo-dropdown-loading")).toBeInTheDocument();
|
||||
expect(screen.getByText("HOME$LOADING_REPOSITORIES")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows dropdown when repositories are loaded", () => {
|
||||
// Setup loaded repositories
|
||||
mockUseUserRepositories.mockReturnValue({
|
||||
data: {
|
||||
pages: [
|
||||
{
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
full_name: "user/repo1",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
full_name: "user/repo2",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
render(<RepositorySelectionForm onRepoSelection={mockOnRepoSelection} />);
|
||||
|
||||
// Check if dropdown is displayed
|
||||
expect(screen.getByTestId("repo-dropdown")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows error message when repository fetch fails", () => {
|
||||
// Setup error state
|
||||
mockUseUserRepositories.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
error: new Error("Failed to fetch repositories"),
|
||||
});
|
||||
|
||||
render(<RepositorySelectionForm onRepoSelection={mockOnRepoSelection} />);
|
||||
|
||||
// Check if error message is displayed
|
||||
expect(screen.getByTestId("repo-dropdown-error")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("HOME$FAILED_TO_LOAD_REPOSITORIES"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Spinner } from "@heroui/react";
|
||||
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
|
||||
import { useUserRepositories } from "#/hooks/query/use-user-repositories";
|
||||
import { useIsCreatingConversation } from "#/hooks/use-is-creating-conversation";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { SettingsDropdownInput } from "../settings/settings-dropdown-input";
|
||||
|
||||
interface RepositorySelectionFormProps {
|
||||
onRepoSelection: (repoTitle: string | null) => void;
|
||||
}
|
||||
|
||||
// Loading state component
|
||||
function RepositoryLoadingState() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div
|
||||
data-testid="repo-dropdown-loading"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded"
|
||||
>
|
||||
<Spinner size="sm" />
|
||||
<span className="text-sm">{t("HOME$LOADING_REPOSITORIES")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Error state component
|
||||
function RepositoryErrorState() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div
|
||||
data-testid="repo-dropdown-error"
|
||||
className="flex items-center gap-2 max-w-[500px] h-10 px-3 bg-tertiary border border-[#717888] rounded text-red-500"
|
||||
>
|
||||
<span className="text-sm">{t("HOME$FAILED_TO_LOAD_REPOSITORIES")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Repository dropdown component
|
||||
interface RepositoryDropdownProps {
|
||||
items: { key: React.Key; label: string }[];
|
||||
onSelectionChange: (key: React.Key | null) => void;
|
||||
onInputChange: (value: string) => void;
|
||||
}
|
||||
|
||||
function RepositoryDropdown({
|
||||
items,
|
||||
onSelectionChange,
|
||||
onInputChange,
|
||||
}: RepositoryDropdownProps) {
|
||||
return (
|
||||
<SettingsDropdownInput
|
||||
testId="repo-dropdown"
|
||||
name="repo-dropdown"
|
||||
placeholder="Select a repo"
|
||||
items={items}
|
||||
wrapperClassName="max-w-[500px]"
|
||||
onSelectionChange={onSelectionChange}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function RepositorySelectionForm({
|
||||
onRepoSelection,
|
||||
}: RepositorySelectionFormProps) {
|
||||
const [selectedRepository, setSelectedRepository] =
|
||||
React.useState<GitRepository | null>(null);
|
||||
const {
|
||||
data: repositories,
|
||||
isLoading: isLoadingRepositories,
|
||||
isError: isRepositoriesError,
|
||||
} = useUserRepositories();
|
||||
const {
|
||||
mutate: createConversation,
|
||||
isPending,
|
||||
isSuccess,
|
||||
} = useCreateConversation();
|
||||
const isCreatingConversationElsewhere = useIsCreatingConversation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// We check for isSuccess because the app might require time to render
|
||||
// into the new conversation screen after the conversation is created.
|
||||
const isCreatingConversation =
|
||||
isPending || isSuccess || isCreatingConversationElsewhere;
|
||||
|
||||
const repositoriesList = repositories?.pages.flatMap((page) => page.data);
|
||||
const repositoriesItems = repositoriesList?.map((repo) => ({
|
||||
key: repo.id,
|
||||
label: repo.full_name,
|
||||
}));
|
||||
|
||||
const handleRepoSelection = (key: React.Key | null) => {
|
||||
const selectedRepo = repositoriesList?.find(
|
||||
(repo) => repo.id.toString() === key,
|
||||
);
|
||||
|
||||
if (selectedRepo) onRepoSelection(selectedRepo.full_name);
|
||||
setSelectedRepository(selectedRepo || null);
|
||||
};
|
||||
|
||||
const handleInputChange = (value: string) => {
|
||||
if (value === "") {
|
||||
setSelectedRepository(null);
|
||||
onRepoSelection(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Render the appropriate UI based on the loading/error state
|
||||
const renderRepositorySelector = () => {
|
||||
if (isLoadingRepositories) {
|
||||
return <RepositoryLoadingState />;
|
||||
}
|
||||
|
||||
if (isRepositoriesError) {
|
||||
return <RepositoryErrorState />;
|
||||
}
|
||||
|
||||
return (
|
||||
<RepositoryDropdown
|
||||
items={repositoriesItems || []}
|
||||
onSelectionChange={handleRepoSelection}
|
||||
onInputChange={handleInputChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderRepositorySelector()}
|
||||
|
||||
<BrandButton
|
||||
testId="repo-launch-button"
|
||||
variant="primary"
|
||||
type="button"
|
||||
isDisabled={
|
||||
!selectedRepository ||
|
||||
isCreatingConversation ||
|
||||
isLoadingRepositories ||
|
||||
isRepositoriesError
|
||||
}
|
||||
onClick={() => createConversation({ selectedRepository })}
|
||||
>
|
||||
{!isCreatingConversation && "Launch"}
|
||||
{isCreatingConversation && t("HOME$LOADING")}
|
||||
</BrandButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Provider } from "#/types/settings";
|
||||
import { SuggestedTaskType } from "./task.types";
|
||||
|
||||
// Helper function to get provider-specific terminology
|
||||
const getProviderTerms = (git_provider: Provider) => {
|
||||
if (git_provider === "gitlab") {
|
||||
return {
|
||||
requestType: "Merge Request",
|
||||
requestTypeShort: "MR",
|
||||
apiName: "GitLab API",
|
||||
tokenEnvVar: "GITLAB_TOKEN",
|
||||
ciSystem: "CI pipelines",
|
||||
ciProvider: "GitLab",
|
||||
requestVerb: "merge request",
|
||||
};
|
||||
}
|
||||
return {
|
||||
requestType: "Pull Request",
|
||||
requestTypeShort: "PR",
|
||||
apiName: "GitHub API",
|
||||
tokenEnvVar: "GITHUB_TOKEN",
|
||||
ciSystem: "GitHub Actions",
|
||||
ciProvider: "GitHub",
|
||||
requestVerb: "pull request",
|
||||
};
|
||||
};
|
||||
|
||||
export const getMergeConflictPrompt = (
|
||||
git_provider: Provider,
|
||||
issueNumber: number,
|
||||
repo: string,
|
||||
) => {
|
||||
const terms = getProviderTerms(git_provider);
|
||||
|
||||
return `You are working on ${terms.requestType} #${issueNumber} in repository ${repo}. You need to fix the merge conflicts.
|
||||
Use the ${terms.apiName} with the ${terms.tokenEnvVar} environment variable to retrieve the ${terms.requestTypeShort} details. Check out the branch from that ${terms.requestVerb} and look at the diff versus the base branch of the ${terms.requestTypeShort} to understand the ${terms.requestTypeShort}'s intention.
|
||||
Then resolve the merge conflicts. If you aren't sure what the right solution is, look back through the commit history at the commits that introduced the conflict and resolve them accordingly.`;
|
||||
};
|
||||
|
||||
export const getFailingChecksPrompt = (
|
||||
git_provider: Provider,
|
||||
issueNumber: number,
|
||||
repo: string,
|
||||
) => {
|
||||
const terms = getProviderTerms(git_provider);
|
||||
|
||||
return `You are working on ${terms.requestType} #${issueNumber} in repository ${repo}. You need to fix the failing CI checks.
|
||||
Use the ${terms.apiName} with the ${terms.tokenEnvVar} environment variable to retrieve the ${terms.requestTypeShort} details. Check out the branch from that ${terms.requestVerb} and look at the diff versus the base branch of the ${terms.requestTypeShort} to understand the ${terms.requestTypeShort}'s intention.
|
||||
Then use the ${terms.apiName} to look at the ${terms.ciSystem} that are failing on the most recent commit. Try and reproduce the failure locally.
|
||||
Get things working locally, then push your changes. Sleep for 30 seconds at a time until the ${terms.ciProvider} ${terms.ciSystem.toLowerCase()} have run again. If they are still failing, repeat the process.`;
|
||||
};
|
||||
|
||||
export const getUnresolvedCommentsPrompt = (
|
||||
git_provider: Provider,
|
||||
issueNumber: number,
|
||||
repo: string,
|
||||
) => {
|
||||
const terms = getProviderTerms(git_provider);
|
||||
|
||||
return `You are working on ${terms.requestType} #${issueNumber} in repository ${repo}. You need to resolve the remaining comments from reviewers.
|
||||
Use the ${terms.apiName} with the ${terms.tokenEnvVar} environment variable to retrieve the ${terms.requestTypeShort} details. Check out the branch from that ${terms.requestVerb} and look at the diff versus the base branch of the ${terms.requestTypeShort} to understand the ${terms.requestTypeShort}'s intention.
|
||||
Then use the ${terms.apiName} to retrieve all the feedback on the ${terms.requestTypeShort} so far. If anything hasn't been addressed, address it and commit your changes back to the same branch.`;
|
||||
};
|
||||
|
||||
export const getOpenIssuePrompt = (
|
||||
git_provider: Provider,
|
||||
issueNumber: number,
|
||||
repo: string,
|
||||
) => {
|
||||
const terms = getProviderTerms(git_provider);
|
||||
|
||||
return `You are working on Issue #${issueNumber} in repository ${repo}. Your goal is to fix the issue.
|
||||
Use the ${terms.apiName} with the ${terms.tokenEnvVar} environment variable to retrieve the issue details and any comments on the issue. Then check out a new branch and investigate what changes will need to be made.
|
||||
Finally, make the required changes and open up a ${terms.requestVerb}. Be sure to reference the issue in the ${terms.requestTypeShort} description.`;
|
||||
};
|
||||
|
||||
export const getPromptForQuery = (
|
||||
git_provider: Provider,
|
||||
type: SuggestedTaskType,
|
||||
issueNumber: number,
|
||||
repo: string,
|
||||
) => {
|
||||
switch (type) {
|
||||
case "MERGE_CONFLICTS":
|
||||
return getMergeConflictPrompt(git_provider, issueNumber, repo);
|
||||
case "FAILING_CHECKS":
|
||||
return getFailingChecksPrompt(git_provider, issueNumber, repo);
|
||||
case "UNRESOLVED_COMMENTS":
|
||||
return getUnresolvedCommentsPrompt(git_provider, issueNumber, repo);
|
||||
case "OPEN_ISSUE":
|
||||
return getOpenIssuePrompt(git_provider, issueNumber, repo);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SuggestedTask } from "./task.types";
|
||||
import { useIsCreatingConversation } from "#/hooks/use-is-creating-conversation";
|
||||
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { useUserRepositories } from "#/hooks/query/use-user-repositories";
|
||||
import { getPromptForQuery } from "./get-prompt-for-query";
|
||||
import { TaskIssueNumber } from "./task-issue-number";
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
const getTaskTypeMap = (
|
||||
t: (key: string) => string,
|
||||
): Record<SuggestedTask["task_type"], string> => ({
|
||||
FAILING_CHECKS: t("HOME$FIX_FAILING_CHECKS"),
|
||||
MERGE_CONFLICTS: t("HOME$RESOLVE_MERGE_CONFLICTS"),
|
||||
OPEN_ISSUE: t("HOME$OPEN_ISSUE"),
|
||||
UNRESOLVED_COMMENTS: t("HOME$RESOLVE_UNRESOLVED_COMMENTS"),
|
||||
});
|
||||
|
||||
interface TaskCardProps {
|
||||
task: SuggestedTask;
|
||||
}
|
||||
|
||||
export function TaskCard({ task }: TaskCardProps) {
|
||||
const { data: repositories } = useUserRepositories();
|
||||
const { mutate: createConversation, isPending } = useCreateConversation();
|
||||
const isCreatingConversation = useIsCreatingConversation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getRepo = (repo: string, git_provider: Provider) => {
|
||||
const repositoriesList = repositories?.pages.flatMap((page) => page.data);
|
||||
const selectedRepo = repositoriesList?.find(
|
||||
(repository) =>
|
||||
repository.full_name === repo &&
|
||||
repository.git_provider === git_provider,
|
||||
);
|
||||
|
||||
return selectedRepo;
|
||||
};
|
||||
|
||||
const handleLaunchConversation = () => {
|
||||
const repo = getRepo(task.repo, task.git_provider);
|
||||
const query = getPromptForQuery(
|
||||
task.git_provider,
|
||||
task.task_type,
|
||||
task.issue_number,
|
||||
task.repo,
|
||||
);
|
||||
|
||||
return createConversation({
|
||||
selectedRepository: repo,
|
||||
q: query,
|
||||
});
|
||||
};
|
||||
|
||||
// Determine the correct URL format based on git provider
|
||||
let href: string;
|
||||
if (task.git_provider === "gitlab") {
|
||||
const issueType =
|
||||
task.task_type === "OPEN_ISSUE" ? "issues" : "merge_requests";
|
||||
href = `https://gitlab.com/${task.repo}/-/${issueType}/${task.issue_number}`;
|
||||
} else {
|
||||
const hrefType = task.task_type === "OPEN_ISSUE" ? "issues" : "pull";
|
||||
href = `https://github.com/${task.repo}/${hrefType}/${task.issue_number}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="py-3 border-b border-[#717888] flex items-center pr-6">
|
||||
<TaskIssueNumber issueNumber={task.issue_number} href={href} />
|
||||
|
||||
<div className="w-full pl-8">
|
||||
<p className="font-semibold">{getTaskTypeMap(t)[task.task_type]}</p>
|
||||
<p>{task.title}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
data-testid="task-launch-button"
|
||||
className={cn(
|
||||
"underline underline-offset-2 disabled:opacity-80",
|
||||
isPending && "no-underline font-bold",
|
||||
)}
|
||||
disabled={isCreatingConversation}
|
||||
onClick={handleLaunchConversation}
|
||||
>
|
||||
{!isPending && t("HOME$LAUNCH")}
|
||||
{isPending && t("HOME$LOADING")}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { TaskCard } from "./task-card";
|
||||
import { TaskItemTitle } from "./task-item-title";
|
||||
import { SuggestedTask } from "./task.types";
|
||||
|
||||
interface TaskGroupProps {
|
||||
title: string;
|
||||
tasks: SuggestedTask[];
|
||||
}
|
||||
|
||||
export function TaskGroup({ title, tasks }: TaskGroupProps) {
|
||||
return (
|
||||
<div className="text-content-2">
|
||||
<TaskItemTitle>{title}</TaskItemTitle>
|
||||
|
||||
<ul className="text-sm">
|
||||
{tasks.map((task) => (
|
||||
<TaskCard key={task.issue_number} task={task} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
interface TaskIssueNumberProps {
|
||||
issueNumber: number;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export function TaskIssueNumber({ href, issueNumber }: TaskIssueNumberProps) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-testid="task-id"
|
||||
>
|
||||
#<span className="underline underline-offset-2">{issueNumber}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export function TaskItemTitle({ children: title }: React.PropsWithChildren) {
|
||||
return (
|
||||
<div className="py-3 border-b-1 border-[#717888]">
|
||||
<h3 className="text-[16px] leading-6 font-[500]">{title}</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
const VALID_WIDTHS = ["w-1/4", "w-1/2", "w-3/4"];
|
||||
|
||||
const getRandomWidth = () =>
|
||||
VALID_WIDTHS[Math.floor(Math.random() * VALID_WIDTHS.length)];
|
||||
|
||||
const getRandomNumber = (from = 3, to = 5) =>
|
||||
Math.floor(Math.random() * (to - from + 1)) + from;
|
||||
|
||||
function TaskCardSkeleton() {
|
||||
return (
|
||||
<li className="py-3 border-b border-[#717888] flex items-center pr-6">
|
||||
<div className="h-5 w-8 skeleton" />
|
||||
|
||||
<div className="w-full pl-8">
|
||||
<div className="h-5 w-24 skeleton mb-2" />
|
||||
<div className={cn("h-5 skeleton", getRandomWidth())} />
|
||||
</div>
|
||||
|
||||
<div className="h-5 w-16 skeleton" />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
interface TaskGroupSkeletonProps {
|
||||
items?: number;
|
||||
}
|
||||
|
||||
function TaskGroupSkeleton({ items = 3 }: TaskGroupSkeletonProps) {
|
||||
return (
|
||||
<div data-testid="task-group-skeleton">
|
||||
<div className="py-3 border-b border-[#717888]">
|
||||
<div className="h-6 w-40 skeleton" />
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{Array.from({ length: items }).map((_, index) => (
|
||||
<TaskCardSkeleton key={index} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TaskSuggestionsSkeleton() {
|
||||
return Array.from({ length: getRandomNumber(2, 3) }).map((_, index) => (
|
||||
<TaskGroupSkeleton key={index} items={getRandomNumber(3, 5)} />
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { TaskGroup } from "./task-group";
|
||||
import { useSuggestedTasks } from "#/hooks/query/use-suggested-tasks";
|
||||
import { TaskSuggestionsSkeleton } from "./task-suggestions-skeleton";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { ConnectToProviderMessage } from "../connect-to-provider-message";
|
||||
|
||||
interface TaskSuggestionsProps {
|
||||
filterFor?: string | null;
|
||||
}
|
||||
|
||||
export function TaskSuggestions({ filterFor }: TaskSuggestionsProps) {
|
||||
const { providersAreSet } = useAuth();
|
||||
|
||||
const { data: tasks, isLoading } = useSuggestedTasks();
|
||||
const suggestedTasks = filterFor
|
||||
? tasks?.filter((task) => task.title === filterFor)
|
||||
: tasks;
|
||||
|
||||
const hasSuggestedTasks = suggestedTasks && suggestedTasks.length > 0;
|
||||
|
||||
return (
|
||||
<section
|
||||
data-testid="task-suggestions"
|
||||
className={cn("flex flex-col w-full", !hasSuggestedTasks && "gap-6")}
|
||||
>
|
||||
<h2 className="heading">Suggested Tasks</h2>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
{!providersAreSet && <ConnectToProviderMessage />}
|
||||
{isLoading && <TaskSuggestionsSkeleton />}
|
||||
{!hasSuggestedTasks && !isLoading && <p>No tasks available</p>}
|
||||
{suggestedTasks?.map((taskGroup, index) => (
|
||||
<TaskGroup
|
||||
key={index}
|
||||
title={taskGroup.title}
|
||||
tasks={taskGroup.tasks}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
export type SuggestedTaskType =
|
||||
| "MERGE_CONFLICTS"
|
||||
| "FAILING_CHECKS"
|
||||
| "UNRESOLVED_COMMENTS"
|
||||
| "OPEN_ISSUE"; // This is a task type identifier, not a UI string
|
||||
|
||||
export interface SuggestedTask {
|
||||
git_provider: Provider;
|
||||
issue_number: number;
|
||||
repo: string;
|
||||
title: string;
|
||||
task_type: SuggestedTaskType;
|
||||
}
|
||||
|
||||
export interface SuggestedTaskGroup {
|
||||
title: string;
|
||||
tasks: SuggestedTask[];
|
||||
}
|
||||
@@ -40,10 +40,6 @@ export function PaymentForm() {
|
||||
data-testid="billing-settings"
|
||||
className="flex flex-col gap-6 px-11 py-9"
|
||||
>
|
||||
<h2 className="text-[28px] leading-8 tracking-[-0.02em] font-bold">
|
||||
{t(I18nKey.PAYMENT$MANAGE_CREDITS)}
|
||||
</h2>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-between w-[680px] bg-[#7F7445] rounded px-3 py-2",
|
||||
@@ -52,7 +48,7 @@ export function PaymentForm() {
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<MoneyIcon width={22} height={14} />
|
||||
<span>Balance</span>
|
||||
<span>{t(I18nKey.PAYMENT$MANAGE_CREDITS)}</span>
|
||||
</div>
|
||||
{!isLoading && (
|
||||
<span data-testid="user-balance">${Number(balance).toFixed(2)}</span>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
|
||||
interface ApiKeyModalBaseProps {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
width?: string;
|
||||
children: ReactNode;
|
||||
footer: ReactNode;
|
||||
}
|
||||
|
||||
export function ApiKeyModalBase({
|
||||
isOpen,
|
||||
title,
|
||||
width = "500px",
|
||||
children,
|
||||
footer,
|
||||
}: ApiKeyModalBaseProps) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<ModalBackdrop>
|
||||
<div
|
||||
className="bg-base-secondary p-6 rounded-xl flex flex-col gap-4 border border-tertiary"
|
||||
style={{ width }}
|
||||
>
|
||||
<h3 className="text-xl font-bold">{title}</h3>
|
||||
{children}
|
||||
<div className="w-full flex gap-2 mt-2">{footer}</div>
|
||||
</div>
|
||||
</ModalBackdrop>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "#/components/features/settings/brand-button";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import { ApiKey, CreateApiKeyResponse } from "#/api/api-keys";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { CreateApiKeyModal } from "./create-api-key-modal";
|
||||
import { DeleteApiKeyModal } from "./delete-api-key-modal";
|
||||
import { NewApiKeyModal } from "./new-api-key-modal";
|
||||
import { useApiKeys } from "#/hooks/query/use-api-keys";
|
||||
|
||||
export function ApiKeysManager() {
|
||||
const { t } = useTranslation();
|
||||
const { data: apiKeys = [], isLoading, error } = useApiKeys();
|
||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||
const [keyToDelete, setKeyToDelete] = useState<ApiKey | null>(null);
|
||||
const [newlyCreatedKey, setNewlyCreatedKey] =
|
||||
useState<CreateApiKeyResponse | null>(null);
|
||||
const [showNewKeyModal, setShowNewKeyModal] = useState(false);
|
||||
|
||||
// Display error toast if the query fails
|
||||
if (error) {
|
||||
displayErrorToast(t(I18nKey.ERROR$GENERIC));
|
||||
}
|
||||
|
||||
const handleKeyCreated = (newKey: CreateApiKeyResponse) => {
|
||||
setNewlyCreatedKey(newKey);
|
||||
setCreateModalOpen(false);
|
||||
setShowNewKeyModal(true);
|
||||
};
|
||||
|
||||
const handleCloseCreateModal = () => {
|
||||
setCreateModalOpen(false);
|
||||
};
|
||||
|
||||
const handleCloseDeleteModal = () => {
|
||||
setDeleteModalOpen(false);
|
||||
setKeyToDelete(null);
|
||||
};
|
||||
|
||||
const handleCloseNewKeyModal = () => {
|
||||
setShowNewKeyModal(false);
|
||||
setNewlyCreatedKey(null);
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string | null) => {
|
||||
if (!dateString) return "Never";
|
||||
return new Date(dateString).toLocaleString();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={() => setCreateModalOpen(true)}
|
||||
>
|
||||
{t(I18nKey.SETTINGS$CREATE_API_KEY)}
|
||||
</BrandButton>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-300">
|
||||
{t(I18nKey.SETTINGS$API_KEYS_DESCRIPTION)}
|
||||
</p>
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex justify-center p-4">
|
||||
<LoadingSpinner size="large" />
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && Array.isArray(apiKeys) && apiKeys.length > 0 && (
|
||||
<div className="border border-tertiary rounded-md overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-base-tertiary">
|
||||
<tr>
|
||||
<th className="text-left p-3 text-sm font-medium">
|
||||
{t(I18nKey.SETTINGS$NAME)}
|
||||
</th>
|
||||
<th className="text-left p-3 text-sm font-medium">
|
||||
{t(I18nKey.SETTINGS$CREATED_AT)}
|
||||
</th>
|
||||
<th className="text-left p-3 text-sm font-medium">
|
||||
{t(I18nKey.SETTINGS$LAST_USED)}
|
||||
</th>
|
||||
<th className="text-right p-3 text-sm font-medium">
|
||||
{t(I18nKey.SETTINGS$ACTIONS)}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{apiKeys.map((key) => (
|
||||
<tr key={key.id} className="border-t border-tertiary">
|
||||
<td className="p-3 text-sm">{key.name}</td>
|
||||
<td className="p-3 text-sm">
|
||||
{formatDate(key.created_at)}
|
||||
</td>
|
||||
<td className="p-3 text-sm">
|
||||
{formatDate(key.last_used_at)}
|
||||
</td>
|
||||
<td className="p-3 text-right">
|
||||
<button
|
||||
type="button"
|
||||
className="underline"
|
||||
onClick={() => {
|
||||
setKeyToDelete(key);
|
||||
setDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{t(I18nKey.BUTTON$DELETE)}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Create API Key Modal */}
|
||||
<CreateApiKeyModal
|
||||
isOpen={createModalOpen}
|
||||
onClose={handleCloseCreateModal}
|
||||
onKeyCreated={handleKeyCreated}
|
||||
/>
|
||||
|
||||
{/* Delete API Key Modal */}
|
||||
<DeleteApiKeyModal
|
||||
isOpen={deleteModalOpen}
|
||||
keyToDelete={keyToDelete}
|
||||
onClose={handleCloseDeleteModal}
|
||||
/>
|
||||
|
||||
{/* Show New API Key Modal */}
|
||||
<NewApiKeyModal
|
||||
isOpen={showNewKeyModal}
|
||||
newlyCreatedKey={newlyCreatedKey}
|
||||
onClose={handleCloseNewKeyModal}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { cn } from "#/utils/utils";
|
||||
|
||||
interface BrandButtonProps {
|
||||
testId?: string;
|
||||
variant: "primary" | "secondary";
|
||||
variant: "primary" | "secondary" | "danger";
|
||||
type: React.ButtonHTMLAttributes<HTMLButtonElement>["type"];
|
||||
isDisabled?: boolean;
|
||||
className?: string;
|
||||
@@ -29,9 +29,10 @@ export function BrandButton({
|
||||
type={type}
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"w-fit p-2 rounded disabled:opacity-30 disabled:cursor-not-allowed",
|
||||
"w-fit p-2 text-sm rounded disabled:opacity-30 disabled:cursor-not-allowed hover:opacity-80",
|
||||
variant === "primary" && "bg-primary text-[#0D0F11]",
|
||||
variant === "secondary" && "border border-primary text-primary",
|
||||
variant === "danger" && "bg-red-600 text-white hover:bg-red-700",
|
||||
startContent && "flex items-center justify-center gap-2",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "#/components/features/settings/brand-button";
|
||||
import { SettingsInput } from "#/components/features/settings/settings-input";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import { CreateApiKeyResponse } from "#/api/api-keys";
|
||||
import {
|
||||
displayErrorToast,
|
||||
displaySuccessToast,
|
||||
} from "#/utils/custom-toast-handlers";
|
||||
import { ApiKeyModalBase } from "./api-key-modal-base";
|
||||
import { useCreateApiKey } from "#/hooks/mutation/use-create-api-key";
|
||||
|
||||
interface CreateApiKeyModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onKeyCreated: (newKey: CreateApiKeyResponse) => void;
|
||||
}
|
||||
|
||||
export function CreateApiKeyModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onKeyCreated,
|
||||
}: CreateApiKeyModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [newKeyName, setNewKeyName] = useState("");
|
||||
|
||||
const createApiKeyMutation = useCreateApiKey();
|
||||
|
||||
const handleCreateKey = async () => {
|
||||
if (!newKeyName.trim()) {
|
||||
displayErrorToast(t(I18nKey.ERROR$REQUIRED_FIELD));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newKey = await createApiKeyMutation.mutateAsync(newKeyName);
|
||||
onKeyCreated(newKey);
|
||||
displaySuccessToast(t(I18nKey.SETTINGS$API_KEY_CREATED));
|
||||
setNewKeyName("");
|
||||
} catch (error) {
|
||||
displayErrorToast(t(I18nKey.ERROR$GENERIC));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setNewKeyName("");
|
||||
onClose();
|
||||
};
|
||||
|
||||
const modalFooter = (
|
||||
<>
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="primary"
|
||||
className="grow"
|
||||
onClick={handleCreateKey}
|
||||
isDisabled={createApiKeyMutation.isPending || !newKeyName.trim()}
|
||||
>
|
||||
{createApiKeyMutation.isPending ? (
|
||||
<LoadingSpinner size="small" />
|
||||
) : (
|
||||
t(I18nKey.BUTTON$CREATE)
|
||||
)}
|
||||
</BrandButton>
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="grow"
|
||||
onClick={handleCancel}
|
||||
isDisabled={createApiKeyMutation.isPending}
|
||||
>
|
||||
{t(I18nKey.BUTTON$CANCEL)}
|
||||
</BrandButton>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<ApiKeyModalBase
|
||||
isOpen={isOpen}
|
||||
title={t(I18nKey.SETTINGS$CREATE_API_KEY)}
|
||||
footer={modalFooter}
|
||||
>
|
||||
<div data-testid="create-api-key-modal">
|
||||
<p className="text-sm text-gray-300">
|
||||
{t(I18nKey.SETTINGS$CREATE_API_KEY_DESCRIPTION)}
|
||||
</p>
|
||||
<SettingsInput
|
||||
testId="api-key-name-input"
|
||||
label={t(I18nKey.SETTINGS$NAME)}
|
||||
placeholder={t(I18nKey.SETTINGS$API_KEY_NAME_PLACEHOLDER)}
|
||||
value={newKeyName}
|
||||
onChange={(value) => setNewKeyName(value)}
|
||||
className="w-full mt-4"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</ApiKeyModalBase>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "#/components/features/settings/brand-button";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import { ApiKey } from "#/api/api-keys";
|
||||
import {
|
||||
displayErrorToast,
|
||||
displaySuccessToast,
|
||||
} from "#/utils/custom-toast-handlers";
|
||||
import { ApiKeyModalBase } from "./api-key-modal-base";
|
||||
import { useDeleteApiKey } from "#/hooks/mutation/use-delete-api-key";
|
||||
|
||||
interface DeleteApiKeyModalProps {
|
||||
isOpen: boolean;
|
||||
keyToDelete: ApiKey | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function DeleteApiKeyModal({
|
||||
isOpen,
|
||||
keyToDelete,
|
||||
onClose,
|
||||
}: DeleteApiKeyModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const deleteApiKeyMutation = useDeleteApiKey();
|
||||
|
||||
const handleDeleteKey = async () => {
|
||||
if (!keyToDelete) return;
|
||||
|
||||
try {
|
||||
await deleteApiKeyMutation.mutateAsync(keyToDelete.id);
|
||||
displaySuccessToast(t(I18nKey.SETTINGS$API_KEY_DELETED));
|
||||
onClose();
|
||||
} catch (error) {
|
||||
displayErrorToast(t(I18nKey.ERROR$GENERIC));
|
||||
}
|
||||
};
|
||||
|
||||
if (!keyToDelete) return null;
|
||||
|
||||
const modalFooter = (
|
||||
<>
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="danger"
|
||||
className="grow"
|
||||
onClick={handleDeleteKey}
|
||||
isDisabled={deleteApiKeyMutation.isPending}
|
||||
>
|
||||
{deleteApiKeyMutation.isPending ? (
|
||||
<LoadingSpinner size="small" />
|
||||
) : (
|
||||
t(I18nKey.BUTTON$DELETE)
|
||||
)}
|
||||
</BrandButton>
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="grow"
|
||||
onClick={onClose}
|
||||
isDisabled={deleteApiKeyMutation.isPending}
|
||||
>
|
||||
{t(I18nKey.BUTTON$CANCEL)}
|
||||
</BrandButton>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<ApiKeyModalBase
|
||||
isOpen={isOpen && !!keyToDelete}
|
||||
title={t(I18nKey.SETTINGS$DELETE_API_KEY)}
|
||||
footer={modalFooter}
|
||||
>
|
||||
<div data-testid="delete-api-key-modal">
|
||||
<p className="text-sm">
|
||||
{t(I18nKey.SETTINGS$DELETE_API_KEY_CONFIRMATION, {
|
||||
name: keyToDelete.name,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</ApiKeyModalBase>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "#/components/features/settings/brand-button";
|
||||
import { CreateApiKeyResponse } from "#/api/api-keys";
|
||||
import { displaySuccessToast } from "#/utils/custom-toast-handlers";
|
||||
import { ApiKeyModalBase } from "./api-key-modal-base";
|
||||
|
||||
interface NewApiKeyModalProps {
|
||||
isOpen: boolean;
|
||||
newlyCreatedKey: CreateApiKeyResponse | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function NewApiKeyModal({
|
||||
isOpen,
|
||||
newlyCreatedKey,
|
||||
onClose,
|
||||
}: NewApiKeyModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleCopyToClipboard = () => {
|
||||
if (newlyCreatedKey) {
|
||||
navigator.clipboard.writeText(newlyCreatedKey.key);
|
||||
displaySuccessToast(t(I18nKey.SETTINGS$API_KEY_COPIED));
|
||||
}
|
||||
};
|
||||
|
||||
if (!newlyCreatedKey) return null;
|
||||
|
||||
const modalFooter = (
|
||||
<>
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={handleCopyToClipboard}
|
||||
>
|
||||
{t(I18nKey.BUTTON$COPY_TO_CLIPBOARD)}
|
||||
</BrandButton>
|
||||
<BrandButton type="button" variant="secondary" onClick={onClose}>
|
||||
{t(I18nKey.BUTTON$CLOSE)}
|
||||
</BrandButton>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<ApiKeyModalBase
|
||||
isOpen={isOpen && !!newlyCreatedKey}
|
||||
title={t(I18nKey.SETTINGS$API_KEY_CREATED)}
|
||||
width="600px"
|
||||
footer={modalFooter}
|
||||
>
|
||||
<div data-testid="new-api-key-modal">
|
||||
<p className="text-sm">{t(I18nKey.SETTINGS$API_KEY_WARNING)}</p>
|
||||
<div className="bg-base-tertiary p-4 rounded-md font-mono text-sm break-all mt-4">
|
||||
{newlyCreatedKey.key}
|
||||
</div>
|
||||
</div>
|
||||
</ApiKeyModalBase>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +1,56 @@
|
||||
import { Autocomplete, AutocompleteItem } from "@heroui/react";
|
||||
import { ReactNode } from "react";
|
||||
import { OptionalTag } from "./optional-tag";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface SettingsDropdownInputProps {
|
||||
testId: string;
|
||||
label: ReactNode;
|
||||
name: string;
|
||||
items: { key: React.Key; label: string }[];
|
||||
label?: ReactNode;
|
||||
wrapperClassName?: string;
|
||||
placeholder?: string;
|
||||
showOptionalTag?: boolean;
|
||||
isDisabled?: boolean;
|
||||
defaultSelectedKey?: string;
|
||||
isClearable?: boolean;
|
||||
onSelectionChange?: (key: React.Key | null) => void;
|
||||
onInputChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
export function SettingsDropdownInput({
|
||||
testId,
|
||||
label,
|
||||
wrapperClassName,
|
||||
name,
|
||||
items,
|
||||
placeholder,
|
||||
showOptionalTag,
|
||||
isDisabled,
|
||||
defaultSelectedKey,
|
||||
isClearable,
|
||||
onSelectionChange,
|
||||
onInputChange,
|
||||
}: SettingsDropdownInputProps) {
|
||||
return (
|
||||
<label className="flex flex-col gap-2.5 w-[680px]">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm">{label}</span>
|
||||
{showOptionalTag && <OptionalTag />}
|
||||
</div>
|
||||
<label className={cn("flex flex-col gap-2.5", wrapperClassName)}>
|
||||
{label && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm">{label}</span>
|
||||
{showOptionalTag && <OptionalTag />}
|
||||
</div>
|
||||
)}
|
||||
<Autocomplete
|
||||
aria-label={typeof label === "string" ? label : name}
|
||||
data-testid={testId}
|
||||
name={name}
|
||||
defaultItems={items}
|
||||
defaultSelectedKey={defaultSelectedKey}
|
||||
onSelectionChange={onSelectionChange}
|
||||
onInputChange={onInputChange}
|
||||
isClearable={isClearable}
|
||||
isDisabled={isDisabled}
|
||||
placeholder={placeholder}
|
||||
className="w-full"
|
||||
classNames={{
|
||||
popoverContent: "bg-tertiary rounded-xl border border-[#717888]",
|
||||
|
||||
@@ -7,6 +7,7 @@ interface SettingsInputProps {
|
||||
label: string;
|
||||
type: React.HTMLInputTypeAttribute;
|
||||
defaultValue?: string;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
showOptionalTag?: boolean;
|
||||
isDisabled?: boolean;
|
||||
@@ -24,6 +25,7 @@ export function SettingsInput({
|
||||
label,
|
||||
type,
|
||||
defaultValue,
|
||||
value,
|
||||
placeholder,
|
||||
showOptionalTag,
|
||||
isDisabled,
|
||||
@@ -43,11 +45,12 @@ export function SettingsInput({
|
||||
</div>
|
||||
<input
|
||||
data-testid={testId}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
onChange={(e) => onChange && onChange(e.target.value)}
|
||||
name={name}
|
||||
disabled={isDisabled}
|
||||
type={type}
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
min={min}
|
||||
max={max}
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
import React from "react";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
type ModalWidth = "small" | "medium";
|
||||
|
||||
interface ModalBodyProps {
|
||||
testID?: string;
|
||||
children: React.ReactNode;
|
||||
className?: React.HTMLProps<HTMLDivElement>["className"];
|
||||
width?: ModalWidth;
|
||||
}
|
||||
|
||||
export function ModalBody({ testID, children, className }: ModalBodyProps) {
|
||||
export function ModalBody({
|
||||
testID,
|
||||
children,
|
||||
className,
|
||||
width = "small",
|
||||
}: ModalBodyProps) {
|
||||
return (
|
||||
<div
|
||||
data-testid={testID}
|
||||
className={cn(
|
||||
"bg-base-secondary flex flex-col gap-6 items-center w-[384px] p-6 rounded-xl",
|
||||
"bg-base-secondary flex flex-col gap-6 items-center p-6 rounded-xl",
|
||||
width === "small" && "w-[384px]",
|
||||
width === "medium" && "w-[700px]",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -10,6 +10,7 @@ interface AuthContextType {
|
||||
|
||||
interface AuthContextProps extends React.PropsWithChildren {
|
||||
initialProviderTokens?: Provider[];
|
||||
initialProvidersAreSet?: boolean;
|
||||
}
|
||||
|
||||
const AuthContext = React.createContext<AuthContextType | undefined>(undefined);
|
||||
@@ -17,12 +18,15 @@ const AuthContext = React.createContext<AuthContextType | undefined>(undefined);
|
||||
function AuthProvider({
|
||||
children,
|
||||
initialProviderTokens = [],
|
||||
initialProvidersAreSet = false,
|
||||
}: AuthContextProps) {
|
||||
const [providerTokensSet, setProviderTokensSet] = React.useState<Provider[]>(
|
||||
initialProviderTokens,
|
||||
);
|
||||
|
||||
const [providersAreSet, setProvidersAreSet] = React.useState<boolean>(false);
|
||||
const [providersAreSet, setProvidersAreSet] = React.useState<boolean>(
|
||||
initialProvidersAreSet,
|
||||
);
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import ApiKeysClient, { CreateApiKeyResponse } from "#/api/api-keys";
|
||||
import { API_KEYS_QUERY_KEY } from "#/hooks/query/use-api-keys";
|
||||
|
||||
export function useCreateApiKey() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (name: string): Promise<CreateApiKeyResponse> =>
|
||||
ApiKeysClient.createApiKey(name),
|
||||
onSuccess: () => {
|
||||
// Invalidate the API keys query to trigger a refetch
|
||||
queryClient.invalidateQueries({ queryKey: [API_KEYS_QUERY_KEY] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { setInitialPrompt } from "#/state/initial-query-slice";
|
||||
import { RootState } from "#/store";
|
||||
import { GitRepository } from "#/types/git";
|
||||
|
||||
export const useCreateConversation = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -16,11 +17,15 @@ export const useCreateConversation = () => {
|
||||
);
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (variables: { q?: string }) => {
|
||||
mutationKey: ["create-conversation"],
|
||||
mutationFn: async (variables: {
|
||||
q?: string;
|
||||
selectedRepository?: GitRepository | null;
|
||||
}) => {
|
||||
if (variables.q) dispatch(setInitialPrompt(variables.q));
|
||||
|
||||
return OpenHands.createConversation(
|
||||
selectedRepository || undefined,
|
||||
variables.selectedRepository || undefined,
|
||||
variables.q,
|
||||
files,
|
||||
replayJson || undefined,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import ApiKeysClient from "#/api/api-keys";
|
||||
import { API_KEYS_QUERY_KEY } from "#/hooks/query/use-api-keys";
|
||||
|
||||
export function useDeleteApiKey() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (id: string): Promise<void> => {
|
||||
await ApiKeysClient.deleteApiKey(id);
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate the API keys query to trigger a refetch
|
||||
queryClient.invalidateQueries({ queryKey: [API_KEYS_QUERY_KEY] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -29,5 +29,10 @@ export const useLogout = () => {
|
||||
navigate("/");
|
||||
window.location.reload();
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Home screen suggested tasks
|
||||
queryClient.invalidateQueries({ queryKey: ["tasks"] });
|
||||
queryClient.removeQueries({ queryKey: ["tasks"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,15 +4,7 @@ import OpenHands from "#/api/open-hands";
|
||||
import { PostSettings, PostApiSettings } from "#/types/settings";
|
||||
import { useSettings } from "../query/use-settings";
|
||||
|
||||
const saveSettingsMutationFn = async (
|
||||
settings: Partial<PostSettings> | null,
|
||||
) => {
|
||||
// If settings is null, we're resetting
|
||||
if (settings === null) {
|
||||
await OpenHands.resetSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
|
||||
const apiSettings: Partial<PostApiSettings> = {
|
||||
llm_model: settings.LLM_MODEL,
|
||||
llm_base_url: settings.LLM_BASE_URL,
|
||||
@@ -39,12 +31,7 @@ export const useSaveSettings = () => {
|
||||
const { data: currentSettings } = useSettings();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (settings: Partial<PostSettings> | null) => {
|
||||
if (settings === null) {
|
||||
await saveSettingsMutationFn(null);
|
||||
return;
|
||||
}
|
||||
|
||||
mutationFn: async (settings: Partial<PostSettings>) => {
|
||||
const newSettings = { ...currentSettings, ...settings };
|
||||
await saveSettingsMutationFn(newSettings);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ApiKeysClient from "#/api/api-keys";
|
||||
import { useConfig } from "./use-config";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
export const API_KEYS_QUERY_KEY = "api-keys";
|
||||
|
||||
export function useApiKeys() {
|
||||
const { providersAreSet } = useAuth();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
return useQuery({
|
||||
queryKey: [API_KEYS_QUERY_KEY],
|
||||
enabled: providersAreSet && config?.APP_MODE === "saas",
|
||||
queryFn: async () => {
|
||||
const keys = await ApiKeysClient.getApiKeys();
|
||||
return Array.isArray(keys) ? keys : [];
|
||||
},
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { SuggestionsService } from "#/api/suggestions-service/suggestions-service.api";
|
||||
import { groupSuggestedTasks } from "#/utils/group-suggested-tasks";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
export const useSuggestedTasks = () => {
|
||||
const { providersAreSet } = useAuth();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["tasks"],
|
||||
queryFn: SuggestionsService.getSuggestedTasks,
|
||||
select: groupSuggestedTasks,
|
||||
enabled: providersAreSet,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useIsMutating } from "@tanstack/react-query";
|
||||
import { useNavigation } from "react-router";
|
||||
|
||||
export const useIsCreatingConversation = () => {
|
||||
const navigation = useNavigation();
|
||||
const numberOfPendingMutations = useIsMutating({
|
||||
mutationKey: ["create-conversation"],
|
||||
});
|
||||
|
||||
const isNavigating = Boolean(navigation.location);
|
||||
const hasPendingMutations = numberOfPendingMutations > 0;
|
||||
|
||||
return hasPendingMutations || isNavigating;
|
||||
};
|
||||
@@ -1,5 +1,18 @@
|
||||
// this file generate by script, don't modify it manually!!!
|
||||
export enum I18nKey {
|
||||
HOME$CONNECT_PROVIDER_MESSAGE = "HOME$CONNECT_PROVIDER_MESSAGE",
|
||||
HOME$LETS_START_BUILDING = "HOME$LETS_START_BUILDING",
|
||||
HOME$OPENHANDS_DESCRIPTION = "HOME$OPENHANDS_DESCRIPTION",
|
||||
HOME$NOT_SURE_HOW_TO_START = "HOME$NOT_SURE_HOW_TO_START",
|
||||
HOME$CONNECT_TO_REPOSITORY = "HOME$CONNECT_TO_REPOSITORY",
|
||||
HOME$LOADING = "HOME$LOADING",
|
||||
HOME$LOADING_REPOSITORIES = "HOME$LOADING_REPOSITORIES",
|
||||
HOME$FAILED_TO_LOAD_REPOSITORIES = "HOME$FAILED_TO_LOAD_REPOSITORIES",
|
||||
HOME$OPEN_ISSUE = "HOME$OPEN_ISSUE",
|
||||
HOME$FIX_FAILING_CHECKS = "HOME$FIX_FAILING_CHECKS",
|
||||
HOME$RESOLVE_MERGE_CONFLICTS = "HOME$RESOLVE_MERGE_CONFLICTS",
|
||||
HOME$RESOLVE_UNRESOLVED_COMMENTS = "HOME$RESOLVE_UNRESOLVED_COMMENTS",
|
||||
HOME$LAUNCH = "HOME$LAUNCH",
|
||||
CHAT$RESOLVER_INSTRUCTIONS = "CHAT$RESOLVER_INSTRUCTIONS",
|
||||
SETTINGS$ADVANCED = "SETTINGS$ADVANCED",
|
||||
SETTINGS$BASE_URL = "SETTINGS$BASE_URL",
|
||||
@@ -69,7 +82,6 @@ export enum I18nKey {
|
||||
API$DONT_KNOW_KEY = "API$DONT_KNOW_KEY",
|
||||
BUTTON$SAVE = "BUTTON$SAVE",
|
||||
BUTTON$CLOSE = "BUTTON$CLOSE",
|
||||
BUTTON$RESET_TO_DEFAULTS = "BUTTON$RESET_TO_DEFAULTS",
|
||||
MODAL$CONFIRM_RESET_TITLE = "MODAL$CONFIRM_RESET_TITLE",
|
||||
MODAL$CONFIRM_RESET_MESSAGE = "MODAL$CONFIRM_RESET_MESSAGE",
|
||||
MODAL$END_SESSION_TITLE = "MODAL$END_SESSION_TITLE",
|
||||
@@ -256,6 +268,27 @@ export enum I18nKey {
|
||||
SETTINGS$CLICK_FOR_INSTRUCTIONS = "SETTINGS$CLICK_FOR_INSTRUCTIONS",
|
||||
SETTINGS$SAVED = "SETTINGS$SAVED",
|
||||
SETTINGS$RESET = "SETTINGS$RESET",
|
||||
SETTINGS$API_KEYS = "SETTINGS$API_KEYS",
|
||||
SETTINGS$API_KEYS_DESCRIPTION = "SETTINGS$API_KEYS_DESCRIPTION",
|
||||
SETTINGS$CREATE_API_KEY = "SETTINGS$CREATE_API_KEY",
|
||||
SETTINGS$CREATE_API_KEY_DESCRIPTION = "SETTINGS$CREATE_API_KEY_DESCRIPTION",
|
||||
SETTINGS$DELETE_API_KEY = "SETTINGS$DELETE_API_KEY",
|
||||
SETTINGS$DELETE_API_KEY_CONFIRMATION = "SETTINGS$DELETE_API_KEY_CONFIRMATION",
|
||||
SETTINGS$NO_API_KEYS = "SETTINGS$NO_API_KEYS",
|
||||
SETTINGS$NAME = "SETTINGS$NAME",
|
||||
SETTINGS$KEY_PREFIX = "SETTINGS$KEY_PREFIX",
|
||||
SETTINGS$CREATED_AT = "SETTINGS$CREATED_AT",
|
||||
SETTINGS$LAST_USED = "SETTINGS$LAST_USED",
|
||||
SETTINGS$ACTIONS = "SETTINGS$ACTIONS",
|
||||
SETTINGS$API_KEY_CREATED = "SETTINGS$API_KEY_CREATED",
|
||||
SETTINGS$API_KEY_DELETED = "SETTINGS$API_KEY_DELETED",
|
||||
SETTINGS$API_KEY_WARNING = "SETTINGS$API_KEY_WARNING",
|
||||
SETTINGS$API_KEY_COPIED = "SETTINGS$API_KEY_COPIED",
|
||||
SETTINGS$API_KEY_NAME_PLACEHOLDER = "SETTINGS$API_KEY_NAME_PLACEHOLDER",
|
||||
BUTTON$CREATE = "BUTTON$CREATE",
|
||||
BUTTON$DELETE = "BUTTON$DELETE",
|
||||
BUTTON$COPY_TO_CLIPBOARD = "BUTTON$COPY_TO_CLIPBOARD",
|
||||
ERROR$REQUIRED_FIELD = "ERROR$REQUIRED_FIELD",
|
||||
PLANNER$EMPTY_MESSAGE = "PLANNER$EMPTY_MESSAGE",
|
||||
FEEDBACK$PUBLIC_LABEL = "FEEDBACK$PUBLIC_LABEL",
|
||||
FEEDBACK$PRIVATE_LABEL = "FEEDBACK$PRIVATE_LABEL",
|
||||
@@ -287,7 +320,6 @@ export enum I18nKey {
|
||||
SETTINGS_FORM$ENABLE_CONFIRMATION_MODE_LABEL = "SETTINGS_FORM$ENABLE_CONFIRMATION_MODE_LABEL",
|
||||
SETTINGS_FORM$SAVE_LABEL = "SETTINGS_FORM$SAVE_LABEL",
|
||||
SETTINGS_FORM$CLOSE_LABEL = "SETTINGS_FORM$CLOSE_LABEL",
|
||||
SETTINGS_FORM$RESET_TO_DEFAULTS_LABEL = "SETTINGS_FORM$RESET_TO_DEFAULTS_LABEL",
|
||||
SETTINGS_FORM$CANCEL_LABEL = "SETTINGS_FORM$CANCEL_LABEL",
|
||||
SETTINGS_FORM$END_SESSION_LABEL = "SETTINGS_FORM$END_SESSION_LABEL",
|
||||
SETTINGS_FORM$CHANGING_WORKSPACE_WARNING_MESSAGE = "SETTINGS_FORM$CHANGING_WORKSPACE_WARNING_MESSAGE",
|
||||
@@ -306,6 +338,7 @@ export enum I18nKey {
|
||||
STATUS$LLM_RETRY = "STATUS$LLM_RETRY",
|
||||
AGENT_ERROR$BAD_ACTION = "AGENT_ERROR$BAD_ACTION",
|
||||
AGENT_ERROR$ACTION_TIMEOUT = "AGENT_ERROR$ACTION_TIMEOUT",
|
||||
AGENT_ERROR$TOO_MANY_CONVERSATIONS = "AGENT_ERROR$TOO_MANY_CONVERSATIONS",
|
||||
PROJECT_MENU_CARD_CONTEXT_MENU$CONNECT_TO_GITHUB_LABEL = "PROJECT_MENU_CARD_CONTEXT_MENU$CONNECT_TO_GITHUB_LABEL",
|
||||
PROJECT_MENU_CARD_CONTEXT_MENU$PUSH_TO_GITHUB_LABEL = "PROJECT_MENU_CARD_CONTEXT_MENU$PUSH_TO_GITHUB_LABEL",
|
||||
PROJECT_MENU_CARD_CONTEXT_MENU$DOWNLOAD_FILES_LABEL = "PROJECT_MENU_CARD_CONTEXT_MENU$DOWNLOAD_FILES_LABEL",
|
||||
@@ -326,6 +359,7 @@ export enum I18nKey {
|
||||
ACTION_MESSAGE$BROWSE = "ACTION_MESSAGE$BROWSE",
|
||||
ACTION_MESSAGE$BROWSE_INTERACTIVE = "ACTION_MESSAGE$BROWSE_INTERACTIVE",
|
||||
ACTION_MESSAGE$THINK = "ACTION_MESSAGE$THINK",
|
||||
ACTION_MESSAGE$SYSTEM = "ACTION_MESSAGE$SYSTEM",
|
||||
OBSERVATION_MESSAGE$RUN = "OBSERVATION_MESSAGE$RUN",
|
||||
OBSERVATION_MESSAGE$RUN_IPYTHON = "OBSERVATION_MESSAGE$RUN_IPYTHON",
|
||||
OBSERVATION_MESSAGE$READ = "OBSERVATION_MESSAGE$READ",
|
||||
@@ -416,4 +450,11 @@ export enum I18nKey {
|
||||
DIFF_VIEWER$ASK_OH = "DIFF_VIEWER$ASK_OH",
|
||||
DIFF_VIEWER$NO_CHANGES = "DIFF_VIEWER$NO_CHANGES",
|
||||
DIFF_VIEWER$WAITING_FOR_RUNTIME = "DIFF_VIEWER$WAITING_FOR_RUNTIME",
|
||||
SYSTEM_MESSAGE_MODAL$TITLE = "SYSTEM_MESSAGE_MODAL$TITLE",
|
||||
SYSTEM_MESSAGE_MODAL$AGENT_CLASS = "SYSTEM_MESSAGE_MODAL$AGENT_CLASS",
|
||||
SYSTEM_MESSAGE_MODAL$OPENHANDS_VERSION = "SYSTEM_MESSAGE_MODAL$OPENHANDS_VERSION",
|
||||
SYSTEM_MESSAGE_MODAL$SYSTEM_MESSAGE_TAB = "SYSTEM_MESSAGE_MODAL$SYSTEM_MESSAGE_TAB",
|
||||
SYSTEM_MESSAGE_MODAL$TOOLS_TAB = "SYSTEM_MESSAGE_MODAL$TOOLS_TAB",
|
||||
SYSTEM_MESSAGE_MODAL$PARAMETERS = "SYSTEM_MESSAGE_MODAL$PARAMETERS",
|
||||
SYSTEM_MESSAGE_MODAL$NO_TOOLS = "SYSTEM_MESSAGE_MODAL$NO_TOOLS",
|
||||
}
|
||||
|
||||
@@ -1,4 +1,199 @@
|
||||
{
|
||||
"HOME$CONNECT_PROVIDER_MESSAGE": {
|
||||
"en": "To get started with suggested tasks, please connect your GitHub or GitLab account.",
|
||||
"ja": "提案されたタスクを始めるには、GitHubまたはGitLabアカウントを接続してください。",
|
||||
"zh-CN": "要开始使用建议的任务,请连接您的GitHub或GitLab账户。",
|
||||
"zh-TW": "要開始使用建議的任務,請連接您的GitHub或GitLab帳戶。",
|
||||
"ko-KR": "제안된 작업을 시작하려면 GitHub 또는 GitLab 계정을 연결하세요.",
|
||||
"no": "For å komme i gang med foreslåtte oppgaver, vennligst koble til GitHub eller GitLab-kontoen din.",
|
||||
"it": "Per iniziare con le attività suggerite, collega il tuo account GitHub o GitLab.",
|
||||
"pt": "Para começar com tarefas sugeridas, conecte sua conta GitHub ou GitLab.",
|
||||
"es": "Para comenzar con las tareas sugeridas, conecte su cuenta de GitHub o GitLab.",
|
||||
"ar": "للبدء بالمهام المقترحة، يرجى ربط حساب GitHub أو GitLab الخاص بك.",
|
||||
"fr": "Pour commencer avec les tâches suggérées, veuillez connecter votre compte GitHub ou GitLab.",
|
||||
"tr": "Önerilen görevlerle başlamak için lütfen GitHub veya GitLab hesabınızı bağlayın.",
|
||||
"de": "Um mit vorgeschlagenen Aufgaben zu beginnen, verbinden Sie bitte Ihr GitHub- oder GitLab-Konto."
|
||||
},
|
||||
"HOME$LETS_START_BUILDING": {
|
||||
"en": "Let's Start Building!",
|
||||
"ja": "構築を始めましょう!",
|
||||
"zh-CN": "让我们开始构建!",
|
||||
"zh-TW": "讓我們開始構建!",
|
||||
"ko-KR": "구축을 시작합시다!",
|
||||
"no": "La oss begynne å bygge!",
|
||||
"it": "Iniziamo a costruire!",
|
||||
"pt": "Vamos começar a construir!",
|
||||
"es": "¡Comencemos a construir!",
|
||||
"ar": "لنبدأ البناء!",
|
||||
"fr": "Commençons à construire !",
|
||||
"tr": "Hadi İnşa Etmeye Başlayalım!",
|
||||
"de": "Lass uns anfangen zu bauen!"
|
||||
},
|
||||
"HOME$OPENHANDS_DESCRIPTION": {
|
||||
"en": "OpenHands makes it easy to build and maintain software using AI-driven development.",
|
||||
"ja": "OpenHandsはAI駆動の開発を使用してソフトウェアの構築と維持を容易にします。",
|
||||
"zh-CN": "OpenHands使用AI驱动的开发方式,轻松构建和维护软件。",
|
||||
"zh-TW": "OpenHands使用AI驅動的開發方式,輕鬆構建和維護軟件。",
|
||||
"ko-KR": "OpenHands는 AI 기반 개발을 사용하여 소프트웨어를 쉽게 구축하고 유지할 수 있게 합니다.",
|
||||
"no": "OpenHands gjør det enkelt å bygge og vedlikeholde programvare ved hjelp av AI-drevet utvikling.",
|
||||
"it": "OpenHands rende facile costruire e mantenere software utilizzando lo sviluppo guidato dall'IA.",
|
||||
"pt": "OpenHands facilita a construção e manutenção de software usando desenvolvimento orientado por IA.",
|
||||
"es": "OpenHands facilita la construcción y el mantenimiento de software utilizando desarrollo impulsado por IA.",
|
||||
"ar": "يجعل OpenHands من السهل بناء وصيانة البرمجيات باستخدام التطوير المدعوم بالذكاء الاصطناعي.",
|
||||
"fr": "OpenHands facilite la création et la maintenance de logiciels grâce au développement piloté par l'IA.",
|
||||
"tr": "OpenHands, yapay zeka destekli geliştirme kullanarak yazılım oluşturmayı ve sürdürmeyi kolaylaştırır.",
|
||||
"de": "OpenHands macht es einfach, Software mit KI-gesteuerter Entwicklung zu erstellen und zu warten."
|
||||
},
|
||||
"HOME$NOT_SURE_HOW_TO_START": {
|
||||
"en": "Not sure how to start?",
|
||||
"ja": "始め方がわからない?",
|
||||
"zh-CN": "不确定如何开始?",
|
||||
"zh-TW": "不確定如何開始?",
|
||||
"ko-KR": "시작 방법을 모르시나요?",
|
||||
"no": "Usikker på hvordan du skal starte?",
|
||||
"it": "Non sei sicuro di come iniziare?",
|
||||
"pt": "Não tem certeza de como começar?",
|
||||
"es": "¿No está seguro de cómo empezar?",
|
||||
"ar": "غير متأكد من كيفية البدء؟",
|
||||
"fr": "Vous ne savez pas par où commencer ?",
|
||||
"tr": "Nasıl başlayacağınızdan emin değil misiniz?",
|
||||
"de": "Nicht sicher, wie man anfängt?"
|
||||
},
|
||||
"HOME$CONNECT_TO_REPOSITORY": {
|
||||
"en": "Connect to a Repository",
|
||||
"ja": "リポジトリに接続",
|
||||
"zh-CN": "连接到仓库",
|
||||
"zh-TW": "連接到存儲庫",
|
||||
"ko-KR": "저장소에 연결",
|
||||
"no": "Koble til et repository",
|
||||
"it": "Connetti a un repository",
|
||||
"pt": "Conectar a um repositório",
|
||||
"es": "Conectar a un repositorio",
|
||||
"ar": "الاتصال بمستودع",
|
||||
"fr": "Se connecter à un dépôt",
|
||||
"tr": "Bir Depoya Bağlan",
|
||||
"de": "Mit einem Repository verbinden"
|
||||
},
|
||||
"HOME$LOADING": {
|
||||
"en": "Loading...",
|
||||
"ja": "読み込み中...",
|
||||
"zh-CN": "加载中...",
|
||||
"zh-TW": "載入中...",
|
||||
"ko-KR": "로딩 중...",
|
||||
"no": "Laster...",
|
||||
"it": "Caricamento in corso...",
|
||||
"pt": "Carregando...",
|
||||
"es": "Cargando...",
|
||||
"ar": "جار التحميل...",
|
||||
"fr": "Chargement...",
|
||||
"tr": "Yükleniyor...",
|
||||
"de": "Wird geladen..."
|
||||
},
|
||||
"HOME$LOADING_REPOSITORIES": {
|
||||
"en": "Loading repositories...",
|
||||
"ja": "リポジトリを読み込み中...",
|
||||
"zh-CN": "加载仓库中...",
|
||||
"zh-TW": "載入儲存庫中...",
|
||||
"ko-KR": "저장소 로딩 중...",
|
||||
"no": "Laster repositories...",
|
||||
"it": "Caricamento repository in corso...",
|
||||
"pt": "Carregando repositórios...",
|
||||
"es": "Cargando repositorios...",
|
||||
"ar": "جار تحميل المستودعات...",
|
||||
"fr": "Chargement des dépôts...",
|
||||
"tr": "Depolar yükleniyor...",
|
||||
"de": "Repositories werden geladen..."
|
||||
},
|
||||
"HOME$FAILED_TO_LOAD_REPOSITORIES": {
|
||||
"en": "Failed to load repositories",
|
||||
"ja": "リポジトリの読み込みに失敗しました",
|
||||
"zh-CN": "加载仓库失败",
|
||||
"zh-TW": "載入儲存庫失敗",
|
||||
"ko-KR": "저장소 로딩 실패",
|
||||
"no": "Kunne ikke laste repositories",
|
||||
"it": "Impossibile caricare i repository",
|
||||
"pt": "Falha ao carregar repositórios",
|
||||
"es": "Error al cargar repositorios",
|
||||
"ar": "فشل في تحميل المستودعات",
|
||||
"fr": "Échec du chargement des dépôts",
|
||||
"tr": "Depolar yüklenemedi",
|
||||
"de": "Fehler beim Laden der Repositories"
|
||||
},
|
||||
"HOME$OPEN_ISSUE": {
|
||||
"en": "Open issue",
|
||||
"ja": "オープンな課題",
|
||||
"zh-CN": "打开问题",
|
||||
"zh-TW": "開放議題",
|
||||
"ko-KR": "열린 이슈",
|
||||
"no": "Åpent problem",
|
||||
"it": "Problema aperto",
|
||||
"pt": "Problema aberto",
|
||||
"es": "Problema abierto",
|
||||
"ar": "مشكلة مفتوحة",
|
||||
"fr": "Problème ouvert",
|
||||
"tr": "Açık sorun",
|
||||
"de": "Offenes Problem"
|
||||
},
|
||||
"HOME$FIX_FAILING_CHECKS": {
|
||||
"en": "Fix failing checks",
|
||||
"ja": "失敗したチェックを修正",
|
||||
"zh-CN": "修复失败的检查",
|
||||
"zh-TW": "修復失敗的檢查",
|
||||
"ko-KR": "실패한 검사 수정",
|
||||
"no": "Fikse mislykkede kontroller",
|
||||
"it": "Correggere i controlli falliti",
|
||||
"pt": "Corrigir verificações com falha",
|
||||
"es": "Corregir comprobaciones fallidas",
|
||||
"ar": "إصلاح الفحوصات الفاشلة",
|
||||
"fr": "Corriger les vérifications échouées",
|
||||
"tr": "Başarısız kontrolleri düzelt",
|
||||
"de": "Fehlgeschlagene Prüfungen beheben"
|
||||
},
|
||||
"HOME$RESOLVE_MERGE_CONFLICTS": {
|
||||
"en": "Resolve merge conflicts",
|
||||
"ja": "マージ競合を解決",
|
||||
"zh-CN": "解决合并冲突",
|
||||
"zh-TW": "解決合併衝突",
|
||||
"ko-KR": "병합 충돌 해결",
|
||||
"no": "Løse fletteproblemer",
|
||||
"it": "Risolvere i conflitti di unione",
|
||||
"pt": "Resolver conflitos de mesclagem",
|
||||
"es": "Resolver conflictos de fusión",
|
||||
"ar": "حل تعارضات الدمج",
|
||||
"fr": "Résoudre les conflits de fusion",
|
||||
"tr": "Birleştirme çakışmalarını çöz",
|
||||
"de": "Merge-Konflikte lösen"
|
||||
},
|
||||
"HOME$RESOLVE_UNRESOLVED_COMMENTS": {
|
||||
"en": "Resolve unresolved comments",
|
||||
"ja": "未解決のコメントを解決",
|
||||
"zh-CN": "解决未解决的评论",
|
||||
"zh-TW": "解決未解決的評論",
|
||||
"ko-KR": "미해결 댓글 해결",
|
||||
"no": "Løse uløste kommentarer",
|
||||
"it": "Risolvere i commenti non risolti",
|
||||
"pt": "Resolver comentários não resolvidos",
|
||||
"es": "Resolver comentarios no resueltos",
|
||||
"ar": "حل التعليقات غير المحلولة",
|
||||
"fr": "Résoudre les commentaires non résolus",
|
||||
"tr": "Çözülmemiş yorumları çöz",
|
||||
"de": "Ungelöste Kommentare beheben"
|
||||
},
|
||||
"HOME$LAUNCH": {
|
||||
"en": "Launch",
|
||||
"ja": "起動",
|
||||
"zh-CN": "启动",
|
||||
"zh-TW": "啟動",
|
||||
"ko-KR": "실행",
|
||||
"no": "Start",
|
||||
"it": "Avvia",
|
||||
"pt": "Iniciar",
|
||||
"es": "Iniciar",
|
||||
"ar": "تشغيل",
|
||||
"fr": "Lancer",
|
||||
"tr": "Başlat",
|
||||
"de": "Starten"
|
||||
},
|
||||
"CHAT$RESOLVER_INSTRUCTIONS": {
|
||||
"en": "Resolver Instructions",
|
||||
"ja": "リゾルバの指示",
|
||||
@@ -1036,21 +1231,6 @@
|
||||
"tr": "Kapat",
|
||||
"de": "Schließen"
|
||||
},
|
||||
"BUTTON$RESET_TO_DEFAULTS": {
|
||||
"en": "Reset to defaults",
|
||||
"ja": "デフォルトにリセット",
|
||||
"zh-CN": "重置为默认值",
|
||||
"zh-TW": "還原為預設值",
|
||||
"ko-KR": "기본값으로 재설정",
|
||||
"no": "Tilbakestill til standard",
|
||||
"it": "Ripristina valori predefiniti",
|
||||
"pt": "Restaurar padrões",
|
||||
"es": "Restablecer valores predeterminados",
|
||||
"ar": "إعادة التعيين إلى الإعدادات الافتراضية",
|
||||
"fr": "Réinitialiser aux valeurs par défaut",
|
||||
"tr": "Varsayılanlara sıfırla",
|
||||
"de": "Auf Standardwerte zurücksetzen"
|
||||
},
|
||||
"MODAL$CONFIRM_RESET_TITLE": {
|
||||
"en": "Are you sure?",
|
||||
"ja": "本当によろしいですか?",
|
||||
@@ -3818,6 +3998,69 @@
|
||||
"tr": "Ayarlar sıfırlandı",
|
||||
"de": "Einstellungen zurückgesetzt"
|
||||
},
|
||||
"SETTINGS$API_KEYS": {
|
||||
"en": "API Keys"
|
||||
},
|
||||
"SETTINGS$API_KEYS_DESCRIPTION": {
|
||||
"en": "API keys allow you to authenticate with the OpenHands API programmatically. Keep your API keys secure; anyone with your API key can access your account."
|
||||
},
|
||||
"SETTINGS$CREATE_API_KEY": {
|
||||
"en": "Create API Key"
|
||||
},
|
||||
"SETTINGS$CREATE_API_KEY_DESCRIPTION": {
|
||||
"en": "Give your API key a descriptive name to help you identify it later."
|
||||
},
|
||||
"SETTINGS$DELETE_API_KEY": {
|
||||
"en": "Delete API Key"
|
||||
},
|
||||
"SETTINGS$DELETE_API_KEY_CONFIRMATION": {
|
||||
"en": "Are you sure you want to delete the API key \"{{name}}\"? This action cannot be undone."
|
||||
},
|
||||
"SETTINGS$NO_API_KEYS": {
|
||||
"en": "You don't have any API keys yet. Create one to get started."
|
||||
},
|
||||
"SETTINGS$NAME": {
|
||||
"en": "Name"
|
||||
},
|
||||
"SETTINGS$KEY_PREFIX": {
|
||||
"en": "Key Prefix"
|
||||
},
|
||||
"SETTINGS$CREATED_AT": {
|
||||
"en": "Created"
|
||||
},
|
||||
"SETTINGS$LAST_USED": {
|
||||
"en": "Last Used"
|
||||
},
|
||||
"SETTINGS$ACTIONS": {
|
||||
"en": "Actions"
|
||||
},
|
||||
"SETTINGS$API_KEY_CREATED": {
|
||||
"en": "API Key Created"
|
||||
},
|
||||
"SETTINGS$API_KEY_DELETED": {
|
||||
"en": "API key deleted successfully"
|
||||
},
|
||||
"SETTINGS$API_KEY_WARNING": {
|
||||
"en": "This is the only time your API key will be displayed. Please copy it now and store it securely."
|
||||
},
|
||||
"SETTINGS$API_KEY_COPIED": {
|
||||
"en": "API key copied to clipboard"
|
||||
},
|
||||
"SETTINGS$API_KEY_NAME_PLACEHOLDER": {
|
||||
"en": "My API Key"
|
||||
},
|
||||
"BUTTON$CREATE": {
|
||||
"en": "Create"
|
||||
},
|
||||
"BUTTON$DELETE": {
|
||||
"en": "Delete"
|
||||
},
|
||||
"BUTTON$COPY_TO_CLIPBOARD": {
|
||||
"en": "Copy to Clipboard"
|
||||
},
|
||||
"ERROR$REQUIRED_FIELD": {
|
||||
"en": "This field is required"
|
||||
},
|
||||
"PLANNER$EMPTY_MESSAGE": {
|
||||
"en": "No plan created.",
|
||||
"zh-CN": "计划未创建",
|
||||
@@ -4283,21 +4526,6 @@
|
||||
"pt": "Fechar",
|
||||
"tr": "Kapat"
|
||||
},
|
||||
"SETTINGS_FORM$RESET_TO_DEFAULTS_LABEL": {
|
||||
"en": "Reset to defaults",
|
||||
"es": "Reiniciar valores por defect",
|
||||
"zh-CN": "重置为默认值",
|
||||
"zh-TW": "還原為預設值",
|
||||
"ko-KR": "기본값으로 재설정",
|
||||
"ja": "デフォルトに戻す",
|
||||
"no": "Tilbakestill til standardverdier",
|
||||
"ar": "إعادة التعيين إلى الإعدادات الافتراضية",
|
||||
"de": "Auf Standardwerte zurücksetzen",
|
||||
"fr": "Réinitialiser aux valeurs par défaut",
|
||||
"it": "Ripristina valori predefiniti",
|
||||
"pt": "Restaurar padrões",
|
||||
"tr": "Varsayılanlara sıfırla"
|
||||
},
|
||||
"SETTINGS_FORM$CANCEL_LABEL": {
|
||||
"en": "Cancel",
|
||||
"es": "Cancelar",
|
||||
@@ -4568,6 +4796,9 @@
|
||||
"es": "La acción expiró",
|
||||
"tr": "İşlem zaman aşımına uğradı"
|
||||
},
|
||||
"AGENT_ERROR$TOO_MANY_CONVERSATIONS": {
|
||||
"en": "Too many conversations at once."
|
||||
},
|
||||
"PROJECT_MENU_CARD_CONTEXT_MENU$CONNECT_TO_GITHUB_LABEL": {
|
||||
"en": "Connect to GitHub",
|
||||
"es": "Conectar a GitHub",
|
||||
@@ -4869,6 +5100,21 @@
|
||||
"es": "Pensando",
|
||||
"tr": "Düşünüyor"
|
||||
},
|
||||
"ACTION_MESSAGE$SYSTEM": {
|
||||
"en": "System Message",
|
||||
"zh-CN": "系统消息",
|
||||
"zh-TW": "系統訊息",
|
||||
"ko-KR": "시스템 메시지",
|
||||
"ja": "システムメッセージ",
|
||||
"no": "Systemmelding",
|
||||
"ar": "رسالة النظام",
|
||||
"de": "Systemnachricht",
|
||||
"fr": "Message Système",
|
||||
"it": "Messaggio di Sistema",
|
||||
"pt": "Mensagem do Sistema",
|
||||
"es": "Mensaje del Sistema",
|
||||
"tr": "Sistem Mesajı"
|
||||
},
|
||||
"OBSERVATION_MESSAGE$RUN": {
|
||||
"en": "Ran <cmd>{{observation.payload.extras.command}}</cmd>",
|
||||
"zh-CN": "运行 <cmd>{{observation.payload.extras.command}}</cmd>",
|
||||
@@ -6207,5 +6453,110 @@
|
||||
"fr": "En attente du démarrage de l'exécution...",
|
||||
"tr": "Çalışma zamanının başlamasını bekliyor...",
|
||||
"de": "Warten auf den Start der Laufzeit..."
|
||||
},
|
||||
"SYSTEM_MESSAGE_MODAL$TITLE": {
|
||||
"en": "Agent Tools & Metadata",
|
||||
"zh-CN": "代理工具和元数据",
|
||||
"zh-TW": "代理工具和元數據",
|
||||
"ko-KR": "에이전트 도구 및 메타데이터",
|
||||
"ja": "エージェントツールとメタデータ",
|
||||
"no": "Agent-verktøy og metadata",
|
||||
"ar": "أدوات الوكيل والبيانات الوصفية",
|
||||
"de": "Agent-Tools und Metadaten",
|
||||
"fr": "Outils d'agent et métadonnées",
|
||||
"it": "Strumenti e metadati dell'agente",
|
||||
"pt": "Ferramentas e metadados do agente",
|
||||
"es": "Herramientas y metadatos del agente",
|
||||
"tr": "Ajan Araçları ve Meta Verileri"
|
||||
},
|
||||
"SYSTEM_MESSAGE_MODAL$AGENT_CLASS": {
|
||||
"en": "Agent Class:",
|
||||
"zh-CN": "代理类别:",
|
||||
"zh-TW": "代理類別:",
|
||||
"ko-KR": "에이전트 클래스:",
|
||||
"ja": "エージェントクラス:",
|
||||
"no": "Agent-klasse:",
|
||||
"ar": "فئة الوكيل:",
|
||||
"de": "Agent-Klasse:",
|
||||
"fr": "Classe d'agent :",
|
||||
"it": "Classe agente:",
|
||||
"pt": "Classe do agente:",
|
||||
"es": "Clase de agente:",
|
||||
"tr": "Ajan Sınıfı:"
|
||||
},
|
||||
"SYSTEM_MESSAGE_MODAL$OPENHANDS_VERSION": {
|
||||
"en": "OpenHands Version:",
|
||||
"zh-CN": "OpenHands 版本:",
|
||||
"zh-TW": "OpenHands 版本:",
|
||||
"ko-KR": "OpenHands 버전:",
|
||||
"ja": "OpenHands バージョン:",
|
||||
"no": "OpenHands-versjon:",
|
||||
"ar": "إصدار OpenHands:",
|
||||
"de": "OpenHands-Version:",
|
||||
"fr": "Version OpenHands :",
|
||||
"it": "Versione OpenHands:",
|
||||
"pt": "Versão OpenHands:",
|
||||
"es": "Versión de OpenHands:",
|
||||
"tr": "OpenHands Sürümü:"
|
||||
},
|
||||
"SYSTEM_MESSAGE_MODAL$SYSTEM_MESSAGE_TAB": {
|
||||
"en": "System Message",
|
||||
"zh-CN": "系统消息",
|
||||
"zh-TW": "系統訊息",
|
||||
"ko-KR": "시스템 메시지",
|
||||
"ja": "システムメッセージ",
|
||||
"no": "Systemmelding",
|
||||
"ar": "رسالة النظام",
|
||||
"de": "Systemnachricht",
|
||||
"fr": "Message système",
|
||||
"it": "Messaggio di sistema",
|
||||
"pt": "Mensagem do sistema",
|
||||
"es": "Mensaje del sistema",
|
||||
"tr": "Sistem Mesajı"
|
||||
},
|
||||
"SYSTEM_MESSAGE_MODAL$TOOLS_TAB": {
|
||||
"en": "Available Tools",
|
||||
"zh-CN": "可用工具",
|
||||
"zh-TW": "可用工具",
|
||||
"ko-KR": "사용 가능한 도구",
|
||||
"ja": "利用可能なツール",
|
||||
"no": "Tilgjengelige verktøy",
|
||||
"ar": "الأدوات المتاحة",
|
||||
"de": "Verfügbare Tools",
|
||||
"fr": "Outils disponibles",
|
||||
"it": "Strumenti disponibili",
|
||||
"pt": "Ferramentas disponíveis",
|
||||
"es": "Herramientas disponibles",
|
||||
"tr": "Kullanılabilir Araçlar"
|
||||
},
|
||||
"SYSTEM_MESSAGE_MODAL$PARAMETERS": {
|
||||
"en": "Parameters:",
|
||||
"zh-CN": "参数:",
|
||||
"zh-TW": "參數:",
|
||||
"ko-KR": "매개변수:",
|
||||
"ja": "パラメータ:",
|
||||
"no": "Parametere:",
|
||||
"ar": "المعلمات:",
|
||||
"de": "Parameter:",
|
||||
"fr": "Paramètres :",
|
||||
"it": "Parametri:",
|
||||
"pt": "Parâmetros:",
|
||||
"es": "Parámetros:",
|
||||
"tr": "Parametreler:"
|
||||
},
|
||||
"SYSTEM_MESSAGE_MODAL$NO_TOOLS": {
|
||||
"en": "No tools available for this agent",
|
||||
"zh-CN": "此代理没有可用的工具",
|
||||
"zh-TW": "此代理沒有可用的工具",
|
||||
"ko-KR": "이 에이전트에 사용 가능한 도구가 없습니다",
|
||||
"ja": "このエージェントで利用可能なツールはありません",
|
||||
"no": "Ingen verktøy tilgjengelig for denne agenten",
|
||||
"ar": "لا توجد أدوات متاحة لهذا الوكيل",
|
||||
"de": "Keine Tools für diesen Agenten verfügbar",
|
||||
"fr": "Aucun outil disponible pour cet agent",
|
||||
"it": "Nessuno strumento disponibile per questo agente",
|
||||
"pt": "Nenhuma ferramenta disponível para este agente",
|
||||
"es": "No hay herramientas disponibles para este agente",
|
||||
"tr": "Bu ajan için kullanılabilir araç yok"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<svg width="102" height="69" viewBox="0 0 102 69" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_40000615_2266)">
|
||||
<g clip-path="url(#clip1_40000615_2266)">
|
||||
<path d="M97.9659 20.9744C94.7109 18.9968 92.5363 22.0276 92.8031 26.1428L92.7759 26.1739C92.785 21.8765 92.1973 17.1303 90.2443 13.2906C89.5526 11.9307 88.1511 9.69537 85.3798 10.7486C84.1637 11.2108 83.0606 12.6018 83.6302 16.1926C83.6302 16.1926 84.2631 19.9611 84.1456 24.6985V24.7651C83.3544 11.6996 80.3707 7.71333 76.103 7.97108C74.7376 8.21106 72.8705 8.79323 73.4989 12.8107C73.4989 12.8107 74.1816 17.0014 74.4031 20.3389L74.4167 20.5077H74.4031C72.3958 13.3039 69.6924 13.2062 67.7348 13.4861C65.9581 13.7395 64.0187 15.5615 64.9997 19.1523C68.0784 30.418 67.4771 43.9857 67.2466 45.9277C66.6182 44.5989 66.4238 43.5457 65.5512 42.088C62.043 36.2352 60.3748 35.8042 58.3269 35.7153C56.2925 35.6264 54.0953 36.8663 54.24 39.2261C54.3892 41.5859 55.6053 41.9769 57.3323 45.2655C58.6795 47.8253 59.0638 51.1806 61.7763 57.2778C64.0232 62.3262 69.8958 67.8635 80.5922 67.2058C89.2587 66.9214 102.202 63.9261 99.9506 44.2567C99.39 40.8393 99.8104 37.9773 100.104 35.0442C100.561 30.4935 101.23 22.952 97.9704 20.9699L97.9659 20.9744Z" fill="#E3D495"/>
|
||||
<path d="M43.8511 35.8532C41.8032 35.9821 40.1485 36.4398 36.7488 42.3548C35.9034 43.8258 35.7316 44.8835 35.1258 46.2212C34.8591 44.2835 34.0046 30.7292 36.8709 19.4102C37.7841 15.8061 35.813 14.0196 34.0318 13.7974C32.0697 13.553 29.3617 13.6996 27.4901 20.9745H27.4675L27.4946 20.7656C27.6528 17.4237 28.2586 13.2241 28.2586 13.2241C28.8056 9.19334 26.934 8.64672 25.5642 8.42896C21.3055 8.2512 18.4031 12.2553 17.8425 25.2052H17.8335C17.6391 20.5168 18.1951 16.7838 18.1951 16.7838C18.697 13.1797 17.5667 11.8109 16.3416 11.3709C13.5522 10.3666 12.1914 12.6286 11.5268 14.0018C9.64616 17.877 9.14434 22.6321 9.23476 26.9295L9.20763 26.8984C9.39299 22.7788 7.1642 19.788 3.94533 21.8233C0.726467 23.8631 1.5357 31.3914 2.07821 35.9332C2.43084 38.8618 2.90101 41.7149 2.40371 45.1413C0.523028 64.8462 13.5205 67.6059 22.1916 67.7348C32.897 68.2014 38.6657 62.553 40.8176 57.4646C43.4126 51.3229 43.7381 47.9588 45.0356 45.3768C46.6993 42.0571 47.9109 41.6438 48.0148 39.284C48.1188 36.9242 45.8991 35.7243 43.8647 35.8487L43.8511 35.8532Z" fill="#E3D495"/>
|
||||
<path d="M48.1374 35.8835C47.0524 34.8392 45.4204 34.2837 43.7612 34.3815C41.1029 34.5459 39.2177 35.5014 36.3198 40.1676C36.243 34.8303 36.5459 26.8177 38.3271 19.7694C38.9962 17.1208 38.3226 15.392 37.6354 14.4099C36.8352 13.2633 35.592 12.5078 34.2176 12.339C32.9653 12.1834 31.3288 12.1745 29.7329 13.4633C29.7329 13.4455 29.7374 13.4233 29.7374 13.4233C30.2528 9.63699 28.9237 7.4683 25.7907 6.97945L25.6189 6.96167C23.6885 6.87724 22.0339 7.4994 20.7002 8.80595C19.9859 9.50367 19.3665 10.4058 18.8286 11.5213C18.2273 10.6547 17.4542 10.2147 16.8439 9.99251C13.1142 8.64597 11.0527 11.5302 10.1575 13.37C9.11772 15.512 8.47123 17.9118 8.10956 20.3605C8.03271 20.3116 7.96037 20.2627 7.88352 20.2183C7.06072 19.7516 5.34731 19.1828 3.12304 20.5916C-0.647376 22.9825 -0.168163 29.853 0.582303 36.1102C0.622991 36.4435 0.663679 36.7724 0.704367 37.1057C1.02535 39.6699 1.32825 42.0919 0.912327 44.9405L0.903285 45.0116C0.148299 52.9176 1.66279 59.0015 5.40608 63.1033C9.0047 67.0497 14.6422 69.1028 22.1152 69.2139C22.6623 69.2361 23.1957 69.245 23.7156 69.2406C36.4826 69.125 41.0622 60.7036 42.1879 58.0371C43.6437 54.5885 44.3986 52.0154 44.9999 49.9445C45.4656 48.3447 45.8363 47.0825 46.3607 46.0337C46.9575 44.8427 47.4819 44.0695 47.943 43.3851C48.7296 42.2208 49.4078 41.2164 49.4892 39.3499C49.5479 37.9989 49.0777 36.799 48.1238 35.8835H48.1374ZM22.8205 10.8947C23.5393 10.1925 24.3666 9.88141 25.4155 9.90363C26.3242 10.0503 27.147 10.268 26.7717 13.0234C26.7446 13.1967 26.1569 17.3341 25.9987 20.7027C25.9987 20.7249 25.9987 20.7471 25.9987 20.7693C25.1714 24.0579 24.4797 28.7953 24.1089 35.7191C22.504 35.8169 20.9036 35.9858 19.3485 36.208C18.8466 22.1603 20.0085 13.6411 22.816 10.8947H22.8205ZM12.87 14.641C13.9957 12.3301 14.9406 12.4456 15.8177 12.7612C17.0338 13.2011 16.8439 15.5876 16.7038 16.5786C16.6811 16.7386 16.1386 20.4583 16.3285 25.1912C16.1884 28.5109 16.2065 32.3372 16.3737 36.7146C14.8864 37.0034 13.4758 37.3367 12.1874 37.6967C11.5771 35.6791 8.87811 22.8625 12.87 14.6454V14.641ZM45.4565 41.7541C44.9683 42.4741 44.3625 43.3718 43.6798 44.7316C43.0333 46.016 42.6355 47.3936 42.1246 49.1313C41.5414 51.1266 40.8136 53.6109 39.4257 56.9039C38.4401 59.2326 34.299 66.7963 22.2057 66.2631C15.465 66.1653 10.7 64.4854 7.63939 61.1302C4.48382 57.6727 3.2225 52.3487 3.88706 45.3138C4.34819 42.0963 4.00913 39.3721 3.6791 36.7412C3.63842 36.4124 3.59773 36.088 3.55704 35.7591C3.19537 32.7238 2.2279 24.6579 4.74603 23.0625C5.42416 22.6314 5.98023 22.5336 6.39163 22.7647C7.09237 23.1602 7.79762 24.6001 7.69816 26.831C7.69364 26.951 7.7072 27.0666 7.72981 27.1821C7.85639 32.3816 8.80126 36.9057 9.33924 38.6033C8.45767 38.9277 7.68008 39.2655 7.02907 39.6032C6.29669 39.9854 6.02092 40.8742 6.40971 41.5941C6.68097 42.0963 7.20539 42.3807 7.74789 42.3763C7.97846 42.3763 8.21354 42.3185 8.43507 42.203C12.0925 40.2965 20.768 38.3589 28.4128 38.5677C29.2447 38.5811 29.9273 37.95 29.9499 37.1368C29.9725 36.3235 29.3215 35.648 28.4942 35.6258C28.0376 35.6125 27.5765 35.6125 27.1153 35.6125C27.8794 21.6493 29.9816 17.0941 31.6181 15.7476C32.2963 15.1921 32.9427 15.1476 33.8379 15.2587C34.0865 15.2898 34.7104 15.432 35.158 16.072C35.6417 16.7697 35.7321 17.8007 35.4157 19.0539C32.6489 29.9907 33.2728 42.8518 33.5892 45.9715C33.535 46.0782 33.4852 46.1848 33.4265 46.2959C32.78 47.4914 31.5774 48.7357 30.1443 48.6513C29.326 48.6113 28.6072 49.2246 28.5575 50.0334C28.5078 50.8467 29.1362 51.5444 29.9635 51.5933C32.3867 51.7355 34.6697 50.2734 36.0712 47.6825C36.2204 47.407 36.347 47.1448 36.46 46.8914C36.469 46.8737 36.4781 46.8515 36.4871 46.8337C36.7493 46.256 36.9392 45.7271 37.1065 45.2516C37.3687 44.5139 37.5947 43.8739 38.0468 43.0829C41.2566 37.4923 42.5586 37.4123 43.9375 37.3279C44.7468 37.279 45.5515 37.5323 46.0262 37.9945C46.3652 38.3189 46.5144 38.7233 46.4918 39.2299C46.4466 40.2743 46.1527 40.7098 45.443 41.7586L45.4565 41.7541Z" fill="#0D0F11"/>
|
||||
<path d="M101.42 44.0245C100.954 41.1848 101.212 38.7583 101.483 36.1896C101.519 35.8563 101.556 35.5275 101.587 35.1942C102.22 28.9281 102.573 22.0442 98.7526 19.7244C96.5013 18.3557 94.7969 18.9556 93.9831 19.4356C93.9063 19.48 93.8339 19.5333 93.7571 19.5822C93.3457 17.1424 92.6585 14.756 91.578 12.6317C90.6512 10.8097 88.54 7.96104 84.8329 9.37425C84.2271 9.60534 83.4676 10.0586 82.8798 10.9386C82.3193 9.83198 81.6818 8.94317 80.954 8.25879C79.5977 6.9789 77.9295 6.3834 76.0036 6.50339L75.8318 6.52117C72.7079 7.06779 71.4194 9.2587 72.0071 13.045C72.0071 13.045 72.0071 13.0628 72.0117 13.0761C70.3932 11.814 68.7566 11.854 67.5089 12.0318C66.139 12.2273 64.9094 13.005 64.1318 14.1649C63.4672 15.1604 62.8207 16.898 63.5395 19.5333C65.4564 26.5505 65.9085 34.5587 65.9311 39.896C62.9473 35.2831 61.044 34.3631 58.3857 34.2476C56.7311 34.1765 55.0991 34.7675 54.0366 35.8297C53.1008 36.7629 52.6533 37.9717 52.7392 39.3182C52.8567 41.1803 53.5529 42.1758 54.3622 43.3223C54.8368 43.9978 55.3748 44.7622 55.9942 45.9399C56.5412 46.9798 56.9345 48.233 57.4318 49.824C58.0738 51.8816 58.874 54.4413 60.3975 57.8633C61.573 60.5075 66.3108 68.849 79.0416 68.729C79.557 68.7245 80.0905 68.7067 80.633 68.6712C88.1467 68.4268 93.7435 66.267 97.2698 62.2584C100.932 58.0899 102.333 51.9793 101.434 44.0867L101.424 44.0156L101.42 44.0245ZM85.1041 15.9692C84.9459 14.9649 84.7063 12.5829 85.9179 12.1251C86.7859 11.7918 87.7353 11.6629 88.9017 13.9516C93.0473 22.0976 90.5925 34.9631 90.0183 36.9896C88.7208 36.6518 87.3058 36.3452 85.8139 36.083C85.8953 31.7056 85.841 27.8748 85.6421 24.5596C85.7416 19.8266 85.1312 16.1159 85.1041 15.9692ZM76.2658 9.44091C77.3192 9.39647 78.151 9.69422 78.8789 10.3875C81.7361 13.085 83.0607 21.5776 82.8211 35.6341C81.2614 35.4386 79.661 35.3008 78.0515 35.2297C77.5543 28.3103 76.7721 23.5908 75.8815 20.3155C75.8815 20.2933 75.8815 20.2711 75.8815 20.2488C75.66 16.8802 74.9909 12.7562 74.9638 12.5962C74.5343 9.83643 75.3526 9.60534 76.2613 9.44091H76.2658ZM98.4588 44.46C99.2545 51.4816 98.0926 56.8278 95.0048 60.343C92.0075 63.7516 87.2741 65.5204 80.4928 65.7426C68.4673 66.4892 64.1725 59.0054 63.1462 56.6945C61.6905 53.4237 60.9174 50.9572 60.2981 48.9707C59.7556 47.2375 59.3306 45.8732 58.6615 44.5978C57.9562 43.2512 57.3324 42.3669 56.8306 41.6558C56.1027 40.6204 55.7998 40.1893 55.732 39.1449C55.7003 38.6383 55.845 38.2295 56.175 37.9006C56.6452 37.4295 57.4409 37.1584 58.2546 37.1984C59.6335 37.2607 60.9355 37.314 64.2538 42.8468C64.724 43.629 64.9591 44.2645 65.2349 44.9977C65.4157 45.4733 65.6146 46.0021 65.8904 46.5754C65.8994 46.5932 65.904 46.6109 65.913 46.6243C66.0351 46.8776 66.1662 47.1353 66.3199 47.4109C67.7711 49.9751 70.0812 51.3972 72.4999 51.2105C73.3227 51.1483 73.9421 50.4373 73.8788 49.6284C73.8155 48.8196 73.0967 48.2197 72.2693 48.273C70.8362 48.3797 69.6111 47.1576 68.942 45.9754C68.8787 45.8643 68.829 45.7621 68.7747 45.6555C69.0324 42.5357 69.4167 29.6613 66.4419 18.7779C66.0984 17.5291 66.1707 16.4981 66.6409 15.7915C67.0794 15.1426 67.6987 14.9871 67.9474 14.9515C68.838 14.8227 69.489 14.8582 70.1762 15.4004C71.8399 16.7203 74.028 21.2354 75.0497 35.1808C74.5886 35.1853 74.1274 35.1986 73.6753 35.2208C72.848 35.2564 72.2106 35.9452 72.2467 36.7585C72.2829 37.5717 72.9701 38.185 73.811 38.1628C81.4467 37.8162 90.163 39.5938 93.852 41.4381C94.0735 41.5492 94.3086 41.598 94.5437 41.598C95.0862 41.5936 95.6061 41.3003 95.8683 40.7892C96.2436 40.0649 95.9497 39.176 95.2083 38.8072C94.5528 38.4783 93.7661 38.1584 92.88 37.8473C93.3864 36.1408 94.2498 31.5989 94.277 26.3994C94.2996 26.2839 94.3086 26.1683 94.2996 26.0483C94.1549 23.8218 94.8376 22.3686 95.5293 21.9598C95.9361 21.7198 96.4922 21.8087 97.1794 22.2264C99.7292 23.7774 98.9154 31.8567 98.608 34.9009C98.5763 35.2297 98.5402 35.5541 98.504 35.883C98.2237 38.5228 97.9344 41.247 98.4588 44.46Z" fill="#0D0F11"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_40000615_2266">
|
||||
<rect width="101.919" height="68.945" fill="white" transform="translate(0.0621948 0.29541)"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_40000615_2266">
|
||||
<rect width="101.919" height="68.945" fill="white" transform="translate(0.0621948 0.29541)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user